Merge pull request #2350 from odoo-dev/master-momentjs-csn
[odoo/odoo.git] / addons / mail / mail_mail.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 base64
23 import logging
24 from email.utils import formataddr
25 from urlparse import urljoin
26
27 from openerp import api, tools
28 from openerp import SUPERUSER_ID
29 from openerp.addons.base.ir.ir_mail_server import MailDeliveryException
30 from openerp.osv import fields, osv
31 from openerp.tools.safe_eval import safe_eval as eval
32 from openerp.tools.translate import _
33 import openerp.tools as tools
34
35 _logger = logging.getLogger(__name__)
36
37
38 class mail_mail(osv.Model):
39     """ Model holding RFC2822 email messages to send. This model also provides
40         facilities to queue and send new email messages.  """
41     _name = 'mail.mail'
42     _description = 'Outgoing Mails'
43     _inherits = {'mail.message': 'mail_message_id'}
44     _order = 'id desc'
45     _rec_name = 'subject'
46
47     _columns = {
48         'mail_message_id': fields.many2one('mail.message', 'Message', required=True, ondelete='cascade', auto_join=True),
49         'state': fields.selection([
50             ('outgoing', 'Outgoing'),
51             ('sent', 'Sent'),
52             ('received', 'Received'),
53             ('exception', 'Delivery Failed'),
54             ('cancel', 'Cancelled'),
55         ], 'Status', readonly=True, copy=False),
56         'auto_delete': fields.boolean('Auto Delete',
57             help="Permanently delete this email after sending it, to save space"),
58         'references': fields.text('References', help='Message references, such as identifiers of previous messages', readonly=1),
59         'email_to': fields.text('To', help='Message recipients (emails)'),
60         'recipient_ids': fields.many2many('res.partner', string='To (Partners)'),
61         'email_cc': fields.char('Cc', help='Carbon copy message recipients'),
62         'body_html': fields.text('Rich-text Contents', help="Rich-text/HTML message"),
63         'headers': fields.text('Headers', copy=False),
64         'failure_reason': fields.text('Failure Reason', help="Failure reason. This is usually the exception thrown by the email server, stored to ease the debugging of mailing issues.", readonly=1),
65         # Auto-detected based on create() - if 'mail_message_id' was passed then this mail is a notification
66         # and during unlink() we will not cascade delete the parent and its attachments
67         'notification': fields.boolean('Is Notification',
68             help='Mail has been created to notify people of an existing mail.message'),
69     }
70
71     _defaults = {
72         'state': 'outgoing',
73     }
74
75     def default_get(self, cr, uid, fields, context=None):
76         # protection for `default_type` values leaking from menu action context (e.g. for invoices)
77         # To remove when automatic context propagation is removed in web client
78         if context and context.get('default_type') and context.get('default_type') not in self._all_columns['type'].column.selection:
79             context = dict(context, default_type=None)
80         return super(mail_mail, self).default_get(cr, uid, fields, context=context)
81
82     def create(self, cr, uid, values, context=None):
83         # notification field: if not set, set if mail comes from an existing mail.message
84         if 'notification' not in values and values.get('mail_message_id'):
85             values['notification'] = True
86         return super(mail_mail, self).create(cr, uid, values, context=context)
87
88     def unlink(self, cr, uid, ids, context=None):
89         # cascade-delete the parent message for all mails that are not created for a notification
90         ids_to_cascade = self.search(cr, uid, [('notification', '=', False), ('id', 'in', ids)])
91         parent_msg_ids = [m.mail_message_id.id for m in self.browse(cr, uid, ids_to_cascade, context=context)]
92         res = super(mail_mail, self).unlink(cr, uid, ids, context=context)
93         self.pool.get('mail.message').unlink(cr, uid, parent_msg_ids, context=context)
94         return res
95
96     def mark_outgoing(self, cr, uid, ids, context=None):
97         return self.write(cr, uid, ids, {'state': 'outgoing'}, context=context)
98
99     def cancel(self, cr, uid, ids, context=None):
100         return self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
101
102     @api.cr_uid
103     def process_email_queue(self, cr, uid, ids=None, context=None):
104         """Send immediately queued messages, committing after each
105            message is sent - this is not transactional and should
106            not be called during another transaction!
107
108            :param list ids: optional list of emails ids to send. If passed
109                             no search is performed, and these ids are used
110                             instead.
111            :param dict context: if a 'filters' key is present in context,
112                                 this value will be used as an additional
113                                 filter to further restrict the outgoing
114                                 messages to send (by default all 'outgoing'
115                                 messages are sent).
116         """
117         if context is None:
118             context = {}
119         if not ids:
120             filters = [('state', '=', 'outgoing')]
121             if 'filters' in context:
122                 filters.extend(context['filters'])
123             ids = self.search(cr, uid, filters, context=context)
124         res = None
125         try:
126             # Force auto-commit - this is meant to be called by
127             # the scheduler, and we can't allow rolling back the status
128             # of previously sent emails!
129             res = self.send(cr, uid, ids, auto_commit=True, context=context)
130         except Exception:
131             _logger.exception("Failed processing mail queue")
132         return res
133
134     def _postprocess_sent_message(self, cr, uid, mail, context=None, mail_sent=True):
135         """Perform any post-processing necessary after sending ``mail``
136         successfully, including deleting it completely along with its
137         attachment if the ``auto_delete`` flag of the mail was set.
138         Overridden by subclasses for extra post-processing behaviors.
139
140         :param browse_record mail: the mail that was just sent
141         :return: True
142         """
143         if mail_sent and mail.auto_delete:
144             # done with SUPERUSER_ID to avoid giving large unlink access rights
145             self.unlink(cr, SUPERUSER_ID, [mail.id], context=context)
146         return True
147
148     #------------------------------------------------------
149     # mail_mail formatting, tools and send mechanism
150     #------------------------------------------------------
151
152     def _get_partner_access_link(self, cr, uid, mail, partner=None, context=None):
153         """Generate URLs for links in mails: partner has access (is user):
154         link to action_mail_redirect action that will redirect to doc or Inbox """
155         if context is None:
156             context = {}
157         if partner and partner.user_ids:
158             base_url = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url')
159             mail_model = mail.model or 'mail.thread'
160             url = urljoin(base_url, self.pool[mail_model]._get_access_link(cr, uid, mail, partner, context=context))
161             return "<span class='oe_mail_footer_access'><small>%(access_msg)s <a style='color:inherit' href='%(portal_link)s'>%(portal_msg)s</a></small></span>" % {
162                 'access_msg': _('about') if mail.record_name else _('access'),
163                 'portal_link': url,
164                 'portal_msg': '%s %s' % (context.get('model_name', ''), mail.record_name) if mail.record_name else _('your messages'),
165             }
166         else:
167             return None
168
169     def send_get_mail_subject(self, cr, uid, mail, force=False, partner=None, context=None):
170         """If subject is void, set the subject as 'Re: <Resource>' or
171         'Re: <mail.parent_id.subject>'
172
173             :param boolean force: force the subject replacement
174         """
175         if (force or not mail.subject) and mail.record_name:
176             return 'Re: %s' % (mail.record_name)
177         elif (force or not mail.subject) and mail.parent_id and mail.parent_id.subject:
178             return 'Re: %s' % (mail.parent_id.subject)
179         return mail.subject
180
181     def send_get_mail_body(self, cr, uid, mail, partner=None, context=None):
182         """Return a specific ir_email body. The main purpose of this method
183         is to be inherited to add custom content depending on some module."""
184         body = mail.body_html
185
186         # generate access links for notifications or emails linked to a specific document with auto threading
187         link = None
188         if mail.notification or (mail.model and mail.res_id and not mail.no_auto_thread):
189             link = self._get_partner_access_link(cr, uid, mail, partner, context=context)
190         if link:
191             body = tools.append_content_to_html(body, link, plaintext=False, container_tag='div')
192         return body
193
194     def send_get_mail_to(self, cr, uid, mail, partner=None, context=None):
195         """Forge the email_to with the following heuristic:
196           - if 'partner', recipient specific (Partner Name <email>)
197           - else fallback on mail.email_to splitting """
198         if partner:
199             email_to = [formataddr((partner.name, partner.email))]
200         else:
201             email_to = tools.email_split(mail.email_to)
202         return email_to
203
204     def send_get_email_dict(self, cr, uid, mail, partner=None, context=None):
205         """Return a dictionary for specific email values, depending on a
206         partner, or generic to the whole recipients given by mail.email_to.
207
208             :param browse_record mail: mail.mail browse_record
209             :param browse_record partner: specific recipient partner
210         """
211         body = self.send_get_mail_body(cr, uid, mail, partner=partner, context=context)
212         body_alternative = tools.html2plaintext(body)
213         res = {
214             'body': body,
215             'body_alternative': body_alternative,
216             'subject': self.send_get_mail_subject(cr, uid, mail, partner=partner, context=context),
217             'email_to': self.send_get_mail_to(cr, uid, mail, partner=partner, context=context),
218         }
219         return res
220
221     def send(self, cr, uid, ids, auto_commit=False, raise_exception=False, context=None):
222         """ Sends the selected emails immediately, ignoring their current
223             state (mails that have already been sent should not be passed
224             unless they should actually be re-sent).
225             Emails successfully delivered are marked as 'sent', and those
226             that fail to be deliver are marked as 'exception', and the
227             corresponding error mail is output in the server logs.
228
229             :param bool auto_commit: whether to force a commit of the mail status
230                 after sending each mail (meant only for scheduler processing);
231                 should never be True during normal transactions (default: False)
232             :param bool raise_exception: whether to raise an exception if the
233                 email sending process has failed
234             :return: True
235         """
236         context = dict(context or {})
237         ir_mail_server = self.pool.get('ir.mail_server')
238         ir_attachment = self.pool['ir.attachment']
239         for mail in self.browse(cr, SUPERUSER_ID, ids, context=context):
240             try:
241                 # TDE note: remove me when model_id field is present on mail.message - done here to avoid doing it multiple times in the sub method
242                 if mail.model:
243                     model_id = self.pool['ir.model'].search(cr, SUPERUSER_ID, [('model', '=', mail.model)], context=context)[0]
244                     model = self.pool['ir.model'].browse(cr, SUPERUSER_ID, model_id, context=context)
245                 else:
246                     model = None
247                 if model:
248                     context['model_name'] = model.name
249
250                 # load attachment binary data with a separate read(), as prefetching all
251                 # `datas` (binary field) could bloat the browse cache, triggerring
252                 # soft/hard mem limits with temporary data.
253                 attachment_ids = [a.id for a in mail.attachment_ids]
254                 attachments = [(a['datas_fname'], base64.b64decode(a['datas']))
255                                  for a in ir_attachment.read(cr, SUPERUSER_ID, attachment_ids,
256                                                              ['datas_fname', 'datas'])]
257
258                 # specific behavior to customize the send email for notified partners
259                 email_list = []
260                 if mail.email_to:
261                     email_list.append(self.send_get_email_dict(cr, uid, mail, context=context))
262                 for partner in mail.recipient_ids:
263                     email_list.append(self.send_get_email_dict(cr, uid, mail, partner=partner, context=context))
264                 # headers
265                 headers = {}
266                 bounce_alias = self.pool['ir.config_parameter'].get_param(cr, uid, "mail.bounce.alias", context=context)
267                 catchall_domain = self.pool['ir.config_parameter'].get_param(cr, uid, "mail.catchall.domain", context=context)
268                 if bounce_alias and catchall_domain:
269                     if mail.model and mail.res_id:
270                         headers['Return-Path'] = '%s-%d-%s-%d@%s' % (bounce_alias, mail.id, mail.model, mail.res_id, catchall_domain)
271                     else:
272                         headers['Return-Path'] = '%s-%d@%s' % (bounce_alias, mail.id, catchall_domain)
273                 if mail.headers:
274                     try:
275                         headers.update(eval(mail.headers))
276                     except Exception:
277                         pass
278
279                 # Writing on the mail object may fail (e.g. lock on user) which
280                 # would trigger a rollback *after* actually sending the email.
281                 # To avoid sending twice the same email, provoke the failure earlier
282                 mail.write({
283                     'state': 'exception', 
284                     'failure_reason': _('Error without exception. Probably due do sending an email without computed recipients.'),
285                 })
286                 mail_sent = False
287
288                 # build an RFC2822 email.message.Message object and send it without queuing
289                 res = None
290                 for email in email_list:
291                     msg = ir_mail_server.build_email(
292                         email_from=mail.email_from,
293                         email_to=email.get('email_to'),
294                         subject=email.get('subject'),
295                         body=email.get('body'),
296                         body_alternative=email.get('body_alternative'),
297                         email_cc=tools.email_split(mail.email_cc),
298                         reply_to=mail.reply_to,
299                         attachments=attachments,
300                         message_id=mail.message_id,
301                         references=mail.references,
302                         object_id=mail.res_id and ('%s-%s' % (mail.res_id, mail.model)),
303                         subtype='html',
304                         subtype_alternative='plain',
305                         headers=headers)
306                     res = ir_mail_server.send_email(cr, uid, msg,
307                                                     mail_server_id=mail.mail_server_id.id,
308                                                     context=context)
309
310                 if res:
311                     mail.write({'state': 'sent', 'message_id': res, 'failure_reason': False})
312                     mail_sent = True
313
314                 # /!\ can't use mail.state here, as mail.refresh() will cause an error
315                 # see revid:odo@openerp.com-20120622152536-42b2s28lvdv3odyr in 6.1
316                 if mail_sent:
317                     _logger.info('Mail with ID %r and Message-Id %r successfully sent', mail.id, mail.message_id)
318                 self._postprocess_sent_message(cr, uid, mail, context=context, mail_sent=mail_sent)
319             except MemoryError:
320                 # prevent catching transient MemoryErrors, bubble up to notify user or abort cron job
321                 # instead of marking the mail as failed
322                 _logger.exception('MemoryError while processing mail with ID %r and Msg-Id %r. '\
323                                       'Consider raising the --limit-memory-hard startup option',
324                                   mail.id, mail.message_id)
325                 raise
326             except Exception as e:
327                 failure_reason = tools.ustr(e)
328                 _logger.exception('failed sending mail (id: %s) due to %s', mail.id, failure_reason)
329                 mail.write({'state': 'exception', 'failure_reason': failure_reason})
330                 self._postprocess_sent_message(cr, uid, mail, context=context, mail_sent=False)
331                 if raise_exception:
332                     if isinstance(e, AssertionError):
333                         # get the args of the original error, wrap into a value and throw a MailDeliveryException
334                         # that is an except_orm, with name and value as arguments
335                         value = '. '.join(e.args)
336                         raise MailDeliveryException(_("Mail Delivery Failed"), value)
337                     raise
338
339             if auto_commit is True:
340                 cr.commit()
341         return True