Aller au contenu
Krizaka TypeScript
Krizaka TypeScript

@krizaka/mail

Courriel en file : envoyé après le commit, une fois par clé, jamais à une adresse supprimée ; le fournisseur est le choix de l'admin ; rebonds et plaintes suppriment.

Installer

bashTerminal
npm install @krizaka/mail

Aussi publié en sous-chemins : @krizaka/mail/settings, @krizaka/mail/testkit.

Le README ci-dessous est celui du paquet sur npm, en anglais.

Mail for every Krizaka product: queued in the caller's transaction through @krizaka/outbox, delivered once per idempotency key, never to a suppressed address, with the provider the admin chose. No vendor SDK, no environment variable, no wall clock, no database client.

  • It takes mail already rendered. Templates are @krizaka/email's (a UI package this one never imports). emailPorts(store, clock) gives @krizaka/email its three ports (DeliveryLog, SuppressionList, IdempotencyStore) over the same tables, so a direct sendEmail and the queue share one log, one suppression list and one set of keys.
  • At least once, never lost. deliver checks the suppression list, checks the key, sends, and only then claims the key: a crash between the send and the claim sends again (a duplicate reset mail beats a lost one); a redelivery after the claim is a no-op.
  • Addresses are hashed (SHA-256 of the normal form) in the suppression list and in the delivery log. The queued message holds the address until the outbox purges it.
  • Providers are adapters over an injected fetch: mailgunProvider, resendProvider, logProvider (development, writes no body). providerFor(settings.provider, keys, fetch, write) picks the one the admin chose and refuses a provider without its key instead of falling back to log.
  • Bounces and complaints come back through the provider's signed webhook: parseMailgunWebhook and parseResendWebhook verify the signature and the age, and answer the addresses to suppress (a permanent failure or hard bounce, a complaint, an unsubscribe). A transient bounce is ignored.
  • The admin's settings (@krizaka/mail/settings): mail.provider (log, mailgun, resend), mail.from, mail.replyTo, mail.retentionDays. Keys, the Mailgun domain and the webhook signing keys stay in the environment (MAIL_PROVIDER-style names, read by the product and passed in).
import { createOutbox, runWorker } from "@krizaka/outbox";
import { createMail, dbMailStore, MAIL_TOPIC, providerFor } from "@krizaka/mail";
import { mailSettings, readMailSettings } from "@krizaka/mail/settings";

const mail = createMail({
  clock,
  outbox: createOutbox({ clock, settings: outboxSettings }),
  settings: () => readMailSettings((key) => settings.get(key)),
  store: dbMailStore(query),
  provider: (s) => providerFor(s.provider, { apiKey: env.MAIL_API_KEY, domain: env.MAIL_DOMAIN }, fetch, console.log),
});

// in the use case's transaction: the mail leaves only if the transaction commits
await mail.queue(tx, { to, subject, text, category: "transactional", template: "password-reset", locale: "fr", idempotencyKey: `reset:${resetId}` });

// in the worker process
await runWorker({ outbox, query, owner, topic: MAIL_TOPIC, deliver: (m) => mail.deliver(m).then(() => undefined), sleep });

// in the webhook route
const suppression = parseMailgunWebhook(rawBody, env.MAIL_WEBHOOK_SIGNING_KEY, nowSeconds);
if (suppression) await mail.suppress(suppression);

Tables (schema mail, mailSchema): delivery, suppression, idempotency. Event: evt.mail.requested.v1 on topic mail. mailStoreContract (@krizaka/mail/testkit) is the contract every store runs.

Apache-2.0.

Exports

Les 50 noms que le paquet exporte, lus dans ses types.

