[MERGE] Forward port of addons until 8903
[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 urllib import urlencode
25 from urlparse import urljoin
26
27 from openerp import tools
28 from openerp import SUPERUSER_ID
29 from openerp.osv import fields, osv
30 from openerp.osv.orm import except_orm
31 from openerp.tools.translate import _
32
33 _logger = logging.getLogger(__name__)
34
35
36 class mail_mail(osv.Model):
37     """ Model holding RFC2822 email messages to send. This model also provides
38         facilities to queue and send new email messages.  """
39     _name = 'mail.mail'
40     _description = 'Outgoing Mails'
41     _inherits = {'mail.message': 'mail_message_id'}
42     _order = 'id desc'
43
44     _columns = {
45         'mail_message_id': fields.many2one('mail.message', 'Message', required=True, ondelete='cascade'),
46         'mail_server_id': fields.many2one('ir.mail_server', 'Outgoing mail server', readonly=1),
47         'state': fields.selection([
48             ('outgoing', 'Outgoing'),
49             ('sent', 'Sent'),
50             ('received', 'Received'),
51             ('exception', 'Delivery Failed'),
52             ('cancel', 'Cancelled'),
53         ], 'Status', readonly=True),
54         'auto_delete': fields.boolean('Auto Delete',
55             help="Permanently delete this email after sending it, to save space"),
56         'references': fields.text('References', help='Message references, such as identifiers of previous messages', readonly=1),
57         'email_from': fields.char('From', help='Message sender, taken from user preferences.'),
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         # If not set in create values, auto-detected based on create values (res_id, model, email_from)
63         'reply_to': fields.char('Reply-To',
64             help='Preferred response address for the message'),
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     def _get_default_from(self, cr, uid, context=None):
72         this = self.pool.get('res.users').browse(cr, SUPERUSER_ID, uid, context=context)
73         if this.alias_domain:
74             return '%s <%s@%s>' % (this.name, this.alias_name, this.alias_domain)
75         elif this.email:
76             return '%s <%s>' % (this.name, this.email)
77         raise osv.except_osv(_('Invalid Action!'), _("Unable to send email, please configure the sender's email address or alias."))
78
79     _defaults = {
80         'state': 'outgoing',
81         'email_from': lambda self, cr, uid, ctx=None: self._get_default_from(cr, uid, ctx),
82     }
83
84     def default_get(self, cr, uid, fields, context=None):
85         # protection for `default_type` values leaking from menu action context (e.g. for invoices)
86         # To remove when automatic context propagation is removed in web client
87         if context and context.get('default_type') and context.get('default_type') not in self._all_columns['type'].column.selection:
88             context = dict(context, default_type=None)
89         return super(mail_mail, self).default_get(cr, uid, fields, context=context)
90
91     def _get_reply_to(self, cr, uid, values, context=None):
92         """ Return a specific reply_to: alias of the document through message_get_reply_to
93             or take the email_from
94         """
95         if values.get('reply_to'):
96             return values.get('reply_to')
97         email_reply_to = False
98
99         # if model and res_id: try to use ``message_get_reply_to`` that returns the document alias
100         if values.get('model') and values.get('res_id') and hasattr(self.pool.get(values.get('model')), 'message_get_reply_to'):
101             email_reply_to = self.pool.get(values.get('model')).message_get_reply_to(cr, uid, [values.get('res_id')], context=context)[0]
102         # no alias reply_to -> reply_to will be the email_from, only the email part
103         if not email_reply_to and values.get('email_from'):
104             emails = tools.email_split(values.get('email_from'))
105             if emails:
106                 email_reply_to = emails[0]
107
108         # format 'Document name <email_address>'
109         if email_reply_to and values.get('model') and values.get('res_id'):
110             document_name = self.pool.get(values.get('model')).name_get(cr, SUPERUSER_ID, [values.get('res_id')], context=context)[0]
111             if document_name:
112                 email_reply_to = _('Followers of %s <%s>') % (document_name[1], email_reply_to)
113
114         return email_reply_to
115
116     def create(self, cr, uid, values, context=None):
117         # notification field: if not set, set if mail comes from an existing mail.message
118         if 'notification' not in values and values.get('mail_message_id'):
119             values['notification'] = True
120         # reply_to: if not set, set with default values that require creation values
121         if not values.get('reply_to'):
122             values['reply_to'] = self._get_reply_to(cr, uid, values, context=context)
123         return super(mail_mail, self).create(cr, uid, values, context=context)
124
125     def unlink(self, cr, uid, ids, context=None):
126         # cascade-delete the parent message for all mails that are not created for a notification
127         ids_to_cascade = self.search(cr, uid, [('notification', '=', False), ('id', 'in', ids)])
128         parent_msg_ids = [m.mail_message_id.id for m in self.browse(cr, uid, ids_to_cascade, context=context)]
129         res = super(mail_mail, self).unlink(cr, uid, ids, context=context)
130         self.pool.get('mail.message').unlink(cr, uid, parent_msg_ids, context=context)
131         return res
132
133     def mark_outgoing(self, cr, uid, ids, context=None):
134         return self.write(cr, uid, ids, {'state': 'outgoing'}, context=context)
135
136     def cancel(self, cr, uid, ids, context=None):
137         return self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
138
139     def process_email_queue(self, cr, uid, ids=None, context=None):
140         """Send immediately queued messages, committing after each
141            message is sent - this is not transactional and should
142            not be called during another transaction!
143
144            :param list ids: optional list of emails ids to send. If passed
145                             no search is performed, and these ids are used
146                             instead.
147            :param dict context: if a 'filters' key is present in context,
148                                 this value will be used as an additional
149                                 filter to further restrict the outgoing
150                                 messages to send (by default all 'outgoing'
151                                 messages are sent).
152         """
153         if context is None:
154             context = {}
155         if not ids:
156             filters = ['&', ('state', '=', 'outgoing'), ('type', '=', 'email')]
157             if 'filters' in context:
158                 filters.extend(context['filters'])
159             ids = self.search(cr, uid, filters, context=context)
160         res = None
161         try:
162             # Force auto-commit - this is meant to be called by
163             # the scheduler, and we can't allow rolling back the status
164             # of previously sent emails!
165             res = self.send(cr, uid, ids, auto_commit=True, context=context)
166         except Exception:
167             _logger.exception("Failed processing mail queue")
168         return res
169
170     def _postprocess_sent_message(self, cr, uid, mail, context=None):
171         """Perform any post-processing necessary after sending ``mail``
172         successfully, including deleting it completely along with its
173         attachment if the ``auto_delete`` flag of the mail was set.
174         Overridden by subclasses for extra post-processing behaviors.
175
176         :param browse_record mail: the mail that was just sent
177         :return: True
178         """
179         if mail.auto_delete:
180             # done with SUPERUSER_ID to avoid giving large unlink access rights
181             self.unlink(cr, SUPERUSER_ID, [mail.id], context=context)
182         return True
183
184     def send_get_mail_subject(self, cr, uid, mail, force=False, partner=None, context=None):
185         """ If subject is void and record_name defined: '<Author> posted on <Resource>'
186
187             :param boolean force: force the subject replacement
188             :param browse_record mail: mail.mail browse_record
189             :param browse_record partner: specific recipient partner
190         """
191         if force or (not mail.subject and mail.model and mail.res_id):
192             return 'Re: %s' % (mail.record_name)
193         return mail.subject
194
195     def send_get_mail_body_footer(self, cr, uid, mail, partner=None, context=None):
196         """ Return a specific footer for the ir_email body.  The main purpose of this method
197             is to be inherited by Portal, to add modify the link for signing in, in
198             each notification email a partner receives.
199         """
200         body_footer = ""
201         # partner is a user, link to a related document (incentive to install portal)
202         if partner and partner.user_ids and mail.model and mail.res_id \
203                 and self.check_access_rights(cr, partner.user_ids[0].id, 'read', raise_exception=False):
204             related_user = partner.user_ids[0]
205             try:
206                 self.pool.get(mail.model).check_access_rule(cr, related_user.id, [mail.res_id], 'read', context=context)
207                 base_url = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url')
208                 # the parameters to encode for the query and fragment part of url
209                 query = {'db': cr.dbname}
210                 fragment = {
211                     'login': related_user.login,
212                     'model': mail.model,
213                     'id': mail.res_id,
214                 }
215                 url = urljoin(base_url, "?%s#%s" % (urlencode(query), urlencode(fragment)))
216                 body_footer = _("""<small>Access this document <a style='color:inherit' href="%s">directly in OpenERP</a></small>""") % url
217             except except_orm, e:
218                 pass
219         return body_footer
220
221     def send_get_mail_body(self, cr, uid, mail, partner=None, context=None):
222         """ Return a specific ir_email body. The main purpose of this method
223             is to be inherited to add custom content depending on some module.
224
225             :param browse_record mail: mail.mail browse_record
226             :param browse_record partner: specific recipient partner
227         """
228         body = mail.body_html
229
230         # add footer
231         body_footer = self.send_get_mail_body_footer(cr, uid, mail, partner=partner, context=context)
232         body = tools.append_content_to_html(body, body_footer, plaintext=False, container_tag='div')
233         return body
234
235     def send_get_email_dict(self, cr, uid, mail, partner=None, context=None):
236         """ Return a dictionary for specific email values, depending on a
237             partner, or generic to the whole recipients given by mail.email_to.
238
239             :param browse_record mail: mail.mail browse_record
240             :param browse_record partner: specific recipient partner
241         """
242         body = self.send_get_mail_body(cr, uid, mail, partner=partner, context=context)
243         subject = self.send_get_mail_subject(cr, uid, mail, partner=partner, context=context)
244         body_alternative = tools.html2plaintext(body)
245         email_to = ['%s <%s>' % (partner.name, partner.email)] if partner else tools.email_split(mail.email_to)
246         return {
247             'body': body,
248             'body_alternative': body_alternative,
249             'subject': subject,
250             'email_to': email_to,
251         }
252
253     def send(self, cr, uid, ids, auto_commit=False, context=None):
254         """ Sends the selected emails immediately, ignoring their current
255             state (mails that have already been sent should not be passed
256             unless they should actually be re-sent).
257             Emails successfully delivered are marked as 'sent', and those
258             that fail to be deliver are marked as 'exception', and the
259             corresponding error mail is output in the server logs.
260
261             :param bool auto_commit: whether to force a commit of the mail status
262                 after sending each mail (meant only for scheduler processing);
263                 should never be True during normal transactions (default: False)
264             :return: True
265         """
266         ir_mail_server = self.pool.get('ir.mail_server')
267         for mail in self.browse(cr, SUPERUSER_ID, ids, context=context):
268             try:
269                 # handle attachments
270                 attachments = []
271                 for attach in mail.attachment_ids:
272                     attachments.append((attach.datas_fname, base64.b64decode(attach.datas)))
273                 # specific behavior to customize the send email for notified partners
274                 email_list = []
275                 if mail.email_to:
276                     email_list.append(self.send_get_email_dict(cr, uid, mail, context=context))
277                 for partner in mail.recipient_ids:
278                     email_list.append(self.send_get_email_dict(cr, uid, mail, partner=partner, context=context))
279
280                 # build an RFC2822 email.message.Message object and send it without queuing
281                 for email in email_list:
282                     msg = ir_mail_server.build_email(
283                         email_from = mail.email_from,
284                         email_to = email.get('email_to'),
285                         subject = email.get('subject'),
286                         body = email.get('body'),
287                         body_alternative = email.get('body_alternative'),
288                         email_cc = tools.email_split(mail.email_cc),
289                         reply_to = mail.reply_to,
290                         attachments = attachments,
291                         message_id = mail.message_id,
292                         references = mail.references,
293                         object_id = mail.res_id and ('%s-%s' % (mail.res_id, mail.model)),
294                         subtype = 'html',
295                         subtype_alternative = 'plain')
296                     res = ir_mail_server.send_email(cr, uid, msg,
297                         mail_server_id=mail.mail_server_id.id, context=context)
298                 if res:
299                     mail.write({'state': 'sent', 'message_id': res})
300                     mail_sent = True
301                 else:
302                     mail.write({'state': 'exception'})
303                     mail_sent = False
304
305                 # /!\ can't use mail.state here, as mail.refresh() will cause an error
306                 # see revid:odo@openerp.com-20120622152536-42b2s28lvdv3odyr in 6.1
307                 if mail_sent:
308                     self._postprocess_sent_message(cr, uid, mail, context=context)
309             except Exception:
310                 _logger.exception('failed sending mail.mail %s', mail.id)
311                 mail.write({'state': 'exception'})
312
313             if auto_commit == True:
314                 cr.commit()
315         return True