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