feat: add basic email handler support
still no way to define "profiles" for admin in web app yet
This commit is contained in:
parent
b401fac04f
commit
131ad88a16
6
docs/api/wuttjamaican/email.handler.rst
Normal file
6
docs/api/wuttjamaican/email.handler.rst
Normal file
|
@ -0,0 +1,6 @@
|
|||
|
||||
``wuttjamaican.email.handler``
|
||||
==============================
|
||||
|
||||
.. automodule:: wuttjamaican.email.handler
|
||||
:members:
|
6
docs/api/wuttjamaican/email.message.rst
Normal file
6
docs/api/wuttjamaican/email.message.rst
Normal file
|
@ -0,0 +1,6 @@
|
|||
|
||||
``wuttjamaican.email.message``
|
||||
==============================
|
||||
|
||||
.. automodule:: wuttjamaican.email.message
|
||||
:members:
|
6
docs/api/wuttjamaican/email.rst
Normal file
6
docs/api/wuttjamaican/email.rst
Normal file
|
@ -0,0 +1,6 @@
|
|||
|
||||
``wuttjamaican.email``
|
||||
======================
|
||||
|
||||
.. automodule:: wuttjamaican.email
|
||||
:members:
|
|
@ -17,6 +17,9 @@
|
|||
db.model.base
|
||||
db.model.upgrades
|
||||
db.sess
|
||||
email
|
||||
email.handler
|
||||
email.message
|
||||
enum
|
||||
exc
|
||||
people
|
||||
|
|
|
@ -124,6 +124,12 @@ Glossary
|
|||
In practice this generally refers to a
|
||||
:class:`~wuttjamaican.db.sess.Session` instance.
|
||||
|
||||
email handler
|
||||
The :term:`handler` responsible for sending email on behalf of
|
||||
the :term:`app`.
|
||||
|
||||
Default is :class:`~wuttjamaican.email.handler.EmailHandler`.
|
||||
|
||||
entry point
|
||||
This refers to a "setuptools-style" entry point specifically,
|
||||
which is a mechanism used to register "plugins" and the like.
|
||||
|
|
|
@ -35,6 +35,7 @@ dependencies = [
|
|||
|
||||
[project.optional-dependencies]
|
||||
db = ["SQLAlchemy<2", "alembic", "alembic-postgresql-enum", "passlib"]
|
||||
email = ["Mako"]
|
||||
docs = ["Sphinx", "sphinxcontrib-programoutput", "enum-tools[sphinx]", "furo"]
|
||||
tests = ["pytest-cov", "tox"]
|
||||
|
||||
|
|
|
@ -81,6 +81,7 @@ class AppHandler:
|
|||
default_model_spec = 'wuttjamaican.db.model'
|
||||
default_enum_spec = 'wuttjamaican.enum'
|
||||
default_auth_handler_spec = 'wuttjamaican.auth:AuthHandler'
|
||||
default_email_handler_spec = 'wuttjamaican.email:EmailHandler'
|
||||
default_people_handler_spec = 'wuttjamaican.people:PeopleHandler'
|
||||
|
||||
def __init__(self, config):
|
||||
|
@ -606,6 +607,21 @@ class AppHandler:
|
|||
self.handlers['auth'] = factory(self.config, **kwargs)
|
||||
return self.handlers['auth']
|
||||
|
||||
def get_email_handler(self, **kwargs):
|
||||
"""
|
||||
Get the configured :term:`email handler`.
|
||||
|
||||
See also :meth:`send_email()`.
|
||||
|
||||
:rtype: :class:`~wuttjamaican.email.handler.EmailHandler`
|
||||
"""
|
||||
if 'email' not in self.handlers:
|
||||
spec = self.config.get(f'{self.appname}.email.handler',
|
||||
default=self.default_email_handler_spec)
|
||||
factory = self.load_object(spec)
|
||||
self.handlers['email'] = factory(self.config, **kwargs)
|
||||
return self.handlers['email']
|
||||
|
||||
def get_people_handler(self, **kwargs):
|
||||
"""
|
||||
Get the configured "people" :term:`handler`.
|
||||
|
@ -634,6 +650,15 @@ class AppHandler:
|
|||
"""
|
||||
return self.get_people_handler().get_person(obj, **kwargs)
|
||||
|
||||
def send_email(self, *args, **kwargs):
|
||||
"""
|
||||
Send an email message.
|
||||
|
||||
This is a convenience wrapper around
|
||||
:meth:`~wuttjamaican.email.handler.EmailHandler.send_email()`.
|
||||
"""
|
||||
self.get_email_handler().send_email(*args, **kwargs)
|
||||
|
||||
|
||||
class AppProvider:
|
||||
"""
|
||||
|
|
|
@ -440,8 +440,8 @@ class WuttaConfig:
|
|||
|
||||
# raise error if required value not found
|
||||
if require:
|
||||
message = message or "missing or invalid config"
|
||||
raise ConfigurationError(f"{message}; please set config value for: {key}")
|
||||
message = message or "missing config"
|
||||
raise ConfigurationError(f"{message}; set value for: {key}")
|
||||
|
||||
# give the default value if specified
|
||||
if default is not UNSPECIFIED:
|
||||
|
|
33
src/wuttjamaican/email/__init__.py
Normal file
33
src/wuttjamaican/email/__init__.py
Normal file
|
@ -0,0 +1,33 @@
|
|||
# -*- 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
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
|
423
src/wuttjamaican/email/handler.py
Normal file
423
src/wuttjamaican/email/handler.py
Normal file
|
@ -0,0 +1,423 @@
|
|||
# -*- 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
Email Handler
|
||||
"""
|
||||
|
||||
import logging
|
||||
import smtplib
|
||||
|
||||
from wuttjamaican.app import GenericHandler
|
||||
from wuttjamaican.util import resource_path
|
||||
from wuttjamaican.email.message import Message
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EmailHandler(GenericHandler):
|
||||
"""
|
||||
Base class and default implementation for the :term:`email
|
||||
handler`.
|
||||
|
||||
Responsible for sending email messages on behalf of the
|
||||
:term:`app`.
|
||||
|
||||
You normally would not create this directly, but instead call
|
||||
:meth:`~wuttjamaican.app.AppHandler.get_email_handler()` on your
|
||||
:term:`app handler`.
|
||||
"""
|
||||
|
||||
# nb. this is fallback/default subject for auto-message
|
||||
universal_subject = "Automated message"
|
||||
|
||||
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:
|
||||
|
||||
# otherwise use all available paths, from app providers
|
||||
available = []
|
||||
for provider in self.app.providers.values():
|
||||
if hasattr(provider, 'email_templates'):
|
||||
templates = provider.email_templates
|
||||
if isinstance(templates, str):
|
||||
templates = [templates]
|
||||
if templates:
|
||||
available.extend(templates)
|
||||
templates = available
|
||||
|
||||
# convert all to true file paths
|
||||
if templates:
|
||||
templates = [resource_path(p) for p in templates]
|
||||
|
||||
# will use these lookups from now on
|
||||
self.txt_templates = TemplateLookup(directories=templates)
|
||||
self.html_templates = TemplateLookup(directories=templates,
|
||||
# nb. escape HTML special chars
|
||||
# TODO: sounds great but i forget why?
|
||||
default_filters=['h'])
|
||||
|
||||
def make_message(self, **kwargs):
|
||||
"""
|
||||
Make and return a new email message.
|
||||
|
||||
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.
|
||||
"""
|
||||
return Message(**kwargs)
|
||||
|
||||
def make_auto_message(self, key, context={}, **kwargs):
|
||||
"""
|
||||
Make a new email message using config to determine its
|
||||
properties, and auto-generating body from a template.
|
||||
|
||||
Once everything has been collected/prepared,
|
||||
:meth:`make_message()` is called to create the final message,
|
||||
and that is returned.
|
||||
|
||||
:param key: Unique key for this particular "type" of message.
|
||||
This key is used as a prefix for all config settings and
|
||||
template names pertinent to the message.
|
||||
|
||||
:param context: Context dict used to render template(s) for
|
||||
the message.
|
||||
|
||||
:param \**kwargs: Any remaining kwargs are passed as-is to
|
||||
:meth:`make_message()`. More on this below.
|
||||
|
||||
:returns: :class:`~wuttjamaican.email.message.Message` object.
|
||||
|
||||
This method may invoke some others, to gather the message
|
||||
attributes. Each will check config, or render a template, or
|
||||
both. However if a particular attribute is provided by the
|
||||
caller, the corresponding "auto" method is skipped.
|
||||
|
||||
* :meth:`get_auto_sender()`
|
||||
* :meth:`get_auto_subject()`
|
||||
* :meth:`get_auto_to()`
|
||||
* :meth:`get_auto_cc()`
|
||||
* :meth:`get_auto_bcc()`
|
||||
* :meth:`get_auto_txt_body()`
|
||||
* :meth:`get_auto_html_body()`
|
||||
"""
|
||||
kwargs['key'] = key
|
||||
if 'sender' not in kwargs:
|
||||
kwargs['sender'] = self.get_auto_sender(key)
|
||||
if 'subject' not in kwargs:
|
||||
kwargs['subject'] = self.get_auto_subject(key, context)
|
||||
if 'to' not in kwargs:
|
||||
kwargs['to'] = self.get_auto_to(key)
|
||||
if 'cc' not in kwargs:
|
||||
kwargs['cc'] = self.get_auto_cc(key)
|
||||
if 'bcc' not in kwargs:
|
||||
kwargs['bcc'] = self.get_auto_bcc(key)
|
||||
if 'txt_body' not in kwargs:
|
||||
kwargs['txt_body'] = self.get_auto_txt_body(key, context)
|
||||
if 'html_body' not in kwargs:
|
||||
kwargs['html_body'] = self.get_auto_html_body(key, context)
|
||||
return self.make_message(**kwargs)
|
||||
|
||||
def get_auto_sender(self, key):
|
||||
"""
|
||||
Returns automatic
|
||||
:attr:`~wuttjamaican.email.message.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')
|
||||
if sender:
|
||||
return sender
|
||||
|
||||
# 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):
|
||||
"""
|
||||
Returns automatic
|
||||
:attr:`~wuttjamaican.email.message.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.
|
||||
|
||||
: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)
|
||||
if not rendered:
|
||||
return template
|
||||
return Template(template).render(**context)
|
||||
|
||||
def get_auto_subject_template(self, key):
|
||||
"""
|
||||
Returns the template string to use for automatic subject line
|
||||
of a message, as determined by config.
|
||||
|
||||
In many cases this will be a simple string and not a
|
||||
"template" per se; however it is still treated as a template.
|
||||
|
||||
The template returned from this method is used to render the
|
||||
final subject line in :meth:`get_auto_subject()`.
|
||||
"""
|
||||
# prefer configured subject specific to key
|
||||
template = self.config.get(f'{self.config.appname}.email.{key}.subject')
|
||||
if template:
|
||||
return template
|
||||
|
||||
# fall back to global default
|
||||
return self.config.get(f'{self.config.appname}.email.default.subject',
|
||||
default=self.universal_subject)
|
||||
|
||||
def get_auto_to(self, key):
|
||||
"""
|
||||
Returns automatic
|
||||
:attr:`~wuttjamaican.email.message.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.
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
return self.get_auto_recips(key, 'bcc')
|
||||
|
||||
def get_auto_recips(self, key, typ):
|
||||
""" """
|
||||
typ = typ.lower()
|
||||
if typ not in ('to', 'cc', 'bcc'):
|
||||
raise ValueError("requested type not supported")
|
||||
|
||||
# prefer configured recips specific to key
|
||||
recips = self.config.get_list(f'{self.config.appname}.email.{key}.{typ}')
|
||||
if recips:
|
||||
return recips
|
||||
|
||||
# fall back to global default
|
||||
return self.config.get_list(f'{self.config.appname}.email.default.{typ}',
|
||||
default=[])
|
||||
|
||||
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.
|
||||
"""
|
||||
template = self.get_auto_body_template(key, 'txt')
|
||||
if template:
|
||||
return template.render(**context)
|
||||
|
||||
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.
|
||||
"""
|
||||
template = self.get_auto_body_template(key, 'html')
|
||||
if template:
|
||||
return template.render(**context)
|
||||
|
||||
def get_auto_body_template(self, key, typ):
|
||||
""" """
|
||||
from mako.exceptions import TopLevelLookupException
|
||||
|
||||
typ = typ.lower()
|
||||
if typ not in ('txt', 'html'):
|
||||
raise ValueError("requested type not supported")
|
||||
|
||||
if typ == 'txt':
|
||||
templates = self.txt_templates
|
||||
elif typ == 'html':
|
||||
templates = self.html_templates
|
||||
|
||||
try:
|
||||
return templates.get_template(f'{key}.{typ}.mako')
|
||||
except TopLevelLookupException:
|
||||
pass
|
||||
|
||||
def deliver_message(self, message, sender=None, recips=None):
|
||||
"""
|
||||
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 sender: Optional sender address to use for delivery.
|
||||
If not specified, will be read from ``message``.
|
||||
|
||||
:param recips: Optional recipient address(es) for delivery.
|
||||
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.
|
||||
|
||||
Note also, this method does not (yet?) have robust error
|
||||
handling, so if an error occurs with the SMTP session, it will
|
||||
simply raise to caller.
|
||||
|
||||
:returns: ``None``
|
||||
"""
|
||||
if not sender:
|
||||
sender = message.sender
|
||||
if not sender:
|
||||
raise ValueError("no sender identified for message delivery")
|
||||
|
||||
if not recips:
|
||||
recips = set()
|
||||
if message.to:
|
||||
recips.update(message.to)
|
||||
if message.cc:
|
||||
recips.update(message.cc)
|
||||
if message.bcc:
|
||||
recips.update(message.bcc)
|
||||
elif isinstance(recips, str):
|
||||
recips = [recips]
|
||||
|
||||
recips = set(recips)
|
||||
if not recips:
|
||||
raise ValueError("no recipients identified for message delivery")
|
||||
|
||||
if not isinstance(message, str):
|
||||
message = message.as_string()
|
||||
|
||||
# get smtp info
|
||||
server = self.config.get(f'{self.config.appname}.mail.smtp.server', default='localhost')
|
||||
username = self.config.get(f'{self.config.appname}.mail.smtp.username')
|
||||
password = self.config.get(f'{self.config.appname}.mail.smtp.password')
|
||||
|
||||
# make sure sending is enabled
|
||||
log.debug("sending email from %s; to %s", sender, recips)
|
||||
if not self.sending_is_enabled():
|
||||
log.debug("nevermind, config says no emails")
|
||||
return
|
||||
|
||||
# smtp connect
|
||||
session = smtplib.SMTP(server)
|
||||
if username and password:
|
||||
session.login(username, password)
|
||||
|
||||
# smtp send
|
||||
session.sendmail(sender, recips, message)
|
||||
session.quit()
|
||||
log.debug("email was sent")
|
||||
|
||||
def sending_is_enabled(self):
|
||||
"""
|
||||
Returns boolean indicating if email sending is enabled.
|
||||
|
||||
Set this flag in config like this:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[wutta.mail]
|
||||
send_emails = true
|
||||
|
||||
Note that it is OFF by default.
|
||||
"""
|
||||
return self.config.get_bool(f'{self.config.appname}.mail.send_emails',
|
||||
default=False)
|
||||
|
||||
def send_email(self, key=None, context={}, message=None, sender=None, recips=None, **kwargs):
|
||||
"""
|
||||
Send an email message.
|
||||
|
||||
This method can send a ``message`` you provide, or it can
|
||||
construct one automatically from key/config/templates.
|
||||
|
||||
:param key: Indicates which "type" of automatic email to send.
|
||||
Used to lookup config settings and template files.
|
||||
|
||||
:param context: Context dict for rendering automatic email
|
||||
template(s).
|
||||
|
||||
:param message: Optional pre-built message instance, to send
|
||||
as-is.
|
||||
|
||||
:param sender: Optional sender address for the
|
||||
message/delivery.
|
||||
|
||||
If ``message`` is not provided, then the ``sender`` (if
|
||||
provided) will also be used when constructing the
|
||||
auto-message (i.e. to set the ``From:`` header).
|
||||
|
||||
In any case if ``sender`` is provided, it will be used for
|
||||
the actual SMTP delivery.
|
||||
|
||||
:param recips: Optional list of recipient addresses for
|
||||
delivery. If not specified, will be read from the message
|
||||
itself (after auto-generating it, if applicable).
|
||||
|
||||
.. note::
|
||||
|
||||
This param does not affect an auto-generated message; it
|
||||
is used for delivery only. As such it must contain
|
||||
*all* true recipients.
|
||||
|
||||
If you provide the ``message`` but not the ``recips``,
|
||||
the latter will be read from message headers: ``To:``,
|
||||
``Cc:`` and ``Bcc:``
|
||||
|
||||
If you want an auto-generated message but also want to
|
||||
override various recipient headers, then you must
|
||||
provide those explicitly::
|
||||
|
||||
context = {'data': [1, 2, 3]}
|
||||
app.send_email('foo', context, to='me@example.com', cc='bobby@example.com')
|
||||
|
||||
:param \**kwargs: Any remaining kwargs are passed along to
|
||||
:meth:`make_auto_message()`. So, not used if you provide
|
||||
the ``message``.
|
||||
"""
|
||||
if message is None:
|
||||
if sender:
|
||||
kwargs['sender'] = sender
|
||||
message = self.make_auto_message(key, context, **kwargs)
|
||||
|
||||
self.deliver_message(message, recips=recips)
|
158
src/wuttjamaican/email/message.py
Normal file
158
src/wuttjamaican/email/message.py
Normal file
|
@ -0,0 +1,158 @@
|
|||
# -*- 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
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()
|
0
tests/email/__init__.py
Normal file
0
tests/email/__init__.py
Normal file
1
tests/email/templates/test_foo.html.mako
Normal file
1
tests/email/templates/test_foo.html.mako
Normal file
|
@ -0,0 +1 @@
|
|||
<p>hello from foo html template</p>
|
1
tests/email/templates/test_foo.txt.mako
Normal file
1
tests/email/templates/test_foo.txt.mako
Normal file
|
@ -0,0 +1 @@
|
|||
hello from foo txt template
|
403
tests/email/test_handler.py
Normal file
403
tests/email/test_handler.py
Normal file
|
@ -0,0 +1,403 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
|
||||
from unittest import TestCase
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
from wuttjamaican.email import handler as mod
|
||||
from wuttjamaican.email import Message
|
||||
from wuttjamaican.conf import WuttaConfig
|
||||
from wuttjamaican.util import resource_path
|
||||
from wuttjamaican.exc import ConfigurationError
|
||||
|
||||
|
||||
class TestEmailHandler(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.config = WuttaConfig()
|
||||
self.app = self.config.get_app()
|
||||
|
||||
def make_handler(self, **kwargs):
|
||||
return mod.EmailHandler(self.config, **kwargs)
|
||||
|
||||
def test_constructor_lookups(self):
|
||||
|
||||
# empty lookup paths by default, if no providers
|
||||
with patch.object(self.app, 'providers', new={}):
|
||||
handler = self.make_handler()
|
||||
self.assertEqual(handler.txt_templates.directories, [])
|
||||
self.assertEqual(handler.html_templates.directories, [])
|
||||
|
||||
# provider may specify paths as list
|
||||
providers = {
|
||||
'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')
|
||||
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'),
|
||||
}
|
||||
with patch.object(self.app, 'providers', new=providers):
|
||||
handler = self.make_handler()
|
||||
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)
|
||||
|
||||
def test_make_auto_message(self):
|
||||
handler = self.make_handler()
|
||||
|
||||
# error if default sender not defined
|
||||
self.assertRaises(ConfigurationError, handler.make_auto_message, 'foo')
|
||||
|
||||
# so let's define that
|
||||
self.config.setdefault('wutta.email.default.sender', 'bob@example.com')
|
||||
|
||||
# message is empty by default
|
||||
msg = handler.make_auto_message('foo')
|
||||
self.assertIsInstance(msg, Message)
|
||||
self.assertEqual(msg.key, 'foo')
|
||||
self.assertEqual(msg.sender, 'bob@example.com')
|
||||
self.assertEqual(msg.subject, "Automated message")
|
||||
self.assertEqual(msg.to, [])
|
||||
self.assertEqual(msg.cc, [])
|
||||
self.assertEqual(msg.bcc, [])
|
||||
self.assertIsNone(msg.replyto)
|
||||
self.assertIsNone(msg.txt_body)
|
||||
self.assertIsNone(msg.html_body)
|
||||
|
||||
# but if there is a proper email profile configured for key,
|
||||
# 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')
|
||||
handler = self.make_handler()
|
||||
msg = handler.make_auto_message('test_foo')
|
||||
self.assertEqual(msg.key, 'test_foo')
|
||||
self.assertEqual(msg.sender, 'bob@example.com')
|
||||
self.assertEqual(msg.subject, "hello foo")
|
||||
self.assertEqual(msg.to, ['sally@example.com'])
|
||||
self.assertEqual(msg.cc, [])
|
||||
self.assertEqual(msg.bcc, [])
|
||||
self.assertIsNone(msg.replyto)
|
||||
self.assertEqual(msg.txt_body, "hello from foo txt template\n")
|
||||
self.assertEqual(msg.html_body, "<p>hello from foo html template</p>\n")
|
||||
|
||||
# *some* auto methods get skipped if caller specifies the
|
||||
# kwarg at all; others get skipped if kwarg is empty
|
||||
|
||||
# sender
|
||||
with patch.object(handler, 'get_auto_sender') as get_auto_sender:
|
||||
msg = handler.make_auto_message('foo', sender=None)
|
||||
get_auto_sender.assert_not_called()
|
||||
msg = handler.make_auto_message('foo')
|
||||
get_auto_sender.assert_called_once_with('foo')
|
||||
|
||||
# subject
|
||||
with patch.object(handler, 'get_auto_subject') as get_auto_subject:
|
||||
msg = handler.make_auto_message('foo', subject=None)
|
||||
get_auto_subject.assert_not_called()
|
||||
msg = handler.make_auto_message('foo')
|
||||
get_auto_subject.assert_called_once_with('foo', {})
|
||||
|
||||
# to
|
||||
with patch.object(handler, 'get_auto_to') as get_auto_to:
|
||||
msg = handler.make_auto_message('foo', to=None)
|
||||
get_auto_to.assert_not_called()
|
||||
get_auto_to.return_value = None
|
||||
msg = handler.make_auto_message('foo')
|
||||
get_auto_to.assert_called_once_with('foo')
|
||||
|
||||
# cc
|
||||
with patch.object(handler, 'get_auto_cc') as get_auto_cc:
|
||||
msg = handler.make_auto_message('foo', cc=None)
|
||||
get_auto_cc.assert_not_called()
|
||||
get_auto_cc.return_value = None
|
||||
msg = handler.make_auto_message('foo')
|
||||
get_auto_cc.assert_called_once_with('foo')
|
||||
|
||||
# bcc
|
||||
with patch.object(handler, 'get_auto_bcc') as get_auto_bcc:
|
||||
msg = handler.make_auto_message('foo', bcc=None)
|
||||
get_auto_bcc.assert_not_called()
|
||||
get_auto_bcc.return_value = None
|
||||
msg = handler.make_auto_message('foo')
|
||||
get_auto_bcc.assert_called_once_with('foo')
|
||||
|
||||
# txt_body
|
||||
with patch.object(handler, 'get_auto_txt_body') as get_auto_txt_body:
|
||||
msg = handler.make_auto_message('foo', txt_body=None)
|
||||
get_auto_txt_body.assert_not_called()
|
||||
msg = handler.make_auto_message('foo')
|
||||
get_auto_txt_body.assert_called_once_with('foo', {})
|
||||
|
||||
# html_body
|
||||
with patch.object(handler, 'get_auto_html_body') as get_auto_html_body:
|
||||
msg = handler.make_auto_message('foo', html_body=None)
|
||||
get_auto_html_body.assert_not_called()
|
||||
msg = handler.make_auto_message('foo')
|
||||
get_auto_html_body.assert_called_once_with('foo', {})
|
||||
|
||||
def test_get_auto_sender(self):
|
||||
handler = self.make_handler()
|
||||
|
||||
# error if none configured
|
||||
self.assertRaises(ConfigurationError, handler.get_auto_sender, 'foo')
|
||||
|
||||
# can set global default
|
||||
self.config.setdefault('wutta.email.default.sender', 'bob@example.com')
|
||||
self.assertEqual(handler.get_auto_sender('foo'), 'bob@example.com')
|
||||
|
||||
# can set for key
|
||||
self.config.setdefault('wutta.email.foo.sender', 'sally@example.com')
|
||||
self.assertEqual(handler.get_auto_sender('foo'), 'sally@example.com')
|
||||
|
||||
def test_get_auto_subject_template(self):
|
||||
handler = self.make_handler()
|
||||
|
||||
# global default
|
||||
template = handler.get_auto_subject_template('foo')
|
||||
self.assertEqual(template, "Automated message")
|
||||
|
||||
# can configure alternate global default
|
||||
self.config.setdefault('wutta.email.default.subject', "Wutta Message")
|
||||
template = handler.get_auto_subject_template('foo')
|
||||
self.assertEqual(template, "Wutta Message")
|
||||
|
||||
# can configure just for key
|
||||
self.config.setdefault('wutta.email.foo.subject', "Foo Message")
|
||||
template = handler.get_auto_subject_template('foo')
|
||||
self.assertEqual(template, "Foo Message")
|
||||
|
||||
def test_get_auto_subject(self):
|
||||
handler = self.make_handler()
|
||||
|
||||
# global default
|
||||
subject = handler.get_auto_subject('foo')
|
||||
self.assertEqual(subject, "Automated message")
|
||||
|
||||
# can configure alternate global default
|
||||
self.config.setdefault('wutta.email.default.subject', "Wutta Message")
|
||||
subject = handler.get_auto_subject('foo')
|
||||
self.assertEqual(subject, "Wutta Message")
|
||||
|
||||
# can configure just for key
|
||||
self.config.setdefault('wutta.email.foo.subject', "Foo Message")
|
||||
subject = handler.get_auto_subject('foo')
|
||||
self.assertEqual(subject, "Foo Message")
|
||||
|
||||
# proper template is rendered
|
||||
self.config.setdefault('wutta.email.bar.subject', "${foo} Message")
|
||||
subject = handler.get_auto_subject('bar', {'foo': "FOO"})
|
||||
self.assertEqual(subject, "FOO Message")
|
||||
|
||||
# unless we ask it not to
|
||||
subject = handler.get_auto_subject('bar', {'foo': "FOO"}, rendered=False)
|
||||
self.assertEqual(subject, "${foo} Message")
|
||||
|
||||
def test_get_auto_recips(self):
|
||||
handler = self.make_handler()
|
||||
|
||||
# error if bad type requested
|
||||
self.assertRaises(ValueError, handler.get_auto_recips, 'foo', 'doesnotexist')
|
||||
|
||||
# can configure global default
|
||||
self.config.setdefault('wutta.email.default.to', 'admin@example.com')
|
||||
recips = handler.get_auto_recips('foo', 'to')
|
||||
self.assertEqual(recips, ['admin@example.com'])
|
||||
|
||||
# can configure just for key
|
||||
self.config.setdefault('wutta.email.foo.to', 'bob@example.com')
|
||||
recips = handler.get_auto_recips('foo', 'to')
|
||||
self.assertEqual(recips, ['bob@example.com'])
|
||||
|
||||
def test_get_auto_body_template(self):
|
||||
from mako.template import Template
|
||||
|
||||
handler = self.make_handler()
|
||||
|
||||
# error if bad request
|
||||
self.assertRaises(ValueError, handler.get_auto_body_template, 'foo', 'BADTYPE')
|
||||
|
||||
# empty by default
|
||||
template = handler.get_auto_body_template('foo', 'txt')
|
||||
self.assertIsNone(template)
|
||||
|
||||
# but returns a template if it exists
|
||||
providers = {
|
||||
'wuttatest': MagicMock(email_templates=['tests.email:templates']),
|
||||
}
|
||||
with patch.object(self.app, 'providers', new=providers):
|
||||
handler = self.make_handler()
|
||||
template = handler.get_auto_body_template('test_foo', 'txt')
|
||||
self.assertIsInstance(template, Template)
|
||||
self.assertEqual(template.uri, 'test_foo.txt.mako')
|
||||
|
||||
def test_get_auto_txt_body(self):
|
||||
handler = self.make_handler()
|
||||
|
||||
# empty by default
|
||||
body = handler.get_auto_txt_body('some-random-email')
|
||||
self.assertIsNone(body)
|
||||
|
||||
# but returns body if template exists
|
||||
providers = {
|
||||
'wuttatest': MagicMock(email_templates=['tests.email:templates']),
|
||||
}
|
||||
with patch.object(self.app, 'providers', new=providers):
|
||||
handler = self.make_handler()
|
||||
body = handler.get_auto_txt_body('test_foo')
|
||||
self.assertEqual(body, 'hello from foo txt template\n')
|
||||
|
||||
def test_get_auto_html_body(self):
|
||||
handler = self.make_handler()
|
||||
|
||||
# empty by default
|
||||
body = handler.get_auto_html_body('some-random-email')
|
||||
self.assertIsNone(body)
|
||||
|
||||
# but returns body if template exists
|
||||
providers = {
|
||||
'wuttatest': MagicMock(email_templates=['tests.email:templates']),
|
||||
}
|
||||
with patch.object(self.app, 'providers', new=providers):
|
||||
handler = self.make_handler()
|
||||
body = handler.get_auto_html_body('test_foo')
|
||||
self.assertEqual(body, '<p>hello from foo html template</p>\n')
|
||||
|
||||
def test_deliver_message(self):
|
||||
handler = self.make_handler()
|
||||
|
||||
msg = handler.make_message(sender='bob@example.com', to='sally@example.com')
|
||||
with patch.object(msg, 'as_string', return_value='msg-str'):
|
||||
|
||||
# no smtp session since sending email is disabled by default
|
||||
with patch.object(mod, 'smtplib') as smtplib:
|
||||
session = MagicMock()
|
||||
smtplib.SMTP.return_value = session
|
||||
handler.deliver_message(msg)
|
||||
smtplib.SMTP.assert_not_called()
|
||||
session.login.assert_not_called()
|
||||
session.sendmail.assert_not_called()
|
||||
|
||||
# now let's enable sending
|
||||
self.config.setdefault('wutta.mail.send_emails', 'true')
|
||||
|
||||
# smtp login not attempted by default
|
||||
with patch.object(mod, 'smtplib') as smtplib:
|
||||
session = MagicMock()
|
||||
smtplib.SMTP.return_value = session
|
||||
handler.deliver_message(msg)
|
||||
smtplib.SMTP.assert_called_once_with('localhost')
|
||||
session.login.assert_not_called()
|
||||
session.sendmail.assert_called_once_with('bob@example.com', {'sally@example.com'}, 'msg-str')
|
||||
|
||||
# but login attempted if config has credentials
|
||||
self.config.setdefault('wutta.mail.smtp.username', 'bob')
|
||||
self.config.setdefault('wutta.mail.smtp.password', 'seekrit')
|
||||
with patch.object(mod, 'smtplib') as smtplib:
|
||||
session = MagicMock()
|
||||
smtplib.SMTP.return_value = session
|
||||
handler.deliver_message(msg)
|
||||
smtplib.SMTP.assert_called_once_with('localhost')
|
||||
session.login.assert_called_once_with('bob', 'seekrit')
|
||||
session.sendmail.assert_called_once_with('bob@example.com', {'sally@example.com'}, 'msg-str')
|
||||
|
||||
# error if no sender
|
||||
msg = handler.make_message(to='sally@example.com')
|
||||
self.assertRaises(ValueError, handler.deliver_message, msg)
|
||||
|
||||
# error if no recips
|
||||
msg = handler.make_message(sender='bob@example.com')
|
||||
self.assertRaises(ValueError, handler.deliver_message, msg)
|
||||
|
||||
# can set recips as list
|
||||
msg = handler.make_message(sender='bob@example.com')
|
||||
with patch.object(msg, 'as_string', return_value='msg-str'):
|
||||
with patch.object(mod, 'smtplib') as smtplib:
|
||||
session = MagicMock()
|
||||
smtplib.SMTP.return_value = session
|
||||
handler.deliver_message(msg, recips=['sally@example.com'])
|
||||
smtplib.SMTP.assert_called_once_with('localhost')
|
||||
session.sendmail.assert_called_once_with('bob@example.com', {'sally@example.com'}, 'msg-str')
|
||||
|
||||
# can set recips as string
|
||||
msg = handler.make_message(sender='bob@example.com')
|
||||
with patch.object(msg, 'as_string', return_value='msg-str'):
|
||||
with patch.object(mod, 'smtplib') as smtplib:
|
||||
session = MagicMock()
|
||||
smtplib.SMTP.return_value = session
|
||||
handler.deliver_message(msg, recips='sally@example.com')
|
||||
smtplib.SMTP.assert_called_once_with('localhost')
|
||||
session.sendmail.assert_called_once_with('bob@example.com', {'sally@example.com'}, 'msg-str')
|
||||
|
||||
# can set recips via to
|
||||
msg = handler.make_message(sender='bob@example.com', to='sally@example.com')
|
||||
with patch.object(msg, 'as_string', return_value='msg-str'):
|
||||
with patch.object(mod, 'smtplib') as smtplib:
|
||||
session = MagicMock()
|
||||
smtplib.SMTP.return_value = session
|
||||
handler.deliver_message(msg)
|
||||
smtplib.SMTP.assert_called_once_with('localhost')
|
||||
session.sendmail.assert_called_once_with('bob@example.com', {'sally@example.com'}, 'msg-str')
|
||||
|
||||
# can set recips via cc
|
||||
msg = handler.make_message(sender='bob@example.com', cc='sally@example.com')
|
||||
with patch.object(msg, 'as_string', return_value='msg-str'):
|
||||
with patch.object(mod, 'smtplib') as smtplib:
|
||||
session = MagicMock()
|
||||
smtplib.SMTP.return_value = session
|
||||
handler.deliver_message(msg)
|
||||
smtplib.SMTP.assert_called_once_with('localhost')
|
||||
session.sendmail.assert_called_once_with('bob@example.com', {'sally@example.com'}, 'msg-str')
|
||||
|
||||
# can set recips via bcc
|
||||
msg = handler.make_message(sender='bob@example.com', bcc='sally@example.com')
|
||||
with patch.object(msg, 'as_string', return_value='msg-str'):
|
||||
with patch.object(mod, 'smtplib') as smtplib:
|
||||
session = MagicMock()
|
||||
smtplib.SMTP.return_value = session
|
||||
handler.deliver_message(msg)
|
||||
smtplib.SMTP.assert_called_once_with('localhost')
|
||||
session.sendmail.assert_called_once_with('bob@example.com', {'sally@example.com'}, 'msg-str')
|
||||
|
||||
def test_sending_is_enabled(self):
|
||||
handler = self.make_handler()
|
||||
|
||||
# off by default
|
||||
self.assertFalse(handler.sending_is_enabled())
|
||||
|
||||
# but can be turned on
|
||||
self.config.setdefault('wutta.mail.send_emails', 'true')
|
||||
self.assertTrue(handler.sending_is_enabled())
|
||||
|
||||
def test_send_email(self):
|
||||
with patch.object(mod.EmailHandler, 'deliver_message') as deliver_message:
|
||||
handler = self.make_handler()
|
||||
|
||||
# deliver_message() is called
|
||||
handler.send_email('foo', sender='bob@example.com', to='sally@example.com',
|
||||
txt_body='hello world')
|
||||
deliver_message.assert_called_once()
|
||||
|
||||
# make_auto_message() called only if needed
|
||||
with patch.object(handler, 'make_auto_message') as make_auto_message:
|
||||
|
||||
msg = handler.make_message()
|
||||
handler.send_email(message=msg)
|
||||
make_auto_message.assert_not_called()
|
||||
|
||||
handler.send_email('foo', sender='bob@example.com', to='sally@example.com',
|
||||
txt_body='hello world')
|
||||
make_auto_message.assert_called_once_with('foo', {},
|
||||
sender='bob@example.com',
|
||||
to='sally@example.com',
|
||||
txt_body='hello world')
|
76
tests/email/test_message.py
Normal file
76
tests/email/test_message.py
Normal file
|
@ -0,0 +1,76 @@
|
|||
# -*- 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="<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)
|
|
@ -397,12 +397,25 @@ class TestAppHandler(FileConfigTestCase):
|
|||
auth = self.app.get_auth_handler()
|
||||
self.assertIsInstance(auth, AuthHandler)
|
||||
|
||||
def test_get_email_handler(self):
|
||||
from wuttjamaican.email import EmailHandler
|
||||
|
||||
mail = self.app.get_email_handler()
|
||||
self.assertIsInstance(mail, EmailHandler)
|
||||
|
||||
def test_get_people_handler(self):
|
||||
from wuttjamaican.people import PeopleHandler
|
||||
|
||||
people = self.app.get_people_handler()
|
||||
self.assertIsInstance(people, PeopleHandler)
|
||||
|
||||
def test_get_send_email(self):
|
||||
from wuttjamaican.email import EmailHandler
|
||||
|
||||
with patch.object(EmailHandler, 'send_email') as send_email:
|
||||
self.app.send_email('foo')
|
||||
send_email.assert_called_once_with('foo')
|
||||
|
||||
|
||||
class TestAppProvider(TestCase):
|
||||
|
||||
|
|
Loading…
Reference in a new issue