@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.
- package/package.json +5 -5
- package/store.d.ts +9 -0
- 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.
|
|
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.
|
|
13
|
-
"@bobfrankston/mailx-settings": "^0.1.
|
|
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.
|
|
33
|
-
"@bobfrankston/mailx-settings": "^0.1.
|
|
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.
|