@bobfrankston/rmfmail 1.2.50 → 1.2.66
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/client/android-bootstrap.bundle.js +63 -2
- package/client/android-bootstrap.bundle.js.map +4 -4
- package/client/app.bundle.js +417 -63
- package/client/app.bundle.js.map +3 -3
- package/client/app.js +182 -24
- package/client/app.js.map +1 -1
- package/client/app.ts +166 -21
- package/client/components/calendar-sidebar.js +5 -5
- package/client/components/calendar-sidebar.js.map +1 -1
- package/client/components/calendar-sidebar.ts +5 -5
- package/client/components/folder-tree.js +81 -0
- package/client/components/folder-tree.js.map +1 -1
- package/client/components/folder-tree.ts +70 -0
- package/client/components/message-list.js +282 -38
- package/client/components/message-list.js.map +1 -1
- package/client/components/message-list.ts +264 -36
- package/client/compose/compose.bundle.js +70 -14
- package/client/compose/compose.bundle.js.map +2 -2
- package/client/compose/compose.css +28 -7
- package/client/compose/compose.js +64 -4
- package/client/compose/compose.js.map +1 -1
- package/client/compose/compose.ts +66 -4
- package/client/compose/editor.js +23 -7
- package/client/compose/editor.js.map +1 -1
- package/client/compose/editor.ts +23 -7
- package/client/help/search-help.js +4 -3
- package/client/help/search-help.js.map +1 -1
- package/client/help/search-help.ts +4 -3
- package/client/index.html +5 -1
- package/client/lib/api-client.js +4 -2
- package/client/lib/api-client.js.map +1 -1
- package/client/lib/api-client.ts +4 -2
- package/client/lib/mailxapi.js +2 -2
- package/client/lib/rmf-tiny.js +39 -6
- package/client/styles/components.css +11 -0
- package/docs/azure.md +58 -0
- package/docs/config-help.md +131 -0
- package/docs/edit-in-word-docx.md +98 -0
- package/docs/host-abstraction-plan.md +169 -0
- package/docs/local-first-plan.md +303 -0
- package/docs/npmglobalize-transitive-workspace-deps.md +107 -0
- package/docs/outlook.md +80 -18
- package/docs/outlook.txt +35 -0
- package/docs/rules-design.md +172 -0
- package/package.json +9 -9
- package/packages/mailx-imap/index.d.ts +48 -1
- package/packages/mailx-imap/index.d.ts.map +1 -1
- package/packages/mailx-imap/index.js +320 -68
- package/packages/mailx-imap/index.js.map +1 -1
- package/packages/mailx-imap/index.ts +311 -65
- package/packages/mailx-imap/microsoft-credentials.json +8 -0
- package/packages/mailx-imap/package-lock.json +2 -2
- package/packages/mailx-imap/package.json +1 -1
- package/packages/mailx-imap/providers/outlook-api.d.ts +7 -0
- package/packages/mailx-imap/providers/outlook-api.d.ts.map +1 -0
- package/packages/mailx-imap/providers/outlook-api.js +7 -0
- package/packages/mailx-imap/providers/outlook-api.js.map +1 -0
- package/packages/mailx-service/index.d.ts +7 -1
- package/packages/mailx-service/index.d.ts.map +1 -1
- package/packages/mailx-service/index.js +8 -8
- package/packages/mailx-service/index.js.map +1 -1
- package/packages/mailx-service/index.ts +8 -7
- package/packages/mailx-service/jsonrpc.js +1 -1
- package/packages/mailx-service/jsonrpc.js.map +1 -1
- package/packages/mailx-service/jsonrpc.ts +1 -1
- package/packages/mailx-settings/docs/azure.md +58 -0
- package/packages/mailx-settings/docs/config-help.md +131 -0
- package/packages/mailx-settings/docs/edit-in-word-docx.md +98 -0
- package/packages/mailx-settings/docs/host-abstraction-plan.md +169 -0
- package/packages/mailx-settings/docs/local-first-plan.md +303 -0
- package/packages/mailx-settings/docs/npmglobalize-transitive-workspace-deps.md +107 -0
- package/packages/mailx-settings/docs/outlook.md +80 -18
- package/packages/mailx-settings/docs/rules-design.md +172 -0
- package/packages/mailx-settings/index.d.ts.map +1 -1
- package/packages/mailx-settings/index.js +9 -8
- package/packages/mailx-settings/index.js.map +1 -1
- package/packages/mailx-settings/index.ts +9 -8
- package/packages/mailx-settings/package.json +1 -1
- package/packages/mailx-store/db.d.ts +36 -1
- package/packages/mailx-store/db.d.ts.map +1 -1
- package/packages/mailx-store/db.js +100 -6
- package/packages/mailx-store/db.js.map +1 -1
- package/packages/mailx-store/db.ts +100 -6
- package/packages/mailx-store/package.json +1 -1
- package/packages/mailx-types/contact-rules.d.ts +1 -1
- package/packages/mailx-types/contact-rules.js +1 -1
- package/packages/mailx-types/contact-rules.js.map +1 -1
- package/packages/mailx-types/contact-rules.jsonc +1 -1
- package/packages/mailx-types/contact-rules.ts +1 -1
- package/packages/mailx-types/package.json +1 -1
|
@@ -67,6 +67,24 @@ function isJunkContact(email: string, name: string): boolean {
|
|
|
67
67
|
for (const re of _denyPatterns) { if (re.test(lower)) return true; }
|
|
68
68
|
return false;
|
|
69
69
|
}
|
|
70
|
+
/** Sanitize a harvested display name. A real display name carries none of
|
|
71
|
+
* `@ < > |` — their presence means an address parser mis-split a header
|
|
72
|
+
* (e.g. a List-Id / "domain | Name@addr" fragment) and the "name" now
|
|
73
|
+
* embeds structural mail punctuation or a whole email address. The worst
|
|
74
|
+
* case is an embedded address that DIFFERS from this contact's own email:
|
|
75
|
+
* autocomplete then shows a TRUSTED name while sending ELSEWHERE. Live
|
|
76
|
+
* example (Bob 2026-06-22): a `discovered` row name `bob.ma | Bob@bob.ma`
|
|
77
|
+
* was wired to `miyakot@kt.rim.or.jp`, so test mail Bob addressed to
|
|
78
|
+
* himself silently went to a stranger in Japan ("MIA"). Drop the name;
|
|
79
|
+
* keep the contact (the email is still useful) — autocomplete falls back
|
|
80
|
+
* to showing the bare address, which the user won't mistake for someone. */
|
|
81
|
+
export function cleanContactName(name: string): string {
|
|
82
|
+
const n = (name || "").trim().replace(/^"(.*)"$/, "$1").trim();
|
|
83
|
+
if (!n) return "";
|
|
84
|
+
if (/[@<>|]/.test(n)) return "";
|
|
85
|
+
return n;
|
|
86
|
+
}
|
|
87
|
+
|
|
70
88
|
/** User-configured patterns from contacts.jsonc `denylistPatterns`. Compiled
|
|
71
89
|
* once on settings load; invalid regexes are skipped with a warning. */
|
|
72
90
|
export function setContactsDenyPatterns(patterns: string[]): void {
|
|
@@ -792,14 +810,21 @@ export class MailxDB {
|
|
|
792
810
|
const last = this.getKv("contacts", "purge_v");
|
|
793
811
|
if (last === TARGET) return;
|
|
794
812
|
let dropped = 0;
|
|
813
|
+
let renamed = 0;
|
|
795
814
|
try {
|
|
796
815
|
const rows = this.db.prepare("SELECT id, email, name FROM contacts WHERE source IN ('discovered','sent','received')").all() as { id: number; email: string; name: string }[];
|
|
797
816
|
const del = this.db.prepare("DELETE FROM contacts WHERE id = ?");
|
|
817
|
+
const blank = this.db.prepare("UPDATE contacts SET name = '' WHERE id = ?");
|
|
798
818
|
for (const r of rows) {
|
|
799
|
-
if (isJunkContact(r.email || "", r.name || "")) { del.run(r.id); dropped++; }
|
|
819
|
+
if (isJunkContact(r.email || "", r.name || "")) { del.run(r.id); dropped++; continue; }
|
|
820
|
+
// Blank mis-parsed display names that embed `@ < > |` — these
|
|
821
|
+
// can impersonate one identity while sending to another (the
|
|
822
|
+
// "bob.ma | Bob@bob.ma" → miyakot@kt.rim.or.jp cross-wire,
|
|
823
|
+
// Bob 2026-06-22). Keep the contact; drop only the unsafe name.
|
|
824
|
+
if (r.name && cleanContactName(r.name) !== r.name) { blank.run(r.id); renamed++; }
|
|
800
825
|
}
|
|
801
826
|
this.setKv("contacts", "purge_v", TARGET);
|
|
802
|
-
if (dropped > 0) console.log(` [contacts] one-shot purge: dropped ${dropped} junk rows (${TARGET})`);
|
|
827
|
+
if (dropped > 0 || renamed > 0) console.log(` [contacts] one-shot purge: dropped ${dropped} junk rows, blanked ${renamed} unsafe names (${TARGET})`);
|
|
803
828
|
} catch (e: any) {
|
|
804
829
|
console.warn(`[contacts] one-shot purge failed: ${e.message}`);
|
|
805
830
|
}
|
|
@@ -1379,6 +1404,55 @@ export class MailxDB {
|
|
|
1379
1404
|
return Number(result.lastInsertRowid);
|
|
1380
1405
|
}
|
|
1381
1406
|
|
|
1407
|
+
/** Rename a folder row's path (+leaf name) and rewrite every descendant
|
|
1408
|
+
* folder whose path lives under the old prefix. A single IMAP RENAME on the
|
|
1409
|
+
* server renames child mailboxes too (RFC 3501), and Gmail/Outlook rename
|
|
1410
|
+
* cascades the same way, so the local mirror must move the whole subtree in
|
|
1411
|
+
* lockstep. All in one transaction so a crash can't leave half the subtree
|
|
1412
|
+
* pointing at the old prefix.
|
|
1413
|
+
*
|
|
1414
|
+
* `newName` is the new LEAF name written to the renamed row's `name`
|
|
1415
|
+
* column; descendant rows keep their own leaf names (only the prefix
|
|
1416
|
+
* changes). The (account_id, path) UNIQUE constraint means the caller must
|
|
1417
|
+
* have already verified newPath is free. */
|
|
1418
|
+
renameFolderPath(accountId: string, oldPath: string, newPath: string, newName: string, delimiter: string): void {
|
|
1419
|
+
const childPrefix = oldPath + delimiter;
|
|
1420
|
+
this.db.exec("BEGIN");
|
|
1421
|
+
try {
|
|
1422
|
+
// The folder itself — path + leaf name both change.
|
|
1423
|
+
this.db.prepare(
|
|
1424
|
+
"UPDATE folders SET path = ?, name = ? WHERE account_id = ? AND path = ?"
|
|
1425
|
+
).run(newPath, newName, accountId, oldPath);
|
|
1426
|
+
// Descendants — rewrite only the path prefix, keep their leaf names.
|
|
1427
|
+
// SUBSTR(path, len(childPrefix)+1) is the part after the old prefix.
|
|
1428
|
+
const descendants = this.db.prepare(
|
|
1429
|
+
"SELECT id, path FROM folders WHERE account_id = ? AND path LIKE ? ESCAPE '\\'"
|
|
1430
|
+
).all(accountId, childPrefix.replace(/[\\%_]/g, "\\$&") + "%") as { id: number; path: string }[];
|
|
1431
|
+
const upd = this.db.prepare("UPDATE folders SET path = ? WHERE id = ?");
|
|
1432
|
+
for (const d of descendants) {
|
|
1433
|
+
const rest = d.path.slice(oldPath.length); // includes the leading delimiter
|
|
1434
|
+
upd.run(newPath + rest, d.id);
|
|
1435
|
+
}
|
|
1436
|
+
this.db.exec("COMMIT");
|
|
1437
|
+
} catch (e) {
|
|
1438
|
+
this.db.exec("ROLLBACK");
|
|
1439
|
+
throw e;
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
/** Update only a folder's display `name`, leaving `path` untouched. For
|
|
1444
|
+
* providers whose `path` is an opaque, rename-stable id (Outlook/Graph:
|
|
1445
|
+
* path = folder id), a rename/reparent changes the display name but NOT
|
|
1446
|
+
* the id — so synthesizing a new hierarchical path (as renameFolderPath
|
|
1447
|
+
* does) would corrupt the id-as-path invariant every later Graph op relies
|
|
1448
|
+
* on. This updates the name in place. No descendant rewrite: opaque-id
|
|
1449
|
+
* children don't carry the parent in their path. */
|
|
1450
|
+
renameFolderName(accountId: string, folderId: number, newName: string): void {
|
|
1451
|
+
this.db.prepare(
|
|
1452
|
+
"UPDATE folders SET name = ? WHERE account_id = ? AND id = ?"
|
|
1453
|
+
).run(newName, accountId, folderId);
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1382
1456
|
getFolders(accountId: string): Folder[] {
|
|
1383
1457
|
const rows = this.db.prepare(
|
|
1384
1458
|
"SELECT * FROM folders WHERE account_id = ? ORDER BY path"
|
|
@@ -2823,6 +2897,7 @@ export class MailxDB {
|
|
|
2823
2897
|
const lower = email.toLowerCase();
|
|
2824
2898
|
if (this.isAddressDenylisted(lower)) return;
|
|
2825
2899
|
if (isJunkContact(lower, name)) return;
|
|
2900
|
+
name = cleanContactName(name);
|
|
2826
2901
|
const now = Date.now();
|
|
2827
2902
|
// discovered tier holds one row per email — bump if present, else
|
|
2828
2903
|
// insert. Doesn't touch preferred or google rows for the same email;
|
|
@@ -2937,6 +3012,7 @@ export class MailxDB {
|
|
|
2937
3012
|
if (!email || !VALID.test(email)) return;
|
|
2938
3013
|
if (isJunkContact(email, name)) return;
|
|
2939
3014
|
if (this.isAddressDenylisted(email)) return;
|
|
3015
|
+
name = cleanContactName(name);
|
|
2940
3016
|
const e = agg.get(email);
|
|
2941
3017
|
if (e) {
|
|
2942
3018
|
e.cnt++;
|
|
@@ -3477,6 +3553,7 @@ export class MailxDB {
|
|
|
3477
3553
|
for (const part of parts) {
|
|
3478
3554
|
const fromMatch = part.match(/^from:(.+)$/i);
|
|
3479
3555
|
const toMatch = part.match(/^to:(.+)$/i);
|
|
3556
|
+
const ccMatch = part.match(/^cc:(.+)$/i);
|
|
3480
3557
|
const subjectMatch = part.match(/^subject:(.+)$/i);
|
|
3481
3558
|
const dateMatch = part.match(/^date:([><]?=?)(.+)$/i);
|
|
3482
3559
|
const afterMatch = part.match(/^after:(.+)$/i);
|
|
@@ -3491,8 +3568,15 @@ export class MailxDB {
|
|
|
3491
3568
|
const term = ftsClean(fromMatch[1]);
|
|
3492
3569
|
if (term) frags.push({ s: `{from_name from_address}:${term}*`, op: false });
|
|
3493
3570
|
} else if (toMatch) {
|
|
3571
|
+
// `to:` deliberately spans To AND Cc — when someone searches
|
|
3572
|
+
// "to:bob" they mean "addressed to bob", and being Cc'd counts.
|
|
3573
|
+
// Use `cc:` for a Cc-only match. (Bcc isn't indexed: it's absent
|
|
3574
|
+
// on received mail by design, and only meaningful in Sent.)
|
|
3494
3575
|
const term = ftsClean(toMatch[1]);
|
|
3495
3576
|
if (term) frags.push({ s: `{to_text cc_text}:${term}*`, op: false });
|
|
3577
|
+
} else if (ccMatch) {
|
|
3578
|
+
const term = ftsClean(ccMatch[1]);
|
|
3579
|
+
if (term) frags.push({ s: `cc_text:${term}*`, op: false });
|
|
3496
3580
|
} else if (subjectMatch) {
|
|
3497
3581
|
const term = ftsClean(subjectMatch[1]);
|
|
3498
3582
|
if (term) frags.push({ s: `subject:${term}*`, op: false });
|
|
@@ -3564,6 +3648,9 @@ export class MailxDB {
|
|
|
3564
3648
|
ftsQuery += sep + frags[i].s;
|
|
3565
3649
|
}
|
|
3566
3650
|
ftsQuery = ftsQuery.trim();
|
|
3651
|
+
// (Header-scoped search is reachable via the `subject:`/`from:`/`to:`
|
|
3652
|
+
// qualifiers — no separate "headers only" mode needed, and body search
|
|
3653
|
+
// isn't slow enough to warrant one.)
|
|
3567
3654
|
// No real FTS term — either qualifier-only ("is:flagged after:1w") or
|
|
3568
3655
|
// every term sanitized away (user typed only punctuation). There is NO
|
|
3569
3656
|
// valid FTS5 "match everything" string — `MATCH '*'` is itself an FTS5
|
|
@@ -3659,13 +3746,20 @@ export class MailxDB {
|
|
|
3659
3746
|
}
|
|
3660
3747
|
}
|
|
3661
3748
|
|
|
3662
|
-
/** Rebuild FTS index from existing messages
|
|
3749
|
+
/** Rebuild FTS index from existing messages.
|
|
3750
|
+
* body_text is seeded from `preview` here (all db.ts can reach); the full
|
|
3751
|
+
* message body is backfilled separately by mailx-imap's backfillFtsBodies()
|
|
3752
|
+
* pass, which can read the cached .eml. */
|
|
3663
3753
|
rebuildSearchIndex(): number {
|
|
3664
|
-
// Drop and recreate
|
|
3754
|
+
// Drop and recreate. MUST be the STANDALONE schema (no content=messages):
|
|
3755
|
+
// the external-content form indexes to_text/cc_text/body_text columns
|
|
3756
|
+
// that don't exist on `messages`, so content-dereferencing silently
|
|
3757
|
+
// drops those terms — the exact breakage migrateFtsSchema() repairs.
|
|
3758
|
+
// Recreating it here used to quietly reintroduce that bug on every
|
|
3759
|
+
// -reindex (Bob 2026-06-05 "search missed to/cc/body").
|
|
3665
3760
|
try { this.db.exec("DROP TABLE IF EXISTS messages_fts"); } catch { /* ignore */ }
|
|
3666
3761
|
this.db.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
|
|
3667
|
-
subject, from_name, from_address, to_text, cc_text, body_text
|
|
3668
|
-
content=messages, content_rowid=id
|
|
3762
|
+
subject, from_name, from_address, to_text, cc_text, body_text
|
|
3669
3763
|
)`);
|
|
3670
3764
|
|
|
3671
3765
|
// Use a single transaction + prepared statement for speed (~50x faster than individual inserts)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export declare const CONTACT_RULES: {
|
|
2
|
-
readonly rulesVersion: "
|
|
2
|
+
readonly rulesVersion: "v4-name-hygiene";
|
|
3
3
|
readonly junk: {
|
|
4
4
|
readonly localExact: "^(no-?reply|do-?not-?reply|noreply|mailer-daemon|postmaster|abuse|automated|bounce(s|d)?|list-?(server|admin|owner|manager)?|notification|notifications?|admin@.*automated|root|daemon|nobody|undisclosed)$";
|
|
5
5
|
readonly localSuffix: "(-bounces|\\+bounces|-noreply|-no-reply|-notifications?|-mailer)$";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// AUTO-GENERATED from contact-rules.jsonc — do not edit.
|
|
2
2
|
// Edit the .jsonc and run `node build-rules.js` (or `npm run build`).
|
|
3
3
|
export const CONTACT_RULES = {
|
|
4
|
-
"rulesVersion": "
|
|
4
|
+
"rulesVersion": "v4-name-hygiene",
|
|
5
5
|
"junk": {
|
|
6
6
|
"localExact": "^(no-?reply|do-?not-?reply|noreply|mailer-daemon|postmaster|abuse|automated|bounce(s|d)?|list-?(server|admin|owner|manager)?|notification|notifications?|admin@.*automated|root|daemon|nobody|undisclosed)$",
|
|
7
7
|
"localSuffix": "(-bounces|\\+bounces|-noreply|-no-reply|-notifications?|-mailer)$",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contact-rules.js","sourceRoot":"","sources":["contact-rules.ts"],"names":[],"mappings":"AAAA,yDAAyD;AACzD,sEAAsE;AAEtE,MAAM,CAAC,MAAM,aAAa,GAAG;IACzB,cAAc,EAAE,
|
|
1
|
+
{"version":3,"file":"contact-rules.js","sourceRoot":"","sources":["contact-rules.ts"],"names":[],"mappings":"AAAA,yDAAyD;AACzD,sEAAsE;AAEtE,MAAM,CAAC,MAAM,aAAa,GAAG;IACzB,cAAc,EAAE,iBAAiB;IACjC,MAAM,EAAE;QACJ,YAAY,EAAE,6MAA6M;QAC3N,aAAa,EAAE,mEAAmE;QAClF,aAAa,EAAE,4FAA4F;QAC3G,aAAa,EAAE,8CAA8C;QAC7D,QAAQ,EAAE,8EAA8E;KAC3F;CACK,CAAC"}
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
// (e.g. "16179691997.15082868100.nfblcbll1x")
|
|
18
18
|
// domain — entire domain match (Google Voice SMS, reply.fb.com)
|
|
19
19
|
{
|
|
20
|
-
"rulesVersion": "
|
|
20
|
+
"rulesVersion": "v4-name-hygiene",
|
|
21
21
|
"junk": {
|
|
22
22
|
"localExact": "^(no-?reply|do-?not-?reply|noreply|mailer-daemon|postmaster|abuse|automated|bounce(s|d)?|list-?(server|admin|owner|manager)?|notification|notifications?|admin@.*automated|root|daemon|nobody|undisclosed)$",
|
|
23
23
|
"localSuffix": "(-bounces|\\+bounces|-noreply|-no-reply|-notifications?|-mailer)$",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Edit the .jsonc and run `node build-rules.js` (or `npm run build`).
|
|
3
3
|
|
|
4
4
|
export const CONTACT_RULES = {
|
|
5
|
-
"rulesVersion": "
|
|
5
|
+
"rulesVersion": "v4-name-hygiene",
|
|
6
6
|
"junk": {
|
|
7
7
|
"localExact": "^(no-?reply|do-?not-?reply|noreply|mailer-daemon|postmaster|abuse|automated|bounce(s|d)?|list-?(server|admin|owner|manager)?|notification|notifications?|admin@.*automated|root|daemon|nobody|undisclosed)$",
|
|
8
8
|
"localSuffix": "(-bounces|\\+bounces|-noreply|-no-reply|-notifications?|-mailer)$",
|