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:

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:

  1. applications['<application_id>'].services.email.<key>, only when an application_id is passed.

  2. A matching SYSTEM_EMAIL_<KEY> variable defined anywhere in Ansible variable scope (inventory, group vars, host vars).

  3. 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:

  • environment reads external, TLS_ENABLED, DOCKER_IN_CONTAINER.

  • tls reads external and TLS_ENABLED.

  • port reads external and tls.

  • host reads environment.

  • auth reads environment and tls.

  • from reads external and the no-reply user.

  • username reads from.

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:

  • DomainLookup resolves the SMTP host when environment points to an external Mailu deployment. Failures MUST fall back to localhost.

  • UsersLookup yields the no-reply user dict used to derive from and password. Missing entries MUST yield an empty dict. The fallback for from is then root@<inventory_hostname>.localdomain.

  • ApplicationsLookup reads services.email for the target application_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 %}
    
  • from is 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 call lookup('email') without an application_id so 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.

See Also ๐Ÿ”—ยถ

  • email.md for the administrator configuration reference.

  • base.md for the broader service registration, loading, and injection model.