SMTP adapter with TLS and a fake, outbox with retries, backoff, rate limit and restart safety, scheduler tick with job-run records started by gunicorn, and a send_test_mail command. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N3t4WL7bd9bdHHNjEsxzqC
2.2 KiB
2.2 KiB
title, type, ticket, created, status, baseline_revision, route, route_source, risk, review, review_source, context
| title | type | ticket | created | status | baseline_revision | route | route_source | risk | review | review_source | context | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Scheduler and mail outbox | feature | 7 | 2026-10-18 | built | 0dc079a |
full | auto | medium | quick | pinned |
|
Intent
Problem: Later epics must send e-mail (password change notices, confirmation links, announcements) without blocking requests, losing mail on restart or exceeding the provider's limits, and need a place to run repeating jobs.
Approach: adapters/mail is the only code that talks SMTP (TLS always, certificate verified), with an in-memory fake. mailer owns the outbox; other code calls enqueue(template, recipient, context) in its own transaction. scheduler owns JobRun and one 30-second tick that runs idempotent jobs, started in the gunicorn worker by docker/gunicorn.conf.py (AD-11, AD-12, AD-19).
Decisions (made by the build; the spine left them open)
- Retries: 5 attempts, waits of 1, 5, 15 and 60 minutes. Rate limit: 20 mails a minute. Named constants until the settings registry exists; the IONOS limit is still unknown (spec open question).
- Mails are rendered when queued, in German formal (
MAIL_LANGUAGE), as plain text only. HTML parts wait for the sanitizer (AD-16). - The body is removed from the database once the mail is sent or has failed, because it may hold a one-time link.
- Restart safety: a row is claimed (
queuedtosending) before sending. A row stillsendingafter 10 minutes is marked failed with an unknown outcome and is not sent again: a lost notice is better than a duplicate. - A refused login is treated as temporary, so a corrected password works on the next retry. A dead server stops the run after three consecutive outages.
manage.py send_test_mail <address>queues and sends a test mail at once, for the done check.- Only the
mail.flushjob exists; later entries add theirs toJOBS.
Implementation Notes
pytest319 passed, 6 skipped;ruff checkclean.- Not verified: a real SMTP server and the scheduler thread inside the container (needs the Linux host).