# 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: 'kontakt@klient.pl', subject: 'Nowa wiadomość', html: '

Cześć

Treść…

', // 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. `forms@intecion.pl`). ### Podział konfiguracji (celowy) **Sekrety w `.env`** (agencyjne — Wasz Exchange, klient nie widzi): ```bash GRAPH_TENANT_ID=... GRAPH_CLIENT_ID=... GRAPH_CLIENT_SECRET=... GRAPH_SENDER=forms@intecion.pl # 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 `forms@intecion.pl` → 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. `noreply@klient.pl`), a sender = `forms@intecion.pl` — 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ć.