152 lines
5.6 KiB
Markdown
152 lines
5.6 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. |