Upyo changelog
Version 0.6.0
To be released.
@upyo/core
- Added
Bloband replayable async content factories for attachments, withreadAttachmentContent()anditerateAttachmentContent()helpers.createMessage()now retainsFilecontent without reading it; use the helpers instead of awaitingattachment.contentdirectly. [#56, #59] - Added a
calendarfield toMessageandcreateMessage(), which carries an iCalendar object so that a transport can compose thetext/calendarpart that makes a message a meeting invitation, a reply, or a cancellation. The method is read from the object's ownMETHODproperty; passingmethodasserts what that property says rather than supplying it, and content that is not a single well-formedVCALENDARobject declaring exactly one supported method is rejected with aTypeError. Every content line is checked for valid names, parameters, and value characters; property-specific value syntax and scheduling requirements remain the caller's responsibility. Line endings are normalized to CRLF. [#63, #69, #70, #71] - Added the
@upyo/core/calendarmodule, with theCalendarMethod,CalendarContent, andCalendarConstructortypes and theparseCalendarMethod(),resolveCalendarContent(), andcreateCalendarAttachment()functions. [#63, #69] - Added
messageId,date,inReplyTo, andreferencesfields toMessageandcreateMessage(), so that an application can choose the outgoing message identifier, store it, and correlate a reply that arrives with a matchingIn-Reply-To. The identifiers are held without their enclosing angle brackets, whichcreateMessage()strips when they are supplied, and an invalid one is rejected with aTypeError. ForinReplyToandreferences, leaving the field unset defers to anIn-Reply-ToorReferencesheader supplied throughheaders, while an empty array suppresses it. [#58, #61] - Added the
@upyo/core/message-idmodule, withgenerateMessageId(),parseMessageId(),formatMessageId(), andresolveThreadingHeaders(). [#58, #61] - Clarified that
Receipt.messageIdis the delivery handle a transport or a provider reports back, which is not the RFC 5322Message-IDthe message carries. [#58, #61] - Added support for internationalized mailbox addresses, including UTF-8 local parts and Unicode domains, to
parseAddress()andcreateMessage(). [#45, #50] - Added the optional
RawTransportinterface and replayableRawMessagetypes for delivering serialized MIME with an explicit envelope. Raw sources support incremental validation and cancellation. An explicit8bitencoding requires the caller to guarantee ASCII headers in nested MIME parts, which Upyo does not parse. [#64, #72] - Added the optional
VerifiableTransportinterface andisVerifiableTransport()type guard for checking transport configuration without sending a message. [#67, #73]
@upyo/jmap
- A message carrying
calendaris now composed with atext/calendarbody part carrying themethodparameter. [#63, #69] - Added
JmapTransport.sendRaw()to stream serialized MIME through blob upload, import, and submission with an explicit envelope. Raw mutations are not retried automatically; uncertain submission outcomes are reported as non-retryable to prevent duplicate delivery. HTTP failures retain their status, retry delay, and response details; authentication rejections are non-retryable even without a JMAP error body. [#64, #72] - Added
JmapTransport.verify()to check live session, account, drafts mailbox, and identity settings without sending mail or changing the session cache. Verification rejects on failure and bounds each discovery operation, including response bodies and retries, by the configured timeout. [#67, #73] - Fixed cancellation during JMAP retry delays so cancelled requests stop waiting immediately.
- Added support for
Bloband replayable async attachment factories, including cancellation while reading their content. Provider payloads remain buffered in memory. [#56, #59] - Added support for the
messageId,date,inReplyTo, andreferencesfields ofMessage, which map onto themessageId,sentAt,inReplyTo, andreferencesproperties of the JMAPEmailobject. A raw header of the same name is dropped when the corresponding field is set, since RFC 8621 §4.6 forbids two properties representing one header field. [#58, #61] - Fixed
Email/setcreating an email with theheadersproperty, which RFC 8621 §4.6 forbids on create; each header field is now written as an individualheader:property. Custom headers that duplicate a structured property, includingBccand anyContent-*field, are no longer sent, and a header value containing a carriage return or line feed is rejected. Breaking:JmapEmailCreateno longer has aheadersproperty. [#58, #61] - Fixed an empty
textorhtmlbody being dropped from the composed message. A body the caller supplied as an empty string is now sent as an empty part, the way @upyo/smtp composes it, rather than omitted. [#69]
@upyo/lettermint
- A message carrying
calendarnow sends the iCalendar object as an invite.ics attachment whose content type keeps themethodparameter, ahead of the message's own attachments. This transport cannot compose atext/calendarbody alternative, so the scheduling semantics are not guaranteed to survive; see the documentation on calendar invitations. [#63, #69] - Added support for
Bloband replayable async attachment factories, including cancellation while reading their content. Provider payloads remain buffered in memory. [#56, #59] - Added support for the
inReplyToandreferencesfields ofMessage, which are sent as custom headers. A field left unset defers to anIn-Reply-ToorReferencesheader supplied throughheaders, and an empty array suppresses it.messageIdanddateare not sent, because the provider does not document whether a supplied value survives. [#58, #61]
@upyo/logtape
Added LogTape observability transport. [#33]
- Added
LogTapeTransportclass for logging email delivery lifecycle events with configurable categories and levels. - Supports log-only development use and decorating another transport without changing its receipts or errors.
- Supports optional full-message recording as structured properties or as development-friendly logs with inline subjects and bodies.
- Supports streaming
sendMany(),AbortSignalcancellation, and wrapped transport disposal.
- Added
@upyo/maileroo
A message carrying
calendarnow sends the iCalendar object as an invite.ics attachment whose content type keeps themethodparameter, ahead of the message's own attachments. This transport cannot compose atext/calendarbody alternative, so the scheduling semantics are not guaranteed to survive; see the documentation on calendar invitations. [#63, #69]Added Maileroo transport. [#30, #31]
- Added
MailerooTransportclass. - Added
MailerooConfiginterface. - Added
ResolvedMailerooConfigtype. - Added
MailerooApiErrorclass. - Added
MailerooTimeoutErrorclass. - Supports single sends, sequential
sendMany(), attachments, inline images, custom headers, tags, tracking settings, retries, structured failure receipts, andAbortSignalcancellation.
- Added
Added support for
Bloband replayable async attachment factories, including cancellation while reading their content. Provider payloads remain buffered in memory. [#56, #59]Added support for the
inReplyToandreferencesfields ofMessage, which are sent as custom headers. A field left unset defers to anIn-Reply-ToorReferencesheader supplied throughheaders, and an empty array suppresses it.messageIdanddateare not sent, because the provider does not document whether a supplied value survives. [#58, #61]
@upyo/mailgun
- A message carrying
calendarnow sends the iCalendar object as an invite.ics attachment whose content type keeps themethodparameter, ahead of the message's own attachments. This transport cannot compose atext/calendarbody alternative, so the scheduling semantics are not guaranteed to survive; see the documentation on calendar invitations. [#63, #69] - Added support for
Bloband replayable async attachment factories, including cancellation while reading their content. Provider payloads remain buffered in memory. [#56, #59] - Added support for the
inReplyToandreferencesfields ofMessage, which are sent as custom headers. A field left unset defers to anIn-Reply-ToorReferencesheader supplied throughheaders, and an empty array suppresses it.messageIdanddateare not sent, because the provider does not document whether a supplied value survives. [#58, #61]
@upyo/mailtrap
A message carrying
calendarnow sends the iCalendar object as an invite.ics attachment whose content type keeps themethodparameter, ahead of the message's own attachments. This transport cannot compose atext/calendarbody alternative, so the scheduling semantics are not guaranteed to survive; see the documentation on calendar invitations. [#63, #69]Added Mailtrap transport. [#32 by Narek Hovhannisyan]
- Added
MailtrapTransportclass. - Added
MailtrapConfiginterface. - Added
ResolvedMailtrapConfigtype. - Added
MailtrapApiErrorclass. - Added
MailtrapTimeoutErrorclass. - Supports Email API and Email Sandbox sending, batch sends, attachments, categories, custom variables, config-level metadata, structured failure receipts, and
AbortSignalcancellation.
- Added
Added support for
Bloband replayable async attachment factories, including cancellation while reading their content. Provider payloads remain buffered in memory. [#56, #59]Added support for the
inReplyToandreferencesfields ofMessage, which are sent as custom headers. A field left unset defers to anIn-Reply-ToorReferencesheader supplied throughheaders, and an empty array suppresses it.messageIdanddateare not sent, because the provider does not document whether a supplied value survives. [#58, #61]
@upyo/opentelemetry
- Fixed the
email.content.typespan attribute and thecontent_typemetric label, which reportedtextorhtmlfor a message carrying a calendar. Such a message is multipart on every transport, either as atext/calendaralternative or as an invite.ics attachment, and is now labelledmultipart. [#69] - Fixed the estimated message size reported on a span and in the
email.message.sizehistogram, which counted UTF-16 code units rather than the bytes it claimed. A subject or body outside Basic Latin was undercounted, a Korean one by roughly two thirds. [#69] - The estimated message size reported on a span now includes the calendar payload. [#63, #69]
@upyo/plunk
- A message carrying
calendarnow sends the iCalendar object as an invite.ics attachment whose content type keeps themethodparameter, ahead of the message's own attachments. This transport cannot compose atext/calendarbody alternative, so the scheduling semantics are not guaranteed to survive; see the documentation on calendar invitations. [#63, #69] - A calendar message that would exceed the limit of five attachments is now rejected with a
RangeError, rather than silently dropping one of the message's own files to make room for the invitation. [#63, #69] - Added support for
Bloband replayable async attachment factories. Attachment reads remain buffered, and failed reads still omit the attachment; caller cancellation now aborts the send instead. [#56, #59] - Added support for the
inReplyToandreferencesfields ofMessage, which are sent as custom headers. A field left unset defers to anIn-Reply-ToorReferencesheader supplied throughheaders, and an empty array suppresses it.messageIdanddateare not sent, because the provider does not document whether a supplied value survives. [#58, #61]
@upyo/resend
- A message carrying
calendarnow sends the iCalendar object as an invite.ics attachment whose content type keeps themethodparameter, ahead of the message's own attachments. This transport cannot compose atext/calendarbody alternative, so the scheduling semantics are not guaranteed to survive; see the documentation on calendar invitations. [#63, #69] - A calendar message is now sent individually rather than through the batch API, which accepts no attachments. [#63, #69]
- Added support for
Bloband replayable async attachment factories, including cancellation while reading their content. Provider payloads remain buffered in memory. [#56, #59] - Added support for the
inReplyToandreferencesfields ofMessage, which are sent as custom headers. A field left unset defers to anIn-Reply-ToorReferencesheader supplied throughheaders, and an empty array suppresses it.messageIdanddateare not sent, because the provider does not document whether a supplied value survives. [#58, #61] - Fixed incorrect Base64 padding that corrupted attachment contents. [#59]
@upyo/sendgrid
- A message carrying
calendarnow sends the iCalendar object as an invite.ics attachment whose content type keeps themethodparameter, ahead of the message's own attachments. This transport cannot compose atext/calendarbody alternative, so the scheduling semantics are not guaranteed to survive; see the documentation on calendar invitations. [#63, #69] - Added support for
Bloband replayable async attachment factories, including cancellation while reading their content. Provider payloads remain buffered in memory. [#56, #59] - Added support for the
inReplyToandreferencesfields ofMessage, which are sent as custom headers. A field left unset defers to anIn-Reply-ToorReferencesheader supplied throughheaders, and an empty array suppresses it.messageIdanddateare not sent, because the provider does not document whether a supplied value survives. [#58, #61]
@upyo/ses
- A message carrying
calendarnow sends the iCalendar object as an invite.ics attachment whose content type keeps themethodparameter, ahead of the message's own attachments. This transport cannot compose atext/calendarbody alternative, so the scheduling semantics are not guaranteed to survive; see the documentation on calendar invitations. [#63, #69] - An attachment with no content ID no longer sends an empty
ContentIdfield. [#63, #69] - Added support for
Bloband replayable async attachment factories, including cancellation while reading their content. Provider payloads remain buffered in memory. [#56, #59]
@upyo/smtp
- A message carrying
calendaris now composed with atext/calendaralternative, placed after the text and HTML bodies and Base64 encoded so that the object's line structure survives unchanged. Themethodparameter repeats the object's ownMETHODproperty, as RFC 6047 §2.4 requires. [#63, #69] - Added SMTP envelope overrides for using different
MAIL FROMandRCPT TOaddresses without changing the visible message headers. Overrides support null reverse-paths and per-message resolvers for bulk VERP delivery. [#52, #54] - Added
SmtpTransport.sendRaw()for delivering serialized MIME with an explicit envelope, streaming validation, DSN, and cancellation. Raw sends preserve existing headers and signatures and bypass configured DKIM signing. [#64, #72] - Added
SmtpTransport.verify()to check a fresh connection, TLS policy, and configured authentication without sending mail. Verification shares the connection limit, closes its connection afterward, and rejects on failure.SmtpResponseErrorandSmtpAuthResponseErrorare now exported for inspecting server replies. [#67, #73] - Fixed cancellation during SMTP connection setup and while waiting for a custom OAuth2 token provider, so cancelled operations release their connections promptly.
- Added automatic SMTPUTF8 delivery for internationalized sender, recipient, and reply-to addresses. Servers must advertise
SMTPUTF8and8BITMIME; unsupported sends fail without starting a mail transaction. [#45, #50] - Added incremental SMTP attachment encoding and backpressured DATA writes. Set
dkim.bodyModeto"streaming"for bounded attachment memory with two source reads, or keep the default"buffered"mode for one read. Changed replay content fails withsmtp.attachment-replay-mismatchbefore acceptance. Source inactivity and size-limit failures close unfinished DATA connections. [#56, #59] - Added structured enhanced status codes to SMTP delivery failures. Valid RFC 2034 reply prefixes expose their class, subject, and detail while preserving the server's original text; address, content, and network statuses also receive more specific error categories. [#46, #51]
- Added support for the
messageId,date,inReplyTo, andreferencesfields ofMessage. Each takes precedence over the matchingMessage-ID,Date,In-Reply-To, orReferencesheader, which still applies when the field is left unset. [#58, #61] - Changed the generated
Message-IDto use the sender's domain instead of the fixedupyo.local, which RFC 6762 reserves for multicast DNS. [#58, #61] - Fixed the
Dateheader ending in the obsoleteGMTzone rather than the numeric+0000RFC 5322 §3.3 asks for. [#58, #61] - Fixed SMTPUTF8 not being negotiated for a non-ASCII value in a header field written verbatim, such as a
Message-IDsupplied as a custom header. [#58, #61] - Changed
In-Reply-ToandReferencessupplied as custom headers to be written verbatim rather than RFC 2047 encoded, since both carry structured values that the encoding made invalid. A non-ASCII value in one therefore now requires a server that advertises SMTPUTF8, and a value containing a carriage return or line feed is rejected instead of being neutralized by the encoding. [#58, #61] - Added the
requireTlsconfiguration option. When enabled for a plaintext connection, the transport issuesSTARTTLSeven if the server does not advertise it and fails delivery unless the TLS upgrade succeeds. The option defaults tofalse; the existing protection against cleartext authentication to non-loopback hosts remains in effect. STARTTLS on Deno requires Deno 2.7.13 or later. - Fixed connection pooling when
poolis omitted so that the documented default oftruetakes effect. Dispose the transport withawait usingor callcloseAllConnections()to release pooled connections when finished. - Reduced SMTP round trips by sending
MAIL FROMand allRCPT TOcommands together when the server advertises thePIPELININGextension. Replies, including multiline replies, remain associated with their commands and rejected recipients in server response order. [#42, #47] - Added support for the SMTP
SIZEextension. The transport declares each encoded message's size onMAIL FROMand returns a failed receipt before uploading messages that exceed an advertised fixed maximum. BareSIZEandSIZE 0advertisements continue without a local limit. [#43, #48] - Added SMTP delivery status notification requests through the RFC 3461
DSNextension. Per-messageRETandENVIDoptions and per-recipientNOTIFYandORCPToptions are validated and serialized on the SMTP envelope. Requests fail beforeMAIL FROMwhen the server does not advertiseDSN. [#44, #49] - Fixed DKIM signatures using
ed25519-sha256orsimpleheader canonicalization failing verification by receiving mail servers. - Fixed SMTP connections to infer the
securedefault from the port. Port 465 uses implicit TLS; all other ports start with plaintext and upgrade with STARTTLS when advertised. Setsecure: trueexplicitly to use implicit TLS on a nonstandard port. [#53, #55]
Version 0.5.6
Released on September 9, 2026.
@upyo/smtp
Fixed
closeAllConnections()and async disposal leaving connections open when sends were still in progress. Shutdown now waits for previously started sends, including those waiting for a connection, and closes their connections instead of returning them to the pool. New sends wait until shutdown completes, after which the transport can be reused. Finish or return any startedsendMany()iteration before awaiting shutdown. [#66]Added regression coverage for the
sendMany()connection cleanup fix introduced in 0.5.5. Breaking out of the iteration releases the connection without sending the remaining messages, with pooling enabled or disabled and with synchronous or asynchronous message sources. [#65]
Version 0.5.5
Released on September 8, 2026.
@upyo/smtp
SmtpConfig.poolSizeis now enforced as a hard limit on how many connections one transport may have open at the same time. Concurrentsend()andsendMany()calls used to open a connection each, so a transport configured withpoolSize: 5could open a hundred connections for a hundred concurrent sends and trip a provider's simultaneous-connection limit. A call that arrives once the limit is reached now waits for a connection to be handed back, and cancelling it through itsAbortSignalrejects without sending the message. [#62]The limit counts connections that are being established, connections that are currently sending, and idle connections retained for reuse, and it applies whether or not
SmtpConfig.poolis enabled. Note that asendMany()call holds its connection for the whole iteration, so running more concurrentsendMany()calls thanpoolSizemakes the extra ones wait.Fixed overlapping returns retaining more idle connections than
SmtpConfig.poolSize. Returning a connection checked the pool size before awaitingRSET, so several returns could pass the check together and then all be retained. [#62]Abandoning a
sendMany()iteration early, such as withbreak, now returns its connection instead of leaving it open until the transport is disposed.new SmtpTransport()now throws aRangeErrorwhenpoolSizeis neither a positive integer norInfinity, rather than accepting a value that no connection could satisfy. PassInfinityto opt out of the limit and keep the unbounded behaviour of earlier versions.
Version 0.5.4
Released on September 7, 2026.
@upyo/core
createMessage()now rejects a carriage return or line feed in an address, an attachment's content type, or an attachment's content ID with aTypeError. A transport that composes the message itself writes these values into header fields as given, so either character ended the field and let the rest of the value appear as further header fields. [#60]Address strings were already rejected by
parseAddress(), but the object form passed through unchecked, socreateMessage({ to: { address: "[email protected]\r\nBcc: …" } })forged aBccfield. Attachments were unchecked in either form, and an uploaded file's declared content type is routinely chosen by whoever uploaded it.Values that arrive through a
Messageobject built by hand rather than throughcreateMessage()are still passed on as given.
@upyo/smtp
Stopped emitting duplicate header fields when a custom header collides with one the transport composes itself. RFC 5322 §3.6 permits at most one
Date,From,Message-ID,Subject, and similar field per message, but every custom header used to be appended after the composed ones. A duplicateContent-Typewas the worst case: it preceded the real one, so receivers that take the first occurrence misread the body and ignored the MIME boundaries. [#57]DateandMessage-IDhave no counterpart onMessage, so a custom header now replaces the generated default instead of adding a second field. Applications can finally choose an outgoing message identifier for reply correlation. Header names are matched case-insensitively, and the values are written verbatim rather than RFC 2047 encoded. A value containing a carriage return or line feed is rejected with aTypeError, so it cannot inject additional header fields. Messages that supply neither header keep the previous generated values.Custom
From,To,Cc,Bcc,Reply-To,Subject,MIME-Version,Content-Type, andContent-Transfer-Encodingheaders are now ignored, because the correspondingMessagefields and the MIME structure are authoritative. Set the structured fields instead. A customBccheader used to disclose blind recipients to everyone who received the message.Custom
X-PriorityandX-MSMail-Priorityheaders are still sent for messages of normal priority, but no longer duplicate the headers derived from ahighorlowpriority.
Version 0.5.3
Released on August 27, 2026.
@upyo/smtp
Accepted
251responses toRCPT TO, allowing delivery to continue when an SMTP server accepts responsibility for forwarding a recipient. [#37]Continued SMTP delivery when at least one envelope recipient is accepted. Successful
SmtpReceiptvalues now list rejected recipients with their reply codes, response text, and retryability so callers can detect partial delivery and retry only temporarily rejected recipients. [#38]Kept multibyte UTF-8 characters within a single RFC 2047 encoded word when splitting long non-ASCII header values. [#39]
Folded long address, subject, custom, and attachment header fields to keep their physical lines within the RFC 5322 hard limit of 998 characters. Long attachment filenames now use RFC 2231 continuations, while a custom header token that cannot be folded produces a failed receipt instead of an invalid message. [#40]
Retried the SMTP greeting with
HELOwhen a legacy server rejectsEHLOas an unrecognized or unimplemented command. [#41]
Version 0.5.2
Released on August 27, 2026.
@upyo/smtp
- Prevented SMTP authentication from sending passwords or OAuth 2.0 access tokens over cleartext connections to non-loopback hosts. Configurations using
secure: falsenow require a successful STARTTLS upgrade before authenticating; otherwise delivery returns a failedReceiptwithout transmitting credentials. Cleartext authentication to loopback hosts remains available for local development. [#36]
Version 0.5.1
Released on July 16, 2026.
@upyo/plunk
- Updated the default Plunk API base URL to
https://next-api.useplunk.comand adapted request and response handling to the new API contract. [#35]
Version 0.5.0
Released on July 1, 2026.
@upyo/core
Added structured delivery error metadata to failed
Receiptvalues. Failed receipts still includeerrorMessagesfor existing code, and may now also includeerrors,retryable,provider,attempts, andtimestampfields for programmatic error handling. Structured errors may also include provider-specificproviderDetailswhen a transport exposes them. [#25, #27]Receipt,ReceiptError, andTransportare now generic over the transport provider id, andTransportimplementations expose a stableidfield soprovidermetadata can be type-checked.- Added the
ReceiptErrorinterface. - Added the
ReceiptErrorCategorytype. - Added the
ReceiptErrorClassificationinterface. - Added the
CreateReceiptErrorOptionsinterface. - Added the
CreateFailedReceiptOptionsinterface. - Added the
classifyHttpStatus()function. - Added the
classifyReceiptError()function. - Added the
createReceiptError()function. - Added the
createFailedReceipt()function. - Added the
parseRetryAfter()function.
- Added the
@upyo/jmap
- Updated
JmapTransportto return structured failure receipts with provider ids, retryability, HTTP status codes, retry delay metadata fromRetry-After, and attempt counts where available. [#25, #27]
@upyo/lettermint
Added Lettermint transport. [#22, #23]
- Added
LettermintTransportclass. - Added
LettermintConfiginterface. - Added
ResolvedLettermintConfigtype. - Added
LettermintSettingsinterface. - Added
LettermintApiErrorclass. - Added
LettermintBatchResponsetype. - Added
LettermintErrorinterface. - Added
LettermintResponseinterface. - Added
LettermintStatustype. - Supports single sends, batch sends, idempotency keys, attachments, inline images, routes, tags, metadata, and tracking settings.
- Added
@upyo/mailgun
- Updated
MailgunTransportto return structured failure receipts with provider ids, retryability, HTTP status codes, retry delay metadata fromRetry-After, and attempt counts where available. [#25, #27]
@upyo/plunk
- Updated
PlunkTransportto return structured failure receipts with provider ids, retryability, HTTP status codes, retry delay metadata fromRetry-After, and attempt counts where available. [#25, #27]
@upyo/resend
- Updated
ResendTransportto return structured failure receipts with provider ids, retryability, HTTP status codes, retry delay metadata fromRetry-After, and attempt counts where available. Batch item failures now include structured error metadata as well. [#25, #27]
@upyo/sendgrid
- Updated
SendGridTransportto return structured failure receipts with provider ids, retryability, HTTP status codes, retry delay metadata fromRetry-After, and attempt counts where available. [#25, #27]
@upyo/ses
- Updated
SesTransportto return structured failure receipts with provider ids, retryability, HTTP status codes, retry delay metadata fromRetry-After, and attempt counts where available. [#25, #27]
@upyo/smtp
Updated
SmtpTransportto return structured failure receipts with provider ids, retryability, attempt counts, and classified network, timeout, authentication, and rejection errors. [#25, #27]Added OAuth 2.0 authentication support to the SMTP transport, using the SASL XOAUTH2 and OAUTHBEARER (RFC 7628) mechanisms. This enables authentication with providers such as Gmail and Outlook that require OAuth 2.0 instead of passwords. [#19, #24]
SmtpAuthis now a discriminated union ofSmtpUserPassAuth(username/password, as before) and the OAuth 2.0 variants. Existing{ user, pass }configurations continue to work unchanged.- Added
SmtpOAuth2Auth,SmtpOAuth2TokenAuth, andSmtpOAuth2RefreshAuthinterfaces, and theSmtpUserPassAuthinterface. - Added the
OAuth2TokenProvidertype. TheaccessTokenfield accepts either a static token string or a callback returning a fresh token, which can integrate an external OAuth client (e.g.,google-auth-libraryormsal-node) for transparent refresh. - Added the built-in
refresh_tokengrant flow (SmtpOAuth2RefreshAuth): provideclientId,refreshToken, andtokenEndpoint, and the transport exchanges them for an access token, caching it across pooled connections until shortly before it expires. - Added the
SmtpAuthErrorclass, thrown on OAuth token acquisition and SASL authentication failures.
Changed
SmtpTransport.send()andSmtpTransport.sendMany()to report connection and authentication setup failures as a failedReceiptinstead of throwing. Previously, a failure to connect or authenticate (e.g., an invalid host, a refused connection, or rejected credentials) rejected the returned promise, which was inconsistent with message-level failures (such as a rejected recipient) that were already reported as a failedReceipt. Now all delivery failures—including setup failures—are reported uniformly as a failedReceipt. Cancellation viaAbortSignalcontinues to reject. [#19, #24]
@upyo/pool
- Updated
PoolTransportto preserve child transport provider ids in aggregated structured errors. The pool itself reports failed aggregate receipts with the"pool"provider id, while child errors keep the underlying transport ids as a type-safe union. [#25, #27]
@upyo/retry
- Added retry transport.
RetryTransportwraps any existing transport and retries transient failed receipts or thrown transient errors using configurable exponential backoff, jitter,Retry-Aftermetadata, andsendMany()launch throttling. Use it when one provider should absorb temporary rate limits, server errors, and network failures before handing a failure back to application code. [#26, #28]
@upyo/opentelemetry
- Changed the built-in error category labels from underscore spelling to hyphen spelling:
rate-limit,service-unavailable, andserver-error. OpenTelemetry metrics now prefer structured receipt categories when a wrapped transport returns them, then fall back to the configured error classifier. [#25, #27]
Version 0.4.1
Released on July 16, 2026.
@upyo/plunk
- Updated the default Plunk API base URL to
https://next-api.useplunk.comand adapted request and response handling to the new API contract. [#35]
Version 0.4.0
Released on December 25, 2025. Happy Holidays!
@upyo/core
Added
idempotencyKeyproperty toMessageinterface. [#16]This allows users to provide their own idempotency key for request deduplication when retrying failed send operations. By including the key in the message itself, retries become simpler—just resend the same message object. If not provided, transports may generate their own key internally (behavior varies by transport implementation).
@upyo/smtp
Added DKIM (DomainKeys Identified Mail) signing support. [#18]
Outgoing emails can now be signed with DKIM for improved deliverability and authentication. The implementation uses the standard Web Crypto API for cross-runtime compatibility (Node.js, Deno, Bun, edge functions).
- Added
DkimConfiginterface for configuring DKIM signing. - Added
DkimSignatureinterface for individual signature settings. - Added
DkimAlgorithmtype supportingrsa-sha256(RFC 6376) anded25519-sha256(RFC 8463). - Added
DkimCanonicalizationtype for header/body canonicalization. - Added
DkimSigningFailureActiontype (throworsend-unsigned). - Supports multiple DKIM signatures per message.
- Configurable failure handling: throw error or send unsigned.
- Accepts both PEM strings and
CryptoKeyobjects for private keys.
- Added
@upyo/jmap
Added JMAP transport for sending emails via JMAP protocol. [#10]
JMAP (JSON Meta Application Protocol) is a modern, efficient protocol for email access and submission, defined in RFC 8620 (core) and RFC 8621 (mail). This transport provides:
- Automatic session discovery and caching
- Automatic identity resolution from sender email
- Bearer token authentication
- Exponential backoff retry with configurable attempts
- Request timeout and
AbortSignalsupport - Text and HTML message content (
multipart/alternative) - Priority headers (
X-Priority,Importance) - Custom headers
- File attachments via blob upload
- Inline attachments (
multipart/related)
@upyo/resend
Added support for user-provided idempotency keys via
Message.idempotencyKey. [#16]Each message can now include an
idempotencyKeyto ensure it is not sent multiple times during retries. For batch operations viasendMany(), the first message's key is used for the entire batch request. If not provided, a unique key is automatically generated for each request.
Version 0.3.4
Released on December 18, 2025.
@upyo/resend
Fixed idempotency key not being sent as an HTTP request header. [#16]
The Resend transport was incorrectly adding the
Idempotency-Keyto the email's custom headers inside the JSON payload, but the Resend API expects it as an HTTP request header. This fix moves the idempotency key to the proper location.
Version 0.3.3
Released on December 13, 2025.
@upyo/core
Fixed a potential SMTP command injection vulnerability.
Email addresses are now validated to prevent newline characters (
\ror\n), which could be used to inject malicious SMTP commands.
Version 0.3.2
Released on December 12, 2025.
@upyo/ses
Fixed headers serialization issue on edge runtimes. [#15]
The SES transport now converts the
Headersobject to a plain object before passing tofetch(), which resolves “Missing Authentication Token” errors on edge runtimes like Bunny CDN Edge that don't properly serializeHeadersobjects.
Version 0.3.1
Released on November 3, 2025.
@upyo/smtp
- Added STARTTLS support for secure connection upgrade. The SMTP transport now automatically detects and uses STARTTLS when connecting to servers that advertise this capability (such as Protonmail, Office 365, and others). This allows using port 587 with
secure: falsefor automatic encryption upgrade. [#14]
Version 0.3.0
Released on September 16, 2025.
@upyo/pool
Added pool transport for combining multiple email providers with load balancing and failover strategies. [#8]
- Added
PoolTransportclass withAsyncDisposablesupport. - Added
PoolConfiginterface. - Added
ResolvedPoolConfigtype. - Added
TransportEntryinterface. - Added
ResolvedTransportEntrytype. - Added
PoolStrategytype for built-in strategies. - Added
Strategyinterface for custom routing logic. - Added
TransportSelectioninterface. - Added
RoundRobinStrategyclass for equal distribution. - Added
WeightedStrategyclass for proportional distribution. - Added
PriorityStrategyclass for failover-based routing. - Added
SelectorBasedStrategyclass for content-based routing. - Support for round-robin, weighted, priority, and selector-based strategies.
- Automatic failover and retry logic with configurable limits.
- Comprehensive error aggregation across multiple providers.
- Added
@upyo/plunk
- Added
PlunkTransportclass. - Added
PlunkConfiginterface. - Added
ResolvedPlunkConfiginterface. - Added
PlunkErrorinterface. - Added
PlunkResponseinterface. - Support for both cloud-hosted and self-hosted Plunk instances.
- Added
@upyo/resend
- Added
ResendTransportclass. - Added
ResendConfiginterface. - Added
ResolvedResendConfiginterface. - Added
ResendApiErrorinterface. - Added
ResendBatchResponseinterface. - Added
ResendErrorinterface. - Added
ResendResponseinterface.
- Added
Version 0.2.4
Released on December 13, 2025.
@upyo/core
Fixed a potential SMTP command injection vulnerability.
Email addresses are now validated to prevent newline characters (
\ror\n), which could be used to inject malicious SMTP commands.
Version 0.2.3
Released on December 12, 2025.
@upyo/ses
Fixed headers serialization issue on edge runtimes. [#15]
The SES transport now converts the
Headersobject to a plain object before passing tofetch(), which resolves “Missing Authentication Token” errors on edge runtimes like Bunny CDN Edge that don't properly serializeHeadersobjects.
Version 0.2.2
Released on November 3, 2025.
@upyo/smtp
- Added STARTTLS support for secure connection upgrade. The SMTP transport now automatically detects and uses STARTTLS when connecting to servers that advertise this capability (such as Protonmail, Office 365, and others). This allows using port 587 with
secure: falsefor automatic encryption upgrade. [#14]
Version 0.2.1
Released on August 4, 2025.
@upyo/smtp
Fixed “Maximum call stack size exceeded” error when sending large attachments. [#6]
The SMTP transport now uses
Buffer.from().toString('base64')instead of the legacybtoa()function for base64 encoding, which resolves stack overflow issues with large attachments (e.g., 500KB+ files).Fixed UTF-8 encoding issue where email addresses were incorrectly encoded in SMTP headers. [#7]
Only display names are now encoded using RFC 2047 encoding, while email addresses remain unencoded. For example,
German ÄÖÜ <[email protected]>now correctly becomes=?UTF-8?B?...?= <[email protected]>instead of encoding the entire string including the email address.
Version 0.2.0
Released on July 17, 2025.
@upyo/core
Improved type safety by making array fields readonly.
- Changed the type of
Message.recipientsproperty fromAddress[]toreadonly Address[]. - Changed the type of
Message.ccRecipientsproperty fromAddress[]toreadonly Address[]. - Changed the type of
Message.bccRecipientsproperty fromAddress[]toreadonly Address[]. - Changed the type of
Message.replyRecipientsproperty fromAddress[]toreadonly Address[]. - Changed the type of
Message.attachmentsproperty fromAttachment[]toreadonly Attachment[]. - Changed the type of
Message.tagsproperty fromstring[]toreadonly string[].
- Changed the type of
Enhanced email address type safety with template literal types.
- Added
EmailAddresstype. - Changed
Address.addressproperty type fromstringtoEmailAddress. - Added
isEmailAddress()type guard function for runtime email validation.
- Added
@upyo/ses
Added Amazon SES transport. [#3]
- Added
SesTransportclass. - Added
SesConfiginterface. - Added
SesAuthenticationinterface.
- Added
@upyo/opentelemetry
Added OpenTelemetry observability support. [#5]
- Added
OpenTelemetryTransportclass. - Added
OpenTelemetryConfiginterface. - Added
ObservabilityConfiginterface. - Added
MetricsConfiginterface. - Added
TracingConfiginterface. - Added
AttributeExtractortype. - Added
ErrorClassifiertype. - Added
createErrorClassifier()function. - Added
defaultErrorClassifier()function. - Added
AutoConfiginterface. - Added
createOpenTelemetryTransport()function. - Added
CreateOpenTelemetryTransportConfiginterface. - Added
createEmailAttributeExtractor()function.
- Added
Version 0.1.2
Released on Nowember 3, 2025.
@upyo/smtp
- Added STARTTLS support for secure connection upgrade. The SMTP transport now automatically detects and uses STARTTLS when connecting to servers that advertise this capability (such as Protonmail, Office 365, and others). This allows using port 587 with
secure: falsefor automatic encryption upgrade. [#14]
Version 0.1.1
Released on July 14, 2025.
@upyo/smtp
- Fixed CJK character encoding corruption in SMTP transport HTML emails. Korean, Japanese, and Chinese characters are now properly encoded using UTF-8 quoted-printable encoding. [#4]
Version 0.1.0
Initial release. Released on July 13, 2025.