[IMP] sale order line invisible type
[odoo/odoo.git] / addons / mail / mail_message.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2010-today OpenERP SA (<http://www.openerp.com>)
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>
19 #
20 ##############################################################################
21
22 import ast
23 import base64
24 import dateutil.parser
25 import email
26 import logging
27 import re
28 import time
29 import datetime
30 from email.header import decode_header
31 from email.message import Message
32
33 from openerp import SUPERUSER_ID
34 from osv import osv
35 from osv import fields
36 import pytz
37 from tools import DEFAULT_SERVER_DATETIME_FORMAT
38 from tools.translate import _
39 import tools
40
41 _logger = logging.getLogger(__name__)
42
43 """ Some tools for parsing / creating email fields """
44 def decode(text):
45     """Returns unicode() string conversion of the the given encoded smtp header text"""
46     if text:
47         text = decode_header(text.replace('\r', ''))
48         return ''.join([tools.ustr(x[0], x[1]) for x in text])
49
50 def mail_tools_to_email(text):
51     """Return a list of the email addresses found in ``text``"""
52     if not text: return []
53     return re.findall(r'([^ ,<@]+@[^> ,]+)', text)
54
55 # TODO: remove that after cleaning
56 def to_email(text):
57     return mail_tools_to_email(text)
58
59 class mail_message_common(osv.TransientModel):
60     """ Common abstract class for holding the main attributes of a 
61         message object. It could be reused as parent model for any
62         database model or wizard screen that needs to hold a kind of
63         message.
64         All internal logic should be in another model while this
65         model holds the basics of a message. For example, a wizard for writing
66         emails should inherit from this class and not from mail.message."""
67
68     def get_body(self, cr, uid, ids, name, arg, context=None):
69         """ get correct body version: body_html for html messages, and
70             body_text for plain text messages
71         """
72         result = dict.fromkeys(ids, '')
73         for message in self.browse(cr, uid, ids, context=context):
74             if message.content_subtype == 'html':
75                 result[message.id] = message.body_html
76             else:
77                 result[message.id] = message.body_text
78         return result
79     
80     def search_body(self, cr, uid, obj, name, args, context=None):
81         # will receive:
82         #   - obj: mail.message object
83         #   - name: 'body'
84         #   - args: [('body', 'ilike', 'blah')]
85         return ['|', '&', ('content_subtype', '=', 'html'), ('body_html', args[0][1], args[0][2]), ('body_text', args[0][1], args[0][2])]
86     
87     def get_record_name(self, cr, uid, ids, name, arg, context=None):
88         result = dict.fromkeys(ids, '')
89         for message in self.browse(cr, uid, ids, context=context):
90             if not message.model or not message.res_id:
91                 continue
92             result[message.id] = self.pool.get(message.model).name_get(cr, uid, [message.res_id], context=context)[0][1]
93         return result
94
95     def name_get(self, cr, uid, ids, context=None):
96         # name_get may receive int id instead of an id list
97         if isinstance(ids, (int, long)):
98             ids = [ids]
99         res = []
100         for message in self.browse(cr, uid, ids, context=context):
101             name = ''
102             if message.subject:
103                 name = '%s: ' % (message.subject)
104             if message.body_text:
105                 name = '%s%s ' % (name, message.body_text[0:20])
106             if message.date:
107                 name = '%s(%s)' % (name, message.date)
108             res.append((message.id, name))
109         return res
110
111     _name = 'mail.message.common'
112     _rec_name = 'subject'
113     _columns = {
114         'subject': fields.char('Subject', size=512),
115         'model': fields.char('Related Document Model', size=128, select=1),
116         'res_id': fields.integer('Related Document ID', select=1),
117         'record_name': fields.function(get_record_name, type='string',
118             string='Message Record Name',
119             help="Name get of the related document."),
120         'date': fields.datetime('Date'),
121         'email_from': fields.char('From', size=128, help='Message sender, taken from user preferences.'),
122         'email_to': fields.char('To', size=256, help='Message recipients'),
123         'email_cc': fields.char('Cc', size=256, help='Carbon copy message recipients'),
124         'email_bcc': fields.char('Bcc', size=256, help='Blind carbon copy message recipients'),
125         'reply_to':fields.char('Reply-To', size=256, help='Preferred response address for the message'),
126         'headers': fields.text('Message Headers', readonly=1,
127             help="Full message headers, e.g. SMTP session headers (usually available on inbound messages only)"),
128         'message_id': fields.char('Message-Id', size=256, help='Message unique identifier', select=1, readonly=1),
129         'references': fields.text('References', help='Message references, such as identifiers of previous messages', readonly=1),
130         'content_subtype': fields.char('Message content subtype', size=32,
131             oldname="subtype", readonly=1,
132             help="Type of message, usually 'html' or 'plain', used to select "\
133                   "plain-text or rich-text contents accordingly"),
134         'body_text': fields.text('Text Contents', help="Plain-text version of the message"),
135         'body_html': fields.html('Rich-text Contents', help="Rich-text/HTML version of the message"),
136         'body': fields.function(get_body, fnct_search = search_body, type='text',
137             string='Message Content', store=True,
138             help="Content of the message. This content equals the body_text field "\
139                  "for plain-test messages, and body_html for rich-text/HTML "\
140                  "messages. This allows having one field if we want to access "\
141                  "the content matching the message content_subtype."),
142         'parent_id': fields.many2one('mail.message.common', 'Parent Message',
143             select=True, ondelete='set null',
144             help="Parent message, used for displaying as threads with hierarchy"),
145     }
146
147     _defaults = {
148         'content_subtype': 'plain',
149         'date': (lambda *a: fields.datetime.now()),
150     }
151
152 class mail_message(osv.Model):
153     """Model holding messages: system notification (replacing res.log
154        notifications), comments (for OpenChatter feature) and
155        RFC2822 email messages. This model also provides facilities to
156        parse, queue and send new email messages. Type of messages
157        are differentiated using the 'type' column. """
158
159     _name = 'mail.message'
160     _inherit = 'mail.message.common'
161     _description = 'Mail Message (email, comment, notification)'
162     _order = 'date desc'
163
164     def open_document(self, cr, uid, ids, context=None):
165         """ Open the message related document. Note that only the document of
166             ids[0] will be opened.
167             TODO: how to determine the action to use ?
168         """
169         action_data = False
170         if not ids:
171             return action_data
172         msg = self.browse(cr, uid, ids[0], context=context)
173         ir_act_window = self.pool.get('ir.actions.act_window')
174         action_ids = ir_act_window.search(cr, uid, [('res_model', '=', msg.model)], context=context)
175         if action_ids:
176             action_data = ir_act_window.read(cr, uid, action_ids[0], context=context)
177             action_data.update({
178                     'domain' : "[('id', '=', %d)]" % (msg.res_id),
179                     'nodestroy': True,
180                     'context': {}
181                     })
182         return action_data
183
184     def open_attachment(self, cr, uid, ids, context=None):
185         """ Open the message related attachments.
186             TODO: how to determine the action to use ?
187         """
188         action_data = False
189         if not ids:
190             return action_data
191         action_pool = self.pool.get('ir.actions.act_window')
192         messages = self.browse(cr, uid, ids, context=context)
193         att_ids = [x.id for message in messages for x in message.attachment_ids]
194         action_ids = action_pool.search(cr, uid, [('res_model', '=', 'ir.attachment')], context=context)
195         if action_ids:
196             action_data = action_pool.read(cr, uid, action_ids[0], context=context)
197             action_data.update({
198                 'domain': [('id', 'in', att_ids)],
199                 'nodestroy': True
200                 })
201         return action_data
202     
203     _columns = {
204         'type': fields.selection([
205                         ('email', 'email'),
206                         ('comment', 'Comment'),
207                         ('notification', 'System notification'),
208                         ], 'Type',
209             help="Message type: email for email message, notification for system "\
210                   "message, comment for other messages such as user replies"),
211         'partner_id': fields.many2one('res.partner', 'Related partner',
212             help="Deprecated field. Use partner_ids instead."),
213         'partner_ids': fields.many2many('res.partner',
214             'mail_message_res_partner_rel',
215             'message_id', 'partner_id', 'Destination partners',
216             help="When sending emails through the social network composition wizard"\
217                  "you may choose to send a copy of the mail to partners."),
218         'user_id': fields.many2one('res.users', 'Related User', readonly=1),
219         'attachment_ids': fields.many2many('ir.attachment', 'message_attachment_rel',
220             'message_id', 'attachment_id', 'Attachments'),
221         'mail_server_id': fields.many2one('ir.mail_server', 'Outgoing mail server', readonly=1),
222         'state': fields.selection([
223                         ('outgoing', 'Outgoing'),
224                         ('sent', 'Sent'),
225                         ('received', 'Received'),
226                         ('exception', 'Delivery Failed'),
227                         ('cancel', 'Cancelled'),
228                         ], 'Status', readonly=True),
229         'auto_delete': fields.boolean('Auto Delete',
230             help="Permanently delete this email after sending it, to save space"),
231         'original': fields.binary('Original', readonly=1,
232             help="Original version of the message, as it was sent on the network"),
233         'parent_id': fields.many2one('mail.message', 'Parent Message',
234             select=True, ondelete='set null',
235             help="Parent message, used for displaying as threads with hierarchy"),
236         'child_ids': fields.one2many('mail.message', 'parent_id', 'Child Messages'),
237     }
238         
239     _defaults = {
240         'type': 'email',
241         'state': 'received',
242     }
243     
244     #------------------------------------------------------
245     # Email api
246     #------------------------------------------------------
247     
248     def init(self, cr):
249         cr.execute("""SELECT indexname FROM pg_indexes WHERE indexname = 'mail_message_model_res_id_idx'""")
250         if not cr.fetchone():
251             cr.execute("""CREATE INDEX mail_message_model_res_id_idx ON mail_message (model, res_id)""")
252
253     def check(self, cr, uid, ids, mode, context=None, values=None):
254         """Restricts the access to a mail.message, according to referred model
255         """
256         if not ids:
257             return
258         res_ids = {}
259         if isinstance(ids, (int, long)):
260             ids = [ids]
261         cr.execute('SELECT DISTINCT model, res_id FROM mail_message WHERE id = ANY (%s)', (ids,))
262         for rmod, rid in cr.fetchall():
263             if not (rmod and rid):
264                 continue
265             res_ids.setdefault(rmod,set()).add(rid)
266         if values:
267             if 'res_model' in values and 'res_id' in values:
268                 res_ids.setdefault(values['res_model'],set()).add(values['res_id'])
269
270         ima_obj = self.pool.get('ir.model.access')
271         for model, mids in res_ids.items():
272             # ignore mail messages that are not attached to a resource anymore when checking access rights
273             # (resource was deleted but message was not)
274             mids = self.pool.get(model).exists(cr, uid, mids)
275             ima_obj.check(cr, uid, model, mode)
276             self.pool.get(model).check_access_rule(cr, uid, mids, mode, context=context)
277     
278     def create(self, cr, uid, values, context=None):
279         self.check(cr, uid, [], mode='create', context=context, values=values)
280         return super(mail_message, self).create(cr, uid, values, context)
281
282     def read(self, cr, uid, ids, fields_to_read=None, context=None, load='_classic_read'):
283         self.check(cr, uid, ids, 'read', context=context)
284         return super(mail_message, self).read(cr, uid, ids, fields_to_read, context, load)
285
286     def copy(self, cr, uid, id, default=None, context=None):
287         """Overridden to avoid duplicating fields that are unique to each email"""
288         if default is None:
289             default = {}
290         self.check(cr, uid, [id], 'read', context=context)
291         default.update(message_id=False, original=False, headers=False)
292         return super(mail_message,self).copy(cr, uid, id, default=default, context=context)
293     
294     def write(self, cr, uid, ids, vals, context=None):
295         self.check(cr, uid, ids, 'write', context=context, values=vals)
296         return super(mail_message, self).write(cr, uid, ids, vals, context)
297
298     def unlink(self, cr, uid, ids, context=None):
299         self.check(cr, uid, ids, 'unlink', context=context)
300         return super(mail_message, self).unlink(cr, uid, ids, context)
301
302     def schedule_with_attach(self, cr, uid, email_from, email_to, subject, body, model=False, type='email',
303                              email_cc=None, email_bcc=None, reply_to=False, partner_ids=None, attachments=None,
304                              message_id=False, references=False, res_id=False, content_subtype='plain',
305                              headers=None, mail_server_id=False, auto_delete=False, context=None):
306         """ Schedule sending a new email message, to be sent the next time the
307             mail scheduler runs, or the next time :meth:`process_email_queue` is
308             called explicitly.
309
310             :param string email_from: sender email address
311             :param list email_to: list of recipient addresses (to be joined with commas) 
312             :param string subject: email subject (no pre-encoding/quoting necessary)
313             :param string body: email body, according to the ``content_subtype`` 
314                 (by default, plaintext). If html content_subtype is used, the
315                 message will be automatically converted to plaintext and wrapped
316                 in multipart/alternative.
317             :param list email_cc: optional list of string values for CC header
318                 (to be joined with commas)
319             :param list email_bcc: optional list of string values for BCC header
320                 (to be joined with commas)
321             :param string model: optional model name of the document this mail
322                 is related to (this will also be used to generate a tracking id,
323                 used to match any response related to the same document)
324             :param int res_id: optional resource identifier this mail is related
325                 to (this will also be used to generate a tracking id, used to
326                 match any response related to the same document)
327             :param string reply_to: optional value of Reply-To header
328             :param partner_ids: destination partner_ids
329             :param string content_subtype: optional mime content_subtype for
330                 the text body (usually 'plain' or 'html'), must match the format
331                 of the ``body`` parameter. Default is 'plain', making the content
332                 part of the mail "text/plain".
333             :param dict attachments: map of filename to filecontents, where
334                 filecontents is a string containing the bytes of the attachment
335             :param dict headers: optional map of headers to set on the outgoing
336                 mail (may override the other headers, including Subject,
337                 Reply-To, Message-Id, etc.)
338             :param int mail_server_id: optional id of the preferred outgoing
339                 mail server for this mail
340             :param bool auto_delete: optional flag to turn on auto-deletion of
341                 the message after it has been successfully sent (default to False)
342         """
343         if context is None:
344             context = {}
345         if attachments is None:
346             attachments = {}
347         if partner_ids is None:
348             partner_ids = []
349         attachment_obj = self.pool.get('ir.attachment')
350         for param in (email_to, email_cc, email_bcc):
351             if param and not isinstance(param, list):
352                 param = [param]
353         msg_vals = {
354                 'subject': subject,
355                 'date': fields.datetime.now(),
356                 'user_id': uid,
357                 'model': model,
358                 'res_id': res_id,
359                 'type': type,
360                 'body_text': body if content_subtype != 'html' else False,
361                 'body_html': body if content_subtype == 'html' else False,
362                 'email_from': email_from,
363                 'email_to': email_to and ','.join(email_to) or '',
364                 'email_cc': email_cc and ','.join(email_cc) or '',
365                 'email_bcc': email_bcc and ','.join(email_bcc) or '',
366                 'partner_ids': partner_ids,
367                 'reply_to': reply_to,
368                 'message_id': message_id,
369                 'references': references,
370                 'content_subtype': content_subtype,
371                 'headers': headers, # serialize the dict on the fly
372                 'mail_server_id': mail_server_id,
373                 'state': 'outgoing',
374                 'auto_delete': auto_delete
375             }
376         email_msg_id = self.create(cr, uid, msg_vals, context)
377         attachment_ids = []
378         for attachment in attachments:
379             fname, fcontent = attachment
380             attachment_data = {
381                     'name': fname,
382                     'datas_fname': fname,
383                     'datas': fcontent and fcontent.encode('base64'),
384                     'res_model': self._name,
385                     'res_id': email_msg_id,
386             }
387             if context.has_key('default_type'):
388                 del context['default_type']
389             attachment_ids.append(attachment_obj.create(cr, uid, attachment_data, context))
390         if attachment_ids:
391             self.write(cr, uid, email_msg_id, { 'attachment_ids': [(6, 0, attachment_ids)]}, context=context)
392         return email_msg_id
393
394     def mark_outgoing(self, cr, uid, ids, context=None):
395         return self.write(cr, uid, ids, {'state':'outgoing'}, context=context)
396
397     def cancel(self, cr, uid, ids, context=None):
398         return self.write(cr, uid, ids, {'state':'cancel'}, context=context)
399
400     def process_email_queue(self, cr, uid, ids=None, context=None):
401         """Send immediately queued messages, committing after each
402            message is sent - this is not transactional and should
403            not be called during another transaction!
404
405            :param list ids: optional list of emails ids to send. If passed
406                             no search is performed, and these ids are used
407                             instead.
408            :param dict context: if a 'filters' key is present in context,
409                                 this value will be used as an additional
410                                 filter to further restrict the outgoing
411                                 messages to send (by default all 'outgoing'
412                                 messages are sent).
413         """
414         if context is None:
415             context = {}
416         if not ids:
417             filters = ['&', ('state', '=', 'outgoing'), ('type', '=', 'email')]
418             if 'filters' in context:
419                 filters.extend(context['filters'])
420             ids = self.search(cr, uid, filters, context=context)
421         res = None
422         try:
423             # Force auto-commit - this is meant to be called by
424             # the scheduler, and we can't allow rolling back the status
425             # of previously sent emails!
426             res = self.send(cr, uid, ids, auto_commit=True, context=context)
427         except Exception:
428             _logger.exception("Failed processing mail queue")
429         return res
430
431     def parse_message(self, message, save_original=False, context=None):
432         """Parses a string or email.message.Message representing an
433            RFC-2822 email, and returns a generic dict holding the
434            message details.
435
436            :param message: the message to parse
437            :type message: email.message.Message | string | unicode
438            :param bool save_original: whether the returned dict
439                should include an ``original`` entry with the base64
440                encoded source of the message.
441            :rtype: dict
442            :return: A dict with the following structure, where each
443                     field may not be present if missing in original
444                     message::
445
446                     { 'message-id': msg_id,
447                       'subject': subject,
448                       'from': from,
449                       'to': to,
450                       'cc': cc,
451                       'headers' : { 'X-Mailer': mailer,
452                                     #.. all X- headers...
453                                   },
454                       'content_subtype': msg_mime_subtype,
455                       'body_text': plaintext_body
456                       'body_html': html_body,
457                       'attachments': [('file1', 'bytes'),
458                                        ('file2', 'bytes') }
459                        # ...
460                        'original': source_of_email,
461                     }
462         """
463         msg_txt = message
464         if isinstance(message, str):
465             msg_txt = email.message_from_string(message)
466
467         # Warning: message_from_string doesn't always work correctly on unicode,
468         # we must use utf-8 strings here :-(
469         if isinstance(message, unicode):
470             message = message.encode('utf-8')
471             msg_txt = email.message_from_string(message)
472
473         message_id = msg_txt.get('message-id', False)
474         msg = {}
475
476         if save_original:
477             # save original, we need to be able to read the original email sometimes
478             msg['original'] = message.as_string() if isinstance(message, Message) \
479                                                   else message
480             msg['original'] = base64.b64encode(msg['original']) # binary fields are b64
481
482         if not message_id:
483             # Very unusual situation, be we should be fault-tolerant here
484             message_id = time.time()
485             msg_txt['message-id'] = message_id
486             _logger.info('Parsing Message without message-id, generating a random one: %s', message_id)
487
488         msg_fields = msg_txt.keys()
489         msg['id'] = message_id
490         msg['message-id'] = message_id
491
492         if 'Subject' in msg_fields:
493             msg['subject'] = decode(msg_txt.get('Subject'))
494
495         if 'Content-Type' in msg_fields:
496             msg['content-type'] = msg_txt.get('Content-Type')
497
498         if 'From' in msg_fields:
499             msg['from'] = decode(msg_txt.get('From') or msg_txt.get_unixfrom())
500
501         if 'To' in msg_fields:
502             msg['to'] = decode(msg_txt.get('To'))
503
504         if 'Delivered-To' in msg_fields:
505             msg['to'] = decode(msg_txt.get('Delivered-To'))
506
507         if 'CC' in msg_fields:
508             msg['cc'] = decode(msg_txt.get('CC'))
509
510         if 'Cc' in msg_fields:
511             msg['cc'] = decode(msg_txt.get('Cc'))
512
513         if 'Reply-To' in msg_fields:
514             msg['reply'] = decode(msg_txt.get('Reply-To'))
515
516         if 'Date' in msg_fields:
517             date_hdr = decode(msg_txt.get('Date'))
518             # convert from email timezone to server timezone
519             date_server_datetime = dateutil.parser.parse(date_hdr).astimezone(pytz.timezone(tools.get_server_timezone()))
520             date_server_datetime_str = date_server_datetime.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
521             msg['date'] = date_server_datetime_str
522
523         if 'Content-Transfer-Encoding' in msg_fields:
524             msg['encoding'] = msg_txt.get('Content-Transfer-Encoding')
525
526         if 'References' in msg_fields:
527             msg['references'] = msg_txt.get('References')
528
529         if 'In-Reply-To' in msg_fields:
530             msg['in-reply-to'] = msg_txt.get('In-Reply-To')
531
532         msg['headers'] = {}
533         msg['content_subtype'] = 'plain'
534         for item in msg_txt.items():
535             if item[0].startswith('X-'):
536                 msg['headers'].update({item[0]: item[1]})
537         if not msg_txt.is_multipart() or 'text/plain' in msg.get('content-type', ''):
538             encoding = msg_txt.get_content_charset()
539             body = msg_txt.get_payload(decode=True)
540             if 'text/html' in msg.get('content-type', ''):
541                 msg['body_html'] =  body
542                 msg['content_subtype'] = 'html'
543                 if body:
544                     body = tools.html2plaintext(body)
545             msg['body_text'] = tools.ustr(body, encoding)
546
547         attachments = []
548         if msg_txt.is_multipart() or 'multipart/alternative' in msg.get('content-type', ''):
549             body = ""
550             if 'multipart/alternative' in msg.get('content-type', ''):
551                 msg['content_subtype'] = 'alternative'
552             else:
553                 msg['content_subtype'] = 'mixed'
554             for part in msg_txt.walk():
555                 if part.get_content_maintype() == 'multipart':
556                     continue
557
558                 encoding = part.get_content_charset()
559                 filename = part.get_filename()
560                 if part.get_content_maintype()=='text':
561                     content = part.get_payload(decode=True)
562                     if filename:
563                         attachments.append((filename, content))
564                     content = tools.ustr(content, encoding)
565                     if part.get_content_subtype() == 'html':
566                         msg['body_html'] = content
567                         msg['content_subtype'] = 'html' # html version prevails
568                         body = tools.ustr(tools.html2plaintext(content))
569                         body = body.replace('&#13;', '')
570                     elif part.get_content_subtype() == 'plain':
571                         body = content
572                 elif part.get_content_maintype() in ('application', 'image'):
573                     if filename :
574                         attachments.append((filename,part.get_payload(decode=True)))
575                     else:
576                         res = part.get_payload(decode=True)
577                         body += tools.ustr(res, encoding)
578
579             msg['body_text'] = body
580         msg['attachments'] = attachments
581
582         # for backwards compatibility:
583         msg['body'] = msg['body_text']
584         msg['sub_type'] = msg['content_subtype'] or 'plain'
585         return msg
586
587     def _postprocess_sent_message(self, cr, uid, message, context=None):
588         """Perform any post-processing necessary after sending ``message``
589         successfully, including deleting it completely along with its
590         attachment if the ``auto_delete`` flag of the message was set.
591         Overridden by subclasses for extra post-processing behaviors. 
592
593         :param browse_record message: the message that was just sent
594         :return: True
595         """
596         if message.auto_delete:
597             self.pool.get('ir.attachment').unlink(cr, uid,
598                 [x.id for x in message.attachment_ids
599                     if x.res_model == self._name and x.res_id == message.id],
600                 context=context)
601             message.unlink()
602         return True
603
604     def send(self, cr, uid, ids, auto_commit=False, context=None):
605         """Sends the selected emails immediately, ignoring their current
606            state (mails that have already been sent should not be passed
607            unless they should actually be re-sent).
608            Emails successfully delivered are marked as 'sent', and those
609            that fail to be deliver are marked as 'exception', and the
610            corresponding error message is output in the server logs.
611
612            :param bool auto_commit: whether to force a commit of the message
613                                     status after sending each message (meant
614                                     only for processing by the scheduler),
615                                     should never be True during normal
616                                     transactions (default: False)
617            :return: True
618         """
619         ir_mail_server = self.pool.get('ir.mail_server')
620         self.write(cr, uid, ids, {'state': 'outgoing'}, context=context)
621         for message in self.browse(cr, uid, ids, context=context):
622             try:
623                 attachments = []
624                 for attach in message.attachment_ids:
625                     attachments.append((attach.datas_fname, base64.b64decode(attach.datas)))
626
627                 body = message.body_html if message.content_subtype == 'html' else message.body_text
628                 body_alternative = None
629                 content_subtype_alternative = None
630                 if message.content_subtype == 'html' and message.body_text:
631                     # we have a plain text alternative prepared, pass it to 
632                     # build_message instead of letting it build one
633                     body_alternative = message.body_text
634                     content_subtype_alternative = 'plain'
635
636                 # handle destination_partners
637                 partner_ids_email_to = ''
638                 for partner in message.partner_ids:
639                     partner_ids_email_to += '%s ' % (partner.email or '')
640                 message_email_to = '%s %s' % (partner_ids_email_to, message.email_to or '')
641
642                 # build an RFC2822 email.message.Message object and send it
643                 # without queuing
644                 msg = ir_mail_server.build_email(
645                     email_from=message.email_from,
646                     email_to=mail_tools_to_email(message_email_to),
647                     subject=message.subject,
648                     body=body,
649                     body_alternative=body_alternative,
650                     email_cc=mail_tools_to_email(message.email_cc),
651                     email_bcc=mail_tools_to_email(message.email_bcc),
652                     reply_to=message.reply_to,
653                     attachments=attachments, message_id=message.message_id,
654                     references = message.references,
655                     object_id=message.res_id and ('%s-%s' % (message.res_id,message.model)),
656                     subtype=message.content_subtype,
657                     subtype_alternative=content_subtype_alternative,
658                     headers=message.headers and ast.literal_eval(message.headers))
659                 res = ir_mail_server.send_email(cr, uid, msg,
660                                                 mail_server_id=message.mail_server_id.id,
661                                                 context=context)
662                 if res:
663                     message.write({'state':'sent', 'message_id': res, 'email_to': message_email_to})
664                 else:
665                     message.write({'state':'exception', 'email_to': message_email_to})
666                 message.refresh()
667                 if message.state == 'sent':
668                     self._postprocess_sent_message(cr, uid, message, context=context)
669             except Exception:
670                 _logger.exception('failed sending mail.message %s', message.id)
671                 message.write({'state':'exception'})
672
673             if auto_commit == True:
674                 cr.commit()
675         return True
676
677
678
679 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: