Skip to content

Add @upyo/jmap package for JMAP email transport#20

Merged
dahlia merged 22 commits intomainfrom
jmap
Dec 24, 2025
Merged

Add @upyo/jmap package for JMAP email transport#20
dahlia merged 22 commits intomainfrom
jmap

Conversation

@dahlia
Copy link
Owner

@dahlia dahlia commented Dec 24, 2025

Summary

This PR adds @upyo/jmap, a new transport package that implements email sending via the JMAP protocol (RFC 8620/8621). JMAP is a modern, JSON-based protocol designed to replace IMAP and offers advantages like stateless HTTP communication and batch request support.

Closes #10.

Overview

Unlike SMTP which requires maintaining persistent connections, JMAP uses standard HTTP POST requests with JSON payloads. This makes it a natural fit for serverless and edge environments where connection pooling is impractical. The implementation follows the same patterns established by other HTTP-based transports in Upyo (Mailgun, SendGrid, SES).

A key feature of JMAP is its support for batch requests: multiple method calls can be combined into a single HTTP request, with later calls referencing results from earlier ones. The sendMany() method takes advantage of this by batching all emails into a single request, significantly reducing HTTP round-trips when sending multiple messages.

Usage

import { JmapTransport } from "@upyo/jmap";

const transport = new JmapTransport({
  sessionUrl: "https://jmap.example.com/.well-known/jmap",
  bearerToken: "your-token",
});

await transport.send({
  sender: { address: "[email protected]", name: "Sender" },
  recipients: [{ address: "[email protected]" }],
  subject: "Hello via JMAP",
  content: { text: "Hello, World!", html: "<p>Hello, World!</p>" },
});

Features

The transport supports all standard Upyo message features including text/HTML content, attachments (both inline and regular), CC/BCC recipients, reply-to addresses, custom headers, and priority settings. Session information is cached to avoid redundant round-trips, and the transport automatically discovers the mail-capable account and resolves sender identities.

Comparison with original plan

The implementation follows the plan outlined in issue #10 with a few differences:

Aspect Original plan Implementation
Authentication Bearer token only Bearer token + Basic auth
Session caching TTL-based option Per-instance caching
sendMany() Phase 3 optimization Fully implemented with batch processing
Delayed send Phase 4 (optional) Deferred to future release
Undo support Phase 4 (optional) Deferred to future release

The delayed send and undo features were intentionally deferred as they require additional protocol support that not all JMAP servers implement consistently.

Testing

The package includes comprehensive unit tests covering configuration, session management, message conversion, HTTP client behavior, and batch processing. E2E tests run against Stalwart Mail Server in Docker, which is automatically started in CI via docker-compose.

Documentation

  • Package README with installation and usage instructions
  • VitePress documentation page at docs/transports/jmap.md
  • CHANGES.md updated with new package announcement

dahlia and others added 16 commits December 24, 2025 15:55
- Add package README with installation, usage, and configuration docs
- Add VitePress documentation page for JMAP transport
- Update root README with @upyo/jmap in packages table
- Update CHANGES.md with @upyo/jmap changelog entry
- Add @upyo/jmap to docs devDependencies for twoslash type checking
- Make jsrRef plugin errors silent for unpublished packages

Co-Authored-By: Claude <[email protected]>
- Remove unnecessary async keywords from test fetch mocks
- Replace async with Promise.resolve() for lint compliance
- Fix unused variable by prefixing with underscore
- Format code with deno fmt

Co-Authored-By: Claude <[email protected]>
- Add E2E tests with Stalwart Mail Server for real JMAP testing
- Add docker-compose.yml with fixed admin password for consistent testing
- Add scripts/setup-stalwart.sh for automated user/domain setup
- Add basicAuth and baseUrl config options for Docker environments
- Add test-utils/test-config.ts for E2E test configuration
- Run E2E tests sequentially to avoid race conditions

Co-Authored-By: Claude <[email protected]>
TypeScript 5.9.2 (shipped with Deno 2.6.3) no longer considers
Uint8Array directly assignable to BlobPart. This fixes the type error
by extracting the underlying ArrayBuffer with slice() before creating
the Blob.

Co-Authored-By: Claude <[email protected]>
Use docker-compose to run Stalwart Mail Server in GitHub Actions CI.
This enables E2E testing of the JMAP transport against a real JMAP server.

Co-Authored-By: Claude <[email protected]>
Instead of sending emails sequentially, sendMany() now batches multiple
messages into a single JMAP request with multiple Email/set creates and
EmailSubmission/set creates. This reduces HTTP round-trips and improves
throughput when sending multiple emails.

Co-Authored-By: Claude <[email protected]>
@dahlia dahlia linked an issue Dec 24, 2025 that may be closed by this pull request
@codecov
Copy link

codecov bot commented Dec 24, 2025

Welcome to Codecov 🎉

Once you merge this PR into your default branch, you're all set! Codecov will compare coverage reports and display results in all future pull requests.

Thanks for integrating Codecov - We've got you covered ☂️

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces the @upyo/jmap package, implementing email transport via the JMAP protocol (RFC 8620/8621). JMAP is a modern JSON-based protocol designed as an alternative to SMTP/IMAP, offering stateless HTTP communication and batch request capabilities well-suited for serverless environments.

Key Changes:

  • Complete JMAP transport implementation with session discovery, identity resolution, and batch processing
  • Comprehensive test coverage including unit tests and E2E tests against Stalwart Mail Server
  • Full documentation including package README and VitePress guide

Reviewed changes

Copilot reviewed 33 out of 35 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
packages/jmap/src/*.ts Core transport implementation with config, HTTP client, session management, message conversion, and blob upload
packages/jmap/src/*.test.ts Comprehensive unit and E2E test suites
packages/jmap/README.md Package documentation with installation and usage examples
docs/transports/jmap.md VitePress documentation page with detailed configuration guide
packages/jmap/docker-compose.yml Stalwart Mail Server setup for E2E testing
.github/workflows/main.yaml CI integration for automated testing
CHANGES.md Changelog entry for the new package
pnpm-lock.yaml Dependency updates including jmap-rfc-types
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

dahlia and others added 5 commits December 24, 2025 23:53
This allows timeout: 0 (instant timeout) and retries: 0 (no retries)
to be valid configurations instead of being replaced with defaults.

#20 (comment)

Co-Authored-By: Claude <[email protected]>
Attachments are fully implemented, not planned for future release.

#20 (comment)

Co-Authored-By: Claude <[email protected]>
Now that config uses nullish coalescing, 0 is a valid value.

#20 (comment)

Co-Authored-By: Claude <[email protected]>
Prefer AbortSignal.any() when available, with fallback to manual
signal mirroring. Also preserve the abort reason when propagating.

#20 (comment)

Co-Authored-By: Claude <[email protected]>
Track processing stage (session fetch, mailbox discovery, identity
resolution, attachment upload, message conversion, batch request
execution) and provide detailed error messages that indicate where
the failure occurred. For attachment upload failures, also report
how many messages had their attachments uploaded before the failure.

#20 (comment)

Co-Authored-By: Claude <[email protected]>
@dahlia dahlia merged commit dc78ee7 into main Dec 24, 2025
28 of 32 checks passed
@dahlia dahlia deleted the jmap branch December 24, 2025 15:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support JMAP

2 participants