@agenticmail/core 0.5.43 → 0.5.45

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/dist/index.js CHANGED
@@ -1,3 +1,9 @@
1
+ import {
2
+ SPAM_THRESHOLD,
3
+ WARNING_THRESHOLD,
4
+ isInternalEmail,
5
+ scoreEmail
6
+ } from "./chunk-TIAKW5DC.js";
1
7
  import {
2
8
  __require
3
9
  } from "./chunk-3RG5ZIWI.js";
@@ -54,15 +60,28 @@ var MailSender = class {
54
60
  };
55
61
  const composer = new MailComposer(mailOpts);
56
62
  const raw = await composer.compile().build();
57
- const result = await this.transporter.sendMail(mailOpts);
58
- return {
59
- messageId: result.messageId,
60
- envelope: {
61
- from: result.envelope.from || "",
62
- to: Array.isArray(result.envelope.to) ? result.envelope.to : result.envelope.to ? [result.envelope.to] : []
63
- },
64
- raw
65
- };
63
+ const MAX_RETRIES = 2;
64
+ let lastError = null;
65
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
66
+ try {
67
+ const result = await this.transporter.sendMail(mailOpts);
68
+ return {
69
+ messageId: result.messageId,
70
+ envelope: {
71
+ from: result.envelope.from || "",
72
+ to: Array.isArray(result.envelope.to) ? result.envelope.to : result.envelope.to ? [result.envelope.to] : []
73
+ },
74
+ raw
75
+ };
76
+ } catch (err) {
77
+ lastError = err;
78
+ const code = err?.responseCode ?? err?.code;
79
+ const isTransient = typeof code === "number" && code >= 400 && code < 500 || code === "ECONNRESET" || code === "ETIMEDOUT" || code === "ESOCKET";
80
+ if (!isTransient || attempt === MAX_RETRIES) throw err;
81
+ await new Promise((resolve) => setTimeout(resolve, 1e3 * (attempt + 1)));
82
+ }
83
+ }
84
+ throw lastError;
66
85
  }
