# 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 or message-specific options 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, { loginUrl, temporaryPassword })` — subject from `getNotification('emails.invitation.subject', getNotification('app.title'))`; `html()` reads `invitation/invitationTemplate.html` and replaces `{appTitle}`, `{loginUrl}`, `{email}`, and `{temporaryPassword}`. Values are HTML-escaped before substitution. ## 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. - `backend/.env` is the checked-in development override file. It carries the non-secret SMTP defaults (`EMAIL_FROM`, `EMAIL_HOST`, `EMAIL_PORT`) plus empty `EMAIL_USER`/`EMAIL_PASS` slots. Fill those two credentials only in the local/VM environment that should send mail. Do not commit production SMTP credentials. - 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 a frontend `/verify-email?token=...` URL, and sends an `EmailAddressVerificationEmail`. - `sendPasswordResetEmail(email, type = 'register', host?)` generates a password-reset token, builds a frontend `/password-reset?token=...` URL, and sends a `PasswordResetEmail`. - `sendInvitationEmail(email, temporaryPassword, host?)` builds a frontend `/login` URL and sends an `InvitationEmail` containing the login URL, account email, and plaintext temporary password. The plaintext value is generated by the users service and is not stored. - `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` generate temporary passwords, store only their bcrypt hashes, mark invited internal accounts verified, and call `AuthService.sendInvitationEmail(...)` after commit. Bulk import sends invitations only for rows that the repository reports as created. When `EmailSender.isConfigured` is false, single-user create responses include the generated `temporaryPassword` once so the creator can copy it from the UI and deliver it manually. ## Tests - `src/services/users.test.ts` covers temporary-password hashing, invitation delivery, and the duplicate/ignored-row guard for bulk import. ## Configuration Set these variables in `backend/.env` for local development or in the VM process environment for deployment: ```bash EMAIL_FROM="School Chain Manager " EMAIL_HOST=email-smtp.us-east-1.amazonaws.com EMAIL_PORT=587 EMAIL_USER= EMAIL_PASS= ``` `EMAIL_USER` and `EMAIL_PASS` are the activation switch: if either is empty, `EmailSender.isConfigured` returns `false`, signup verification emails are skipped, and local sign-in treats users as verified so development is not blocked. When both are present, the platform sends verification, password-reset, and invitation emails through nodemailer. While email is disabled, `POST /api/users` and `POST /api/users/owner-with-organization` return the generated temporary password once in the API response. The frontend displays it in the create form with copy instructions. This fallback is only for the internal manual handoff workflow; the password is not persisted as plaintext on the backend. For VM deployments, keep SMTP credentials in the VM secret environment (for example `~/executor/.env` or the process manager environment) and restart the backend process after changing them. The checked-in `backend/.env` should stay limited to non-secret defaults and local placeholders. ## Related - `backend-architecture.md` (layering), `permissions.md`, and the auth slice (`src/services/auth.ts`, `src/api/controllers/auth.controller.ts`).