feat: add "email settings" feature for admin, previews
This commit is contained in:
parent
6c8f1c973d
commit
491df09f2f
|
@ -196,6 +196,20 @@ Glossary
|
|||
must be unique across the app, so the correct template files and
|
||||
other settings are used when sending etc.
|
||||
|
||||
email module
|
||||
This refers to a Python module which contains :term:`email
|
||||
setting` definitions.
|
||||
|
||||
email setting
|
||||
This refers to the settings for a particular :term:`email type`,
|
||||
i.e. its sender and recipients, subject etc. So each email type
|
||||
has a "collection" of settings, and that collection is referred
|
||||
to simply as an "email setting" in the singular.
|
||||
|
||||
email template
|
||||
Usually this refers to the HTML or TXT template file, used to
|
||||
render the message body when sending an email.
|
||||
|
||||
email type
|
||||
The :term:`app` is capable of sending many types of emails,
|
||||
e.g. daily reports, alerts of various kinds etc. Each "type" of
|
||||
|
|
|
@ -837,7 +837,7 @@ class AppProvider:
|
|||
|
||||
:param config: The app :term:`config object`.
|
||||
|
||||
Instances have the following attributes:
|
||||
``AppProvider`` instances have the following attributes:
|
||||
|
||||
.. attribute:: config
|
||||
|
||||
|
@ -846,6 +846,29 @@ class AppProvider:
|
|||
.. attribute:: app
|
||||
|
||||
Reference to the parent app handler.
|
||||
|
||||
Some things which a subclass may define, in order to register
|
||||
various features with the app:
|
||||
|
||||
.. attribute:: email_modules
|
||||
|
||||
List of :term:`email modules <email module>` provided. Should
|
||||
be a list of strings; each is a dotted module path, e.g.::
|
||||
|
||||
email_modules = ['poser.emails']
|
||||
|
||||
.. attribute:: email_templates
|
||||
|
||||
List of :term:`email template` folders provided. Can be a list
|
||||
of paths, or a single path as string::
|
||||
|
||||
email_templates = ['poser:templates/email']
|
||||
|
||||
email_templates = 'poser:templates/email'
|
||||
|
||||
Note the syntax, which specifies python module, then colon
|
||||
(``:``), then filesystem path below that. However absolute
|
||||
file paths may be used as well, when applicable.
|
||||
"""
|
||||
|
||||
def __init__(self, config):
|
||||
|
|
|
@ -24,11 +24,16 @@
|
|||
Email Handler
|
||||
"""
|
||||
|
||||
import importlib
|
||||
import logging
|
||||
import smtplib
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
|
||||
from mako.lookup import TemplateLookup
|
||||
from mako.template import Template
|
||||
from mako.exceptions import TopLevelLookupException
|
||||
|
||||
from wuttjamaican.app import GenericHandler
|
||||
from wuttjamaican.util import resource_path
|
||||
|
||||
|
@ -36,6 +41,75 @@ from wuttjamaican.util import resource_path
|
|||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EmailSetting:
|
||||
"""
|
||||
Base class for all :term:`email settings <email setting>`.
|
||||
|
||||
Each :term:`email type` which needs to have settings exposed
|
||||
e.g. for editing, should define a subclass within the appropriate
|
||||
:term:`email module`.
|
||||
|
||||
The name of each subclass should match the :term:`email key` which
|
||||
it represents. For instance::
|
||||
|
||||
from wuttjamaican.email import EmailSetting
|
||||
|
||||
class poser_alert_foo(EmailSetting):
|
||||
\"""
|
||||
Sent when something happens that we think deserves an alert.
|
||||
\"""
|
||||
|
||||
default_subject = "Something happened!"
|
||||
|
||||
def sample_data(self):
|
||||
return {
|
||||
'foo': 1234,
|
||||
'msg': "Something happened, thought you should know.",
|
||||
}
|
||||
|
||||
# (and elsewhere..)
|
||||
app.send_email('poser_alert_foo', {
|
||||
'foo': 5678,
|
||||
'msg': "Can't take much more, she's gonna blow!",
|
||||
})
|
||||
|
||||
Defining a subclass for each email type can be a bit tedious, so
|
||||
why do it? In fact there is no need, if you just want to *send*
|
||||
emails.
|
||||
|
||||
The purpose of defining a subclass for each email type is 2-fold,
|
||||
but really the answer is "for maintenance sake" -
|
||||
|
||||
* gives the app a way to discover all emails, so settings for each
|
||||
can be exposed for editing
|
||||
* allows for hard-coded sample context which can be used to render
|
||||
templates for preview
|
||||
|
||||
.. attribute:: default_subject
|
||||
|
||||
Default :attr:`Message.subject` for the email, if none is
|
||||
configured.
|
||||
|
||||
This is technically a Mako template string, so it will be
|
||||
rendered with the email context. But in most cases that
|
||||
feature can be ignored, and this will be a simple string.
|
||||
"""
|
||||
default_subject = None
|
||||
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
self.app = config.get_app()
|
||||
self.key = self.__class__.__name__
|
||||
|
||||
def sample_data(self):
|
||||
"""
|
||||
Should return a dict with sample context needed to render the
|
||||
:term:`email template` for message body. This can be used to
|
||||
show a "preview" of the email.
|
||||
"""
|
||||
return {}
|
||||
|
||||
|
||||
class Message:
|
||||
"""
|
||||
Represents an email message to be sent.
|
||||
|
@ -185,8 +259,6 @@ class EmailHandler(GenericHandler):
|
|||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
from mako.lookup import TemplateLookup
|
||||
|
||||
# prefer configured list of template lookup paths, if set
|
||||
templates = self.config.get_list(f'{self.config.appname}.email.templates')
|
||||
if not templates:
|
||||
|
@ -213,6 +285,67 @@ class EmailHandler(GenericHandler):
|
|||
# TODO: sounds great but i forget why?
|
||||
default_filters=['h'])
|
||||
|
||||
def get_email_modules(self):
|
||||
"""
|
||||
Returns a list of all known :term:`email modules <email
|
||||
module>`.
|
||||
|
||||
This will discover all email modules exposed by the
|
||||
:term:`app`, and/or its :term:`providers <provider>`.
|
||||
"""
|
||||
if not hasattr(self, '_email_modules'):
|
||||
self._email_modules = []
|
||||
for provider in self.app.providers.values():
|
||||
if hasattr(provider, 'email_modules'):
|
||||
modules = provider.email_modules
|
||||
if modules:
|
||||
if isinstance(modules, str):
|
||||
modules = [modules]
|
||||
for module in modules:
|
||||
module = importlib.import_module(module)
|
||||
self._email_modules.append(module)
|
||||
|
||||
return self._email_modules
|
||||
|
||||
def get_email_settings(self):
|
||||
"""
|
||||
Returns a dict of all known :term:`email settings <email
|
||||
setting>`, keyed by :term:`email key`.
|
||||
|
||||
This calls :meth:`get_email_modules()` and for each module, it
|
||||
discovers all the email settings it contains.
|
||||
"""
|
||||
if not hasattr(self, '_email_settings'):
|
||||
self._email_settings = {}
|
||||
for module in self.get_email_modules():
|
||||
for name in dir(module):
|
||||
obj = getattr(module, name)
|
||||
if (isinstance(obj, type)
|
||||
and obj is not EmailSetting
|
||||
and issubclass(obj, EmailSetting)):
|
||||
self._email_settings[obj.__name__] = obj
|
||||
|
||||
return self._email_settings
|
||||
|
||||
def get_email_setting(self, key, instance=True):
|
||||
"""
|
||||
Retrieve the :term:`email setting` for the given :term:`email
|
||||
key` (if it exists).
|
||||
|
||||
:param key: Key for the :term:`email type`.
|
||||
|
||||
:param instance: Whether to return the class, or an instance.
|
||||
|
||||
:returns: :class:`EmailSetting` class or instance, or ``None``
|
||||
if the setting could not be found.
|
||||
"""
|
||||
settings = self.get_email_settings()
|
||||
if key in settings:
|
||||
setting = settings[key]
|
||||
if instance:
|
||||
setting = setting(self.config)
|
||||
return setting
|
||||
|
||||
def make_message(self, **kwargs):
|
||||
"""
|
||||
Make and return a new email message.
|
||||
|
@ -289,26 +422,49 @@ class EmailHandler(GenericHandler):
|
|||
# fall back to global default (required!)
|
||||
return self.config.require(f'{self.config.appname}.email.default.sender')
|
||||
|
||||
def get_auto_subject(self, key, context={}, rendered=True):
|
||||
def get_auto_replyto(self, key):
|
||||
"""
|
||||
Returns automatic :attr:`~wuttjamaican.email.Message.replyto`
|
||||
address for a message, as determined by config.
|
||||
"""
|
||||
# prefer configured replyto specific to key
|
||||
replyto = self.config.get(f'{self.config.appname}.email.{key}.replyto')
|
||||
if replyto:
|
||||
return replyto
|
||||
|
||||
# fall back to global default, if present
|
||||
return self.config.get(f'{self.config.appname}.email.default.replyto')
|
||||
|
||||
def get_auto_subject(self, key, context={}, rendered=True, setting=None):
|
||||
"""
|
||||
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.
|
||||
(usually) renders the result using the given context.
|
||||
|
||||
:param key: Key for the :term:`email type`.
|
||||
|
||||
:param context: Dict of context for rendering the subject
|
||||
template, if applicable.
|
||||
|
||||
:param rendered: If this is ``False``, the "raw" subject
|
||||
template will be returned, instead of the final/rendered
|
||||
subject text.
|
||||
"""
|
||||
from mako.template import Template
|
||||
|
||||
template = self.get_auto_subject_template(key)
|
||||
:param setting: Optional :class:`EmailSetting` class or
|
||||
instance. This is passed along to
|
||||
:meth:`get_auto_subject_template()`.
|
||||
|
||||
:returns: Final subject text, either "raw" or rendered.
|
||||
"""
|
||||
template = self.get_auto_subject_template(key, setting=setting)
|
||||
if not rendered:
|
||||
return template
|
||||
|
||||
return Template(template).render(**context)
|
||||
|
||||
def get_auto_subject_template(self, key):
|
||||
def get_auto_subject_template(self, key, setting=None):
|
||||
"""
|
||||
Returns the template string to use for automatic subject line
|
||||
of a message, as determined by config.
|
||||
|
@ -318,12 +474,28 @@ class EmailHandler(GenericHandler):
|
|||
|
||||
The template returned from this method is used to render the
|
||||
final subject line in :meth:`get_auto_subject()`.
|
||||
|
||||
:param key: Key for the :term:`email type`.
|
||||
|
||||
:param setting: Optional :class:`EmailSetting` class or
|
||||
instance. This may be used to determine the "default"
|
||||
subject if none is configured. You can specify this as an
|
||||
optimization; otherwise it will be fetched if needed via
|
||||
:meth:`get_email_setting()`.
|
||||
|
||||
:returns: Final subject template, as raw text.
|
||||
"""
|
||||
# prefer configured subject specific to key
|
||||
template = self.config.get(f'{self.config.appname}.email.{key}.subject')
|
||||
if template:
|
||||
return template
|
||||
|
||||
# or subject from email setting, if defined
|
||||
if not setting:
|
||||
setting = self.get_email_setting(key)
|
||||
if setting and setting.default_subject:
|
||||
return setting.default_subject
|
||||
|
||||
# fall back to global default
|
||||
return self.config.get(f'{self.config.appname}.email.default.subject',
|
||||
default=self.universal_subject)
|
||||
|
@ -385,24 +557,32 @@ class EmailHandler(GenericHandler):
|
|||
if template:
|
||||
return template.render(**context)
|
||||
|
||||
def get_auto_body_template(self, key, typ):
|
||||
def get_auto_body_template(self, key, mode):
|
||||
""" """
|
||||
from mako.exceptions import TopLevelLookupException
|
||||
mode = mode.lower()
|
||||
if mode not in ('txt', 'html'):
|
||||
raise ValueError("requested mode not supported")
|
||||
|
||||
typ = typ.lower()
|
||||
if typ not in ('txt', 'html'):
|
||||
raise ValueError("requested type not supported")
|
||||
|
||||
if typ == 'txt':
|
||||
if mode == 'txt':
|
||||
templates = self.txt_templates
|
||||
elif typ == 'html':
|
||||
elif mode == 'html':
|
||||
templates = self.html_templates
|
||||
|
||||
try:
|
||||
return templates.get_template(f'{key}.{typ}.mako')
|
||||
return templates.get_template(f'{key}.{mode}.mako')
|
||||
except TopLevelLookupException:
|
||||
pass
|
||||
|
||||
def get_notes(self, key):
|
||||
"""
|
||||
Returns configured "notes" for the given :term:`email key`.
|
||||
|
||||
:param key: Key for the :term:`email type`.
|
||||
|
||||
:returns: Notes as string if found; otherwise ``None``.
|
||||
"""
|
||||
return self.config.get(f'{self.config.appname}.email.{key}.notes')
|
||||
|
||||
def is_enabled(self, key):
|
||||
"""
|
||||
Returns flag indicating whether the given email type is
|
||||
|
|
|
@ -6,21 +6,102 @@ from unittest.mock import patch, MagicMock
|
|||
import pytest
|
||||
|
||||
from wuttjamaican import email as mod
|
||||
from wuttjamaican.conf import WuttaConfig
|
||||
from wuttjamaican.util import resource_path
|
||||
from wuttjamaican.exc import ConfigurationError
|
||||
from wuttjamaican.testing import ConfigTestCase
|
||||
|
||||
|
||||
class TestEmailHandler(TestCase):
|
||||
class TestEmailSetting(ConfigTestCase):
|
||||
|
||||
def setUp(self):
|
||||
try:
|
||||
import mako
|
||||
except ImportError:
|
||||
pytest.skip("test not relevant without mako")
|
||||
def test_constructor(self):
|
||||
setting = mod.EmailSetting(self.config)
|
||||
self.assertIs(setting.config, self.config)
|
||||
self.assertIs(setting.app, self.app)
|
||||
self.assertEqual(setting.key, 'EmailSetting')
|
||||
|
||||
self.config = WuttaConfig()
|
||||
self.app = self.config.get_app()
|
||||
def test_sample_data(self):
|
||||
setting = mod.EmailSetting(self.config)
|
||||
self.assertEqual(setting.sample_data(), {})
|
||||
|
||||
|
||||
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="<p>hello world</p>")
|
||||
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="<p>hello world</p>")
|
||||
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="<p>hello world</p>")
|
||||
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)
|
||||
|
||||
|
||||
class mock_foo(mod.EmailSetting):
|
||||
default_subject = "MOCK FOO!"
|
||||
def sample_data(self):
|
||||
return {'foo': 'mock'}
|
||||
|
||||
|
||||
class TestEmailHandler(ConfigTestCase):
|
||||
|
||||
def make_handler(self, **kwargs):
|
||||
return mod.EmailHandler(self.config, **kwargs)
|
||||
|
@ -53,6 +134,69 @@ class TestEmailHandler(TestCase):
|
|||
self.assertEqual(handler.txt_templates.directories, [path])
|
||||
self.assertEqual(handler.html_templates.directories, [path])
|
||||
|
||||
def test_get_email_modules(self):
|
||||
|
||||
# no providers, no email modules
|
||||
with patch.object(self.app, 'providers', new={}):
|
||||
handler = self.make_handler()
|
||||
self.assertEqual(handler.get_email_modules(), [])
|
||||
|
||||
# provider may specify modules as list
|
||||
providers = {
|
||||
'wuttatest': MagicMock(email_modules=['wuttjamaican.email']),
|
||||
}
|
||||
with patch.object(self.app, 'providers', new=providers):
|
||||
handler = self.make_handler()
|
||||
modules = handler.get_email_modules()
|
||||
self.assertEqual(len(modules), 1)
|
||||
self.assertIs(modules[0], mod)
|
||||
|
||||
# provider may specify modules as list
|
||||
providers = {
|
||||
'wuttatest': MagicMock(email_modules='wuttjamaican.email'),
|
||||
}
|
||||
with patch.object(self.app, 'providers', new=providers):
|
||||
handler = self.make_handler()
|
||||
modules = handler.get_email_modules()
|
||||
self.assertEqual(len(modules), 1)
|
||||
self.assertIs(modules[0], mod)
|
||||
|
||||
def test_get_email_settings(self):
|
||||
|
||||
# no providers, no email settings
|
||||
with patch.object(self.app, 'providers', new={}):
|
||||
handler = self.make_handler()
|
||||
self.assertEqual(handler.get_email_settings(), {})
|
||||
|
||||
# provider may define email settings (via modules)
|
||||
providers = {
|
||||
'wuttatest': MagicMock(email_modules=['tests.test_email']),
|
||||
}
|
||||
with patch.object(self.app, 'providers', new=providers):
|
||||
handler = self.make_handler()
|
||||
settings = handler.get_email_settings()
|
||||
self.assertEqual(len(settings), 1)
|
||||
self.assertIn('mock_foo', settings)
|
||||
|
||||
def test_get_email_setting(self):
|
||||
|
||||
providers = {
|
||||
'wuttatest': MagicMock(email_modules=['tests.test_email']),
|
||||
}
|
||||
|
||||
with patch.object(self.app, 'providers', new=providers):
|
||||
handler = self.make_handler()
|
||||
|
||||
# as instance
|
||||
setting = handler.get_email_setting('mock_foo')
|
||||
self.assertIsInstance(setting, mod.EmailSetting)
|
||||
self.assertIsInstance(setting, mock_foo)
|
||||
|
||||
# as class
|
||||
setting = handler.get_email_setting('mock_foo', instance=False)
|
||||
self.assertTrue(issubclass(setting, mod.EmailSetting))
|
||||
self.assertIs(setting, mock_foo)
|
||||
|
||||
def test_make_message(self):
|
||||
handler = self.make_handler()
|
||||
msg = handler.make_message()
|
||||
|
@ -166,6 +310,20 @@ class TestEmailHandler(TestCase):
|
|||
self.config.setdefault('wutta.email.foo.sender', 'sally@example.com')
|
||||
self.assertEqual(handler.get_auto_sender('foo'), 'sally@example.com')
|
||||
|
||||
def test_get_auto_replyto(self):
|
||||
handler = self.make_handler()
|
||||
|
||||
# null by default
|
||||
self.assertIsNone(handler.get_auto_replyto('foo'))
|
||||
|
||||
# can set global default
|
||||
self.config.setdefault('wutta.email.default.replyto', 'george@example.com')
|
||||
self.assertEqual(handler.get_auto_replyto('foo'), 'george@example.com')
|
||||
|
||||
# can set for key
|
||||
self.config.setdefault('wutta.email.foo.replyto', 'kathy@example.com')
|
||||
self.assertEqual(handler.get_auto_replyto('foo'), 'kathy@example.com')
|
||||
|
||||
def test_get_auto_subject_template(self):
|
||||
handler = self.make_handler()
|
||||
|
||||
|
@ -183,6 +341,15 @@ class TestEmailHandler(TestCase):
|
|||
template = handler.get_auto_subject_template('foo')
|
||||
self.assertEqual(template, "Foo Message")
|
||||
|
||||
# setting can provide default subject
|
||||
providers = {
|
||||
'wuttatest': MagicMock(email_modules=['tests.test_email']),
|
||||
}
|
||||
with patch.object(self.app, 'providers', new=providers):
|
||||
handler = self.make_handler()
|
||||
template = handler.get_auto_subject_template('mock_foo')
|
||||
self.assertEqual(template, "MOCK FOO!")
|
||||
|
||||
def test_get_auto_subject(self):
|
||||
handler = self.make_handler()
|
||||
|
||||
|
@ -279,6 +446,16 @@ class TestEmailHandler(TestCase):
|
|||
body = handler.get_auto_html_body('test_foo')
|
||||
self.assertEqual(body, '<p>hello from foo html template</p>\n')
|
||||
|
||||
def test_get_notes(self):
|
||||
handler = self.make_handler()
|
||||
|
||||
# null by default
|
||||
self.assertIsNone(handler.get_notes('foo'))
|
||||
|
||||
# configured notes
|
||||
self.config.setdefault('wutta.email.foo.notes', 'hello world')
|
||||
self.assertEqual(handler.get_notes('foo'), 'hello world')
|
||||
|
||||
def test_is_enabled(self):
|
||||
handler = self.make_handler()
|
||||
|
||||
|
@ -455,74 +632,3 @@ 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="<p>hello world</p>")
|
||||
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="<p>hello world</p>")
|
||||
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="<p>hello world</p>")
|
||||
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)
|
||||
|
|
Loading…
Reference in a new issue