@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 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,
@@ -1237,7 +1238,8 @@ var InboxWatcher = class extends import_node_events.EventEmitter {
1237
1238
  this.emit("close");
1238
1239
  this._scheduleReconnect();
1239
1240
  });
1240
- this._lock = lock;
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 localPart = this.config.email.slice(0, atIdx);
3727
+ const localPart2 = this.config.email.slice(0, atIdx);
3547
3728
  const domain = this.config.email.slice(atIdx + 1);
3548
- const relayFrom = `${localPart}+${agentName}@${domain}`;
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?.join(" "),
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 localPart = this.config.email.slice(0, atIdx);
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(localPart)}\\+[^@]+@${escapeRegex(domain)}$`, "i");
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 localPart = this.config.email.slice(0, atIdx);
3827
- if (match[1].toLowerCase() === localPart.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 normalize = (s) => s.replace(/^["']|["']$/g, "");
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 || normalize(r.content ?? "").startsWith(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) => normalize(r.content) === ourSpf);
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) => normalize(r.content) === ourDkim);
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?.join(" "),
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?.join(" ") || void 0,
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
- this._lock = lock;
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 localPart = this.config.email.slice(0, atIdx);
2970
+ const localPart2 = this.config.email.slice(0, atIdx);
2791
2971
  const domain = this.config.email.slice(atIdx + 1);
2792
- const relayFrom = `${localPart}+${agentName}@${domain}`;
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?.join(" "),
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 localPart = this.config.email.slice(0, atIdx);
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(localPart)}\\+[^@]+@${escapeRegex(domain)}$`, "i");
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 localPart = this.config.email.slice(0, atIdx);
3071
- if (match[1].toLowerCase() === localPart.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 normalize = (s) => s.replace(/^["']|["']$/g, "");
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 || normalize(r.content ?? "").startsWith(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) => normalize(r.content) === ourSpf);
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) => normalize(r.content) === ourDkim);
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?.join(" "),
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?.join(" ") || void 0,
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agenticmail/core",
3
- "version": "0.5.55",
3
+ "version": "0.5.58",
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",