--- 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
` 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).