1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2010-today OpenERP SA (<http://www.openerp.com>)
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
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
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/>
20 ##############################################################################
24 from email.utils import formataddr
25 from urlparse import urljoin
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 _
34 _logger = logging.getLogger(__name__)
37 class mail_mail(osv.Model):
38 """ Model holding RFC2822 email messages to send. This model also provides
39 facilities to queue and send new email messages. """
41 _description = 'Outgoing Mails'
42 _inherits = {'mail.message': 'mail_message_id'}
47 'mail_message_id': fields.many2one('mail.message', 'Message', required=True, ondelete='cascade', auto_join=True),
48 'state': fields.selection([
49 ('outgoing', 'Outgoing'),
51 ('received', 'Received'),
52 ('exception', 'Delivery Failed'),
53 ('cancel', 'Cancelled'),
54 ], 'Status', readonly=True, copy=False),
55 'auto_delete': fields.boolean('Auto Delete',
56 help="Permanently delete this email after sending it, to save space"),
57 'references': fields.text('References', help='Message references, such as identifiers of previous messages', readonly=1),
58 'email_to': fields.text('To', help='Message recipients (emails)'),
59 'recipient_ids': fields.many2many('res.partner', string='To (Partners)'),
60 'email_cc': fields.char('Cc', help='Carbon copy message recipients'),
61 'body_html': fields.text('Rich-text Contents', help="Rich-text/HTML message"),
62 'headers': fields.text('Headers', copy=False),
63 # Auto-detected based on create() - if 'mail_message_id' was passed then this mail is a notification
64 # and during unlink() we will not cascade delete the parent and its attachments
65 'notification': fields.boolean('Is Notification',
66 help='Mail has been created to notify people of an existing mail.message'),
73 def default_get(self, cr, uid, fields, context=None):
74 # protection for `default_type` values leaking from menu action context (e.g. for invoices)
75 # To remove when automatic context propagation is removed in web client
76 if context and context.get('default_type') and context.get('default_type') not in self._fields['type'].selection:
77 context = dict(context, default_type=None)
78 return super(mail_mail, self).default_get(cr, uid, fields, context=context)
80 def create(self, cr, uid, values, context=None):
81 # notification field: if not set, set if mail comes from an existing mail.message
82 if 'notification' not in values and values.get('mail_message_id'):
83 values['notification'] = True
84 return super(mail_mail, self).create(cr, uid, values, context=context)
86 def unlink(self, cr, uid, ids, context=None):
87 # cascade-delete the parent message for all mails that are not created for a notification
88 ids_to_cascade = self.search(cr, uid, [('notification', '=', False), ('id', 'in', ids)])
89 parent_msg_ids = [m.mail_message_id.id for m in self.browse(cr, uid, ids_to_cascade, context=context)]
90 res = super(mail_mail, self).unlink(cr, uid, ids, context=context)
91 self.pool.get('mail.message').unlink(cr, uid, parent_msg_ids, context=context)
94 def mark_outgoing(self, cr, uid, ids, context=None):
95 return self.write(cr, uid, ids, {'state': 'outgoing'}, context=context)
97 def cancel(self, cr, uid, ids, context=None):
98 return self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
101 def process_email_queue(self, cr, uid, ids=None, context=None):
102 """Send immediately queued messages, committing after each
103 message is sent - this is not transactional and should
104 not be called during another transaction!
106 :param list ids: optional list of emails ids to send. If passed
107 no search is performed, and these ids are used
109 :param dict context: if a 'filters' key is present in context,
110 this value will be used as an additional
111 filter to further restrict the outgoing
112 messages to send (by default all 'outgoing'
118 filters = [('state', '=', 'outgoing')]
119 if 'filters' in context:
120 filters.extend(context['filters'])
121 ids = self.search(cr, uid, filters, context=context)
124 # Force auto-commit - this is meant to be called by
125 # the scheduler, and we can't allow rolling back the status
126 # of previously sent emails!
127 res = self.send(cr, uid, ids, auto_commit=True, context=context)
129 _logger.exception("Failed processing mail queue")
132 def _postprocess_sent_message(self, cr, uid, mail, context=None, mail_sent=True):
133 """Perform any post-processing necessary after sending ``mail``
134 successfully, including deleting it completely along with its
135 attachment if the ``auto_delete`` flag of the mail was set.
136 Overridden by subclasses for extra post-processing behaviors.
138 :param browse_record mail: the mail that was just sent
141 if mail_sent and mail.auto_delete:
142 # done with SUPERUSER_ID to avoid giving large unlink access rights
143 self.unlink(cr, SUPERUSER_ID, [mail.id], context=context)
146 #------------------------------------------------------
147 # mail_mail formatting, tools and send mechanism
148 #------------------------------------------------------
150 def _get_partner_access_link(self, cr, uid, mail, partner=None, context=None):
151 """Generate URLs for links in mails: partner has access (is user):
152 link to action_mail_redirect action that will redirect to doc or Inbox """
155 if partner and partner.user_ids:
156 base_url = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url')
157 mail_model = mail.model or 'mail.thread'
158 url = urljoin(base_url, self.pool[mail_model]._get_access_link(cr, uid, mail, partner, context=context))
159 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>" % {
160 'access_msg': _('about') if mail.record_name else _('access'),
162 'portal_msg': '%s %s' % (context.get('model_name', ''), mail.record_name) if mail.record_name else _('your messages'),
167 def send_get_mail_subject(self, cr, uid, mail, force=False, partner=None, context=None):
168 """If subject is void, set the subject as 'Re: <Resource>' or
169 'Re: <mail.parent_id.subject>'
171 :param boolean force: force the subject replacement
173 if (force or not mail.subject) and mail.record_name:
174 return 'Re: %s' % (mail.record_name)
175 elif (force or not mail.subject) and mail.parent_id and mail.parent_id.subject:
176 return 'Re: %s' % (mail.parent_id.subject)
179 def send_get_mail_body(self, cr, uid, mail, partner=None, context=None):
180 """Return a specific ir_email body. The main purpose of this method
181 is to be inherited to add custom content depending on some module."""
182 body = mail.body_html
184 # generate access links for notifications or emails linked to a specific document with auto threading
186 if mail.notification or (mail.model and mail.res_id and not mail.no_auto_thread):
187 link = self._get_partner_access_link(cr, uid, mail, partner, context=context)
189 body = tools.append_content_to_html(body, link, plaintext=False, container_tag='div')
192 def send_get_mail_to(self, cr, uid, mail, partner=None, context=None):
193 """Forge the email_to with the following heuristic:
194 - if 'partner', recipient specific (Partner Name <email>)
195 - else fallback on mail.email_to splitting """
197 email_to = [formataddr((partner.name, partner.email))]
199 email_to = tools.email_split(mail.email_to)
202 def send_get_email_dict(self, cr, uid, mail, partner=None, context=None):
203 """Return a dictionary for specific email values, depending on a
204 partner, or generic to the whole recipients given by mail.email_to.
206 :param browse_record mail: mail.mail browse_record
207 :param browse_record partner: specific recipient partner
209 body = self.send_get_mail_body(cr, uid, mail, partner=partner, context=context)
210 body_alternative = tools.html2plaintext(body)
213 'body_alternative': body_alternative,
214 'subject': self.send_get_mail_subject(cr, uid, mail, partner=partner, context=context),
215 'email_to': self.send_get_mail_to(cr, uid, mail, partner=partner, context=context),
219 def send(self, cr, uid, ids, auto_commit=False, raise_exception=False, context=None):
220 """ Sends the selected emails immediately, ignoring their current
221 state (mails that have already been sent should not be passed
222 unless they should actually be re-sent).
223 Emails successfully delivered are marked as 'sent', and those
224 that fail to be deliver are marked as 'exception', and the
225 corresponding error mail is output in the server logs.
227 :param bool auto_commit: whether to force a commit of the mail status
228 after sending each mail (meant only for scheduler processing);
229 should never be True during normal transactions (default: False)
230 :param bool raise_exception: whether to raise an exception if the
231 email sending process has failed
234 context = dict(context or {})
235 ir_mail_server = self.pool.get('ir.mail_server')
236 ir_attachment = self.pool['ir.attachment']
237 for mail in self.browse(cr, SUPERUSER_ID, ids, context=context):
239 # 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
241 model_id = self.pool['ir.model'].search(cr, SUPERUSER_ID, [('model', '=', mail.model)], context=context)[0]
242 model = self.pool['ir.model'].browse(cr, SUPERUSER_ID, model_id, context=context)
246 context['model_name'] = model.name
248 # load attachment binary data with a separate read(), as prefetching all
249 # `datas` (binary field) could bloat the browse cache, triggerring
250 # soft/hard mem limits with temporary data.
251 attachment_ids = [a.id for a in mail.attachment_ids]
252 attachments = [(a['datas_fname'], base64.b64decode(a['datas']))
253 for a in ir_attachment.read(cr, SUPERUSER_ID, attachment_ids,
254 ['datas_fname', 'datas'])]
256 # specific behavior to customize the send email for notified partners
259 email_list.append(self.send_get_email_dict(cr, uid, mail, context=context))
260 for partner in mail.recipient_ids:
261 email_list.append(self.send_get_email_dict(cr, uid, mail, partner=partner, context=context))
264 bounce_alias = self.pool['ir.config_parameter'].get_param(cr, uid, "mail.bounce.alias", context=context)
265 catchall_domain = self.pool['ir.config_parameter'].get_param(cr, uid, "mail.catchall.domain", context=context)
266 if bounce_alias and catchall_domain:
267 if mail.model and mail.res_id:
268 headers['Return-Path'] = '%s-%d-%s-%d@%s' % (bounce_alias, mail.id, mail.model, mail.res_id, catchall_domain)
270 headers['Return-Path'] = '%s-%d@%s' % (bounce_alias, mail.id, catchall_domain)
273 headers.update(eval(mail.headers))
277 # Writing on the mail object may fail (e.g. lock on user) which
278 # would trigger a rollback *after* actually sending the email.
279 # To avoid sending twice the same email, provoke the failure earlier
280 mail.write({'state': 'exception'})
283 # build an RFC2822 email.message.Message object and send it without queuing
285 for email in email_list:
286 msg = ir_mail_server.build_email(
287 email_from=mail.email_from,
288 email_to=email.get('email_to'),
289 subject=email.get('subject'),
290 body=email.get('body'),
291 body_alternative=email.get('body_alternative'),
292 email_cc=tools.email_split(mail.email_cc),
293 reply_to=mail.reply_to,
294 attachments=attachments,
295 message_id=mail.message_id,
296 references=mail.references,
297 object_id=mail.res_id and ('%s-%s' % (mail.res_id, mail.model)),
299 subtype_alternative='plain',
302 res = ir_mail_server.send_email(cr, uid, msg,
303 mail_server_id=mail.mail_server_id.id,
305 except AssertionError as error:
306 if error.message == ir_mail_server.NO_VALID_RECIPIENT:
307 # No valid recipient found for this particular
308 # mail item -> ignore error to avoid blocking
309 # delivery to next recipients, if any. If this is
310 # the only recipient, the mail will show as failed.
311 _logger.warning("Ignoring invalid recipients for mail.mail %s: %s",
312 mail.message_id, email.get('email_to'))
316 mail.write({'state': 'sent', 'message_id': res})
319 # /!\ can't use mail.state here, as mail.refresh() will cause an error
320 # see revid:odo@openerp.com-20120622152536-42b2s28lvdv3odyr in 6.1
322 _logger.info('Mail with ID %r and Message-Id %r successfully sent', mail.id, mail.message_id)
323 self._postprocess_sent_message(cr, uid, mail, context=context, mail_sent=mail_sent)
325 # prevent catching transient MemoryErrors, bubble up to notify user or abort cron job
326 # instead of marking the mail as failed
327 _logger.exception('MemoryError while processing mail with ID %r and Msg-Id %r. '\
328 'Consider raising the --limit-memory-hard startup option',
329 mail.id, mail.message_id)
331 except Exception as e:
332 _logger.exception('failed sending mail.mail %s', mail.id)
333 mail.write({'state': 'exception'})
334 self._postprocess_sent_message(cr, uid, mail, context=context, mail_sent=False)
336 if isinstance(e, AssertionError):
337 # get the args of the original error, wrap into a value and throw a MailDeliveryException
338 # that is an except_orm, with name and value as arguments
339 value = '. '.join(e.args)
340 raise MailDeliveryException(_("Mail Delivery Failed"), value)
343 if auto_commit is True: