106 lines
5.8 KiB
Markdown
106 lines
5.8 KiB
Markdown
# 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`).
|