imapflow 2.1.2 → 2.2.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.2.1](https://github.com/postalsys/imapflow/compare/v2.2.0...v2.2.1) (2026-10-01)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * do not enable IMAP4rev2 on Strato RZimapd ([dce30f8](https://github.com/postalsys/imapflow/commit/dce30f86ab5caa8c1d31320d21c16cb73a611a0c)), closes [#411](https://github.com/postalsys/imapflow/issues/411)
9
+
10
+ ## [2.2.0](https://github.com/postalsys/imapflow/compare/v2.1.2...v2.2.0) (2026-10-01)
11
+
12
+
13
+ ### Features
14
+
15
+ * let the client choose the hash behind the fallback message id ([0eb74e1](https://github.com/postalsys/imapflow/commit/0eb74e1915b7b568ed137b69d0cfbcce0100679f))
16
+
3
17
  ## [2.1.2](https://github.com/postalsys/imapflow/compare/v2.1.1...v2.1.2) (2026-09-28)
4
18
 
5
19
 
@@ -182,7 +182,7 @@ async function fetch(connection, range, query, options) {
182
182
  // (useful for large result sets). Otherwise, collect all into messages.list.
183
183
  FETCH: async (untagged) => {
184
184
  messages.count++;
185
- let formatted = await (0, tools_js_1.formatMessageResponse)(untagged, mailbox);
185
+ let formatted = await (0, tools_js_1.formatMessageResponse)(untagged, mailbox, connection.idHashAlgorithm);
186
186
  if (typeof options.onUntaggedFetch === 'function') {
187
187
  await new Promise((resolve, reject) => {
188
188
  options.onUntaggedFetch(formatted, err => {
@@ -136,6 +136,8 @@ export declare class ImapFlow extends EventEmitter {
136
136
  idling: boolean;
137
137
  /** Whether log entries are also emitted as 'log' events, see the `emitLogs` option */
138
138
  emitLogs: boolean;
139
+ /** Hash algorithm for the fallback message id, see `ImapFlowOptions.idHashAlgorithm` */
140
+ idHashAlgorithm: string;
139
141
  /** The personal namespace, from the NAMESPACE command */
140
142
  namespace: NamespaceObject | undefined;
141
143
  /** Every namespace the server reported */
@@ -83,6 +83,12 @@ const RAW_SENSITIVE_COMMANDS = new Set(['LOGIN', 'AUTHENTICATE']);
83
83
  // Stand-in payload for a withheld raw client frame. Fixed width, so the entry says nothing
84
84
  // about the length of what it replaced.
85
85
  const RAW_HIDDEN_PLACEHOLDER = Buffer.from('(* value hidden *)\r\n').toString('base64');
86
+ // Servers, matched by the name in their ID response, that advertise IMAP4rev2 next to
87
+ // IMAP4rev1 and accept ENABLE IMAP4rev2, but misbehave once it is enabled. ENABLE cannot
88
+ // be undone (RFC 5161), so these are kept in IMAP4rev1 mode from the start. Strato's
89
+ // RZimapd (7.1.12, 2026-09) answers every SEARCH in a rev2 session with an ESEARCH
90
+ // response that omits ALL, which reads as "no matches" for any query. Names are lowercase
91
+ const BROKEN_REV2_SERVERS = new Set(['rzimapd']);
86
92
  // Whether any attribute of a command is marked as a secret. Recurses into nested lists because
87
93
  // the command compiler honors `sensitive` at any depth, and the two must agree on what counts.
88
94
  function hasSensitiveAttribute(attributes) {
@@ -285,6 +291,7 @@ class ImapFlow extends node_events_1.EventEmitter {
285
291
  this._openDownloads = 0;
286
292
  this.missingIdleCommand = (this.options.missingIdleCommand || '').toString().toUpperCase().trim() || 'NOOP';
287
293
  this.disableBinary = !!this.options.disableBinary;
294
+ this.idHashAlgorithm = this.options.idHashAlgorithm || 'md5';
288
295
  this.skipListSubscribedArg = false;
289
296
  this.skipListStatusArgs = false;
290
297
  this.skipListAuxArgs = false;
@@ -1164,6 +1171,12 @@ class ImapFlow extends node_events_1.EventEmitter {
1164
1171
  // re-request ID after LOGIN
1165
1172
  this.idRequested = await this.run('ID', this.clientInfo);
1166
1173
  }
1174
+ let serverName = this.serverInfo?.name;
1175
+ if (!this.skipRev2 && typeof serverName === 'string' && BROKEN_REV2_SERVERS.has(serverName.trim().toLowerCase())) {
1176
+ // Same effect as disableIMAP4rev2, decided before autoEnable() can send the ENABLE
1177
+ this.skipRev2 = true;
1178
+ this.log.info({ msg: 'Not enabling IMAP4rev2, the server is known to answer SEARCH incorrectly in that mode', server: serverName, cid: this.id });
1179
+ }
1167
1180
  // Make sure we have namespace set. This should also throw if Exchange actually failed authentication
1168
1181
  let nsResponse = await this.run('NAMESPACE');
1169
1182
  if (nsResponse && nsResponse.error && nsResponse.status === 'BAD' && /User is authenticated but not connected/i.test(nsResponse.text)) {
@@ -1809,7 +1822,7 @@ class ImapFlow extends node_events_1.EventEmitter {
1809
1822
  // mailbox closed, ignore
1810
1823
  return;
1811
1824
  }
1812
- let message = await (0, tools_js_1.formatMessageResponse)(untagged, mailbox);
1825
+ let message = await (0, tools_js_1.formatMessageResponse)(untagged, mailbox, this.idHashAlgorithm);
1813
1826
  if (message.flags) {
1814
1827
  let updateEvent = {
1815
1828
  path: mailbox.path,
@@ -1,3 +1,3 @@
1
1
  export declare const name = "imapflow";
2
- export declare const version = "2.1.2";
2
+ export declare const version = "2.2.1";
3
3
  export declare const homepage = "https://imapflow.com/";
@@ -3,5 +3,5 @@
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.homepage = exports.version = exports.name = void 0;
5
5
  exports.name = "imapflow";
6
- exports.version = "2.1.2";
6
+ exports.version = "2.2.1";
7
7
  exports.homepage = "https://imapflow.com/";
@@ -284,9 +284,10 @@ export declare function getColorFlags(color: string | null | undefined): {
284
284
  *
285
285
  * @param untagged - Parsed untagged IMAP response
286
286
  * @param mailbox - Current mailbox state object
287
+ * @param idHashAlgorithm - Hash for the fallback message id, `md5` unless the client was told otherwise
287
288
  * @returns Formatted message object with properties like seq, uid, flags, envelope, etc.
288
289
  */
289
- export declare function formatMessageResponse(untagged: ImapResponse, mailbox: MailboxObject): Promise<FetchMessageObject>;
290
+ export declare function formatMessageResponse(untagged: ImapResponse, mailbox: MailboxObject, idHashAlgorithm?: string): Promise<FetchMessageObject>;
290
291
  /**
291
292
  * Strips surrounding double quotes from a name string.
292
293
  *
package/dist/cjs/tools.js CHANGED
@@ -700,9 +700,10 @@ function getColorFlags(color) {
700
700
  *
701
701
  * @param untagged - Parsed untagged IMAP response
702
702
  * @param mailbox - Current mailbox state object
703
+ * @param idHashAlgorithm - Hash for the fallback message id, `md5` unless the client was told otherwise
703
704
  * @returns Formatted message object with properties like seq, uid, flags, envelope, etc.
704
705
  */
705
- async function formatMessageResponse(untagged, mailbox) {
706
+ async function formatMessageResponse(untagged, mailbox, idHashAlgorithm) {
706
707
  let map = {};
707
708
  // The sequence number indexes into mailbox state, so an unusable one is dropped rather
708
709
  // than coerced to NaN or Infinity
@@ -884,13 +885,14 @@ async function formatMessageResponse(untagged, mailbox) {
884
885
  // ignore
885
886
  }
886
887
  }
887
- // Non-cryptographic identifier: MD5 is used only to derive a stable, compact
888
- // account-unique id from non-secret data (path:uidValidity:uid). No security
889
- // property (collision/preimage resistance, secrecy) is relied upon, so a fast
890
- // hash is the appropriate choice here - not a security-sensitive use.
888
+ // Non-cryptographic identifier: the hash only derives a stable, compact account-unique
889
+ // id from non-secret data (path:uidValidity:uid). No security property (collision or
890
+ // preimage resistance, secrecy) is relied upon, so the fast default is MD5; a host whose
891
+ // OpenSSL runs in FIPS mode has no MD5 and names another algorithm through the client
892
+ // option, at the price of ids that differ from the default ones
891
893
  map.id =
892
894
  map.emailId ||
893
- (0, node_crypto_1.createHash)('md5')
895
+ (0, node_crypto_1.createHash)(idHashAlgorithm || 'md5')
894
896
  .update([path, mailbox.uidValidity?.toString() || '', map.uid.toString()].join(':'))
895
897
  .digest('hex');
896
898
  }
@@ -44,6 +44,14 @@ export interface ImapFlowOptions {
44
44
  clientInfo?: IdInfoObject | undefined;
45
45
  /** If true, then do not start IDLE when connection is established */
46
46
  disableAutoIdle?: boolean | undefined;
47
+ /**
48
+ * Hash algorithm (any name Node's crypto supports) for the fallback message `id` that is
49
+ * derived from the mailbox path, UIDVALIDITY and UID when the server provides no email id
50
+ * through OBJECTID or X-GM-EXT-1. Defaults to `'md5'`, which keeps the ids earlier releases
51
+ * produced. A host whose OpenSSL runs in FIPS mode does not offer MD5, and has to set a
52
+ * different one, for example `'sha256'`, which yields a 64 character hex id
53
+ */
54
+ idHashAlgorithm?: string | undefined;
47
55
  /**
48
56
  * How long (in ms) the connection has to be inactive before IDLE is started automatically.
49
57
  * Keep it above the pause your own code usually leaves between two commands, otherwise every
@@ -97,7 +105,7 @@ export interface ImapFlowOptions {
97
105
  disableBinary?: boolean | undefined;
98
106
  /** If true, do not enable supported extensions */
99
107
  disableAutoEnable?: boolean | undefined;
100
- /** If true, do not enable IMAP4rev2 mode even if the server advertises it, and do not treat the advertisement as support for anything IMAP4rev2 implies, such as the extended LIST syntax */
108
+ /** If true, do not enable IMAP4rev2 mode even if the server advertises it, and do not treat the advertisement as support for anything IMAP4rev2 implies, such as the extended LIST syntax. Servers known to misbehave in IMAP4rev2 mode (Strato RZimapd, detected from its ID response) are kept out of it without this option */
101
109
  disableIMAP4rev2?: boolean | undefined;
102
110
  /**
103
111
  * How long to wait for a usable transport, covering DNS resolution, proxy negotiation and the
@@ -179,7 +179,7 @@ export default async function fetch(connection, range, query, options) {
179
179
  // (useful for large result sets). Otherwise, collect all into messages.list.
180
180
  FETCH: async (untagged) => {
181
181
  messages.count++;
182
- let formatted = await formatMessageResponse(untagged, mailbox);
182
+ let formatted = await formatMessageResponse(untagged, mailbox, connection.idHashAlgorithm);
183
183
  if (typeof options.onUntaggedFetch === 'function') {
184
184
  await new Promise((resolve, reject) => {
185
185
  options.onUntaggedFetch(formatted, err => {
@@ -136,6 +136,8 @@ export declare class ImapFlow extends EventEmitter {
136
136
  idling: boolean;
137
137
  /** Whether log entries are also emitted as 'log' events, see the `emitLogs` option */
138
138
  emitLogs: boolean;
139
+ /** Hash algorithm for the fallback message id, see `ImapFlowOptions.idHashAlgorithm` */
140
+ idHashAlgorithm: string;
139
141
  /** The personal namespace, from the NAMESPACE command */
140
142
  namespace: NamespaceObject | undefined;
141
143
  /** Every namespace the server reported */
@@ -42,6 +42,12 @@ const RAW_SENSITIVE_COMMANDS = new Set(['LOGIN', 'AUTHENTICATE']);
42
42
  // Stand-in payload for a withheld raw client frame. Fixed width, so the entry says nothing
43
43
  // about the length of what it replaced.
44
44
  const RAW_HIDDEN_PLACEHOLDER = Buffer.from('(* value hidden *)\r\n').toString('base64');
45
+ // Servers, matched by the name in their ID response, that advertise IMAP4rev2 next to
46
+ // IMAP4rev1 and accept ENABLE IMAP4rev2, but misbehave once it is enabled. ENABLE cannot
47
+ // be undone (RFC 5161), so these are kept in IMAP4rev1 mode from the start. Strato's
48
+ // RZimapd (7.1.12, 2026-09) answers every SEARCH in a rev2 session with an ESEARCH
49
+ // response that omits ALL, which reads as "no matches" for any query. Names are lowercase
50
+ const BROKEN_REV2_SERVERS = new Set(['rzimapd']);
45
51
  // Whether any attribute of a command is marked as a secret. Recurses into nested lists because
46
52
  // the command compiler honors `sensitive` at any depth, and the two must agree on what counts.
47
53
  function hasSensitiveAttribute(attributes) {
@@ -244,6 +250,7 @@ export class ImapFlow extends EventEmitter {
244
250
  this._openDownloads = 0;
245
251
  this.missingIdleCommand = (this.options.missingIdleCommand || '').toString().toUpperCase().trim() || 'NOOP';
246
252
  this.disableBinary = !!this.options.disableBinary;
253
+ this.idHashAlgorithm = this.options.idHashAlgorithm || 'md5';
247
254
  this.skipListSubscribedArg = false;
248
255
  this.skipListStatusArgs = false;
249
256
  this.skipListAuxArgs = false;
@@ -1123,6 +1130,12 @@ export class ImapFlow extends EventEmitter {
1123
1130
  // re-request ID after LOGIN
1124
1131
  this.idRequested = await this.run('ID', this.clientInfo);
1125
1132
  }
1133
+ let serverName = this.serverInfo?.name;
1134
+ if (!this.skipRev2 && typeof serverName === 'string' && BROKEN_REV2_SERVERS.has(serverName.trim().toLowerCase())) {
1135
+ // Same effect as disableIMAP4rev2, decided before autoEnable() can send the ENABLE
1136
+ this.skipRev2 = true;
1137
+ this.log.info({ msg: 'Not enabling IMAP4rev2, the server is known to answer SEARCH incorrectly in that mode', server: serverName, cid: this.id });
1138
+ }
1126
1139
  // Make sure we have namespace set. This should also throw if Exchange actually failed authentication
1127
1140
  let nsResponse = await this.run('NAMESPACE');
1128
1141
  if (nsResponse && nsResponse.error && nsResponse.status === 'BAD' && /User is authenticated but not connected/i.test(nsResponse.text)) {
@@ -1768,7 +1781,7 @@ export class ImapFlow extends EventEmitter {
1768
1781
  // mailbox closed, ignore
1769
1782
  return;
1770
1783
  }
1771
- let message = await formatMessageResponse(untagged, mailbox);
1784
+ let message = await formatMessageResponse(untagged, mailbox, this.idHashAlgorithm);
1772
1785
  if (message.flags) {
1773
1786
  let updateEvent = {
1774
1787
  path: mailbox.path,
@@ -1,3 +1,3 @@
1
1
  export declare const name = "imapflow";
2
- export declare const version = "2.1.2";
2
+ export declare const version = "2.2.1";
3
3
  export declare const homepage = "https://imapflow.com/";
@@ -1,4 +1,4 @@
1
1
  // Generated by scripts/build.js from package.json. Do not edit by hand.
2
2
  export const name = "imapflow";
3
- export const version = "2.1.2";
3
+ export const version = "2.2.1";
4
4
  export const homepage = "https://imapflow.com/";
@@ -284,9 +284,10 @@ export declare function getColorFlags(color: string | null | undefined): {
284
284
  *
285
285
  * @param untagged - Parsed untagged IMAP response
286
286
  * @param mailbox - Current mailbox state object
287
+ * @param idHashAlgorithm - Hash for the fallback message id, `md5` unless the client was told otherwise
287
288
  * @returns Formatted message object with properties like seq, uid, flags, envelope, etc.
288
289
  */
289
- export declare function formatMessageResponse(untagged: ImapResponse, mailbox: MailboxObject): Promise<FetchMessageObject>;
290
+ export declare function formatMessageResponse(untagged: ImapResponse, mailbox: MailboxObject, idHashAlgorithm?: string): Promise<FetchMessageObject>;
290
291
  /**
291
292
  * Strips surrounding double quotes from a name string.
292
293
  *
package/dist/esm/tools.js CHANGED
@@ -645,9 +645,10 @@ export function getColorFlags(color) {
645
645
  *
646
646
  * @param untagged - Parsed untagged IMAP response
647
647
  * @param mailbox - Current mailbox state object
648
+ * @param idHashAlgorithm - Hash for the fallback message id, `md5` unless the client was told otherwise
648
649
  * @returns Formatted message object with properties like seq, uid, flags, envelope, etc.
649
650
  */
650
- export async function formatMessageResponse(untagged, mailbox) {
651
+ export async function formatMessageResponse(untagged, mailbox, idHashAlgorithm) {
651
652
  let map = {};
652
653
  // The sequence number indexes into mailbox state, so an unusable one is dropped rather
653
654
  // than coerced to NaN or Infinity
@@ -829,13 +830,14 @@ export async function formatMessageResponse(untagged, mailbox) {
829
830
  // ignore
830
831
  }
831
832
  }
832
- // Non-cryptographic identifier: MD5 is used only to derive a stable, compact
833
- // account-unique id from non-secret data (path:uidValidity:uid). No security
834
- // property (collision/preimage resistance, secrecy) is relied upon, so a fast
835
- // hash is the appropriate choice here - not a security-sensitive use.
833
+ // Non-cryptographic identifier: the hash only derives a stable, compact account-unique
834
+ // id from non-secret data (path:uidValidity:uid). No security property (collision or
835
+ // preimage resistance, secrecy) is relied upon, so the fast default is MD5; a host whose
836
+ // OpenSSL runs in FIPS mode has no MD5 and names another algorithm through the client
837
+ // option, at the price of ids that differ from the default ones
836
838
  map.id =
837
839
  map.emailId ||
838
- createHash('md5')
840
+ createHash(idHashAlgorithm || 'md5')
839
841
  .update([path, mailbox.uidValidity?.toString() || '', map.uid.toString()].join(':'))
840
842
  .digest('hex');
841
843
  }
@@ -44,6 +44,14 @@ export interface ImapFlowOptions {
44
44
  clientInfo?: IdInfoObject | undefined;
45
45
  /** If true, then do not start IDLE when connection is established */
46
46
  disableAutoIdle?: boolean | undefined;
47
+ /**
48
+ * Hash algorithm (any name Node's crypto supports) for the fallback message `id` that is
49
+ * derived from the mailbox path, UIDVALIDITY and UID when the server provides no email id
50
+ * through OBJECTID or X-GM-EXT-1. Defaults to `'md5'`, which keeps the ids earlier releases
51
+ * produced. A host whose OpenSSL runs in FIPS mode does not offer MD5, and has to set a
52
+ * different one, for example `'sha256'`, which yields a 64 character hex id
53
+ */
54
+ idHashAlgorithm?: string | undefined;
47
55
  /**
48
56
  * How long (in ms) the connection has to be inactive before IDLE is started automatically.
49
57
  * Keep it above the pause your own code usually leaves between two commands, otherwise every
@@ -97,7 +105,7 @@ export interface ImapFlowOptions {
97
105
  disableBinary?: boolean | undefined;
98
106
  /** If true, do not enable supported extensions */
99
107
  disableAutoEnable?: boolean | undefined;
100
- /** If true, do not enable IMAP4rev2 mode even if the server advertises it, and do not treat the advertisement as support for anything IMAP4rev2 implies, such as the extended LIST syntax */
108
+ /** If true, do not enable IMAP4rev2 mode even if the server advertises it, and do not treat the advertisement as support for anything IMAP4rev2 implies, such as the extended LIST syntax. Servers known to misbehave in IMAP4rev2 mode (Strato RZimapd, detected from its ID response) are kept out of it without this option */
101
109
  disableIMAP4rev2?: boolean | undefined;
102
110
  /**
103
111
  * How long to wait for a usable transport, covering DNS resolution, proxy negotiation and the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "imapflow",
3
- "version": "2.1.2",
3
+ "version": "2.2.1",
4
4
  "description": "IMAP Client for Node",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/imap-flow.js",
@@ -72,13 +72,13 @@
72
72
  "c8": "12.0.0",
73
73
  "eslint": "10.11.0",
74
74
  "eslint-config-prettier": "10.1.8",
75
- "globals": "17.12.0",
75
+ "globals": "17.13.0",
76
76
  "prettier": "3.9.9",
77
77
  "tsx": "4.23.15",
78
78
  "types-node-legacy": "npm:@types/node@20.0.0",
79
79
  "typescript": "6.0.3",
80
- "typescript-eslint": "8.70.1",
81
- "wrangler": "4.142.0"
80
+ "typescript-eslint": "8.71.0",
81
+ "wrangler": "4.145.0"
82
82
  },
83
83
  "dependencies": {
84
84
  "@zone-eu/mailsplit": "5.4.19",