Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,8 @@ jobs:
cache: pnpm

- run: pnpm install --frozen-lockfile
- run: pnpm -r typecheck
# Build first so workspace packages that consume a sibling's built output
# (e.g. examples/basic-worker importing @mvrx/mail subpaths, whose types
# resolve to dist/) can be type-checked.
- run: pnpm -r build
- run: pnpm -r typecheck
16 changes: 16 additions & 0 deletions examples/basic-worker/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "@mvrx/example-basic-worker",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@mvrx/mail": "workspace:*"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20250109.0",
"typescript": "^5.8.0"
}
}
104 changes: 93 additions & 11 deletions examples/basic-worker/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,103 @@
import { parse, wrappers, type CloudflareMailEnv } from "@mvrx/mail";
import { parse, d1Init, d1Store, loadRules, evaluateRules } from "@mvrx/mail";
import { cfTransport } from "@mvrx/mail/transports";
import { cfProvider } from "@mvrx/mail/providers";
import { classify } from "@mvrx/mail/ai-tools";
import { compose } from "@mvrx/mail/compose";
import { processors } from "@mvrx/mail/attachments";
import { publishEvent, hubRouter } from "@mvrx/mail/hub";

// Register the UserHub Durable Object (backs real-time SSE events).
export { UserHub } from "@mvrx/mail/hub";

interface Env {
DB: D1Database;
BLOBS: R2Bucket;
AI: Ai;
EMAIL: SendEmail;
HUB: DurableObjectNamespace;
AGENT_MODEL_CLASSIFY: string;
AGENT_MODEL_CHAT: string;
}

