diff --git a/modules/account_dunning_email/account.py b/modules/account_dunning_email/account.py index 817f5999a7..29f58a5da2 100644 --- a/modules/account_dunning_email/account.py +++ b/modules/account_dunning_email/account.py @@ -8,7 +8,7 @@ from trytond.model import fields from trytond.pool import Pool, PoolMeta from trytond.pyson import Bool, Eval from trytond.report import get_email -from trytond.sendmail import SMTPDataManager, sendmail_transactional +from trytond.sendmail import SMTPDataManager, send_message_transactional from trytond.tools.email_ import convert_ascii_email, set_from_header from trytond.transaction import Transaction from trytond.wizard import StateTransition @@ -143,15 +143,14 @@ class Dunning(metaclass=PoolMeta): languages.add(lang) msg, title = self._email(from_, to, cc, bcc, languages) - to_addrs = [e for _, e in getaddresses(to + cc + bcc)] + addr_fields = filter(None, (msg['To'], msg['Cc'], msg['Bcc'])) + to_addrs = [e for _, e in getaddresses(addr_fields)] if to_addrs: - if not pool.test: - sendmail_transactional( - from_, to_addrs, msg, datamanager=datamanager) + send_message_transactional(msg, datamanager=datamanager) return Email( - recipients=', '.join(to), - recipients_secondary=', '.join(cc), - recipients_hidden=', '.join(bcc), + recipients=msg['To'], + recipients_secondary=msg['Cc'], + recipients_hidden=msg['Bcc'], addresses=[{'address': a} for a in to_addrs], subject=title, resource=self, diff --git a/modules/account_dunning_email/tests/scenario_account_dunning_email.rst b/modules/account_dunning_email/tests/scenario_account_dunning_email.rst index f004638849..4fe4adced0 100644 --- a/modules/account_dunning_email/tests/scenario_account_dunning_email.rst +++ b/modules/account_dunning_email/tests/scenario_account_dunning_email.rst @@ -6,13 +6,22 @@ Imports:: >>> import datetime >>> from decimal import Decimal + >>> from unittest.mock import patch >>> from proteus import Model, Wizard >>> from trytond.modules.account.tests.tools import ( ... create_chart, create_fiscalyear, get_accounts) + >>> from trytond.modules.account_dunning_email import account >>> from trytond.modules.company.tests.tools import create_company, get_company >>> from trytond.tests.tools import activate_modules, assertEqual +Patch send_message_transactional:: + + >>> smtp_calls = patch.object( + ... account, 'send_message_transactional').start() + >>> manager = patch.object( + ... account, 'SMTPDataManager').start() + Activate modules:: >>> config = activate_modules('account_dunning_email') diff --git a/modules/account_payment_stripe/payment.py b/modules/account_payment_stripe/payment.py index 074b18442f..f83686ad5b 100644 --- a/modules/account_payment_stripe/payment.py +++ b/modules/account_payment_stripe/payment.py @@ -26,7 +26,7 @@ from trytond.pool import Pool, PoolMeta from trytond.pyson import Eval, TimeDelta from trytond.report import Report, get_email from trytond.rpc import RPC -from trytond.sendmail import sendmail_transactional +from trytond.sendmail import send_message_transactional from trytond.tools import sql_pairing from trytond.tools.email_ import set_from_header from trytond.transaction import Transaction @@ -391,7 +391,7 @@ class Payment(StripeCustomerMethodMixin, CheckoutMixin, metaclass=PoolMeta): set_from_header(msg, from_cfg, from_ or from_cfg) msg['To'] = ','.join(emails) msg['Subject'] = title - sendmail_transactional(from_cfg, emails, msg) + send_message_transactional(msg) def _emails_checkout(self): emails = [] diff --git a/modules/marketing_automation/marketing_automation.py b/modules/marketing_automation/marketing_automation.py index 67b24b30a8..6f40563e4d 100644 --- a/modules/marketing_automation/marketing_automation.py +++ b/modules/marketing_automation/marketing_automation.py @@ -37,7 +37,7 @@ from trytond.model import ( from trytond.pool import Pool from trytond.pyson import Eval, If, PYSONDecoder, TimeDelta from trytond.report import Report -from trytond.sendmail import SMTPDataManager, sendmail_transactional +from trytond.sendmail import SMTPDataManager, send_message_transactional from trytond.tools import grouped_slice, pairwise_longest, reduce_ids from trytond.tools.email_ import convert_ascii_email, set_from_header from trytond.transaction import Transaction @@ -741,11 +741,12 @@ class Activity( to_addrs = [convert_ascii_email(a) for _, a in getaddresses([str(to)])] if to_addrs: - sendmail_transactional( - from_, to_addrs, msg, datamanager=smtpd_datamanager) + send_message_transactional(msg, datamanager=smtpd_datamanager) email = Email( - recipients=to, + recipients=msg['To'], + recipients_secondary=msg['Cc'], + recipients_hidden=msg['Bcc'], addresses=[{'address': a} for a in to_addrs], subject=title, resource=record.record, diff --git a/modules/marketing_automation/tests/scenario_marketing_automation.rst b/modules/marketing_automation/tests/scenario_marketing_automation.rst index 3a3214229d..717be1512d 100644 --- a/modules/marketing_automation/tests/scenario_marketing_automation.rst +++ b/modules/marketing_automation/tests/scenario_marketing_automation.rst @@ -14,10 +14,10 @@ Imports:: ... activate_modules, assertEqual, assertIn, assertTrue) >>> from trytond.tools import file_open -Patch sendmail_transactional:: +Patch send_message_transactional:: >>> smtp_calls = patch.object( - ... marketing_automation, 'sendmail_transactional').start() + ... marketing_automation, 'send_message_transactional').start() >>> manager = patch.object( ... marketing_automation, 'SMTPDataManager').start() @@ -149,10 +149,9 @@ Check email sent:: >>> smtp_calls.call_count 1 - >>> from_, to, msg = smtp_calls.call_args[0] + >>> msg, = smtp_calls.call_args[0] >>> smtp_calls.reset_mock() >>> msg = msg.get_body().get_content() - >>> assertEqual(to, [contact.value]) >>> assertEqual(re.search(r'Hello, (.*)!', msg).group(1), party.name) >>> open_url.shortened_url in msg True diff --git a/modules/marketing_automation/tests/scenario_marketing_automation_unsubscribable.rst b/modules/marketing_automation/tests/scenario_marketing_automation_unsubscribable.rst index a3ce74ce2d..799bc92399 100644 --- a/modules/marketing_automation/tests/scenario_marketing_automation_unsubscribable.rst +++ b/modules/marketing_automation/tests/scenario_marketing_automation_unsubscribable.rst @@ -12,10 +12,10 @@ Imports:: >>> from trytond.tests.tools import activate_modules, assertEqual >>> from trytond.tools import file_open -Patch sendmail_transactional:: +Patch send_message_transactional:: >>> smtp_calls = patch.object( - ... marketing_automation, 'sendmail_transactional').start() + ... marketing_automation, 'send_message_transactional').start() >>> manager = patch.object( ... marketing_automation, 'SMTPDataManager').start() diff --git a/modules/marketing_email/marketing.py b/modules/marketing_email/marketing.py index 6316884541..0fda95708a 100644 --- a/modules/marketing_email/marketing.py +++ b/modules/marketing_email/marketing.py @@ -5,7 +5,6 @@ import time from collections import defaultdict from email.headerregistry import Address from email.message import EmailMessage -from email.utils import getaddresses from functools import lru_cache, partial from urllib.parse import ( parse_qs, parse_qsl, urlencode, urljoin, urlsplit, urlunsplit) @@ -28,11 +27,10 @@ from trytond.model import ( from trytond.pool import Pool from trytond.pyson import Eval from trytond.report import Report, get_email -from trytond.sendmail import SMTPDataManager, sendmail_transactional +from trytond.sendmail import SMTPDataManager, send_message_transactional from trytond.tools import grouped_slice, reduce_ids from trytond.tools.email_ import ( - EmailNotValidError, convert_ascii_email, normalize_email, set_from_header, - validate_email) + EmailNotValidError, normalize_email, set_from_header, validate_email) from trytond.transaction import Transaction, inactive_records from trytond.url import http_host from trytond.wizard import Button, StateTransition, StateView, Wizard @@ -317,8 +315,7 @@ class EmailList(DeactivableMixin, ModelSQL, ModelView): set_from_header(msg, from_cfg, from_ or from_cfg) msg['To'] = record.email msg['Subject'] = title - sendmail_transactional( - from_cfg, [convert_ascii_email(record.email)], msg) + send_message_transactional(msg) def request_unsubscribe(self, email, from_=None): pool = Pool() @@ -343,8 +340,7 @@ class EmailList(DeactivableMixin, ModelSQL, ModelView): set_from_header(msg, from_cfg, from_ or from_cfg) msg['To'] = record.email msg['Subject'] = title - sendmail_transactional( - from_cfg, [convert_ascii_email(record.email)], msg) + send_message_transactional(msg) class Message(Workflow, ModelSQL, ModelView): @@ -522,9 +518,7 @@ class Message(Workflow, ModelSQL, ModelView): content_text = converter.handle(content) msg.add_alternative(content_text, subtype='plain') - sendmail_transactional( - from_cfg, getaddresses([str(to)]), msg, - datamanager=smtpd_datamanager) + send_message_transactional(msg, datamanager=smtpd_datamanager) if not emails: cls.sent(messages) diff --git a/modules/marketing_email/tests/test_module.py b/modules/marketing_email/tests/test_module.py index 3580942358..0fbc63df42 100644 --- a/modules/marketing_email/tests/test_module.py +++ b/modules/marketing_email/tests/test_module.py @@ -53,9 +53,10 @@ class MarketingEmailTestCase(ModuleTestCase): email_list.save() with patch.object( - marketing_module, 'sendmail_transactional') as sendmail: + marketing_module, + 'send_message_transactional') as send_message: email_list.request_subscribe('user@example.com') - sendmail.assert_called_once_with(FROM, ['user@example.com'], ANY) + send_message.assert_called_once() with inactive_records(): email, = Email.search([ @@ -98,9 +99,10 @@ class MarketingEmailTestCase(ModuleTestCase): self.assertTrue(email.active) with patch.object( - marketing_module, 'sendmail_transactional') as sendmail: + marketing_module, + 'send_message_transactional') as send_message: email_list.request_unsubscribe('user@example.com') - sendmail.assert_called_once_with(FROM, ['user@example.com'], ANY) + send_message.assert_called_once() self.assertEqual( email.get_email_unsubscribe_url(), @@ -142,13 +144,12 @@ class MarketingEmailTestCase(ModuleTestCase): with patch.object( marketing_module, - 'sendmail_transactional') as sendmail: + 'send_message_transactional') as send_message: smtpd_datamanager = Mock() Message.process(smtpd_datamanager=smtpd_datamanager) - sendmail.assert_called_once_with( - FROM, [('', 'user@example.com')], ANY, - datamanager=smtpd_datamanager) + send_message.assert_called_once_with( + ANY, datamanager=smtpd_datamanager) urls = ShortenedURL.search([ ('record', '=', str(message)), ]) diff --git a/modules/notification_email/notification.py b/modules/notification_email/notification.py index 99317e3228..e679337f73 100644 --- a/modules/notification_email/notification.py +++ b/modules/notification_email/notification.py @@ -12,8 +12,8 @@ from trytond.model import ModelSQL, ModelView, fields from trytond.pool import Pool from trytond.pyson import Eval, TimeDelta from trytond.report import get_email -from trytond.sendmail import SMTPDataManager, sendmail_transactional -from trytond.tools.email_ import convert_ascii_email, set_from_header +from trytond.sendmail import SMTPDataManager, send_message_transactional +from trytond.tools.email_ import set_from_header from trytond.transaction import Transaction from .exceptions import TemplateError @@ -171,9 +171,12 @@ class Email(ModelSQL, ModelView): # TODO order languages to get default as last one for title msg, title = get_email(self.content, record, languages) - language = list(languages)[-1] + if languages: + language = list(languages)[-1].code + else: + language = None from_ = sender - with Transaction().set_context(language=language.code): + with Transaction().set_context(language=language): notification = self.__class__(self.id) if notification.from_: from_ = notification.from_ @@ -184,7 +187,7 @@ class Email(ModelSQL, ModelView): if self.attachments: for report in self.attachments: - name, data = Attachment.get(report, record, language.code) + name, data = Attachment.get(report, record, language) ctype, _ = mimetypes.guess_type(name) if not ctype: ctype = 'application/octet-stream' @@ -198,6 +201,7 @@ class Email(ModelSQL, ModelView): set_from_header(msg, sender, from_) msg['To'] = [Address(n, addr_spec=a) for n, a in getaddresses(to)] msg['Cc'] = [Address(n, addr_spec=a) for n, a in getaddresses(cc)] + msg['Bcc'] = [Address(n, addr_spec=a) for n, a in getaddresses(bcc)] msg['Subject'] = title msg['Auto-Submitted'] = 'auto-generated' return msg, title @@ -239,17 +243,15 @@ class Email(ModelSQL, ModelView): cc, cc_languages = self._get_cc(record) bcc, bcc_languages = self._get_bcc(record) languages = to_languages | cc_languages | bcc_languages - to_addrs = [ - convert_ascii_email(e) for _, e in getaddresses(to + cc + bcc)] + msg, title = self.get_email(record, from_, to, cc, bcc, languages) + to_addrs = [e for _, e in getaddresses( + filter(None, (msg['To'], msg['Cc'], msg['Bcc'])))] if to_addrs: - msg, title = self.get_email( - record, from_, to, cc, bcc, languages) - sendmail_transactional( - from_, to_addrs, msg, datamanager=datamanager) + send_message_transactional(msg, datamanager=datamanager) emails.append(Email( - recipients=', '.join(to), - recipients_secondary=', '.join(cc), - recipients_hidden=', '.join(bcc), + recipients=msg['To'], + recipients_secondary=msg['Cc'], + recipients_hidden=msg['Bcc'], addresses=[{'address': a} for a in to_addrs], subject=title, resource=record, diff --git a/modules/notification_email/tests/test_module.py b/modules/notification_email/tests/test_module.py index f7d97e490a..89ec08d20d 100644 --- a/modules/notification_email/tests/test_module.py +++ b/modules/notification_email/tests/test_module.py @@ -3,7 +3,7 @@ import datetime as dt import sys import unittest -from unittest.mock import ANY, patch +from unittest.mock import patch from trytond.config import config @@ -104,14 +104,13 @@ class NotificationEmailTestCase(CompanyTestMixin, ModuleTestCase): }]) with patch.object( - notification_module, 'sendmail_transactional') as sendmail, \ + notification_module, + 'send_message_transactional') as send_message, \ patch.object(notification_module, 'SMTPDataManager'): user, = User.create([{'name': "Michael Scott", 'login': "msc"}]) self.run_tasks() - sendmail.assert_called_once_with( - FROM, ['user@example.com'], ANY, - datamanager=ANY) - _, _, msg = sendmail.call_args[0] + send_message.assert_called_once() + msg, = send_message.call_args[0] self.assertEqual(msg['From'], FROM) self.assertEqual(msg['Subject'], 'Notification Email') self.assertEqual(msg['To'], 'Administrator ') @@ -156,16 +155,15 @@ class NotificationEmailTestCase(CompanyTestMixin, ModuleTestCase): }]) with patch.object( - notification_module, 'sendmail_transactional') as sendmail, \ + notification_module, + 'send_message_transactional') as send_message, \ patch.object(notification_module, 'SMTPDataManager'): user, = User.create([{ 'name': "Michael Scott", 'login': "msc", 'email': 'msc@example.com'}]) self.run_tasks() - sendmail.assert_called_once_with( - FROM, ['msc@example.com'], ANY, - datamanager=ANY) + send_message.assert_called_once() email, = Email.search([]) self.assertEqual(email.recipients, 'Michael Scott ') @@ -200,14 +198,14 @@ class NotificationEmailTestCase(CompanyTestMixin, ModuleTestCase): }]) with patch.object( - notification_module, 'sendmail_transactional') as sendmail, \ + notification_module, + 'send_message_transactional') as send_message, \ patch.object(notification_module, 'SMTPDataManager'): User.create([{'name': "Michael Scott", 'login': "msc"}]) self.run_tasks(1) - sendmail.assert_not_called() + send_message.assert_not_called() self.run_tasks() - sendmail.assert_called_once_with( - FROM, ['user@example.com'], ANY, datamanager=ANY) + send_message.assert_called_once() @with_transaction() def test_notification_email_attachment(self): @@ -355,14 +353,13 @@ class NotificationEmailTestCase(CompanyTestMixin, ModuleTestCase): }]) with patch.object( - notification_module, 'sendmail_transactional') as sendmail, \ + notification_module, + 'send_message_transactional') as send_message, \ patch.object(notification_module, 'SMTPDataManager'): User.create([{'name': "Michael Scott", 'login': "msc"}]) self.run_tasks() - sendmail.assert_called_once_with( - FROM, ['fallback@example.com'], ANY, - datamanager=ANY) - _, _, msg = sendmail.call_args[0] + send_message.assert_called_once() + msg, = send_message.call_args[0] self.assertEqual(msg['To'], 'Fallback ') @with_transaction() @@ -393,11 +390,12 @@ class NotificationEmailTestCase(CompanyTestMixin, ModuleTestCase): }]) with patch.object( - notification_module, 'get_email') as get_email, \ + notification_module, + 'send_message_transactional') as send_message_transactional, \ patch.object(notification_module, 'SMTPDataManager'): User.create([{'name': "Michael Scott", 'login': "msc"}]) self.run_tasks() - get_email.assert_not_called() + send_message_transactional.assert_not_called() del ModuleTestCase diff --git a/modules/sale_gift_card/sale.py b/modules/sale_gift_card/sale.py index bbc6d51cb1..8916122c56 100644 --- a/modules/sale_gift_card/sale.py +++ b/modules/sale_gift_card/sale.py @@ -13,7 +13,7 @@ from trytond.modules.currency.fields import Monetary from trytond.pool import Pool, PoolMeta from trytond.pyson import Bool, Eval, Id from trytond.report import Report, get_email -from trytond.sendmail import sendmail_transactional +from trytond.sendmail import send_message_transactional from trytond.tools.email_ import ( EmailNotValidError, set_from_header, validate_email) from trytond.transaction import Transaction @@ -198,7 +198,7 @@ class GiftCard(ModelSQL, ModelView): set_from_header(msg, from_cfg, from_ or from_cfg) msg['To'] = email msg['Subject'] = title - sendmail_transactional(from_cfg, [email], msg, strict=True) + send_message_transactional(msg, strict=True) class GiftCardReport(Report): diff --git a/modules/sale_gift_card/tests/scenario_sale_gift_card.rst b/modules/sale_gift_card/tests/scenario_sale_gift_card.rst index f4a496c798..d00731220b 100644 --- a/modules/sale_gift_card/tests/scenario_sale_gift_card.rst +++ b/modules/sale_gift_card/tests/scenario_sale_gift_card.rst @@ -13,10 +13,10 @@ Imports:: >>> from trytond.modules.sale_gift_card import sale >>> from trytond.tests.tools import activate_modules, assertEqual -Patch sendmail_transactional:: +Patch send_message_transactional:: >>> smtp_calls = patch.object( - ... sale, 'sendmail_transactional').start() + ... sale, 'send_message_transactional').start() Activate modules:: @@ -136,9 +136,7 @@ Check gift cards:: False >>> smtp_calls.call_count 2 - >>> from_, to, msg = smtp_calls.call_args[0] - >>> to - ['customer@example.com'] + >>> msg, = smtp_calls.call_args[0] >>> card.number in msg.get_body().get_content() True diff --git a/modules/web_user/tests/test_module.py b/modules/web_user/tests/test_module.py index bf0530e5a6..8fadbf905e 100644 --- a/modules/web_user/tests/test_module.py +++ b/modules/web_user/tests/test_module.py @@ -1,7 +1,7 @@ # This file is part of Tryton. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. -from unittest.mock import ANY, patch +from unittest.mock import patch from trytond.config import config from trytond.modules.party.tests import PartyCheckEraseMixin @@ -101,9 +101,10 @@ class WebUserTestCase(PartyCheckEraseMixin, ModuleTestCase): self.assertFalse(user.email_valid) self.assertFalse(user.email_token) - with patch.object(user_module, 'sendmail_transactional') as sendmail: + with patch.object( + user_module, 'send_message_transactional') as send_message: User.validate_email([user]) - sendmail.assert_called_once_with(FROM, ['user@example.com'], ANY) + send_message.assert_called_once() token = user.email_token self.assertTrue(token) @@ -133,9 +134,10 @@ class WebUserTestCase(PartyCheckEraseMixin, ModuleTestCase): user.save() self.assertFalse(user.password_hash) - with patch.object(user_module, 'sendmail_transactional') as sendmail: + with patch.object( + user_module, 'send_message_transactional') as send_message: User.reset_password([user]) - sendmail.assert_called_once_with(FROM, ['user@example.com'], ANY) + send_message.assert_called_once() token = user.reset_password_token self.assertTrue(token) diff --git a/modules/web_user/user.py b/modules/web_user/user.py index 11469725cc..e17736dadb 100644 --- a/modules/web_user/user.py +++ b/modules/web_user/user.py @@ -33,7 +33,7 @@ from trytond.pool import Pool from trytond.pyson import Eval from trytond.report import Report, get_email from trytond.res.user import CRYPT_CONTEXT, LoginAttempt -from trytond.sendmail import sendmail_transactional +from trytond.sendmail import send_message_transactional from trytond.tools.email_ import ( EmailNotValidError, normalize_email, set_from_header, validate_email) from trytond.transaction import Transaction, without_check_access @@ -50,7 +50,7 @@ def _send_email(from_, users, email_func): set_from_header(msg, from_cfg, from_ or from_cfg) msg['To'] = user.email msg['Subject'] = title - sendmail_transactional(from_cfg, [user.email], msg) + send_message_transactional(msg) def _add_params(url, **params): diff --git a/trytond/CHANGELOG b/trytond/CHANGELOG index 03c50504e7..ddd88dcf73 100644 --- a/trytond/CHANGELOG +++ b/trytond/CHANGELOG @@ -1,3 +1,4 @@ +* Add send_message methods * Store model on ir.rule.group * Store model on ir.model.button * Store model and field name on ir.model.field.access diff --git a/trytond/doc/ref/sendmail.rst b/trytond/doc/ref/sendmail.rst index 3e022d2623..ec10e7fc51 100644 --- a/trytond/doc/ref/sendmail.rst +++ b/trytond/doc/ref/sendmail.rst @@ -10,7 +10,7 @@ Sendmail The required arguments are an :rfc:`5322` from-address string, a list of :rfc:`5322` to-address strings (a bare string is treated as a list with 1 - address), and an email message. + address), and an :py:class:`email.message.Message`. The caller may pass a :class:`~trytond.transaction.Transaction` instance to join otherwise the current one is joined. A specific data manager can be specified otherwise the default @@ -21,6 +21,12 @@ Sendmail An SMTP failure is only logged without raising any exception. +.. function:: send_message_transactional(msg[, from_addr[, to_addrs[, transaction[, datamanager[, strict]]]]]) + + It is a convenience method for calling :func:`sendmail_transactional`. + If ``from_addr`` is ``None`` or ``to_addrs`` is ``None``, if fills those + arguments with addresses extracted from the headers of ``msg``. + .. function:: sendmail(from_addr, to_addrs, msg[, server[, strict]]) Send email message like :meth:`sendmail_transactional` but directly without @@ -32,6 +38,11 @@ Sendmail If strict is ``True``, an exception is raised if it is not possible to connect to the server. +.. function:: send_message(msg[, from_addr[, to_addrs[, server[, strict]]]]) + + Same convenience method as :func:`send_message_transactional` but for + calling :func:`sendmail`. + .. function:: get_smtp_server([uri[, strict]]) Return a SMTP instance from `smtplib`_ using the ``uri`` or the one defined diff --git a/trytond/trytond/ir/email_.py b/trytond/trytond/ir/email_.py index c5c29bb6e8..3a05cfd3e1 100644 --- a/trytond/trytond/ir/email_.py +++ b/trytond/trytond/ir/email_.py @@ -20,7 +20,7 @@ from trytond.pool import Pool from trytond.pyson import Bool, Eval, PYSONDecoder from trytond.report import Report from trytond.rpc import RPC -from trytond.sendmail import sendmail_transactional +from trytond.sendmail import send_message_transactional from trytond.tools import escape_wildcard from trytond.tools.email_ import convert_ascii_email, set_from_header from trytond.tools.string_ import StringMatcher @@ -161,17 +161,20 @@ class Email(ResourceAccessMixin, ModelSQL, ModelView): msg['Cc'] = ', '.join( formataddr((n, convert_ascii_email(a))) for n, a in getaddresses([cc])) + msg['Bcc'] = ', '.join( + formataddr((n, convert_ascii_email(a))) + for n, a in getaddresses([bcc])) msg['Subject'] = subject to_addrs = list(filter(None, map( str.strip, _get_emails(to) + _get_emails(cc) + _get_emails(bcc)))) - sendmail_transactional(from_, to_addrs, msg, strict=True) + send_message_transactional(msg, strict=True) email = cls( - recipients=to, - recipients_secondary=cc, - recipients_hidden=bcc, + recipients=msg['To'], + recipients_secondary=msg['Cc'], + recipients_hidden=msg['Bcc'], addresses=[{'address': a} for a in to_addrs], subject=subject, body=body, diff --git a/trytond/trytond/res/user.py b/trytond/trytond/res/user.py index 80abd945be..eeee86e579 100644 --- a/trytond/trytond/res/user.py +++ b/trytond/trytond/res/user.py @@ -51,7 +51,7 @@ from trytond.pool import Pool from trytond.pyson import Bool, Eval, PYSONEncoder from trytond.report import Report, get_email from trytond.rpc import RPC -from trytond.sendmail import sendmail_transactional +from trytond.sendmail import send_message_transactional from trytond.tools import grouped_slice from trytond.tools.email_ import ( EmailNotValidError, normalize_email, set_from_header, validate_email) @@ -96,7 +96,7 @@ def _send_email(from_, users, email_func): set_from_header(msg, from_cfg, from_ or from_cfg) msg['To'] = user.email msg['Subject'] = title - sendmail_transactional(from_cfg, [user.email], msg) + send_message_transactional(msg) class PasswordError(UserError): diff --git a/trytond/trytond/sendmail.py b/trytond/trytond/sendmail.py index 2f43ff44e7..66a5bbb378 100644 --- a/trytond/trytond/sendmail.py +++ b/trytond/trytond/sendmail.py @@ -11,7 +11,10 @@ from urllib.parse import parse_qs, unquote_plus from trytond.config import config, parse_uri from trytond.transaction import Transaction -__all__ = ['sendmail_transactional', 'sendmail', 'SMTPDataManager'] +__all__ = [ + 'sendmail_transactional', 'sendmail', + 'send_message_transactional', 'send_message', + 'SMTPDataManager'] logger = logging.getLogger(__name__) retry = config.getint('email', 'retry', default=5) @@ -19,6 +22,13 @@ retry = config.getint('email', 'retry', default=5) def sendmail_transactional( from_addr, to_addrs, msg, transaction=None, datamanager=None, strict=False): + send_message_transactional( + msg, from_addr, to_addrs, transaction, datamanager, strict) + + +def send_message_transactional( + msg, from_addr=None, to_addrs=None, transaction=None, datamanager=None, + strict=False): if transaction is None: transaction = Transaction() assert isinstance(transaction, Transaction), transaction @@ -29,6 +39,11 @@ def sendmail_transactional( def sendmail(from_addr, to_addrs, msg, server=None, strict=False): + return send_message(msg, from_addr, to_addrs, server, strict) + + +def send_message( + msg, from_addr=None, to_addrs=None, server=None, strict=False): if server is None: server = get_smtp_server(strict=strict) if not server: @@ -43,7 +58,7 @@ def sendmail(from_addr, to_addrs, msg, server=None, strict=False): if count != retry: time.sleep(0.02 * (retry - count)) try: - senderrs = server.sendmail(from_addr, to_addrs, msg.as_string()) + senderrs = server.send_message(msg, from_addr, to_addrs) except smtplib.SMTPServerDisconnected: if count: server = get_smtp_server(strict=strict) @@ -82,8 +97,7 @@ def send_test_email(to_addrs, server=None): msg['From'] = from_ msg['To'] = to_addrs msg['Subject'] = 'Tryton test email' - sendmail( - config.get('email', 'from'), to_addrs, msg, server=server, strict=True) + send_message(msg, server=server, strict=True) def get_smtp_server(uri=None, strict=False): @@ -153,8 +167,8 @@ class SMTPDataManager(object): def tpc_finish(self, trans): if self._server is not None: for from_addr, to_addrs, msg in self.queue: - new_server = sendmail( - from_addr, to_addrs, msg, server=self._server) + new_server = send_message( + msg, from_addr, to_addrs, server=self._server) if new_server: self._server = new_server self._server.quit() diff --git a/trytond/trytond/tests/test_ir.py b/trytond/trytond/tests/test_ir.py index 330ea76bda..f4beb959e9 100644 --- a/trytond/trytond/tests/test_ir.py +++ b/trytond/trytond/tests/test_ir.py @@ -17,7 +17,6 @@ except ImportError: from dateutil.relativedelta import relativedelta -from trytond.config import config from trytond.ir.exceptions import SequenceAffixError from trytond.ir.lang import _replace from trytond.pool import Pool @@ -339,7 +338,9 @@ class IrTestCase(ModuleTestCase): ) report.save() - with patch('trytond.ir.email_.sendmail_transactional') as sendmail: + with patch( + 'trytond.ir.email_.send_message_transactional' + ) as send_message: email = Email.send( to='"John Doe" , Jane ', cc='User ', @@ -359,11 +360,10 @@ class IrTestCase(ModuleTestCase): 'jane@example.com', 'user@example.com', 'me@example.com'] - sendmail.assert_called_once_with( - config.get('email', 'from'), addresses, ANY, strict=True) + send_message.assert_called_once_with(ANY, strict=True) self.assertEqual( email.recipients, - '"John Doe" , Jane ') + 'John Doe , Jane ') self.assertEqual(email.recipients_secondary, 'User ') self.assertEqual(email.recipients_hidden, 'me@example.com') self.assertEqual( diff --git a/trytond/trytond/tests/test_sendmail.py b/trytond/trytond/tests/test_sendmail.py index 89915a0a60..f884763823 100644 --- a/trytond/trytond/tests/test_sendmail.py +++ b/trytond/trytond/tests/test_sendmail.py @@ -37,8 +37,8 @@ class SendmailTestCase(unittest.TestCase): server = Mock() sendmail( 'tryton@example.com', 'foo@example.com', message, server=server) - server.sendmail.assert_called_with( - 'tryton@example.com', 'foo@example.com', message.as_string()) + server.send_message.assert_called_with( + message, 'tryton@example.com', 'foo@example.com') server.quit.assert_not_called() def test_get_smtp_server(self): @@ -97,9 +97,9 @@ class SendmailTestCase(unittest.TestCase): transaction.commit() - server.sendmail.assert_has_calls([ - call('foo@example.com', 'bar@example.com', msg1.as_string()), - call('bar@example.com', 'foo@example.com', msg2.as_string()), + server.send_message.assert_has_calls([ + call(msg1, 'foo@example.com', 'bar@example.com'), + call(msg2, 'bar@example.com', 'foo@example.com'), ]) server.quit.assert_called_once_with() self.assertFalse(datamanager.queue) @@ -110,5 +110,5 @@ class SendmailTestCase(unittest.TestCase): 'foo@example.com', 'bar@example.com', MagicMock(Message)) transaction.rollback() - server.sendmail.assert_not_called() + server.send_message.assert_not_called() self.assertFalse(datamanager.queue) diff --git a/trytond/trytond/tests/test_user.py b/trytond/trytond/tests/test_user.py index 9ff53382e1..480ae9ae80 100644 --- a/trytond/trytond/tests/test_user.py +++ b/trytond/trytond/tests/test_user.py @@ -4,7 +4,7 @@ import datetime import os import unittest from contextlib import contextmanager -from unittest.mock import ANY, Mock, patch +from unittest.mock import Mock, patch from trytond.config import config from trytond.pool import Pool @@ -151,9 +151,10 @@ class UserTestCase(unittest.TestCase): user = self.create_user('user', '12345', email='user@example.com') - with patch.object(user_module, 'sendmail_transactional') as sendmail: + with patch.object( + user_module, 'send_message_transactional') as send_message: User.reset_password([user], length=8) - sendmail.assert_called_once_with(FROM, ['user@example.com'], ANY) + send_message.assert_called_once() cursor.execute(*user_table.select( user_table.password_hash, @@ -174,7 +175,7 @@ class UserTestCase(unittest.TestCase): user = User(login='user', email='user@example.com') user.save() - with patch.object(user_module, 'sendmail_transactional'): + with patch.object(user_module, 'send_message_transactional'): User.reset_password([user], length=8) user.password_reset_expire = (