`config/main.yml` exists in no role: f65f4d60fb migrated the last one. A guard
against the string already exists, `TestNoLegacyPathReferences`, and it names
`config/main.yml` among its LEGACY_TOKENS. It scanned plugins, utils, cli,
roles, scripts, filter_plugins and lookup_plugins, which is every tree except
the two the stale references actually lived in.
So nine of them survived in `tests/`, and the guard reported green the whole
time. The code underneath was already correct everywhere: `test_csp_consistency`
and `test_role_wiring` iterate ROLE_FILE_META_SERVICES, and only their docstrings
and one assertion message still said `config/main.yml`. Had any of them globbed
the old path the test would have gone vacuous rather than red, which is the
failure mode worth naming here.
`tests` and `tasks` are now scanned. The targets come from
`utils/roles/mapping.py`, not from guesswork: `config/main.yml` is
`meta/services.yml`, `schema/main.yml` is `meta/secrets.yml` (confirmed at
`cli/administration/inventory/provision/reset.py:67`, which reads exactly that
file), and `users/main.yml` is `meta/users.yml`.
Scanning `tasks` exposed a second dead pointer. The migration tool became a
package on 2026-09-14, but EXEMPT_FILES still named `migrate_meta_layout.py`, a
file that no longer exists, so the exemption was inert and only invisible
because `tasks` went unscanned. Two dead pointers covering for each other. The
exemption is now a directory, so a new module in that package cannot fall out of
it. `test_no_direct_calls.py` named the same dead path in prose; that lint
allows through nocheck markers at the call site, so nothing was broken there.
`docs/administration/configuration/email.md` was the worst of them, because it
instructed operators rather than describing tests: it told them to write
`compose.services.email` into `roles/<application_id>/config/main.yml`. The
consumer, `plugins/lookup/email.py:255`, reads `entry["services"]["email"]` and
says in its own comment that there is no `compose.services` envelope. The role
snippet, the inventory snippet and the precedence list now match the reader.
`group_vars/all/TODO.md` pointed at the same dead file.
Verified by watching the guard fail, twice. Re-introducing `config/main.yml` in
a test file reports it; pointing the new directory exemption at a wrong path
reports `tasks/utils/migrate_meta_layout/credentials.py`, which proves both that
`tasks` is scanned and that the exemption is the only thing letting the
migration tool through. Restored, 11 passed.
The changelogs under `docs/changelog/` keep their mentions, and `.md` stays out
of the scanned suffixes for that reason.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
3.8 KiB
Email Configuration 📬
This page documents the administrator-facing surface of outgoing email configuration. For the plugin contract and extension rules, see email.md.
Infinito.Nexus exposes outgoing email settings through the email lookup
plugin. All role templates and tasks read email settings via lookup('email')
(global) or lookup('email', '<application_id>') (per-app). No global
SYSTEM_EMAIL_* variable file ships with the project. Resolution defaults
derive from project-wide inputs: DOMAIN_PRIMARY, TLS_ENABLED,
DOCKER_IN_CONTAINER, the web-app-mailu group membership, and the
no-reply user.
Resolved Keys 🔑
Every call returns a flat dict with the following keys. Each key MAY be
overridden from inventory by setting the matching SYSTEM_EMAIL_<KEY> variable
(uppercase, with the SYSTEM_EMAIL_ prefix).
| Key | Override Variable | Default | Meaning |
|---|---|---|---|
enabled |
SYSTEM_EMAIL_ENABLED |
true |
Whether outgoing mail is enabled. |
timeout |
SYSTEM_EMAIL_TIMEOUT |
"30" |
SMTP/client timeout in seconds. |
external |
SYSTEM_EMAIL_EXTERNAL |
true if the host is in the web-app-mailu group, else false |
True when a web-app-mailu host is reachable (otherwise local delivery). |
environment |
SYSTEM_EMAIL_ENVIRONMENT |
external if external or TLS_ENABLED, else localhost, plus _container suffix if DOCKER_IN_CONTAINER and TLS_ENABLED is false |
One of external, external_container, localhost, localhost_container. |
domain |
SYSTEM_EMAIL_DOMAIN |
DOMAIN_PRIMARY |
Primary mail domain. |
tls |
SYSTEM_EMAIL_TLS |
false if not external, else TLS_ENABLED |
True when SMTPS SHOULD be used. |
port |
SYSTEM_EMAIL_PORT |
465 when external and tls, else 25 |
SMTP port. |
host |
SYSTEM_EMAIL_HOST |
localhost for external_container, localhost, localhost_container, else the web-app-mailu domain |
SMTP host. |
auth |
SYSTEM_EMAIL_AUTH |
false for external_container and localhost, else equal to tls |
Whether SMTP auth is required. |
start_tls |
SYSTEM_EMAIL_START_TLS |
false |
Whether STARTTLS is used. |
smtp |
SYSTEM_EMAIL_SMTP |
true |
Whether SMTP delivery is enabled. |
from |
SYSTEM_EMAIL_FROM |
email of the no-reply user when external, else root@<inventory_hostname>.localdomain |
Envelope/from address. |
username |
SYSTEM_EMAIL_USERNAME |
equal to from |
SMTP username. |
password |
SYSTEM_EMAIL_PASSWORD |
no-reply user's web-app-mailu token, else empty string |
SMTP password. |
Per-Application Overrides 🧩
Applications MAY override any subset of these keys under
services.email in their role or inventory.
Role defaults:
# roles/<application_id>/meta/services.yml
email:
host: smtp.app.example.org
port: 587
tls: true
Inventory overrides:
# group_vars/<group>.yml or host_vars/<host>.yml
applications:
web-app-nextcloud:
services:
email:
username: nextcloud@example.org
Overrides are merged on top of the resolved defaults. Keys MUST use the short, lowercased names listed above.
Precedence 📊
For each key, the first defined source wins:
applications['<application_id>'].services.email.<key>, applied only when anapplication_idis passed.- A
SYSTEM_EMAIL_<KEY>variable defined in inventory, group vars, or host vars. No such file ships with the project. This form is an escape hatch for site-specific overrides. - The plugin's built-in fallback computed from
DOMAIN_PRIMARY, theweb-app-mailugroup, theno-replyuser, and related inputs.
See Also 🔗
- email.md for the plugin contract and extension rules.