v1.1: deliverability, compliance, ergonomics#94
Merged
Conversation
- `msg.unsubscribe` auto-injects List-Unsubscribe + List-Unsubscribe-Post headers (Gmail/Yahoo Feb 2024 compliance). - `unemail/compliance` ships sign/verify HMAC tokens + framework-agnostic one-click handler. - `unemail/suppression` ships SuppressionStore interface + memory and unstorage-backed stores. - `withSuppression` middleware blocks / drops suppressed recipients before driver.send. Closes #57, #59. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…Result helpers - Queue (memory + unstorage): honour msg.scheduledAt when deciding visibility; deferred jobs stay in the queue until their time comes. - withRetry: new backoff strategies — exponential-jitter, full-jitter, decorrelated-jitter (AWS-style); new deadLetter option routes the exhausted message to a fallback driver with ctx.meta.deadLetterReason set. - New unemail/result module — isOk/isErr/unwrap/unwrapOr/mapOk/mapErr/ tryAsync helpers for the Result<T> union. Closes #61, #83, #88. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- unemail/ics — zero-dep RFC 5545 VEVENT builder (REQUEST/PUBLISH/CANCEL/REPLY). Ships organizers, attendees with roles + partstat + rsvp, VALARMs, proper line folding and text escaping. Returns an Attachment the driver can forward natively. - email.sendBatchStream(messages) — async iterable that yields one Result per message without short-circuiting on first error. Lets 10k-recipient fan-outs run with constant memory. Closes #64, #89. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tchers - EmailDriver gains optional cancel(id) and retrieve(id) hooks gated by flags.cancelable / flags.retrievable. email.cancel(id) and email.retrieve(id) return UNSUPPORTED for drivers without them. - Resend driver implements both, maps last_event → SendStatusState across scheduled/sent/delivered/bounced/opened/clicked/cancelled. - unemail/test: new matchers toHaveSentTo, toHaveSentWithSubject, toHaveSentWithAttachment, toHaveSentMatching; plus toEmailSnapshot() helper that scrubs volatile headers for snapshot-testing. Closes #65, #74. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- EmailMessage gains 3 new first-class fields:
- tracking: { opens, clicks, unsubscribes } — per-message overrides
- sandbox: true — test-send without delivery
- metadata: Record<string, string> — echoed back by webhooks
- SendGrid driver maps them to tracking_settings, mail_settings.sandbox_mode,
personalization custom_args.
- Mailgun maps to o:tracking-opens/-clicks, o:testmode, v:key.
- Postmark maps to TrackOpens, TrackLinks (HtmlAndText|None), Metadata.
- Resend X-Metadata-* headers.
Closes #66.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New first-class EmailMessage.template: { id, alias?, variables?, locale? }
mapped to each provider's native template API:
- SendGrid → template_id + personalization.dynamic_template_data
- Mailgun → template + h:X-Mailgun-Variables (JSON)
- Postmark → /email/withTemplate + TemplateId/TemplateAlias + TemplateModel
(batch uses /email/batchWithTemplates when any message has a template)
- Brevo → templateId + params
- MailerSend → template_id + personalization[].data
- Loops → transactionalId + dataVariables (+ tags still merge in)
- Zeptomail → template_key / template_alias + merge_info
- Resend → unchanged (uses react/jsx pipeline)
Closes #60.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- New _smtp/dkim.ts — zero-dep DKIM signer, relaxed/relaxed canonicalization. Supports RFC 6376 (RSA-SHA256) and RFC 8463 (Ed25519-SHA256). Pure Web Crypto, so the SMTP driver can still run on edge runtimes (given an SMTP-capable transport). - SMTP driver accepts `dkim: DkimSignerOptions | (msg) => DkimSignerOptions | null`. Per-message resolver supports multi-tenant senders. - Test generates a fresh RSA keypair, signs, parses the DKIM-Signature, and verifies it against the public key. Closes #58. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- unemail/inbound/reply — stripReply(rawText) returns { text, quoted }.
Heuristics for English, Turkish, German, French, Spanish header-
bodied replies plus Outlook original-message blocks and '-- \n'
signatures.
- unemail/inbound/thread — threadKey(parsedEmail) picks the canonical
Message-ID (first References > In-Reply-To > Message-ID).
buildThreads(batch) returns a Map grouping messages by root.
Closes #68.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rifier - unemail/webhooks/standard — verifyStandardWebhook(request, options) and signStandardWebhook(secret, id, ts, body). - Accepts webhook-id / webhook-timestamp / webhook-signature headers. - Supports space-separated multi-version signatures (secret rotation). - whsec_ prefix auto-stripped. Tolerance window defaults to 5 minutes. - Pure Web Crypto, <5 kB gzipped vs Svix's ~1 MB. Closes #71. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The inline `import("../types.ts").EmailMessage` in the DKIM function
type leaked to the generated `.d.mts` as a literal `.ts` path, which
Are The Types Wrong flagged as 'Internal resolution error'. Switching
to a top-level `import type` lets obuild rewrite it as `../types.mjs`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced Apr 17, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
First batch of v1.1 work, built on top of the v1.0 driver base. Each commit closes 1-2 related issues so reviewers can track scope cleanly.
14 issues closed
Critical
withSuppressionmiddlewaremsg.templatepass-through across SendGrid, Mailgun, Postmark, Brevo, MailerSend, Loops, Zeptomailmsg.scheduledAt(memory + unstorage)tracking/sandbox/metadatamapped into SendGrid, Mailgun, Postmark, ResendHigh
driver.cancel(id)/driver.retrieve(id)contract + Resend impl + SendStatus typeResult<T>helpers — isOk/isErr/unwrap/unwrapOr/mapOk/mapErr/tryAsyncemail.sendBatchStream(messages)async iterator for streaming large fan-outsNumbers
Follow-ups still open in v1.1
#62 (OAuth2 refresh), #63 (personalizations), #67 (typed getInstance), #69 (DMARC parser), #70 (unified events), #72 (package-split RFC), #73 (preheader + juice + dark-mode), #75 (per-driver rate limit), #76 (multi-region), #77 (queue adapters), #78 (ARC), #79 (MTA-STS), #80 (ARF), #81 (Address primitive), #82 (dedupe), #84 (preferences), #85 (SES inbound), #86 (metrics), #87 (handlebars/liquid/i18n), #90 (PII scrubber), #91 (AMP/DSN/raw), #92 (CID rewrite).
Test plan
pnpm test— 214/214 passingpnpm typecheck— cleanpnpm fmt— clean🤖 Generated with Claude Code