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._all_columns['type'].column.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>about <a style='color:inherit' href="%s">%s %s</a></small></span>""") % (url, context.get('model_name', ''), mail.record_name)
163 def send_get_mail_subject(self, cr, uid, mail, force=False, partner=None, context=None):
164 """If subject is void, set the subject as 'Re: <Resource>' or
165 'Re: <mail.parent_id.subject>'
167 :param boolean force: force the subject replacement
169 if (force or not mail.subject) and mail.record_name:
170 return 'Re: %s' % (mail.record_name)
171 elif (force or not mail.subject) and mail.parent_id and mail.parent_id.subject:
172 return 'Re: %s' % (mail.parent_id.subject)
175 def send_get_mail_body(self, cr, uid, mail, partner=None, context=None):
176 """Return a specific ir_email body. The main purpose of this method
177 is to be inherited to add custom content depending on some module."""
178 body = mail.body_html
180 # generate access links for notifications or emails linked to a specific document with auto threading
182 if mail.notification or (mail.model and mail.res_id and not mail.no_auto_thread):
183 link = self._get_partner_access_link(cr, uid, mail, partner, context=context)
185 body = tools.append_content_to_html(body, link, plaintext=False, container_tag='div')
188 def send_get_mail_to(self, cr, uid, mail, partner=None, context=None):
189 """Forge the email_to with the following heuristic:
190 - if 'partner', recipient specific (Partner Name <email>)
191 - else fallback on mail.email_to splitting """
193 email_to = [formataddr((partner.name, partner.email))]
195 email_to = tools.email_split(mail.email_to)
198 def send_get_email_dict(self, cr, uid, mail, partner=None, context=None):
199 """Return a dictionary for specific email values, depending on a
200 partner, or generic to the whole recipients given by mail.email_to.
202 :param browse_record mail: mail.mail browse_record
203 :param browse_record partner: specific recipient partner
205 body = self.send_get_mail_body(cr, uid, mail, partner=partner, context=context)
206 body_alternative = tools.html2plaintext(body)
209 'body_alternative': body_alternative,
210 'subject': self.send_get_mail_subject(cr, uid, mail, partner=partner, context=context),
211 'email_to': self.send_get_mail_to(cr, uid, mail, partner=partner, context=context),
215 def send(self, cr, uid, ids, auto_commit=False, raise_exception=False, context=None):
216 """ Sends the selected emails immediately, ignoring their current
217 state (mails that have already been sent should not be passed
218 unless they should actually be re-sent).
219 Emails successfully delivered are marked as 'sent', and those
220 that fail to be deliver are marked as 'exception', and the
221 corresponding error mail is output in the server logs.
223 :param bool auto_commit: whether to force a commit of the mail status
224 after sending each mail (meant only for scheduler processing);
225 should never be True during normal transactions (default: False)
226 :param bool raise_exception: whether to raise an exception if the
227 email sending process has failed
230 context = dict(context or {})
231 ir_mail_server = self.pool.get('ir.mail_server')
232 ir_attachment = self.pool['ir.attachment']
233 for mail in self.browse(cr, SUPERUSER_ID, ids, context=context):
235 # 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
237 model_id = self.pool['ir.model'].search(cr, SUPERUSER_ID, [('model', '=', mail.model)], context=context)[0]
238 model = self.pool['ir.model'].browse(cr, SUPERUSER_ID, model_id, context=context)
242 context['model_name'] = model.name
244 # load attachment binary data with a separate read(), as prefetching all
245 # `datas` (binary field) could bloat the browse cache, triggerring
246 # soft/hard mem limits with temporary data.
247 attachment_ids = [a.id for a in mail.attachment_ids]
248 attachments = [(a['datas_fname'], base64.b64decode(a['datas']))
249 for a in ir_attachment.read(cr, SUPERUSER_ID, attachment_ids,
250 ['datas_fname', 'datas'])]
252 # specific behavior to customize the send email for notified partners
255 email_list.append(self.send_get_email_dict(cr, uid, mail, context=context))
256 for partner in mail.recipient_ids:
257 email_list.append(self.send_get_email_dict(cr, uid, mail, partner=partner, context=context))
260 bounce_alias = self.pool['ir.config_parameter'].get_param(cr, uid, "mail.bounce.alias", context=context)
261 catchall_domain = self.pool['ir.config_parameter'].get_param(cr, uid, "mail.catchall.domain", context=context)
262 if bounce_alias and catchall_domain:
263 if mail.model and mail.res_id:
264 headers['Return-Path'] = '%s-%d-%s-%d@%s' % (bounce_alias, mail.id, mail.model, mail.res_id, catchall_domain)
266 headers['Return-Path'] = '%s-%d@%s' % (bounce_alias, mail.id, catchall_domain)
269 headers.update(eval(mail.headers))
273 # build an RFC2822 email.message.Message object and send it without queuing
275 for email in email_list:
276 msg = ir_mail_server.build_email(
277 email_from=mail.email_from,
278 email_to=email.get('email_to'),
279 subject=email.get('subject'),
280 body=email.get('body'),
281 body_alternative=email.get('body_alternative'),
282 email_cc=tools.email_split(mail.email_cc),
283 reply_to=mail.reply_to,
284 attachments=attachments,
285 message_id=mail.message_id,
286 references=mail.references,
287 object_id=mail.res_id and ('%s-%s' % (mail.res_id, mail.model)),
289 subtype_alternative='plain',
291 res = ir_mail_server.send_email(cr, uid, msg,
292 mail_server_id=mail.mail_server_id.id,
296 mail.write({'state': 'sent', 'message_id': res})
299 mail.write({'state': 'exception'})
302 # /!\ can't use mail.state here, as mail.refresh() will cause an error
303 # see revid:odo@openerp.com-20120622152536-42b2s28lvdv3odyr in 6.1
305 _logger.info('Mail with ID %r and Message-Id %r successfully sent', mail.id, mail.message_id)
306 self._postprocess_sent_message(cr, uid, mail, context=context, mail_sent=mail_sent)
308 # prevent catching transient MemoryErrors, bubble up to notify user or abort cron job
309 # instead of marking the mail as failed
310 _logger.exception('MemoryError while processing mail with ID %r and Msg-Id %r. '\
311 'Consider raising the --limit-memory-hard startup option',
312 mail.id, mail.message_id)
314 except Exception as e:
315 _logger.exception('failed sending mail.mail %s', mail.id)
316 mail.write({'state': 'exception'})
317 self._postprocess_sent_message(cr, uid, mail, context=context, mail_sent=False)
319 if isinstance(e, AssertionError):
320 # get the args of the original error, wrap into a value and throw a MailDeliveryException
321 # that is an except_orm, with name and value as arguments
322 value = '. '.join(e.args)
323 raise MailDeliveryException(_("Mail Delivery Failed"), value)
326 if auto_commit is True: