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:
1 parent
d3f93b46f8
commit
5307e3303a
3 files changed
+143
-15
No files matched your search
+1
-1
@@ -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,
|
||||
|
||||
@@ -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
|
||||
@@ -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()
|
||||
|
||||
Reference in new issue
Block a user