@agenticmail/core 0.5.55 → 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 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 15 major components organized into modules:
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,
@@ -2240,6 +2241,178 @@ var AgentDeletionService = class {
2240
2241
  // src/index.ts
2241
2242
  init_spam_filter();
2242
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
+
2243
2416
  // src/mail/sanitizer.ts
2244
2417
  var RE_TAG_BLOCK = /[\u{E0001}-\u{E007F}]/gu;
2245
2418
  var RE_ZERO_WIDTH = /[\u200B\u200C\u200D\uFEFF]/g;
@@ -3543,9 +3716,9 @@ var RelayGateway = class {
3543
3716
  throw new Error("Relay not configured. Call setup() first.");
3544
3717
  }
3545
3718
  const atIdx = this.config.email.lastIndexOf("@");
3546
- const localPart = this.config.email.slice(0, atIdx);
3719
+ const localPart2 = this.config.email.slice(0, atIdx);
3547
3720
  const domain = this.config.email.slice(atIdx + 1);
3548
- const relayFrom = `${localPart}+${agentName}@${domain}`;
3721
+ const relayFrom = `${localPart2}+${agentName}@${domain}`;
3549
3722
  const displayName = mail.fromName || agentName;
3550
3723
  const mailOpts = {
3551
3724
  from: `${displayName} <${relayFrom}>`,
@@ -3557,7 +3730,7 @@ var RelayGateway = class {
3557
3730
  html: mail.html,
3558
3731
  replyTo: relayFrom,
3559
3732
  inReplyTo: mail.inReplyTo,
3560
- references: mail.references?.join(" "),
3733
+ references: Array.isArray(mail.references) ? mail.references.join(" ") : mail.references,
3561
3734
  headers: mail.headers,
3562
3735
  attachments: mail.attachments?.map((a) => ({
3563
3736
  filename: a.filename,
@@ -3781,9 +3954,9 @@ var RelayGateway = class {
3781
3954
  isOurRelaySender(address) {
3782
3955
  if (!this.config) return false;
3783
3956
  const atIdx = this.config.email.lastIndexOf("@");
3784
- const localPart = this.config.email.slice(0, atIdx);
3957
+ const localPart2 = this.config.email.slice(0, atIdx);
3785
3958
  const domain = this.config.email.slice(atIdx + 1);
3786
- const pattern = new RegExp(`^${escapeRegex(localPart)}\\+[^@]+@${escapeRegex(domain)}$`, "i");
3959
+ const pattern = new RegExp(`^${escapeRegex(localPart2)}\\+[^@]+@${escapeRegex(domain)}$`, "i");
3787
3960
  return pattern.test(address);
3788
3961
  }
3789
3962
  /**
@@ -3823,8 +3996,8 @@ var RelayGateway = class {
3823
3996
  const match = addr.match(/^([^+]+)\+([^@]+)@/);
3824
3997
  if (match && this.config) {
3825
3998
  const atIdx = this.config.email.lastIndexOf("@");
3826
- const localPart = this.config.email.slice(0, atIdx);
3827
- if (match[1].toLowerCase() === localPart.toLowerCase()) {
3999
+ const localPart2 = this.config.email.slice(0, atIdx);
4000
+ if (match[1].toLowerCase() === localPart2.toLowerCase()) {
3828
4001
  return match[2];
3829
4002
  }
3830
4003
  }
@@ -4365,9 +4538,9 @@ var DNSConfigurator = class {
4365
4538
  const records = [];
4366
4539
  const removed = [];
4367
4540
  const existing = await this.cf.listDnsRecords(zoneId);
4368
- const normalize = (s) => s.replace(/^["']|["']$/g, "");
4541
+ const normalize2 = (s) => s.replace(/^["']|["']$/g, "");
4369
4542
  const findRecords = (type, name, contentPrefix) => existing.filter(
4370
- (r) => r.type === type && r.name === name && (!contentPrefix || normalize(r.content ?? "").startsWith(contentPrefix))
4543
+ (r) => r.type === type && r.name === name && (!contentPrefix || normalize2(r.content ?? "").startsWith(contentPrefix))
4371
4544
  );
4372
4545
  const existingMx = findRecords("MX", domain);
4373
4546
  const cfEmailRoutingMx = existingMx.filter((r) => (r.content ?? "").startsWith("_dc-mx."));
@@ -4391,7 +4564,7 @@ var DNSConfigurator = class {
4391
4564
  const ipClause = serverIp ? `ip4:${serverIp} ` : "";
4392
4565
  const ourSpf = `v=spf1 ${ipClause}include:_spf.mx.cloudflare.net mx ~all`;
4393
4566
  const existingSpf = findRecords("TXT", domain, "v=spf1");
4394
- const alreadyHasOurSpf = existingSpf.some((r) => normalize(r.content) === ourSpf);
4567
+ const alreadyHasOurSpf = existingSpf.some((r) => normalize2(r.content) === ourSpf);
4395
4568
  if (!alreadyHasOurSpf) {
4396
4569
  for (const spf of existingSpf) {
4397
4570
  await this.cf.deleteDnsRecord(zoneId, spf.id);
@@ -4422,7 +4595,7 @@ var DNSConfigurator = class {
4422
4595
  const dkimName = `${options.dkimSelector}._domainkey.${domain}`;
4423
4596
  const ourDkim = `v=DKIM1; k=rsa; p=${options.dkimPublicKey}`;
4424
4597
  const existingDkim = findRecords("TXT", dkimName, "v=DKIM1");
4425
- const alreadyCorrect = existingDkim.some((r) => normalize(r.content) === ourDkim);
4598
+ const alreadyCorrect = existingDkim.some((r) => normalize2(r.content) === ourDkim);
4426
4599
  if (!alreadyCorrect) {
4427
4600
  for (const rec of existingDkim) {
4428
4601
  await this.cf.deleteDnsRecord(zoneId, rec.id);
@@ -5264,7 +5437,7 @@ var GatewayManager = class {
5264
5437
  html: mail.html || void 0,
5265
5438
  replyTo: mail.from,
5266
5439
  inReplyTo: mail.inReplyTo,
5267
- references: mail.references?.join(" "),
5440
+ references: Array.isArray(mail.references) ? mail.references.join(" ") : mail.references,
5268
5441
  headers: {
5269
5442
  "X-AgenticMail-Relay": "inbound",
5270
5443
  "X-Original-From": mail.from,
@@ -5753,7 +5926,7 @@ var GatewayManager = class {
5753
5926
  html: mail.html || void 0,
5754
5927
  replyTo: mail.replyTo || from,
5755
5928
  inReplyTo: mail.inReplyTo || void 0,
5756
- references: mail.references?.join(" ") || void 0,
5929
+ references: Array.isArray(mail.references) ? mail.references.join(" ") : mail.references || void 0,
5757
5930
  headers: {
5758
5931
  "X-Mailer": "AgenticMail/1.0"
5759
5932
  },
@@ -7535,6 +7708,7 @@ secret = "${password}"
7535
7708
  TunnelManager,
7536
7709
  WARNING_THRESHOLD,
7537
7710
  buildInboundSecurityAdvisory,
7711
+ classifyEmailRoute,
7538
7712
  closeDatabase,
7539
7713
  createTestDatabase,
7540
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
@@ -1484,6 +1484,178 @@ var AgentDeletionService = class {
1484
1484
  }
1485
1485
  };
1486
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
+
1487
1659
  // src/mail/sanitizer.ts
1488
1660
  var RE_TAG_BLOCK = /[\u{E0001}-\u{E007F}]/gu;
1489
1661
  var RE_ZERO_WIDTH = /[\u200B\u200C\u200D\uFEFF]/g;
@@ -2787,9 +2959,9 @@ var RelayGateway = class {
2787
2959
  throw new Error("Relay not configured. Call setup() first.");
2788
2960
  }
2789
2961
  const atIdx = this.config.email.lastIndexOf("@");
2790
- const localPart = this.config.email.slice(0, atIdx);
2962
+ const localPart2 = this.config.email.slice(0, atIdx);
2791
2963
  const domain = this.config.email.slice(atIdx + 1);
2792
- const relayFrom = `${localPart}+${agentName}@${domain}`;
2964
+ const relayFrom = `${localPart2}+${agentName}@${domain}`;
2793
2965
  const displayName = mail.fromName || agentName;
2794
2966
  const mailOpts = {
2795
2967
  from: `${displayName} <${relayFrom}>`,
@@ -2801,7 +2973,7 @@ var RelayGateway = class {
2801
2973
  html: mail.html,
2802
2974
  replyTo: relayFrom,
2803
2975
  inReplyTo: mail.inReplyTo,
2804
- references: mail.references?.join(" "),
2976
+ references: Array.isArray(mail.references) ? mail.references.join(" ") : mail.references,
2805
2977
  headers: mail.headers,
2806
2978
  attachments: mail.attachments?.map((a) => ({
2807
2979
  filename: a.filename,
@@ -3025,9 +3197,9 @@ var RelayGateway = class {
3025
3197
  isOurRelaySender(address) {
3026
3198
  if (!this.config) return false;
3027
3199
  const atIdx = this.config.email.lastIndexOf("@");
3028
- const localPart = this.config.email.slice(0, atIdx);
3200
+ const localPart2 = this.config.email.slice(0, atIdx);
3029
3201
  const domain = this.config.email.slice(atIdx + 1);
3030
- const pattern = new RegExp(`^${escapeRegex(localPart)}\\+[^@]+@${escapeRegex(domain)}$`, "i");
3202
+ const pattern = new RegExp(`^${escapeRegex(localPart2)}\\+[^@]+@${escapeRegex(domain)}$`, "i");
3031
3203
  return pattern.test(address);
3032
3204
  }
3033
3205
  /**
@@ -3067,8 +3239,8 @@ var RelayGateway = class {
3067
3239
  const match = addr.match(/^([^+]+)\+([^@]+)@/);
3068
3240
  if (match && this.config) {
3069
3241
  const atIdx = this.config.email.lastIndexOf("@");
3070
- const localPart = this.config.email.slice(0, atIdx);
3071
- if (match[1].toLowerCase() === localPart.toLowerCase()) {
3242
+ const localPart2 = this.config.email.slice(0, atIdx);
3243
+ if (match[1].toLowerCase() === localPart2.toLowerCase()) {
3072
3244
  return match[2];
3073
3245
  }
3074
3246
  }
@@ -3609,9 +3781,9 @@ var DNSConfigurator = class {
3609
3781
  const records = [];
3610
3782
  const removed = [];
3611
3783
  const existing = await this.cf.listDnsRecords(zoneId);
3612
- const normalize = (s) => s.replace(/^["']|["']$/g, "");
3784
+ const normalize2 = (s) => s.replace(/^["']|["']$/g, "");
3613
3785
  const findRecords = (type, name, contentPrefix) => existing.filter(
3614
- (r) => r.type === type && r.name === name && (!contentPrefix || normalize(r.content ?? "").startsWith(contentPrefix))
3786
+ (r) => r.type === type && r.name === name && (!contentPrefix || normalize2(r.content ?? "").startsWith(contentPrefix))
3615
3787
  );
3616
3788
  const existingMx = findRecords("MX", domain);
3617
3789
  const cfEmailRoutingMx = existingMx.filter((r) => (r.content ?? "").startsWith("_dc-mx."));
@@ -3635,7 +3807,7 @@ var DNSConfigurator = class {
3635
3807
  const ipClause = serverIp ? `ip4:${serverIp} ` : "";
3636
3808
  const ourSpf = `v=spf1 ${ipClause}include:_spf.mx.cloudflare.net mx ~all`;
3637
3809
  const existingSpf = findRecords("TXT", domain, "v=spf1");
3638
- const alreadyHasOurSpf = existingSpf.some((r) => normalize(r.content) === ourSpf);
3810
+ const alreadyHasOurSpf = existingSpf.some((r) => normalize2(r.content) === ourSpf);
3639
3811
  if (!alreadyHasOurSpf) {
3640
3812
  for (const spf of existingSpf) {
3641
3813
  await this.cf.deleteDnsRecord(zoneId, spf.id);
@@ -3666,7 +3838,7 @@ var DNSConfigurator = class {
3666
3838
  const dkimName = `${options.dkimSelector}._domainkey.${domain}`;
3667
3839
  const ourDkim = `v=DKIM1; k=rsa; p=${options.dkimPublicKey}`;
3668
3840
  const existingDkim = findRecords("TXT", dkimName, "v=DKIM1");
3669
- const alreadyCorrect = existingDkim.some((r) => normalize(r.content) === ourDkim);
3841
+ const alreadyCorrect = existingDkim.some((r) => normalize2(r.content) === ourDkim);
3670
3842
  if (!alreadyCorrect) {
3671
3843
  for (const rec of existingDkim) {
3672
3844
  await this.cf.deleteDnsRecord(zoneId, rec.id);
@@ -4507,7 +4679,7 @@ var GatewayManager = class {
4507
4679
  html: mail.html || void 0,
4508
4680
  replyTo: mail.from,
4509
4681
  inReplyTo: mail.inReplyTo,
4510
- references: mail.references?.join(" "),
4682
+ references: Array.isArray(mail.references) ? mail.references.join(" ") : mail.references,
4511
4683
  headers: {
4512
4684
  "X-AgenticMail-Relay": "inbound",
4513
4685
  "X-Original-From": mail.from,
@@ -4996,7 +5168,7 @@ var GatewayManager = class {
4996
5168
  html: mail.html || void 0,
4997
5169
  replyTo: mail.replyTo || from,
4998
5170
  inReplyTo: mail.inReplyTo || void 0,
4999
- references: mail.references?.join(" ") || void 0,
5171
+ references: Array.isArray(mail.references) ? mail.references.join(" ") : mail.references || void 0,
5000
5172
  headers: {
5001
5173
  "X-Mailer": "AgenticMail/1.0"
5002
5174
  },
@@ -6777,6 +6949,7 @@ export {
6777
6949
  TunnelManager,
6778
6950
  WARNING_THRESHOLD,
6779
6951
  buildInboundSecurityAdvisory,
6952
+ classifyEmailRoute,
6780
6953
  closeDatabase,
6781
6954
  createTestDatabase,
6782
6955
  debug,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agenticmail/core",
3
- "version": "0.5.55",
3
+ "version": "0.5.56",
4
4
  "description": "Core SDK for AgenticMail — email, SMS, and phone number access for AI agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",