[MERGE] forward port of branch 7.0 up to be7c894
[odoo/odoo.git] / addons / mail / mail_mail.py
index 6bbfa9b..9dcea07 100644 (file)
 #
 ##############################################################################
 
-# import ast
 import base64
 import logging
-import tools
+import re
+from email.utils import formataddr
+from urllib import urlencode
+from urlparse import urljoin
 
-from osv import osv
-from osv import fields
-from tools.translate import _
+from openerp import tools
+from openerp import SUPERUSER_ID
+from openerp.addons.base.ir.ir_mail_server import MailDeliveryException
+from openerp.osv import fields, osv
+from openerp.tools.translate import _
 
 _logger = logging.getLogger(__name__)
 
@@ -41,7 +45,6 @@ class mail_mail(osv.Model):
 
     _columns = {
         'mail_message_id': fields.many2one('mail.message', 'Message', required=True, ondelete='cascade'),
-        'mail_server_id': fields.many2one('ir.mail_server', 'Outgoing mail server', readonly=1),
         'state': fields.selection([
             ('outgoing', 'Outgoing'),
             ('sent', 'Sent'),
@@ -52,29 +55,29 @@ class mail_mail(osv.Model):
         'auto_delete': fields.boolean('Auto Delete',
             help="Permanently delete this email after sending it, to save space"),
         'references': fields.text('References', help='Message references, such as identifiers of previous messages', readonly=1),
-        'email_from': fields.char('From', help='Message sender, taken from user preferences.'),
-        'email_to': fields.text('To', help='Message recipients'),
+        'email_to': fields.text('To', help='Message recipients (emails)'),
+        'recipient_ids': fields.many2many('res.partner', string='To (Partners)'),
         'email_cc': fields.char('Cc', help='Carbon copy message recipients'),
-        'reply_to': fields.char('Reply-To', help='Preferred response address for the message'),
         'body_html': fields.text('Rich-text Contents', help="Rich-text/HTML message"),
-
         # Auto-detected based on create() - if 'mail_message_id' was passed then this mail is a notification
         # and during unlink() we will not cascade delete the parent and its attachments
-        'notification': fields.boolean('Is Notification')
+        'notification': fields.boolean('Is Notification',
+            help='Mail has been created to notify people of an existing mail.message'),
     }
 
-    def _get_default_from(self, cr, uid, context=None):
-        cur = self.pool.get('res.users').browse(cr, uid, uid, context=context)
-        if not cur.alias_domain:
-            raise osv.except_osv(_('Invalid Action!'), _('Unable to send email, set an alias domain in your server settings.'))
-        return cur.alias_name + '@' + cur.alias_domain
-
     _defaults = {
         'state': 'outgoing',
-        'email_from': lambda self, cr, uid, ctx=None: self._get_default_from(cr, uid, ctx),
     }
 
+    def default_get(self, cr, uid, fields, context=None):
+        # protection for `default_type` values leaking from menu action context (e.g. for invoices)
+        # To remove when automatic context propagation is removed in web client
+        if context and context.get('default_type') and context.get('default_type') not in self._all_columns['type'].column.selection:
+            context = dict(context, default_type=None)
+        return super(mail_mail, self).default_get(cr, uid, fields, context=context)
+
     def create(self, cr, uid, values, context=None):
+        # notification field: if not set, set if mail comes from an existing mail.message
         if 'notification' not in values and values.get('mail_message_id'):
             values['notification'] = True
         return super(mail_mail, self).create(cr, uid, values, context=context)
@@ -110,7 +113,7 @@ class mail_mail(osv.Model):
         if context is None:
             context = {}
         if not ids:
-            filters = ['&', ('state', '=', 'outgoing'), ('type', '=', 'email')]
+            filters = [('state', '=', 'outgoing')]
             if 'filters' in context:
                 filters.extend(context['filters'])
             ids = self.search(cr, uid, filters, context=context)
@@ -134,9 +137,36 @@ class mail_mail(osv.Model):
         :return: True
         """
         if mail.auto_delete:
-            mail.unlink()
+            # done with SUPERUSER_ID to avoid giving large unlink access rights
+            self.unlink(cr, SUPERUSER_ID, [mail.id], context=context)
         return True
 
+    #------------------------------------------------------
+    # mail_mail formatting, tools and send mechanism
+    #------------------------------------------------------
+
+    def _get_partner_access_link(self, cr, uid, mail, partner=None, context=None):
+        """ Generate URLs for links in mails:
+            - partner is an user and has read access to the document: direct link to document with model, res_id
+        """
+        if partner and partner.user_ids:
+            base_url = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url')
+            # the parameters to encode for the query and fragment part of url
+            query = {'db': cr.dbname}
+            fragment = {
+                'login': partner.user_ids[0].login,
+                'action': 'mail.action_mail_redirect',
+            }
+            if mail.notification:
+                fragment['message_id'] = mail.mail_message_id.id
+            elif mail.model and mail.res_id:
+                fragment.update(model=mail.model, res_id=mail.res_id)
+
+            url = urljoin(base_url, "/web?%s#%s" % (urlencode(query), urlencode(fragment)))
+            return _("""<span class='oe_mail_footer_access'><small>Access your messages and documents <a style='color:inherit' href="%s">in OpenERP</a></small></span>""") % url
+        else:
+            return None
+
     def send_get_mail_subject(self, cr, uid, mail, force=False, partner=None, context=None):
         """ If subject is void and record_name defined: '<Author> posted on <Resource>'
 
@@ -144,19 +174,26 @@ class mail_mail(osv.Model):
             :param browse_record mail: mail.mail browse_record
             :param browse_record partner: specific recipient partner
         """
-        if force or (not mail.subject and mail.model and mail.res_id):
-            return '%s posted on %s' % (mail.author_id.name, mail.record_name)
+        if (force or not mail.subject) and mail.record_name:
+            return 'Re: %s' % (mail.record_name)
+        elif (force or not mail.subject) and mail.parent_id and mail.parent_id.subject:
+            return 'Re: %s' % (mail.parent_id.subject)
         return mail.subject
 
     def send_get_mail_body(self, cr, uid, mail, partner=None, context=None):
         """ Return a specific ir_email body. The main purpose of this method
-            is to be inherited by Portal, to add a link for signing in, in
-            each notification email a partner receives.
+            is to be inherited to add custom content depending on some module.
 
             :param browse_record mail: mail.mail browse_record
             :param browse_record partner: specific recipient partner
         """
-        return mail.body_html
+        body = mail.body_html
+
+        # generate footer
+        link = self._get_partner_access_link(cr, uid, mail, partner, context=context)
+        if link:
+            body = tools.append_content_to_html(body, link, plaintext=False, container_tag='div')
+        return body
 
     def send_get_email_dict(self, cr, uid, mail, partner=None, context=None):
         """ Return a dictionary for specific email values, depending on a
@@ -168,7 +205,18 @@ class mail_mail(osv.Model):
         body = self.send_get_mail_body(cr, uid, mail, partner=partner, context=context)
         subject = self.send_get_mail_subject(cr, uid, mail, partner=partner, context=context)
         body_alternative = tools.html2plaintext(body)
-        email_to = [partner.email] if partner else tools.email_split(mail.email_to)
+
+        # generate email_to, heuristic:
+        # 1. if 'partner' is specified and there is a related document: Followers of 'Doc' <email>
+        # 2. if 'partner' is specified, but no related document: Partner Name <email>
+        # 3; fallback on mail.email_to that we split to have an email addresses list
+        if partner and mail.record_name:
+            email_to = [formataddr((_('Followers of %s') % mail.record_name, partner.email))]
+        elif partner:
+            email_to = [formataddr((partner.name, partner.email))]
+        else:
+            email_to = tools.email_split(mail.email_to)
+
         return {
             'body': body,
             'body_alternative': body_alternative,
@@ -176,7 +224,7 @@ class mail_mail(osv.Model):
             'email_to': email_to,
         }
 
-    def send(self, cr, uid, ids, auto_commit=False, recipient_ids=None, context=None):
+    def send(self, cr, uid, ids, auto_commit=False, raise_exception=False, context=None):
         """ Sends the selected emails immediately, ignoring their current
             state (mails that have already been sent should not be passed
             unless they should actually be re-sent).
@@ -187,55 +235,99 @@ class mail_mail(osv.Model):
             :param bool auto_commit: whether to force a commit of the mail status
                 after sending each mail (meant only for scheduler processing);
                 should never be True during normal transactions (default: False)
-            :param list recipient_ids: specific list of res.partner recipients.
-                If set, one email is sent to each partner. Its is possible to
-                tune the sent email through ``send_get_mail_body`` and ``send_get_mail_subject``.
-                If not specified, one email is sent to mail_mail.email_to.
+            :param bool raise_exception: whether to raise an exception if the
+                email sending process has failed
             :return: True
         """
         ir_mail_server = self.pool.get('ir.mail_server')
-        for mail in self.browse(cr, uid, ids, context=context):
+        ir_attachment = self.pool['ir.attachment']
+
+        for mail in self.browse(cr, SUPERUSER_ID, ids, context=context):
             try:
-                # handle attachments
-                attachments = []
-                for attach in mail.attachment_ids:
-                    attachments.append((attach.datas_fname, base64.b64decode(attach.datas)))
+                # load attachment binary data with a separate read(), as prefetching all
+                # `datas` (binary field) could bloat the browse cache, triggerring
+                # soft/hard mem limits with temporary data.
+                attachment_ids = [a.id for a in mail.attachment_ids]
+                attachments = [(a['datas_fname'], base64.b64decode(a['datas']))
+                                 for a in ir_attachment.read(cr, SUPERUSER_ID, attachment_ids,
+                                                             ['datas_fname', 'datas'])]
                 # specific behavior to customize the send email for notified partners
                 email_list = []
-                if recipient_ids:
-                    for partner in self.pool.get('res.partner').browse(cr, uid, recipient_ids, context=context):
-                        email_list.append(self.send_get_email_dict(cr, uid, mail, partner=partner, context=context))
-                else:
+                if mail.email_to:
                     email_list.append(self.send_get_email_dict(cr, uid, mail, context=context))
+                for partner in mail.recipient_ids:
+                    email_list.append(self.send_get_email_dict(cr, uid, mail, partner=partner, context=context))
+                # headers
+                headers = {}
+                bounce_alias = self.pool['ir.config_parameter'].get_param(cr, uid, "mail.bounce.alias", context=context)
+                catchall_domain = self.pool['ir.config_parameter'].get_param(cr, uid, "mail.catchall.domain", context=context)
+                if bounce_alias and catchall_domain:
+                    if mail.model and mail.res_id:
+                        headers['Return-Path'] = '%s-%d-%s-%d@%s' % (bounce_alias, mail.id, mail.model, mail.res_id, catchall_domain)
+                    else:
+                        headers['Return-Path'] = '%s-%d@%s' % (bounce_alias, mail.id, catchall_domain)
 
                 # build an RFC2822 email.message.Message object and send it without queuing
+                res = None
                 for email in email_list:
                     msg = ir_mail_server.build_email(
-                        email_from = mail.email_from,
-                        email_to = email.get('email_to'),
-                        subject = email.get('subject'),
-                        body = email.get('body'),
-                        body_alternative = email.get('body_alternative'),
-                        email_cc = tools.email_split(mail.email_cc),
-                        reply_to = mail.reply_to,
-                        attachments = attachments,
-                        message_id = mail.message_id,
-                        references = mail.references,
-                        object_id = mail.res_id and ('%s-%s' % (mail.res_id, mail.model)),
-                        subtype = 'html',
-                        subtype_alternative = 'plain')
-                    res = ir_mail_server.send_email(cr, uid, msg,
-                        mail_server_id=mail.mail_server_id.id, context=context)
+                        email_from=mail.email_from,
+                        email_to=email.get('email_to'),
+                        subject=email.get('subject'),
+                        body=email.get('body'),
+                        body_alternative=email.get('body_alternative'),
+                        email_cc=tools.email_split(mail.email_cc),
+                        reply_to=mail.reply_to,
+                        attachments=attachments,
+                        message_id=mail.message_id,
+                        references=mail.references,
+                        object_id=mail.res_id and ('%s-%s' % (mail.res_id, mail.model)),
+                        subtype='html',
+                        subtype_alternative='plain',
+                        headers=headers)
+                    try:
+                        res = ir_mail_server.send_email(cr, uid, msg,
+                                                    mail_server_id=mail.mail_server_id.id,
+                                                    context=context)
+                    except AssertionError as error:
+                        if error.message == ir_mail_server.NO_VALID_RECIPIENT:
+                            # No valid recipient found for this particular
+                            # mail item -> ignore error to avoid blocking
+                            # delivery to next recipients, if any. If this is
+                            # the only recipient, the mail will show as failed.
+                            _logger.warning("Ignoring invalid recipients for mail.mail %s: %s",
+                                            mail.message_id, email.get('email_to'))
+                        else:
+                            raise
                 if res:
                     mail.write({'state': 'sent', 'message_id': res})
+                    mail_sent = True
                 else:
                     mail.write({'state': 'exception'})
-                mail.refresh()
-                if mail.state == 'sent':
+                    mail_sent = False
+
+                # /!\ can't use mail.state here, as mail.refresh() will cause an error
+                # see revid:odo@openerp.com-20120622152536-42b2s28lvdv3odyr in 6.1
+                if mail_sent:
+                    _logger.info('Mail with ID %r and Message-Id %r successfully sent', mail.id, mail.message_id)
                     self._postprocess_sent_message(cr, uid, mail, context=context)
-            except Exception:
+            except MemoryError:
+                # prevent catching transient MemoryErrors, bubble up to notify user or abort cron job
+                # instead of marking the mail as failed
+                _logger.exception('MemoryError while processing mail with ID %r and Msg-Id %r. '\
+                                      'Consider raising the --limit-memory-hard startup option',
+                                  mail.id, mail.message_id)
+                raise
+            except Exception as e:
                 _logger.exception('failed sending mail.mail %s', mail.id)
                 mail.write({'state': 'exception'})
+                if raise_exception:
+                    if isinstance(e, AssertionError):
+                        # get the args of the original error, wrap into a value and throw a MailDeliveryException
+                        # that is an except_orm, with name and value as arguments
+                        value = '. '.join(e.args)
+                        raise MailDeliveryException(_("Mail Delivery Failed"), value)
+                    raise
 
             if auto_commit == True:
                 cr.commit()