@bobfrankston/rmfmail 1.2.97 → 1.2.100

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.
Files changed (41) hide show
  1. package/client/android-bootstrap.bundle.js +85 -22
  2. package/client/android-bootstrap.bundle.js.map +2 -2
  3. package/client/app.bundle.js +36 -1
  4. package/client/app.bundle.js.map +2 -2
  5. package/client/app.js +25 -0
  6. package/client/app.js.map +1 -1
  7. package/client/app.ts +25 -0
  8. package/client/components/message-viewer.js +29 -1
  9. package/client/components/message-viewer.js.map +1 -1
  10. package/client/components/message-viewer.ts +25 -1
  11. package/package.json +3 -3
  12. package/packages/mailx-imap/index.d.ts.map +1 -1
  13. package/packages/mailx-imap/index.js +6 -2
  14. package/packages/mailx-imap/index.js.map +1 -1
  15. package/packages/mailx-imap/index.ts +6 -2
  16. package/packages/mailx-imap/package-lock.json +2 -2
  17. package/packages/mailx-imap/package.json +1 -1
  18. package/packages/mailx-store/db.d.ts +7 -0
  19. package/packages/mailx-store/db.d.ts.map +1 -1
  20. package/packages/mailx-store/db.js +13 -1
  21. package/packages/mailx-store/db.js.map +1 -1
  22. package/packages/mailx-store/db.ts +19 -1
  23. package/packages/mailx-store/package.json +1 -1
  24. package/packages/mailx-store-web/android-bootstrap.d.ts.map +1 -1
  25. package/packages/mailx-store-web/android-bootstrap.js +74 -19
  26. package/packages/mailx-store-web/android-bootstrap.js.map +1 -1
  27. package/packages/mailx-store-web/android-bootstrap.ts +71 -16
  28. package/packages/mailx-store-web/db.d.ts +1 -0
  29. package/packages/mailx-store-web/db.d.ts.map +1 -1
  30. package/packages/mailx-store-web/db.js +21 -5
  31. package/packages/mailx-store-web/db.js.map +1 -1
  32. package/packages/mailx-store-web/db.ts +20 -7
  33. package/packages/mailx-store-web/imap-web-provider.d.ts.map +1 -1
  34. package/packages/mailx-store-web/imap-web-provider.js +1 -0
  35. package/packages/mailx-store-web/imap-web-provider.js.map +1 -1
  36. package/packages/mailx-store-web/imap-web-provider.ts +1 -0
  37. package/packages/mailx-store-web/package.json +1 -1
  38. package/packages/mailx-store-web/sync-manager.d.ts.map +1 -1
  39. package/packages/mailx-store-web/sync-manager.js +4 -0
  40. package/packages/mailx-store-web/sync-manager.js.map +1 -1
  41. package/packages/mailx-store-web/sync-manager.ts +4 -0
@@ -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.getProvider(accountId);
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
- fetchPromise,
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;