docs: design Gmail SMTP email migration
Co-Authored-By: Claude <noreply@anthropic.com> Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
parent
31d67f5796
commit
19620cc1c4
1 changed files with 176 additions and 0 deletions
176
docs/superpowers/specs/2026-08-03-gmail-smtp-design.md
Normal file
176
docs/superpowers/specs/2026-08-03-gmail-smtp-design.md
Normal file
|
|
@ -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
|
||||
Loading…
Reference in a new issue