@agenticmail/core 0.5.55 → 0.5.58
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 +196 -14
- package/dist/index.d.cts +23 -1
- package/dist/index.d.ts +23 -1
- package/dist/index.js +195 -14
- 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,
|
|
@@ -1237,7 +1238,8 @@ var InboxWatcher = class extends import_node_events.EventEmitter {
|
|
|
1237
1238
|
this.emit("close");
|
|
1238
1239
|
this._scheduleReconnect();
|
|
1239
1240
|
});
|
|
1240
|
-
|
|
1241
|
+
lock.release();
|
|
1242
|
+
this._lock = null;
|
|
1241
1243
|
} catch (err) {
|
|
1242
1244
|
lock.release();
|
|
1243
1245
|
throw err;
|
|
@@ -1917,6 +1919,13 @@ var AccountManager = class {
|
|
|
1917
1919
|
const principalName = options.name.toLowerCase();
|
|
1918
1920
|
const email = `${principalName}@${domain}`;
|
|
1919
1921
|
await this.stalwart.ensureDomain(domain);
|
|
1922
|
+
const existsInSqlite = await this.getByName(options.name) != null;
|
|
1923
|
+
if (!existsInSqlite) {
|
|
1924
|
+
try {
|
|
1925
|
+
await this.stalwart.deletePrincipal(principalName);
|
|
1926
|
+
} catch {
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1920
1929
|
await this.stalwart.createPrincipal({
|
|
1921
1930
|
type: "individual",
|
|
1922
1931
|
name: principalName,
|
|
@@ -2240,6 +2249,178 @@ var AgentDeletionService = class {
|
|
|
2240
2249
|
// src/index.ts
|
|
2241
2250
|
init_spam_filter();
|
|
2242
2251
|
|
|
2252
|
+
// src/mail/route-classifier.ts
|
|
2253
|
+
var DEAL_TERMS = [
|
|
2254
|
+
"contract",
|
|
2255
|
+
"proposal",
|
|
2256
|
+
"quote",
|
|
2257
|
+
"pricing",
|
|
2258
|
+
"price",
|
|
2259
|
+
"budget",
|
|
2260
|
+
"purchase order",
|
|
2261
|
+
"invoice",
|
|
2262
|
+
"deal",
|
|
2263
|
+
"renewal",
|
|
2264
|
+
"msa",
|
|
2265
|
+
"sow",
|
|
2266
|
+
"deadline",
|
|
2267
|
+
"urgent",
|
|
2268
|
+
"asap",
|
|
2269
|
+
"time sensitive"
|
|
2270
|
+
];
|
|
2271
|
+
var INSTRUCTION_TERMS = [
|
|
2272
|
+
"task",
|
|
2273
|
+
"instruction",
|
|
2274
|
+
"please",
|
|
2275
|
+
"can you",
|
|
2276
|
+
"could you",
|
|
2277
|
+
"follow up",
|
|
2278
|
+
"draft",
|
|
2279
|
+
"reply",
|
|
2280
|
+
"send",
|
|
2281
|
+
"research",
|
|
2282
|
+
"summarize",
|
|
2283
|
+
"investigate",
|
|
2284
|
+
"action item",
|
|
2285
|
+
"todo"
|
|
2286
|
+
];
|
|
2287
|
+
var AUTOMATION_SUBJECT_TERMS = [
|
|
2288
|
+
"receipt",
|
|
2289
|
+
"notification",
|
|
2290
|
+
"alert",
|
|
2291
|
+
"build",
|
|
2292
|
+
"deployment",
|
|
2293
|
+
"backup",
|
|
2294
|
+
"statement",
|
|
2295
|
+
"verification code",
|
|
2296
|
+
"security code",
|
|
2297
|
+
"login code"
|
|
2298
|
+
];
|
|
2299
|
+
function normalize(value) {
|
|
2300
|
+
return (value ?? "").toLowerCase();
|
|
2301
|
+
}
|
|
2302
|
+
function textFor(email) {
|
|
2303
|
+
return `${email.subject ?? ""}
|
|
2304
|
+
${email.text ?? ""}
|
|
2305
|
+
${email.html ?? ""}`.toLowerCase();
|
|
2306
|
+
}
|
|
2307
|
+
function firstAddress(email) {
|
|
2308
|
+
return normalize(email.from[0]?.address);
|
|
2309
|
+
}
|
|
2310
|
+
function header(email, name) {
|
|
2311
|
+
const wanted = name.toLowerCase();
|
|
2312
|
+
for (const [key, value] of email.headers) {
|
|
2313
|
+
if (key.toLowerCase() === wanted) return normalize(value);
|
|
2314
|
+
}
|
|
2315
|
+
return "";
|
|
2316
|
+
}
|
|
2317
|
+
function localPart(address) {
|
|
2318
|
+
return address.split("@")[0] ?? "";
|
|
2319
|
+
}
|
|
2320
|
+
function containsAny(text, terms) {
|
|
2321
|
+
return terms.some((term) => text.includes(term));
|
|
2322
|
+
}
|
|
2323
|
+
function accountPolicy(account) {
|
|
2324
|
+
const metadata = account?.metadata ?? {};
|
|
2325
|
+
const value = metadata.emailRoutePolicy ?? metadata.routePolicy ?? metadata.mailboxPolicy;
|
|
2326
|
+
return typeof value === "string" ? value.toLowerCase() : "";
|
|
2327
|
+
}
|
|
2328
|
+
function isInternalAddress(address) {
|
|
2329
|
+
return address.endsWith("@localhost");
|
|
2330
|
+
}
|
|
2331
|
+
function isNewsletter(email) {
|
|
2332
|
+
const from = firstAddress(email);
|
|
2333
|
+
const subjectAndBody = textFor(email);
|
|
2334
|
+
return Boolean(
|
|
2335
|
+
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")
|
|
2336
|
+
);
|
|
2337
|
+
}
|
|
2338
|
+
function isAutomated(email) {
|
|
2339
|
+
const from = firstAddress(email);
|
|
2340
|
+
const subject = normalize(email.subject);
|
|
2341
|
+
const precedence = header(email, "precedence");
|
|
2342
|
+
const autoSubmitted = header(email, "auto-submitted");
|
|
2343
|
+
return Boolean(
|
|
2344
|
+
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)
|
|
2345
|
+
);
|
|
2346
|
+
}
|
|
2347
|
+
function classifyEmailRoute(input) {
|
|
2348
|
+
const { email, spam, account } = input;
|
|
2349
|
+
const policy = accountPolicy(account);
|
|
2350
|
+
const from = firstAddress(email);
|
|
2351
|
+
const allText = textFor(email);
|
|
2352
|
+
if (spam?.isSpam) {
|
|
2353
|
+
return {
|
|
2354
|
+
routeClass: "ignore_spam",
|
|
2355
|
+
action: "ignore",
|
|
2356
|
+
gateRequired: false,
|
|
2357
|
+
confidence: "high",
|
|
2358
|
+
reason: `Spam score ${spam.score} exceeded the spam threshold`
|
|
2359
|
+
};
|
|
2360
|
+
}
|
|
2361
|
+
if (policy === "human" || policy === "private") {
|
|
2362
|
+
return {
|
|
2363
|
+
routeClass: "human_private",
|
|
2364
|
+
action: "notify",
|
|
2365
|
+
gateRequired: true,
|
|
2366
|
+
confidence: "high",
|
|
2367
|
+
reason: "Account policy marks this mailbox as human/private"
|
|
2368
|
+
};
|
|
2369
|
+
}
|
|
2370
|
+
if (isNewsletter(email)) {
|
|
2371
|
+
return {
|
|
2372
|
+
routeClass: "ignore_newsletter",
|
|
2373
|
+
action: "ignore",
|
|
2374
|
+
gateRequired: false,
|
|
2375
|
+
confidence: "high",
|
|
2376
|
+
reason: "Newsletter headers or unsubscribe signals were detected"
|
|
2377
|
+
};
|
|
2378
|
+
}
|
|
2379
|
+
if (isAutomated(email) && !containsAny(allText, DEAL_TERMS)) {
|
|
2380
|
+
return {
|
|
2381
|
+
routeClass: "archive_automated",
|
|
2382
|
+
action: "archive",
|
|
2383
|
+
gateRequired: false,
|
|
2384
|
+
confidence: "medium",
|
|
2385
|
+
reason: "Automated sender or notification pattern detected"
|
|
2386
|
+
};
|
|
2387
|
+
}
|
|
2388
|
+
if ((policy === "agent" || isInternalAddress(from)) && containsAny(allText, INSTRUCTION_TERMS)) {
|
|
2389
|
+
return {
|
|
2390
|
+
routeClass: "agent_instruction",
|
|
2391
|
+
action: "create_task",
|
|
2392
|
+
gateRequired: true,
|
|
2393
|
+
confidence: isInternalAddress(from) ? "high" : "medium",
|
|
2394
|
+
reason: "Instruction-like content for an agent mailbox was detected"
|
|
2395
|
+
};
|
|
2396
|
+
}
|
|
2397
|
+
if (containsAny(allText, DEAL_TERMS)) {
|
|
2398
|
+
return {
|
|
2399
|
+
routeClass: "deal_escalation",
|
|
2400
|
+
action: "escalate",
|
|
2401
|
+
gateRequired: true,
|
|
2402
|
+
confidence: "medium",
|
|
2403
|
+
reason: "Commercial, deadline, or negotiation language was detected"
|
|
2404
|
+
};
|
|
2405
|
+
}
|
|
2406
|
+
if (spam?.isWarning) {
|
|
2407
|
+
return {
|
|
2408
|
+
routeClass: "project_update",
|
|
2409
|
+
action: "notify",
|
|
2410
|
+
gateRequired: true,
|
|
2411
|
+
confidence: "low",
|
|
2412
|
+
reason: `Spam warning category ${spam.topCategory ?? "unknown"} requires cautious handling`
|
|
2413
|
+
};
|
|
2414
|
+
}
|
|
2415
|
+
return {
|
|
2416
|
+
routeClass: "project_update",
|
|
2417
|
+
action: "notify",
|
|
2418
|
+
gateRequired: false,
|
|
2419
|
+
confidence: "low",
|
|
2420
|
+
reason: "Default route for non-spam, non-automated email"
|
|
2421
|
+
};
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2243
2424
|
// src/mail/sanitizer.ts
|
|
2244
2425
|
var RE_TAG_BLOCK = /[\u{E0001}-\u{E007F}]/gu;
|
|
2245
2426
|
var RE_ZERO_WIDTH = /[\u200B\u200C\u200D\uFEFF]/g;
|
|
@@ -3543,9 +3724,9 @@ var RelayGateway = class {
|
|
|
3543
3724
|
throw new Error("Relay not configured. Call setup() first.");
|
|
3544
3725
|
}
|
|
3545
3726
|
const atIdx = this.config.email.lastIndexOf("@");
|
|
3546
|
-
const
|
|
3727
|
+
const localPart2 = this.config.email.slice(0, atIdx);
|
|
3547
3728
|
const domain = this.config.email.slice(atIdx + 1);
|
|
3548
|
-
const relayFrom = `${
|
|
3729
|
+
const relayFrom = `${localPart2}+${agentName}@${domain}`;
|
|
3549
3730
|
const displayName = mail.fromName || agentName;
|
|
3550
3731
|
const mailOpts = {
|
|
3551
3732
|
from: `${displayName} <${relayFrom}>`,
|
|
@@ -3557,7 +3738,7 @@ var RelayGateway = class {
|
|
|
3557
3738
|
html: mail.html,
|
|
3558
3739
|
replyTo: relayFrom,
|
|
3559
3740
|
inReplyTo: mail.inReplyTo,
|
|
3560
|
-
references: mail.references
|
|
3741
|
+
references: Array.isArray(mail.references) ? mail.references.join(" ") : mail.references,
|
|
3561
3742
|
headers: mail.headers,
|
|
3562
3743
|
attachments: mail.attachments?.map((a) => ({
|
|
3563
3744
|
filename: a.filename,
|
|
@@ -3781,9 +3962,9 @@ var RelayGateway = class {
|
|
|
3781
3962
|
isOurRelaySender(address) {
|
|
3782
3963
|
if (!this.config) return false;
|
|
3783
3964
|
const atIdx = this.config.email.lastIndexOf("@");
|
|
3784
|
-
const
|
|
3965
|
+
const localPart2 = this.config.email.slice(0, atIdx);
|
|
3785
3966
|
const domain = this.config.email.slice(atIdx + 1);
|
|
3786
|
-
const pattern = new RegExp(`^${escapeRegex(
|
|
3967
|
+
const pattern = new RegExp(`^${escapeRegex(localPart2)}\\+[^@]+@${escapeRegex(domain)}$`, "i");
|
|
3787
3968
|
return pattern.test(address);
|
|
3788
3969
|
}
|
|
3789
3970
|
/**
|
|
@@ -3823,8 +4004,8 @@ var RelayGateway = class {
|
|
|
3823
4004
|
const match = addr.match(/^([^+]+)\+([^@]+)@/);
|
|
3824
4005
|
if (match && this.config) {
|
|
3825
4006
|
const atIdx = this.config.email.lastIndexOf("@");
|
|
3826
|
-
const
|
|
3827
|
-
if (match[1].toLowerCase() ===
|
|
4007
|
+
const localPart2 = this.config.email.slice(0, atIdx);
|
|
4008
|
+
if (match[1].toLowerCase() === localPart2.toLowerCase()) {
|
|
3828
4009
|
return match[2];
|
|
3829
4010
|
}
|
|
3830
4011
|
}
|
|
@@ -4365,9 +4546,9 @@ var DNSConfigurator = class {
|
|
|
4365
4546
|
const records = [];
|
|
4366
4547
|
const removed = [];
|
|
4367
4548
|
const existing = await this.cf.listDnsRecords(zoneId);
|
|
4368
|
-
const
|
|
4549
|
+
const normalize2 = (s) => s.replace(/^["']|["']$/g, "");
|
|
4369
4550
|
const findRecords = (type, name, contentPrefix) => existing.filter(
|
|
4370
|
-
(r) => r.type === type && r.name === name && (!contentPrefix ||
|
|
4551
|
+
(r) => r.type === type && r.name === name && (!contentPrefix || normalize2(r.content ?? "").startsWith(contentPrefix))
|
|
4371
4552
|
);
|
|
4372
4553
|
const existingMx = findRecords("MX", domain);
|
|
4373
4554
|
const cfEmailRoutingMx = existingMx.filter((r) => (r.content ?? "").startsWith("_dc-mx."));
|
|
@@ -4391,7 +4572,7 @@ var DNSConfigurator = class {
|
|
|
4391
4572
|
const ipClause = serverIp ? `ip4:${serverIp} ` : "";
|
|
4392
4573
|
const ourSpf = `v=spf1 ${ipClause}include:_spf.mx.cloudflare.net mx ~all`;
|
|
4393
4574
|
const existingSpf = findRecords("TXT", domain, "v=spf1");
|
|
4394
|
-
const alreadyHasOurSpf = existingSpf.some((r) =>
|
|
4575
|
+
const alreadyHasOurSpf = existingSpf.some((r) => normalize2(r.content) === ourSpf);
|
|
4395
4576
|
if (!alreadyHasOurSpf) {
|
|
4396
4577
|
for (const spf of existingSpf) {
|
|
4397
4578
|
await this.cf.deleteDnsRecord(zoneId, spf.id);
|
|
@@ -4422,7 +4603,7 @@ var DNSConfigurator = class {
|
|
|
4422
4603
|
const dkimName = `${options.dkimSelector}._domainkey.${domain}`;
|
|
4423
4604
|
const ourDkim = `v=DKIM1; k=rsa; p=${options.dkimPublicKey}`;
|
|
4424
4605
|
const existingDkim = findRecords("TXT", dkimName, "v=DKIM1");
|
|
4425
|
-
const alreadyCorrect = existingDkim.some((r) =>
|
|
4606
|
+
const alreadyCorrect = existingDkim.some((r) => normalize2(r.content) === ourDkim);
|
|
4426
4607
|
if (!alreadyCorrect) {
|
|
4427
4608
|
for (const rec of existingDkim) {
|
|
4428
4609
|
await this.cf.deleteDnsRecord(zoneId, rec.id);
|
|
@@ -5264,7 +5445,7 @@ var GatewayManager = class {
|
|
|
5264
5445
|
html: mail.html || void 0,
|
|
5265
5446
|
replyTo: mail.from,
|
|
5266
5447
|
inReplyTo: mail.inReplyTo,
|
|
5267
|
-
references: mail.references
|
|
5448
|
+
references: Array.isArray(mail.references) ? mail.references.join(" ") : mail.references,
|
|
5268
5449
|
headers: {
|
|
5269
5450
|
"X-AgenticMail-Relay": "inbound",
|
|
5270
5451
|
"X-Original-From": mail.from,
|
|
@@ -5753,7 +5934,7 @@ var GatewayManager = class {
|
|
|
5753
5934
|
html: mail.html || void 0,
|
|
5754
5935
|
replyTo: mail.replyTo || from,
|
|
5755
5936
|
inReplyTo: mail.inReplyTo || void 0,
|
|
5756
|
-
references: mail.references
|
|
5937
|
+
references: Array.isArray(mail.references) ? mail.references.join(" ") : mail.references || void 0,
|
|
5757
5938
|
headers: {
|
|
5758
5939
|
"X-Mailer": "AgenticMail/1.0"
|
|
5759
5940
|
},
|
|
@@ -7535,6 +7716,7 @@ secret = "${password}"
|
|
|
7535
7716
|
TunnelManager,
|
|
7536
7717
|
WARNING_THRESHOLD,
|
|
7537
7718
|
buildInboundSecurityAdvisory,
|
|
7719
|
+
classifyEmailRoute,
|
|
7538
7720
|
closeDatabase,
|
|
7539
7721
|
createTestDatabase,
|
|
7540
7722
|
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
|
@@ -484,7 +484,8 @@ var InboxWatcher = class extends EventEmitter {
|
|
|
484
484
|
this.emit("close");
|
|
485
485
|
this._scheduleReconnect();
|
|
486
486
|
});
|
|
487
|
-
|
|
487
|
+
lock.release();
|
|
488
|
+
this._lock = null;
|
|
488
489
|
} catch (err) {
|
|
489
490
|
lock.release();
|
|
490
491
|
throw err;
|
|
@@ -1164,6 +1165,13 @@ var AccountManager = class {
|
|
|
1164
1165
|
const principalName = options.name.toLowerCase();
|
|
1165
1166
|
const email = `${principalName}@${domain}`;
|
|
1166
1167
|
await this.stalwart.ensureDomain(domain);
|
|
1168
|
+
const existsInSqlite = await this.getByName(options.name) != null;
|
|
1169
|
+
if (!existsInSqlite) {
|
|
1170
|
+
try {
|
|
1171
|
+
await this.stalwart.deletePrincipal(principalName);
|
|
1172
|
+
} catch {
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1167
1175
|
await this.stalwart.createPrincipal({
|
|
1168
1176
|
type: "individual",
|
|
1169
1177
|
name: principalName,
|
|
@@ -1484,6 +1492,178 @@ var AgentDeletionService = class {
|
|
|
1484
1492
|
}
|
|
1485
1493
|
};
|
|
1486
1494
|
|
|
1495
|
+
// src/mail/route-classifier.ts
|
|
1496
|
+
var DEAL_TERMS = [
|
|
1497
|
+
"contract",
|
|
1498
|
+
"proposal",
|
|
1499
|
+
"quote",
|
|
1500
|
+
"pricing",
|
|
1501
|
+
"price",
|
|
1502
|
+
"budget",
|
|
1503
|
+
"purchase order",
|
|
1504
|
+
"invoice",
|
|
1505
|
+
"deal",
|
|
1506
|
+
"renewal",
|
|
1507
|
+
"msa",
|
|
1508
|
+
"sow",
|
|
1509
|
+
"deadline",
|
|
1510
|
+
"urgent",
|
|
1511
|
+
"asap",
|
|
1512
|
+
"time sensitive"
|
|
1513
|
+
];
|
|
1514
|
+
var INSTRUCTION_TERMS = [
|
|
1515
|
+
"task",
|
|
1516
|
+
"instruction",
|
|
1517
|
+
"please",
|
|
1518
|
+
"can you",
|
|
1519
|
+
"could you",
|
|
1520
|
+
"follow up",
|
|
1521
|
+
"draft",
|
|
1522
|
+
"reply",
|
|
1523
|
+
"send",
|
|
1524
|
+
"research",
|
|
1525
|
+
"summarize",
|
|
1526
|
+
"investigate",
|
|
1527
|
+
"action item",
|
|
1528
|
+
"todo"
|
|
1529
|
+
];
|
|
1530
|
+
var AUTOMATION_SUBJECT_TERMS = [
|
|
1531
|
+
"receipt",
|
|
1532
|
+
"notification",
|
|
1533
|
+
"alert",
|
|
1534
|
+
"build",
|
|
1535
|
+
"deployment",
|
|
1536
|
+
"backup",
|
|
1537
|
+
"statement",
|
|
1538
|
+
"verification code",
|
|
1539
|
+
"security code",
|
|
1540
|
+
"login code"
|
|
1541
|
+
];
|
|
1542
|
+
function normalize(value) {
|
|
1543
|
+
return (value ?? "").toLowerCase();
|
|
1544
|
+
}
|
|
1545
|
+
function textFor(email) {
|
|
1546
|
+
return `${email.subject ?? ""}
|
|
1547
|
+
${email.text ?? ""}
|
|
1548
|
+
${email.html ?? ""}`.toLowerCase();
|
|
1549
|
+
}
|
|
1550
|
+
function firstAddress(email) {
|
|
1551
|
+
return normalize(email.from[0]?.address);
|
|
1552
|
+
}
|
|
1553
|
+
function header(email, name) {
|
|
1554
|
+
const wanted = name.toLowerCase();
|
|
1555
|
+
for (const [key, value] of email.headers) {
|
|
1556
|
+
if (key.toLowerCase() === wanted) return normalize(value);
|
|
1557
|
+
}
|
|
1558
|
+
return "";
|
|
1559
|
+
}
|
|
1560
|
+
function localPart(address) {
|
|
1561
|
+
return address.split("@")[0] ?? "";
|
|
1562
|
+
}
|
|
1563
|
+
function containsAny(text, terms) {
|
|
1564
|
+
return terms.some((term) => text.includes(term));
|
|
1565
|
+
}
|
|
1566
|
+
function accountPolicy(account) {
|
|
1567
|
+
const metadata = account?.metadata ?? {};
|
|
1568
|
+
const value = metadata.emailRoutePolicy ?? metadata.routePolicy ?? metadata.mailboxPolicy;
|
|
1569
|
+
return typeof value === "string" ? value.toLowerCase() : "";
|
|
1570
|
+
}
|
|
1571
|
+
function isInternalAddress(address) {
|
|
1572
|
+
return address.endsWith("@localhost");
|
|
1573
|
+
}
|
|
1574
|
+
function isNewsletter(email) {
|
|
1575
|
+
const from = firstAddress(email);
|
|
1576
|
+
const subjectAndBody = textFor(email);
|
|
1577
|
+
return Boolean(
|
|
1578
|
+
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")
|
|
1579
|
+
);
|
|
1580
|
+
}
|
|
1581
|
+
function isAutomated(email) {
|
|
1582
|
+
const from = firstAddress(email);
|
|
1583
|
+
const subject = normalize(email.subject);
|
|
1584
|
+
const precedence = header(email, "precedence");
|
|
1585
|
+
const autoSubmitted = header(email, "auto-submitted");
|
|
1586
|
+
return Boolean(
|
|
1587
|
+
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)
|
|
1588
|
+
);
|
|
1589
|
+
}
|
|
1590
|
+
function classifyEmailRoute(input) {
|
|
1591
|
+
const { email, spam, account } = input;
|
|
1592
|
+
const policy = accountPolicy(account);
|
|
1593
|
+
const from = firstAddress(email);
|
|
1594
|
+
const allText = textFor(email);
|
|
1595
|
+
if (spam?.isSpam) {
|
|
1596
|
+
return {
|
|
1597
|
+
routeClass: "ignore_spam",
|
|
1598
|
+
action: "ignore",
|
|
1599
|
+
gateRequired: false,
|
|
1600
|
+
confidence: "high",
|
|
1601
|
+
reason: `Spam score ${spam.score} exceeded the spam threshold`
|
|
1602
|
+
};
|
|
1603
|
+
}
|
|
1604
|
+
if (policy === "human" || policy === "private") {
|
|
1605
|
+
return {
|
|
1606
|
+
routeClass: "human_private",
|
|
1607
|
+
action: "notify",
|
|
1608
|
+
gateRequired: true,
|
|
1609
|
+
confidence: "high",
|
|
1610
|
+
reason: "Account policy marks this mailbox as human/private"
|
|
1611
|
+
};
|
|
1612
|
+
}
|
|
1613
|
+
if (isNewsletter(email)) {
|
|
1614
|
+
return {
|
|
1615
|
+
routeClass: "ignore_newsletter",
|
|
1616
|
+
action: "ignore",
|
|
1617
|
+
gateRequired: false,
|
|
1618
|
+
confidence: "high",
|
|
1619
|
+
reason: "Newsletter headers or unsubscribe signals were detected"
|
|
1620
|
+
};
|
|
1621
|
+
}
|
|
1622
|
+
if (isAutomated(email) && !containsAny(allText, DEAL_TERMS)) {
|
|
1623
|
+
return {
|
|
1624
|
+
routeClass: "archive_automated",
|
|
1625
|
+
action: "archive",
|
|
1626
|
+
gateRequired: false,
|
|
1627
|
+
confidence: "medium",
|
|
1628
|
+
reason: "Automated sender or notification pattern detected"
|
|
1629
|
+
};
|
|
1630
|
+
}
|
|
1631
|
+
if ((policy === "agent" || isInternalAddress(from)) && containsAny(allText, INSTRUCTION_TERMS)) {
|
|
1632
|
+
return {
|
|
1633
|
+
routeClass: "agent_instruction",
|
|
1634
|
+
action: "create_task",
|
|
1635
|
+
gateRequired: true,
|
|
1636
|
+
confidence: isInternalAddress(from) ? "high" : "medium",
|
|
1637
|
+
reason: "Instruction-like content for an agent mailbox was detected"
|
|
1638
|
+
};
|
|
1639
|
+
}
|
|
1640
|
+
if (containsAny(allText, DEAL_TERMS)) {
|
|
1641
|
+
return {
|
|
1642
|
+
routeClass: "deal_escalation",
|
|
1643
|
+
action: "escalate",
|
|
1644
|
+
gateRequired: true,
|
|
1645
|
+
confidence: "medium",
|
|
1646
|
+
reason: "Commercial, deadline, or negotiation language was detected"
|
|
1647
|
+
};
|
|
1648
|
+
}
|
|
1649
|
+
if (spam?.isWarning) {
|
|
1650
|
+
return {
|
|
1651
|
+
routeClass: "project_update",
|
|
1652
|
+
action: "notify",
|
|
1653
|
+
gateRequired: true,
|
|
1654
|
+
confidence: "low",
|
|
1655
|
+
reason: `Spam warning category ${spam.topCategory ?? "unknown"} requires cautious handling`
|
|
1656
|
+
};
|
|
1657
|
+
}
|
|
1658
|
+
return {
|
|
1659
|
+
routeClass: "project_update",
|
|
1660
|
+
action: "notify",
|
|
1661
|
+
gateRequired: false,
|
|
1662
|
+
confidence: "low",
|
|
1663
|
+
reason: "Default route for non-spam, non-automated email"
|
|
1664
|
+
};
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1487
1667
|
// src/mail/sanitizer.ts
|
|
1488
1668
|
var RE_TAG_BLOCK = /[\u{E0001}-\u{E007F}]/gu;
|
|
1489
1669
|
var RE_ZERO_WIDTH = /[\u200B\u200C\u200D\uFEFF]/g;
|
|
@@ -2787,9 +2967,9 @@ var RelayGateway = class {
|
|
|
2787
2967
|
throw new Error("Relay not configured. Call setup() first.");
|
|
2788
2968
|
}
|
|
2789
2969
|
const atIdx = this.config.email.lastIndexOf("@");
|
|
2790
|
-
const
|
|
2970
|
+
const localPart2 = this.config.email.slice(0, atIdx);
|
|
2791
2971
|
const domain = this.config.email.slice(atIdx + 1);
|
|
2792
|
-
const relayFrom = `${
|
|
2972
|
+
const relayFrom = `${localPart2}+${agentName}@${domain}`;
|
|
2793
2973
|
const displayName = mail.fromName || agentName;
|
|
2794
2974
|
const mailOpts = {
|
|
2795
2975
|
from: `${displayName} <${relayFrom}>`,
|
|
@@ -2801,7 +2981,7 @@ var RelayGateway = class {
|
|
|
2801
2981
|
html: mail.html,
|
|
2802
2982
|
replyTo: relayFrom,
|
|
2803
2983
|
inReplyTo: mail.inReplyTo,
|
|
2804
|
-
references: mail.references
|
|
2984
|
+
references: Array.isArray(mail.references) ? mail.references.join(" ") : mail.references,
|
|
2805
2985
|
headers: mail.headers,
|
|
2806
2986
|
attachments: mail.attachments?.map((a) => ({
|
|
2807
2987
|
filename: a.filename,
|
|
@@ -3025,9 +3205,9 @@ var RelayGateway = class {
|
|
|
3025
3205
|
isOurRelaySender(address) {
|
|
3026
3206
|
if (!this.config) return false;
|
|
3027
3207
|
const atIdx = this.config.email.lastIndexOf("@");
|
|
3028
|
-
const
|
|
3208
|
+
const localPart2 = this.config.email.slice(0, atIdx);
|
|
3029
3209
|
const domain = this.config.email.slice(atIdx + 1);
|
|
3030
|
-
const pattern = new RegExp(`^${escapeRegex(
|
|
3210
|
+
const pattern = new RegExp(`^${escapeRegex(localPart2)}\\+[^@]+@${escapeRegex(domain)}$`, "i");
|
|
3031
3211
|
return pattern.test(address);
|
|
3032
3212
|
}
|
|
3033
3213
|
/**
|
|
@@ -3067,8 +3247,8 @@ var RelayGateway = class {
|
|
|
3067
3247
|
const match = addr.match(/^([^+]+)\+([^@]+)@/);
|
|
3068
3248
|
if (match && this.config) {
|
|
3069
3249
|
const atIdx = this.config.email.lastIndexOf("@");
|
|
3070
|
-
const
|
|
3071
|
-
if (match[1].toLowerCase() ===
|
|
3250
|
+
const localPart2 = this.config.email.slice(0, atIdx);
|
|
3251
|
+
if (match[1].toLowerCase() === localPart2.toLowerCase()) {
|
|
3072
3252
|
return match[2];
|
|
3073
3253
|
}
|
|
3074
3254
|
}
|
|
@@ -3609,9 +3789,9 @@ var DNSConfigurator = class {
|
|
|
3609
3789
|
const records = [];
|
|
3610
3790
|
const removed = [];
|
|
3611
3791
|
const existing = await this.cf.listDnsRecords(zoneId);
|
|
3612
|
-
const
|
|
3792
|
+
const normalize2 = (s) => s.replace(/^["']|["']$/g, "");
|
|
3613
3793
|
const findRecords = (type, name, contentPrefix) => existing.filter(
|
|
3614
|
-
(r) => r.type === type && r.name === name && (!contentPrefix ||
|
|
3794
|
+
(r) => r.type === type && r.name === name && (!contentPrefix || normalize2(r.content ?? "").startsWith(contentPrefix))
|
|
3615
3795
|
);
|
|
3616
3796
|
const existingMx = findRecords("MX", domain);
|
|
3617
3797
|
const cfEmailRoutingMx = existingMx.filter((r) => (r.content ?? "").startsWith("_dc-mx."));
|
|
@@ -3635,7 +3815,7 @@ var DNSConfigurator = class {
|
|
|
3635
3815
|
const ipClause = serverIp ? `ip4:${serverIp} ` : "";
|
|
3636
3816
|
const ourSpf = `v=spf1 ${ipClause}include:_spf.mx.cloudflare.net mx ~all`;
|
|
3637
3817
|
const existingSpf = findRecords("TXT", domain, "v=spf1");
|
|
3638
|
-
const alreadyHasOurSpf = existingSpf.some((r) =>
|
|
3818
|
+
const alreadyHasOurSpf = existingSpf.some((r) => normalize2(r.content) === ourSpf);
|
|
3639
3819
|
if (!alreadyHasOurSpf) {
|
|
3640
3820
|
for (const spf of existingSpf) {
|
|
3641
3821
|
await this.cf.deleteDnsRecord(zoneId, spf.id);
|
|
@@ -3666,7 +3846,7 @@ var DNSConfigurator = class {
|
|
|
3666
3846
|
const dkimName = `${options.dkimSelector}._domainkey.${domain}`;
|
|
3667
3847
|
const ourDkim = `v=DKIM1; k=rsa; p=${options.dkimPublicKey}`;
|
|
3668
3848
|
const existingDkim = findRecords("TXT", dkimName, "v=DKIM1");
|
|
3669
|
-
const alreadyCorrect = existingDkim.some((r) =>
|
|
3849
|
+
const alreadyCorrect = existingDkim.some((r) => normalize2(r.content) === ourDkim);
|
|
3670
3850
|
if (!alreadyCorrect) {
|
|
3671
3851
|
for (const rec of existingDkim) {
|
|
3672
3852
|
await this.cf.deleteDnsRecord(zoneId, rec.id);
|
|
@@ -4507,7 +4687,7 @@ var GatewayManager = class {
|
|
|
4507
4687
|
html: mail.html || void 0,
|
|
4508
4688
|
replyTo: mail.from,
|
|
4509
4689
|
inReplyTo: mail.inReplyTo,
|
|
4510
|
-
references: mail.references
|
|
4690
|
+
references: Array.isArray(mail.references) ? mail.references.join(" ") : mail.references,
|
|
4511
4691
|
headers: {
|
|
4512
4692
|
"X-AgenticMail-Relay": "inbound",
|
|
4513
4693
|
"X-Original-From": mail.from,
|
|
@@ -4996,7 +5176,7 @@ var GatewayManager = class {
|
|
|
4996
5176
|
html: mail.html || void 0,
|
|
4997
5177
|
replyTo: mail.replyTo || from,
|
|
4998
5178
|
inReplyTo: mail.inReplyTo || void 0,
|
|
4999
|
-
references: mail.references
|
|
5179
|
+
references: Array.isArray(mail.references) ? mail.references.join(" ") : mail.references || void 0,
|
|
5000
5180
|
headers: {
|
|
5001
5181
|
"X-Mailer": "AgenticMail/1.0"
|
|
5002
5182
|
},
|
|
@@ -6777,6 +6957,7 @@ export {
|
|
|
6777
6957
|
TunnelManager,
|
|
6778
6958
|
WARNING_THRESHOLD,
|
|
6779
6959
|
buildInboundSecurityAdvisory,
|
|
6960
|
+
classifyEmailRoute,
|
|
6780
6961
|
closeDatabase,
|
|
6781
6962
|
createTestDatabase,
|
|
6782
6963
|
debug,
|