@bobfrankston/mailx-imap 0.1.25 → 0.1.27
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/index.d.ts +21 -1
- package/index.js +111 -95
- package/package.json +5 -5
package/index.d.ts
CHANGED
|
@@ -247,13 +247,33 @@ export declare class ImapManager extends EventEmitter {
|
|
|
247
247
|
stopPeriodicSync(): void;
|
|
248
248
|
/** Check if an account is OAuth (Gmail/Outlook — generous connection limits) */
|
|
249
249
|
isOAuthAccount(accountId: string): boolean;
|
|
250
|
-
/** Start IMAP IDLE
|
|
250
|
+
/** Start an IMAP IDLE watcher per account on INBOX. Other folders are
|
|
251
|
+
* not special-cased here — sync runs uniformly on every folder via the
|
|
252
|
+
* periodic full-sync timer (STATUS-before-SELECT optimization is the
|
|
253
|
+
* right way to make that cheap, not piling per-folder IDLE sockets). */
|
|
251
254
|
startWatching(): Promise<void>;
|
|
252
255
|
/** Stop all IDLE watchers */
|
|
253
256
|
stopWatching(): Promise<void>;
|
|
254
257
|
/** Unlink the on-disk body file for a message by reading its `body_path`
|
|
255
258
|
* from the DB. Safe to call either before or after `db.deleteMessage`
|
|
256
259
|
* — read body_path first, store it, then unlink whenever. */
|
|
260
|
+
/** Per-(account, folder, uid) deferred-delete timer. Reconcile populates
|
|
261
|
+
* this; the timer fires 60s later and re-checks whether the row at that
|
|
262
|
+
* exact (folder, uid) still exists. If yes → really delete (server
|
|
263
|
+
* expunged it). If no → skip (move-detect rebound it elsewhere; the
|
|
264
|
+
* row already moved to a new folder/uid in the same DB row, so the
|
|
265
|
+
* original key no longer matches anything). Net effect: server-side
|
|
266
|
+
* moves preserve UUID + body cache + flags. */
|
|
267
|
+
private deferredDeletes;
|
|
268
|
+
/** Grace window for reconcile-delete. Set to 30 minutes because the
|
|
269
|
+
* full-folder sync loop walks ~96 folders sequentially and each one
|
|
270
|
+
* takes 1-30 seconds; a server-side move from INBOX to a folder that
|
|
271
|
+
* syncs late (alphabetically far from INBOX) can take 10+ minutes
|
|
272
|
+
* before move-detect fires in the destination. 60s was way too short
|
|
273
|
+
* in production — the grace expired and rows were committed-deleted
|
|
274
|
+
* before _Spam / Sent / archive folders had a chance to rebind. */
|
|
275
|
+
private static readonly RECONCILE_DELETE_GRACE_MS;
|
|
276
|
+
private scheduleDeferredReconcileDelete;
|
|
257
277
|
private unlinkBodyFile;
|
|
258
278
|
/** Fetch a single message body on demand, caching in the store.
|
|
259
279
|
*
|
package/index.js
CHANGED
|
@@ -199,6 +199,19 @@ export class ImapManager extends EventEmitter {
|
|
|
199
199
|
this.transportFactory = transportFactory;
|
|
200
200
|
const storePath = getStorePath();
|
|
201
201
|
this.bodyStore = new FileMessageStore(storePath);
|
|
202
|
+
// Cancel pending deferred-delete when move-detect rebinds a row.
|
|
203
|
+
// Without this, the source folder's reconcile-delete grace timer
|
|
204
|
+
// would fire 30 minutes after detection for a row that's already
|
|
205
|
+
// moved elsewhere — the user would see the message vanish.
|
|
206
|
+
this.db.setOnMoveDetected((info) => {
|
|
207
|
+
const key = `${info.accountId}:${info.fromFolderId}:${info.fromUid}`;
|
|
208
|
+
const t = this.deferredDeletes.get(key);
|
|
209
|
+
if (t) {
|
|
210
|
+
clearTimeout(t);
|
|
211
|
+
this.deferredDeletes.delete(key);
|
|
212
|
+
console.log(` [reconcile-cancel] ${info.accountId} ${info.fromFolderId}/${info.fromUid}: deferred delete cancelled (move-detect rebound to ${info.toFolderId}/${info.toUid})`);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
202
215
|
}
|
|
203
216
|
/** Get OAuth access token for an account (for SMTP auth) */
|
|
204
217
|
async getOAuthToken(accountId) {
|
|
@@ -813,8 +826,15 @@ export class ImapManager extends EventEmitter {
|
|
|
813
826
|
const hex = Buffer.from(msg.subject, "utf-8").subarray(0, 40).toString("hex");
|
|
814
827
|
console.log(` [encoding] subject: "${msg.subject.substring(0, 60)}" hex: ${hex}`);
|
|
815
828
|
}
|
|
816
|
-
|
|
817
|
-
|
|
829
|
+
// CRITICAL: do NOT skip on `uid <= highestUid`. That check
|
|
830
|
+
// was a major bug — it silently dropped every gap-filled
|
|
831
|
+
// message (gap-fill specifically recovers UIDs in the
|
|
832
|
+
// already-scanned range, all of which are <= highestUid).
|
|
833
|
+
// upsertMessage's UNIQUE(account, folder, uid) constraint
|
|
834
|
+
// already deduplicates: if mailx truly has the row, the
|
|
835
|
+
// upsert turns into an UPDATE (cheap); if it doesn't, the
|
|
836
|
+
// insert proceeds. Trust the constraint — don't second-
|
|
837
|
+
// guess it on a stale highestUid snapshot.
|
|
818
838
|
// Tombstone check: if the user locally deleted this Message-ID,
|
|
819
839
|
// don't re-import it. Server-side EXPUNGE may lag, or reconcile
|
|
820
840
|
// may find the message in an old list snapshot. Without this,
|
|
@@ -878,6 +898,11 @@ export class ImapManager extends EventEmitter {
|
|
|
878
898
|
// monotonically increasing within a UIDVALIDITY (RFC 3501); a
|
|
879
899
|
// high-water mark is the right anchor for incremental fetch.
|
|
880
900
|
const highestUid = this.db.getHighestUid(accountId, folderId);
|
|
901
|
+
// STATUS-before-SELECT was here. Removed — added a round-trip per
|
|
902
|
+
// folder with no measured benefit on Bob's link, and the speculative
|
|
903
|
+
// "skip SELECT when nothing changed" optimization didn't actually
|
|
904
|
+
// skip anything in practice (server count vs local count nearly
|
|
905
|
+
// always differs slightly because of in-flight deletes/moves).
|
|
881
906
|
console.log(` [sync] ${accountId}/${folder.path}: highestUid=${highestUid}, fetching...`);
|
|
882
907
|
let messages;
|
|
883
908
|
const firstSync = highestUid === 0;
|
|
@@ -997,21 +1022,14 @@ export class ImapManager extends EventEmitter {
|
|
|
997
1022
|
try {
|
|
998
1023
|
for (let i = batchStart; i < batchEnd; i++) {
|
|
999
1024
|
const msg = messages[i];
|
|
1000
|
-
//
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
if (msg.answered)
|
|
1009
|
-
flags.push("\\Answered");
|
|
1010
|
-
if (msg.draft)
|
|
1011
|
-
flags.push("\\Draft");
|
|
1012
|
-
this.db.updateMessageFlags(accountId, msg.uid, flags);
|
|
1013
|
-
continue;
|
|
1014
|
-
}
|
|
1025
|
+
// CRITICAL: was `if (msg.uid <= highestUid) { update flags; continue }`.
|
|
1026
|
+
// Same bug as the streamy storeMessages path on line ~861:
|
|
1027
|
+
// it dropped every gap-fill message because gap-fills
|
|
1028
|
+
// recover UIDs in the already-scanned range (all <=
|
|
1029
|
+
// highestUid by definition). Trust upsertMessage's
|
|
1030
|
+
// UNIQUE constraint to dedupe — if mailx truly has the
|
|
1031
|
+
// row, the upsert becomes an UPDATE that refreshes
|
|
1032
|
+
// flags too, all in one path.
|
|
1015
1033
|
// Tombstone check — same reason as the streamy onChunk path
|
|
1016
1034
|
// at storeMessages: a locally-deleted message that the server
|
|
1017
1035
|
// hasn't EXPUNGEd yet would otherwise reappear on next sync.
|
|
@@ -1102,17 +1120,25 @@ export class ImapManager extends EventEmitter {
|
|
|
1102
1120
|
else if (localUids.length > 0 && toDelete.length / localUids.length > 0.5) {
|
|
1103
1121
|
console.log(` [sync] ${accountId}/${folder.path}: reconcile REFUSED — would delete ${toDelete.length}/${localUids.length} (${Math.round(toDelete.length / localUids.length * 100)}%) — probably a sync bug, skipping`);
|
|
1104
1122
|
}
|
|
1105
|
-
else {
|
|
1123
|
+
else if (toDelete.length > 0) {
|
|
1124
|
+
// DEFERRED DELETE — DO NOT delete immediately. Server-side
|
|
1125
|
+
// moves (Sieve filters, IMAP MOVE from another client)
|
|
1126
|
+
// make a UID disappear from the source folder before
|
|
1127
|
+
// showing up in the destination. mailx syncs folders
|
|
1128
|
+
// sequentially; if we delete on first detection, the
|
|
1129
|
+
// dest's `upsertMessage`-side move-detect can't rebind
|
|
1130
|
+
// (the row is gone), and we lose the UUID + body cache,
|
|
1131
|
+
// forcing a re-fetch from server.
|
|
1132
|
+
//
|
|
1133
|
+
// Instead: schedule the delete 60s out. If between now
|
|
1134
|
+
// and then move-detect rebinds the row (folder_id changes
|
|
1135
|
+
// to dest), the (acc, folder, uid) lookup at fire time
|
|
1136
|
+
// won't match — skip. If still at original (truly gone
|
|
1137
|
+
// from server, not just moved), commit the delete.
|
|
1106
1138
|
for (const uid of toDelete) {
|
|
1107
|
-
|
|
1108
|
-
const tag = env ? `msgid=${env.messageId || "?"} subj="${(env.subject || "").slice(0, 60)}"` : "unknown";
|
|
1109
|
-
console.log(` [reconcile-delete] ${accountId}/${folder.path} uid=${uid} ${tag}`);
|
|
1110
|
-
this.unlinkBodyFile(accountId, uid, folderId).catch(() => { });
|
|
1111
|
-
this.db.deleteMessage(accountId, uid);
|
|
1112
|
-
deletedCount++;
|
|
1139
|
+
this.scheduleDeferredReconcileDelete(accountId, folderId, uid, folder.path);
|
|
1113
1140
|
}
|
|
1114
|
-
|
|
1115
|
-
console.log(` removed ${deletedCount} deleted messages`);
|
|
1141
|
+
console.log(` [reconcile-defer] ${accountId}/${folder.path}: scheduled ${toDelete.length} deletes (60s grace for move-detect)`);
|
|
1116
1142
|
}
|
|
1117
1143
|
}
|
|
1118
1144
|
catch (e) {
|
|
@@ -1484,7 +1510,7 @@ export class ImapManager extends EventEmitter {
|
|
|
1484
1510
|
const tag = env ? `msgid=${env.messageId || "?"} subj="${(env.subject || "").slice(0, 60)}"` : "unknown";
|
|
1485
1511
|
console.log(` [reconcile-delete] ${accountId}/${folder.path} uid=${uid} ${tag}`);
|
|
1486
1512
|
this.unlinkBodyFile(accountId, uid, folder.id).catch(() => { });
|
|
1487
|
-
this.db.deleteMessage(accountId, uid);
|
|
1513
|
+
this.db.deleteMessage(accountId, uid, "Gmail-API reconcile: server list missing this UID", `mailx-imap Gmail reconcile (${folder.path})`);
|
|
1488
1514
|
}
|
|
1489
1515
|
if (toDelete.length > 0)
|
|
1490
1516
|
console.log(` [api] ${accountId}/${folder.path}: ${toDelete.length} deleted`);
|
|
@@ -1863,7 +1889,10 @@ export class ImapManager extends EventEmitter {
|
|
|
1863
1889
|
const config = this.configs.get(accountId);
|
|
1864
1890
|
return !!config?.tokenProvider;
|
|
1865
1891
|
}
|
|
1866
|
-
/** Start IMAP IDLE
|
|
1892
|
+
/** Start an IMAP IDLE watcher per account on INBOX. Other folders are
|
|
1893
|
+
* not special-cased here — sync runs uniformly on every folder via the
|
|
1894
|
+
* periodic full-sync timer (STATUS-before-SELECT optimization is the
|
|
1895
|
+
* right way to make that cheap, not piling per-folder IDLE sockets). */
|
|
1867
1896
|
async startWatching() {
|
|
1868
1897
|
for (const [accountId] of this.configs) {
|
|
1869
1898
|
if (this.watchers.has(accountId))
|
|
@@ -1873,70 +1902,16 @@ export class ImapManager extends EventEmitter {
|
|
|
1873
1902
|
// is parked in IDLE, it's unusable for any other command, so
|
|
1874
1903
|
// it can't share the ops queue. Counts against the per-host
|
|
1875
1904
|
// semaphore (one slot for the IDLE socket).
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
// full sync.
|
|
1882
|
-
const stops = [];
|
|
1883
|
-
const clients = [];
|
|
1884
|
-
const watchOne = async (mailboxLabel, path) => {
|
|
1885
|
-
const client = await this.createClient(accountId, "idle");
|
|
1886
|
-
clients.push(client);
|
|
1887
|
-
const stop = await client.watchMailbox(path, (newCount) => {
|
|
1888
|
-
console.log(` [idle] ${accountId} ${path}: ${newCount} new message(s)`);
|
|
1889
|
-
if (mailboxLabel === "inbox") {
|
|
1890
|
-
// Fast path: incremental fetch of NEW UIDs only.
|
|
1891
|
-
// Heavy reconcile runs on the 5-minute STATUS poll.
|
|
1892
|
-
this.syncInboxNewOnly(accountId).catch(e => console.error(` [idle] inbox sync error: ${e.message}`));
|
|
1893
|
-
}
|
|
1894
|
-
else {
|
|
1895
|
-
// Sent / Drafts changed elsewhere. Use the
|
|
1896
|
-
// standard folder sync — picks up the new UID,
|
|
1897
|
-
// rebinds any optimistic local row by Message-ID.
|
|
1898
|
-
const folder = this.findFolder(accountId, mailboxLabel);
|
|
1899
|
-
if (folder) {
|
|
1900
|
-
this.syncFolder(accountId, folder.id).catch(e => console.error(` [idle] ${path} sync error: ${e.message}`));
|
|
1901
|
-
}
|
|
1902
|
-
}
|
|
1903
|
-
});
|
|
1904
|
-
stops.push(stop);
|
|
1905
|
-
};
|
|
1906
|
-
await watchOne("inbox", "INBOX");
|
|
1907
|
-
const sent = this.findFolder(accountId, "sent");
|
|
1908
|
-
if (sent) {
|
|
1909
|
-
try {
|
|
1910
|
-
await watchOne("sent", sent.path);
|
|
1911
|
-
}
|
|
1912
|
-
catch (e) {
|
|
1913
|
-
console.error(` [idle] Failed to watch ${sent.path}: ${e.message}`);
|
|
1914
|
-
}
|
|
1915
|
-
}
|
|
1916
|
-
const drafts = this.findFolder(accountId, "drafts");
|
|
1917
|
-
if (drafts) {
|
|
1918
|
-
try {
|
|
1919
|
-
await watchOne("drafts", drafts.path);
|
|
1920
|
-
}
|
|
1921
|
-
catch (e) {
|
|
1922
|
-
console.error(` [idle] Failed to watch ${drafts.path}: ${e.message}`);
|
|
1923
|
-
}
|
|
1924
|
-
}
|
|
1905
|
+
const watchClient = await this.createClient(accountId, "idle");
|
|
1906
|
+
const stop = await watchClient.watchMailbox("INBOX", (newCount) => {
|
|
1907
|
+
console.log(` [idle] ${accountId}: ${newCount} new message(s)`);
|
|
1908
|
+
this.syncInboxNewOnly(accountId).catch(e => console.error(` [idle] sync error: ${e.message}`));
|
|
1909
|
+
});
|
|
1925
1910
|
this.watchers.set(accountId, async () => {
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
await stop();
|
|
1929
|
-
}
|
|
1930
|
-
catch { /* ignore */ }
|
|
1931
|
-
}
|
|
1932
|
-
for (const c of clients) {
|
|
1933
|
-
try {
|
|
1934
|
-
await c.logout();
|
|
1935
|
-
}
|
|
1936
|
-
catch { /* ignore */ }
|
|
1937
|
-
}
|
|
1911
|
+
await stop();
|
|
1912
|
+
await watchClient.logout();
|
|
1938
1913
|
});
|
|
1939
|
-
console.log(` [idle] Watching INBOX
|
|
1914
|
+
console.log(` [idle] Watching INBOX for ${accountId}`);
|
|
1940
1915
|
}
|
|
1941
1916
|
catch (e) {
|
|
1942
1917
|
console.error(` [idle] Failed to watch ${accountId}: ${e.message}`);
|
|
@@ -1956,6 +1931,47 @@ export class ImapManager extends EventEmitter {
|
|
|
1956
1931
|
/** Unlink the on-disk body file for a message by reading its `body_path`
|
|
1957
1932
|
* from the DB. Safe to call either before or after `db.deleteMessage`
|
|
1958
1933
|
* — read body_path first, store it, then unlink whenever. */
|
|
1934
|
+
/** Per-(account, folder, uid) deferred-delete timer. Reconcile populates
|
|
1935
|
+
* this; the timer fires 60s later and re-checks whether the row at that
|
|
1936
|
+
* exact (folder, uid) still exists. If yes → really delete (server
|
|
1937
|
+
* expunged it). If no → skip (move-detect rebound it elsewhere; the
|
|
1938
|
+
* row already moved to a new folder/uid in the same DB row, so the
|
|
1939
|
+
* original key no longer matches anything). Net effect: server-side
|
|
1940
|
+
* moves preserve UUID + body cache + flags. */
|
|
1941
|
+
deferredDeletes = new Map();
|
|
1942
|
+
/** Grace window for reconcile-delete. Set to 30 minutes because the
|
|
1943
|
+
* full-folder sync loop walks ~96 folders sequentially and each one
|
|
1944
|
+
* takes 1-30 seconds; a server-side move from INBOX to a folder that
|
|
1945
|
+
* syncs late (alphabetically far from INBOX) can take 10+ minutes
|
|
1946
|
+
* before move-detect fires in the destination. 60s was way too short
|
|
1947
|
+
* in production — the grace expired and rows were committed-deleted
|
|
1948
|
+
* before _Spam / Sent / archive folders had a chance to rebind. */
|
|
1949
|
+
static RECONCILE_DELETE_GRACE_MS = 30 * 60_000;
|
|
1950
|
+
scheduleDeferredReconcileDelete(accountId, folderId, uid, folderPath) {
|
|
1951
|
+
const key = `${accountId}:${folderId}:${uid}`;
|
|
1952
|
+
// If already pending, don't reset — the 60s clock should run from
|
|
1953
|
+
// the FIRST detection so a flapping server can't keep deferring.
|
|
1954
|
+
if (this.deferredDeletes.has(key))
|
|
1955
|
+
return;
|
|
1956
|
+
const t = setTimeout(() => {
|
|
1957
|
+
this.deferredDeletes.delete(key);
|
|
1958
|
+
// Verify the row still exists at the original (folder, uid).
|
|
1959
|
+
// If move-detect rebound it during the grace window, this
|
|
1960
|
+
// lookup returns null — skip the delete.
|
|
1961
|
+
const env = this.db.getMessageByUid(accountId, uid, folderId);
|
|
1962
|
+
if (!env || env.folderId !== folderId) {
|
|
1963
|
+
console.log(` [reconcile-skip] ${accountId}/${folderPath} uid=${uid}: row moved during grace window (rebound to folder ${env?.folderId ?? "deleted"})`);
|
|
1964
|
+
return;
|
|
1965
|
+
}
|
|
1966
|
+
const tag = env.messageId ? `msgid=${env.messageId} subj="${(env.subject || "").slice(0, 60)}"` : "no-msgid";
|
|
1967
|
+
console.log(` [reconcile-delete] ${accountId}/${folderPath} uid=${uid} ${tag} (after 60s grace, no rebind)`);
|
|
1968
|
+
this.unlinkBodyFile(accountId, uid, folderId).catch(() => { });
|
|
1969
|
+
this.db.deleteMessage(accountId, uid, "reconcile: server missing this UID 60s after detection (no move-detect rebind)", `mailx-imap syncFolder deferred reconcile (${folderPath})`);
|
|
1970
|
+
this.db.recalcFolderCounts(folderId);
|
|
1971
|
+
this.emit("folderCountsChanged", accountId, {});
|
|
1972
|
+
}, ImapManager.RECONCILE_DELETE_GRACE_MS);
|
|
1973
|
+
this.deferredDeletes.set(key, t);
|
|
1974
|
+
}
|
|
1959
1975
|
async unlinkBodyFile(accountId, uid, folderId) {
|
|
1960
1976
|
try {
|
|
1961
1977
|
const row = this.db.getMessageByUid(accountId, uid, folderId);
|
|
@@ -2204,7 +2220,7 @@ export class ImapManager extends EventEmitter {
|
|
|
2204
2220
|
continue;
|
|
2205
2221
|
try {
|
|
2206
2222
|
this.unlinkBodyFile(accountId, uid, folderId).catch(() => { });
|
|
2207
|
-
this.db.deleteMessage(accountId, uid);
|
|
2223
|
+
this.db.deleteMessage(accountId, uid, "prefetch batch: server didn't return body for queued UID — assumed deleted", "mailx-imap prefetchBodies (Gmail batch)");
|
|
2208
2224
|
counters.deleted++;
|
|
2209
2225
|
madeProgress = true;
|
|
2210
2226
|
}
|
|
@@ -2304,7 +2320,7 @@ export class ImapManager extends EventEmitter {
|
|
|
2304
2320
|
continue;
|
|
2305
2321
|
try {
|
|
2306
2322
|
this.unlinkBodyFile(accountId, uid, folderId).catch(() => { });
|
|
2307
|
-
this.db.deleteMessage(accountId, uid);
|
|
2323
|
+
this.db.deleteMessage(accountId, uid, "prefetch batch: server didn't return body for queued UID — assumed deleted", "mailx-imap prefetchBodies (IMAP batch)");
|
|
2308
2324
|
counters.deleted++;
|
|
2309
2325
|
madeProgress = true;
|
|
2310
2326
|
}
|
|
@@ -2340,7 +2356,7 @@ export class ImapManager extends EventEmitter {
|
|
|
2340
2356
|
// Local first — remove all from DB immediately
|
|
2341
2357
|
for (const msg of messages) {
|
|
2342
2358
|
this.unlinkBodyFile(accountId, msg.uid, msg.folderId).catch(() => { });
|
|
2343
|
-
this.db.deleteMessage(accountId, msg.uid);
|
|
2359
|
+
this.db.deleteMessage(accountId, msg.uid, "user-initiated delete (bulk)", "mailx-imap deleteMessages");
|
|
2344
2360
|
}
|
|
2345
2361
|
console.log(` Deleted ${messages.length} messages locally`);
|
|
2346
2362
|
// Queue IMAP actions
|
|
@@ -2393,7 +2409,7 @@ export class ImapManager extends EventEmitter {
|
|
|
2393
2409
|
const trash = this.findFolder(accountId, "trash");
|
|
2394
2410
|
// Local first — remove from DB immediately
|
|
2395
2411
|
this.unlinkBodyFile(accountId, uid, folderId).catch(() => { });
|
|
2396
|
-
this.db.deleteMessage(accountId, uid);
|
|
2412
|
+
this.db.deleteMessage(accountId, uid, "user-initiated trash", "mailx-imap trashMessage");
|
|
2397
2413
|
// Queue IMAP action + log the resolution so "I deleted a message and
|
|
2398
2414
|
// now it's in neither trash nor deleted" is diagnosable from the log.
|
|
2399
2415
|
if (trash && trash.id !== folderId) {
|
|
@@ -2435,7 +2451,7 @@ export class ImapManager extends EventEmitter {
|
|
|
2435
2451
|
if (!msg)
|
|
2436
2452
|
throw new Error(`Message UID ${uid} not found in ${fromFolder.path}`);
|
|
2437
2453
|
await sourceClient.moveMessageToServer(msg, fromFolder.path, targetClient, toFolder.path);
|
|
2438
|
-
this.db.deleteMessage(fromAccountId, uid);
|
|
2454
|
+
this.db.deleteMessage(fromAccountId, uid, `cross-account move to ${toAccountId}/${toFolder.path}`, "mailx-imap moveBetweenAccounts");
|
|
2439
2455
|
console.log(` Cross-account move: ${fromAccountId}/${fromFolder.path} UID ${uid} → ${toAccountId}/${toFolder.path}`);
|
|
2440
2456
|
});
|
|
2441
2457
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/mailx-imap",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.27",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@bobfrankston/mailx-types": "^0.1.10",
|
|
13
13
|
"@bobfrankston/mailx-settings": "^0.1.13",
|
|
14
|
-
"@bobfrankston/mailx-store": "^0.1.
|
|
15
|
-
"@bobfrankston/iflow-direct": "^0.1.
|
|
14
|
+
"@bobfrankston/mailx-store": "^0.1.12",
|
|
15
|
+
"@bobfrankston/iflow-direct": "^0.1.35",
|
|
16
16
|
"@bobfrankston/tcp-transport": "^0.1.5",
|
|
17
17
|
"@bobfrankston/smtp-direct": "^0.1.5",
|
|
18
18
|
"@bobfrankston/mailx-sync": "^0.1.15",
|
|
@@ -39,8 +39,8 @@
|
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@bobfrankston/mailx-types": "^0.1.10",
|
|
41
41
|
"@bobfrankston/mailx-settings": "^0.1.13",
|
|
42
|
-
"@bobfrankston/mailx-store": "^0.1.
|
|
43
|
-
"@bobfrankston/iflow-direct": "^0.1.
|
|
42
|
+
"@bobfrankston/mailx-store": "^0.1.12",
|
|
43
|
+
"@bobfrankston/iflow-direct": "^0.1.35",
|
|
44
44
|
"@bobfrankston/tcp-transport": "^0.1.5",
|
|
45
45
|
"@bobfrankston/smtp-direct": "^0.1.5",
|
|
46
46
|
"@bobfrankston/mailx-sync": "^0.1.15",
|