new-site/infra/ansible/roles/mail/defaults/main.yml
justin 3ca960aca5 docs+infra(deliverability): document bulk subdomain; ansible signs send.performancewest.net
- infra/ansible/roles/mail: refactor OpenDKIM to support multiple signing domains
  via opendkim_signing_domains list (root + send.performancewest.net). Loops
  keygen/ownership/keytable/signingtable so the live two-domain setup is
  reproducible from ansible.
- infra/ansible group_vars: add bulk_mail_subdomain + campaign_from_* +
  campaign_reply_to documentation vars (map to CAMPAIGN_FROM / HC_CAMPAIGN_FROM
  env read by the builder scripts). smtp_from (transactional) stays on root.
- docs/deliverability.md: rewrite TL;DR with the carrierone-vs-performancewest
  A/B proof (same server/IPs, different From domain -> Inbox vs Junk) and the
  ~85% Microsoft / 14% Google / <1% Yahoo audience mix; add the bulk-subdomain
  section, SPF trim, rehab-disabled, and the Hestia DNS automation runbook.
2026-06-18 23:12:05 -05:00

35 lines
1.7 KiB
YAML

---
# OpenDKIM signing for outbound mail (Postfix milter).
#
# CRITICAL: campaign mail is injected into Postfix from the Listmonk containers
# over the Docker bridge network, NOT from localhost. OpenDKIM only signs mail
# whose client is in InternalHosts; if the Docker subnet is missing there,
# OpenDKIM *verifies* (rather than *signs*) campaign mail, so every cold email
# goes out UNSIGNED. Since Feb 2024 Gmail/Yahoo require DKIM on bulk mail, so
# unsigned campaigns get junked/blocked (this caused the Jun 2026 deliverability
# collapse: ~23% delivery, Gmail 550-5.7.1). The Docker subnet below MUST be in
# opendkim_internal_hosts.
opendkim_selector: mail
opendkim_signing_domain: performancewest.net
opendkim_socket: "inet:8891@localhost"
# Signing domains. The root domain carries transactional/verification mail; the
# dedicated bulk subdomain (send.performancewest.net) carries Listmonk campaign
# mail so its sending reputation is isolated from the root domain (which then
# stays clean and recovers faster). Each entry generates its own key + selector
# and contributes a line to KeyTable/SigningTable. The first entry is treated as
# the primary (kept for backwards-compat with opendkim_signing_domain above).
# See docs/deliverability.md.
opendkim_signing_domains:
- domain: "{{ opendkim_signing_domain }}"
selector: "{{ opendkim_selector }}"
- domain: "send.performancewest.net"
selector: "send"
# Hosts OpenDKIM will SIGN for (vs verify). Must include the Docker bridge
# subnet so Listmonk container traffic is signed.
opendkim_internal_hosts:
- "127.0.0.1"
- "localhost"
- "172.16.0.0/12" # Docker bridge networks (Listmonk, workers, etc.)
- "10.0.0.0/8"