imapflow 1.2.8 → 1.2.10

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 (58) hide show
  1. package/.ncurc.js +1 -1
  2. package/.release-please-manifest.json +1 -1
  3. package/CHANGELOG.md +14 -0
  4. package/README.md +36 -58
  5. package/eslint.config.js +18 -16
  6. package/lib/charsets.js +15 -0
  7. package/lib/commands/append.js +62 -54
  8. package/lib/commands/authenticate.js +99 -52
  9. package/lib/commands/capability.js +12 -2
  10. package/lib/commands/close.js +11 -1
  11. package/lib/commands/compress.js +10 -1
  12. package/lib/commands/copy.js +18 -1
  13. package/lib/commands/create.js +15 -2
  14. package/lib/commands/delete.js +10 -1
  15. package/lib/commands/enable.js +12 -1
  16. package/lib/commands/expunge.js +18 -2
  17. package/lib/commands/fetch.js +39 -4
  18. package/lib/commands/id.js +22 -3
  19. package/lib/commands/idle.js +39 -4
  20. package/lib/commands/list.js +86 -48
  21. package/lib/commands/login.js +12 -1
  22. package/lib/commands/logout.js +11 -2
  23. package/lib/commands/move.js +17 -1
  24. package/lib/commands/namespace.js +32 -2
  25. package/lib/commands/noop.js +6 -1
  26. package/lib/commands/quota.js +33 -14
  27. package/lib/commands/rename.js +13 -1
  28. package/lib/commands/search.js +16 -1
  29. package/lib/commands/select.js +76 -33
  30. package/lib/commands/starttls.js +6 -1
  31. package/lib/commands/status.js +64 -52
  32. package/lib/commands/store.js +27 -4
  33. package/lib/commands/subscribe.js +7 -1
  34. package/lib/commands/unsubscribe.js +7 -1
  35. package/lib/handler/imap-compiler.js +44 -2
  36. package/lib/handler/imap-formal-syntax.js +51 -3
  37. package/lib/handler/imap-handler.js +8 -0
  38. package/lib/handler/imap-parser.js +23 -2
  39. package/lib/handler/imap-stream.js +84 -31
  40. package/lib/handler/parser-instance.js +61 -1
  41. package/lib/handler/token-parser.js +66 -9
  42. package/lib/imap-commands.js +11 -0
  43. package/lib/imap-flow.d.ts +6 -0
  44. package/lib/imap-flow.js +175 -46
  45. package/lib/jp-decoder.js +10 -0
  46. package/lib/limited-passthrough.js +12 -5
  47. package/lib/proxy-connection.js +18 -12
  48. package/lib/search-compiler.js +3 -11
  49. package/lib/special-use.js +23 -16
  50. package/lib/tools.js +218 -13
  51. package/package.json +5 -17
  52. package/test/commands-integration-test.js +33 -0
  53. package/test/connection-edge-cases-test.js +105 -0
  54. package/test/special-use-test.js +32 -0
  55. package/.babelrc +0 -6
  56. package/.eslintrc +0 -16
  57. package/assets/favicon.ico +0 -0
  58. package/jsdoc.json +0 -28
