@bobfrankston/mailx 1.0.121 → 1.0.122

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx",
3
- "version": "1.0.121",
3
+ "version": "1.0.122",
4
4
  "description": "Local-first email client with IMAP sync and standalone native app",
5
5
  "type": "module",
6
6
  "main": "bin/mailx.js",
@@ -52,9 +52,10 @@ export declare class ImapManager extends EventEmitter {
52
52
  /** Track active IMAP connections for diagnostics */
53
53
  private activeConnections;
54
54
  /** Create a fresh IMAP client for an account (disposable, single-use).
55
- * Returns CompatImapClient (native) or ImapClient (imapflow) based on useNativeClient flag. */
55
+ * Returns CompatImapClient (native) or ImapClient (imapflow) based on useNativeClient flag.
56
+ * The client's logout() is wrapped to auto-decrement the connection counter. */
56
57
  private createClient;
57
- /** Track client logout for connection counting */
58
+ /** Track client logout for connection counting (called automatically by wrapped logout) */
58
59
  private trackLogout;
59
60
  /** Number of registered IMAP accounts */
60
61
  getAccountCount(): number;
@@ -187,7 +187,8 @@ export class ImapManager extends EventEmitter {
187
187
  /** Track active IMAP connections for diagnostics */
188
188
  activeConnections = new Map(); // accountId → count
189
189
  /** Create a fresh IMAP client for an account (disposable, single-use).
190
- * Returns CompatImapClient (native) or ImapClient (imapflow) based on useNativeClient flag. */
190
+ * Returns CompatImapClient (native) or ImapClient (imapflow) based on useNativeClient flag.
191
+ * The client's logout() is wrapped to auto-decrement the connection counter. */
191
192
  createClient(accountId) {
192
193
  if (this.reauthenticating.has(accountId))
193
194
  throw new Error(`Account ${accountId} is re-authenticating`);
@@ -203,12 +204,26 @@ export class ImapManager extends EventEmitter {
203
204
  this.activeConnections.set(accountId, count);
204
205
  const clientType = this.useNativeClient ? "native" : "imapflow";
205
206
  console.log(` [conn] ${accountId}: +1 ${clientType} (${count} active)`);
207
+ let client;
206
208
  if (this.useNativeClient) {
207
- return new CompatImapClient(config, () => new NodeTransport({ rejectUnauthorized: config.rejectUnauthorized !== false }));
209
+ client = new CompatImapClient(config, () => new NodeTransport({ rejectUnauthorized: config.rejectUnauthorized !== false }));
208
210
  }
209
- return new ImapClient(config);
211
+ else {
212
+ client = new ImapClient(config);
213
+ }
214
+ // Wrap logout to auto-decrement connection counter (prevents leaks from missed trackLogout calls)
215
+ const originalLogout = client.logout.bind(client);
216
+ let loggedOut = false;
217
+ client.logout = async () => {
218
+ await originalLogout();
219
+ if (!loggedOut) {
220
+ loggedOut = true;
221
+ this.trackLogout(accountId);
222
+ }
223
+ };
224
+ return client;
210
225
  }
211
- /** Track client logout for connection counting */
226
+ /** Track client logout for connection counting (called automatically by wrapped logout) */
212
227
  trackLogout(accountId) {
213
228
  const count = Math.max(0, (this.activeConnections.get(accountId) || 1) - 1);
214
229
  this.activeConnections.set(accountId, count);
@@ -299,26 +314,37 @@ export class ImapManager extends EventEmitter {
299
314
  const highestUid = this.db.getHighestUid(accountId, folderId);
300
315
  let messages;
301
316
  const firstSync = highestUid === 0;
317
+ const historyDays = getHistoryDays(accountId);
318
+ const startDate = historyDays > 0
319
+ ? new Date(Date.now() - historyDays * 86400000)
320
+ : new Date(0);
302
321
  if (highestUid > 0) {
303
322
  // Incremental: only fetch messages newer than what we have
304
323
  const fetched = await client.fetchMessagesSinceUid(folder.path, highestUid, { source: true });
305
324
  // Filter out the last known message (IMAP * always returns at least one)
306
325
  messages = fetched.filter(m => m.uid > highestUid);
326
+ // Backfill: if historyDays extends further back than our oldest message, fetch the gap
327
+ const oldestDate = this.db.getOldestDate(accountId, folderId);
328
+ if (oldestDate > 0 && startDate.getTime() < oldestDate) {
329
+ const existingUids = new Set(this.db.getUidsForFolder(accountId, folderId));
330
+ const backfill = await client.fetchMessageByDate(folder.path, startDate, new Date(oldestDate), { source: true });
331
+ const newBackfill = backfill.filter(m => !existingUids.has(m.uid));
332
+ if (newBackfill.length > 0) {
333
+ console.log(` ${folder.path}: backfilling ${newBackfill.length} older messages`);
334
+ messages.push(...newBackfill);
335
+ }
336
+ }
307
337
  }
308
338
  else {
309
339
  // First sync: use date-based fetch with bodies for local-first
310
- const historyDays = getHistoryDays(accountId);
311
- const startDate = historyDays > 0
312
- ? new Date(Date.now() - historyDays * 86400000)
313
- : new Date(0);
314
340
  messages = await client.fetchMessageByDate(folder.path, startDate, new Date(), { source: true });
315
- // Sort newest first so most recent messages appear in the UI immediately
316
- messages.sort((a, b) => {
317
- const da = a.date instanceof Date ? a.date.getTime() : (typeof a.date === "number" ? a.date : 0);
318
- const db = b.date instanceof Date ? b.date.getTime() : (typeof b.date === "number" ? b.date : 0);
319
- return db - da;
320
- });
321
341
  }
342
+ // Sort newest first so most recent messages appear in the UI immediately
343
+ messages.sort((a, b) => {
344
+ const da = a.date instanceof Date ? a.date.getTime() : (typeof a.date === "number" ? a.date : 0);
345
+ const db = b.date instanceof Date ? b.date.getTime() : (typeof b.date === "number" ? b.date : 0);
346
+ return db - da;
347
+ });
322
348
  if (messages.length > 0)
323
349
  console.log(` ${folder.path}: ${messages.length} new messages`);
324
350
  let newCount = 0;
@@ -1236,8 +1262,14 @@ export class ImapManager extends EventEmitter {
1236
1262
  try {
1237
1263
  // Get all UIDs in Outbox
1238
1264
  const uids = await client.getUids(outboxFolder.path);
1239
- if (uids.length === 0)
1265
+ if (uids.length === 0) {
1266
+ try {
1267
+ await client.logout();
1268
+ }
1269
+ catch { }
1270
+ this.trackLogout(accountId);
1240
1271
  return;
1272
+ }
1241
1273
  const sendingFlag = `$Sending-${this.hostname}`;
1242
1274
  for (const uid of uids) {
1243
1275
  // Check flags — skip if already being sent or permanently failed
@@ -1341,6 +1373,7 @@ export class ImapManager extends EventEmitter {
1341
1373
  await client.logout();
1342
1374
  }
1343
1375
  catch { /* ignore */ }
1376
+ this.trackLogout(accountId);
1344
1377
  }
1345
1378
  }
1346
1379
  /** Start background Outbox worker — runs immediately then every 10 seconds */
@@ -59,6 +59,7 @@ export declare class MailxDB {
59
59
  updateMessageFlags(accountId: string, uid: number, flags: string[]): void;
60
60
  updateBodyPath(accountId: string, uid: number, bodyPath: string): void;
61
61
  getHighestUid(accountId: string, folderId: number): number;
62
+ getOldestDate(accountId: string, folderId: number): number;
62
63
  /** Get all UIDs for a folder */
63
64
  getUidsForFolder(accountId: string, folderId: number): number[];
64
65
  /** Delete a message by account + UID */
@@ -324,6 +324,10 @@ export class MailxDB {
324
324
  const r = this.db.prepare("SELECT MAX(uid) as maxUid FROM messages WHERE account_id = ? AND folder_id = ?").get(accountId, folderId);
325
325
  return r?.maxUid || 0;
326
326
  }
327
+ getOldestDate(accountId, folderId) {
328
+ const r = this.db.prepare("SELECT MIN(date) as minDate FROM messages WHERE account_id = ? AND folder_id = ?").get(accountId, folderId);
329
+ return r?.minDate || 0;
330
+ }
327
331
  /** Get all UIDs for a folder */
328
332
  getUidsForFolder(accountId, folderId) {
329
333
  const rows = this.db.prepare("SELECT uid FROM messages WHERE account_id = ? AND folder_id = ?").all(accountId, folderId);