From 6221f9c6bc23c35626e80edd79ab39c014af647e Mon Sep 17 00:00:00 2001 From: Sean Silvius Date: Sat, 11 Apr 2026 04:47:20 -0700 Subject: [PATCH 1/3] chore(publish): per-package READMEs and MIT license field for all 9 packages 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) --- packages/better-auth-resend/README.md | 50 +++++++++++++++ packages/better-auth-resend/package.json | 1 + packages/cloudflare/README.md | 75 ++++++++++++++++++++++ packages/cloudflare/package.json | 1 + packages/core/README.md | 68 ++++++++++++++++++++ packages/core/package.json | 1 + packages/imap-cloudflare/README.md | 72 +++++++++++++++++++++ packages/imap-cloudflare/package.json | 1 + packages/imap-server/README.md | 82 ++++++++++++++++++++++++ packages/imap-server/package.json | 1 + packages/imap/README.md | 77 ++++++++++++++++++++++ packages/imap/package.json | 1 + packages/react-email/README.md | 74 +++++++++++++++++++++ packages/react-email/package.json | 1 + packages/resend/README.md | 78 ++++++++++++++++++++++ packages/resend/package.json | 1 + packages/workers-ai/README.md | 59 +++++++++++++++++ packages/workers-ai/package.json | 1 + 18 files changed, 644 insertions(+) create mode 100644 packages/better-auth-resend/README.md create mode 100644 packages/cloudflare/README.md create mode 100644 packages/core/README.md create mode 100644 packages/imap-cloudflare/README.md create mode 100644 packages/imap-server/README.md create mode 100644 packages/imap/README.md create mode 100644 packages/react-email/README.md create mode 100644 packages/resend/README.md create mode 100644 packages/workers-ai/README.md diff --git a/packages/better-auth-resend/README.md b/packages/better-auth-resend/README.md new file mode 100644 index 0000000..d3f8e9f --- /dev/null +++ b/packages/better-auth-resend/README.md @@ -0,0 +1,50 @@ +# @rafters/better-auth-resend + +One-line emailOTP integration for [better-auth](https://better-auth.com) using [`@rafters/mail-resend`](https://www.npmjs.com/package/@rafters/mail-resend) + [`@rafters/mail-react-email`](https://www.npmjs.com/package/@rafters/mail-react-email). Glues the three together so better-auth's `emailOTP` plugin sends real emails through Resend with the rafters `OtpEmail` template. + +Part of [@rafters/mail](https://github.com/rafters-studio/mail). + +## Install + +```bash +pnpm add @rafters/better-auth-resend @rafters/mail-resend @rafters/mail-react-email better-auth +``` + +## Usage + +```typescript +import { betterAuth } from "better-auth"; +import { emailOTP } from "better-auth/plugins"; +import { resendOTP } from "@rafters/better-auth-resend"; + +export const auth = betterAuth({ + // ... your database, session, etc. + plugins: [ + emailOTP({ + sendVerificationOTP: resendOTP({ + apiKey: env.RESEND_API_KEY, + from: "noreply@example.com", + appName: "Example", + }), + }), + ], +}); +``` + +That is the whole integration. `resendOTP` builds the Resend provider, renders `OtpEmail` via the React Email renderer, and returns the `sendVerificationOTP` function that better-auth's emailOTP plugin expects. + +## Configuration + +| Option | Required | Description | +| --------- | -------- | -------------------------------------------------------------- | +| `apiKey` | yes | Resend API key | +| `from` | yes | Sender address -- must be a verified domain in Resend | +| `appName` | yes | Shown in the email subject and body (e.g., "Example") | + +## Documentation + +See the [monorepo README](https://github.com/rafters-studio/mail#readme) for the wider @rafters/mail architecture. + +## License + +MIT diff --git a/packages/better-auth-resend/package.json b/packages/better-auth-resend/package.json index 0df0cdd..bb8098e 100644 --- a/packages/better-auth-resend/package.json +++ b/packages/better-auth-resend/package.json @@ -2,6 +2,7 @@ "name": "@rafters/better-auth-resend", "version": "0.0.1", "description": "better-auth emailOTP glue for @rafters/mail-resend + @rafters/mail-react-email", + "license": "MIT", "files": [ "dist" ], diff --git a/packages/cloudflare/README.md b/packages/cloudflare/README.md new file mode 100644 index 0000000..e795af8 --- /dev/null +++ b/packages/cloudflare/README.md @@ -0,0 +1,75 @@ +# @rafters/mail-cloudflare + +Cloudflare inbound email adapter for [@rafters/mail](https://github.com/rafters-studio/mail). R2 blob storage implementation, RFC 5322 email header parsing, and content hashing for idempotent ingestion. + +Pairs Cloudflare Email Routing (inbound) with R2 (blob storage) to give you the two hardest pieces of edge email: receiving raw messages and storing them somewhere durable. + +## Install + +```bash +pnpm add @rafters/mail-cloudflare @rafters/mail +``` + +## Usage + +### R2 blob storage + +```typescript +import { createR2Storage } from "@rafters/mail-cloudflare/storage"; + +const storage = createR2Storage(env.BLOB_STORAGE); + +await storage.put("messages/msg-123/raw.eml", rawEmailBuffer); +const raw = await storage.get("messages/msg-123/raw.eml"); +``` + +`createR2Storage` implements the `BlobStorage` interface from `@rafters/mail`, so it drops into any service that expects a blob store. + +### Email parsing + +```typescript +import { parseEmailHeaders, hashContent } from "@rafters/mail-cloudflare/parsing"; + +const headers = parseEmailHeaders(rawEmail); +// { messageId, from, to, subject, references, inReplyTo, date, ... } + +const hash = await hashContent(rawEmail); +// sha256 for dedupe and integrity checks +``` + +### Inbound Email Routing handler + +Wire Email Routing to a Worker that stores the raw email in R2, parses headers, dedupes by hash, and hands off to your threading service: + +```typescript +export default { + async email(message: ForwardableEmailMessage, env: Env, ctx: ExecutionContext) { + const raw = await new Response(message.raw).arrayBuffer(); + const hash = await hashContent(new Uint8Array(raw)); + + if (await env.DB.get("messages", { hash })) return; // already ingested + + const headers = parseEmailHeaders(new TextDecoder().decode(raw)); + const storage = createR2Storage(env.BLOB_STORAGE); + await storage.put(`messages/${hash}/raw.eml`, new Uint8Array(raw)); + + // Thread matching, DB insert, classification dispatch, etc. + }, +}; +``` + +## Exports + +| Subpath | What | +| ----------- | --------------------------------------------------- | +| `.` | Top-level re-exports | +| `./storage` | `createR2Storage` (R2 implementation of `BlobStorage`) | +| `./parsing` | `parseEmailHeaders`, `hashContent` | + +## Documentation + +See the [monorepo README](https://github.com/rafters-studio/mail#readme) for the complete inbound / outbound / classification flow. + +## License + +MIT diff --git a/packages/cloudflare/package.json b/packages/cloudflare/package.json index 9ab85aa..8e253d7 100644 --- a/packages/cloudflare/package.json +++ b/packages/cloudflare/package.json @@ -2,6 +2,7 @@ "name": "@rafters/mail-cloudflare", "version": "0.0.1", "description": "Cloudflare Email Routing inbound adapter and R2 blob storage for @rafters/mail", + "license": "MIT", "files": [ "dist" ], diff --git a/packages/core/README.md b/packages/core/README.md new file mode 100644 index 0000000..9079e0f --- /dev/null +++ b/packages/core/README.md @@ -0,0 +1,68 @@ +# @rafters/mail + +Email inbox framework for the edge. The core package: schema, types, service interfaces, threading logic, and database migrations. + +Part of [@rafters/mail](https://github.com/rafters-studio/mail), an email inbox framework for teams building on Cloudflare Workers, AWS Lambda, Deno Deploy, Vercel Edge, or anywhere Node runs. + +## Install + +```bash +pnpm add @rafters/mail +``` + +## What you get + +**13 Drizzle tables** covering the full inbox + newsletter data model: mailboxes (personal and shared), folders, labels (system, AI, user), threads, messages, attachments, assignments, notes, audiences, subscribers, and broadcast audit. + +**Zod validators** for every API boundary operation. Types are inferred from schemas with `z.infer<>`, never hand-written. + +**Service implementations** for thread management, folder CRUD, label application, assignments, internal notes, and compose/reply with RFC 5322 header generation. + +**Adapter interfaces** that decouple core from any vendor: + +| Interface | Purpose | Ships in | +| ------------------ | ---------------------------------------------- | ---------------------------- | +| `EmailProvider` | Send email and manage mailing lists | `@rafters/mail-resend` | +| `BlobStorage` | Store and retrieve raw email and parsed bodies | `@rafters/mail-cloudflare` | +| `EmailClassifier` | Classify email content | `@rafters/mail-workers-ai` | +| `TemplateRenderer` | Render email templates to HTML and text | `@rafters/mail-react-email` | +| `AuthAdapter` | Resolve user identity and access control | You implement | +| `InboundAdapter` | Receive email from external sources | `@rafters/mail-cloudflare` | + +## Usage + +```typescript +// Schema (Drizzle tables) +import { mailbox, inboxThread, inboxMessage } from "@rafters/mail/schema"; + +// Validators (API boundaries) +import { composeEmailSchema, listThreadsSchema } from "@rafters/mail/schema"; + +// Service implementations +import { createMailServices, createInboxEmailService } from "@rafters/mail/services"; + +// Threading +import { generateMessageId, buildReferences } from "@rafters/mail/threading"; + +// Auth adapter interface +import type { AuthAdapter } from "@rafters/mail/auth"; + +// Types +import type { Thread, Folder, Label, ComposeEmail } from "@rafters/mail"; +``` + +## Design principles + +1. **Zod is source of truth.** Types inferred via `z.infer<>`, never hand-written. +2. **Zero vendor lock-in in core.** No Resend, Cloudflare, or React Email dependencies here. +3. **Drizzle for queries, you own migrations.** Core exports schema and raw SQL; your app runs migrations with your tooling. +4. **Plain text user references.** `ownerId`, `assigneeId`, and `authorId` are text columns with no FK to external auth tables. +5. **Platform vocabulary.** `MailingList` (not Audience), `Subscriber` (not Contact), `Campaign` (not Broadcast). Vendor terms stay inside adapters. + +## Documentation + +See the [monorepo README](https://github.com/rafters-studio/mail#readme) for architecture, the full package list, and design notes. + +## License + +MIT diff --git a/packages/core/package.json b/packages/core/package.json index 5d67392..38acbeb 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -2,6 +2,7 @@ "name": "@rafters/mail", "version": "0.0.1", "description": "Email inbox framework for the edge. Schema, types, service interfaces, threading logic.", + "license": "MIT", "files": [ "dist" ], diff --git a/packages/imap-cloudflare/README.md b/packages/imap-cloudflare/README.md new file mode 100644 index 0000000..4765587 --- /dev/null +++ b/packages/imap-cloudflare/README.md @@ -0,0 +1,72 @@ +# @rafters/mail-imap-cloudflare + +Cloudflare Durable Object runtime for [`@rafters/mail-imap`](https://www.npmjs.com/package/@rafters/mail-imap). One Durable Object per mailbox, WebSocket transport, hibernation for IDLE, and an inbound-signal bridge so new mail wakes idle clients with an EXISTS notification. + +Near-zero idle cost: when no clients are connected, the DO hibernates. When a client holds IDLE, the DO stays warm only long enough to serve the connection and then hibernates again until new mail arrives or the client disconnects. + +## Install + +```bash +pnpm add @rafters/mail-imap-cloudflare @rafters/mail-imap @rafters/mail +``` + +## Usage + +```typescript +// src/worker.ts +import { createImapDurableObject, createImapWorker } from "@rafters/mail-imap-cloudflare"; +import { createAuthAdapter } from "./adapters/auth.ts"; +import { createMailboxAdapter } from "./adapters/mailbox.ts"; +import { createMessageAdapter } from "./adapters/message.ts"; + +// The DO class. Export the return value from your worker entry. +export const ImapMailboxDO = createImapDurableObject({ + createAdapters(env) { + return { + authAdapter: createAuthAdapter(env.DB), + mailboxAdapter: createMailboxAdapter(env.DB), + messageAdapter: createMessageAdapter(env.DB, env.BLOB_STORAGE), + }; + }, +}); + +// The worker entry that routes WebSocket upgrades to the correct DO +// (one DO per email address). +export default createImapWorker(); +``` + +```jsonc +// wrangler.jsonc +{ + "name": "mail-imap", + "compatibility_date": "2025-04-01", + "durable_objects": { + "bindings": [ + { "name": "IMAP_MAILBOX", "class_name": "ImapMailboxDO" }, + ], + }, + "migrations": [ + { "tag": "v1", "new_classes": ["ImapMailboxDO"] }, + ], + "d1_databases": [ + { "binding": "DB", "database_name": "mail", "database_id": "..." }, + ], + "r2_buckets": [ + { "binding": "BLOB_STORAGE", "bucket_name": "mail-blobs" }, + ], +} +``` + +Clients connect with `wss://imap.example.com/?email=user@example.com&mailboxId=mbx-123`. Standard email clients do not speak IMAP-over-WebSocket natively; this runtime is designed for web clients and custom bridges. For native TCP on port 993, use [`@rafters/mail-imap-server`](https://www.npmjs.com/package/@rafters/mail-imap-server). + +## Inbound signaling + +The DO exposes a `POST /notify?count=N` endpoint that your inbound email Worker calls after storing a new message. IDLE clients receive an EXISTS notification on the next tick. + +## Documentation + +See the [monorepo README](https://github.com/rafters-studio/mail#readme) for the complete IMAP architecture. + +## License + +MIT diff --git a/packages/imap-cloudflare/package.json b/packages/imap-cloudflare/package.json index 7c85406..6cf9fa2 100644 --- a/packages/imap-cloudflare/package.json +++ b/packages/imap-cloudflare/package.json @@ -2,6 +2,7 @@ "name": "@rafters/mail-imap-cloudflare", "version": "0.0.1", "description": "Cloudflare Durable Object runtime adapter for @rafters/mail-imap", + "license": "MIT", "files": [ "dist" ], diff --git a/packages/imap-server/README.md b/packages/imap-server/README.md new file mode 100644 index 0000000..f17a2a3 --- /dev/null +++ b/packages/imap-server/README.md @@ -0,0 +1,82 @@ +# @rafters/mail-imap-server + +Node TCP/TLS runtime for [`@rafters/mail-imap`](https://www.npmjs.com/package/@rafters/mail-imap). Listens on port 993 (IMAPS). One TCP connection per IMAP session. Deploys anywhere Node runs: Fly, Railway, AWS Fargate, DigitalOcean, Docker, VPS. + +Standard email clients (Apple Mail, Thunderbird, Outlook, K-9) connect directly over TLS, no local proxy. + +## Install + +```bash +pnpm add @rafters/mail-imap-server @rafters/mail-imap @rafters/mail +``` + +## Usage + +### TLS-terminated at the server + +```typescript +import { createImapServer } from "@rafters/mail-imap-server"; +import { readFileSync } from "node:fs"; +import { createAuthAdapter, createMailboxAdapter, createMessageAdapter } from "./adapters.ts"; + +const server = createImapServer({ + adapters: { + authAdapter: createAuthAdapter(), + mailboxAdapter: createMailboxAdapter(), + messageAdapter: createMessageAdapter(), + }, + tls: { + cert: readFileSync("/etc/letsencrypt/live/imap.example.com/fullchain.pem"), + key: readFileSync("/etc/letsencrypt/live/imap.example.com/privkey.pem"), + }, + host: "0.0.0.0", + port: 993, + async resolveMailboxId(email) { + return lookupMailboxIdByEmail(email); + }, +}); + +await server.listen(); +``` + +### TLS-terminating proxy mode (Fly, Railway, ALB) + +On platforms that terminate TLS at the edge and forward plain TCP to your app, omit the `tls` config: + +```typescript +const server = createImapServer({ + adapters: { /* ... */ }, + // No tls field -- plain TCP mode. + host: "0.0.0.0", + port: Number(process.env.PORT ?? 993), + async resolveMailboxId(email) { /* ... */ }, +}); + +await server.listen(); +``` + +The proxy handles TLS on 993 with its own certificate and forwards plain TCP to the server's internal port. + +## Configuration + +| Option | Default | Description | +| ------------------- | ------------- | ------------------------------------------------------------- | +| `adapters` | required | `authAdapter`, `mailboxAdapter`, `messageAdapter`, `extensionAdapter` | +| `resolveMailboxId` | required | Function mapping authenticated email to mailbox ID | +| `tls` | optional | `{ cert, key }`. Omit for plain TCP behind a TLS proxy | +| `host` | `0.0.0.0` | Bind address | +| `port` | `993` | Listen port | +| `maxConnections` | `1000` | Concurrent connection cap. Excess get `BYE Server too busy` | +| `sessionTimeoutMs` | `30 * 60_000` | Idle session timeout before `BYE Session timeout` | + +## Deployment + +The server is a long-lived Node process. It cannot run on serverless-function runtimes that do not support persistent TCP connections (Vercel, Deno Deploy). For those, deploy the Node server in Docker on a platform that does (Fly, Railway, Fargate, VPS). + +## Documentation + +See the [monorepo README](https://github.com/rafters-studio/mail#readme) for the complete IMAP architecture and deployment guidance. + +## License + +MIT diff --git a/packages/imap-server/package.json b/packages/imap-server/package.json index 1d505d7..3950e64 100644 --- a/packages/imap-server/package.json +++ b/packages/imap-server/package.json @@ -2,6 +2,7 @@ "name": "@rafters/mail-imap-server", "version": "0.0.1", "description": "Node TCP/TLS IMAP server runtime for @rafters/mail-imap", + "license": "MIT", "files": [ "dist" ], diff --git a/packages/imap/README.md b/packages/imap/README.md new file mode 100644 index 0000000..f43a339 --- /dev/null +++ b/packages/imap/README.md @@ -0,0 +1,77 @@ +# @rafters/mail-imap + +IMAP4rev1 protocol layer for [@rafters/mail](https://github.com/rafters-studio/mail). Transport-agnostic command handlers, session state machine, UID mapping, and adapter interfaces. The protocol surface that makes standard email clients (Apple Mail, Thunderbird, Outlook, K-9) connect to your edge-native inbox. + +This package contains no transport. Pair it with a runtime: + +- [`@rafters/mail-imap-cloudflare`](https://www.npmjs.com/package/@rafters/mail-imap-cloudflare) -- Durable Object over WebSocket (hibernation, near-zero cost when idle) +- [`@rafters/mail-imap-server`](https://www.npmjs.com/package/@rafters/mail-imap-server) -- Node TCP/TLS server for Fly, Railway, Fargate, Docker, VPS + +## Install + +```bash +pnpm add @rafters/mail-imap @rafters/mail +``` + +You will almost always install one of the runtime adapters alongside this package. Use this package directly only if you are writing your own runtime. + +## What you get + +**IMAP4rev1 command handlers** covering the RFC 3501 surface plus common extensions: + +- Authentication: `CAPABILITY`, `LOGIN`, `LOGOUT` +- Mailbox: `SELECT`, `EXAMINE`, `LIST`, `LSUB`, `STATUS` +- Message: `FETCH`, `STORE`, `SEARCH`, `EXPUNGE`, `NOOP`, `CLOSE`, `UID` prefix +- Session: `IDLE` (RFC 2177), `UNSELECT` (RFC 3691) +- Extensions: `COPY`, `MOVE` (RFC 6851), `APPEND` + +**Session state machine** with `Not Authenticated`, `Authenticated`, and `Selected` states plus the RFC-correct transitions between them. + +**UID map** for stable, monotonic message UIDs per folder across sessions. + +**Flag mapping** between IMAP flags and `@rafters/mail` message field booleans (seen, answered, flagged, deleted, draft). + +**Adapter interfaces** that the runtime implementations wire to your database: + +- `AuthAdapter` -- `verifyAppPassword(email, password)` to your credential store +- `MailboxAdapter` -- folder listing, folder stats, UID enumeration +- `MessageAdapter` -- fetch messages, update flags, delete, blob retrieval, search +- `ExtensionAdapter` -- `COPY`, `MOVE`, and `APPEND` operations + +You bring your own auth system. This package does not own credential storage, hashing, or app-password generation. + +## Usage + +Importing individual subpaths keeps bundle size small: + +```typescript +// Protocol layer +import { parseCommand, formatTagged, generateGreeting } from "@rafters/mail-imap"; + +// Command handlers +import { handleLogin, handleLogout } from "@rafters/mail-imap/commands/auth"; +import { handleSelect, handleList, handleStatus } from "@rafters/mail-imap/commands/mailbox"; +import { handleFetch, handleStore, handleSearch } from "@rafters/mail-imap/commands/message"; +import { handleIdleStart, handleIdleDone } from "@rafters/mail-imap/commands/session"; +import { handleCopy, handleMove, handleAppend } from "@rafters/mail-imap/commands/extensions"; + +// Session state and UID map +import { ImapSession } from "@rafters/mail-imap/session"; +import { UidMap } from "@rafters/mail-imap/uid-map"; + +// Adapter interfaces +import type { + AuthAdapter, + MailboxAdapter, + MessageAdapter, + ExtensionAdapter, +} from "@rafters/mail-imap"; +``` + +## Documentation + +See the [monorepo README](https://github.com/rafters-studio/mail#readme) for the IMAP architecture, runtime options, and the broader @rafters/mail framework. + +## License + +MIT diff --git a/packages/imap/package.json b/packages/imap/package.json index f20eb5f..97e52df 100644 --- a/packages/imap/package.json +++ b/packages/imap/package.json @@ -2,6 +2,7 @@ "name": "@rafters/mail-imap", "version": "0.0.1", "description": "IMAP4rev1 protocol layer for @rafters/mail. Transport-agnostic command handlers and adapter interfaces.", + "license": "MIT", "files": [ "dist" ], diff --git a/packages/react-email/README.md b/packages/react-email/README.md new file mode 100644 index 0000000..77c3d9a --- /dev/null +++ b/packages/react-email/README.md @@ -0,0 +1,74 @@ +# @rafters/mail-react-email + +React Email template renderer for [@rafters/mail](https://github.com/rafters-studio/mail). Ships two baseline templates (`BaseEmail` and `OtpEmail`) and a `TemplateRenderer` implementation that produces HTML + text output from any React Email component. + +## Install + +```bash +pnpm add @rafters/mail-react-email @rafters/mail +``` + +## Usage + +### Renderer + +```typescript +import { createReactEmailRenderer } from "@rafters/mail-react-email/renderer"; +import { OtpEmail } from "@rafters/mail-react-email/otp"; + +const renderer = createReactEmailRenderer(); + +const { html, text } = await renderer.render( + OtpEmail({ otp: "123456", appName: "Example" }), +); +``` + +`createReactEmailRenderer` returns a `TemplateRenderer` matching the interface in `@rafters/mail`, so it drops into any service that expects a renderer. + +### BaseEmail template + +`BaseEmail` is the shared layout for all rafters templates. Use it directly or extend it for your own templates: + +```typescript +import { BaseEmail } from "@rafters/mail-react-email/templates"; + +export function WelcomeEmail({ name }: { name: string }) { + return ( + +

