@bobfrankston/mailx-types 0.1.13 → 0.1.15

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.
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Shared `contacts.jsonc` mutations — denylist + preferred.
3
+ *
4
+ * Pure logic with cloud I/O injected, so the desktop service
5
+ * (`mailx-service`, Node) and the Android service (`mailx-store-web`, in the
6
+ * WebView) run ONE implementation instead of each keeping its own copy.
7
+ * `web-service.ts` used to stub `addToDenylist` / `addPreferredContact` as
8
+ * `notImpl()`, which is why the phone's ⊘ / ★ buttons did nothing.
9
+ *
10
+ * Nothing here is platform-specific: it's a `cloudRead` → mutate → `cloudWrite`
11
+ * round-trip. Each caller passes its own platform's cloud functions.
12
+ */
13
+ export type CloudReadFn = (filename: string) => Promise<string | null>;
14
+ export type CloudWriteFn = (filename: string, content: string) => Promise<unknown>;
15
+ export interface PreferredContactEntry {
16
+ name: string;
17
+ email: string;
18
+ source?: string;
19
+ organization?: string;
20
+ }
21
+ /** Append an email to `contacts.jsonc#denylist[]` — idempotent and
22
+ * case-insensitive. Writes only when the entry is genuinely new. */
23
+ export declare function addContactsDenylistEntry(email: string, cloudRead: CloudReadFn, cloudWrite: CloudWriteFn): Promise<void>;
24
+ /** Append a preferred contact to `contacts.jsonc#preferred[]` — dedup by
25
+ * source|email|name. Writes only when the entry is genuinely new. */
26
+ export declare function addContactsPreferredEntry(entry: PreferredContactEntry, cloudRead: CloudReadFn, cloudWrite: CloudWriteFn): Promise<void>;
27
+ //# sourceMappingURL=contacts-config.d.ts.map
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Shared `contacts.jsonc` mutations — denylist + preferred.
3
+ *
4
+ * Pure logic with cloud I/O injected, so the desktop service
5
+ * (`mailx-service`, Node) and the Android service (`mailx-store-web`, in the
6
+ * WebView) run ONE implementation instead of each keeping its own copy.
7
+ * `web-service.ts` used to stub `addToDenylist` / `addPreferredContact` as
8
+ * `notImpl()`, which is why the phone's ⊘ / ★ buttons did nothing.
9
+ *
10
+ * Nothing here is platform-specific: it's a `cloudRead` → mutate → `cloudWrite`
11
+ * round-trip. Each caller passes its own platform's cloud functions.
12
+ */
13
+ /** Loose-parse a JSONC blob. The file mailx writes is plain JSON
14
+ * (`JSON.stringify`), but a hand-edit may add `//` comments or trailing
15
+ * commas — strip those and retry rather than losing the whole file. */
16
+ function parseContactsConfig(raw) {
17
+ if (!raw)
18
+ return {};
19
+ try {
20
+ return JSON.parse(raw) || {};
21
+ }
22
+ catch { /* fall through to loose */ }
23
+ try {
24
+ const stripped = raw
25
+ .replace(/^\s*\/\/.*$/gm, "") // line comments
26
+ .replace(/,(\s*[}\]])/g, "$1"); // trailing commas
27
+ return JSON.parse(stripped) || {};
28
+ }
29
+ catch {
30
+ return {};
31
+ }
32
+ }
33
+ /** Append an email to `contacts.jsonc#denylist[]` — idempotent and
34
+ * case-insensitive. Writes only when the entry is genuinely new. */
35
+ export async function addContactsDenylistEntry(email, cloudRead, cloudWrite) {
36
+ const lower = (email || "").trim().toLowerCase();
37
+ if (!lower)
38
+ return;
39
+ const cfg = parseContactsConfig(await cloudRead("contacts.jsonc"));
40
+ if (!Array.isArray(cfg.denylist))
41
+ cfg.denylist = [];
42
+ if (cfg.denylist.some((e) => (e || "").toLowerCase() === lower))
43
+ return;
44
+ cfg.denylist.push(email);
45
+ await cloudWrite("contacts.jsonc", JSON.stringify(cfg, null, 2));
46
+ }
47
+ /** Append a preferred contact to `contacts.jsonc#preferred[]` — dedup by
48
+ * source|email|name. Writes only when the entry is genuinely new. */
49
+ export async function addContactsPreferredEntry(entry, cloudRead, cloudWrite) {
50
+ if (!entry?.email)
51
+ return;
52
+ const cfg = parseContactsConfig(await cloudRead("contacts.jsonc"));
53
+ if (!Array.isArray(cfg.preferred))
54
+ cfg.preferred = [];
55
+ const key = (s, e, n) => `${(s || "preferred").toLowerCase()}|${(e || "").toLowerCase()}|${(n || "").toLowerCase()}`;
56
+ const dupKey = key(entry.source || "", entry.email, entry.name || "");
57
+ if (cfg.preferred.some((e) => key(e?.source || "", e?.email || "", e?.name || "") === dupKey))
58
+ return;
59
+ const row = { name: entry.name || "", email: entry.email };
60
+ if (entry.source)
61
+ row.source = entry.source;
62
+ if (entry.organization)
63
+ row.organization = entry.organization;
64
+ cfg.preferred.push(row);
65
+ await cloudWrite("contacts.jsonc", JSON.stringify(cfg, null, 2));
66
+ }
67
+ //# sourceMappingURL=contacts-config.js.map
package/index.d.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  */
6
6
  export { CONTACT_RULES } from "./contact-rules.js";
