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_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 fallback_key = None
""" """
Optional fallback key to use for config/template lookup, if 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() 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): def get_default_subject(self):
""" """
This must return the default subject, for sending emails of 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) return Message(**kwargs)
def make_auto_message( 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 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` :param default_subject: Optional :attr:`~Message.subject`
template/string to use, if config does not specify one. 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 :param fallback_key: Optional fallback :term:`email key` to
use for config/template lookup, if nothing is found for use for config/template lookup, if nothing is found for
``key``. ``key``.
@ -483,7 +518,12 @@ class EmailHandler(GenericHandler): # pylint: disable=too-many-public-methods
kwargs["sender"] = self.get_auto_sender(key) kwargs["sender"] = self.get_auto_sender(key)
if "subject" not in kwargs: if "subject" not in kwargs:
kwargs["subject"] = self.get_auto_subject( 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: if "to" not in kwargs:
kwargs["to"] = self.get_auto_to(key) kwargs["to"] = self.get_auto_to(key)
@ -567,16 +607,19 @@ class EmailHandler(GenericHandler): # pylint: disable=too-many-public-methods
key, key,
context=None, context=None,
rendered=True, rendered=True,
setting=None,
default=None, default=None,
fallback_key=None, fallback_key=None,
setting=None,
prefix=True,
default_prefix=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
(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 :param key: Key for the :term:`email type`. See also the
``fallback_key`` param, below. ``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 template will be returned, instead of the final/rendered
subject text. 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 default: Default subject to use if none is configured.
:param fallback_key: Optional fallback :term:`email key` to :param fallback_key: Optional fallback :term:`email key` to
use for config lookup, if nothing is found for ``key``. 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. :returns: Final subject text, either "raw" or rendered.
""" """
template = self.get_auto_subject_template( template = self.get_auto_subject_template(
@ -606,10 +656,18 @@ class EmailHandler(GenericHandler): # pylint: disable=too-many-public-methods
return template return template
context = self.get_email_context(key, context) 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( 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 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 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 :param setting: Optional :class:`EmailSetting` class or
instance. This may be used to determine the "default" instance. This may be used to determine the "default"
subject if none is configured. You can specify this as an subject if none is configured. You can specify this as an
optimization; otherwise it will be fetched if needed via optimization; otherwise it will be fetched if needed via
:meth:`get_email_setting()`. :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. :returns: Final subject template, as raw text.
""" """
# prefer configured subject specific to key # prefer configured subject specific to key
@ -664,6 +722,64 @@ class EmailHandler(GenericHandler): # pylint: disable=too-many-public-methods
default=self.universal_subject, 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): def get_auto_to(self, key):
""" """
Returns automatic :attr:`~wuttjamaican.email.Message.to` Returns automatic :attr:`~wuttjamaican.email.Message.to`

View file

@ -31,6 +31,17 @@ class TestEmailSetting(ConfigTestCase):
setting = MockSetting(self.config) setting = MockSetting(self.config)
self.assertEqual(setting.get_description(), "this should be a good test") 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): def test_sample_data(self):
setting = mod.EmailSetting(self.config) setting = mod.EmailSetting(self.config)
self.assertEqual(setting.sample_data(), {}) self.assertEqual(setting.sample_data(), {})
@ -136,6 +147,7 @@ class TestMessage(FileTestCase):
class mock_foo(mod.EmailSetting): class mock_foo(mod.EmailSetting):
default_subject = "MOCK FOO!" default_subject = "MOCK FOO!"
default_prefix = "[mock_foo]"
def sample_data(self): def sample_data(self):
return {"foo": "mock"} return {"foo": "mock"}
@ -253,7 +265,7 @@ class TestEmailHandler(ConfigTestCase):
self.assertIsInstance(msg, mod.Message) self.assertIsInstance(msg, mod.Message)
self.assertEqual(msg.key, "foo") self.assertEqual(msg.key, "foo")
self.assertEqual(msg.sender, "root@localhost") 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.to, [])
self.assertEqual(msg.cc, []) self.assertEqual(msg.cc, [])
self.assertEqual(msg.bcc, []) self.assertEqual(msg.bcc, [])
@ -270,7 +282,7 @@ class TestEmailHandler(ConfigTestCase):
self.assertIsInstance(msg, mod.Message) self.assertIsInstance(msg, mod.Message)
self.assertEqual(msg.key, "foo") self.assertEqual(msg.key, "foo")
self.assertEqual(msg.sender, "bob@example.com") 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.to, [])
self.assertEqual(msg.cc, []) self.assertEqual(msg.cc, [])
self.assertEqual(msg.bcc, []) self.assertEqual(msg.bcc, [])
@ -287,7 +299,7 @@ class TestEmailHandler(ConfigTestCase):
msg = handler.make_auto_message("test_foo") msg = handler.make_auto_message("test_foo")
self.assertEqual(msg.key, "test_foo") self.assertEqual(msg.key, "test_foo")
self.assertEqual(msg.sender, "bob@example.com") 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.to, ["sally@example.com"])
self.assertEqual(msg.cc, []) self.assertEqual(msg.cc, [])
self.assertEqual(msg.bcc, []) self.assertEqual(msg.bcc, [])
@ -311,7 +323,12 @@ class TestEmailHandler(ConfigTestCase):
get_auto_subject.assert_not_called() get_auto_subject.assert_not_called()
msg = handler.make_auto_message("foo") msg = handler.make_auto_message("foo")
get_auto_subject.assert_called_once_with( 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 # to
@ -421,36 +438,80 @@ class TestEmailHandler(ConfigTestCase):
) )
self.assertEqual(template, "whatever is clever") 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): def test_get_auto_subject(self):
handler = self.make_handler() handler = self.make_handler()
# global default # global default
subject = handler.get_auto_subject("foo") subject = handler.get_auto_subject("foo")
self.assertEqual(subject, "Automated message") self.assertEqual(subject, "[WuttJamaican] Automated message")
# can configure alternate global default # can configure alternate global default
self.config.setdefault("wutta.email.default.subject", "Wutta Message") self.config.setdefault("wutta.email.default.subject", "Wutta Message")
subject = handler.get_auto_subject("foo") subject = handler.get_auto_subject("foo")
self.assertEqual(subject, "Wutta Message") self.assertEqual(subject, "[WuttJamaican] Wutta Message")
# caller can provide default subject # caller can provide default subject
subject = handler.get_auto_subject("foo", default="whatever is clever") 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 # can configure just for key
self.config.setdefault("wutta.email.foo.subject", "Foo Message") self.config.setdefault("wutta.email.foo.subject", "Foo Message")
subject = handler.get_auto_subject("foo") 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") self.config.setdefault("wutta.email.bar.subject", "${foo} Message")
subject = handler.get_auto_subject("bar", {"foo": "FOO"}) 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) subject = handler.get_auto_subject("bar", {"foo": "FOO"}, rendered=False)
# nb. no prefix for unrendered template
self.assertEqual(subject, "${foo} Message") 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): def test_get_auto_recips(self):
handler = self.make_handler() handler = self.make_handler()