@bobfrankston/iflow-direct 0.1.49 → 0.1.51
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/imap-compat.d.ts +1 -0
- package/imap-compat.js +1 -1
- package/imap-native.d.ts +7 -1
- package/imap-native.js +60 -3
- package/package.json +1 -1
package/imap-compat.d.ts
CHANGED
|
@@ -160,6 +160,7 @@ export declare class CompatImapClient {
|
|
|
160
160
|
watchMailbox(mailbox: string, onNew: (count: number) => void, opts?: {
|
|
161
161
|
notifySpec?: string;
|
|
162
162
|
onMailboxStatus?: (mailbox: string, data: proto.StatusData) => void;
|
|
163
|
+
onExpunge?: () => void;
|
|
163
164
|
}): Promise<() => Promise<void>>;
|
|
164
165
|
/** Copy a message to another server (cross-account) */
|
|
165
166
|
moveMessageToServer(msg: any, fromMailbox: string, targetClient: CompatImapClient, toMailbox: string): Promise<void>;
|
package/imap-compat.js
CHANGED
|
@@ -349,7 +349,7 @@ export class CompatImapClient {
|
|
|
349
349
|
this.native.onMailboxStatus = opts.onMailboxStatus;
|
|
350
350
|
if (opts?.notifySpec)
|
|
351
351
|
await this.native.notify(opts.notifySpec);
|
|
352
|
-
return this.native.startIdle(onNew);
|
|
352
|
+
return this.native.startIdle(onNew, opts?.onExpunge);
|
|
353
353
|
}
|
|
354
354
|
/** Copy a message to another server (cross-account) */
|
|
355
355
|
async moveMessageToServer(msg, fromMailbox, targetClient, toMailbox) {
|
package/imap-native.d.ts
CHANGED
|
@@ -113,6 +113,12 @@ export declare class NativeImapClient {
|
|
|
113
113
|
private _connected;
|
|
114
114
|
private idleTag;
|
|
115
115
|
private idleCallback;
|
|
116
|
+
/** Fires when the SELECTED mailbox loses messages while parked in IDLE —
|
|
117
|
+
* an EXISTS decrease, or an unsolicited `* EXPUNGE` / `* VANISHED`
|
|
118
|
+
* (RFC 5465 NOTIFY SELECTED MessageExpunge). Lets the caller reconcile
|
|
119
|
+
* a server-side deletion in real time instead of waiting for the next
|
|
120
|
+
* periodic poll. Distinct from `idleCallback` (new mail only). */
|
|
121
|
+
private idleExpungeCallback;
|
|
116
122
|
private idleRefreshTimer;
|
|
117
123
|
/** RFC 5465 NOTIFY: fires on unsolicited STATUS responses for non-selected
|
|
118
124
|
* mailboxes (the server pushes these when the client has issued NOTIFY
|
|
@@ -257,7 +263,7 @@ export declare class NativeImapClient {
|
|
|
257
263
|
* SELECT and BEFORE startIdle — the server holds the spec for the
|
|
258
264
|
* lifetime of the connection. */
|
|
259
265
|
notify(spec: string): Promise<void>;
|
|
260
|
-
startIdle(onNewMail: (count: number) => void): Promise<() => Promise<void>>;
|
|
266
|
+
startIdle(onNewMail: (count: number) => void, onExpunge?: () => void): Promise<() => Promise<void>>;
|
|
261
267
|
/**
|
|
262
268
|
* If IDLE is currently active, send DONE and wait for its tagged OK so the
|
|
263
269
|
* connection is free to accept a new command. Saves the active callback so
|
package/imap-native.js
CHANGED
|
@@ -39,6 +39,12 @@ export class NativeImapClient {
|
|
|
39
39
|
_connected = false;
|
|
40
40
|
idleTag = null;
|
|
41
41
|
idleCallback = null;
|
|
42
|
+
/** Fires when the SELECTED mailbox loses messages while parked in IDLE —
|
|
43
|
+
* an EXISTS decrease, or an unsolicited `* EXPUNGE` / `* VANISHED`
|
|
44
|
+
* (RFC 5465 NOTIFY SELECTED MessageExpunge). Lets the caller reconcile
|
|
45
|
+
* a server-side deletion in real time instead of waiting for the next
|
|
46
|
+
* periodic poll. Distinct from `idleCallback` (new mail only). */
|
|
47
|
+
idleExpungeCallback = null;
|
|
42
48
|
idleRefreshTimer = null;
|
|
43
49
|
/** RFC 5465 NOTIFY: fires on unsolicited STATUS responses for non-selected
|
|
44
50
|
* mailboxes (the server pushes these when the client has issued NOTIFY
|
|
@@ -126,8 +132,29 @@ export class NativeImapClient {
|
|
|
126
132
|
}
|
|
127
133
|
});
|
|
128
134
|
this.transport.onError((err) => {
|
|
135
|
+
// Diagnose transport-level errors fully. Node emits socket errors
|
|
136
|
+
// with a populated `.message` in normal cases (ECONNRESET, ETIMEDOUT,
|
|
137
|
+
// etc.) — but TLS-layer errors, abrupt FINs surfaced as errors,
|
|
138
|
+
// and some wrapper paths produce an Error with empty `.message`
|
|
139
|
+
// and the useful info on `.code` / `.errno` / `.syscall` instead.
|
|
140
|
+
// Logging only `.message` left "Transport error: " in the log
|
|
141
|
+
// with no diagnostic content, which masked why bobma INBOX prefetch
|
|
142
|
+
// kept dying (2026-05-27). Build a richer description.
|
|
143
|
+
const errAny = err;
|
|
144
|
+
const parts = [];
|
|
145
|
+
if (errAny?.message)
|
|
146
|
+
parts.push(String(errAny.message));
|
|
147
|
+
if (errAny?.code)
|
|
148
|
+
parts.push(`code=${errAny.code}`);
|
|
149
|
+
if (errAny?.errno !== undefined)
|
|
150
|
+
parts.push(`errno=${errAny.errno}`);
|
|
151
|
+
if (errAny?.syscall)
|
|
152
|
+
parts.push(`syscall=${errAny.syscall}`);
|
|
153
|
+
const desc = parts.length > 0
|
|
154
|
+
? parts.join(" ")
|
|
155
|
+
: `<no message> ${typeof err} ${err?.constructor?.name || ""}`.trim();
|
|
129
156
|
if (this.verbose)
|
|
130
|
-
console.error(` [imap] Transport error: ${
|
|
157
|
+
console.error(` [imap] Transport error: ${desc}`);
|
|
131
158
|
// Transport errors (DNS failure, ECONNRESET, TLS failure, etc.) mean
|
|
132
159
|
// the connection is dead — clear the flag so ensureConnected() will
|
|
133
160
|
// reconnect on the next call, and drop the stale IDLE state so we
|
|
@@ -143,7 +170,17 @@ export class NativeImapClient {
|
|
|
143
170
|
if (this.pendingCommand) {
|
|
144
171
|
const { reject } = this.pendingCommand;
|
|
145
172
|
this.pendingCommand = null;
|
|
146
|
-
|
|
173
|
+
// Make sure the error that propagates to upper-layer catches
|
|
174
|
+
// (prefetch, sync, etc.) has a non-empty message — wrap if
|
|
175
|
+
// the underlying err has a blank message.
|
|
176
|
+
if (err instanceof Error && !err.message) {
|
|
177
|
+
const wrapped = new Error(desc);
|
|
178
|
+
wrapped.cause = err;
|
|
179
|
+
reject(wrapped);
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
reject(err);
|
|
183
|
+
}
|
|
147
184
|
}
|
|
148
185
|
});
|
|
149
186
|
await this.transport.connect(this.config.server, this.config.port, useTls, this.config.server);
|
|
@@ -731,8 +768,9 @@ export class NativeImapClient {
|
|
|
731
768
|
}
|
|
732
769
|
}
|
|
733
770
|
// ── IDLE ──
|
|
734
|
-
async startIdle(onNewMail) {
|
|
771
|
+
async startIdle(onNewMail, onExpunge) {
|
|
735
772
|
this.idleCallback = onNewMail;
|
|
773
|
+
this.idleExpungeCallback = onExpunge ?? null;
|
|
736
774
|
this.idleStopped = false;
|
|
737
775
|
const beginIdleCycle = async () => {
|
|
738
776
|
const tag = proto.nextTag();
|
|
@@ -764,6 +802,7 @@ export class NativeImapClient {
|
|
|
764
802
|
const tag = this.idleTag;
|
|
765
803
|
this.idleTag = null;
|
|
766
804
|
this.idleCallback = null;
|
|
805
|
+
this.idleExpungeCallback = null;
|
|
767
806
|
if (tag) {
|
|
768
807
|
// Arm tagged-wait BEFORE DONE — avoid missing the OK.
|
|
769
808
|
const waitTag = this.waitForTagged(tag);
|
|
@@ -1291,6 +1330,24 @@ export class NativeImapClient {
|
|
|
1291
1330
|
this.mailboxInfo.exists = count;
|
|
1292
1331
|
this.idleCallback(newCount);
|
|
1293
1332
|
}
|
|
1333
|
+
else if (count < this.mailboxInfo.exists) {
|
|
1334
|
+
// Count dropped — a message was expunged on the selected
|
|
1335
|
+
// mailbox (another client / Thunderbird deleted it).
|
|
1336
|
+
// Signal the caller to reconcile the deletion now.
|
|
1337
|
+
this.mailboxInfo.exists = count;
|
|
1338
|
+
if (this.idleExpungeCallback)
|
|
1339
|
+
this.idleExpungeCallback();
|
|
1340
|
+
}
|
|
1341
|
+
continue;
|
|
1342
|
+
}
|
|
1343
|
+
// During IDLE, an unsolicited `* n EXPUNGE` / `* VANISHED` for the
|
|
1344
|
+
// selected mailbox (RFC 5465 NOTIFY SELECTED MessageExpunge, or a
|
|
1345
|
+
// plain post-EXPUNGE notification). Push a deletion reconcile.
|
|
1346
|
+
if (this.idleTag && resp.tag === "*" && (resp.type === "EXPUNGE" || resp.type === "VANISHED")) {
|
|
1347
|
+
if (this.mailboxInfo.exists > 0)
|
|
1348
|
+
this.mailboxInfo.exists--;
|
|
1349
|
+
if (this.idleExpungeCallback)
|
|
1350
|
+
this.idleExpungeCallback();
|
|
1294
1351
|
continue;
|
|
1295
1352
|
}
|
|
1296
1353
|
// RFC 5465 NOTIFY: unsolicited STATUS responses for non-selected
|