Skip to content

Compared with alternatives ​

@alexify/mailer is an email library in the place of the libraries below, built towards a reliability layer none of them has. The table compares fourteen solutions in five directions: SMTP clients (Nodemailer, emailjs), unified libraries over many providers (Upyo, unemail, sently), SMTP for edge runtimes (worker-mailer, edge-mailer), a MIME builder (MIMEText) and the official SDKs of the providers (Resend, SendGrid, Mailgun, Postmark, Mailtrap, Amazon SES). It follows the idea of Upyo's own comparison, with every feature in one table rather than a few convenient libraries in a few convenient rows. @alexify/mailer itself is the first column, in teal and, on a wide screen, pinned beside the features as the table scrolls, so every row reads against it.

What the table shows ​

  • None of the fourteen has a persistent queue, a local audit log and one model of delivery events. unemail plans a queue. This is the gap @alexify/mailer is built for; see Why @alexify/mailer?.
  • Breadth and correctness pull against each other. sently covers SMS, push, WhatsApp and fourteen HTTP providers, but reading its code points to a bug in the canonicalization of the DKIM body and in SendGrid batches. Nodemailer and unemail are narrower and more precise.
  • Edge runtimes and SMTP barely mix. It works only through cloudflare:sockets (sently, worker-mailer, edge-mailer), and port 25 is blocked in Workers. On the edge, an HTTP provider is the realistic option.
  • Idempotency is in 2 of the 14 (Resend and unemail). SES v2 has no ClientToken, so a retry after a timeout can send a message twice.

The table ​

