imapflow 1.3.4 → 1.3.6

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.
@@ -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.4"
2
+ ".": "1.3.6"
3
3
  }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.3.6](https://github.com/postalsys/imapflow/compare/v1.3.5...v1.3.6) (2026-06-05)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * harden STARTTLS upgrade, literal bounds, and connection error paths ([35eca81](https://github.com/postalsys/imapflow/commit/35eca81075401e89f0b3aca84e26c41710a62c28))
9
+
10
+ ## [1.3.5](https://github.com/postalsys/imapflow/compare/v1.3.4...v1.3.5) (2026-06-01)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * correct flag removal, literal parsing, path normalization, and harden the parser ([d15c053](https://github.com/postalsys/imapflow/commit/d15c0536f4b7db2ebca5e258fc885da0d194b5fd))
16
+ * prevent host process crash from socket errors after unbind() when COMPRESS=DEFLATE is active ([e30fca1](https://github.com/postalsys/imapflow/commit/e30fca1a42a4d2ab6f7a6a766effafe3df6b1aff))
17
+
3
18
  ## [1.3.4](https://github.com/postalsys/imapflow/compare/v1.3.3...v1.3.4) (2026-05-29)
4
19
 
5
20
 
package/SECURITY.md ADDED
@@ -0,0 +1,88 @@
1
+ # Security Policy
2
+
3
+ ImapFlow is a Node.js IMAP client library. It opens TLS/cleartext connections to
4
+ IMAP servers, sends account credentials, and parses untrusted protocol responses
5
+ from those servers. Because it handles credentials and processes data from
6
+ remote servers that may be malicious or buggy, we take security reports
7
+ seriously and aim to respond quickly.
8
+
9
+ ## Supported Versions
10
+
11
+ Security fixes are released only against the latest version. We do not backport
12
+ patches to older releases - upgrading to the current release line is the
13
+ supported way to receive security updates.
14
+
15
+ | Version | Supported |
16
+ | ------- | ------------------ |
17
+ | 1.x | :white_check_mark: |
18
+ | < 1.0 | :x: |
19
+
20
+ If you are on an older version, please upgrade. See the release notes at
21
+ <https://github.com/postalsys/imapflow/releases> before updating.
22
+
23
+ ## Reporting a Vulnerability
24
+
25
+ **Please do not report security vulnerabilities through public GitHub issues,
26
+ pull requests, or discussions.**
27
+
28
+ Report privately through one of the following channels:
29
+
30
+ 1. **GitHub Security Advisories (preferred).** Open a private report at
31
+ <https://github.com/postalsys/imapflow/security/advisories/new>. This keeps
32
+ the discussion private until a fix is published and lets us credit you.
33
+ 2. **Email.** Send details to **andris@postalsys.com** (the contact listed in
34
+ [`SECURITY.txt`](SECURITY.txt)). Encrypt sensitive details with the PGP key
35
+ referenced there if possible.
36
+
37
+ When reporting, please include as much of the following as you can:
38
+
39
+ - The affected version(s) and environment (ImapFlow version, Node.js version,
40
+ OS).
41
+ - The component involved (e.g. the IMAP response stream/parser, literal and line
42
+ handling, TLS/STARTTLS upgrade, credential handling, the command compiler, or
43
+ the public client API).
44
+ - A clear description of the issue and its impact (e.g. memory exhaustion or
45
+ denial of service from a malicious server, parser crash, credential
46
+ disclosure, TLS verification bypass, injection into the IMAP command stream,
47
+ prototype pollution, information disclosure).
48
+ - A minimal proof of concept or reproduction steps - ideally a sample server
49
+ response or a short script that triggers the issue.
50
+ - Any suggested remediation, if you have one.
51
+
52
+ We are a small team, so there is no guaranteed response time - sometimes reports
53
+ are handled within hours, sometimes they take longer. Accepted issues are fixed
54
+ in a new release and coordinated through a GitHub Security Advisory, and
55
+ reporters who wish to be named are credited.
56
+
57
+ ## CVEs
58
+
59
+ We track and disclose vulnerabilities through GitHub Security Advisories. We do
60
+ not request or manage CVE identifiers ourselves. If you need a CVE assigned for a
61
+ reported issue, please request one yourself - for example, through GitHub's own
62
+ CVE request flow on the published advisory, or another CNA.
63
+
64
+ ## Scope
65
+
66
+ In scope: the ImapFlow library source in this repository - the IMAP response
67
+ stream and token parser (including handling of hostile or malformed server
68
+ responses, literals, line lengths, and resource/DoS bounds), the TLS and
69
+ STARTTLS upgrade path and certificate handling, credential handling during
70
+ authentication, the command compiler that builds outgoing IMAP commands, and the
71
+ public client API.
72
+
73
+ Out of scope:
74
+
75
+ - Vulnerabilities in your own application code that uses ImapFlow.
76
+ - Misconfiguration of your usage - for example, disabling TLS certificate
77
+ verification (`tls.rejectUnauthorized: false`), connecting over cleartext when
78
+ TLS is available, or passing untrusted input directly into mailbox paths or
79
+ command arguments without validation.
80
+ - Vulnerabilities in the IMAP servers you connect to, or in third-party
81
+ dependencies (please report those to their respective maintainers; we will
82
+ upgrade once a fix is available).
83
+ - Issues that require an already-compromised host or local access to the machine
84
+ running ImapFlow.
85
+ - Social-engineering reports and theoretical issues without a demonstrated,
86
+ concrete impact.
87
+
88
+ Thank you for helping keep ImapFlow and its users safe.
package/SECURITY.txt ADDED
@@ -0,0 +1,27 @@
1
+ -----BEGIN PGP SIGNED MESSAGE-----
2
+ Hash: SHA256
3
+
4
+ Contact: https://github.com/postalsys/imapflow/security/advisories/new
5
+ Contact: mailto:andris@postalsys.com
6
+ Expires: 2027-06-01T00:00:00.000Z
7
+ Encryption: https://keys.openpgp.org/vks/v1/by-fingerprint/5D952A46E1D8C931F6364E01DC6C83F4D584D364
8
+ Preferred-Languages: en, et
9
+ Canonical: https://github.com/postalsys/imapflow/blob/master/SECURITY.txt
10
+ Policy: https://github.com/postalsys/imapflow/blob/master/SECURITY.md
11
+ -----BEGIN PGP SIGNATURE-----
12
+
13
+ iQJiBAEBCABMFiEEXZUqRuHYyTH2Nk4B3GyD9NWE02QFAmodTFwbFIAAAAAABAAO
14
+ bWFudTIsMi41KzEuMTIsMCwzEhxhbmRyaXNAcmVpbm1hbi5ldQAKCRDcbIP01YTT
15
+ ZGcgD/42Wdd07wEclXDm/lM0Ax+aX9sPjwV33DAICqhDoILf8yG4yPkCRuuTCayw
16
+ QLTB1pCi+tk8xVs9TH88bDwinfGVqzbmUyHSa2h32TDx/0/b+rg2LO2Ru8JtJkX8
17
+ CMY8/UhkouaQnHKDjkbLniF8HpQsBzOiPzvkfX91BY4HBwzE8aCC8MXC0zyjBkQX
18
+ dqVJ/fYMQKPkblKemnrMB/XM1jui2M7vgzpqilLcZqYv5oNFQhgpd+JOEVKMDUZv
19
+ 0WaLrFWRegFvFApS/cIvzu5RLIhDWuL0ao577Cpdj1QE7nznfbBqBfGEzZotOShs
20
+ CdzdGloSt9C1gVLWItsncXByXVuNrF8l67hoOAnRwO4c8PKg3b4CvzBelmDiCWrS
21
+ 73LGWmFwp1luyLrfZREUYsF5hqHCr6ULmDEAVp0MRtzxL56YjiS20d1sa9v96P4O
22
+ KFCvOT5LbCK/KqAFzhRzRXd3sawt5SXcR2FGgjFrRYNriyHNSk0k1h5IYv1kNoPb
23
+ VN+DxsOhORd2EsqqOBqHoEES472OOsmLTYa0A5z+zyA0IcaY5hXsMDrX9OMIUHUW
24
+ 4JAOviDYyejefVJd8SsLrAmQ82p1jPY0LnuIyLz/sXDoe7QduNZIFKoUYw3KVybM
25
+ y0X7yP92/buzcZY41FOQT8dBFGDzboWQo+RPEh16qXEV75ss2A==
26
+ =Wlrz
27
+ -----END PGP SIGNATURE-----
@@ -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) {
@@ -57,7 +57,7 @@ module.exports = async (connection, range, flags, options) => {
57
57
  .map(flag => {
58
58
  flag = formatFlag(flag);
59
59
 
60
- if (!canUseFlag(connection.mailbox, flag) && operation !== 'remove') {
60
+ if (!canUseFlag(connection.mailbox, flag) && options.operation !== 'remove') {
61
61
  return false;
62
62
  }
63
63
 
@@ -16,6 +16,11 @@ const CURLY_CLOSE = 0x7d;
16
16
  // Maximum allowed literal size: 1GB (1073741824 bytes)
17
17
  const MAX_LITERAL_SIZE = 1024 * 1024 * 1024;
18
18
 
19
+ // Default maximum length of a single line (a response without a literal). Matches the literal cap:
20
+ // large literal-free responses (e.g. big SEARCH/LIST results) are legitimate, so this bound exists
21
+ // only to stop a server that never sends a line terminator, not to constrain normal traffic.
22
+ const MAX_LINE_SIZE = MAX_LITERAL_SIZE;
23
+
19
24
  /**
20
25
  * A Transform stream that parses raw IMAP protocol data from a socket into structured
21
26
  * command/response objects. Reads binary input, splits it into lines delimited by LF,
@@ -34,6 +39,13 @@ class ImapStream extends Transform {
34
39
  * @param {Object} [options.logger] - A pino-compatible logger instance. If not provided, a default child logger is created.
35
40
  * @param {boolean} [options.logRaw] - If true, logs raw socket data at trace level.
36
41
  * @param {boolean} [options.secureConnection] - Whether the connection uses TLS.
42
+ * @param {number} [options.maxLineLength] - Maximum allowed length (in bytes) of a single
43
+ * line (a response without a literal). Defaults to MAX_LITERAL_SIZE (1GB). Guards against a
44
+ * malicious or broken server that never sends a line terminator, which would otherwise grow
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.
37
49
  */
38
50
  constructor(options) {
39
51
  super({
@@ -55,10 +67,23 @@ class ImapStream extends Transform {
55
67
 
56
68
  this.readBytesCounter = 0;
57
69
 
70
+ // Maximum length of a single line (response without a literal). Bounds the line buffer
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;
81
+
58
82
  this.state = LINE;
59
83
  this.literalWaiting = 0;
60
84
  this.inputBuffer = []; // lines
61
85
  this.lineBuffer = []; // current line
86
+ this.lineBytes = 0; // bytes currently buffered for the in-progress line
62
87
  this.literalBuffer = [];
63
88
  this.literals = [];
64
89
 
@@ -102,7 +127,7 @@ class ImapStream extends Transform {
102
127
  // Scan backwards through the line to find an IMAP literal marker: {size}\r\n
103
128
  // The format is: '{' followed by one or more ASCII digits followed by '}'
104
129
  let numBytes = [];
105
- for (; pos > 0; pos--) {
130
+ for (; pos >= 0; pos--) {
106
131
  let c = line[pos];
107
132
  if (c >= NUM_0 && c <= NUM_9) {
108
133
  numBytes.unshift(c);
@@ -111,11 +136,11 @@ class ImapStream extends Transform {
111
136
  if (c === CURLY_OPEN && numBytes.length) {
112
137
  const literalSize = Number(Buffer.from(numBytes).toString());
113
138
 
114
- if (literalSize > MAX_LITERAL_SIZE) {
115
- 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`);
116
141
  err.code = 'LiteralTooLarge';
117
142
  err.literalSize = literalSize;
118
- err.maxSize = MAX_LITERAL_SIZE;
143
+ err.maxSize = this.maxLiteralSize;
119
144
  this.emit('error', err);
120
145
  return false;
121
146
  }
@@ -158,6 +183,7 @@ class ImapStream extends Transform {
158
183
 
159
184
  this.inputBuffer.push(line);
160
185
  this.lineBuffer = [];
186
+ this.lineBytes = 0;
161
187
 
162
188
  // try to detect if this is a literal start
163
189
  if (this.checkLiteralMarker(line)) {
@@ -182,15 +208,35 @@ class ImapStream extends Transform {
182
208
  }
183
209
 
184
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;
185
217
  await new Promise(resolve => {
186
- this.push({ payload, literals, next: resolve });
218
+ this.push({ payload, literals, next: resolve, trailingAfterLine });
187
219
  });
188
220
  }
189
221
  }
190
222
  }
191
223
  }
192
224
  if (lineStart < chunk.length) {
193
- this.lineBuffer.push(chunk.slice(lineStart));
225
+ // No line terminator was found in the remaining bytes; carry the tail over to
226
+ // the next chunk. Enforce the line-length cap here, since this is the only
227
+ // path that grows the line buffer across chunks.
228
+ let tail = chunk.slice(lineStart);
229
+ let lineLength = this.lineBytes + tail.length;
230
+ if (lineLength > this.maxLineLength) {
231
+ const err = new Error(`Line length ${lineLength} exceeds maximum allowed size of ${this.maxLineLength} bytes`);
232
+ err.code = 'LineTooLarge';
233
+ err.lineLength = lineLength;
234
+ err.maxSize = this.maxLineLength;
235
+ this.emit('error', err);
236
+ return;
237
+ }
238
+ this.lineBytes = lineLength;
239
+ this.lineBuffer.push(tail);
194
240
  }
195
241
  break;
196
242
  }
@@ -303,6 +349,7 @@ class ImapStream extends Transform {
303
349
  _destroy(err, callback) {
304
350
  this.inputBuffer = [];
305
351
  this.lineBuffer = [];
352
+ this.lineBytes = 0;
306
353
  this.literalBuffer = [];
307
354
  this.literals = [];
308
355
  // Clear inputQueue and call any pending callbacks
@@ -349,6 +349,13 @@ class TokenParser {
349
349
  this.currentNode.type = 'ATOM';
350
350
  // jump i to the ']'
351
351
  i = this.str.indexOf(']', i + 10);
352
+ if (i < 0) {
353
+ // Malformed REFERRAL with no closing ']'. Consume the rest
354
+ // of the string (there is no ']' to exclude) instead of
355
+ // computing a negative-index substring, which would yield
356
+ // garbage.
357
+ i = this.str.length;
358
+ }
352
359
  this.currentNode.endPos = this.pos + i - 1;
353
360
  this.currentNode.value = this.str.substring(this.currentNode.startPos - this.pos, this.currentNode.endPos - this.pos + 1);
354
361
  this.currentNode = this.currentNode.parentNode;
@@ -60,6 +60,17 @@ export interface ImapFlowOptions {
60
60
  greetingTimeout?: number;
61
61
  /** How long to wait for socket inactivity before timing out the connection. Defaults to 5 minutes */
62
62
  socketTimeout?: number;
63
+ /**
64
+ * Maximum allowed length in bytes of a single response line (a response without a literal).
65
+ * Guards against a malicious or broken server that never sends a line terminator. Defaults to
66
+ * 1GB.
67
+ */
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;
63
74
  /**
64
75
  * Threshold in milliseconds for warning that a mailbox lock has been held
65
76
  * for a long time (diagnostic for forgotten release() calls). Defaults to