Add email constructor from message

This commit is contained in:
Cédric Krier
2024-02-21 21:43:50 +01:00
parent c5d85c1a4f
commit cd3395b233
8 changed files with 92 additions and 113 deletions
+11 -29
View File
@@ -1,7 +1,5 @@
# 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 email.header import Header
from email.utils import formataddr, getaddresses
from trytond.config import config
from trytond.model import fields
@@ -9,18 +7,11 @@ from trytond.pool import Pool, PoolMeta
from trytond.pyson import Bool, Eval
from trytond.report import get_email
from trytond.sendmail import SMTPDataManager, send_message_transactional
from trytond.tools.email_ import convert_ascii_email, set_from_header
from trytond.tools.email_ import format_address, has_rcpt, set_from_header
from trytond.transaction import Transaction
from trytond.wizard import StateTransition
def _formataddr(pair):
name, address = pair
if name:
name = str(Header(name, 'utf-8'))
return formataddr((name, convert_ascii_email(address)))
class Configuration(metaclass=PoolMeta):
__name__ = 'account.configuration'
@@ -126,11 +117,11 @@ class Dunning(metaclass=PoolMeta):
'email', usage=self.level.email_contact_mechanism)
if contact and contact.email:
name = contact.name or self.party.rec_name
to.append(_formataddr((name, contact.email)))
to.append(format_address(contact.email, name))
elif account_config.dunning_email_fallback:
user = account_config.get_multivalue(
'dunning_email_fallback', company=self.company.id)
to.append(_formataddr((self.party.rec_name, user.email)))
to.append(format_address(user.email, self.party.rec_name))
cc = []
bcc = []
languages = set()
@@ -142,20 +133,11 @@ class Dunning(metaclass=PoolMeta):
], limit=1)
languages.add(lang)
msg, title = self._email(from_, to, cc, bcc, languages)
addr_fields = filter(None, (msg['To'], msg['Cc'], msg['Bcc']))
to_addrs = [e for _, e in getaddresses(addr_fields)]
if to_addrs:
msg = self._email(from_, to, cc, bcc, languages)
if has_rcpt(msg):
send_message_transactional(msg, datamanager=datamanager)
return Email(
recipients=msg['To'],
recipients_secondary=msg['Cc'],
recipients_hidden=msg['Bcc'],
addresses=[{'address': a} for a in to_addrs],
subject=title,
resource=self,
dunning_level=self.level,
)
return Email.from_message(
msg, resource=self, dunning_level=self.level)
def _email(self, sender, to, cc, bcc, languages):
# TODO order languages to get default as last one for title
@@ -167,9 +149,9 @@ class Dunning(metaclass=PoolMeta):
if dunning.level.email_from:
from_ = dunning.level.email_from
set_from_header(msg, sender, from_)
msg['To'] = ', '.join(to)
msg['Cc'] = ', '.join(cc)
msg['Bcc'] = ', '.join(bcc)
msg['To'] = to
msg['Cc'] = cc
msg['Bcc'] = bcc
msg['Subject'] = title
msg['Auto-Submitted'] = 'auto-generated'
return msg, title
return msg
+1 -1
View File
@@ -389,7 +389,7 @@ class Payment(StripeCustomerMethodMixin, CheckoutMixin, metaclass=PoolMeta):
msg, title = get_email(
'account.payment.stripe.email_checkout', self, languages)
set_from_header(msg, from_cfg, from_ or from_cfg)
msg['To'] = ','.join(emails)
msg['To'] = emails
msg['Subject'] = title
send_message_transactional(msg)
@@ -5,9 +5,7 @@ import logging
import time
import uuid
from collections import defaultdict
from email.headerregistry import Address
from email.message import EmailMessage
from email.utils import getaddresses
from functools import partial
from urllib.parse import (
parse_qsl, quote, urlencode, urljoin, urlsplit, urlunsplit)
@@ -39,7 +37,7 @@ from trytond.pyson import Eval, If, PYSONDecoder, TimeDelta
from trytond.report import Report
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.tools.email_ import format_address, has_rcpt, set_from_header
from trytond.transaction import Transaction
from trytond.url import http_host
from trytond.wsgi import Base64Converter
@@ -652,9 +650,8 @@ class Activity(
party = record.marketing_party
contact = party.contact_mechanism_get('email')
if contact and contact.email:
return Address(
contact.name or party.rec_name,
addr_spec=contact.email)
return format_address(
contact.email, contact.name or party.rec_name)
def execute_send_email(
self, record_activity, smtpd_datamanager=None, **kwargs):
@@ -668,8 +665,6 @@ class Activity(
translated = self.__class__(self.id)
to = self._email_recipient(record.record)
if not to:
return
def unsubscribe(redirect):
parts = urlsplit(urljoin(
@@ -727,35 +722,14 @@ class Activity(
.filter(convert_href)
.render())
from_ = (config.get('marketing', 'email_from')
or config.get('email', 'from'))
msg = EmailMessage()
set_from_header(msg, from_, translated.email_from or from_)
msg['To'] = to
msg['Subject'] = title
if html2text:
converter = html2text.HTML2Text()
content_text = converter.handle(content)
msg.add_alternative(content_text, subtype='plain')
if msg.is_multipart():
msg.add_alternative(content, subtype='html')
else:
msg.set_content(content, subtype='html')
msg = self._email(translated.email_from, to, title, content)
to_addrs = [convert_ascii_email(a) for _, a in getaddresses([str(to)])]
if to_addrs:
if has_rcpt(msg):
send_message_transactional(msg, datamanager=smtpd_datamanager)
email = Email(
recipients=msg['To'],
recipients_secondary=msg['Cc'],
recipients_hidden=msg['Bcc'],
addresses=[{'address': a} for a in to_addrs],
subject=title,
resource=record.record,
email = Email.from_message(
msg, resource=record.record,
marketing_automation_activity=self,
marketing_automation_record=record,
)
marketing_automation_record=record)
email.save()
def email_context(self, record):
@@ -769,6 +743,23 @@ class Activity(
'datetime': datetime,
}
def _email(self, sender, to, subject, content):
msg = EmailMessage()
from_ = (config.get('marketing', 'email_from')
or config.get('email', 'from'))
set_from_header(msg, from_, sender or from_)
msg['To'] = to
msg['Subject'] = subject
if html2text:
converter = html2text.HTML2Text()
content_text = converter.handle(content)
msg.add_alternative(content_text, subtype='plain')
if msg.is_multipart():
msg.add_alternative(content, subtype='html')
else:
msg.set_content(content, subtype='html')
return msg
class Record(ModelSQL, ModelView):
"Marketing Record"
+13 -18
View File
@@ -1,7 +1,6 @@
# 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 mimetypes
from email.headerregistry import Address
from email.utils import getaddresses
from genshi.template import TextTemplate
@@ -13,7 +12,7 @@ from trytond.pool import Pool
from trytond.pyson import Eval, TimeDelta
from trytond.report import get_email
from trytond.sendmail import SMTPDataManager, send_message_transactional
from trytond.tools.email_ import set_from_header
from trytond.tools.email_ import format_address, has_rcpt, set_from_header
from trytond.transaction import Transaction
from .exceptions import TemplateError
@@ -157,7 +156,10 @@ class Email(ModelSQL, ModelView):
pool = Pool()
EmailTemplate = pool.get('ir.email.template')
with Transaction().set_context(usage=self.contact_mechanism):
return EmailTemplate.get_addresses(value)
# reformat addresses with encoding
return [
format_address(e, n)
for n, e in getaddresses(EmailTemplate.get_addresses(value))]
def _get_languages(self, value):
pool = Pool()
@@ -199,12 +201,12 @@ class Email(ModelSQL, ModelView):
filename=('utf-8', '', name))
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['To'] = to
msg['Cc'] = cc
msg['Bcc'] = bcc
msg['Subject'] = title
msg['Auto-Submitted'] = 'auto-generated'
return msg, title
return msg
@classmethod
def trigger(cls, records, trigger):
@@ -243,18 +245,11 @@ 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
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 = self.get_email(record, from_, to, cc, bcc, languages)
if has_rcpt(msg):
send_message_transactional(msg, datamanager=datamanager)
emails.append(Email(
recipients=msg['To'],
recipients_secondary=msg['Cc'],
recipients_hidden=msg['Bcc'],
addresses=[{'address': a} for a in to_addrs],
subject=title,
resource=record,
emails.append(Email.from_message(
msg, resource=record,
notification_email=trigger.notification_email,
notification_trigger=trigger))
Email.save(emails)
@@ -237,7 +237,7 @@ class NotificationEmailTestCase(CompanyTestMixin, ModuleTestCase):
user, = User.create([{'name': "Michael Scott", 'login': "msc"}])
msg, title = notification_email.get_email(
msg = notification_email.get_email(
user, FROM, ['Administrator <user@example.com>'], [], [], [en])
self.assertEqual(msg['From'], FROM)
@@ -275,7 +275,7 @@ class NotificationEmailTestCase(CompanyTestMixin, ModuleTestCase):
user, = User.create([{'name': "Michael Scott", 'login': "msc"}])
msg, title = notification_email.get_email(
msg = notification_email.get_email(
user, FROM, ['Administrator <user@example.com>'], [], [], [en])
self.assertEqual(msg['Subject'], 'Notification for Michael Scott')
@@ -311,7 +311,7 @@ class NotificationEmailTestCase(CompanyTestMixin, ModuleTestCase):
'language': es.id,
}])
msg, title = notification_email.get_email(
msg = notification_email.get_email(
user, FROM, ['Administrator <user@example.com>'], [], [], [es])
self.assertEqual(msg['Subject'], 'Notificación para Michael Scott')
+8
View File
@@ -8,6 +8,14 @@ Email
Fill email headers to appear at best from the address.
.. function:: has_rcpt(message)
Return if the :py:class:`~email.message.Message` has any recipient.
.. function:: format_address(email[, name])
Return a string suitable for an RFC 2822 From, To or Cc header.
.. function:: validate_email(email)
Validate the email address.
+19 -27
View File
@@ -4,7 +4,7 @@ import heapq
import mimetypes
import re
from email.message import EmailMessage
from email.utils import formataddr, getaddresses
from email.utils import getaddresses
try:
import html2text
@@ -22,7 +22,8 @@ from trytond.report import Report
from trytond.rpc import RPC
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.email_ import (
convert_ascii_email, format_address, set_from_header)
from trytond.tools.string_ import StringMatcher
from trytond.transaction import Transaction
@@ -43,11 +44,6 @@ class EmailTemplateError(ValidationError):
pass
def _get_emails(value):
"Return list of email from the comma separated list"
return [e for n, e in getaddresses([value]) if e]
def _formataddr(pair):
"Format address without encoding"
name, address = pair
@@ -158,30 +154,14 @@ class Email(ResourceAccessMixin, ModelSQL, ModelView):
filename=('utf-8', '', name))
from_ = config.get('email', 'from')
set_from_header(msg, from_, user.email or from_)
msg['To'] = ', '.join(
formataddr((n, convert_ascii_email(a)))
for n, a in getaddresses([to]))
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['To'] = [format_address(a, n) for n, a in getaddresses([to])]
msg['Cc'] = [format_address(a, n) for n, a in getaddresses([cc])]
msg['Bcc'] = [format_address(a, n) 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))))
send_message_transactional(msg, strict=True)
email = cls(
recipients=msg['To'],
recipients_secondary=msg['Cc'],
recipients_hidden=msg['Bcc'],
addresses=[{'address': a} for a in to_addrs],
subject=subject,
body=body,
resource=record)
email = cls.from_message(msg, body=body, resource=record)
email.save()
if files:
attachments_ = []
@@ -236,6 +216,18 @@ class Email(ResourceAccessMixin, ModelSQL, ModelView):
], order=[]):
yield user.name, user.email
@classmethod
def from_message(cls, msg, **values):
to_addrs = [e for _, e in getaddresses(
filter(None, (msg['To'], msg['Cc'], msg['Bcc'])))]
return cls(
recipients=msg['To'],
recipients_secondary=msg['Cc'],
recipients_hidden=msg['Bcc'],
addresses=[{'address': a} for a in to_addrs],
subject=msg['Subject'],
**values)
class EmailAddress(ModelSQL):
"Email Address"
+12 -1
View File
@@ -1,6 +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 email.utils import parseaddr
from email.header import Header
from email.utils import formataddr, parseaddr
from trytond.pool import Pool
@@ -29,6 +30,10 @@ def set_from_header(message, sender, from_):
message['From'] = from_
def has_rcpt(msg):
return any((msg['To'], msg['Cc'], msg['Bcc']))
try:
from dns.exception import DNSException
from email_validator import EmailNotValidError, caching_resolver
@@ -80,3 +85,9 @@ except ImportError:
class EmailNotValidError(Exception):
pass
def format_address(email, name=None):
if name:
name = str(Header(name, 'utf-8'))
return formataddr((name, convert_ascii_email(email)))