Files
ipal-kit/docs/email.md
T

179 lines
6.8 KiB
Markdown

# email
Wysyłka maili z dwoma transportami do wyboru: **SMTP z panelu**
(`panelSmtpAdapter`, uniwersalny) albo **Microsoft Graph** (`graphAdapter`,
przez Exchange/M365). Oba implementują ten sam interfejs `PayloadEmailAdapter`,
więc `payload.sendEmail` i maile z formularzy działają niezależnie od wyboru.
Klient/projekt wybiera transport w configu.
## Zależność
```json
"dependencies": { "nodemailer": "^6.9.0" }
```
(+ `@types/nodemailer` w devDependencies)
## Config
Brak opcji — SMTP (host, port, user, password, from) jest w SiteIntegrations
(tab SMTP). Edytor konfiguruje w panelu.
## Front — sendEmail (server)
```ts
import { sendEmail } from '@intecion/ipal-kit/server'
const result = await sendEmail({
payload,
to: '[email protected]',
subject: 'Nowa wiadomość',
html: '<h1>Cześć</h1><p>Treść…</p>', // gotowy HTML (Twój wygląd)
// text: 'wersja plain', from: '...', replyTo: '...'
})
if (result.sent) {
// result.messageId
} else {
// result.error — np. "SMTP is not configured..."
}
```
- **HTML to Twój argument** — plugin nie ma templatek, wysyła to co podasz.
Wygląd maila składasz na froncie.
- Zwraca `{ sent: true, messageId }` albo `{ sent: false, error }` — nie
rzuca wyjątku, nie wycieka detali SMTP do klienta.
- Port 465 → implicit TLS, inne → STARTTLS.
- `server-only` — hasło SMTP nigdy w bundlu przeglądarki.
## Uwaga: maile systemowe Payloada
Reset hasła / weryfikacja email idą przez wbudowany mechanizm Payload
(`config.email`), którego ten moduł **nie** konfiguruje (celowo — wymagałby
SMTP w env). Jeśli ich potrzebujesz, to osobna konfiguracja adaptera przy
`buildConfig`.
## Adapter — SMTP z panelu dla całego Payloada
`sendEmail` wysyła własnym transportem. Osobno plugin daje adapter, który
podłącza tę samą skrzynkę pod `payload.sendEmail`:
```ts
// payload.config.ts
import { panelSmtpAdapter } from '@intecion/ipal-kit'
export default buildConfig({
email: panelSmtpAdapter(),
// panelSmtpAdapter({ fallbackFromAddress, fallbackFromName }) — używane tylko
// zanim panel zostanie wypełniony (Payload wymaga adresu synchronicznie przy
// starcie, zanim można odczytać globala)
})
```
Po wpięciu wszystko, co w Payloadzie wysyła maile, idzie przez SMTP z Site
Integrations — w tym wbudowane maile form-buildera (Forms → Emails), maile
resetu hasła i weryfikacji konta. Adapter czyta konfigurację przy każdym
wysłaniu, więc zmiana skrzynki w panelu działa bez restartu.
Bez adaptera Payload używa mocka, który tylko loguje do konsoli — maile
form-buildera nie wyjdą.
## Maskowanie sekretów w panelu (MaskedField)
Wrażliwe pola w Site Integrations (smtpPassword, r2SecretAccessKey,
turnstileSecretKey) są maskowane w UI — pokazują `••••` zamiast plaintextu, z
przyciskiem Reveal/Hide. To maskowanie UI, NIE hashowanie ani szyfrowanie:
wartość w bazie jest plaintext (musi być odzyskiwalna do autentykacji SMTP/R2).
Chroni przed patrzeniem przez ramię i przypadkowym pokazaniem panelu.
Podpięte przez `admin.components.Field: '@intecion/ipal-kit/client#MaskedField'`.
Działa na dowolnym polu `text`. Po wpięciu w projekcie może być konieczne
`payload generate:importmap`, żeby panel rozpoznał komponent.
> Główną ochroną sekretów pozostaje `read: isAdmin` na globalu SiteIntegrations
> (anonim nie dostaje). Maskowanie to warstwa dodatkowa (shoulder-surfing), nie
> ochrona bazy — przy wycieku DB sekrety są czytelne.
## Adapter — Microsoft Graph (Exchange / M365)
Alternatywa dla SMTP: wysyłka przez Microsoft Graph API, przez skrzynkę w
Waszym (agencyjnym) tenancie M365. Wszystkie maile z formularzy wszystkich
projektów idą przez JEDNĄ skrzynkę nadawczą (np. `[email protected]`).
### Podział konfiguracji (celowy)
**Sekrety w `.env`** (agencyjne — Wasz Exchange, klient nie widzi):
```bash
GRAPH_TENANT_ID=...
GRAPH_CLIENT_ID=...
GRAPH_CLIENT_SECRET=...
GRAPH_SENDER=[email protected] # jedna skrzynka dla wszystkich projektów
```
**From-display w panelu** (per projekt): czyta istniejące `smtpFromAddress` /
`smtpFromName` z SiteIntegrations — bo „from" to ten sam koncept niezależnie od
transportu. Nie trzeba nowego pola.
### Wpięcie — wybór transportu
```ts
// payload.config.ts
import { panelSmtpAdapter, graphAdapter } from '@intecion/ipal-kit'
email: process.env.GRAPH_CLIENT_ID
? graphAdapter() // Graph, gdy sekrety w .env
: panelSmtpAdapter(), // SMTP z panelu (fallback)
```
### Setup Azure / Exchange (jednorazowo, Wasza strona, POZA kodem)
1. **App registration** w Azure AD → `tenantId`, `clientId`
2. **Client secret** → `clientSecret`
3. **API Permissions** → Microsoft Graph → **Application** → `Mail.Send` →
**Grant admin consent** (bez tego: `Insufficient privileges`)
4. **Exchange Admin Center** → skrzynka `[email protected]` → Mailbox
Delegation → aplikacja do **"Send As"** (bez tego: `ErrorAccessDenied`)
### Szczegóły techniczne
- Auth: client credentials flow, scope `https://graph.microsoft.com/.default`
(NIE `Mail.Send` — Azure odrzuca, AADSTS1002012)
- Wysyłka: `POST /users/{sender}/sendMail` (NIE `/me` — app-only nie ma „me")
- Sukces: HTTP 202 (pusty body)
- Czysty REST (fetch), zero bibliotek Microsoft, zero nowych zależności
### PUŁAPKA — from vs Send-As
Jeśli `from` w panelu = cudza domena (np. `[email protected]`), a sender =
`[email protected]` — Exchange zablokuje, chyba że aplikacja ma Send-As na tę
domenę. Najbezpieczniej: `from` = `GRAPH_SENDER` (Wasza skrzynka), a adres
klienta w `replyTo` (odpowiedzi trafią do klienta). Wtedy Send-As na cudze
domeny nie jest potrzebny.
## Przełącznik transportu — mailAdapter
`mailAdapter()` to dyspozytor: jeden adapter wpięty w config, wybiera transport
(SMTP/Graph) przy KAŻDEJ wysyłce, czytając ustawienie z panelu. Dzięki temu
przełącznik działa w panelu (Payload buduje adapter raz przy starcie, więc nie
da się podmieniać osobnych adapterów w runtime — dyspozytor deleguje wewnątrz).
```ts
// payload.config.ts — JEDEN adapter, wybór wewnątrz
import { mailAdapter } from '@intecion/ipal-kit'
email: mailAdapter()
```
Panel → Site Integrations → SMTP → **Email Transport** (SMTP / Microsoft Graph).
Dyspozytor czyta ten wybór per wysyłka. Guard: jeśli wybrano Graph, ale brak
sekretów w .env → log + fallback na SMTP (nie cicha awaria).
## Test wysyłki — przycisk w panelu
W tabie SMTP jest przycisk **Send test**: podaj adres, kliknij, wyślij testowy
mail przez AKTUALNY transport. Pokazuje wynik (✓/✗ z błędem). Endpoint
`POST /api/ipal/test-email` (admin-only). Zapisz zmiany przed testem — endpoint
czyta z bazy, nie z pola na ekranie.
> Bezcenne przy diagnozie Graph — od razu widzisz `ErrorSendAsDenied`,
> `Insufficient privileges` itp. zamiast zgadywać.