Files
computer-playbook/roles/web-app-stalwart/vars/main.yml
kevinveenbirkenbachandClaude Opus 5.5 c4d0283300 feat(mail): publish a DMARC aggregate report address (rua)
Both mail providers published "v=DMARC1; p=reject; ruf=...; adkim=s;
aspf=s" without a rua tag, so the domain owner received no aggregate
reports and could not see who sends in the domain's name or whether
legitimate mail fails alignment.

services.<entity>.dmarc.rua_user in web-app-stalwart and web-app-mailu
now names the users-catalogue key whose mailbox receives the aggregate
reports. It is set to administrator, the one user every deployment
provisions with a mailbox (user-administrator), and an inventory may
name another key. The address is built as <username>@<mail domain>,
which is the exact account or alias the provider creates, so the record
never points at an address the server does not serve. An assert in the
DNS task fails the deploy before any record is written when the user
carries no mailbox account (Stalwart), or neither a mailbox account nor
a forward alias (Mailu), so reports cannot bounce. Mailu's DMARC_RUA
env, which only feeds the record its admin UI suggests, now carries the
same local part instead of the unprovisioned "admin".

production_facts.py takes a required --dmarc-rua. check_dmarc parses
the record's tags and fails unless rua names that mailbox (size
suffixes and several URIs are accepted). The policy check now reads the
p tag instead of matching the substring "p=", which "sp=" also matched.

Closes #1125

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 20:56:40 +01:00

116 lines
6.5 KiB
YAML

