From 53ebddd6949e8e7fffb701a8a2c2ccbb3a431c49 Mon Sep 17 00:00:00 2001 From: Sergey Lavrinenko Date: Fri, 24 Jul 2015 22:09:06 +0300 Subject: [PATCH 1/2] Message.send supports recipients list --- emails/backend/inmemory/__init__.py | 36 ++++++++++++++++++++++++ emails/message.py | 35 +++++++++++++---------- emails/testsuite/message/test_message.py | 11 ++++++++ emails/utils.py | 18 ++++++++++++ 4 files changed, 85 insertions(+), 15 deletions(-) create mode 100644 emails/backend/inmemory/__init__.py diff --git a/emails/backend/inmemory/__init__.py b/emails/backend/inmemory/__init__.py new file mode 100644 index 0000000..9d1e8c5 --- /dev/null +++ b/emails/backend/inmemory/__init__.py @@ -0,0 +1,36 @@ +# encoding: utf-8 +from __future__ import unicode_literals + +__all__ = ['InMemoryBackend', ] + +import logging + + +class InMemoryBackend(object): + + """ + InMemoryBackend store message in memory for testing purposes. + """ + + def __init__(self, **kwargs): + self.kwargs = kwargs + self.messages = {} + + def sendmail(self, from_addr, to_addrs, msg, **kwargs): + + logging.debug('InMemoryBackend.sendmail(%s, %s, %r, %s)', from_addr, to_addrs, msg, kwargs) + + if not to_addrs: + return None + + if not isinstance(to_addrs, (list, tuple)): + to_addrs = [to_addrs, ] + + for addr in to_addrs: + data = dict(from_addr=from_addr, + message=msg.as_string(), + source_message=msg, + **kwargs) + self.messages.setdefault(addr.lower(), []).append(data) + + return True diff --git a/emails/message.py b/emails/message.py index cca653f..36d3b7b 100644 --- a/emails/message.py +++ b/emails/message.py @@ -7,7 +7,8 @@ from .compat import (string_types, is_callable, formataddr as compat_formataddr, from .utils import (SafeMIMEText, SafeMIMEMultipart, sanitize_address, parse_name_and_email, load_email_charsets, encode_header as encode_header_, - renderable, format_date_header, parse_name_and_email_list) + renderable, format_date_header, parse_name_and_email_list, + cached_property) from .exc import BadHeaderError from .backend import ObjectFactory, SMTPBackend from .store import MemoryFileStore, BaseFile @@ -73,6 +74,13 @@ class BaseMessage(object): mail_to = property(get_mail_to, set_mail_to) + def get_recipients_emails(self): + """ + Returns message recipient's emails for actual sending. + :return: + """ + return [a[1] for a in self._mail_to] + def set_headers(self, headers): self._headers = headers or {} @@ -312,12 +320,9 @@ class MessageSendMixin(object): smtp_pool_factory = ObjectFactory smtp_cls = SMTPBackend - @property + @cached_property def smtp_pool(self): - pool = getattr(self, '_smtp_pool', None) - if pool is None: - pool = self._smtp_pool = self.smtp_pool_factory(cls=self.smtp_cls) - return pool + return self.smtp_pool_factory(cls=self.smtp_cls) def send(self, to=None, @@ -342,17 +347,17 @@ class MessageSendMixin(object): raise ValueError( "smtp must be a dict or an object with method 'sendmail'. got %s" % type(smtp)) - mail_to = to - if mail_to: - mail_to = parse_name_and_email(mail_to) - to_addr = mail_to[1] + to_addrs = None + + if to: if set_mail_to: - self.set_mail_to(mail_to) + self.set_mail_to(to) + else: + to_addrs = [a[1] for a in parse_name_and_email_list(to)] - else: - to_addr = self._mail_to[0][1] + to_addrs = to_addrs or self.get_recipients_emails() - if not to_addr: + if not to_addrs: raise ValueError('No to-addr') if mail_from: @@ -368,7 +373,7 @@ class MessageSendMixin(object): if not from_addr: raise ValueError('No "from" addr') - params = dict(from_addr=from_addr, to_addrs=[to_addr, ], msg=self, + params = dict(from_addr=from_addr, to_addrs=to_addrs, msg=self, mail_options=smtp_mail_options, rcpt_options=smtp_rcpt_options) return smtp.sendmail(**params) diff --git a/emails/testsuite/message/test_message.py b/emails/testsuite/message/test_message.py index 3a16434..eda90c3 100644 --- a/emails/testsuite/message/test_message.py +++ b/emails/testsuite/message/test_message.py @@ -10,6 +10,7 @@ from emails import Message import emails.exc from emails.compat import to_unicode, StringIO, is_py2, is_py34_plus from emails.utils import decode_header, MessageID +from emails.backend.inmemory import InMemoryBackend from .helpers import common_email_data @@ -172,6 +173,9 @@ def test_message_id(): def test_several_recipients_in_to_header(): + + # Test multiple recipients in "To" header + params = dict(html='...', mail_from='a@b.c') m = Message(mail_to=['d@e.f', 'g@h.i'], **params) @@ -180,3 +184,10 @@ def test_several_recipients_in_to_header(): m = Message(mail_to=[('♡', 'd@e.f'), ('웃', 'g@h.i')], **params) assert m.as_message()['To'] == '=?utf-8?b?4pmh?= , =?utf-8?b?7JuD?= ' + # Test sending to several emails + + backend = InMemoryBackend() + m = Message(mail_to=[('♡', 'd@e.f'), ('웃', 'g@h.i')], **params) + m.send(smtp=backend) + assert len(backend.messages['d@e.f']) == 1 + assert len(backend.messages['g@h.i']) == 1 diff --git a/emails/utils.py b/emails/utils.py index 67d2985..46a5abd 100644 --- a/emails/utils.py +++ b/emails/utils.py @@ -41,6 +41,24 @@ def load_email_charsets(): charset) +class cached_property(object): + """ + A property that is only computed once per instance and then replaces itself + with an ordinary attribute. Deleting the attribute resets the property. + Source: https://github.com/bottlepy/bottle/commit/fa7733e075da0d790d809aa3d2f53071897e6f76 + """ # noqa + + def __init__(self, func): + self.__doc__ = getattr(func, '__doc__') + self.func = func + + def __get__(self, obj, cls): + if obj is None: + return self + value = obj.__dict__[self.func.__name__] = self.func(obj) + return value + + # Django's CachedDnsName: # Cached the hostname, but do it lazily: socket.getfqdn() can take a couple of # seconds, which slows down the restart of the server. From 433293636e656a87e150f35d13e280ef7fb81054 Mon Sep 17 00:00:00 2001 From: Sergey Lavrinenko Date: Fri, 24 Jul 2015 23:13:00 +0300 Subject: [PATCH 2/2] Message supports cc and bcc --- emails/message.py | 34 ++++++++++++++++++++---- emails/testsuite/message/test_message.py | 17 ++++++------ 2 files changed, 38 insertions(+), 13 deletions(-) diff --git a/emails/message.py b/emails/message.py index 36d3b7b..df35bde 100644 --- a/emails/message.py +++ b/emails/message.py @@ -38,7 +38,9 @@ class BaseMessage(object): headers=None, html=None, text=None, - attachments=None): + attachments=None, + cc=None, + bcc=None): self._attachments = None self.charset = charset or 'utf-8' # utf-8 is standard de-facto, yeah @@ -47,6 +49,8 @@ class BaseMessage(object): self.set_date(date) self.set_mail_from(mail_from) self.set_mail_to(mail_to) + self.set_cc(cc) + self.set_bcc(bcc) self.set_headers(headers) self.set_html(html=html) self.set_text(text=text) @@ -74,12 +78,28 @@ class BaseMessage(object): mail_to = property(get_mail_to, set_mail_to) + def set_cc(self, addr): + self._cc = parse_name_and_email_list(addr) + + def get_cc(self): + return self._cc + + cc = property(get_cc, set_cc) + + def set_bcc(self, addr): + self._bcc = parse_name_and_email_list(addr) + + def get_bcc(self): + return self._bcc + + bcc = property(get_bcc, set_bcc) + def get_recipients_emails(self): """ Returns message recipient's emails for actual sending. - :return: + :return: list of emails """ - return [a[1] for a in self._mail_to] + return list(set([a[1] for a in self._mail_to] + [a[1] for a in self._cc] + [a[1] for a in self._bcc])) def set_headers(self, headers): self._headers = headers or {} @@ -232,8 +252,12 @@ class MessageBuildMixin(object): self.set_header(msg, 'Subject', subject) self.set_header(msg, 'From', self.encode_address_header(self._mail_from), encode=False) - self.set_header(msg, 'To', self._mail_to and ", ".join([self.encode_address_header(addr) - for addr in self._mail_to]) or None, encode=False) + + if self._mail_to: + self.set_header(msg, 'To', ", ".join([self.encode_address_header(addr) for addr in self._mail_to]), encode=False) + + if self._cc: + self.set_header(msg, 'Cc', ", ".join([self.encode_address_header(addr) for addr in self._cc]), encode=False) return msg diff --git a/emails/testsuite/message/test_message.py b/emails/testsuite/message/test_message.py index eda90c3..30561b0 100644 --- a/emails/testsuite/message/test_message.py +++ b/emails/testsuite/message/test_message.py @@ -172,22 +172,23 @@ def test_message_id(): assert m.as_message()['Message-ID'] == 'XXX' -def test_several_recipients_in_to_header(): +def test_several_recipients(): # Test multiple recipients in "To" header params = dict(html='...', mail_from='a@b.c') - m = Message(mail_to=['d@e.f', 'g@h.i'], **params) - assert m.as_message()['To'] == 'd@e.f, g@h.i' + m = Message(mail_to=['a@x.z', 'b@x.z'], cc='c@x.z', **params) + assert m.as_message()['To'] == 'a@x.z, b@x.z' + assert m.as_message()['cc'] == 'c@x.z' - m = Message(mail_to=[('♡', 'd@e.f'), ('웃', 'g@h.i')], **params) - assert m.as_message()['To'] == '=?utf-8?b?4pmh?= , =?utf-8?b?7JuD?= ' + m = Message(mail_to=[('♡', 'a@x.z'), ('웃', 'b@x.z')], **params) + assert m.as_message()['To'] == '=?utf-8?b?4pmh?= , =?utf-8?b?7JuD?= ' # Test sending to several emails backend = InMemoryBackend() - m = Message(mail_to=[('♡', 'd@e.f'), ('웃', 'g@h.i')], **params) + m = Message(mail_to=[('♡', 'a@x.z'), ('웃', 'b@x.z')], cc=['c@x.z', 'b@x.z'], bcc=['c@x.z', 'd@x.z'], **params) m.send(smtp=backend) - assert len(backend.messages['d@e.f']) == 1 - assert len(backend.messages['g@h.i']) == 1 + for addr in ['a@x.z', 'b@x.z', 'c@x.z', 'd@x.z']: + assert len(backend.messages[addr]) == 1