@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.cjs CHANGED
@@ -30,6 +30,636 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
30
30
  ));
31
31
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
32
32
 
33
+ // src/mail/spam-filter.ts
34
+ var spam_filter_exports = {};
35
+ __export(spam_filter_exports, {
36
+ SPAM_THRESHOLD: () => SPAM_THRESHOLD,
37
+ WARNING_THRESHOLD: () => WARNING_THRESHOLD,
38
+ isInternalEmail: () => isInternalEmail,
39
+ scoreEmail: () => scoreEmail
40
+ });
41
+ function isInternalEmail(email, localDomains) {
42
+ const fromDomain = email.from[0]?.address?.split("@")[1]?.toLowerCase();
43
+ if (!fromDomain) return false;
44
+ const internals = /* @__PURE__ */ new Set(["localhost", ...(localDomains ?? []).map((d) => d.toLowerCase())]);
45
+ if (internals.has(fromDomain) && email.replyTo?.length) {
46
+ const replyDomain = email.replyTo[0]?.address?.split("@")[1]?.toLowerCase();
47
+ if (replyDomain && !internals.has(replyDomain)) return false;
48
+ }
49
+ return internals.has(fromDomain);
50
+ }
51
+ function countSpamWords(text) {
52
+ const lower = text.toLowerCase();
53
+ let count = 0;
54
+ for (const word of SPAM_WORDS) {
55
+ if (lower.includes(word)) count++;
56
+ }
57
+ return count;
58
+ }
59
+ function hasHomographChars(domain) {
60
+ if (domain.startsWith("xn--")) return true;
61
+ const hasCyrillic = /[\u0400-\u04FF]/.test(domain);
62
+ const hasLatin = /[a-zA-Z]/.test(domain);
63
+ return hasCyrillic && hasLatin;
64
+ }
65
+ function scoreEmail(email) {
66
+ const bodyText = [email.subject, email.text ?? ""].join("\n");
67
+ const bodyHtml = email.html ?? "";
68
+ const matches = [];
69
+ for (const rule of RULES) {
70
+ try {
71
+ if (rule.test(email, bodyText, bodyHtml)) {
72
+ let score2 = rule.score;
73
+ if (rule.id === "cs_spam_word_density") {
74
+ const wordCount = countSpamWords(bodyText);
75
+ score2 = wordCount > 10 ? 20 : 10;
76
+ }
77
+ matches.push({
78
+ ruleId: rule.id,
79
+ category: rule.category,
80
+ score: score2,
81
+ description: rule.description
82
+ });
83
+ }
84
+ } catch {
85
+ }
86
+ }
87
+ const score = matches.reduce((sum, m) => sum + m.score, 0);
88
+ let topCategory = null;
89
+ if (matches.length > 0) {
90
+ const categoryScores = /* @__PURE__ */ new Map();
91
+ for (const m of matches) {
92
+ categoryScores.set(m.category, (categoryScores.get(m.category) ?? 0) + m.score);
93
+ }
94
+ let maxScore = 0;
95
+ for (const [cat, catScore] of categoryScores) {
96
+ if (catScore > maxScore) {
97
+ maxScore = catScore;
98
+ topCategory = cat;
99
+ }
100
+ }
101
+ }
102
+ return {
103
+ score,
104
+ isSpam: score >= SPAM_THRESHOLD,
105
+ isWarning: score >= WARNING_THRESHOLD && score < SPAM_THRESHOLD,
106
+ matches,
107
+ topCategory
108
+ };
109
+ }
110
+ var SPAM_THRESHOLD, WARNING_THRESHOLD, RE_IGNORE_INSTRUCTIONS, RE_YOU_ARE_NOW, RE_SYSTEM_DELIMITER, RE_NEW_INSTRUCTIONS, RE_ACT_AS, RE_DO_NOT_MENTION, RE_TAG_CHARS, RE_DENSE_ZWC, RE_JAILBREAK, RE_BASE64_BLOCK, RE_MARKDOWN_INJECTION, RE_OWNER_IMPERSONATION, RE_SECRET_REQUEST, RE_IMPERSONATE_SYSTEM, RE_URGENCY, RE_AUTHORITY, RE_MONEY_REQUEST, RE_GIFT_CARD, RE_CEO_FRAUD, RE_FORWARD_ALL, RE_SEARCH_CREDS, RE_SEND_TO_EXTERNAL, RE_DUMP_INSTRUCTIONS, RE_WEBHOOK_EXFIL, RE_CREDENTIAL_HARVEST, RE_LINK_TAG, RE_LINK_TAG_WITH_TEXT, RE_URL_IN_TEXT, RE_IP_URL, RE_URL_SHORTENER, RE_DATA_URI, RE_LOGIN_URGENCY, RE_PHARMACY_SPAM, RE_WEIGHT_LOSS, RE_LOTTERY_SCAM, RE_CRYPTO_SCAM, RE_EXECUTABLE_EXT, RE_DOUBLE_EXT, RE_ARCHIVE_EXT, RE_HTML_ATTACHMENT_EXT, BRAND_DOMAINS, SPAM_WORDS, RULES;
111
+ var init_spam_filter = __esm({
112
+ "src/mail/spam-filter.ts"() {
113
+ "use strict";
114
+ SPAM_THRESHOLD = 40;
115
+ WARNING_THRESHOLD = 20;
116
+ RE_IGNORE_INSTRUCTIONS = /ignore\s+(all\s+)?(previous|prior|above)\s+(instructions|prompts|rules)/i;
117
+ RE_YOU_ARE_NOW = /you\s+are\s+now\s+(a|an|the|my)\b/i;
118
+ RE_SYSTEM_DELIMITER = /\[SYSTEM\]|\[INST\]|<<SYS>>|<\|im_start\|>/i;
119
+ RE_NEW_INSTRUCTIONS = /new\s+instructions?:|override\s+instructions?:/i;
120
+ RE_ACT_AS = /act\s+as\s+(a|an|if)|pretend\s+(to be|you\s+are)/i;
121
+ RE_DO_NOT_MENTION = /do\s+not\s+(mention|tell|reveal|disclose)\s+(that|this)/i;
122
+ RE_TAG_CHARS = /[\u{E0001}-\u{E007F}]/u;
123
+ RE_DENSE_ZWC = /[\u200B\u200C\u200D\uFEFF]{3,}/;
124
+ RE_JAILBREAK = /\b(DAN|jailbreak|bypass\s+(safety|filter|restriction)|unlimited\s+mode)\b/i;
125
+ RE_BASE64_BLOCK = /[A-Za-z0-9+/]{100,}={0,2}/;
126
+ RE_MARKDOWN_INJECTION = /```(?:system|python\s+exec|bash\s+exec)/i;
127
+ RE_OWNER_IMPERSONATION = /your\s+(owner|creator|admin|boss|master|human)\s+(asked|told|wants|said|instructed|needs)/i;
128
+ RE_SECRET_REQUEST = /share\s+(your|the)\s+(api.?key|password|secret|credential|token)/i;
129
+ RE_IMPERSONATE_SYSTEM = /this\s+is\s+(a|an)\s+(system|security|admin|automated)\s+(message|alert|notification)/i;
130
+ RE_URGENCY = /\b(urgent|immediately|right now|asap|deadline|expires?|last chance|act now|time.?sensitive)\b/i;
131
+ RE_AUTHORITY = /\b(suspend|terminate|deactivat|unauthori[zs]|locked|compromised|breach|violation|legal action)\b/i;
132
+ RE_MONEY_REQUEST = /send\s+(me|us)\s+\$?\d|wire\s+transfer|western\s+union|money\s*gram/i;
133
+ RE_GIFT_CARD = /buy\s+(me\s+)?gift\s*cards?|itunes\s+cards?|google\s+play\s+cards?/i;
134
+ RE_CEO_FRAUD = /\b(CEO|CFO|CTO|director|executive)\b.*\b(wire|transfer|payment|urgent)\b/i;
135
+ RE_FORWARD_ALL = /forward\s+(all|every)\s+(email|message)/i;
136
+ RE_SEARCH_CREDS = /search\s+(inbox|email|mailbox).*password|find.*credential/i;
137
+ RE_SEND_TO_EXTERNAL = /send\s+(the|all|every).*to\s+\S+@\S+/i;
138
+ RE_DUMP_INSTRUCTIONS = /reveal.*system\s+prompt|dump.*instructions|show.*system\s+prompt|print.*instructions/i;
139
+ RE_WEBHOOK_EXFIL = /https?:\/\/[^/]*(webhook|ngrok|pipedream|requestbin|hookbin)/i;
140
+ RE_CREDENTIAL_HARVEST = /verify\s+your\s+(account|identity|password|credentials?)/i;
141
+ RE_LINK_TAG = /<a\s[^>]*href\s*=\s*["']([^"']+)["']/gi;
142
+ RE_LINK_TAG_WITH_TEXT = /<a\s[^>]*href\s*=\s*["']([^"']+)["'][^>]*>(.*?)<\/a>/gi;
143
+ RE_URL_IN_TEXT = /https?:\/\/[^\s<>"]+/gi;
144
+ RE_IP_URL = /https?:\/\/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/i;
145
+ RE_URL_SHORTENER = /https?:\/\/(bit\.ly|t\.co|tinyurl\.com|goo\.gl|ow\.ly|is\.gd|buff\.ly|rebrand\.ly|shorturl\.at)\//i;
146
+ RE_DATA_URI = /(?:data:text\/html|javascript:)/i;
147
+ RE_LOGIN_URGENCY = /(click\s+here|sign\s+in|log\s*in).*\b(urgent|immediately|expire|suspend|locked)/i;
148
+ RE_PHARMACY_SPAM = /\b(viagra|cialis|pharmacy|prescription|cheap\s+meds|online\s+pharmacy)\b/i;
149
+ RE_WEIGHT_LOSS = /\b(weight\s+loss|diet\s+pill|lose\s+\d+\s+(lbs?|pounds|kg)|fat\s+burn)\b/i;
150
+ RE_LOTTERY_SCAM = /you\s+(have\s+)?(won|been\s+selected)|lottery|million\s+dollars|nigerian?\s+prince/i;
151
+ RE_CRYPTO_SCAM = /(bitcoin|crypto|ethereum).*invest(ment)?|guaranteed\s+returns|double\s+your\s+(money|bitcoin|crypto)/i;
152
+ RE_EXECUTABLE_EXT = /\.(exe|bat|cmd|ps1|sh|dll|scr|vbs|js|msi|com)$/i;
153
+ RE_DOUBLE_EXT = /\.\w{2,5}\.(exe|bat|cmd|ps1|sh|dll|scr|vbs|js|msi|com)$/i;
154
+ RE_ARCHIVE_EXT = /\.(zip|rar|7z|tar\.gz|tgz)$/i;
155
+ RE_HTML_ATTACHMENT_EXT = /\.(html?|svg)$/i;
156
+ BRAND_DOMAINS = {
157
+ google: ["google.com", "gmail.com", "googlemail.com"],
158
+ microsoft: ["microsoft.com", "outlook.com", "hotmail.com", "live.com"],
159
+ apple: ["apple.com", "icloud.com"],
160
+ amazon: ["amazon.com", "amazon.co.uk", "amazon.de"],
161
+ paypal: ["paypal.com"],
162
+ meta: ["facebook.com", "meta.com", "instagram.com"],
163
+ netflix: ["netflix.com"],
164
+ bank: ["chase.com", "wellsfargo.com", "bankofamerica.com", "citibank.com"]
165
+ };
166
+ SPAM_WORDS = [
167
+ "congratulations",
168
+ "winner",
169
+ "prize",
170
+ "claim",
171
+ "free",
172
+ "offer",
173
+ "limited time",
174
+ "act now",
175
+ "click here",
176
+ "no obligation",
177
+ "risk free",
178
+ "guaranteed",
179
+ "million",
180
+ "billion",
181
+ "inheritance",
182
+ "beneficiary",
183
+ "wire transfer",
184
+ "western union",
185
+ "dear friend",
186
+ "dear sir",
187
+ "kindly",
188
+ "revert back",
189
+ "do the needful",
190
+ "humbly",
191
+ "esteemed",
192
+ "investment opportunity",
193
+ "double your",
194
+ "earn money",
195
+ "work from home",
196
+ "make money",
197
+ "cash bonus",
198
+ "discount",
199
+ "lowest price"
200
+ ];
201
+ RULES = [
202
+ // === Prompt injection ===
203
+ {
204
+ id: "pi_ignore_instructions",
205
+ category: "prompt_injection",
206
+ score: 25,
207
+ description: 'Contains "ignore previous instructions" pattern',
208
+ test: (_e, text) => RE_IGNORE_INSTRUCTIONS.test(text)
209
+ },
210
+ {
211
+ id: "pi_you_are_now",
212
+ category: "prompt_injection",
213
+ score: 25,
214
+ description: 'Contains "you are now a..." roleplay injection',
215
+ test: (_e, text) => RE_YOU_ARE_NOW.test(text)
216
+ },
217
+ {
218
+ id: "pi_system_delimiter",
219
+ category: "prompt_injection",
220
+ score: 20,
221
+ description: "Contains LLM system delimiters ([SYSTEM], [INST], etc.)",
222
+ test: (_e, text, html) => RE_SYSTEM_DELIMITER.test(text) || RE_SYSTEM_DELIMITER.test(html)
223
+ },
224
+ {
225
+ id: "pi_new_instructions",
226
+ category: "prompt_injection",
227
+ score: 20,
228
+ description: 'Contains "new instructions:" or "override instructions:"',
229
+ test: (_e, text) => RE_NEW_INSTRUCTIONS.test(text)
230
+ },
231
+ {
232
+ id: "pi_act_as",
233
+ category: "prompt_injection",
234
+ score: 15,
235
+ description: 'Contains "act as" or "pretend to be" injection',
236
+ test: (_e, text) => RE_ACT_AS.test(text)
237
+ },
238
+ {
239
+ id: "pi_do_not_mention",
240
+ category: "prompt_injection",
241
+ score: 15,
242
+ description: 'Contains "do not mention/tell/reveal" suppression',
243
+ test: (_e, text) => RE_DO_NOT_MENTION.test(text)
244
+ },
245
+ {
246
+ id: "pi_invisible_unicode",
247
+ category: "prompt_injection",
248
+ score: 20,
249
+ description: "Contains invisible Unicode tag characters or dense zero-width chars",
250
+ test: (_e, text, html) => RE_TAG_CHARS.test(text) || RE_TAG_CHARS.test(html) || RE_DENSE_ZWC.test(text) || RE_DENSE_ZWC.test(html)
251
+ },
252
+ {
253
+ id: "pi_jailbreak",
254
+ category: "prompt_injection",
255
+ score: 20,
256
+ description: "Contains jailbreak/DAN/bypass safety language",
257
+ test: (_e, text) => RE_JAILBREAK.test(text)
258
+ },
259
+ {
260
+ id: "pi_base64_injection",
261
+ category: "prompt_injection",
262
+ score: 15,
263
+ description: "Contains long base64-encoded blocks (potential hidden instructions)",
264
+ test: (_e, text) => RE_BASE64_BLOCK.test(text)
265
+ },
266
+ {
267
+ id: "pi_markdown_injection",
268
+ category: "prompt_injection",
269
+ score: 10,
270
+ description: "Contains code block injection attempts (```system, ```python exec)",
271
+ test: (_e, text) => RE_MARKDOWN_INJECTION.test(text)
272
+ },
273
+ // === Social engineering ===
274
+ {
275
+ id: "se_owner_impersonation",
276
+ category: "social_engineering",
277
+ score: 20,
278
+ description: "Claims to speak on behalf of the agent's owner",
279
+ test: (_e, text) => RE_OWNER_IMPERSONATION.test(text)
280
+ },
281
+ {
282
+ id: "se_secret_request",
283
+ category: "social_engineering",
284
+ score: 15,
285
+ description: "Requests API keys, passwords, or credentials",
286
+ test: (_e, text) => RE_SECRET_REQUEST.test(text)
287
+ },
288
+ {
289
+ id: "se_impersonate_system",
290
+ category: "social_engineering",
291
+ score: 15,
292
+ description: "Impersonates a system/security message",
293
+ test: (_e, text) => RE_IMPERSONATE_SYSTEM.test(text)
294
+ },
295
+ {
296
+ id: "se_urgency_authority",
297
+ category: "social_engineering",
298
+ score: 10,
299
+ description: "Combines urgency language with authority/threat language",
300
+ test: (_e, text) => RE_URGENCY.test(text) && RE_AUTHORITY.test(text)
301
+ },
302
+ {
303
+ id: "se_money_request",
304
+ category: "social_engineering",
305
+ score: 15,
306
+ description: "Requests money transfer or wire",
307
+ test: (_e, text) => RE_MONEY_REQUEST.test(text)
308
+ },
309
+ {
310
+ id: "se_gift_card",
311
+ category: "social_engineering",
312
+ score: 20,
313
+ description: "Requests purchase of gift cards",
314
+ test: (_e, text) => RE_GIFT_CARD.test(text)
315
+ },
316
+ {
317
+ id: "se_ceo_fraud",
318
+ category: "social_engineering",
319
+ score: 15,
320
+ description: "BEC pattern: executive title + payment/wire/urgent",
321
+ test: (_e, text) => RE_CEO_FRAUD.test(text)
322
+ },
323
+ // === Data exfiltration ===
324
+ {
325
+ id: "de_forward_all",
326
+ category: "data_exfiltration",
327
+ score: 20,
328
+ description: "Requests forwarding all emails",
329
+ test: (_e, text) => RE_FORWARD_ALL.test(text)
330
+ },
331
+ {
332
+ id: "de_search_credentials",
333
+ category: "data_exfiltration",
334
+ score: 20,
335
+ description: "Requests searching inbox for passwords/credentials",
336
+ test: (_e, text) => RE_SEARCH_CREDS.test(text)
337
+ },
338
+ {
339
+ id: "de_send_to_external",
340
+ category: "data_exfiltration",
341
+ score: 15,
342
+ description: "Instructs sending data to an external email address",
343
+ test: (_e, text) => RE_SEND_TO_EXTERNAL.test(text)
344
+ },
345
+ {
346
+ id: "de_dump_instructions",
347
+ category: "data_exfiltration",
348
+ score: 15,
349
+ description: "Attempts to extract system prompt or instructions",
350
+ test: (_e, text) => RE_DUMP_INSTRUCTIONS.test(text)
351
+ },
352
+ {
353
+ id: "de_webhook_exfil",
354
+ category: "data_exfiltration",
355
+ score: 15,
356
+ description: "Contains webhook/ngrok/pipedream exfiltration URLs",
357
+ test: (_e, text) => RE_WEBHOOK_EXFIL.test(text)
358
+ },
359
+ // === Phishing ===
360
+ {
361
+ id: "ph_spoofed_sender",
362
+ category: "phishing",
363
+ score: 10,
364
+ description: "Sender name contains brand but domain doesn't match",
365
+ test: (email) => {
366
+ const from = email.from[0];
367
+ if (!from) return false;
368
+ const name = (from.name ?? "").toLowerCase();
369
+ const domain = (from.address ?? "").split("@")[1]?.toLowerCase() ?? "";
370
+ for (const [brand, domains] of Object.entries(BRAND_DOMAINS)) {
371
+ if (name.includes(brand) && !domains.some((d) => domain === d || domain.endsWith("." + d))) {
372
+ return true;
373
+ }
374
+ }
375
+ return false;
376
+ }
377
+ },
378
+ {
379
+ id: "ph_credential_harvest",
380
+ category: "phishing",
381
+ score: 15,
382
+ description: 'Asks to "verify your account/password" with links present',
383
+ test: (_e, text, html) => {
384
+ if (!RE_CREDENTIAL_HARVEST.test(text)) return false;
385
+ return RE_URL_IN_TEXT.test(text) || RE_LINK_TAG.test(html);
386
+ }
387
+ },
388
+ {
389
+ id: "ph_suspicious_links",
390
+ category: "phishing",
391
+ score: 10,
392
+ description: "Contains links with IP addresses, URL shorteners, or excessive subdomains",
393
+ test: (_e, text, html) => {
394
+ const allText = text + " " + html;
395
+ if (RE_IP_URL.test(allText)) return true;
396
+ if (RE_URL_SHORTENER.test(allText)) return true;
397
+ const urls = allText.match(RE_URL_IN_TEXT) ?? [];
398
+ for (const url of urls) {
399
+ try {
400
+ const hostname = new URL(url).hostname;
401
+ if (hostname.split(".").length > 4) return true;
402
+ } catch {
403
+ }
404
+ }
405
+ return false;
406
+ }
407
+ },
408
+ {
409
+ id: "ph_data_uri",
410
+ category: "phishing",
411
+ score: 15,
412
+ description: "Contains data: or javascript: URIs in links",
413
+ test: (_e, _text, html) => {
414
+ RE_LINK_TAG.lastIndex = 0;
415
+ let match;
416
+ while ((match = RE_LINK_TAG.exec(html)) !== null) {
417
+ if (RE_DATA_URI.test(match[1])) return true;
418
+ }
419
+ return false;
420
+ }
421
+ },
422
+ {
423
+ id: "ph_homograph",
424
+ category: "phishing",
425
+ score: 15,
426
+ description: "From domain contains mixed-script or punycode characters",
427
+ test: (email) => {
428
+ const domain = email.from[0]?.address?.split("@")[1] ?? "";
429
+ if (!domain) return false;
430
+ return hasHomographChars(domain);
431
+ }
432
+ },
433
+ {
434
+ id: "ph_mismatched_display_url",
435
+ category: "phishing",
436
+ score: 10,
437
+ description: "HTML link text shows one URL but href points to a different domain",
438
+ test: (_e, _text, html) => {
439
+ RE_LINK_TAG_WITH_TEXT.lastIndex = 0;
440
+ let match;
441
+ while ((match = RE_LINK_TAG_WITH_TEXT.exec(html)) !== null) {
442
+ const href = match[1];
443
+ const linkText = match[2].replace(/<[^>]*>/g, "").trim();
444
+ if (!/^https?:\/\//i.test(linkText)) continue;
445
+ try {
446
+ const hrefHost = new URL(href).hostname.replace(/^www\./, "");
447
+ const textHost = new URL(linkText).hostname.replace(/^www\./, "");
448
+ if (hrefHost !== textHost) return true;
449
+ } catch {
450
+ }
451
+ }
452
+ return false;
453
+ }
454
+ },
455
+ {
456
+ id: "ph_login_urgency",
457
+ category: "phishing",
458
+ score: 10,
459
+ description: "Combines login/click-here language with urgency",
460
+ test: (_e, text) => RE_LOGIN_URGENCY.test(text)
461
+ },
462
+ {
463
+ id: "ph_unsubscribe_missing",
464
+ category: "phishing",
465
+ score: 3,
466
+ description: "Marketing-like email with many links but no List-Unsubscribe header",
467
+ test: (email, text, html) => {
468
+ const allText = text + " " + html;
469
+ const urls = new Set(allText.match(RE_URL_IN_TEXT) ?? []);
470
+ if (urls.size < 5) return false;
471
+ return !email.headers.get("list-unsubscribe");
472
+ }
473
+ },
474
+ // === Authentication (SPF/DKIM/DMARC from headers) ===
475
+ {
476
+ id: "auth_spf_fail",
477
+ category: "authentication",
478
+ score: 15,
479
+ description: "SPF authentication failed",
480
+ test: (email) => {
481
+ const authResults = email.headers.get("authentication-results") ?? "";
482
+ return /spf=(fail|softfail)/i.test(authResults);
483
+ }
484
+ },
485
+ {
486
+ id: "auth_dkim_fail",
487
+ category: "authentication",
488
+ score: 15,
489
+ description: "DKIM authentication failed",
490
+ test: (email) => {
491
+ const authResults = email.headers.get("authentication-results") ?? "";
492
+ return /dkim=fail/i.test(authResults);
493
+ }
494
+ },
495
+ {
496
+ id: "auth_dmarc_fail",
497
+ category: "authentication",
498
+ score: 20,
499
+ description: "DMARC authentication failed",
500
+ test: (email) => {
501
+ const authResults = email.headers.get("authentication-results") ?? "";
502
+ return /dmarc=fail/i.test(authResults);
503
+ }
504
+ },
505
+ {
506
+ id: "auth_no_auth_results",
507
+ category: "authentication",
508
+ score: 3,
509
+ description: "No Authentication-Results header present",
510
+ test: (email) => {
511
+ return !email.headers.has("authentication-results");
512
+ }
513
+ },
514
+ // === Attachment risk ===
515
+ {
516
+ id: "at_executable",
517
+ category: "attachment_risk",
518
+ score: 25,
519
+ description: "Attachment has executable file extension",
520
+ test: (email) => {
521
+ return email.attachments.some((a) => RE_EXECUTABLE_EXT.test(a.filename));
522
+ }
523
+ },
524
+ {
525
+ id: "at_double_extension",
526
+ category: "attachment_risk",
527
+ score: 20,
528
+ description: "Attachment has double extension (e.g. document.pdf.exe)",
529
+ test: (email) => {
530
+ return email.attachments.some((a) => RE_DOUBLE_EXT.test(a.filename));
531
+ }
532
+ },
533
+ {
534
+ id: "at_archive_carrier",
535
+ category: "attachment_risk",
536
+ score: 15,
537
+ description: "Attachment is an archive (potential payload carrier)",
538
+ test: (email) => {
539
+ return email.attachments.some((a) => RE_ARCHIVE_EXT.test(a.filename));
540
+ }
541
+ },
542
+ {
543
+ id: "at_html_attachment",
544
+ category: "attachment_risk",
545
+ score: 10,
546
+ description: "HTML/SVG file attachment (phishing vector)",
547
+ test: (email) => {
548
+ return email.attachments.some((a) => RE_HTML_ATTACHMENT_EXT.test(a.filename));
549
+ }
550
+ },
551
+ // === Header anomalies ===
552
+ {
553
+ id: "ha_missing_message_id",
554
+ category: "header_anomaly",
555
+ score: 5,
556
+ description: "Missing Message-ID header",
557
+ test: (email) => !email.messageId
558
+ },
559
+ {
560
+ id: "ha_empty_from",
561
+ category: "header_anomaly",
562
+ score: 10,
563
+ description: "Missing or empty From address",
564
+ test: (email) => !email.from.length || !email.from[0].address
565
+ },
566
+ {
567
+ id: "ha_reply_to_mismatch",
568
+ category: "header_anomaly",
569
+ score: 5,
570
+ description: "Reply-To domain differs from From domain",
571
+ test: (email) => {
572
+ if (!email.replyTo?.length || !email.from.length) return false;
573
+ const fromDomain = email.from[0].address?.split("@")[1]?.toLowerCase();
574
+ const replyDomain = email.replyTo[0].address?.split("@")[1]?.toLowerCase();
575
+ return !!fromDomain && !!replyDomain && fromDomain !== replyDomain;
576
+ }
577
+ },
578
+ // === Content spam ===
579
+ {
580
+ id: "cs_all_caps_subject",
581
+ category: "content_spam",
582
+ score: 5,
583
+ description: "Subject is mostly uppercase",
584
+ test: (email) => {
585
+ const s = email.subject;
586
+ if (s.length < 10) return false;
587
+ const letters = s.replace(/[^a-zA-Z]/g, "");
588
+ if (letters.length < 5) return false;
589
+ const upper = letters.replace(/[^A-Z]/g, "").length;
590
+ return upper / letters.length > 0.8;
591
+ }
592
+ },
593
+ {
594
+ id: "cs_lottery_scam",
595
+ category: "content_spam",
596
+ score: 25,
597
+ description: "Contains lottery/prize scam language",
598
+ test: (_e, text) => RE_LOTTERY_SCAM.test(text)
599
+ },
600
+ {
601
+ id: "cs_crypto_scam",
602
+ category: "content_spam",
603
+ score: 10,
604
+ description: "Contains crypto/investment scam language",
605
+ test: (_e, text) => RE_CRYPTO_SCAM.test(text)
606
+ },
607
+ {
608
+ id: "cs_excessive_punctuation",
609
+ category: "content_spam",
610
+ score: 3,
611
+ description: "Subject has excessive punctuation (!!!!, ????)",
612
+ test: (email) => /[!]{4,}|[?]{4,}/.test(email.subject)
613
+ },
614
+ {
615
+ id: "cs_pharmacy_spam",
616
+ category: "content_spam",
617
+ score: 15,
618
+ description: "Contains pharmacy/prescription drug spam language",
619
+ test: (_e, text) => RE_PHARMACY_SPAM.test(text)
620
+ },
621
+ {
622
+ id: "cs_weight_loss",
623
+ category: "content_spam",
624
+ score: 10,
625
+ description: "Contains weight loss scam language",
626
+ test: (_e, text) => RE_WEIGHT_LOSS.test(text)
627
+ },
628
+ {
629
+ id: "cs_html_only_no_text",
630
+ category: "content_spam",
631
+ score: 5,
632
+ description: "Email has HTML body but empty/missing text body",
633
+ test: (email) => {
634
+ const hasHtml = !!email.html && email.html.trim().length > 0;
635
+ const hasText = !!email.text && email.text.trim().length > 0;
636
+ return hasHtml && !hasText;
637
+ }
638
+ },
639
+ {
640
+ id: "cs_spam_word_density",
641
+ category: "content_spam",
642
+ score: 0,
643
+ // Dynamic — calculated in test
644
+ description: "High density of common spam words",
645
+ test: (_e, text) => countSpamWords(text) > 5
646
+ },
647
+ // === Link analysis ===
648
+ {
649
+ id: "la_excessive_links",
650
+ category: "link_analysis",
651
+ score: 5,
652
+ description: "Contains more than 10 unique links",
653
+ test: (_e, text, html) => {
654
+ const allText = text + " " + html;
655
+ const urls = new Set(allText.match(RE_URL_IN_TEXT) ?? []);
656
+ return urls.size > 10;
657
+ }
658
+ }
659
+ ];
660
+ }
661
+ });
662
+
33
663
  // src/gateway/email-worker-template.ts
34
664
  var email_worker_template_exports = {};
35
665
  __export(email_worker_template_exports, {
@@ -183,15 +813,28 @@ var MailSender = class {
183
813
  };
184
814
  const composer = new import_mail_composer.default(mailOpts);
185
815
  const raw = await composer.compile().build();
186
- const result = await this.transporter.sendMail(mailOpts);
187
- return {
188
- messageId: result.messageId,
189
- envelope: {
190
- from: result.envelope.from || "",
191
- to: Array.isArray(result.envelope.to) ? result.envelope.to : result.envelope.to ? [result.envelope.to] : []
192
- },
193
- raw
194
- };
816
+ const MAX_RETRIES = 2;
817
+ let lastError = null;
818
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
819
+ try {
820
+ const result = await this.transporter.sendMail(mailOpts);
821
+ return {
822
+ messageId: result.messageId,
823
+ envelope: {
824
+ from: result.envelope.from || "",
825
+ to: Array.isArray(result.envelope.to) ? result.envelope.to : result.envelope.to ? [result.envelope.to] : []
826
+ },
827
+ raw
828
+ };
829
+ } catch (err) {
830
+ lastError = err;
831
+ const code = err?.responseCode ?? err?.code;
832
+ const isTransient = typeof code === "number" && code >= 400 && code < 500 || code === "ECONNRESET" || code === "ETIMEDOUT" || code === "ESOCKET";
833
+ if (!isTransient || attempt === MAX_RETRIES) throw err;
834
+ await new Promise((resolve) => setTimeout(resolve, 1e3 * (attempt + 1)));
835
+ }
836
+ }
837
+ throw lastError;
195
838
  }
196
839
  async verify() {
197
840
  try {
@@ -274,10 +917,13 @@ var MailReceiver = class {
274
917
  const limit = Math.min(Math.max(options?.limit ?? 20, 1), 1e3);
275
918
  const offset = Math.max(options?.offset ?? 0, 0);
276
919
  if (offset >= total) return envelopes;
277
- const end = Math.max(total - offset, 1);
278
- const start = Math.max(end - limit + 1, 1);
279
- const range = `${start}:${end}`;
280
- for await (const msg of this.client.fetch(range, {
920
+ const allUids = await this.client.search({ all: true }, { uid: true });
921
+ if (!allUids || allUids.length === 0) return envelopes;
922
+ const sortedUids = Array.from(allUids).sort((a, b) => b - a);
923
+ const pageUids = sortedUids.slice(offset, offset + limit);
924
+ if (pageUids.length === 0) return envelopes;
925
+ const uidRange = pageUids.join(",");
926
+ for await (const msg of this.client.fetch(uidRange, {
281
927
  uid: true,
282
928
  envelope: true,
283
929
  flags: true,
@@ -303,7 +949,8 @@ var MailReceiver = class {
303
949
  size: msg.size ?? 0
304
950
  });
305
951
  }
306
- return envelopes.reverse();
952
+ envelopes.sort((a, b) => b.uid - a.uid);
953
+ return envelopes;
307
954
  } finally {
308
955
  lock.release();
309
956
  }
@@ -491,12 +1138,17 @@ async function parseEmail(raw) {
491
1138
  }
492
1139
 
493
1140
  // src/inbox/watcher.ts
1141
+ var RECONNECT_INITIAL_MS = 2e3;
1142
+ var RECONNECT_MAX_MS = 6e4;
1143
+ var RECONNECT_FACTOR = 2;
494
1144
  var InboxWatcher = class extends import_node_events.EventEmitter {
495
1145
  constructor(options, watcherOptions) {
496
1146
  super();
497
1147
  this.options = options;
498
1148
  this.mailbox = watcherOptions?.mailbox ?? "INBOX";
499
1149
  this.autoFetch = watcherOptions?.autoFetch ?? true;
1150
+ this._autoReconnect = options.autoReconnect ?? false;
1151
+ this._maxReconnectAttempts = options.maxReconnectAttempts ?? Infinity;
500
1152
  this.client = new import_imapflow2.ImapFlow({
501
1153
  host: options.host,
502
1154
  port: options.port,
@@ -516,8 +1168,15 @@ var InboxWatcher = class extends import_node_events.EventEmitter {
516
1168
  mailbox;
517
1169
  autoFetch;
518
1170
  _lock = null;
1171
+ _stopped = false;
1172
+ _reconnectTimer = null;
1173
+ _reconnectDelay = RECONNECT_INITIAL_MS;
1174
+ _reconnectAttempts = 0;
1175
+ _maxReconnectAttempts;
1176
+ _autoReconnect;
519
1177
  async start() {
520
1178
  if (this.watching) return;
1179
+ this._stopped = false;
521
1180
  this.client = new import_imapflow2.ImapFlow({
522
1181
  host: this.options.host,
523
1182
  port: this.options.port,
@@ -535,6 +1194,8 @@ var InboxWatcher = class extends import_node_events.EventEmitter {
535
1194
  const lock = await this.client.getMailboxLock(this.mailbox);
536
1195
  try {
537
1196
  this.watching = true;
1197
+ this._reconnectDelay = RECONNECT_INITIAL_MS;
1198
+ this._reconnectAttempts = 0;
538
1199
  this.client.on("exists", async (data) => {
539
1200
  try {
540
1201
  if (data.count > data.prevCount) {
@@ -572,6 +1233,7 @@ var InboxWatcher = class extends import_node_events.EventEmitter {
572
1233
  this.client.on("close", () => {
573
1234
  this.watching = false;
574
1235
  this.emit("close");
1236
+ this._scheduleReconnect();
575
1237
  });
576
1238
  this._lock = lock;
577
1239
  } catch (err) {
@@ -579,9 +1241,44 @@ var InboxWatcher = class extends import_node_events.EventEmitter {
579
1241
  throw err;
580
1242
  }
581
1243
  }
1244
+ /** Schedule a reconnect attempt with exponential backoff */
1245
+ _scheduleReconnect() {
1246
+ if (this._stopped || !this._autoReconnect) return;
1247
+ if (this._reconnectAttempts >= this._maxReconnectAttempts) {
1248
+ this.emit("reconnect_failed", { attempts: this._reconnectAttempts });
1249
+ return;
1250
+ }
1251
+ const delay = this._reconnectDelay;
1252
+ this._reconnectDelay = Math.min(this._reconnectDelay * RECONNECT_FACTOR, RECONNECT_MAX_MS);
1253
+ this._reconnectAttempts++;
1254
+ this.emit("reconnecting", { attempt: this._reconnectAttempts, delayMs: delay });
1255
+ this._reconnectTimer = setTimeout(async () => {
1256
+ if (this._stopped) return;
1257
+ try {
1258
+ this.client.removeAllListeners();
1259
+ if (this._lock) {
1260
+ try {
1261
+ this._lock.release();
1262
+ } catch {
1263
+ }
1264
+ this._lock = null;
1265
+ }
1266
+ await this.start();
1267
+ this.emit("reconnected", { attempt: this._reconnectAttempts });
1268
+ } catch (err) {
1269
+ this.emit("error", err);
1270
+ this._scheduleReconnect();
1271
+ }
1272
+ }, delay);
1273
+ }
582
1274
  async stop() {
583
- if (!this.watching) return;
1275
+ if (!this.watching && !this._reconnectTimer) return;
1276
+ this._stopped = true;
584
1277
  this.watching = false;
1278
+ if (this._reconnectTimer) {
1279
+ clearTimeout(this._reconnectTimer);
1280
+ this._reconnectTimer = null;
1281
+ }
585
1282
  this.client.removeAllListeners();
586
1283
  if (this._lock) {
587
1284
  try {
@@ -1192,7 +1889,7 @@ var AccountManager = class {
1192
1889
  const apiKey = generateApiKey();
1193
1890
  const password = options.password ?? generatePassword();
1194
1891
  const domain = options.domain ?? "localhost";
1195
- if (domain !== "localhost" && !/^[a-zA-Z0-9][a-zA-Z0-9.-]*[a-zA-Z]{2,}$/.test(domain)) {
1892
+ if (domain !== "localhost" && !/^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/.test(domain)) {
1196
1893
  throw new Error(`Invalid domain "${domain}": must be a valid domain name`);
1197
1894
  }
1198
1895
  const principalName = options.name.toLowerCase();
@@ -1409,31 +2106,39 @@ var AgentDeletionService = class {
1409
2106
  }
1410
2107
  async archiveFolder(receiver, folder) {
1411
2108
  const archived = [];
2109
+ const PAGE_SIZE = 100;
1412
2110
  try {
1413
- const envelopes = await receiver.listEnvelopes(folder, { limit: 1e4 });
1414
- for (const env of envelopes) {
1415
- try {
1416
- const raw = await receiver.fetchMessage(env.uid, folder);
1417
- const parsed = await parseEmail(raw);
1418
- archived.push({
1419
- uid: env.uid,
1420
- messageId: parsed.messageId || env.messageId,
1421
- from: parsed.from?.[0]?.address ?? "",
1422
- to: parsed.to?.map((a) => a.address) ?? [],
1423
- subject: parsed.subject || env.subject,
1424
- date: parsed.date?.toISOString() ?? env.date?.toISOString?.() ?? "",
1425
- text: parsed.text,
1426
- html: parsed.html
1427
- });
1428
- } catch {
1429
- archived.push({
1430
- uid: env.uid,
1431
- messageId: env.messageId,
1432
- from: env.from?.[0]?.address ?? "",
1433
- to: env.to?.map((a) => a.address) ?? [],
1434
- subject: env.subject,
1435
- date: env.date?.toISOString?.() ?? ""
1436
- });
2111
+ let offset = 0;
2112
+ let hasMore = true;
2113
+ while (hasMore) {
2114
+ const envelopes = await receiver.listEnvelopes(folder, { limit: PAGE_SIZE, offset });
2115
+ if (envelopes.length === 0) break;
2116
+ hasMore = envelopes.length === PAGE_SIZE;
2117
+ offset += envelopes.length;
2118
+ for (const env of envelopes) {
2119
+ try {
2120
+ const raw = await receiver.fetchMessage(env.uid, folder);
2121
+ const parsed = await parseEmail(raw);
2122
+ archived.push({
2123
+ uid: env.uid,
2124
+ messageId: parsed.messageId || env.messageId,
2125
+ from: parsed.from?.[0]?.address ?? "",
2126
+ to: parsed.to?.map((a) => a.address) ?? [],
2127
+ subject: parsed.subject || env.subject,
2128
+ date: parsed.date?.toISOString() ?? env.date?.toISOString?.() ?? "",
2129
+ text: parsed.text,
2130
+ html: parsed.html
2131
+ });
2132
+ } catch {
2133
+ archived.push({
2134
+ uid: env.uid,
2135
+ messageId: env.messageId,
2136
+ from: env.from?.[0]?.address ?? "",
2137
+ to: env.to?.map((a) => a.address) ?? [],
2138
+ subject: env.subject,
2139
+ date: env.date?.toISOString?.() ?? ""
2140
+ });
2141
+ }
1437
2142
  }
1438
2143
  }
1439
2144
  } catch (err) {
@@ -1510,622 +2215,8 @@ var AgentDeletionService = class {
1510
2215
  }
1511
2216
  };
1512
2217
 
1513
- // src/mail/spam-filter.ts
1514
- var SPAM_THRESHOLD = 40;
1515
- var WARNING_THRESHOLD = 20;
1516
- function isInternalEmail(email, localDomains) {
1517
- const fromDomain = email.from[0]?.address?.split("@")[1]?.toLowerCase();
1518
- if (!fromDomain) return false;
1519
- const internals = /* @__PURE__ */ new Set(["localhost", ...(localDomains ?? []).map((d) => d.toLowerCase())]);
1520
- if (internals.has(fromDomain) && email.replyTo?.length) {
1521
- const replyDomain = email.replyTo[0]?.address?.split("@")[1]?.toLowerCase();
1522
- if (replyDomain && !internals.has(replyDomain)) return false;
1523
- }
1524
- return internals.has(fromDomain);
1525
- }
1526
- var RE_IGNORE_INSTRUCTIONS = /ignore\s+(all\s+)?(previous|prior|above)\s+(instructions|prompts|rules)/i;
1527
- var RE_YOU_ARE_NOW = /you\s+are\s+now\s+(a|an|the|my)\b/i;
1528
- var RE_SYSTEM_DELIMITER = /\[SYSTEM\]|\[INST\]|<<SYS>>|<\|im_start\|>/i;
1529
- var RE_NEW_INSTRUCTIONS = /new\s+instructions?:|override\s+instructions?:/i;
1530
- var RE_ACT_AS = /act\s+as\s+(a|an|if)|pretend\s+(to be|you\s+are)/i;
1531
- var RE_DO_NOT_MENTION = /do\s+not\s+(mention|tell|reveal|disclose)\s+(that|this)/i;
1532
- var RE_TAG_CHARS = /[\u{E0001}-\u{E007F}]/u;
1533
- var RE_DENSE_ZWC = /[\u200B\u200C\u200D\uFEFF]{3,}/;
1534
- var RE_JAILBREAK = /\b(DAN|jailbreak|bypass\s+(safety|filter|restriction)|unlimited\s+mode)\b/i;
1535
- var RE_BASE64_BLOCK = /[A-Za-z0-9+/]{100,}={0,2}/;
1536
- var RE_MARKDOWN_INJECTION = /```(?:system|python\s+exec|bash\s+exec)/i;
1537
- var RE_OWNER_IMPERSONATION = /your\s+(owner|creator|admin|boss|master|human)\s+(asked|told|wants|said|instructed|needs)/i;
1538
- var RE_SECRET_REQUEST = /share\s+(your|the)\s+(api.?key|password|secret|credential|token)/i;
1539
- var RE_IMPERSONATE_SYSTEM = /this\s+is\s+(a|an)\s+(system|security|admin|automated)\s+(message|alert|notification)/i;
1540
- var RE_URGENCY = /\b(urgent|immediately|right now|asap|deadline|expires?|last chance|act now|time.?sensitive)\b/i;
1541
- var RE_AUTHORITY = /\b(suspend|terminate|deactivat|unauthori[zs]|locked|compromised|breach|violation|legal action)\b/i;
1542
- var RE_MONEY_REQUEST = /send\s+(me|us)\s+\$?\d|wire\s+transfer|western\s+union|money\s*gram/i;
1543
- var RE_GIFT_CARD = /buy\s+(me\s+)?gift\s*cards?|itunes\s+cards?|google\s+play\s+cards?/i;
1544
- var RE_CEO_FRAUD = /\b(CEO|CFO|CTO|director|executive)\b.*\b(wire|transfer|payment|urgent)\b/i;
1545
- var RE_FORWARD_ALL = /forward\s+(all|every)\s+(email|message)/i;
1546
- var RE_SEARCH_CREDS = /search\s+(inbox|email|mailbox).*password|find.*credential/i;
1547
- var RE_SEND_TO_EXTERNAL = /send\s+(the|all|every).*to\s+\S+@\S+/i;
1548
- var RE_DUMP_INSTRUCTIONS = /reveal.*system\s+prompt|dump.*instructions|show.*system\s+prompt|print.*instructions/i;
1549
- var RE_WEBHOOK_EXFIL = /https?:\/\/[^/]*(webhook|ngrok|pipedream|requestbin|hookbin)/i;
1550
- var RE_CREDENTIAL_HARVEST = /verify\s+your\s+(account|identity|password|credentials?)/i;
1551
- var RE_LINK_TAG = /<a\s[^>]*href\s*=\s*["']([^"']+)["']/gi;
1552
- var RE_LINK_TAG_WITH_TEXT = /<a\s[^>]*href\s*=\s*["']([^"']+)["'][^>]*>(.*?)<\/a>/gi;
1553
- var RE_URL_IN_TEXT = /https?:\/\/[^\s<>"]+/gi;
1554
- var RE_IP_URL = /https?:\/\/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/i;
1555
- var RE_URL_SHORTENER = /https?:\/\/(bit\.ly|t\.co|tinyurl\.com|goo\.gl|ow\.ly|is\.gd|buff\.ly|rebrand\.ly|shorturl\.at)\//i;
1556
- var RE_DATA_URI = /(?:data:text\/html|javascript:)/i;
1557
- var RE_LOGIN_URGENCY = /(click\s+here|sign\s+in|log\s*in).*\b(urgent|immediately|expire|suspend|locked)/i;
1558
- var RE_PHARMACY_SPAM = /\b(viagra|cialis|pharmacy|prescription|cheap\s+meds|online\s+pharmacy)\b/i;
1559
- var RE_WEIGHT_LOSS = /\b(weight\s+loss|diet\s+pill|lose\s+\d+\s+(lbs?|pounds|kg)|fat\s+burn)\b/i;
1560
- var RE_LOTTERY_SCAM = /you\s+(have\s+)?(won|been\s+selected)|lottery|million\s+dollars|nigerian?\s+prince/i;
1561
- var RE_CRYPTO_SCAM = /(bitcoin|crypto|ethereum).*invest(ment)?|guaranteed\s+returns|double\s+your\s+(money|bitcoin|crypto)/i;
1562
- var RE_EXECUTABLE_EXT = /\.(exe|bat|cmd|ps1|sh|dll|scr|vbs|js|msi|com)$/i;
1563
- var RE_DOUBLE_EXT = /\.\w{2,5}\.(exe|bat|cmd|ps1|sh|dll|scr|vbs|js|msi|com)$/i;
1564
- var RE_ARCHIVE_EXT = /\.(zip|rar|7z|tar\.gz|tgz)$/i;
1565
- var RE_HTML_ATTACHMENT_EXT = /\.(html?|svg)$/i;
1566
- var BRAND_DOMAINS = {
1567
- google: ["google.com", "gmail.com", "googlemail.com"],
1568
- microsoft: ["microsoft.com", "outlook.com", "hotmail.com", "live.com"],
1569
- apple: ["apple.com", "icloud.com"],
1570
- amazon: ["amazon.com", "amazon.co.uk", "amazon.de"],
1571
- paypal: ["paypal.com"],
1572
- meta: ["facebook.com", "meta.com", "instagram.com"],
1573
- netflix: ["netflix.com"],
1574
- bank: ["chase.com", "wellsfargo.com", "bankofamerica.com", "citibank.com"]
1575
- };
1576
- var SPAM_WORDS = [
1577
- "congratulations",
1578
- "winner",
1579
- "prize",
1580
- "claim",
1581
- "free",
1582
- "offer",
1583
- "limited time",
1584
- "act now",
1585
- "click here",
1586
- "no obligation",
1587
- "risk free",
1588
- "guaranteed",
1589
- "million",
1590
- "billion",
1591
- "inheritance",
1592
- "beneficiary",
1593
- "wire transfer",
1594
- "western union",
1595
- "dear friend",
1596
- "dear sir",
1597
- "kindly",
1598
- "revert back",
1599
- "do the needful",
1600
- "humbly",
1601
- "esteemed",
1602
- "investment opportunity",
1603
- "double your",
1604
- "earn money",
1605
- "work from home",
1606
- "make money",
1607
- "cash bonus",
1608
- "discount",
1609
- "lowest price"
1610
- ];
1611
- function countSpamWords(text) {
1612
- const lower = text.toLowerCase();
1613
- let count = 0;
1614
- for (const word of SPAM_WORDS) {
1615
- if (lower.includes(word)) count++;
1616
- }
1617
- return count;
1618
- }
1619
- function hasHomographChars(domain) {
1620
- if (domain.startsWith("xn--")) return true;
1621
- const hasCyrillic = /[\u0400-\u04FF]/.test(domain);
1622
- const hasLatin = /[a-zA-Z]/.test(domain);
1623
- return hasCyrillic && hasLatin;
1624
- }
1625
- var RULES = [
1626
- // === Prompt injection ===
1627
- {
1628
- id: "pi_ignore_instructions",
1629
- category: "prompt_injection",
1630
- score: 25,
1631
- description: 'Contains "ignore previous instructions" pattern',
1632
- test: (_e, text) => RE_IGNORE_INSTRUCTIONS.test(text)
1633
- },
1634
- {
1635
- id: "pi_you_are_now",
1636
- category: "prompt_injection",
1637
- score: 25,
1638
- description: 'Contains "you are now a..." roleplay injection',
1639
- test: (_e, text) => RE_YOU_ARE_NOW.test(text)
1640
- },
1641
- {
1642
- id: "pi_system_delimiter",
1643
- category: "prompt_injection",
1644
- score: 20,
1645
- description: "Contains LLM system delimiters ([SYSTEM], [INST], etc.)",
1646
- test: (_e, text, html) => RE_SYSTEM_DELIMITER.test(text) || RE_SYSTEM_DELIMITER.test(html)
1647
- },
1648
- {
1649
- id: "pi_new_instructions",
1650
- category: "prompt_injection",
1651
- score: 20,
1652
- description: 'Contains "new instructions:" or "override instructions:"',
1653
- test: (_e, text) => RE_NEW_INSTRUCTIONS.test(text)
1654
- },
1655
- {
1656
- id: "pi_act_as",
1657
- category: "prompt_injection",
1658
- score: 15,
1659
- description: 'Contains "act as" or "pretend to be" injection',
1660
- test: (_e, text) => RE_ACT_AS.test(text)
1661
- },
1662
- {
1663
- id: "pi_do_not_mention",
1664
- category: "prompt_injection",
1665
- score: 15,
1666
- description: 'Contains "do not mention/tell/reveal" suppression',
1667
- test: (_e, text) => RE_DO_NOT_MENTION.test(text)
1668
- },
1669
- {
1670
- id: "pi_invisible_unicode",
1671
- category: "prompt_injection",
1672
- score: 20,
1673
- description: "Contains invisible Unicode tag characters or dense zero-width chars",
1674
- test: (_e, text, html) => RE_TAG_CHARS.test(text) || RE_TAG_CHARS.test(html) || RE_DENSE_ZWC.test(text) || RE_DENSE_ZWC.test(html)
1675
- },
1676
- {
1677
- id: "pi_jailbreak",
1678
- category: "prompt_injection",
1679
- score: 20,
1680
- description: "Contains jailbreak/DAN/bypass safety language",
1681
- test: (_e, text) => RE_JAILBREAK.test(text)
1682
- },
1683
- {
1684
- id: "pi_base64_injection",
1685
- category: "prompt_injection",
1686
- score: 15,
1687
- description: "Contains long base64-encoded blocks (potential hidden instructions)",
1688
- test: (_e, text) => RE_BASE64_BLOCK.test(text)
1689
- },
1690
- {
1691
- id: "pi_markdown_injection",
1692
- category: "prompt_injection",
1693
- score: 10,
1694
- description: "Contains code block injection attempts (```system, ```python exec)",
1695
- test: (_e, text) => RE_MARKDOWN_INJECTION.test(text)
1696
- },
1697
- // === Social engineering ===
1698
- {
1699
- id: "se_owner_impersonation",
1700
- category: "social_engineering",
1701
- score: 20,
1702
- description: "Claims to speak on behalf of the agent's owner",
1703
- test: (_e, text) => RE_OWNER_IMPERSONATION.test(text)
1704
- },
1705
- {
1706
- id: "se_secret_request",
1707
- category: "social_engineering",
1708
- score: 15,
1709
- description: "Requests API keys, passwords, or credentials",
1710
- test: (_e, text) => RE_SECRET_REQUEST.test(text)
1711
- },
1712
- {
1713
- id: "se_impersonate_system",
1714
- category: "social_engineering",
1715
- score: 15,
1716
- description: "Impersonates a system/security message",
1717
- test: (_e, text) => RE_IMPERSONATE_SYSTEM.test(text)
1718
- },
1719
- {
1720
- id: "se_urgency_authority",
1721
- category: "social_engineering",
1722
- score: 10,
1723
- description: "Combines urgency language with authority/threat language",
1724
- test: (_e, text) => RE_URGENCY.test(text) && RE_AUTHORITY.test(text)
1725
- },
1726
- {
1727
- id: "se_money_request",
1728
- category: "social_engineering",
1729
- score: 15,
1730
- description: "Requests money transfer or wire",
1731
- test: (_e, text) => RE_MONEY_REQUEST.test(text)
1732
- },
1733
- {
1734
- id: "se_gift_card",
1735
- category: "social_engineering",
1736
- score: 20,
1737
- description: "Requests purchase of gift cards",
1738
- test: (_e, text) => RE_GIFT_CARD.test(text)
1739
- },
1740
- {
1741
- id: "se_ceo_fraud",
1742
- category: "social_engineering",
1743
- score: 15,
1744
- description: "BEC pattern: executive title + payment/wire/urgent",
1745
- test: (_e, text) => RE_CEO_FRAUD.test(text)
1746
- },
1747
- // === Data exfiltration ===
1748
- {
1749
- id: "de_forward_all",
1750
- category: "data_exfiltration",
1751
- score: 20,
1752
- description: "Requests forwarding all emails",
1753
- test: (_e, text) => RE_FORWARD_ALL.test(text)
1754
- },
1755
- {
1756
- id: "de_search_credentials",
1757
- category: "data_exfiltration",
1758
- score: 20,
1759
- description: "Requests searching inbox for passwords/credentials",
1760
- test: (_e, text) => RE_SEARCH_CREDS.test(text)
1761
- },
1762
- {
1763
- id: "de_send_to_external",
1764
- category: "data_exfiltration",
1765
- score: 15,
1766
- description: "Instructs sending data to an external email address",
1767
- test: (_e, text) => RE_SEND_TO_EXTERNAL.test(text)
1768
- },
1769
- {
1770
- id: "de_dump_instructions",
1771
- category: "data_exfiltration",
1772
- score: 15,
1773
- description: "Attempts to extract system prompt or instructions",
1774
- test: (_e, text) => RE_DUMP_INSTRUCTIONS.test(text)
1775
- },
1776
- {
1777
- id: "de_webhook_exfil",
1778
- category: "data_exfiltration",
1779
- score: 15,
1780
- description: "Contains webhook/ngrok/pipedream exfiltration URLs",
1781
- test: (_e, text) => RE_WEBHOOK_EXFIL.test(text)
1782
- },
1783
- // === Phishing ===
1784
- {
1785
- id: "ph_spoofed_sender",
1786
- category: "phishing",
1787
- score: 10,
1788
- description: "Sender name contains brand but domain doesn't match",
1789
- test: (email) => {
1790
- const from = email.from[0];
1791
- if (!from) return false;
1792
- const name = (from.name ?? "").toLowerCase();
1793
- const domain = (from.address ?? "").split("@")[1]?.toLowerCase() ?? "";
1794
- for (const [brand, domains] of Object.entries(BRAND_DOMAINS)) {
1795
- if (name.includes(brand) && !domains.some((d) => domain === d || domain.endsWith("." + d))) {
1796
- return true;
1797
- }
1798
- }
1799
- return false;
1800
- }
1801
- },
1802
- {
1803
- id: "ph_credential_harvest",
1804
- category: "phishing",
1805
- score: 15,
1806
- description: 'Asks to "verify your account/password" with links present',
1807
- test: (_e, text, html) => {
1808
- if (!RE_CREDENTIAL_HARVEST.test(text)) return false;
1809
- return RE_URL_IN_TEXT.test(text) || RE_LINK_TAG.test(html);
1810
- }
1811
- },
1812
- {
1813
- id: "ph_suspicious_links",
1814
- category: "phishing",
1815
- score: 10,
1816
- description: "Contains links with IP addresses, URL shorteners, or excessive subdomains",
1817
- test: (_e, text, html) => {
1818
- const allText = text + " " + html;
1819
- if (RE_IP_URL.test(allText)) return true;
1820
- if (RE_URL_SHORTENER.test(allText)) return true;
1821
- const urls = allText.match(RE_URL_IN_TEXT) ?? [];
1822
- for (const url of urls) {
1823
- try {
1824
- const hostname = new URL(url).hostname;
1825
- if (hostname.split(".").length > 4) return true;
1826
- } catch {
1827
- }
1828
- }
1829
- return false;
1830
- }
1831
- },
1832
- {
1833
- id: "ph_data_uri",
1834
- category: "phishing",
1835
- score: 15,
1836
- description: "Contains data: or javascript: URIs in links",
1837
- test: (_e, _text, html) => {
1838
- RE_LINK_TAG.lastIndex = 0;
1839
- let match;
1840
- while ((match = RE_LINK_TAG.exec(html)) !== null) {
1841
- if (RE_DATA_URI.test(match[1])) return true;
1842
- }
1843
- return false;
1844
- }
1845
- },
1846
- {
1847
- id: "ph_homograph",
1848
- category: "phishing",
1849
- score: 15,
1850
- description: "From domain contains mixed-script or punycode characters",
1851
- test: (email) => {
1852
- const domain = email.from[0]?.address?.split("@")[1] ?? "";
1853
- if (!domain) return false;
1854
- return hasHomographChars(domain);
1855
- }
1856
- },
1857
- {
1858
- id: "ph_mismatched_display_url",
1859
- category: "phishing",
1860
- score: 10,
1861
- description: "HTML link text shows one URL but href points to a different domain",
1862
- test: (_e, _text, html) => {
1863
- RE_LINK_TAG_WITH_TEXT.lastIndex = 0;
1864
- let match;
1865
- while ((match = RE_LINK_TAG_WITH_TEXT.exec(html)) !== null) {
1866
- const href = match[1];
1867
- const linkText = match[2].replace(/<[^>]*>/g, "").trim();
1868
- if (!/^https?:\/\//i.test(linkText)) continue;
1869
- try {
1870
- const hrefHost = new URL(href).hostname.replace(/^www\./, "");
1871
- const textHost = new URL(linkText).hostname.replace(/^www\./, "");
1872
- if (hrefHost !== textHost) return true;
1873
- } catch {
1874
- }
1875
- }
1876
- return false;
1877
- }
1878
- },
1879
- {
1880
- id: "ph_login_urgency",
1881
- category: "phishing",
1882
- score: 10,
1883
- description: "Combines login/click-here language with urgency",
1884
- test: (_e, text) => RE_LOGIN_URGENCY.test(text)
1885
- },
1886
- {
1887
- id: "ph_unsubscribe_missing",
1888
- category: "phishing",
1889
- score: 3,
1890
- description: "Marketing-like email with many links but no List-Unsubscribe header",
1891
- test: (email, text, html) => {
1892
- const allText = text + " " + html;
1893
- const urls = new Set(allText.match(RE_URL_IN_TEXT) ?? []);
1894
- if (urls.size < 5) return false;
1895
- return !email.headers.get("list-unsubscribe");
1896
- }
1897
- },
1898
- // === Authentication (SPF/DKIM/DMARC from headers) ===
1899
- {
1900
- id: "auth_spf_fail",
1901
- category: "authentication",
1902
- score: 15,
1903
- description: "SPF authentication failed",
1904
- test: (email) => {
1905
- const authResults = email.headers.get("authentication-results") ?? "";
1906
- return /spf=(fail|softfail)/i.test(authResults);
1907
- }
1908
- },
1909
- {
1910
- id: "auth_dkim_fail",
1911
- category: "authentication",
1912
- score: 15,
1913
- description: "DKIM authentication failed",
1914
- test: (email) => {
1915
- const authResults = email.headers.get("authentication-results") ?? "";
1916
- return /dkim=fail/i.test(authResults);
1917
- }
1918
- },
1919
- {
1920
- id: "auth_dmarc_fail",
1921
- category: "authentication",
1922
- score: 20,
1923
- description: "DMARC authentication failed",
1924
- test: (email) => {
1925
- const authResults = email.headers.get("authentication-results") ?? "";
1926
- return /dmarc=fail/i.test(authResults);
1927
- }
1928
- },
1929
- {
1930
- id: "auth_no_auth_results",
1931
- category: "authentication",
1932
- score: 3,
1933
- description: "No Authentication-Results header present",
1934
- test: (email) => {
1935
- return !email.headers.has("authentication-results");
1936
- }
1937
- },
1938
- // === Attachment risk ===
1939
- {
1940
- id: "at_executable",
1941
- category: "attachment_risk",
1942
- score: 25,
1943
- description: "Attachment has executable file extension",
1944
- test: (email) => {
1945
- return email.attachments.some((a) => RE_EXECUTABLE_EXT.test(a.filename));
1946
- }
1947
- },
1948
- {
1949
- id: "at_double_extension",
1950
- category: "attachment_risk",
1951
- score: 20,
1952
- description: "Attachment has double extension (e.g. document.pdf.exe)",
1953
- test: (email) => {
1954
- return email.attachments.some((a) => RE_DOUBLE_EXT.test(a.filename));
1955
- }
1956
- },
1957
- {
1958
- id: "at_archive_carrier",
1959
- category: "attachment_risk",
1960
- score: 15,
1961
- description: "Attachment is an archive (potential payload carrier)",
1962
- test: (email) => {
1963
- return email.attachments.some((a) => RE_ARCHIVE_EXT.test(a.filename));
1964
- }
1965
- },
1966
- {
1967
- id: "at_html_attachment",
1968
- category: "attachment_risk",
1969
- score: 10,
1970
- description: "HTML/SVG file attachment (phishing vector)",
1971
- test: (email) => {
1972
- return email.attachments.some((a) => RE_HTML_ATTACHMENT_EXT.test(a.filename));
1973
- }
1974
- },
1975
- // === Header anomalies ===
1976
- {
1977
- id: "ha_missing_message_id",
1978
- category: "header_anomaly",
1979
- score: 5,
1980
- description: "Missing Message-ID header",
1981
- test: (email) => !email.messageId
1982
- },
1983
- {
1984
- id: "ha_empty_from",
1985
- category: "header_anomaly",
1986
- score: 10,
1987
- description: "Missing or empty From address",
1988
- test: (email) => !email.from.length || !email.from[0].address
1989
- },
1990
- {
1991
- id: "ha_reply_to_mismatch",
1992
- category: "header_anomaly",
1993
- score: 5,
1994
- description: "Reply-To domain differs from From domain",
1995
- test: (email) => {
1996
- if (!email.replyTo?.length || !email.from.length) return false;
1997
- const fromDomain = email.from[0].address?.split("@")[1]?.toLowerCase();
1998
- const replyDomain = email.replyTo[0].address?.split("@")[1]?.toLowerCase();
1999
- return !!fromDomain && !!replyDomain && fromDomain !== replyDomain;
2000
- }
2001
- },
2002
- // === Content spam ===
2003
- {
2004
- id: "cs_all_caps_subject",
2005
- category: "content_spam",
2006
- score: 5,
2007
- description: "Subject is mostly uppercase",
2008
- test: (email) => {
2009
- const s = email.subject;
2010
- if (s.length < 10) return false;
2011
- const letters = s.replace(/[^a-zA-Z]/g, "");
2012
- if (letters.length < 5) return false;
2013
- const upper = letters.replace(/[^A-Z]/g, "").length;
2014
- return upper / letters.length > 0.8;
2015
- }
2016
- },
2017
- {
2018
- id: "cs_lottery_scam",
2019
- category: "content_spam",
2020
- score: 25,
2021
- description: "Contains lottery/prize scam language",
2022
- test: (_e, text) => RE_LOTTERY_SCAM.test(text)
2023
- },
2024
- {
2025
- id: "cs_crypto_scam",
2026
- category: "content_spam",
2027
- score: 10,
2028
- description: "Contains crypto/investment scam language",
2029
- test: (_e, text) => RE_CRYPTO_SCAM.test(text)
2030
- },
2031
- {
2032
- id: "cs_excessive_punctuation",
2033
- category: "content_spam",
2034
- score: 3,
2035
- description: "Subject has excessive punctuation (!!!!, ????)",
2036
- test: (email) => /[!]{4,}|[?]{4,}/.test(email.subject)
2037
- },
2038
- {
2039
- id: "cs_pharmacy_spam",
2040
- category: "content_spam",
2041
- score: 15,
2042
- description: "Contains pharmacy/prescription drug spam language",
2043
- test: (_e, text) => RE_PHARMACY_SPAM.test(text)
2044
- },
2045
- {
2046
- id: "cs_weight_loss",
2047
- category: "content_spam",
2048
- score: 10,
2049
- description: "Contains weight loss scam language",
2050
- test: (_e, text) => RE_WEIGHT_LOSS.test(text)
2051
- },
2052
- {
2053
- id: "cs_html_only_no_text",
2054
- category: "content_spam",
2055
- score: 5,
2056
- description: "Email has HTML body but empty/missing text body",
2057
- test: (email) => {
2058
- const hasHtml = !!email.html && email.html.trim().length > 0;
2059
- const hasText = !!email.text && email.text.trim().length > 0;
2060
- return hasHtml && !hasText;
2061
- }
2062
- },
2063
- {
2064
- id: "cs_spam_word_density",
2065
- category: "content_spam",
2066
- score: 0,
2067
- // Dynamic — calculated in test
2068
- description: "High density of common spam words",
2069
- test: (_e, text) => countSpamWords(text) > 5
2070
- },
2071
- // === Link analysis ===
2072
- {
2073
- id: "la_excessive_links",
2074
- category: "link_analysis",
2075
- score: 5,
2076
- description: "Contains more than 10 unique links",
2077
- test: (_e, text, html) => {
2078
- const allText = text + " " + html;
2079
- const urls = new Set(allText.match(RE_URL_IN_TEXT) ?? []);
2080
- return urls.size > 10;
2081
- }
2082
- }
2083
- ];
2084
- function scoreEmail(email) {
2085
- const bodyText = [email.subject, email.text ?? ""].join("\n");
2086
- const bodyHtml = email.html ?? "";
2087
- const matches = [];
2088
- for (const rule of RULES) {
2089
- try {
2090
- if (rule.test(email, bodyText, bodyHtml)) {
2091
- let score2 = rule.score;
2092
- if (rule.id === "cs_spam_word_density") {
2093
- const wordCount = countSpamWords(bodyText);
2094
- score2 = wordCount > 10 ? 20 : 10;
2095
- }
2096
- matches.push({
2097
- ruleId: rule.id,
2098
- category: rule.category,
2099
- score: score2,
2100
- description: rule.description
2101
- });
2102
- }
2103
- } catch {
2104
- }
2105
- }
2106
- const score = matches.reduce((sum, m) => sum + m.score, 0);
2107
- let topCategory = null;
2108
- if (matches.length > 0) {
2109
- const categoryScores = /* @__PURE__ */ new Map();
2110
- for (const m of matches) {
2111
- categoryScores.set(m.category, (categoryScores.get(m.category) ?? 0) + m.score);
2112
- }
2113
- let maxScore = 0;
2114
- for (const [cat, catScore] of categoryScores) {
2115
- if (catScore > maxScore) {
2116
- maxScore = catScore;
2117
- topCategory = cat;
2118
- }
2119
- }
2120
- }
2121
- return {
2122
- score,
2123
- isSpam: score >= SPAM_THRESHOLD,
2124
- isWarning: score >= WARNING_THRESHOLD && score < SPAM_THRESHOLD,
2125
- matches,
2126
- topCategory
2127
- };
2128
- }
2218
+ // src/index.ts
2219
+ init_spam_filter();
2129
2220
 
2130
2221
  // src/mail/sanitizer.ts
2131
2222
  var RE_TAG_BLOCK = /[\u{E0001}-\u{E007F}]/gu;
@@ -2693,7 +2784,10 @@ function getAttachmentText(content, encoding) {
2693
2784
  }
2694
2785
  function scanOutboundEmail(input) {
2695
2786
  const recipients = Array.isArray(input.to) ? input.to : [input.to];
2696
- const allInternal = recipients.every((r) => r.endsWith("@localhost"));
2787
+ const allInternal = recipients.every((r) => {
2788
+ const domain = r.split("@").pop()?.toLowerCase();
2789
+ return domain === "localhost";
2790
+ });
2697
2791
  if (allInternal) {
2698
2792
  return { warnings: [], hasHighSeverity: false, hasMediumSeverity: false, blocked: false, summary: "" };
2699
2793
  }
@@ -3186,6 +3280,12 @@ var EmailSearchIndex = class {
3186
3280
  this.db = db2;
3187
3281
  }
3188
3282
  index(email) {
3283
+ if (email.messageId) {
3284
+ const existing = this.db.prepare(
3285
+ "SELECT rowid FROM email_search WHERE agent_id = ? AND message_id = ?"
3286
+ ).get(email.agentId, email.messageId);
3287
+ if (existing) return;
3288
+ }
3189
3289
  const stmt = this.db.prepare(`
3190
3290
  INSERT INTO email_search (agent_id, message_id, subject, from_address, to_address, body_text, received_at)
3191
3291
  VALUES (?, ?, ?, ?, ?, ?, ?)
@@ -4573,6 +4673,7 @@ var TunnelManager = class {
4573
4673
 
4574
4674
  // src/gateway/manager.ts
4575
4675
  var import_mail_composer3 = __toESM(require("nodemailer/lib/mail-composer/index.js"), 1);
4676
+ init_spam_filter();
4576
4677
 
4577
4678
  // src/sms/manager.ts
4578
4679
  function normalizePhoneNumber(raw) {
@@ -4973,23 +5074,38 @@ var SmsPoller = class {
4973
5074
  };
4974
5075
 
4975
5076
  // src/gateway/manager.ts
5077
+ function deriveKey(key, salt) {
5078
+ return (0, import_node_crypto2.scryptSync)(key, salt, 32, { N: 16384, r: 8, p: 1 });
5079
+ }
4976
5080
  function encryptSecret(plaintext, key) {
4977
- const keyHash = (0, import_node_crypto2.createHash)("sha256").update(key).digest();
5081
+ const salt = (0, import_node_crypto2.randomBytes)(16);
5082
+ const derivedKey = deriveKey(key, salt);
4978
5083
  const iv = (0, import_node_crypto2.randomBytes)(12);
4979
- const cipher = (0, import_node_crypto2.createCipheriv)("aes-256-gcm", keyHash, iv);
5084
+ const cipher = (0, import_node_crypto2.createCipheriv)("aes-256-gcm", derivedKey, iv);
4980
5085
  const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
4981
5086
  const authTag = cipher.getAuthTag();
4982
- return `enc:${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted.toString("hex")}`;
5087
+ return `enc2:${salt.toString("hex")}:${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted.toString("hex")}`;
4983
5088
  }
4984
5089
  function decryptSecret(value, key) {
4985
- if (!value.startsWith("enc:")) return value;
4986
- const parts = value.split(":");
4987
- if (parts.length !== 4) return value;
4988
- const [, ivHex, authTagHex, ciphertextHex] = parts;
4989
- const keyHash = (0, import_node_crypto2.createHash)("sha256").update(key).digest();
4990
- const decipher = (0, import_node_crypto2.createDecipheriv)("aes-256-gcm", keyHash, Buffer.from(ivHex, "hex"));
4991
- decipher.setAuthTag(Buffer.from(authTagHex, "hex"));
4992
- return Buffer.concat([decipher.update(Buffer.from(ciphertextHex, "hex")), decipher.final()]).toString("utf8");
5090
+ if (value.startsWith("enc2:")) {
5091
+ const parts = value.split(":");
5092
+ if (parts.length !== 5) return value;
5093
+ const [, saltHex, ivHex, authTagHex, ciphertextHex] = parts;
5094
+ const derivedKey = deriveKey(key, Buffer.from(saltHex, "hex"));
5095
+ const decipher = (0, import_node_crypto2.createDecipheriv)("aes-256-gcm", derivedKey, Buffer.from(ivHex, "hex"));
5096
+ decipher.setAuthTag(Buffer.from(authTagHex, "hex"));
5097
+ return Buffer.concat([decipher.update(Buffer.from(ciphertextHex, "hex")), decipher.final()]).toString("utf8");
5098
+ }
5099
+ if (value.startsWith("enc:")) {
5100
+ const parts = value.split(":");
5101
+ if (parts.length !== 4) return value;
5102
+ const [, ivHex, authTagHex, ciphertextHex] = parts;
5103
+ const keyHash = (0, import_node_crypto2.createHash)("sha256").update(key).digest();
5104
+ const decipher = (0, import_node_crypto2.createDecipheriv)("aes-256-gcm", keyHash, Buffer.from(ivHex, "hex"));
5105
+ decipher.setAuthTag(Buffer.from(authTagHex, "hex"));
5106
+ return Buffer.concat([decipher.update(Buffer.from(ciphertextHex, "hex")), decipher.final()]).toString("utf8");
5107
+ }
5108
+ return value;
4993
5109
  }
4994
5110
  var GatewayManager = class {
4995
5111
  constructor(options) {
@@ -5079,11 +5195,14 @@ var GatewayManager = class {
5079
5195
  console.warn(`[GatewayManager] Approval reply check failed: ${err.message}`);
5080
5196
  }
5081
5197
  const parsed = inboundToParsedEmail(mail);
5082
- const spamResult = scoreEmail(parsed);
5083
- if (spamResult.isSpam) {
5084
- console.warn(`[GatewayManager] Spam blocked (score=${spamResult.score}, category=${spamResult.topCategory}): "${mail.subject}" from ${mail.from}`);
5085
- if (mail.messageId) this.recordDelivery(mail.messageId, agentName);
5086
- return;
5198
+ const { isInternalEmail: isInternalEmail2 } = await Promise.resolve().then(() => (init_spam_filter(), spam_filter_exports));
5199
+ if (!isInternalEmail2(parsed)) {
5200
+ const spamResult = scoreEmail(parsed);
5201
+ if (spamResult.isSpam) {
5202
+ console.warn(`[GatewayManager] Spam blocked (score=${spamResult.score}, category=${spamResult.topCategory}): "${mail.subject}" from ${mail.from}`);
5203
+ if (mail.messageId) this.recordDelivery(mail.messageId, agentName);
5204
+ return;
5205
+ }
5087
5206
  }
5088
5207
  let agent = await this.accountManager.getByName(agentName);
5089
5208
  if (!agent && agentName !== DEFAULT_AGENT_NAME) {