@agenticmail/core 0.5.52 → 0.5.56

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