
# Sostituisci il provider email

`PUT /v1/email-provider` · autorizzazione `email-provider:write` · tutti i piani

Sostituisce il modo in cui l'ambiente invia le email. Puoi scrivere cinque modalità:

- `platform_default`: KeenAuth invia dal proprio indirizzo. Si può impostare solo `reply_to`.
- `platform_managed_domain`: KeenAuth invia dal tuo dominio, firmato con DKIM per quel dominio. Aggiungi prima il dominio con [domini mittenti email](../email-sender-domains/create.md) e pubblica prima i suoi record DNS; il dominio deve essere `verified`. Imposta `sender_domain_id` e un `from_address` esattamente in quel dominio (non in un suo sottodominio). Solo ambienti di produzione. Se il dominio in seguito smette di essere verificato, le email vengono inviate come `platform_default` finché il dominio non torna verificato; la configurazione in sé non cambia.
- `customer_smtp`: il tuo server SMTP sulla porta `465` (TLS implicito) o `587` (STARTTLS è obbligatorio). Il certificato deve essere valido per il nome host.
- `customer_api`: il tuo account presso `ses`, `sendgrid`, `postmark`, `mailgun` o `brevo`.
- `customer_webhook`: KeenAuth genera il messaggio e lo consegna a uno dei tuoi [endpoint webhook](../webhook-endpoints/create.md); la tua applicazione lo invia (vedi [Consegna webhook](#consegna-webhook)). Imposta `webhook_endpoint_id` su un endpoint abilitato di questo ambiente.

`If-Match` è obbligatorio e contiene la versione che hai letto (l'`ETag` di [get](get.md)). Per un ambiente mai configurato, invia `If-Match: "0"`; `*` non corrisponde. Una mancata corrispondenza restituisce `412`.

La credenziale può essere scritta una sola volta. Inviala in `credentials`; nessuna risposta la restituisce. Se la ometti, la credenziale salvata viene mantenuta solo se modalità e destinazione (host SMTP, porta e nome utente, oppure provider) non cambiano. Altrimenti la richiesta fallisce con `/credentials` `required`.

Una modifica cancella il risultato dell'ultimo test di connessione e scrive un record di audit (`email_provider.updated`) con la modalità, il provider e solo l'id del dominio mittente, mai un indirizzo o la credenziale. Dopo una modifica, esegui un [test di connessione](test.md).

## Richiesta

| Campo | Tipo | Obbligatorio | Descrizione |
|---|---|---|---|
| `mode` | stringa | sì | `platform_default`, `platform_managed_domain`, `customer_smtp`, `customer_api` o `customer_webhook`. |
| `sender_domain_id` | stringa | `platform_managed_domain` | Un dominio mittente `verified` di questo ambiente (`sdom_…`). Non ammesso in altre modalità. |
| `webhook_endpoint_id` | stringa | `customer_webhook` | Un [endpoint webhook](../webhook-endpoints/get.md) abilitato di questo ambiente (`whe_…`). Non ammesso in altre modalità. |
| `from_address` | stringa | tutte tranne `platform_default` | Indirizzo mittente semplice, ad esempio `noreply@example.com`. In `platform_managed_domain` il suo dominio deve essere esattamente il dominio mittente. Non ammesso in `platform_default`. |
| `reply_to` | stringa | no | Indirizzo di risposta semplice. |
| `provider` | stringa | `customer_api` | `ses`, `sendgrid`, `postmark`, `mailgun` o `brevo`. |
| `smtp_host` | stringa | `customer_smtp` | Nome DNS con almeno due etichette (non un indirizzo IP). |
| `smtp_port` | intero | `customer_smtp` | `465` o `587`. |
| `smtp_username` | stringa | `customer_smtp` | Nome utente SMTP AUTH, da 1 a 256 caratteri. |
| `credentials` | stringa | vedi sopra | Solo scrittura, da 1 a 1024 byte. `customer_smtp`: la password SMTP. `customer_api`: `<region>:<access key id>:<secret access key>` per `ses`; la chiave API per `sendgrid` e `mailgun` (prefisso `eu:` per l'endpoint UE del provider); il token del server per `postmark`; la chiave API per `brevo`. |

## Risposta

`200 OK` con la configurazione salvata (stessa struttura di [get](get.md)) e `ETag` impostato sulla nuova versione.

## Errori

| HTTP | code | Quando |
|---|---|---|
| 400 | `unknown_field` | Il corpo contiene un campo sconosciuto oppure non è un oggetto JSON. |
| 412 | `precondition_failed` | `If-Match` non corrisponde alla versione salvata (qualcuno ha salvato nel frattempo), oppure è `*` prima della prima scrittura. |
| 413 | `payload_too_large` | Il corpo supera i 16 KiB. |
| 428 | `precondition_required` | `If-Match` manca. |
| 422 | `validation_failed` | Un campo non supera la validazione; `errors[]` indica il campo e la regola (vedi sotto). |
| 401 | `unauthenticated` | La credenziale manca, non è valida o è stata revocata. |
| 403 | `insufficient_permissions` | La credenziale non ha l'autorizzazione richiesta. |
| 429 | `rate_limited` | È stato superato il limite di richieste; riprova dopo il periodo indicato in `Retry-After`. |
| 503 | `service_unavailable` | Una dipendenza non è disponibile temporaneamente; riprova dopo il periodo indicato in `Retry-After`. |

Regole in `errors[]`:

| Puntatore | Regola | Significato |
|---|---|---|
| `/mode` | `required`, `invalid` | La modalità manca o è sconosciuta. |
| `/mode` | `delivery_mode_unavailable` | La modalità non può inviare in questa installazione: `customer_webhook` richiede che la connessione di consegna webhook sia abilitata, `platform_managed_domain` richiede che la firma DKIM sia abilitata, e le modalità cliente richiedono che la consegna in uscita sia abilitata. |
| `/mode` | `production_only` | `platform_managed_domain` è consentito solo negli ambienti di produzione. |
| `/mode` | `fields_not_allowed` | È stato inviato un campo che la modalità non accetta. `customer_webhook` non accetta `provider`, campi SMTP, `credentials` o `sender_domain_id`. |
| `/sender_domain_id` | `required`, `invalid` | `platform_managed_domain` è stato inviato senza un id di dominio mittente, oppure l'id non è un id `sdom_…`. |
| `/sender_domain_id` | `not_found` | Questo ambiente non ha un dominio mittente con quell'id. Un dominio di un altro ambiente riceve la stessa risposta. |
| `/sender_domain_id` | `not_verified` | Il dominio mittente esiste ma non è `verified` (record DNS in attesa, degradato o disabilitato). |
| `/from_address` | `not_in_sender_domain` | L'indirizzo mittente non è esattamente nel dominio mittente. |
| `/webhook_endpoint_id` | `required`, `invalid` | `customer_webhook` è stato inviato senza un id endpoint, oppure l'id non è un id `whe_…`. |
| `/webhook_endpoint_id` | `not_found` | Questo ambiente non ha un endpoint webhook con quell'id. Un endpoint di un altro ambiente riceve la stessa risposta. |
| `/webhook_endpoint_id` | `not_enabled` | L'endpoint esiste ma non è abilitato. |
| `/credentials` | `required` | La modalità o la destinazione sono cambiate e non è stata inviata una nuova credenziale. |
| `/credentials` | `invalid` | La credenziale è vuota, troppo lunga, contiene caratteri di controllo o non rispetta il formato del provider. |
| `/from_address`, `/reply_to`, `/provider`, `/smtp_host`, `/smtp_port`, `/smtp_username` | `required`, `invalid` | Il campo manca o non è valido per la modalità. |

## Consegna webhook

Nella modalità `customer_webhook` KeenAuth non invia il messaggio e non ricorre mai al proprio invio. Per ogni messaggio che genera (oggetto, HTML e testo), invia un evento firmato all'endpoint tramite HTTPS:

- La richiesta è `POST` con `Content-Type: application/json` e gli header Standard Webhooks v1 `webhook-id`, `webhook-timestamp` e `webhook-signature`. `webhook-id` è l'id del messaggio (`msg_…`) ed è lo stesso in ogni tentativo dello stesso messaggio, quindi deduplica su di esso. Il tipo di evento non deve trovarsi nella sottoscrizione agli eventi dell'endpoint: l'endpoint lo riceve perché è l'endpoint di consegna.
- Il successo è qualsiasi risposta `2xx`. I reindirizzamenti non vengono seguiti e contano come errori. Un tentativo scade dopo 10 secondi.
- Un tentativo fallito viene ripetuto dopo 15 secondi, per un massimo di 3 tentativi in totale. Dopo il terzo errore il messaggio viene contrassegnato come fallito e non viene inviata alcuna email. Un endpoint eliminato o disabilitato dopo che hai salvato la configurazione fa fallire il messaggio subito, senza tentativi.
- Il contenuto del messaggio non viene memorizzato dal servizio webhook. Invia solo a endpoint di cui ti fidi: il corpo contiene l'indirizzo del destinatario e qualsiasi codice di accesso o link.

Corpo (`schema_version` 1):

```json
{
  "id": "msg_01k7xq3m5r8t9v0w1x2y3z4a5b",
  "type": "email.send_requested",
  "schema_version": 1,
  "environment_id": "env_01k7xq3m5r8t9v0w1x2y3z4a5b",
  "occurred_at": "2026-10-27T09:00:00Z",
  "data": {
    "message_id": "msg_01k7xq3m5r8t9v0w1x2y3z4a5b",
    "family": "email_otp",
    "locale": "en",
    "to": "ada@example.com",
    "from": "auth@example.com",
    "from_name": "Example",
    "reply_to": "support@example.com",
    "subject": "Your sign-in code",
    "html": "<p>Your code is 123456</p>",
    "text": "Your code is 123456",
    "link": "https://example.keenauth.app/...",
    "code": "123456"
  }
}
```

| Campo di `data` | Descrizione |
|---|---|
| `message_id`, `family`, `locale` | L'id del messaggio (uguale all'`id` dell'envelope), la famiglia di template e la lingua in cui il messaggio è stato generato. |
| `to` | Indirizzo del destinatario. |
| `from`, `from_name`, `reply_to` | Presenti solo se impostati: `from` è il `from_address` di questa configurazione. |
| `subject` | Oggetto generato. |
| `html`, `text` | Contenuto generato; almeno uno dei due è presente. |
| `link`, `code` | Presenti solo quando il messaggio contiene un link di accesso o un codice monouso. |

La firma è `v1,` seguito dal base64 di `HMAC-SHA256(key, "<webhook-id>.<webhook-timestamp>.<raw body>")`. La chiave è il valore di 32 byte codificato dai 43 caratteri base62 che seguono il prefisso `idp_whsec_` del segreto di firma dell'endpoint. Verifica il corpo grezzo della richiesta prima di analizzarlo, e rifiuta un timestamp che si discosta più di 300 secondi dal tuo orologio. Durante la rotazione di un segreto, `webhook-signature` contiene due firme separate da spazi.

Python:

```python
import base64, hashlib, hmac, time

ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
PREFIX = "idp_whsec_"


def signing_key(secret: str) -> bytes:
    """The HMAC key is the 32-byte value behind the 43 base62 characters after the prefix."""
    body = secret[len(PREFIX):len(PREFIX) + 43]
    n = 0
    for ch in body:
        n = n * 62 + ALPHABET.index(ch)
    return n.to_bytes(32, "big")


def verify(secret: str, headers: dict, raw_body: bytes, tolerance: int = 300) -> bool:
    msg_id = headers["webhook-id"]
    ts = headers["webhook-timestamp"]
    if abs(time.time() - int(ts)) > tolerance:
        return False
    signed = f"{msg_id}.{ts}.".encode() + raw_body
    mac = hmac.new(signing_key(secret), signed, hashlib.sha256).digest()
    expected = "v1," + base64.b64encode(mac).decode()
    # webhook-signature holds one or more space-separated signatures.
    return any(hmac.compare_digest(expected, s) for s in headers["webhook-signature"].split(" "))
```

Node.js:

```javascript
import { createHmac, timingSafeEqual } from "node:crypto";

const ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
const PREFIX = "idp_whsec_";

// The HMAC key is the 32-byte value behind the 43 base62 characters after the prefix.
function signingKey(secret) {
  let n = 0n;
  for (const ch of secret.slice(PREFIX.length, PREFIX.length + 43)) n = n * 62n + BigInt(ALPHABET.indexOf(ch));
  return Buffer.from(n.toString(16).padStart(64, "0"), "hex");
}

export function verify(secret, headers, rawBody, tolerance = 300) {
  const id = headers["webhook-id"];
  const ts = headers["webhook-timestamp"];
  if (Math.abs(Date.now() / 1000 - Number(ts)) > tolerance) return false;
  const mac = createHmac("sha256", signingKey(secret)).update(`${id}.${ts}.`).update(rawBody).digest();
  const expected = Buffer.from(`v1,${mac.toString("base64")}`);
  // webhook-signature holds one or more space-separated signatures.
  return headers["webhook-signature"].split(" ").some((s) => {
    const got = Buffer.from(s);
    return got.length === expected.length && timingSafeEqual(got, expected);
  });
}
```

## Esempio

```bash
curl -X PUT https://api.eu.keenauth.com/v1/email-provider \
  -H "Authorization: Bearer $KEENAUTH_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -H 'If-Match: "0"' \
  -d @email-provider.json
```

`email-provider.json`:

```json
{
  "mode": "customer_smtp",
  "from_address": "no-reply@example.com",
  "smtp_host": "smtp.example.com",
  "smtp_port": 587,
  "smtp_username": "mailer",
  "credentials": "<SMTP password>"
}
```

Per inviare i messaggi a un endpoint webhook:

```json
{
  "mode": "customer_webhook",
  "webhook_endpoint_id": "whe_01k7xq3m5r8t9v0w1x2y3z4a5b",
  "from_address": "auth@example.com"
}
```

In questa modalità non c'è alcuna connessione da verificare con [test](test.md); testa l'endpoint con [Testa un endpoint webhook](../webhook-endpoints/test.md).

Per inviare dal tuo dominio mittente verificato:

```json
{
  "mode": "platform_managed_domain",
  "sender_domain_id": "sdom_01k7xq3m5r8t9v0w1x2y3z4a5b",
  "from_address": "auth@mail.example.com",
  "reply_to": "support@example.com"
}
```

In questa modalità non c'è alcuna connessione da verificare con [test](test.md); controlla invece il dominio con [verify](../email-sender-domains/verify.md).

CLI: `keenauth email-provider put --if-match '"0"' --data @email-provider.json`. Conserva la credenziale in un file oppure passala dallo standard input (`--data -`), così non finisce nella cronologia della shell.
