diff --git a/addons/l10n_it_edi/models/ir_mail_server.py b/addons/l10n_it_edi/models/ir_mail_server.py index 580635dfc3d..d4575c90fe1 100644 --- a/addons/l10n_it_edi/models/ir_mail_server.py +++ b/addons/l10n_it_edi/models/ir_mail_server.py @@ -6,6 +6,7 @@ import io import re import logging import email +import email.policy import dateutil import pytz import base64 @@ -74,7 +75,7 @@ class FetchmailServer(models.Model): message = bytes(message.data) if isinstance(message, str): message = message.encode('utf-8') - msg_txt = email.message_from_bytes(message) + msg_txt = email.message_from_bytes(message, policy=email.policy.SMTP) try: self._attachment_invoice(msg_txt) diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index 94ae2e4b448..15d5f882f0a 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -5,6 +5,7 @@ import base64 import datetime import dateutil import email +import email.policy import hashlib import hmac import lxml @@ -20,7 +21,7 @@ except ImportError: import xmlrpclib from collections import namedtuple -from email.message import Message +from email.message import EmailMessage from email.utils import formataddr from lxml import etree from werkzeug import url_encode @@ -876,8 +877,8 @@ class MailThread(models.AbstractModel): :raises: ValueError, TypeError """ - if not isinstance(message, Message): - raise TypeError('message must be an email.message.Message at this point') + if not isinstance(message, EmailMessage): + raise TypeError('message must be an email.message.EmailMessage at this point') catchall_alias = self.env['ir.config_parameter'].sudo().get_param("mail.catchall.alias") bounce_alias = self.env['ir.config_parameter'].sudo().get_param("mail.bounce.alias") fallback_model = model @@ -1093,7 +1094,7 @@ class MailThread(models.AbstractModel): message = bytes(message.data) if isinstance(message, str): message = message.encode('utf-8') - message = email.message_from_bytes(message) + message = email.message_from_bytes(message, policy=email.policy.SMTP) # parse the message, verify we are not in a loop by checking message_id is not duplicated msg_dict = self.message_parse(message, save_original=save_original) @@ -1301,8 +1302,8 @@ class MailThread(models.AbstractModel): :param message_dict: dictionary holding already-parsed values and in which bounce-related values will be added; """ - if not isinstance(email_message, Message): - raise TypeError('message must be an email.message.Message at this point') + if not isinstance(email_message, EmailMessage): + raise TypeError('message must be an email.message.EmailMessage at this point') email_part = next((part for part in email_message.walk() if part.get_content_type() == 'message/rfc822'), None) dsn_part = next((part for part in email_message.walk() if part.get_content_type() == 'message/delivery-status'), None) @@ -1361,8 +1362,8 @@ class MailThread(models.AbstractModel): ('file2', 'bytes')} } """ - if not isinstance(message, Message): - raise ValueError(_('Message should be a valid Message instance')) + if not isinstance(message, EmailMessage): + raise ValueError(_('Message should be a valid EmailMessage instance')) msg_dict = {'message_type': 'email'} message_id = message.get('Message-Id') diff --git a/addons/test_mail/tests/test_mail_gateway.py b/addons/test_mail/tests/test_mail_gateway.py index 52ed239bc0e..ff7b3580559 100644 --- a/addons/test_mail/tests/test_mail_gateway.py +++ b/addons/test_mail/tests/test_mail_gateway.py @@ -2,6 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. import email +import email.policy import socket from email.utils import formataddr @@ -17,6 +18,9 @@ from odoo.addons.test_mail.tests.common import mail_new_test_user from odoo.tests import tagged from odoo.tools import email_split_and_format, mute_logger, pycompat +def from_string(text): + return email.message_from_string(pycompat.to_text(text), policy=email.policy.SMTP) + @tagged('mail_gateway') class TestEmailParsing(BaseFunctionalTest, MockEmails): @@ -24,16 +28,16 @@ class TestEmailParsing(BaseFunctionalTest, MockEmails): def test_message_parse_body(self): # test pure plaintext plaintext = self.format(test_mail_data.MAIL_TEMPLATE_PLAINTEXT, email_from='Sylvie Lelitre ') - res = self.env['mail.thread'].message_parse(email.message_from_string(pycompat.to_text(plaintext))) + res = self.env['mail.thread'].message_parse(from_string(plaintext)) self.assertIn('Please call me as soon as possible this afternoon!', res['body']) # test multipart / text and html -> html has priority multipart = self.format(MAIL_TEMPLATE, email_from='Sylvie Lelitre ') - res = self.env['mail.thread'].message_parse(email.message_from_string(pycompat.to_text(multipart))) + res = self.env['mail.thread'].message_parse(from_string(multipart)) self.assertIn('

Please call me as soon as possible this afternoon!

', res['body']) # test multipart / mixed - res = self.env['mail.thread'].message_parse(email.message_from_string(pycompat.to_text(test_mail_data.MAIL_MULTIPART_MIXED))) + res = self.env['mail.thread'].message_parse(from_string(test_mail_data.MAIL_MULTIPART_MIXED)) self.assertNotIn( 'Should create a multipart/mixed: from gmail, *bold*, with attachment', res['body'], 'message_parse: text version should not be in body after parsing multipart/mixed') @@ -41,7 +45,7 @@ class TestEmailParsing(BaseFunctionalTest, MockEmails): '
Should create a multipart/mixed: from gmail, bold, with attachment.

', res['body'], 'message_parse: html version should be in body after parsing multipart/mixed') - res = self.env['mail.thread'].message_parse(email.message_from_string(pycompat.to_text(test_mail_data.MAIL_MULTIPART_MIXED_TWO))) + res = self.env['mail.thread'].message_parse(from_string(test_mail_data.MAIL_MULTIPART_MIXED_TWO)) self.assertNotIn('First and second part', res['body'], 'message_parse: text version should not be in body after parsing multipart/mixed') self.assertIn('First part', res['body'], @@ -49,24 +53,24 @@ class TestEmailParsing(BaseFunctionalTest, MockEmails): self.assertIn('Second part', res['body'], 'message_parse: second part of the html version should be in body after parsing multipart/mixed') - res = self.env['mail.thread'].message_parse(email.message_from_string(pycompat.to_text(test_mail_data.MAIL_SINGLE_BINARY))) + res = self.env['mail.thread'].message_parse(from_string(test_mail_data.MAIL_SINGLE_BINARY)) self.assertEqual(res['body'], '') self.assertEqual(res['attachments'][0][0], 'thetruth.pdf') def test_message_parse_eml(self): # Test that the parsing of mail with embedded emails as eml(msg) which generates empty attachments, can be processed. mail = self.format(test_mail_data.MAIL_EML_ATTACHMENT, email_from='Sylvie Lelitre ', to='generic@test.com') - self.env['mail.thread'].message_parse(email.message_from_string(pycompat.to_text(mail))) + self.env['mail.thread'].message_parse(from_string(mail)) def test_message_parse_plaintext(self): """ Incoming email in plaintext should be stored as html """ mail = self.format(test_mail_data.MAIL_TEMPLATE_PLAINTEXT, email_from='Sylvie Lelitre ', to='generic@test.com') - res = self.env['mail.thread'].message_parse(email.message_from_string(pycompat.to_text(mail))) + res = self.env['mail.thread'].message_parse(from_string(mail)) self.assertIn('
\nPlease call me as soon as possible this afternoon!\n\n--\nSylvie\n
', res['body']) def test_message_parse_xhtml(self): # Test that the parsing of XHTML mails does not fail - self.env['mail.thread'].message_parse(email.message_from_string(pycompat.to_text(test_mail_data.MAIL_XHTML))) + self.env['mail.thread'].message_parse(from_string(test_mail_data.MAIL_XHTML)) @tagged('mail_gateway')