67
86
  async verify() {
68
87
  try {
@@ -145,10 +164,13 @@ var MailReceiver = class {
145
164
  const limit = Math.min(Math.max(options?.limit ?? 20, 1), 1e3);
146
165
  const offset = Math.max(options?.offset ?? 0, 0);
147
166
  if (offset >= total) return envelopes;
148
- const end = Math.max(total - offset, 1);
149
- const start = Math.max(end - limit + 1, 1);
150
- const range = `${start}:${end}`;
151
- for await (const msg of this.client.fetch(range, {
167
+ const allUids = await this.client.search({ all: true }, { uid: true });
168
+ if (!allUids || allUids.length === 0) return envelopes;
169
+ const sortedUids = Array.from(allUids).sort((a, b) => b - a);
170
+ const pageUids = sortedUids.slice(offset, offset + limit);
171
+ if (pageUids.length === 0) return envelopes;
172
+ const uidRange = pageUids.join(",");
173
+ for await (const msg of this.client.fetch(uidRange, {
152
174
  uid: true,
153
175
  envelope: true,
154
176
  flags: true,
@@ -174,7 +196,8 @@ var MailReceiver = class {
174
196
  size: msg.size ?? 0
175
197
  });
176
198
  }
177
- return envelopes.reverse();
199
+ envelopes.sort((a, b) => b.uid - a.uid);
200
+ return envelopes;
178
201
  } finally {
179
202
  lock.release();
180
203
  }
@@ -362,12 +385,17 @@ async function parseEmail(raw) {
362
385
  }
363
386
 
364
387
  // src/inbox/watcher.ts
388
+ var RECONNECT_INITIAL_MS = 2e3;
389
+ var RECONNECT_MAX_MS = 6e4;
390
+ var RECONNECT_FACTOR = 2;
365
391
  var InboxWatcher = class extends EventEmitter {
366
392
  constructor(options, watcherOptions) {
367
393
  super();
368
394
  this.options = options;
369
395
  this.mailbox = watcherOptions?.mailbox ?? "INBOX";
370
396
  this.autoFetch = watcherOptions?.autoFetch ?? true;
397
+ this._autoReconnect = options.autoReconnect ?? false;
398
+ this._maxReconnectAttempts = options.maxReconnectAttempts ?? Infinity;
371
399
  this.client = new ImapFlow2({
372
400
  host: options.host,
373
401
  port: options.port,
@@ -387,8 +415,15 @@ var InboxWatcher = class extends EventEmitter {
387
415
  mailbox;
388
416
  autoFetch;
389
417
  _lock = null;
418
+ _stopped = false;
419
+ _reconnectTimer = null;
420
+ _reconnectDelay = RECONNECT_INITIAL_MS;
421
+ _reconnectAttempts = 0;
422
+ _maxReconnectAttempts;
423
+ _autoReconnect;
390
424
  async start() {
391
425
  if (this.watching) return;
426
+ this._stopped = false;
392
427
  this.client = new ImapFlow2({
393
428
  host: this.options.host,
394
429
  port: this.options.port,
@@ -406,6 +441,8 @@ var InboxWatcher = class extends EventEmitter {
406
441
  const lock = await this.client.getMailboxLock(this.mailbox);
407
442
  try {
408
443
  this.watching = true;
444
+ this._reconnectDelay = RECONNECT_INITIAL_MS;
445
+ this._reconnectAttempts = 0;
409
446
  this.client.on("exists", async (data) => {
410
447
  try {
411
448
  if (data.count > data.prevCount) {
@@ -443,6 +480,7 @@ var InboxWatcher = class extends EventEmitter {
443
480
  this.client.on("close", () => {
444
481
  this.watching = false;
445
482
  this.emit("close");
483
+ this._scheduleReconnect();
446
484
  });
447
485
  this._lock = lock;
448
486
  } catch (err) {
@@ -450,9 +488,44 @@ var InboxWatcher = class extends EventEmitter {
450
488
  throw err;
451
489
  }
452
490
  }
491
+ /** Schedule a reconnect attempt with exponential backoff */
492
+ _scheduleReconnect() {
493
+ if (this._stopped || !this._autoReconnect) return;
494
+ if (this._reconnectAttempts >= this._maxReconnectAttempts) {
495
+ this.emit("reconnect_failed", { attempts: this._reconnectAttempts });
496
+ return;
497
+ }
498
+ const delay = this._reconnectDelay;
499
+ this._reconnectDelay = Math.min(this._reconnectDelay * RECONNECT_FACTOR, RECONNECT_MAX_MS);
500
+ this._reconnectAttempts++;
501
+ this.emit("reconnecting", { attempt: this._reconnectAttempts, delayMs: delay });
502
+ this._reconnectTimer = setTimeout(async () => {
503
+ if (this._stopped) return;
504
+ try {
505
+ this.client.removeAllListeners();
506
+ if (this._lock) {
507
+ try {
508
+ this._lock.release();
509
+ } catch {
510
+ }
511
+ this._lock = null;
512
+ }
513
+ await this.start();
514
+ this.emit("reconnected", { attempt: this._reconnectAttempts });
515
+ } catch (err) {
516
+ this.emit("error", err);
517
+ this._scheduleReconnect();
518
+ }
519
+ }, delay);
520
+ }
453
521
  async stop() {
454
- if (!this.watching) return;
522
+ if (!this.watching && !this._reconnectTimer) return;
523
+ this._stopped = true;
455
524
  this.watching = false;
525
+ if (this._reconnectTimer) {
526
+ clearTimeout(this._reconnectTimer);
527
+ this._reconnectTimer = null;
528
+ }
456
529
  this.client.removeAllListeners();
457
530
  if (this._lock) {
458
531
  try {
@@ -1063,7 +1136,7 @@ var AccountManager = class {
1063
1136
  const apiKey = generateApiKey();
1064
1137
  const password = options.password ?? generatePassword();
1065
1138
  const domain = options.domain ?? "localhost";
1066
- if (domain !== "localhost" && !/^[a-zA-Z0-9][a-zA-Z0-9.-]*[a-zA-Z]{2,}$/.test(domain)) {
1139
+ if (domain !== "localhost" && !/^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/.test(domain)) {
1067
1140
  throw new Error(`Invalid domain "${domain}": must be a valid domain name`);
1068
1141
  }
1069
1142
  const principalName = options.name.toLowerCase();
@@ -1280,31 +1353,39 @@ var AgentDeletionService = class {
1280
1353
  }
1281
1354
  async archiveFolder(receiver, folder) {
1282
1355
  const archived = [];
1356
+ const PAGE_SIZE = 100;
1283
1357
  try {
1284
- const envelopes = await receiver.listEnvelopes(folder, { limit: 1e4 });
1285
- for (const env of envelopes) {
1286
- try {
1287
- const raw = await receiver.fetchMessage(env.uid, folder);
1288
- const parsed = await parseEmail(raw);
1289
- archived.push({
1290
- uid: env.uid,
1291
- messageId: parsed.messageId || env.messageId,
1292
- from: parsed.from?.[0]?.address ?? "",
1293
- to: parsed.to?.map((a) => a.address) ?? [],
1294
- subject: parsed.subject || env.subject,
1295
- date: parsed.date?.toISOString() ?? env.date?.toISOString?.() ?? "",
1296
- text: parsed.text,
1297
- html: parsed.html
1298
- });
1299
- } catch {
1300
- archived.push({
1301
- uid: env.uid,
1302
- messageId: env.messageId,
1303
- from: env.from?.[0]?.address ?? "",
1304
- to: env.to?.map((a) => a.address) ?? [],
1305
- subject: env.subject,
1306
- date: env.date?.toISOString?.() ?? ""
1307
- });
1358
+ let offset = 0;
1359
+ let hasMore = true;
1360
+ while (hasMore) {
1361
+ const envelopes = await receiver.listEnvelopes(folder, { limit: PAGE_SIZE, offset });
1362
+ if (envelopes.length === 0) break;
1363
+ hasMore = envelopes.length === PAGE_SIZE;
1364
+ offset += envelopes.length;
1365
+ for (const env of envelopes) {
1366
+ try {
1367
+ const raw = await receiver.fetchMessage(env.uid, folder);
1368
+ const parsed = await parseEmail(raw);
1369
+ archived.push({
1370
+ uid: env.uid,
1371
+ messageId: parsed.messageId || env.messageId,
1372
+ from: parsed.from?.[0]?.address ?? "",
1373
+ to: parsed.to?.map((a) => a.address) ?? [],
1374
+ subject: parsed.subject || env.subject,
1375
+ date: parsed.date?.toISOString() ?? env.date?.toISOString?.() ?? "",
1376
+ text: parsed.text,
1377
+ html: parsed.html
1378
+ });
1379
+ } catch {
1380
+ archived.push({
1381
+ uid: env.uid,
1382
+ messageId: env.messageId,
1383
+ from: env.from?.[0]?.address ?? "",
1384
+ to: env.to?.map((a) => a.address) ?? [],
1385
+ subject: env.subject,
1386
+ date: env.date?.toISOString?.() ?? ""
1387
+ });
1388
+ }
1308
1389
  }
1309
1390
  }
1310
1391
  } catch (err) {
@@ -1381,623 +1462,6 @@ var AgentDeletionService = class {
1381
1462
  }
1382
1463
  };
1383
1464
 
1384
- // src/mail/spam-filter.ts
1385
- var SPAM_THRESHOLD = 40;
1386
- var WARNING_THRESHOLD = 20;
1387
- function isInternalEmail(email, localDomains) {
1388
- const fromDomain = email.from[0]?.address?.split("@")[1]?.toLowerCase();
1389
- if (!fromDomain) return false;
1390
- const internals = /* @__PURE__ */ new Set(["localhost", ...(localDomains ?? []).map((d) => d.toLowerCase())]);
1391
- if (internals.has(fromDomain) && email.replyTo?.length) {
1392
- const replyDomain = email.replyTo[0]?.address?.split("@")[1]?.toLowerCase();
1393
- if (replyDomain && !internals.has(replyDomain)) return false;
1394
- }
1395
- return internals.has(fromDomain);
1396
- }
1397
- var RE_IGNORE_INSTRUCTIONS = /ignore\s+(all\s+)?(previous|prior|above)\s+(instructions|prompts|rules)/i;
1398
- var RE_YOU_ARE_NOW = /you\s+are\s+now\s+(a|an|the|my)\b/i;
1399
- var RE_SYSTEM_DELIMITER = /\[SYSTEM\]|\[INST\]|<<SYS>>|<\|im_start\|>/i;
1400
- var RE_NEW_INSTRUCTIONS = /new\s+instructions?:|override\s+instructions?:/i;
1401
- var RE_ACT_AS = /act\s+as\s+(a|an|if)|pretend\s+(to be|you\s+are)/i;
1402
- var RE_DO_NOT_MENTION = /do\s+not\s+(mention|tell|reveal|disclose)\s+(that|this)/i;
1403
- var RE_TAG_CHARS = /[\u{E0001}-\u{E007F}]/u;
1404
- var RE_DENSE_ZWC = /[\u200B\u200C\u200D\uFEFF]{3,}/;
1405
- var RE_JAILBREAK = /\b(DAN|jailbreak|bypass\s+(safety|filter|restriction)|unlimited\s+mode)\b/i;
1406
- var RE_BASE64_BLOCK = /[A-Za-z0-9+/]{100,}={0,2}/;
1407
- var RE_MARKDOWN_INJECTION = /```(?:system|python\s+exec|bash\s+exec)/i;
1408
- var RE_OWNER_IMPERSONATION = /your\s+(owner|creator|admin|boss|master|human)\s+(asked|told|wants|said|instructed|needs)/i;
1409
- var RE_SECRET_REQUEST = /share\s+(your|the)\s+(api.?key|password|secret|credential|token)/i;
1410
- var RE_IMPERSONATE_SYSTEM = /this\s+is\s+(a|an)\s+(system|security|admin|automated)\s+(message|alert|notification)/i;
1411
- var RE_URGENCY = /\b(urgent|immediately|right now|asap|deadline|expires?|last chance|act now|time.?sensitive)\b/i;
1412
- var RE_AUTHORITY = /\b(suspend|terminate|deactivat|unauthori[zs]|locked|compromised|breach|violation|legal action)\b/i;
1413
- var RE_MONEY_REQUEST = /send\s+(me|us)\s+\$?\d|wire\s+transfer|western\s+union|money\s*gram/i;
1414
- var RE_GIFT_CARD = /buy\s+(me\s+)?gift\s*cards?|itunes\s+cards?|google\s+play\s+cards?/i;
1415
- var RE_CEO_FRAUD = /\b(CEO|CFO|CTO|director|executive)\b.*\b(wire|transfer|payment|urgent)\b/i;
1416
- var RE_FORWARD_ALL = /forward\s+(all|every)\s+(email|message)/i;
1417
- var RE_SEARCH_CREDS = /search\s+(inbox|email|mailbox).*password|find.*credential/i;
1418
- var RE_SEND_TO_EXTERNAL = /send\s+(the|all|every).*to\s+\S+@\S+/i;
1419
- var RE_DUMP_INSTRUCTIONS = /reveal.*system\s+prompt|dump.*instructions|show.*system\s+prompt|print.*instructions/i;
1420
- var RE_WEBHOOK_EXFIL = /https?:\/\/[^/]*(webhook|ngrok|pipedream|requestbin|hookbin)/i;
1421
- var RE_CREDENTIAL_HARVEST = /verify\s+your\s+(account|identity|password|credentials?)/i;
1422
- var RE_LINK_TAG = /<a\s[^>]*href\s*=\s*["']([^"']+)["']/gi;
1423
- var RE_LINK_TAG_WITH_TEXT = /<a\s[^>]*href\s*=\s*["']([^"']+)["'][^>]*>(.*?)<\/a>/gi;
1424
- var RE_URL_IN_TEXT = /https?:\/\/[^\s<>"]+/gi;
1425
- var RE_IP_URL = /https?:\/\/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/i;
1426
- var RE_URL_SHORTENER = /https?:\/\/(bit\.ly|t\.co|tinyurl\.com|goo\.gl|ow\.ly|is\.gd|buff\.ly|rebrand\.ly|shorturl\.at)\//i;
1427
- var RE_DATA_URI = /(?:data:text\/html|javascript:)/i;
1428
- var RE_LOGIN_URGENCY = /(click\s+here|sign\s+in|log\s*in).*\b(urgent|immediately|expire|suspend|locked)/i;
1429
- var RE_PHARMACY_SPAM = /\b(viagra|cialis|pharmacy|prescription|cheap\s+meds|online\s+pharmacy)\b/i;
1430
- var RE_WEIGHT_LOSS = /\b(weight\s+loss|diet\s+pill|lose\s+\d+\s+(lbs?|pounds|kg)|fat\s+burn)\b/i;
1431
- var RE_LOTTERY_SCAM = /you\s+(have\s+)?(won|been\s+selected)|lottery|million\s+dollars|nigerian?\s+prince/i;
1432
- var RE_CRYPTO_SCAM = /(bitcoin|crypto|ethereum).*invest(ment)?|guaranteed\s+returns|double\s+your\s+(money|bitcoin|crypto)/i;
1433
- var RE_EXECUTABLE_EXT = /\.(exe|bat|cmd|ps1|sh|dll|scr|vbs|js|msi|com)$/i;
1434
- var RE_DOUBLE_EXT = /\.\w{2,5}\.(exe|bat|cmd|ps1|sh|dll|scr|vbs|js|msi|com)$/i;
1435
- var RE_ARCHIVE_EXT = /\.(zip|rar|7z|tar\.gz|tgz)$/i;
1436
- var RE_HTML_ATTACHMENT_EXT = /\.(html?|svg)$/i;
1437
- var BRAND_DOMAINS = {
1438
- google: ["google.com", "gmail.com", "googlemail.com"],
1439
- microsoft: ["microsoft.com", "outlook.com", "hotmail.com", "live.com"],
1440
- apple: ["apple.com", "icloud.com"],
1441
- amazon: ["amazon.com", "amazon.co.uk", "amazon.de"],
1442
- paypal: ["paypal.com"],
1443
- meta: ["facebook.com", "meta.com", "instagram.com"],
1444
- netflix: ["netflix.com"],
1445
- bank: ["chase.com", "wellsfargo.com", "bankofamerica.com", "citibank.com"]
1446
- };
1447
- var SPAM_WORDS = [
1448
- "congratulations",
1449
- "winner",
1450
- "prize",
1451
- "claim",
1452
- "free",
1453
- "offer",
1454
- "limited time",
1455
- "act now",
1456
- "click here",
1457
- "no obligation",
1458
- "risk free",
1459
- "guaranteed",
1460
- "million",
1461
- "billion",
1462
- "inheritance",
1463
- "beneficiary",
1464
- "wire transfer",
1465
- "western union",
1466
- "dear friend",
1467
- "dear sir",
1468
- "kindly",
1469
- "revert back",
1470
- "do the needful",
1471
- "humbly",
1472
- "esteemed",
1473
- "investment opportunity",
1474
- "double your",
1475
- "earn money",
1476
- "work from home",
1477
- "make money",
1478
- "cash bonus",
1479
- "discount",
1480
- "lowest price"
1481
- ];
1482
- function countSpamWords(text) {
1483
- const lower = text.toLowerCase();
1484
- let count = 0;
1485
- for (const word of SPAM_WORDS) {
1486
- if (lower.includes(word)) count++;
1487
- }
1488
- return count;
1489
- }
1490
- function hasHomographChars(domain) {
1491
- if (domain.startsWith("xn--")) return true;
1492
- const hasCyrillic = /[\u0400-\u04FF]/.test(domain);
1493
- const hasLatin = /[a-zA-Z]/.test(domain);
1494
- return hasCyrillic && hasLatin;
1495
- }
1496
- var RULES = [
1497
- // === Prompt injection ===
1498
- {
1499
- id: "pi_ignore_instructions",
1500
- category: "prompt_injection",
1501
- score: 25,
1502
- description: 'Contains "ignore previous instructions" pattern',
1503
- test: (_e, text) => RE_IGNORE_INSTRUCTIONS.test(text)
1504
- },
1505
- {
1506
- id: "pi_you_are_now",
1507
- category: "prompt_injection",
1508
- score: 25,
1509
- description: 'Contains "you are now a..." roleplay injection',
1510
- test: (_e, text) => RE_YOU_ARE_NOW.test(text)
1511
- },
1512
- {
1513
- id: "pi_system_delimiter",
1514
- category: "prompt_injection",
1515
- score: 20,
1516
- description: "Contains LLM system delimiters ([SYSTEM], [INST], etc.)",
1517
- test: (_e, text, html) => RE_SYSTEM_DELIMITER.test(text) || RE_SYSTEM_DELIMITER.test(html)
1518
- },
1519
- {
1520
- id: "pi_new_instructions",
1521
- category: "prompt_injection",
1522
- score: 20,
1523
- description: 'Contains "new instructions:" or "override instructions:"',
1524
- test: (_e, text) => RE_NEW_INSTRUCTIONS.test(text)
1525
- },
1526
- {
1527
- id: "pi_act_as",
1528
- category: "prompt_injection",
1529
- score: 15,
1530
- description: 'Contains "act as" or "pretend to be" injection',
1531
- test: (_e, text) => RE_ACT_AS.test(text)
1532
- },
1533
- {
1534
- id: "pi_do_not_mention",
1535
- category: "prompt_injection",
1536
- score: 15,
1537
- description: 'Contains "do not mention/tell/reveal" suppression',
1538
- test: (_e, text) => RE_DO_NOT_MENTION.test(text)
1539
- },
1540
- {
1541
- id: "pi_invisible_unicode",
1542
- category: "prompt_injection",
1543
- score: 20,
1544
- description: "Contains invisible Unicode tag characters or dense zero-width chars",
1545
- test: (_e, text, html) => RE_TAG_CHARS.test(text) || RE_TAG_CHARS.test(html) || RE_DENSE_ZWC.test(text) || RE_DENSE_ZWC.test(html)
1546
- },
1547
- {
1548
- id: "pi_jailbreak",
1549
- category: "prompt_injection",
1550
- score: 20,
1551
- description: "Contains jailbreak/DAN/bypass safety language",
1552
- test: (_e, text) => RE_JAILBREAK.test(text)
1553
- },
1554
- {
1555
- id: "pi_base64_injection",
1556
- category: "prompt_injection",
1557
- score: 15,
1558
- description: "Contains long base64-encoded blocks (potential hidden instructions)",
1559
- test: (_e, text) => RE_BASE64_BLOCK.test(text)
1560
- },
1561
- {
1562
- id: "pi_markdown_injection",
1563
- category: "prompt_injection",
1564
- score: 10,
1565
- description: "Contains code block injection attempts (```system, ```python exec)",
1566
- test: (_e, text) => RE_MARKDOWN_INJECTION.test(text)
1567
- },
1568
- // === Social engineering ===
1569
- {
1570
- id: "se_owner_impersonation",
1571
- category: "social_engineering",
1572
- score: 20,
1573
- description: "Claims to speak on behalf of the agent's owner",
1574
- test: (_e, text) => RE_OWNER_IMPERSONATION.test(text)
1575
- },
1576
- {
1577
- id: "se_secret_request",
1578
- category: "social_engineering",
1579
- score: 15,
1580
- description: "Requests API keys, passwords, or credentials",
1581
- test: (_e, text) => RE_SECRET_REQUEST.test(text)
1582
- },
1583
- {
1584
- id: "se_impersonate_system",
1585
- category: "social_engineering",
1586
- score: 15,
1587
- description: "Impersonates a system/security message",
1588
- test: (_e, text) => RE_IMPERSONATE_SYSTEM.test(text)
1589
- },
1590
- {
1591
- id: "se_urgency_authority",
1592
- category: "social_engineering",
1593
- score: 10,
1594
- description: "Combines urgency language with authority/threat language",
1595
- test: (_e, text) => RE_URGENCY.test(text) && RE_AUTHORITY.test(text)
1596
- },
1597
- {
1598
- id: "se_money_request",
1599
- category: "social_engineering",
1600
- score: 15,
1601
- description: "Requests money transfer or wire",
1602
- test: (_e, text) => RE_MONEY_REQUEST.test(text)
1603
- },
1604
- {
1605
- id: "se_gift_card",
1606
- category: "social_engineering",
1607
- score: 20,
1608
- description: "Requests purchase of gift cards",
1609
- test: (_e, text) => RE_GIFT_CARD.test(text)
1610
- },
1611
- {
1612
- id: "se_ceo_fraud",
1613
- category: "social_engineering",
1614
- score: 15,
1615
- description: "BEC pattern: executive title + payment/wire/urgent",
1616
- test: (_e, text) => RE_CEO_FRAUD.test(text)
1617
- },
1618
- // === Data exfiltration ===
1619
- {
1620
- id: "de_forward_all",
1621
- category: "data_exfiltration",
1622
- score: 20,
1623
- description: "Requests forwarding all emails",
1624
- test: (_e, text) => RE_FORWARD_ALL.test(text)
1625
- },
1626
- {
1627
- id: "de_search_credentials",
1628
- category: "data_exfiltration",
1629
- score: 20,
1630
- description: "Requests searching inbox for passwords/credentials",
1631
- test: (_e, text) => RE_SEARCH_CREDS.test(text)
1632
- },
1633
- {
1634
- id: "de_send_to_external",
1635
- category: "data_exfiltration",
1636
- score: 15,
1637
- description: "Instructs sending data to an external email address",
1638
- test: (_e, text) => RE_SEND_TO_EXTERNAL.test(text)
1639
- },
1640
- {
1641
- id: "de_dump_instructions",
1642
- category: "data_exfiltration",
1643
- score: 15,
1644
- description: "Attempts to extract system prompt or instructions",
1645
- test: (_e, text) => RE_DUMP_INSTRUCTIONS.test(text)
1646
- },
1647
- {
1648
- id: "de_webhook_exfil",
1649
- category: "data_exfiltration",
1650
- score: 15,
1651
- description: "Contains webhook/ngrok/pipedream exfiltration URLs",
1652
- test: (_e, text) => RE_WEBHOOK_EXFIL.test(text)
1653
- },
1654
- // === Phishing ===
1655
- {
1656
- id: "ph_spoofed_sender",
1657
- category: "phishing",
1658
- score: 10,
1659
- description: "Sender name contains brand but domain doesn't match",
1660
- test: (email) => {
1661
- const from = email.from[0];
1662
- if (!from) return false;
1663
- const name = (from.name ?? "").toLowerCase();
1664
- const domain = (from.address ?? "").split("@")[1]?.toLowerCase() ?? "";
1665
- for (const [brand, domains] of Object.entries(BRAND_DOMAINS)) {
1666
- if (name.includes(brand) && !domains.some((d) => domain === d || domain.endsWith("." + d))) {
1667
- return true;
1668
- }
1669
- }
1670
- return false;
1671
- }
1672
- },
1673
- {
1674
- id: "ph_credential_harvest",
1675
- category: "phishing",
1676
- score: 15,
1677
- description: 'Asks to "verify your account/password" with links present',
1678
- test: (_e, text, html) => {
1679
- if (!RE_CREDENTIAL_HARVEST.test(text)) return false;
1680
- return RE_URL_IN_TEXT.test(text) || RE_LINK_TAG.test(html);
1681
- }
1682
- },
1683
- {
1684
- id: "ph_suspicious_links",
1685
- category: "phishing",
1686
- score: 10,
1687
- description: "Contains links with IP addresses, URL shorteners, or excessive subdomains",
1688
- test: (_e, text, html) => {
1689
- const allText = text + " " + html;
1690
- if (RE_IP_URL.test(allText)) return true;
1691
- if (RE_URL_SHORTENER.test(allText)) return true;
1692
- const urls = allText.match(RE_URL_IN_TEXT) ?? [];
1693
- for (const url of urls) {
1694
- try {
1695
- const hostname = new URL(url).hostname;
1696
- if (hostname.split(".").length > 4) return true;
1697
- } catch {
1698
- }
1699
- }
1700
- return false;
1701
- }
1702
- },
1703
- {
1704
- id: "ph_data_uri",
1705
- category: "phishing",
1706
- score: 15,
1707
- description: "Contains data: or javascript: URIs in links",
1708
- test: (_e, _text, html) => {
1709
- RE_LINK_TAG.lastIndex = 0;
1710
- let match;
1711
- while ((match = RE_LINK_TAG.exec(html)) !== null) {
1712
- if (RE_DATA_URI.test(match[1])) return true;
1713
- }
1714
- return false;
1715
- }
1716
- },
1717
- {
1718
- id: "ph_homograph",
1719
- category: "phishing",
1720
- score: 15,
1721
- description: "From domain contains mixed-script or punycode characters",
1722
- test: (email) => {
1723
- const domain = email.from[0]?.address?.split("@")[1] ?? "";
1724
- if (!domain) return false;
1725
- return hasHomographChars(domain);
1726
- }
1727
- },
1728
- {
1729
- id: "ph_mismatched_display_url",
1730
- category: "phishing",
1731
- score: 10,
1732
- description: "HTML link text shows one URL but href points to a different domain",
1733
- test: (_e, _text, html) => {
1734
- RE_LINK_TAG_WITH_TEXT.lastIndex = 0;
1735
- let match;
1736
- while ((match = RE_LINK_TAG_WITH_TEXT.exec(html)) !== null) {
1737
- const href = match[1];
1738
- const linkText = match[2].replace(/<[^>]*>/g, "").trim();
1739
- if (!/^https?:\/\//i.test(linkText)) continue;
1740
- try {
1741
- const hrefHost = new URL(href).hostname.replace(/^www\./, "");
1742
- const textHost = new URL(linkText).hostname.replace(/^www\./, "");
1743
- if (hrefHost !== textHost) return true;
1744
- } catch {
1745
- }
1746
- }
1747
- return false;
1748
- }
1749
- },
1750
- {
1751
- id: "ph_login_urgency",
1752
- category: "phishing",
1753
- score: 10,
1754
- description: "Combines login/click-here language with urgency",
1755
- test: (_e, text) => RE_LOGIN_URGENCY.test(text)
1756
- },
1757
- {
1758
- id: "ph_unsubscribe_missing",
1759
- category: "phishing",
1760
- score: 3,
1761
- description: "Marketing-like email with many links but no List-Unsubscribe header",
1762
- test: (email, text, html) => {
1763
- const allText = text + " " + html;
1764
- const urls = new Set(allText.match(RE_URL_IN_TEXT) ?? []);
1765
- if (urls.size < 5) return false;
1766
- return !email.headers.get("list-unsubscribe");
1767
- }
1768
- },
1769
- // === Authentication (SPF/DKIM/DMARC from headers) ===
1770
- {
1771
- id: "auth_spf_fail",
1772
- category: "authentication",
1773
- score: 15,
1774
- description: "SPF authentication failed",
1775
- test: (email) => {
1776
- const authResults = email.headers.get("authentication-results") ?? "";
1777
- return /spf=(fail|softfail)/i.test(authResults);
1778
- }
1779
- },
1780
- {
1781
- id: "auth_dkim_fail",
1782
- category: "authentication",
1783
- score: 15,
1784
- description: "DKIM authentication failed",
1785
- test: (email) => {
1786
- const authResults = email.headers.get("authentication-results") ?? "";
1787
- return /dkim=fail/i.test(authResults);
1788
- }
1789
- },
1790
- {
1791
- id: "auth_dmarc_fail",
1792
- category: "authentication",
1793
- score: 20,
1794
- description: "DMARC authentication failed",
1795
- test: (email) => {
1796
- const authResults = email.headers.get("authentication-results") ?? "";
1797
- return /dmarc=fail/i.test(authResults);
1798
- }
1799
- },
1800
- {
1801
- id: "auth_no_auth_results",
1802
- category: "authentication",
1803
- score: 3,
1804
- description: "No Authentication-Results header present",
1805
- test: (email) => {
1806
- return !email.headers.has("authentication-results");
1807
- }
1808
- },
1809
- // === Attachment risk ===
1810
- {
1811
- id: "at_executable",
1812
- category: "attachment_risk",
1813
- score: 25,
1814
- description: "Attachment has executable file extension",
1815
- test: (email) => {
1816
- return email.attachments.some((a) => RE_EXECUTABLE_EXT.test(a.filename));
1817
- }
1818
- },
1819
- {
1820
- id: "at_double_extension",
1821
- category: "attachment_risk",
1822
- score: 20,
1823
- description: "Attachment has double extension (e.g. document.pdf.exe)",
1824
- test: (email) => {
1825
- return email.attachments.some((a) => RE_DOUBLE_EXT.test(a.filename));
1826
- }
1827
- },
1828
- {
1829
- id: "at_archive_carrier",
1830
- category: "attachment_risk",
1831
- score: 15,
1832
- description: "Attachment is an archive (potential payload carrier)",
1833
- test: (email) => {
1834
- return email.attachments.some((a) => RE_ARCHIVE_EXT.test(a.filename));
1835
- }
1836
- },
1837
- {
1838
- id: "at_html_attachment",
1839
- category: "attachment_risk",
1840
- score: 10,
1841
- description: "HTML/SVG file attachment (phishing vector)",
1842
- test: (email) => {
1843
- return email.attachments.some((a) => RE_HTML_ATTACHMENT_EXT.test(a.filename));
1844
- }
1845
- },
1846
- // === Header anomalies ===
1847
- {
1848
- id: "ha_missing_message_id",
1849
- category: "header_anomaly",
1850
- score: 5,
1851
- description: "Missing Message-ID header",
1852
- test: (email) => !email.messageId
1853
- },
1854
- {
1855
- id: "ha_empty_from",
1856
- category: "header_anomaly",
1857
- score: 10,
1858
- description: "Missing or empty From address",
1859
- test: (email) => !email.from.length || !email.from[0].address
1860
- },
1861
- {
1862
- id: "ha_reply_to_mismatch",
1863
- category: "header_anomaly",
1864
- score: 5,
1865
- description: "Reply-To domain differs from From domain",
1866
- test: (email) => {
1867
- if (!email.replyTo?.length || !email.from.length) return false;
1868
- const fromDomain = email.from[0].address?.split("@")[1]?.toLowerCase();
1869
- const replyDomain = email.replyTo[0].address?.split("@")[1]?.toLowerCase();
1870
- return !!fromDomain && !!replyDomain && fromDomain !== replyDomain;
1871
- }
1872
- },
1873
- // === Content spam ===
1874
- {
1875
- id: "cs_all_caps_subject",
1876
- category: "content_spam",
1877
- score: 5,
1878
- description: "Subject is mostly uppercase",
1879
- test: (email) => {
1880
- const s = email.subject;
1881
- if (s.length < 10) return false;
1882
- const letters = s.replace(/[^a-zA-Z]/g, "");
1883
- if (letters.length < 5) return false;
1884
- const upper = letters.replace(/[^A-Z]/g, "").length;
1885
- return upper / letters.length > 0.8;
1886
- }
1887
- },
1888
- {
1889
- id: "cs_lottery_scam",
1890
- category: "content_spam",
1891
- score: 25,
1892
- description: "Contains lottery/prize scam language",
1893
- test: (_e, text) => RE_LOTTERY_SCAM.test(text)
1894
- },
1895
- {
1896
- id: "cs_crypto_scam",
1897
- category: "content_spam",
1898
- score: 10,
1899
- description: "Contains crypto/investment scam language",
1900
- test: (_e, text) => RE_CRYPTO_SCAM.test(text)
1901
- },
1902
- {
1903
- id: "cs_excessive_punctuation",
1904
- category: "content_spam",
1905
- score: 3,
1906
- description: "Subject has excessive punctuation (!!!!, ????)",
1907
- test: (email) => /[!]{4,}|[?]{4,}/.test(email.subject)
1908
- },
1909
- {
1910
- id: "cs_pharmacy_spam",
1911
- category: "content_spam",
1912
- score: 15,
1913
- description: "Contains pharmacy/prescription drug spam language",
1914
- test: (_e, text) => RE_PHARMACY_SPAM.test(text)
1915
- },
1916
- {
1917
- id: "cs_weight_loss",
1918
- category: "content_spam",
1919
- score: 10,
1920
- description: "Contains weight loss scam language",
1921
- test: (_e, text) => RE_WEIGHT_LOSS.test(text)
1922
- },
1923
- {
1924
- id: "cs_html_only_no_text",
1925
- category: "content_spam",
1926
- score: 5,
1927
- description: "Email has HTML body but empty/missing text body",
1928
- test: (email) => {
1929
- const hasHtml = !!email.html && email.html.trim().length > 0;
1930
- const hasText = !!email.text && email.text.trim().length > 0;
1931
- return hasHtml && !hasText;
1932
- }
1933
- },
1934
- {
1935
- id: "cs_spam_word_density",
1936
- category: "content_spam",
1937
- score: 0,
1938
- // Dynamic — calculated in test
1939
- description: "High density of common spam words",
1940
- test: (_e, text) => countSpamWords(text) > 5
1941
- },
1942
- // === Link analysis ===
1943
- {
1944
- id: "la_excessive_links",
1945
- category: "link_analysis",
1946
- score: 5,
1947
- description: "Contains more than 10 unique links",
1948
- test: (_e, text, html) => {
1949
- const allText = text + " " + html;
1950
- const urls = new Set(allText.match(RE_URL_IN_TEXT) ?? []);
1951
- return urls.size > 10;
1952
- }
1953
- }
1954
- ];
1955
- function scoreEmail(email) {
1956
- const bodyText = [email.subject, email.text ?? ""].join("\n");
1957
- const bodyHtml = email.html ?? "";
1958
- const matches = [];
1959
- for (const rule of RULES) {
1960
- try {
1961
- if (rule.test(email, bodyText, bodyHtml)) {
1962
- let score2 = rule.score;
1963
- if (rule.id === "cs_spam_word_density") {
1964
- const wordCount = countSpamWords(bodyText);
1965
- score2 = wordCount > 10 ? 20 : 10;
1966
- }
1967
- matches.push({
1968
- ruleId: rule.id,
1969
- category: rule.category,
1970
- score: score2,
1971
- description: rule.description
1972
- });
1973
- }
1974
- } catch {
1975
- }
1976
- }
1977
- const score = matches.reduce((sum, m) => sum + m.score, 0);
1978
- let topCategory = null;
1979
- if (matches.length > 0) {
1980
- const categoryScores = /* @__PURE__ */ new Map();
1981
- for (const m of matches) {
1982
- categoryScores.set(m.category, (categoryScores.get(m.category) ?? 0) + m.score);
1983
- }
1984
- let maxScore = 0;
1985
- for (const [cat, catScore] of categoryScores) {
1986
- if (catScore > maxScore) {
1987
- maxScore = catScore;
1988
- topCategory = cat;
1989
- }
1990
- }
1991
- }
1992
- return {
1993
- score,
1994
- isSpam: score >= SPAM_THRESHOLD,
1995
- isWarning: score >= WARNING_THRESHOLD && score < SPAM_THRESHOLD,
1996
- matches,
1997
- topCategory
1998
- };
1999
- }
2000
-
2001
1465
  // src/mail/sanitizer.ts
2002
1466
  var RE_TAG_BLOCK = /[\u{E0001}-\u{E007F}]/gu;
2003
1467
  var RE_ZERO_WIDTH = /[\u200B\u200C\u200D\uFEFF]/g;
@@ -2564,7 +2028,10 @@ function getAttachmentText(content, encoding) {
2564
2028
  }
2565
2029
  function scanOutboundEmail(input) {
2566
2030
  const recipients = Array.isArray(input.to) ? input.to : [input.to];
2567
- const allInternal = recipients.every((r) => r.endsWith("@localhost"));
2031
+ const allInternal = recipients.every((r) => {
2032
+ const domain = r.split("@").pop()?.toLowerCase();
2033
+ return domain === "localhost";
2034
+ });
2568
2035
  if (allInternal) {
2569
2036
  return { warnings: [], hasHighSeverity: false, hasMediumSeverity: false, blocked: false, summary: "" };
2570
2037
  }
@@ -3057,6 +2524,12 @@ var EmailSearchIndex = class {
3057
2524
  this.db = db2;
3058
2525
  }
3059
2526
  index(email) {
2527
+ if (email.messageId) {
2528
+ const existing = this.db.prepare(
2529
+ "SELECT rowid FROM email_search WHERE agent_id = ? AND message_id = ?"
2530
+ ).get(email.agentId, email.messageId);
2531
+ if (existing) return;
2532
+ }
3060
2533
  const stmt = this.db.prepare(`
3061
2534
  INSERT INTO email_search (agent_id, message_id, subject, from_address, to_address, body_text, received_at)
3062
2535
  VALUES (?, ?, ?, ?, ?, ?, ?)
@@ -3203,7 +2676,7 @@ var DomainManager = class {
3203
2676
 
3204
2677
  // src/gateway/manager.ts
3205
2678
  import { join as join4 } from "path";
3206
- import { createCipheriv, createDecipheriv, randomBytes as randomBytes2, createHash } from "crypto";
2679
+ import { createCipheriv, createDecipheriv, randomBytes as randomBytes2, createHash, scryptSync } from "crypto";
3207
2680
  import nodemailer3 from "nodemailer";
3208
2681
 
3209
2682
  // src/debug.ts
@@ -4844,23 +4317,38 @@ var SmsPoller = class {
4844
4317
  };
4845
4318
 
4846
4319
  // src/gateway/manager.ts
4320
+ function deriveKey(key, salt) {
4321
+ return scryptSync(key, salt, 32, { N: 16384, r: 8, p: 1 });
4322
+ }
4847
4323
  function encryptSecret(plaintext, key) {
4848
- const keyHash = createHash("sha256").update(key).digest();
4324
+ const salt = randomBytes2(16);
4325
+ const derivedKey = deriveKey(key, salt);
4849
4326
  const iv = randomBytes2(12);
4850
- const cipher = createCipheriv("aes-256-gcm", keyHash, iv);
4327
+ const cipher = createCipheriv("aes-256-gcm", derivedKey, iv);
4851
4328
  const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
4852
4329
  const authTag = cipher.getAuthTag();
4853
- return `enc:${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted.toString("hex")}`;
4330
+ return `enc2:${salt.toString("hex")}:${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted.toString("hex")}`;
4854
4331
  }
4855
4332
  function decryptSecret(value, key) {
4856
- if (!value.startsWith("enc:")) return value;
4857
- const parts = value.split(":");
4858
- if (parts.length !== 4) return value;
4859
- const [, ivHex, authTagHex, ciphertextHex] = parts;
4860
- const keyHash = createHash("sha256").update(key).digest();
4861
- const decipher = createDecipheriv("aes-256-gcm", keyHash, Buffer.from(ivHex, "hex"));
4862
- decipher.setAuthTag(Buffer.from(authTagHex, "hex"));
4863
- return Buffer.concat([decipher.update(Buffer.from(ciphertextHex, "hex")), decipher.final()]).toString("utf8");
4333
+ if (value.startsWith("enc2:")) {
4334
+ const parts = value.split(":");
4335
+ if (parts.length !== 5) return value;
4336
+ const [, saltHex, ivHex, authTagHex, ciphertextHex] = parts;
4337
+ const derivedKey = deriveKey(key, Buffer.from(saltHex, "hex"));
4338
+ const decipher = createDecipheriv("aes-256-gcm", derivedKey, Buffer.from(ivHex, "hex"));
4339
+ decipher.setAuthTag(Buffer.from(authTagHex, "hex"));
4340
+ return Buffer.concat([decipher.update(Buffer.from(ciphertextHex, "hex")), decipher.final()]).toString("utf8");
4341
+ }
4342
+ if (value.startsWith("enc:")) {
4343
+ const parts = value.split(":");
4344
+ if (parts.length !== 4) return value;
4345
+ const [, ivHex, authTagHex, ciphertextHex] = parts;
4346
+ const keyHash = createHash("sha256").update(key).digest();
4347
+ const decipher = createDecipheriv("aes-256-gcm", keyHash, Buffer.from(ivHex, "hex"));
4348
+ decipher.setAuthTag(Buffer.from(authTagHex, "hex"));
4349
+ return Buffer.concat([decipher.update(Buffer.from(ciphertextHex, "hex")), decipher.final()]).toString("utf8");
4350
+ }
4351
+ return value;
4864
4352
  }
4865
4353
  var GatewayManager = class {
4866
4354
  constructor(options) {
@@ -4950,11 +4438,14 @@ var GatewayManager = class {
4950
4438
  console.warn(`[GatewayManager] Approval reply check failed: ${err.message}`);
4951
4439
  }
4952
4440
  const parsed = inboundToParsedEmail(mail);
4953
- const spamResult = scoreEmail(parsed);
4954
- if (spamResult.isSpam) {
4955
- console.warn(`[GatewayManager] Spam blocked (score=${spamResult.score}, category=${spamResult.topCategory}): "${mail.subject}" from ${mail.from}`);
4956
- if (mail.messageId) this.recordDelivery(mail.messageId, agentName);
4957
- return;
4441
+ const { isInternalEmail: isInternalEmail2 } = await import("./spam-filter-L6KNZ7QI.js");
4442
+ if (!isInternalEmail2(parsed)) {
4443
+ const spamResult = scoreEmail(parsed);
4444
+ if (spamResult.isSpam) {
4445
+ console.warn(`[GatewayManager] Spam blocked (score=${spamResult.score}, category=${spamResult.topCategory}): "${mail.subject}" from ${mail.from}`);
4446
+ if (mail.messageId) this.recordDelivery(mail.messageId, agentName);
4447
+ return;
4448
+ }
4958
4449
  }
4959
4450
  let agent = await this.accountManager.getByName(agentName);
4960
4451
  if (!agent && agentName !== DEFAULT_AGENT_NAME) {