swap-email-provider
Send email through something other than the self-hosted Stalwart server (another SMTP server, or an HTTP API such as SES or Postmark). Use when the user decides to move to a different mail path. Not f
Files1
View on GitHubSKILL.md.claude/skills/swap-email-provider/SKILL.md
GitHubSwap the email provider
- Interface:
EmailTransport(send(email): Promise<{ providerMessageId }>) inapps/notifications/src/channels/email/email-transport.ts. - Today:
SmtpTransport, the only transport: Mailpit locally, the Stalwart mail server in the cluster (and locally withbun run db:up:mail). - Created in:
apps/notifications/src/channels/email/email.module.ts. - Env:
SMTP_URL,EMAIL_FROM(an address of the accountSMTP_URLsigns in as; Stalwart refuses other senders). In productionSMTP_URLmust carry credentials and use TLS (smtps://…:465, orsmtp://…:587?requireTLS=true); the rules are insmtp-url.tsnext to the transport.
Another SMTP server needs no code: change SMTP_URL. The whole stack is self-hosted on
purpose; check with the user before moving mail to a managed service.
Add an HTTP provider
- A class implementing
EmailTransportnext toSmtpTransport. SendidempotencyKeyas the provider's idempotency key (retried jobs must not double-send), passheadersthrough (List-Unsubscribe), set a request timeout, and throw on non-2xx. - A setting that chooses it, and its key, in
apps/notifications/src/env.ts, with a boot-time check that the key is present, plus.env.exampleanddocs/environment.md. Choose the transport inemail.module.ts. - Deployed: the key goes in the notifications Secret (rotate-secrets skill).
- Bounces: the provider's feedback webhook needs its own route in apps/webhooks, like
src/inbound/stalwart.routes.ts, emittingemail.feedback_received.v1(add the provider to that event'sproviderenum in packages/contracts).
The SMS email sink (apps/notifications/src/channels/sms/email-sink.ts) always uses SMTP
and is for development only.
Tests
apps/notifications/test/notifications.integration.test.ts reads delivered mail from
Mailpit, and test/stalwart.integration.test.ts sends through a real Stalwart. For an
HTTP provider, add a small local fake of its API (like apps/notifications/test/fake-twilio.ts)
and a test that sends through it.
Finish
- The verify skill.
- Ask the
revieweragent to review the change, and thesecurity-revieweragent: a new implementation brings its own credentials and sends data somewhere new. If you wrote a migration, themigration-revieweragent too. - Update the seam's row in the README's "Scaling path" table if what's "Now" changed.