---
application_id: "web-app-stalwart"
http_port: "{{ lookup('config', application_id, 'services.stalwart.ports.local.http') }}"
database_password: "{{ lookup('config', application_id, 'secrets.credentials.database_password') }}" # nocheck: lookup-config-path
STALWART_VOLUMES_DIR: "{{ lookup('container', application_id, 'directories.volumes') }}"
STALWART_CONFIG_DIR: "{{ [ STALWART_VOLUMES_DIR, 'etc' ] | path_join }}"
STALWART_CONFIG_FILE: "{{ [ STALWART_CONFIG_DIR, 'config.json' ] | path_join }}"
container_port: "{{ lookup('config', application_id, 'services.stalwart.ports.internal.http') }}"
STALWART_LOCAL_BASE_URL: "http://localhost:{{ container_port }}"
STALWART_JMAP_URL: "{{ STALWART_LOCAL_BASE_URL }}/jmap/"
STALWART_LOG_DIR: "{{ lookup('volume', application_id, 'data').target }}"
STALWART_BAN_TRACER:
"@type": "Stdout"
buffered: false
level: "debug"
eventsPolicy: "include"
events:
"security.authentication-ban": true
"security.abuse-ban": true
"security.scan-ban": true
"security.loiter-ban": true
"auth.failed": true
STALWART_JMAP_APPLICATION_GET: >-
{{ {'using': ['urn:ietf:params:jmap:core', 'urn:stalwart:jmap'],
'methodCalls': [['x:Application/get', {'ids': none}, 'c0']]} | to_json }}
STALWART_JMAP_TRANSIENT_ERRORS:
- serverUnavailable
- rateLimit
STALWART_CONTAINER_ADDRESS: "{{ lookup('container_address', application_id, 'stalwart') }}"
STALWART_DOMAIN: "{{ lookup('config', application_id, 'services.stalwart.domain') }}"
STALWART_DOMAIN_DNS_ZONE: "{{ STALWART_DOMAIN | to_zone }}"
STALWART_EMAIL: "{{ lookup('email', application_id) }}"
STALWART_HOSTNAME: "{{ STALWART_EMAIL.host }}"
STALWART_WEB_DOMAIN: "{{ lookup('domain', application_id) }}"
STALWART_HOSTNAME_DNS_ZONE: "{{ STALWART_HOSTNAME | to_zone }}"
container_hostname: "{{ lookup('container_hostname', application_id) }}"
STALWART_OIDC_ONION_ISSUER: "{{ lookup('tls', 'web-app-keycloak', 'domain').endswith('.onion') }}"
STALWART_OIDC_SOCKS_PROXY: "{{ lookup('tor_socks_proxy') if STALWART_OIDC_ONION_ISSUER | bool else '' }}"
# Keycloak access tokens carry neither a Stalwart audience nor a urn:ietf:params:oauth:scope:mail scope, so the v1 defaults reject every one of them.
STALWART_OIDC_TOKEN_VALIDATION:
skipAudienceValidation: true
unscopedTokens: "interoperable"
STALWART_CONTAINER: "{{ lookup('config', application_id, 'services.stalwart.name') }}"
STALWART_DATABASE_SHARED: "{{ lookup('database', application_id, 'shared') | bool }}"
STALWART_STORE_LAYOUT_VERSION: "1"
STALWART_STORE_MARKER_FILE: "{{ [ lookup('container', application_id, 'directories.instance'), 'store.layout' ] | path_join }}"
STALWART_ANTIVIRUS_ENABLED: "{{ lookup('config', application_id, 'services.clamav.enabled') | bool }}"
STALWART_CLAMAV_HOST: "clamav"
STALWART_CLAMAV_MILTER_PORT: "{{ lookup('config', application_id, 'services.clamav.ports.internal.milter') }}"
STALWART_CLAMAV_MILTER_TIMEOUT_CONNECT_MS: 5000
STALWART_CLAMAV_MILTER_TIMEOUT_COMMAND_MS: 5000
STALWART_CLAMAV_MILTER_TIMEOUT_DATA_MS: 15000
STALWART_CLAMAV_MILTER_CONF: "{{ [ STALWART_CONFIG_DIR, 'clamav-milter.conf' ] | path_join }}"
STALWART_WEBMAIL_BASE_URL: "{{ lookup('canonical_url', application_id, 'webmail') }}"
STALWART_WEBMAIL_DOMAIN: "{{ STALWART_WEBMAIL_BASE_URL | regex_replace('^https?://', '') }}"
STALWART_WEBMAIL_HTTP_PORT: "{{ lookup('config', application_id, 'services.webmail.ports.local.http') }}"
STALWART_WEBMAIL_CONFIG_DIR: "{{ [ STALWART_VOLUMES_DIR, 'webmail' ] | path_join }}"
STALWART_WEBMAIL_OAUTH_CONFIG: "{{ [ STALWART_WEBMAIL_CONFIG_DIR, 'oauth.inc.php' ] | path_join }}"
STALWART_WEBMAIL_OIDC_CLIENT_ID: "{{ lookup('config', application_id, 'services.sso.oidc.clients.webmail') }}"
STALWART_WEBUI_VERSION: "{{ lookup('config', application_id, 'services.stalwart.webui_version') }}"
STALWART_WEBUI_RESOURCE_URL: "https://github.com/stalwartlabs/webui/releases/download/{{ STALWART_WEBUI_VERSION }}/webui.zip"
STALWART_WEBUI_CACHE_FILE: "/var/cache/infinito/core/cache/package/stalwart-webui/webui-{{ STALWART_WEBUI_VERSION }}.zip"
STALWART_WEBUI_LOCAL_FILE: "{{ [ STALWART_CONFIG_DIR, 'webui.zip' ] | path_join }}"
STALWART_WEBUI_CONTAINER_FILE: "{{ lookup('volume', application_id, 'webui_bundle').target }}"
STALWART_WEBUI_LOCAL_URL: "file://{{ STALWART_WEBUI_CONTAINER_FILE }}"
STALWART_WEBUI_APP_DESCRIPTION: "Stalwart Web Interface"
STALWART_WEBUI_OAUTH_CLIENT_ID: "{{ lookup('config', application_id, 'services.sso.oidc.clients.webui') }}"
STALWART_WEBUI_BASE_URL: "{{ lookup('canonical_url', application_id) }}"
STALWART_IP4_PUBLIC: "{{ DOCKER_PUBLIC_BIND_HOST }}"
STALWART_ACTIVE_PROVIDER: "{{ lookup('mail_provider') == application_id }}"
STALWART_ADMIN_USER: "{{ lookup('users', 'stalwartadmin').username }}"
STALWART_ADMIN_PASSWORD: "{{ lookup('users', 'stalwartadmin').password }}"
STALWART_USERS: "{{ lookup('stalwart_users') }}"
STALWART_DMARC_RUF: "{{ lookup('config', application_id, 'users.administrator.email') }}"
STALWART_DMARC_RUA_USER: "{{ lookup('config', application_id, 'services.stalwart.dmarc.rua_user') }}"
STALWART_DMARC_RUA: "{{ lookup('users', STALWART_DMARC_RUA_USER).username }}@{{ STALWART_DOMAIN }}"
STALWART_BOT_APP_PASSWORD_DESCRIPTION: "Infinito.Nexus submission"
STALWART_BOT_APP_PASSWORD_RETIRED_DESCRIPTION: "Infinito.Nexus submission, retired"
stalwart_app_password_stale: {}
STALWART_RELAY_RULE: "!is_empty(authenticated_as)"
STALWART_BAN_PERIOD_MS: "{{ (lookup('config', application_id, 'services.stalwart.ban_period_seconds') | int) * 1000 }}"
STALWART_BAN_PERIODS:
authBanPeriod: "{{ STALWART_BAN_PERIOD_MS | int }}"
abuseBanPeriod: "{{ STALWART_BAN_PERIOD_MS | int }}"
loiterBanPeriod: "{{ STALWART_BAN_PERIOD_MS | int }}"
scanBanPeriod: "{{ STALWART_BAN_PERIOD_MS | int }}"
STALWART_SSO_ENABLED: "{{ lookup('sso', application_id, 'is_enabled') | bool }}"
STALWART_IMPORT_MAILU: "{{ lookup('config', application_id, 'services.stalwart.migration.import_mailu') | bool }}"
STALWART_PRODUCTION_HOST: "{{ MAIL_PRODUCTION_HOST | trim }}"
STALWART_PRODUCTION_RESOLVER: "{{ MAIL_PRODUCTION_RESOLVER | trim }}"
STALWART_PROMETHEUS_ENABLED: "{{ lookup('config', application_id, 'services.prometheus.enabled') | bool }}"
STALWART_MAIL_FACTS_METRICS_FILE: "{{ [ DIR_METRICS_TEXTFILE, 'stalwart-mail-facts.prom' ] | path_join }}"
STALWART_ONION_ACTIVE: "{{ 'svc-net-tor' in group_names }}"
STALWART_ONION_GATEWAY_ID: "svc-net-tor-smtp"
STALWART_ONION_ROUTE_NAME: "onion"
STALWART_ONION_RELAY_HOST: "{{ lookup('config', STALWART_ONION_GATEWAY_ID, 'services.tor-smtp.name') }}"
STALWART_ONION_RELAY_PORT: "{{ lookup('config', STALWART_ONION_GATEWAY_ID, 'services.tor-smtp.ports.internal.smtp') | int }}"