7
7
  export type { MailxApi } from "./mailx-api.js";
8
+ export { addContactsDenylistEntry, addContactsPreferredEntry, type PreferredContactEntry, type CloudReadFn, type CloudWriteFn, } from "./contacts-config.js";
8
9
  export { expandRecipients, splitRecipients, isAddressToken, extractAddress, } from "./groups.js";
9
10
  export type { GroupMap, RecipientToken, ExpansionResult } from "./groups.js";
10
11
  /** Supported authentication methods */
package/index.js CHANGED
@@ -7,6 +7,8 @@
7
7
  // this barrel so a single source-of-truth (contact-rules.jsonc) drives
8
8
  // junk-contact filtering on every platform.
9
9
  export { CONTACT_RULES } from "./contact-rules.js";
10
+ // Shared contacts.jsonc mutations — one implementation for desktop + Android.
11
+ export { addContactsDenylistEntry, addContactsPreferredEntry, } from "./contacts-config.js";
10
12
  // Group-name expansion for recipient fields. Lets users type a group name
11
13
  // (e.g. "family") in To/Cc/Bcc and have it expand to the address list at
12
14
  // send time. Both desktop and Android send paths consume this expander
@@ -80,6 +82,21 @@ export function sanitizeHtml(html) {
80
82
  return `url("") /* blocked: ${url} */`;
81
83
  });
82
84
  clean = clean.replace(/<\/?form\b[^>]*>/gi, "");
85
+ // Strip every interactive form element. Even without the surrounding
86
+ // <form>, an <input type="password"> in an email body is dangerous —
87
+ // WebView2's password manager offers to autofill it, and the user
88
+ // might type a real password into what looks like an in-email control.
89
+ // Same for <textarea>, <select>/<option>, and <button>. Bob 2026-05-14:
90
+ // observed a phishing-shaped "CHANGE YOUR PASSWORD" email rendering
91
+ // with working input fields + the OS autofill chip. Replace with a
92
+ // visible "[blocked: input]" marker so the user sees something was
93
+ // stripped (a silent removal would hide the phishing attempt entirely
94
+ // — they'd think "this email is just text" when it actually tried to
95
+ // capture credentials).
96
+ clean = clean.replace(/<input\b[^>]*\/?>/gi, "[blocked: form input]");
97
+ clean = clean.replace(/<textarea\b[^>]*>[\s\S]*?<\/textarea>/gi, "[blocked: textarea]");
98
+ clean = clean.replace(/<select\b[^>]*>[\s\S]*?<\/select>/gi, "[blocked: select]");
99
+ clean = clean.replace(/<button\b[^>]*>([\s\S]*?)<\/button>/gi, "$1");
83
100
  clean = clean.replace(/<iframe\b[^>]*>[\s\S]*?<\/iframe>/gi, "");
84
101
  return { html: clean, hasRemoteContent };
85
102
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx-types",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "type": "module",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",