Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 27 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@

## Design goals

| Goal | How `unemail` delivers |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **One API, many transports** | `createEmail({ driver })` — 15+ built-in drivers (SMTP, Resend, SES, Postmark, SendGrid, Mailgun, Brevo, MailerSend, Loops, Zeptomail, MailChannels, Cloudflare Email, …) |
| **Cross-runtime** | Node, Bun, Deno, Cloudflare Workers, browser — core is zero-dep and Web-API only. No `axios`, ever. |
| **Compliance-ready** | RFC 8058 one-click List-Unsubscribe, DKIM + ARC signing, suppression/preference stores, DMARC + TLS-RPT + ARF parsers |
| **Resilient by default** | Idempotency, retry w/ jitter, per-provider rate-limit, circuit breaker, dedupe, dead-letter, provider fallback |
| **Unified observability** | Structured logging, OpenTelemetry, Prometheus metrics, normalized `EmailEvent` stream across send + webhook paths |
| **Modern DX** | `{ data, error }` Result discriminated union, typed `Address` primitive, `react:`/`mjml:`/`handlebars:`/`liquid:` props |
| **Testing-first** | `createTestEmail()` with inbox + `waitFor` + 5 Vitest matchers + snapshot helper |
| Goal | How `unemail` delivers |
| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **One API, many transports** | `createEmail({ driver })` — 15+ built-in drivers (SMTP, Resend, SES, Postmark, SendGrid, Mailgun, Mailtrap, Brevo, MailerSend, Loops, Zeptomail, MailChannels, Cloudflare Email, …) |
| **Cross-runtime** | Node, Bun, Deno, Cloudflare Workers, browser — core is zero-dep and Web-API only. No `axios`, ever. |
| **Compliance-ready** | RFC 8058 one-click List-Unsubscribe, DKIM + ARC signing, suppression/preference stores, DMARC + TLS-RPT + ARF parsers |
| **Resilient by default** | Idempotency, retry w/ jitter, per-provider rate-limit, circuit breaker, dedupe, dead-letter, provider fallback |
| **Unified observability** | Structured logging, OpenTelemetry, Prometheus metrics, normalized `EmailEvent` stream across send + webhook paths |
| **Modern DX** | `{ data, error }` Result discriminated union, typed `Address` primitive, `react:`/`mjml:`/`handlebars:`/`liquid:` props |
| **Testing-first** | `createTestEmail()` with inbox + `waitFor` + 5 Vitest matchers + snapshot helper |

## Install

Expand Down Expand Up @@ -71,6 +71,24 @@ console.log(data.id) // data: EmailResult — TS narrows after the error check
Every driver implements the same contract, so swapping providers is a
one-line change.

### Mailtrap (Email API + Email Sandbox)

```ts
import mailtrap from "unemail/driver/mailtrap"

const email = createEmail({
driver: mailtrap({
apiKey: process.env.MAILTRAP_API_KEY!,
inboxId: process.env.MAILTRAP_INBOX_ID,
sandbox: process.env.MAILTRAP_USE_SANDBOX === "true",
}),
})

await email.send({ from: "a@b.com", to: "c@d.com", subject: "Test", text: "hi", sandbox: true })
```

See [docs/drivers.md](docs/drivers.md) for Email API vs sandbox routing.

## Message streams (Postmark-style)

```ts
Expand Down
30 changes: 30 additions & 0 deletions docs/drivers.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ touch it again; swapping providers is a one-line change.
| `unemail/driver/ses` | all (Web-Crypto) | ✓ | ✓ (seq) | – | – | – | ✓ | – |
| `unemail/driver/sendgrid` | all | ✓ | – | ✓ | – | ✓ | ✓ | – |
| `unemail/driver/mailgun` | all | ✓ | – | ✓ | – | – | ✓ | – |
| `unemail/driver/mailtrap` | all | ✓ | ✓ | – | – | ✓ | ✓ | – |
| `unemail/driver/brevo` | all | ✓ | – | ✓ | – | ✓ | ✓ | – |
| `unemail/driver/mailersend` | all | ✓ | ✓ | ✓ | – | – | ✓ | – |
| `unemail/driver/loops` | all | – | – | – | – | ✓ | ✓ | – |
Expand All @@ -24,6 +25,35 @@ touch it again; swapping providers is a one-line change.
| `unemail/driver/cloudflare-email` | CF Workers binding | ✓ | – | – | – | – | – | – |
| `unemail/driver/http` | all | (custom) | – | (custom) | – | – | – | – |

### Mailtrap (Email API + Email Sandbox)

The Mailtrap driver uses one API token for both environments. Email API sends
go to `send.api.mailtrap.io`; Email Sandbox (test inbox capture) uses
`sandbox.api.mailtrap.io` with your inbox ID in the path.

```ts
import { createEmail } from "unemail"
import mailtrap from "unemail/driver/mailtrap"

const email = createEmail({
driver: mailtrap({
apiKey: process.env.MAILTRAP_API_KEY!,
inboxId: process.env.MAILTRAP_INBOX_ID,
sandbox: process.env.MAILTRAP_USE_SANDBOX === "true",
}),
})

// Sandbox (captured in test inbox)
await email.send({ from: "a@b.com", to: "c@d.com", subject: "x", text: "hi", sandbox: true })

// Email API
await email.send({ from: "a@verified.com", to: "c@d.com", subject: "x", text: "hi" })
```

Set `msg.sandbox` per message to override the driver default. `sendBatch`
works in both modes; all messages in one batch must target the same environment.
Sandbox sends require `inboxId` (from `mailtrap.io/sandboxes/{id}`).

### Meta drivers

These wrap other drivers:
Expand Down
1 change: 1 addition & 0 deletions jsr.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"./driver/zeptomail": "./src/driver/zeptomail.ts",
"./driver/sendgrid": "./src/driver/sendgrid.ts",
"./driver/mailgun": "./src/driver/mailgun.ts",
"./driver/mailtrap": "./src/driver/mailtrap.ts",
"./driver/brevo": "./src/driver/brevo.ts",
"./driver/mailersend": "./src/driver/mailersend.ts",
"./driver/loops": "./src/driver/loops.ts",
Expand Down
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"mailcrab",
"mailersend",
"mailgun",
"mailtrap",
"mjml",
"postal-mime",
"postmark",
Expand Down Expand Up @@ -92,6 +93,10 @@
"types": "./dist/driver/mailgun.d.mts",
"default": "./dist/driver/mailgun.mjs"
},
"./driver/mailtrap": {
"types": "./dist/driver/mailtrap.d.mts",
"default": "./dist/driver/mailtrap.mjs"
},
"./driver/brevo": {
"types": "./dist/driver/brevo.d.mts",
"default": "./dist/driver/brevo.mjs"
Expand Down
2 changes: 1 addition & 1 deletion src/driver/_http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Result } from "../types.ts"
import { createError, toEmailError } from "../errors.ts"

/** Thin wrapper around `fetch` used by every HTTP-based driver (Resend,
* Postmark, SendGrid, Mailgun, Brevo, MailerSend, Loops, Zeptomail,
* Postmark, SendGrid, Mailgun, Mailtrap, Brevo, MailerSend, Loops, Zeptomail,
* MailChannels, HTTP). Handles JSON encoding, response parsing, and
* mapping HTTP status codes to our `EmailErrorCode` taxonomy.
*
Expand Down
Loading