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
|
must be unique across the app, so the correct template files and
|
||||||
other settings are used when sending etc.
|
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
|
email type
|
||||||
The :term:`app` is capable of sending many types of emails,
|
The :term:`app` is capable of sending many types of emails,
|
||||||
e.g. daily reports, alerts of various kinds etc. Each "type" of
|
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`.
|
:param config: The app :term:`config object`.
|
||||||
|
|
||||||
Instances have the following attributes:
|
``AppProvider`` instances have the following attributes:
|
||||||
|
|
||||||
.. attribute:: config
|
.. attribute:: config
|
||||||
|
|
||||||
|
@ -846,6 +846,29 @@ class AppProvider:
|
||||||
.. attribute:: app
|
.. attribute:: app
|
||||||
|
|
||||||
Reference to the parent app handler.
|
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):
|
def __init__(self, config):
|
||||||
|
|
|
@ -24,11 +24,16 @@
|
||||||
Email Handler
|
Email Handler
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import importlib
|
||||||
import logging
|
import logging
|
||||||
import smtplib
|
import smtplib
|
||||||
from email.mime.multipart import MIMEMultipart
|
from email.mime.multipart import MIMEMultipart
|
||||||
from email.mime.text import MIMEText
|
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.app import GenericHandler
|
||||||
from wuttjamaican.util import resource_path
|
from wuttjamaican.util import resource_path
|
||||||
|
|
||||||
|
@ -36,6 +41,75 @@ from wuttjamaican.util import resource_path
|
||||||
log = logging.getLogger(__name__)
|
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:
|
class Message:
|
||||||
"""
|
"""
|
||||||
Represents an email message to be sent.
|
Represents an email message to be sent.
|
||||||
|
@ -185,8 +259,6 @@ class EmailHandler(GenericHandler):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
from mako.lookup import TemplateLookup
|
|
||||||
|
|
||||||
# prefer configured list of template lookup paths, if set
|
# prefer configured list of template lookup paths, if set
|
||||||
templates = self.config.get_list(f'{self.config.appname}.email.templates')
|
templates = self.config.get_list(f'{self.config.appname}.email.templates')
|
||||||
if not templates:
|
if not templates:
|
||||||
|
@ -213,6 +285,67 @@ class EmailHandler(GenericHandler):
|
||||||
# TODO: sounds great but i forget why?
|
# TODO: sounds great but i forget why?
|
||||||
default_filters=['h'])
|
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):
|
def make_message(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
Make and return a new email message.
|
Make and return a new email message.
|
||||||
|
@ -289,26 +422,49 @@ class EmailHandler(GenericHandler):
|
||||||
# fall back to global default (required!)
|
# fall back to global default (required!)
|
||||||
return self.config.require(f'{self.config.appname}.email.default.sender')
|
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`
|
Returns automatic :attr:`~wuttjamaican.email.Message.subject`
|
||||||
line for a message, as determined by config.
|
line for a message, as determined by config.
|
||||||
|
|
||||||
This calls :meth:`get_auto_subject_template()` and then
|
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
|
:param rendered: If this is ``False``, the "raw" subject
|
||||||
template will be returned, instead of the final/rendered
|
template will be returned, instead of the final/rendered
|
||||||
subject text.
|
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:
|
if not rendered:
|
||||||
return template
|
return template
|
||||||
|
|
||||||
return Template(template).render(**context)
|
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
|
Returns the template string to use for automatic subject line
|
||||||
of a message, as determined by config.
|
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
|
The template returned from this method is used to render the
|
||||||
final subject line in :meth:`get_auto_subject()`.
|
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
|
# prefer configured subject specific to key
|
||||||
template = self.config.get(f'{self.config.appname}.email.{key}.subject')
|
template = self.config.get(f'{self.config.appname}.email.{key}.subject')
|
||||||
if template:
|
if template:
|
||||||
return 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
|
# fall back to global default
|
||||||
return self.config.get(f'{self.config.appname}.email.default.subject',
|
return self.config.get(f'{self.config.appname}.email.default.subject',
|
||||||
default=self.universal_subject)
|
default=self.universal_subject)
|
||||||
|
@ -385,24 +557,32 @@ class EmailHandler(GenericHandler):
|
||||||
if template:
|
if template:
|
||||||
return template.render(**context)
|
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 mode == 'txt':
|
||||||
if typ not in ('txt', 'html'):
|
|
||||||
raise ValueError("requested type not supported")
|
|
||||||
|
|
||||||
if typ == 'txt':
|
|
||||||
templates = self.txt_templates
|
templates = self.txt_templates
|
||||||
elif typ == 'html':
|
elif mode == 'html':
|
||||||
templates = self.html_templates
|
templates = self.html_templates
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return templates.get_template(f'{key}.{typ}.mako')
|
return templates.get_template(f'{key}.{mode}.mako')
|
||||||
except TopLevelLookupException:
|
except TopLevelLookupException:
|
||||||
pass
|
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):
|
def is_enabled(self, key):
|
||||||
"""
|
"""
|
||||||
Returns flag indicating whether the given email type is
|
Returns flag indicating whether the given email type is
|
||||||
|
|
|
@ -6,21 +6,102 @@ from unittest.mock import patch, MagicMock
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from wuttjamaican import email as mod
|
from wuttjamaican import email as mod
|
||||||
from wuttjamaican.conf import WuttaConfig
|
|
||||||
from wuttjamaican.util import resource_path
|
from wuttjamaican.util import resource_path
|
||||||
from wuttjamaican.exc import ConfigurationError
|
from wuttjamaican.exc import ConfigurationError
|
||||||
|
from wuttjamaican.testing import ConfigTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestEmailHandler(TestCase):
|
class TestEmailSetting(ConfigTestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def test_constructor(self):
|
||||||
try:
|
setting = mod.EmailSetting(self.config)
|
||||||
import mako
|
self.assertIs(setting.config, self.config)
|
||||||
except ImportError:
|
self.assertIs(setting.app, self.app)
|
||||||
pytest.skip("test not relevant without mako")
|
self.assertEqual(setting.key, 'EmailSetting')
|
||||||
|
|
||||||
self.config = WuttaConfig()
|
def test_sample_data(self):
|
||||||
self.app = self.config.get_app()
|
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):
|
def make_handler(self, **kwargs):
|
||||||
return mod.EmailHandler(self.config, **kwargs)
|
return mod.EmailHandler(self.config, **kwargs)
|
||||||
|
@ -53,6 +134,69 @@ class TestEmailHandler(TestCase):
|
||||||
self.assertEqual(handler.txt_templates.directories, [path])
|
self.assertEqual(handler.txt_templates.directories, [path])
|
||||||
self.assertEqual(handler.html_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):
|
def test_make_message(self):
|
||||||
handler = self.make_handler()
|
handler = self.make_handler()
|
||||||
msg = handler.make_message()
|
msg = handler.make_message()
|
||||||
|
@ -166,6 +310,20 @@ class TestEmailHandler(TestCase):
|
||||||
self.config.setdefault('wutta.email.foo.sender', 'sally@example.com')
|
self.config.setdefault('wutta.email.foo.sender', 'sally@example.com')
|
||||||
self.assertEqual(handler.get_auto_sender('foo'), '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):
|
def test_get_auto_subject_template(self):
|
||||||
handler = self.make_handler()
|
handler = self.make_handler()
|
||||||
|
|
||||||
|
@ -183,6 +341,15 @@ class TestEmailHandler(TestCase):
|
||||||
template = handler.get_auto_subject_template('foo')
|
template = handler.get_auto_subject_template('foo')
|
||||||
self.assertEqual(template, "Foo Message")
|
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):
|
def test_get_auto_subject(self):
|
||||||
handler = self.make_handler()
|
handler = self.make_handler()
|
||||||
|
|
||||||
|
@ -279,6 +446,16 @@ class TestEmailHandler(TestCase):
|
||||||
body = handler.get_auto_html_body('test_foo')
|
body = handler.get_auto_html_body('test_foo')
|
||||||
self.assertEqual(body, '<p>hello from foo html template</p>\n')
|
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):
|
def test_is_enabled(self):
|
||||||
handler = self.make_handler()
|
handler = self.make_handler()
|
||||||
|
|
||||||
|
@ -455,74 +632,3 @@ class TestEmailHandler(TestCase):
|
||||||
self.config.setdefault('wutta.email.default.enabled', False)
|
self.config.setdefault('wutta.email.default.enabled', False)
|
||||||
handler.send_email('bar', sender='bar@example.com', txt_body="hello world")
|
handler.send_email('bar', sender='bar@example.com', txt_body="hello world")
|
||||||
self.assertFalse(deliver_message.called)
|
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