@bobfrankston/rmfmail 1.0.546 → 1.0.548
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 +51 -1
- package/client/app.js.map +1 -1
- package/client/app.ts +50 -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/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-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 +112 -12
- package/packages/mailx-service/local-store.js.map +1 -1
- package/packages/mailx-service/reconciler.d.ts +62 -0
- package/packages/mailx-service/reconciler.d.ts.map +1 -0
- package/packages/mailx-service/reconciler.js +141 -0
- package/packages/mailx-service/reconciler.js.map +1 -0
- package/packages/mailx-service/reconciler.ts +151 -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
|
@@ -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;IAmJvH,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,70 @@ 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
|
+
emlPath: storedPath,
|
|
242
|
+
isFlagged,
|
|
143
243
|
};
|
|
144
244
|
}
|
|
145
245
|
// ── 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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"}
|
|
@@ -0,0 +1,62 @@
|
|
|
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
|
+
}
|
|
41
|
+
export declare class Reconciler {
|
|
42
|
+
private db;
|
|
43
|
+
private imapManager;
|
|
44
|
+
private queue;
|
|
45
|
+
private opts;
|
|
46
|
+
private running;
|
|
47
|
+
private statusTimer;
|
|
48
|
+
private inFlightFetches;
|
|
49
|
+
constructor(db: MailxDB, imapManager: ImapManager, queue: SyncQueue, opts?: ReconcilerOptions);
|
|
50
|
+
start(): void;
|
|
51
|
+
stop(): void;
|
|
52
|
+
/** User-visible "sync now" — runs an immediate poll on all accounts
|
|
53
|
+
* (or just one) without waiting for the next periodic tick. */
|
|
54
|
+
syncNow(accountId?: string): void;
|
|
55
|
+
/** Drain the body-fetch lane up to `bodyFetchConcurrency`. Each
|
|
56
|
+
* fetch is fire-and-forget; on completion it kicks the pump again
|
|
57
|
+
* so a queue that grows during work stays drained. */
|
|
58
|
+
private pumpBodyFetches;
|
|
59
|
+
/** External nudge — call after enqueueing a high-priority body fetch. */
|
|
60
|
+
kick(): void;
|
|
61
|
+
}
|
|
62
|
+
//# 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;CACjC;AAOD,qBAAa,UAAU;IAOf,OAAO,CAAC,EAAE;IACV,OAAO,CAAC,WAAW;IACnB,OAAO,CAAC,KAAK;IARjB,OAAO,CAAC,IAAI,CAA8B;IAC1C,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,WAAW,CAA+C;IAClE,OAAO,CAAC,eAAe,CAAK;gBAGhB,EAAE,EAAE,OAAO,EACX,WAAW,EAAE,WAAW,EACxB,KAAK,EAAE,SAAS,EACxB,IAAI,GAAE,iBAAsB;IAKhC,KAAK,IAAI,IAAI;IAeb,IAAI,IAAI,IAAI;IAKZ;oEACgE;IAChE,OAAO,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI;IAOjC;;2DAEuD;IACvD,OAAO,CAAC,eAAe;IAmDvB,yEAAyE;IACzE,IAAI,IAAI,IAAI;CAGf"}
|
|
@@ -0,0 +1,141 @@
|
|
|
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
|
+
const DEFAULTS = {
|
|
31
|
+
bodyFetchConcurrency: 2,
|
|
32
|
+
statusEmitIntervalMs: 5_000,
|
|
33
|
+
};
|
|
34
|
+
export class Reconciler {
|
|
35
|
+
db;
|
|
36
|
+
imapManager;
|
|
37
|
+
queue;
|
|
38
|
+
opts;
|
|
39
|
+
running = false;
|
|
40
|
+
statusTimer = null;
|
|
41
|
+
inFlightFetches = 0;
|
|
42
|
+
constructor(db, imapManager, queue, opts = {}) {
|
|
43
|
+
this.db = db;
|
|
44
|
+
this.imapManager = imapManager;
|
|
45
|
+
this.queue = queue;
|
|
46
|
+
this.opts = { ...DEFAULTS, ...opts };
|
|
47
|
+
}
|
|
48
|
+
start() {
|
|
49
|
+
if (this.running)
|
|
50
|
+
return;
|
|
51
|
+
this.running = true;
|
|
52
|
+
// Status pill: emit current totals every few seconds so the UI
|
|
53
|
+
// can show "Syncing N items" without polling.
|
|
54
|
+
this.statusTimer = setInterval(() => {
|
|
55
|
+
const counts = this.queue.pendingCount();
|
|
56
|
+
this.imapManager.emit("syncStateChanged", { ...counts, inFlightFetches: this.inFlightFetches });
|
|
57
|
+
}, this.opts.statusEmitIntervalMs);
|
|
58
|
+
// Kick the body-fetch loop.
|
|
59
|
+
this.pumpBodyFetches();
|
|
60
|
+
}
|
|
61
|
+
stop() {
|
|
62
|
+
this.running = false;
|
|
63
|
+
if (this.statusTimer) {
|
|
64
|
+
clearInterval(this.statusTimer);
|
|
65
|
+
this.statusTimer = null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/** User-visible "sync now" — runs an immediate poll on all accounts
|
|
69
|
+
* (or just one) without waiting for the next periodic tick. */
|
|
70
|
+
syncNow(accountId) {
|
|
71
|
+
const accts = accountId ? [{ id: accountId }] : this.db.getAccounts();
|
|
72
|
+
for (const a of accts) {
|
|
73
|
+
this.imapManager.processSyncActions(a.id).catch(() => { });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** Drain the body-fetch lane up to `bodyFetchConcurrency`. Each
|
|
77
|
+
* fetch is fire-and-forget; on completion it kicks the pump again
|
|
78
|
+
* so a queue that grows during work stays drained. */
|
|
79
|
+
pumpBodyFetches() {
|
|
80
|
+
if (!this.running)
|
|
81
|
+
return;
|
|
82
|
+
while (this.inFlightFetches < this.opts.bodyFetchConcurrency) {
|
|
83
|
+
const next = this.queue.nextBodyFetch();
|
|
84
|
+
if (!next)
|
|
85
|
+
return;
|
|
86
|
+
this.inFlightFetches++;
|
|
87
|
+
(async () => {
|
|
88
|
+
try {
|
|
89
|
+
const raw = await this.imapManager.fetchMessageBody(next.accountId, next.folderId, next.uid);
|
|
90
|
+
if (raw) {
|
|
91
|
+
this.imapManager.emit("bodyAvailable", {
|
|
92
|
+
accountId: next.accountId, folderId: next.folderId, uid: next.uid,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
if (err?.isNotFound) {
|
|
98
|
+
try {
|
|
99
|
+
this.db.deleteMessage(next.accountId, next.uid);
|
|
100
|
+
this.db.recalcFolderCounts(next.folderId);
|
|
101
|
+
this.imapManager.emit("messageRemoved", {
|
|
102
|
+
accountId: next.accountId, folderId: next.folderId, uid: next.uid,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
catch { /* */ }
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
const msg = err?.message || "body fetch failed";
|
|
109
|
+
const transient = /connection|Too many|UNAVAILABLE|rate|429|5\d\d|timeout|ENOTFOUND|ECONNRESET|ETIMEDOUT/i.test(msg);
|
|
110
|
+
// Transient: re-enqueue (budget=3) so the next pump
|
|
111
|
+
// tick retries. Surface the error only when the
|
|
112
|
+
// budget is exhausted — otherwise we'd flash banners
|
|
113
|
+
// for every blip on a slow server.
|
|
114
|
+
if (transient && this.queue.requeueBodyFetch(next)) {
|
|
115
|
+
// Backoff so we don't immediately re-hit the same
|
|
116
|
+
// wedge. Body-fetch lane is in-memory; setTimeout
|
|
117
|
+
// is fine.
|
|
118
|
+
const backoffMs = 500 * Math.pow(2, next.attempts);
|
|
119
|
+
setTimeout(() => this.pumpBodyFetches(), backoffMs);
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
this.imapManager.emit("bodyFetchError", {
|
|
123
|
+
accountId: next.accountId, folderId: next.folderId, uid: next.uid,
|
|
124
|
+
error: msg, transient,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
this.inFlightFetches--;
|
|
131
|
+
this.pumpBodyFetches();
|
|
132
|
+
}
|
|
133
|
+
})();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/** External nudge — call after enqueueing a high-priority body fetch. */
|
|
137
|
+
kick() {
|
|
138
|
+
this.pumpBodyFetches();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=reconciler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reconciler.js","sourceRoot":"","sources":["reconciler.ts"],"names":[],"mappings":"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"}
|