From ca4ba84d98acb3b0f7e8a4a6a73e0da07eb7c1ca Mon Sep 17 00:00:00 2001 From: Julien Van Roy Date: Mon, 19 Jun 2023 13:25:20 +0000 Subject: [PATCH] [FIX] mail: UTF-8 text/xml attachment and omitted charset When parsing an email containing an xml attachment, the `email` python module will decode the base64 attachment using the charset or ascii if the charset is missing. In some cases, the payload is in UTF-8 but the charset is omitted. This results in replacement characters for the non ASCII characters. The solution is to force the charset to UTF-8, since it is a superset of ASCII that should not be a problem. NB1: Omitting the charset for text/xml is not recommended. See the RFC (section 6.4): https://www.ietf.org/rfc/rfc2376.txt opw-3144519 closes odoo/odoo#126474 X-original-commit: e3a5b46c7b4f5030e165a6d943ec2275aea5d583 Signed-off-by: Julien Castiaux (juc) Signed-off-by: Julien Van Roy (juvr) --- addons/mail/models/mail_thread.py | 4 ++ addons/test_mail/data/test_mail_data.py | 44 +++++++++++++++++++++ addons/test_mail/tests/test_mail_gateway.py | 8 ++++ 3 files changed, 56 insertions(+) diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index 3e388bcece2..629fcded8dd 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -1420,6 +1420,10 @@ class MailThread(models.AbstractModel): continue # skip container filename = part.get_filename() # I may not properly handle all charsets + if part.get_content_type() == 'text/xml' and not part.get_param('charset'): + # for text/xml with omitted charset, the charset is assumed to be ASCII by the `email` module + # although the payload might be in UTF8 + part.set_charset('utf-8') encoding = part.get_content_charset() # None if attachment content = part.get_content() diff --git a/addons/test_mail/data/test_mail_data.py b/addons/test_mail/data/test_mail_data.py index 9baf09c3c5b..5597df0c611 100644 --- a/addons/test_mail/data/test_mail_data.py +++ b/addons/test_mail/data/test_mail_data.py @@ -326,6 +326,50 @@ Q2hhdXNz77+977+9ZSBkZSBCcnV4ZWxsZXM8L2NiYzpTdHJlZXROYW1lPg0KPC9JbnZvaWNlPg0K --00000000000005d9da05fa394cc0-- """ +MAIL_MULTIPART_OMITTED_CHARSET = """Return-Path: +To: {to} +cc: {cc} +Received: by mail1.openerp.com (Postfix, from userid 10002) + id 5DF9ABFB2A; Fri, 10 Aug 2012 16:16:39 +0200 (CEST) +From: {email_from} +Subject: {subject} +MIME-Version: 1.0 +Content-Type: multipart/alternative; + boundary="00000000000005d9da05fa394cc0" +Date: Fri, 10 Aug 2012 14:16:26 +0000 +Message-ID: {msg_id} +{extra} + +--00000000000005d9da05fa394cc0 +Content-Type: multipart/alternative; boundary="00000000000005d9d905fa394cbe" + +--00000000000005d9d905fa394cbe +Content-Type: text/plain; charset="UTF-8" + +Dear customer, + +Please find attached the UBL attachment of your invoice + +Cheers, + +--00000000000005d9d905fa394cbe +Content-Type: text/html; charset="UTF-8" + +
Dear customer,

Please find attached the UBL attachment of your invoice

Cheers,
+ +--00000000000005d9d905fa394cbe-- + +--00000000000005d9da05fa394cc0 +Content-Disposition: attachment; filename="bis3.xml" +Content-Transfer-Encoding: base64 +Content-Type: text/xml; name="bis3.xml" +Content-ID: +X-Attachment-Id: f_lgxgdqx40 + +PEludm9pY2U+Q2hhdXNzw6llIGRlIEJydXhlbGxlczwvSW52b2ljZT4= +--00000000000005d9da05fa394cc0-- +""" + MAIL_SINGLE_BINARY = """X-Original-To: raoul@grosbedon.fr Delivered-To: raoul@grosbedon.fr diff --git a/addons/test_mail/tests/test_mail_gateway.py b/addons/test_mail/tests/test_mail_gateway.py index 32cfbdaed7c..f402ef62f03 100644 --- a/addons/test_mail/tests/test_mail_gateway.py +++ b/addons/test_mail/tests/test_mail_gateway.py @@ -1763,6 +1763,14 @@ class TestMailgateway(MailCommon): # This explains the multiple "�" in the attachment. self.assertIn("Chauss������e de Bruxelles", record.message_ids.attachment_ids.raw.decode()) + def test_message_process_file_omitted_charset(self): + """ For incoming email containing an xml attachment with omitted charset and containing an UTF8 payload we + should parse the attachment using UTF-8. + """ + record = self.format_and_process(test_mail_data.MAIL_MULTIPART_OMITTED_CHARSET, self.email_from, 'groups@test.com') + self.assertEqual(record.message_ids.attachment_ids.name, 'bis3.xml') + self.assertEqual("Chaussée de Bruxelles", record.message_ids.attachment_ids.raw.decode()) + def test_message_route_reply_model_none(self): """ Test the message routing and reply functionality when the model is None.