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 _
33 import openerp.tools as tools
35 _logger = logging.getLogger(__name__)
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. """
42 _description = 'Outgoing Mails'
43 _inherits = {'mail.message': 'mail_message_id'}
48 'mail_message_id': fields.many2one('mail.message', 'Message', required=True, ondelete='cascade', auto_join=True),
49 'state': fields.selection([
50 ('outgoing', 'Outgoing'),
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'),
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)
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)
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)
96 def mark_outgoing(self, cr, uid, ids, context=None):
97 return self.write(cr, uid, ids, {'state': 'outgoing'}, context=context)
99 def cancel(self, cr, uid, ids, context=None):
100 return self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
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!
108 :param list ids: optional list of emails ids to send. If passed
109 no search is performed, and these ids are used
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'
120 filters = [('state', '=', 'outgoing')]
121 if 'filters' in context:
122 filters.extend(context['filters'])
123 ids = self.search(cr, uid, filters, context=context)
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)
131 _logger.exception("Failed processing mail queue")
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.
140 :param browse_record mail: the mail that was just sent
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)
148 #------------------------------------------------------
149 # mail_mail formatting, tools and send mechanism
150 #------------------------------------------------------
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 """
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'),
164 'portal_msg': '%s %s' % (context.get('model_name', ''), mail.record_name) if mail.record_name else _('your messages'),
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>'
173 :param boolean force: force the subject replacement
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)
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
186 # generate access links for notifications or emails linked to a specific document with auto threading
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)
191 body = tools.append_content_to_html(body, link, plaintext=False, container_tag='div')
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 """
199 email_to = [formataddr((partner.name, partner.email))]
201 email_to = tools.email_split(mail.email_to)
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.
208 :param browse_record mail: mail.mail browse_record
209 :param browse_record partner: specific recipient partner
211 body = self.send_get_mail_body(cr, uid, mail, partner=partner, context=context)
212 body_alternative = tools.html2plaintext(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),
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.
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
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):
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
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)
248 context['model_name'] = model.name
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'])]
258 # specific behavior to customize the send email for notified partners
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))
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)
272 headers['Return-Path'] = '%s-%d@%s' % (bounce_alias, mail.id, catchall_domain)
275 headers.update(eval(mail.headers))
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
283 'state': 'exception',
284 'failure_reason': _('Error without exception. Probably due do sending an email without computed recipients.'),
288 # build an RFC2822 email.message.Message object and send it without queuing
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)),
304 subtype_alternative='plain',
306 res = ir_mail_server.send_email(cr, uid, msg,
307 mail_server_id=mail.mail_server_id.id,
311 mail.write({'state': 'sent', 'message_id': res, 'failure_reason': False})
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
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)
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)
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)
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)
339 if auto_commit is True: