[IMP] mail: signature
[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'),
59         'email_cc': fields.char('Cc', help='Carbon copy message recipients'),
60         'reply_to': fields.char('Reply-To', help='Preferred response address for the message'),
61         'body_html': fields.text('Rich-text Contents', help="Rich-text/HTML message"),
62
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     }
67
68     def _get_default_from(self, cr, uid, context=None):
69         this = self.pool.get('res.users').browse(cr, uid, uid, context=context)
70         if this.alias_domain:
71             return '%s@%s' % (this.alias_name, this.alias_domain)
72         elif this.email:
73             return this.email
74         raise osv.except_osv(_('Invalid Action!'), _("Unable to send email, please configure the sender's email address or alias."))
75
76     _defaults = {
77         'state': 'outgoing',
78         'email_from': lambda self, cr, uid, ctx=None: self._get_default_from(cr, uid, ctx),
79     }
80
81     def create(self, cr, uid, values, context=None):
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)
85
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)
92         return res
93
94     def mark_outgoing(self, cr, uid, ids, context=None):
95         return self.write(cr, uid, ids, {'state': 'outgoing'}, context=context)
96
97     def cancel(self, cr, uid, ids, context=None):
98         return self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
99
100     def process_email_queue(self, cr, uid, ids=None, context=None):
101         """Send immediately queued messages, committing after each
102            message is sent - this is not transactional and should
103            not be called during another transaction!
104
105            :param list ids: optional list of emails ids to send. If passed
106                             no search is performed, and these ids are used
107                             instead.
108            :param dict context: if a 'filters' key is present in context,
109                                 this value will be used as an additional
110                                 filter to further restrict the outgoing
111                                 messages to send (by default all 'outgoing'
112                                 messages are sent).
113         """
114         if context is None:
115             context = {}
116         if not ids:
117             filters = ['&', ('state', '=', 'outgoing'), ('type', '=', 'email')]
118             if 'filters' in context:
119                 filters.extend(context['filters'])
120             ids = self.search(cr, uid, filters, context=context)
121         res = None
122         try:
123             # Force auto-commit - this is meant to be called by
124             # the scheduler, and we can't allow rolling back the status
125             # of previously sent emails!
126             res = self.send(cr, uid, ids, auto_commit=True, context=context)
127         except Exception:
128             _logger.exception("Failed processing mail queue")
129         return res
130
131     def _postprocess_sent_message(self, cr, uid, mail, context=None):
132         """Perform any post-processing necessary after sending ``mail``
133         successfully, including deleting it completely along with its
134         attachment if the ``auto_delete`` flag of the mail was set.
135         Overridden by subclasses for extra post-processing behaviors.
136
137         :param browse_record mail: the mail that was just sent
138         :return: True
139         """
140         if mail.auto_delete:
141             # done with SUPERUSER_ID to avoid giving large unlink access rights
142             self.unlink(cr, SUPERUSER_ID, [mail.id], context=context)
143         return True
144
145     def send_get_mail_subject(self, cr, uid, mail, force=False, partner=None, context=None):
146         """ If subject is void and record_name defined: '<Author> posted on <Resource>'
147
148             :param boolean force: force the subject replacement
149             :param browse_record mail: mail.mail browse_record
150             :param browse_record partner: specific recipient partner
151         """
152         if force or (not mail.subject and mail.model and mail.res_id):
153             return 'Re: %s' % (mail.record_name)
154         return mail.subject
155
156     def send_get_mail_body(self, cr, uid, mail, partner=None, context=None):
157         """ Return a specific ir_email body. The main purpose of this method
158             is to be inherited by Portal, to add a link for signing in, in
159             each notification email a partner receives.
160
161             :param browse_record mail: mail.mail browse_record
162             :param browse_record partner: specific recipient partner
163         """
164         body = mail.body_html
165         # partner is a user, link to a related document (incentive to install portal)
166         if partner and partner.user_ids and mail.model and mail.res_id \
167                 and self.check_access_rights(cr, partner.user_ids[0].id, 'read', raise_exception=False):
168             related_user = partner.user_ids[0]
169             try:
170                 self.pool.get(mail.model).check_access_rule(cr, related_user.id, [mail.res_id], 'read', context=context)
171                 base_url = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url')
172                 # the parameters to encode for the query and fragment part of url
173                 query = {'db': cr.dbname}
174                 fragment = {
175                     'login': related_user.login,
176                     'model': mail.model,
177                     'id': mail.res_id,
178                 }
179                 url = urljoin(base_url, "?%s#%s" % (urlencode(query), urlencode(fragment)))
180                 text = _("""<small>Access this document <a href="%s">directly in OpenERP</a></small>""") % url
181                 body = tools.append_content_to_html(body, ("<div><p>%s</p></div>" % text), plaintext=False, container_tag='div')
182             except except_orm, e:
183                 pass
184         return body
185
186     def send_get_mail_reply_to(self, cr, uid, mail, partner=None, context=None):
187         """ Return a specific ir_email body. The main purpose of this method
188             is to be inherited by Portal, to add a link for signing in, in
189             each notification email a partner receives.
190
191             :param browse_record mail: mail.mail browse_record
192             :param browse_record partner: specific recipient partner
193         """
194         if mail.reply_to:
195             return mail.reply_to
196         if not mail.model or not mail.res_id:
197             return False
198         if not hasattr(self.pool.get(mail.model), 'message_get_reply_to'):
199             return False
200         return self.pool.get(mail.model).message_get_reply_to(cr, uid, [mail.res_id], context=context)[0]
201
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.
205
206             :param browse_record mail: mail.mail browse_record
207             :param browse_record partner: specific recipient partner
208         """
209         body = self.send_get_mail_body(cr, uid, mail, partner=partner, context=context)
210         subject = self.send_get_mail_subject(cr, uid, mail, partner=partner, context=context)
211         reply_to = self.send_get_mail_reply_to(cr, uid, mail, partner=partner, context=context)
212         body_alternative = tools.html2plaintext(body)
213         email_to = [partner.email] if partner else tools.email_split(mail.email_to)
214         return {
215             'body': body,
216             'body_alternative': body_alternative,
217             'subject': subject,
218             'email_to': email_to,
219             'reply_to': reply_to,
220         }
221
222     def send(self, cr, uid, ids, auto_commit=False, recipient_ids=None, context=None):
223         """ Sends the selected emails immediately, ignoring their current
224             state (mails that have already been sent should not be passed
225             unless they should actually be re-sent).
226             Emails successfully delivered are marked as 'sent', and those
227             that fail to be deliver are marked as 'exception', and the
228             corresponding error mail is output in the server logs.
229
230             :param bool auto_commit: whether to force a commit of the mail status
231                 after sending each mail (meant only for scheduler processing);
232                 should never be True during normal transactions (default: False)
233             :param list recipient_ids: specific list of res.partner recipients.
234                 If set, one email is sent to each partner. Its is possible to
235                 tune the sent email through ``send_get_mail_body`` and ``send_get_mail_subject``.
236                 If not specified, one email is sent to mail_mail.email_to.
237             :return: True
238         """
239         ir_mail_server = self.pool.get('ir.mail_server')
240         for mail in self.browse(cr, uid, ids, context=context):
241             try:
242                 # handle attachments
243                 attachments = []
244                 for attach in mail.attachment_ids:
245                     attachments.append((attach.datas_fname, base64.b64decode(attach.datas)))
246                 # specific behavior to customize the send email for notified partners
247                 email_list = []
248                 if recipient_ids:
249                     for partner in self.pool.get('res.partner').browse(cr, SUPERUSER_ID, recipient_ids, context=context):
250                         email_list.append(self.send_get_email_dict(cr, uid, mail, partner=partner, context=context))
251                 else:
252                     email_list.append(self.send_get_email_dict(cr, uid, mail, context=context))
253
254                 # build an RFC2822 email.message.Message object and send it without queuing
255                 for email in email_list:
256                     msg = ir_mail_server.build_email(
257                         email_from = mail.email_from,
258                         email_to = email.get('email_to'),
259                         subject = email.get('subject'),
260                         body = email.get('body'),
261                         body_alternative = email.get('body_alternative'),
262                         email_cc = tools.email_split(mail.email_cc),
263                         reply_to = email.get('reply_to'),
264                         attachments = attachments,
265                         message_id = mail.message_id,
266                         references = mail.references,
267                         object_id = mail.res_id and ('%s-%s' % (mail.res_id, mail.model)),
268                         subtype = 'html',
269                         subtype_alternative = 'plain')
270                     res = ir_mail_server.send_email(cr, uid, msg,
271                         mail_server_id=mail.mail_server_id.id, context=context)
272                 if res:
273                     mail.write({'state': 'sent', 'message_id': res})
274                     mail_sent = True
275                 else:
276                     mail.write({'state': 'exception'})
277                     mail_sent = False
278
279                 # /!\ can't use mail.state here, as mail.refresh() will cause an error
280                 # see revid:odo@openerp.com-20120622152536-42b2s28lvdv3odyr in 6.1
281                 if mail_sent:
282                     self._postprocess_sent_message(cr, uid, mail, context=context)
283             except Exception:
284                 _logger.exception('failed sending mail.mail %s', mail.id)
285                 mail.write({'state': 'exception'})
286
287             if auto_commit == True:
288                 cr.commit()
289         return True