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.osv.orm import except_orm
from openerp.tools.translate import _
_logger = logging.getLogger(__name__)
_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'),
'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):
- this = self.pool.get('res.users').browse(cr, uid, uid, context=context)
- if this.alias_domain:
- return '%s@%s' % (this.alias_name, this.alias_domain)
- elif this.email:
- return this.email
- raise osv.except_osv(_('Invalid Action!'), _("Unable to send email, please configure the sender's email address or alias."))
-
_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):
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)
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)
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>'
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
"""
body = mail.body_html
- # partner is a user, link to a related document (incentive to install portal)
- if partner and partner.user_ids and mail.model and mail.res_id \
- and self.check_access_rights(cr, partner.user_ids[0].id, 'read', raise_exception=False):
- related_user = partner.user_ids[0]
- try:
- self.pool.get(mail.model).check_access_rule(cr, related_user.id, [mail.res_id], 'read', context=context)
- 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': related_user.login,
- 'model': mail.model,
- 'id': mail.res_id,
- }
- url = urljoin(base_url, "?%s#%s" % (urlencode(query), urlencode(fragment)))
- text = _("""<p>Access this document <a href="%s">directly in OpenERP</a></p>""") % url
- body = tools.append_content_to_html(body, ("<div><p>%s</p></div>" % text), plaintext=False)
- except except_orm, e:
- pass
- return body
- def send_get_mail_reply_to(self, cr, uid, mail, partner=None, context=None):
- """ Return a specific ir_email reply_to.
-
- :param browse_record mail: mail.mail browse_record
- :param browse_record partner: specific recipient partner
- """
- if mail.reply_to:
- return mail.reply_to
- email_reply_to = False
-
- # if model and res_id: try to use ``message_get_reply_to`` that returns the document alias
- if mail.model and mail.res_id and hasattr(self.pool.get(mail.model), 'message_get_reply_to'):
- email_reply_to = self.pool.get(mail.model).message_get_reply_to(cr, uid, [mail.res_id], context=context)[0]
- # no alias reply_to -> reply_to will be the email_from, only the email part
- if not email_reply_to and mail.email_from:
- emails = tools.email_split(mail.email_from)
- if emails:
- email_reply_to = emails[0]
-
- # format 'Document name <email_address>'
- if email_reply_to and mail.model and mail.res_id:
- document_name = self.pool.get(mail.model).name_get(cr, SUPERUSER_ID, [mail.res_id], context=context)[0]
- if document_name:
- # generate reply to
-
- email_reply_to = formataddr((_('Followers of %s') % document_name[1], email_reply_to))
-
- return email_reply_to
+ # 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
"""
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)
- reply_to = self.send_get_mail_reply_to(cr, uid, mail, partner=partner, context=context)
body_alternative = tools.html2plaintext(body)
# generate email_to, heuristic:
'body_alternative': body_alternative,
'subject': subject,
'email_to': email_to,
- 'reply_to': reply_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).
: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:
- partner_obj = self.pool.get('res.partner')
- existing_recipient_ids = partner_obj.exists(cr, SUPERUSER_ID, recipient_ids, context=context)
- for partner in partner_obj.browse(cr, SUPERUSER_ID, existing_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 = email.get('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')
+ 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
- )
+ 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
# /!\ 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:
+ 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()