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.
Files changed (44) hide show
  1. package/.github/codeql/codeql-config.yml +12 -0
  2. package/.github/workflows/codeql.yml +102 -0
  3. package/.github/workflows/stale.yml +5 -0
  4. package/.github/workflows/test.yml +3 -0
  5. package/.release-please-manifest.json +1 -1
  6. package/CHANGELOG.md +14 -0
  7. package/lib/commands/authenticate.js +5 -5
  8. package/lib/commands/fetch.js +1 -0
  9. package/lib/commands/logout.js +1 -0
  10. package/lib/commands/select.js +2 -1
  11. package/lib/commands/starttls.js +3 -0
  12. package/lib/commands/store.js +1 -0
  13. package/lib/handler/imap-stream.js +23 -6
  14. package/lib/handler/token-parser.js +0 -5
  15. package/lib/imap-flow.d.ts +5 -0
  16. package/lib/imap-flow.js +205 -29
  17. package/lib/proxy-connection.js +26 -1
  18. package/lib/tools.js +12 -0
  19. package/package.json +1 -1
  20. package/test/commands-branches-test.js +1068 -0
  21. package/test/connection-edge-cases-test.js +138 -1
  22. package/test/fixtures/test-tls.js +8 -0
  23. package/test/handler-branches-test.js +334 -0
  24. package/test/imap-compiler-test.js +47 -0
  25. package/test/imap-flow-compress-test.js +154 -0
  26. package/test/imap-flow-coverage-test.js +609 -0
  27. package/test/imap-flow-fetch-download-test.js +801 -0
  28. package/test/imap-flow-internals-test.js +450 -0
  29. package/test/imap-flow-methods-test.js +738 -0
  30. package/test/imap-flow-proxy-paths-test.js +215 -0
  31. package/test/imap-flow-secure-test.js +327 -0
  32. package/test/imap-flow-server-test.js +1046 -0
  33. package/test/imap-formal-syntax-test.js +18 -0
  34. package/test/imap-parser-test.js +52 -0
  35. package/test/imap-stream-edge-cases-test.js +122 -0
  36. package/test/jp-decoder-test.js +48 -0
  37. package/test/limited-passthrough-test.js +17 -0
  38. package/test/proxy-connection-test.js +86 -0
  39. package/test/reliability-improvements-test.js +88 -0
  40. package/test/search-compiler-test.js +31 -0
  41. package/test/search-test.js +61 -0
  42. package/test/starttls-injection-test.js +181 -0
  43. package/test/token-parser-test.js +64 -0
  44. 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}}"
@@ -3,6 +3,11 @@ on:
3
3
  schedule:
4
4
  - cron: '30 1 * * *'
5
5
 
6
+ permissions:
7
+ contents: read
8
+ issues: write
9
+ pull-requests: write
10
+
6
11
  jobs:
7
12
  stale:
8
13
  runs-on: ubuntu-latest
@@ -4,6 +4,9 @@ on:
4
4
  push:
5
5
  pull_request:
6
6
 
7
+ permissions:
8
+ contents: read
9
+
7
10
  jobs:
8
11
  test:
9
12
  strategy:
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "1.3.5"
2
+ ".": "1.3.7"
3
3
  }
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
- * @throws {Error} Always throws the enriched error
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
- throw err;
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
 
@@ -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 = [];
@@ -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
@@ -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 || (!untagged.attributes.length && Array.isArray(untagged.attributes[0]))) {
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);
@@ -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) {
@@ -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
- this.maxLineLength = this.options.maxLineLength || MAX_LINE_SIZE;
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 > MAX_LITERAL_SIZE) {
129
- const err = new Error(`Literal size ${literalSize} exceeds maximum allowed size of ${MAX_LITERAL_SIZE} bytes`);
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 = MAX_LITERAL_SIZE;
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
  }
@@ -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