@bobfrankston/mailx 1.0.253 → 1.0.260
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/bin/mailx.js +112 -0
- package/client/.msger-window.json +1 -1
- package/client/components/message-viewer.js +82 -7
- package/package.json +9 -8
- package/packages/mailx-imap/index.d.ts +6 -0
- package/packages/mailx-imap/index.js +244 -57
- package/packages/mailx-imap/package.json +2 -1
- package/packages/mailx-imap/providers/gmail-api.d.ts +5 -29
- package/packages/mailx-imap/providers/gmail-api.js +5 -286
- package/packages/mailx-imap/providers/types.d.ts +6 -59
- package/packages/mailx-imap/providers/types.js +5 -2
- package/packages/mailx-service/index.d.ts +0 -4
- package/packages/mailx-service/index.js +18 -62
- package/packages/mailx-store-web/android-bootstrap.js +37 -22
- package/packages/mailx-store-web/db.js +8 -7
- package/packages/mailx-store-web/gmail-api-web.d.ts +7 -33
- package/packages/mailx-store-web/gmail-api-web.js +7 -258
- package/packages/mailx-store-web/imap-web-provider.d.ts +1 -1
- package/packages/mailx-store-web/imap-web-provider.js +2 -2
- package/packages/mailx-store-web/main-thread-host.d.ts +15 -0
- package/packages/mailx-store-web/main-thread-host.js +287 -0
- package/packages/mailx-store-web/package.json +2 -1
- package/packages/mailx-store-web/provider-types.d.ts +4 -47
- package/packages/mailx-store-web/provider-types.js +3 -3
- package/packages/mailx-store-web/sync-manager.d.ts +61 -0
- package/packages/mailx-store-web/sync-manager.js +422 -0
- package/packages/mailx-store-web/web-service.d.ts +0 -4
- package/packages/mailx-store-web/web-service.js +1 -59
- package/packages/mailx-store-web/worker-entry.d.ts +8 -0
- package/packages/mailx-store-web/worker-entry.js +187 -0
- package/packages/mailx-store-web/worker-tcp-transport.d.ts +28 -0
- package/packages/mailx-store-web/worker-tcp-transport.js +98 -0
- package/packages/mailx-types/index.d.ts +14 -0
- package/packages/mailx-types/index.js +96 -1
|
@@ -1259,11 +1259,15 @@ export class ImapManager extends EventEmitter {
|
|
|
1259
1259
|
// which gives instant push — the STATUS poll is just a fallback
|
|
1260
1260
|
// in case IDLE silently dropped.
|
|
1261
1261
|
const isGmail = this.isGmailAccount(accountId);
|
|
1262
|
-
//
|
|
1263
|
-
//
|
|
1264
|
-
//
|
|
1265
|
-
//
|
|
1266
|
-
|
|
1262
|
+
// IMAP accounts: IDLE gives instant push; STATUS poll is just a
|
|
1263
|
+
// safety net for silent IDLE drops — keep it infrequent.
|
|
1264
|
+
// Gmail accounts: no IDLE (Gmail API doesn't expose it), so the
|
|
1265
|
+
// quick poll IS the primary path to new-mail latency. Drop to 30s
|
|
1266
|
+
// so Gmail mail appears in ~15s average. Gmail quota budget is
|
|
1267
|
+
// huge (250 units/sec per user, 1.2B/day) — 120 polls/hour × 5
|
|
1268
|
+
// units ≈ 600/hour, trivial. Dovecot accounts stay at 5min to
|
|
1269
|
+
// respect connection limits (each poll = fresh connection).
|
|
1270
|
+
const interval = isGmail ? 30000 : 300000; // Gmail: 30s; IMAP: 5min
|
|
1267
1271
|
const timer = setInterval(() => {
|
|
1268
1272
|
this.quickInboxCheckAccount(accountId).catch(() => { });
|
|
1269
1273
|
}, interval);
|
|
@@ -1422,8 +1426,18 @@ export class ImapManager extends EventEmitter {
|
|
|
1422
1426
|
// fetchOne returned null — message doesn't exist on the server anymore
|
|
1423
1427
|
throw makeNotFoundError(accountId, folderId, uid);
|
|
1424
1428
|
}
|
|
1425
|
-
if (!msg.source)
|
|
1429
|
+
if (!msg.source) {
|
|
1430
|
+
// Gmail returned a message object but no raw bytes. Seen when:
|
|
1431
|
+
// (a) the message exists but is larger than the format=raw cap (~10MB),
|
|
1432
|
+
// (b) UID→Gmail-ID resolution picked a collision and the target
|
|
1433
|
+
// exists only as a stub, or (c) the listMessageIds top-1000
|
|
1434
|
+
// didn't include our UID and fetchOne returned null above —
|
|
1435
|
+
// wait, that would hit the !msg branch. So (a)/(b) remain.
|
|
1436
|
+
// Log enough to distinguish; surface the reason up via a non-null
|
|
1437
|
+
// return so the UI stops showing a generic "fetch returned nothing".
|
|
1438
|
+
console.error(` [api] Body fetch empty source (${accountId}/${uid}): Gmail returned no raw body — likely too-large-for-format-raw or UID hash collision`);
|
|
1426
1439
|
return null;
|
|
1440
|
+
}
|
|
1427
1441
|
const raw = Buffer.from(msg.source, "utf-8");
|
|
1428
1442
|
const bodyPath = await this.bodyStore.putMessage(accountId, folderId, uid, raw);
|
|
1429
1443
|
this.db.updateBodyPath(accountId, uid, bodyPath);
|
|
@@ -1443,77 +1457,223 @@ export class ImapManager extends EventEmitter {
|
|
|
1443
1457
|
* stale row locally and keep going. Only unrelated errors (network,
|
|
1444
1458
|
* auth, rate limits) count against the error budget, and the budget is
|
|
1445
1459
|
* generous so a few transient failures don't kill the whole run. */
|
|
1460
|
+
/** Guard against concurrent prefetchBodies for the same account — mirror of
|
|
1461
|
+
* `sendingAccounts`. Without this, every periodic-sync tick spawns a new
|
|
1462
|
+
* prefetch session alongside any still in flight, blowing through Gmail's
|
|
1463
|
+
* per-minute quota and racing on disk writes. One prefetch per account. */
|
|
1464
|
+
prefetchingAccounts = new Set();
|
|
1446
1465
|
async prefetchBodies(accountId) {
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1466
|
+
if (this.prefetchingAccounts.has(accountId))
|
|
1467
|
+
return;
|
|
1468
|
+
this.prefetchingAccounts.add(accountId);
|
|
1469
|
+
try {
|
|
1470
|
+
await this._prefetchBodies(accountId);
|
|
1471
|
+
}
|
|
1472
|
+
finally {
|
|
1473
|
+
this.prefetchingAccounts.delete(accountId);
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
async _prefetchBodies(accountId) {
|
|
1477
|
+
const counters = { totalFetched: 0, deleted: 0, errors: 0 };
|
|
1451
1478
|
const ERROR_BUDGET = 20;
|
|
1452
|
-
// Pace body fetches to avoid slamming Gmail's rate limit. Without a
|
|
1453
|
-
// delay, 500+ body-fetch API calls fire in a burst, every one hits 429,
|
|
1454
|
-
// and the error budget drains before any bodies land.
|
|
1455
|
-
const FETCH_DELAY_MS = this.isGmailAccount(accountId) ? 1000 : 200;
|
|
1456
1479
|
const RATE_LIMIT_PAUSE_MS = 30000;
|
|
1480
|
+
const BATCH_SIZE = 100;
|
|
1481
|
+
const isGmail = this.isGmailAccount(accountId);
|
|
1482
|
+
// Gmail still uses per-message fetch (HTTP /batch is a separate TODO in this file's
|
|
1483
|
+
// governing unit). IMAP uses the batched `fetchBodiesBatch` path via iflow-direct —
|
|
1484
|
+
// one SELECT + one UID FETCH per folder per tick instead of N round trips.
|
|
1485
|
+
let announced = false;
|
|
1457
1486
|
while (true) {
|
|
1458
|
-
const missing = this.db.getMessagesWithoutBody(accountId,
|
|
1487
|
+
const missing = this.db.getMessagesWithoutBody(accountId, BATCH_SIZE);
|
|
1459
1488
|
if (missing.length === 0)
|
|
1460
1489
|
break;
|
|
1461
|
-
if (
|
|
1490
|
+
if (!announced) {
|
|
1462
1491
|
console.log(` [prefetch] ${accountId}: ${missing.length}+ bodies to fetch`);
|
|
1492
|
+
announced = true;
|
|
1493
|
+
}
|
|
1463
1494
|
let madeProgress = false;
|
|
1464
|
-
|
|
1465
|
-
//
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1495
|
+
if (isGmail) {
|
|
1496
|
+
// Gmail batch path: group by label (what mailx calls "folder"),
|
|
1497
|
+
// list once per label, bounded-concurrency fetch. Far fewer
|
|
1498
|
+
// HTTP round trips than the old one-listMessageIds-per-body path.
|
|
1499
|
+
// Note on the model: Gmail has labels, not folders. A message in
|
|
1500
|
+
// multiple labels gets fetched twice under current grouping. A
|
|
1501
|
+
// deeper label-native redesign is tracked as a separate TODO.
|
|
1502
|
+
const byFolder = new Map();
|
|
1503
|
+
for (const m of missing) {
|
|
1504
|
+
let arr = byFolder.get(m.folderId);
|
|
1505
|
+
if (!arr) {
|
|
1506
|
+
arr = [];
|
|
1507
|
+
byFolder.set(m.folderId, arr);
|
|
1508
|
+
}
|
|
1509
|
+
arr.push(m.uid);
|
|
1510
|
+
}
|
|
1511
|
+
const folders = this.db.getFolders(accountId);
|
|
1512
|
+
const api = this.getGmailProvider(accountId);
|
|
1513
|
+
try {
|
|
1514
|
+
for (const [folderId, uidsInFolder] of byFolder) {
|
|
1515
|
+
const folder = folders.find(f => f.id === folderId);
|
|
1516
|
+
if (!folder)
|
|
1517
|
+
continue;
|
|
1518
|
+
const received = new Set();
|
|
1519
|
+
const pending = [];
|
|
1520
|
+
let batchSucceeded = false;
|
|
1521
|
+
try {
|
|
1522
|
+
await api.fetchBodiesBatch(folder.path, uidsInFolder, (uid, source) => {
|
|
1523
|
+
received.add(uid);
|
|
1524
|
+
pending.push((async () => {
|
|
1525
|
+
try {
|
|
1526
|
+
const raw = Buffer.from(source, "utf-8");
|
|
1527
|
+
const bodyPath = await this.bodyStore.putMessage(accountId, folderId, uid, raw);
|
|
1528
|
+
this.db.updateBodyPath(accountId, uid, bodyPath);
|
|
1529
|
+
counters.totalFetched++;
|
|
1530
|
+
madeProgress = true;
|
|
1531
|
+
}
|
|
1532
|
+
catch (e) {
|
|
1533
|
+
console.error(` [prefetch] ${accountId}/${uid}: store write failed: ${e.message}`);
|
|
1534
|
+
}
|
|
1535
|
+
})());
|
|
1536
|
+
});
|
|
1537
|
+
batchSucceeded = true;
|
|
1538
|
+
}
|
|
1539
|
+
catch (e) {
|
|
1540
|
+
const isRate = /429|rate|too many/i.test(String(e?.message || ""));
|
|
1541
|
+
if (isRate) {
|
|
1542
|
+
console.log(` [prefetch] ${accountId}: rate-limited — pausing ${RATE_LIMIT_PAUSE_MS / 1000}s`);
|
|
1543
|
+
await new Promise(r => setTimeout(r, RATE_LIMIT_PAUSE_MS));
|
|
1544
|
+
}
|
|
1545
|
+
else {
|
|
1546
|
+
console.error(` [prefetch] ${accountId} folder ${folder.path}: Gmail batch fetch failed: ${e.message}`);
|
|
1547
|
+
counters.errors++;
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
await Promise.all(pending);
|
|
1551
|
+
// CRITICAL: only prune as "server-deleted" when the batch
|
|
1552
|
+
// actually completed. If the batch threw (403, 429, network
|
|
1553
|
+
// error, etc.) NOTHING was received, and treating every
|
|
1554
|
+
// requested UID as deleted silently wipes 100 messages per
|
|
1555
|
+
// batch. That's a data-loss bug. Earlier version did this
|
|
1556
|
+
// and pruned 296 messages on a 403 auth error.
|
|
1557
|
+
if (batchSucceeded) {
|
|
1558
|
+
for (const uid of uidsInFolder) {
|
|
1559
|
+
if (received.has(uid))
|
|
1560
|
+
continue;
|
|
1561
|
+
try {
|
|
1562
|
+
this.db.deleteMessage(accountId, uid);
|
|
1563
|
+
this.bodyStore.deleteMessage(accountId, folderId, uid).catch(() => { });
|
|
1564
|
+
counters.deleted++;
|
|
1565
|
+
madeProgress = true;
|
|
1566
|
+
}
|
|
1567
|
+
catch { /* ignore */ }
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
if (counters.errors >= ERROR_BUDGET)
|
|
1571
|
+
break;
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
finally {
|
|
1575
|
+
try {
|
|
1576
|
+
await api.close();
|
|
1577
|
+
}
|
|
1578
|
+
catch { /* ignore */ }
|
|
1579
|
+
}
|
|
1580
|
+
if (counters.errors >= ERROR_BUDGET) {
|
|
1581
|
+
console.error(` [prefetch] ${accountId}: stopping after ${counters.errors} errors (${counters.totalFetched} cached, ${counters.deleted} pruned)`);
|
|
1582
|
+
return;
|
|
1470
1583
|
}
|
|
1471
|
-
|
|
1472
|
-
|
|
1584
|
+
}
|
|
1585
|
+
else {
|
|
1586
|
+
// IMAP batch path: group by folder, one UID FETCH per folder.
|
|
1587
|
+
const byFolder = new Map();
|
|
1588
|
+
for (const m of missing) {
|
|
1589
|
+
let arr = byFolder.get(m.folderId);
|
|
1590
|
+
if (!arr) {
|
|
1591
|
+
arr = [];
|
|
1592
|
+
byFolder.set(m.folderId, arr);
|
|
1593
|
+
}
|
|
1594
|
+
arr.push(m.uid);
|
|
1473
1595
|
}
|
|
1596
|
+
const folders = this.db.getFolders(accountId);
|
|
1597
|
+
let client = null;
|
|
1474
1598
|
try {
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1599
|
+
client = await this.createClientWithLimit(accountId);
|
|
1600
|
+
for (const [folderId, uids] of byFolder) {
|
|
1601
|
+
const folder = folders.find(f => f.id === folderId);
|
|
1602
|
+
if (!folder)
|
|
1603
|
+
continue;
|
|
1604
|
+
const received = new Set();
|
|
1605
|
+
// onBody fires synchronously as each message streams in from the server.
|
|
1606
|
+
// Disk/DB writes are kicked off fire-and-forget; we await them after the
|
|
1607
|
+
// batch command finishes. This keeps streaming throughput high while
|
|
1608
|
+
// still giving us a single await point for progress accounting.
|
|
1609
|
+
const pending = [];
|
|
1610
|
+
let batchSucceeded = false;
|
|
1611
|
+
try {
|
|
1612
|
+
await client.fetchBodiesBatch(folder.path, uids, (uid, source) => {
|
|
1613
|
+
received.add(uid);
|
|
1614
|
+
pending.push((async () => {
|
|
1615
|
+
try {
|
|
1616
|
+
const raw = Buffer.from(source, "utf-8");
|
|
1617
|
+
const bodyPath = await this.bodyStore.putMessage(accountId, folderId, uid, raw);
|
|
1618
|
+
this.db.updateBodyPath(accountId, uid, bodyPath);
|
|
1619
|
+
counters.totalFetched++;
|
|
1620
|
+
madeProgress = true;
|
|
1621
|
+
}
|
|
1622
|
+
catch (e) {
|
|
1623
|
+
// EBUSY / disk error — non-fatal per message
|
|
1624
|
+
console.error(` [prefetch] ${accountId}/${uid}: store write failed: ${e.message}`);
|
|
1625
|
+
}
|
|
1626
|
+
})());
|
|
1627
|
+
});
|
|
1628
|
+
batchSucceeded = true;
|
|
1629
|
+
}
|
|
1630
|
+
catch (e) {
|
|
1631
|
+
console.error(` [prefetch] ${accountId} folder ${folder.path}: batch fetch failed: ${e.message}`);
|
|
1632
|
+
counters.errors++;
|
|
1633
|
+
if (counters.errors >= ERROR_BUDGET)
|
|
1634
|
+
break;
|
|
1635
|
+
}
|
|
1636
|
+
await Promise.all(pending);
|
|
1637
|
+
// CRITICAL: only prune when the batch actually completed.
|
|
1638
|
+
// A thrown batch means NOTHING was received and we must
|
|
1639
|
+
// not treat absence-from-received as server-deletion.
|
|
1640
|
+
if (batchSucceeded)
|
|
1641
|
+
for (const uid of uids) {
|
|
1642
|
+
if (received.has(uid))
|
|
1643
|
+
continue;
|
|
1644
|
+
try {
|
|
1645
|
+
this.db.deleteMessage(accountId, uid);
|
|
1646
|
+
this.bodyStore.deleteMessage(accountId, folderId, uid).catch(() => { });
|
|
1647
|
+
counters.deleted++;
|
|
1648
|
+
madeProgress = true;
|
|
1649
|
+
}
|
|
1650
|
+
catch { /* ignore */ }
|
|
1651
|
+
}
|
|
1479
1652
|
}
|
|
1480
1653
|
}
|
|
1481
|
-
|
|
1482
|
-
if (
|
|
1483
|
-
// Message deleted on the server — drop the stale row so
|
|
1484
|
-
// we stop re-asking. This also moves the loop forward
|
|
1485
|
-
// (next getMessagesWithoutBody call won't return it).
|
|
1654
|
+
finally {
|
|
1655
|
+
if (client) {
|
|
1486
1656
|
try {
|
|
1487
|
-
|
|
1488
|
-
this.bodyStore.deleteMessage(accountId, msg.folderId, msg.uid).catch(() => { });
|
|
1489
|
-
deleted++;
|
|
1490
|
-
madeProgress = true;
|
|
1657
|
+
await client.logout();
|
|
1491
1658
|
}
|
|
1492
1659
|
catch { /* ignore */ }
|
|
1493
|
-
continue;
|
|
1494
|
-
}
|
|
1495
|
-
// If the error is a rate limit (429), don't count against
|
|
1496
|
-
// the budget — just slow down. The API will accept requests
|
|
1497
|
-
// again after a brief pause.
|
|
1498
|
-
if (/429|rate|too many/i.test(String(e?.message || ""))) {
|
|
1499
|
-
rateLimited = true;
|
|
1500
|
-
}
|
|
1501
|
-
else {
|
|
1502
|
-
errors++;
|
|
1503
|
-
}
|
|
1504
|
-
if (errors >= ERROR_BUDGET) {
|
|
1505
|
-
console.error(` [prefetch] ${accountId}: stopping after ${errors} errors (${totalFetched} cached, ${deleted} pruned)`);
|
|
1506
|
-
return;
|
|
1507
1660
|
}
|
|
1508
1661
|
}
|
|
1662
|
+
if (counters.errors >= ERROR_BUDGET) {
|
|
1663
|
+
console.error(` [prefetch] ${accountId}: stopping after ${counters.errors} errors (${counters.totalFetched} cached, ${counters.deleted} pruned)`);
|
|
1664
|
+
return;
|
|
1665
|
+
}
|
|
1509
1666
|
}
|
|
1510
|
-
// Safety:
|
|
1511
|
-
// we'd loop forever on rows that keep failing without isNotFound.
|
|
1667
|
+
// Safety: zero progress this tick → bail rather than loop forever.
|
|
1512
1668
|
if (!madeProgress)
|
|
1513
1669
|
break;
|
|
1670
|
+
// Emit so the UI refreshes the open-circle → filled-teal indicator
|
|
1671
|
+
// without waiting for the next sync cycle.
|
|
1672
|
+
this.emit("folderCountsChanged", accountId, {});
|
|
1514
1673
|
}
|
|
1515
|
-
if (totalFetched > 0 || deleted > 0) {
|
|
1516
|
-
console.log(` [prefetch] ${accountId}: ${totalFetched} bodies cached, ${deleted} stale rows pruned (done)`);
|
|
1674
|
+
if (counters.totalFetched > 0 || counters.deleted > 0) {
|
|
1675
|
+
console.log(` [prefetch] ${accountId}: ${counters.totalFetched} bodies cached, ${counters.deleted} stale rows pruned (done)`);
|
|
1676
|
+
this.emit("folderCountsChanged", accountId, {});
|
|
1517
1677
|
}
|
|
1518
1678
|
}
|
|
1519
1679
|
/** Get the body store for direct access */
|
|
@@ -1781,8 +1941,35 @@ export class ImapManager extends EventEmitter {
|
|
|
1781
1941
|
console.error(` [drafts] searchByHeader for ${draftId} failed: ${e.message}`);
|
|
1782
1942
|
}
|
|
1783
1943
|
}
|
|
1784
|
-
// Append new draft
|
|
1785
|
-
|
|
1944
|
+
// Append new draft. If the server returns [TRYCREATE] (RFC 3501 §7.1),
|
|
1945
|
+
// the folder doesn't exist on the server even though mailx's DB has
|
|
1946
|
+
// it — happens when the folder was never created, or when the local
|
|
1947
|
+
// special-folder detection latched onto a path that doesn't match
|
|
1948
|
+
// the server's actual name. Create it then retry. Logs the path so
|
|
1949
|
+
// we can diagnose a mis-detected Drafts folder.
|
|
1950
|
+
let result;
|
|
1951
|
+
try {
|
|
1952
|
+
result = await client.appendMessage(drafts.path, rawMessage, ["\\Draft", "\\Seen"]);
|
|
1953
|
+
}
|
|
1954
|
+
catch (e) {
|
|
1955
|
+
const msg = String(e?.message || e);
|
|
1956
|
+
if (/TRYCREATE/i.test(msg)) {
|
|
1957
|
+
console.log(` [drafts] APPEND got TRYCREATE for "${drafts.path}" — creating folder and retrying`);
|
|
1958
|
+
try {
|
|
1959
|
+
await client.createmailbox(drafts.path);
|
|
1960
|
+
}
|
|
1961
|
+
catch (ce) {
|
|
1962
|
+
// "already exists" is benign; others we surface
|
|
1963
|
+
if (!/already exists/i.test(String(ce?.message || ""))) {
|
|
1964
|
+
console.error(` [drafts] Folder create failed for "${drafts.path}": ${ce.message}`);
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
result = await client.appendMessage(drafts.path, rawMessage, ["\\Draft", "\\Seen"]);
|
|
1968
|
+
}
|
|
1969
|
+
else {
|
|
1970
|
+
throw e;
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1786
1973
|
// APPENDUID returns the UID directly; imapflow returns { destination, uid }
|
|
1787
1974
|
const uid = typeof result === "number" ? result : result?.uid || null;
|
|
1788
1975
|
return uid;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/mailx-imap",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"@bobfrankston/iflow-direct": "file:../../../MailApps/iflow-direct",
|
|
16
16
|
"@bobfrankston/tcp-transport": "file:../../../MailApps/tcp-transport",
|
|
17
17
|
"@bobfrankston/smtp-direct": "file:../../../MailApps/smtp-direct",
|
|
18
|
+
"@bobfrankston/mailx-sync": "file:../../../MailApps/mailx-sync",
|
|
18
19
|
"@bobfrankston/oauthsupport": "file:../../../../projects/oauth/oauthsupport"
|
|
19
20
|
},
|
|
20
21
|
"repository": {
|
|
@@ -1,32 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Back-compat re-export. The canonical Gmail provider lives in
|
|
3
|
+
* @bobfrankston/mailx-sync. mailx-imap re-exports it under its old name so
|
|
4
|
+
* call sites here keep compiling. Both desktop (this package) and Android
|
|
5
|
+
* (mailx-store-web) consume the same single implementation now.
|
|
4
6
|
*/
|
|
5
|
-
|
|
6
|
-
export declare class GmailApiProvider implements MailProvider {
|
|
7
|
-
private tokenProvider;
|
|
8
|
-
constructor(tokenProvider: () => Promise<string>);
|
|
9
|
-
private fetch;
|
|
10
|
-
listFolders(): Promise<ProviderFolder[]>;
|
|
11
|
-
/** List message IDs matching a query, handling pagination.
|
|
12
|
-
* IMPORTANT: on any error we throw — do NOT return a partial list, because
|
|
13
|
-
* callers use this for sync reconciliation and a short list would delete
|
|
14
|
-
* real messages from the local DB. Returning [] silently caused the
|
|
15
|
-
* "INBOX empty in mailx" bug when a rate-limit hit mid-pagination. */
|
|
16
|
-
private listMessageIds;
|
|
17
|
-
/** Batch-fetch message metadata or full content */
|
|
18
|
-
private batchFetch;
|
|
19
|
-
/** Parse a Gmail API message response into ProviderMessage */
|
|
20
|
-
private parseMessage;
|
|
21
|
-
fetchSince(folder: string, sinceUid: number, options?: FetchOptions): Promise<ProviderMessage[]>;
|
|
22
|
-
fetchByDate(folder: string, since: Date, before: Date, options?: FetchOptions, onChunk?: (msgs: ProviderMessage[]) => void): Promise<ProviderMessage[]>;
|
|
23
|
-
fetchByUids(folder: string, uids: number[], options?: FetchOptions): Promise<ProviderMessage[]>;
|
|
24
|
-
fetchOne(folder: string, uid: number, options?: FetchOptions): Promise<ProviderMessage | null>;
|
|
25
|
-
getUids(folder: string): Promise<number[]>;
|
|
26
|
-
close(): Promise<void>;
|
|
27
|
-
/** Map folder path to Gmail label query term */
|
|
28
|
-
private folderToLabel;
|
|
29
|
-
/** Format date for Gmail query (YYYY/MM/DD) */
|
|
30
|
-
private formatDate;
|
|
31
|
-
}
|
|
7
|
+
export { GmailApiProvider } from "@bobfrankston/mailx-sync";
|
|
32
8
|
//# sourceMappingURL=gmail-api.d.ts.map
|