Files
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

70 lines
4.4 KiB
Markdown

# 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.