# 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<string> }`.
- `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<string>` 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 <app@flatlogic.app>`), `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`).
