@bobfrankston/mailx-store 0.1.89 → 0.1.93

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.
Files changed (3) hide show
  1. package/package.json +5 -5
  2. package/store.d.ts +9 -0
  3. package/store.js +39 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx-store",
3
- "version": "0.1.89",
3
+ "version": "0.1.93",
4
4
  "type": "module",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -9,8 +9,8 @@
9
9
  },
10
10
  "license": "ISC",
11
11
  "dependencies": {
12
- "@bobfrankston/mailx-types": "^0.1.57",
13
- "@bobfrankston/mailx-settings": "^0.1.58",
12
+ "@bobfrankston/mailx-types": "^0.1.61",
13
+ "@bobfrankston/mailx-settings": "^0.1.60",
14
14
  "@bobfrankston/mailx-bus": "^0.1.2",
15
15
  "mailparser": "^3.7.2"
16
16
  },
@@ -29,8 +29,8 @@
29
29
  },
30
30
  ".transformedSnapshot": {
31
31
  "dependencies": {
32
- "@bobfrankston/mailx-types": "^0.1.57",
33
- "@bobfrankston/mailx-settings": "^0.1.58",
32
+ "@bobfrankston/mailx-types": "^0.1.61",
33
+ "@bobfrankston/mailx-settings": "^0.1.60",
34
34
  "@bobfrankston/mailx-bus": "^0.1.2",
35
35
  "mailparser": "^3.7.2"
36
36
  }
package/store.d.ts CHANGED
@@ -19,6 +19,8 @@
19
19
  * be able to consume it without depending on mailx-service — that's the
20
20
  * "arrow points the right way" property of the architecture.
21
21
  */
22
+ import { type TrustFinding } from "@bobfrankston/mailx-types";
23
+ import type { ParsedInvite } from "@bobfrankston/mailx-types";
22
24
  import { MailxDB } from "./db.js";
23
25
  import { FileMessageStore } from "./file-store.js";
24
26
  import type { StoreBus } from "./bus.js";
@@ -52,6 +54,9 @@ export interface StoreMessage extends MessageEnvelope {
52
54
  bodyOnDisk: boolean;
53
55
  deliveredTo: string;
54
56
  returnPath: string;
57
+ /** Evidence that this message is forged — empty for ordinary mail. See
58
+ * assessMessageTrust; the viewer renders these above the body. */
59
+ trust: TrustFinding[];
55
60
  listUnsubscribe: string;
56
61
  listUnsubscribeMail: string;
57
62
  listUnsubscribeHttp: string;
@@ -63,6 +68,10 @@ export interface StoreMessage extends MessageEnvelope {
63
68
  * back into saveDraft so the server-side dedup keeps replacing the
64
69
  * same draft instead of stacking up a new one every save. */
65
70
  mailxDraftId: string;
71
+ /** Parsed VEVENT when the message carries a text/calendar part, else null.
72
+ * Lets the viewer render an invite card instead of handing invite.ics to
73
+ * whatever program happens to own .ics (Bob 2026-08-22). */
74
+ invite: ParsedInvite | null;
66
75
  }
67
76
  export declare class Store {
68
77
  /** SQLite metadata index. Exposed as a public field — sync clients
package/store.js CHANGED
@@ -21,6 +21,8 @@
21
21
  */
22
22
  import * as fs from "node:fs";
23
23
  import { parseSerial } from "./parse-serial.js";
24
+ import { assessMessageTrust } from "@bobfrankston/mailx-types";
25
+ import { parseInvite } from "@bobfrankston/mailx-types";
24
26
  import { storeBus } from "./bus.js";
25
27
  import { sanitizeHtml, htmlToPlainText } from "@bobfrankston/mailx-types";
26
28
  import { loadSettings, loadAllowlist } from "@bobfrankston/mailx-settings";
@@ -291,6 +293,7 @@ export class Store {
291
293
  hasRemoteContent: false, remoteAllowed: allowRemote,
292
294
  attachments: [],
293
295
  cached: false,
296
+ invite: null,
294
297
  bodyOnDisk: false, // overridden to true on the parse-pending return
295
298
  deliveredTo: "", returnPath: "",
296
299
  listUnsubscribe: "", listUnsubscribeMail: "", listUnsubscribeHttp: "", listUnsubscribeOneClick: false,
@@ -445,6 +448,24 @@ export class Store {
445
448
  size: x.a.size || 0,
446
449
  contentId: x.a.contentId || "",
447
450
  }));
451
+ // Calendar invite. Read from the RAW attachment list, not the filtered
452
+ // one above: a text/calendar alternative has no filename and inline
453
+ // disposition, so the spurious-text-part filter (correctly) drops it
454
+ // from the attachment chips — which would also hide it from us.
455
+ // A malformed invite must never break rendering the message.
456
+ let invite = null;
457
+ try {
458
+ const calPart = (parsed.attachments || []).find((a) => /text\/calendar|application\/ics/i.test(a.contentType || "")
459
+ || /\.ics$/i.test(a.filename || ""));
460
+ if (calPart) {
461
+ const c = calPart.content;
462
+ const text = Buffer.isBuffer(c) ? c.toString("utf8") : String(c ?? "");
463
+ invite = parseInvite(text);
464
+ }
465
+ }
466
+ catch (e) {
467
+ console.warn("[store] invite parse failed:", e?.message || e);
468
+ }
448
469
  // Header extraction — Delivered-To, Return-Path, List-Unsubscribe.
449
470
  // Each delivery agent PREPENDS its Delivered-To, so the FIRST
450
471
  // (topmost) header is the final delivery to the user's actual
@@ -512,6 +533,22 @@ export class Store {
512
533
  if (dAddr && recipients.includes(dAddr))
513
534
  allowRemote = true;
514
535
  }
536
+ // Forgery evidence. Runs on the RAW html on purpose: sanitizeHtml
537
+ // disarms the image-map overlay, so by the time bodyHtml is safe the
538
+ // thing worth reporting is gone. Header-only checks would be immune,
539
+ // but the link checks would silently stop firing.
540
+ //
541
+ // "Own addresses" is the union of the recipient allowlist and the
542
+ // address this copy was delivered to. Delivered-To matters most: it
543
+ // needs no configuration and it is exactly the address a self-spoof
544
+ // forges, so an alias the user never got round to listing is still
545
+ // covered. (Claude Code 2026-08-25)
546
+ const trust = assessMessageTrust({
547
+ headerLines: (parsed.headerLines || []).map(h => ({ key: h.key, line: h.line })),
548
+ fromAddress: envelope.from?.address || "",
549
+ ownAddresses: [...recipients, (deliveredTo.match(/[^\s<>]+@[^\s<>]+/) || [""])[0]].filter(Boolean),
550
+ bodyHtml,
551
+ });
515
552
  if (bodyHtml && !allowRemote) {
516
553
  const result = sanitizeHtml(bodyHtml);
517
554
  bodyHtml = result.html;
@@ -523,7 +560,7 @@ export class Store {
523
560
  hasRemoteContent, remoteAllowed: allowRemote,
524
561
  attachments,
525
562
  cached: true,
526
- deliveredTo, returnPath,
563
+ deliveredTo, returnPath, trust,
527
564
  listUnsubscribe, listUnsubscribeMail, listUnsubscribeHttp, listUnsubscribeOneClick,
528
565
  // body_path in the DB is stored relative to the body-store
529
566
  // basePath; resolve to absolute so the UI's "Source" button
@@ -532,6 +569,7 @@ export class Store {
532
569
  emlPath: this.bodyStore.absolutePath(storedPath),
533
570
  isFlagged,
534
571
  mailxDraftId,
572
+ invite,
535
573
  };
536
574
  // Memoize for instant re-view of the same message. cacheKey is
537
575
  // path+mtime so a re-fetch (mtime changes) invalidates naturally.