Files
smb-online/playbooks/mailcow-admin.md
T
mivanchenko d66511156a Track paris-barbershop client stack + mailcow-admin playbook in git
Both existed only on the live server / untracked locally until now.
Includes the open/closed status fix: the Filiale status badge was
hardcoded as "Geöffnet · schließt um 20:00" regardless of actual time
(caught showing "open" at 3am) -- now computed against the shop's own
Europe/Berlin clock and real weekly hours.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-12 03:11:10 +02:00

4.4 KiB

Playbook — Mailcow Administration

Provisioning and maintaining transactional email for smb-online clients. Mailcow lives at https://mail.mivanchenko.de/ (VPS, not the homelab box — separate host, already the MX for mivanchenko.de, already has a correct PTR record). DNS for mivanchenko.de and its subdomains is hosted at 1&1/IONOS (ns1107.ui-dns.com).

This is the reference for anyone (human or agent) provisioning a new client's mail, or doing general mailcow upkeep.

What "client mail" means here

Each client gets a per-client sending identity: noreply@<slug>.mivanchenko.de. The smb-crm backend (backoffice/app/booking_mail.py:_sender_for) auto-derives the From address from the client's domain field — it always sends as noreply@<domain>, never anything else, and this is not configurable per-send. Plan mailbox naming around that fact rather than fighting it.

Agent brief

You manage transactional email for the smb-online clients via mailcow (https://mail.mivanchenko.de/, admin UI) and 1&1/IONOS DNS for mivanchenko.de.

Your job, when asked to set up mail for a new client <slug>.mivanchenko.de:

  1. In mailcow: add <slug>.mivanchenko.de as a domain, generate a 2048-bit DKIM key for it, create one real mailbox (commonly mailing@<slug>.mivanchenko.de — a real inbox someone monitors), and add noreply@<slug>.mivanchenko.de as an alias of that mailbox with "Als dieser Alias senden erlauben" (send-as) enabled — this is required because booking_mail.py always sends From noreply@<domain>, never from the real mailbox address.
  2. Tell the operator the exact DNS records to add at 1&1/IONOS: MX (<slug>mail.mivanchenko.de, priority 10), SPF TXT (<slug>v=spf1 mx -all), DKIM TXT (host + value from mailcow's key page), and DMARC TXT (_dmarc.<slug>v=DMARC1; p=none; rua=mailto:mailing@<slug>.mivanchenko.de). You do not have DNS write access yourself unless explicitly given IONOS credentials — default to instructing the operator rather than assuming access.
  3. Once the mailbox password is available, set on the homelab server (/home/mivanchenko/smb-crm/.env, then docker compose up -d --no-deps --force-recreate smb-crm): SMTP_HOST=mail.mivanchenko.de, SMTP_PORT=587, SMTP_USERNAME=mailing@<slug>.mivanchenko.de, SMTP_PASSWORD=<password>, MAIL_FALLBACK_FROM=noreply@<slug>.mivanchenko.de. Append-only — never overwrite existing .env lines, and never cat/print the file's existing contents (it holds live secrets for other services too).
  4. Verify: trigger one real booking through /book/<slug> and confirm the confirmation email actually arrives (check spam folder too, first time for a new domain).

Guardrails: never delete a mailcow domain/mailbox/alias without explicit confirmation from the operator — that's a hard stop. Never disable "Als dieser Alias senden erlauben" on an alias that's actively used as a From address, since that silently breaks confirmation email delivery (it fails closed — mailer.py's _send_now logs and swallows the error, so this fails silently unless someone checks docker logs smb-crm). Treat mailbox passwords the same as any other secret: never read them from .env on the server, only accept them freshly from the operator or generate+set them without echoing back.

New client mail checklist (condensed)

  1. Mailcow → Mail Setup → Domains → add <slug>.mivanchenko.de.
  2. Mailcow → Mail Setup → Mailboxes → add mailing@<slug>.mivanchenko.de + strong password.
  3. Mailcow → ARC/DKIM keys → generate for <slug>.mivanchenko.de → copy the TXT record.
  4. Mailcow → domain → Add alias → noreply@<slug>.mivanchenko.de → target mailing@<slug>.mivanchenko.de → keep "Als dieser Alias senden erlauben" checked.
  5. 1&1 DNS: MX, SPF, DKIM, DMARC records per the agent brief above.
  6. smb-crm/.env: SMTP_* + MAIL_FALLBACK_FROM, then recreate the container.
  7. Test-book through the client's public booking page, confirm the email lands.

Known constraint

Sender identity is fixed to noreply@<client's domain field> by the current code — there is no per-client override to send from a different local part (e.g. mailing@ or termine@). If a client specifically wants a different visible sending address, that needs a small booking_mail.py change (a sender_local_part column or similar), not just mailbox setup.