From 19620cc1c4ef7b23258e37cc4699d8c1207b6214 Mon Sep 17 00:00:00 2001 From: SJCoetz Date: Mon, 3 Aug 2026 00:30:44 +0200 Subject: [PATCH] docs: design Gmail SMTP email migration Co-Authored-By: Claude Co-authored-by: Orca --- .../specs/2026-08-03-gmail-smtp-design.md | 176 ++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-03-gmail-smtp-design.md diff --git a/docs/superpowers/specs/2026-08-03-gmail-smtp-design.md b/docs/superpowers/specs/2026-08-03-gmail-smtp-design.md new file mode 100644 index 00000000..21b1eddf --- /dev/null +++ b/docs/superpowers/specs/2026-08-03-gmail-smtp-design.md @@ -0,0 +1,176 @@ +# Gmail SMTP Email Migration Design + +**Date:** 2026-08-03 +**Status:** Approved for implementation + +## Goal + +Move RJ Bulk's transactional email delivery from Resend to Gmail SMTP for the currently working mailbox `rjbulk01@gmail.com`. The application should continue sending customer statement PDFs and test emails without changing statement generation or the existing send-statement workflow. + +The app does not currently have a company domain, so Gmail SMTP is the interim provider. The Gmail app password will be configured outside the repository and outside PocketBase. + +## Non-goals + +- No migration to the Gmail API or OAuth in this change. +- No individual delivery-note email workflow. +- No report-email workflow. +- No automatic payment/status changes when a statement is sent. +- No default CC recipient. The old `rjbulksupplies@gmail.com` address will not be used. +- No storage of Gmail credentials in PocketBase or in the Settings form. + +## Current state + +Resend is currently used in two server actions: + +- `actions/statements.ts` sends a generated statement PDF through `resend.emails.send`. +- `actions/settings.ts` sends the Settings test email through `resend.emails.send`. + +The application already validates the statement recipient, generates the PDF, handles provider errors, and records a statement log after a successful send. The email settings UI still contains legacy SMTP/Resend fields, including a plaintext password field that is not used by Resend. + +## Provider decision + +Use Gmail SMTP through Nodemailer. This matches the old Flask application's transport model and is considerably smaller than introducing Gmail API OAuth. + +### Environment configuration + +Use only these production secrets/configuration values: + +```text +GMAIL_SMTP_USER=rjbulk01@gmail.com +GMAIL_SMTP_APP_PASSWORD=(secret configured outside the repository) +``` + +`GMAIL_SMTP_APP_PASSWORD` must be a newly generated app password. The credential previously pasted in chat must be revoked and must not be reused. + +The SMTP transport uses Gmail's standard settings: + +```text +host: smtp.gmail.com +port: 587 +secure: false (STARTTLS) +``` + +The authenticated Gmail account is also the sender. The code must not permit a caller or PocketBase setting to override the sender address. + +## Architecture + +### Shared mailer helper + +Create a server-only helper at `lib/email.ts` with one responsibility: send an application email through Gmail SMTP. + +The helper will accept: + +- `to` +- `subject` +- HTML body +- optional PDF/file attachments + +It will lazily validate the two required environment variables and create/use the Gmail transport. It will return or throw a safe, user-facing error without including the app password or raw credentials. + +### Statement action + +Update `actions/statements.ts` to: + +1. Keep authentication, PocketBase lookups, date-range validation, note loading, PDF generation, and statement logging unchanged. +2. Remove the Resend import and provider-specific code. +3. Call `lib/email.ts` with `rjbulk01@gmail.com` as the authenticated sender, the submitted recipient, the existing subject/body, and the generated PDF buffer. +4. Send no CC. +5. Continue creating the `statement_log` record only after Gmail accepts the message. + +### Test-email action + +Update `actions/settings.ts` to remove Resend and call the shared mailer for the existing test-email button. The test message will have no attachment and will use the authenticated Gmail sender. + +## Settings UI and legacy fields + +Replace the misleading editable email configuration fields with a clear delivery-settings panel: + +- Explain that Gmail delivery is configured by the deployment environment. +- Do not render or accept an email password input. +- Do not render a sender override field. +- Do not render the obsolete CC field, since there is no working CC mailbox. +- Retain the recipient input and `Send test email` action. +- Show that the configured sender is `rjbulk01@gmail.com` without allowing it to be edited in PocketBase. + +The old PocketBase fields may remain in the schema temporarily for compatibility with existing records, but no send path will read `statement_email_password`, `statement_from_email`, `statement_app_email`, or `statement_cc_email`. + +## Data flow + +### Customer statement + +1. Authenticated user submits the existing Send Statement form. +2. The server action validates the customer, date range, recipient, and pricing flag. +3. It loads the customer and delivery notes from PocketBase. +4. It generates the existing PDF statement. +5. The shared Gmail helper sends the HTML email with the PDF attachment. +6. On successful provider acceptance, the action writes `Email Sent` to `statement_log` and revalidates the existing pages. +7. On failure, the action returns an error and does not write a successful send log. + +### Test email + +1. User enters a test recipient in Settings. +2. The server action validates that a recipient exists. +3. The shared Gmail helper sends the existing plain test message without an attachment. +4. The UI displays the existing success or error toast. + +## Error handling + +The helper/action boundary must handle: + +- Missing Gmail username +- Missing Gmail app password +- Invalid Gmail authentication +- SMTP connection/TLS errors +- Recipient rejection +- Provider send failures +- Attachment/message construction failures + +Errors shown to the user will be concise and safe. Credentials, SMTP authentication values, and raw configuration objects must never appear in returned errors or logs. + +Statement preview remains independent of email configuration and must continue working without Gmail credentials. + +## Security + +- Store the app password only in `.env.local` for local development and the hosting provider's secret environment configuration in production. +- Keep `.env*` ignored by Git. +- Never store the app password in PocketBase. +- Remove the current password placeholder behavior from the Settings form. +- Revoke the previously exposed Gmail app password before testing. +- Keep the sender fixed to the authenticated mailbox to avoid sender spoofing or Gmail rejection. + +## Dependencies and documentation + +- Add the `nodemailer` runtime dependency and the `@types/nodemailer` TypeScript declarations. +- Remove the Resend dependency once no imports remain. +- Update `AGENTS.md`, `Dockerfile`, and any email-related documentation to describe Gmail SMTP and the two new environment variables. +- Do not place actual credential values in documentation. + +## Verification plan + +Automated/static checks: + +1. Run the project's lint command. +2. Run `pnpm build`. +3. Confirm no `resend` imports, `RESEND_API_KEY`, or `RESEND_FROM` references remain in active email code/documentation. +4. Confirm no statement send path supplies a CC. +5. Confirm missing Gmail variables return a safe error. +6. Confirm statement logging remains after successful send only. + +Manual checks after the user configures a fresh Gmail app password: + +1. Use Settings → Send test email and confirm receipt at a test mailbox. +2. Send a statement to a customer address and confirm the PDF attachment opens. +3. Confirm the sender is `rjbulk01@gmail.com`. +4. Confirm no email is sent to the old CC address. +5. Test an invalid or missing configuration and verify the UI reports failure without exposing credentials. + +## Expected implementation files + +- Add: `lib/email.ts` +- Modify: `actions/statements.ts` +- Modify: `actions/settings.ts` +- Modify: `components/forms/settings-forms.tsx` +- Modify: `package.json` and lockfile +- Modify: `AGENTS.md` +- Modify: `Dockerfile` if its environment-variable guidance is provider-specific +- Potentially modify: `lib/types.ts`/PocketBase setup only if removing obsolete fields is later desired; schema removal is not required for this migration