@bobfrankston/mailx-store 0.1.5 → 0.1.8
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/db.d.ts +32 -0
- package/db.js +128 -7
- package/file-store.d.ts +31 -13
- package/file-store.js +75 -31
- package/package.json +8 -5
package/db.d.ts
CHANGED
|
@@ -4,12 +4,30 @@
|
|
|
4
4
|
* Message bodies are NOT here -- they live in the MessageStore backend.
|
|
5
5
|
*/
|
|
6
6
|
import type { MessageEnvelope, Folder, EmailAddress, PagedResult, MessageQuery } from "@bobfrankston/mailx-types";
|
|
7
|
+
/** User-configured patterns from contacts.jsonc `denylistPatterns`. Compiled
|
|
8
|
+
* once on settings load; invalid regexes are skipped with a warning. */
|
|
9
|
+
export declare function setContactsDenyPatterns(patterns: string[]): void;
|
|
7
10
|
export declare class MailxDB {
|
|
8
11
|
private db;
|
|
9
12
|
constructor(dbDir: string);
|
|
10
13
|
/** Fail loud + early if expected columns are missing. Cheap (PRAGMA only
|
|
11
14
|
* runs at startup). The user-facing message names the recovery command. */
|
|
12
15
|
private verifySchema;
|
|
16
|
+
/** One-shot: rewrite absolute body_path entries to relative-to-store
|
|
17
|
+
* paths. Idempotent; flag stored in kv. Caller passes a FileMessageStore
|
|
18
|
+
* whose `rewriteAbsoluteToRelative` does the actual mapping (mailx-store
|
|
19
|
+
* doesn't depend on file-store directly to avoid a cycle). */
|
|
20
|
+
runOneShotBodyPathMigration(rewriteFn: (rows: Array<{
|
|
21
|
+
id: number;
|
|
22
|
+
body_path: string;
|
|
23
|
+
}>, update: (id: number, p: string) => void) => number): void;
|
|
24
|
+
/** One-shot purge: when the in-DB filter rules tighten (new prefix or
|
|
25
|
+
* domain matchers), historical contacts harvested under the old rules
|
|
26
|
+
* remain in the table and keep being written back to contacts.jsonc on
|
|
27
|
+
* every cloud save. Bumping the version below re-runs the purge once
|
|
28
|
+
* per device. The kv flag (`contacts:purge_v`) records the last version
|
|
29
|
+
* that ran so we don't keep doing the work on every startup. */
|
|
30
|
+
private runOneShotJunkContactPurge;
|
|
13
31
|
/** Fetch a string from the kv table. Returns null when not set. */
|
|
14
32
|
getKv(scope: string, key: string): string | null;
|
|
15
33
|
/** Upsert a kv row. Pass `null` to delete. */
|
|
@@ -201,6 +219,17 @@ export declare class MailxDB {
|
|
|
201
219
|
private _denylist;
|
|
202
220
|
isAddressDenylisted(emailLower: string): boolean;
|
|
203
221
|
setContactsDenylist(emails: string[]): void;
|
|
222
|
+
/** Priority sender / domain index — populated from contacts.jsonc on reload.
|
|
223
|
+
* Used by the client to flag list rows. Source of truth is the JSONC; this
|
|
224
|
+
* is just a fast in-memory lookup. */
|
|
225
|
+
private _prioritySenders;
|
|
226
|
+
private _priorityDomains;
|
|
227
|
+
setPriorityIndex(senders: string[], domains: string[]): void;
|
|
228
|
+
getPriorityIndex(): {
|
|
229
|
+
senders: string[];
|
|
230
|
+
domains: string[];
|
|
231
|
+
};
|
|
232
|
+
isPrioritySender(addr: string): boolean;
|
|
204
233
|
/** Callback fired when local-DB contacts mutations happen (sends adding
|
|
205
234
|
* to discovered, corpus seeder finding new addresses). The service
|
|
206
235
|
* registers a debounced cloud flush here so the GDrive copy stays in
|
|
@@ -237,8 +266,11 @@ export declare class MailxDB {
|
|
|
237
266
|
source?: string;
|
|
238
267
|
organization?: string;
|
|
239
268
|
org?: string;
|
|
269
|
+
priority?: boolean;
|
|
240
270
|
}[];
|
|
241
271
|
denylist?: string[];
|
|
272
|
+
denylistPatterns?: string[];
|
|
273
|
+
priorityDomains?: string[];
|
|
242
274
|
discovered?: {
|
|
243
275
|
name?: string;
|
|
244
276
|
email: string;
|
package/db.js
CHANGED
|
@@ -7,18 +7,33 @@ import { DatabaseSync } from "node:sqlite";
|
|
|
7
7
|
import { randomUUID } from "node:crypto";
|
|
8
8
|
import * as path from "node:path";
|
|
9
9
|
import * as fs from "node:fs";
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
const JUNK_LOCAL_RE =
|
|
15
|
-
const JUNK_LOCAL_SUFFIX_RE =
|
|
10
|
+
import { CONTACT_RULES } from "@bobfrankston/mailx-types";
|
|
11
|
+
/** Addresses that have no business in autocomplete. Patterns load from
|
|
12
|
+
* contact-rules.jsonc in mailx-types — a single source of truth shipped
|
|
13
|
+
* with every release. To change the rules edit the JSONC and rebuild. */
|
|
14
|
+
const JUNK_LOCAL_RE = new RegExp(CONTACT_RULES.junk.localExact, "i");
|
|
15
|
+
const JUNK_LOCAL_SUFFIX_RE = new RegExp(CONTACT_RULES.junk.localSuffix, "i");
|
|
16
|
+
const JUNK_LOCAL_PREFIX_RE = new RegExp(CONTACT_RULES.junk.localPrefix, "i");
|
|
17
|
+
const JUNK_DOMAIN_RE = new RegExp(CONTACT_RULES.junk.domain, "i");
|
|
18
|
+
const JUNK_LOCAL_ONEOFF_RE = new RegExp(CONTACT_RULES.junk.localOneoff, "i");
|
|
19
|
+
/** Optional user-configurable pattern list, refreshed when contacts.jsonc
|
|
20
|
+
* reloads. Each entry is a regex compiled lazily in setContactsDenyPatterns. */
|
|
21
|
+
let _denyPatterns = [];
|
|
16
22
|
function isJunkContact(email, name) {
|
|
17
|
-
const
|
|
23
|
+
const lower = (email || "").toLowerCase();
|
|
24
|
+
const at = lower.indexOf("@");
|
|
25
|
+
const local = at >= 0 ? lower.slice(0, at) : lower;
|
|
26
|
+
const domain = at >= 0 ? lower.slice(at + 1) : "";
|
|
18
27
|
if (JUNK_LOCAL_RE.test(local))
|
|
19
28
|
return true;
|
|
20
29
|
if (JUNK_LOCAL_SUFFIX_RE.test(local))
|
|
21
30
|
return true;
|
|
31
|
+
if (JUNK_LOCAL_PREFIX_RE.test(local))
|
|
32
|
+
return true;
|
|
33
|
+
if (JUNK_LOCAL_ONEOFF_RE.test(local))
|
|
34
|
+
return true;
|
|
35
|
+
if (domain && JUNK_DOMAIN_RE.test(domain))
|
|
36
|
+
return true;
|
|
22
37
|
// Bare numeric / hex addresses (rotating IDs from automated systems)
|
|
23
38
|
// — three or fewer chars is too short to be useful regardless.
|
|
24
39
|
if (local.length < 2)
|
|
@@ -26,8 +41,27 @@ function isJunkContact(email, name) {
|
|
|
26
41
|
const lname = (name || "").trim().toLowerCase();
|
|
27
42
|
if (lname.includes("mailer-daemon") || lname.includes("postmaster"))
|
|
28
43
|
return true;
|
|
44
|
+
for (const re of _denyPatterns) {
|
|
45
|
+
if (re.test(lower))
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
29
48
|
return false;
|
|
30
49
|
}
|
|
50
|
+
/** User-configured patterns from contacts.jsonc `denylistPatterns`. Compiled
|
|
51
|
+
* once on settings load; invalid regexes are skipped with a warning. */
|
|
52
|
+
export function setContactsDenyPatterns(patterns) {
|
|
53
|
+
_denyPatterns = [];
|
|
54
|
+
for (const p of patterns) {
|
|
55
|
+
if (!p)
|
|
56
|
+
continue;
|
|
57
|
+
try {
|
|
58
|
+
_denyPatterns.push(new RegExp(p, "i"));
|
|
59
|
+
}
|
|
60
|
+
catch (e) {
|
|
61
|
+
console.warn(`[contacts] invalid denylistPattern "${p}": ${e.message}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
31
65
|
const SCHEMA = `
|
|
32
66
|
CREATE TABLE IF NOT EXISTS accounts (
|
|
33
67
|
id TEXT PRIMARY KEY,
|
|
@@ -364,6 +398,58 @@ export class MailxDB {
|
|
|
364
398
|
throw new Error(`[mailx-store] table "${table}" is missing columns [${missing.join(", ")}] — schema migration did not complete. Run 'mailx -rebuild' to rebuild the local store.`);
|
|
365
399
|
}
|
|
366
400
|
}
|
|
401
|
+
this.runOneShotJunkContactPurge();
|
|
402
|
+
}
|
|
403
|
+
/** One-shot: rewrite absolute body_path entries to relative-to-store
|
|
404
|
+
* paths. Idempotent; flag stored in kv. Caller passes a FileMessageStore
|
|
405
|
+
* whose `rewriteAbsoluteToRelative` does the actual mapping (mailx-store
|
|
406
|
+
* doesn't depend on file-store directly to avoid a cycle). */
|
|
407
|
+
runOneShotBodyPathMigration(rewriteFn) {
|
|
408
|
+
const TARGET = "v1-relative";
|
|
409
|
+
const last = this.getKv("body_paths", "migrate_v");
|
|
410
|
+
if (last === TARGET)
|
|
411
|
+
return;
|
|
412
|
+
try {
|
|
413
|
+
const rows = this.db.prepare("SELECT id, body_path FROM messages WHERE body_path IS NOT NULL AND body_path != ''").all();
|
|
414
|
+
const upd = this.db.prepare("UPDATE messages SET body_path = ? WHERE id = ?");
|
|
415
|
+
const update = (id, newPath) => upd.run(newPath, id);
|
|
416
|
+
const n = rewriteFn(rows, update);
|
|
417
|
+
this.setKv("body_paths", "migrate_v", TARGET);
|
|
418
|
+
if (n > 0)
|
|
419
|
+
console.log(` [body-paths] one-shot: rewrote ${n} of ${rows.length} body_path entries to relative`);
|
|
420
|
+
}
|
|
421
|
+
catch (e) {
|
|
422
|
+
console.warn(`[body-paths] one-shot migration failed: ${e.message}`);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
/** One-shot purge: when the in-DB filter rules tighten (new prefix or
|
|
426
|
+
* domain matchers), historical contacts harvested under the old rules
|
|
427
|
+
* remain in the table and keep being written back to contacts.jsonc on
|
|
428
|
+
* every cloud save. Bumping the version below re-runs the purge once
|
|
429
|
+
* per device. The kv flag (`contacts:purge_v`) records the last version
|
|
430
|
+
* that ran so we don't keep doing the work on every startup. */
|
|
431
|
+
runOneShotJunkContactPurge() {
|
|
432
|
+
const TARGET = CONTACT_RULES.rulesVersion; // bump in contact-rules.jsonc to re-run
|
|
433
|
+
const last = this.getKv("contacts", "purge_v");
|
|
434
|
+
if (last === TARGET)
|
|
435
|
+
return;
|
|
436
|
+
let dropped = 0;
|
|
437
|
+
try {
|
|
438
|
+
const rows = this.db.prepare("SELECT id, email, name FROM contacts WHERE source IN ('discovered','sent','received')").all();
|
|
439
|
+
const del = this.db.prepare("DELETE FROM contacts WHERE id = ?");
|
|
440
|
+
for (const r of rows) {
|
|
441
|
+
if (isJunkContact(r.email || "", r.name || "")) {
|
|
442
|
+
del.run(r.id);
|
|
443
|
+
dropped++;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
this.setKv("contacts", "purge_v", TARGET);
|
|
447
|
+
if (dropped > 0)
|
|
448
|
+
console.log(` [contacts] one-shot purge: dropped ${dropped} junk rows (${TARGET})`);
|
|
449
|
+
}
|
|
450
|
+
catch (e) {
|
|
451
|
+
console.warn(`[contacts] one-shot purge failed: ${e.message}`);
|
|
452
|
+
}
|
|
367
453
|
}
|
|
368
454
|
/** Fetch a string from the kv table. Returns null when not set. */
|
|
369
455
|
getKv(scope, key) {
|
|
@@ -1150,6 +1236,8 @@ export class MailxDB {
|
|
|
1150
1236
|
const lower = email.toLowerCase();
|
|
1151
1237
|
if (this.isAddressDenylisted(lower))
|
|
1152
1238
|
return;
|
|
1239
|
+
if (isJunkContact(lower, name))
|
|
1240
|
+
return;
|
|
1153
1241
|
const now = Date.now();
|
|
1154
1242
|
// discovered tier holds one row per email — bump if present, else
|
|
1155
1243
|
// insert. Doesn't touch preferred or google rows for the same email;
|
|
@@ -1172,6 +1260,29 @@ export class MailxDB {
|
|
|
1172
1260
|
setContactsDenylist(emails) {
|
|
1173
1261
|
this._denylist = new Set(emails.map(e => (e || "").trim().toLowerCase()).filter(Boolean));
|
|
1174
1262
|
}
|
|
1263
|
+
/** Priority sender / domain index — populated from contacts.jsonc on reload.
|
|
1264
|
+
* Used by the client to flag list rows. Source of truth is the JSONC; this
|
|
1265
|
+
* is just a fast in-memory lookup. */
|
|
1266
|
+
_prioritySenders = new Set();
|
|
1267
|
+
_priorityDomains = new Set();
|
|
1268
|
+
setPriorityIndex(senders, domains) {
|
|
1269
|
+
this._prioritySenders = new Set((senders || []).map(s => (s || "").trim().toLowerCase()).filter(Boolean));
|
|
1270
|
+
this._priorityDomains = new Set((domains || []).map(d => (d || "").trim().toLowerCase()).filter(Boolean));
|
|
1271
|
+
}
|
|
1272
|
+
getPriorityIndex() {
|
|
1273
|
+
return {
|
|
1274
|
+
senders: Array.from(this._prioritySenders).sort(),
|
|
1275
|
+
domains: Array.from(this._priorityDomains).sort(),
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1278
|
+
isPrioritySender(addr) {
|
|
1279
|
+
const lower = (addr || "").trim().toLowerCase();
|
|
1280
|
+
if (this._prioritySenders.has(lower))
|
|
1281
|
+
return true;
|
|
1282
|
+
const at = lower.indexOf("@");
|
|
1283
|
+
const domain = at >= 0 ? lower.slice(at + 1) : "";
|
|
1284
|
+
return !!domain && this._priorityDomains.has(domain);
|
|
1285
|
+
}
|
|
1175
1286
|
/** Callback fired when local-DB contacts mutations happen (sends adding
|
|
1176
1287
|
* to discovered, corpus seeder finding new addresses). The service
|
|
1177
1288
|
* registers a debounced cloud flush here so the GDrive copy stays in
|
|
@@ -1312,8 +1423,18 @@ export class MailxDB {
|
|
|
1312
1423
|
applyContactsConfig(cfg) {
|
|
1313
1424
|
const preferred = Array.isArray(cfg.preferred) ? cfg.preferred : [];
|
|
1314
1425
|
const denylist = Array.isArray(cfg.denylist) ? cfg.denylist : [];
|
|
1426
|
+
const denylistPatterns = Array.isArray(cfg.denylistPatterns) ? cfg.denylistPatterns : [];
|
|
1427
|
+
const priorityDomains = Array.isArray(cfg.priorityDomains) ? cfg.priorityDomains : [];
|
|
1315
1428
|
const discovered = Array.isArray(cfg.discovered) ? cfg.discovered : [];
|
|
1316
1429
|
this.setContactsDenylist(denylist);
|
|
1430
|
+
setContactsDenyPatterns(denylistPatterns);
|
|
1431
|
+
// Priority index — preferred[] entries with priority:true plus the
|
|
1432
|
+
// top-level priorityDomains[]. Address book and visual highlight
|
|
1433
|
+
// are independent: not every preferred contact is priority.
|
|
1434
|
+
const prioritySenders = preferred
|
|
1435
|
+
.filter(e => e && e.priority === true && e.email)
|
|
1436
|
+
.map(e => e.email);
|
|
1437
|
+
this.setPriorityIndex(prioritySenders, priorityDomains);
|
|
1317
1438
|
// Wipe and rewrite preferred-tier rows owned by contacts.jsonc.
|
|
1318
1439
|
// The address-book UI's legacy `upsertContact` still writes
|
|
1319
1440
|
// source='manual' rows; those are owned by the address-book code
|
package/file-store.d.ts
CHANGED
|
@@ -14,20 +14,38 @@ import type { MessageStore } from "@bobfrankston/mailx-types";
|
|
|
14
14
|
export declare class FileMessageStore implements MessageStore {
|
|
15
15
|
private basePath;
|
|
16
16
|
constructor(basePath: string);
|
|
17
|
-
/** Fresh opaque path per call
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
*
|
|
21
|
-
private
|
|
22
|
-
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
17
|
+
/** Fresh opaque path per call, returned RELATIVE to `basePath`. The DB
|
|
18
|
+
* stores this relative form so the body store can be relocated (or
|
|
19
|
+
* the user folder renamed — e.g. `.mailx → .rmfmail`) without the
|
|
20
|
+
* body_path entries breaking. */
|
|
21
|
+
private newRelativePath;
|
|
22
|
+
/** Resolve a stored path. Accepts either a relative path (post-2026-05
|
|
23
|
+
* format) or an absolute path (legacy entries until next-launch
|
|
24
|
+
* migration rewrites them). Either way, refuse anything outside the
|
|
25
|
+
* store as a directory-traversal guard. */
|
|
26
|
+
private resolveStored;
|
|
27
|
+
/** Write a new body. Returns a path RELATIVE to basePath; caller stores
|
|
28
|
+
* it in `body_path`. The (folderId, uid) args are kept for interface
|
|
29
|
+
* compatibility; they do NOT affect the filename. */
|
|
26
30
|
putMessage(accountId: string, _folderId: number, _uid: number, raw: Buffer): Promise<string>;
|
|
27
|
-
/** Read by
|
|
28
|
-
readByPath(
|
|
29
|
-
hasByPath(
|
|
30
|
-
unlinkByPath(
|
|
31
|
+
/** Read by stored path (relative or absolute). */
|
|
32
|
+
readByPath(stored: string): Promise<Buffer>;
|
|
33
|
+
hasByPath(stored: string): Promise<boolean>;
|
|
34
|
+
unlinkByPath(stored: string): Promise<void>;
|
|
35
|
+
/** One-shot migration: rewrite absolute body_path values that point at
|
|
36
|
+
* this store's basePath into relative form. Called once from db.ts on
|
|
37
|
+
* startup (gated by a kv flag); silently skipped after first run.
|
|
38
|
+
* Returns the count of rewritten paths.
|
|
39
|
+
*
|
|
40
|
+
* Bodies whose absolute path is OUTSIDE basePath (the legacy
|
|
41
|
+
* `~/.mailx/mailxstore/...` entries on a machine where basePath is
|
|
42
|
+
* now `~/.rmfmail/mailxstore`) are left alone — the migration in
|
|
43
|
+
* mailx-settings already moves the files; this rewrite is purely
|
|
44
|
+
* about getting body_path to a relative shape. */
|
|
45
|
+
rewriteAbsoluteToRelative(rows: Array<{
|
|
46
|
+
id: number;
|
|
47
|
+
body_path: string;
|
|
48
|
+
}>, update: (id: number, newPath: string) => void): number;
|
|
31
49
|
getMessagePath(_accountId: string, _folderId: number, _uid: number): string;
|
|
32
50
|
getMessage(_accountId: string, _folderId: number, _uid: number): Promise<Buffer>;
|
|
33
51
|
hasMessage(_accountId: string, _folderId: number, _uid: number): Promise<boolean>;
|
package/file-store.js
CHANGED
|
@@ -19,46 +19,90 @@ export class FileMessageStore {
|
|
|
19
19
|
this.basePath = basePath;
|
|
20
20
|
fs.mkdirSync(basePath, { recursive: true });
|
|
21
21
|
}
|
|
22
|
-
/** Fresh opaque path per call
|
|
23
|
-
|
|
22
|
+
/** Fresh opaque path per call, returned RELATIVE to `basePath`. The DB
|
|
23
|
+
* stores this relative form so the body store can be relocated (or
|
|
24
|
+
* the user folder renamed — e.g. `.mailx → .rmfmail`) without the
|
|
25
|
+
* body_path entries breaking. */
|
|
26
|
+
newRelativePath(accountId) {
|
|
24
27
|
const uuid = randomUUID().replace(/-/g, "");
|
|
25
28
|
const prefix = uuid.slice(0, 2);
|
|
26
|
-
return path.join(
|
|
29
|
+
return path.join(accountId, prefix, `${uuid}.eml`);
|
|
27
30
|
}
|
|
28
|
-
/**
|
|
29
|
-
*
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
/** Resolve a stored path. Accepts either a relative path (post-2026-05
|
|
32
|
+
* format) or an absolute path (legacy entries until next-launch
|
|
33
|
+
* migration rewrites them). Either way, refuse anything outside the
|
|
34
|
+
* store as a directory-traversal guard. */
|
|
35
|
+
resolveStored(p) {
|
|
36
|
+
if (!p)
|
|
37
|
+
return "";
|
|
38
|
+
const abs = path.isAbsolute(p) ? p : path.resolve(this.basePath, p);
|
|
39
|
+
const rel = path.relative(path.resolve(this.basePath), abs);
|
|
40
|
+
if (rel.startsWith("..") || path.isAbsolute(rel))
|
|
41
|
+
return "";
|
|
42
|
+
return abs;
|
|
35
43
|
}
|
|
36
|
-
/** Write a new body.
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* NOT affect the filename. */
|
|
44
|
+
/** Write a new body. Returns a path RELATIVE to basePath; caller stores
|
|
45
|
+
* it in `body_path`. The (folderId, uid) args are kept for interface
|
|
46
|
+
* compatibility; they do NOT affect the filename. */
|
|
40
47
|
async putMessage(accountId, _folderId, _uid, raw) {
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
fs.
|
|
44
|
-
|
|
48
|
+
const rel = this.newRelativePath(accountId);
|
|
49
|
+
const abs = path.join(this.basePath, rel);
|
|
50
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
51
|
+
fs.writeFileSync(abs, raw);
|
|
52
|
+
return rel;
|
|
45
53
|
}
|
|
46
|
-
/** Read by
|
|
47
|
-
async readByPath(
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
54
|
+
/** Read by stored path (relative or absolute). */
|
|
55
|
+
async readByPath(stored) {
|
|
56
|
+
const abs = this.resolveStored(stored);
|
|
57
|
+
if (!abs)
|
|
58
|
+
throw new Error(`refusing to read outside store: ${stored}`);
|
|
59
|
+
return fs.readFileSync(abs);
|
|
51
60
|
}
|
|
52
|
-
async hasByPath(
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
return fs.existsSync(fullPath);
|
|
61
|
+
async hasByPath(stored) {
|
|
62
|
+
const abs = this.resolveStored(stored);
|
|
63
|
+
return !!abs && fs.existsSync(abs);
|
|
56
64
|
}
|
|
57
|
-
async unlinkByPath(
|
|
58
|
-
|
|
65
|
+
async unlinkByPath(stored) {
|
|
66
|
+
const abs = this.resolveStored(stored);
|
|
67
|
+
if (!abs)
|
|
59
68
|
return;
|
|
60
|
-
if (fs.existsSync(
|
|
61
|
-
fs.unlinkSync(
|
|
69
|
+
if (fs.existsSync(abs))
|
|
70
|
+
fs.unlinkSync(abs);
|
|
71
|
+
}
|
|
72
|
+
/** One-shot migration: rewrite absolute body_path values that point at
|
|
73
|
+
* this store's basePath into relative form. Called once from db.ts on
|
|
74
|
+
* startup (gated by a kv flag); silently skipped after first run.
|
|
75
|
+
* Returns the count of rewritten paths.
|
|
76
|
+
*
|
|
77
|
+
* Bodies whose absolute path is OUTSIDE basePath (the legacy
|
|
78
|
+
* `~/.mailx/mailxstore/...` entries on a machine where basePath is
|
|
79
|
+
* now `~/.rmfmail/mailxstore`) are left alone — the migration in
|
|
80
|
+
* mailx-settings already moves the files; this rewrite is purely
|
|
81
|
+
* about getting body_path to a relative shape. */
|
|
82
|
+
rewriteAbsoluteToRelative(rows, update) {
|
|
83
|
+
let n = 0;
|
|
84
|
+
const baseAbs = path.resolve(this.basePath);
|
|
85
|
+
for (const r of rows) {
|
|
86
|
+
if (!r.body_path || !path.isAbsolute(r.body_path))
|
|
87
|
+
continue;
|
|
88
|
+
const rel = path.relative(baseAbs, path.resolve(r.body_path));
|
|
89
|
+
if (rel.startsWith("..") || path.isAbsolute(rel)) {
|
|
90
|
+
// Outside this store — try mapping `.mailx → .rmfmail` if the
|
|
91
|
+
// legacy dir was the source. Then re-test.
|
|
92
|
+
const remapped = r.body_path.replace(/[\\/]\.mailx[\\/]mailxstore[\\/]/i, `${path.sep}.rmfmail${path.sep}mailxstore${path.sep}`);
|
|
93
|
+
if (remapped !== r.body_path) {
|
|
94
|
+
const rel2 = path.relative(baseAbs, path.resolve(remapped));
|
|
95
|
+
if (!rel2.startsWith("..") && !path.isAbsolute(rel2)) {
|
|
96
|
+
update(r.id, rel2.replace(/\\/g, "/"));
|
|
97
|
+
n++;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
update(r.id, rel.replace(/\\/g, "/"));
|
|
103
|
+
n++;
|
|
104
|
+
}
|
|
105
|
+
return n;
|
|
62
106
|
}
|
|
63
107
|
// MessageStore interface compatibility (unused once all callers migrate to
|
|
64
108
|
// path-based reads). These used to compose {folderId}/{uid}.eml and they
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/mailx-store",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -9,21 +9,24 @@
|
|
|
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.7",
|
|
13
|
+
"@bobfrankston/mailx-settings": "^0.1.8"
|
|
14
14
|
},
|
|
15
15
|
"repository": {
|
|
16
16
|
"type": "git",
|
|
17
17
|
"url": "https://github.com/BobFrankston/mailx-store.git"
|
|
18
18
|
},
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
},
|
|
19
22
|
".dependencies": {
|
|
20
23
|
"@bobfrankston/mailx-types": "file:../mailx-types",
|
|
21
24
|
"@bobfrankston/mailx-settings": "file:../mailx-settings"
|
|
22
25
|
},
|
|
23
26
|
".transformedSnapshot": {
|
|
24
27
|
"dependencies": {
|
|
25
|
-
"@bobfrankston/mailx-types": "^0.1.
|
|
26
|
-
"@bobfrankston/mailx-settings": "^0.1.
|
|
28
|
+
"@bobfrankston/mailx-types": "^0.1.7",
|
|
29
|
+
"@bobfrankston/mailx-settings": "^0.1.8"
|
|
27
30
|
}
|
|
28
31
|
}
|
|
29
32
|
}
|