@bobfrankston/iflow-direct 0.1.18 → 0.1.20
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/README.md +10 -0
- package/imap-compat.d.ts +6 -2
- package/imap-compat.js +10 -5
- package/imap-native.d.ts +5 -0
- package/imap-native.js +52 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -106,6 +106,16 @@ the same connection). Suspend sends DONE, awaits the tagged OK, runs the
|
|
|
106
106
|
command, then re-enters IDLE. If the caller stops IDLE (via the `startIdle`
|
|
107
107
|
stop function) during the command, IDLE is not re-entered.
|
|
108
108
|
|
|
109
|
+
## Connection liveness
|
|
110
|
+
|
|
111
|
+
`NativeImapClient.connected` (and `CompatImapClient`'s lazy `ensureConnected`
|
|
112
|
+
path, which now delegates to it) is authoritative: it gets cleared on transport
|
|
113
|
+
error, transport close, inactivity-timeout socket kill, and write failure. Any
|
|
114
|
+
of those paths also drops IDLE state so auto-suspend doesn't try to DONE a dead
|
|
115
|
+
socket. Callers that detect connection-style errors (e.g. DNS "hostname not
|
|
116
|
+
known" from a dead Android socket) can safely retry — the next operation will
|
|
117
|
+
reconnect from scratch rather than reusing the dead socket.
|
|
118
|
+
|
|
109
119
|
## Configuration
|
|
110
120
|
|
|
111
121
|
`ImapClientConfig` fields relevant to throughput/resilience:
|
package/imap-compat.d.ts
CHANGED
|
@@ -23,11 +23,15 @@ export interface SpecialFolders {
|
|
|
23
23
|
*/
|
|
24
24
|
export declare class CompatImapClient {
|
|
25
25
|
private native;
|
|
26
|
-
private _connected;
|
|
27
26
|
constructor(config: ImapClientConfig, transportFactory: TransportFactory);
|
|
28
27
|
/** Connect and authenticate */
|
|
29
28
|
connect(): Promise<void>;
|
|
30
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* Ensure connected (lazy connect). Checks both the native client's connected
|
|
31
|
+
* flag AND the transport's connected state. The transport flag catches cases
|
|
32
|
+
* where the socket died without a close/error event reaching NativeImapClient
|
|
33
|
+
* (common on Android where the bridge may not relay all TCP events).
|
|
34
|
+
*/
|
|
31
35
|
private ensureConnected;
|
|
32
36
|
logout(): Promise<void>;
|
|
33
37
|
/** Get folder list */
|
package/imap-compat.js
CHANGED
|
@@ -11,22 +11,27 @@ import { FetchedMessage } from "./fetched-message.js";
|
|
|
11
11
|
*/
|
|
12
12
|
export class CompatImapClient {
|
|
13
13
|
native;
|
|
14
|
-
_connected = false;
|
|
15
14
|
constructor(config, transportFactory) {
|
|
16
15
|
this.native = new NativeImapClient(config, transportFactory);
|
|
17
16
|
}
|
|
18
17
|
/** Connect and authenticate */
|
|
19
18
|
async connect() {
|
|
20
19
|
await this.native.connect();
|
|
21
|
-
this._connected = true;
|
|
22
20
|
}
|
|
23
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* Ensure connected (lazy connect). Checks both the native client's connected
|
|
23
|
+
* flag AND the transport's connected state. The transport flag catches cases
|
|
24
|
+
* where the socket died without a close/error event reaching NativeImapClient
|
|
25
|
+
* (common on Android where the bridge may not relay all TCP events).
|
|
26
|
+
*/
|
|
24
27
|
async ensureConnected() {
|
|
25
|
-
if (!this.
|
|
28
|
+
if (!this.native.connected || !this.native.transportConnected) {
|
|
29
|
+
// Force a fresh transport — the old one may be in a bad state.
|
|
30
|
+
this.native.resetTransport();
|
|
26
31
|
await this.connect();
|
|
32
|
+
}
|
|
27
33
|
}
|
|
28
34
|
async logout() {
|
|
29
|
-
this._connected = false;
|
|
30
35
|
await this.native.logout();
|
|
31
36
|
}
|
|
32
37
|
/** Get folder list */
|
package/imap-native.d.ts
CHANGED
|
@@ -65,6 +65,11 @@ export declare class NativeImapClient {
|
|
|
65
65
|
private continuationResolve;
|
|
66
66
|
constructor(config: ImapClientConfig, transportFactory: TransportFactory);
|
|
67
67
|
get connected(): boolean;
|
|
68
|
+
/** Check the underlying transport's connected state — catches silently dead sockets. */
|
|
69
|
+
get transportConnected(): boolean;
|
|
70
|
+
/** Replace the transport with a fresh one from the factory. Call before reconnect
|
|
71
|
+
* when the old transport is in a bad state (dead socket, stale bridge stream). */
|
|
72
|
+
resetTransport(): void;
|
|
68
73
|
connect(): Promise<void>;
|
|
69
74
|
private readGreeting;
|
|
70
75
|
private authenticate;
|
package/imap-native.js
CHANGED
|
@@ -36,13 +36,35 @@ export class NativeImapClient {
|
|
|
36
36
|
this.fetchChunkSizeMax = config.fetchChunkSizeMax ?? 500;
|
|
37
37
|
}
|
|
38
38
|
get connected() { return this._connected; }
|
|
39
|
+
/** Check the underlying transport's connected state — catches silently dead sockets. */
|
|
40
|
+
get transportConnected() { return this.transport?.connected ?? false; }
|
|
41
|
+
/** Replace the transport with a fresh one from the factory. Call before reconnect
|
|
42
|
+
* when the old transport is in a bad state (dead socket, stale bridge stream). */
|
|
43
|
+
resetTransport() {
|
|
44
|
+
try {
|
|
45
|
+
this.transport?.close?.();
|
|
46
|
+
}
|
|
47
|
+
catch { /* ignore */ }
|
|
48
|
+
this.transport = this.transportFactory();
|
|
49
|
+
this._connected = false;
|
|
50
|
+
this.buffer = "";
|
|
51
|
+
this.pendingCommand = null;
|
|
52
|
+
this.selectedMailbox = null;
|
|
53
|
+
}
|
|
39
54
|
// ── Connection ──
|
|
40
55
|
async connect() {
|
|
41
56
|
const useTls = this.config.port === 993;
|
|
42
57
|
this.transport.onData((data) => this.handleData(data));
|
|
43
58
|
this.transport.onClose(() => {
|
|
44
59
|
this._connected = false;
|
|
45
|
-
//
|
|
60
|
+
// Drop stale IDLE state — the socket is gone.
|
|
61
|
+
this.idleTag = null;
|
|
62
|
+
this.idleCallback = null;
|
|
63
|
+
this.idleStopped = true;
|
|
64
|
+
if (this.idleRefreshTimer) {
|
|
65
|
+
clearTimeout(this.idleRefreshTimer);
|
|
66
|
+
this.idleRefreshTimer = null;
|
|
67
|
+
}
|
|
46
68
|
if (this.pendingCommand) {
|
|
47
69
|
const { reject } = this.pendingCommand;
|
|
48
70
|
this.pendingCommand = null;
|
|
@@ -52,7 +74,18 @@ export class NativeImapClient {
|
|
|
52
74
|
this.transport.onError((err) => {
|
|
53
75
|
if (this.verbose)
|
|
54
76
|
console.error(` [imap] Transport error: ${err.message}`);
|
|
55
|
-
//
|
|
77
|
+
// Transport errors (DNS failure, ECONNRESET, TLS failure, etc.) mean
|
|
78
|
+
// the connection is dead — clear the flag so ensureConnected() will
|
|
79
|
+
// reconnect on the next call, and drop the stale IDLE state so we
|
|
80
|
+
// don't try to DONE a dead socket during auto-suspend.
|
|
81
|
+
this._connected = false;
|
|
82
|
+
this.idleTag = null;
|
|
83
|
+
this.idleCallback = null;
|
|
84
|
+
this.idleStopped = true;
|
|
85
|
+
if (this.idleRefreshTimer) {
|
|
86
|
+
clearTimeout(this.idleRefreshTimer);
|
|
87
|
+
this.idleRefreshTimer = null;
|
|
88
|
+
}
|
|
56
89
|
if (this.pendingCommand) {
|
|
57
90
|
const { reject } = this.pendingCommand;
|
|
58
91
|
this.pendingCommand = null;
|
|
@@ -674,7 +707,10 @@ export class NativeImapClient {
|
|
|
674
707
|
const onTimeout = () => {
|
|
675
708
|
this.commandTimer = null;
|
|
676
709
|
this.pendingCommand = null;
|
|
677
|
-
// Kill the connection — a timed-out connection has stale data in the pipe
|
|
710
|
+
// Kill the connection — a timed-out connection has stale data in the pipe.
|
|
711
|
+
// Mark disconnected so ensureConnected() reconnects on the next call —
|
|
712
|
+
// transport.close() may or may not fire onClose synchronously.
|
|
713
|
+
this._connected = false;
|
|
678
714
|
this.transport.close?.();
|
|
679
715
|
reject(new Error(`IMAP inactivity timeout (${this.inactivityTimeout / 1000}s): ${command.split("\r")[0].substring(0, 80)}`));
|
|
680
716
|
};
|
|
@@ -687,8 +723,18 @@ export class NativeImapClient {
|
|
|
687
723
|
clearTimeout(this.commandTimer); this.commandTimer = null; reject(err); },
|
|
688
724
|
onUntagged,
|
|
689
725
|
};
|
|
690
|
-
this.transport.write(command).catch((err) => {
|
|
691
|
-
|
|
726
|
+
this.transport.write(command).catch((err) => {
|
|
727
|
+
if (this.commandTimer)
|
|
728
|
+
clearTimeout(this.commandTimer);
|
|
729
|
+
this.commandTimer = null;
|
|
730
|
+
// Write failure = dead socket. Clear pendingCommand + disconnect flag
|
|
731
|
+
// so the next ensureConnected() does a fresh connect instead of
|
|
732
|
+
// reusing a socket that the OS layer has already declared dead
|
|
733
|
+
// (the source of the "hostname nor servname provided" cascade).
|
|
734
|
+
this.pendingCommand = null;
|
|
735
|
+
this._connected = false;
|
|
736
|
+
reject(err);
|
|
737
|
+
});
|
|
692
738
|
});
|
|
693
739
|
}
|
|
694
740
|
waitForContinuation(tag) {
|
|
@@ -718,6 +764,7 @@ export class NativeImapClient {
|
|
|
718
764
|
if (this.pendingCommand) {
|
|
719
765
|
const cmd = this.pendingCommand;
|
|
720
766
|
this.pendingCommand = null;
|
|
767
|
+
this._connected = false;
|
|
721
768
|
this.transport.close?.();
|
|
722
769
|
cmd.reject(new Error(`IMAP inactivity timeout (${this.inactivityTimeout / 1000}s)`));
|
|
723
770
|
}
|