3
0
Fork 0

fix: add auto-prefix for message subject when sending email

This commit is contained in:
Lance Edgar 2025-12-20 23:00:24 -06:00
parent 5de9c15bbd
commit 8a0830b35d
2 changed files with 203 additions and 26 deletions

View file

@ -124,6 +124,14 @@ class EmailSetting: # pylint: disable=too-few-public-methods
default_subject = None
default_prefix = None
"""
Default subject prefix for emails of this type.
Calling code should not access this directly, but instead use
:meth:`get_default_prefix()` .
"""
fallback_key = None
"""
Optional fallback key to use for config/template lookup, if
@ -147,6 +155,20 @@ class EmailSetting: # pylint: disable=too-few-public-methods
"""
return self.__class__.__doc__.strip()
def get_default_prefix(self):
"""
This returns the default subject prefix, for sending emails of
this type.
Default logic here returns :attr:`default_prefix` as-is.
This method will often return ``None`` in which case the
global default prefix is used.
:returns: Default subject prefix as string, or ``None``
"""
return self.default_prefix
def get_default_subject(self):
"""
This must return the default subject, for sending emails of
@ -434,7 +456,14 @@ class EmailHandler(GenericHandler): # pylint: disable=too-many-public-methods
return Message(**kwargs)
def make_auto_message(
self, key, context=None, default_subject=None, fallback_key=None, **kwargs
self,
key,
context=None,
default_subject=None,
prefix_subject=True,
default_prefix=None,
fallback_key=None,
**kwargs,
):
"""
Make a new email message using config to determine its
@ -455,6 +484,12 @@ class EmailHandler(GenericHandler): # pylint: disable=too-many-public-methods
:param default_subject: Optional :attr:`~Message.subject`
template/string to use, if config does not specify one.
:param prefix_subject: Boolean indicating the message subject
should be auto-prefixed.
:param default_prefix: Default subject prefix to use if none
is configured.
:param fallback_key: Optional fallback :term:`email key` to
use for config/template lookup, if nothing is found for
``key``.
@ -483,7 +518,12 @@ class EmailHandler(GenericHandler): # pylint: disable=too-many-public-methods
kwargs["sender"] = self.get_auto_sender(key)
if "subject" not in kwargs:
kwargs["subject"] = self.get_auto_subject(
key, context, default=default_subject, fallback_key=fallback_key
key,
context,
default=default_subject,
prefix=prefix_subject,
default_prefix=default_prefix,
fallback_key=fallback_key,
)
if "to" not in kwargs:
kwargs["to"] = self.get_auto_to(key)
@ -567,16 +607,19 @@ class EmailHandler(GenericHandler): # pylint: disable=too-many-public-methods
key,
context=None,
rendered=True,
setting=None,
default=None,
fallback_key=None,
setting=None,
prefix=True,
default_prefix=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
(usually) renders the result using the given context.
(usually) renders the result using the given context, and adds
the :meth:`get_auto_subject_prefix()`.
:param key: Key for the :term:`email type`. See also the
``fallback_key`` param, below.
@ -588,15 +631,22 @@ class EmailHandler(GenericHandler): # pylint: disable=too-many-public-methods
template will be returned, instead of the final/rendered
subject text.
:param setting: Optional :class:`EmailSetting` class or
instance. This is passed along to
:meth:`get_auto_subject_template()`.
:param default: Default subject to use if none is configured.
:param fallback_key: Optional fallback :term:`email key` to
use for config lookup, if nothing is found for ``key``.
:param setting: Optional :class:`EmailSetting` class or
instance. This is passed along to
:meth:`get_auto_subject_template()`.
:param prefix: Boolean indicating the message subject should
be auto-prefixed. This is ignored when ``rendered`` param
is false.
:param default_prefix: Default subject prefix to use if none
is configured.
:returns: Final subject text, either "raw" or rendered.
"""
template = self.get_auto_subject_template(
@ -606,10 +656,18 @@ class EmailHandler(GenericHandler): # pylint: disable=too-many-public-methods
return template
context = self.get_email_context(key, context)
return Template(template).render(**context)
subject = Template(template).render(**context)
if prefix:
if prefix := self.get_auto_subject_prefix(
key, default=default_prefix, setting=setting, fallback_key=fallback_key
):
subject = f"{prefix} {subject}"
return subject
def get_auto_subject_template(
self, key, setting=None, default=None, fallback_key=None
self, key, default=None, fallback_key=None, setting=None
):
"""
Returns the template string to use for automatic subject line
@ -623,17 +681,17 @@ class EmailHandler(GenericHandler): # pylint: disable=too-many-public-methods
:param key: Key for the :term:`email type`.
:param default: Default subject to use if none is configured.
:param fallback_key: Optional fallback :term:`email key` to
use for config lookup, if nothing is found for ``key``.
: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()`.
:param default: Default subject to use if none is configured.
:param fallback_key: Optional fallback :term:`email key` to
use for config lookup, if nothing is found for ``key``.
:returns: Final subject template, as raw text.
"""
# prefer configured subject specific to key
@ -664,6 +722,64 @@ class EmailHandler(GenericHandler): # pylint: disable=too-many-public-methods
default=self.universal_subject,
)
def get_auto_subject_prefix(
self, key, default=None, fallback_key=None, setting=None
):
"""
Returns the string to use for automatic subject prefix, as
determined by config. This is called by
:meth:`get_auto_subject()`.
Note that unlike the subject proper, the prefix is just a
normal string, not a template.
Example prefix is ``"[Wutta]"`` - trailing space will be added
automatically when applying the prefix to a message subject.
:param key: The :term:`email key` requested.
:param default: Default prefix to use if none is configured.
:param fallback_key: Optional fallback :term:`email key` to
use for config lookup, if nothing is found for ``key``.
:param setting: Optional :class:`EmailSetting` class or
instance. This may be used to determine the "default"
prefix 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 prefix string
"""
# prefer configured prefix specific to key
if prefix := self.config.get(f"{self.config.appname}.email.{key}.prefix"):
return prefix
# or use caller-specified default, if applicable
if default:
return default
# or use fallback key, if provided
if fallback_key:
if prefix := self.config.get(
f"{self.config.appname}.email.{fallback_key}.prefix"
):
return prefix
# or prefix from email setting, if defined
if not setting:
setting = self.get_email_setting(key)
if setting:
if prefix := setting.get_default_prefix():
return prefix
# fall back to global default
return self.config.get(
f"{self.config.appname}.email.default.prefix",
default=f"[{self.app.get_node_title()}]",
)
def get_auto_to(self, key):
"""
Returns automatic :attr:`~wuttjamaican.email.Message.to`

View file

@ -31,6 +31,17 @@ class TestEmailSetting(ConfigTestCase):
setting = MockSetting(self.config)
self.assertEqual(setting.get_description(), "this should be a good test")
def test_get_default_prefix(self):
# empty by default
setting = mod.EmailSetting(self.config)
self.assertIsNone(setting.default_prefix)
self.assertIsNone(setting.get_default_prefix())
# but can override
setting.default_prefix = "[foo]"
self.assertEqual(setting.get_default_prefix(), "[foo]")
def test_sample_data(self):
setting = mod.EmailSetting(self.config)
self.assertEqual(setting.sample_data(), {})
@ -136,6 +147,7 @@ class TestMessage(FileTestCase):
class mock_foo(mod.EmailSetting):
default_subject = "MOCK FOO!"
default_prefix = "[mock_foo]"
def sample_data(self):
return {"foo": "mock"}
@ -253,7 +265,7 @@ class TestEmailHandler(ConfigTestCase):
self.assertIsInstance(msg, mod.Message)
self.assertEqual(msg.key, "foo")
self.assertEqual(msg.sender, "root@localhost")
self.assertEqual(msg.subject, "Automated message")
self.assertEqual(msg.subject, "[WuttJamaican] Automated message")
self.assertEqual(msg.to, [])
self.assertEqual(msg.cc, [])
self.assertEqual(msg.bcc, [])
@ -270,7 +282,7 @@ class TestEmailHandler(ConfigTestCase):
self.assertIsInstance(msg, mod.Message)
self.assertEqual(msg.key, "foo")
self.assertEqual(msg.sender, "bob@example.com")
self.assertEqual(msg.subject, "Attention required")
self.assertEqual(msg.subject, "[WuttJamaican] Attention required")
self.assertEqual(msg.to, [])
self.assertEqual(msg.cc, [])
self.assertEqual(msg.bcc, [])
@ -287,7 +299,7 @@ class TestEmailHandler(ConfigTestCase):
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.subject, "[WuttJamaican] hello foo")
self.assertEqual(msg.to, ["sally@example.com"])
self.assertEqual(msg.cc, [])
self.assertEqual(msg.bcc, [])
@ -311,7 +323,12 @@ class TestEmailHandler(ConfigTestCase):
get_auto_subject.assert_not_called()
msg = handler.make_auto_message("foo")
get_auto_subject.assert_called_once_with(
"foo", {}, default=None, fallback_key=None
"foo",
{},
default=None,
prefix=True,
default_prefix=None,
fallback_key=None,
)
# to
@ -421,36 +438,80 @@ class TestEmailHandler(ConfigTestCase):
)
self.assertEqual(template, "whatever is clever")
def test_get_auto_subject_prefix(self):
handler = self.make_handler()
# global default
prefix = handler.get_auto_subject_prefix("foo")
self.assertEqual(prefix, "[WuttJamaican]")
# can configure alternate global default
self.config.setdefault("wutta.email.default.prefix", "[bar]")
prefix = handler.get_auto_subject_prefix("foo")
self.assertEqual(prefix, "[bar]")
# can configure just for key
self.config.setdefault("wutta.email.foo.prefix", "[foo]")
prefix = handler.get_auto_subject_prefix("foo")
self.assertEqual(prefix, "[foo]")
# can configure via fallback_key
self.config.setdefault("wutta.email.bar.prefix", "[baz]")
prefix = handler.get_auto_subject_prefix("foofoo", fallback_key="bar")
self.assertEqual(prefix, "[baz]")
# EmailSetting can provide default prefix
providers = {
"wuttatest": MagicMock(email_modules=["tests.test_email"]),
}
with patch.object(self.app, "providers", new=providers):
handler = self.make_handler()
prefix = handler.get_auto_subject_prefix("mock_foo")
self.assertEqual(prefix, "[mock_foo]")
# or caller can provide default
prefix = handler.get_auto_subject_prefix("mock_foo", default="[zzz]")
self.assertEqual(prefix, "[zzz]")
def test_get_auto_subject(self):
handler = self.make_handler()
# global default
subject = handler.get_auto_subject("foo")
self.assertEqual(subject, "Automated message")
self.assertEqual(subject, "[WuttJamaican] 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")
self.assertEqual(subject, "[WuttJamaican] Wutta Message")
# caller can provide default subject
subject = handler.get_auto_subject("foo", default="whatever is clever")
self.assertEqual(subject, "whatever is clever")
self.assertEqual(subject, "[WuttJamaican] whatever is clever")
# 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")
self.assertEqual(subject, "[WuttJamaican] Foo Message")
# proper template is rendered
# 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")
self.assertEqual(subject, "[WuttJamaican] FOO Message")
# unless we ask it not to
# ..unless we ask it not to
subject = handler.get_auto_subject("bar", {"foo": "FOO"}, rendered=False)
# nb. no prefix for unrendered template
self.assertEqual(subject, "${foo} Message")
# now suppress/override the prefix
subject = handler.get_auto_subject("foo")
self.assertEqual(subject, "[WuttJamaican] Foo Message")
subject = handler.get_auto_subject("foo", prefix=False)
self.assertEqual(subject, "Foo Message")
subject = handler.get_auto_subject("foo", default_prefix="[foo]")
self.assertEqual(subject, "[foo] Foo Message")
def test_get_auto_recips(self):
handler = self.make_handler()