@agenticmail/core 0.5.52 → 0.5.56
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 +2 -1
- package/dist/index.cjs +200 -15
- package/dist/index.d.cts +23 -1
- package/dist/index.d.ts +23 -1
- package/dist/index.js +199 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ Every other AgenticMail package depends on this one.
|
|
|
28
28
|
|
|
29
29
|
## What This Package Does
|
|
30
30
|
|
|
31
|
-
AgenticMail Core provides
|
|
31
|
+
AgenticMail Core provides 16 major components organized into modules:
|
|
32
32
|
|
|
33
33
|
### Agent Management
|
|
34
34
|
- **AccountManager** — Creates, lists, finds, and deletes AI agent accounts. Each agent gets their own email address, login credentials, and a unique API key. Agent names must be email-safe (letters, numbers, dots, hyphens, underscores only).
|
|
@@ -58,6 +58,7 @@ AgenticMail Core provides 15 major components organized into modules:
|
|
|
58
58
|
- **scanOutboundEmail** — Scans every outgoing email before it's sent, looking for sensitive data that an AI agent shouldn't be leaking. Detects API keys (AWS, OpenAI, GitHub, Stripe, and many more), passwords, private keys (SSH, PGP, RSA), personally identifiable information (Social Security numbers, credit card numbers, bank account numbers, passport numbers, dates of birth, driver's licenses), database connection strings, JWT tokens, cryptocurrency wallet addresses, webhook URLs, environment variable blocks, and more. Also checks attachment filenames for risky file types. If any high-severity match is found, the email is blocked. Emails between local agents (`@localhost` recipients) skip scanning entirely.
|
|
59
59
|
- **sanitizeEmail** — Cleans up incoming email HTML to remove hidden content that could be used for prompt injection or phishing. Strips invisible Unicode characters (tag characters, zero-width joiners, bidirectional controls, soft hyphens), removes hidden HTML elements (display:none, visibility:hidden, font-size:0, white-on-white text, off-screen positioned elements, hidden iframes), removes script tags, strips data: and javascript: URIs, and removes suspicious HTML comments that contain words like "ignore", "system", "instruction", or "prompt". Returns both the cleaned content and a list of everything it found and removed.
|
|
60
60
|
- **scoreEmail** — Scores incoming email for spam and threat indicators using 47 pattern-matching rules across 9 categories. Returns a numeric score (0-100), whether it's classified as spam (score 40+) or a warning (score 20-39), the top threat category, and a list of every rule that matched with its score contribution.
|
|
61
|
+
- **classifyEmailRoute** — Assigns incoming mail a route class such as `ignore_spam`, `ignore_newsletter`, `archive_automated`, `project_update`, `deal_escalation`, or `agent_instruction`. The classification includes the suggested action, confidence, reason, and whether a human gate is required before downstream action.
|
|
61
62
|
- **isInternalEmail** — Detects whether an email is from another local agent (agent-to-agent communication on `@localhost`). Importantly, it recognizes relay emails — if the "from" address is `@localhost` but the reply-to address is external, it's a forwarded relay email and should be treated as external, not internal.
|
|
62
63
|
- **buildInboundSecurityAdvisory** — Analyzes incoming email attachments and spam matches to build a structured security advisory with risk levels (critical, high, medium) for attachments, double-extension detection (like `invoice.pdf.exe`), and link warnings.
|
|
63
64
|
|
package/dist/index.cjs
CHANGED
|
@@ -737,6 +737,7 @@ __export(index_exports, {
|
|
|
737
737
|
TunnelManager: () => TunnelManager,
|
|
738
738
|
WARNING_THRESHOLD: () => WARNING_THRESHOLD,
|
|
739
739
|
buildInboundSecurityAdvisory: () => buildInboundSecurityAdvisory,
|
|
740
|
+
classifyEmailRoute: () => classifyEmailRoute,
|
|
740
741
|
closeDatabase: () => closeDatabase,
|
|
741
742
|
createTestDatabase: () => createTestDatabase,
|
|
742
743
|
debug: () => debug,
|
|
@@ -1107,9 +1108,11 @@ var import_mailparser = require("mailparser");
|
|
|
1107
1108
|
async function parseEmail(raw) {
|
|
1108
1109
|
const parsed = await (0, import_mailparser.simpleParser)(raw);
|
|
1109
1110
|
const xOriginalFrom = parsed.headers?.get("x-original-from");
|
|
1111
|
+
const xAgenticMailRelay = parsed.headers?.get("x-agenticmail-relay");
|
|
1110
1112
|
const originalFromAddr = typeof xOriginalFrom === "string" ? xOriginalFrom.trim() : void 0;
|
|
1113
|
+
const isAgenticMailInboundRelay = xAgenticMailRelay === "inbound";
|
|
1111
1114
|
let fromAddrs = parsed.from?.value ?? [];
|
|
1112
|
-
if (originalFromAddr && fromAddrs.length > 0 && fromAddrs[0].address?.endsWith("@localhost")) {
|
|
1115
|
+
if (originalFromAddr && fromAddrs.length > 0 && (isAgenticMailInboundRelay || fromAddrs[0].address?.endsWith("@localhost"))) {
|
|
1113
1116
|
fromAddrs = [{ name: fromAddrs[0].name || "", address: originalFromAddr }];
|
|
1114
1117
|
}
|
|
1115
1118
|
const toAddrs = parsed.to ? Array.isArray(parsed.to) ? parsed.to.flatMap((t) => t.value) : parsed.to.value : [];
|
|
@@ -2238,6 +2241,178 @@ var AgentDeletionService = class {
|
|
|
2238
2241
|
// src/index.ts
|
|
2239
2242
|
init_spam_filter();
|
|
2240
2243
|
|
|
2244
|
+
// src/mail/route-classifier.ts
|
|
2245
|
+
var DEAL_TERMS = [
|
|
2246
|
+
"contract",
|
|
2247
|
+
"proposal",
|
|
2248
|
+
"quote",
|
|
2249
|
+
"pricing",
|
|
2250
|
+
"price",
|
|
2251
|
+
"budget",
|
|
2252
|
+
"purchase order",
|
|
2253
|
+
"invoice",
|
|
2254
|
+
"deal",
|
|
2255
|
+
"renewal",
|
|
2256
|
+
"msa",
|
|
2257
|
+
"sow",
|
|
2258
|
+
"deadline",
|
|
2259
|
+
"urgent",
|
|
2260
|
+
"asap",
|
|
2261
|
+
"time sensitive"
|
|
2262
|
+
];
|
|
2263
|
+
var INSTRUCTION_TERMS = [
|
|
2264
|
+
"task",
|
|
2265
|
+
"instruction",
|
|
2266
|
+
"please",
|
|
2267
|
+
"can you",
|
|
2268
|
+
"could you",
|
|
2269
|
+
"follow up",
|
|
2270
|
+
"draft",
|
|
2271
|
+
"reply",
|
|
2272
|
+
"send",
|
|
2273
|
+
"research",
|
|
2274
|
+
"summarize",
|
|
2275
|
+
"investigate",
|
|
2276
|
+
"action item",
|
|
2277
|
+
"todo"
|
|
2278
|
+
];
|
|
2279
|
+
var AUTOMATION_SUBJECT_TERMS = [
|
|
2280
|
+
"receipt",
|
|
2281
|
+
"notification",
|
|
2282
|
+
"alert",
|
|
2283
|
+
"build",
|
|
2284
|
+
"deployment",
|
|
2285
|
+
"backup",
|
|
2286
|
+
"statement",
|
|
2287
|
+
"verification code",
|
|
2288
|
+
"security code",
|
|
2289
|
+
"login code"
|
|
2290
|
+
];
|
|
2291
|
+
function normalize(value) {
|
|
2292
|
+
return (value ?? "").toLowerCase();
|
|
2293
|
+
}
|
|
2294
|
+
function textFor(email) {
|
|
2295
|
+
return `${email.subject ?? ""}
|
|
2296
|
+
${email.text ?? ""}
|
|
2297
|
+
${email.html ?? ""}`.toLowerCase();
|
|
2298
|
+
}
|
|
2299
|
+
function firstAddress(email) {
|
|
2300
|
+
return normalize(email.from[0]?.address);
|
|
2301
|
+
}
|
|
2302
|
+
function header(email, name) {
|
|
2303
|
+
const wanted = name.toLowerCase();
|
|
2304
|
+
for (const [key, value] of email.headers) {
|
|
2305
|
+
if (key.toLowerCase() === wanted) return normalize(value);
|
|
2306
|
+
}
|
|
2307
|
+
return "";
|
|
2308
|
+
}
|
|
2309
|
+
function localPart(address) {
|
|
2310
|
+
return address.split("@")[0] ?? "";
|
|
2311
|
+
}
|
|
2312
|
+
function containsAny(text, terms) {
|
|
2313
|
+
return terms.some((term) => text.includes(term));
|
|
2314
|
+
}
|
|
2315
|
+
function accountPolicy(account) {
|
|
2316
|
+
const metadata = account?.metadata ?? {};
|
|
2317
|
+
const value = metadata.emailRoutePolicy ?? metadata.routePolicy ?? metadata.mailboxPolicy;
|
|
2318
|
+
return typeof value === "string" ? value.toLowerCase() : "";
|
|
2319
|
+
}
|
|
2320
|
+
function isInternalAddress(address) {
|
|
2321
|
+
return address.endsWith("@localhost");
|
|
2322
|
+
}
|
|
2323
|
+
function isNewsletter(email) {
|
|
2324
|
+
const from = firstAddress(email);
|
|
2325
|
+
const subjectAndBody = textFor(email);
|
|
2326
|
+
return Boolean(
|
|
2327
|
+
header(email, "list-unsubscribe") || header(email, "list-id") || header(email, "x-campaign-id") || header(email, "x-mailchimp-campaign") || header(email, "precedence") === "list" || localPart(from).includes("newsletter") || subjectAndBody.includes("unsubscribe") || subjectAndBody.includes("newsletter") || subjectAndBody.includes("weekly digest")
|
|
2328
|
+
);
|
|
2329
|
+
}
|
|
2330
|
+
function isAutomated(email) {
|
|
2331
|
+
const from = firstAddress(email);
|
|
2332
|
+
const subject = normalize(email.subject);
|
|
2333
|
+
const precedence = header(email, "precedence");
|
|
2334
|
+
const autoSubmitted = header(email, "auto-submitted");
|
|
2335
|
+
return Boolean(
|
|
2336
|
+
autoSubmitted && autoSubmitted !== "no" || precedence === "bulk" || precedence === "auto" || localPart(from).includes("no-reply") || localPart(from).includes("noreply") || localPart(from).includes("donotreply") || containsAny(subject, AUTOMATION_SUBJECT_TERMS)
|
|
2337
|
+
);
|
|
2338
|
+
}
|
|
2339
|
+
function classifyEmailRoute(input) {
|
|
2340
|
+
const { email, spam, account } = input;
|
|
2341
|
+
const policy = accountPolicy(account);
|
|
2342
|
+
const from = firstAddress(email);
|
|
2343
|
+
const allText = textFor(email);
|
|
2344
|
+
if (spam?.isSpam) {
|
|
2345
|
+
return {
|
|
2346
|
+
routeClass: "ignore_spam",
|
|
2347
|
+
action: "ignore",
|
|
2348
|
+
gateRequired: false,
|
|
2349
|
+
confidence: "high",
|
|
2350
|
+
reason: `Spam score ${spam.score} exceeded the spam threshold`
|
|
2351
|
+
};
|
|
2352
|
+
}
|
|
2353
|
+
if (policy === "human" || policy === "private") {
|
|
2354
|
+
return {
|
|
2355
|
+
routeClass: "human_private",
|
|
2356
|
+
action: "notify",
|
|
2357
|
+
gateRequired: true,
|
|
2358
|
+
confidence: "high",
|
|
2359
|
+
reason: "Account policy marks this mailbox as human/private"
|
|
2360
|
+
};
|
|
2361
|
+
}
|
|
2362
|
+
if (isNewsletter(email)) {
|
|
2363
|
+
return {
|
|
2364
|
+
routeClass: "ignore_newsletter",
|
|
2365
|
+
action: "ignore",
|
|
2366
|
+
gateRequired: false,
|
|
2367
|
+
confidence: "high",
|
|
2368
|
+
reason: "Newsletter headers or unsubscribe signals were detected"
|
|
2369
|
+
};
|
|
2370
|
+
}
|
|
2371
|
+
if (isAutomated(email) && !containsAny(allText, DEAL_TERMS)) {
|
|
2372
|
+
return {
|
|
2373
|
+
routeClass: "archive_automated",
|
|
2374
|
+
action: "archive",
|
|
2375
|
+
gateRequired: false,
|
|
2376
|
+
confidence: "medium",
|
|
2377
|
+
reason: "Automated sender or notification pattern detected"
|
|
2378
|
+
};
|
|
2379
|
+
}
|
|
2380
|
+
if ((policy === "agent" || isInternalAddress(from)) && containsAny(allText, INSTRUCTION_TERMS)) {
|
|
2381
|
+
return {
|
|
2382
|
+
routeClass: "agent_instruction",
|
|
2383
|
+
action: "create_task",
|
|
2384
|
+
gateRequired: true,
|
|
2385
|
+
confidence: isInternalAddress(from) ? "high" : "medium",
|
|
2386
|
+
reason: "Instruction-like content for an agent mailbox was detected"
|
|
2387
|
+
};
|
|
2388
|
+
}
|
|
2389
|
+
if (containsAny(allText, DEAL_TERMS)) {
|
|
2390
|
+
return {
|
|
2391
|
+
routeClass: "deal_escalation",
|
|
2392
|
+
action: "escalate",
|
|
2393
|
+
gateRequired: true,
|
|
2394
|
+
confidence: "medium",
|
|
2395
|
+
reason: "Commercial, deadline, or negotiation language was detected"
|
|
2396
|
+
};
|
|
2397
|
+
}
|
|
2398
|
+
if (spam?.isWarning) {
|
|
2399
|
+
return {
|
|
2400
|
+
routeClass: "project_update",
|
|
2401
|
+
action: "notify",
|
|
2402
|
+
gateRequired: true,
|
|
2403
|
+
confidence: "low",
|
|
2404
|
+
reason: `Spam warning category ${spam.topCategory ?? "unknown"} requires cautious handling`
|
|
2405
|
+
};
|
|
2406
|
+
}
|
|
2407
|
+
return {
|
|
2408
|
+
routeClass: "project_update",
|
|
2409
|
+
action: "notify",
|
|
2410
|
+
gateRequired: false,
|
|
2411
|
+
confidence: "low",
|
|
2412
|
+
reason: "Default route for non-spam, non-automated email"
|
|
2413
|
+
};
|
|
2414
|
+
}
|
|
2415
|
+
|
|
2241
2416
|
// src/mail/sanitizer.ts
|
|
2242
2417
|
var RE_TAG_BLOCK = /[\u{E0001}-\u{E007F}]/gu;
|
|
2243
2418
|
var RE_ZERO_WIDTH = /[\u200B\u200C\u200D\uFEFF]/g;
|
|
@@ -3541,9 +3716,9 @@ var RelayGateway = class {
|
|
|
3541
3716
|
throw new Error("Relay not configured. Call setup() first.");
|
|
3542
3717
|
}
|
|
3543
3718
|
const atIdx = this.config.email.lastIndexOf("@");
|
|
3544
|
-
const
|
|
3719
|
+
const localPart2 = this.config.email.slice(0, atIdx);
|
|
3545
3720
|
const domain = this.config.email.slice(atIdx + 1);
|
|
3546
|
-
const relayFrom = `${
|
|
3721
|
+
const relayFrom = `${localPart2}+${agentName}@${domain}`;
|
|
3547
3722
|
const displayName = mail.fromName || agentName;
|
|
3548
3723
|
const mailOpts = {
|
|
3549
3724
|
from: `${displayName} <${relayFrom}>`,
|
|
@@ -3555,7 +3730,7 @@ var RelayGateway = class {
|
|
|
3555
3730
|
html: mail.html,
|
|
3556
3731
|
replyTo: relayFrom,
|
|
3557
3732
|
inReplyTo: mail.inReplyTo,
|
|
3558
|
-
references: mail.references
|
|
3733
|
+
references: Array.isArray(mail.references) ? mail.references.join(" ") : mail.references,
|
|
3559
3734
|
headers: mail.headers,
|
|
3560
3735
|
attachments: mail.attachments?.map((a) => ({
|
|
3561
3736
|
filename: a.filename,
|
|
@@ -3779,9 +3954,9 @@ var RelayGateway = class {
|
|
|
3779
3954
|
isOurRelaySender(address) {
|
|
3780
3955
|
if (!this.config) return false;
|
|
3781
3956
|
const atIdx = this.config.email.lastIndexOf("@");
|
|
3782
|
-
const
|
|
3957
|
+
const localPart2 = this.config.email.slice(0, atIdx);
|
|
3783
3958
|
const domain = this.config.email.slice(atIdx + 1);
|
|
3784
|
-
const pattern = new RegExp(`^${escapeRegex(
|
|
3959
|
+
const pattern = new RegExp(`^${escapeRegex(localPart2)}\\+[^@]+@${escapeRegex(domain)}$`, "i");
|
|
3785
3960
|
return pattern.test(address);
|
|
3786
3961
|
}
|
|
3787
3962
|
/**
|
|
@@ -3821,8 +3996,8 @@ var RelayGateway = class {
|
|
|
3821
3996
|
const match = addr.match(/^([^+]+)\+([^@]+)@/);
|
|
3822
3997
|
if (match && this.config) {
|
|
3823
3998
|
const atIdx = this.config.email.lastIndexOf("@");
|
|
3824
|
-
const
|
|
3825
|
-
if (match[1].toLowerCase() ===
|
|
3999
|
+
const localPart2 = this.config.email.slice(0, atIdx);
|
|
4000
|
+
if (match[1].toLowerCase() === localPart2.toLowerCase()) {
|
|
3826
4001
|
return match[2];
|
|
3827
4002
|
}
|
|
3828
4003
|
}
|
|
@@ -4363,9 +4538,9 @@ var DNSConfigurator = class {
|
|
|
4363
4538
|
const records = [];
|
|
4364
4539
|
const removed = [];
|
|
4365
4540
|
const existing = await this.cf.listDnsRecords(zoneId);
|
|
4366
|
-
const
|
|
4541
|
+
const normalize2 = (s) => s.replace(/^["']|["']$/g, "");
|
|
4367
4542
|
const findRecords = (type, name, contentPrefix) => existing.filter(
|
|
4368
|
-
(r) => r.type === type && r.name === name && (!contentPrefix ||
|
|
4543
|
+
(r) => r.type === type && r.name === name && (!contentPrefix || normalize2(r.content ?? "").startsWith(contentPrefix))
|
|
4369
4544
|
);
|
|
4370
4545
|
const existingMx = findRecords("MX", domain);
|
|
4371
4546
|
const cfEmailRoutingMx = existingMx.filter((r) => (r.content ?? "").startsWith("_dc-mx."));
|
|
@@ -4389,7 +4564,7 @@ var DNSConfigurator = class {
|
|
|
4389
4564
|
const ipClause = serverIp ? `ip4:${serverIp} ` : "";
|
|
4390
4565
|
const ourSpf = `v=spf1 ${ipClause}include:_spf.mx.cloudflare.net mx ~all`;
|
|
4391
4566
|
const existingSpf = findRecords("TXT", domain, "v=spf1");
|
|
4392
|
-
const alreadyHasOurSpf = existingSpf.some((r) =>
|
|
4567
|
+
const alreadyHasOurSpf = existingSpf.some((r) => normalize2(r.content) === ourSpf);
|
|
4393
4568
|
if (!alreadyHasOurSpf) {
|
|
4394
4569
|
for (const spf of existingSpf) {
|
|
4395
4570
|
await this.cf.deleteDnsRecord(zoneId, spf.id);
|
|
@@ -4420,7 +4595,7 @@ var DNSConfigurator = class {
|
|
|
4420
4595
|
const dkimName = `${options.dkimSelector}._domainkey.${domain}`;
|
|
4421
4596
|
const ourDkim = `v=DKIM1; k=rsa; p=${options.dkimPublicKey}`;
|
|
4422
4597
|
const existingDkim = findRecords("TXT", dkimName, "v=DKIM1");
|
|
4423
|
-
const alreadyCorrect = existingDkim.some((r) =>
|
|
4598
|
+
const alreadyCorrect = existingDkim.some((r) => normalize2(r.content) === ourDkim);
|
|
4424
4599
|
if (!alreadyCorrect) {
|
|
4425
4600
|
for (const rec of existingDkim) {
|
|
4426
4601
|
await this.cf.deleteDnsRecord(zoneId, rec.id);
|
|
@@ -5262,7 +5437,7 @@ var GatewayManager = class {
|
|
|
5262
5437
|
html: mail.html || void 0,
|
|
5263
5438
|
replyTo: mail.from,
|
|
5264
5439
|
inReplyTo: mail.inReplyTo,
|
|
5265
|
-
references: mail.references
|
|
5440
|
+
references: Array.isArray(mail.references) ? mail.references.join(" ") : mail.references,
|
|
5266
5441
|
headers: {
|
|
5267
5442
|
"X-AgenticMail-Relay": "inbound",
|
|
5268
5443
|
"X-Original-From": mail.from,
|
|
@@ -5744,12 +5919,14 @@ var GatewayManager = class {
|
|
|
5744
5919
|
const mailOpts = {
|
|
5745
5920
|
from,
|
|
5746
5921
|
to: recipients.join(", "),
|
|
5922
|
+
cc: mail.cc ? Array.isArray(mail.cc) ? mail.cc.join(", ") : mail.cc : void 0,
|
|
5923
|
+
bcc: mail.bcc ? Array.isArray(mail.bcc) ? mail.bcc.join(", ") : mail.bcc : void 0,
|
|
5747
5924
|
subject: mail.subject,
|
|
5748
5925
|
text: mail.text || void 0,
|
|
5749
5926
|
html: mail.html || void 0,
|
|
5750
5927
|
replyTo: mail.replyTo || from,
|
|
5751
5928
|
inReplyTo: mail.inReplyTo || void 0,
|
|
5752
|
-
references: mail.references
|
|
5929
|
+
references: Array.isArray(mail.references) ? mail.references.join(" ") : mail.references || void 0,
|
|
5753
5930
|
headers: {
|
|
5754
5931
|
"X-Mailer": "AgenticMail/1.0"
|
|
5755
5932
|
},
|
|
@@ -7411,7 +7588,14 @@ IMAP_PORT=143
|
|
|
7411
7588
|
const composePath = (0, import_node_path8.join)(dataDir, "docker-compose.yml");
|
|
7412
7589
|
(0, import_node_fs7.writeFileSync)(composePath, `services:
|
|
7413
7590
|
stalwart:
|
|
7414
|
-
|
|
7591
|
+
# Pinned to v0.15.5 \u2014 Stalwart 0.16+ moved its config to JSON
|
|
7592
|
+
# at /etc/stalwart/config.json (hardcoded into the container
|
|
7593
|
+
# CMD), runs as UID 2000, and silently ignores our pre-0.10
|
|
7594
|
+
# TOML mount. On those builds Stalwart enters bootstrap mode
|
|
7595
|
+
# and the setup wizard 404s on the admin API. Pinning until
|
|
7596
|
+
# the templates are migrated to the 0.16+ JSON layout.
|
|
7597
|
+
# Tracking: https://github.com/agenticmail/agenticmail/issues/10
|
|
7598
|
+
image: stalwartlabs/stalwart:v0.15.5
|
|
7415
7599
|
container_name: agenticmail-stalwart
|
|
7416
7600
|
ports:
|
|
7417
7601
|
- "127.0.0.1:8080:8080" # HTTP Admin + JMAP (localhost only)
|
|
@@ -7524,6 +7708,7 @@ secret = "${password}"
|
|
|
7524
7708
|
TunnelManager,
|
|
7525
7709
|
WARNING_THRESHOLD,
|
|
7526
7710
|
buildInboundSecurityAdvisory,
|
|
7711
|
+
classifyEmailRoute,
|
|
7527
7712
|
closeDatabase,
|
|
7528
7713
|
createTestDatabase,
|
|
7529
7714
|
debug,
|
package/dist/index.d.cts
CHANGED
|
@@ -507,6 +507,28 @@ declare const WARNING_THRESHOLD = 20;
|
|
|
507
507
|
declare function isInternalEmail(email: ParsedEmail, localDomains?: string[]): boolean;
|
|
508
508
|
declare function scoreEmail(email: ParsedEmail): SpamResult;
|
|
509
509
|
|
|
510
|
+
type EmailRouteClass = 'ignore_spam' | 'ignore_newsletter' | 'archive_automated' | 'project_update' | 'deal_escalation' | 'agent_instruction' | 'human_private';
|
|
511
|
+
type EmailRouteAction = 'ignore' | 'archive' | 'notify' | 'escalate' | 'create_task' | 'draft_reply';
|
|
512
|
+
interface EmailRouteAccountContext {
|
|
513
|
+
name?: string;
|
|
514
|
+
email?: string;
|
|
515
|
+
role?: string;
|
|
516
|
+
metadata?: Record<string, unknown>;
|
|
517
|
+
}
|
|
518
|
+
interface EmailRouteInput {
|
|
519
|
+
email: ParsedEmail;
|
|
520
|
+
spam?: Pick<SpamResult, 'score' | 'isSpam' | 'isWarning' | 'topCategory'>;
|
|
521
|
+
account?: EmailRouteAccountContext;
|
|
522
|
+
}
|
|
523
|
+
interface EmailRouteClassification {
|
|
524
|
+
routeClass: EmailRouteClass;
|
|
525
|
+
action: EmailRouteAction;
|
|
526
|
+
gateRequired: boolean;
|
|
527
|
+
confidence: 'low' | 'medium' | 'high';
|
|
528
|
+
reason: string;
|
|
529
|
+
}
|
|
530
|
+
declare function classifyEmailRoute(input: EmailRouteInput): EmailRouteClassification;
|
|
531
|
+
|
|
510
532
|
interface SanitizeDetection {
|
|
511
533
|
type: string;
|
|
512
534
|
description: string;
|
|
@@ -1673,4 +1695,4 @@ declare class SetupManager {
|
|
|
1673
1695
|
isInitialized(): boolean;
|
|
1674
1696
|
}
|
|
1675
1697
|
|
|
1676
|
-
export { AGENT_ROLES, AccountManager, type AddressInfo, type Agent, AgentDeletionService, type AgentRole, AgenticMailClient, type AgenticMailClientOptions, type AgenticMailConfig, type ArchiveAndDeleteOptions, type ArchivedEmail, type Attachment, type AttachmentAdvisory, CloudflareClient, type CreateAgentOptions, DEFAULT_AGENT_NAME, DEFAULT_AGENT_ROLE, DNSConfigurator, type DeletionReport, type DeletionSummary, DependencyChecker, DependencyInstaller, type DependencyStatus, type DnsRecord, type DnsSetupResult, type DomainInfo, DomainManager, type DomainModeConfig, type DomainPurchaseResult, DomainPurchaser, type DomainSearchResult, type DomainSetupResult, type EmailEnvelope, EmailSearchIndex, type FolderInfo, type GatewayConfig, GatewayManager, type GatewayManagerOptions, type GatewayMode, type GatewayStatus, type InboundEmail, type InboxEvent, type InboxExpungeEvent, type InboxFlagsEvent, type InboxNewEvent, InboxWatcher, type InboxWatcherOptions, type InstallProgress, type LinkAdvisory, type LocalSmtpConfig, MailReceiver, type MailReceiverOptions, MailSender, type MailSenderOptions, type MailboxInfo, type OutboundCategory, type OutboundScanInput, type OutboundScanResult, type OutboundWarning, type ParsedAttachment, type ParsedEmail, type ParsedSms, type PurchasedDomain, RELAY_PRESETS, RelayBridge, type RelayBridgeOptions, type RelayConfig, RelayGateway, type RelayProvider, type RelaySearchResult, SPAM_THRESHOLD, type SanitizeDetection, type SanitizeResult, type SearchCriteria, type SearchableEmail, type SecurityAdvisory, type SendMailOptions, type SendResult, type SendResultWithRaw, ServiceManager, type ServiceStatus, type SetupConfig, SetupManager, type SetupResult, type Severity, type SmsConfig, SmsManager, type SmsMessage, SmsPoller, type SpamCategory, type SpamResult, type SpamRuleMatch, StalwartAdmin, type StalwartAdminOptions, type StalwartPrincipal, type TunnelConfig, TunnelManager, WARNING_THRESHOLD, type WatcherOptions, buildInboundSecurityAdvisory, closeDatabase, createTestDatabase, debug, debugWarn, ensureDataDir, extractVerificationCode, flushTelemetry, getDatabase, isInternalEmail, isValidPhoneNumber, normalizePhoneNumber, parseEmail, parseGoogleVoiceSms, recordToolCall, resolveConfig, sanitizeEmail, saveConfig, scanOutboundEmail, scoreEmail, setTelemetryVersion, startRelayBridge };
|
|
1698
|
+
export { AGENT_ROLES, AccountManager, type AddressInfo, type Agent, AgentDeletionService, type AgentRole, AgenticMailClient, type AgenticMailClientOptions, type AgenticMailConfig, type ArchiveAndDeleteOptions, type ArchivedEmail, type Attachment, type AttachmentAdvisory, CloudflareClient, type CreateAgentOptions, DEFAULT_AGENT_NAME, DEFAULT_AGENT_ROLE, DNSConfigurator, type DeletionReport, type DeletionSummary, DependencyChecker, DependencyInstaller, type DependencyStatus, type DnsRecord, type DnsSetupResult, type DomainInfo, DomainManager, type DomainModeConfig, type DomainPurchaseResult, DomainPurchaser, type DomainSearchResult, type DomainSetupResult, type EmailEnvelope, type EmailRouteAction, type EmailRouteClass, type EmailRouteClassification, type EmailRouteInput, EmailSearchIndex, type FolderInfo, type GatewayConfig, GatewayManager, type GatewayManagerOptions, type GatewayMode, type GatewayStatus, type InboundEmail, type InboxEvent, type InboxExpungeEvent, type InboxFlagsEvent, type InboxNewEvent, InboxWatcher, type InboxWatcherOptions, type InstallProgress, type LinkAdvisory, type LocalSmtpConfig, MailReceiver, type MailReceiverOptions, MailSender, type MailSenderOptions, type MailboxInfo, type OutboundCategory, type OutboundScanInput, type OutboundScanResult, type OutboundWarning, type ParsedAttachment, type ParsedEmail, type ParsedSms, type PurchasedDomain, RELAY_PRESETS, RelayBridge, type RelayBridgeOptions, type RelayConfig, RelayGateway, type RelayProvider, type RelaySearchResult, SPAM_THRESHOLD, type SanitizeDetection, type SanitizeResult, type SearchCriteria, type SearchableEmail, type SecurityAdvisory, type SendMailOptions, type SendResult, type SendResultWithRaw, ServiceManager, type ServiceStatus, type SetupConfig, SetupManager, type SetupResult, type Severity, type SmsConfig, SmsManager, type SmsMessage, SmsPoller, type SpamCategory, type SpamResult, type SpamRuleMatch, StalwartAdmin, type StalwartAdminOptions, type StalwartPrincipal, type TunnelConfig, TunnelManager, WARNING_THRESHOLD, type WatcherOptions, buildInboundSecurityAdvisory, classifyEmailRoute, closeDatabase, createTestDatabase, debug, debugWarn, ensureDataDir, extractVerificationCode, flushTelemetry, getDatabase, isInternalEmail, isValidPhoneNumber, normalizePhoneNumber, parseEmail, parseGoogleVoiceSms, recordToolCall, resolveConfig, sanitizeEmail, saveConfig, scanOutboundEmail, scoreEmail, setTelemetryVersion, startRelayBridge };
|
package/dist/index.d.ts
CHANGED
|
@@ -507,6 +507,28 @@ declare const WARNING_THRESHOLD = 20;
|
|
|
507
507
|
declare function isInternalEmail(email: ParsedEmail, localDomains?: string[]): boolean;
|
|
508
508
|
declare function scoreEmail(email: ParsedEmail): SpamResult;
|
|
509
509
|
|
|
510
|
+
type EmailRouteClass = 'ignore_spam' | 'ignore_newsletter' | 'archive_automated' | 'project_update' | 'deal_escalation' | 'agent_instruction' | 'human_private';
|
|
511
|
+
type EmailRouteAction = 'ignore' | 'archive' | 'notify' | 'escalate' | 'create_task' | 'draft_reply';
|
|
512
|
+
interface EmailRouteAccountContext {
|
|
513
|
+
name?: string;
|
|
514
|
+
email?: string;
|
|
515
|
+
role?: string;
|
|
516
|
+
metadata?: Record<string, unknown>;
|
|
517
|
+
}
|
|
518
|
+
interface EmailRouteInput {
|
|
519
|
+
email: ParsedEmail;
|
|
520
|
+
spam?: Pick<SpamResult, 'score' | 'isSpam' | 'isWarning' | 'topCategory'>;
|
|
521
|
+
account?: EmailRouteAccountContext;
|
|
522
|
+
}
|
|
523
|
+
interface EmailRouteClassification {
|
|
524
|
+
routeClass: EmailRouteClass;
|
|
525
|
+
action: EmailRouteAction;
|
|
526
|
+
gateRequired: boolean;
|
|
527
|
+
confidence: 'low' | 'medium' | 'high';
|
|
528
|
+
reason: string;
|
|
529
|
+
}
|
|
530
|
+
declare function classifyEmailRoute(input: EmailRouteInput): EmailRouteClassification;
|
|
531
|
+
|
|
510
532
|
interface SanitizeDetection {
|
|
511
533
|
type: string;
|
|
512
534
|
description: string;
|
|
@@ -1673,4 +1695,4 @@ declare class SetupManager {
|
|
|
1673
1695
|
isInitialized(): boolean;
|
|
1674
1696
|
}
|
|
1675
1697
|
|
|
1676
|
-
export { AGENT_ROLES, AccountManager, type AddressInfo, type Agent, AgentDeletionService, type AgentRole, AgenticMailClient, type AgenticMailClientOptions, type AgenticMailConfig, type ArchiveAndDeleteOptions, type ArchivedEmail, type Attachment, type AttachmentAdvisory, CloudflareClient, type CreateAgentOptions, DEFAULT_AGENT_NAME, DEFAULT_AGENT_ROLE, DNSConfigurator, type DeletionReport, type DeletionSummary, DependencyChecker, DependencyInstaller, type DependencyStatus, type DnsRecord, type DnsSetupResult, type DomainInfo, DomainManager, type DomainModeConfig, type DomainPurchaseResult, DomainPurchaser, type DomainSearchResult, type DomainSetupResult, type EmailEnvelope, EmailSearchIndex, type FolderInfo, type GatewayConfig, GatewayManager, type GatewayManagerOptions, type GatewayMode, type GatewayStatus, type InboundEmail, type InboxEvent, type InboxExpungeEvent, type InboxFlagsEvent, type InboxNewEvent, InboxWatcher, type InboxWatcherOptions, type InstallProgress, type LinkAdvisory, type LocalSmtpConfig, MailReceiver, type MailReceiverOptions, MailSender, type MailSenderOptions, type MailboxInfo, type OutboundCategory, type OutboundScanInput, type OutboundScanResult, type OutboundWarning, type ParsedAttachment, type ParsedEmail, type ParsedSms, type PurchasedDomain, RELAY_PRESETS, RelayBridge, type RelayBridgeOptions, type RelayConfig, RelayGateway, type RelayProvider, type RelaySearchResult, SPAM_THRESHOLD, type SanitizeDetection, type SanitizeResult, type SearchCriteria, type SearchableEmail, type SecurityAdvisory, type SendMailOptions, type SendResult, type SendResultWithRaw, ServiceManager, type ServiceStatus, type SetupConfig, SetupManager, type SetupResult, type Severity, type SmsConfig, SmsManager, type SmsMessage, SmsPoller, type SpamCategory, type SpamResult, type SpamRuleMatch, StalwartAdmin, type StalwartAdminOptions, type StalwartPrincipal, type TunnelConfig, TunnelManager, WARNING_THRESHOLD, type WatcherOptions, buildInboundSecurityAdvisory, closeDatabase, createTestDatabase, debug, debugWarn, ensureDataDir, extractVerificationCode, flushTelemetry, getDatabase, isInternalEmail, isValidPhoneNumber, normalizePhoneNumber, parseEmail, parseGoogleVoiceSms, recordToolCall, resolveConfig, sanitizeEmail, saveConfig, scanOutboundEmail, scoreEmail, setTelemetryVersion, startRelayBridge };
|
|
1698
|
+
export { AGENT_ROLES, AccountManager, type AddressInfo, type Agent, AgentDeletionService, type AgentRole, AgenticMailClient, type AgenticMailClientOptions, type AgenticMailConfig, type ArchiveAndDeleteOptions, type ArchivedEmail, type Attachment, type AttachmentAdvisory, CloudflareClient, type CreateAgentOptions, DEFAULT_AGENT_NAME, DEFAULT_AGENT_ROLE, DNSConfigurator, type DeletionReport, type DeletionSummary, DependencyChecker, DependencyInstaller, type DependencyStatus, type DnsRecord, type DnsSetupResult, type DomainInfo, DomainManager, type DomainModeConfig, type DomainPurchaseResult, DomainPurchaser, type DomainSearchResult, type DomainSetupResult, type EmailEnvelope, type EmailRouteAction, type EmailRouteClass, type EmailRouteClassification, type EmailRouteInput, EmailSearchIndex, type FolderInfo, type GatewayConfig, GatewayManager, type GatewayManagerOptions, type GatewayMode, type GatewayStatus, type InboundEmail, type InboxEvent, type InboxExpungeEvent, type InboxFlagsEvent, type InboxNewEvent, InboxWatcher, type InboxWatcherOptions, type InstallProgress, type LinkAdvisory, type LocalSmtpConfig, MailReceiver, type MailReceiverOptions, MailSender, type MailSenderOptions, type MailboxInfo, type OutboundCategory, type OutboundScanInput, type OutboundScanResult, type OutboundWarning, type ParsedAttachment, type ParsedEmail, type ParsedSms, type PurchasedDomain, RELAY_PRESETS, RelayBridge, type RelayBridgeOptions, type RelayConfig, RelayGateway, type RelayProvider, type RelaySearchResult, SPAM_THRESHOLD, type SanitizeDetection, type SanitizeResult, type SearchCriteria, type SearchableEmail, type SecurityAdvisory, type SendMailOptions, type SendResult, type SendResultWithRaw, ServiceManager, type ServiceStatus, type SetupConfig, SetupManager, type SetupResult, type Severity, type SmsConfig, SmsManager, type SmsMessage, SmsPoller, type SpamCategory, type SpamResult, type SpamRuleMatch, StalwartAdmin, type StalwartAdminOptions, type StalwartPrincipal, type TunnelConfig, TunnelManager, WARNING_THRESHOLD, type WatcherOptions, buildInboundSecurityAdvisory, classifyEmailRoute, closeDatabase, createTestDatabase, debug, debugWarn, ensureDataDir, extractVerificationCode, flushTelemetry, getDatabase, isInternalEmail, isValidPhoneNumber, normalizePhoneNumber, parseEmail, parseGoogleVoiceSms, recordToolCall, resolveConfig, sanitizeEmail, saveConfig, scanOutboundEmail, scoreEmail, setTelemetryVersion, startRelayBridge };
|
package/dist/index.js
CHANGED
|
@@ -354,9 +354,11 @@ import { simpleParser } from "mailparser";
|
|
|
354
354
|
async function parseEmail(raw) {
|
|
355
355
|
const parsed = await simpleParser(raw);
|
|
356
356
|
const xOriginalFrom = parsed.headers?.get("x-original-from");
|
|
357
|
+
const xAgenticMailRelay = parsed.headers?.get("x-agenticmail-relay");
|
|
357
358
|
const originalFromAddr = typeof xOriginalFrom === "string" ? xOriginalFrom.trim() : void 0;
|
|
359
|
+
const isAgenticMailInboundRelay = xAgenticMailRelay === "inbound";
|
|
358
360
|
let fromAddrs = parsed.from?.value ?? [];
|
|
359
|
-
if (originalFromAddr && fromAddrs.length > 0 && fromAddrs[0].address?.endsWith("@localhost")) {
|
|
361
|
+
if (originalFromAddr && fromAddrs.length > 0 && (isAgenticMailInboundRelay || fromAddrs[0].address?.endsWith("@localhost"))) {
|
|
360
362
|
fromAddrs = [{ name: fromAddrs[0].name || "", address: originalFromAddr }];
|
|
361
363
|
}
|
|
362
364
|
const toAddrs = parsed.to ? Array.isArray(parsed.to) ? parsed.to.flatMap((t) => t.value) : parsed.to.value : [];
|
|
@@ -1482,6 +1484,178 @@ var AgentDeletionService = class {
|
|
|
1482
1484
|
}
|
|
1483
1485
|
};
|
|
1484
1486
|
|
|
1487
|
+
// src/mail/route-classifier.ts
|
|
1488
|
+
var DEAL_TERMS = [
|
|
1489
|
+
"contract",
|
|
1490
|
+
"proposal",
|
|
1491
|
+
"quote",
|
|
1492
|
+
"pricing",
|
|
1493
|
+
"price",
|
|
1494
|
+
"budget",
|
|
1495
|
+
"purchase order",
|
|
1496
|
+
"invoice",
|
|
1497
|
+
"deal",
|
|
1498
|
+
"renewal",
|
|
1499
|
+
"msa",
|
|
1500
|
+
"sow",
|
|
1501
|
+
"deadline",
|
|
1502
|
+
"urgent",
|
|
1503
|
+
"asap",
|
|
1504
|
+
"time sensitive"
|
|
1505
|
+
];
|
|
1506
|
+
var INSTRUCTION_TERMS = [
|
|
1507
|
+
"task",
|
|
1508
|
+
"instruction",
|
|
1509
|
+
"please",
|
|
1510
|
+
"can you",
|
|
1511
|
+
"could you",
|
|
1512
|
+
"follow up",
|
|
1513
|
+
"draft",
|
|
1514
|
+
"reply",
|
|
1515
|
+
"send",
|
|
1516
|
+
"research",
|
|
1517
|
+
"summarize",
|
|
1518
|
+
"investigate",
|
|
1519
|
+
"action item",
|
|
1520
|
+
"todo"
|
|
1521
|
+
];
|
|
1522
|
+
var AUTOMATION_SUBJECT_TERMS = [
|
|
1523
|
+
"receipt",
|
|
1524
|
+
"notification",
|
|
1525
|
+
"alert",
|
|
1526
|
+
"build",
|
|
1527
|
+
"deployment",
|
|
1528
|
+
"backup",
|
|
1529
|
+
"statement",
|
|
1530
|
+
"verification code",
|
|
1531
|
+
"security code",
|
|
1532
|
+
"login code"
|
|
1533
|
+
];
|
|
1534
|
+
function normalize(value) {
|
|
1535
|
+
return (value ?? "").toLowerCase();
|
|
1536
|
+
}
|
|
1537
|
+
function textFor(email) {
|
|
1538
|
+
return `${email.subject ?? ""}
|
|
1539
|
+
${email.text ?? ""}
|
|
1540
|
+
${email.html ?? ""}`.toLowerCase();
|
|
1541
|
+
}
|
|
1542
|
+
function firstAddress(email) {
|
|
1543
|
+
return normalize(email.from[0]?.address);
|
|
1544
|
+
}
|
|
1545
|
+
function header(email, name) {
|
|
1546
|
+
const wanted = name.toLowerCase();
|
|
1547
|
+
for (const [key, value] of email.headers) {
|
|
1548
|
+
if (key.toLowerCase() === wanted) return normalize(value);
|
|
1549
|
+
}
|
|
1550
|
+
return "";
|
|
1551
|
+
}
|
|
1552
|
+
function localPart(address) {
|
|
1553
|
+
return address.split("@")[0] ?? "";
|
|
1554
|
+
}
|
|
1555
|
+
function containsAny(text, terms) {
|
|
1556
|
+
return terms.some((term) => text.includes(term));
|
|
1557
|
+
}
|
|
1558
|
+
function accountPolicy(account) {
|
|
1559
|
+
const metadata = account?.metadata ?? {};
|
|
1560
|
+
const value = metadata.emailRoutePolicy ?? metadata.routePolicy ?? metadata.mailboxPolicy;
|
|
1561
|
+
return typeof value === "string" ? value.toLowerCase() : "";
|
|
1562
|
+
}
|
|
1563
|
+
function isInternalAddress(address) {
|
|
1564
|
+
return address.endsWith("@localhost");
|
|
1565
|
+
}
|
|
1566
|
+
function isNewsletter(email) {
|
|
1567
|
+
const from = firstAddress(email);
|
|
1568
|
+
const subjectAndBody = textFor(email);
|
|
1569
|
+
return Boolean(
|
|
1570
|
+
header(email, "list-unsubscribe") || header(email, "list-id") || header(email, "x-campaign-id") || header(email, "x-mailchimp-campaign") || header(email, "precedence") === "list" || localPart(from).includes("newsletter") || subjectAndBody.includes("unsubscribe") || subjectAndBody.includes("newsletter") || subjectAndBody.includes("weekly digest")
|
|
1571
|
+
);
|
|
1572
|
+
}
|
|
1573
|
+
function isAutomated(email) {
|
|
1574
|
+
const from = firstAddress(email);
|
|
1575
|
+
const subject = normalize(email.subject);
|
|
1576
|
+
const precedence = header(email, "precedence");
|
|
1577
|
+
const autoSubmitted = header(email, "auto-submitted");
|
|
1578
|
+
return Boolean(
|
|
1579
|
+
autoSubmitted && autoSubmitted !== "no" || precedence === "bulk" || precedence === "auto" || localPart(from).includes("no-reply") || localPart(from).includes("noreply") || localPart(from).includes("donotreply") || containsAny(subject, AUTOMATION_SUBJECT_TERMS)
|
|
1580
|
+
);
|
|
1581
|
+
}
|
|
1582
|
+
function classifyEmailRoute(input) {
|
|
1583
|
+
const { email, spam, account } = input;
|
|
1584
|
+
const policy = accountPolicy(account);
|
|
1585
|
+
const from = firstAddress(email);
|
|
1586
|
+
const allText = textFor(email);
|
|
1587
|
+
if (spam?.isSpam) {
|
|
1588
|
+
return {
|
|
1589
|
+
routeClass: "ignore_spam",
|
|
1590
|
+
action: "ignore",
|
|
1591
|
+
gateRequired: false,
|
|
1592
|
+
confidence: "high",
|
|
1593
|
+
reason: `Spam score ${spam.score} exceeded the spam threshold`
|
|
1594
|
+
};
|
|
1595
|
+
}
|
|
1596
|
+
if (policy === "human" || policy === "private") {
|
|
1597
|
+
return {
|
|
1598
|
+
routeClass: "human_private",
|
|
1599
|
+
action: "notify",
|
|
1600
|
+
gateRequired: true,
|
|
1601
|
+
confidence: "high",
|
|
1602
|
+
reason: "Account policy marks this mailbox as human/private"
|
|
1603
|
+
};
|
|
1604
|
+
}
|
|
1605
|
+
if (isNewsletter(email)) {
|
|
1606
|
+
return {
|
|
1607
|
+
routeClass: "ignore_newsletter",
|
|
1608
|
+
action: "ignore",
|
|
1609
|
+
gateRequired: false,
|
|
1610
|
+
confidence: "high",
|
|
1611
|
+
reason: "Newsletter headers or unsubscribe signals were detected"
|
|
1612
|
+
};
|
|
1613
|
+
}
|
|
1614
|
+
if (isAutomated(email) && !containsAny(allText, DEAL_TERMS)) {
|
|
1615
|
+
return {
|
|
1616
|
+
routeClass: "archive_automated",
|
|
1617
|
+
action: "archive",
|
|
1618
|
+
gateRequired: false,
|
|
1619
|
+
confidence: "medium",
|
|
1620
|
+
reason: "Automated sender or notification pattern detected"
|
|
1621
|
+
};
|
|
1622
|
+
}
|
|
1623
|
+
if ((policy === "agent" || isInternalAddress(from)) && containsAny(allText, INSTRUCTION_TERMS)) {
|
|
1624
|
+
return {
|
|
1625
|
+
routeClass: "agent_instruction",
|
|
1626
|
+
action: "create_task",
|
|
1627
|
+
gateRequired: true,
|
|
1628
|
+
confidence: isInternalAddress(from) ? "high" : "medium",
|
|
1629
|
+
reason: "Instruction-like content for an agent mailbox was detected"
|
|
1630
|
+
};
|
|
1631
|
+
}
|
|
1632
|
+
if (containsAny(allText, DEAL_TERMS)) {
|
|
1633
|
+
return {
|
|
1634
|
+
routeClass: "deal_escalation",
|
|
1635
|
+
action: "escalate",
|
|
1636
|
+
gateRequired: true,
|
|
1637
|
+
confidence: "medium",
|
|
1638
|
+
reason: "Commercial, deadline, or negotiation language was detected"
|
|
1639
|
+
};
|
|
1640
|
+
}
|
|
1641
|
+
if (spam?.isWarning) {
|
|
1642
|
+
return {
|
|
1643
|
+
routeClass: "project_update",
|
|
1644
|
+
action: "notify",
|
|
1645
|
+
gateRequired: true,
|
|
1646
|
+
confidence: "low",
|
|
1647
|
+
reason: `Spam warning category ${spam.topCategory ?? "unknown"} requires cautious handling`
|
|
1648
|
+
};
|
|
1649
|
+
}
|
|
1650
|
+
return {
|
|
1651
|
+
routeClass: "project_update",
|
|
1652
|
+
action: "notify",
|
|
1653
|
+
gateRequired: false,
|
|
1654
|
+
confidence: "low",
|
|
1655
|
+
reason: "Default route for non-spam, non-automated email"
|
|
1656
|
+
};
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1485
1659
|
// src/mail/sanitizer.ts
|
|
1486
1660
|
var RE_TAG_BLOCK = /[\u{E0001}-\u{E007F}]/gu;
|
|
1487
1661
|
var RE_ZERO_WIDTH = /[\u200B\u200C\u200D\uFEFF]/g;
|
|
@@ -2785,9 +2959,9 @@ var RelayGateway = class {
|
|
|
2785
2959
|
throw new Error("Relay not configured. Call setup() first.");
|
|
2786
2960
|
}
|
|
2787
2961
|
const atIdx = this.config.email.lastIndexOf("@");
|
|
2788
|
-
const
|
|
2962
|
+
const localPart2 = this.config.email.slice(0, atIdx);
|
|
2789
2963
|
const domain = this.config.email.slice(atIdx + 1);
|
|
2790
|
-
const relayFrom = `${
|
|
2964
|
+
const relayFrom = `${localPart2}+${agentName}@${domain}`;
|
|
2791
2965
|
const displayName = mail.fromName || agentName;
|
|
2792
2966
|
const mailOpts = {
|
|
2793
2967
|
from: `${displayName} <${relayFrom}>`,
|
|
@@ -2799,7 +2973,7 @@ var RelayGateway = class {
|
|
|
2799
2973
|
html: mail.html,
|
|
2800
2974
|
replyTo: relayFrom,
|
|
2801
2975
|
inReplyTo: mail.inReplyTo,
|
|
2802
|
-
references: mail.references
|
|
2976
|
+
references: Array.isArray(mail.references) ? mail.references.join(" ") : mail.references,
|
|
2803
2977
|
headers: mail.headers,
|
|
2804
2978
|
attachments: mail.attachments?.map((a) => ({
|
|
2805
2979
|
filename: a.filename,
|
|
@@ -3023,9 +3197,9 @@ var RelayGateway = class {
|
|
|
3023
3197
|
isOurRelaySender(address) {
|
|
3024
3198
|
if (!this.config) return false;
|
|
3025
3199
|
const atIdx = this.config.email.lastIndexOf("@");
|
|
3026
|
-
const
|
|
3200
|
+
const localPart2 = this.config.email.slice(0, atIdx);
|
|
3027
3201
|
const domain = this.config.email.slice(atIdx + 1);
|
|
3028
|
-
const pattern = new RegExp(`^${escapeRegex(
|
|
3202
|
+
const pattern = new RegExp(`^${escapeRegex(localPart2)}\\+[^@]+@${escapeRegex(domain)}$`, "i");
|
|
3029
3203
|
return pattern.test(address);
|
|
3030
3204
|
}
|
|
3031
3205
|
/**
|
|
@@ -3065,8 +3239,8 @@ var RelayGateway = class {
|
|
|
3065
3239
|
const match = addr.match(/^([^+]+)\+([^@]+)@/);
|
|
3066
3240
|
if (match && this.config) {
|
|
3067
3241
|
const atIdx = this.config.email.lastIndexOf("@");
|
|
3068
|
-
const
|
|
3069
|
-
if (match[1].toLowerCase() ===
|
|
3242
|
+
const localPart2 = this.config.email.slice(0, atIdx);
|
|
3243
|
+
if (match[1].toLowerCase() === localPart2.toLowerCase()) {
|
|
3070
3244
|
return match[2];
|
|
3071
3245
|
}
|
|
3072
3246
|
}
|
|
@@ -3607,9 +3781,9 @@ var DNSConfigurator = class {
|
|
|
3607
3781
|
const records = [];
|
|
3608
3782
|
const removed = [];
|
|
3609
3783
|
const existing = await this.cf.listDnsRecords(zoneId);
|
|
3610
|
-
const
|
|
3784
|
+
const normalize2 = (s) => s.replace(/^["']|["']$/g, "");
|
|
3611
3785
|
const findRecords = (type, name, contentPrefix) => existing.filter(
|
|
3612
|
-
(r) => r.type === type && r.name === name && (!contentPrefix ||
|
|
3786
|
+
(r) => r.type === type && r.name === name && (!contentPrefix || normalize2(r.content ?? "").startsWith(contentPrefix))
|
|
3613
3787
|
);
|
|
3614
3788
|
const existingMx = findRecords("MX", domain);
|
|
3615
3789
|
const cfEmailRoutingMx = existingMx.filter((r) => (r.content ?? "").startsWith("_dc-mx."));
|
|
@@ -3633,7 +3807,7 @@ var DNSConfigurator = class {
|
|
|
3633
3807
|
const ipClause = serverIp ? `ip4:${serverIp} ` : "";
|
|
3634
3808
|
const ourSpf = `v=spf1 ${ipClause}include:_spf.mx.cloudflare.net mx ~all`;
|
|
3635
3809
|
const existingSpf = findRecords("TXT", domain, "v=spf1");
|
|
3636
|
-
const alreadyHasOurSpf = existingSpf.some((r) =>
|
|
3810
|
+
const alreadyHasOurSpf = existingSpf.some((r) => normalize2(r.content) === ourSpf);
|
|
3637
3811
|
if (!alreadyHasOurSpf) {
|
|
3638
3812
|
for (const spf of existingSpf) {
|
|
3639
3813
|
await this.cf.deleteDnsRecord(zoneId, spf.id);
|
|
@@ -3664,7 +3838,7 @@ var DNSConfigurator = class {
|
|
|
3664
3838
|
const dkimName = `${options.dkimSelector}._domainkey.${domain}`;
|
|
3665
3839
|
const ourDkim = `v=DKIM1; k=rsa; p=${options.dkimPublicKey}`;
|
|
3666
3840
|
const existingDkim = findRecords("TXT", dkimName, "v=DKIM1");
|
|
3667
|
-
const alreadyCorrect = existingDkim.some((r) =>
|
|
3841
|
+
const alreadyCorrect = existingDkim.some((r) => normalize2(r.content) === ourDkim);
|
|
3668
3842
|
if (!alreadyCorrect) {
|
|
3669
3843
|
for (const rec of existingDkim) {
|
|
3670
3844
|
await this.cf.deleteDnsRecord(zoneId, rec.id);
|
|
@@ -4505,7 +4679,7 @@ var GatewayManager = class {
|
|
|
4505
4679
|
html: mail.html || void 0,
|
|
4506
4680
|
replyTo: mail.from,
|
|
4507
4681
|
inReplyTo: mail.inReplyTo,
|
|
4508
|
-
references: mail.references
|
|
4682
|
+
references: Array.isArray(mail.references) ? mail.references.join(" ") : mail.references,
|
|
4509
4683
|
headers: {
|
|
4510
4684
|
"X-AgenticMail-Relay": "inbound",
|
|
4511
4685
|
"X-Original-From": mail.from,
|
|
@@ -4987,12 +5161,14 @@ var GatewayManager = class {
|
|
|
4987
5161
|
const mailOpts = {
|
|
4988
5162
|
from,
|
|
4989
5163
|
to: recipients.join(", "),
|
|
5164
|
+
cc: mail.cc ? Array.isArray(mail.cc) ? mail.cc.join(", ") : mail.cc : void 0,
|
|
5165
|
+
bcc: mail.bcc ? Array.isArray(mail.bcc) ? mail.bcc.join(", ") : mail.bcc : void 0,
|
|
4990
5166
|
subject: mail.subject,
|
|
4991
5167
|
text: mail.text || void 0,
|
|
4992
5168
|
html: mail.html || void 0,
|
|
4993
5169
|
replyTo: mail.replyTo || from,
|
|
4994
5170
|
inReplyTo: mail.inReplyTo || void 0,
|
|
4995
|
-
references: mail.references
|
|
5171
|
+
references: Array.isArray(mail.references) ? mail.references.join(" ") : mail.references || void 0,
|
|
4996
5172
|
headers: {
|
|
4997
5173
|
"X-Mailer": "AgenticMail/1.0"
|
|
4998
5174
|
},
|
|
@@ -6654,7 +6830,14 @@ IMAP_PORT=143
|
|
|
6654
6830
|
const composePath = join9(dataDir, "docker-compose.yml");
|
|
6655
6831
|
writeFileSync5(composePath, `services:
|
|
6656
6832
|
stalwart:
|
|
6657
|
-
|
|
6833
|
+
# Pinned to v0.15.5 \u2014 Stalwart 0.16+ moved its config to JSON
|
|
6834
|
+
# at /etc/stalwart/config.json (hardcoded into the container
|
|
6835
|
+
# CMD), runs as UID 2000, and silently ignores our pre-0.10
|
|
6836
|
+
# TOML mount. On those builds Stalwart enters bootstrap mode
|
|
6837
|
+
# and the setup wizard 404s on the admin API. Pinning until
|
|
6838
|
+
# the templates are migrated to the 0.16+ JSON layout.
|
|
6839
|
+
# Tracking: https://github.com/agenticmail/agenticmail/issues/10
|
|
6840
|
+
image: stalwartlabs/stalwart:v0.15.5
|
|
6658
6841
|
container_name: agenticmail-stalwart
|
|
6659
6842
|
ports:
|
|
6660
6843
|
- "127.0.0.1:8080:8080" # HTTP Admin + JMAP (localhost only)
|
|
@@ -6766,6 +6949,7 @@ export {
|
|
|
6766
6949
|
TunnelManager,
|
|
6767
6950
|
WARNING_THRESHOLD,
|
|
6768
6951
|
buildInboundSecurityAdvisory,
|
|
6952
|
+
classifyEmailRoute,
|
|
6769
6953
|
closeDatabase,
|
|
6770
6954
|
createTestDatabase,
|
|
6771
6955
|
debug,
|