Feature@alexify/mailer
unified + reliability
Nodemailer 10
SMTP/MIME
emailjs 5
SMTP
Upyo
unified
unemail
unified
sently
unified + multi-channel
worker-mailer
edge SMTP
edge-mailer
edge SMTP
MIMEText
MIME builder
Resend SDK
SDK
SendGrid SDK
SDK
Mailgun.js
SDK
Postmark.js
SDK
Mailtrap SDK
SDK
AWS SES v3
SDK, client-sesv2
Runtimes (from imports and CI, not run)
Node.js, minimum version✅ ≥22, CI on 22 and 24✅ ≥20⚠️ ≥18, CI 20+✅ ≥20✅ ≥20.11✅ ≥18❌ Workers only⚠️ needs a connector of your own✅ no engines field✅ ≥20✅ ≥12✅ ≥18✅ ≥18✅ ≥16.20✅ ≥20
Deno✅ CI: unit and runtime suites1❌ not declared❌ not declared✅ CI: deno test⚠️ HTTP drivers, not SMTP✅ Deno.connect❌✅ edge-mailer/deno⚠️ browser-entry⚠️ fetch, not tested⚠️ through Node compatibility⚠️ not declared⚠️ fetch, not declared⚠️ not declared⚠️ through npm:
Bun✅ CI: unit and runtime suites✅ CI: test:bun❌ not declared✅ CI: bun test✅ CI: bun✅ CI: bun test❌❌ no connector⚠️ browser-entry⚠️ fetch⚠️ through Node compatibility⚠️ not declared⚠️ not declared⚠️ not declared⚠️ through Node compatibility
Cloudflare Workers / edge❌ not a target yet⚠️ only with nodejs_compat❌⚠️ HTTP packages yes, SMTP no⚠️ HTTP + 3 Cloudflare drivers, no SMTP✅ HTTP + SMTP (cloudflare:sockets)✅ the only target✅ no node:*⚠️ browser-entry⚠️ fetch, not tested⚠️ fs, axios; not declared⚠️ fetch mode⚠️ declared in 5.0.0❌ http/https agents⚠️ explicit credentials needed
Browser❌ by design❌❌⚠️ no target, no tests✅ HTTP drivers only⚠️ throws❌❌✅ browser-bundle⚠️ secret key in the client❌✅ needs a CORS proxy❌❌✅ explicit credentials
Transports and delivery reliability
SMTP client✅ PIPELINING, SIZE, DSN✅ SMTP + LMTP✅ no LMTP✅ PIPELINING, SIZE, DSN✅ own implementation✅ own implementation✅✅—❌❌❌❌❌❌
Sendmail / local binary✅ ./sendmail, exit codes classified✅❌❌❌❌❌❌—❌❌❌❌❌❌
JMAP✅ RFC 8620/8621❌❌✅ RFC 8620/8621❌❌❌❌—❌❌❌❌❌❌
HTTP providers, count181 (SES, needs the SDK)0818 + http + 2 CF14 + CF binding00—111111
Failover between providers✅ RR, weighted, priority, circuit breaker2⚠️ host DNS fallback only❌✅ RR, weighted, priority✅ fallback, RR, weighted, tee✅ fallback + weighted❌❌—❌❌❌❌❌⚠️ multi-region endpoint on the AWS side
SMTP connection pool✅ on by default, 5, 100 messages each✅ max 5, 100 msg❌ one connection✅ on by default✅ off by default✅ off by default❌✅———————
Retry / backoff✅ jitter, Retry-After, no blind resend2⚠️ requeue in the pool only⚠️ 1 retry on 450/451✅ jitter, Retry-After✅ 5 modes, jitter✅ no jitter, retries SMTP 5xx❌❌ retryHint only—❌❌❌❌❌✅ standard / adaptive
Rate limiting✅ token bucket per key, FIFO, shared store✅ rateLimit in the pool❌⚠️ sendMany only✅ token bucket⚠️ pool + sendBulk❌❌—❌⚠️ contact importer only❌⚠️ 429 → error❌⚠️ adaptive retry
Circuit breaker✅ one probe in half-open❌❌❌✅ half-open without a single probe⚠️ cooldownMs❌❌—❌❌❌❌❌❌
Timeout / AbortSignal✅ signal + timeout over the body⚠️ timeouts, no AbortSignal⚠️ timeout, no signal✅ signal + timeout⚠️ signal for HTTP, not SMTP⚠️ SMTP connect only⚠️ socket timeouts✅ signal on connect and send—⚠️ signal, no timeout⚠️ global signal only⚠️ timeout, no signal⚠️ 180 s timeout⚠️ hard 10 s✅ abortSignal + requestTimeout
Persistent queue (survives a restart)✅ MongoDB outbox (transactional), BullMQ driver, memory queue3❌❌❌🔜 planned❌❌⚠️ sample outbox only—❌❌❌❌❌❌
Messages
Attachments✅ path, URL, data URI, stream, bytes, Blob4✅ path, URL, Buffer, stream, data-URI⚠️ path, stream, no URL⚠️ bytes, Blob, File, factory⚠️ bytes, string, URL⚠️ bytes + path⚠️ base64 only⚠️ string, bytes, Blob⚠️ base64 only⚠️ base64 or URL⚠️ base64, helper for Buffer✅ Buffer, Blob, stream, 50 MB✅ base64✅ string / Buffer✅ Uint8Array or Raw MIME
Streaming large attachments⚠️ SMTP, sendmail; HTTP providers buffer⚠️ SMTP yes; DKIM and SES buffer✅⚠️ SMTP only❌❌❌❌❌❌❌⚠️ accepts a stream❌❌❌
Inline images (CID)✅ multipart/related✅ multipart/related⚠️ manual Content-ID⚠️ mixed, not related; Mailgun bug⚠️ mixed, not related⚠️ HTTP: 3 providers only❌✅✅✅✅✅✅✅✅
Sending raw MIME✅ SMTP, sendmail, JMAP, SES, Mailgun, SparkPost, Cloudflare✅❌⚠️ SMTP/JMAP only⚠️ smtp driver only❌❌❌— builds MIME only❌❌✅ /messages.mime❌❌✅ Content.Raw
iCalendar✅ every METHOD✅ icalEvent + METHOD⚠️ manual✅ every METHOD🔜 planned❌❌✅⚠️ as an attachment❌❌⚠️ as an attachment⚠️ as an attachment⚠️ as an attachment⚠️ through Raw MIME
Unicode / IDN✅ punycode + SMTPUTF8✅ punycode + SMTPUTF8⚠️ Q-encoding, no punycode⚠️ SMTPUTF8, no automatic punycode⚠️ RFC 2047/2231⚠️ one RFC 2047 word⚠️ RFC 2047⚠️ SMTPUTF8, no punycode⚠️ Subject as one B-word⚠️ UTF-8 JSON⚠️ UTF-8 JSON❌❌⚠️ UTF-8 JSON⚠️ Charset, no IDN
List-Unsubscribe✅ RFC 8058 one-click, DKIM-signed✅ list.unsubscribe❌⚠️ manual header✅ RFC 8058 oneClick❌⚠️ headers⚠️ headers❌⚠️ headers, topics⚠️ headers, asm⚠️ headers⚠️ headers⚠️ headers✅ ListManagementOptions
Built-in templates✅ 9 engines passed in: EJS, Handlebars, Pug, MJML, Liquid, Nunjucks, Eta, Mustache, React Email5❌❌❌⚠️ Renderer interface✅ + plugins❌❌❌✅ Templates API✅ dynamic templates✅ Templates API✅ Templates API✅ Templates API✅ Templates API
React Email✅ render passed in❌❌❌✅✅❌❌❌✅❌❌❌❌❌
Batch / bulk✅ native: Resend, Postmark, Mailjet, Mailtrap, JMAP; concurrency elsewhere❌❌✅ native: Resend, Mailtrap, JMAP✅ sendBatch⚠️ sendBulk, SendGrid bug⚠️ queue per session✅—✅ up to 100, no attachments✅ personalizations⚠️ recipient-variables✅ batch + bulk API✅ batchSend, 500✅ SendBulkEmail
Scheduled sending✅ queue delay; native in 8 providers6❌❌❌⚠️ 8 drivers❌❌❌—✅✅✅ up to 3 days❌⚠️ campaigns only❌
Tags / metadata✅ metadata in 17 providers, tags in 13⚠️ headers; tags on SES only⚠️ headers⚠️ tags in 7 providers✅⚠️ headers only⚠️ headers⚠️ headers⚠️ headers✅✅✅✅✅✅
Open/click tracking✅ per message in 10 providers❌❌⚠️ config only✅ 10 drivers❌❌❌—⚠️ per domain✅✅✅⚠️ per domain✅ per-message override
Idempotency key✅ decorator with reserved keys, a MongoDB store + 5 providers❌❌⚠️ Resend, Lettermint, Maileroo✅ middleware + providers⚠️ decorator; batch bypasses it❌❌—✅❌❌❌❌❌
Security and authentication
DKIM signing in the library✅ RSA + Ed25519, streaming, over-signing✅ RSA-SHA256❌✅ RSA + Ed25519, streaming✅ RSA + Ed25519, SMTP only⚠️ body canonicalization bug❌✅ RSA❌⚠️ keys through the API⚠️ whitelabel, generic⚠️ keys through the API⚠️ keys through the API⚠️ domain DNS records⚠️ Easy / BYO DKIM
OAuth2 for SMTP✅ refresh, OAUTHBEARER✅ refresh + service account⚠️ bearer token only✅ refresh, OAUTHBEARER⚠️ callback, no refresh✅ refresh client❌⚠️ no refresh———————
ARC❌❌❌❌❌❌❌❌❌——————
DSN✅✅❌✅❌❌✅✅———————
Proxy✅ SMTP: HTTP CONNECT or any connect; HTTP: fetch⚠️ HTTP CONNECT, SOCKS through a module❌❌⚠️ custom fetch❌❌❌—⚠️ baseUrl only⚠️ axios proxy✅⚠️ custom fetch + undici⚠️ environment only⚠️ through requestHandler
verify() / connection check✅ SMTP, sendmail, JMAP, 13 HTTP providers7✅⚠️ VRFY, not a check⚠️ SMTP/JMAP only⚠️ isAvailable(), nominal✅❌⚠️ probe() without auth———⚠️ domain DNS only⚠️ getServer()⚠️ getAllAccounts()⚠️ GetAccount
SMTP AUTH mechanismsPLAIN, LOGIN, XOAUTH2, OAUTHBEARERPLAIN, LOGIN, CRAM-MD5, XOAUTH2CRAM-MD5, LOGIN, PLAIN, XOAUTH2PLAIN, LOGIN, XOAUTH2, OAUTHBEARERPLAIN, LOGIN, CRAM-MD5, XOAUTH2PLAIN, LOGIN, CRAM-MD5, XOAUTH2PLAIN, LOGIN, CRAM-MD5PLAIN, LOGIN, CRAM-MD5, XOAUTH2———————
SMTP TLS policies✅ requireTls, SNI, CA, cert, minVersion✅ REQUIRETLS, SNI, no DANE⚠️ STARTTLS accepts any certificate✅ requireTls, CA, cert, minVersion✅ requireTLS, off by default✅ requireTLS by default with auth⚠️ falls back to plaintext + AUTH✅ tlsPolicy ×4, REQUIRETLS———————
Platform features (events, domains, provider API)
Webhooks: parsing and signature verification✅ 15 providers, verified before parsing8❌❌❌🔜 planned✅ 10 parsers, HMAC in 5❌❌—✅ standardwebhooks✅ ECDSA⚠️ CRUD, no verification⚠️ CRUD + types, no verification✅ HMAC verify❌ SNS signature not in the SDK
Inbound mail❌ out of scope❌❌❌🔜 planned❌❌❌—✅⚠️ parser with a bug⚠️ routes✅✅❌ legacy client-ses only
Suppression / bounce API🔜 per tenant, before the queue9❌❌❌🔜 planned❌❌❌—✅⚠️ generic client✅✅✅✅
Domain management (API)❌ out of scope❌❌❌❌❌❌❌—✅⚠️ generic client✅✅✅✅ identities
Address validation⚠️ RFC 5322/6531 syntax⚠️ parser only⚠️ parser only⚠️ RFC 5322/6531 syntax⚠️ regex⚠️ regex❌❌ syntax assert❌❌❌✅ /v4/address/validate❌❌✅ GetEmailAddressInsights
Sending log on the provider side—————————✅ logs API⚠️ generic /v3/messages✅ logs API⚠️ events, opens, clicks⚠️ emailLogs⚠️ event destinations + archiving
Provider API coverage—————————18 namespaces, ~117 callsonly mail/send typed; ~251 endpoints in USAGE.md21 clients, ~155 calls85 methods~126 calls116 commands
Multi-channel: SMS, WhatsApp, push❌ out of scope❌❌❌❌✅ SMS, WhatsApp, webpush, FCM❌❌—❌❌❌❌❌❌
Observability and extensibility
OpenTelemetry✅ decorator: spans + metrics, and those of the queue's worker❌❌✅ decorator: spans + metrics⚠️ withLogger⚠️ hooks + consoleObserver❌❌❌❌❌❌❌❌⚠️ middlewareStack
Hooks / middleware✅ ./hooks, decorators, a pino-like logger✅ use(compile|stream)⚠️ logger function⚠️ decorators, LogTape✅ Middleware pipeline✅ hooks + plugins⚠️ logger✅ onEvent⚠️ dump()⚠️ console.error⚠️ setClient❌❌❌✅ logger + middlewareStack
Mock / test mode✅ ./mock✅ jsonTransport, streamTransport, Ethereal⚠️ Message#read()✅ @upyo/mock✅ mock + Mailpit⚠️ preview, Mailpit❌❌—❌✅ sandboxMode✅ testmode⚠️ Sandbox server✅ Sandbox API⚠️ TestRenderEmailTemplate
Local audit log / event journal⚠️ state model + sinks, MongoDB among them; crypto-shredding next10❌❌❌❌❌❌⚠️ events, not stored—❌❌❌❌❌❌
Quality, license, maturity
TypeScript types✅ hand-written, tsd✅ built in✅✅✅✅✅✅✅✅✅ hand-written .d.ts✅✅✅✅ generated
Runtime dependencies0000–1 (@noble/hashes)0000422+ (axios)3 (axios)02 (axios, qs)9
Module formatCJS, ESM importESM + CJSESM onlyESM + CJS + JSRESM onlyESM onlyESM + CJSESM + CJS + JSRESM + CJSESM + CJSCJS onlyESM + CJSCJS onlyCJS onlyESM + CJS
LicenseMITMIT-0MITMITMITMITMITMITMITMITMITMITMITMITApache-2.0
Version (2026-10-07)1.2.010.0.165.0.30.7.0 (unreleased; release 0.6.0)0.7.01.2.21.2.10.8.53.0.24 (tag 3.0.28)6.32.18.1.614.0.25.1.04.10.0 (HEAD ahead)3.1147.0
Last commit2026-10 (this release)2026-10-072026-08-262026-09-092026-09-022026-08-192025-11-152026-09-082026-01-282026-10-062025-09-192026-10-072026-07-102026-10-072026-10-06
Tests: files / cases183 / ~269061 / ~133015 / ~153109 / ~130040 / ~77366 / ~5324 / ~18914 / ~2586 / ~3039 / ~44143 / ~45049 files23 (16 integration)55 files2 smoke (the rest in the monorepo)
Owner / maintenance risk1 author, Alexis Technologies1 maintainerno SECURITY.md1 main author1 author1 author, 0.1 → 1.2 in ~80 days1 author1 author, 0.xno SECURITY.mdvendorvendor, old toolchainvendorvendorvendorvendor

