[ADD] Added first not-working draft for widget. Cleaned message search function.
[odoo/odoo.git] / addons / mail / mail_message.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2010-2011 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 from email.header import decode_header
30 from email.message import Message
31
32 import tools
33 from osv import osv
34 from osv import fields
35 from tools.translate import _
36
37 _logger = logging.getLogger('mail')
38
39 def format_date_tz(date, tz=None):
40     if not date:
41         return 'n/a'
42     format = tools.DEFAULT_SERVER_DATETIME_FORMAT
43     return tools.server_to_local_timestamp(date, format, format, tz)
44
45 def truncate_text(text):
46     lines = text and text.split('\n') or []
47     if len(lines) > 3:
48         res = '\n\t'.join(lines[:3]) + '...'
49     else:
50         res = '\n\t'.join(lines)
51     return res
52
53 def decode(text):
54     """Returns unicode() string conversion of the the given encoded smtp header text"""
55     if text:
56         text = decode_header(text.replace('\r', ''))
57         return ''.join([tools.ustr(x[0], x[1]) for x in text])
58
59 def to_email(text):
60     """Return a list of the email addresses found in ``text``"""
61     if not text: return []
62     return re.findall(r'([^ ,<@]+@[^> ,]+)', text)
63
64 class mail_message_common(osv.osv_memory):
65     """Common abstract class for holding the main attributes of a 
66        message object. It could be reused as parent model for any
67        database model or wizard screen that needs to hold a kind of
68        message"""
69
70     _name = 'mail.message.common'
71     _rec_name = 'subject'
72     _columns = {
73         'subject': fields.char('Subject', size=512, required=True),
74         'model': fields.char('Related Document model', size=128, select=1), # was readonly
75         'res_id': fields.integer('Related Document ID', select=1), # was readonly
76         'date': fields.datetime('Date'),
77         'email_from': fields.char('From', size=128, help='Message sender, taken from user preferences. If empty, this is not a mail but a message.'),
78         'email_to': fields.char('To', size=256, help='Message recipients'),
79         'email_cc': fields.char('Cc', size=256, help='Carbon copy message recipients'),
80         'email_bcc': fields.char('Bcc', size=256, help='Blind carbon copy message recipients'),
81         'reply_to':fields.char('Reply-To', size=256, help='Preferred response address for the message'),
82         'headers': fields.text('Message headers', readonly=1,
83                                help="Full message headers, e.g. SMTP session headers "
84                                     "(usually available on inbound messages only)"),
85         'message_id': fields.char('Message-Id', size=256, help='Message unique identifier', select=1, readonly=1),
86         'references': fields.text('References', help='Message references, such as identifiers of previous messages', readonly=1),
87         'subtype': fields.char('Message type', size=32, help="Type of message, usually 'html' or 'plain', used to "
88                                                              "select plaintext or rich text contents accordingly", readonly=1),
89         'body_text': fields.text('Text contents', help="Plain-text version of the message"),
90         'body_html': fields.text('Rich-text contents', help="Rich-text/HTML version of the message"),
91     }
92
93     _defaults = {
94         'subtype': 'plain'
95     }
96
97 class mail_message(osv.osv):
98     '''Model holding RFC2822 email messages, and providing facilities
99        to parse, queue and send new messages
100
101        Messages that do not have a value for the email_from column
102        are simple log messages (e.g. document state changes), while
103        actual e-mails have the email_from value set.
104        The ``display_text`` field will have a slightly different
105        presentation for real emails and for log messages.
106        '''
107
108     _name = 'mail.message'
109     _inherit = 'mail.message.common'
110     _description = 'Generic Message (Email, Comment, Notification)'
111     _order = 'date desc'
112
113     # XXX to review - how to determine action to use?
114     def open_document(self, cr, uid, ids, context=None):
115         action_data = False
116         if ids:
117             msg = self.browse(cr, uid, ids[0], context=context)
118             model = msg.model
119             res_id = msg.res_id
120
121             ir_act_window = self.pool.get('ir.actions.act_window')
122             action_ids = ir_act_window.search(cr, uid, [('res_model', '=', model)])
123             if action_ids:
124                 action_data = ir_act_window.read(cr, uid, action_ids[0], context=context)
125                 action_data.update({
126                     'domain' : "[('id','=',%d)]"%(res_id),
127                     'nodestroy': True,
128                     'context': {}
129                     })
130         return action_data
131
132     # XXX to review - how to determine action to use?
133     def open_attachment(self, cr, uid, ids, context=None):
134         action_data = False
135         action_pool = self.pool.get('ir.actions.act_window')
136         message = self.browse(cr, uid, ids, context=context)[0]
137         att_ids = [x.id for x in message.attachment_ids]
138         action_ids = action_pool.search(cr, uid, [('res_model', '=', 'ir.attachment')])
139         if action_ids:
140             action_data = action_pool.read(cr, uid, action_ids[0], context=context)
141             action_data.update({
142                 'domain': [('id','in',att_ids)],
143                 'nodestroy': True
144                 })
145         return action_data
146
147     def _get_display_text(self, cr, uid, ids, name, arg, context=None):
148         if context is None:
149             context = {}
150         tz = context.get('tz')
151         result = {}
152         for message in self.browse(cr, uid, ids, context=context):
153             msg_txt = ''
154             if message.email_from:
155                 msg_txt += _('%s wrote on %s: \n Subject: %s \n\t') % (message.email_from or '/', format_date_tz(message.date, tz), message.subject)
156                 if message.body_text:
157                     msg_txt += truncate_text(message.body_text)
158             else:
159                 msg_txt = (message.user_id.name or '/') + _(' on ') + format_date_tz(message.date, tz) + ':\n\t'
160                 msg_txt += message.subject
161             result[message.id] = msg_txt
162         return result
163
164     _columns = {
165         'partner_id': fields.many2one('res.partner', 'Related partner'),
166         'user_id': fields.many2one('res.users', 'Related user', readonly=1),
167         'attachment_ids': fields.many2many('ir.attachment', 'message_attachment_rel', 'message_id', 'attachment_id', 'Attachments'),
168         'display_text': fields.function(_get_display_text, method=True, type='text', size="512", string='Display Text'),
169         'mail_server_id': fields.many2one('ir.mail_server', 'Outgoing mail server', readonly=1),
170         'state': fields.selection([
171                         ('outgoing', 'Outgoing'),
172                         ('sent', 'Sent'),
173                         ('received', 'Received'),
174                         ('exception', 'Delivery Failed'),
175                         ('cancel', 'Cancelled'),
176                         ], 'State', readonly=True),
177         'auto_delete': fields.boolean('Auto Delete', help="Permanently delete this email after sending it, to save space"),
178         'original': fields.binary('Original', help="Original version of the message, as it was sent on the network", readonly=1),
179         # note feature: add type (email, comment, notification) and need_action
180         'type': fields.selection([
181                         ('email', 'e-mail'),
182                         ('comment', 'Comment'),
183                         ('notification', 'Notification'),
184                         ], 'Type', help="Message type: e-mail for e-mail message, notification for system message, comment for other messages such as user replies"),
185         'need_action': fields.boolean('Need action', help="Asks the user to perform an action"),
186     }
187
188     _defaults = {
189         'state': 'received',
190         'type': 'notification',
191     }
192
193     #------------------------------------------------------
194     # Generic api
195     #------------------------------------------------------
196     
197     def create(self, cr, uid, vals, context=None):
198         msg_id = super(mail_message, self).create(cr, uid, vals, context)
199         # push the message to suscribed users
200         subscription_obj = self.pool.get('mail.subscription')
201         notification_obj = self.pool.get('mail.notification')
202         sub_ids = subscription_obj.search(cr, uid, ['&', ('res_model', '=', vals['model']), ('user_id', '=', uid)], context=context)
203         subs = subscription_obj.browse(cr, uid, sub_ids, context=context)
204         for sub in subs:
205             notification_obj.create(cr, uid, {'user_id': sub.user_id, 'message_id': msg_id}, context=context)
206         return msg_id
207     
208     def get_pushed_messages(self, cr, uid, context=None):
209         """Wall: get messages to display"""
210         notification_obj = self.pool.get('mail.notification')
211         notification_ids = notification_obj.search(cr, uid, [('user_id', '=', uid)], context=context)
212         notifications = notification_obj.browse(cr, uid, notification_ids, context=context)
213         
214         # TODO / REMARK: classify based on res_model / res_id to have a 1_level hierarchy ?
215         
216         return notifications
217     
218     
219     #------------------------------------------------------
220     # Note specific api
221     #------------------------------------------------------
222     
223     #def tmp_backup(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
224         #if not context or not context.has_key('filter_search'):
225             #return super(mail_message, self).search(cr, uid, args, offset=offset, limit=limit, order=order, context=context, count=count)
226         
227         ## get subscriptions
228         #sub_obj = self.pool.get('mail.subscription')
229         #sub_ids = sub_obj.search(cr, uid, [('user_id', '=', uid)])
230         #subs = sub_obj.browse(cr, uid, sub_ids)
231         
232         ## stock tweets to find
233         #res_model_ids_dict = {}
234         #res_model_all_list = []
235         
236         ## check all subscriptions
237         #for sub in subs:
238             #if sub.res_model and sub.res_id == 0 and sub.res_domain == False:
239                 #print "s-1"
240                 #if sub.res_model not in res_model_all_list:
241                     #res_model_all_list.append(sub.res_model)
242             #elif sub.res_model and sub.res_id:
243                 #print "s-2"
244                 #if res_model_ids_dict.has_key(sub.res_model):
245                     #res_model_ids_dict[sub.res_model].append(sub.res_id)
246                 #else:
247                     #res_model_ids_dict[sub.res_model] = [sub.res_id]
248             #elif sub.res_model and sub.res_domain:
249                 #print "s-3"
250                 #res_obj = self.pool.get(sub.res_model)
251                 #print sub.res_domain
252                 ##res_ids = res_obj.search(cr, uid, [('id', 'in', [1,2])])
253                 #res_ids = res_obj.search(cr, uid, eval(sub.res_domain))
254                 #if res_model_ids_dict.has_key(sub.res_model):
255                     #res_model_ids_dict[sub.res_model] += res_ids
256                 #else:
257                     #res_model_ids_dict[sub.res_model] = res_ids
258                 #print 'cacaprout'
259             #else:
260                 #print 'erreur !!!'
261                 #print sub
262         
263         ## add fully-followed domains
264         #args.append('|')
265         #args.append(['model', 'in', res_model_all_list])
266         
267         ## add partially-followed domains
268         #for x in range(0, len(res_model_ids_dict.keys())-1):
269             #args.append('|')
270         
271         #for res_model in res_model_ids_dict.keys():
272             #if res_model not in res_model_all_list:
273                 #args.append('&')
274                 #args.append(['model', '=', res_model])
275                 #args.append(['res_id', 'in', res_model_ids_dict[res_model]])
276         
277         #if context and context.has_key('filter_search'):
278             #pass
279         #else:
280             #args = []
281         #print args
282         #return super(mail_message, self).search(cr, uid, args, offset=offset, limit=limit,order=order, context=context, count=count)
283     
284     #------------------------------------------------------
285     # E-Mail api
286     #------------------------------------------------------
287     
288     def init(self, cr):
289         cr.execute("""SELECT indexname FROM pg_indexes WHERE indexname = 'mail_message_model_res_id_idx'""")
290         if not cr.fetchone():
291             cr.execute("""CREATE INDEX mail_message_model_res_id_idx ON mail_message (model, res_id)""")
292
293     def copy(self, cr, uid, id, default=None, context=None):
294         """Overridden to avoid duplicating fields that are unique to each email"""
295         if default is None:
296             default = {}
297         default.update(message_id=False,original=False,headers=False)
298         return super(mail_message,self).copy(cr, uid, id, default=default, context=context)
299
300     def schedule_with_attach(self, cr, uid, email_from, email_to, subject, body, model=False, email_cc=None,
301                              email_bcc=None, reply_to=False, attachments=None, message_id=False, references=False,
302                              res_id=False, subtype='plain', headers=None, mail_server_id=False, auto_delete=False,
303                              context=None):
304         """Schedule sending a new email message, to be sent the next time the mail scheduler runs, or
305            the next time :meth:`process_email_queue` is called explicitly.
306
307            :param string email_from: sender email address
308            :param list email_to: list of recipient addresses (to be joined with commas) 
309            :param string subject: email subject (no pre-encoding/quoting necessary)
310            :param string body: email body, according to the ``subtype`` (by default, plaintext).
311                                If html subtype is used, the message will be automatically converted
312                                to plaintext and wrapped in multipart/alternative.
313            :param list email_cc: optional list of string values for CC header (to be joined with commas)
314            :param list email_bcc: optional list of string values for BCC header (to be joined with commas)
315            :param string model: optional model name of the document this mail is related to (this will also
316                                 be used to generate a tracking id, used to match any response related to the
317                                 same document)
318            :param int res_id: optional resource identifier this mail is related to (this will also
319                               be used to generate a tracking id, used to match any response related to the
320                               same document)
321            :param string reply_to: optional value of Reply-To header
322            :param string subtype: optional mime subtype for the text body (usually 'plain' or 'html'),
323                                   must match the format of the ``body`` parameter. Default is 'plain',
324                                   making the content part of the mail "text/plain".
325            :param dict attachments: map of filename to filecontents, where filecontents is a string
326                                     containing the bytes of the attachment
327            :param dict headers: optional map of headers to set on the outgoing mail (may override the
328                                 other headers, including Subject, Reply-To, Message-Id, etc.)
329            :param int mail_server_id: optional id of the preferred outgoing mail server for this mail
330            :param bool auto_delete: optional flag to turn on auto-deletion of the message after it has been
331                                     successfully sent (default to False)
332
333         """
334         if context is None:
335             context = {}
336         if attachments is None:
337             attachments = {}
338         attachment_obj = self.pool.get('ir.attachment')
339         for param in (email_to, email_cc, email_bcc):
340             if param and not isinstance(param, list):
341                 param = [param]
342         msg_vals = {
343                 'subject': subject,
344                 'date': time.strftime('%Y-%m-%d %H:%M:%S'),
345                 'user_id': uid,
346                 'model': model,
347                 'res_id': res_id,
348                 'body_text': body if subtype != 'html' else False,
349                 'body_html': body if subtype == 'html' else False,
350                 'email_from': email_from,
351                 'email_to': email_to and ','.join(email_to) or '',
352                 'email_cc': email_cc and ','.join(email_cc) or '',
353                 'email_bcc': email_bcc and ','.join(email_bcc) or '',
354                 'reply_to': reply_to,
355                 'message_id': message_id,
356                 'references': references,
357                 'subtype': subtype,
358                 'headers': headers, # serialize the dict on the fly
359                 'mail_server_id': mail_server_id,
360                 'state': 'outgoing',
361                 'auto_delete': auto_delete
362             }
363         email_msg_id = self.create(cr, uid, msg_vals, context)
364         attachment_ids = []
365         for fname, fcontent in attachments.iteritems():
366             attachment_data = {
367                     'name': fname,
368                     'datas_fname': fname,
369                     'datas': fcontent,
370                     'res_model': self._name,
371                     'res_id': email_msg_id,
372             }
373             if context.has_key('default_type'):
374                 del context['default_type']
375             attachment_ids.append(attachment_obj.create(cr, uid, attachment_data, context))
376         if attachment_ids:
377             self.write(cr, uid, email_msg_id, { 'attachment_ids': [(6, 0, attachment_ids)]}, context=context)
378         return email_msg_id
379
380     def mark_outgoing(self, cr, uid, ids, context=None):
381         return self.write(cr, uid, ids, {'state':'outgoing'}, context)
382
383     def process_email_queue(self, cr, uid, ids=None, context=None):
384         """Send immediately queued messages, committing after each
385            message is sent - this is not transactional and should
386            not be called during another transaction!
387
388            :param list ids: optional list of emails ids to send. If passed
389                             no search is performed, and these ids are used
390                             instead.
391            :param dict context: if a 'filters' key is present in context,
392                                 this value will be used as an additional
393                                 filter to further restrict the outgoing
394                                 messages to send (by default all 'outgoing'
395                                 messages are sent).
396         """
397         if context is None:
398             context = {}
399         if not ids:
400             filters = [('state', '=', 'outgoing')]
401             if 'filters' in context:
402                 filters.extend(context['filters'])
403             ids = self.search(cr, uid, filters, context=context)
404         res = None
405         try:
406             # Force auto-commit - this is meant to be called by
407             # the scheduler, and we can't allow rolling back the status
408             # of previously sent emails!
409             res = self.send(cr, uid, ids, auto_commit=True, context=context)
410         except Exception:
411             _logger.exception("Failed processing mail queue")
412         return res
413
414     def parse_message(self, message, save_original=False):
415         """Parses a string or email.message.Message representing an
416            RFC-2822 email, and returns a generic dict holding the
417            message details.
418
419            :param message: the message to parse
420            :type message: email.message.Message | string | unicode
421            :param bool save_original: whether the returned dict
422                should include an ``original`` entry with the base64
423                encoded source of the message.
424            :rtype: dict
425            :return: A dict with the following structure, where each
426                     field may not be present if missing in original
427                     message::
428
429                     { 'message-id': msg_id,
430                       'subject': subject,
431                       'from': from,
432                       'to': to,
433                       'cc': cc,
434                       'headers' : { 'X-Mailer': mailer,
435                                     #.. all X- headers...
436                                   },
437                       'subtype': msg_mime_subtype,
438                       'body_text': plaintext_body
439                       'body_html': html_body,
440                       'attachments': [('file1', 'bytes'),
441                                        ('file2', 'bytes') }
442                        # ...
443                        'original': source_of_email,
444                     }
445         """
446         msg_txt = message
447         if isinstance(message, str):
448             msg_txt = email.message_from_string(message)
449
450         # Warning: message_from_string doesn't always work correctly on unicode,
451         # we must use utf-8 strings here :-(
452         if isinstance(message, unicode):
453             message = message.encode('utf-8')
454             msg_txt = email.message_from_string(message)
455
456         message_id = msg_txt.get('message-id', False)
457         msg = {}
458
459         if save_original:
460             # save original, we need to be able to read the original email sometimes
461             msg['original'] = message.as_string() if isinstance(message, Message) \
462                                                   else message
463             msg['original'] = base64.b64encode(msg['original']) # binary fields are b64
464
465         if not message_id:
466             # Very unusual situation, be we should be fault-tolerant here
467             message_id = time.time()
468             msg_txt['message-id'] = message_id
469             _logger.info('Parsing Message without message-id, generating a random one: %s', message_id)
470
471         fields = msg_txt.keys()
472         msg['id'] = message_id
473         msg['message-id'] = message_id
474
475         if 'Subject' in fields:
476             msg['subject'] = decode(msg_txt.get('Subject'))
477
478         if 'Content-Type' in fields:
479             msg['content-type'] = msg_txt.get('Content-Type')
480
481         if 'From' in fields:
482             msg['from'] = decode(msg_txt.get('From') or msg_txt.get_unixfrom())
483
484         if 'To' in fields:
485             msg['to'] = decode(msg_txt.get('To'))
486
487         if 'Delivered-To' in fields:
488             msg['to'] = decode(msg_txt.get('Delivered-To'))
489
490         if 'CC' in fields:
491             msg['cc'] = decode(msg_txt.get('CC'))
492
493         if 'Cc' in fields:
494             msg['cc'] = decode(msg_txt.get('Cc'))
495
496         if 'Reply-To' in fields:
497             msg['reply'] = decode(msg_txt.get('Reply-To'))
498
499         if 'Date' in fields:
500             date_hdr = decode(msg_txt.get('Date'))
501             msg['date'] = dateutil.parser.parse(date_hdr).strftime("%Y-%m-%d %H:%M:%S")
502
503         if 'Content-Transfer-Encoding' in fields:
504             msg['encoding'] = msg_txt.get('Content-Transfer-Encoding')
505
506         if 'References' in fields:
507             msg['references'] = msg_txt.get('References')
508
509         if 'In-Reply-To' in fields:
510             msg['in-reply-to'] = msg_txt.get('In-Reply-To')
511
512         msg['headers'] = {}
513         msg['subtype'] = 'plain'
514         for item in msg_txt.items():
515             if item[0].startswith('X-'):
516                 msg['headers'].update({item[0]: item[1]})
517         if not msg_txt.is_multipart() or 'text/plain' in msg.get('content-type', ''):
518             encoding = msg_txt.get_content_charset()
519             body = msg_txt.get_payload(decode=True)
520             if 'text/html' in msg.get('content-type', ''):
521                 msg['body_html'] =  body
522                 msg['subtype'] = 'html'
523                 body = tools.html2plaintext(body)
524             msg['body_text'] = tools.ustr(body, encoding)
525
526         attachments = []
527         if msg_txt.is_multipart() or 'multipart/alternative' in msg.get('content-type', ''):
528             body = ""
529             if 'multipart/alternative' in msg.get('content-type', ''):
530                 msg['subtype'] = 'alternative'
531             else:
532                 msg['subtype'] = 'mixed'
533             for part in msg_txt.walk():
534                 if part.get_content_maintype() == 'multipart':
535                     continue
536
537                 encoding = part.get_content_charset()
538                 filename = part.get_filename()
539                 if part.get_content_maintype()=='text':
540                     content = part.get_payload(decode=True)
541                     if filename:
542                         attachments.append((filename, content))
543                     content = tools.ustr(content, encoding)
544                     if part.get_content_subtype() == 'html':
545                         msg['body_html'] = content
546                         msg['subtype'] = 'html' # html version prevails
547                         body = tools.ustr(tools.html2plaintext(content))
548                     elif part.get_content_subtype() == 'plain':
549                         body = content
550                 elif part.get_content_maintype() in ('application', 'image'):
551                     if filename :
552                         attachments.append((filename,part.get_payload(decode=True)))
553                     else:
554                         res = part.get_payload(decode=True)
555                         body += tools.ustr(res, encoding)
556
557             msg['body_text'] = body
558         msg['attachments'] = attachments
559
560         # for backwards compatibility:
561         msg['body'] = msg['body_text']
562         msg['sub_type'] = msg['subtype'] or 'plain'
563         return msg
564
565
566     def send(self, cr, uid, ids, auto_commit=False, context=None):
567         """Sends the selected emails immediately, ignoring their current
568            state (mails that have already been sent should not be passed
569            unless they should actually be re-sent).
570            Emails successfully delivered are marked as 'sent', and those
571            that fail to be deliver are marked as 'exception', and the
572            corresponding error message is output in the server logs.
573
574            :param bool auto_commit: whether to force a commit of the message
575                                     status after sending each message (meant
576                                     only for processing by the scheduler),
577                                     should never be True during normal
578                                     transactions (default: False)
579            :return: True
580         """
581         if context is None:
582             context = {}
583         ir_mail_server = self.pool.get('ir.mail_server')
584         self.write(cr, uid, ids, {'state': 'outgoing'}, context=context)
585         for message in self.browse(cr, uid, ids, context=context):
586             try:
587                 attachments = []
588                 for attach in message.attachment_ids:
589                     attachments.append((attach.datas_fname, base64.b64decode(attach.datas)))
590
591                 body = message.body_html if message.subtype == 'html' else message.body_text
592                 body_alternative = None
593                 subtype_alternative = None
594                 if message.subtype == 'html' and message.body_text:
595                     # we have a plain text alternative prepared, pass it to 
596                     # build_message instead of letting it build one
597                     body_alternative = message.body_text
598                     subtype_alternative = 'plain'
599
600                 msg = ir_mail_server.build_email(
601                     email_from=message.email_from,
602                     email_to=to_email(message.email_to),
603                     subject=message.subject,
604                     body=body,
605                     body_alternative=body_alternative,
606                     email_cc=to_email(message.email_cc),
607                     email_bcc=to_email(message.email_bcc),
608                     reply_to=message.reply_to,
609                     attachments=attachments, message_id=message.message_id,
610                     references = message.references,
611                     object_id=message.res_id and ('%s-%s' % (message.res_id,message.model)),
612                     subtype=message.subtype,
613                     subtype_alternative=subtype_alternative,
614                     headers=message.headers and ast.literal_eval(message.headers))
615                 res = ir_mail_server.send_email(cr, uid, msg,
616                                                 mail_server_id=message.mail_server_id.id,
617                                                 context=context)
618                 if res:
619                     message.write({'state':'sent', 'message_id': res})
620                 else:
621                     message.write({'state':'exception'})
622
623                 # if auto_delete=True then delete that sent messages as well as attachments
624                 message.refresh()
625                 if message.state == 'sent' and message.auto_delete:
626                     self.pool.get('ir.attachment').unlink(cr, uid,
627                                                           [x.id for x in message.attachment_ids],
628                                                           context=context)
629                     message.unlink()
630             except Exception:
631                 _logger.exception('failed sending mail.message %s', message.id)
632                 message.write({'state':'exception'})
633
634             if auto_commit == True:
635                 cr.commit()
636         return True
637
638     def cancel(self, cr, uid, ids, context=None):
639         self.write(cr, uid, ids, {'state':'cancel'}, context=context)
640         return True
641
642 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: