imapflow 1.2.9 → 1.2.11
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 +14 -0
- package/README.md +36 -58
- package/eslint.config.js +18 -16
- package/lib/imap-flow.js +41 -19
- package/lib/tools.js +1 -6
- package/package.json +8 -13
- package/test/bodystructure-test.js +0 -231
- package/test/connection-edge-cases-test.js +105 -0
- package/test/fetch-generator-test.js +216 -0
- package/.babelrc +0 -6
- package/.eslintrc +0 -16
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.2.11](https://github.com/postalsys/imapflow/compare/v1.2.10...v1.2.11) (2026-03-03)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* release backpressure in fetch() generator on early break/throw ([d42a377](https://github.com/postalsys/imapflow/commit/d42a377f8102f44fc76d8fa9a268bbf2526a7fa8)), closes [#338](https://github.com/postalsys/imapflow/issues/338)
|
|
9
|
+
|
|
10
|
+
## [1.2.10](https://github.com/postalsys/imapflow/compare/v1.2.9...v1.2.10) (2026-02-19)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* add null guards in compress() to prevent crash on TLS teardown ([84cd3ff](https://github.com/postalsys/imapflow/commit/84cd3ff8815d8ba37a37c1e7a09c244e80b61948))
|
|
16
|
+
|
|
3
17
|
## [1.2.9](https://github.com/postalsys/imapflow/compare/v1.2.8...v1.2.9) (2026-02-06)
|
|
4
18
|
|
|
5
19
|
|
package/README.md
CHANGED
|
@@ -1,109 +1,87 @@
|
|
|
1
1
|
# ImapFlow
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Modern and easy-to-use IMAP client library for Node.js.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
The focus for ImapFlow is to provide easy to use API over IMAP. Using ImapFlow does not expect knowledge about specific IMAP details. A general understanding is good enough.
|
|
9
|
-
|
|
10
|
-
IMAP extensions are handled in the background, so, for example, you can always request `labels` value from a `fetch()` call, but if the IMAP server does not support `X-GM-EXT-1` extension, then `labels` value is not included in the response.
|
|
5
|
+
[](https://www.npmjs.com/package/imapflow)
|
|
6
|
+
[](https://github.com/postalsys/imapflow/blob/master/LICENSE)
|
|
11
7
|
|
|
12
|
-
|
|
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.
|
|
13
9
|
|
|
14
|
-
|
|
10
|
+
## Features
|
|
15
11
|
|
|
16
|
-
|
|
12
|
+
- **Async/await API** - all methods return Promises
|
|
13
|
+
- **Automatic IMAP extension handling** - CONDSTORE, QRESYNC, IDLE, COMPRESS, and [more](https://imapflow.com/docs/)
|
|
14
|
+
- **Message streaming** - async iterators for efficient processing
|
|
15
|
+
- **Mailbox locking** - built-in locking mechanism for safe concurrent access
|
|
16
|
+
- **TypeScript support** - type definitions included
|
|
17
|
+
- **Proxy support** - SOCKS and HTTP CONNECT proxies
|
|
18
|
+
- **Gmail support** - labels, raw search via X-GM-EXT-1
|
|
17
19
|
|
|
18
|
-
|
|
20
|
+
## Installation
|
|
19
21
|
|
|
20
|
-
```
|
|
22
|
+
```bash
|
|
21
23
|
npm install imapflow
|
|
22
24
|
```
|
|
23
25
|
|
|
24
|
-
|
|
26
|
+
## Quick Example
|
|
25
27
|
|
|
26
28
|
```js
|
|
27
29
|
const { ImapFlow } = require('imapflow');
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
### Promises
|
|
31
30
|
|
|
32
|
-
All ImapFlow methods use Promises, so you need to wait using `await` or wait for the `then()` method to fire until you get the response.
|
|
33
|
-
|
|
34
|
-
```js
|
|
35
|
-
const { ImapFlow } = require('imapflow');
|
|
36
31
|
const client = new ImapFlow({
|
|
37
|
-
host: '
|
|
32
|
+
host: 'imap.example.com',
|
|
38
33
|
port: 993,
|
|
39
34
|
secure: true,
|
|
40
35
|
auth: {
|
|
41
|
-
user: '
|
|
42
|
-
pass: '
|
|
36
|
+
user: 'user@example.com',
|
|
37
|
+
pass: 'password'
|
|
43
38
|
}
|
|
44
39
|
});
|
|
45
40
|
|
|
46
41
|
const main = async () => {
|
|
47
|
-
// Wait until client connects and authorizes
|
|
48
42
|
await client.connect();
|
|
49
43
|
|
|
50
|
-
// Select and lock a mailbox. Throws if mailbox does not exist
|
|
51
44
|
let lock = await client.getMailboxLock('INBOX');
|
|
52
45
|
try {
|
|
53
|
-
// fetch latest message
|
|
54
|
-
// client.mailbox includes information about currently selected mailbox
|
|
55
|
-
// "exists" value is also the largest sequence number available in the mailbox
|
|
46
|
+
// fetch latest message
|
|
56
47
|
let message = await client.fetchOne(client.mailbox.exists, { source: true });
|
|
57
48
|
console.log(message.source.toString());
|
|
58
49
|
|
|
59
50
|
// list subjects for all messages
|
|
60
|
-
// uid value is always included in FETCH response, envelope strings are in unicode.
|
|
61
51
|
for await (let message of client.fetch('1:*', { envelope: true })) {
|
|
62
52
|
console.log(`${message.uid}: ${message.envelope.subject}`);
|
|
63
53
|
}
|
|
64
54
|
} finally {
|
|
65
|
-
//
|
|
55
|
+
// always release the lock
|
|
66
56
|
lock.release();
|
|
67
57
|
}
|
|
68
58
|
|
|
69
|
-
// log out and close connection
|
|
70
59
|
await client.logout();
|
|
71
60
|
};
|
|
72
61
|
|
|
73
|
-
main().catch(
|
|
62
|
+
main().catch(console.error);
|
|
74
63
|
```
|
|
75
64
|
|
|
76
|
-
|
|
65
|
+
See the [Quick Start guide](https://imapflow.com/docs/getting-started/quick-start) for more examples, including Gmail, Outlook, and Yahoo configuration.
|
|
77
66
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
```js
|
|
81
|
-
const { ImapFlow } = require('imapflow');
|
|
82
|
-
const client = new ImapFlow({
|
|
83
|
-
host: 'mail.example.com',
|
|
84
|
-
port: 993,
|
|
85
|
-
secure: true,
|
|
86
|
-
auth: {
|
|
87
|
-
user: 'admin@example.com', // Admin credentials (authentication identity)
|
|
88
|
-
pass: 'adminpassword',
|
|
89
|
-
authzid: 'user@example.com', // User to impersonate (authorization identity)
|
|
90
|
-
loginMethod: 'AUTH=PLAIN' // Must use PLAIN mechanism for authzid
|
|
91
|
-
}
|
|
92
|
-
});
|
|
67
|
+
## Documentation
|
|
93
68
|
|
|
94
|
-
|
|
95
|
-
await client.connect();
|
|
96
|
-
// Now operating on user@example.com's mailbox as admin
|
|
97
|
-
```
|
|
69
|
+
Full documentation is available at **[imapflow.com](https://imapflow.com/docs/)**.
|
|
98
70
|
|
|
99
|
-
|
|
71
|
+
- [Installation](https://imapflow.com/docs/getting-started/installation) - requirements and setup
|
|
72
|
+
- [Quick Start](https://imapflow.com/docs/getting-started/quick-start) - your first ImapFlow application
|
|
73
|
+
- [Basic Usage](https://imapflow.com/docs/guides/basic-usage) - core concepts and patterns
|
|
74
|
+
- [Configuration](https://imapflow.com/docs/guides/configuration) - connection options and settings
|
|
75
|
+
- [Fetching Messages](https://imapflow.com/docs/guides/fetching-messages) - reading email data
|
|
76
|
+
- [Searching](https://imapflow.com/docs/guides/searching) - finding messages with search queries
|
|
77
|
+
- [Mailbox Management](https://imapflow.com/docs/guides/mailbox-management) - creating, renaming, and deleting mailboxes
|
|
78
|
+
- [API Reference](https://imapflow.com/docs/api/imapflow-client) - complete method and event documentation
|
|
100
79
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
[API reference](https://imapflow.com/docs/api/imapflow-client).
|
|
80
|
+
> [!NOTE]
|
|
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.
|
|
104
82
|
|
|
105
83
|
## License
|
|
106
84
|
|
|
107
|
-
|
|
85
|
+
Copyright (c) 2020-2025 Postal Systems OU
|
|
108
86
|
|
|
109
|
-
Licensed under
|
|
87
|
+
Licensed under the MIT license.
|
package/eslint.config.js
CHANGED
|
@@ -1,38 +1,40 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { FlatCompat } = require('@eslint/eslintrc');
|
|
4
3
|
const js = require('@eslint/js');
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
recommendedConfig: js.configs.recommended
|
|
9
|
-
});
|
|
4
|
+
const globals = require('globals');
|
|
5
|
+
const prettierConfig = require('eslint-config-prettier/flat');
|
|
6
|
+
const nodemailerConfig = require('eslint-config-nodemailer');
|
|
10
7
|
|
|
11
8
|
module.exports = [
|
|
12
9
|
{
|
|
13
10
|
ignores: ['node_modules/**', 'examples/**', 'docs/**']
|
|
14
11
|
},
|
|
15
|
-
|
|
12
|
+
js.configs.recommended,
|
|
16
13
|
{
|
|
17
14
|
languageOptions: {
|
|
18
|
-
ecmaVersion:
|
|
15
|
+
ecmaVersion: 2022,
|
|
19
16
|
sourceType: 'script',
|
|
20
|
-
globals: {
|
|
21
|
-
BigInt: 'readonly'
|
|
22
|
-
},
|
|
23
|
-
parser: require('@babel/eslint-parser'),
|
|
24
17
|
parserOptions: {
|
|
25
|
-
|
|
18
|
+
ecmaFeatures: {
|
|
19
|
+
globalReturn: true
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
globals: {
|
|
23
|
+
...globals.node,
|
|
24
|
+
...globals.es2021,
|
|
25
|
+
it: 'readonly',
|
|
26
|
+
describe: 'readonly',
|
|
27
|
+
beforeEach: 'readonly',
|
|
28
|
+
afterEach: 'readonly'
|
|
26
29
|
}
|
|
27
30
|
},
|
|
28
|
-
plugins: {
|
|
29
|
-
'@babel': require('@babel/eslint-plugin')
|
|
30
|
-
},
|
|
31
31
|
rules: {
|
|
32
|
+
...nodemailerConfig.rules,
|
|
32
33
|
'no-await-in-loop': 0,
|
|
33
34
|
'require-atomic-updates': 0
|
|
34
35
|
}
|
|
35
36
|
},
|
|
37
|
+
prettierConfig,
|
|
36
38
|
{
|
|
37
39
|
files: ['eslint.config.js', '.prettierrc.js', '.ncurc.js'],
|
|
38
40
|
rules: {
|
package/lib/imap-flow.js
CHANGED
|
@@ -1018,7 +1018,7 @@ class ImapFlow extends EventEmitter {
|
|
|
1018
1018
|
processedChunks = 0;
|
|
1019
1019
|
|
|
1020
1020
|
let chunk;
|
|
1021
|
-
while ((chunk = this.writeSocket.read()) !== null) {
|
|
1021
|
+
while (this.writeSocket && (chunk = this.writeSocket.read()) !== null) {
|
|
1022
1022
|
if (this._deflate && this._deflate.write(chunk) === false) {
|
|
1023
1023
|
return this._deflate.once('drain', readNext);
|
|
1024
1024
|
}
|
|
@@ -1027,6 +1027,9 @@ class ImapFlow extends EventEmitter {
|
|
|
1027
1027
|
processedChunks++;
|
|
1028
1028
|
if (processedChunks % 100 === 0) {
|
|
1029
1029
|
await new Promise(resolve => setImmediate(resolve));
|
|
1030
|
+
if (!this.writeSocket) {
|
|
1031
|
+
break;
|
|
1032
|
+
}
|
|
1030
1033
|
}
|
|
1031
1034
|
}
|
|
1032
1035
|
|
|
@@ -1042,17 +1045,21 @@ class ImapFlow extends EventEmitter {
|
|
|
1042
1045
|
};
|
|
1043
1046
|
|
|
1044
1047
|
this.writeSocket.on('readable', () => {
|
|
1045
|
-
if (!reading) {
|
|
1048
|
+
if (!reading && this.writeSocket) {
|
|
1046
1049
|
readNext();
|
|
1047
1050
|
}
|
|
1048
1051
|
});
|
|
1049
1052
|
this.writeSocket.on('error', err => {
|
|
1050
|
-
this.socket
|
|
1053
|
+
if (this.socket) {
|
|
1054
|
+
this.socket.emit('error', err);
|
|
1055
|
+
}
|
|
1051
1056
|
});
|
|
1052
1057
|
|
|
1053
1058
|
this._deflate.pipe(this.socket);
|
|
1054
1059
|
this._deflate.on('error', err => {
|
|
1055
|
-
this.socket
|
|
1060
|
+
if (this.socket) {
|
|
1061
|
+
this.socket.emit('error', err);
|
|
1062
|
+
}
|
|
1056
1063
|
});
|
|
1057
1064
|
}
|
|
1058
1065
|
|
|
@@ -2711,6 +2718,7 @@ class ImapFlow extends EventEmitter {
|
|
|
2711
2718
|
// two: when the consumer is waiting and the queue is empty, `push` is set to
|
|
2712
2719
|
// a function that wakes up the consumer when new data arrives.
|
|
2713
2720
|
let finished = false;
|
|
2721
|
+
let aborted = false;
|
|
2714
2722
|
let push = false;
|
|
2715
2723
|
let rowQueue = [];
|
|
2716
2724
|
|
|
@@ -2748,6 +2756,10 @@ class ImapFlow extends EventEmitter {
|
|
|
2748
2756
|
binary: options.binary,
|
|
2749
2757
|
changedSince: options.changedSince,
|
|
2750
2758
|
onUntaggedFetch: (untagged, next) => {
|
|
2759
|
+
if (aborted) {
|
|
2760
|
+
next();
|
|
2761
|
+
return;
|
|
2762
|
+
}
|
|
2751
2763
|
rowQueue.push({
|
|
2752
2764
|
value: {
|
|
2753
2765
|
response: untagged,
|
|
@@ -2772,26 +2784,36 @@ class ImapFlow extends EventEmitter {
|
|
|
2772
2784
|
}
|
|
2773
2785
|
});
|
|
2774
2786
|
|
|
2775
|
-
let
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2787
|
+
let lastRes = null;
|
|
2788
|
+
try {
|
|
2789
|
+
let res;
|
|
2790
|
+
while ((res = await getNext())) {
|
|
2791
|
+
lastRes = res;
|
|
2792
|
+
|
|
2793
|
+
if (this.isClosed || !this.socket || this.socket.destroyed) {
|
|
2794
|
+
let error = new Error('Connection closed');
|
|
2795
|
+
error.code = 'EConnectionClosed';
|
|
2796
|
+
throw error;
|
|
2797
|
+
}
|
|
2782
2798
|
|
|
2783
|
-
if (res !== null) {
|
|
2784
2799
|
yield res.response;
|
|
2785
2800
|
// Signal the FETCH handler to process the next untagged response
|
|
2786
2801
|
res.next();
|
|
2802
|
+
lastRes = null;
|
|
2803
|
+
}
|
|
2804
|
+
} finally {
|
|
2805
|
+
aborted = true;
|
|
2806
|
+
// Release backpressure for the item that was yielded but whose
|
|
2807
|
+
// next() was not yet called (happens on break/return/throw)
|
|
2808
|
+
if (lastRes && typeof lastRes.next === 'function') {
|
|
2809
|
+
lastRes.next();
|
|
2810
|
+
}
|
|
2811
|
+
while (rowQueue.length) {
|
|
2812
|
+
let entry = rowQueue.shift();
|
|
2813
|
+
if (entry.value && typeof entry.value.next === 'function') {
|
|
2814
|
+
entry.value.next();
|
|
2815
|
+
}
|
|
2787
2816
|
}
|
|
2788
|
-
}
|
|
2789
|
-
|
|
2790
|
-
if (!finished) {
|
|
2791
|
-
// FETCH never finished!
|
|
2792
|
-
let error = new Error('FETCH did not finish');
|
|
2793
|
-
error.code = 'ENotFinished';
|
|
2794
|
-
throw error;
|
|
2795
2817
|
}
|
|
2796
2818
|
}
|
|
2797
2819
|
|
package/lib/tools.js
CHANGED
|
@@ -206,11 +206,7 @@ const tools = {
|
|
|
206
206
|
if (cur) {
|
|
207
207
|
node = cur;
|
|
208
208
|
} else {
|
|
209
|
-
// not yet set
|
|
210
|
-
cur = {
|
|
211
|
-
name: parent,
|
|
212
|
-
folders: []
|
|
213
|
-
};
|
|
209
|
+
// not yet set, will be created by the caller
|
|
214
210
|
}
|
|
215
211
|
}
|
|
216
212
|
|
|
@@ -901,7 +897,6 @@ const tools = {
|
|
|
901
897
|
if (node[i]) {
|
|
902
898
|
curNode.location = ((node[i] || {}).value || '').toString();
|
|
903
899
|
}
|
|
904
|
-
i++;
|
|
905
900
|
}
|
|
906
901
|
|
|
907
902
|
return curNode;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "imapflow",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.11",
|
|
4
4
|
"description": "IMAP Client for Node",
|
|
5
5
|
"main": "lib/imap-flow.js",
|
|
6
6
|
"types": "lib/imap-flow.d.ts",
|
|
@@ -27,21 +27,16 @@
|
|
|
27
27
|
},
|
|
28
28
|
"homepage": "https://imapflow.com/",
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@
|
|
31
|
-
"@
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"@eslint/eslintrc": "3.3.3",
|
|
35
|
-
"@eslint/js": "9.39.2",
|
|
36
|
-
"@types/node": "25.2.1",
|
|
37
|
-
"c8": "10.1.3",
|
|
38
|
-
"eslint": "9.39.2",
|
|
30
|
+
"@eslint/js": "10.0.1",
|
|
31
|
+
"@types/node": "25.3.3",
|
|
32
|
+
"c8": "11.0.0",
|
|
33
|
+
"eslint": "10.0.2",
|
|
39
34
|
"eslint-config-nodemailer": "1.2.0",
|
|
40
35
|
"eslint-config-prettier": "10.1.8",
|
|
41
36
|
"grunt": "1.6.1",
|
|
42
37
|
"grunt-cli": "1.5.0",
|
|
43
38
|
"grunt-contrib-nodeunit": "5.0.0",
|
|
44
|
-
"grunt-eslint": "
|
|
39
|
+
"grunt-eslint": "26.0.0",
|
|
45
40
|
"prettier": "3.8.1",
|
|
46
41
|
"proxyquire": "^2.1.3",
|
|
47
42
|
"typescript": "5.9.3"
|
|
@@ -53,8 +48,8 @@
|
|
|
53
48
|
"libbase64": "1.3.0",
|
|
54
49
|
"libmime": "5.3.7",
|
|
55
50
|
"libqp": "2.1.1",
|
|
56
|
-
"nodemailer": "8.0.
|
|
57
|
-
"pino": "10.3.
|
|
51
|
+
"nodemailer": "8.0.1",
|
|
52
|
+
"pino": "10.3.1",
|
|
58
53
|
"socks": "2.8.7"
|
|
59
54
|
}
|
|
60
55
|
}
|
|
@@ -234,237 +234,6 @@ module.exports['Process correct TEXT with line count'] = test => {
|
|
|
234
234
|
null
|
|
235
235
|
];
|
|
236
236
|
|
|
237
|
-
attribute = [
|
|
238
|
-
[
|
|
239
|
-
[
|
|
240
|
-
{
|
|
241
|
-
type: 'STRING',
|
|
242
|
-
value: 'text'
|
|
243
|
-
},
|
|
244
|
-
{
|
|
245
|
-
type: 'STRING',
|
|
246
|
-
value: 'plain'
|
|
247
|
-
},
|
|
248
|
-
[
|
|
249
|
-
{
|
|
250
|
-
type: 'STRING',
|
|
251
|
-
value: 'charset'
|
|
252
|
-
},
|
|
253
|
-
{
|
|
254
|
-
type: 'STRING',
|
|
255
|
-
value: 'UTF-8'
|
|
256
|
-
}
|
|
257
|
-
],
|
|
258
|
-
null,
|
|
259
|
-
null,
|
|
260
|
-
{
|
|
261
|
-
type: 'STRING',
|
|
262
|
-
value: '7bit'
|
|
263
|
-
},
|
|
264
|
-
{
|
|
265
|
-
type: 'ATOM',
|
|
266
|
-
value: '23'
|
|
267
|
-
},
|
|
268
|
-
{
|
|
269
|
-
type: 'ATOM',
|
|
270
|
-
value: '1'
|
|
271
|
-
},
|
|
272
|
-
null,
|
|
273
|
-
null,
|
|
274
|
-
null,
|
|
275
|
-
null
|
|
276
|
-
],
|
|
277
|
-
[
|
|
278
|
-
{
|
|
279
|
-
type: 'STRING',
|
|
280
|
-
value: 'text'
|
|
281
|
-
},
|
|
282
|
-
{
|
|
283
|
-
type: 'STRING',
|
|
284
|
-
value: 'html'
|
|
285
|
-
},
|
|
286
|
-
[
|
|
287
|
-
{
|
|
288
|
-
type: 'STRING',
|
|
289
|
-
value: 'charset'
|
|
290
|
-
},
|
|
291
|
-
{
|
|
292
|
-
type: 'STRING',
|
|
293
|
-
value: 'UTF-8'
|
|
294
|
-
}
|
|
295
|
-
],
|
|
296
|
-
null,
|
|
297
|
-
null,
|
|
298
|
-
{
|
|
299
|
-
type: 'STRING',
|
|
300
|
-
value: '7bit'
|
|
301
|
-
},
|
|
302
|
-
{
|
|
303
|
-
type: 'ATOM',
|
|
304
|
-
value: '49'
|
|
305
|
-
},
|
|
306
|
-
{
|
|
307
|
-
type: 'ATOM',
|
|
308
|
-
value: '1'
|
|
309
|
-
},
|
|
310
|
-
null,
|
|
311
|
-
null,
|
|
312
|
-
null,
|
|
313
|
-
null
|
|
314
|
-
],
|
|
315
|
-
{
|
|
316
|
-
type: 'STRING',
|
|
317
|
-
value: 'alternative'
|
|
318
|
-
},
|
|
319
|
-
[
|
|
320
|
-
{
|
|
321
|
-
type: 'STRING',
|
|
322
|
-
value: 'boundary'
|
|
323
|
-
},
|
|
324
|
-
{
|
|
325
|
-
type: 'STRING',
|
|
326
|
-
value: '000000000000b56402062b1fba83'
|
|
327
|
-
}
|
|
328
|
-
],
|
|
329
|
-
null,
|
|
330
|
-
null,
|
|
331
|
-
null
|
|
332
|
-
],
|
|
333
|
-
[
|
|
334
|
-
{
|
|
335
|
-
type: 'STRING',
|
|
336
|
-
value: 'text'
|
|
337
|
-
},
|
|
338
|
-
{
|
|
339
|
-
type: 'STRING',
|
|
340
|
-
value: 'plain'
|
|
341
|
-
},
|
|
342
|
-
[
|
|
343
|
-
{
|
|
344
|
-
type: 'STRING',
|
|
345
|
-
value: 'charset'
|
|
346
|
-
},
|
|
347
|
-
{
|
|
348
|
-
type: 'STRING',
|
|
349
|
-
value: 'US-ASCII'
|
|
350
|
-
},
|
|
351
|
-
{
|
|
352
|
-
type: 'STRING',
|
|
353
|
-
value: 'name'
|
|
354
|
-
},
|
|
355
|
-
{
|
|
356
|
-
type: 'STRING',
|
|
357
|
-
value: 'log_imap_missing_attachments.txt'
|
|
358
|
-
}
|
|
359
|
-
],
|
|
360
|
-
{
|
|
361
|
-
type: 'STRING',
|
|
362
|
-
value: '<f_m5mnf91y1>'
|
|
363
|
-
},
|
|
364
|
-
null,
|
|
365
|
-
{
|
|
366
|
-
type: 'STRING',
|
|
367
|
-
value: 'base64'
|
|
368
|
-
},
|
|
369
|
-
{
|
|
370
|
-
type: 'ATOM',
|
|
371
|
-
value: '33954'
|
|
372
|
-
},
|
|
373
|
-
{
|
|
374
|
-
type: 'ATOM',
|
|
375
|
-
value: '435'
|
|
376
|
-
},
|
|
377
|
-
null,
|
|
378
|
-
[
|
|
379
|
-
{
|
|
380
|
-
type: 'STRING',
|
|
381
|
-
value: 'attachment'
|
|
382
|
-
},
|
|
383
|
-
[
|
|
384
|
-
{
|
|
385
|
-
type: 'STRING',
|
|
386
|
-
value: 'filename'
|
|
387
|
-
},
|
|
388
|
-
{
|
|
389
|
-
type: 'STRING',
|
|
390
|
-
value: 'log_imap_missing_attachments.txt'
|
|
391
|
-
}
|
|
392
|
-
]
|
|
393
|
-
],
|
|
394
|
-
null,
|
|
395
|
-
null
|
|
396
|
-
],
|
|
397
|
-
[
|
|
398
|
-
{
|
|
399
|
-
type: 'STRING',
|
|
400
|
-
value: 'image'
|
|
401
|
-
},
|
|
402
|
-
{
|
|
403
|
-
type: 'STRING',
|
|
404
|
-
value: 'jpeg'
|
|
405
|
-
},
|
|
406
|
-
[
|
|
407
|
-
{
|
|
408
|
-
type: 'STRING',
|
|
409
|
-
value: 'name'
|
|
410
|
-
},
|
|
411
|
-
{
|
|
412
|
-
type: 'STRING',
|
|
413
|
-
value: 'img2.jpeg'
|
|
414
|
-
}
|
|
415
|
-
],
|
|
416
|
-
{
|
|
417
|
-
type: 'STRING',
|
|
418
|
-
value: '<f_m5mnf4c70>'
|
|
419
|
-
},
|
|
420
|
-
null,
|
|
421
|
-
{
|
|
422
|
-
type: 'STRING',
|
|
423
|
-
value: 'base64'
|
|
424
|
-
},
|
|
425
|
-
{
|
|
426
|
-
type: 'ATOM',
|
|
427
|
-
value: '3776960'
|
|
428
|
-
},
|
|
429
|
-
null,
|
|
430
|
-
[
|
|
431
|
-
{
|
|
432
|
-
type: 'STRING',
|
|
433
|
-
value: 'attachment'
|
|
434
|
-
},
|
|
435
|
-
[
|
|
436
|
-
{
|
|
437
|
-
type: 'STRING',
|
|
438
|
-
value: 'filename'
|
|
439
|
-
},
|
|
440
|
-
{
|
|
441
|
-
type: 'STRING',
|
|
442
|
-
value: 'img2.jpeg'
|
|
443
|
-
}
|
|
444
|
-
]
|
|
445
|
-
],
|
|
446
|
-
null,
|
|
447
|
-
null
|
|
448
|
-
],
|
|
449
|
-
{
|
|
450
|
-
type: 'STRING',
|
|
451
|
-
value: 'mixed'
|
|
452
|
-
},
|
|
453
|
-
[
|
|
454
|
-
{
|
|
455
|
-
type: 'STRING',
|
|
456
|
-
value: 'boundary'
|
|
457
|
-
},
|
|
458
|
-
{
|
|
459
|
-
type: 'STRING',
|
|
460
|
-
value: '000000000000b56402062b1fba85'
|
|
461
|
-
}
|
|
462
|
-
],
|
|
463
|
-
null,
|
|
464
|
-
null,
|
|
465
|
-
null
|
|
466
|
-
];
|
|
467
|
-
|
|
468
237
|
let bodyStruct = parseBodystructure(attribute);
|
|
469
238
|
|
|
470
239
|
test.deepEqual(bodyStruct, {
|
|
@@ -1464,3 +1464,108 @@ module.exports['Connection Edge: connect throws if called twice'] = async test =
|
|
|
1464
1464
|
|
|
1465
1465
|
test.done();
|
|
1466
1466
|
};
|
|
1467
|
+
|
|
1468
|
+
// Helper to create a mock client with compression enabled
|
|
1469
|
+
async function setupCompressedClient() {
|
|
1470
|
+
let client = new ImapFlow({
|
|
1471
|
+
host: 'imap.example.com',
|
|
1472
|
+
port: 993,
|
|
1473
|
+
auth: { user: 'test', pass: 'test' }
|
|
1474
|
+
});
|
|
1475
|
+
|
|
1476
|
+
let mockSocket = new EventEmitter();
|
|
1477
|
+
mockSocket.pipe = dest => dest;
|
|
1478
|
+
mockSocket.unpipe = () => {};
|
|
1479
|
+
mockSocket.destroy = () => {};
|
|
1480
|
+
mockSocket.destroyed = false;
|
|
1481
|
+
client.socket = mockSocket;
|
|
1482
|
+
client.streamer = new EventEmitter();
|
|
1483
|
+
|
|
1484
|
+
client.run = async command => {
|
|
1485
|
+
if (command === 'COMPRESS') {
|
|
1486
|
+
return true;
|
|
1487
|
+
}
|
|
1488
|
+
};
|
|
1489
|
+
|
|
1490
|
+
await client.compress();
|
|
1491
|
+
return { client, mockSocket };
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
module.exports['Connection Edge: compress writeSocket error forwarded to socket when live'] = async test => {
|
|
1495
|
+
let { client, mockSocket } = await setupCompressedClient();
|
|
1496
|
+
|
|
1497
|
+
let errorReceived = false;
|
|
1498
|
+
mockSocket.on('error', err => {
|
|
1499
|
+
errorReceived = true;
|
|
1500
|
+
test.equal(err.message, 'writeSocket error');
|
|
1501
|
+
});
|
|
1502
|
+
|
|
1503
|
+
client.writeSocket.emit('error', new Error('writeSocket error'));
|
|
1504
|
+
|
|
1505
|
+
test.ok(errorReceived, 'Error should be forwarded to socket');
|
|
1506
|
+
test.done();
|
|
1507
|
+
};
|
|
1508
|
+
|
|
1509
|
+
module.exports['Connection Edge: compress _deflate error forwarded to socket when live'] = async test => {
|
|
1510
|
+
let { client, mockSocket } = await setupCompressedClient();
|
|
1511
|
+
|
|
1512
|
+
let errorReceived = false;
|
|
1513
|
+
mockSocket.on('error', err => {
|
|
1514
|
+
errorReceived = true;
|
|
1515
|
+
test.equal(err.message, 'deflate error');
|
|
1516
|
+
});
|
|
1517
|
+
|
|
1518
|
+
client._deflate.emit('error', new Error('deflate error'));
|
|
1519
|
+
|
|
1520
|
+
test.ok(errorReceived, 'Error should be forwarded to socket');
|
|
1521
|
+
test.done();
|
|
1522
|
+
};
|
|
1523
|
+
|
|
1524
|
+
module.exports['Connection Edge: compress readable after close does not crash'] = async test => {
|
|
1525
|
+
let { client } = await setupCompressedClient();
|
|
1526
|
+
|
|
1527
|
+
// Save reference before close() nulls it
|
|
1528
|
+
let writeSocket = client.writeSocket;
|
|
1529
|
+
|
|
1530
|
+
client.close();
|
|
1531
|
+
|
|
1532
|
+
// Emit readable on the saved reference after close has nulled this.writeSocket
|
|
1533
|
+
// This should not throw (guards at lines 1048 and 1021)
|
|
1534
|
+
test.doesNotThrow(() => {
|
|
1535
|
+
writeSocket.emit('readable');
|
|
1536
|
+
});
|
|
1537
|
+
|
|
1538
|
+
test.done();
|
|
1539
|
+
};
|
|
1540
|
+
|
|
1541
|
+
module.exports['Connection Edge: compress writeSocket error after close does not crash'] = async test => {
|
|
1542
|
+
let { client } = await setupCompressedClient();
|
|
1543
|
+
|
|
1544
|
+
let writeSocket = client.writeSocket;
|
|
1545
|
+
|
|
1546
|
+
client.close();
|
|
1547
|
+
|
|
1548
|
+
// Emit error on saved writeSocket after close has nulled this.socket
|
|
1549
|
+
// This should not throw (guard at lines 1053-1055)
|
|
1550
|
+
test.doesNotThrow(() => {
|
|
1551
|
+
writeSocket.emit('error', new Error('late writeSocket error'));
|
|
1552
|
+
});
|
|
1553
|
+
|
|
1554
|
+
test.done();
|
|
1555
|
+
};
|
|
1556
|
+
|
|
1557
|
+
module.exports['Connection Edge: compress _deflate error after close does not crash'] = async test => {
|
|
1558
|
+
let { client } = await setupCompressedClient();
|
|
1559
|
+
|
|
1560
|
+
let deflate = client._deflate;
|
|
1561
|
+
|
|
1562
|
+
client.close();
|
|
1563
|
+
|
|
1564
|
+
// Emit error on saved _deflate after close has nulled this.socket
|
|
1565
|
+
// This should not throw (guard at lines 1060-1062)
|
|
1566
|
+
test.doesNotThrow(() => {
|
|
1567
|
+
deflate.emit('error', new Error('late deflate error'));
|
|
1568
|
+
});
|
|
1569
|
+
|
|
1570
|
+
test.done();
|
|
1571
|
+
};
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { ImapFlow } = require('../lib/imap-flow');
|
|
4
|
+
|
|
5
|
+
// Helper: create a minimal mock context with the fetch() generator bound to it.
|
|
6
|
+
// The `run` override controls how untagged FETCH responses are delivered.
|
|
7
|
+
const createFetchContext = runOverride => ({
|
|
8
|
+
mailbox: { path: 'INBOX', exists: 10 },
|
|
9
|
+
isClosed: false,
|
|
10
|
+
socket: { destroyed: false },
|
|
11
|
+
resolveRange: async range => range,
|
|
12
|
+
run: runOverride
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// Helper: invoke the fetch generator on a mock context
|
|
16
|
+
function callFetch(ctx, range, query, options) {
|
|
17
|
+
return ImapFlow.prototype.fetch.call(ctx, range, query, options);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Helper: create a mock `run` that delivers `count` untagged FETCH responses,
|
|
21
|
+
// each with a `next` callback for backpressure. Returns a tracking object.
|
|
22
|
+
function createMockRun(count) {
|
|
23
|
+
const tracker = {
|
|
24
|
+
nextCalls: [], // records which message indices had next() called
|
|
25
|
+
onUntaggedFetch: null,
|
|
26
|
+
runResolved: false
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const run = async (_cmd, _range, _query, opts) => {
|
|
30
|
+
tracker.onUntaggedFetch = opts.onUntaggedFetch;
|
|
31
|
+
|
|
32
|
+
for (let i = 0; i < count; i++) {
|
|
33
|
+
await new Promise(resolve => {
|
|
34
|
+
opts.onUntaggedFetch({ seq: i + 1, uid: 100 + i }, () => {
|
|
35
|
+
tracker.nextCalls.push(i);
|
|
36
|
+
resolve();
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
tracker.runResolved = true;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return { tracker, run };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Test: early break releases backpressure
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
module.exports['fetch generator: early break releases backpressure'] = async test => {
|
|
50
|
+
const { tracker, run } = createMockRun(5);
|
|
51
|
+
const ctx = createFetchContext(run);
|
|
52
|
+
|
|
53
|
+
const gen = callFetch(ctx, '1:5', { uid: true });
|
|
54
|
+
// Consume only the first message and break
|
|
55
|
+
for await (let msg of gen) {
|
|
56
|
+
test.ok(msg.seq === 1, 'should receive the first message');
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Allow microtasks to flush (the finally block runs synchronously with
|
|
61
|
+
// generator return, but the remaining run() deliveries are async)
|
|
62
|
+
await new Promise(r => setTimeout(r, 50));
|
|
63
|
+
|
|
64
|
+
// The first message had next() called by the yield loop (res.next()).
|
|
65
|
+
// Remaining queued/future messages should have next() called by the
|
|
66
|
+
// finally block or the aborted guard.
|
|
67
|
+
test.equal(tracker.nextCalls.length, 5, 'all 5 messages should have next() called');
|
|
68
|
+
test.done();
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// Test: subsequent operations work after early exit
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
module.exports['fetch generator: connection usable after early break'] = async test => {
|
|
75
|
+
let runCallCount = 0;
|
|
76
|
+
|
|
77
|
+
const run = async (_cmd, _range, _query, opts) => {
|
|
78
|
+
runCallCount++;
|
|
79
|
+
// Deliver 3 messages
|
|
80
|
+
for (let i = 0; i < 3; i++) {
|
|
81
|
+
await new Promise(resolve => {
|
|
82
|
+
opts.onUntaggedFetch({ seq: i + 1, uid: 200 + i }, () => resolve());
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const ctx = createFetchContext(run);
|
|
88
|
+
|
|
89
|
+
// First fetch: break early
|
|
90
|
+
const gen1 = callFetch(ctx, '1:3', { uid: true });
|
|
91
|
+
for await (let msg of gen1) {
|
|
92
|
+
test.ok(msg.seq === 1);
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
await new Promise(r => setTimeout(r, 50));
|
|
97
|
+
|
|
98
|
+
// Second fetch: should work (run gets called again)
|
|
99
|
+
const gen2 = callFetch(ctx, '1:3', { uid: true });
|
|
100
|
+
let secondFetchMessages = [];
|
|
101
|
+
for await (let msg of gen2) {
|
|
102
|
+
secondFetchMessages.push(msg);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
test.ok(runCallCount === 2, 'run was called for both fetch operations');
|
|
106
|
+
test.ok(secondFetchMessages.length === 3, 'second fetch received all 3 messages');
|
|
107
|
+
test.done();
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
// Test: normal full iteration works correctly
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
module.exports['fetch generator: normal full iteration receives all messages'] = async test => {
|
|
114
|
+
const { tracker, run } = createMockRun(4);
|
|
115
|
+
const ctx = createFetchContext(run);
|
|
116
|
+
|
|
117
|
+
let messages = [];
|
|
118
|
+
for await (let msg of callFetch(ctx, '1:4', { uid: true })) {
|
|
119
|
+
messages.push(msg);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
test.equal(messages.length, 4, 'should receive all 4 messages');
|
|
123
|
+
test.equal(messages[0].seq, 1);
|
|
124
|
+
test.equal(messages[3].seq, 4);
|
|
125
|
+
test.equal(tracker.nextCalls.length, 4, 'all next() callbacks were called');
|
|
126
|
+
test.done();
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
// Test: error thrown in loop body releases backpressure
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
module.exports['fetch generator: error in loop body releases backpressure'] = async test => {
|
|
133
|
+
const { tracker, run } = createMockRun(5);
|
|
134
|
+
const ctx = createFetchContext(run);
|
|
135
|
+
|
|
136
|
+
const gen = callFetch(ctx, '1:5', { uid: true });
|
|
137
|
+
|
|
138
|
+
let caught = false;
|
|
139
|
+
try {
|
|
140
|
+
for await (let msg of gen) {
|
|
141
|
+
if (msg.seq === 2) {
|
|
142
|
+
throw new Error('intentional test error');
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
} catch (err) {
|
|
146
|
+
caught = true;
|
|
147
|
+
test.equal(err.message, 'intentional test error');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
await new Promise(r => setTimeout(r, 50));
|
|
151
|
+
|
|
152
|
+
test.ok(caught, 'error was caught');
|
|
153
|
+
// Messages 0 and 1 had next() called by the yield loop. The rest should
|
|
154
|
+
// be drained by the finally block or the aborted guard.
|
|
155
|
+
test.equal(tracker.nextCalls.length, 5, 'all 5 messages should have next() called');
|
|
156
|
+
test.done();
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// Test: zero messages from server
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
module.exports['fetch generator: zero messages yields nothing'] = async test => {
|
|
163
|
+
const { tracker, run } = createMockRun(0);
|
|
164
|
+
const ctx = createFetchContext(run);
|
|
165
|
+
|
|
166
|
+
let messages = [];
|
|
167
|
+
for await (let msg of callFetch(ctx, '1:*', { uid: true })) {
|
|
168
|
+
messages.push(msg);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
test.equal(messages.length, 0, 'no messages should be yielded');
|
|
172
|
+
test.equal(tracker.nextCalls.length, 0, 'no next() calls needed');
|
|
173
|
+
test.ok(tracker.runResolved, 'run should have resolved');
|
|
174
|
+
test.done();
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
// Test: connection closed during iteration throws and releases backpressure
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
module.exports['fetch generator: connection closed mid-iteration throws and cleans up'] = async test => {
|
|
181
|
+
const tracker = {
|
|
182
|
+
nextCalls: []
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const run = async (_cmd, _range, _query, opts) => {
|
|
186
|
+
for (let i = 0; i < 3; i++) {
|
|
187
|
+
await new Promise(resolve => {
|
|
188
|
+
opts.onUntaggedFetch({ seq: i + 1, uid: 300 + i }, () => {
|
|
189
|
+
tracker.nextCalls.push(i);
|
|
190
|
+
resolve();
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const ctx = createFetchContext(run);
|
|
197
|
+
|
|
198
|
+
let caught = false;
|
|
199
|
+
try {
|
|
200
|
+
for await (let msg of callFetch(ctx, '1:3', { uid: true })) {
|
|
201
|
+
if (msg.seq === 1) {
|
|
202
|
+
// Simulate connection closing after first message
|
|
203
|
+
ctx.isClosed = true;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
} catch (err) {
|
|
207
|
+
caught = true;
|
|
208
|
+
test.equal(err.code, 'EConnectionClosed', 'should throw EConnectionClosed');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
await new Promise(r => setTimeout(r, 50));
|
|
212
|
+
|
|
213
|
+
test.ok(caught, 'error was caught');
|
|
214
|
+
test.equal(tracker.nextCalls.length, 3, 'all next() callbacks should be called for cleanup');
|
|
215
|
+
test.done();
|
|
216
|
+
};
|
package/.babelrc
DELETED
package/.eslintrc
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"rules": {
|
|
3
|
-
"no-await-in-loop": 0,
|
|
4
|
-
"require-atomic-updates": 0
|
|
5
|
-
},
|
|
6
|
-
"globals": {
|
|
7
|
-
"BigInt": true
|
|
8
|
-
},
|
|
9
|
-
"extends": ["nodemailer", "prettier"],
|
|
10
|
-
"parser": "@babel/eslint-parser",
|
|
11
|
-
"parserOptions": {
|
|
12
|
-
"ecmaVersion": 2020,
|
|
13
|
-
"sourceType": "script"
|
|
14
|
-
},
|
|
15
|
-
"plugins": ["@babel"]
|
|
16
|
-
}
|