✅ in the code · ⚠️ partial, or only on the provider side · ❌ none · 🔜 planned by the author, no code yet · — does not apply. Every cell was decided by reading the source of the project as of 2026-10-07, not its README; the column of @alexify/mailer is this repository as of 2026-10-18.

  1. Through npm:@alexify/mailer. Deno’s fetch sends a request again when a reused connection closes before its answer, so the docs give it a fetch of its own (known issues).
  2. A failure after which the provider may have taken the message is marked deliveryUnknown: ./retry does not retry it and ./pool does not fail over with it.
  3. Permanent failures fail at once, Retry-After is a minimum, and a delivery of unknown outcome is never sent blindly again; in MongoDB, a job is written in the transaction of its cause. Jobs are signed (HMAC) and may be sealed, their message dropped once done with; tenants take turns, and a job that kills its worker is given up.
  4. Nothing is read implicitly: fromPath and fromUrl are called by the application, fromUrl checks every redirect against allowUrl (no private address by default) and bounds time and bytes.
  5. Names, never paths; locales, layouts, front-matter subjects, CSS inlined without remote fetches, text derived.
  6. A transport that cannot schedule refuses the message before sending it, rather than sending it at once.
  7. The other five have no request that checks a sending key without sending a message.
  8. A signature where the provider signs (SNS, Svix, ECDSA, HMAC), its Basic credentials or token where it does not; a window of time, and an id kept as taken only once the handler commits its events.
  9. On the roadmap of the reliability layer: designed, with no code yet.
  10. Each state of a message, from the queue to the webhooks, one event per change, written once by its id. The keyed HMAC and encrypted recipients are on the roadmap.

