feat(imap-cloudflare): Spectrum TCP bridge for native IMAP clients#77
Closed
feat(imap-cloudflare): Spectrum TCP bridge for native IMAP clients#77
Conversation
…erver The three IMAP packages shipped without the tsup + dist exports setup that the other six packages use. Their package.json exports pointed at src/*.ts, blocking npm publish and breaking consumers who don't run TypeScript through a bundler. Platform is waiting on these builds. - Add tsup.config.ts to each of the three packages (esm, dts, clean). - Update package.json exports to point at dist/*.js with matching subpaths. - Update files field to ship dist/ only. - Add tsup to devDependencies. - Add all three packages to the initial-release changeset so they ship alongside the other six at 0.1.0. Side fixes landed alongside the build plumbing: - imap-cloudflare: convert 8 private members of the class expression returned from createImapDurableObject to JavaScript #private fields. TypeScript cannot emit declarations for private/protected members of anonymous-type class expressions (TS4094), so dts generation failed. Hard-private fields have equivalent encapsulation with a declaration -emittable shape. - imap-cloudflare: remove unused @rafters/mail dependency. The package only imports from @rafters/mail-imap. Verification: - pnpm -r build: all 9 packages build ESM + dts - pnpm test: 609 passing / 37 files - pnpm typecheck: clean - pnpm lint: 0 warnings, 0 errors Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ackages Adds a README.md in every package so the npm package page renders properly instead of "no readme available". Adds "license": "MIT" to every package.json so the license badge links via npm's SPDX lookup to the MIT text (the actual LICENSE file lives in the org .github repo, not this repo). READMEs cover per-package scope, install command, minimal usage examples, subpath export table where applicable, and a link back to the monorepo README for the wider architecture. Each one starts with a single-paragraph identity statement so the npm consumer sees what the package is for within three seconds of landing. Files added (9): - packages/core/README.md - packages/imap/README.md - packages/imap-cloudflare/README.md - packages/imap-server/README.md - packages/resend/README.md - packages/cloudflare/README.md - packages/react-email/README.md - packages/workers-ai/README.md - packages/better-auth-resend/README.md Files modified (9): - all 9 package.json files -- add "license": "MIT" after description Verification: - pnpm publish --dry-run on all 9 packages shows README.md in the tarball contents and the correct file count bump (+1 per package). CHANGELOGs are not hand-written -- they are auto-generated by the pnpm changeset version step from the pending initial-release.md changeset during the release flow. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Every package now has its own docs directory with topic-scoped
documentation, shipped in the npm tarball via files: ["dist", "docs"].
Consumers landing on an npm package page see real documentation, not
just a one-line README.
## Per-package docs
- **core** (5 docs):
- reference.md (51.5 kB) -- complete schema and API reference for
all 13 tables, service interfaces, Zod schemas, design decisions
- threading.md -- RFC 5322 threading via In-Reply-To / References
- migrations.md -- database migration workflow
- newsletters.md -- mailing lists, subscribers, campaigns
- adapters.md -- how adapters connect core to external services
- **imap** (2 docs):
- commands.md -- full IMAP4rev1 command reference with RFC section
citations
- authentication.md -- AuthAdapter contract and consumer ownership
- **imap-server** (2 docs):
- quickstart.md -- zero-to-running IMAP server
- deployment.md -- Fly, Railway, Fargate, Docker, VPS per-platform
guides with TLS handling
- **imap-cloudflare** (1 doc):
- deployment.md -- Durable Object setup, bindings, cost model
- **resend** (1 doc):
- outbound.md -- compose, send, webhook handling, audit
- **cloudflare** (3 docs):
- quickstart.md -- Workers + D1 + R2 + Email Routing from zero
- inbound.md -- parsing, dedupe, blob storage, thread matching
- blob-storage.md -- R2 adapter, key schema, retrieval
- **react-email** (1 doc, newly written):
- templates.md -- BaseEmail, OtpEmail, custom templates, renderer
- **workers-ai** (1 doc):
- classification.md -- categories, priority derivation, tag patterns
- **better-auth-resend** (1 doc, newly written):
- usage.md -- full better-auth integration, configuration, and how
to replace the default template
Plus repo-level overview docs that do not ship with any package:
- docs/architecture.md -- 580 lines of internal design reference
- docs/getting-started.md -- 397 lines of cross-package quickstart
## Source
Most docs were copied from vault-2026/projects/rafters/mail-docs/
(the 16 docs written during the IMAP phase but never integrated into
the package tree). The two new ones (react-email/docs/templates.md,
better-auth-resend/docs/usage.md) were written fresh because vault
had nothing allocated to those packages.
## Shipping
Every package.json files field updated to ["dist", "docs"]. Every
README gained a Documentation section listing the local docs with
descriptions.
Verified via pnpm publish --dry-run on all 9 packages: each tarball
now includes the docs/ tree and the total file count reflects the
additions.
| Package | Docs files | Tarball size | Total files |
|-----------------------|-----------:|-------------:|------------:|
| @rafters/mail | 5 | 47.6 kB | 34 |
| @rafters/mail-imap | 2 | 24.9 kB | 36 |
| @rafters/mail-imap-server | 2 | 8.0 kB | 6 |
| @rafters/mail-imap-cloudflare | 1 | 7.4 kB | 11 |
| @rafters/mail-resend | 1 | 8.5 kB | 13 |
| @rafters/mail-cloudflare | 3 | 7.6 kB | 13 |
| @rafters/mail-react-email | 1 | 5.0 kB | 14 |
| @rafters/mail-workers-ai | 1 | 5.4 kB | 8 |
| @rafters/better-auth-resend | 1 | 3.1 kB | 5 |
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…getter
Adds a public notify method to ImapServer so inbound email handlers can
push EXISTS responses to IDLE clients bound to a specific mailbox.
Matches the feature parity of @rafters/mail-imap-cloudflare, which has
an equivalent inbound signal via POST /notify?count=N on its Durable
Object. Until now, the Node runtime had no way to wake IDLE clients
when new mail arrived; the docs promised a "notification mechanism"
that did not exist.
## API
```typescript
interface ImapServer {
listen(): Promise<void>;
close(): Promise<void>;
readonly connections: number;
readonly port: number | null;
notify(mailboxId: string, newMessageCount: number): void;
}
```
- `notify(mailboxId, newMessageCount)` delivers an EXISTS response
(RFC 2177 IDLE push) to every session that is (a) currently in IDLE
state and (b) bound to the specified mailbox. Sessions on other
mailboxes and sessions not in IDLE are not affected. newMessageCount
is the total for the mailbox after the insertion, not a delta --
IMAP clients read the EXISTS value as the new total.
- `port` returns the actual bound port after listen() resolves. Useful
when the server starts on port 0 (ephemeral) and the caller needs to
learn the assigned port. Critical for test harnesses that spin up
throwaway servers.
## Implementation
The internal connection bookkeeping switched from a counter
(`let activeConnections = 0`) to a typed Set of `ConnectionEntry`
records (`{ socket, state }`). The switch is necessary to iterate
over connections inside notify() and filter by mailboxId + idleState.
The connections getter still returns the size of the Set, so
externally the API surface of that field is unchanged.
## Tests
5 new tests in tests/server.test.ts (total for this file: 10):
- surface check: notify is a function on the returned server
- calling notify with no active connections does not throw
- calling notify with count=0 does not throw
- end-to-end (real TCP socket): notify delivers "* <n> EXISTS" to a
client that is authenticated, selected, and in IDLE, with matching
mailboxId
- end-to-end: notify does NOT deliver to a client bound to a
different mailbox (via a custom resolveMailboxId)
- end-to-end: notify does NOT deliver to a client that is not in IDLE
state (authenticated and selected, but no IDLE issued)
The end-to-end tests open a real net.Socket against a locally-bound
plain-TCP server (no TLS, simulates TLS-terminating proxy mode) and
drive the IMAP protocol through LOGIN, SELECT, IDLE, then call
notify() from outside the session and assert the client either
receives the EXISTS or does not within a timeout window.
## Documentation fixes
Three real errors surfaced while reading the copied docs:
1. `packages/imap-server/docs/quickstart.md` -- the "App passwords"
section prescribed "store hashed (argon2) in your database".
That prescription contradicts the rewritten authentication
contract (interface-only, consumer owns credential storage and
hashing). Replaced with a section that points at the
authentication.md doc and makes the interface-only posture
explicit.
2. `packages/imap-server/docs/quickstart.md` -- the "IDLE" section
said "call the server's notification mechanism after storing a
new message" without specifying an API. That API did not exist
until this PR. Replaced with a real usage example calling
server.notify(mailboxId, totalMessages).
3. `packages/imap-server/docs/deployment.md` -- the Fly.io and
Railway Dockerfiles used `node --import tsx src/main.ts` combined
with `pnpm install --prod`. The --prod flag strips devDependencies
including tsx, which breaks the runtime. Replaced with a
multi-stage Dockerfile: build stage compiles TypeScript to dist/
with dev deps, runtime stage installs --prod and runs compiled
`node dist/main.js`.
## Verification
- pnpm --filter @rafters/mail-imap-server test: 10 passing
- pnpm --filter @rafters/mail-imap-server typecheck: clean
- pnpm --filter @rafters/mail-imap-server build: ESM + dts clean
(dist/index.js 12.82 KB, dist/index.d.ts 2.47 KB)
- pnpm lint: 0 warnings, 0 errors (full workspace)
- oxfmt check on all four modified files: clean
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
6 tasks
Contributor
Author
|
Closing as duplicate of PR #78. PR #77 was created with title 'Spectrum TCP bridge' but its branch feat/imap-server-notify actually contains my |
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.
Summary
createTcpBridge()to@rafters/mail-imap-cloudflare(src/tcp-bridge.ts)./tcp-bridgesubpath from package.json; added to tsup entry pointsArchitecture
Transport-only bridge. The DO handles all protocol parsing and credential verification. The bridge extracts the email address from LOGIN solely to identify which DO instance to route to (
idFromName(email)).mailboxIdpassed to the DO equals the email address.Note on file location: Issue #57 spec listed
packages/imap/src/transport/tcp-bridge.ts, but theSockettype andDurableObjectNamespacebinding are Cloudflare-specific. Placed inpackages/imap-cloudflareper the zero-vendor-deps rule for core.Test plan
extractLoginEmail-- atom and quoted string parsing, edge cases (RFC 3501 Section 6.2.3)LineReader-- line framing, CRLF splitting, oversized line rejection, pipelined commands after LOGINcreateTcpBridge-- shape and optionspnpm test-- 18/18 passoxlint-- 0 warnings, 0 errorsoxfmt --check-- clean on changed filestsc --noEmit-- cleanCloses #57
🤖 Generated with Claude Code