Welcome, {name}

+

Thanks for signing up.

+
+ ); +} +``` + +### OtpEmail template + +Prebuilt one-time password email, used by `@rafters/better-auth-resend` for the emailOTP flow: + +```typescript +import { OtpEmail } from "@rafters/mail-react-email/otp"; + +const component = OtpEmail({ + otp: "123456", + appName: "Example", + expiresInMinutes: 10, +}); +``` + +## Exports + +| Subpath | What | +| ------------- | --------------------------------------------- | +| `.` | Top-level re-exports | +| `./renderer` | `createReactEmailRenderer` | +| `./templates` | `BaseEmail` | +| `./otp` | `OtpEmail` | + +## Documentation + +See the [monorepo README](https://github.com/rafters-studio/mail#readme) for the @rafters/mail architecture. + +## License + +MIT diff --git a/packages/react-email/package.json b/packages/react-email/package.json index 19cc842..03adf09 100644 --- a/packages/react-email/package.json +++ b/packages/react-email/package.json @@ -2,6 +2,7 @@ "name": "@rafters/mail-react-email", "version": "0.0.1", "description": "React Email template renderer for @rafters/mail", + "license": "MIT", "files": [ "dist" ], diff --git a/packages/resend/README.md b/packages/resend/README.md new file mode 100644 index 0000000..c98a019 --- /dev/null +++ b/packages/resend/README.md @@ -0,0 +1,78 @@ +# @rafters/mail-resend + +Resend outbound email adapter for [@rafters/mail](https://github.com/rafters-studio/mail). Implements the `EmailProvider` interface (transactional send + mailing list management), plus a Resend webhook handler that maps delivery events to mail service updates, plus a mock provider for local tests. + +Uses Resend's HTTP API directly via `fetch`. No Resend SDK dependency, so the bundle stays edge-safe. + +## Install + +```bash +pnpm add @rafters/mail-resend @rafters/mail +``` + +## Usage + +### Transactional provider + +```typescript +import { createResendProvider } from "@rafters/mail-resend"; + +const provider = createResendProvider({ + apiKey: env.RESEND_API_KEY, + defaultFrom: "hello@example.com", +}); + +await provider.send({ + to: "user@example.com", + subject: "Welcome", + html: "

Hi there.

", + text: "Hi there.", +}); +``` + +### Mock provider for tests + +```typescript +import { createMockEmailProvider } from "@rafters/mail-resend/mock"; + +const provider = createMockEmailProvider(); +await provider.send({ to: "u@test", subject: "hi", html: "

hi

", text: "hi" }); + +expect(provider.sentEmails).toHaveLength(1); +``` + +### Webhook handler + +```typescript +import { createResendWebhookHandler } from "@rafters/mail-resend/webhooks"; + +const handler = createResendWebhookHandler({ + signingSecret: env.RESEND_WEBHOOK_SECRET, + onDelivered(event) { /* ... */ }, + onBounced(event) { /* ... */ }, + onComplained(event) { /* ... */ }, +}); + +// In your Hono / Worker route: +app.post("/webhooks/resend", async (c) => { + const result = await handler(c.req.raw); + return result.ok ? c.text("ok") : c.text(result.error, 400); +}); +``` + +## Exports + +| Subpath | What | +| ------------ | ----------------------------------------------------- | +| `.` | `createResendProvider`, `ResendService` | +| `./mock` | `createMockEmailProvider` | +| `./webhooks` | `createResendWebhookHandler` | +| `./types` | Resend API request and response types | + +## Documentation + +See the [monorepo README](https://github.com/rafters-studio/mail#readme) for the full @rafters/mail architecture. + +## License + +MIT diff --git a/packages/resend/package.json b/packages/resend/package.json index 8a3e8a7..590dd89 100644 --- a/packages/resend/package.json +++ b/packages/resend/package.json @@ -2,6 +2,7 @@ "name": "@rafters/mail-resend", "version": "0.0.1", "description": "Resend outbound email adapter for @rafters/mail", + "license": "MIT", "files": [ "dist" ], diff --git a/packages/workers-ai/README.md b/packages/workers-ai/README.md new file mode 100644 index 0000000..46a3516 --- /dev/null +++ b/packages/workers-ai/README.md @@ -0,0 +1,59 @@ +# @rafters/mail-workers-ai + +Cloudflare Workers AI email classifier for [@rafters/mail](https://github.com/rafters-studio/mail). Uses DeBERTa-v3 zero-shot classification to assign a category, priority, and auto-tags to every inbound message. + +Implements the `EmailClassifier` interface from core, so classification results flow through the standard message pipeline. + +## Install + +```bash +pnpm add @rafters/mail-workers-ai @rafters/mail +``` + +## Usage + +```typescript +import { createWorkersAIClassifier } from "@rafters/mail-workers-ai"; +import { DEFAULT_TAG_PATTERNS } from "@rafters/mail-workers-ai/config"; + +const classifier = createWorkersAIClassifier({ + ai: env.AI, // Cloudflare Workers AI binding + tagPatterns: DEFAULT_TAG_PATTERNS, +}); + +const result = await classifier.classify({ + subject: "Your order has shipped", + body: "Tracking: 1Z999...", +}); + +// result = { +// category: "transactional", +// confidence: 0.94, +// priority: "normal", +// tags: ["shipping", "order"], +// } +``` + +## Categories + +The classifier emits one of a small fixed set of categories per message: + +- `transactional` -- order confirmations, receipts, password resets +- `notification` -- system or app alerts +- `marketing` -- promotional and newsletter content +- `personal` -- human correspondence +- `spam` -- unwanted mail (moved to the spam folder by the pipeline) + +Priority (`urgent`, `high`, `normal`, `low`) is derived from the category plus subject-line signals. Tags are pattern-matched against configurable regex patterns -- override `tagPatterns` to add your own. + +## Runtime + +This package is designed for Cloudflare Workers AI and expects an `AI` binding. If you run it in another runtime, provide a stub that matches the `Ai` interface and returns compatible responses. + +## Documentation + +See the [monorepo README](https://github.com/rafters-studio/mail#readme) for the inbound classification flow and how it wires into message processing. + +## License + +MIT diff --git a/packages/workers-ai/package.json b/packages/workers-ai/package.json index 6c4dcdd..f5ef886 100644 --- a/packages/workers-ai/package.json +++ b/packages/workers-ai/package.json @@ -2,6 +2,7 @@ "name": "@rafters/mail-workers-ai", "version": "0.0.1", "description": "Workers AI email classifier adapter for @rafters/mail", + "license": "MIT", "files": [ "dist" ], From ad4dd467c0ad13e37657287d852cd6247c2210b0 Mon Sep 17 00:00:00 2001 From: Sean Silvius Date: Sat, 11 Apr 2026 04:52:54 -0700 Subject: [PATCH 2/3] docs(packages): per-package docs/ folders shipped in npm tarballs 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) --- docs/architecture.md | 580 ++++++++ docs/getting-started.md | 397 ++++++ packages/better-auth-resend/README.md | 4 + packages/better-auth-resend/docs/usage.md | 102 ++ packages/better-auth-resend/package.json | 3 +- packages/cloudflare/README.md | 8 +- packages/cloudflare/docs/blob-storage.md | 91 ++ packages/cloudflare/docs/inbound.md | 121 ++ packages/cloudflare/docs/quickstart.md | 189 +++ packages/cloudflare/package.json | 3 +- packages/core/README.md | 10 +- packages/core/docs/adapters.md | 967 +++++++++++++ packages/core/docs/migrations.md | 79 ++ packages/core/docs/newsletters.md | 118 ++ packages/core/docs/reference.md | 1383 +++++++++++++++++++ packages/core/docs/threading.md | 83 ++ packages/core/package.json | 3 +- packages/imap-cloudflare/README.md | 4 + packages/imap-cloudflare/docs/deployment.md | 237 ++++ packages/imap-cloudflare/package.json | 3 +- packages/imap-server/README.md | 7 +- packages/imap-server/docs/deployment.md | 237 ++++ packages/imap-server/docs/quickstart.md | 168 +++ packages/imap-server/package.json | 3 +- packages/imap/README.md | 7 +- packages/imap/docs/authentication.md | 116 ++ packages/imap/docs/commands.md | 241 ++++ packages/imap/package.json | 3 +- packages/react-email/README.md | 4 + packages/react-email/docs/templates.md | 106 ++ packages/react-email/package.json | 3 +- packages/resend/README.md | 4 + packages/resend/docs/outbound.md | 131 ++ packages/resend/package.json | 3 +- packages/workers-ai/README.md | 6 +- packages/workers-ai/docs/classification.md | 111 ++ packages/workers-ai/package.json | 3 +- 37 files changed, 5524 insertions(+), 14 deletions(-) create mode 100644 docs/architecture.md create mode 100644 docs/getting-started.md create mode 100644 packages/better-auth-resend/docs/usage.md create mode 100644 packages/cloudflare/docs/blob-storage.md create mode 100644 packages/cloudflare/docs/inbound.md create mode 100644 packages/cloudflare/docs/quickstart.md create mode 100644 packages/core/docs/adapters.md create mode 100644 packages/core/docs/migrations.md create mode 100644 packages/core/docs/newsletters.md create mode 100644 packages/core/docs/reference.md create mode 100644 packages/core/docs/threading.md create mode 100644 packages/imap-cloudflare/docs/deployment.md create mode 100644 packages/imap-server/docs/deployment.md create mode 100644 packages/imap-server/docs/quickstart.md create mode 100644 packages/imap/docs/authentication.md create mode 100644 packages/imap/docs/commands.md create mode 100644 packages/react-email/docs/templates.md create mode 100644 packages/resend/docs/outbound.md create mode 100644 packages/workers-ai/docs/classification.md diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..62250a0 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,580 @@ +# @rafters/mail Architecture + +Technical reference for the internal design of @rafters/mail, an edge-native email inbox framework. + +This document covers the type system, data model, adapter boundaries, data flows, and package structure. Read this before contributing or building adapters. + +--- + +## The gap + +There is no open-source email inbox framework for edge/serverless runtimes. ActionMailbox (Rails) covers inbound ingestion only, roughly 15% of the surface needed for a production inbox. It does not handle outbound sending, threading, classification, folder/label management, team collaboration, or blob storage. + +Chatwoot has an open GitHub issue for Cloudflare Email Workers support. They cannot add it. Their architecture assumes a traditional server runtime with persistent processes, background jobs, and a PostgreSQL database. + +@rafters/mail fills this gap. Inbound, outbound, threading, classification, folders, labels, team collaboration, and blob storage. All designed for SQLite-based edge databases (D1, Turso, libSQL) with object storage (R2, S3). + +--- + +## Architecture principles + +### 1. Zod is the source of truth for all types + +Every data structure starts as a Zod schema. TypeScript types are derived via `z.infer<>`. No handwritten interfaces for data shapes. This gives three things at once: static types, runtime validation at system boundaries, and mock generation with Zocker for tests. + +```typescript +// This is how types are defined. Always. +const threadStatusSchema = z.enum(['open', 'pending', 'resolved', 'closed']); +type ThreadStatus = z.infer; + +// Never this. +interface ThreadStatus { ... } +``` + +### 2. Zero vendor lock-in in core + +`@rafters/mail` has zero dependencies on Resend, Cloudflare, React Email, or Workers AI. Every external concern is an adapter in a separate package. The core defines interfaces. Adapters implement them. + +### 3. Drizzle for schema, wrangler owns migrations + +The core exports Drizzle table definitions for type-safe queries and raw SQL for migration files. The package never runs `drizzle-kit push` or `drizzle-kit migrate`. Apps copy migration SQL into wrangler-managed migration files and apply them with `wrangler d1 migrations apply`. + +### 4. User references are plain text + +All user ID columns (`ownerId`, `assigneeId`, `assignedBy`, `authorId`, `appliedBy`) are plain `text` with no foreign key constraints. The `AuthAdapter` interface resolves user identity at runtime. This means the mail schema works with any auth system: better-auth, Lucia, Clerk, a custom JWT, anything. + +### 5. Ship what we use + +Initial adapters cover Cloudflare Workers + Resend + React Email + Workers AI because that stack runs in production. No speculative adapters. Community contributors can add Postmark, Mailgun, SES, Deno KV, S3, and whatever else they run. + +### 6. Platform vocabulary over vendor vocabulary + +Internal code uses platform terms. Vendor terms only appear inside adapter implementations. + +| Platform term | Vendor term (Resend) | +|---|---| +| MailingList | Audience | +| Subscriber | Contact | +| Campaign | Broadcast | + +### 7. No barrel files + +Edge runtimes have bundle size constraints. Workers enforces a 1MB compressed limit. Barrel exports (`index.ts` re-exporting everything) pull the entire module graph into every consumer. All packages use subpath exports in `package.json` so consumers import exactly what they need. + +```typescript +// Correct: subpath import +import { createResendProvider } from '@rafters/mail-resend'; +import { createR2BlobStorage } from '@rafters/mail-cloudflare/storage'; + +// Wrong: barrel import that pulls in everything +import { createResendProvider, createR2BlobStorage } from '@rafters/mail'; +``` + +--- + +## Package structure + +Six packages. Core has zero vendor dependencies. + +``` +@rafters/mail Core: schema, types, interfaces, threading +@rafters/mail-resend Outbound adapter (Resend API via raw fetch) +@rafters/mail-cloudflare Inbound adapter (CF Email Routing) + R2 blob storage +@rafters/mail-react-email Template renderer (React Email) +@rafters/mail-workers-ai Classifier (Workers AI, DeBERTa-v3) +@rafters/better-auth-resend Glue: wires Resend + React Email into better-auth OTP +``` + +Dependency graph: + +``` +@rafters/mail <-- @rafters/mail-resend + <-- @rafters/mail-cloudflare + <-- @rafters/mail-react-email + <-- @rafters/mail-workers-ai + +@rafters/mail-resend + @rafters/mail-react-email <-- @rafters/better-auth-resend +``` + +Every adapter depends only on `@rafters/mail`. The `better-auth-resend` glue is the only package with two adapter dependencies. + +--- + +## Data model + +### Schema: 10 inbox tables + 3 newsletter tables + +All IDs are UUIDv7 via `$defaultFn`. All timestamps use `integer` with `mode: 'timestamp_ms'` and `unixepoch('subsecond') * 1000` defaults (the D1/SQLite pattern). All tables have soft delete via `deletedAt`. JSON columns use SQLite text with `mode: 'json'`. + +#### Inbox tables + +| Table | Purpose | +|---|---| +| `mailbox` | Email addresses that send/receive. Personal (one owner) or shared (team). | +| `inbox_folder` | System folders + custom folders. Per-mailbox. | +| `inbox_label` | System, AI-generated, and user-created labels. Per-mailbox. | +| `inbox_thread` | Conversation grouping. Subject, snippet, participants, folder, status, priority. | +| `inbox_message` | Individual messages. RFC 5322 headers, envelope data, AI classification fields, blob keys. | +| `inbox_message_label` | Many-to-many: message to label. Tracks who/what applied the label. | +| `inbox_thread_label` | Many-to-many: thread to label. Thread-level filtering. | +| `inbox_attachment` | Attachment metadata. Content in blob storage. Supports inline (Content-ID) and regular. | +| `thread_assignment` | Thread assignment for shared mailbox collaboration. Status: active/completed/reassigned. | +| `thread_note` | Internal notes on threads. Markdown. Not visible to external parties. | + +#### Newsletter tables + +| Table | Purpose | +|---|---| +| `platform_audience` | Platform-wide mailing lists. | +| `platform_subscriber` | User subscriptions to audiences. | +| `broadcast_audit` | Compliance trail: who sent what, when, to which audience, recipient count. | + +The email provider (Resend) is the source of truth for subscriber data. The local tables store the registry, mappings, and provider sync identifiers. Subscriber email addresses, unsubscribe status, and campaign content live in the provider. + +### System folders + +Every mailbox gets six immutable system folders on creation: + +| Slug | Purpose | +|---|---| +| `inbox` | Default landing folder for inbound email | +| `sent` | Outbound emails | +| `drafts` | Unsent drafts | +| `spam` | AI-classified or manually flagged spam | +| `trash` | Soft-deleted, auto-purge after 30 days | +| `archive` | Archived conversations | + +Custom folders can be created per-mailbox. + +### Label types + +Three kinds: + +- **System labels**: `important`, `starred`, `unread`. Immutable. +- **AI-generated labels**: created by the classifier. `isAiGenerated = true`. Based on regex patterns (e.g., `bug-report`, `feature-request`). +- **User-created labels**: custom tags created by staff. + +Labels are many-to-many on both messages and threads. Junction tables track who applied the label and when. Null `appliedBy` means system or AI. + +### Zod schema layer + +Every table has corresponding Zod schemas: + +- **Insert schemas** for creating records +- **Select schemas** for reading records +- **Update schemas** for partial updates +- **Enum schemas**: `mailboxTypeSchema`, `threadStatusSchema`, `threadPrioritySchema`, `aiCategorySchema`, `systemFolderSchema` + +Types are always inferred: + +```typescript +type ThreadStatus = z.infer; +type InboxMessage = z.infer; +``` + +--- + +## Adapter interfaces + +All adapter interfaces are defined as Zod schemas in `@rafters/mail`. Types are inferred. Adapter packages implement the interfaces. + +### EmailProvider (outbound) + +Sends email. Manages mailing lists, subscribers, and campaigns. + +```typescript +interface EmailProvider { + sendEmail(params: EmailParams): Promise<{ id: string }>; + createMailingList(name: string): Promise; + addSubscriber(listId: string, email: string, data?: SubscriberData): Promise; + sendCampaign(params: CampaignParams): Promise<{ id: string }>; + // + getMailingList, removeSubscriber, listSubscribers, campaign draft flow, etc. +} +``` + +Implementation: `@rafters/mail-resend` (Resend API via raw `fetch`, no SDK dependency). + +### InboundAdapter + +Receives email from an external source, stores it. + +```typescript +interface InboundAdapter { + handleIncoming(email: InboundEmail): Promise<{ messageId: string; threadId: string }>; +} +``` + +Implementation: `@rafters/mail-cloudflare` (Cloudflare Email Routing). + +### BlobStorage + +Stores and retrieves raw email content and parsed bodies. + +```typescript +interface BlobStorage { + put(key: string, content: string | ArrayBuffer, options?: BlobPutOptions): Promise; + get(key: string, options?: BlobGetOptions): Promise; + delete(key: string): Promise; + generateKey(contentHash: string, extension: string): string; +} +``` + +Key format: `emails/{year}/{month}/{sha256-first-16-chars}.{eml|html|txt}` (month is zero-padded) + +Implementation: `@rafters/mail-cloudflare` (R2). + +### TemplateRenderer + +Renders email templates to HTML and plain text. + +```typescript +interface TemplateRenderer { + render(template: string, props: Record): Promise<{ html: string; text?: string }>; +} +``` + +Implementation: `@rafters/mail-react-email`. + +### EmailClassifier + +Classifies email content into categories with confidence scores. + +```typescript +interface EmailClassifier { + classify(from: string, subject: string, body: string): Promise; +} +``` + +Where `EmailClassification` is: + +```typescript +const emailClassificationSchema = z.object({ + category: z.enum(['support', 'feedback', 'abuse', 'partnership', 'spam', 'billing', 'legal', 'other']), + confidence: z.number().min(0).max(100), + tags: z.array(z.string()), + priority: z.enum(['low', 'normal', 'high', 'urgent']), +}); +``` + +Implementation: `@rafters/mail-workers-ai` (DeBERTa-v3 zero-shot on Workers AI). + +### AuthAdapter + +Resolves user identity and mailbox access. App-provided, no default implementation. + +```typescript +interface AuthAdapter { + getCurrentUser(): Promise; + getUserById(id: string): Promise; + hasMailboxAccess(userId: string, mailboxId: string): Promise; + getUserRole(userId: string, mailboxId: string): Promise; +} +``` + +Roles: `owner`, `admin`, `agent`, `viewer`. + +--- + +## Data flows + +### Inbound flow + +An email arrives from the outside world and lands in the inbox. + +``` +External sender + | + v +Cloudflare Email Routing + | + v +CF Email Worker (InboundAdapter) + | + +---> [1] Parse RFC 5322 headers + | From, To, CC, Subject, Message-ID, In-Reply-To, References, Date + | + +---> [2] Store raw .eml in blob storage (R2) + | Key: emails/{year}/{month-zero-padded}/{sha256-16}.eml + | + +---> [3] Store parsed HTML and plain text as separate blobs + | Keys: .../{sha256-16}.html, .../{sha256-16}.txt + | + +---> [4] Insert metadata row into D1 (inbox_message) + | Columns: message ID, subject, from/to/cc, blob keys, + | isRead=false, isOutbound=false + | + +---> [5] Thread matching + | Look up In-Reply-To against existing inbox_message.messageId + | If no match, check References header entries + | If no match, create new thread + | Update thread snippet and participant list + | + +---> [6] Dispatch to classification queue/workflow +``` + +The raw `.eml` is the source of truth. D1 stores parsed metadata for fast queries. If metadata is ever wrong, it can be re-derived from the raw email in blob storage. + +### Outbound flow + +A user replies to a thread or composes a new email. + +``` +App calls InboxEmailService.replyToThread() or composeEmail() + | + +---> [1] Look up thread and latest message + | + +---> [2] Generate Message-ID: + | + +---> [3] Set In-Reply-To to latest message's Message-ID + | Append to References chain (RFC 5322) + | + +---> [4] Render template via TemplateRenderer + | Pass props (brandName, logoUrl, content, etc.) + | Returns { html, text } + | + +---> [5] Send via EmailProvider (Resend adapter) + | Provider returns { id } + | + +---> [6] Store outbound message in D1 + | isOutbound=true, blob keys for raw RFC 822 content + | Store rendered content in blob storage + | + +---> [7] Update thread + Set snippet to first 200 chars of plain text + Update participant list with any new addresses + Move to sent folder if new compose, keep in current folder if reply +``` + +### Classification flow + +Runs asynchronously after inbound storage. Triggered by a Cloudflare Queue message or Workflow step. + +``` +Queue/Workflow picks up message + | + +---> [1] Fetch first 4KB of message body from blob storage + | Truncation is intentional. Classification does not need + | the full body. 4KB covers subject + opening content. + | + +---> [2] Zero-shot classify with Workers AI + | Model: @cf/microsoft/deberta-v3-base-zeroshot-v1.1-all-33 + | Labels: support, feedback, abuse, partnership, spam, billing, legal, other + | Returns: category + confidence score (0-100) + | + +---> [3] Determine priority + | abuse, legal -> always high + | Urgent keywords (urgent, emergency, asap, immediately, + | critical, broken, down, outage) -> urgent + | High keywords (important, priority, help, issue, + | problem, error, bug, crash) -> high + | support, billing -> normal + | feedback, partnership -> normal + | Everything else -> low + | + +---> [4] Auto-tag via regex patterns + | install|setup|download -> installation + | crash|error|bug|broken -> bug-report + | feature|request|suggest -> feature-request + | account|login|password|auth -> account + | payment|billing|subscribe|refund -> billing + | (Apps add domain-specific patterns via config) + | + +---> [5] Update D1 record + | Set aiCategory, aiConfidence, aiPriority on inbox_message + | + +---> [6] Update R2 metadata + | Attach classification data to the blob object + | + +---> [7] Spam handling + | If category=spam, move thread to spam folder + | + +---> [8] Apply AI-generated labels + Find-or-create labels from tags + Insert into inbox_message_label with appliedBy=null (AI) +``` + +The classifier ships a default set of tag patterns. Apps extend or override via `ClassifierConfig`: + +```typescript +interface ClassifierConfig { + tagPatterns?: Array<{ pattern: RegExp; tag: string }>; + urgentKeywords?: string[]; + highPriorityKeywords?: string[]; + classificationLabels?: string[]; + maxInputLength?: number; // default: 4000 +} +``` + +### Threading logic + +RFC 5322 References/In-Reply-To, Gmail-style: + +- **Inbound**: match `In-Reply-To` against existing `inbox_message.messageId`. If no match, walk the `References` header. If still no match, create a new thread. +- **Outbound**: generate `` as Message-ID. Set `In-Reply-To` to the latest message in the thread. Append all prior Message-IDs to the References chain. +- **Thread subject**: from the first message. +- **Thread snippet**: first 200 characters of the latest message's plain text body. +- **Thread participants**: JSON array of all email addresses that have appeared in From, To, or CC across all messages in the thread. + +--- + +## Service interfaces + +Core exports six service interfaces. Apps compose them from adapters. + +```typescript +interface InboxEmailService { + replyToThread(params: ReplyToThreadParams): Promise<{ messageId: string }>; + composeEmail(params: ComposeEmailParams): Promise<{ threadId: string; messageId: string }>; +} + +interface ThreadService { + getThread(threadId: string): Promise; + listThreads(mailboxId: string, folderId?: string): Promise; + moveToFolder(threadId: string, folderId: string): Promise; + updateStatus(threadId: string, status: ThreadStatus): Promise; + updatePriority(threadId: string, priority: ThreadPriority): Promise; + archive(threadId: string): Promise; + trash(threadId: string): Promise; +} + +interface FolderService { + createFolder(mailboxId: string, name: string): Promise; + listFolders(mailboxId: string): Promise; + deleteFolder(folderId: string): Promise; + initSystemFolders(mailboxId: string): Promise; +} + +interface LabelService { + createLabel(mailboxId: string, name: string): Promise