How the table was made ​

  • From the code, not the README. Every cell was decided by reading the source of a local clone of each project as of 2026-10-07.
  • The column of @alexify/mailer is this repository, as of 2026-10-18. ✅ and ⚠️ are code that is here: the transport ported from upyo, with the upstream bugs fixed since, and what 1.1 took from nodemailer, unemail and sently, with their bugs fixed in the port. 🔜 is the reliability layer of the plan, designed but with no code yet. Its gaps are shown as such: no edge runtime, streaming only over SMTP and sendmail, and scheduling only where the provider schedules.
  • Defects were not reproduced, except in emailjs, which was run against fake SMTP servers. A cell that names a bug is a finding from reading the code; reproduce it before relying on it.
  • The runtimes of the others were not run. Their Deno, Bun and Workers support is inferred from imports, exports maps and CI configurations. The suites of @alexify/mailer run on Bun and Deno in its own CI.
  • The clones are shallow (one commit), so the history, the number of maintainers and the pace of releases are not taken from git. Test cases were counted with a regular expression.
  • Amazon SES means the newest SES client of the AWS SDK for JavaScript v3, @aws-sdk/client-sesv2 3.1147.0 (API 2019-09-27, 116 commands). The older client-ses (API 2010-12-01, 71 commands) is still supported and is needed only for inbound rules. Its retry internals, the defaults of NodeHttpHandler and AbortSignal live in @smithy packages and were judged from their configuration types.
  • Size limits of messages and attachments are set by the providers' documentation, not by the code of their SDKs, and are not in the table.

Sources ​

Released under the MIT License.