Email Lookup Plugin ๐ฌยถ
This page documents the contract of the email Ansible lookup plugin and the rules contributors MUST follow when extending it or consuming it from a role.
For administrator-facing configuration (keys, override variables, precedence), see email.md.
Primary files:
email.py for the plugin.
test_email.py for unit tests.
Call Shapes ๐ยถ
lookup('email') # 0 terms: global defaults
lookup('email', '<application_id>') # 1 term: defaults + per-app overrides
Two or more terms MUST fail with AnsibleError.
Resolution Model ๐งฎยถ
For each short key, the plugin MUST resolve the value from the first defined source in this order:
applications['<application_id>'].services.email.<key>, only when anapplication_idis passed.A matching
SYSTEM_EMAIL_<KEY>variable defined anywhere in Ansible variable scope (inventory, group vars, host vars).A computed fallback defined in
_compute(short_key, resolved, variables).
Per-app overrides MUST be applied after global resolution completes so that an overridden key wins over both the inventory form and the computed fallback.
Intra-Key Dependencies ๐ยถ
Computed fallbacks are not independent.
_compute reads earlier keys from the in-progress resolved dict, so the declaration order in RESOLUTION_ORDER matters:
enabled โ timeout โ external โ environment โ domain โ tls โ port โ host โ
auth โ start_tls โ smtp โ from โ username โ password
Concrete dependencies contributors MUST preserve when changing fallbacks:
environmentreadsexternal,TLS_ENABLED,DOCKER_IN_CONTAINER.tlsreadsexternalandTLS_ENABLED.portreadsexternalandtls.hostreadsenvironment.authreadsenvironmentandtls.fromreadsexternaland theno-replyuser.usernamereadsfrom.
A new key MUST be placed in RESOLUTION_ORDER after every key it depends on, and the administrator key table MUST be updated to document the new field.
Helper Lookups ๐งฐยถ
_compute MUST delegate to three other lookup plugins rather than re-reading project state directly:
DomainLookupresolves the SMTP host whenenvironmentpoints to an external Mailu deployment. Failures MUST fall back tolocalhost.UsersLookupyields theno-replyuser dict used to derivefromandpassword. Missing entries MUST yield an empty dict. The fallback forfromis thenroot@<inventory_hostname>.localdomain.ApplicationsLookupreadsservices.emailfor the targetapplication_id. Unknown applications MUST yield an empty dict.
Each helper MUST forward roles_dir when present in self._kwargs so tests can point the lookup at a temporary role tree.
Templar and Boolean Coercion ๐งชยถ
Raw variable values MAY contain Jinja expressions.
Any string containing {{ MUST be rendered through self._templar with fail_on_undefined=False.
When no templar is attached (as in some unit tests), the value MUST be returned as-is so the plugin stays usable from direct Python tests.
_as_bool MUST accept the usual string forms (true|false|yes|no|1|0|on|off), booleans, and numbers.
Every boolean decision in the fallbacks MUST use it so that SYSTEM_EMAIL_* values coming from YAML, environment variables, or already-coerced facts all behave the same.
Consumer Rules ๐งทยถ
Role templates and tasks that consume the lookup MUST follow these rules:
Set the lookup result to a local variable once per section and reference its fields, for example:
{% set email = lookup('email', application_id) %} {% if email.enabled | bool %} SMTP_HOST={{ email.host }} SMTP_PORT={{ email.port }} SMTP_FROM={{ email['from'] }} SMTP_USERNAME={{ email.username }} SMTP_PASSWORD={{ email.password }} {% endif %}fromis a reserved word in Jinja. It MUST be accessed with bracket notation (email['from']), not dotted access.For
vars/main.yml, contributors MUST define a single holder variable (<ROLE>_EMAIL: "{{ lookup('email', application_id) }}") and reference its keys. Re-calling the lookup on every reference would re-run the resolution.Non-app consumers (any
sys-*role) MUST calllookup('email')without anapplication_idso they never trigger a per-app merge.
Mail Provider Dependency Detection ๐จยถ
Roles that call lookup('email', ...) are treated as dependents of the mail provider (web-app-stalwart).
The integration check test_mail_provider_dependency.py scans every role for the lookup('email' pattern and fails if such a role does not declare services.email with enabled: true and shared: true at the file root of its meta/services.yml.
A new email consumer MUST declare that service block in its roleโs meta/services.yml.