@agenticmail/core 0.5.42 → 0.5.44
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/chunk-TIAKW5DC.js +623 -0
- package/dist/index.cjs +792 -673
- package/dist/index.d.cts +12 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +166 -675
- package/dist/spam-filter-L6KNZ7QI.js +13 -0
- package/package.json +1 -1
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
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
|
149
|
-
|
|
150
|
-
const
|
|
151
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
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) =>
|
|
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
|
|
4324
|
+
const salt = randomBytes2(16);
|
|
4325
|
+
const derivedKey = deriveKey(key, salt);
|
|
4849
4326
|
const iv = randomBytes2(12);
|
|
4850
|
-
const cipher = createCipheriv("aes-256-gcm",
|
|
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 `
|
|
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 (
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
|
|
4860
|
-
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
|
|
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
|
|
4954
|
-
if (
|
|
4955
|
-
|
|
4956
|
-
if (
|
|
4957
|
-
|
|
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) {
|