package/.ncurc.js CHANGED
@@ -1,4 +1,4 @@
1
1
  module.exports = {
2
2
  upgrade: true,
3
- reject: ['jsdoc', 'grunt-eslint']
3
+ reject: ['grunt-eslint']
4
4
  };
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "1.2.8"
2
+ ".": "1.2.10"
3
3
  }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.2.10](https://github.com/postalsys/imapflow/compare/v1.2.9...v1.2.10) (2026-02-19)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * add null guards in compress() to prevent crash on TLS teardown ([84cd3ff](https://github.com/postalsys/imapflow/commit/84cd3ff8815d8ba37a37c1e7a09c244e80b61948))
9
+
10
+ ## [1.2.9](https://github.com/postalsys/imapflow/compare/v1.2.8...v1.2.9) (2026-02-06)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * trigger build ([fd7a4c2](https://github.com/postalsys/imapflow/commit/fd7a4c2012d0b7742c45a2aced2d05da8401aa9a))
16
+
3
17
  ## [1.2.8](https://github.com/postalsys/imapflow/compare/v1.2.7...v1.2.8) (2026-01-28)
4
18
 
5
19
 
package/README.md CHANGED
@@ -1,109 +1,87 @@
1
1
  # ImapFlow
2
2
 
3
- ImapFlow is a modern and easy-to-use IMAP client library for Node.js.
3
+ Modern and easy-to-use IMAP client library for Node.js.
4
4
 
5
- > [!NOTE]
6
- > Managing an IMAP connection is cool, but if you are only looking for an easy way to integrate email accounts, then ImapFlow was built for [EmailEngine Email API](https://emailengine.app/). It's a self-hosted software that converts all IMAP accounts to easy-to-use REST interfaces.
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 {@link FetchQueryObject|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
+ [![npm](https://img.shields.io/npm/v/imapflow)](https://www.npmjs.com/package/imapflow)
6
+ [![license](https://img.shields.io/npm/l/imapflow)](https://github.com/postalsys/imapflow/blob/master/LICENSE)
11
7
 
12
- ## Source
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
- Source code is available from [Github](https://github.com/postalsys/imapflow).
10
+ ## Features
15
11
 
16
- ## Usage
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
- First install the module from npm:
20
+ ## Installation
19
21
 
20
- ```
22
+ ```bash
21
23
  npm install imapflow
22
24
  ```
23
25
 
24
- next import the ImapFlow class into your script:
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: 'ethereal.email',
32
+ host: 'imap.example.com',
38
33
  port: 993,
39
34
  secure: true,
40
35
  auth: {
41
- user: 'garland.mcclure71@ethereal.email',
42
- pass: 'mW6e4wWWnEd3H4hT5B'
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 source
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
- // Make sure lock is released, otherwise next `getMailboxLock()` never returns
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(err => console.error(err));
62
+ main().catch(console.error);
74
63
  ```
75
64
 
76
- ### Admin Impersonation / Delegation (SASL PLAIN with authzid)
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
- ImapFlow supports admin impersonation for mail systems like Zimbra that allow administrators to access user mailboxes. This is done using the SASL PLAIN mechanism with an authorization identity (`authzid`).
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
- // Connection will authenticate as admin but authorize as the specified user
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
- **Note:** The `authzid` parameter only works with the `AUTH=PLAIN` mechanism. The server must support admin delegation/impersonation for this to work.
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
- ## Documentation
102
-
103
- [API reference](https://imapflow.com/module-imapflow-ImapFlow.html).
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
- © 2020-2024 Postal Systems OÜ
85
+ Copyright (c) 2020-2025 Postal Systems OU
108
86
 
109
- Licensed under **MIT-license**
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 compat = new FlatCompat({
7
- baseDirectory: __dirname,
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
- ...compat.extends('nodemailer', 'prettier'),
12
+ js.configs.recommended,
16
13
  {
17
14
  languageOptions: {
18
- ecmaVersion: 2020,
15
+ ecmaVersion: 2022,
19
16
  sourceType: 'script',
20
- globals: {
21
- BigInt: 'readonly'
22
- },
23
- parser: require('@babel/eslint-parser'),
24
17
  parserOptions: {
25
- requireConfigFile: false
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/charsets.js CHANGED
@@ -1,5 +1,8 @@
1
1
  'use strict';
2
2
 
3
+ // Subset of the IANA Character Sets registry (https://www.iana.org/assignments/character-sets/).
4
+ // Used to validate and resolve charset names found in MIME Content-Type parameters.
5
+ // This list covers the most commonly encountered charsets in email messages.
3
6
  const CHARACTER_SETS = [
4
7
  'US-ASCII',
5
8
  'ISO-8859-1',
@@ -262,6 +265,14 @@ const CHARACTER_SETS = [
262
265
 
263
266
  const CHARSET_MAP = new Map();
264
267
 
268
+ // Build a lookup map with normalized keys for fuzzy charset resolution.
269
+ // Normalization strategy:
270
+ // 1. Strip all underscores, hyphens, and spaces, then lowercase (e.g., "ISO-8859-1" -> "iso88591")
271
+ // 2. Create additional aliases for common alternative prefixes:
272
+ // - "windows" -> "win" (e.g., "windows1252" also matches as "win1252")
273
+ // - "usascii" -> "ascii" (common shorthand)
274
+ // - "iso8859" -> "latin" (e.g., "iso88591" also matches as "latin1")
275
+ // This handles the many variant spellings found in real-world email headers.
265
276
  CHARACTER_SETS.forEach(entry => {
266
277
  let key = entry.replace(/[_-\s]/g, '').toLowerCase();
267
278
  let modifiedKey = key
@@ -274,6 +285,10 @@ CHARACTER_SETS.forEach(entry => {
274
285
  }
275
286
  });
276
287
 
288
+ // Resolves a charset name to its canonical IANA form using case-insensitive,
289
+ // symbol-stripping normalization. For example, "WIN-1252", "windows_1252",
290
+ // and "WINDOWS-1252" all resolve to "windows-1252".
291
+ // Returns null if the charset is not recognized.
277
292
  module.exports.resolveCharset = charset => {
278
293
  let key = charset.replace(/[_-\s]/g, '').toLowerCase();
279
294
  if (CHARSET_MAP.has(key)) {
@@ -2,7 +2,17 @@
2
2
 
3
3
  const { formatFlag, canUseFlag, formatDateTime, normalizePath, encodePath, comparePaths, enhanceCommandError } = require('../tools.js');
4
4
 
5
- // Appends a message to a mailbox
5
+ /**
6
+ * Appends a message to a mailbox.
7
+ *
8
+ * @param {Object} connection - IMAP connection instance
9
+ * @param {string} destination - Destination mailbox path
10
+ * @param {Buffer|string} content - Message content (RFC 822 format)
11
+ * @param {string|string[]} [flags] - Message flags to set on the appended message
12
+ * @param {Date|string} [idate] - Internal date to set for the message
13
+ * @returns {Promise<{destination: string, path?: string, uid?: number, uidValidity?: BigInt, seq?: number}|undefined>} Append result with UID info if available, or undefined if preconditions not met
14
+ * @throws {Error} If the APPEND command fails or message exceeds APPENDLIMIT
15
+ */
6
16
  module.exports = async (connection, destination, content, flags, idate) => {
7
17
  if (![connection.states.AUTHENTICATED, connection.states.SELECTED].includes(connection.state) || !destination) {
8
18
  // nothing to do here
@@ -13,6 +23,8 @@ module.exports = async (connection, destination, content, flags, idate) => {
13
23
  content = Buffer.from(content);
14
24
  }
15
25
 
26
+ // APPENDLIMIT capability (RFC 7889): server may advertise the maximum message
27
+ // size it accepts. Check before sending to avoid a wasted round-trip.
16
28
  if (connection.capabilities.has('APPENDLIMIT')) {
17
29
  let appendLimit = connection.capabilities.get('APPENDLIMIT');
18
30
  if (typeof appendLimit === 'number' && appendLimit < content.length) {
@@ -24,28 +36,36 @@ module.exports = async (connection, destination, content, flags, idate) => {
24
36
 
25
37
  destination = normalizePath(connection, destination);
26
38
 
39
+ // If appending to the currently selected mailbox, we can listen for the
40
+ // untagged EXISTS response to capture the new message's sequence number.
27
41
  let expectExists = comparePaths(connection, connection.mailbox.path, destination);
28
42
 
43
+ // Validate and format flags. Only flags allowed by the mailbox's permanentFlags are included.
29
44
  flags = (Array.isArray(flags) ? flags : [].concat(flags || []))
30
45
  .map(flag => flag && formatFlag(flag.toString()))
31
46
  .filter(flag => flag && canUseFlag(connection.mailbox, flag));
32
47
 
48
+ // APPEND command format: APPEND <mailbox> [<flags>] [<date-time>] <literal>
33
49
  let attributes = [{ type: 'ATOM', value: encodePath(connection, destination) }];
34
50
 
51
+ // Internal date: the date the server should record for this message.
52
+ // Must be quoted (STRING type) per the IMAP date-time grammar.
35
53
  idate = idate ? formatDateTime(idate) : false;
36
54
 
55
+ // Flags and date are optional; flags must come before date if both are present
37
56
  if (flags.length || idate) {
38
57
  attributes.push(flags.map(flag => ({ type: 'ATOM', value: flag })));
39
58
  }
40
59
 
41
60
  if (idate) {
42
- attributes.push({ type: 'STRING', value: idate }); // force quotes as required by date-time
61
+ attributes.push({ type: 'STRING', value: idate });
43
62
  }
44
63
 
64
+ // BINARY extension (RFC 3516): if the message content contains NUL bytes,
65
+ // use literal8 syntax (~{size}\r\n) instead of regular literal ({size}\r\n).
66
+ // Regular literals cannot contain NUL bytes per the IMAP grammar.
45
67
  let isLiteral8 = false;
46
68
  if (connection.capabilities.has('BINARY') && !connection.disableBinary) {
47
- // Value is literal8 if it contains NULL bytes. The server must support the BINARY extension
48
- // and if it does not then send the value as a regular literal and hope for the best
49
69
  isLiteral8 = content.indexOf(Buffer.from([0])) >= 0;
50
70
  }
51
71
 
@@ -56,72 +76,58 @@ module.exports = async (connection, destination, content, flags, idate) => {
56
76
  map.path = connection.mailbox.path;
57
77
  }
58
78
 
79
+ // Handler for untagged EXISTS: captures the new message count which gives
80
+ // us the sequence number of the appended message (it's the latest message).
81
+ const handleExistsUpdate = untagged => {
82
+ map.seq = Number(untagged.command);
83
+
84
+ // Update the connection's mailbox state and emit 'exists' event if the
85
+ // count changed (notifies listeners about the new message).
86
+ if (expectExists) {
87
+ let prevCount = connection.mailbox.exists;
88
+ if (map.seq !== prevCount) {
89
+ connection.mailbox.exists = map.seq;
90
+ connection.emit('exists', {
91
+ path: connection.mailbox.path,
92
+ count: map.seq,
93
+ prevCount
94
+ });
95
+ }
96
+ }
97
+ };
98
+
59
99
  let response;
60
100
  try {
61
101
  response = await connection.exec('APPEND', attributes, {
62
- untagged: expectExists
63
- ? {
64
- EXISTS: async untagged => {
65
- map.seq = Number(untagged.command);
66
-
67
- if (expectExists) {
68
- let prevCount = connection.mailbox.exists;
69
- if (map.seq !== prevCount) {
70
- connection.mailbox.exists = map.seq;
71
- connection.emit('exists', {
72
- path: connection.mailbox.path,
73
- count: map.seq,
74
- prevCount
75
- });
76
- }
77
- }
78
- }
79
- }
80
- : false
102
+ // Only listen for EXISTS if we're appending to the currently selected mailbox
103
+ untagged: expectExists ? { EXISTS: handleExistsUpdate } : false
81
104
  });
82
105
 
106
+ // UIDPLUS (RFC 4315): the server may include APPENDUID response code in
107
+ // the tagged OK. Format: [APPENDUID <uidValidity> <uid>]
83
108
  let section = response.response.attributes && response.response.attributes[0] && response.response.attributes[0].section;
84
109
  if (section && section.length) {
85
110
  let responseCode = section[0] && typeof section[0].value === 'string' ? section[0].value : '';
86
- switch (responseCode.toUpperCase()) {
87
- case 'APPENDUID':
88
- {
89
- let uidValidity = section[1] && typeof section[1].value === 'string' && !isNaN(section[1].value) ? BigInt(section[1].value) : false;
90
- let uid = section[2] && typeof section[2].value === 'string' && !isNaN(section[2].value) ? Number(section[2].value) : false;
91
- if (uidValidity) {
92
- map.uidValidity = uidValidity;
93
- }
94
- if (uid) {
95
- map.uid = uid;
96
- }
97
- }
98
- break;
111
+ if (responseCode.toUpperCase() === 'APPENDUID') {
112
+ let uidValidity = section[1] && typeof section[1].value === 'string' && !isNaN(section[1].value) ? BigInt(section[1].value) : false;
113
+ let uid = section[2] && typeof section[2].value === 'string' && !isNaN(section[2].value) ? Number(section[2].value) : false;
114
+ if (uidValidity) {
115
+ map.uidValidity = uidValidity;
116
+ }
117
+ if (uid) {
118
+ map.uid = uid;
119
+ }
99
120
  }
100
121
  }
101
122
 
102
123
  response.next();
103
124
 
125
+ // If we didn't get an EXISTS during APPEND (some servers don't send it
126
+ // until the next command), issue a NOOP to flush pending notifications.
104
127
  if (expectExists && !map.seq) {
105
- // try to use NOOP to get the new sequence number
106
128
  try {
107
129
  response = await connection.exec('NOOP', false, {
108
- untagged: {
109
- EXISTS: async untagged => {
110
- map.seq = Number(untagged.command);
111
-
112
- if (expectExists) {
113
- let prevCount = connection.mailbox.exists;
114
- if (map.seq !== prevCount) {
115
- connection.mailbox.exists = map.seq;
116
- connection.emit('exists', {
117
- path: connection.mailbox.path,
118
- count: map.seq,
119
- prevCount
120
- });
121
- }
122
- }
123
- }
124
- },
130
+ untagged: { EXISTS: handleExistsUpdate },
125
131
  comment: 'Sequence not found from APPEND output'
126
132
  });
127
133
  response.next();
@@ -130,6 +136,8 @@ module.exports = async (connection, destination, content, flags, idate) => {
130
136
  }
131
137
  }
132
138
 
139
+ // If we have a sequence number but no UID (server doesn't support UIDPLUS),
140
+ // look up the UID via SEARCH to provide a consistent result to the caller.
133
141
  if (map.seq && !map.uid) {
134
142
  let list = await connection.search({ seq: map.seq }, { uid: true });
135
143
  if (list && list.length) {