Skip to content

feat: mail module, and tenant invitations are emailed (#496) - #562

Merged
antosubash merged 19 commits into
mainfrom
feat/mail-module-496
Oct 21, 2026
Merged

antosubash merged 19 commits into
mainfrom
feat/mail-module-496

Conversation

@antosubash

Copy link
Copy Markdown
Owner

Closes #496

Summary

  • New mail module (simple_module_mail) owns outgoing mail. It provides the SMTP or console transport, its settings (backend, smtp_*, with the port limited to 1–65535), a 15 s SMTP timeout, and the on-demand mail.mailer check behind "Test mailer connection". A data migration (d7b3a9e5c1f2) moves the stored users.mailer / users.smtp_* rows to mail.*.
  • Core interface simple_module_core.mail provides MailMessage, the MailSender protocol, current_mail_sender() and resolve_mail_sender(db).
    • The sender and resolver are held per process, because the outbox worker has no app.
    • With nothing installed, mail falls back to a logging sender that never writes the message body, since bodies carry one-time tokens.
    • The captured_mail pytest fixture is new.
  • users sends through the core sender (TemplatedMailer). Templates, subjects and the Mailer protocol are unchanged, and simple_module_users now depends on simple_module_mail.
  • tenants emails invitations.
    • With background_tasks installed, the email goes through the durable outbox (tenants.send_invitation_email, retried). Otherwise it is sent after commit.
    • Nothing is queued without a delivering transport or an absolute link (public_base_url).
    • A worker started without Mail fails the delivery so it retries, instead of marking it sent.
    • InvitationIssued.emailed is new, and the invite form now says "Invitation emailed to …" or "Not emailed — share this link". The link and copy button stay in both cases.

Behaviour changes (in the CHANGELOG): the users mailer settings moved to mail, and the users.mailer health check is now mail.mailer.

Known trade-off (documented in docs/modules/tenants.md): an outbox-delivered invitation keeps its accept link, one-time token included, in the outbox row until the background_tasks retention purge.

Verification

  • /ship ran optimize, two review rounds (clean) and two browser QA rounds. QA used a local SMTP sink and a Celery outbox worker against a multi-tenant SQLite stack.
  • Local CI after rebasing on main: make lint passes, make test-py 4952 passed / 22 skipped, make test-js 668 passed.
  • make migrations-roundtrip-pg was not run locally; CI runs it.

QA Report

  • 2 rounds, 3 iterations; 1 bug found (P2: smtp_port accepted -1/0/99999) and fixed. A stale "emailed" banner on a duplicate invite was also fixed.
  • Categories: happy path (settings, invites over SMTP, console and relative links, password reset, Add people), form validation.
  • Full QA report: https://claude.ai/artifact/EyD4tNsYxPVNKzjNabKnhV

Report links are Claude Artifacts and start private. The author can share them from claude.ai/code/artifacts if reviewers need access.

Follow-ups (not in this PR)

  • SMTP on port 587 uses implicit TLS when smtp_tls is on, carried over from users. STARTTLS support would be a separate change.
  • Before the next release, register a PyPI pending publisher for the new simple_module_mail package, or its publish job fails.

Test plan

  • Set mail backend = smtp and tenants public_base_url, invite someone from /tenants/members, and confirm the email arrives with the accept link.
  • CI is green, including migrations-roundtrip-pg.

https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4

Queue invitation mail only when a transport is installed and the link is
absolute, so the one-time token never sits in the outbox for nothing; a
worker without the mail module fails the delivery (retry) instead of
marking it sent. Document the token-in-outbox trade-off.

Claude-Session: https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4
SMTP operations time out after 15s, and the tenants invitation send runs
after cache invalidation and event publish in the after-commit hook.

Claude-Session: https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4
Queue invitation mail only for a sender that delivers; docs name the
mail.mailer check and the mail settings section.

Claude-Session: https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Deploying simple-module-python with  Cloudflare Pages  Cloudflare Pages

Latest commit: 7a3cc52
Status: ✅  Deploy successful!
Preview URL: https://1569d8b8.simple-module-python.pages.dev
Branch Preview URL: https://feat-mail-module-496.simple-module-python.pages.dev

View logs

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
🔒 Security Review ✅ Completed 2026-10-20T13:03:07.148390Z 7a3cc52 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@antosubash
antosubash merged commit 05dabda into main Oct 21, 2026
14 checks passed
@antosubash
antosubash deleted the feat/mail-module-496 branch October 21, 2026 04:51
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.

tenants: invitation emails are never sent (InvitationCreated has no subscriber)

1 participant