imapflow 1.6.6 → 1.7.1
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/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +20 -0
- package/README.md +2 -3
- package/lib/commands/authenticate.js +15 -5
- package/lib/commands/idle.js +46 -18
- package/lib/imap-flow.d.ts +15 -1
- package/lib/imap-flow.js +373 -92
- package/lib/proxy-connection.js +14 -4
- package/lib/tools.js +29 -0
- package/package.json +3 -3
- package/test/auto-idle-test.js +470 -0
- package/test/connection-edge-cases-test.js +3 -1
- package/test/fixtures/test-client.js +57 -0
- package/test/idle-polling-test.js +88 -0
- package/test/imap-flow-coverage-test.js +4 -10
- package/test/imap-flow-fetch-download-test.js +3 -10
- package/test/imap-flow-internals-test.js +168 -50
- package/test/timer-policy-test.js +4 -17
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.7.1](https://github.com/postalsys/imapflow/compare/v1.7.0...v1.7.1) (2026-08-14)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **logging:** mask credential frames in the raw log and keep error detail ([2d6563b](https://github.com/postalsys/imapflow/commit/2d6563b75410a2cd9d4fbca08a76230589b60079))
|
|
9
|
+
|
|
10
|
+
## [1.7.0](https://github.com/postalsys/imapflow/compare/v1.6.6...v1.7.0) (2026-08-11)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* **idle:** make the auto-IDLE delay configurable ([311fe0c](https://github.com/postalsys/imapflow/commit/311fe0ccb0d75d4ddbe797bf5bc755df5bf0485f))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Bug Fixes
|
|
19
|
+
|
|
20
|
+
* **idle:** align the socket watchdog with the auto-IDLE busy guard ([d5e7191](https://github.com/postalsys/imapflow/commit/d5e71915ac7a1829b7e1340da6195a5e3e9984b2))
|
|
21
|
+
* **idle:** validate autoIdleDelay and keep auto-IDLE off a busy connection ([aeafdf6](https://github.com/postalsys/imapflow/commit/aeafdf62fab3b29cd488d9b8cbdffc8896107d6e))
|
|
22
|
+
|
|
3
23
|
## [1.6.6](https://github.com/postalsys/imapflow/compare/v1.6.5...v1.6.6) (2026-08-07)
|
|
4
24
|
|
|
5
25
|
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ Modern and easy-to-use IMAP client library for Node.js.
|
|
|
5
5
|
[](https://www.npmjs.com/package/imapflow)
|
|
6
6
|
[](https://github.com/postalsys/imapflow/blob/master/LICENSE)
|
|
7
7
|
|
|
8
|
-
ImapFlow provides a clean, promise-based API for working with IMAP, so you don't need in-depth knowledge of the protocol. IMAP extensions are detected and handled automatically. You write the same code regardless of server capabilities, and ImapFlow adapts behind the scenes.
|
|
8
|
+
ImapFlow provides a clean, promise-based API for working with IMAP, so you don't need in-depth knowledge of the protocol. IMAP extensions are detected and handled automatically. You write the same code regardless of server capabilities, and ImapFlow adapts behind the scenes. ImapFlow is the IMAP engine that powers [EmailEngine](https://emailengine.app/?utm_source=imapflow-readme&utm_medium=readme&utm_campaign=oss-docs&utm_content=intro), a self-hosted email API built by the same team.
|
|
9
9
|
|
|
10
10
|
## Features
|
|
11
11
|
|
|
@@ -77,8 +77,7 @@ Full documentation is available at **[imapflow.com](https://imapflow.com/docs/)*
|
|
|
77
77
|
- [Mailbox Management](https://imapflow.com/docs/guides/mailbox-management) - creating, renaming, and deleting mailboxes
|
|
78
78
|
- [API Reference](https://imapflow.com/docs/api/imapflow-client) - complete method and event documentation
|
|
79
79
|
|
|
80
|
-
> [
|
|
81
|
-
> If you are looking for a complete email integration solution, ImapFlow was built for [EmailEngine](https://emailengine.app/), a self-hosted email gateway that provides REST API access to IMAP and SMTP accounts.
|
|
80
|
+
> ImapFlow was built for **[EmailEngine](https://emailengine.app/?utm_source=imapflow-readme&utm_medium=readme&utm_campaign=oss-docs&utm_content=note)**, a self-hosted email API that turns Gmail, Microsoft 365, and IMAP accounts into REST endpoints, with managed OAuth2 and webhooks for incoming mail. If you need a production email integration rather than an IMAP client, start there.
|
|
82
81
|
|
|
83
82
|
## License
|
|
84
83
|
|
|
@@ -81,11 +81,16 @@ async function authOauth(connection, username, accessToken) {
|
|
|
81
81
|
try {
|
|
82
82
|
errorResponse = JSON.parse(Buffer.from(resp.attributes[0].value, 'base64').toString());
|
|
83
83
|
} catch (err) {
|
|
84
|
-
connection.log.debug({
|
|
84
|
+
connection.log.debug({
|
|
85
|
+
msg: 'Failed to parse OAuth error response',
|
|
86
|
+
errorResponse: resp.attributes[0].value,
|
|
87
|
+
err,
|
|
88
|
+
cid: connection.id
|
|
89
|
+
});
|
|
85
90
|
}
|
|
86
91
|
}
|
|
87
92
|
|
|
88
|
-
connection.log.debug({ src: 'c', msg: breaker, comment: `Error response for ${command}
|
|
93
|
+
connection.log.debug({ src: 'c', msg: breaker, comment: `Error response for ${command}`, cid: connection.id });
|
|
89
94
|
connection.write(breaker);
|
|
90
95
|
}
|
|
91
96
|
}
|
|
@@ -126,10 +131,10 @@ async function authLogin(connection, username, password) {
|
|
|
126
131
|
|
|
127
132
|
if (question === 'username' || question === 'user name') {
|
|
128
133
|
let encodedUsername = Buffer.from(username).toString('base64');
|
|
129
|
-
connection.log.debug({ src: 'c', msg: encodedUsername, comment: `Encoded username for AUTH=LOGIN
|
|
134
|
+
connection.log.debug({ src: 'c', msg: encodedUsername, comment: `Encoded username for AUTH=LOGIN`, cid: connection.id });
|
|
130
135
|
connection.write(encodedUsername);
|
|
131
136
|
} else if (question === 'password') {
|
|
132
|
-
connection.log.debug({ src: 'c', msg: '(* value hidden *)', comment: `Encoded password for AUTH=LOGIN
|
|
137
|
+
connection.log.debug({ src: 'c', msg: '(* value hidden *)', comment: `Encoded password for AUTH=LOGIN`, cid: connection.id });
|
|
133
138
|
connection.write(Buffer.from(password).toString('base64'));
|
|
134
139
|
} else {
|
|
135
140
|
throw new Error(`Unknown LOGIN question "${question}"`);
|
|
@@ -169,7 +174,12 @@ async function authPlain(connection, username, password, authzid) {
|
|
|
169
174
|
let authzidValue = authzid || '';
|
|
170
175
|
let encodedResponse = Buffer.from([authzidValue, username, password].join('\x00')).toString('base64');
|
|
171
176
|
let loggedResponse = Buffer.from([authzidValue, username, '(* value hidden *)'].join('\x00')).toString('base64');
|
|
172
|
-
connection.log.debug({
|
|
177
|
+
connection.log.debug({
|
|
178
|
+
src: 'c',
|
|
179
|
+
msg: loggedResponse,
|
|
180
|
+
comment: `Encoded response for AUTH=PLAIN${authzid ? ' with authzid' : ''}`,
|
|
181
|
+
cid: connection.id
|
|
182
|
+
});
|
|
173
183
|
connection.write(encodedResponse);
|
|
174
184
|
}
|
|
175
185
|
});
|
package/lib/commands/idle.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { hasCapability, unrefTimer } = require('../tools.js');
|
|
3
|
+
const { hasCapability, logConnectionError, unrefTimer } = require('../tools.js');
|
|
4
4
|
|
|
5
5
|
const NOOP_INTERVAL = 2 * 60 * 1000;
|
|
6
6
|
|
|
@@ -64,7 +64,8 @@ async function runIdle(connection) {
|
|
|
64
64
|
msg: `DONE`,
|
|
65
65
|
comment: `breaking IDLE`,
|
|
66
66
|
lockId: connection.currentLock?.lockId,
|
|
67
|
-
path: connection.mailbox && connection.mailbox.path
|
|
67
|
+
path: connection.mailbox && connection.mailbox.path,
|
|
68
|
+
cid: connection.id
|
|
68
69
|
});
|
|
69
70
|
connection.write('DONE');
|
|
70
71
|
doneSent = true;
|
|
@@ -95,10 +96,11 @@ async function runIdle(connection) {
|
|
|
95
96
|
queued: preCheckWaitQueue.length,
|
|
96
97
|
doneRequested,
|
|
97
98
|
canEnd,
|
|
98
|
-
doneSent
|
|
99
|
+
doneSent,
|
|
100
|
+
cid: connection.id
|
|
99
101
|
});
|
|
100
102
|
|
|
101
|
-
preCheck().catch(err => connection
|
|
103
|
+
preCheck().catch(err => logConnectionError(connection, 'Failed to break IDLE', err));
|
|
102
104
|
|
|
103
105
|
return handler;
|
|
104
106
|
};
|
|
@@ -112,13 +114,18 @@ async function runIdle(connection) {
|
|
|
112
114
|
// After this, the server will push untagged responses for mailbox changes.
|
|
113
115
|
// We can now safely send DONE if a break was already requested.
|
|
114
116
|
onPlusTag: async () => {
|
|
115
|
-
connection.log.debug({
|
|
117
|
+
connection.log.debug({
|
|
118
|
+
msg: `Initiated IDLE, waiting for server input`,
|
|
119
|
+
lockId: connection.currentLock?.lockId,
|
|
120
|
+
doneRequested,
|
|
121
|
+
cid: connection.id
|
|
122
|
+
});
|
|
116
123
|
canEnd = true;
|
|
117
124
|
if (doneRequested) {
|
|
118
125
|
try {
|
|
119
126
|
await preCheck();
|
|
120
127
|
} catch (err) {
|
|
121
|
-
connection
|
|
128
|
+
logConnectionError(connection, 'Failed to break IDLE', err);
|
|
122
129
|
}
|
|
123
130
|
}
|
|
124
131
|
},
|
|
@@ -128,7 +135,7 @@ async function runIdle(connection) {
|
|
|
128
135
|
response.next();
|
|
129
136
|
return;
|
|
130
137
|
} catch (err) {
|
|
131
|
-
connection
|
|
138
|
+
logConnectionError(connection, 'IDLE session failed', err);
|
|
132
139
|
while (preCheckWaitQueue.length) {
|
|
133
140
|
let { reject } = preCheckWaitQueue.shift();
|
|
134
141
|
reject(err);
|
|
@@ -166,12 +173,12 @@ async function pollOnce(connection, session) {
|
|
|
166
173
|
|
|
167
174
|
switch (connection.missingIdleCommand) {
|
|
168
175
|
case 'SELECT':
|
|
169
|
-
connection.log.debug({
|
|
176
|
+
connection.log.debug({ msg: `Running SELECT to detect changes in folder`, cid: connection.id });
|
|
170
177
|
await connection.runInternal('SELECT', path, { readOnly: session.selectCommand.command === 'EXAMINE' });
|
|
171
178
|
break;
|
|
172
179
|
|
|
173
180
|
case 'STATUS': {
|
|
174
|
-
connection.log.debug({
|
|
181
|
+
connection.log.debug({ msg: `Running STATUS to detect changes in folder`, cid: connection.id });
|
|
175
182
|
// HIGHESTMODSEQ is filtered out again unless the server advertises CONDSTORE, so a
|
|
176
183
|
// CONDSTORE session keeps mailbox.highestModseq current without asking a plain
|
|
177
184
|
// server for an item it does not know.
|
|
@@ -240,7 +247,7 @@ async function runPollingFallback(connection, maxIdleTime) {
|
|
|
240
247
|
};
|
|
241
248
|
|
|
242
249
|
session.preCheck = async () => {
|
|
243
|
-
connection.log.debug({
|
|
250
|
+
connection.log.debug({ msg: `Breaking NOOP loop`, cid: connection.id });
|
|
244
251
|
cancel();
|
|
245
252
|
};
|
|
246
253
|
connection.preCheck = session.preCheck;
|
|
@@ -257,24 +264,45 @@ async function runPollingFallback(connection, maxIdleTime) {
|
|
|
257
264
|
|
|
258
265
|
pollOnce(connection, session)
|
|
259
266
|
.then(() => {
|
|
267
|
+
// Stamped only after a poll actually completed: a failed poll must not
|
|
268
|
+
// satisfy the resumed schedule below, or the next session would defer
|
|
269
|
+
// its first poll a full interval past an attempt that checked nothing.
|
|
270
|
+
connection._lastPollAt = Date.now();
|
|
260
271
|
// Cancellation is re-checked here: the session may have been broken while
|
|
261
272
|
// this poll was in flight, and an orphaned poller must not schedule again.
|
|
262
273
|
if (session.cancelled) {
|
|
263
274
|
return;
|
|
264
275
|
}
|
|
265
|
-
|
|
266
|
-
// Background polling must not keep the process alive
|
|
267
|
-
unrefTimer(session.timer);
|
|
276
|
+
scheduleNextPoll(interval);
|
|
268
277
|
})
|
|
269
278
|
.catch(err => {
|
|
270
|
-
connection
|
|
279
|
+
logConnectionError(connection, 'Failed to poll for mailbox changes', err);
|
|
271
280
|
cancel();
|
|
272
281
|
});
|
|
273
282
|
};
|
|
274
283
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
284
|
+
function scheduleNextPoll(delay) {
|
|
285
|
+
session.timer = setTimeout(runPoll, delay);
|
|
286
|
+
// Background polling must not keep the process alive
|
|
287
|
+
unrefTimer(session.timer);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
connection.log.debug({ msg: `Initiated NOOP loop`, cid: connection.id });
|
|
291
|
+
|
|
292
|
+
// Every auto-IDLE restart begins a fresh polling session, so an unconditional first
|
|
293
|
+
// poll would tie the poll rate to how often the caller runs commands rather than to
|
|
294
|
+
// `interval`: with a short autoIdleDelay, a command every few seconds turns into a
|
|
295
|
+
// poll every few seconds. The last poll timestamp lives on the connection, so a new
|
|
296
|
+
// session resumes the previous one's schedule instead of restarting it.
|
|
297
|
+
// Clamped at zero because a backward wall-clock step (NTP, VM resume) leaves the
|
|
298
|
+
// stamp in the future; however large the jump, the next poll must never be more
|
|
299
|
+
// than one full interval away.
|
|
300
|
+
let sinceLastPoll = Math.max(0, Date.now() - (connection._lastPollAt || 0));
|
|
301
|
+
if (sinceLastPoll >= interval) {
|
|
302
|
+
runPoll();
|
|
303
|
+
} else {
|
|
304
|
+
scheduleNextPoll(interval - sinceLastPoll);
|
|
305
|
+
}
|
|
278
306
|
});
|
|
279
307
|
} finally {
|
|
280
308
|
session.cancelled = true;
|
|
@@ -318,7 +346,7 @@ module.exports = async (connection, maxIdleTime) => {
|
|
|
318
346
|
stillIdling = true;
|
|
319
347
|
// request IDLE break if IDLE has been running for allowed time
|
|
320
348
|
connection.log.trace({ msg: 'Max allowed IDLE time reached', cid: connection.id });
|
|
321
|
-
connection.preCheck().catch(err => connection
|
|
349
|
+
connection.preCheck().catch(err => logConnectionError(connection, 'Failed to break IDLE for restart', err));
|
|
322
350
|
}
|
|
323
351
|
}
|
|
324
352
|
}, maxIdleTime);
|
package/lib/imap-flow.d.ts
CHANGED
|
@@ -30,11 +30,25 @@ export interface ImapFlowOptions {
|
|
|
30
30
|
clientInfo?: IdInfoObject;
|
|
31
31
|
/** If true, then do not start IDLE when connection is established */
|
|
32
32
|
disableAutoIdle?: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* How long (in ms) the connection has to be inactive before IDLE is started automatically.
|
|
35
|
+
* Keep it above the pause your own code usually leaves between two commands, otherwise every
|
|
36
|
+
* command is followed by an IDLE that the next command has to break, costing two extra
|
|
37
|
+
* round-trips per command. To turn auto-IDLE off use `disableAutoIdle` rather than a very
|
|
38
|
+
* large delay: the value is capped below `socketTimeout`, because auto-IDLE has to start
|
|
39
|
+
* before the inactivity watchdog fires. On servers without IDLE support this controls when
|
|
40
|
+
* the polling fallback starts, not how often it polls - the poll interval is `maxIdleTime`,
|
|
41
|
+
* capped at 2 minutes. Default: 15000 ms.
|
|
42
|
+
*/
|
|
43
|
+
autoIdleDelay?: number;
|
|
33
44
|
/** Additional TLS options (see Node.js TLS documentation) */
|
|
34
45
|
tls?: ConnectionOptions;
|
|
35
46
|
/** Custom logger instance. Set to false to disable logging */
|
|
36
47
|
logger?: Logger | false;
|
|
37
|
-
/**
|
|
48
|
+
/**
|
|
49
|
+
* If true, log data read and written to socket encoded in base64. Client frames that carry
|
|
50
|
+
* credentials are replaced with a fixed placeholder and marked with `hidden: true`.
|
|
51
|
+
*/
|
|
38
52
|
logRaw?: boolean;
|
|
39
53
|
/** If true, emit 'log' events */
|
|
40
54
|
emitLogs?: boolean;
|