Skip to content

Add deploy preflight checks for email configuration - #102

Draft
davegaeddert wants to merge 2 commits into
masterfrom
claude/email-backend-preflight-check-95h3wy
Draft

Add deploy preflight checks for email configuration#102
davegaeddert wants to merge 2 commits into
masterfrom
claude/email-backend-preflight-check-95h3wy

Conversation

@davegaeddert

Copy link
Copy Markdown
Member

Email misconfiguration is a silent failure. The console, preview, and locmem backends all return a success count from send_messages() — nothing raises, so no exception tracker sees the loss. Anything that depends on email (password resets via plain.passwords, login links via plain.loginlink) just stops working with no signal.

Adds two deploy=True preflight checks in plain.email, following the security.debug pattern for "dev convenience left on in production."

Checks

Check Result id Flags Severity
email.backend email.backend_does_not_deliver EMAIL_BACKEND set to the console, preview, or in-memory backend Error
email.smtp_host email.smtp_host_empty SMTP backend with an empty EMAIL_HOST Error
email.smtp_host email.smtp_host_is_default SMTP backend with EMAIL_HOST still at its default "localhost" Warning

email.smtp_host exists so email.backend doesn't hand out false confidence: the more common production failure isn't the console backend left on, it's a correct-looking SMTP config that was never pointed at a mail server. An empty host is an error because smtplib skips connecting when the host is falsy, so starttls() raises SMTPServerDisconnected on the first send. The default "localhost" is only a warning — a mail relay on the same host is a legitimate setup.

Deployments that intentionally send no email silence by result id; the README documents both ids.

Notes

  • Gating is free: preflight autodiscovery imports per installed package, so the checks only exist when plain.email is in INSTALLED_PACKAGES.
  • Backend import paths move to plain/email/backends/__init__.py, so the toolbar, the mailoutbox fixture, and the checks share one definition instead of three copies of the same string.
  • example/app/settings.py uses the preview backend, so plain preflight --deploy there now errors. That's the check working as intended; no CI job passes --deploy.

Testing

  • 8 new tests in plain-email/tests/public/test_preflight.py (22 pass in the package).
  • Verified end-to-end: plain preflight --deploy in the test app reports email.backend_does_not_deliver against its console backend, and a run without --deploy skips both checks.
  • ruff check / ruff format --check / ./scripts/type-check plain-email all clean.

The full ./scripts/test and ./scripts/fix runs could not complete in this environment — no Postgres available, and the oxlint/oxfmt download is blocked by the sandbox proxy. Python linting, formatting, type checks, and the plain-email suite were all run directly instead.


Generated by Claude Code

claude added 2 commits August 19, 2026 01:21
Email misconfiguration is a silent failure: the console, preview, and
locmem backends all report every send as a success, so nothing raises and
no exception tracker sees the loss. Anything depending on email (password
resets, login links) just stops working.

Add two `deploy=True` preflight checks in plain.email:

- `email.backend` (error) flags a non-delivering backend.
- `email.smtp_host` (warning) flags the SMTP backend still pointed at the
  default "localhost". Only a warning — a local mail relay is a legitimate
  setup — but it closes the gap where a correct-looking SMTP config fails
  at send time.

Backend import paths move to plain/email/backends/__init__.py so the
toolbar, the mailoutbox fixture, and the checks share one definition.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015tkqCu7EDmedbxieWcHSLk
- email.smtp_host now flags an empty EMAIL_HOST as an error. smtplib skips
  connecting when the host is falsy, so starttls() raises
  SMTPServerDisconnected on the first send — exactly the failure the check
  exists to catch, previously passing because it only matched "localhost".
- Document both silenceable result ids in the README. The local-relay setup
  the docs call legitimate had no id to copy, and a guess would trip
  preflight.unused_silence.
- Correct the module docstring: deploy checks also run from the admin
  preflight view and toolbar badge whenever DEBUG is False, not only under
  `plain preflight --deploy`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015tkqCu7EDmedbxieWcHSLk
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants