@bobfrankston/rmfmail 1.1.233 → 1.1.235

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.
@@ -255,17 +255,22 @@ export class Store {
255
255
  const allowList = this.getCachedAllowlist() as any;
256
256
  const senderAddr = (envelope.from?.address || "").toLowerCase();
257
257
  const senderDomain = senderAddr.split("@")[1] || "";
258
- const toAddrs = (envelope.to || []).map((a: any) => (a.address || "").toLowerCase());
258
+ // Recipient allowlist matches against every recipient-ish address, not
259
+ // just To: a mailing list often carries other subscribers in To/Cc and
260
+ // delivers to the user's allowlisted alias only via Delivered-To (added
261
+ // post-parse below).
262
+ const recipients = (allowList.recipients || []).map((r: string) => (r || "").toLowerCase());
263
+ const rcptAddrs = [...(envelope.to || []), ...(envelope.cc || [])]
264
+ .map((a: any) => (a.address || "").toLowerCase());
259
265
 
260
266
  // Allowlist auto-allow: trusted sender / domain / recipient skips
261
267
  // sanitization. Same rule as the legacy service implementation.
262
268
  if (!allowRemote) {
263
269
  const senders = (allowList.senders || []).map((s: string) => (s || "").toLowerCase());
264
270
  const domains = (allowList.domains || []).map((d: string) => (d || "").toLowerCase());
265
- const recipients = (allowList.recipients || []).map((r: string) => (r || "").toLowerCase());
266
271
  if (senders.includes(senderAddr) ||
267
272
  domains.includes(senderDomain) ||
268
- toAddrs.some((a: string) => recipients.includes(a))) {
273
+ rcptAddrs.some((a: string) => recipients.includes(a))) {
269
274
  allowRemote = true;
270
275
  }
271
276
  }
@@ -307,8 +312,12 @@ export class Store {
307
312
  const cached = this.parsedLruGet(cacheKey);
308
313
  if (cached) {
309
314
  // Allowlist state can change between views even with the same
310
- // body cached; recompute the volatile fields and overlay.
311
- return { ...cached, remoteAllowed: allowRemote, isFlagged };
315
+ // body cached; recompute the volatile fields and overlay. OR with
316
+ // the cached flag: if the body was already parsed-and-allowed (e.g.
317
+ // via a Delivered-To recipient match, which the early envelope-only
318
+ // check can't see), keep it allowed — the cached HTML is unsanitized
319
+ // and the flag must agree.
320
+ return { ...cached, remoteAllowed: allowRemote || cached.remoteAllowed, isFlagged };
312
321
  }
313
322
 
314
323
  // Synchronous parse. The .eml is on disk; read + parse it inline
@@ -408,12 +417,6 @@ export class Store {
408
417
  contentId: x.a.contentId || "",
409
418
  }));
410
419
 
411
- if (bodyHtml && !allowRemote) {
412
- const result = sanitizeHtml(bodyHtml);
413
- bodyHtml = result.html;
414
- hasRemoteContent = result.hasRemoteContent;
415
- }
416
-
417
420
  // Header extraction — Delivered-To, Return-Path, List-Unsubscribe.
418
421
  // Each delivery agent PREPENDS its Delivered-To, so the FIRST
419
422
  // (topmost) header is the final delivery to the user's actual
@@ -421,6 +424,8 @@ export class Store {
421
424
  // routing artifacts. Take [0]. (The old code took the LAST entry —
422
425
  // on a forwarded message that grabbed a stale hop, e.g. a malformed
423
426
  // `…@elkin.ws@trap-prot` address — Bob 2026-05-21.)
427
+ // This runs BEFORE sanitization because Delivered-To feeds the
428
+ // recipient-allowlist re-check just below.
424
429
  let deliveredTo = "";
425
430
  const rawDelivered = parsed.headers.get("delivered-to");
426
431
  if (rawDelivered) {
@@ -447,6 +452,22 @@ export class Store {
447
452
  parseListUnsubscribe(parsed.headers);
448
453
  const listUnsubscribe = listUnsubscribeHttp || listUnsubscribeMail;
449
454
 
455
+ // Recipient-allowlist re-check with Delivered-To in hand. The early
456
+ // pass (envelope only) can't see Delivered-To, so a list message whose
457
+ // To/Cc are other subscribers and whose only allowlisted address is the
458
+ // Delivered-To alias (e.g. shsaa@bob.ma) would still be sanitized.
459
+ // Match the bare address out of "Name <addr>" / "<addr>" / "addr".
460
+ if (!allowRemote && deliveredTo) {
461
+ const dAddr = (deliveredTo.match(/[^\s<>]+@[^\s<>]+/)?.[0] || "").toLowerCase();
462
+ if (dAddr && recipients.includes(dAddr)) allowRemote = true;
463
+ }
464
+
465
+ if (bodyHtml && !allowRemote) {
466
+ const result = sanitizeHtml(bodyHtml);
467
+ bodyHtml = result.html;
468
+ hasRemoteContent = result.hasRemoteContent;
469
+ }
470
+
450
471
  const result: StoreMessage = {
451
472
  ...envelope,
452
473
  bodyHtml, bodyText,