3
0
Fork 0

fix: add is_enabled() method for email handler, to check per type

also add some more descriptive errors when email template not found,
body empty
This commit is contained in:
Lance Edgar 2024-12-19 18:20:57 -06:00
parent 089d9d7ec6
commit 902412322e
3 changed files with 152 additions and 21 deletions

View file

@ -191,6 +191,17 @@ Glossary
Default is :class:`~wuttjamaican.email.handler.EmailHandler`.
email key
String idenfier for a certain :term:`email type`. Each email key
must be unique across the app, so the correct template files and
other settings are used when sending etc.
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
email then will have its own template(s) and sender/recipient
settings etc. See also :term:`email key`.
entry point
This refers to a "setuptools-style" entry point specifically,
which is a mechanism used to register "plugins" and the like.

View file

@ -277,6 +277,52 @@ class EmailHandler(GenericHandler):
except TopLevelLookupException:
pass
def is_enabled(self, key):
"""
Returns flag indicating whether the given email type is
"enabled" - i.e. whether it should ever be sent out (enabled)
or always suppressed (disabled).
All email types are enabled by default, unless config says
otherwise; e.g. to disable ``foo`` emails:
.. code-block:: ini
[wutta.email]
# nb. this is fallback if specific type is not configured
default.enabled = true
# this disables 'foo' but e.g 'bar' is still enabled per default above
foo.enabled = false
In a development setup you may want a reverse example, where
all emails are disabled by default but you can turn on just
one type for testing:
.. code-block:: ini
[wutta.email]
# do not send any emails unless explicitly enabled
default.enabled = false
# turn on 'bar' for testing
bar.enabled = true
See also :meth:`sending_is_enabled()` which is more of a
master shutoff switch.
:param key: Unique identifier for the email type.
:returns: True if this email type is enabled, otherwise false.
"""
for key in set([key, 'default']):
enabled = self.config.get_bool(f'{self.config.appname}.email.{key}.enabled')
if enabled is not None:
return enabled
return True
def deliver_message(self, message, sender=None, recips=None):
"""
Deliver a message via SMTP smarthost.
@ -368,17 +414,25 @@ class EmailHandler(GenericHandler):
"""
Send an email message.
This method can send a ``message`` you provide, or it can
construct one automatically from key/config/templates.
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.
The most common use case is assumed to be the latter, where
caller does not provide the message proper, but specifies key
and context so the message is auto-created. In that case this
method will also check :meth:`is_enabled()` and skip the
sending if that returns false.
:param key: When auto-creating a message, this is the
:term:`email key` identifying the type of 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.
as-is. If specified, nothing about the message will be
auto-assigned from config.
:param sender: Optional sender address for the
message/delivery.
@ -415,9 +469,27 @@ class EmailHandler(GenericHandler):
:meth:`make_auto_message()`. So, not used if you provide
the ``message``.
"""
if key and not self.is_enabled(key):
log.debug("skipping disabled email: %s", key)
return
if message is None:
if not key:
raise ValueError("must specify email key (and/or message object)")
# auto-create message from key + context
if sender:
kwargs['sender'] = sender
message = self.make_auto_message(key, context, **kwargs)
if not (message.txt_body or message.html_body):
raise RuntimeError(f"message (type: {key}) has no body - "
"perhaps template file not found?")
if not (message.txt_body or message.html_body):
if key:
msg = f"message (type: {key}) has no body content"
else:
msg = "message has no body content"
raise ValueError(msg)
self.deliver_message(message, recips=recips)

View file

@ -280,6 +280,22 @@ class TestEmailHandler(TestCase):
body = handler.get_auto_html_body('test_foo')
self.assertEqual(body, '<p>hello from foo html template</p>\n')
def test_is_enabled(self):
handler = self.make_handler()
# enabled by default
self.assertTrue(handler.is_enabled('default'))
self.assertTrue(handler.is_enabled('foo'))
# specific type disabled
self.config.setdefault('wutta.email.foo.enabled', 'false')
self.assertFalse(handler.is_enabled('foo'))
# default is disabled
self.assertTrue(handler.is_enabled('bar'))
self.config.setdefault('wutta.email.default.enabled', 'false')
self.assertFalse(handler.is_enabled('bar'))
def test_deliver_message(self):
handler = self.make_handler()
@ -387,24 +403,56 @@ class TestEmailHandler(TestCase):
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()
handler = self.make_handler()
with patch.object(handler, 'deliver_message') as deliver_message:
# 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()
# specify message w/ no body
msg = handler.make_message()
self.assertRaises(ValueError, handler.send_email, message=msg)
self.assertFalse(deliver_message.called)
# make_auto_message() called only if needed
with patch.object(handler, 'make_auto_message') as make_auto_message:
# again, but also specify key
msg = handler.make_message()
self.assertRaises(ValueError, handler.send_email, 'foo', message=msg)
self.assertFalse(deliver_message.called)
msg = handler.make_message()
handler.send_email(message=msg)
make_auto_message.assert_not_called()
# specify complete message
deliver_message.reset_mock()
msg = handler.make_message(txt_body="hello world")
handler.send_email(message=msg)
deliver_message.assert_called_once_with(msg, recips=None)
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')
# again, but also specify key
deliver_message.reset_mock()
msg = handler.make_message(txt_body="hello world")
handler.send_email('foo', message=msg)
deliver_message.assert_called_once_with(msg, recips=None)
# no key, no message
deliver_message.reset_mock()
self.assertRaises(ValueError, handler.send_email)
# auto-create message w/ no template
deliver_message.reset_mock()
self.assertRaises(RuntimeError, handler.send_email, 'foo', sender='foo@example.com')
self.assertFalse(deliver_message.called)
# auto create w/ body
deliver_message.reset_mock()
handler.send_email('foo', sender='foo@example.com', txt_body="hello world")
self.assertTrue(deliver_message.called)
# type is disabled
deliver_message.reset_mock()
self.config.setdefault('wutta.email.foo.enabled', False)
handler.send_email('foo', sender='foo@example.com', txt_body="hello world")
self.assertFalse(deliver_message.called)
# default is disabled
deliver_message.reset_mock()
handler.send_email('bar', sender='bar@example.com', txt_body="hello world")
self.assertTrue(deliver_message.called)
deliver_message.reset_mock()
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)