@bobfrankston/mailx 1.0.125 → 1.0.127

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/blowaway.cmd CHANGED
@@ -1,3 +1,3 @@
1
1
  del C:\Users\Bob\.mailx\mailx.db C:\Users\Bob\.mailx\mailx.db-shm C:\Users\Bob\.mailx\mailx.db-wal
2
- rmdir /s /q C:\Users\Bob\.mailx\mailxstore
2
+ if .%1==.all rmdir /s /q C:\Users\Bob\.mailx\mailxstore
3
3
 
@@ -282,6 +282,15 @@ function appendMessages(body, accountId, items) {
282
282
  else {
283
283
  from.textContent = msg.from.name || msg.from.address;
284
284
  }
285
+ // Account tag for unified inbox
286
+ if (!accountId && msgAccountId) {
287
+ const tag = document.createElement("span");
288
+ tag.className = "ml-account-tag";
289
+ const label = msgAccountId;
290
+ tag.textContent = label.charAt(0).toUpperCase();
291
+ tag.title = label;
292
+ from.prepend(tag);
293
+ }
285
294
  const subject = document.createElement("span");
286
295
  subject.className = "ml-subject";
287
296
  subject.innerHTML = escapeHtml(msg.subject);
@@ -370,6 +370,21 @@ button.tb-menu-item { background: none; border: none; color: inherit; width: 100
370
370
  .ml-row.flagged { background: color-mix(in oklch, oklch(0.75 0.15 60) 8%, transparent); }
371
371
 
372
372
  .ml-from { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
373
+ .ml-account-tag {
374
+ display: inline-block;
375
+ width: 1.1em; height: 1.1em;
376
+ line-height: 1.1em;
377
+ text-align: center;
378
+ font-size: 0.7rem;
379
+ font-weight: bold;
380
+ border-radius: 2px;
381
+ margin-right: 4px;
382
+ color: #fff;
383
+ background: var(--color-accent);
384
+ vertical-align: baseline;
385
+ }
386
+ .ml-account-tag[title="gmail"] { background: #d93025; }
387
+ .ml-account-tag[title="bobma"] { background: #1a73e8; }
373
388
  .ml-subject {
374
389
  overflow: hidden;
375
390
  text-overflow: ellipsis;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx",
3
- "version": "1.0.125",
3
+ "version": "1.0.127",
4
4
  "description": "Local-first email client with IMAP sync and standalone native app",
5
5
  "type": "module",
6
6
  "main": "bin/mailx.js",
@@ -20,7 +20,7 @@
20
20
  "postinstall": "node launcher/builder/postinstall.js"
21
21
  },
22
22
  "dependencies": {
23
- "@bobfrankston/iflow": "^1.0.49",
23
+ "@bobfrankston/iflow": "^1.0.50",
24
24
  "@bobfrankston/miscinfo": "^1.0.7",
25
25
  "@bobfrankston/oauthsupport": "^1.0.20",
26
26
  "@bobfrankston/rust-builder": "^0.1.3",
@@ -323,6 +323,34 @@ export class ImapManager extends EventEmitter {
323
323
  const fetched = await client.fetchMessagesSinceUid(folder.path, highestUid, { source: false });
324
324
  // Filter out the last known message (IMAP * always returns at least one)
325
325
  messages = fetched.filter(m => m.uid > highestUid);
326
+ // Gap detection: compare DB count vs IMAP count to catch interrupted syncs
327
+ const dbCount = this.db.getMessageCount(accountId, folderId);
328
+ try {
329
+ const imapCount = await client.getMessagesCount(folder.path);
330
+ const gap = imapCount - dbCount - messages.length;
331
+ if (gap > 0) {
332
+ console.log(` ${folder.path}: gap detected — IMAP has ${imapCount}, DB has ${dbCount}, ${messages.length} new → ${gap} missing`);
333
+ // Full UID reconciliation: get all IMAP UIDs, find missing ones
334
+ const existingUids = new Set(this.db.getUidsForFolder(accountId, folderId));
335
+ const newUids = new Set(messages.map(m => m.uid));
336
+ const allImapUids = await client.getUids(folder.path);
337
+ const missingUids = allImapUids.filter(uid => !existingUids.has(uid) && !newUids.has(uid));
338
+ if (missingUids.length > 0) {
339
+ console.log(` ${folder.path}: reconciling ${missingUids.length} missing messages`);
340
+ // Fetch in chunks to avoid huge commands
341
+ const chunkSize = 500;
342
+ for (let i = 0; i < missingUids.length; i += chunkSize) {
343
+ const chunk = missingUids.slice(i, i + chunkSize);
344
+ const range = chunk.join(",");
345
+ const recovered = await client.fetchMessages(folder.path, range, { source: false });
346
+ messages.push(...recovered);
347
+ }
348
+ }
349
+ }
350
+ }
351
+ catch (e) {
352
+ console.error(` ${folder.path}: gap detection failed: ${e.message}`);
353
+ }
326
354
  // Backfill: if historyDays extends further back than our oldest message, fetch the gap
327
355
  const oldestDate = this.db.getOldestDate(accountId, folderId);
328
356
  if (oldestDate > 0 && startDate.getTime() < oldestDate) {
@@ -60,6 +60,7 @@ export declare class MailxDB {
60
60
  updateBodyPath(accountId: string, uid: number, bodyPath: string): void;
61
61
  getHighestUid(accountId: string, folderId: number): number;
62
62
  getOldestDate(accountId: string, folderId: number): number;
63
+ getMessageCount(accountId: string, folderId: number): number;
63
64
  /** Get all UIDs for a folder */
64
65
  getUidsForFolder(accountId: string, folderId: number): number[];
65
66
  /** Delete a message by account + UID */
@@ -328,6 +328,10 @@ export class MailxDB {
328
328
  const r = this.db.prepare("SELECT MIN(date) as minDate FROM messages WHERE account_id = ? AND folder_id = ?").get(accountId, folderId);
329
329
  return r?.minDate || 0;
330
330
  }
331
+ getMessageCount(accountId, folderId) {
332
+ const r = this.db.prepare("SELECT count(*) as cnt FROM messages WHERE account_id = ? AND folder_id = ?").get(accountId, folderId);
333
+ return r?.cnt || 0;
334
+ }
331
335
  /** Get all UIDs for a folder */
332
336
  getUidsForFolder(accountId, folderId) {
333
337
  const rows = this.db.prepare("SELECT uid FROM messages WHERE account_id = ? AND folder_id = ?").all(accountId, folderId);