From 6c8f1c973d2728c498014e28bcc6e9fab40d0552 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 19 Dec 2024 18:34:03 -0600 Subject: [PATCH] fix: move `email` stuff from subpackage to module --- docs/api/wuttjamaican.email.handler.rst | 6 - docs/api/wuttjamaican.email.message.rst | 6 - docs/glossary.rst | 2 +- docs/index.rst | 2 - docs/narr/email/custom.rst | 5 +- src/wuttjamaican/app.py | 4 +- .../{email/handler.py => email.py} | 189 +++++++++++++++--- src/wuttjamaican/email/__init__.py | 33 --- src/wuttjamaican/email/message.py | 158 --------------- .../test_foo.html.mako | 0 .../test_foo.txt.mako | 0 tests/email/__init__.py | 0 tests/email/test_message.py | 76 ------- .../{email/test_handler.py => test_email.py} | 94 +++++++-- 14 files changed, 244 insertions(+), 331 deletions(-) delete mode 100644 docs/api/wuttjamaican.email.handler.rst delete mode 100644 docs/api/wuttjamaican.email.message.rst rename src/wuttjamaican/{email/handler.py => email.py} (75%) delete mode 100644 src/wuttjamaican/email/__init__.py delete mode 100644 src/wuttjamaican/email/message.py rename tests/{email/templates => email-templates}/test_foo.html.mako (100%) rename tests/{email/templates => email-templates}/test_foo.txt.mako (100%) delete mode 100644 tests/email/__init__.py delete mode 100644 tests/email/test_message.py rename tests/{email/test_handler.py => test_email.py} (85%) diff --git a/docs/api/wuttjamaican.email.handler.rst b/docs/api/wuttjamaican.email.handler.rst deleted file mode 100644 index 4e4900f..0000000 --- a/docs/api/wuttjamaican.email.handler.rst +++ /dev/null @@ -1,6 +0,0 @@ - -``wuttjamaican.email.handler`` -============================== - -.. automodule:: wuttjamaican.email.handler - :members: diff --git a/docs/api/wuttjamaican.email.message.rst b/docs/api/wuttjamaican.email.message.rst deleted file mode 100644 index 1656196..0000000 --- a/docs/api/wuttjamaican.email.message.rst +++ /dev/null @@ -1,6 +0,0 @@ - -``wuttjamaican.email.message`` -============================== - -.. automodule:: wuttjamaican.email.message - :members: diff --git a/docs/glossary.rst b/docs/glossary.rst index c3cd7a7..f696c66 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -189,7 +189,7 @@ Glossary The :term:`handler` responsible for sending email on behalf of the :term:`app`. - Default is :class:`~wuttjamaican.email.handler.EmailHandler`. + Default is :class:`~wuttjamaican.email.EmailHandler`. email key String idenfier for a certain :term:`email type`. Each email key diff --git a/docs/index.rst b/docs/index.rst index cb99327..ef8acce 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -82,8 +82,6 @@ Contents api/wuttjamaican.db.sess api/wuttjamaican.db.util api/wuttjamaican.email - api/wuttjamaican.email.handler - api/wuttjamaican.email.message api/wuttjamaican.enum api/wuttjamaican.exc api/wuttjamaican.install diff --git a/docs/narr/email/custom.rst b/docs/narr/email/custom.rst index f91d844..dac70fc 100644 --- a/docs/narr/email/custom.rst +++ b/docs/narr/email/custom.rst @@ -50,9 +50,8 @@ for one or both of the ``text/plain`` and ``text/html`` content-types (using ``txt`` and ``html`` as shorthand name, respectively). Template files must use the :doc:`Mako template language ` -and be named based on the -:attr:`~wuttjamaican.email.message.Message.key` for the email type, as -well as content-type. +and be named based on the :attr:`~wuttjamaican.email.Message.key` for +the email type, as well as content-type. Therefore a new email of type ``poser_alert_foo`` would need one or both of these defined: diff --git a/src/wuttjamaican/app.py b/src/wuttjamaican/app.py index 102d5db..becbf70 100644 --- a/src/wuttjamaican/app.py +++ b/src/wuttjamaican/app.py @@ -768,7 +768,7 @@ class AppHandler: See also :meth:`send_email()`. - :rtype: :class:`~wuttjamaican.email.handler.EmailHandler` + :rtype: :class:`~wuttjamaican.email.EmailHandler` """ if 'email' not in self.handlers: spec = self.config.get(f'{self.appname}.email.handler', @@ -823,7 +823,7 @@ class AppHandler: Send an email message. This is a convenience wrapper around - :meth:`~wuttjamaican.email.handler.EmailHandler.send_email()`. + :meth:`~wuttjamaican.email.EmailHandler.send_email()`. """ self.get_email_handler().send_email(*args, **kwargs) diff --git a/src/wuttjamaican/email/handler.py b/src/wuttjamaican/email.py similarity index 75% rename from src/wuttjamaican/email/handler.py rename to src/wuttjamaican/email.py index 519bac0..df4961e 100644 --- a/src/wuttjamaican/email/handler.py +++ b/src/wuttjamaican/email.py @@ -26,15 +26,146 @@ Email Handler import logging import smtplib +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText from wuttjamaican.app import GenericHandler from wuttjamaican.util import resource_path -from wuttjamaican.email.message import Message log = logging.getLogger(__name__) +class Message: + """ + Represents an email message to be sent. + + :param to: Recipient(s) for the message. This may be either a + string, or list of strings. If a string, it will be converted + to a list since that is how the :attr:`to` attribute tracks it. + Similar logic is used for :attr:`cc` and :attr:`bcc`. + + All attributes shown below may also be specified via constructor. + + .. attribute:: key + + Unique key indicating the "type" of message. An "ad-hoc" + message created arbitrarily may not have/need a key; however + one created via + :meth:`~wuttjamaican.email.EmailHandler.make_auto_message()` + will always have a key. + + This key is not used for anything within the ``Message`` class + logic. It is used by + :meth:`~wuttjamaican.email.EmailHandler.make_auto_message()` + when constructing the message, and the key is set on the final + message only as a reference. + + .. attribute:: sender + + Sender (``From:``) address for the message. + + .. attribute:: subject + + Subject text for the message. + + .. attribute:: to + + List of ``To:`` recipients for the message. + + .. attribute:: cc + + List of ``Cc:`` recipients for the message. + + .. attribute:: bcc + + List of ``Bcc:`` recipients for the message. + + .. attribute:: replyto + + Optional reply-to (``Reply-To:``) address for the message. + + .. attribute:: txt_body + + String with the ``text/plain`` body content. + + .. attribute:: html_body + + String with the ``text/html`` body content. + """ + + def __init__( + self, + key=None, + sender=None, + subject=None, + to=None, + cc=None, + bcc=None, + replyto=None, + txt_body=None, + html_body=None, + ): + self.key = key + self.sender = sender + self.subject = subject + self.set_recips('to', to) + self.set_recips('cc', cc) + self.set_recips('bcc', bcc) + self.replyto = replyto + self.txt_body = txt_body + self.html_body = html_body + + def set_recips(self, name, value): + """ """ + if value: + if isinstance(value, str): + value = [value] + if not isinstance(value, (list, tuple)): + raise ValueError("must specify a string, tuple or list value") + else: + value = [] + setattr(self, name, list(value)) + + def as_string(self): + """ + Returns the complete message as string. This is called from + within + :meth:`~wuttjamaican.email.EmailHandler.deliver_message()` to + obtain the SMTP payload. + """ + msg = None + + if self.txt_body and self.html_body: + txt = MIMEText(self.txt_body, _charset='utf_8') + html = MIMEText(self.html_body, _subtype='html', _charset='utf_8') + msg = MIMEMultipart(_subtype='alternative', _subparts=[txt, html]) + + elif self.txt_body: + msg = MIMEText(self.txt_body, _charset='utf_8') + + elif self.html_body: + msg = MIMEText(self.html_body, 'html', _charset='utf_8') + + if not msg: + raise ValueError("message has no body parts") + + msg['Subject'] = self.subject + msg['From'] = self.sender + + for addr in self.to: + msg['To'] = addr + for addr in self.cc: + msg['Cc'] = addr + for addr in self.bcc: + msg['Bcc'] = addr + + if self.replyto: + msg.add_header('Reply-To', self.replyto) + + return msg.as_string() + + class EmailHandler(GenericHandler): """ Base class and default implementation for the :term:`email @@ -89,7 +220,7 @@ class EmailHandler(GenericHandler): This is the "raw" factory which is simply a wrapper around the class constructor. See also :meth:`make_auto_message()`. - :returns: :class:`~wuttjamaican.email.message.Message` object. + :returns: :class:`~wuttjamaican.email.Message` object. """ return Message(**kwargs) @@ -112,7 +243,7 @@ class EmailHandler(GenericHandler): :param \**kwargs: Any remaining kwargs are passed as-is to :meth:`make_message()`. More on this below. - :returns: :class:`~wuttjamaican.email.message.Message` object. + :returns: :class:`~wuttjamaican.email.Message` object. This method may invoke some others, to gather the message attributes. Each will check config, or render a template, or @@ -147,8 +278,8 @@ class EmailHandler(GenericHandler): def get_auto_sender(self, key): """ Returns automatic - :attr:`~wuttjamaican.email.message.Message.sender` address for - a message, as determined by config. + :attr:`~wuttjamaican.email.Message.sender` address for a + message, as determined by config. """ # prefer configured sender specific to key sender = self.config.get(f'{self.config.appname}.email.{key}.sender') @@ -160,9 +291,8 @@ class EmailHandler(GenericHandler): def get_auto_subject(self, key, context={}, rendered=True): """ - Returns automatic - :attr:`~wuttjamaican.email.message.Message.subject` line for a - message, as determined by config. + Returns automatic :attr:`~wuttjamaican.email.Message.subject` + line for a message, as determined by config. This calls :meth:`get_auto_subject_template()` and then renders the result using the given context. @@ -200,25 +330,22 @@ class EmailHandler(GenericHandler): def get_auto_to(self, key): """ - Returns automatic - :attr:`~wuttjamaican.email.message.Message.to` recipient - address(es) for a message, as determined by config. + Returns automatic :attr:`~wuttjamaican.email.Message.to` + recipient address(es) for a message, as determined by config. """ return self.get_auto_recips(key, 'to') def get_auto_cc(self, key): """ - Returns automatic - :attr:`~wuttjamaican.email.message.Message.cc` recipient - address(es) for a message, as determined by config. + Returns automatic :attr:`~wuttjamaican.email.Message.cc` + recipient address(es) for a message, as determined by config. """ return self.get_auto_recips(key, 'cc') def get_auto_bcc(self, key): """ - Returns automatic - :attr:`~wuttjamaican.email.message.Message.bcc` recipient - address(es) for a message, as determined by config. + Returns automatic :attr:`~wuttjamaican.email.Message.bcc` + recipient address(es) for a message, as determined by config. """ return self.get_auto_recips(key, 'bcc') @@ -239,10 +366,9 @@ class EmailHandler(GenericHandler): def get_auto_txt_body(self, key, context={}): """ - Returns automatic - :attr:`~wuttjamaican.email.message.Message.txt_body` content - for a message, as determined by config. This renders a - template with the given context. + Returns automatic :attr:`~wuttjamaican.email.Message.txt_body` + content for a message, as determined by config. This renders + a template with the given context. """ template = self.get_auto_body_template(key, 'txt') if template: @@ -251,9 +377,9 @@ class EmailHandler(GenericHandler): def get_auto_html_body(self, key, context={}): """ Returns automatic - :attr:`~wuttjamaican.email.message.Message.html_body` content - for a message, as determined by config. This renders a - template with the given context. + :attr:`~wuttjamaican.email.Message.html_body` content for a + message, as determined by config. This renders a template + with the given context. """ template = self.get_auto_body_template(key, 'html') if template: @@ -327,10 +453,9 @@ class EmailHandler(GenericHandler): """ Deliver a message via SMTP smarthost. - :param message: Either a - :class:`~wuttjamaican.email.message.Message` object or - similar, or a string representing the complete message to - be sent as-is. + :param message: Either a :class:`~wuttjamaican.email.Message` + object or similar, or a string representing the complete + message to be sent as-is. :param sender: Optional sender address to use for delivery. If not specified, will be read from ``message``. @@ -339,10 +464,10 @@ class EmailHandler(GenericHandler): If not specified, will be read from ``message``. A general rule here is that you can either provide a proper - :class:`~wuttjamaican.email.message.Message` object, **or** - you *must* provide ``sender`` and ``recips``. The logic is - not smart enough (yet?) to parse sender/recips from a simple - string message. + :class:`~wuttjamaican.email.Message` object, **or** you *must* + provide ``sender`` and ``recips``. The logic is not smart + enough (yet?) to parse sender/recips from a simple string + message. Note also, this method does not (yet?) have robust error handling, so if an error occurs with the SMTP session, it will diff --git a/src/wuttjamaican/email/__init__.py b/src/wuttjamaican/email/__init__.py deleted file mode 100644 index 8702f9d..0000000 --- a/src/wuttjamaican/email/__init__.py +++ /dev/null @@ -1,33 +0,0 @@ -# -*- coding: utf-8; -*- -################################################################################ -# -# WuttJamaican -- Base package for Wutta Framework -# Copyright © 2023-2024 Lance Edgar -# -# This file is part of Wutta Framework. -# -# Wutta Framework is free software: you can redistribute it and/or modify it -# under the terms of the GNU General Public License as published by the Free -# Software Foundation, either version 3 of the License, or (at your option) any -# later version. -# -# Wutta Framework is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for -# more details. -# -# You should have received a copy of the GNU General Public License along with -# Wutta Framework. If not, see . -# -################################################################################ -""" -Email Utilities - -The following are available in this ``wuttjamaican.email`` namespace: - -* :class:`~wuttjamaican.email.handler.EmailHandler` -* :class:`~wuttjamaican.email.message.Message` -""" - -from .handler import EmailHandler -from .message import Message diff --git a/src/wuttjamaican/email/message.py b/src/wuttjamaican/email/message.py deleted file mode 100644 index 0e9f25e..0000000 --- a/src/wuttjamaican/email/message.py +++ /dev/null @@ -1,158 +0,0 @@ -# -*- coding: utf-8; -*- -################################################################################ -# -# WuttJamaican -- Base package for Wutta Framework -# Copyright © 2023-2024 Lance Edgar -# -# This file is part of Wutta Framework. -# -# Wutta Framework is free software: you can redistribute it and/or modify it -# under the terms of the GNU General Public License as published by the Free -# Software Foundation, either version 3 of the License, or (at your option) any -# later version. -# -# Wutta Framework is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for -# more details. -# -# You should have received a copy of the GNU General Public License along with -# Wutta Framework. If not, see . -# -################################################################################ -""" -Email Message -""" - -from email.mime.multipart import MIMEMultipart -from email.mime.text import MIMEText - - -class Message: - """ - Represents an email message to be sent. - - :param to: Recipient(s) for the message. This may be either a - string, or list of strings. If a string, it will be converted - to a list since that is how the :attr:`to` attribute tracks it. - Similar logic is used for :attr:`cc` and :attr:`bcc`. - - All attributes shown below may also be specified via constructor. - - .. attribute:: key - - Unique key indicating the "type" of message. An "ad-hoc" - message created arbitrarily may not have/need a key; however - one created via - :meth:`~wuttjamaican.email.handler.EmailHandler.make_auto_message()` - will always have a key. - - This key is not used for anything within the ``Message`` class - logic. It is used by - :meth:`~wuttjamaican.email.handler.EmailHandler.make_auto_message()` - when constructing the message, and the key is set on the final - message only as a reference. - - .. attribute:: sender - - Sender (``From:``) address for the message. - - .. attribute:: subject - - Subject text for the message. - - .. attribute:: to - - List of ``To:`` recipients for the message. - - .. attribute:: cc - - List of ``Cc:`` recipients for the message. - - .. attribute:: bcc - - List of ``Bcc:`` recipients for the message. - - .. attribute:: replyto - - Optional reply-to (``Reply-To:``) address for the message. - - .. attribute:: txt_body - - String with the ``text/plain`` body content. - - .. attribute:: html_body - - String with the ``text/html`` body content. - """ - - def __init__( - self, - key=None, - sender=None, - subject=None, - to=None, - cc=None, - bcc=None, - replyto=None, - txt_body=None, - html_body=None, - ): - self.key = key - self.sender = sender - self.subject = subject - self.set_recips('to', to) - self.set_recips('cc', cc) - self.set_recips('bcc', bcc) - self.replyto = replyto - self.txt_body = txt_body - self.html_body = html_body - - def set_recips(self, name, value): - """ """ - if value: - if isinstance(value, str): - value = [value] - if not isinstance(value, (list, tuple)): - raise ValueError("must specify a string, tuple or list value") - else: - value = [] - setattr(self, name, list(value)) - - def as_string(self): - """ - Returns the complete message as string. This is called from - within - :meth:`~wuttjamaican.email.handler.EmailHandler.deliver_message()` - to obtain the SMTP payload. - """ - msg = None - - if self.txt_body and self.html_body: - txt = MIMEText(self.txt_body, _charset='utf_8') - html = MIMEText(self.html_body, _subtype='html', _charset='utf_8') - msg = MIMEMultipart(_subtype='alternative', _subparts=[txt, html]) - - elif self.txt_body: - msg = MIMEText(self.txt_body, _charset='utf_8') - - elif self.html_body: - msg = MIMEText(self.html_body, 'html', _charset='utf_8') - - if not msg: - raise ValueError("message has no body parts") - - msg['Subject'] = self.subject - msg['From'] = self.sender - - for addr in self.to: - msg['To'] = addr - for addr in self.cc: - msg['Cc'] = addr - for addr in self.bcc: - msg['Bcc'] = addr - - if self.replyto: - msg.add_header('Reply-To', self.replyto) - - return msg.as_string() diff --git a/tests/email/templates/test_foo.html.mako b/tests/email-templates/test_foo.html.mako similarity index 100% rename from tests/email/templates/test_foo.html.mako rename to tests/email-templates/test_foo.html.mako diff --git a/tests/email/templates/test_foo.txt.mako b/tests/email-templates/test_foo.txt.mako similarity index 100% rename from tests/email/templates/test_foo.txt.mako rename to tests/email-templates/test_foo.txt.mako diff --git a/tests/email/__init__.py b/tests/email/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/email/test_message.py b/tests/email/test_message.py deleted file mode 100644 index f8ff67a..0000000 --- a/tests/email/test_message.py +++ /dev/null @@ -1,76 +0,0 @@ -# -*- coding: utf-8; -*- - -from unittest import TestCase - -from wuttjamaican.email import message as mod - - -class TestMessage(TestCase): - - def make_message(self, **kwargs): - return mod.Message(**kwargs) - - def test_set_recips(self): - msg = self.make_message() - self.assertEqual(msg.to, []) - - # set as list - msg.set_recips('to', ['sally@example.com']) - self.assertEqual(msg.to, ['sally@example.com']) - - # set as tuple - msg.set_recips('to', ('barney@example.com',)) - self.assertEqual(msg.to, ['barney@example.com']) - - # set as string - msg.set_recips('to', 'wilma@example.com') - self.assertEqual(msg.to, ['wilma@example.com']) - - # set as null - msg.set_recips('to', None) - self.assertEqual(msg.to, []) - - # otherwise error - self.assertRaises(ValueError, msg.set_recips, 'to', {'foo': 'foo@example.com'}) - - def test_as_string(self): - - # error if no body - msg = self.make_message() - self.assertRaises(ValueError, msg.as_string) - - # txt body - msg = self.make_message(sender='bob@example.com', - txt_body="hello world") - complete = msg.as_string() - self.assertIn('From: bob@example.com', complete) - - # html body - msg = self.make_message(sender='bob@example.com', - html_body="

