Files
computer-playbook/docs/administration/configuration/email.md
T
kevinveenbirkenbachandClaude Opus 5 d88fe1d83a test(lint): let the legacy-path guard see the trees that still named the old layout
`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>
2026-09-30 21:27:40 +02:00

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:

  1. applications['<application_id>'].services.email.<key>, applied only when an application_id is passed.
  2. 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.
  3. The plugin's built-in fallback computed from DOMAIN_PRIMARY, the web-app-mailu group, the no-reply user, and related inputs.

See Also 🔗

  • email.md for the plugin contract and extension rules.