@bobfrankston/rmfmail 1.0.546 → 1.0.549
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/bin/mailx.js +13 -0
- package/bin/mailx.js.map +1 -1
- package/bin/mailx.ts +13 -0
- package/client/app.js +54 -1
- package/client/app.js.map +1 -1
- package/client/app.ts +53 -1
- package/client/components/message-list.js +1 -3
- package/client/components/message-list.js.map +1 -1
- package/client/components/message-list.ts +1 -3
- package/client/components/message-viewer.js +65 -97
- package/client/components/message-viewer.js.map +1 -1
- package/client/components/message-viewer.ts +51 -90
- package/client/compose/compose.js +2 -2
- package/client/compose/compose.js.map +1 -1
- package/client/compose/compose.ts +2 -2
- package/client/lib/api-client.js +5 -0
- package/client/lib/api-client.js.map +1 -1
- package/client/lib/api-client.ts +5 -1
- package/client/styles/components.css +0 -12
- package/package.json +1 -1
- package/packages/mailx-imap/index.d.ts +9 -1
- package/packages/mailx-imap/index.d.ts.map +1 -1
- package/packages/mailx-imap/index.js +25 -38
- package/packages/mailx-imap/index.js.map +1 -1
- package/packages/mailx-imap/index.ts +30 -42
- package/packages/mailx-imap/package-lock.json +2 -2
- package/packages/mailx-imap/package.json +1 -1
- package/packages/mailx-server/index.d.ts.map +1 -1
- package/packages/mailx-server/index.js +13 -0
- package/packages/mailx-server/index.js.map +1 -1
- package/packages/mailx-server/index.ts +14 -0
- package/packages/mailx-service/index.d.ts +26 -0
- package/packages/mailx-service/index.d.ts.map +1 -1
- package/packages/mailx-service/index.js +82 -235
- package/packages/mailx-service/index.js.map +1 -1
- package/packages/mailx-service/index.ts +45 -54
- package/packages/mailx-service/local-store.d.ts +5 -0
- package/packages/mailx-service/local-store.d.ts.map +1 -1
- package/packages/mailx-service/local-store.js +116 -12
- package/packages/mailx-service/local-store.js.map +1 -1
- package/packages/mailx-service/local-store.ts +5 -1
- package/packages/mailx-service/reconciler.d.ts +90 -0
- package/packages/mailx-service/reconciler.d.ts.map +1 -0
- package/packages/mailx-service/reconciler.js +209 -0
- package/packages/mailx-service/reconciler.js.map +1 -0
- package/packages/mailx-service/reconciler.ts +230 -0
- package/packages/mailx-service/sync-queue.d.ts +79 -0
- package/packages/mailx-service/sync-queue.d.ts.map +1 -0
- package/packages/mailx-service/sync-queue.js +118 -0
- package/packages/mailx-service/sync-queue.js.map +1 -0
- package/packages/mailx-service/sync-queue.ts +134 -0
- package/packages/mailx-settings/docs/contacts.md +6 -1
- package/packages/mailx-settings/docs/search.md +99 -0
- package/packages/mailx-settings/package.json +1 -1
- package/packages/mailx-store/file-store.d.ts +6 -0
- package/packages/mailx-store/file-store.d.ts.map +1 -1
- package/packages/mailx-store/file-store.js +8 -0
- package/packages/mailx-store/file-store.js.map +1 -1
- package/packages/mailx-store/file-store.ts +9 -0
- package/packages/mailx-store/package.json +1 -1
|
@@ -14,6 +14,8 @@ import { ImapManager } from "@bobfrankston/mailx-imap";
|
|
|
14
14
|
import * as gsync from "./google-sync.js";
|
|
15
15
|
import { sniffAndFixCharset } from "./charset.js";
|
|
16
16
|
import { LocalStore } from "./local-store.js";
|
|
17
|
+
import { SyncQueue } from "./sync-queue.js";
|
|
18
|
+
import { Reconciler } from "./reconciler.js";
|
|
17
19
|
import { loadSettings, saveSettings, loadAccounts, loadAccountsAsync, saveAccounts, initCloudConfig, loadAllowlist, saveAllowlist, loadAutocomplete, saveAutocomplete, loadKeys, saveKeys, ensureKeysSectionExists, getStorePath, getStorageInfo, getConfigDir } from "@bobfrankston/mailx-settings";
|
|
18
20
|
import type { AccountConfig, Folder, AutocompleteRequest, AutocompleteResponse, AutocompleteSettings, AiTransformRequest, AiTransformResponse, ExtractedEvent } from "@bobfrankston/mailx-types";
|
|
19
21
|
import { sanitizeHtml, encodeQuotedPrintable, htmlToPlainText } from "@bobfrankston/mailx-types";
|
|
@@ -121,22 +123,25 @@ export class MailxService {
|
|
|
121
123
|
|
|
122
124
|
/** Local-first read/write facade. Every UI IPC handler that touches the
|
|
123
125
|
* local DB or body store goes through this — no awaiting IMAP, no
|
|
124
|
-
* awaiting Gmail API, no awaiting SMTP.
|
|
125
|
-
* all server I/O. See docs/local-first-plan.md. */
|
|
126
|
+
* awaiting Gmail API, no awaiting SMTP. See docs/local-first-plan.md. */
|
|
126
127
|
private localStore: LocalStore;
|
|
127
128
|
|
|
128
|
-
/**
|
|
129
|
-
*
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
129
|
+
/** Persistent (and in-memory body-fetch) queue. UI handlers commit
|
|
130
|
+
* locally, then enqueue a server-mirror task here. */
|
|
131
|
+
private syncQueue: SyncQueue;
|
|
132
|
+
|
|
133
|
+
/** Background loop: drains body-fetch tasks, retries failed message
|
|
134
|
+
* actions, emits sync-state events for the status pill. */
|
|
135
|
+
private reconciler: Reconciler;
|
|
134
136
|
|
|
135
137
|
constructor(
|
|
136
138
|
private db: MailxDB,
|
|
137
139
|
private imapManager: ImapManager,
|
|
138
140
|
) {
|
|
139
141
|
this.localStore = new LocalStore(db, imapManager.getBodyStore());
|
|
142
|
+
this.syncQueue = new SyncQueue(db, imapManager);
|
|
143
|
+
this.reconciler = new Reconciler(db, imapManager, this.syncQueue);
|
|
144
|
+
this.reconciler.start();
|
|
140
145
|
|
|
141
146
|
// Invalidate account cache when accounts.jsonc changes on disk or GDrive.
|
|
142
147
|
this.imapManager.on?.("configChanged", (filename: string) => {
|
|
@@ -390,10 +395,12 @@ export class MailxService {
|
|
|
390
395
|
if (!local) throw new Error("Message not found");
|
|
391
396
|
|
|
392
397
|
if (!local.cached) {
|
|
393
|
-
// Body not on disk.
|
|
394
|
-
//
|
|
395
|
-
//
|
|
396
|
-
|
|
398
|
+
// Body not on disk. Enqueue an interactive-lane fetch — the
|
|
399
|
+
// reconciler picks it up, emits `bodyAvailable` on success or
|
|
400
|
+
// `bodyFetchError` on failure. Return envelope-only; UI shows
|
|
401
|
+
// a placeholder and re-renders on the event.
|
|
402
|
+
this.syncQueue.enqueueBodyFetch(accountId, local.folderId as any, local.uid as any, "interactive");
|
|
403
|
+
this.reconciler.kick();
|
|
397
404
|
return local;
|
|
398
405
|
}
|
|
399
406
|
|
|
@@ -410,37 +417,11 @@ export class MailxService {
|
|
|
410
417
|
return { ...local, reputation };
|
|
411
418
|
}
|
|
412
419
|
|
|
413
|
-
/**
|
|
414
|
-
*
|
|
415
|
-
*
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
const key = `${accountId}:${folderId}:${uid}`;
|
|
419
|
-
if (this.bodyFetchInFlight.has(key)) return;
|
|
420
|
-
this.bodyFetchInFlight.add(key);
|
|
421
|
-
|
|
422
|
-
(async () => {
|
|
423
|
-
try {
|
|
424
|
-
const raw = await this.imapManager.fetchMessageBody(accountId, folderId, uid);
|
|
425
|
-
if (raw) {
|
|
426
|
-
this.imapManager.emit("bodyAvailable", { accountId, folderId, uid });
|
|
427
|
-
}
|
|
428
|
-
} catch (err: any) {
|
|
429
|
-
if (err?.isNotFound) {
|
|
430
|
-
try {
|
|
431
|
-
this.db.deleteMessage(accountId, uid);
|
|
432
|
-
this.db.recalcFolderCounts(folderId);
|
|
433
|
-
this.imapManager.emit("messageRemoved", { accountId, folderId, uid });
|
|
434
|
-
} catch { /* ignore */ }
|
|
435
|
-
return;
|
|
436
|
-
}
|
|
437
|
-
const msg = err?.message || "body fetch failed";
|
|
438
|
-
const transient = /connection|Too many|UNAVAILABLE|rate|429|5\d\d|timeout|ENOTFOUND|ECONNRESET|ETIMEDOUT/i.test(msg);
|
|
439
|
-
this.imapManager.emit("bodyFetchError", { accountId, folderId, uid, error: msg, transient });
|
|
440
|
-
} finally {
|
|
441
|
-
this.bodyFetchInFlight.delete(key);
|
|
442
|
-
}
|
|
443
|
-
})();
|
|
420
|
+
/** Diagnostic accessor — exposed for the sync-status pill / debug UI.
|
|
421
|
+
* Returns the current queue counts so the UI can render
|
|
422
|
+
* "Sync OK / Syncing N items" without polling per-account state. */
|
|
423
|
+
getSyncStatus(): { messageActions: number; bodyFetches: number } {
|
|
424
|
+
return this.syncQueue.pendingCount();
|
|
444
425
|
}
|
|
445
426
|
|
|
446
427
|
/** RFC 8058 one-click unsubscribe: POST `List-Unsubscribe=One-Click` to the
|
|
@@ -1079,7 +1060,7 @@ export class MailxService {
|
|
|
1079
1060
|
.catch(e => this.handleGoogleRefreshError("calendar", e));
|
|
1080
1061
|
}
|
|
1081
1062
|
}
|
|
1082
|
-
return this.
|
|
1063
|
+
return this.localStore.getCalendarEvents(acctId, fromMs, toMs);
|
|
1083
1064
|
}
|
|
1084
1065
|
|
|
1085
1066
|
/** Returns true if the feature is currently in a quota-exceeded cooldown. */
|
|
@@ -1236,7 +1217,7 @@ export class MailxService {
|
|
|
1236
1217
|
.catch(e => this.handleGoogleRefreshError("tasks", e));
|
|
1237
1218
|
}
|
|
1238
1219
|
}
|
|
1239
|
-
return this.
|
|
1220
|
+
return this.localStore.getTasks(acctId, includeCompleted);
|
|
1240
1221
|
}
|
|
1241
1222
|
|
|
1242
1223
|
private async refreshTasks(accountId: string, includeCompleted: boolean): Promise<boolean> {
|
|
@@ -1887,8 +1868,22 @@ export class MailxService {
|
|
|
1887
1868
|
async getAttachment(accountId: string, uid: number, attachmentId: number, folderId?: number): Promise<{ content: Buffer; contentType: string; filename: string }> {
|
|
1888
1869
|
const envelope = this.db.getMessageByUid(accountId, uid, folderId);
|
|
1889
1870
|
if (!envelope) throw new Error("Message not found");
|
|
1890
|
-
|
|
1871
|
+
|
|
1872
|
+
// Prefer the on-disk body — when the user just opened the message,
|
|
1873
|
+
// LocalStore.getMessage either returned cached:true (body already on
|
|
1874
|
+
// disk) or kicked a background fetch that wrote it. Either way a
|
|
1875
|
+
// re-fetch here would be wasted IMAP work and risks racing.
|
|
1876
|
+
const bodyStore = this.imapManager.getBodyStore();
|
|
1877
|
+
let raw: Buffer | null = null;
|
|
1878
|
+
const storedPath = envelope.bodyPath || this.db.getMessageBodyPath(accountId, uid) || "";
|
|
1879
|
+
if (storedPath && await bodyStore.hasByPath(storedPath)) {
|
|
1880
|
+
try { raw = await bodyStore.readByPath(storedPath); } catch { raw = null; }
|
|
1881
|
+
}
|
|
1882
|
+
if (!raw) {
|
|
1883
|
+
raw = await this.imapManager.fetchMessageBody(accountId, envelope.folderId, envelope.uid);
|
|
1884
|
+
}
|
|
1891
1885
|
if (!raw) throw new Error("Message body not available");
|
|
1886
|
+
|
|
1892
1887
|
const parsed = await simpleParser(raw);
|
|
1893
1888
|
const att = parsed.attachments?.[attachmentId];
|
|
1894
1889
|
if (!att) throw new Error("Attachment not found");
|
|
@@ -1954,14 +1949,10 @@ export class MailxService {
|
|
|
1954
1949
|
}
|
|
1955
1950
|
} catch { /* non-fatal — draft stays in memory at least */ }
|
|
1956
1951
|
|
|
1957
|
-
// Background reconcile to server Drafts folder
|
|
1958
|
-
//
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
// Surface as an event so the UI can show a status-bar hint without
|
|
1962
|
-
// blocking the caller. Draft is preserved on disk regardless.
|
|
1963
|
-
(this as any).emit?.("draftSaveDeferred", { accountId, draftId: id, error: String(e?.message || e) });
|
|
1964
|
-
});
|
|
1952
|
+
// Background reconcile to server Drafts folder via the SyncQueue
|
|
1953
|
+
// seam. Fire-and-forget; deferred failures surface as a `draftSaveDeferred`
|
|
1954
|
+
// event the UI can render in the status bar.
|
|
1955
|
+
this.syncQueue.enqueueDraftPush(accountId, raw, previousDraftUid, id);
|
|
1965
1956
|
|
|
1966
1957
|
return { draftUid: null, draftId: id };
|
|
1967
1958
|
}
|
|
@@ -38,6 +38,11 @@ export interface LocalMessage extends MessageEnvelope {
|
|
|
38
38
|
deliveredTo: string;
|
|
39
39
|
returnPath: string;
|
|
40
40
|
listUnsubscribe: string;
|
|
41
|
+
listUnsubscribeMail: string;
|
|
42
|
+
listUnsubscribeHttp: string;
|
|
43
|
+
listUnsubscribeOneClick: boolean;
|
|
44
|
+
emlPath: string;
|
|
45
|
+
isFlagged: boolean;
|
|
41
46
|
}
|
|
42
47
|
export declare class LocalStore {
|
|
43
48
|
private db;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"local-store.d.ts","sourceRoot":"","sources":["local-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAIH,OAAO,KAAK,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAC3E,OAAO,KAAK,EACR,eAAe,EAAW,YAAY,EAAE,WAAW,EACtD,MAAM,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"local-store.d.ts","sourceRoot":"","sources":["local-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAIH,OAAO,KAAK,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAC3E,OAAO,KAAK,EACR,eAAe,EAAW,YAAY,EAAE,WAAW,EACtD,MAAM,2BAA2B,CAAC;AAuCnC;;;;mEAImE;AACnE,MAAM,WAAW,YAAa,SAAQ,eAAe;IACjD,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,aAAa,EAAE,OAAO,CAAC;IACvB,WAAW,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACxG,MAAM,EAAE,OAAO,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;IACxB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,uBAAuB,EAAE,OAAO,CAAC;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,OAAO,CAAC;CACtB;AAED,qBAAa,UAAU;IAEf,OAAO,CAAC,EAAE;IACV,OAAO,CAAC,SAAS;gBADT,EAAE,EAAE,OAAO,EACX,SAAS,EAAE,gBAAgB;IAMvC;;;6DAGyD;IACzD,WAAW,IAAI;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,EAAE;IAM9E,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,GAAG,EAAE;IAMpC;;4CAEwC;IACxC,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,eAAe,GAAG,IAAI;IAK7F,iEAAiE;IACjE,WAAW,CAAC,KAAK,EAAE,YAAY,GAAG,WAAW,CAAC,eAAe,CAAC;IAI9D,mEAAmE;IACnE,eAAe,CAAC,IAAI,SAAI,EAAE,QAAQ,SAAK,GAAG,WAAW,CAAC,eAAe,CAAC;IAItE,sEAAsE;IACtE,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,SAAI,EAAE,QAAQ,SAAK,EAAE,SAAS,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,WAAW,CAAC,eAAe,CAAC;IAM3H;;;;;;;sEAOkE;IAC5D,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC;IAuJvH,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,GAAG,EAAE;IAIzE,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,gBAAgB,UAAQ,GAAG,GAAG,EAAE;IAI5D,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,SAAK,GAAG,GAAG,EAAE;IAIhD,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,SAAI,EAAE,QAAQ,SAAM,GAAG,GAAG;CAK7D"}
|
|
@@ -17,7 +17,43 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { simpleParser } from "mailparser";
|
|
19
19
|
import { sanitizeHtml } from "@bobfrankston/mailx-types";
|
|
20
|
+
import { loadSettings, loadAllowlist } from "@bobfrankston/mailx-settings";
|
|
20
21
|
import { sniffAndFixCharset } from "./charset.js";
|
|
22
|
+
/** Parse `List-Unsubscribe` (RFC 2369) and `List-Unsubscribe-Post` (RFC 8058).
|
|
23
|
+
* mailparser only exposes ONE of mail/url even when both are present, so we
|
|
24
|
+
* also scan the raw header text for the full set of angle-bracketed URIs. */
|
|
25
|
+
function parseListUnsubscribe(headers) {
|
|
26
|
+
let mail = "";
|
|
27
|
+
let http = "";
|
|
28
|
+
let oneClick = false;
|
|
29
|
+
const raw = headers.get("list-unsubscribe");
|
|
30
|
+
const rawStr = typeof raw === "string" ? raw : (raw && typeof raw.text === "string" ? raw.text : "");
|
|
31
|
+
if (rawStr) {
|
|
32
|
+
const matches = rawStr.match(/<([^>]+)>/g) || [];
|
|
33
|
+
for (const m of matches) {
|
|
34
|
+
const url = m.slice(1, -1).trim();
|
|
35
|
+
if (!mail && /^mailto:/i.test(url))
|
|
36
|
+
mail = url;
|
|
37
|
+
else if (!http && /^https?:/i.test(url))
|
|
38
|
+
http = url;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (!mail && !http) {
|
|
42
|
+
const listHeaders = headers.get("list");
|
|
43
|
+
if (listHeaders?.unsubscribe) {
|
|
44
|
+
const unsub = listHeaders.unsubscribe;
|
|
45
|
+
if (unsub.url)
|
|
46
|
+
http = Array.isArray(unsub.url) ? unsub.url[0] : unsub.url;
|
|
47
|
+
if (unsub.mail)
|
|
48
|
+
mail = `mailto:${Array.isArray(unsub.mail) ? unsub.mail[0] : unsub.mail}`;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const post = headers.get("list-unsubscribe-post");
|
|
52
|
+
const postStr = typeof post === "string" ? post : (post && typeof post.text === "string" ? post.text : "");
|
|
53
|
+
if (postStr && /one-?click/i.test(postStr))
|
|
54
|
+
oneClick = true;
|
|
55
|
+
return { listUnsubscribeMail: mail, listUnsubscribeHttp: http, listUnsubscribeOneClick: oneClick };
|
|
56
|
+
}
|
|
21
57
|
export class LocalStore {
|
|
22
58
|
db;
|
|
23
59
|
bodyStore;
|
|
@@ -71,6 +107,24 @@ export class LocalStore {
|
|
|
71
107
|
const envelope = this.db.getMessageByUid(accountId, uid, folderId);
|
|
72
108
|
if (!envelope)
|
|
73
109
|
return null;
|
|
110
|
+
const allowList = loadAllowlist();
|
|
111
|
+
const senderAddr = (envelope.from?.address || "").toLowerCase();
|
|
112
|
+
const senderDomain = senderAddr.split("@")[1] || "";
|
|
113
|
+
const toAddrs = (envelope.to || []).map((a) => (a.address || "").toLowerCase());
|
|
114
|
+
// Allowlist auto-allow: trusted sender / domain / recipient skips
|
|
115
|
+
// sanitization. Same rule as the legacy service implementation.
|
|
116
|
+
if (!allowRemote) {
|
|
117
|
+
const senders = (allowList.senders || []).map((s) => (s || "").toLowerCase());
|
|
118
|
+
const domains = (allowList.domains || []).map((d) => (d || "").toLowerCase());
|
|
119
|
+
const recipients = (allowList.recipients || []).map((r) => (r || "").toLowerCase());
|
|
120
|
+
if (senders.includes(senderAddr) ||
|
|
121
|
+
domains.includes(senderDomain) ||
|
|
122
|
+
toAddrs.some((a) => recipients.includes(a))) {
|
|
123
|
+
allowRemote = true;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const isFlagged = !!((allowList.flaggedSenders || []).some((s) => (s || "").toLowerCase() === senderAddr) ||
|
|
127
|
+
(allowList.flaggedDomains || []).some((d) => (d || "").toLowerCase() === senderDomain));
|
|
74
128
|
// Resolve body path: prefer the row's own bodyPath, fall back to
|
|
75
129
|
// the historical lookup. Either may be empty (body never fetched).
|
|
76
130
|
let storedPath = envelope.bodyPath || "";
|
|
@@ -79,10 +133,13 @@ export class LocalStore {
|
|
|
79
133
|
const empty = {
|
|
80
134
|
...envelope,
|
|
81
135
|
bodyHtml: "", bodyText: "",
|
|
82
|
-
hasRemoteContent: false, remoteAllowed:
|
|
136
|
+
hasRemoteContent: false, remoteAllowed: allowRemote,
|
|
83
137
|
attachments: [],
|
|
84
138
|
cached: false,
|
|
85
|
-
deliveredTo: "", returnPath: "",
|
|
139
|
+
deliveredTo: "", returnPath: "",
|
|
140
|
+
listUnsubscribe: "", listUnsubscribeMail: "", listUnsubscribeHttp: "", listUnsubscribeOneClick: false,
|
|
141
|
+
emlPath: "",
|
|
142
|
+
isFlagged,
|
|
86
143
|
};
|
|
87
144
|
if (!storedPath)
|
|
88
145
|
return empty;
|
|
@@ -107,7 +164,6 @@ export class LocalStore {
|
|
|
107
164
|
let bodyHtml = parsed.html || "";
|
|
108
165
|
const bodyText = parsed.text || "";
|
|
109
166
|
let hasRemoteContent = false;
|
|
110
|
-
let remoteAllowed = allowRemote;
|
|
111
167
|
const attachments = (parsed.attachments || []).map((a, i) => ({
|
|
112
168
|
id: i,
|
|
113
169
|
filename: a.filename || `attachment-${i}`,
|
|
@@ -120,26 +176,74 @@ export class LocalStore {
|
|
|
120
176
|
bodyHtml = result.html;
|
|
121
177
|
hasRemoteContent = result.hasRemoteContent;
|
|
122
178
|
}
|
|
123
|
-
// Header extraction — Delivered-To
|
|
124
|
-
//
|
|
179
|
+
// Header extraction — Delivered-To with relay-domain unwrapping,
|
|
180
|
+
// Return-Path, List-Unsubscribe variants. The relay/prefix rules
|
|
181
|
+
// come from accounts.jsonc settings; cheap local read.
|
|
182
|
+
const settings = loadSettings() || {};
|
|
183
|
+
const acctConfig = (settings.accounts || []).find((a) => a.id === accountId) || {};
|
|
184
|
+
const relayDomains = acctConfig.relayDomains || [];
|
|
185
|
+
const prefixes = acctConfig.deliveredToPrefix || [];
|
|
186
|
+
let deliveredTo = "";
|
|
187
|
+
const rawDelivered = parsed.headers.get("delivered-to");
|
|
188
|
+
if (rawDelivered) {
|
|
189
|
+
const deliveredList = Array.isArray(rawDelivered) ? rawDelivered : [rawDelivered];
|
|
190
|
+
for (let i = deliveredList.length - 1; i >= 0; i--) {
|
|
191
|
+
const d = deliveredList[i];
|
|
192
|
+
const addr = typeof d === "string" ? d : d?.text || d?.address || String(d);
|
|
193
|
+
if (!relayDomains.some(rd => addr.includes(`@${rd}`))) {
|
|
194
|
+
deliveredTo = addr;
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (!deliveredTo && deliveredList.length > 0) {
|
|
199
|
+
const d = deliveredList[deliveredList.length - 1];
|
|
200
|
+
deliveredTo = typeof d === "string" ? d : d?.text || d?.address || String(d);
|
|
201
|
+
}
|
|
202
|
+
if (deliveredTo && prefixes.length > 0) {
|
|
203
|
+
const [local, domain] = deliveredTo.split("@");
|
|
204
|
+
for (const prefix of prefixes) {
|
|
205
|
+
if (local.startsWith(prefix)) {
|
|
206
|
+
deliveredTo = `${local.slice(prefix.length)}@${domain}`;
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
125
212
|
const hdr = (key) => {
|
|
126
|
-
|
|
213
|
+
let v = parsed.headers.get(key);
|
|
127
214
|
if (!v)
|
|
128
215
|
return "";
|
|
216
|
+
if (Array.isArray(v))
|
|
217
|
+
v = v[0];
|
|
129
218
|
if (typeof v === "string")
|
|
130
219
|
return v;
|
|
131
|
-
|
|
220
|
+
if (typeof v === "object" && v !== null) {
|
|
221
|
+
if ("text" in v)
|
|
222
|
+
return v.text || "";
|
|
223
|
+
if ("value" in v)
|
|
224
|
+
return String(v.value);
|
|
225
|
+
if ("address" in v)
|
|
226
|
+
return v.address || "";
|
|
227
|
+
}
|
|
228
|
+
return String(v);
|
|
132
229
|
};
|
|
133
|
-
const
|
|
134
|
-
const
|
|
135
|
-
const listUnsubscribe =
|
|
230
|
+
const returnPath = hdr("return-path").replace(/[<>]/g, "");
|
|
231
|
+
const { listUnsubscribeMail, listUnsubscribeHttp, listUnsubscribeOneClick } = parseListUnsubscribe(parsed.headers);
|
|
232
|
+
const listUnsubscribe = listUnsubscribeHttp || listUnsubscribeMail;
|
|
136
233
|
return {
|
|
137
234
|
...envelope,
|
|
138
235
|
bodyHtml, bodyText,
|
|
139
|
-
hasRemoteContent, remoteAllowed,
|
|
236
|
+
hasRemoteContent, remoteAllowed: allowRemote,
|
|
140
237
|
attachments,
|
|
141
238
|
cached: true,
|
|
142
|
-
deliveredTo, returnPath,
|
|
239
|
+
deliveredTo, returnPath,
|
|
240
|
+
listUnsubscribe, listUnsubscribeMail, listUnsubscribeHttp, listUnsubscribeOneClick,
|
|
241
|
+
// body_path in the DB is stored relative to the body-store
|
|
242
|
+
// basePath; resolve to absolute so the UI's "Source" button
|
|
243
|
+
// can hand the path to an OS file open without re-deriving
|
|
244
|
+
// basePath every time.
|
|
245
|
+
emlPath: this.bodyStore.absolutePath(storedPath),
|
|
246
|
+
isFlagged,
|
|
143
247
|
};
|
|
144
248
|
}
|
|
145
249
|
// ── Calendar / tasks / contacts (read paths) ──
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"local-store.js","sourceRoot":"","sources":["local-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAGH,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAK1C,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzD,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"local-store.js","sourceRoot":"","sources":["local-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -280,7 +280,11 @@ export class LocalStore {
|
|
|
280
280
|
cached: true,
|
|
281
281
|
deliveredTo, returnPath,
|
|
282
282
|
listUnsubscribe, listUnsubscribeMail, listUnsubscribeHttp, listUnsubscribeOneClick,
|
|
283
|
-
|
|
283
|
+
// body_path in the DB is stored relative to the body-store
|
|
284
|
+
// basePath; resolve to absolute so the UI's "Source" button
|
|
285
|
+
// can hand the path to an OS file open without re-deriving
|
|
286
|
+
// basePath every time.
|
|
287
|
+
emlPath: this.bodyStore.absolutePath(storedPath),
|
|
284
288
|
isFlagged,
|
|
285
289
|
};
|
|
286
290
|
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reconciler — single background loop that drains the SyncQueue, polls
|
|
3
|
+
* for server changes, and emits state events the UI listens for.
|
|
4
|
+
*
|
|
5
|
+
* Local-first contract: the Reconciler is the ONLY code path that touches
|
|
6
|
+
* IMAP / Gmail API on behalf of UI actions. UI handlers commit locally
|
|
7
|
+
* and enqueue; the Reconciler picks up and mirrors. Reads from the UI
|
|
8
|
+
* never await the Reconciler.
|
|
9
|
+
*
|
|
10
|
+
* What this owns today:
|
|
11
|
+
* - body-fetch lane: drains `SyncQueue.nextBodyFetch()`, calls
|
|
12
|
+
* `imapManager.fetchMessageBody`, emits `bodyAvailable` / `bodyFetchError`.
|
|
13
|
+
* This is the new piece; previously body fetches happened inline on
|
|
14
|
+
* every getMessage call.
|
|
15
|
+
* - sync-state pill events: emits `syncStateChanged` so the UI can show
|
|
16
|
+
* "Sync OK / Syncing N items".
|
|
17
|
+
*
|
|
18
|
+
* What it delegates (intentional — no duplication):
|
|
19
|
+
* - message-action drain: ImapManager already runs a 30s actionsInterval
|
|
20
|
+
* that processes both outbox (processSendActions) and sync_actions
|
|
21
|
+
* (processSyncActions) per account. Per plan decision #5, outbox stays
|
|
22
|
+
* under ImapManager; we don't fork that work.
|
|
23
|
+
* - poll loops (per-account quick check, full sync, prefetch, tombstone
|
|
24
|
+
* prune): all owned by ImapManager's startPeriodicSync. The Reconciler
|
|
25
|
+
* does NOT shadow them — back-pressure on those timers can be added
|
|
26
|
+
* later via a single `setBackPressure(level)` hook.
|
|
27
|
+
*
|
|
28
|
+
* Part of docs/local-first-plan.md (step 4).
|
|
29
|
+
*/
|
|
30
|
+
import type { ImapManager } from "@bobfrankston/mailx-imap";
|
|
31
|
+
import type { MailxDB } from "@bobfrankston/mailx-store";
|
|
32
|
+
import type { SyncQueue } from "./sync-queue.js";
|
|
33
|
+
export interface ReconcilerOptions {
|
|
34
|
+
/** Max concurrent body fetches across all accounts (in addition to
|
|
35
|
+
* per-account semaphore in ImapManager). Keeps the interactive lane
|
|
36
|
+
* responsive even when prefetch is hammering. */
|
|
37
|
+
bodyFetchConcurrency?: number;
|
|
38
|
+
/** Periodic emit of syncStateChanged — drives the UI status pill. */
|
|
39
|
+
statusEmitIntervalMs?: number;
|
|
40
|
+
/** How often to fire the prefetch tick. Default 60s. */
|
|
41
|
+
prefetchIntervalMs?: number;
|
|
42
|
+
/** First prefetch fires this soon after start so the "not downloaded"
|
|
43
|
+
* dots fill in without making the user wait a minute. */
|
|
44
|
+
prefetchInitialDelayMs?: number;
|
|
45
|
+
/** Skip prefetch when the interactive body-fetch lane has at least
|
|
46
|
+
* this many items pending. Back-pressure keeps clicks responsive. */
|
|
47
|
+
prefetchBackpressureThreshold?: number;
|
|
48
|
+
/** Tombstone prune interval — bookkeeping only, hourly default. */
|
|
49
|
+
tombstonePruneIntervalMs?: number;
|
|
50
|
+
/** Tombstone retention — local-delete records older than this are
|
|
51
|
+
* removed by the periodic prune. */
|
|
52
|
+
tombstoneRetentionDays?: number;
|
|
53
|
+
}
|
|
54
|
+
export declare class Reconciler {
|
|
55
|
+
private db;
|
|
56
|
+
private imapManager;
|
|
57
|
+
private queue;
|
|
58
|
+
private opts;
|
|
59
|
+
private running;
|
|
60
|
+
private statusTimer;
|
|
61
|
+
private prefetchTimer;
|
|
62
|
+
private tombstoneTimer;
|
|
63
|
+
private inFlightFetches;
|
|
64
|
+
/** Last status payload — used to suppress no-op syncStateChanged events.
|
|
65
|
+
* During a heavy multi-folder sync the 5s tick fires a lot of identical
|
|
66
|
+
* events; the WebView's event handler runs DOM work for each, which
|
|
67
|
+
* stacks up under load. Diff-only emits keep the channel quiet. */
|
|
68
|
+
private lastStatus;
|
|
69
|
+
constructor(db: MailxDB, imapManager: ImapManager, queue: SyncQueue, opts?: ReconcilerOptions);
|
|
70
|
+
start(): void;
|
|
71
|
+
stop(): void;
|
|
72
|
+
/** Hourly bookkeeping — drop tombstones older than the retention window
|
|
73
|
+
* so the deleted-message lookup stays fast. */
|
|
74
|
+
private runTombstonePrune;
|
|
75
|
+
/** Fire prefetchBodies on each account unless the interactive body-fetch
|
|
76
|
+
* lane has user-clicked work pending — in which case skip this tick.
|
|
77
|
+
* ImapManager's per-account `prefetchingAccounts` guard de-dupes if
|
|
78
|
+
* the previous tick is still in flight. */
|
|
79
|
+
private runPrefetchTick;
|
|
80
|
+
/** User-visible "sync now" — runs an immediate poll on all accounts
|
|
81
|
+
* (or just one) without waiting for the next periodic tick. */
|
|
82
|
+
syncNow(accountId?: string): void;
|
|
83
|
+
/** Drain the body-fetch lane up to `bodyFetchConcurrency`. Each
|
|
84
|
+
* fetch is fire-and-forget; on completion it kicks the pump again
|
|
85
|
+
* so a queue that grows during work stays drained. */
|
|
86
|
+
private pumpBodyFetches;
|
|
87
|
+
/** External nudge — call after enqueueing a high-priority body fetch. */
|
|
88
|
+
kick(): void;
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=reconciler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reconciler.d.ts","sourceRoot":"","sources":["reconciler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAC;AACzD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAEjD,MAAM,WAAW,iBAAiB;IAC9B;;sDAEkD;IAClD,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,qEAAqE;IACrE,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,wDAAwD;IACxD,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;8DAC0D;IAC1D,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC;0EACsE;IACtE,6BAA6B,CAAC,EAAE,MAAM,CAAC;IACvC,mEAAmE;IACnE,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC;yCACqC;IACrC,sBAAsB,CAAC,EAAE,MAAM,CAAC;CACnC;AAYD,qBAAa,UAAU;IAcf,OAAO,CAAC,EAAE;IACV,OAAO,CAAC,WAAW;IACnB,OAAO,CAAC,KAAK;IAfjB,OAAO,CAAC,IAAI,CAA8B;IAC1C,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,WAAW,CAA+C;IAClE,OAAO,CAAC,aAAa,CAA+C;IACpE,OAAO,CAAC,cAAc,CAA+C;IACrE,OAAO,CAAC,eAAe,CAAK;IAC5B;;;wEAGoE;IACpE,OAAO,CAAC,UAAU,CAAgE;gBAGtE,EAAE,EAAE,OAAO,EACX,WAAW,EAAE,WAAW,EACxB,KAAK,EAAE,SAAS,EACxB,IAAI,GAAE,iBAAsB;IAKhC,KAAK,IAAI,IAAI;IAyCb,IAAI,IAAI,IAAI;IAOZ;oDACgD;IAChD,OAAO,CAAC,iBAAiB;IAMzB;;;gDAG4C;IAC5C,OAAO,CAAC,eAAe;IAcvB;oEACgE;IAChE,OAAO,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI;IAOjC;;2DAEuD;IACvD,OAAO,CAAC,eAAe;IAmDvB,yEAAyE;IACzE,IAAI,IAAI,IAAI;CAGf"}
|