Set email content with maintype when needed and correctly set Content-Language header

When the content is not a string, EmailMessage.set_content requires a maintype
argument but not when it is a string. The maintype is then deduced using the
mimetypes module.
The Content-Language of content or alternate must be set using the headers
parameter instead of params which is for the Content-Type.
And finally the Content-Language must be set on the main message only if it
contains multiparts otherwise it is set twice.

Closes #14028
This commit is contained in:
Cédric Krier committed 2025-05-23 17:07:57 +02:00
1 parent d3f93b46f8
commit 5307e3303a
3 files changed
+143 -15

No files matched your search

+1 -1
View File
@@ -53,7 +53,7 @@ else:
download_url = 'http://downloads.tryton.org/%s.%s/' % (
major_version, minor_version)
tests_require = ['pillow', 'pydot']
tests_require = ['pillow', 'pydot', 'html2text']
setup(name=name,
version=version,
+29 -13
View File
@@ -4,6 +4,7 @@ import datetime
import inspect
import logging
import math
import mimetypes
import operator
import os
import pathlib
@@ -594,7 +595,7 @@ def get_email(report, record, languages):
converter = None
title = None
msg = EmailMessage()
msg.add_header('Content-Language', ', '.join(l.code for l in languages))
header_factory = msg.policy.header_factory
for alternative, language in enumerate(languages):
with Transaction().set_context(
language=language.code, with_rec_name=False):
@@ -603,21 +604,36 @@ def get_email(report, record, languages):
'action_id': report_id,
'language': language,
})
if ext == 'txt':
ext = 'plain'
if ext == 'html' and html2text:
for map in [mimetypes.types_map, mimetypes.common_types]:
if '.' + ext in map:
mimetype = map['.' + ext]
maintype, subtype = mimetype.split('/')
break
else:
maintype, subtype = 'application', ext
if maintype == 'text' and subtype == 'html' and html2text:
if not converter:
converter = html2text.HTML2Text()
content_text = converter.handle(content)
msg.add_alternative(content_text, subtype='plain', params={
'Content-Language': language.code,
})
msg.add_alternative(content_text, subtype='plain', headers=[
header_factory('Content-Language', language.code),
])
types = {
'subtype': subtype,
}
if not isinstance(content, str):
types['maintype'] = maintype
if alternative or msg.is_multipart():
msg.add_alternative(content, subtype=ext, params={
'Content-Language': language.code,
})
msg.add_alternative(
content, **types, headers=[
header_factory('Content-Language', language.code),
])
else:
msg.set_content(content, subtype=ext, params={
'Content-Language': language.code,
})
msg.set_content(
content, **types, headers=[
header_factory('Content-Language', language.code),
])
if msg.is_multipart():
msg.add_header(
'Content-Language', ', '.join(l.code for l in languages))
return msg, title
+113 -1
View File
@@ -1,12 +1,15 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
import datetime
from email.message import EmailMessage
from unittest.mock import Mock, patch
from trytond.model.exceptions import AccessError
from trytond.pool import Pool
from trytond.report.report import Report
from trytond.report.report import Report, get_email
from trytond.tests.test_tryton import (
TestCase, activate_module, with_transaction)
from trytond.transaction import Transaction
class ReportTestCase(TestCase):
@@ -84,6 +87,115 @@ class ReportTestCase(TestCase):
with self.assertRaises(AccessError):
Report.execute([1], {'model': 'test.access'})
@with_transaction()
def test_get_email_html(self):
"Test get email"
class FakeReport(Report):
@classmethod
def execute(cls, *args, **kwargs):
return (
'html', '<!doctype html><title>Test</title>', False,
"Title")
record = Mock()
language = Mock()
language.code = 'en'
msg, title = get_email(FakeReport, record, [language])
self.assertEqual(title, "Title")
self.assertIsInstance(msg, EmailMessage)
self.assertEqual(msg['Content-Language'], 'en')
self.assertTrue(msg.is_multipart())
plain = msg.get_body(preferencelist=('plain',))
self.assertEqual(plain['Content-Language'], 'en')
self.assertEqual(plain.get_content().strip(), "Test")
html = msg.get_body(preferencelist=('html',))
self.assertEqual(html['Content-Language'], 'en')
self.assertEqual(
html.get_content().strip(),
'<!doctype html><title>Test</title>')
@with_transaction()
@patch('trytond.report.report.html2text')
def test_get_email_html_without_html2text(self, html2text):
html2text.__bool__.side_effect = lambda: False
class FakeReport(Report):
@classmethod
def execute(cls, *args, **kwargs):
return (
'html', '<!doctype html><title>Test</title>', False,
"Title")
record = Mock()
language = Mock()
language.code = 'en'
msg, title = get_email(FakeReport, record, [language])
self.assertEqual(title, "Title")
self.assertIsInstance(msg, EmailMessage)
self.assertEqual(msg['Content-Language'], 'en')
self.assertFalse(msg.is_multipart())
self.assertEqual(
msg.get_content().strip(),
'<!doctype html><title>Test</title>')
@with_transaction()
def test_get_email_html_multilanguage(self):
"Test get email multi-language"
class FakeReport(Report):
@classmethod
def execute(cls, *args, **kwargs):
language = Transaction().language
return (
'html', f'<!doctype html><title>{language}</title>',
False, language.upper())
record = Mock()
french = Mock()
french.code = 'fr'
english = Mock()
english.code = 'en'
msg, title = get_email(FakeReport, record, [french, english])
self.assertEqual(title, "EN")
self.assertIsInstance(msg, EmailMessage)
self.assertEqual(msg['Content-Language'], 'fr, en')
self.assertTrue(msg.is_multipart())
self.assertEqual(len(list(msg.walk())), 5)
plain = msg.get_body(preferencelist=('plain',))
self.assertEqual(plain['Content-Language'], 'fr')
self.assertEqual(plain.get_content().strip(), "fr")
html = msg.get_body(preferencelist=('html',))
self.assertEqual(html['Content-Language'], 'fr')
self.assertEqual(
html.get_content().strip(),
'<!doctype html><title>fr</title>')
@with_transaction()
def test_get_email_binary(self):
"Test get email binary"
class FakeReport(Report):
@classmethod
def execute(cls, *args, **kwargs):
return 'pdf', b'PDF', False, "Title"
record = Mock()
language = Mock()
language.code = 'en'
msg, title = get_email(FakeReport, record, [language])
self.assertEqual(title, "Title")
self.assertIsInstance(msg, EmailMessage)
self.assertEqual(msg['Content-Language'], 'en')
self.assertFalse(msg.is_multipart())
self.assertEqual(msg.get_content_maintype(), 'application')
self.assertEqual(msg.get_content_subtype(), 'pdf')
def create_test_format_timedelta(i, in_, out):
@with_transaction()