Skip to content

refactor(email): migrate email sending from Resend to Amazon SES - #85

Open
klin2686 wants to merge 6 commits into
mainfrom
refactor/migrate-email-to-ses
Open

klin2686 wants to merge 6 commits into
mainfrom
refactor/migrate-email-to-ses

Conversation

@klin2686

Copy link
Copy Markdown
Collaborator

Summary

Switches email sending from Resend (flat monthly subscription) to Amazon SES (pay per email), which fits Science Olympiad's seasonal usage much better. Behavior-preserving: every email keeps the same subject, body, and trigger — only _send() in email_service.py changes.

Closes #84

What changed

Backend

Logic

  • _send() now calls SES v2 send_email via boto3, run in asyncio.to_thread so it never blocks the event loop.
  • One cached sesv2 client per credential set, built from a dedicated boto3 Session (clients are thread-safe, the default session isn't).
  • Retries pinned to botocore standard mode — the boto3 default (legacy) doesn't retry SES v2's TooManyRequestsException.
  • New _SendRateLimiter spaces all sends 1 / SES_MAX_SEND_RATE seconds apart so bulk staff invites don't get throttled by SES limits.
  • A throttle is retried up to SES_MAX_ATTEMPTS times before the address is reported as failed.
  • With no AWS keys: dev logs and skips (dev also logs the email body so links are clickable); production raises error.
  • From-address is now configurable (EMAIL_FROM_ADDRESS) instead of hardcoded; the default is unchanged.
  • Rate limiting is per process — with 2+ uvicorn workers, set SES_MAX_SEND_RATE to quota / N (commented in code).

Config

  • Replaces RESEND_API_KEY with AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION (default us-west-2), EMAIL_FROM_ADDRESS, SES_MAX_SEND_RATE (default 1, the sandbox cap), SES_MAX_ATTEMPTS (default 4).
  • Settings now ignore unknown .env keys, so a leftover RESEND_API_KEY doesn't crash startup.
  • resend==2.32.2 removed from dependencies; boto3==1.43.103 added.

Out of scope

  • SES bounce/complaint handling (configuration set, SNS notifications, suppression).

Test plan

  • pytest passes locally

Automated

  • New tests/services/test_email_service.py: request shape, failure propagation, staff-invite partial failure, real-botocore retry behavior, rate-limit spacing, event loop responsiveness, and unconfigured no-op/prod-raise.
  • Existing tests using the mock_send_email fixture (auth, users, staff invites) pass unchanged.

Manual (with personal sandbox SES account in us-west-2, sending from a verified test domain nexus.kennethlin.dev, Easy DKIM + custom MAIL FROM mail.nexus.kennethlin.dev)

  • Signup verification: email arrives from NEXUS <[email protected]>, link verifies the account.
  • Forgot password: reset email arrives, link sets a new password, "password changed" notice arrives.
  • Email change: confirmation reaches the new address and the security notice reaches the old one; confirm link switches the email.
  • Staff invite, mixed batch (verified and unverified sandbox addresses): sent and failed counts match the verified and unverified recipient emails.
  • Received email shows that SPF, DKIM, and DMARC all pass, aligned to the sending domain.
  • Works with no AWS keys in dev: resend-verification returns 200 and logs the skipped email with its body.

@klin2686 klin2686 linked an issue Sep 26, 2026 that may be closed by this pull request
@railway-app

railway-app Bot commented Sep 26, 2026

Copy link
Copy Markdown

This PR was not deployed automatically as @klin2686 does not have access to the Railway project.

In order to get automatic PR deploys, please add @klin2686 to your workspace on Railway.

@vercel

vercel Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
nexus Ready Ready Preview Oct 2, 2026 6:28am UTC

This branch was successfully deployed

1 active and 1 inactive deployments
nexus / test2 — a5dfa8de Deployed Oct 2, 2026 by railway-app[bot]
Preview — a5dfa8de Deployed Oct 2, 2026 by vercel[bot]
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.

refactor(email): migrate email sending from Resend to Amazon SES

1 participant