3
0
Fork 0

docs: add some docs about sending app emails, and logging to email

This commit is contained in:
Lance Edgar 2024-12-18 22:09:59 -06:00
parent fa76eb6aa9
commit c1f3fcc412
6 changed files with 337 additions and 2 deletions

View file

@ -55,6 +55,7 @@ Contents
narr/config/index
narr/db/index
narr/cli/index
narr/email/index
narr/handlers/index
narr/providers/index

View file

@ -1,6 +1,6 @@
Command Line Interface
======================
Command Line
============
Most apps will need some sort of command line usage, via cron or
otherwise. There are two main aspects to it:

113
docs/narr/email/custom.rst Normal file
View file

@ -0,0 +1,113 @@
Custom Emails
=============
It is fairly straightforward to add a new type of email for your app
to send.
Configure Template Dir
----------------------
Your project should contain a folder dedicated to email templates;
these would be committed to your repo as for other project files.
This is often ``email/templates`` or ``templates/email`` under the
package root.
Also the app config must include this path for the email templates
setting. The order matters here as the first template found via
lookup will be used, for a given key (and content-type). To include
"Poser" as well as built-in WuttaWeb template dirs:
.. code-block:: ini
[wutta.email]
templates =
poser:templates/email
wuttaweb:email/templates
Often you can set this in your :term:`config extension` instead of
needing to set it in the config file::
from wuttjamaican.conf import WuttaConfigExtension
class PoserConfigExtension(WuttaConfigExtension):
def configure(self, config):
config.setdefault('wutta.email.templates',
'poser:templates/email wuttaweb:email/templates')
However the config file value, if set, will override the extension
default.
Create the Template
-------------------
Now that you have a configured template folder, create the template
file(s) within it. Each type of email is expected to have templates
for one or both of the ``text/plain`` and ``text/html`` content-types
(using ``txt`` and ``html`` as shorthand name, respectively).
Template files must use the :doc:`Mako template language <mako:index>`
and be named based on the
:attr:`~wuttjamaican.email.message.Message.key` for the email type, as
well as content-type.
Therefore a new email of type ``poser_alert_foo`` would need one or
both of these defined:
* ``poser_alert_foo.html.mako``
* ``poser_alert_foo.txt.mako``
It is generally a good idea to create both templates but for internal
emails, it is often sufficient to define only the HTML template. And
styles for email messages are notoriously wonky but again, for
internal use one need not worry about that so much.
Keep in mind, any context you wish to reference within the template,
must be provided by caller when sending email.
.. note::
At this time there are no built-in email templates for
WuttJamaican. However there is (at least) one template defined in
`wuttaweb:email/templates
<https://forgejo.wuttaproject.org/wutta/wuttaweb/src/branch/master/src/wuttaweb/email/templates>`_
which you can reference as a real example.
Configure Sending
-----------------
With template file in the right place, your email can already be used.
However it would be sent only to the (app-wide) "default" recipients,
and with generic subject line.
To fix that you can add to your config file, again based on your email
key:
.. code-block:: ini
[wutta.email]
poser_alert_foo.subject = HIGH ALERT TYPE ${alert_type.upper()}
poser_alert_foo.sender = poser@example.com
poser_alert_foo.to = alert-monitor@example.com
poser_alert_foo.cc = admin@example.com
Note the subject line can be a Mako template string, referencing the
template context etc.
Test Sending
------------
Now you should be all set. When sending the email, you must provide
any context which may be needed for the template rendering. Assuming
you have that, call :meth:`~wuttjamaican.app.AppHandler.send_email()`
on your :term:`app handler`, giving it key and context::
app.send_email('poser_alert_foo', {
'alert_type': 'foo',
'alert_msg': "foo has unexpected value! or something happened, etc.",
})

18
docs/narr/email/index.rst Normal file
View file

@ -0,0 +1,18 @@
Email
=====
There is some built-in support for sending emails from your app. The
primary intended use case is for "internal" emails, e.g. sending daily
reports to the finance team etc. Each "type" of email can be sent to
different recipients.
You also may want to configure logging such that you receive email
when errors are logged.
.. toctree::
:maxdepth: 2
sending
custom
logging

107
docs/narr/email/logging.rst Normal file
View file

