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:
SJCoetz 2026-08-03 00:30:44 +02:00
parent 31d67f5796
commit 19620cc1c4

View 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