Skip to content

Connettore: SendGrid (mail via API REST) ​

Invia mail con l'API v3 di SendGrid (POST /v3/mail/send), con una chiave propria dell'ETL e fetch nativo, senza SDK. Codice in src/connectors/sendgrid/, subpath @noeva/etl-core/connectors/sendgrid; mailAction e mailSink stanno in @noeva/etl-core/connectors/mail. La mail è la stessa del connettore SMTP: una pipeline cambia trasporto cambiando nodo e istanza, non il template.

FunzioneTipoCosa fa
sendgridActionAction<I, O>Invia una mail per record e passa avanti il record con l'esito
sendgridSinkSink<T>Invia una mail per record come ultimo passo; isolabile per record

Configurazione ​

loadSendgridConfig(prefix?, env?) legge le variabili con prefisso (default SENDGRID_); loadSendgridRegistry(env?) le risolve per istanza: con SENDGRID_INSTANCES=notifiche ogni account ha le sue SENDGRID_<ID>_*, senza lista valgono le variabili base come istanza default. La configurazione si valida all'avvio, e il ConfigError elenca tutte le variabili mancanti, mai i valori.

VariabileDefaultNote
SENDGRID_API_KEY—obbligatoria: una chiave ristretta al solo Mail Send
SENDGRID_FROM—obbligatorio: x@dominio o Nome <x@dominio>, una Sender Identity
SENDGRID_SANDBOXfalsetrue = sandbox_mode: SendGrid valida e risponde 200, non spedisce
SENDGRID_BASE_URLhttps://api.sendgrid.comper un account EU https://api.eu.sendgrid.com; solo https
  • Sandbox per le prove: in locale e in sviluppo SENDGRID_SANDBOX=true controlla mittente, destinatari e corpo senza mandare nulla. L'esito ha sandbox: true e nessun messageId.
  • La chiave non esce: viaggia solo nell'header Authorization. Il descriptor ne dice lo stato e gli errori la tolgono dai messaggi di SendGrid.
  • Il mittente non è nella mail: è SENDGRID_FROM dell'istanza, e un'identità diversa è un'altra istanza.

La mail ​

È la MailInUscita condivisa con SMTP (src/connectors/shared/mail.ts): a, cc, ccn, oggetto, testo e/o html, allegati, rispondiA, chiave. Si valida allo stesso modo prima di partire (vedi smtp.md § La mail): un problema è un ValidationError del record e non parte nessuna richiesta.

Verso l'API diventa una sola personalization con tutti i destinatari, il testo prima dell'HTML, gli allegati in base64 con disposition: attachment. Della chiave parte solo l'impronta, nel custom arg noeva_chiave: si ritrova nell'Activity e negli eventi di SendGrid e rende riconoscibile un reinvio.

Cambiare trasporto da configurazione ​

Per una pipeline che deve poter usare SMTP o SendGrid c'è il nodo neutro di src/connectors/mail/: riceve un TrasportoMail ({ tipo: "smtp", config } o { tipo: "sendgrid", config }) e diventa smtpAction/sendgridAction (o i sink), con il loro descriptor e i loro errori. Il merge riceve l'esito comune EsitoInvioMail (messageId?, accettati). Il grafo resta lo stesso, e il trasporto è un valore della configurazione.

ts
const trasporto: TrasportoMail =
  config.trasportoMail === "sendgrid"
    ? { tipo: "sendgrid", config: loadSendgridRegistry(env).get("notifiche") }
    : { tipo: "smtp", config: loadSmtpRegistry(env).get("notifiche") };

.action("esito", "mail-esito", mailAction<Esito, Esito>(trasporto, {
  instanceId: "notifiche",
  mail: mailEsito,
  merge: (r) => r,
}))

Action — sendgridAction ​

ts
.action("esito", "mail-esito", sendgridAction<Esito, Esito>(config, {
  instanceId: "notifiche",
  mail: mailEsito,
  merge: (r, invio) => ({ ...r, invio }), // EsitoInvioSendgrid: messageId?, accettati, sandbox
}), { deadLetter })

Sink — sendgridSink ​

ts
.tryCatch("esito", "prova")
.sink("prova", sendgridSink<Esito>(config, { name: "mail-esito", mail: mailEsito }))
.sink("prova", mailNonInviate, { fromPort: "catch" })

Implementa loadOne, quindi sotto un try-catch è isolabile per record. Non è retrySafe.

Esito ed errori ​

RispostaChe cosa vuol direEffetto
202presa in caricoesito, con messageId da X-Message-Id
200validata in sandboxesito con sandbox: true
altro 2xxinatteso per la modalitàesito, con un avviso sendgrid.esito-inatteso
4xx (tranne 429)richiesta rifiutataLoadError del record coi messaggi di SendGrid
429, 5xxnon presa in carico, da riprovareretry con backoff; finiti i tentativi, LoadError
rete, timeoutla richiesta potrebbe essere arrivataLoadError del record, senza retry
  • I messaggi di SendGrid arrivano nel report senza la chiave, senza gli indirizzi (sostituiti da [indirizzo]) e troncati; i campi rifiutati (personalizations.0.to.0.email) stanno nei details.
  • Il retry usa withRetry del core: RETRY_SENDGRID è quattro tentativi da 1 a 30 secondi, rispetta Retry-After e si ferma con l'annullamento della run. Si cambia con trasporto: { retry }, si spegne con { attempts: 1 }. Ogni retry è un log sendgrid.retry con lo status, senza dati della mail.
  • trasporto: { timeoutMs } cambia il timeout della richiesta (default 30 secondi); trasporto: { fetchImpl } sostituisce fetch, nei test.
  • L'annullamento della run si propaga com'è, non diventa un errore del record.

Idempotenza ​

Una mail inviata non si ritira, e un retry della run la rimanda: la difesa è quella di SMTP, l'invio va dopo il passo che fa da commit (vedi smtp.md § Idempotenza). Il retry interno dei 5xx può, raramente, duplicare una mail che SendGrid aveva preso in carico: è un rischio accettato.

Descriptor (UI del DAG) ​

Kind sendgrid per action e sink, con connection = instanceId se passato. Dettagli: mittente, base e sandbox, ognuno con la sua variabile e la sua origine, e in fondo credenziale: API key · impostata, o la variabile che manca. Mai la chiave né i destinatari. Le variabili sono dichiarate in MANIFEST_SENDGRID; per il piano ci sono istanzaSendgridPerIlPiano(id, env) e loadSendgridInstance(id, env). Per etl env check, istanzaSendgridDichiarata(id) è l'account da passare ad ambienteDellaPipeline: con lo stesso manifest della run il check vede anche SENDGRID_API_KEY, che il piano non mostra.

Test ​

Unit colocati in src/connectors/sendgrid/*.test.ts con un fetch finto (fetchSendgridFinto di @noeva/etl-core/testing): invio riuscito, corpo della richiesta, allegati, sandbox, 4xx con messaggio leggibile e senza chiave, 429 e 5xx ritentati, rete senza retry, annullamento della run (prima, durante la richiesta, durante l'attesa del retry), istanze con nome e configurazione mancante o non valida. Nessun test d'integrazione: la sandbox vuole una chiave vera.

Noeva è un marchio registrato di 4D S.R.L.