@mailslot/core 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,9 +1,14 @@
1
1
  # @mailslot/core
2
2
 
3
- The Mailslot worker: self-hosted email inbox for AI agents on your own
4
- Cloudflare account. One Durable Object per address, MCP + HTTP + webhook
5
- surfaces, read-once OTP extraction.
3
+ The Mailslot worker as a library: a self-hosted email inbox for AI agents on
4
+ your own Cloudflare account. One Durable Object per address, MCP plus HTTP plus
5
+ webhook surfaces, read-once OTP extraction.
6
6
 
7
- Deploy it with the wizard `npx create-mailslot` which scaffolds a thin
8
- project importing this package and walks the whole setup, or from source
9
- with wrangler: see the [setup guide](https://github.com/mailslot/mailslot#quick-start).
7
+ Deploy it with the wizard, `npx create-mailslot`, which scaffolds a thin project
8
+ importing this package and walks the whole setup. Or deploy from source with
9
+ wrangler: see the [setup guide](https://github.com/mailslot/mailslot#quick-start).
10
+
11
+ To customize a deployment, subclass `Inbox` and override the protected
12
+ `onStored(email, message)` hook. It runs after the message is stored, while the
13
+ email proxy is still valid, so it is the place for per-deployment logic like
14
+ auto-replies or custom notifications, without forking core.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mailslot/core",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Self-hosted email inbox for AI agents, on your own Cloudflare account",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -30,7 +30,6 @@
30
30
  },
31
31
  "scripts": {
32
32
  "dev": "wrangler dev",
33
- "deploy": "wrangler deploy",
34
33
  "typecheck": "tsc --noEmit",
35
34
  "test": "vitest run",
36
35
  "cf-typegen": "wrangler types"
package/src/env.ts CHANGED
@@ -18,12 +18,6 @@ declare global {
18
18
  WEBHOOK_URL?: string;
19
19
  /** If set, webhook payloads are HMAC-SHA256 signed (X-Mailslot-Signature). */
20
20
  WEBHOOK_SECRET?: string;
21
- /**
22
- * Comma-separated addresses that auto-reply with a "return receipt"
23
- * showing what the worker parsed (proof-of-handling). Replies are
24
- * suppressed for auto-generated, bulk, and bounce mail.
25
- */
26
- RECEIPT_ADDRESSES?: string;
27
21
  }
28
22
  }
29
23
  }
package/src/inbox.ts CHANGED
@@ -76,13 +76,6 @@ export class Inbox extends Agent<Env> {
76
76
  return this.name;
77
77
  }
78
78
 
79
- private isReceiptAddress(): boolean {
80
- return (this.env.RECEIPT_ADDRESSES ?? "")
81
- .split(",")
82
- .map((a) => a.trim().toLowerCase())
83
- .includes(this.address);
84
- }
85
-
86
79
  async onEmail(email: AgentEmail) {
87
80
  const raw = await email.getRaw();
88
81
  const parsed = await PostalMime.parse(raw);
@@ -111,18 +104,9 @@ export class Inbox extends Agent<Env> {
111
104
  }
112
105
  }
113
106
 
114
- // Return receipt: visible proof the worker handled the mail.
115
- // Reply goes back through Email Routing (no ESP). Guarded against loops.
116
- if (this.isReceiptAddress() && shouldAutoReply(email)) {
117
- try {
118
- await this.replyToEmail(email, {
119
- fromName: "Mailslot",
120
- body: receiptBody(this.address, { id, subject, snippet, receivedAt })
121
- });
122
- } catch (e) {
123
- console.error("receipt reply failed:", e);
124
- }
125
- }
107
+ // Extension point: subclasses may add custom handling (e.g. an auto-reply)
108
+ // here, while the email proxy is still valid and before webhook delivery.
109
+ await this.onStored(email, { id, from: email.from, subject, snippet, receivedAt, consumed: false });
126
110
 
127
111
  if (this.env.WEBHOOK_URL) {
128
112
  await this.deliverWebhook({
@@ -140,6 +124,14 @@ export class Inbox extends Agent<Env> {
140
124
  }
141
125
  }
142
126
 
127
+ /**
128
+ * Extension point, called once per inbound message after it is stored and
129
+ * any forward, before webhook delivery — while the email proxy is still
130
+ * valid. Default: no-op. Override in a subclass to add custom handling such
131
+ * as an auto-reply. Core stays free of outbound/business logic.
132
+ */
133
+ protected async onStored(_email: AgentEmail, _message: MessageSummary): Promise<void> {}
134
+
143
135
  list(opts: ListOptions = {}): MessageSummary[] {
144
136
  const limit = Math.min(Math.max(opts.limit ?? 20, 1), 100);
145
137
  // Small per-address volumes: fetch recent window, filter in JS.
@@ -263,46 +255,6 @@ export class Inbox extends Agent<Env> {
263
255
  }
264
256
  }
265
257
 
266
- /**
267
- * Loop prevention for auto-replies. Never answer bounces, auto-generated,
268
- * or bulk/list mail — replying to a robot is how mail loops are born.
269
- */
270
- function shouldAutoReply(email: AgentEmail): boolean {
271
- const from = (email.from ?? "").toLowerCase();
272
- if (!from || from.startsWith("mailer-daemon") || from.startsWith("postmaster")) return false;
273
-
274
- const h = email.headers;
275
- const autoSubmitted = h.get("auto-submitted");
276
- if (autoSubmitted && autoSubmitted.toLowerCase() !== "no") return false;
277
- if (h.get("x-auto-response-suppress")) return false;
278
- if (h.get("list-id") || h.get("list-unsubscribe")) return false;
279
- const precedence = (h.get("precedence") ?? "").toLowerCase();
280
- if (precedence === "bulk" || precedence === "junk" || precedence === "list") return false;
281
- return true;
282
- }
283
-
284
- function receiptBody(
285
- address: string,
286
- msg: { id: string; subject: string; snippet: string; receivedAt: number }
287
- ): string {
288
- return [
289
- "Return receipt — your mail was handled by a Mailslot worker.",
290
- "",
291
- ` inbox: ${address}`,
292
- ` subject: ${msg.subject || "(none)"}`,
293
- ` parsed: ${msg.snippet || "(empty body)"}`,
294
- ` id: ${msg.id}`,
295
- ` received: ${new Date(msg.receivedAt).toISOString()}`,
296
- "",
297
- "This reply was sent by the same Cloudflare Worker that received,",
298
- "parsed, and stored your message — self-hosted, no email provider",
299
- "involved. An AI agent can now read it over MCP.",
300
- "",
301
- "Mailslot — your agent's email shouldn't come with a landlord.",
302
- "https://mailslot.dev · https://github.com/mailslot/mailslot"
303
- ].join("\n");
304
- }
305
-
306
258
  function toSummary(row: MessageRow): MessageSummary {
307
259
  return {
308
260
  id: row.id,
package/src/index.ts CHANGED
@@ -6,6 +6,8 @@ import { handleApi } from "./api";
6
6
  import { checkBearerToken } from "./auth";
7
7
 
8
8
  export { Inbox, MailslotMcp };
9
+ export type { MessageSummary, MessageDetail } from "./inbox";
10
+ export type { AgentEmail } from "agents";
9
11
 
10
12
  const mcpHandler = MailslotMcp.serve("/mcp", { binding: "MailslotMcp" });
11
13