docs: add Gmail SMTP implementation plan
Co-Authored-By: Claude <noreply@anthropic.com> Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
parent
19620cc1c4
commit
2391a88e35
1 changed files with 660 additions and 0 deletions
660
docs/superpowers/plans/2026-08-03-gmail-smtp.md
Normal file
660
docs/superpowers/plans/2026-08-03-gmail-smtp.md
Normal file
|
|
@ -0,0 +1,660 @@
|
|||
# Gmail SMTP Email Migration Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Replace Resend with Gmail SMTP through Nodemailer so RJ Bulk sends customer statements and test emails from `rjbulk01@gmail.com` without storing Gmail credentials in PocketBase.
|
||||
|
||||
**Architecture:** Add a small server-side mailer in `lib/email.ts` that owns Gmail SMTP configuration, sender selection, safe configuration errors, and attachment handling. Both existing server actions call this helper; statement generation, recipient validation, logging, and the test-email UI remain in their current layers. Remove the obsolete editable email settings and keep Gmail credentials in deployment environment variables only.
|
||||
|
||||
**Tech Stack:** Next.js 16 App Router and Server Actions, TypeScript strict mode, Nodemailer SMTP transport, Node `node:test` via the existing `tsx` dev dependency, pnpm, Gmail SMTP with STARTTLS on port 587.
|
||||
|
||||
---
|
||||
|
||||
## File map
|
||||
|
||||
- Create `lib/email.ts` — typed Gmail SMTP transport and provider-independent message sender.
|
||||
- Create `lib/email.test.ts` — unit tests for configuration validation, message mapping, attachment forwarding, and safe transport errors.
|
||||
- Modify `package.json` — add `nodemailer`, `@types/nodemailer`, and a `test` script; remove `resend` after migration.
|
||||
- Modify `pnpm-lock.yaml` — update the lockfile through pnpm commands.
|
||||
- Modify `actions/statements.ts:1-157` — replace the Resend send call while preserving statement business logic and success-only logging.
|
||||
- Modify `actions/settings.ts:1-128` — remove the PocketBase email-settings update action and route test email through the shared mailer.
|
||||
- Modify `components/forms/settings-forms.tsx:1-168` — replace editable Resend/SMTP fields with read-only Gmail delivery information and the existing test-recipient control.
|
||||
- Modify `AGENTS.md:19,72-74` — document Gmail SMTP and the two environment variables.
|
||||
- Modify `Dockerfile:4-6` — update the runtime environment comment.
|
||||
- Leave `lib/types.ts` and the PocketBase schema unchanged — legacy fields remain for compatibility but are no longer read by any send path.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Add Gmail mailer dependencies and test command
|
||||
|
||||
**Files:**
|
||||
- Modify: `package.json:4-43`
|
||||
- Modify: `pnpm-lock.yaml`
|
||||
|
||||
- [ ] **Step 1: Add Nodemailer and its TypeScript declarations**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pnpm add nodemailer
|
||||
pnpm add -D @types/nodemailer
|
||||
```
|
||||
|
||||
Expected: `package.json` contains `nodemailer` under `dependencies` and `@types/nodemailer` under `devDependencies`; `pnpm-lock.yaml` contains both packages.
|
||||
|
||||
- [ ] **Step 2: Add the test script**
|
||||
|
||||
Add this entry to the existing `scripts` object in `package.json`:
|
||||
|
||||
```json
|
||||
"test": "tsx --test lib/email.test.ts"
|
||||
```
|
||||
|
||||
Keep the existing `dev`, `build`, `start`, `lint`, and `pb:setup` scripts unchanged.
|
||||
|
||||
- [ ] **Step 3: Verify the dependency setup**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pnpm install --frozen-lockfile
|
||||
```
|
||||
|
||||
Expected: pnpm completes without changing the lockfile. If the lockfile changed during dependency installation, inspect the diff and keep the generated lockfile changes.
|
||||
|
||||
- [ ] **Step 4: Commit the dependency setup**
|
||||
|
||||
```bash
|
||||
git add package.json pnpm-lock.yaml
|
||||
git commit -m "build: add Gmail SMTP dependencies"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Write failing tests for the mailer contract
|
||||
|
||||
**Files:**
|
||||
- Create: `lib/email.test.ts`
|
||||
- Test target: `lib/email.ts` (not created until Task 3)
|
||||
|
||||
- [ ] **Step 1: Write the failing tests**
|
||||
|
||||
Create `lib/email.test.ts` with this complete content:
|
||||
|
||||
```ts
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { getGmailCredentials, sendEmail, type MailTransport } from "./email";
|
||||
|
||||
const env = {
|
||||
GMAIL_SMTP_USER: "rjbulk01@gmail.com",
|
||||
GMAIL_SMTP_APP_PASSWORD: "test-app-password",
|
||||
};
|
||||
|
||||
test("getGmailCredentials rejects missing Gmail configuration", () => {
|
||||
assert.throws(
|
||||
() => getGmailCredentials({}),
|
||||
{ message: "Gmail email credentials are not configured" },
|
||||
);
|
||||
});
|
||||
|
||||
test("sendEmail uses the authenticated Gmail user and forwards the message", async () => {
|
||||
const sent: unknown[] = [];
|
||||
const transport: MailTransport = {
|
||||
async sendMail(message) {
|
||||
sent.push(message);
|
||||
return {};
|
||||
},
|
||||
};
|
||||
|
||||
await sendEmail(
|
||||
{
|
||||
to: "customer@example.com",
|
||||
subject: "Delivery statement",
|
||||
html: "<p>Statement attached.</p>",
|
||||
attachments: [
|
||||
{
|
||||
filename: "statement.pdf",
|
||||
content: Buffer.from("pdf-bytes"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{ env, transport },
|
||||
);
|
||||
|
||||
assert.deepEqual(sent, [
|
||||
{
|
||||
from: "rjbulk01@gmail.com",
|
||||
to: "customer@example.com",
|
||||
subject: "Delivery statement",
|
||||
html: "<p>Statement attached.</p>",
|
||||
attachments: [
|
||||
{
|
||||
filename: "statement.pdf",
|
||||
content: Buffer.from("pdf-bytes"),
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test("sendEmail hides provider error details and credentials", async () => {
|
||||
const secret = "test-app-password";
|
||||
const transport: MailTransport = {
|
||||
async sendMail() {
|
||||
throw new Error(`authentication failed for ${secret}`);
|
||||
},
|
||||
};
|
||||
|
||||
await assert.rejects(
|
||||
sendEmail(
|
||||
{
|
||||
to: "customer@example.com",
|
||||
subject: "Delivery statement",
|
||||
html: "<p>Statement attached.</p>",
|
||||
},
|
||||
{ env, transport },
|
||||
),
|
||||
(error: unknown) =>
|
||||
error instanceof Error &&
|
||||
error.message === "Gmail email send failed" &&
|
||||
!error.message.includes(secret),
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
The injected `MailTransport` keeps the tests independent of Gmail and prevents tests from sending real mail.
|
||||
|
||||
- [ ] **Step 2: Run the tests and verify they fail for the intended reason**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pnpm test
|
||||
```
|
||||
|
||||
Expected: FAIL because `lib/email.ts` does not exist yet. Do not add a mock module or skip the tests; Task 3 supplies the implementation.
|
||||
|
||||
- [ ] **Step 3: Commit the failing tests**
|
||||
|
||||
```bash
|
||||
git add lib/email.test.ts
|
||||
git commit -m "test: define Gmail mailer behavior"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Implement the shared Gmail mailer
|
||||
|
||||
**Files:**
|
||||
- Create: `lib/email.ts`
|
||||
- Test: `lib/email.test.ts`
|
||||
|
||||
- [ ] **Step 1: Implement configuration validation and SMTP transport**
|
||||
|
||||
Create `lib/email.ts` with this complete implementation:
|
||||
|
||||
```ts
|
||||
import nodemailer, { type SendMailOptions } from "nodemailer";
|
||||
|
||||
export type EmailAttachment = {
|
||||
filename: string;
|
||||
content: Buffer;
|
||||
};
|
||||
|
||||
export type EmailMessage = {
|
||||
to: string;
|
||||
subject: string;
|
||||
html: string;
|
||||
attachments?: EmailAttachment[];
|
||||
};
|
||||
|
||||
export type MailTransport = {
|
||||
sendMail(message: SendMailOptions): Promise<unknown>;
|
||||
};
|
||||
|
||||
type EmailEnvironment = {
|
||||
GMAIL_SMTP_USER?: string;
|
||||
GMAIL_SMTP_APP_PASSWORD?: string;
|
||||
};
|
||||
|
||||
export function getGmailCredentials(env: EmailEnvironment = process.env) {
|
||||
const user = env.GMAIL_SMTP_USER?.trim();
|
||||
const appPassword = env.GMAIL_SMTP_APP_PASSWORD?.trim();
|
||||
|
||||
if (!user || !appPassword) {
|
||||
throw new Error("Gmail email credentials are not configured");
|
||||
}
|
||||
|
||||
return { user, appPassword };
|
||||
}
|
||||
|
||||
export function createGmailTransport(env: EmailEnvironment = process.env): MailTransport {
|
||||
const { user, appPassword } = getGmailCredentials(env);
|
||||
|
||||
return nodemailer.createTransport({
|
||||
host: "smtp.gmail.com",
|
||||
port: 587,
|
||||
secure: false,
|
||||
requireTLS: true,
|
||||
auth: {
|
||||
user,
|
||||
pass: appPassword,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function sendEmail(
|
||||
message: EmailMessage,
|
||||
options: { env?: EmailEnvironment; transport?: MailTransport } = {},
|
||||
): Promise<void> {
|
||||
const env = options.env ?? process.env;
|
||||
const { user } = getGmailCredentials(env);
|
||||
const transport = options.transport ?? createGmailTransport(env);
|
||||
|
||||
try {
|
||||
await transport.sendMail({
|
||||
from: user,
|
||||
to: message.to,
|
||||
subject: message.subject,
|
||||
html: message.html,
|
||||
attachments: message.attachments,
|
||||
});
|
||||
} catch {
|
||||
throw new Error("Gmail email send failed");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The helper fixes the sender to the authenticated Gmail user, enables STARTTLS on port 587, does not accept CC values, and never propagates raw provider error text.
|
||||
|
||||
- [ ] **Step 2: Run the focused tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pnpm test
|
||||
```
|
||||
|
||||
Expected: all three tests PASS.
|
||||
|
||||
- [ ] **Step 3: Run TypeScript validation**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pnpm exec tsc --noEmit
|
||||
```
|
||||
|
||||
Expected: TypeScript exits successfully. If Nodemailer's type declarations require a different import form, adjust only the import/types in `lib/email.ts` and rerun the focused tests.
|
||||
|
||||
- [ ] **Step 4: Commit the mailer implementation**
|
||||
|
||||
```bash
|
||||
git add lib/email.ts lib/email.test.ts
|
||||
git commit -m "feat: add Gmail SMTP mailer"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Migrate statement and test-email actions
|
||||
|
||||
**Files:**
|
||||
- Modify: `actions/statements.ts:1-5,107-138`
|
||||
- Modify: `actions/settings.ts:1-70`
|
||||
|
||||
- [ ] **Step 1: Replace the provider import in the statement action**
|
||||
|
||||
In `actions/statements.ts`, remove:
|
||||
|
||||
```ts
|
||||
import { Resend } from "resend";
|
||||
```
|
||||
|
||||
Add:
|
||||
|
||||
```ts
|
||||
import { sendEmail } from "@/lib/email";
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Replace the statement Resend call**
|
||||
|
||||
Replace the existing block from `// Email via Resend` through the `if (result.error)` return with:
|
||||
|
||||
```ts
|
||||
// Email via Gmail SMTP
|
||||
await sendEmail({
|
||||
to: parsed.data.recipient_email,
|
||||
subject: `Delivery Statement — ${cust.name} (${start} to ${end})`,
|
||||
html: `<p>Hi ${cust.name},</p>
|
||||
<p>Please find your delivery statement attached for the period <strong>${start}</strong> to <strong>${end}</strong>.</p>
|
||||
<p>Total litres delivered: <strong>${totalQty.toFixed(2)} L</strong><br/>
|
||||
Total value: <strong>R ${totalValue.toFixed(2)}</strong></p>
|
||||
<p>Thank you for your business.</p>
|
||||
<p>— RJ Bulk Supplies</p>`,
|
||||
attachments: [
|
||||
{
|
||||
filename: `delivery_statement_${cust.name.replace(/\s+/g, "_")}_${start.replace(/-/g, "")}.pdf`,
|
||||
content: Buffer.from(pdfBytes),
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
Do not add a `cc` property. Leave the existing `statement_log` creation immediately after this block so it still runs only after `sendEmail` resolves.
|
||||
|
||||
- [ ] **Step 3: Replace the test-email provider call**
|
||||
|
||||
In `actions/settings.ts`, remove:
|
||||
|
||||
```ts
|
||||
import { Resend } from "resend";
|
||||
```
|
||||
|
||||
Add:
|
||||
|
||||
```ts
|
||||
import { sendEmail } from "@/lib/email";
|
||||
```
|
||||
|
||||
Replace the body of `sendTestEmail` with:
|
||||
|
||||
```ts
|
||||
export async function sendTestEmail(to: string): Promise<ActionResult> {
|
||||
if (!to) return { ok: false, error: "Recipient email required" };
|
||||
|
||||
try {
|
||||
await sendEmail({
|
||||
to,
|
||||
subject: "RJ Bulk test email",
|
||||
html: "<p>This is a test email from your RJ Bulk back-office. If you got this, email is working correctly.</p>",
|
||||
});
|
||||
} catch (err: any) {
|
||||
return { ok: false, error: err?.message || "Send failed" };
|
||||
}
|
||||
|
||||
return { ok: true };
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Remove the obsolete email-settings action code**
|
||||
|
||||
Delete `emailSettingsSchema`, `clean`, and `updateEmailSettings` from `actions/settings.ts`. Remove now-unused imports `pbCreate`, `pbList`, and `pbUpdate`; keep `revalidatePath`, `z`, `PocketBase`, `getAuth`, `pbId` only if they are still used by the remaining user actions, and keep `PB_URL` for PocketBase admin operations.
|
||||
|
||||
- [ ] **Step 5: Run focused verification for the action migration**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pnpm exec tsc --noEmit
|
||||
```
|
||||
|
||||
Expected: no TypeScript errors. The existing statement code still owns PDF generation and `statement_log`; only the provider call changed.
|
||||
|
||||
- [ ] **Step 6: Commit the action migration**
|
||||
|
||||
```bash
|
||||
git add actions/statements.ts actions/settings.ts
|
||||
git commit -m "feat: send statements through Gmail SMTP"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Simplify the Settings email panel
|
||||
|
||||
**Files:**
|
||||
- Modify: `components/forms/settings-forms.tsx:31-163`
|
||||
|
||||
- [ ] **Step 1: Remove the old email-settings state and form wiring**
|
||||
|
||||
Delete these declarations from `SettingsForms`:
|
||||
|
||||
```ts
|
||||
const [emailState, emailAction, emailPending] = useActionState(updateEmailSettings, initial);
|
||||
const emailFormRef = useRef<HTMLFormElement>(null);
|
||||
```
|
||||
|
||||
Delete the `useEffect` that watches `emailState`, and change:
|
||||
|
||||
```ts
|
||||
const [testEmail, setTestEmail] = useState(settings?.statement_from_email || "");
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```ts
|
||||
const [testEmail, setTestEmail] = useState("");
|
||||
```
|
||||
|
||||
Remove `updateEmailSettings` from the settings action import. Keep `sendTestEmail`, `addUser`, `deleteUser`, `ActionResult`, and the existing hooks required by the user dialog and other settings controls.
|
||||
|
||||
- [ ] **Step 2: Replace the SMTP/Resend fields with a read-only Gmail panel**
|
||||
|
||||
Replace the existing email `<Card>` from `<Card>` before `Email settings` through its closing `</Card>` with:
|
||||
|
||||
```tsx
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Email delivery</CardTitle>
|
||||
<CardDescription>Customer statements are sent through Gmail SMTP.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="rounded-md border bg-muted/30 p-3 text-sm">
|
||||
<p className="font-medium">Configured sender</p>
|
||||
<p className="text-muted-foreground">rjbulk01@gmail.com</p>
|
||||
<p className="mt-2 text-muted-foreground">
|
||||
Gmail credentials are managed by the deployment environment and are not stored in PocketBase.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-end gap-2 pt-2 border-t">
|
||||
<div className="space-y-2 flex-1">
|
||||
<Label htmlFor="test">Send test email to:</Label>
|
||||
<Input id="test" type="email" value={testEmail} onChange={(e) => setTestEmail(e.target.value)} />
|
||||
</div>
|
||||
<Button type="button" variant="outline" onClick={onSendTest}>
|
||||
Send test
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
```
|
||||
|
||||
This removes sender, CC, fallback, and password inputs while retaining the test-email flow.
|
||||
|
||||
- [ ] **Step 3: Run the settings type check**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pnpm exec tsc --noEmit
|
||||
```
|
||||
|
||||
Expected: no references remain to `emailAction`, `emailPending`, `emailFormRef`, or `updateEmailSettings` in the component.
|
||||
|
||||
- [ ] **Step 4: Commit the Settings cleanup**
|
||||
|
||||
```bash
|
||||
git add components/forms/settings-forms.tsx
|
||||
git commit -m "ui: simplify Gmail delivery settings"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Remove Resend and update project documentation
|
||||
|
||||
**Files:**
|
||||
- Modify: `package.json`
|
||||
- Modify: `pnpm-lock.yaml`
|
||||
- Modify: `AGENTS.md:19,72-74`
|
||||
- Modify: `Dockerfile:4-6`
|
||||
|
||||
- [ ] **Step 1: Remove the unused Resend dependency**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pnpm remove resend
|
||||
```
|
||||
|
||||
Expected: `resend` is removed from `package.json` and `pnpm-lock.yaml`.
|
||||
|
||||
- [ ] **Step 2: Update the stack and email documentation**
|
||||
|
||||
Change the stack bullet in `AGENTS.md` from:
|
||||
|
||||
```md
|
||||
- **Resend** for transactional email
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```md
|
||||
- **Gmail SMTP via Nodemailer** for transactional email
|
||||
```
|
||||
|
||||
Replace the email section with:
|
||||
|
||||
```md
|
||||
## Email (Gmail SMTP)
|
||||
- Set `GMAIL_SMTP_USER` to `rjbulk01@gmail.com` and `GMAIL_SMTP_APP_PASSWORD` to a Google app password in `.env.local` or the production environment.
|
||||
- Gmail SMTP uses `smtp.gmail.com` on port 587 with STARTTLS.
|
||||
- The app sends statements and test emails from `rjbulk01@gmail.com` and does not use a default CC.
|
||||
- Without the Gmail variables, statement sending and test-email sending fail with a clear error; statement preview still works.
|
||||
```
|
||||
|
||||
Do not include an actual app-password value.
|
||||
|
||||
- [ ] **Step 3: Update Docker runtime guidance**
|
||||
|
||||
Change the required environment comment in `Dockerfile` from:
|
||||
|
||||
```dockerfile
|
||||
# POCKETBASE_URL, RESEND_API_KEY, RESEND_FROM
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```dockerfile
|
||||
# POCKETBASE_URL, GMAIL_SMTP_USER, GMAIL_SMTP_APP_PASSWORD
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Check active code and docs for stale provider references**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
grep -RInE 'Resend|resend|RESEND_API_KEY|RESEND_FROM' \
|
||||
actions components lib AGENTS.md Dockerfile package.json \
|
||||
--exclude='*.test.ts' || true
|
||||
```
|
||||
|
||||
Expected: no output. References in the approved migration design under `docs/superpowers/` are historical documentation and do not count as active provider references.
|
||||
|
||||
- [ ] **Step 5: Commit provider cleanup and documentation**
|
||||
|
||||
```bash
|
||||
git add package.json pnpm-lock.yaml AGENTS.md Dockerfile
|
||||
git commit -m "docs: document Gmail SMTP configuration"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 7: Run automated verification
|
||||
|
||||
**Files:**
|
||||
- No source changes expected.
|
||||
|
||||
- [ ] **Step 1: Run the focused mailer tests**
|
||||
|
||||
```bash
|
||||
pnpm test
|
||||
```
|
||||
|
||||
Expected: all three mailer tests pass.
|
||||
|
||||
- [ ] **Step 2: Run lint**
|
||||
|
||||
```bash
|
||||
pnpm lint
|
||||
```
|
||||
|
||||
Expected: ESLint exits with status 0. Fix only migration-related unused imports or formatting issues.
|
||||
|
||||
- [ ] **Step 3: Run a strict TypeScript check**
|
||||
|
||||
```bash
|
||||
pnpm exec tsc --noEmit
|
||||
```
|
||||
|
||||
Expected: no type errors.
|
||||
|
||||
- [ ] **Step 4: Run the production build**
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
```
|
||||
|
||||
Expected: the Next.js production build completes successfully. No email is sent during the build because mail actions execute only when invoked by an authenticated request.
|
||||
|
||||
- [ ] **Step 5: Check the final working tree and diff**
|
||||
|
||||
```bash
|
||||
git status --short
|
||||
git diff origin/main...HEAD --check
|
||||
```
|
||||
|
||||
Expected: only the intended Gmail migration commits/files are present, and `git diff --check` produces no output.
|
||||
|
||||
---
|
||||
|
||||
### Task 8: Configure Gmail and perform manual delivery verification
|
||||
|
||||
**Files:**
|
||||
- Local `.env.local` or the deployment provider's secret environment configuration; never commit this file.
|
||||
|
||||
- [ ] **Step 1: Revoke the previously exposed app password**
|
||||
|
||||
In the Google account security settings for `rjbulk01@gmail.com`, revoke the app password that was previously pasted into chat. Do not reuse it.
|
||||
|
||||
- [ ] **Step 2: Create a fresh Google app password**
|
||||
|
||||
Enable two-step verification if necessary, then create a new app password for the RJ Bulk application. Keep the value private.
|
||||
|
||||
- [ ] **Step 3: Configure the application environment**
|
||||
|
||||
Set these values in the local/deployment environment:
|
||||
|
||||
```text
|
||||
GMAIL_SMTP_USER=rjbulk01@gmail.com
|
||||
GMAIL_SMTP_APP_PASSWORD=(the newly generated private Google app password)
|
||||
```
|
||||
|
||||
Do not paste the value into source files, PocketBase, or chat. Restart the development server or redeploy after changing environment variables.
|
||||
|
||||
- [ ] **Step 4: Send a test email from Settings**
|
||||
|
||||
Open Settings, enter a mailbox that can be checked, and click `Send test`.
|
||||
|
||||
Expected: the recipient receives the test email from `rjbulk01@gmail.com`. If it fails, the UI returns a safe Gmail configuration/send error and does not expose the app password.
|
||||
|
||||
- [ ] **Step 5: Send a real statement**
|
||||
|
||||
Use Send Statement with a customer and date range containing delivery notes.
|
||||
|
||||
Expected: the recipient receives the existing HTML message with the generated PDF attached, the sender is `rjbulk01@gmail.com`, no CC is added, and the Statements log records `Email Sent`.
|
||||
|
||||
- [ ] **Step 6: Verify preview independence**
|
||||
|
||||
Use the statement preview without Gmail variables present in a separate environment or after temporarily removing them.
|
||||
|
||||
Expected: preview still renders because PDF generation does not depend on the email provider.
|
||||
|
||||
---
|
||||
|
||||
## Self-review checklist
|
||||
|
||||
- **Spec coverage:** Gmail SMTP/Nodemailer architecture is covered by Tasks 1–3; action migration by Task 4; Settings cleanup by Task 5; Resend removal and docs by Task 6; automated checks by Task 7; secure credentials and real delivery by Task 8.
|
||||
- **Security coverage:** The plan never requests, stores, or commits an actual app password; sender is fixed to the authenticated user; raw transport errors are hidden.
|
||||
- **Type consistency:** `MailTransport`, `EmailMessage`, `getGmailCredentials`, `createGmailTransport`, and `sendEmail` are defined in Task 3 and used consistently in Tasks 2 and 4.
|
||||
- **No unresolved placeholders:** The only parenthesized environment text is an explicit instruction that the secret belongs outside the repository; no task relies on an unspecified implementation or test.
|
||||
- **Scope:** This is one provider migration subsystem with a shared helper, two callers, UI cleanup, dependency/documentation updates, and manual verification.
|
||||
Loading…
Reference in a new issue