import base64
import logging
import re
+from email.utils import formataddr
from urllib import urlencode
from urlparse import urljoin
# 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'),
- # Bounce and tracking
- 'opened': fields.integer(
- 'Opened',
- help='Number of times this email has been seen, using the OpenERP tracking.'),
- 'replied': fields.integer(
- 'Reply Received',
- help='If checked, a reply to this email has been received.'),
}
_defaults = {
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
def cancel(self, cr, uid, ids, context=None):
return self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
- def set_opened(self, cr, uid, ids, context=None):
- """ Increment opened counter """
- for mail in self.browse(cr, uid, ids, context=context):
- self.write(cr, uid, [mail.id], {'opened': (mail.opened + 1)}, context=context)
- return True
-
- def set_replied(self, cr, uid, ids, context=None):
- """ Increment replied counter """
- for mail in self.browse(cr, uid, ids, context=context):
- self.write(cr, uid, [mail.id], {'replied': (mail.replied + 1)}, context=context)
- return True
-
def process_email_queue(self, cr, uid, ids=None, context=None):
"""Send immediately queued messages, committing after each
message is sent - this is not transactional and should
# 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
'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
- else:
- return None
+ 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)
- def _get_tracking_url(self, cr, uid, mail, partner=None, context=None):
- if not mail.auto_delete:
- base_url = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url')
- track_url = urljoin(base_url, 'mail/track/%d/blank.gif' % mail.id)
- print base_url, track_url
- return '<img src="%s" alt=""/>' % track_url
+ 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 ''
+ 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>'
# generate footer
link = self._get_partner_access_link(cr, uid, mail, partner, context=context)
- tracking_url = self._get_tracking_url(cr, uid, mail, partner, context=context)
if link:
body = tools.append_content_to_html(body, link, plaintext=False, container_tag='div')
- if tracking_url:
- body = tools.append_content_to_html(body, tracking_url, plaintext=False, container_tag='div')
return body
def send_get_email_dict(self, cr, uid, mail, partner=None, context=None):
# 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)
: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:
subtype='html',
subtype_alternative='plain',
headers=headers)
- res = ir_mail_server.send_email(cr, uid, msg,
+ 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
# /!\ 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'})