imapflow 1.3.5 → 1.3.7
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/.github/codeql/codeql-config.yml +12 -0
- package/.github/workflows/codeql.yml +102 -0
- package/.github/workflows/stale.yml +5 -0
- package/.github/workflows/test.yml +3 -0
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +14 -0
- package/lib/commands/authenticate.js +5 -5
- package/lib/commands/fetch.js +1 -0
- package/lib/commands/logout.js +1 -0
- package/lib/commands/select.js +2 -1
- package/lib/commands/starttls.js +3 -0
- package/lib/commands/store.js +1 -0
- package/lib/handler/imap-stream.js +23 -6
- package/lib/handler/token-parser.js +0 -5
- package/lib/imap-flow.d.ts +5 -0
- package/lib/imap-flow.js +205 -29
- package/lib/proxy-connection.js +26 -1
- package/lib/tools.js +12 -0
- package/package.json +1 -1
- package/test/commands-branches-test.js +1068 -0
- package/test/connection-edge-cases-test.js +138 -1
- package/test/fixtures/test-tls.js +8 -0
- package/test/handler-branches-test.js +334 -0
- package/test/imap-compiler-test.js +47 -0
- package/test/imap-flow-compress-test.js +154 -0
- package/test/imap-flow-coverage-test.js +609 -0
- package/test/imap-flow-fetch-download-test.js +801 -0
- package/test/imap-flow-internals-test.js +450 -0
- package/test/imap-flow-methods-test.js +738 -0
- package/test/imap-flow-proxy-paths-test.js +215 -0
- package/test/imap-flow-secure-test.js +327 -0
- package/test/imap-flow-server-test.js +1046 -0
- package/test/imap-formal-syntax-test.js +18 -0
- package/test/imap-parser-test.js +52 -0
- package/test/imap-stream-edge-cases-test.js +122 -0
- package/test/jp-decoder-test.js +48 -0
- package/test/limited-passthrough-test.js +17 -0
- package/test/proxy-connection-test.js +86 -0
- package/test/reliability-improvements-test.js +88 -0
- package/test/search-compiler-test.js +31 -0
- package/test/search-test.js +61 -0
- package/test/starttls-injection-test.js +181 -0
- package/test/token-parser-test.js +64 -0
- package/test/tools-test.js +360 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
name: "ImapFlow CodeQL config"
|
|
2
|
+
|
|
3
|
+
# Exclude code that is not part of the published library runtime from analysis.
|
|
4
|
+
# These paths generate only false-positive noise:
|
|
5
|
+
# - test fixtures intentionally feed malformed/hostile input and exercise
|
|
6
|
+
# edge cases that are not representative of production usage
|
|
7
|
+
# - the example scripts hardcode demo credentials and connection details for
|
|
8
|
+
# local experimentation and are not maintained as production code
|
|
9
|
+
paths-ignore:
|
|
10
|
+
- test
|
|
11
|
+
- '**/test/**'
|
|
12
|
+
- examples
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# For most projects, this workflow file will not need changing; you simply need
|
|
2
|
+
# to commit it to your repository.
|
|
3
|
+
#
|
|
4
|
+
# You may wish to alter this file to override the set of languages analyzed,
|
|
5
|
+
# or to provide custom queries or build logic.
|
|
6
|
+
#
|
|
7
|
+
# ******** NOTE ********
|
|
8
|
+
# We have attempted to detect the languages in your repository. Please check
|
|
9
|
+
# the `language` matrix defined below to confirm you have the correct set of
|
|
10
|
+
# supported CodeQL languages.
|
|
11
|
+
#
|
|
12
|
+
name: "CodeQL Advanced"
|
|
13
|
+
|
|
14
|
+
on:
|
|
15
|
+
push:
|
|
16
|
+
branches: [ "master" ]
|
|
17
|
+
pull_request:
|
|
18
|
+
branches: [ "master" ]
|
|
19
|
+
schedule:
|
|
20
|
+
- cron: '40 17 * * 6'
|
|
21
|
+
|
|
22
|
+
jobs:
|
|
23
|
+
analyze:
|
|
24
|
+
name: Analyze (${{ matrix.language }})
|
|
25
|
+
# Runner size impacts CodeQL analysis time. To learn more, please see:
|
|
26
|
+
# - https://gh.io/recommended-hardware-resources-for-running-codeql
|
|
27
|
+
# - https://gh.io/supported-runners-and-hardware-resources
|
|
28
|
+
# - https://gh.io/using-larger-runners (GitHub.com only)
|
|
29
|
+
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
|
|
30
|
+
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
|
|
31
|
+
permissions:
|
|
32
|
+
# required for all workflows
|
|
33
|
+
security-events: write
|
|
34
|
+
|
|
35
|
+
# required to fetch internal or private CodeQL packs
|
|
36
|
+
packages: read
|
|
37
|
+
|
|
38
|
+
# only required for workflows in private repositories
|
|
39
|
+
actions: read
|
|
40
|
+
contents: read
|
|
41
|
+
|
|
42
|
+
strategy:
|
|
43
|
+
fail-fast: false
|
|
44
|
+
matrix:
|
|
45
|
+
include:
|
|
46
|
+
- language: actions
|
|
47
|
+
build-mode: none
|
|
48
|
+
- language: javascript-typescript
|
|
49
|
+
build-mode: none
|
|
50
|
+
# CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift'
|
|
51
|
+
# Use `c-cpp` to analyze code written in C, C++ or both
|
|
52
|
+
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
|
|
53
|
+
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
|
|
54
|
+
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
|
|
55
|
+
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
|
|
56
|
+
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
|
|
57
|
+
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
|
|
58
|
+
steps:
|
|
59
|
+
- name: Checkout repository
|
|
60
|
+
uses: actions/checkout@v4
|
|
61
|
+
|
|
62
|
+
# Add any setup steps before running the `github/codeql-action/init` action.
|
|
63
|
+
# This includes steps like installing compilers or runtimes (`actions/setup-node`
|
|
64
|
+
# or others). This is typically only required for manual builds.
|
|
65
|
+
# - name: Setup runtime (example)
|
|
66
|
+
# uses: actions/setup-example@v1
|
|
67
|
+
|
|
68
|
+
# Initializes the CodeQL tools for scanning.
|
|
69
|
+
- name: Initialize CodeQL
|
|
70
|
+
uses: github/codeql-action/init@v4
|
|
71
|
+
with:
|
|
72
|
+
languages: ${{ matrix.language }}
|
|
73
|
+
build-mode: ${{ matrix.build-mode }}
|
|
74
|
+
config-file: ./.github/codeql/codeql-config.yml
|
|
75
|
+
# If you wish to specify custom queries, you can do so here or in a config file.
|
|
76
|
+
# By default, queries listed here will override any specified in a config file.
|
|
77
|
+
# Prefix the list here with "+" to use these queries and those in the config file.
|
|
78
|
+
|
|
79
|
+
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
|
80
|
+
# queries: security-extended,security-and-quality
|
|
81
|
+
|
|
82
|
+
# If the analyze step fails for one of the languages you are analyzing with
|
|
83
|
+
# "We were unable to automatically build your code", modify the matrix above
|
|
84
|
+
# to set the build mode to "manual" for that language. Then modify this step
|
|
85
|
+
# to build your code.
|
|
86
|
+
# ℹ️ Command-line programs to run using the OS shell.
|
|
87
|
+
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
|
88
|
+
- name: Run manual build steps
|
|
89
|
+
if: matrix.build-mode == 'manual'
|
|
90
|
+
shell: bash
|
|
91
|
+
run: |
|
|
92
|
+
echo 'If you are using a "manual" build mode for one or more of the' \
|
|
93
|
+
'languages you are analyzing, replace this with the commands to build' \
|
|
94
|
+
'your code, for example:'
|
|
95
|
+
echo ' make bootstrap'
|
|
96
|
+
echo ' make release'
|
|
97
|
+
exit 1
|
|
98
|
+
|
|
99
|
+
- name: Perform CodeQL Analysis
|
|
100
|
+
uses: github/codeql-action/analyze@v4
|
|
101
|
+
with:
|
|
102
|
+
category: "/language:${{matrix.language}}"
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.3.7](https://github.com/postalsys/imapflow/compare/v1.3.6...v1.3.7) (2026-06-08)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* harden FLAGS guard and bring lib to full test coverage ([6666bec](https://github.com/postalsys/imapflow/commit/6666bec7f82072e5aa8a1e63736e35d5c6644d4c))
|
|
9
|
+
|
|
10
|
+
## [1.3.6](https://github.com/postalsys/imapflow/compare/v1.3.5...v1.3.6) (2026-06-05)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* harden STARTTLS upgrade, literal bounds, and connection error paths ([35eca81](https://github.com/postalsys/imapflow/commit/35eca81075401e89f0b3aca84e26c41710a62c28))
|
|
16
|
+
|
|
3
17
|
## [1.3.5](https://github.com/postalsys/imapflow/compare/v1.3.4...v1.3.5) (2026-06-01)
|
|
4
18
|
|
|
5
19
|
|
|
@@ -7,7 +7,7 @@ const { getStatusCode, getErrorText } = require('../tools.js');
|
|
|
7
7
|
*
|
|
8
8
|
* @param {Error} err - The original authentication error
|
|
9
9
|
* @param {Object} [errorResponse] - Optional OAuth error response from the server
|
|
10
|
-
* @
|
|
10
|
+
* @returns {Error} The enriched error; the caller is expected to throw it
|
|
11
11
|
*/
|
|
12
12
|
async function handleAuthError(err, errorResponse) {
|
|
13
13
|
let errorCode = getStatusCode(err.response);
|
|
@@ -19,7 +19,7 @@ async function handleAuthError(err, errorResponse) {
|
|
|
19
19
|
if (errorResponse) {
|
|
20
20
|
err.oauthError = errorResponse;
|
|
21
21
|
}
|
|
22
|
-
|
|
22
|
+
return err;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
/**
|
|
@@ -84,7 +84,7 @@ async function authOauth(connection, username, accessToken) {
|
|
|
84
84
|
|
|
85
85
|
return username;
|
|
86
86
|
} catch (err) {
|
|
87
|
-
await handleAuthError(err, errorResponse);
|
|
87
|
+
throw await handleAuthError(err, errorResponse);
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
|
|
@@ -132,7 +132,7 @@ async function authLogin(connection, username, password) {
|
|
|
132
132
|
|
|
133
133
|
return username;
|
|
134
134
|
} catch (err) {
|
|
135
|
-
await handleAuthError(err, errorResponse);
|
|
135
|
+
throw await handleAuthError(err, errorResponse);
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
138
|
|
|
@@ -169,7 +169,7 @@ async function authPlain(connection, username, password, authzid) {
|
|
|
169
169
|
// Return the identity we're authorized as (authzid if provided, otherwise username)
|
|
170
170
|
return authzid || username;
|
|
171
171
|
} catch (err) {
|
|
172
|
-
await handleAuthError(err, errorResponse);
|
|
172
|
+
throw await handleAuthError(err, errorResponse);
|
|
173
173
|
}
|
|
174
174
|
}
|
|
175
175
|
|
package/lib/commands/fetch.js
CHANGED
|
@@ -41,6 +41,7 @@ module.exports = async (connection, range, query, options) => {
|
|
|
41
41
|
|
|
42
42
|
let response;
|
|
43
43
|
try {
|
|
44
|
+
/* c8 ignore next */ // range is guaranteed truthy by the early-return guard above, so the '*' fallback is unreachable
|
|
44
45
|
let attributes = [{ type: 'SEQUENCE', value: (range || '*').toString() }];
|
|
45
46
|
|
|
46
47
|
let queryStructure = [];
|
package/lib/commands/logout.js
CHANGED
|
@@ -30,6 +30,7 @@ module.exports = async connection => {
|
|
|
30
30
|
}
|
|
31
31
|
connection.log.warn({ err, cid: connection.id });
|
|
32
32
|
return false;
|
|
33
|
+
/* c8 ignore next */ // the catch above is exhaustive (never re-throws), so finally is only ever reached via normal completion
|
|
33
34
|
} finally {
|
|
34
35
|
// Set state to LOGOUT before closing to prevent any further commands from
|
|
35
36
|
// being queued. The socket is closed unconditionally in this finally block
|
package/lib/commands/select.js
CHANGED
|
@@ -70,6 +70,7 @@ module.exports = async (connection, path, options) => {
|
|
|
70
70
|
// send as quoted STRING to avoid parser issues with the ampersand.
|
|
71
71
|
let selectCommand = {
|
|
72
72
|
command: !options.readOnly ? 'SELECT' : 'EXAMINE',
|
|
73
|
+
/* c8 ignore next */ // extraArgs is always initialised to an array, so the [] fallback is unreachable
|
|
73
74
|
arguments: [{ type: encodedPath.indexOf('&') >= 0 ? 'STRING' : 'ATOM', value: encodedPath }].concat(extraArgs || [])
|
|
74
75
|
};
|
|
75
76
|
|
|
@@ -164,7 +165,7 @@ module.exports = async (connection, path, options) => {
|
|
|
164
165
|
// Untagged FLAGS response lists all flags defined for this mailbox
|
|
165
166
|
// (both system flags and custom flags). Example: * FLAGS (\Seen \Answered \Flagged)
|
|
166
167
|
FLAGS: async untagged => {
|
|
167
|
-
if (!untagged.attributes ||
|
|
168
|
+
if (!untagged.attributes || !untagged.attributes.length || !Array.isArray(untagged.attributes[0])) {
|
|
168
169
|
return;
|
|
169
170
|
}
|
|
170
171
|
let flags = untagged.attributes[0].map(flag => (typeof flag.value === 'string' ? flag.value : false)).filter(flag => flag);
|
package/lib/commands/starttls.js
CHANGED
|
@@ -15,6 +15,9 @@ module.exports = async connection => {
|
|
|
15
15
|
let response;
|
|
16
16
|
try {
|
|
17
17
|
response = await connection.exec('STARTTLS');
|
|
18
|
+
// Whether the server sent anything after the STARTTLS OK and before the TLS
|
|
19
|
+
// handshake. upgradeToSTARTTLS() uses this to reject a plaintext injection.
|
|
20
|
+
connection._starttlsHadTrailingData = !!(response && response.hasTrailingData);
|
|
18
21
|
response.next();
|
|
19
22
|
return true;
|
|
20
23
|
} catch (err) {
|
package/lib/commands/store.js
CHANGED
|
@@ -22,6 +22,7 @@ module.exports = async (connection, range, flags, options) => {
|
|
|
22
22
|
return false;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/* c8 ignore next */ // options.useLabels is dereferenced in the guard above, so options is always defined here
|
|
25
26
|
options = options || {};
|
|
26
27
|
|
|
27
28
|
// Build the IMAP STORE operation name. The format is:
|
|
@@ -43,6 +43,9 @@ class ImapStream extends Transform {
|
|
|
43
43
|
* line (a response without a literal). Defaults to MAX_LITERAL_SIZE (1GB). Guards against a
|
|
44
44
|
* malicious or broken server that never sends a line terminator, which would otherwise grow
|
|
45
45
|
* the internal line buffer without bound.
|
|
46
|
+
* @param {number} [options.maxLiteralSize] - Maximum allowed size (in bytes) of a single
|
|
47
|
+
* literal block. Defaults to MAX_LITERAL_SIZE (1GB). Lower it to bound peak memory
|
|
48
|
+
* allocation against a malicious or broken server announcing an oversized literal.
|
|
46
49
|
*/
|
|
47
50
|
constructor(options) {
|
|
48
51
|
super({
|
|
@@ -65,8 +68,16 @@ class ImapStream extends Transform {
|
|
|
65
68
|
this.readBytesCounter = 0;
|
|
66
69
|
|
|
67
70
|
// Maximum length of a single line (response without a literal). Bounds the line buffer
|
|
68
|
-
// so a server that never sends a line terminator cannot exhaust memory.
|
|
69
|
-
|
|
71
|
+
// so a server that never sends a line terminator cannot exhaust memory. A non-negative
|
|
72
|
+
// integer is honored as-is (including 0); anything else falls back to the default, so an
|
|
73
|
+
// explicit 0 is not silently swallowed into the 1GB default the way `|| MAX_LINE_SIZE` was.
|
|
74
|
+
this.maxLineLength = Number.isInteger(this.options.maxLineLength) && this.options.maxLineLength >= 0 ? this.options.maxLineLength : MAX_LINE_SIZE;
|
|
75
|
+
|
|
76
|
+
// Maximum size of a single literal block. Bounds peak memory allocation so a server
|
|
77
|
+
// announcing an oversized literal cannot exhaust memory. As above, a non-negative integer
|
|
78
|
+
// (including an explicit 0, meaning "reject all non-empty literals") is honored as-is.
|
|
79
|
+
this.maxLiteralSize =
|
|
80
|
+
Number.isInteger(this.options.maxLiteralSize) && this.options.maxLiteralSize >= 0 ? this.options.maxLiteralSize : MAX_LITERAL_SIZE;
|
|
70
81
|
|
|
71
82
|
this.state = LINE;
|
|
72
83
|
this.literalWaiting = 0;
|
|
@@ -125,11 +136,11 @@ class ImapStream extends Transform {
|
|
|
125
136
|
if (c === CURLY_OPEN && numBytes.length) {
|
|
126
137
|
const literalSize = Number(Buffer.from(numBytes).toString());
|
|
127
138
|
|
|
128
|
-
if (literalSize >
|
|
129
|
-
const err = new Error(`Literal size ${literalSize} exceeds maximum allowed size of ${
|
|
139
|
+
if (literalSize > this.maxLiteralSize) {
|
|
140
|
+
const err = new Error(`Literal size ${literalSize} exceeds maximum allowed size of ${this.maxLiteralSize} bytes`);
|
|
130
141
|
err.code = 'LiteralTooLarge';
|
|
131
142
|
err.literalSize = literalSize;
|
|
132
|
-
err.maxSize =
|
|
143
|
+
err.maxSize = this.maxLiteralSize;
|
|
133
144
|
this.emit('error', err);
|
|
134
145
|
return false;
|
|
135
146
|
}
|
|
@@ -197,8 +208,14 @@ class ImapStream extends Transform {
|
|
|
197
208
|
}
|
|
198
209
|
|
|
199
210
|
if (payload.length) {
|
|
211
|
+
// Whether more buffered input already followed this command on the
|
|
212
|
+
// wire — more bytes in this chunk or another queued chunk. Captured
|
|
213
|
+
// per emitted command (immutable on the pushed object) so a later
|
|
214
|
+
// command cannot overwrite it; consumers that care about pipelining
|
|
215
|
+
// boundaries can read it from the pushed object.
|
|
216
|
+
let trailingAfterLine = lineStart < chunk.length || this.inputQueue.length > 0;
|
|
200
217
|
await new Promise(resolve => {
|
|
201
|
-
this.push({ payload, literals, next: resolve });
|
|
218
|
+
this.push({ payload, literals, next: resolve, trailingAfterLine });
|
|
202
219
|
});
|
|
203
220
|
}
|
|
204
221
|
}
|
|
@@ -10,7 +10,6 @@ const STATE_NORMAL = 0x003;
|
|
|
10
10
|
const STATE_PARTIAL = 0x004;
|
|
11
11
|
const STATE_SEQUENCE = 0x005;
|
|
12
12
|
const STATE_STRING = 0x006;
|
|
13
|
-
const STATE_TEXT = 0x007;
|
|
14
13
|
|
|
15
14
|
const RE_DIGITS = /^\d+$/;
|
|
16
15
|
const RE_SINGLE_DIGIT = /^\d$/;
|
|
@@ -706,10 +705,6 @@ class TokenParser {
|
|
|
706
705
|
|
|
707
706
|
this.currentNode.value += chr;
|
|
708
707
|
break;
|
|
709
|
-
|
|
710
|
-
case STATE_TEXT:
|
|
711
|
-
this.currentNode.value += chr;
|
|
712
|
-
break;
|
|
713
708
|
}
|
|
714
709
|
}
|
|
715
710
|
}
|
package/lib/imap-flow.d.ts
CHANGED
|
@@ -66,6 +66,11 @@ export interface ImapFlowOptions {
|
|
|
66
66
|
* 1GB.
|
|
67
67
|
*/
|
|
68
68
|
maxLineLength?: number;
|
|
69
|
+
/**
|
|
70
|
+
* Maximum allowed size in bytes of a single IMAP literal block. Bounds peak memory allocation
|
|
71
|
+
* against a malicious or broken server announcing an oversized literal. Defaults to 1GB.
|
|
72
|
+
*/
|
|
73
|
+
maxLiteralSize?: number;
|
|
69
74
|
/**
|
|
70
75
|
* Threshold in milliseconds for warning that a mailbox lock has been held
|
|
71
76
|
* for a long time (diagnostic for forgotten release() calls). Defaults to
|