@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
|
@@ -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.
|
|
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.
|
|
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);
|