@bobfrankston/mailx 1.0.409 → 1.0.413

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.
@@ -373,8 +373,8 @@ export async function showMessage(accountId, uid, folderId, specialUse, isRetry
373
373
  if (anyUrl) {
374
374
  unsubBtn.hidden = false;
375
375
  unsubBtn.textContent = oneClick && httpUrl ? "Unsubscribe (1-click)" : "Unsubscribe";
376
- unsubBtn.title = anyUrl;
377
- unsubBtn.href = "#";
376
+ unsubBtn.removeAttribute("title");
377
+ unsubBtn.href = httpUrl || mailUrl || "#";
378
378
  unsubBtn.onclick = async (e) => {
379
379
  e.preventDefault();
380
380
  const status = document.getElementById("status-sync");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx",
3
- "version": "1.0.409",
3
+ "version": "1.0.413",
4
4
  "description": "Local-first email client with IMAP sync and standalone native app",
5
5
  "type": "module",
6
6
  "main": "bin/mailx.js",
@@ -177,6 +177,12 @@ export declare class ImapManager extends EventEmitter {
177
177
  private reconnectOps;
178
178
  /** Handle sync errors — classify and emit appropriate UI events */
179
179
  private handleSyncError;
180
+ /** Fetch ONLY new messages above highestUid for one account's INBOX —
181
+ * the IDLE callback's hot path. Skips gap detection, backfill, and the
182
+ * server reconcile (each of which fetches a full UID list — multi-second
183
+ * on a large mailbox). The 5-minute STATUS poll path still runs full
184
+ * `syncFolder` so deletions and gaps eventually reconcile. */
185
+ syncInboxNewOnly(accountId: string): Promise<void>;
180
186
  /** Sync just INBOX for each account (fast check for new mail) */
181
187
  syncInbox(): Promise<void>;
182
188
  /** Quick inbox check — per-account lightweight probe.
@@ -1540,6 +1540,46 @@ export class ImapManager extends EventEmitter {
1540
1540
  this.emit("accountError", accountId, errMsg, errMsg, isOAuth);
1541
1541
  }
1542
1542
  }
1543
+ /** Fetch ONLY new messages above highestUid for one account's INBOX —
1544
+ * the IDLE callback's hot path. Skips gap detection, backfill, and the
1545
+ * server reconcile (each of which fetches a full UID list — multi-second
1546
+ * on a large mailbox). The 5-minute STATUS poll path still runs full
1547
+ * `syncFolder` so deletions and gaps eventually reconcile. */
1548
+ async syncInboxNewOnly(accountId) {
1549
+ if (this.isGmailAccount(accountId))
1550
+ return; // IDLE is IMAP-only
1551
+ const inbox = this.db.getFolders(accountId).find(f => f.specialUse === "inbox");
1552
+ if (!inbox)
1553
+ return;
1554
+ try {
1555
+ await this.withConnection(accountId, async (client) => {
1556
+ const highestUid = this.db.getHighestUid(accountId, inbox.id);
1557
+ if (highestUid === 0) {
1558
+ // First sync — fall through to full path so the date-windowed
1559
+ // backfill runs. `syncFolder` handles the no-highestUid case.
1560
+ await this.syncFolder(accountId, inbox.id, client);
1561
+ return;
1562
+ }
1563
+ const fetched = await client.fetchMessagesSinceUid(inbox.path, highestUid, { source: false });
1564
+ const fresh = fetched.filter((m) => m.uid > highestUid);
1565
+ if (fresh.length === 0)
1566
+ return;
1567
+ const stored = await this.storeMessages(accountId, inbox.id, inbox, fresh, highestUid);
1568
+ if (stored > 0) {
1569
+ this.db.recalcFolderCounts(inbox.id);
1570
+ const updated = this.db.getFolders(accountId).find(f => f.id === inbox.id);
1571
+ this.emit("folderCountsChanged", accountId, {
1572
+ [inbox.id]: { total: updated?.totalCount || 0, unread: updated?.unreadCount || 0 }
1573
+ });
1574
+ this.emit("folderSynced", accountId, inbox.id, Date.now());
1575
+ console.log(` [idle-fast] ${accountId}: stored ${stored} new message(s)`);
1576
+ }
1577
+ });
1578
+ }
1579
+ catch (e) {
1580
+ console.error(` [idle-fast] ${accountId}: ${e.message}`);
1581
+ }
1582
+ }
1543
1583
  /** Sync just INBOX for each account (fast check for new mail) */
1544
1584
  async syncInbox() {
1545
1585
  if (this.inboxSyncing)
@@ -1783,8 +1823,10 @@ export class ImapManager extends EventEmitter {
1783
1823
  const watchClient = this.createClient(accountId);
1784
1824
  const stop = await watchClient.watchMailbox("INBOX", (newCount) => {
1785
1825
  console.log(` [idle] ${accountId}: ${newCount} new message(s)`);
1786
- // Sync just INBOX for speedfull sync runs on the configured interval
1787
- this.syncInbox().catch(e => console.error(` [idle] sync error: ${e.message}`));
1826
+ // Fetch only the new UIDs — the heavyweight gap/reconcile
1827
+ // path runs on the 5-minute STATUS poll, so EXISTS lands
1828
+ // in the UI in roughly one round-trip.
1829
+ this.syncInboxNewOnly(accountId).catch(e => console.error(` [idle] sync error: ${e.message}`));
1788
1830
  });
1789
1831
  this.watchers.set(accountId, async () => {
1790
1832
  await stop();
@@ -278,11 +278,12 @@ export class MailxService {
278
278
  parseListUnsubscribe(parsed2.headers));
279
279
  listUnsubscribe = listUnsubscribeHttp || listUnsubscribeMail;
280
280
  }
281
- // EML path: read the real on-disk path from `envelope.bodyPath` (DB
282
- // is authoritative since v1.0.361 files are opaque UUIDs, not the
283
- // old {folderId}/{uid}.eml layout). Synthesizing the legacy path
284
- // here showed users a path that doesn't exist.
285
- const emlPath = envelope.bodyPath || "";
281
+ // EML path: re-read the row after the fetch `fetchMessageBody`
282
+ // writes the body to disk and updates `body_path` on success, but the
283
+ // `envelope` snapshot above pre-dates that write, so trusting it
284
+ // hides the Source button on every just-opened message.
285
+ const refreshed = this.db.getMessageByUid(accountId, uid, folderId);
286
+ const emlPath = refreshed?.bodyPath || envelope.bodyPath || "";
286
287
  return {
287
288
  ...envelope, bodyHtml, bodyText, hasRemoteContent, remoteAllowed: allowRemote,
288
289
  attachments, emlPath, deliveredTo, returnPath,