export default {
async email(message: ForwardableEmailMessage, env: CloudflareMailEnv) {
// Inbound: parse (+ archive attachments to R2 and extract PDF text) → store →
// run rules → notify connected clients → AI classify → auto-acknowledge.
async email(message: ForwardableEmailMessage, env: Env) {
const email = await parse(message, {
wrapper: wrappers.xml("email"),
// Attachment handlers run during parse: store bytes to R2, then pull text
// out of PDFs via Workers AI so it's queryable / AI-ready.
onAttachment: processors.chain(
processors.storeToR2(env.BLOBS, { keyPrefix: "att" }),
processors.pdfToText({ extractor: processors.cfPdfExtractor(env.AI) })
),
});

console.log({
messageId: email.messageId,
threadId: email.threadId,
from: email.metadata.from.email,
subject: email.metadata.subject,
forAI: email.content.forAI,
await d1Init(env.DB);
await d1Store(env.DB, email);

// Single-tenant default: the recipient address is the userId.
const userId = message.to;

// Evaluate stored rules (forward/auto-reply fire through the transport).
const rules = await loadRules(env.DB);
const results = await evaluateRules(email, rules, cfTransport(env.EMAIL));
for (const r of results) {
if (!r.matched) continue;
await publishEvent(env.HUB, userId, {
type: "rule_fired",
payload: {
ruleId: r.ruleId,
messageId: email.messageId,
threadId: email.threadId,
actions: r.actions.map((a) => a.type),
},
});
}

// Push a real-time "new message" event to any connected SSE clients.
await publishEvent(env.HUB, userId, {
type: "new_message",
payload: {
messageId: email.messageId,
threadId: email.threadId,
from: email.metadata.from,
subject: email.metadata.subject,
},
});

// Classify with Workers AI.
const ai = cfProvider(env.AI);
const { category } = await classify(email, ai, { model: env.AGENT_MODEL_CLASSIFY });
console.log({ messageId: email.messageId, category });

// Auto-acknowledge with an AI-drafted reply, threaded correctly.
const { body } = await compose.reply(email, ai, {
intent: "acknowledge receipt and say we'll respond within one business day",
tone: "friendly",
model: env.AGENT_MODEL_CHAT,
});

await compose.send(
{
from: { name: "Support", email: "support@example.com" },
to: [email.metadata.from],
subject: `Re: ${email.metadata.subject ?? ""}`,
inReplyTo: email.messageId,
references: [...email.thread.references, email.messageId],
},
body,
cfTransport(env.EMAIL)
);
},

async fetch(): Promise<Response> {
return new Response("AECS basic Worker example");
// Mount the real-time SSE endpoint: clients connect with `new EventSource("/hub")`.
async fetch(req: Request, env: Env): Promise<Response> {
const url = new URL(req.url);
if (url.pathname === "/hub") {
// Derive the userId from your auth in production; single-tenant demo below.
const userId = url.searchParams.get("user") ?? "demo";
return hubRouter(req, env.HUB, userId);
}
return new Response("AECS mail Worker — receive, store, rules, events, classify, reply");
},
};
13 changes: 13 additions & 0 deletions examples/basic-worker/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2022"],
"types": ["@cloudflare/workers-types"],
"strict": true,
"noEmit": true,
"skipLibCheck": true
},
"include": ["src/**/*.ts"]
}
2 changes: 1 addition & 1 deletion examples/basic-worker/wrangler.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"durable_objects": {
"bindings": [
{
"name": "USER_HUB",
"name": "HUB",
"class_name": "UserHub"
}
]
Expand Down
66 changes: 57 additions & 9 deletions packages/mail/package.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
{
"name": "@mvrx/mail",
"version": "0.1.0",
"version": "1.0.0",
"description": "Cloudflare Email Routing SDK: send, receive, and store AI-ready email using the @mvrx/aecs standard.",
"license": "AGPL-3.0-only",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": ["dist"],
"files": [
"dist"
],
"exports": {
".": {
"import": "./dist/index.js",
Expand Down Expand Up @@ -35,12 +37,41 @@
"./types": {
"import": "./dist/types.js",
"types": "./dist/types.d.ts"
},
"./transports": {
"import": "./dist/transports/index.js",
"types": "./dist/transports/index.d.ts"
},
"./providers": {
"import": "./dist/providers/index.js",
"types": "./dist/providers/index.d.ts"
},
"./tools": {
"import": "./dist/tools.js",
"types": "./dist/tools.d.ts"
},
"./ai-tools": {
"import": "./dist/ai-tools/index.js",
"types": "./dist/ai-tools/index.d.ts"
},
"./compose": {
"import": "./dist/compose/index.js",
"types": "./dist/compose/index.d.ts"
},
"./attachments": {
"import": "./dist/attachments/index.js",
"types": "./dist/attachments/index.d.ts"
},
"./hub": {
"import": "./dist/hub/index.js",
"types": "./dist/hub/index.d.ts"
}
},
"scripts": {
"typecheck": "tsc --noEmit",
"build": "tsc",
"test": "node --test test/*.test.mjs"
"test": "vitest run",
"test:watch": "vitest"
},
"dependencies": {
"@mvrx/aecs": "github:mvrxapp/aecs#main"
Expand All @@ -52,16 +83,33 @@
"zod": "^3.0.0"
},
"peerDependenciesMeta": {
"@cloudflare/workers-types": { "optional": true },
"drizzle-orm": { "optional": true },
"hono": { "optional": true },
"zod": { "optional": true }
"@cloudflare/workers-types": {
"optional": true
},
"drizzle-orm": {
"optional": true
},
"hono": {
"optional": true
},
"zod": {
"optional": true
}
},
"devDependencies": {
"@cloudflare/vitest-pool-workers": "^0.18.0",
"@cloudflare/workers-types": "^4.20250109.0",
"typescript": "^5.8.0"
"typescript": "^5.8.0",
"vitest": "^4.1.0"
},
"keywords": ["email", "ai", "mime", "aecs", "cloudflare", "workers"],
"keywords": [
"email",
"ai",
"mime",
"aecs",
"cloudflare",
"workers"
],
"repository": {
"type": "git",
"url": "https://github.com/mvrxapp/mail.git",
Expand Down
Loading
Loading