feat(imap-server): notify(mailboxId, count) API for IDLE push#78
Merged
feat(imap-server): notify(mailboxId, count) API for IDLE push#78
Conversation
ssilvius
added a commit
that referenced
this pull request
Apr 11, 2026
The JSDoc above `AuthAdapter` said "App passwords are stored hashed (argon2) in D1 and verified here." That's wrong on two axes: 1. The IMAP server does not own credential storage or hashing. That was the original design but it was rewritten: AuthAdapter is an interface contract only, the consumer brings their own auth system. Prescribing argon2 contradicts the rewritten contract. 2. Core docs must be generic: "blob storage" not "R2", "database" not "D1". Referencing D1 by name inside the core protocol package violates the zero-vendor-dependency principle. Replaced with a proper JSDoc block that: - States the interface-only posture explicitly - Names the consumer's ownership (storage, hashing, generation, revocation) - Lists the security guarantees the IMAP server DOES enforce regardless of adapter implementation (generic failure response, rate limit, disconnect after max attempts) - Refers to the adapter lifecycle in neutral language This matches the authoritative `authentication.md` doc shipped in PR #76 and the interface-only posture in the `@rafters/mail-imap-server` quickstart.md fixed in PR #78. Verification: - pnpm --filter @rafters/mail-imap test: 313 passing - pnpm --filter @rafters/mail-imap typecheck: clean - pnpm --filter @rafters/mail-imap build: ESM + dts clean - pnpm lint: 0 warnings, 0 errors Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
5 tasks
2879cba to
4465792
Compare
ssilvius
added a commit
that referenced
this pull request
Apr 11, 2026
The JSDoc above `AuthAdapter` said "App passwords are stored hashed (argon2) in D1 and verified here." That's wrong on two axes: 1. The IMAP server does not own credential storage or hashing. That was the original design but it was rewritten: AuthAdapter is an interface contract only, the consumer brings their own auth system. Prescribing argon2 contradicts the rewritten contract. 2. Core docs must be generic: "blob storage" not "R2", "database" not "D1". Referencing D1 by name inside the core protocol package violates the zero-vendor-dependency principle. Replaced with a proper JSDoc block that: - States the interface-only posture explicitly - Names the consumer's ownership (storage, hashing, generation, revocation) - Lists the security guarantees the IMAP server DOES enforce regardless of adapter implementation (generic failure response, rate limit, disconnect after max attempts) - Refers to the adapter lifecycle in neutral language This matches the authoritative `authentication.md` doc shipped in PR #76 and the interface-only posture in the `@rafters/mail-imap-server` quickstart.md fixed in PR #78. Verification: - pnpm --filter @rafters/mail-imap test: 313 passing - pnpm --filter @rafters/mail-imap typecheck: clean - pnpm --filter @rafters/mail-imap build: ESM + dts clean - pnpm lint: 0 warnings, 0 errors Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
7 tasks
…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>
4465792 to
deb8676
Compare
ssilvius
added a commit
that referenced
this pull request
Apr 11, 2026
The JSDoc above `AuthAdapter` said "App passwords are stored hashed (argon2) in D1 and verified here." That's wrong on two axes: 1. The IMAP server does not own credential storage or hashing. That was the original design but it was rewritten: AuthAdapter is an interface contract only, the consumer brings their own auth system. Prescribing argon2 contradicts the rewritten contract. 2. Core docs must be generic: "blob storage" not "R2", "database" not "D1". Referencing D1 by name inside the core protocol package violates the zero-vendor-dependency principle. Replaced with a proper JSDoc block that: - States the interface-only posture explicitly - Names the consumer's ownership (storage, hashing, generation, revocation) - Lists the security guarantees the IMAP server DOES enforce regardless of adapter implementation (generic failure response, rate limit, disconnect after max attempts) - Refers to the adapter lifecycle in neutral language This matches the authoritative `authentication.md` doc shipped in PR #76 and the interface-only posture in the `@rafters/mail-imap-server` quickstart.md fixed in PR #78. Verification: - pnpm --filter @rafters/mail-imap test: 313 passing - pnpm --filter @rafters/mail-imap typecheck: clean - pnpm --filter @rafters/mail-imap build: ESM + dts clean - pnpm lint: 0 warnings, 0 errors Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ssilvius
added a commit
that referenced
this pull request
Apr 11, 2026
…ramework boundary
The repo-level docs/architecture.md had accumulated significant drift
from the current shipped state:
1. **Package count wrong.** Said "Six packages" -- it is nine now
after the IMAP packages shipped this week (@rafters/mail-imap,
@rafters/mail-imap-cloudflare, @rafters/mail-imap-server).
Updated the count, the list, and the dependency graph.
2. **IMAP section said "designed, not built, planned for post-0.1.0."**
All three IMAP packages ship in 0.1.0. Rewrote the section:
- Status is "shipped in 0.1.0 across three packages"
- Added the Node runtime alongside the DO runtime
- Expanded the command set to match what is actually shipped:
added UNSELECT (RFC 3691), COPY + MOVE (RFC 6851), APPEND
(RFC 4315 APPENDUID). The previous list was the Phase 1 subset.
- Added the advertised capability list from SERVER_CAPABILITIES
- Added a transport comparison with the Spectrum future path
(still blocked on Enterprise tier; issue #57)
- Added the inbound signaling section covering both runtimes
(DO: POST /notify, Node: server.notify() from PR #78)
3. **Invented exports in the "Extracted" list.** Replaced:
- `ResendProvider` implementation -> `createResendProvider` factory
- `MockEmailProvider` class -> `createMockEmailProvider` factory
- `ClassifyEmailWorkflow` -> removed (does not exist)
- Queue consumer `handleEmailClassifyQueue` -> removed (does not exist)
- Added real exports: createImapDurableObject, createImapWorker,
createImapServer, parseEmailHeaders, hashContent, classifier
helper functions
4. **Wrong subpath import example.** `createR2BlobStorage` ->
`createR2Storage`. Same bug as elsewhere.
5. **Inbound flow described as InboundAdapter doing the work.**
The shipped adapters provide building blocks (parseEmailHeaders,
hashContent, createR2Storage); the orchestration is consumer code.
Rewrote the flow diagram with steps 5-8 clearly labeled as
"Consumer-written" and added a callout explaining why there is
no pre-baked handleInboundEmail (schema extensions + auth context
+ pipeline topology are all application decisions).
6. **Classification flow described as framework doing the work.**
Steps 1-4 are the classifier (which ships in mail-workers-ai);
steps 5-8 (DB update, spam folder move, label application) are
consumer-written against their own schema and FolderService.
Rewrote to make the boundary explicit.
7. **Threading logic described as shipped behavior.** Threading.ts
exports generateMessageId, buildReferences, and generateSnippet
as building blocks. Thread matching is NOT shipped -- it is the
consumer's responsibility for inbound (no InboundAdapter
implementation exists yet). Outbound threading in composeEmail/
replyToThread IS shipped via InboxEmailService. Clarified the
split.
No content additions that invent features. Every claim is verified
against actual source in packages/*.
## Verification
- pnpm test: 609 passing
- pnpm typecheck: clean
- pnpm lint: 0 warnings, 0 errors
- pnpm format:check: clean
- All referenced exports, method names, and command names verified
against the actual TypeScript source
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ssilvius
added a commit
that referenced
this pull request
Apr 11, 2026
…on (#79) The JSDoc above `AuthAdapter` said "App passwords are stored hashed (argon2) in D1 and verified here." That's wrong on two axes: 1. The IMAP server does not own credential storage or hashing. That was the original design but it was rewritten: AuthAdapter is an interface contract only, the consumer brings their own auth system. Prescribing argon2 contradicts the rewritten contract. 2. Core docs must be generic: "blob storage" not "R2", "database" not "D1". Referencing D1 by name inside the core protocol package violates the zero-vendor-dependency principle. Replaced with a proper JSDoc block that: - States the interface-only posture explicitly - Names the consumer's ownership (storage, hashing, generation, revocation) - Lists the security guarantees the IMAP server DOES enforce regardless of adapter implementation (generic failure response, rate limit, disconnect after max attempts) - Refers to the adapter lifecycle in neutral language This matches the authoritative `authentication.md` doc shipped in PR #76 and the interface-only posture in the `@rafters/mail-imap-server` quickstart.md fixed in PR #78. Verification: - pnpm --filter @rafters/mail-imap test: 313 passing - pnpm --filter @rafters/mail-imap typecheck: clean - pnpm --filter @rafters/mail-imap build: ESM + dts clean - pnpm lint: 0 warnings, 0 errors Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ssilvius
added a commit
that referenced
this pull request
Apr 11, 2026
…ct-email (#81) * docs: fix interface drift across adapters.md, better-auth-resend, and react-email Second pass of accuracy fixes after finding more drift while reviewing the 967-line adapters.md and cross-referencing against actual source. Some of these errors were also present in files I introduced in PR #76 (now on main) -- fixing those here too. ## packages/core/docs/adapters.md **Resend section:** - Replaced `new ResendService(...) + new ResendProvider(resend)` with the actual factory: `createResendProvider({ apiKey, fromEmail })`. `ResendProvider` is not an exported class; `createResendProvider` is a factory function. - `CampaignParams.audienceId` -> `listId`. The field name in the actual `campaignParamsSchema` is `listId`. - Replaced `new MockEmailProvider()` with `createMockEmailProvider()`. Mock is also a factory, not a class. - Documented the real mock surface: `sentEmails` as a top-level property, `getState()` for full internal state introspection, `clear()` for test resets. **Cloudflare section:** - Removed all references to `handleInboundEmail` (does not exist) and `R2BlobStorage` class (does not exist). Replaced with the actual building blocks: `createR2Storage`, `parseEmailHeaders`, `hashContent`. - Fixed the `BlobStorage` interface to match the real shape: single `put(key, content, options?)`, `get(key, options?)` returns `BlobObject | null`, `delete(key)`, `generateKey(contentHash, extension)`. The doc had `putRaw`/`putText`/`putHtml` three-method split, which is not how the interface is shaped. - Worker entry point example rewritten to use the actual exports and demonstrate the compose-your-own pattern (read bytes, parse headers, hash, store, insert row). - Thread matching: clarified it is the consumer's responsibility, not a feature of the adapter. The adapter provides the parsing building blocks; thread matching queries are the consumer's code. - `InMemoryBlobStorage` example rewritten as a factory function returning `BlobStorage` with the correct four methods. **Workers AI section:** - Removed `ClassifyEmailWorkflow` and `handleEmailClassifyQueue` references (do not exist). The package does not ship a workflow subpath or a queue subpath. - Replaced `createEmailClassifier` with the real factory name `createWorkersAIClassifier(ai, config?)`. - Queue consumer example rewritten to show consumer-implemented queue handling using the real classifier. - Mock AI type is now `AiBinding` from the package, not `Ai`. **React Email section:** - Fixed import paths: `BaseEmail` lives at `/templates`, `OtpEmail` at `/otp`. Both were importing from the package root, which would fail at build time for consumers. - Rendering section rewritten to show the registry pattern: the renderer is name-keyed, templates are registered at construction time (or via `.register()`), and `render(name, props)` looks up by name. Removed the incorrect pass-component-directly pattern. **better-auth-resend section:** - `resendOTP` config shape was entirely wrong. Doc claimed `resendOTP(env)` with `env = { RESEND_API_KEY, FROM_EMAIL }`, plus an optional second argument for branding. Actual is a single `ResendOTPConfig` object: `{ apiKey, fromEmail, brandName, logoUrl?, websiteUrl?, expiryMinutes?, baseUrl? }`. Rewrote integration example and documented the full config shape. **Custom adapter example:** - Replaced `class PostmarkProvider implements EmailProvider` with `createPostmarkProvider(config): EmailProvider` factory closure to match the monorepo factory-pattern convention in CLAUDE.md. **Barrel exports section:** - Replaced 4 wrong imports (`ResendProvider`, `R2BlobStorage`, `handleEmailClassifyQueue`, `ClassifyEmailWorkflow`) with the actual exports via their correct subpaths. ## packages/better-auth-resend/README.md (fixing #76 drift on main) - `resendOTP` config: was `{ apiKey, from, appName }`, actual is `{ apiKey, fromEmail, brandName, ... }`. Fixed the example and the config table. Added the 4 optional fields (`logoUrl`, `websiteUrl`, `expiryMinutes`, `baseUrl`) the real schema exposes. ## packages/better-auth-resend/docs/usage.md (fixing #76 drift on main) - Same `resendOTP` config fix as the README. - "What the user sees" section updated: subject template is `"<brandName> verification code: <otp>"`, not `"Your <appName> code"`. - "Replacing the template" example now uses the actual renderer registry pattern and `provider.sendEmail` (not `.send`). ## packages/react-email/README.md (fixing #76 drift on main) - Renderer section now shows the registry pattern: `createReactEmailRenderer({ otp: OtpEmail })` then `renderer.render("otp", { code, brandName })`. The previous example called `render(OtpEmail({ otp, appName }))` which neither matches the interface nor uses the real OtpEmail prop names. - Fixed OtpEmail prop names: `otp` -> `code`, `appName` -> `brandName`, `expiresInMinutes` -> `expiryMinutes`. ## packages/react-email/docs/templates.md (fixing #76 drift on main) - Renderer section: same registry-pattern fix as the README, plus a "Why a registry instead of passing components" explanation (string-keyed templates let service code reference templates without importing React components). - OtpEmail props: same `code`/`brandName`/`expiryMinutes` fix. - "Writing your own template" example now registers the custom template with the renderer and calls `render(name, props)` instead of passing the component directly. ## Verification - pnpm test: 609 passing - pnpm typecheck: clean - pnpm lint: 0 warnings, 0 errors - pnpm format:check: clean on all modified files Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs(architecture): fix package count, IMAP status, and consumer vs framework boundary The repo-level docs/architecture.md had accumulated significant drift from the current shipped state: 1. **Package count wrong.** Said "Six packages" -- it is nine now after the IMAP packages shipped this week (@rafters/mail-imap, @rafters/mail-imap-cloudflare, @rafters/mail-imap-server). Updated the count, the list, and the dependency graph. 2. **IMAP section said "designed, not built, planned for post-0.1.0."** All three IMAP packages ship in 0.1.0. Rewrote the section: - Status is "shipped in 0.1.0 across three packages" - Added the Node runtime alongside the DO runtime - Expanded the command set to match what is actually shipped: added UNSELECT (RFC 3691), COPY + MOVE (RFC 6851), APPEND (RFC 4315 APPENDUID). The previous list was the Phase 1 subset. - Added the advertised capability list from SERVER_CAPABILITIES - Added a transport comparison with the Spectrum future path (still blocked on Enterprise tier; issue #57) - Added the inbound signaling section covering both runtimes (DO: POST /notify, Node: server.notify() from PR #78) 3. **Invented exports in the "Extracted" list.** Replaced: - `ResendProvider` implementation -> `createResendProvider` factory - `MockEmailProvider` class -> `createMockEmailProvider` factory - `ClassifyEmailWorkflow` -> removed (does not exist) - Queue consumer `handleEmailClassifyQueue` -> removed (does not exist) - Added real exports: createImapDurableObject, createImapWorker, createImapServer, parseEmailHeaders, hashContent, classifier helper functions 4. **Wrong subpath import example.** `createR2BlobStorage` -> `createR2Storage`. Same bug as elsewhere. 5. **Inbound flow described as InboundAdapter doing the work.** The shipped adapters provide building blocks (parseEmailHeaders, hashContent, createR2Storage); the orchestration is consumer code. Rewrote the flow diagram with steps 5-8 clearly labeled as "Consumer-written" and added a callout explaining why there is no pre-baked handleInboundEmail (schema extensions + auth context + pipeline topology are all application decisions). 6. **Classification flow described as framework doing the work.** Steps 1-4 are the classifier (which ships in mail-workers-ai); steps 5-8 (DB update, spam folder move, label application) are consumer-written against their own schema and FolderService. Rewrote to make the boundary explicit. 7. **Threading logic described as shipped behavior.** Threading.ts exports generateMessageId, buildReferences, and generateSnippet as building blocks. Thread matching is NOT shipped -- it is the consumer's responsibility for inbound (no InboundAdapter implementation exists yet). Outbound threading in composeEmail/ replyToThread IS shipped via InboxEmailService. Clarified the split. No content additions that invent features. Every claim is verified against actual source in packages/*. ## Verification - pnpm test: 609 passing - pnpm typecheck: clean - pnpm lint: 0 warnings, 0 errors - pnpm format:check: clean - All referenced exports, method names, and command names verified against the actual TypeScript source Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs(imap/commands): correct SEARCH criteria list to match parser The SEARCH command section listed 6 criteria that the actual parser does not implement: ANSWERED, DELETED, DRAFT, FLAGGED, SEEN, UNSEEN. Any IMAP client sending one of these receives "BAD Unknown search criterion: <name>" -- a hard failure, not a graceful fallback. Verified against packages/imap/src/protocol/parser.ts: the switch statement parseSearchCriteria has cases for ALL, NEW, FROM, TO, CC, BCC, SUBJECT, BEFORE, ON, SINCE, LARGER, SMALLER, TEXT, BODY, UID, NOT, OR plus bare sequence sets. Everything else throws via the default branch. Updated the supported list to reflect reality and added a "Not yet supported" section that: - Names the missing flag-based criteria explicitly - Names other RFC 3501 criteria that are also missing (KEYWORD, UNKEYWORD, HEADER, SENTBEFORE / SENTON / SENTSINCE) - Documents the practical workaround: SEARCH NEW matches RECENT + UNSEEN for the "new mail" UI case, and STATUS INBOX (UNSEEN) returns the unread count without needing the UNSEEN criterion. - Notes that this is a real protocol gap being tracked in an issue Updated the example to use FROM + SINCE (both supported) instead of UNSEEN + SINCE (UNSEEN is not supported). No implementation change in this PR. The parser gap is a separate issue that needs a code change to add the flag-based criteria to parseSearchCriteria and wire them to searchMessages in the message adapter. ## Verification - Verified every listed criterion against the case statements in parser.ts and the SearchCriterion type - pnpm format:check: clean - pnpm test: (unchanged) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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
Adds a public
notify(mailboxId, newMessageCount)method to@rafters/mail-imap-serverso inbound email handlers can push RFC 2177EXISTSresponses to IDLE clients bound to a specific mailbox. Matches feature parity with@rafters/mail-imap-cloudflare, which has an equivalent inbound signal viaPOST /notify?count=Non its Durable Object. Until this PR the Node runtime had no way to wake IDLE clients when new mail arrived, and the docs promised a notification mechanism that did not exist.Also fixes three real errors in the copied docs found during doc review.
API change
notify-- delivers anEXISTSresponse 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.newMessageCountis the total for the mailbox after the insertion, not a delta.port-- returns the actual bound port afterlisten()resolves. Useful when the server starts on port0(ephemeral) and the caller needs the assigned port. Critical for test harnesses.Implementation
Internal bookkeeping switched from a counter (
let activeConnections = 0) to a typedSet<ConnectionEntry>({ socket, state }). The switch is required to iterate connections insidenotify()and filter by mailboxId + idleState. Theconnectionsgetter still returns the set size so external API surface is unchanged.Tests (5 new, 10 total)
All in
packages/imap-server/tests/server.test.ts:notifyexists on the returned objectnotifywith no active connections does not thrownotifywithcount = 0does not thrownet.Socketagainst a locally-bound plain-TCP server, drives LOGIN -> SELECT -> IDLE, callsserver.notify("mbx-1", 7)from outside the session, asserts client receives* 7 EXISTSDocumentation fixes
Three real errors surfaced while reading the docs I shipped in PR #76:
packages/imap-server/docs/quickstart.md-- "App passwords" section prescribed "store hashed (argon2) in your database". Contradicts the rewritten authentication contract:AuthAdapteris interface-only. Fixed.packages/imap-server/docs/quickstart.md-- "IDLE" section said "call the server's notification mechanism" without naming one, because no API existed. Fixed -- shows the realserver.notify()call.packages/imap-server/docs/deployment.md-- Fly.io and Railway Dockerfiles usednode --import tsx src/main.tscombined withpnpm install --prod. The--prodflag strips dev dependencies includingtsx, which breaks runtime. Fixed -- multi-stage Dockerfile compiles TypeScript todist/in a build stage, runsnode dist/main.jsfrom a --prod runtime stage.Verification
pnpm --filter @rafters/mail-imap-server test: 10 passingpnpm --filter @rafters/mail-imap-server typecheck: cleanpnpm --filter @rafters/mail-imap-server build: ESM + dts cleanpnpm lint: 0 warnings, 0 errorsoxfmt --checkon all modified files: cleanlegion-simplifygate: cleanStack
Chains off
chore/package-readmes(#76), which chains offchore/imap-build-pipeline(#73). When #73 and #76 merge, this branch rebases cleanly onto main.Note on PR #77
A parallel agent working on issue #57 (Spectrum TCP bridge) created PR #77 from the original
feat/imap-server-notifybranch with a mismatched title. This PR is the same notify work on a fresh branch (feat/imap-server-notify-api) with the correct title. PR #77 should be closed; Sean can handle that cleanup in the morning.Why it matters
The
@rafters/mail-imap-serverpackage ships IMAP over Node TCP. IDLE push is a core IMAP feature (RFC 2177). Withoutnotify(), a deployed Node IMAP server cannot wake IDLE clients when new mail arrives -- every client would sit in indefinite IDLE and see new mail only after timing out and re-selecting. A silent feature gap that would reach production if this package ships at 0.1.0 without the fix.