NomNatureCe que c’est
bareAddressfonctionThe bare address of Name <address> or address.
checkAddressfonction
checkMessagefonction
checkSettingsfonction
Clocktype
createMailfonctionThe mail of a product.
dbMailStorefonctionThe PostgreSQL store of « mail » over a Query, in schema mail (or the one the product renamed it to).
DELIVERY_STATUSconstante
DeliveryEntrytypeOne line of the delivery log: what, to whom (a hash, never the address), how it went.
DeliveryInputtypeA line to write: the instant is a Date.
DeliveryRecordtypeA line as read back.
DeliveryStatustype
emailPortsfonctionThe three ports of @krizaka/email (DeliveryLog, SuppressionList, IdempotencyStore) over this module's store, so a product that calls sendEmail dire…
FetchLiketypeThe part of fetch the providers use: inject the global one (fetch), or a fake in a test.
hashAddressfonctionThe full SHA-256 of an address: suppressions and the log never hold the address itself (Loi 25).
logProviderfonctionThe development provider: writes one JSON line per mail (without its body) and sends nothing.
Mailtype
MAIL_BOUNDSconstante
MAIL_CODEconstante
MAIL_DEFAULTSconstante
MAIL_EVENTconstante
MAIL_TOPICconstanteThe topic the mail is queued on, and the event that names it: the outbox routes by these.
MailCodetype
MailDepstype
MailErrorclasse
MailgunOptionstype
mailgunProviderfonctionMailgun's messages API (form-encoded, basic auth).
MailMessagetypeThe mail a product queues.
MailOutboxtypeWhat the module needs of @krizaka/outbox (its Outbox satisfies it): append in the caller's transaction.
MailProvidertypeOne provider of mail (an adapter over fetch).
mailSchemaconstanteThe tables of « mail », in their own Postgres schema: delivery (the log, with the recipient as a hash, never a body), suppression (the addresses never ma…
MailSettingstype
MailStoretypeWhere the module keeps its state.
memoryMailStorefonctionAn in-memory store, for unit tests and demos: the behaviour of the database's (the contract kit runs on both).
normalizeAddressfonction
OutgoingMailtypeA mail the provider receives: the message with the sender the settings give.
parseMailgunWebhookfonctionA Mailgun webhook (JSON body): checks the signature (HMAC-SHA256 of timestamp + token with the webhook signing key, hex) and its age, then answers the addr…
parseResendWebhookfonctionA Resend webhook (signed the Svix way): HMAC-SHA256 over id.timestamp.body with the base64 secret that follows whsec_, any of the space-separated `v1,<ba…
providerForfonctionThe provider the admin chose (mail.provider).
ProviderKeystypeThe keys a provider needs: they stay in the environment, the product reads them and passes them here.
Querytype
QueuedMailtypeThe part of an outbox message the deliverer reads (OutboxMessage satisfies it).
ResendOptionstype
resendProviderfonctionResend's emails API (JSON, bearer key, its own Idempotency-Key).
SenttypeWhat the provider answered.
SuppressiontypeWhat a provider's webhook tells us about one address.
SUPPRESSION_REASONconstanteWhy an address is never mailed again.
SuppressionReasontype
SvixHeaderstype
uuidOffonctionA stable UUID (version 5 shaped) from a seed: the same key queued twice is one outbox message.

@krizaka/mail/settings

NomNatureCe que c’est
mailSettingsconstanteThe settings of « mail », a fragment the product adds to its registry (@krizaka/mail/settings): the admin picks the provider and the sender without a redep…
readMailSettingsfonctionThe module's settings as createMail takes them, from the product's accessor ((key) => service.get(key, ctx)).

@krizaka/mail/testkit

NomNatureCe que c’est
mailStoreContractfonctionWhat every MailStore adapter must do: the same behaviour behind the same port.
T0constante

Paquet

Version sur npm0.1.2 (npm)
LicenceApache-2.0
Node.js>=22
Dépendances@krizaka/db workspace:^, @krizaka/settings workspace:^, @krizaka/time workspace:^
Dépendances à fournirvitest >=4, zod >=3.23
Code sourcemail

Journal des versions

0.1.2

Patch changes

0.1.1

Patch changes

0.1.0

Minor changes

  • New package (#58): @krizaka/mail. createMail (queue in the caller's transaction through the outbox topic mail, deliver for the worker: suppression and key checks, send, then claim, so a crash sends again rather than loses; suppress, unsuppress, purge), mailgunProvider, resendProvider, logProvider and providerFor over an injected fetch, parseMailgunWebhook and parseResendWebhook (signature, age, hard bounces and complaints), mailSchema (mail.delivery, mail.suppression as address hashes, mail.idempotency), dbMailStore, memoryMailStore, emailPorts (the three ports of @krizaka/email, structurally), @krizaka/mail/settings (mail.provider, mail.from, mail.replyTo, mail.retentionDays), evt.mail.requested.v1 and mailStoreContract.

    @krizaka/settings gains the text kind (text(group, default, { max, empty })): one trimmed line of free text, for an address or a name.

Patch changes

Sur cette page