Upyo 0.5.0: Structured errors, automatic retries, and OAuth 2.0 #29
dahlia
announced in
Announcements
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
-
Upyo is a cross-runtime email library for JavaScript that provides a unified API for sending email across Node.js, Deno, Bun, and edge functions. It supports SMTP, JMAP, and providers such as Mailgun, SendGrid, Amazon SES, Resend, and Plunk, all through the same
Transportinterface.Upyo 0.5.0 makes delivery failures structured and machine-readable across every transport, adds a retry transport that absorbs transient failures automatically, welcomes a new Lettermint transport, and brings OAuth 2.0 authentication to the SMTP transport.
While we were at it, upyo.org also got a redesign: a longer, scrollable landing page that walks through cross-runtime support, provider independence, and packaging, wrapped in a warmer paper-and-postal-red theme built around the logo.
Structured receipt errors
Until now, a failed
Receiptgave you a human-readableerrorMessagesarray and not much else. If your application wanted to decide whether a failure was worth retrying, or needed the original HTTP status code, it had to parse provider-specific strings by hand.Every transport now attaches structured metadata to failed receipts, so that information is just there:
A structured
ReceiptErrorcan carry a portablecategory(auth,rate-limit,network,timeout,validation,rejected,server-error,service-unavailable,configuration, orunknown), whether the failure isretryable, an HTTP status code, aretryAfterMillisecondsdelay parsed from a provider'sRetry-Afterheader, the attempt count, a timestamp, and rawproviderDetailswhen a transport exposes them. This ships across the board: SMTP, JMAP, Mailgun, SendGrid, Amazon SES, Resend, Plunk, and the new Lettermint transport all populate it, and the pool transport aggregates it from every child provider it tries.To make that aggregation type-safe,
Receipt,ReceiptError, andTransportare now generic over a provider id, and every transport exposes a stableid("smtp","mailgun","pool", and so on). Wrapping or combining transports, like pool or retry, preserves the original provider id on each underlying error instead of collapsing everything into one generic failure.If you write a custom transport,
@upyo/corenow exports the same building blocks the built-in transports use:createFailedReceipt(),createReceiptError(),classifyReceiptError(),classifyHttpStatus(), andparseRetryAfter(). Reach for them instead of hand-rolling error classification, and your transport's failures will look and behave like every other Upyo transport's. See the custom transport guide for the full pattern.None of this touches existing code.
errorMessagesis still there and still works exactly as before;errorsis additive.Retry transport
The new @upyo/retry package adds
RetryTransport, a decorator that wraps any Upyo transport and retries transient failures for you, using the structured error metadata above to decide what's worth retrying.By default it retries rate limits, timeouts, and server errors with exponential backoff and full jitter, honors a provider's
Retry-Afterdelay when one is given, and gives up cleanly, returning a final failed receipt rather than throwing.AbortSignalcancellation is never retried and still rejects immediately, so it remains a hard stop for callers.sendMany()retries each message independently while throttling how many are in flight at once, and still yields receipts in the original input order.Retry transport doesn't replace pool transport; it composes with it. Wrap a single provider for straightforward per-provider retries, wrap a pool for retries of the whole failover chain, or nest it inside a pool so each provider gets its own retry budget before the pool moves to the next one. The retry transport guide walks through backoff tuning,
shouldRetryoverrides, and composition with pool transport in more detail.Lettermint transport
The new @upyo/lettermint package adds support for Lettermint, a transactional email provider with a straightforward HTTP API.
The transport covers the full range of Upyo message features, including HTML and text content, CC/BCC, custom headers, attachments, and inline images, plus Lettermint-specific extras such as routes, tags, metadata, and tracking settings.
sendMany()uses Lettermint's batch endpoint and automatically splits large inputs into chunks of 500 messages, and messages with their ownidempotencyKeyare sent individually so their keys are preserved. See the Lettermint transport guide for the complete feature set.Installation
OAuth 2.0 authentication for SMTP
Gmail, Outlook, and a growing number of providers no longer accept plain passwords over SMTP. The SMTP transport now supports OAuth 2.0 through the SASL XOAUTH2 and OAUTHBEARER (RFC 7628) mechanisms, so it can authenticate the way those providers expect.
A static token is only the starting point. Pass a callback as
accessTokento fetch a fresh token from a client such as google-auth-library or msal-node each time a new connection authenticates, or hand the transport yourclientId,refreshToken, andtokenEndpointand let it run therefresh_tokengrant itself, caching the access token across pooled connections until shortly before it expires. Existing{ user, pass }configurations are untouched; OAuth 2.0 is simply a new member of theSmtpAuthunion.Alongside this, connection and authentication setup failures, such as an invalid host, a refused connection, or rejected credentials, are now reported as a failed
Receiptinstead of rejecting the returned promise, matching how message-level failures like a rejected recipient already worked. Only cancellation viaAbortSignalstill rejects. See the SMTP transport guide for full configuration details.What's next
We continue expanding Upyo's transport coverage and resilience features while keeping the library simple, type-safe, and portable across runtimes. Your feedback and contributions help shape where it goes next.
For the complete changelog and technical details, see CHANGES.md.
For questions or issues, please visit our GitHub repository.
Beta Was this translation helpful? Give feedback.
All reactions