[CLEAN] mail, tools: factorize decode email tools
mail.mail and mail.thread have methods to decode headers. This commit move them in mail tools. It is indeed simplier to have all email tools in the same file. Moreover they have been renamed to avoid confusion with standard library methods: decode and decode_header already exist. * decode in mail_message is now decode_smtp_header * decode_header in mail_thread is now decode_message_header A method to data from references is also moved into tools. A regex was already defined in tools. Now the method using it is already put in tools. Mailgateway will now simply call this method.
This commit is contained in:
@@ -4,11 +4,11 @@
|
||||
import logging
|
||||
|
||||
from odoo import api, models
|
||||
from odoo.addons.mail.models.mail_thread import decode_header
|
||||
from odoo.tools import email_split
|
||||
from odoo.tools import decode_message_header, email_split
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MailThread(models.AbstractModel):
|
||||
_inherit = "mail.thread"
|
||||
|
||||
@@ -18,13 +18,13 @@ class MailThread(models.AbstractModel):
|
||||
|
||||
if res:
|
||||
alias = route[4]
|
||||
email_from = decode_header(message, 'From')
|
||||
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_header(message, 'Return-Path') or email_from
|
||||
bounce_to = decode_message_header(message, 'Return-Path') or email_from
|
||||
bounce_mail_values = {
|
||||
'body_html': body_html,
|
||||
'subject': 'Re: %s' % message.get('subject'),
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
import logging
|
||||
|
||||
from email.header import decode_header
|
||||
from email.utils import formataddr
|
||||
|
||||
from odoo import _, api, fields, models, SUPERUSER_ID, tools
|
||||
@@ -14,16 +13,6 @@ from odoo.osv import expression
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def decode(text):
|
||||
"""Returns unicode() string conversion of the the given encoded smtp header text"""
|
||||
# TDE proposal: move to tools ?
|
||||
if text:
|
||||
text = decode_header(text.replace('\r', ''))
|
||||
# The joining space will not be needed as of Python 3.3
|
||||
# See https://hg.python.org/cpython/rev/8c03fe231877
|
||||
return ' '.join([tools.ustr(x[0], x[1]) for x in text])
|
||||
|
||||
|
||||
class Message(models.Model):
|
||||
""" Messages model: system notification (replacing res.log notifications),
|
||||
comments (OpenChatter discussion) and incoming emails. """
|
||||
|
||||
@@ -9,7 +9,6 @@ import json
|
||||
import lxml
|
||||
import logging
|
||||
import pytz
|
||||
import re
|
||||
import socket
|
||||
import time
|
||||
import xmlrpclib
|
||||
@@ -21,20 +20,12 @@ from lxml import etree
|
||||
from werkzeug import url_encode
|
||||
|
||||
from odoo import _, api, exceptions, fields, models, tools
|
||||
from odoo.addons.mail.models.mail_message import decode
|
||||
from odoo.tools.safe_eval import safe_eval
|
||||
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
mail_header_msgid_re = re.compile('<[^<>]+>')
|
||||
|
||||
|
||||
def decode_header(message, header, separator=' '):
|
||||
return separator.join(map(decode, filter(None, message.get_all(header, []))))
|
||||
|
||||
|
||||
class MailThread(models.AbstractModel):
|
||||
''' mail_thread model is meant to be inherited by any model that needs to
|
||||
act as a discussion topic on which messages can be attached. Public
|
||||
@@ -795,7 +786,7 @@ class MailThread(models.AbstractModel):
|
||||
""" Find partners related to some header fields of the message.
|
||||
|
||||
:param string message: an email.message instance """
|
||||
s = ', '.join([decode(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)))
|
||||
|
||||
@api.model
|
||||
@@ -819,13 +810,13 @@ class MailThread(models.AbstractModel):
|
||||
assert len(route) == 5, 'A route should contain 5 elements: model, thread_id, custom_values, uid, alias record'
|
||||
|
||||
message_id = message.get('Message-Id')
|
||||
email_from = decode_header(message, 'From')
|
||||
email_from = tools.decode_message_header(message, 'From')
|
||||
author_id = message_dict.get('author_id')
|
||||
model, thread_id, alias = route[0], route[1], route[4]
|
||||
record_set = None
|
||||
|
||||
def _create_bounce_email():
|
||||
bounce_to = decode_header(message, 'Return-Path') or email_from
|
||||
bounce_to = tools.decode_smtp_header(message.get('Return-Path')) or email_from
|
||||
bounce_mail_values = {
|
||||
'body_html': '<div><p>Hello,</p>'
|
||||
'<p>The following email sent to %s cannot be accepted because this is '
|
||||
@@ -979,10 +970,10 @@ class MailThread(models.AbstractModel):
|
||||
|
||||
# Get email.message.Message variables for future processing
|
||||
message_id = message.get('Message-Id')
|
||||
email_from = decode_header(message, 'From')
|
||||
email_to = decode_header(message, 'To')
|
||||
references = decode_header(message, 'References')
|
||||
in_reply_to = decode_header(message, 'In-Reply-To').strip()
|
||||
email_from = tools.decode_message_header(message, 'From')
|
||||
email_to = tools.decode_message_header(message, 'To')
|
||||
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
|
||||
|
||||
# 0. First check if this is a bounce message or not.
|
||||
@@ -996,8 +987,8 @@ class MailThread(models.AbstractModel):
|
||||
return []
|
||||
|
||||
# 1. message is a reply to an existing message (exact match of message_id)
|
||||
ref_match = thread_references and tools.reference_re.search(thread_references)
|
||||
msg_references = mail_header_msgid_re.findall(thread_references)
|
||||
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
|
||||
@@ -1017,9 +1008,6 @@ class MailThread(models.AbstractModel):
|
||||
|
||||
# 2. message is a reply to an existign thread (6.1 compatibility)
|
||||
if ref_match:
|
||||
reply_thread_id = int(ref_match.group(1))
|
||||
reply_model = ref_match.group(2) or fallback_model
|
||||
reply_hostname = ref_match.group(3)
|
||||
local_hostname = socket.gethostname()
|
||||
# do not match forwarded emails from another OpenERP system (thread_id collision!)
|
||||
if local_hostname == reply_hostname:
|
||||
@@ -1071,11 +1059,11 @@ class MailThread(models.AbstractModel):
|
||||
# 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([decode_header(message, 'Delivered-To'),
|
||||
decode_header(message, 'To'),
|
||||
decode_header(message, 'Cc'),
|
||||
decode_header(message, 'Resent-To'),
|
||||
decode_header(message, 'Resent-Cc')])
|
||||
','.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)])
|
||||
@@ -1105,7 +1093,7 @@ class MailThread(models.AbstractModel):
|
||||
# 5. Fallback to the provided parameters, if they work
|
||||
if not thread_id:
|
||||
# Legacy: fallback to matching [ID] in the Subject
|
||||
match = tools.res_re.search(decode_header(message, 'Subject'))
|
||||
match = tools.res_re.search(tools.decode_message_header(message, 'Subject'))
|
||||
thread_id = match and match.group(1)
|
||||
# Convert into int (bug spotted in 7.0 because of str)
|
||||
try:
|
||||
@@ -1355,7 +1343,7 @@ class MailThread(models.AbstractModel):
|
||||
# RFC2231
|
||||
filename = email.utils.collapse_rfc2231_value(filename).strip()
|
||||
else:
|
||||
filename = decode(filename)
|
||||
filename = tools.decode_smtp_header(filename)
|
||||
encoding = part.get_content_charset() # None if attachment
|
||||
|
||||
# 0) Inline Attachments -> attachments, with a third part in the tuple to match cid / attachment
|
||||
@@ -1432,19 +1420,19 @@ class MailThread(models.AbstractModel):
|
||||
msg_dict['message_id'] = message_id
|
||||
|
||||
if message.get('Subject'):
|
||||
msg_dict['subject'] = decode(message.get('Subject'))
|
||||
msg_dict['subject'] = tools.decode_smtp_header(message.get('Subject'))
|
||||
|
||||
# Envelope fields not stored in mail.message but made available for message_new()
|
||||
msg_dict['from'] = decode(message.get('from'))
|
||||
msg_dict['to'] = decode(message.get('to'))
|
||||
msg_dict['cc'] = decode(message.get('cc'))
|
||||
msg_dict['email_from'] = decode(message.get('from'))
|
||||
msg_dict['from'] = tools.decode_smtp_header(message.get('from'))
|
||||
msg_dict['to'] = tools.decode_smtp_header(message.get('to'))
|
||||
msg_dict['cc'] = tools.decode_smtp_header(message.get('cc'))
|
||||
msg_dict['email_from'] = tools.decode_smtp_header(message.get('from'))
|
||||
partner_ids = self._message_find_partners(message, ['To', 'Cc'])
|
||||
msg_dict['partner_ids'] = [(4, partner_id) for partner_id in partner_ids]
|
||||
|
||||
if message.get('Date'):
|
||||
try:
|
||||
date_hdr = decode(message.get('Date'))
|
||||
date_hdr = tools.decode_smtp_header(message.get('Date'))
|
||||
parsed_date = dateutil.parser.parse(date_hdr, fuzzy=True)
|
||||
if parsed_date.utcoffset() is None:
|
||||
# naive datetime, so we arbitrarily decide to make it
|
||||
@@ -1461,13 +1449,13 @@ class MailThread(models.AbstractModel):
|
||||
msg_dict['date'] = stored_date.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
|
||||
|
||||
if message.get('In-Reply-To'):
|
||||
parent_ids = self.env['mail.message'].search([('message_id', '=', decode(message['In-Reply-To'].strip()))], limit=1)
|
||||
parent_ids = self.env['mail.message'].search([('message_id', '=', tools.decode_smtp_header(message['In-Reply-To'].strip()))], limit=1)
|
||||
if parent_ids:
|
||||
msg_dict['parent_id'] = parent_ids.id
|
||||
msg_dict['internal'] = parent_ids.subtype_id and parent_ids.subtype_id.internal or False
|
||||
|
||||
if message.get('References') and 'parent_id' not in msg_dict:
|
||||
msg_list = mail_header_msgid_re.findall(decode(message['References']))
|
||||
msg_list = tools.mail_header_msgid_re.findall(tools.decode_smtp_header(message['References']))
|
||||
parent_ids = self.env['mail.message'].search([('message_id', 'in', [x.strip() for x in msg_list])], limit=1)
|
||||
if parent_ids:
|
||||
msg_dict['parent_id'] = parent_ids.id
|
||||
|
||||
@@ -5,10 +5,7 @@ import logging
|
||||
import re
|
||||
|
||||
from odoo import api, models
|
||||
|
||||
from odoo.addons.mail.models.mail_message import decode
|
||||
from odoo.addons.mail.models.mail_thread import decode_header
|
||||
|
||||
from odoo.tools import decode_smtp_header, decode_message_header
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -24,8 +21,8 @@ class MailThread(models.AbstractModel):
|
||||
return False to end the routing process. """
|
||||
bounce_alias = self.env['ir.config_parameter'].get_param("mail.bounce.alias")
|
||||
message_id = message.get('Message-Id')
|
||||
email_from = decode_header(message, 'From')
|
||||
email_to = decode_header(message, 'To')
|
||||
email_from = decode_message_header(message, 'From')
|
||||
email_to = decode_message_header(message, 'To')
|
||||
|
||||
# 0. Verify whether this is a bounced email (wrong destination,...) -> use it to collect data, such as dead leads
|
||||
if bounce_alias and bounce_alias in email_to:
|
||||
@@ -70,6 +67,6 @@ class MailThread(models.AbstractModel):
|
||||
by using the References header of the incoming message and looking for
|
||||
matching message_id in mail.mail.statistics. """
|
||||
if message.get('References'):
|
||||
message_ids = [x.strip() for x in decode(message['References']).split()]
|
||||
message_ids = [x.strip() for x in decode_smtp_header(message['References']).split()]
|
||||
self.env['mail.mail.statistics'].set_replied(mail_message_ids=message_ids)
|
||||
return super(MailThread, self).message_route_process(message, message_dict, routes)
|
||||
|
||||
+35
-3
@@ -1,21 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from lxml import etree
|
||||
import cgi
|
||||
import logging
|
||||
import lxml.html
|
||||
import lxml.html.clean as clean
|
||||
import random
|
||||
import re
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
|
||||
from email.header import decode_header
|
||||
from email.utils import getaddresses, formataddr
|
||||
from lxml import etree
|
||||
|
||||
import openerp
|
||||
from openerp.loglevels import ustr
|
||||
from openerp.tools.translate import _
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -403,7 +403,10 @@ email_re = re.compile(r"""([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,63})""",
|
||||
# matches a string containing only one email
|
||||
single_email_re = re.compile(r"""^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,63}$""", re.VERBOSE)
|
||||
|
||||
# legacy behavior: match [ID] in the Subject, used with the fallback model of the mail gateway
|
||||
res_re = re.compile(r"\[([0-9]+)\]", re.UNICODE)
|
||||
|
||||
# update command in emails body
|
||||
command_re = re.compile("^Set-([a-z]+) *: *(.+)$", re.I + re.UNICODE)
|
||||
|
||||
# Updated in 7.0 to match the model name as well
|
||||
@@ -411,6 +414,8 @@ command_re = re.compile("^Set-([a-z]+) *: *(.+)$", re.I + re.UNICODE)
|
||||
# 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)
|
||||
|
||||
mail_header_msgid_re = re.compile('<[^<>]+>')
|
||||
|
||||
|
||||
def generate_tracking_message_id(res_id):
|
||||
"""Returns a string that can be used in the Message-ID RFC822 header field
|
||||
@@ -487,3 +492,30 @@ def email_split_and_format(text):
|
||||
# is strictly required in RFC2822's `addr-spec`.
|
||||
if addr[1]
|
||||
if '@' in addr[1]]
|
||||
|
||||
def email_references(references):
|
||||
ref_match, model, thread_id, hostname = 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)
|
||||
|
||||
# was mail_message.decode()
|
||||
def decode_smtp_header(smtp_header):
|
||||
"""Returns unicode() string conversion of the given encoded smtp header
|
||||
text. email.header decode_header method return a decoded string and its
|
||||
charset for each decoded par of the header. This method unicodes the
|
||||
decoded header and join them in a complete string. """
|
||||
if smtp_header:
|
||||
text = decode_header(smtp_header.replace('\r', ''))
|
||||
# The joining space will not be needed as of Python 3.3
|
||||
# See https://hg.python.org/cpython/rev/8c03fe231877
|
||||
return ' '.join([ustr(x[0], x[1]) for x in text])
|
||||
return u''
|
||||
|
||||
# was mail_thread.decode_header()
|
||||
def decode_message_header(message, header, separator=' '):
|
||||
return separator.join(map(decode_smtp_header, filter(None, message.get_all(header, []))))
|
||||
|
||||
Reference in New Issue
Block a user