@mailslot/core 0.0.1 → 0.0.2
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/package.json +4 -1
- package/src/env.ts +6 -0
- package/src/inbox.ts +61 -12
package/package.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mailslot/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"description": "Self-hosted email inbox for AI agents, on your own Cloudflare account",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.ts"
|
|
9
|
+
},
|
|
7
10
|
"publishConfig": {
|
|
8
11
|
"access": "public"
|
|
9
12
|
},
|
package/src/env.ts
CHANGED
|
@@ -18,6 +18,12 @@ 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;
|
|
21
27
|
}
|
|
22
28
|
}
|
|
23
29
|
}
|
package/src/inbox.ts
CHANGED
|
@@ -1,19 +1,8 @@
|
|
|
1
|
-
import { Agent } from "agents";
|
|
1
|
+
import { Agent, type AgentEmail } from "agents";
|
|
2
2
|
import PostalMime from "postal-mime";
|
|
3
3
|
import type { Env } from "./env";
|
|
4
4
|
import { extractLinks, extractOtp, htmlToText, makeSnippet } from "./extract";
|
|
5
5
|
|
|
6
|
-
/** Shape of the AgentEmail proxy passed by routeAgentEmail (agents SDK). */
|
|
7
|
-
type AgentEmail = {
|
|
8
|
-
from: string;
|
|
9
|
-
to: string;
|
|
10
|
-
headers: Headers;
|
|
11
|
-
rawSize: number;
|
|
12
|
-
getRaw: () => Promise<Uint8Array>;
|
|
13
|
-
setReject: (reason: string) => void;
|
|
14
|
-
forward: (rcptTo: string, headers?: Headers) => Promise<void>;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
6
|
export type MessageSummary = {
|
|
18
7
|
id: string;
|
|
19
8
|
from: string;
|
|
@@ -87,6 +76,13 @@ export class Inbox extends Agent<Env> {
|
|
|
87
76
|
return this.name;
|
|
88
77
|
}
|
|
89
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
|
+
|
|
90
86
|
async onEmail(email: AgentEmail) {
|
|
91
87
|
const raw = await email.getRaw();
|
|
92
88
|
const parsed = await PostalMime.parse(raw);
|
|
@@ -115,6 +111,19 @@ export class Inbox extends Agent<Env> {
|
|
|
115
111
|
}
|
|
116
112
|
}
|
|
117
113
|
|
|
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
|
+
}
|
|
126
|
+
|
|
118
127
|
if (this.env.WEBHOOK_URL) {
|
|
119
128
|
await this.deliverWebhook({
|
|
120
129
|
v: 1,
|
|
@@ -254,6 +263,46 @@ export class Inbox extends Agent<Env> {
|
|
|
254
263
|
}
|
|
255
264
|
}
|
|
256
265
|
|
|
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
|
+
|
|
257
306
|
function toSummary(row: MessageRow): MessageSummary {
|
|
258
307
|
return {
|
|
259
308
|
id: row.id,
|