From 0028c42136f02fcb7a9a81ed5763396290ffdcb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 17 Aug 2016 13:45:57 +0200 Subject: [PATCH] [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. --- addons/hr/models/mail_thread.py | 39 ++-- addons/mail/models/mail_thread.py | 349 +++++++++++++++--------------- openerp/tools/mail.py | 9 +- 3 files changed, 191 insertions(+), 206 deletions(-) diff --git a/addons/hr/models/mail_thread.py b/addons/hr/models/mail_thread.py index 2e756883856..06eec55240d 100644 --- a/addons/hr/models/mail_thread.py +++ b/addons/hr/models/mail_thread.py @@ -3,7 +3,7 @@ import logging -from odoo import api, models +from odoo import api, models, _ from odoo.tools import decode_message_header, email_split _logger = logging.getLogger(__name__) @@ -13,35 +13,24 @@ class MailThread(models.AbstractModel): _inherit = "mail.thread" @api.model - def message_route_verify(self, message, message_dict, route, update_author=True, assert_model=True, create_fallback=True, allow_private=False): - res = super(MailThread, self).message_route_verify(message, message_dict, route, update_author, assert_model, create_fallback, allow_private) + def message_route_verify(self, message, message_dict, route, + 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: alias = route[4] email_from = decode_message_header(message, 'From') 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 - if alias and alias.alias_contact == 'employees': email_address = email_split(email_from)[0] 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) if not employee: 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) - _create_bounce_email(mail_template.body_html) + self._routing_warn(_('alias %s does not accept unknown employees') % alias.alias_name, _('skipping'), message_id, route, False) + self._routing_create_bounce_email(email_from, mail_template.body_html, message) return False return res diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index 194d7dc25e4..fe15cc9c466 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -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)]) 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 - 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: 1 - if thread_id -> check that document effectively exists; otherwise 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 followers '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' @@ -815,88 +855,55 @@ class MailThread(models.AbstractModel): model, thread_id, alias = route[0], route[1], route[4] record_set = None - def _create_bounce_email(): - bounce_to = tools.decode_smtp_header(message.get('Return-Path')) or email_from - bounce_mail_values = { - 'body_html': '

Hello,

' - '

The following email sent to %s cannot be accepted because this is ' - 'a private email address. Only allowed people can contact us at this address.

' - '
%s
' % (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) + _generic_bounce_body_html = """
+

Hello,

+

The following email sent to %s cannot be accepted because this is a private email address. + Only allowed people can contact us at this address.

+
%s
""" % (message.get('to'), message_dict.get('body')), # Wrong model if model and model not in self.env: - if assert_model: - assert model in self.env, 'Routing: unknown target model %s' % model - _warn('unknown target model %s' % model) + self._routing_warn(_('unknown target model %s') % model, '', message_id, route, assert_model) return () - # Private message: should not contain any thread_id - if not model and thread_id: - if assert_model: - if thread_id: - raise ValueError('Routing: posting a message without model should be with a null res_id (private message), received %s.' % thread_id) - _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 - # Private message: should have a parent_id (only answers) - if not model and not message_dict.get('parent_id'): - if assert_model: - 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 + # Private message + if not model: + # should not contain any thread_id + if 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) + thread_id = 0 + # should have a parent_id (only answers) + if 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) + return False if model and thread_id: record_set = self.env[model].browse(thread_id) elif model: record_set = self.env[model] - # Existing Document: check if exists; if not, fallback on create if allowed - if thread_id and not record_set.exists(): - if create_fallback: - _warn('reply to missing document (%s,%s), fall back on new document creation' % (model, thread_id)) + # Existing Document: check if exists and model accepts the mailgateway; if not, fallback on create if allowed + if thread_id: + if not record_set.exists() and create_fallback: + 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 - 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 thread_id and model and not hasattr(record_set, 'message_update'): - if create_fallback: - _warn('model %s does not accept document update, fall back on document creation' % model) - thread_id = None - 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) + if not record_set.exists(): + self._routing_warn(_('reply to missing document (%s,%s)') % (model, thread_id), _('skipping'), message_id, route, assert_model) + return False + elif not hasattr(record_set, 'message_update'): + self._routing_warn(_('model %s does not accept document update') % model, _('skipping'), message_id, route, assert_model) return False # New Document: check model accepts the mailgateway if not thread_id and model and not hasattr(record_set, 'message_new'): - if 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) + self._routing_warn(_('model %s does not accept document creation') % model, _('skipping'), message_id, route, assert_model) return False - # Update message author if asked - # We do it now because we need it for aliases (contact settings) + # Update message author if asked. We do it now because we need it for aliases (contact settings) 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) 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) ) if not author_id or author_id not in accepted_partner_ids: - _warn('alias %s restricted to internal followers, skipping' % alias.alias_name) - _create_bounce_email() + self._routing_warn(_('alias %s restricted to internal followers') % alias.alias_name, _('skipping'), message_id, route, False) + self._routing_create_bounce_email(email_from, _generic_bounce_body_html, message) return False elif alias and alias.alias_contact == 'partners' and not author_id: - _warn('alias %s does not accept unknown author, skipping' % alias.alias_name) - _create_bounce_email() + self._routing_warn(_('alias %s does not accept unknown author') % alias.alias_name, _('skipping'), message_id, route, False) + self._routing_create_bounce_email(email_from, _generic_bounce_body_html, message) return False 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 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. Multiple values may be returned, if a message had multiple recipients matching existing mail.aliases, for example. 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 - :param dict message_dict: dictionary holding 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 [model, thread_id, custom_values, user_id, alias] + * if the message replies to an existing thread by having a Message-Id + that matches an existing mail_message.message_id, we take the original + message model/thread_id pair and ignore custom_value as no creation will + take place + * if the message replies to an existing thread by having In-Reply-To or + References matching odoo model/thread_id Message-Id and if this thread + has messages without message_id, take this model/thread_id pair and + ignore custom_value as no creation will take place (6.1 compatibility) + * look for a mail.alias entry matching the message recipients and use the + corresponding model, thread_id, custom_values and user_id. This could + lead to a thread update or creation depending on the alias + * fallback on provided ``model``, ``thread_id`` and ``custom_values`` + * 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 """ if not isinstance(message, Message): raise TypeError('message must be an email.message.Message at this point') MailMessage = self.env['mail.message'] - Alias = self.env['mail.alias'] + Alias, dest_aliases = self.env['mail.alias'], self.env['mail.alias'] 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') - 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') in_reply_to = tools.decode_message_header(message, 'In-Reply-To').strip() 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. # See http://datatracker.ietf.org/doc/rfc3462/?include_text=1 # 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" - localpart = (tools.email_split(email_from) or [''])[0].split('@', 1)[0].lower() - if message.get_content_type() == 'multipart/report' or localpart == 'mailer-daemon': - _logger.info("Not routing bounce email from %s to %s with Message-Id %s", - email_from, email_to, message_id) + if message.get_content_type() == 'multipart/report' or email_from_localpart == 'mailer-daemon': + _logger.info('Routing mail with Message-Id %s: not routing bounce email from %s to %s', + message_id, email_from, email_to) return [] - # 1. message is a reply to an existing message (exact match of message_id) - ref_match, reply_model, reply_thread_id, reply_hostname = tools.email_references(thread_references) - msg_references = tools.mail_header_msgid_re.findall(thread_references) - mail_messages = MailMessage.sudo().search([('message_id', 'in', msg_references)], limit=1) - if ref_match and mail_messages: - 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 [] + # 1. message is a reply to an existing message (exact match of message_id or compat-mode) + if reply_match: + compat_mode = False + msg_references = tools.mail_header_msgid_re.findall(thread_references) + mail_messages = MailMessage.sudo().search([('message_id', 'in', msg_references)], limit=1) - # 2. message is a reply to an existign thread (6.1 compatibility) - if ref_match: - local_hostname = socket.gethostname() + # message is a reply to an existing thread (6.1 compatibility) # do not match forwarded emails from another OpenERP system (thread_id collision!) - if local_hostname == reply_hostname: - thread_id, model = reply_thread_id, reply_model - if thread_id and model in self.env: - record = self.env[model].browse(thread_id) - compat_mail_msg_ids = MailMessage.search([ - ('message_id', '=', False), - ('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 [] + if not mail_messages and local_hostname == reply_hostname and reply_thread_id and reply_model in self.env: + mail_messages = MailMessage.search([ + ('message_id', '=', False), + ('model', '=', reply_model), + ('res_id', '=', reply_thread_id)]) + compat_mode = True - # 3. Reply to a private message - if in_reply_to: - mail_message_ids = MailMessage.search([ - ('message_id', '=', in_reply_to), - '!', ('message_id', 'ilike', 'reply_to') - ], limit=1) - if mail_message_ids: + if mail_messages: + model, thread_id = mail_messages.model, mail_messages.res_id + if not reply_private: # TDE note: not sure why private mode as no alias search, copying existing behavior + dest_aliases = Alias.search([('alias_name', '=', email_to_localpart)], limit=1) + + # TDE Note: compat mode = without context key, why ? because route = self.message_route_verify( message, message_dict, - (mail_message_ids.model, mail_message_ids.res_id, custom_values, self._uid, None), - update_author=True, assert_model=True, create_fallback=True, allow_private=True) + (model, thread_id, custom_values, self._uid, dest_aliases), + update_author=True, assert_model=reply_private, create_fallback=True, + allow_private=reply_private, drop_alias=True) 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( - 'Routing mail from %s to %s with Message-Id %s: direct reply to a private message: %s, custom_values: %s, uid: %s', - email_from, email_to, message_id, mail_message_ids.id, custom_values, self._uid) + '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, compat_mode and ' (compat mode)' or '', model, thread_id, custom_values, self._uid) return [route] elif route is False: return [] @@ -1056,20 +1055,11 @@ class MailThread(models.AbstractModel): message_dict.pop('parent_id', None) # 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 - # 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')]) - 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: + if rcpt_tos_localparts: + dest_aliases = Alias.search([('alias_name', 'in', rcpt_tos_localparts)]) + if dest_aliases: routes = [] - for alias in aliases: + for alias in dest_aliases: user_id = alias.alias_user_id.id if not user_id: # 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) except: thread_id = False - route = self.message_route_verify( - message, message_dict, - (fallback_model, thread_id, custom_values, self._uid, None), - update_author=True, assert_model=True) - if route: - _logger.info( - 'Routing mail from %s to %s with Message-Id %s: fallback to model:%s, thread_id:%s, custom_values:%s, uid:%s', - email_from, email_to, message_id, fallback_model, thread_id, custom_values, self._uid) - return [route] + if fallback_model: + route = self.message_route_verify( + message, message_dict, + (fallback_model, thread_id, custom_values, self._uid, None), + update_author=True, assert_model=True) + if route: + _logger.info( + 'Routing mail from %s to %s with Message-Id %s: fallback to model:%s, thread_id:%s, custom_values:%s, uid:%s', + 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 raise ValueError( diff --git a/openerp/tools/mail.py b/openerp/tools/mail.py index 3c9953677fd..9646e4fde74 100644 --- a/openerp/tools/mail.py +++ b/openerp/tools/mail.py @@ -413,6 +413,7 @@ command_re = re.compile("^Set-([a-z]+) *: *(.+)$", re.I + re.UNICODE) # Typical form of references is # 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) +discussion_re = re.compile("<.*-open(?:object|erp)-private[^>]*@([^>]*)>", re.UNICODE) mail_header_msgid_re = re.compile('<[^<>]+>') @@ -494,14 +495,18 @@ def email_split_and_format(text): if '@' in addr[1]] 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: ref_match = reference_re.search(references) if ref_match: model = ref_match.group(2) thread_id = int(ref_match.group(1)) 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() def decode_smtp_header(smtp_header):