feat: add scheduler and mail outbox
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
This commit is contained in:
+36
@@ -0,0 +1,36 @@
|
||||
---
|
||||
title: 'Scheduler and mail outbox'
|
||||
type: 'feature'
|
||||
ticket: '7'
|
||||
created: '2026-10-18'
|
||||
status: 'built'
|
||||
baseline_revision: '0dc079a'
|
||||
route: 'full'
|
||||
route_source: 'auto'
|
||||
risk: 'medium'
|
||||
review: 'quick'
|
||||
review_source: 'pinned'
|
||||
context:
|
||||
- '{project-root}/_bmad-output/architecture/architecture-ping-mee-self-service-platform-2026-10-15/ARCHITECTURE-SPINE.md'
|
||||
---
|
||||
|
||||
## 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 (`queued` to `sending`) before sending. A row still `sending` after 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.flush` job exists; later entries add theirs to `JOBS`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- `pytest` 319 passed, 6 skipped; `ruff check` clean.
|
||||
- Not verified: a real SMTP server and the scheduler thread inside the container (needs the Linux host).
|
||||
Reference in New Issue
Block a user