hello world

") - complete = msg.as_string() - self.assertIn('From: bob@example.com', complete) - - # txt + html body - msg = self.make_message(sender='bob@example.com', - txt_body="hello world", - html_body="

hello world

") - complete = msg.as_string() - self.assertIn('From: bob@example.com', complete) - - # everything - msg = self.make_message(sender='bob@example.com', - subject='meeting follow-up', - to='sally@example.com', - cc='marketing@example.com', - bcc='bob@example.com', - replyto='sales@example.com', - txt_body="hello world", - html_body="

hello world

") - complete = msg.as_string() - self.assertIn('From: bob@example.com', complete) - self.assertIn('Subject: meeting follow-up', complete) - self.assertIn('To: sally@example.com', complete) - self.assertIn('Cc: marketing@example.com', complete) - self.assertIn('Bcc: bob@example.com', complete) - self.assertIn('Reply-To: sales@example.com', complete) diff --git a/tests/email/test_handler.py b/tests/test_email.py similarity index 85% rename from tests/email/test_handler.py rename to tests/test_email.py index 8432144..2d5272b 100644 --- a/tests/email/test_handler.py +++ b/tests/test_email.py @@ -5,8 +5,7 @@ from unittest.mock import patch, MagicMock import pytest -from wuttjamaican.email import handler as mod -from wuttjamaican.email import Message +from wuttjamaican import email as mod from wuttjamaican.conf import WuttaConfig from wuttjamaican.util import resource_path from wuttjamaican.exc import ConfigurationError @@ -36,28 +35,28 @@ class TestEmailHandler(TestCase): # provider may specify paths as list providers = { - 'wuttatest': MagicMock(email_templates=['wuttjamaican.email:templates']), + 'wuttatest': MagicMock(email_templates=['wuttjamaican:email-templates']), } with patch.object(self.app, 'providers', new=providers): handler = self.make_handler() - path = resource_path('wuttjamaican.email:templates') + path = resource_path('wuttjamaican:email-templates') self.assertEqual(handler.txt_templates.directories, [path]) self.assertEqual(handler.html_templates.directories, [path]) # provider may specify paths as string providers = { - 'wuttatest': MagicMock(email_templates='wuttjamaican.email:templates'), + 'wuttatest': MagicMock(email_templates='wuttjamaican:email-templates'), } with patch.object(self.app, 'providers', new=providers): handler = self.make_handler() - path = resource_path('wuttjamaican.email:templates') + path = resource_path('wuttjamaican:email-templates') self.assertEqual(handler.txt_templates.directories, [path]) self.assertEqual(handler.html_templates.directories, [path]) def test_make_message(self): handler = self.make_handler() msg = handler.make_message() - self.assertIsInstance(msg, Message) + self.assertIsInstance(msg, mod.Message) def test_make_auto_message(self): handler = self.make_handler() @@ -70,7 +69,7 @@ class TestEmailHandler(TestCase): # message is empty by default msg = handler.make_auto_message('foo') - self.assertIsInstance(msg, Message) + self.assertIsInstance(msg, mod.Message) self.assertEqual(msg.key, 'foo') self.assertEqual(msg.sender, 'bob@example.com') self.assertEqual(msg.subject, "Automated message") @@ -85,7 +84,7 @@ class TestEmailHandler(TestCase): # then we should get back a more complete message self.config.setdefault('wutta.email.test_foo.subject', "hello foo") self.config.setdefault('wutta.email.test_foo.to', 'sally@example.com') - self.config.setdefault('wutta.email.templates', 'tests.email:templates') + self.config.setdefault('wutta.email.templates', 'tests:email-templates') handler = self.make_handler() msg = handler.make_auto_message('test_foo') self.assertEqual(msg.key, 'test_foo') @@ -240,7 +239,7 @@ class TestEmailHandler(TestCase): # but returns a template if it exists providers = { - 'wuttatest': MagicMock(email_templates=['tests.email:templates']), + 'wuttatest': MagicMock(email_templates=['tests:email-templates']), } with patch.object(self.app, 'providers', new=providers): handler = self.make_handler() @@ -257,7 +256,7 @@ class TestEmailHandler(TestCase): # but returns body if template exists providers = { - 'wuttatest': MagicMock(email_templates=['tests.email:templates']), + 'wuttatest': MagicMock(email_templates=['tests:email-templates']), } with patch.object(self.app, 'providers', new=providers): handler = self.make_handler() @@ -273,7 +272,7 @@ class TestEmailHandler(TestCase): # but returns body if template exists providers = { - 'wuttatest': MagicMock(email_templates=['tests.email:templates']), + 'wuttatest': MagicMock(email_templates=['tests:email-templates']), } with patch.object(self.app, 'providers', new=providers): handler = self.make_handler() @@ -456,3 +455,74 @@ class TestEmailHandler(TestCase): self.config.setdefault('wutta.email.default.enabled', False) handler.send_email('bar', sender='bar@example.com', txt_body="hello world") self.assertFalse(deliver_message.called) + + +class TestMessage(TestCase): + + def make_message(self, **kwargs): + return mod.Message(**kwargs) + + def test_set_recips(self): + msg = self.make_message() + self.assertEqual(msg.to, []) + + # set as list + msg.set_recips('to', ['sally@example.com']) + self.assertEqual(msg.to, ['sally@example.com']) + + # set as tuple + msg.set_recips('to', ('barney@example.com',)) + self.assertEqual(msg.to, ['barney@example.com']) + + # set as string + msg.set_recips('to', 'wilma@example.com') + self.assertEqual(msg.to, ['wilma@example.com']) + + # set as null + msg.set_recips('to', None) + self.assertEqual(msg.to, []) + + # otherwise error + self.assertRaises(ValueError, msg.set_recips, 'to', {'foo': 'foo@example.com'}) + + def test_as_string(self): + + # error if no body + msg = self.make_message() + self.assertRaises(ValueError, msg.as_string) + + # txt body + msg = self.make_message(sender='bob@example.com', + txt_body="hello world") + complete = msg.as_string() + self.assertIn('From: bob@example.com', complete) + + # html body + msg = self.make_message(sender='bob@example.com', + html_body="

hello world

") + complete = msg.as_string() + self.assertIn('From: bob@example.com', complete) + + # txt + html body + msg = self.make_message(sender='bob@example.com', + txt_body="hello world", + html_body="

hello world

") + complete = msg.as_string() + self.assertIn('From: bob@example.com', complete) + + # everything + msg = self.make_message(sender='bob@example.com', + subject='meeting follow-up', + to='sally@example.com', + cc='marketing@example.com', + bcc='bob@example.com', + replyto='sales@example.com', + txt_body="hello world", + html_body="

hello world

") + complete = msg.as_string() + self.assertIn('From: bob@example.com', complete) + self.assertIn('Subject: meeting follow-up', complete) + self.assertIn('To: sally@example.com', complete) + self.assertIn('Cc: marketing@example.com', complete) + self.assertIn('Bcc: bob@example.com', complete) + self.assertIn('Reply-To: sales@example.com', complete)