@bobfrankston/rmfmail 1.2.98 → 1.2.101
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/TODO.md +1 -0
- package/client/android-bootstrap.bundle.js +85 -22
- package/client/android-bootstrap.bundle.js.map +2 -2
- package/client/app.bundle.js +57 -0
- package/client/app.bundle.js.map +2 -2
- package/client/app.js +63 -0
- package/client/app.js.map +1 -1
- package/client/app.ts +58 -0
- package/client/components/message-viewer.js +23 -0
- package/client/components/message-viewer.js.map +1 -1
- package/client/components/message-viewer.ts +19 -0
- package/client/index.html +1 -0
- package/client/styles/components.css +50 -0
- package/docs/preferences.md +1 -0
- package/package.json +7 -7
- package/packages/mailx-imap/index.d.ts.map +1 -1
- package/packages/mailx-imap/index.js +6 -2
- package/packages/mailx-imap/index.js.map +1 -1
- package/packages/mailx-imap/index.ts +6 -2
- package/packages/mailx-imap/package-lock.json +2 -2
- package/packages/mailx-imap/package.json +1 -1
- package/packages/mailx-settings/docs/preferences.md +1 -0
- package/packages/mailx-store/db.d.ts +7 -0
- package/packages/mailx-store/db.d.ts.map +1 -1
- package/packages/mailx-store/db.js +13 -1
- package/packages/mailx-store/db.js.map +1 -1
- package/packages/mailx-store/db.ts +19 -1
- package/packages/mailx-store/package.json +1 -1
- package/packages/mailx-store-web/android-bootstrap.d.ts.map +1 -1
- package/packages/mailx-store-web/android-bootstrap.js +74 -19
- package/packages/mailx-store-web/android-bootstrap.js.map +1 -1
- package/packages/mailx-store-web/android-bootstrap.ts +71 -16
- package/packages/mailx-store-web/db.d.ts +1 -0
- package/packages/mailx-store-web/db.d.ts.map +1 -1
- package/packages/mailx-store-web/db.js +21 -5
- package/packages/mailx-store-web/db.js.map +1 -1
- package/packages/mailx-store-web/db.ts +20 -7
- package/packages/mailx-store-web/imap-web-provider.d.ts.map +1 -1
- package/packages/mailx-store-web/imap-web-provider.js +1 -0
- package/packages/mailx-store-web/imap-web-provider.js.map +1 -1
- package/packages/mailx-store-web/imap-web-provider.ts +1 -0
- package/packages/mailx-store-web/package.json +1 -1
- package/packages/mailx-store-web/sync-manager.d.ts.map +1 -1
- package/packages/mailx-store-web/sync-manager.js +4 -0
- package/packages/mailx-store-web/sync-manager.js.map +1 -1
- package/packages/mailx-store-web/sync-manager.ts +4 -0
package/TODO.md
CHANGED
|
@@ -784,6 +784,7 @@ Was part of S1 "local-first reconciliation refactor" until 2026-04-23 when S1's
|
|
|
784
784
|
- [x] **Validate email addresses on send** — To/Cc/Bcc/From all vetted against `local@domain.tld` shape before send.
|
|
785
785
|
- [x] **Undo move (Ctrl+Z)** — works for last move and last delete (whichever is more recent), 60s window.
|
|
786
786
|
- [ ] **Message list management** — column resize (drag headers), column reorder, sort by clicking column headers (from/date/subject/size with secondary sort by date), show/hide columns (size, account) via View menu
|
|
787
|
+
- [ ] **Generalize "Eleanor view" into finer view control [M]** — the View-menu "Eleanor view" checkbox (2026-07-04) is a named CSS preset: Thunderbird-classic one-line ★|Subject|From|Date table, extra whitespace, monochrome rows. Replace the one-off preset with real per-view settings — column set + order, row density (compact/normal/airy), row tinting on/off, snippet on/off — persisted per folder or as named view profiles. The preset becomes just a saved profile once profiles exist.
|
|
787
788
|
- [ ] **Duplicate Message-ID handling** — detect messages with the same Message-ID (copies from multiple accounts, re-delivered mail). Options: deduplicate in unified inbox, show indicator, or let user choose which to keep.
|
|
788
789
|
|
|
789
790
|
### msger / mailx factoring
|
|
@@ -4797,6 +4797,7 @@ var SCHEMA = `
|
|
|
4797
4797
|
in_reply_to TEXT,
|
|
4798
4798
|
refs TEXT,
|
|
4799
4799
|
date INTEGER NOT NULL,
|
|
4800
|
+
sent_date INTEGER,
|
|
4800
4801
|
subject TEXT DEFAULT '',
|
|
4801
4802
|
from_address TEXT DEFAULT '',
|
|
4802
4803
|
from_name TEXT DEFAULT '',
|
|
@@ -4995,6 +4996,10 @@ var WebMailxDB = class {
|
|
|
4995
4996
|
}
|
|
4996
4997
|
this.db.run("PRAGMA foreign_keys = ON");
|
|
4997
4998
|
this.db.run(SCHEMA);
|
|
4999
|
+
try {
|
|
5000
|
+
this.db.run("ALTER TABLE messages ADD COLUMN sent_date INTEGER");
|
|
5001
|
+
} catch {
|
|
5002
|
+
}
|
|
4998
5003
|
this.scheduleSave();
|
|
4999
5004
|
}
|
|
5000
5005
|
/** Wait for DB initialization */
|
|
@@ -5139,14 +5144,14 @@ var WebMailxDB = class {
|
|
|
5139
5144
|
upsertMessage(msg) {
|
|
5140
5145
|
const existing = this.get("SELECT id FROM messages WHERE account_id = ? AND folder_id = ? AND uid = ?", [msg.accountId, msg.folderId, msg.uid]);
|
|
5141
5146
|
if (existing) {
|
|
5142
|
-
this.run(`UPDATE messages SET flags_json = ?, preview = ?, body_path = ?, cached_at = ? WHERE id = ?`, [JSON.stringify(msg.flags), msg.preview, msg.bodyPath, Date.now(), existing.id]);
|
|
5147
|
+
this.run(`UPDATE messages SET flags_json = ?, preview = ?, body_path = ?, sent_date = COALESCE(sent_date, ?), cached_at = ? WHERE id = ?`, [JSON.stringify(msg.flags), msg.preview, msg.bodyPath, msg.sentDate ?? msg.date, Date.now(), existing.id]);
|
|
5143
5148
|
return existing.id;
|
|
5144
5149
|
}
|
|
5145
5150
|
this.run(`INSERT INTO messages (
|
|
5146
5151
|
account_id, folder_id, uid, message_id, in_reply_to, refs,
|
|
5147
|
-
date, subject, from_address, from_name, to_json, cc_json,
|
|
5152
|
+
date, sent_date, subject, from_address, from_name, to_json, cc_json,
|
|
5148
5153
|
flags_json, size, has_attachments, preview, body_path, cached_at
|
|
5149
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
5154
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
5150
5155
|
msg.accountId,
|
|
5151
5156
|
msg.folderId,
|
|
5152
5157
|
msg.uid,
|
|
@@ -5154,6 +5159,7 @@ var WebMailxDB = class {
|
|
|
5154
5159
|
msg.inReplyTo,
|
|
5155
5160
|
JSON.stringify(msg.references),
|
|
5156
5161
|
msg.date,
|
|
5162
|
+
msg.sentDate ?? msg.date,
|
|
5157
5163
|
msg.subject,
|
|
5158
5164
|
msg.from.address,
|
|
5159
5165
|
msg.from.name,
|
|
@@ -5490,6 +5496,9 @@ var WebMailxDB = class {
|
|
|
5490
5496
|
inReplyTo: r.in_reply_to || "",
|
|
5491
5497
|
references: JSON.parse(r.refs || "[]"),
|
|
5492
5498
|
date: r.date,
|
|
5499
|
+
// NULL for pre-migration rows — the client's `sentDate ?? date`
|
|
5500
|
+
// fallback then shows the received date, same as before.
|
|
5501
|
+
sentDate: r.sent_date ?? void 0,
|
|
5493
5502
|
subject: r.subject,
|
|
5494
5503
|
from: { name: r.from_name, address: r.from_address },
|
|
5495
5504
|
to: JSON.parse(r.to_json),
|
|
@@ -9365,6 +9374,7 @@ function toProviderMessage(m) {
|
|
|
9365
9374
|
messageId: m.messageId || "",
|
|
9366
9375
|
providerId: "",
|
|
9367
9376
|
date: m.date || null,
|
|
9377
|
+
sentDate: m.sentDate || void 0,
|
|
9368
9378
|
subject: m.subject || "",
|
|
9369
9379
|
from: m.from || [],
|
|
9370
9380
|
to: m.to || [],
|
|
@@ -9838,6 +9848,20 @@ var AndroidSyncManager = class {
|
|
|
9838
9848
|
bodyStore;
|
|
9839
9849
|
providers = /* @__PURE__ */ new Map();
|
|
9840
9850
|
tokenProviders = /* @__PURE__ */ new Map();
|
|
9851
|
+
// Dedicated body-fetch lane per IMAP account, mirroring the desktop's
|
|
9852
|
+
// per-workload connections (v1.2.50). Body fetches used the SAME
|
|
9853
|
+
// connection as folder sync; iflow-direct serializes single commands but
|
|
9854
|
+
// not SELECT+FETCH *pairs*, so a concurrent folder sync could re-SELECT
|
|
9855
|
+
// another mailbox between fetchOne's SELECT and its UID FETCH — the FETCH
|
|
9856
|
+
// then ran against the wrong mailbox and returned nothing, which the
|
|
9857
|
+
// viewer showed as "no body text" (Bob 2026-07-03, bobma/4970037-38).
|
|
9858
|
+
// Lazy: the connection opens on the first body fetch. Gmail accounts skip
|
|
9859
|
+
// this — the Gmail provider is stateless HTTP with no SELECT state.
|
|
9860
|
+
fetchProviders = /* @__PURE__ */ new Map();
|
|
9861
|
+
imapAccountConfigs = /* @__PURE__ */ new Map();
|
|
9862
|
+
// Per-account FIFO so two body fetches (interactive click vs prefetch)
|
|
9863
|
+
// can't interleave their own SELECT+FETCH pairs on the fetch lane either.
|
|
9864
|
+
fetchChains = /* @__PURE__ */ new Map();
|
|
9841
9865
|
// One prefetch session per account — prevents every syncAll tick from
|
|
9842
9866
|
// spawning parallel fetch loops that race on IndexedDB and blow through
|
|
9843
9867
|
// Gmail's per-user quota.
|
|
@@ -9888,6 +9912,7 @@ var AndroidSyncManager = class {
|
|
|
9888
9912
|
fetchChunkSizeMax: 100
|
|
9889
9913
|
}, () => new BridgeTcpTransport());
|
|
9890
9914
|
this.providers.set(account.id, provider);
|
|
9915
|
+
this.imapAccountConfigs.set(account.id, account);
|
|
9891
9916
|
vlog(`addAccount ${account.id}: IMAP provider registered (${account.imap.host}:${account.imap.port})`);
|
|
9892
9917
|
console.log(`[sync] ${account.id}: IMAP provider registered (${account.imap.host})`);
|
|
9893
9918
|
} catch (e) {
|
|
@@ -9907,6 +9932,43 @@ var AndroidSyncManager = class {
|
|
|
9907
9932
|
getProvider(accountId) {
|
|
9908
9933
|
return this.providers.get(accountId) || null;
|
|
9909
9934
|
}
|
|
9935
|
+
/** Provider for BODY fetches. IMAP accounts get their own lazily-opened
|
|
9936
|
+
* connection so a fetch can never share (and race) the sync connection's
|
|
9937
|
+
* SELECT state. Gmail (stateless HTTP) reuses the main provider. */
|
|
9938
|
+
getFetchProvider(accountId) {
|
|
9939
|
+
const cfg = this.imapAccountConfigs.get(accountId);
|
|
9940
|
+
if (!cfg?.imap?.host || !cfg.imap.user) return this.getProvider(accountId);
|
|
9941
|
+
let p = this.fetchProviders.get(accountId);
|
|
9942
|
+
if (!p) {
|
|
9943
|
+
try {
|
|
9944
|
+
p = new ImapWebProvider({
|
|
9945
|
+
server: cfg.imap.host,
|
|
9946
|
+
port: cfg.imap.port || 993,
|
|
9947
|
+
username: cfg.imap.user,
|
|
9948
|
+
password: cfg.imap.password,
|
|
9949
|
+
inactivityTimeout: 3e5,
|
|
9950
|
+
fetchChunkSize: 10,
|
|
9951
|
+
fetchChunkSizeMax: 100
|
|
9952
|
+
}, () => new BridgeTcpTransport());
|
|
9953
|
+
this.fetchProviders.set(accountId, p);
|
|
9954
|
+
console.log(`[fetchBody] ${accountId}: dedicated fetch connection opened`);
|
|
9955
|
+
} catch (e) {
|
|
9956
|
+
console.error(`[fetchBody] ${accountId}: fetch-lane provider failed (${e?.message || e}) \u2014 falling back to sync connection`);
|
|
9957
|
+
return this.getProvider(accountId);
|
|
9958
|
+
}
|
|
9959
|
+
}
|
|
9960
|
+
return p;
|
|
9961
|
+
}
|
|
9962
|
+
/** Per-account FIFO for body fetches: one SELECT+FETCH pair at a time on
|
|
9963
|
+
* the fetch lane. A rejected task never breaks the chain. */
|
|
9964
|
+
enqueueFetch(accountId, fn) {
|
|
9965
|
+
const prev = this.fetchChains.get(accountId) || Promise.resolve();
|
|
9966
|
+
const next = prev.then(fn, fn);
|
|
9967
|
+
this.fetchChains.set(accountId, next.then(() => {
|
|
9968
|
+
}, () => {
|
|
9969
|
+
}));
|
|
9970
|
+
return next;
|
|
9971
|
+
}
|
|
9910
9972
|
async syncAll() {
|
|
9911
9973
|
if (this.syncAllInflight) {
|
|
9912
9974
|
console.log("[sync] inbox sync already in flight \u2014 coalescing this trigger");
|
|
@@ -10168,6 +10230,7 @@ var AndroidSyncManager = class {
|
|
|
10168
10230
|
if (msg.answered) flags.push("\\Answered");
|
|
10169
10231
|
if (msg.draft) flags.push("\\Draft");
|
|
10170
10232
|
const bodyPath = msg.providerId ? `gmail:${msg.providerId}` : "";
|
|
10233
|
+
const sentRaw = msg.sentDate instanceof Date ? msg.sentDate.getTime() : NaN;
|
|
10171
10234
|
this.db.upsertMessage({
|
|
10172
10235
|
accountId,
|
|
10173
10236
|
folderId,
|
|
@@ -10176,6 +10239,7 @@ var AndroidSyncManager = class {
|
|
|
10176
10239
|
inReplyTo: "",
|
|
10177
10240
|
references: [],
|
|
10178
10241
|
date: msg.date ? msg.date.getTime() : Date.now(),
|
|
10242
|
+
sentDate: Number.isFinite(sentRaw) ? sentRaw : void 0,
|
|
10179
10243
|
subject: msg.subject || "",
|
|
10180
10244
|
from: toEmailAddress(msg.from?.[0]),
|
|
10181
10245
|
to: msg.to.map((a) => toEmailAddress(a)),
|
|
@@ -10198,7 +10262,7 @@ var AndroidSyncManager = class {
|
|
|
10198
10262
|
if (await this.bodyStore.hasMessage(accountId, folderId, uid)) {
|
|
10199
10263
|
return await this.bodyStore.getMessage(accountId, folderId, uid);
|
|
10200
10264
|
}
|
|
10201
|
-
const provider = this.
|
|
10265
|
+
const provider = this.getFetchProvider(accountId);
|
|
10202
10266
|
if (!provider) {
|
|
10203
10267
|
console.warn(`[fetchBody] No provider for ${accountId}`);
|
|
10204
10268
|
return null;
|
|
@@ -10207,30 +10271,29 @@ var AndroidSyncManager = class {
|
|
|
10207
10271
|
const bp = envelope?.bodyPath || "";
|
|
10208
10272
|
const FETCH_TIMEOUT_MS = 6e4;
|
|
10209
10273
|
let __folderPath = "(n/a)";
|
|
10210
|
-
const fetchPromise = (async () => {
|
|
10211
|
-
if (bp.startsWith("gmail:") && provider.fetchById) {
|
|
10212
|
-
const providerId = bp.substring(6);
|
|
10213
|
-
__folderPath = `gmail:${bp.substring(6)}`;
|
|
10214
|
-
return provider.fetchById(providerId, { source: true });
|
|
10215
|
-
}
|
|
10216
|
-
const folders = this.db.getFolders(accountId);
|
|
10217
|
-
const folder = folders.find((f) => f.id === folderId);
|
|
10218
|
-
if (!folder) {
|
|
10219
|
-
__folderPath = "(folder row missing)";
|
|
10220
|
-
return null;
|
|
10221
|
-
}
|
|
10222
|
-
__folderPath = folder.path || "(empty path)";
|
|
10223
|
-
return provider.fetchOne(folder.path, uid, { source: true });
|
|
10224
|
-
})();
|
|
10225
10274
|
let msg = null;
|
|
10226
10275
|
try {
|
|
10227
|
-
msg = await Promise.race([
|
|
10228
|
-
|
|
10276
|
+
msg = await this.enqueueFetch(accountId, () => Promise.race([
|
|
10277
|
+
(async () => {
|
|
10278
|
+
if (bp.startsWith("gmail:") && provider.fetchById) {
|
|
10279
|
+
const providerId = bp.substring(6);
|
|
10280
|
+
__folderPath = `gmail:${bp.substring(6)}`;
|
|
10281
|
+
return provider.fetchById(providerId, { source: true });
|
|
10282
|
+
}
|
|
10283
|
+
const folders = this.db.getFolders(accountId);
|
|
10284
|
+
const folder = folders.find((f) => f.id === folderId);
|
|
10285
|
+
if (!folder) {
|
|
10286
|
+
__folderPath = "(folder row missing)";
|
|
10287
|
+
return null;
|
|
10288
|
+
}
|
|
10289
|
+
__folderPath = folder.path || "(empty path)";
|
|
10290
|
+
return provider.fetchOne(folder.path, uid, { source: true });
|
|
10291
|
+
})(),
|
|
10229
10292
|
new Promise((_, reject) => setTimeout(
|
|
10230
10293
|
() => reject(new Error(`body-fetch timeout ${FETCH_TIMEOUT_MS / 1e3}s (${accountId}/${folderId}/${uid})`)),
|
|
10231
10294
|
FETCH_TIMEOUT_MS
|
|
10232
10295
|
))
|
|
10233
|
-
]);
|
|
10296
|
+
]));
|
|
10234
10297
|
} catch (e) {
|
|
10235
10298
|
console.error(`[fetchBody] failed ${accountId}/${folderId}/${uid} after ${Date.now() - t0}ms: ${e?.message || e}`);
|
|
10236
10299
|
throw e;
|