[CLEAN] mail: clean mail gateway code
The purpose of this commit is to clean a bit the code related to email routing in the mailgateway: message_route and message_route_verify. message_route is now clearly separeted into two categories. First one handles emails considered as answers to existing threads. Second one handles emails that have to create new threads, either through alias or default behavior of the mailgateway. message_route_verify has been a bit facotrized to simplify the various overrides that appeared lately in the codebase. This commit only contains code cleaning. No functional change should occur due to this commit.
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from odoo import api, models
|
from odoo import api, models, _
|
||||||
from odoo.tools import decode_message_header, email_split
|
from odoo.tools import decode_message_header, email_split
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
@@ -13,35 +13,24 @@ class MailThread(models.AbstractModel):
|
|||||||
_inherit = "mail.thread"
|
_inherit = "mail.thread"
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def message_route_verify(self, message, message_dict, route, update_author=True, assert_model=True, create_fallback=True, allow_private=False):
|
def message_route_verify(self, message, message_dict, route,
|
||||||
res = super(MailThread, self).message_route_verify(message, message_dict, route, update_author, assert_model, create_fallback, allow_private)
|
update_author=True, assert_model=True,
|
||||||
|
create_fallback=True, allow_private=False,
|
||||||
|
drop_alias=False):
|
||||||
|
res = super(MailThread, self).message_route_verify(
|
||||||
|
message, message_dict, route,
|
||||||
|
update_author=update_author,
|
||||||
|
assert_model=assert_model,
|
||||||
|
create_fallback=create_fallback,
|
||||||
|
allow_private=allow_private,
|
||||||
|
drop_alias=drop_alias)
|
||||||
|
|
||||||
if res:
|
if res:
|
||||||
alias = route[4]
|
alias = route[4]
|
||||||
email_from = decode_message_header(message, 'From')
|
email_from = decode_message_header(message, 'From')
|
||||||
message_id = message.get('Message-Id')
|
message_id = message.get('Message-Id')
|
||||||
|
|
||||||
# Identically equal to the definition in mail module because sub methods are local
|
|
||||||
# variables and cannot be called with super
|
|
||||||
def _create_bounce_email(body_html):
|
|
||||||
bounce_to = decode_message_header(message, 'Return-Path') or email_from
|
|
||||||
bounce_mail_values = {
|
|
||||||
'body_html': body_html,
|
|
||||||
'subject': 'Re: %s' % message.get('subject'),
|
|
||||||
'email_to': bounce_to,
|
|
||||||
'auto_delete': True,
|
|
||||||
}
|
|
||||||
bounce_from = self.env['ir.mail_server']._get_default_bounce_address()
|
|
||||||
if bounce_from:
|
|
||||||
bounce_mail_values['email_from'] = 'MAILER-DAEMON <%s>' % bounce_from
|
|
||||||
self.env['mail.mail'].create(bounce_mail_values).send()
|
|
||||||
|
|
||||||
def _warn(message):
|
|
||||||
_logger.info('Routing mail with Message-Id %s: route %s: %s',
|
|
||||||
message_id, route, message)
|
|
||||||
|
|
||||||
# Alias: check alias_contact settings for employees
|
# Alias: check alias_contact settings for employees
|
||||||
|
|
||||||
if alias and alias.alias_contact == 'employees':
|
if alias and alias.alias_contact == 'employees':
|
||||||
email_address = email_split(email_from)[0]
|
email_address = email_split(email_from)[0]
|
||||||
employee = self.env['hr.employee'].search([('work_email', 'ilike', email_address)], limit=1)
|
employee = self.env['hr.employee'].search([('work_email', 'ilike', email_address)], limit=1)
|
||||||
@@ -49,7 +38,7 @@ class MailThread(models.AbstractModel):
|
|||||||
employee = self.env['hr.employee'].search([('user_id.email', 'ilike', email_address)], limit=1)
|
employee = self.env['hr.employee'].search([('user_id.email', 'ilike', email_address)], limit=1)
|
||||||
if not employee:
|
if not employee:
|
||||||
mail_template = self.env.ref('hr.mail_template_data_unknown_employee_email_address')
|
mail_template = self.env.ref('hr.mail_template_data_unknown_employee_email_address')
|
||||||
_warn('alias %s does not accept unknown employees, skipping' % alias.alias_name)
|
self._routing_warn(_('alias %s does not accept unknown employees') % alias.alias_name, _('skipping'), message_id, route, False)
|
||||||
_create_bounce_email(mail_template.body_html)
|
self._routing_create_bounce_email(email_from, mail_template.body_html, message)
|
||||||
return False
|
return False
|
||||||
return res
|
return res
|
||||||
|
|||||||
+170
-179
@@ -789,8 +789,32 @@ class MailThread(models.AbstractModel):
|
|||||||
s = ', '.join([tools.decode_smtp_header(message.get(h)) for h in header_fields if message.get(h)])
|
s = ', '.join([tools.decode_smtp_header(message.get(h)) for h in header_fields if message.get(h)])
|
||||||
return filter(lambda x: x, self._find_partner_from_emails(tools.email_split(s)))
|
return filter(lambda x: x, self._find_partner_from_emails(tools.email_split(s)))
|
||||||
|
|
||||||
|
def _routing_warn(self, error_message, warn_suffix, message_id, route, raise_exception):
|
||||||
|
""" Tools method used in message_route_verify: whether to log a warning or raise an error """
|
||||||
|
full_message = _('Routing mail with Message-Id %s: route %s: %s') % (message_id, route, error_message)
|
||||||
|
if raise_exception:
|
||||||
|
raise ValueError(full_message)
|
||||||
|
else:
|
||||||
|
_logger.info(full_message + warn_suffix and '; %s' % warn_suffix or '')
|
||||||
|
|
||||||
|
def _routing_create_bounce_email(self, email_from, body_html, message):
|
||||||
|
bounce_to = tools.decode_message_header(message, 'Return-Path') or email_from
|
||||||
|
bounce_mail_values = {
|
||||||
|
'body_html': body_html,
|
||||||
|
'subject': 'Re: %s' % message.get('subject'),
|
||||||
|
'email_to': bounce_to,
|
||||||
|
'auto_delete': True,
|
||||||
|
}
|
||||||
|
bounce_from = self.env['ir.mail_server']._get_default_bounce_address()
|
||||||
|
if bounce_from:
|
||||||
|
bounce_mail_values['email_from'] = 'MAILER-DAEMON <%s>' % bounce_from
|
||||||
|
self.env['mail.mail'].create(bounce_mail_values).send()
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def message_route_verify(self, message, message_dict, route, update_author=True, assert_model=True, create_fallback=True, allow_private=False):
|
def message_route_verify(self, message, message_dict, route,
|
||||||
|
update_author=True, assert_model=True,
|
||||||
|
create_fallback=True, allow_private=False,
|
||||||
|
drop_alias=False):
|
||||||
""" Verify route validity. Check and rules:
|
""" Verify route validity. Check and rules:
|
||||||
1 - if thread_id -> check that document effectively exists; otherwise
|
1 - if thread_id -> check that document effectively exists; otherwise
|
||||||
fallback on a message_new by resetting thread_id
|
fallback on a message_new by resetting thread_id
|
||||||
@@ -804,6 +828,22 @@ class MailThread(models.AbstractModel):
|
|||||||
check on alias parent document that the author is in the
|
check on alias parent document that the author is in the
|
||||||
followers
|
followers
|
||||||
'partners': check that author_id id set
|
'partners': check that author_id id set
|
||||||
|
|
||||||
|
:param message: an email.message instance
|
||||||
|
:param message_dict: dictionary of values that will be given to
|
||||||
|
mail_message.create()
|
||||||
|
:param route: route to check which is a tuple (model, thread_id,
|
||||||
|
custom_values, uid, alias)
|
||||||
|
:param update_author: update message_dict['author_id']. TDE TODO: move me
|
||||||
|
:param assert_model: if an error occurs, tell whether to raise an error
|
||||||
|
or just log a warning and try other processing or
|
||||||
|
invalidate route
|
||||||
|
:param create_fallback: if the route aims at updating a record and that
|
||||||
|
record does not exists or does not support update
|
||||||
|
either fallback on creating a new record in the
|
||||||
|
same model or raise / warn
|
||||||
|
:param allow_private: allow void model / thread_id routes, aka private
|
||||||
|
discussions
|
||||||
"""
|
"""
|
||||||
|
|
||||||
assert isinstance(route, (list, tuple)), 'A route should be a list or a tuple'
|
assert isinstance(route, (list, tuple)), 'A route should be a list or a tuple'
|
||||||
@@ -815,88 +855,55 @@ class MailThread(models.AbstractModel):
|
|||||||
model, thread_id, alias = route[0], route[1], route[4]
|
model, thread_id, alias = route[0], route[1], route[4]
|
||||||
record_set = None
|
record_set = None
|
||||||
|
|
||||||
def _create_bounce_email():
|
_generic_bounce_body_html = """<div>
|
||||||
bounce_to = tools.decode_smtp_header(message.get('Return-Path')) or email_from
|
<p>Hello,</p>
|
||||||
bounce_mail_values = {
|
<p>The following email sent to %s cannot be accepted because this is a private email address.
|
||||||
'body_html': '<div><p>Hello,</p>'
|
Only allowed people can contact us at this address.</p>
|
||||||
'<p>The following email sent to %s cannot be accepted because this is '
|
</div><blockquote>%s</blockquote>""" % (message.get('to'), message_dict.get('body')),
|
||||||
'a private email address. Only allowed people can contact us at this address.</p></div>'
|
|
||||||
'<blockquote>%s</blockquote>' % (message.get('to'), message_dict.get('body')),
|
|
||||||
'subject': 'Re: %s' % message.get('subject'),
|
|
||||||
'email_to': bounce_to,
|
|
||||||
'auto_delete': True,
|
|
||||||
}
|
|
||||||
bounce_from = self.env['ir.mail_server']._get_default_bounce_address()
|
|
||||||
if bounce_from:
|
|
||||||
bounce_mail_values['email_from'] = 'MAILER-DAEMON <%s>' % bounce_from
|
|
||||||
self.env['mail.mail'].create(bounce_mail_values).send()
|
|
||||||
|
|
||||||
def _warn(message):
|
|
||||||
_logger.info('Routing mail with Message-Id %s: route %s: %s',
|
|
||||||
message_id, route, message)
|
|
||||||
|
|
||||||
# Wrong model
|
# Wrong model
|
||||||
if model and model not in self.env:
|
if model and model not in self.env:
|
||||||
if assert_model:
|
self._routing_warn(_('unknown target model %s') % model, '', message_id, route, assert_model)
|
||||||
assert model in self.env, 'Routing: unknown target model %s' % model
|
|
||||||
_warn('unknown target model %s' % model)
|
|
||||||
return ()
|
return ()
|
||||||
|
|
||||||
# Private message: should not contain any thread_id
|
# Private message
|
||||||
if not model and thread_id:
|
if not model:
|
||||||
if assert_model:
|
# should not contain any thread_id
|
||||||
if thread_id:
|
if thread_id:
|
||||||
raise ValueError('Routing: posting a message without model should be with a null res_id (private message), received %s.' % thread_id)
|
self._routing_warn(_('posting a message without model should be with a null res_id (private message), received %s') % thread_id, _('resetting thread_id'), message_id, route, assert_model)
|
||||||
_warn('posting a message without model should be with a null res_id (private message), received %s resetting thread_id' % thread_id)
|
thread_id = 0
|
||||||
thread_id = 0
|
# should have a parent_id (only answers)
|
||||||
# Private message: should have a parent_id (only answers)
|
if not message_dict.get('parent_id'):
|
||||||
if not model and not message_dict.get('parent_id'):
|
self._routing_warn(_('posting a message without model should be with a parent_id (private message)'), _('skipping'), message_id, route, assert_model)
|
||||||
if assert_model:
|
return False
|
||||||
if not message_dict.get('parent_id'):
|
|
||||||
raise ValueError('Routing: posting a message without model should be with a parent_id (private mesage).')
|
|
||||||
_warn('posting a message without model should be with a parent_id (private mesage), skipping')
|
|
||||||
return False
|
|
||||||
|
|
||||||
if model and thread_id:
|
if model and thread_id:
|
||||||
record_set = self.env[model].browse(thread_id)
|
record_set = self.env[model].browse(thread_id)
|
||||||
elif model:
|
elif model:
|
||||||
record_set = self.env[model]
|
record_set = self.env[model]
|
||||||
|
|
||||||
# Existing Document: check if exists; if not, fallback on create if allowed
|
# Existing Document: check if exists and model accepts the mailgateway; if not, fallback on create if allowed
|
||||||
if thread_id and not record_set.exists():
|
if thread_id:
|
||||||
if create_fallback:
|
if not record_set.exists() and create_fallback:
|
||||||
_warn('reply to missing document (%s,%s), fall back on new document creation' % (model, thread_id))
|
self._routing_warn(_('reply to missing document (%s,%s), fall back on new document creation') % (model, thread_id), '', message_id, route, False)
|
||||||
|
thread_id = None
|
||||||
|
elif not hasattr(record_set, 'message_update') and create_fallback:
|
||||||
|
self._routing_warn(_('model %s does not accept document update, fall back on document creation') % model, '', message_id, route, False)
|
||||||
thread_id = None
|
thread_id = None
|
||||||
elif assert_model:
|
|
||||||
# TDE FIXME: change assert to some real error
|
|
||||||
assert record_set.exists(), 'Routing: reply to missing document (%s,%s)' % (model, thread_id)
|
|
||||||
else:
|
|
||||||
_warn('reply to missing document (%s,%s), skipping' % (model, thread_id))
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Existing Document: check model accepts the mailgateway
|
if not record_set.exists():
|
||||||
if thread_id and model and not hasattr(record_set, 'message_update'):
|
self._routing_warn(_('reply to missing document (%s,%s)') % (model, thread_id), _('skipping'), message_id, route, assert_model)
|
||||||
if create_fallback:
|
return False
|
||||||
_warn('model %s does not accept document update, fall back on document creation' % model)
|
elif not hasattr(record_set, 'message_update'):
|
||||||
thread_id = None
|
self._routing_warn(_('model %s does not accept document update') % model, _('skipping'), message_id, route, assert_model)
|
||||||
elif assert_model:
|
|
||||||
assert hasattr(record_set, 'message_update'), 'Routing: model %s does not accept document update, crashing' % model
|
|
||||||
else:
|
|
||||||
_warn('model %s does not accept document update, skipping' % model)
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# New Document: check model accepts the mailgateway
|
# New Document: check model accepts the mailgateway
|
||||||
if not thread_id and model and not hasattr(record_set, 'message_new'):
|
if not thread_id and model and not hasattr(record_set, 'message_new'):
|
||||||
if assert_model:
|
self._routing_warn(_('model %s does not accept document creation') % model, _('skipping'), message_id, route, assert_model)
|
||||||
if not hasattr(record_set, 'message_new'):
|
|
||||||
raise ValueError(
|
|
||||||
'Model %s does not accept document creation, crashing' % model
|
|
||||||
)
|
|
||||||
_warn('model %s does not accept document creation, skipping' % model)
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Update message author if asked
|
# Update message author if asked. We do it now because we need it for aliases (contact settings)
|
||||||
# We do it now because we need it for aliases (contact settings)
|
|
||||||
if not author_id and update_author:
|
if not author_id and update_author:
|
||||||
author_ids = self.env['mail.thread']._find_partner_from_emails([email_from], res_model=model, res_id=thread_id)
|
author_ids = self.env['mail.thread']._find_partner_from_emails([email_from], res_model=model, res_id=thread_id)
|
||||||
if author_ids:
|
if author_ids:
|
||||||
@@ -914,140 +921,132 @@ class MailThread(models.AbstractModel):
|
|||||||
set(partner.id for channel in obj.message_channel_ids for partner in channel.channel_partner_ids)
|
set(partner.id for channel in obj.message_channel_ids for partner in channel.channel_partner_ids)
|
||||||
)
|
)
|
||||||
if not author_id or author_id not in accepted_partner_ids:
|
if not author_id or author_id not in accepted_partner_ids:
|
||||||
_warn('alias %s restricted to internal followers, skipping' % alias.alias_name)
|
self._routing_warn(_('alias %s restricted to internal followers') % alias.alias_name, _('skipping'), message_id, route, False)
|
||||||
_create_bounce_email()
|
self._routing_create_bounce_email(email_from, _generic_bounce_body_html, message)
|
||||||
return False
|
return False
|
||||||
elif alias and alias.alias_contact == 'partners' and not author_id:
|
elif alias and alias.alias_contact == 'partners' and not author_id:
|
||||||
_warn('alias %s does not accept unknown author, skipping' % alias.alias_name)
|
self._routing_warn(_('alias %s does not accept unknown author') % alias.alias_name, _('skipping'), message_id, route, False)
|
||||||
_create_bounce_email()
|
self._routing_create_bounce_email(email_from, _generic_bounce_body_html, message)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if not model and not thread_id and not alias and not allow_private:
|
if not model and not thread_id and not alias and not allow_private:
|
||||||
return ()
|
return False
|
||||||
|
|
||||||
return (model, thread_id, route[2], route[3], None if self._context.get('drop_alias', False) else route[4])
|
return (model, thread_id, route[2], route[3], None if drop_alias else route[4])
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def message_route(self, message, message_dict, model=None, thread_id=None, custom_values=None):
|
def message_route(self, message, message_dict, model=None, thread_id=None, custom_values=None):
|
||||||
"""Attempt to figure out the correct target model, thread_id,
|
""" Attempt to figure out the correct target model, thread_id,
|
||||||
custom_values and user_id to use for an incoming message.
|
custom_values and user_id to use for an incoming message.
|
||||||
Multiple values may be returned, if a message had multiple
|
Multiple values may be returned, if a message had multiple
|
||||||
recipients matching existing mail.aliases, for example.
|
recipients matching existing mail.aliases, for example.
|
||||||
|
|
||||||
The following heuristics are used, in this order:
|
The following heuristics are used, in this order:
|
||||||
1. If the message replies to an existing thread_id, and
|
|
||||||
properly contains the thread model in the 'In-Reply-To'
|
|
||||||
header, use this model/thread_id pair, and ignore
|
|
||||||
custom_value (not needed as no creation will take place)
|
|
||||||
2. Look for a mail.alias entry matching the message
|
|
||||||
recipient, and use the corresponding model, thread_id,
|
|
||||||
custom_values and user_id.
|
|
||||||
3. Fallback to the ``model``, ``thread_id`` and ``custom_values``
|
|
||||||
provided.
|
|
||||||
4. If all the above fails, raise an exception.
|
|
||||||
|
|
||||||
:param string message: an email.message instance
|
* if the message replies to an existing thread by having a Message-Id
|
||||||
:param dict message_dict: dictionary holding message variables
|
that matches an existing mail_message.message_id, we take the original
|
||||||
:param string model: the fallback model to use if the message
|
message model/thread_id pair and ignore custom_value as no creation will
|
||||||
does not match any of the currently configured mail aliases
|
take place
|
||||||
(may be None if a matching alias is supposed to be present)
|
* if the message replies to an existing thread by having In-Reply-To or
|
||||||
:type dict custom_values: optional dictionary of default field values
|
References matching odoo model/thread_id Message-Id and if this thread
|
||||||
to pass to ``message_new`` if a new record needs to be created.
|
has messages without message_id, take this model/thread_id pair and
|
||||||
Ignored if the thread record already exists, and also if a
|
ignore custom_value as no creation will take place (6.1 compatibility)
|
||||||
matching mail.alias was found (aliases define their own defaults)
|
* look for a mail.alias entry matching the message recipients and use the
|
||||||
:param int thread_id: optional ID of the record/thread from ``model``
|
corresponding model, thread_id, custom_values and user_id. This could
|
||||||
to which this mail should be attached. Only used if the message
|
lead to a thread update or creation depending on the alias
|
||||||
does not reply to an existing thread and does not match any mail alias.
|
* fallback on provided ``model``, ``thread_id`` and ``custom_values``
|
||||||
:return: list of [model, thread_id, custom_values, user_id, alias]
|
* raise an exception as no route has been found
|
||||||
|
|
||||||
|
:param string message: an email.message instance
|
||||||
|
:param dict message_dict: dictionary holding parsed message variables
|
||||||
|
:param string model: the fallback model to use if the message does not match
|
||||||
|
any of the currently configured mail aliases (may be None if a matching
|
||||||
|
alias is supposed to be present)
|
||||||
|
:type dict custom_values: optional dictionary of default field values
|
||||||
|
to pass to ``message_new`` if a new record needs to be created.
|
||||||
|
Ignored if the thread record already exists, and also if a matching
|
||||||
|
mail.alias was found (aliases define their own defaults)
|
||||||
|
:param int thread_id: optional ID of the record/thread from ``model`` to
|
||||||
|
which this mail should be attached. Only used if the message does not
|
||||||
|
reply to an existing thread and does not match any mail alias.
|
||||||
|
:return: list of routes [(model, thread_id, custom_values, user_id, alias)]
|
||||||
|
|
||||||
:raises: ValueError, TypeError
|
:raises: ValueError, TypeError
|
||||||
"""
|
"""
|
||||||
if not isinstance(message, Message):
|
if not isinstance(message, Message):
|
||||||
raise TypeError('message must be an email.message.Message at this point')
|
raise TypeError('message must be an email.message.Message at this point')
|
||||||
MailMessage = self.env['mail.message']
|
MailMessage = self.env['mail.message']
|
||||||
Alias = self.env['mail.alias']
|
Alias, dest_aliases = self.env['mail.alias'], self.env['mail.alias']
|
||||||
fallback_model = model
|
fallback_model = model
|
||||||
|
|
||||||
# Get email.message.Message variables for future processing
|
# get email.message.Message variables for future processing
|
||||||
|
local_hostname = socket.gethostname()
|
||||||
message_id = message.get('Message-Id')
|
message_id = message.get('Message-Id')
|
||||||
email_from = tools.decode_message_header(message, 'From')
|
|
||||||
email_to = tools.decode_message_header(message, 'To')
|
# compute references to find if message is a reply to an existing thread
|
||||||
references = tools.decode_message_header(message, 'References')
|
references = tools.decode_message_header(message, 'References')
|
||||||
in_reply_to = tools.decode_message_header(message, 'In-Reply-To').strip()
|
in_reply_to = tools.decode_message_header(message, 'In-Reply-To').strip()
|
||||||
thread_references = references or in_reply_to
|
thread_references = references or in_reply_to
|
||||||
|
reply_match, reply_model, reply_thread_id, reply_hostname, reply_private = tools.email_references(thread_references)
|
||||||
|
|
||||||
|
# author and recipients
|
||||||
|
email_from = tools.decode_message_header(message, 'From')
|
||||||
|
email_from_localpart = (tools.email_split(email_from) or [''])[0].split('@', 1)[0].lower()
|
||||||
|
email_to = tools.decode_message_header(message, 'To')
|
||||||
|
email_to_localpart = (tools.email_split(email_to) or [''])[0].split('@', 1)[0].lower()
|
||||||
|
|
||||||
|
# Delivered-To is a safe bet in most modern MTAs, but we have to fallback on To + Cc values
|
||||||
|
# for all the odd MTAs out there, as there is no standard header for the envelope's `rcpt_to` value.
|
||||||
|
rcpt_tos = ','.join([
|
||||||
|
tools.decode_message_header(message, 'Delivered-To'),
|
||||||
|
tools.decode_message_header(message, 'To'),
|
||||||
|
tools.decode_message_header(message, 'Cc'),
|
||||||
|
tools.decode_message_header(message, 'Resent-To'),
|
||||||
|
tools.decode_message_header(message, 'Resent-Cc')])
|
||||||
|
rcpt_tos_localparts = [e.split('@')[0] for e in tools.email_split(rcpt_tos)]
|
||||||
|
|
||||||
# 0. First check if this is a bounce message or not.
|
# 0. First check if this is a bounce message or not.
|
||||||
# See http://datatracker.ietf.org/doc/rfc3462/?include_text=1
|
# See http://datatracker.ietf.org/doc/rfc3462/?include_text=1
|
||||||
# As all MTA does not respect this RFC (googlemail is one of them),
|
# As all MTA does not respect this RFC (googlemail is one of them),
|
||||||
# we also need to verify if the message come from "mailer-daemon"
|
# we also need to verify if the message come from "mailer-daemon"
|
||||||
localpart = (tools.email_split(email_from) or [''])[0].split('@', 1)[0].lower()
|
if message.get_content_type() == 'multipart/report' or email_from_localpart == 'mailer-daemon':
|
||||||
if message.get_content_type() == 'multipart/report' or localpart == 'mailer-daemon':
|
_logger.info('Routing mail with Message-Id %s: not routing bounce email from %s to %s',
|
||||||
_logger.info("Not routing bounce email from %s to %s with Message-Id %s",
|
message_id, email_from, email_to)
|
||||||
email_from, email_to, message_id)
|
|
||||||
return []
|
return []
|
||||||
|
|
||||||
# 1. message is a reply to an existing message (exact match of message_id)
|
# 1. message is a reply to an existing message (exact match of message_id or compat-mode)
|
||||||
ref_match, reply_model, reply_thread_id, reply_hostname = tools.email_references(thread_references)
|
if reply_match:
|
||||||
msg_references = tools.mail_header_msgid_re.findall(thread_references)
|
compat_mode = False
|
||||||
mail_messages = MailMessage.sudo().search([('message_id', 'in', msg_references)], limit=1)
|
msg_references = tools.mail_header_msgid_re.findall(thread_references)
|
||||||
if ref_match and mail_messages:
|
mail_messages = MailMessage.sudo().search([('message_id', 'in', msg_references)], limit=1)
|
||||||
model, thread_id = mail_messages.model, mail_messages.res_id
|
|
||||||
alias = Alias.search([('alias_name', '=', (tools.email_split(email_to) or [''])[0].split('@', 1)[0].lower())])
|
|
||||||
alias = alias[0] if alias else None
|
|
||||||
route = self.with_context(drop_alias=True).message_route_verify(
|
|
||||||
message, message_dict,
|
|
||||||
(model, thread_id, custom_values, self._uid, alias),
|
|
||||||
update_author=True, assert_model=False, create_fallback=True)
|
|
||||||
if route:
|
|
||||||
_logger.info(
|
|
||||||
'Routing mail from %s to %s with Message-Id %s: direct reply to msg: model: %s, thread_id: %s, custom_values: %s, uid: %s',
|
|
||||||
email_from, email_to, message_id, model, thread_id, custom_values, self._uid)
|
|
||||||
return [route]
|
|
||||||
elif route is False:
|
|
||||||
return []
|
|
||||||
|
|
||||||
# 2. message is a reply to an existign thread (6.1 compatibility)
|
# message is a reply to an existing thread (6.1 compatibility)
|
||||||
if ref_match:
|
|
||||||
local_hostname = socket.gethostname()
|
|
||||||
# do not match forwarded emails from another OpenERP system (thread_id collision!)
|
# do not match forwarded emails from another OpenERP system (thread_id collision!)
|
||||||
if local_hostname == reply_hostname:
|
if not mail_messages and local_hostname == reply_hostname and reply_thread_id and reply_model in self.env:
|
||||||
thread_id, model = reply_thread_id, reply_model
|
mail_messages = MailMessage.search([
|
||||||
if thread_id and model in self.env:
|
('message_id', '=', False),
|
||||||
record = self.env[model].browse(thread_id)
|
('model', '=', reply_model),
|
||||||
compat_mail_msg_ids = MailMessage.search([
|
('res_id', '=', reply_thread_id)])
|
||||||
('message_id', '=', False),
|
compat_mode = True
|
||||||
('model', '=', model),
|
|
||||||
('res_id', '=', thread_id)])
|
|
||||||
if compat_mail_msg_ids and record.exists() and hasattr(record, 'message_update'):
|
|
||||||
route = self.message_route_verify(
|
|
||||||
message, message_dict,
|
|
||||||
(model, thread_id, custom_values, self._uid, None),
|
|
||||||
update_author=True, assert_model=True, create_fallback=True)
|
|
||||||
if route:
|
|
||||||
# parent is invalid for a compat-reply
|
|
||||||
message_dict.pop('parent_id', None)
|
|
||||||
_logger.info(
|
|
||||||
'Routing mail from %s to %s with Message-Id %s: direct thread reply (compat-mode) to model: %s, thread_id: %s, custom_values: %s, uid: %s',
|
|
||||||
email_from, email_to, message_id, model, thread_id, custom_values, self._uid)
|
|
||||||
return [route]
|
|
||||||
elif route is False:
|
|
||||||
return []
|
|
||||||
|
|
||||||
# 3. Reply to a private message
|
if mail_messages:
|
||||||
if in_reply_to:
|
model, thread_id = mail_messages.model, mail_messages.res_id
|
||||||
mail_message_ids = MailMessage.search([
|
if not reply_private: # TDE note: not sure why private mode as no alias search, copying existing behavior
|
||||||
('message_id', '=', in_reply_to),
|
dest_aliases = Alias.search([('alias_name', '=', email_to_localpart)], limit=1)
|
||||||
'!', ('message_id', 'ilike', 'reply_to')
|
|
||||||
], limit=1)
|
# TDE Note: compat mode = without context key, why ? because
|
||||||
if mail_message_ids:
|
|
||||||
route = self.message_route_verify(
|
route = self.message_route_verify(
|
||||||
message, message_dict,
|
message, message_dict,
|
||||||
(mail_message_ids.model, mail_message_ids.res_id, custom_values, self._uid, None),
|
(model, thread_id, custom_values, self._uid, dest_aliases),
|
||||||
update_author=True, assert_model=True, create_fallback=True, allow_private=True)
|
update_author=True, assert_model=reply_private, create_fallback=True,
|
||||||
|
allow_private=reply_private, drop_alias=True)
|
||||||
if route:
|
if route:
|
||||||
|
# TDE Note: compat mode: parent is invalid for a compat-reply
|
||||||
|
# message_dict.pop('parent_id', None)
|
||||||
|
# TDE note: add compat mode for compat mode in debug
|
||||||
_logger.info(
|
_logger.info(
|
||||||
'Routing mail from %s to %s with Message-Id %s: direct reply to a private message: %s, custom_values: %s, uid: %s',
|
'Routing mail from %s to %s with Message-Id %s%s: direct reply to msg: model: %s, thread_id: %s, custom_values: %s, uid: %s',
|
||||||
email_from, email_to, message_id, mail_message_ids.id, custom_values, self._uid)
|
email_from, email_to, message_id, compat_mode and ' (compat mode)' or '', model, thread_id, custom_values, self._uid)
|
||||||
return [route]
|
return [route]
|
||||||
elif route is False:
|
elif route is False:
|
||||||
return []
|
return []
|
||||||
@@ -1056,20 +1055,11 @@ class MailThread(models.AbstractModel):
|
|||||||
message_dict.pop('parent_id', None)
|
message_dict.pop('parent_id', None)
|
||||||
|
|
||||||
# 4. Look for a matching mail.alias entry
|
# 4. Look for a matching mail.alias entry
|
||||||
# Delivered-To is a safe bet in most modern MTAs, but we have to fallback on To + Cc values
|
if rcpt_tos_localparts:
|
||||||
# for all the odd MTAs out there, as there is no standard header for the envelope's `rcpt_to` value.
|
dest_aliases = Alias.search([('alias_name', 'in', rcpt_tos_localparts)])
|
||||||
rcpt_tos = \
|
if dest_aliases:
|
||||||
','.join([tools.decode_message_header(message, 'Delivered-To'),
|
|
||||||
tools.decode_message_header(message, 'To'),
|
|
||||||
tools.decode_message_header(message, 'Cc'),
|
|
||||||
tools.decode_message_header(message, 'Resent-To'),
|
|
||||||
tools.decode_message_header(message, 'Resent-Cc')])
|
|
||||||
local_parts = [e.split('@')[0] for e in tools.email_split(rcpt_tos)]
|
|
||||||
if local_parts:
|
|
||||||
aliases = Alias.search([('alias_name', 'in', local_parts)])
|
|
||||||
if aliases:
|
|
||||||
routes = []
|
routes = []
|
||||||
for alias in aliases:
|
for alias in dest_aliases:
|
||||||
user_id = alias.alias_user_id.id
|
user_id = alias.alias_user_id.id
|
||||||
if not user_id:
|
if not user_id:
|
||||||
# TDE note: this could cause crashes, because no clue that the user
|
# TDE note: this could cause crashes, because no clue that the user
|
||||||
@@ -1100,15 +1090,16 @@ class MailThread(models.AbstractModel):
|
|||||||
thread_id = int(thread_id)
|
thread_id = int(thread_id)
|
||||||
except:
|
except:
|
||||||
thread_id = False
|
thread_id = False
|
||||||
route = self.message_route_verify(
|
if fallback_model:
|
||||||
message, message_dict,
|
route = self.message_route_verify(
|
||||||
(fallback_model, thread_id, custom_values, self._uid, None),
|
message, message_dict,
|
||||||
update_author=True, assert_model=True)
|
(fallback_model, thread_id, custom_values, self._uid, None),
|
||||||
if route:
|
update_author=True, assert_model=True)
|
||||||
_logger.info(
|
if route:
|
||||||
'Routing mail from %s to %s with Message-Id %s: fallback to model:%s, thread_id:%s, custom_values:%s, uid:%s',
|
_logger.info(
|
||||||
email_from, email_to, message_id, fallback_model, thread_id, custom_values, self._uid)
|
'Routing mail from %s to %s with Message-Id %s: fallback to model:%s, thread_id:%s, custom_values:%s, uid:%s',
|
||||||
return [route]
|
email_from, email_to, message_id, fallback_model, thread_id, custom_values, self._uid)
|
||||||
|
return [route]
|
||||||
|
|
||||||
# ValueError if no routes found and if no bounce occured
|
# ValueError if no routes found and if no bounce occured
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
|
|||||||
@@ -413,6 +413,7 @@ command_re = re.compile("^Set-([a-z]+) *: *(.+)$", re.I + re.UNICODE)
|
|||||||
# Typical form of references is <timestamp-openerp-record_id-model_name@domain>
|
# Typical form of references is <timestamp-openerp-record_id-model_name@domain>
|
||||||
# group(1) = the record ID ; group(2) = the model (if any) ; group(3) = the domain
|
# group(1) = the record ID ; group(2) = the model (if any) ; group(3) = the domain
|
||||||
reference_re = re.compile("<.*-open(?:object|erp)-(\\d+)(?:-([\w.]+))?[^>]*@([^>]*)>", re.UNICODE)
|
reference_re = re.compile("<.*-open(?:object|erp)-(\\d+)(?:-([\w.]+))?[^>]*@([^>]*)>", re.UNICODE)
|
||||||
|
discussion_re = re.compile("<.*-open(?:object|erp)-private[^>]*@([^>]*)>", re.UNICODE)
|
||||||
|
|
||||||
mail_header_msgid_re = re.compile('<[^<>]+>')
|
mail_header_msgid_re = re.compile('<[^<>]+>')
|
||||||
|
|
||||||
@@ -494,14 +495,18 @@ def email_split_and_format(text):
|
|||||||
if '@' in addr[1]]
|
if '@' in addr[1]]
|
||||||
|
|
||||||
def email_references(references):
|
def email_references(references):
|
||||||
ref_match, model, thread_id, hostname = False, False, False, False
|
ref_match, model, thread_id, hostname, is_private = False, False, False, False, False
|
||||||
if references:
|
if references:
|
||||||
ref_match = reference_re.search(references)
|
ref_match = reference_re.search(references)
|
||||||
if ref_match:
|
if ref_match:
|
||||||
model = ref_match.group(2)
|
model = ref_match.group(2)
|
||||||
thread_id = int(ref_match.group(1))
|
thread_id = int(ref_match.group(1))
|
||||||
hostname = ref_match.group(3)
|
hostname = ref_match.group(3)
|
||||||
return (ref_match, model, thread_id, hostname)
|
else:
|
||||||
|
ref_match = discussion_re.search(references)
|
||||||
|
if ref_match:
|
||||||
|
is_private = True
|
||||||
|
return (ref_match, model, thread_id, hostname, is_private)
|
||||||
|
|
||||||
# was mail_message.decode()
|
# was mail_message.decode()
|
||||||
def decode_smtp_header(smtp_header):
|
def decode_smtp_header(smtp_header):
|
||||||
|
|||||||
Reference in New Issue
Block a user