# Email Backend ## Purpose The transactional email subsystem sends the three account lifecycle messages the backend produces: email-address verification, password reset, and invitation. It owns the SMTP transport (`EmailSender`) and the per-message classes that render an HTML body from a template file. It is infrastructure, not an HTTP slice: nothing here defines routes; the auth and users services call into it. ## Files - `src/services/email/index.ts` — `EmailSender` (default export): the transport wrapper and `isConfigured` guard. - `src/services/email/list/addressVerification.ts` — `EmailAddressVerificationEmail`. - `src/services/email/list/passwordReset.ts` — `PasswordResetEmail`. - `src/services/email/list/invitation.ts` — `InvitationEmail`. - `src/services/email/htmlTemplates/addressVerification/emailAddressVerification.html` - `src/services/email/htmlTemplates/passwordReset/passwordResetEmail.html` - `src/services/email/htmlTemplates/invitation/invitationTemplate.html` - `scripts/copy-assets.mjs` — copies the `htmlTemplates/` tree into `dist/` for the compiled build. Shared used: `@/shared/config` (the `email` config block), `@/shared/logger`, `@/shared/notifications/helpers` (`getNotification`), Node `fs`/`path`, `nodemailer`. ## Public Interface `EmailSender` (`src/services/email/index.ts`): - `new EmailSender(email: EmailMessage)` where `EmailMessage = { to: string; subject: string; html: () => Promise }`. - `send(): Promise<...>` — asserts `email`, `email.to`, `email.subject`, `email.html` are present, awaits `email.html()` to build the body, creates a `nodemailer` transport from `transportConfig`, and sends a mail with `from`, `to`, `subject`, the HTML body, and the header `X-SES-CONFIGURATION-SET: flatlogic-app`. Returns the `transporter.sendMail(...)` result. - `static get isConfigured(): boolean` — `true` only when both `config.email.auth.pass` and `config.email.auth.user` are set. - `get transportConfig()` — returns `config.email`. - `get from()` — returns `config.email.from`. Each message class takes its recipient plus a link/host in the constructor, exposes a `subject` getter, and an `async html(): Promise` that loads its template and substitutes placeholders. They are passed to `new EmailSender(...).send()`: - `EmailAddressVerificationEmail(to: string, link: string)` — subject from `getNotification('emails.emailAddressVerification.subject', getNotification('app.title'))`; `html()` reads `addressVerification/emailAddressVerification.html` and replaces `{appTitle}` with `app.title`, `{signupUrl}` with `link`, `{to}` with `to`. - `PasswordResetEmail(to: string, link: string)` — subject from `getNotification('emails.passwordReset.subject', getNotification('app.title'))`; `html()` reads `passwordReset/passwordResetEmail.html` and replaces `{appTitle}` with `app.title`, `{resetUrl}` with `link`, `{accountName}` with `to`. - `InvitationEmail(to: string, host: string)` — subject from `getNotification('emails.invitation.subject', getNotification('app.title'))`; `html()` reads `invitation/invitationTemplate.html`, builds `signupUrl = `${host}&invitation=true`` and replaces `{appTitle}` with `app.title`, `{signupUrl}` with that URL, `{to}` with `to`. ## Behavior / Notes - SMTP configuration is read from `config.email` (`src/shared/config/index.ts`), built from environment variables with constant defaults from `src/shared/constants/app.ts`: `from` (`EMAIL_FROM`, default `School Chain Manager `), `host` (`EMAIL_HOST`, default `email-smtp.us-east-1.amazonaws.com`), `port` (`EMAIL_PORT`, default `587`), `auth.user` (`EMAIL_USER`, default empty string), `auth.pass` (`EMAIL_PASS`, no default), and `tls.rejectUnauthorized: false`. Because `isConfigured` requires both `auth.user` and `auth.pass`, email sending is effectively disabled until both are provided. - Template loading uses `import.meta.dirname` (aliased to `__dirname` in each file) to resolve the template path relative to the running module, then `fs.readFile(path, 'utf8')`. Substitution is plain global `String.replace` of the `{placeholder}` tokens. On read or render failure each class logs via `logger.error` and rethrows. - Because `tsc` only emits `.ts` and these templates are read at runtime from a path derived from `import.meta`, `scripts/copy-assets.mjs` copies `src/services/email/htmlTemplates` into `dist/services/email/htmlTemplates` so the compiled production build can find them. ## Used By - `src/services/auth.ts` (`AuthService`): - `signup` and the related flow call `sendEmailAddressVerificationEmail(email, host)` only when `EmailSender.isConfigured`. - `signin` treats the user as `emailVerified` when `EmailSender.isConfigured` is false (so unconfigured environments do not block login). - `sendEmailAddressVerificationEmail(email, host?)` generates a verification token, builds `${host}/verify-email?token=...`, and sends an `EmailAddressVerificationEmail`. - `sendPasswordResetEmail(email, type = 'register' | 'invitation', host?)` generates a password-reset token, builds `${host}/password-reset?token=...`, and sends either an `InvitationEmail` (when `type === 'invitation'`) or a `PasswordResetEmail` otherwise. - `src/api/controllers/auth.controller.ts` exposes `EmailSender.isConfigured` over HTTP (`res.status(200).send(EmailSender.isConfigured)`) and calls the two `AuthService` send methods from its request handlers. - `src/services/users.ts` (`UsersService`): `create` and `bulkImport` call `AuthService.sendPasswordResetEmail(email, 'invitation', host)` to send invitations to newly created/imported users. ## Tests None yet. ## Related - `backend-architecture.md` (layering), `permissions.md`, and the auth slice (`src/services/auth.ts`, `src/api/controllers/auth.controller.ts`).