@ -0,0 +1,107 @@
Logging to Email
================
It's possible to configure logging such that when "errors" are logged,
an email can be sent to some recipient(s).
You can set this up however you like of course; see upstream docs for
more info:
* :doc:`python:library/logging`
* :ref:`python:smtp-handler`
But the example shown below does as follows:
* root logger is DEBUG+ and uses 3 handlers: file, console, email
* file handler
* writes to ``app/log/wutta.log`` (you should specify absolute path instead)
* will auto-rotate log file when size reaches 10MB
* uses "generic" entry formatter
* console handler
* writes to STDERR for the current process
* writes only INFO+ entries (so no DEBUG)
* uses "console" entry formatter
* email handler
* writes only ERROR+ entries (so no DEBUG, INFO or WARNING)
* email is From: sender and To: recip(s) with Subject: as shown
* uses "generic" entry formatter (for message body)
.. note::
This will *not* send email when "uncaught exceptions" occur. This
will only send email when an error is *logged*. For example::
import logging
log = logging.getLogger(__name__)
log.debug("do not email this")
log.info("nor this")
log.warning("nor this")
log.error("but *do* email this")
try:
raise RuntimeError
except:
log.exception("this also gets emailed")
# nb. no email is sent *here*, although possibly further up the
# stack another try/except block could be setup to log uncaught
# errors, in which case email may still be sent.
raise RuntimeError("this will just raise up the stack")
Now here is the example, which can be added to a normal :term:`config
file` (modifying as needed):
.. code-block:: ini
[loggers]
keys = root
[handlers]
keys = file, console, email
[formatters]
keys = generic, console
[logger_root]
handlers = file, console, email
level = DEBUG
[handler_file]
class = handlers.RotatingFileHandler
args = ('app/log/wutta.log', 'a', 1000000, 100, 'utf_8')
formatter = generic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
formatter = console
level = INFO
[handler_email]
class = handlers.SMTPHandler
args = ('localhost', 'poser@localhost', ['root@localhost', 'other@localhost'], "[Poser] Logging")
formatter = generic
level = ERROR
[formatter_generic]
format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(funcName)s: %(message)s
datefmt = %Y-%m-%d %H:%M:%S
[formatter_console]
format = %(levelname)-5.5s [%(name)s][%(threadName)s] %(funcName)s: %(message)s

View file

@ -0,0 +1,96 @@
Sending Email
=============
Here we'll describe enough to get started sending email.
Basics
------
To send an email you (usually) need 3 things:
* key - unique key identifying the type of email
* template - template file to render message body
* context - context dict for template file rendering
And actually the template just needs to exist somewhere it can be
found, but when calling
:meth:`~wuttjamaican.app.AppHandler.send_email()` you only need to
give the key and context::
app.send_email('poser_alert_foo', {
'alert_type': 'foo',
'alert_msg': "foo has unexpected value! or something happened, etc.",
})
In that example ``alert_type`` and ``alert_msg`` are the context, and
the template file(s) may display either/both however it wants.
If you do not provide all the needed context, you will likely get a
template rendering error. The only way to know for sure which context
data is needed, is to look at the template file itself.
Email Discovery
---------------
So how does the above work, e.g. how did it find the template? And
how can you know which are the "possible" email types you can send?
This is covered in more detail in :doc:`custom` but for now we'll
just say:
The template folder(s) must be configured, but otherwise "any email
type key" may be used. As long as the template(s) is found, the email
can be sent - albeit to global default recipients, unless that is
further configured for the email type.
In other words there is no "central registry" of the possible email
types, per se. In practice the list of template files, found within
configured template folders, is effectively the list of possible email
types. (There is no "default template" for sending.)
.. note::
At this time there are no built-in email templates for
WuttJamaican. However there is (at least) one template defined in
`wuttaweb:email/templates
<https://forgejo.wuttaproject.org/wutta/wuttaweb/src/branch/master/src/wuttaweb/email/templates>`_.
Email Delivery
--------------
If the email template can be found and rendered, it's time to "really"
send the email. How does that work?
Various message headers may be specified by caller, but usually they
will be auto-obtained from config. This includes the sender and
recipients, and subject line. If neither specifies anything regarding
the current email type, fallback "default" values are used (assuming
those are configured). This again is explained further in
:doc:`custom`.
So we have a complete message with all headers; the final step is to
send this via SMTP. While technically this supports sending to an
"external" SMTP server, the suggested use case is to always send to
localhost; to minimize lag and give full flexibility. (If sending to
localhost you should not need any specific config for that.)
In any case here is sample config if you were to use an external
SMTP server:
.. code-block:: ini
[wutta.mail]
smtp.server = smtp.example.com
smtp.username = mailuser
smtp.password = mailpass
.. note::
As of now, TLS is not supported! Which is because of the preferred
use of localhost as SMTP server. Obviously the local MTA software
(e.g. Postfix) can then send via another relay, and it should
probably use TLS for that.