[MERGE] forward port of branch 7.0 up to be7c894
[odoo/odoo.git] / addons / mail / mail_mail.py
index 9fa80b9..9dcea07 100644 (file)
@@ -22,6 +22,7 @@
 import base64
 import logging
 import re
+from email.utils import formataddr
 from urllib import urlencode
 from urlparse import urljoin
 
@@ -61,15 +62,9 @@ class mail_mail(osv.Model):
         # 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',
-            help='Mail has been created to notify people of an existing mail.message')
+            help='Mail has been created to notify people of an existing mail.message'),
     }
 
-    def _get_default_from(self, cr, uid, context=None):
-        """ Kept for compatibility
-            TDE TODO: remove me in 8.0
-        """
-        return self.pool['mail.message']._get_default_from(cr, uid, context=context)
-
     _defaults = {
         'state': 'outgoing',
     }
@@ -81,74 +76,11 @@ class mail_mail(osv.Model):
             context = dict(context, default_type=None)
         return super(mail_mail, self).default_get(cr, uid, fields, context=context)
 
-    def _get_reply_to(self, cr, uid, values, context=None):
-        """ Return a specific reply_to: alias of the document through message_get_reply_to
-            or take the email_from
-        """
-        # if value specified: directly return it
-        if values.get('reply_to'):
-            return values.get('reply_to')
-        format_name = True  # whether to use a 'Followers of Pigs <pigs@openerp.com' format
-        email_reply_to = None
-
-        ir_config_parameter = self.pool.get("ir.config_parameter")
-        catchall_domain = ir_config_parameter.get_param(cr, uid, "mail.catchall.domain", context=context)
-
-        # model, res_id, email_from: comes from values OR related message
-        model, res_id, email_from = values.get('model'), values.get('res_id'), values.get('email_from')
-        if values.get('mail_message_id'):
-            message = self.pool.get('mail.message').browse(cr, uid, values.get('mail_message_id'), context=context)
-            if message.reply_to:
-                email_reply_to = message.reply_to
-                format_name = False
-            if not model:
-                model = message.model
-            if not res_id:
-                res_id = message.res_id
-            if not email_from:
-                email_from = message.email_from
-
-        # if model and res_id: try to use ``message_get_reply_to`` that returns the document alias
-        if not email_reply_to and model and res_id and hasattr(self.pool[model], 'message_get_reply_to'):
-            email_reply_to = self.pool[model].message_get_reply_to(cr, uid, [res_id], context=context)[0]
-        # no alias reply_to -> catchall alias
-        if not email_reply_to:
-            catchall_alias = ir_config_parameter.get_param(cr, uid, "mail.catchall.alias", context=context)
-            if catchall_domain and catchall_alias:
-                email_reply_to = '%s@%s' % (catchall_alias, catchall_domain)
-
-        # still no reply_to -> reply_to will be the email_from
-        if not email_reply_to and email_from:
-            email_reply_to = email_from
-
-        # format 'Document name <email_address>'
-        if email_reply_to and model and res_id and format_name:
-            emails = tools.email_split(email_reply_to)
-            if emails:
-                email_reply_to = emails[0]
-            document_name = self.pool[model].name_get(cr, SUPERUSER_ID, [res_id], context=context)[0]
-            if document_name:
-                # sanitize document name
-                sanitized_doc_name = re.sub(r'[^\w+.]+', '-', document_name[1])
-                # generate reply to
-                email_reply_to = _('"Followers of %s" <%s>') % (sanitized_doc_name, email_reply_to)
-
-        return email_reply_to
-
     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
-        mail_id = super(mail_mail, self).create(cr, uid, values, context=context)
-
-        # reply_to: if not set, set with default values that require creation values
-        # but delegate after creation because of mail_message.message_id automatic
-        # creation using existence of reply_to
-        if not values.get('reply_to'):
-            reply_to = self._get_reply_to(cr, uid, values, context=context)
-            if reply_to:
-                self.write(cr, uid, [mail_id], {'reply_to': reply_to}, context=context)
-        return mail_id
+        return super(mail_mail, self).create(cr, uid, values, context=context)
 
     def unlink(self, cr, uid, ids, context=None):
         # cascade-delete the parent message for all mails that are not created for a notification
@@ -213,11 +145,6 @@ class mail_mail(osv.Model):
     # mail_mail formatting, tools and send mechanism
     #------------------------------------------------------
 
-    # TODO in 8.0(+): maybe factorize this to enable in modules link generation
-    # independently of mail_mail model
-    # TODO in 8.0(+): factorize doc name sanitized and 'Followers of ...' formatting
-    # because it begins to appear everywhere
-
     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
@@ -231,11 +158,12 @@ class mail_mail(osv.Model):
                 'action': 'mail.action_mail_redirect',
             }
             if mail.notification:
-                fragment.update({
-                        'message_id': mail.mail_message_id.id,
-                    })
-            url = urljoin(base_url, "?%s#%s" % (urlencode(query), urlencode(fragment)))
-            return _("""<small>Access your messages and documents <a style='color:inherit' href="%s">in OpenERP</a></small>""") % url
+                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
 
@@ -283,10 +211,9 @@ class mail_mail(osv.Model):
         # 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:
-            sanitized_record_name = re.sub(r'[^\w+.]+', '-', mail.record_name)
-            email_to = [_('"Followers of %s" <%s>') % (sanitized_record_name, partner.email)]
+            email_to = [formataddr((_('Followers of %s') % mail.record_name, partner.email))]
         elif partner:
-            email_to = ['%s <%s>' % (partner.name, partner.email)]
+            email_to = [formataddr((partner.name, partner.email))]
         else:
             email_to = tools.email_split(mail.email_to)
 
@@ -313,38 +240,65 @@ class mail_mail(osv.Model):
             :return: True
         """
         ir_mail_server = self.pool.get('ir.mail_server')
+        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 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
@@ -355,7 +309,15 @@ class mail_mail(osv.Model):
                 # /!\ 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 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'})