@mailslot/core 0.0.2 → 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 +11 -6
- package/package.json +1 -2
- package/src/env.ts +0 -6
- package/src/inbox.ts +11 -59
- package/src/index.ts +2 -0
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
|
|
4
|
-
Cloudflare account. One Durable Object per address, MCP
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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.
|
|
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
|
-
//
|
|
115
|
-
//
|
|
116
|
-
|
|
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
|
|