|
| 1 | +# mail |
| 2 | + |
| 3 | +Outgoing mail for the app. The module owns the transport (SMTP through `aiosmtplib`, or a `console` backend that logs each message) and its settings. Other modules never import it: they send through `simple_module_core.mail`, which works whether or not this module is installed. |
| 4 | + |
| 5 | +## ModuleMeta |
| 6 | + |
| 7 | +| Field | Value | |
| 8 | +|---|---| |
| 9 | +| `name` | `Mail` | |
| 10 | +| `route_prefix` | *(none)* | |
| 11 | +| `view_prefix` | *(none)* | |
| 12 | +| `depends_on` | `["Settings"]` | |
| 13 | + |
| 14 | +The module has no tables, routes or pages. It is configured on the generic settings page (**Settings -> Modules -> Mail**). |
| 15 | + |
| 16 | +## Settings |
| 17 | + |
| 18 | +DB-backed (`MailSettings`, package `mail`), hot-reloaded without a restart. |
| 19 | + |
| 20 | +| Field | Default | Notes | |
| 21 | +|---|---|---| |
| 22 | +| `backend` | `console` | `console` logs the whole message (development); `smtp` delivers | |
| 23 | +| `smtp_host` | `""` | | |
| 24 | +| `smtp_port` | `587` | | |
| 25 | +| `smtp_username` | `""` | blank means no authentication | |
| 26 | +| `smtp_password` | `""` | masked in the settings UI | |
| 27 | +| `smtp_from` | `no-reply@localhost` | | |
| 28 | +| `smtp_tls` | `true` | implicit TLS (`use_tls`) | |
| 29 | + |
| 30 | +Earlier versions kept these under `users` (`users.mailer`, `users.smtp_*`). A data migration renames the stored keys to `mail.backend` and `mail.smtp_*`; an existing `mail.*` row wins, and the downgrade reverses it. |
| 31 | + |
| 32 | +## Sending mail from a module |
| 33 | + |
| 34 | +```python |
| 35 | +from simple_module_core.mail import MailMessage, current_mail_sender |
| 36 | + |
| 37 | +sender = current_mail_sender() |
| 38 | +await sender.send(MailMessage(to="a@example.com", subject="Hi", text="Plain body")) |
| 39 | +``` |
| 40 | + |
| 41 | +- `current_mail_sender()` is synchronous and never raises; with nothing installed it returns a console sender that logs only the recipient and subject (bodies carry one-time tokens). |
| 42 | +- `sender.delivers_email` is `False` for console senders, so a screen can show a link instead of promising an inbox. |
| 43 | +- Code that may run in the outbox worker (no `app`) uses `await resolve_mail_sender(db)`. The module installs a resolver in `register_event_handlers` before its `app is None` return; it hydrates `MailSettings` from the given session. A worker restricted with `SM_MODULES_ENABLED` must therefore list `Mail` (and any module whose durable handler sends mail, such as `Tenants`); without it the tenants invitation handler raises, so the outbox retries the row (and marks it `dead` after its retries) instead of dropping the email. |
| 44 | + |
| 45 | +## Lifecycle |
| 46 | + |
| 47 | +| Hook | What it does | |
| 48 | +|---|---| |
| 49 | +| `register_settings` | `register_module_settings` mounts `app.state.mail` | |
| 50 | +| `register_health_checks` | adds the on-demand `mail.mailer` check | |
| 51 | +| `register_event_handlers` | installs the resolver; on `SettingsReloaded(package="mail")` rebuilds and reinstalls the sender | |
| 52 | +| `on_startup` | installs the sender built from the hydrated settings | |
| 53 | +| `on_shutdown` | uninstalls the sender and the resolver | |
| 54 | + |
| 55 | +## Health check |
| 56 | + |
| 57 | +`mail.mailer` (`probe=False`, so it never runs on a readiness timer) re-reads the installed sender on each run. The console backend is healthy ("needs no connection"); the SMTP backend connects and authenticates without sending anything. The settings page's **Test connection** button runs it. |
| 58 | + |
| 59 | +## Tests |
| 60 | + |
| 61 | +`simple_module_test` provides a `captured_mail` fixture that installs a recording sender for the test and restores the previous state afterwards. |
0 commit comments