imapflow 1.7.7 → 2.0.0

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 (296) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +8 -2
  3. package/dist/cjs/charsets.d.ts +1 -0
  4. package/dist/cjs/charsets.js +294 -0
  5. package/dist/cjs/commands/append.d.ts +22 -0
  6. package/dist/cjs/commands/append.js +151 -0
  7. package/dist/cjs/commands/authenticate.d.ts +24 -0
  8. package/dist/cjs/commands/authenticate.js +223 -0
  9. package/dist/cjs/commands/capability.d.ts +8 -0
  10. package/dist/cjs/commands/capability.js +32 -0
  11. package/dist/cjs/commands/close.d.ts +8 -0
  12. package/dist/cjs/commands/close.js +39 -0
  13. package/dist/cjs/commands/compress.d.ts +8 -0
  14. package/dist/cjs/commands/compress.js +56 -0
  15. package/dist/cjs/commands/copy.d.ts +13 -0
  16. package/dist/cjs/commands/copy.js +44 -0
  17. package/dist/cjs/commands/copyuid-parser.d.ts +11 -0
  18. package/dist/cjs/commands/copyuid-parser.js +32 -0
  19. package/dist/cjs/commands/create.d.ts +11 -0
  20. package/dist/cjs/commands/create.js +80 -0
  21. package/dist/cjs/commands/delete.d.ts +11 -0
  22. package/dist/cjs/commands/delete.js +40 -0
  23. package/dist/cjs/commands/enable.d.ts +9 -0
  24. package/dist/cjs/commands/enable.js +61 -0
  25. package/dist/cjs/commands/esearch-parser.d.ts +17 -0
  26. package/dist/cjs/commands/esearch-parser.js +91 -0
  27. package/dist/cjs/commands/expunge.d.ts +12 -0
  28. package/dist/cjs/commands/expunge.js +60 -0
  29. package/dist/cjs/commands/fetch.d.ts +30 -0
  30. package/dist/cjs/commands/fetch.js +241 -0
  31. package/dist/cjs/commands/id.d.ts +10 -0
  32. package/dist/cjs/commands/id.js +80 -0
  33. package/dist/cjs/commands/idle.d.ts +9 -0
  34. package/dist/cjs/commands/idle.js +347 -0
  35. package/dist/cjs/commands/list.d.ts +16 -0
  36. package/dist/cjs/commands/list.js +518 -0
  37. package/dist/cjs/commands/login.d.ts +11 -0
  38. package/dist/cjs/commands/login.js +42 -0
  39. package/dist/cjs/commands/logout.d.ts +8 -0
  40. package/dist/cjs/commands/logout.js +47 -0
  41. package/dist/cjs/commands/move.d.ts +13 -0
  42. package/dist/cjs/commands/move.js +57 -0
  43. package/dist/cjs/commands/namespace.d.ts +25 -0
  44. package/dist/cjs/commands/namespace.js +139 -0
  45. package/dist/cjs/commands/noop.d.ts +8 -0
  46. package/dist/cjs/commands/noop.js +22 -0
  47. package/dist/cjs/commands/quota.d.ts +10 -0
  48. package/dist/cjs/commands/quota.js +119 -0
  49. package/dist/cjs/commands/rename.d.ts +12 -0
  50. package/dist/cjs/commands/rename.js +48 -0
  51. package/dist/cjs/commands/search.d.ts +15 -0
  52. package/dist/cjs/commands/search.js +228 -0
  53. package/dist/cjs/commands/select.d.ts +25 -0
  54. package/dist/cjs/commands/select.js +250 -0
  55. package/dist/cjs/commands/starttls.d.ts +8 -0
  56. package/dist/cjs/commands/starttls.js +30 -0
  57. package/dist/cjs/commands/status-fields.d.ts +14 -0
  58. package/dist/cjs/commands/status-fields.js +61 -0
  59. package/dist/cjs/commands/status.d.ts +12 -0
  60. package/dist/cjs/commands/status.js +108 -0
  61. package/dist/cjs/commands/store.d.ts +19 -0
  62. package/dist/cjs/commands/store.js +93 -0
  63. package/dist/cjs/commands/subscribe.d.ts +9 -0
  64. package/dist/cjs/commands/subscribe.js +31 -0
  65. package/dist/cjs/commands/unsubscribe.d.ts +9 -0
  66. package/dist/cjs/commands/unsubscribe.js +31 -0
  67. package/dist/cjs/connection-deadline.d.ts +49 -0
  68. package/dist/cjs/connection-deadline.js +91 -0
  69. package/dist/cjs/errors.d.ts +83 -0
  70. package/dist/cjs/errors.js +13 -0
  71. package/dist/cjs/handler/imap-compiler.d.ts +24 -0
  72. package/dist/cjs/handler/imap-compiler.js +285 -0
  73. package/dist/cjs/handler/imap-formal-syntax.d.ts +28 -0
  74. package/dist/cjs/handler/imap-formal-syntax.js +121 -0
  75. package/dist/cjs/handler/imap-handler.d.ts +9 -0
  76. package/dist/cjs/handler/imap-handler.js +10 -0
  77. package/dist/cjs/handler/imap-parser.d.ts +16 -0
  78. package/dist/cjs/handler/imap-parser.js +90 -0
  79. package/dist/cjs/handler/imap-stream.d.ts +181 -0
  80. package/dist/cjs/handler/imap-stream.js +446 -0
  81. package/dist/cjs/handler/limits.d.ts +25 -0
  82. package/dist/cjs/handler/limits.js +51 -0
  83. package/dist/cjs/handler/parser-instance.d.ts +68 -0
  84. package/dist/cjs/handler/parser-instance.js +223 -0
  85. package/dist/cjs/handler/token-parser.d.ts +91 -0
  86. package/dist/cjs/handler/token-parser.js +673 -0
  87. package/dist/cjs/handler/types.d.ts +91 -0
  88. package/dist/cjs/handler/types.js +4 -0
  89. package/dist/cjs/imap-commands.d.ts +16 -0
  90. package/dist/cjs/imap-commands.js +74 -0
  91. package/dist/cjs/imap-flow.d.ts +676 -0
  92. package/dist/cjs/imap-flow.js +3949 -0
  93. package/dist/cjs/jp-decoder.d.ts +12 -0
  94. package/dist/cjs/jp-decoder.js +79 -0
  95. package/dist/cjs/limited-passthrough.d.ts +25 -0
  96. package/dist/cjs/limited-passthrough.js +54 -0
  97. package/dist/cjs/logger.d.ts +3 -0
  98. package/dist/cjs/logger.js +11 -0
  99. package/dist/cjs/package-info.d.ts +3 -0
  100. package/dist/cjs/package-info.js +7 -0
  101. package/dist/cjs/package.json +3 -0
  102. package/dist/cjs/proxy-connection.d.ts +33 -0
  103. package/dist/cjs/proxy-connection.js +392 -0
  104. package/dist/cjs/search-compiler.d.ts +34 -0
  105. package/dist/cjs/search-compiler.js +476 -0
  106. package/dist/cjs/special-use.d.ts +22 -0
  107. package/dist/cjs/special-use.js +911 -0
  108. package/dist/cjs/tools.d.ts +427 -0
  109. package/dist/cjs/tools.js +1496 -0
  110. package/{lib/imap-flow.d.ts → dist/cjs/types.d.ts} +386 -516
  111. package/dist/cjs/types.js +5 -0
  112. package/dist/esm/charsets.d.ts +1 -0
  113. package/{lib → dist/esm}/charsets.js +1 -6
  114. package/dist/esm/commands/append.d.ts +22 -0
  115. package/{lib → dist/esm}/commands/append.js +22 -52
  116. package/dist/esm/commands/authenticate.d.ts +24 -0
  117. package/{lib → dist/esm}/commands/authenticate.js +62 -87
  118. package/dist/esm/commands/capability.d.ts +8 -0
  119. package/{lib → dist/esm}/commands/capability.js +6 -9
  120. package/dist/esm/commands/close.d.ts +8 -0
  121. package/{lib → dist/esm}/commands/close.js +6 -10
  122. package/dist/esm/commands/compress.d.ts +8 -0
  123. package/{lib → dist/esm}/commands/compress.js +7 -11
  124. package/dist/esm/commands/copy.d.ts +13 -0
  125. package/{lib → dist/esm}/commands/copy.js +12 -20
  126. package/dist/esm/commands/copyuid-parser.d.ts +11 -0
  127. package/{lib → dist/esm}/commands/copyuid-parser.js +9 -15
  128. package/dist/esm/commands/create.d.ts +11 -0
  129. package/{lib → dist/esm}/commands/create.js +13 -27
  130. package/dist/esm/commands/delete.d.ts +11 -0
  131. package/{lib → dist/esm}/commands/delete.js +9 -14
  132. package/dist/esm/commands/enable.d.ts +9 -0
  133. package/{lib → dist/esm}/commands/enable.js +23 -30
  134. package/dist/esm/commands/esearch-parser.d.ts +17 -0
  135. package/dist/esm/commands/esearch-parser.js +88 -0
  136. package/dist/esm/commands/expunge.d.ts +12 -0
  137. package/{lib → dist/esm}/commands/expunge.js +17 -22
  138. package/dist/esm/commands/fetch.d.ts +30 -0
  139. package/{lib → dist/esm}/commands/fetch.js +32 -64
  140. package/dist/esm/commands/id.d.ts +10 -0
  141. package/{lib → dist/esm}/commands/id.js +17 -23
  142. package/dist/esm/commands/idle.d.ts +9 -0
  143. package/{lib → dist/esm}/commands/idle.js +47 -81
  144. package/dist/esm/commands/list.d.ts +16 -0
  145. package/{lib → dist/esm}/commands/list.js +56 -121
  146. package/dist/esm/commands/login.d.ts +11 -0
  147. package/{lib → dist/esm}/commands/login.js +10 -15
  148. package/dist/esm/commands/logout.d.ts +8 -0
  149. package/{lib → dist/esm}/commands/logout.js +9 -11
  150. package/dist/esm/commands/move.d.ts +13 -0
  151. package/{lib → dist/esm}/commands/move.js +13 -21
  152. package/dist/esm/commands/namespace.d.ts +25 -0
  153. package/{lib → dist/esm}/commands/namespace.js +34 -44
  154. package/dist/esm/commands/noop.d.ts +8 -0
  155. package/{lib → dist/esm}/commands/noop.js +6 -7
  156. package/dist/esm/commands/quota.d.ts +10 -0
  157. package/{lib → dist/esm}/commands/quota.js +18 -36
  158. package/dist/esm/commands/rename.d.ts +12 -0
  159. package/{lib → dist/esm}/commands/rename.js +10 -15
  160. package/dist/esm/commands/search.d.ts +15 -0
  161. package/{lib → dist/esm}/commands/search.js +36 -135
  162. package/dist/esm/commands/select.d.ts +25 -0
  163. package/{lib → dist/esm}/commands/select.js +33 -64
  164. package/dist/esm/commands/starttls.d.ts +8 -0
  165. package/{lib → dist/esm}/commands/starttls.js +6 -8
  166. package/dist/esm/commands/status-fields.d.ts +14 -0
  167. package/{lib → dist/esm}/commands/status-fields.js +5 -16
  168. package/dist/esm/commands/status.d.ts +12 -0
  169. package/{lib → dist/esm}/commands/status.js +18 -29
  170. package/dist/esm/commands/store.d.ts +19 -0
  171. package/{lib → dist/esm}/commands/store.js +24 -37
  172. package/dist/esm/commands/subscribe.d.ts +9 -0
  173. package/{lib → dist/esm}/commands/subscribe.js +8 -12
  174. package/dist/esm/commands/unsubscribe.d.ts +9 -0
  175. package/{lib → dist/esm}/commands/unsubscribe.js +8 -12
  176. package/dist/esm/connection-deadline.d.ts +49 -0
  177. package/{lib → dist/esm}/connection-deadline.js +14 -25
  178. package/dist/esm/errors.d.ts +83 -0
  179. package/dist/esm/errors.js +9 -0
  180. package/dist/esm/handler/imap-compiler.d.ts +24 -0
  181. package/{lib → dist/esm}/handler/imap-compiler.js +22 -80
  182. package/dist/esm/handler/imap-formal-syntax.d.ts +28 -0
  183. package/dist/esm/handler/imap-formal-syntax.js +117 -0
  184. package/dist/esm/handler/imap-handler.d.ts +9 -0
  185. package/dist/esm/handler/imap-handler.js +9 -0
  186. package/dist/esm/handler/imap-parser.d.ts +16 -0
  187. package/{lib → dist/esm}/handler/imap-parser.js +31 -44
  188. package/dist/esm/handler/imap-stream.d.ts +181 -0
  189. package/{lib → dist/esm}/handler/imap-stream.js +29 -121
  190. package/dist/esm/handler/limits.d.ts +25 -0
  191. package/{lib → dist/esm}/handler/limits.js +13 -22
  192. package/dist/esm/handler/parser-instance.d.ts +68 -0
  193. package/{lib → dist/esm}/handler/parser-instance.js +19 -47
  194. package/dist/esm/handler/token-parser.d.ts +91 -0
  195. package/{lib → dist/esm}/handler/token-parser.js +71 -155
  196. package/dist/esm/handler/types.d.ts +91 -0
  197. package/dist/esm/handler/types.js +3 -0
  198. package/dist/esm/imap-commands.d.ts +16 -0
  199. package/dist/esm/imap-commands.js +67 -0
  200. package/dist/esm/imap-flow.d.ts +676 -0
  201. package/{lib → dist/esm}/imap-flow.js +785 -1802
  202. package/dist/esm/jp-decoder.d.ts +12 -0
  203. package/{lib → dist/esm}/jp-decoder.js +6 -21
  204. package/dist/esm/limited-passthrough.d.ts +25 -0
  205. package/{lib → dist/esm}/limited-passthrough.js +7 -20
  206. package/dist/esm/logger.d.ts +3 -0
  207. package/dist/esm/logger.js +4 -0
  208. package/dist/esm/package-info.d.ts +3 -0
  209. package/dist/esm/package-info.js +4 -0
  210. package/dist/esm/package.json +3 -0
  211. package/dist/esm/proxy-connection.d.ts +33 -0
  212. package/{lib → dist/esm}/proxy-connection.js +56 -127
  213. package/dist/esm/search-compiler.d.ts +34 -0
  214. package/{lib → dist/esm}/search-compiler.js +54 -110
  215. package/dist/esm/special-use.d.ts +22 -0
  216. package/dist/esm/special-use.js +907 -0
  217. package/dist/esm/tools.d.ts +427 -0
  218. package/dist/esm/tools.js +1446 -0
  219. package/dist/esm/types.d.ts +828 -0
  220. package/dist/esm/types.js +4 -0
  221. package/package.json +60 -20
  222. package/.gitattributes +0 -1
  223. package/.github/CODE_OF_CONDUCT.md +0 -76
  224. package/.github/FUNDING.yml +0 -4
  225. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -40
  226. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -19
  227. package/.github/contributing.md +0 -17
  228. package/.github/workflows/release.yaml +0 -36
  229. package/.github/workflows/stale.yml +0 -29
  230. package/.github/workflows/test.yml +0 -51
  231. package/.ncurc.js +0 -4
  232. package/.prettierignore +0 -4
  233. package/.prettierrc.js +0 -8
  234. package/.release-please-manifest.json +0 -3
  235. package/CLAUDE.md +0 -104
  236. package/Gruntfile.js +0 -23
  237. package/eslint.config.js +0 -45
  238. package/lib/handler/imap-formal-syntax.js +0 -189
  239. package/lib/handler/imap-handler.js +0 -17
  240. package/lib/imap-commands.js +0 -45
  241. package/lib/logger.js +0 -5
  242. package/lib/special-use.js +0 -923
  243. package/lib/tools.js +0 -1612
  244. package/release-please-config.json +0 -10
  245. package/test/authentication-test.js +0 -101
  246. package/test/auto-idle-test.js +0 -470
  247. package/test/bodystructure-test.js +0 -899
  248. package/test/charsets-test.js +0 -161
  249. package/test/commands-branches-test.js +0 -1095
  250. package/test/commands-integration-test.js +0 -11124
  251. package/test/commands-test.js +0 -73
  252. package/test/connection-edge-cases-test.js +0 -1828
  253. package/test/connection-test.js +0 -162
  254. package/test/copyuid-parser-test.js +0 -173
  255. package/test/fetch-generator-test.js +0 -218
  256. package/test/fixtures/fake-timers.js +0 -115
  257. package/test/fixtures/serialized-mimetorture.js +0 -2738
  258. package/test/fixtures/test-client.js +0 -57
  259. package/test/fixtures/test-tls.js +0 -8
  260. package/test/handler-branches-test.js +0 -310
  261. package/test/idle-polling-test.js +0 -518
  262. package/test/imap-compiler-test.js +0 -809
  263. package/test/imap-flow-compress-test.js +0 -166
  264. package/test/imap-flow-coverage-test.js +0 -612
  265. package/test/imap-flow-fetch-download-test.js +0 -873
  266. package/test/imap-flow-internals-test.js +0 -725
  267. package/test/imap-flow-methods-test.js +0 -889
  268. package/test/imap-flow-proxy-paths-test.js +0 -366
  269. package/test/imap-flow-secure-test.js +0 -573
  270. package/test/imap-flow-server-test.js +0 -1474
  271. package/test/imap-formal-syntax-test.js +0 -293
  272. package/test/imap-parser-test.js +0 -1474
  273. package/test/imap-stream-edge-cases-test.js +0 -666
  274. package/test/imap-stream-test.js +0 -177
  275. package/test/imapflow-test.js +0 -258
  276. package/test/integration/README.md +0 -52
  277. package/test/integration/dovecot-test.conf +0 -27
  278. package/test/integration/rev2-live-test.js +0 -431
  279. package/test/integration/run-rev2-tests.sh +0 -75
  280. package/test/integration-test.js +0 -83
  281. package/test/jp-decoder-test.js +0 -304
  282. package/test/limited-passthrough-test.js +0 -299
  283. package/test/memory-cleanup-test.js +0 -144
  284. package/test/memory-leak-test.js +0 -667
  285. package/test/parser-limits-test.js +0 -292
  286. package/test/proxy-connection-test.js +0 -738
  287. package/test/reliability-improvements-test.js +0 -548
  288. package/test/search-compiler-test.js +0 -1300
  289. package/test/search-test.js +0 -329
  290. package/test/special-use-test.js +0 -418
  291. package/test/starttls-injection-test.js +0 -181
  292. package/test/tag-correlation-test.js +0 -333
  293. package/test/timer-policy-test.js +0 -227
  294. package/test/token-parser-test.js +0 -456
  295. package/test/tools-test.js +0 -2013
  296. package/test/unhandled-rejection-test.js +0 -593
@@ -0,0 +1,181 @@
1
+ import { Transform, type TransformCallback } from 'node:stream';
2
+ import type { Logger, InternalLogger } from '../types.js';
3
+ export interface ImapStreamOptions {
4
+ /** Connection identifier used for logging */
5
+ cid?: string | undefined;
6
+ /** A pino-compatible logger instance. If not provided, a default child logger is created */
7
+ logger?: Logger | InternalLogger | false | undefined;
8
+ /** If true, logs raw socket data at trace level */
9
+ logRaw?: boolean | undefined;
10
+ /** Whether the connection uses TLS */
11
+ secureConnection?: boolean | undefined;
12
+ /**
13
+ * Maximum allowed length (in bytes) of a single line (a response without a literal). Defaults
14
+ * to MAX_LITERAL_SIZE (1GB). Guards against a malicious or broken server that never sends a
15
+ * line terminator, which would otherwise grow the internal line buffer without bound. The line
16
+ * terminator counts toward the limit, and a line exactly at the limit is accepted. Exceeding it
17
+ * is terminal: the stream is destroyed with a `LineTooLarge` error and no further input is parsed.
18
+ */
19
+ maxLineLength?: number | undefined;
20
+ /**
21
+ * Maximum allowed size (in bytes) of a single literal block. Defaults to MAX_LITERAL_SIZE
22
+ * (1GB). Lower it to bound peak memory allocation against a malicious or broken server
23
+ * announcing an oversized literal. A literal exactly at the limit is accepted. Exceeding it is
24
+ * terminal: the stream is destroyed with a `LiteralTooLarge` error, the marker line is not
25
+ * emitted, and no byte of the rejected literal body is parsed as protocol.
26
+ */
27
+ maxLiteralSize?: number | undefined;
28
+ /**
29
+ * Maximum allowed total size (in bytes) of a single assembled response: every line segment and
30
+ * literal of one response combined. Defaults to MAX_RESPONSE_SIZE (2GB), which leaves room
31
+ * above the literal cap for a maximum-size literal plus its marker line. The per-line and
32
+ * per-literal caps alone cannot stop a server that spreads attacker-controlled bytes across an
33
+ * unbounded number of tokens of a single response. Declared literal sizes count when their
34
+ * marker is parsed, so an oversized total is rejected before the literal bytes arrive, and a
35
+ * line still being assembled counts against whatever budget is left. Exceeding the limit is
36
+ * terminal: the stream is destroyed with a `ResponseTooLarge` error and no further input is
37
+ * parsed.
38
+ */
39
+ maxResponseSize?: number | undefined;
40
+ }
41
+ /**
42
+ * A queued input chunk with the transform callback that releases it
43
+ */
44
+ export interface ImapStreamInputItem {
45
+ chunk: Buffer;
46
+ next: () => void;
47
+ released?: boolean | undefined;
48
+ }
49
+ /**
50
+ * A Transform stream that parses raw IMAP protocol data from a socket into structured
51
+ * command/response objects. Reads binary input, splits it into lines delimited by LF,
52
+ * extracts literal data blocks based on IMAP literal size markers (e.g., "{123}\r\n"),
53
+ * and emits each complete command as a readable object containing the payload Buffer
54
+ * and any associated literal Buffers. Enforces a maximum literal size of 1GB.
55
+ */
56
+ export declare class ImapStream extends Transform {
57
+ options: ImapStreamOptions;
58
+ cid: string | undefined;
59
+ log: InternalLogger;
60
+ readBytesCounter: number;
61
+ maxLineLength: number;
62
+ maxLiteralSize: number;
63
+ maxResponseSize: number;
64
+ state: number;
65
+ literalWaiting: number;
66
+ inputBuffer: Buffer[];
67
+ lineBuffer: Buffer[];
68
+ lineBytes: number;
69
+ literalBuffer: Buffer[];
70
+ literals: Buffer[];
71
+ responseBytes: number;
72
+ compress: boolean;
73
+ secureConnection: boolean | undefined;
74
+ processingInput: boolean;
75
+ inputQueue: ImapStreamInputItem[];
76
+ activeInput: ImapStreamInputItem | null;
77
+ pendingPush: (() => void) | null;
78
+ /**
79
+ * Creates a new ImapStream instance.
80
+ *
81
+ * @param options - Stream options, see ImapStreamOptions.
82
+ */
83
+ constructor(options?: ImapStreamOptions | undefined);
84
+ /**
85
+ * Terminally fails the stream. Used for response limit violations and for any other
86
+ * error raised while parsing.
87
+ *
88
+ * The stream is destroyed instead of only emitting `error`: emitting on a Transform leaves
89
+ * it running, so the caller would keep scanning the rejected payload and could emit it as
90
+ * protocol (an oversized literal body contains attacker-chosen CRLF delimited lines).
91
+ * Destroying stops all parsing, drops the offending line, and releases every queued
92
+ * transform callback exactly once (see `_destroy()`).
93
+ *
94
+ * `destroyed` (set synchronously by destroy()) is the single liveness flag every other path
95
+ * checks, so a second failure attempt is a no-op and nothing is parsed after the first.
96
+ *
97
+ * @param err - The error to destroy the stream with.
98
+ * @returns Always false, so callers can `return this.failStream(err)`.
99
+ */
100
+ failStream(err: Error): false;
101
+ /**
102
+ * Releases a queued input chunk's transform callback exactly once, signalling the writable
103
+ * side that the chunk was consumed. The mirror image of ImapFlow's releaseStreamData(), which
104
+ * releases the readable items this stream pushes downstream.
105
+ *
106
+ * @param item - Queue entry holding the chunk and its transform callback.
107
+ */
108
+ releaseInput(item: ImapStreamInputItem | null | undefined): void;
109
+ /**
110
+ * Checks whether the given line buffer ends with an IMAP literal size marker
111
+ * (e.g., "{123}\r\n"). If a valid marker is found and the literal size is within
112
+ * the allowed maximum, switches the stream state to LITERAL mode and records
113
+ * the expected number of literal bytes.
114
+ *
115
+ * @param line - The line buffer to check for a trailing literal marker.
116
+ * @returns True if a valid literal marker was found and literal state was activated, false otherwise.
117
+ */
118
+ checkLiteralMarker(line: Buffer): boolean;
119
+ /**
120
+ * Enforces the configured line-length cap for a projected line length. The projected length
121
+ * covers every byte of the line, the line terminator included, whether or not the line was
122
+ * split across input chunks. A line exactly at the limit is accepted.
123
+ *
124
+ * @param lineLength - Total length the current line would reach.
125
+ * @returns True if the line is within the limit, false if the stream was failed.
126
+ */
127
+ checkLineLength(lineLength: number): boolean;
128
+ /**
129
+ * Enforces the configured per-response size cap: the cumulative bytes of every line
130
+ * segment and declared literal of the response currently being assembled. Counting
131
+ * declared literal sizes at marker time means an oversized total is rejected before
132
+ * the literal bytes even arrive. The counter is reset when a response is emitted.
133
+ *
134
+ * @param additionalBytes - Bytes the next token would add to the response.
135
+ * @param peek - Measure only, without committing the bytes to the counter.
136
+ * Used for a line that is still being assembled: its bytes are committed once, when the
137
+ * line completes.
138
+ * @returns True if within the limit, false if the stream was failed.
139
+ */
140
+ checkResponseSize(additionalBytes: number, peek?: boolean | undefined): boolean;
141
+ /**
142
+ * Processes a single input chunk of raw data. In LINE state, scans for LF-terminated
143
+ * lines and checks for literal markers. In LITERAL state, collects the expected number
144
+ * of literal bytes. When a complete command (with all its literals) is assembled, it is
145
+ * pushed downstream as a readable object.
146
+ *
147
+ * @param chunk - The raw data chunk to process.
148
+ * @param startPos - The byte offset within the chunk to start processing from.
149
+ */
150
+ processInputChunk(chunk: Buffer, startPos?: number | undefined): Promise<void>;
151
+ /**
152
+ * Drains the input queue by processing each queued chunk sequentially.
153
+ * Yields to the event loop every 10 chunks to prevent CPU blocking on
154
+ * large bursts of incoming data.
155
+ */
156
+ processInput(): Promise<void>;
157
+ /**
158
+ * Transform stream implementation. Receives raw data chunks from the writable side,
159
+ * converts strings to Buffers, tracks total bytes read, optionally logs raw data,
160
+ * and queues the chunk for asynchronous processing.
161
+ *
162
+ * @param chunk - The incoming data chunk.
163
+ * @param encoding - The encoding if chunk is a string.
164
+ * @param next - Callback to signal that this chunk has been consumed.
165
+ */
166
+ _transform(chunk: Buffer | string, encoding: BufferEncoding, next: TransformCallback): void;
167
+ /**
168
+ * Flush implementation called when the writable side ends. Signals completion immediately.
169
+ *
170
+ * @param next - Callback to signal flush completion.
171
+ */
172
+ _flush(next: TransformCallback): void;
173
+ /**
174
+ * Destroy implementation for cleanup. Clears all internal buffers, drains the input queue
175
+ * by invoking pending callbacks, and forwards the error (if any) to the callback.
176
+ *
177
+ * @param err - The error that caused destruction, or null.
178
+ * @param callback - Callback to signal destruction completion.
179
+ */
180
+ _destroy(err: Error | null, callback: (error?: Error | null) => void): void;
181
+ }
@@ -0,0 +1,446 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ImapStream = void 0;
7
+ const node_stream_1 = require("node:stream");
8
+ const logger_js_1 = __importDefault(require("../logger.js"));
9
+ const limits_js_1 = require("./limits.js");
10
+ const LINE = 0x01;
11
+ const LITERAL = 0x02;
12
+ const LF = 0x0a;
13
+ const CR = 0x0d;
14
+ const NUM_0 = 0x30;
15
+ const NUM_9 = 0x39;
16
+ const CURLY_OPEN = 0x7b;
17
+ const CURLY_CLOSE = 0x7d;
18
+ /**
19
+ * A Transform stream that parses raw IMAP protocol data from a socket into structured
20
+ * command/response objects. Reads binary input, splits it into lines delimited by LF,
21
+ * extracts literal data blocks based on IMAP literal size markers (e.g., "{123}\r\n"),
22
+ * and emits each complete command as a readable object containing the payload Buffer
23
+ * and any associated literal Buffers. Enforces a maximum literal size of 1GB.
24
+ */
25
+ class ImapStream extends node_stream_1.Transform {
26
+ /**
27
+ * Creates a new ImapStream instance.
28
+ *
29
+ * @param options - Stream options, see ImapStreamOptions.
30
+ */
31
+ constructor(options) {
32
+ super({
33
+ //writableHighWaterMark: 3,
34
+ readableObjectMode: true,
35
+ writableObjectMode: false
36
+ });
37
+ this.options = options || {};
38
+ this.cid = this.options.cid;
39
+ this.log =
40
+ this.options.logger && typeof this.options.logger === 'object'
41
+ ? this.options.logger
42
+ : logger_js_1.default.child({
43
+ component: 'imap-connection',
44
+ cid: this.cid
45
+ });
46
+ this.readBytesCounter = 0;
47
+ // Maximum length of a single line (response without a literal). Bounds the line buffer
48
+ // so a server that never sends a line terminator cannot exhaust memory.
49
+ this.maxLineLength = (0, limits_js_1.normalizeLimit)(this.options.maxLineLength, limits_js_1.MAX_LINE_SIZE);
50
+ // Maximum size of a single literal block. Bounds peak memory allocation so a server
51
+ // announcing an oversized literal cannot exhaust memory.
52
+ this.maxLiteralSize = (0, limits_js_1.normalizeLimit)(this.options.maxLiteralSize, limits_js_1.MAX_LITERAL_SIZE);
53
+ this.maxResponseSize = (0, limits_js_1.normalizeLimit)(this.options.maxResponseSize, limits_js_1.MAX_RESPONSE_SIZE);
54
+ this.state = LINE;
55
+ this.literalWaiting = 0;
56
+ this.inputBuffer = []; // lines
57
+ this.lineBuffer = []; // current line
58
+ this.lineBytes = 0; // bytes currently buffered for the in-progress line
59
+ this.literalBuffer = [];
60
+ this.literals = [];
61
+ this.responseBytes = 0; // bytes accumulated for the in-progress response (lines + declared literals)
62
+ this.compress = false;
63
+ this.secureConnection = this.options.secureConnection;
64
+ this.processingInput = false;
65
+ this.inputQueue = []; // unprocessed input chunks
66
+ this.activeInput = null; // chunk currently being processed (already shifted off inputQueue)
67
+ // Resolver of the in-flight push() backpressure promise, so destruction can settle it
68
+ // instead of leaving processInput() awaiting a consumer that will never read again.
69
+ this.pendingPush = null;
70
+ }
71
+ /**
72
+ * Terminally fails the stream. Used for response limit violations and for any other
73
+ * error raised while parsing.
74
+ *
75
+ * The stream is destroyed instead of only emitting `error`: emitting on a Transform leaves
76
+ * it running, so the caller would keep scanning the rejected payload and could emit it as
77
+ * protocol (an oversized literal body contains attacker-chosen CRLF delimited lines).
78
+ * Destroying stops all parsing, drops the offending line, and releases every queued
79
+ * transform callback exactly once (see `_destroy()`).
80
+ *
81
+ * `destroyed` (set synchronously by destroy()) is the single liveness flag every other path
82
+ * checks, so a second failure attempt is a no-op and nothing is parsed after the first.
83
+ *
84
+ * @param err - The error to destroy the stream with.
85
+ * @returns Always false, so callers can `return this.failStream(err)`.
86
+ */
87
+ failStream(err) {
88
+ if (this.destroyed) {
89
+ return false;
90
+ }
91
+ this.destroy(err);
92
+ return false;
93
+ }
94
+ /**
95
+ * Releases a queued input chunk's transform callback exactly once, signalling the writable
96
+ * side that the chunk was consumed. The mirror image of ImapFlow's releaseStreamData(), which
97
+ * releases the readable items this stream pushes downstream.
98
+ *
99
+ * @param item - Queue entry holding the chunk and its transform callback.
100
+ */
101
+ releaseInput(item) {
102
+ if (!item || item.released) {
103
+ return;
104
+ }
105
+ item.released = true;
106
+ if (typeof item.next === 'function') {
107
+ item.next();
108
+ }
109
+ }
110
+ /**
111
+ * Checks whether the given line buffer ends with an IMAP literal size marker
112
+ * (e.g., "{123}\r\n"). If a valid marker is found and the literal size is within
113
+ * the allowed maximum, switches the stream state to LITERAL mode and records
114
+ * the expected number of literal bytes.
115
+ *
116
+ * @param line - The line buffer to check for a trailing literal marker.
117
+ * @returns True if a valid literal marker was found and literal state was activated, false otherwise.
118
+ */
119
+ checkLiteralMarker(line) {
120
+ if (!line || !line.length) {
121
+ return false;
122
+ }
123
+ let pos = line.length - 1;
124
+ if (line[pos] !== LF) {
125
+ return false;
126
+ }
127
+ pos--;
128
+ if (pos >= 0 && line[pos] === CR) {
129
+ pos--;
130
+ }
131
+ if (pos < 0 || !pos || line[pos] !== CURLY_CLOSE) {
132
+ return false;
133
+ }
134
+ pos--;
135
+ // Scan backwards through the line to find an IMAP literal marker: {size}\r\n
136
+ // The format is: '{' followed by one or more ASCII digits followed by '}'.
137
+ // Only the digit run's bounds are tracked - a single linear pass, unlike
138
+ // collecting digits into a growing array, which would make a line of n digits
139
+ // cost O(n^2). The run length is deliberately not capped: the RFC "number"
140
+ // production permits leading zeros, so a long digit run can still denote a
141
+ // small, valid size, and treating the marker as an ordinary line instead
142
+ // would feed the announced literal body to the line parser and desynchronize
143
+ // the session.
144
+ let digitsEnd = pos;
145
+ for (; pos >= 0; pos--) {
146
+ let c = line[pos];
147
+ if (c >= NUM_0 && c <= NUM_9) {
148
+ continue;
149
+ }
150
+ if (c === CURLY_OPEN && pos < digitsEnd) {
151
+ // Skip leading zeros so only the significant digits are converted: a
152
+ // marker padded with megabytes of zeros must not cost a string
153
+ // allocation and Number() parse of the whole run.
154
+ let digitsStart = pos + 1;
155
+ while (digitsStart < digitsEnd && line[digitsStart] === NUM_0) {
156
+ digitsStart++;
157
+ }
158
+ // More significant digits than any number64 has cannot fit any
159
+ // permissible maxLiteralSize; fail closed without materializing them
160
+ if (digitsEnd + 1 - digitsStart > 19) {
161
+ return this.failStream((0, limits_js_1.createLiteralTooLargeError)(Infinity, this.maxLiteralSize, 'the widest permissible literal size (19 digits)'));
162
+ }
163
+ const literalSize = Number(line.toString('latin1', digitsStart, digitsEnd + 1));
164
+ if (literalSize > this.maxLiteralSize) {
165
+ return this.failStream((0, limits_js_1.createLiteralTooLargeError)(literalSize, this.maxLiteralSize));
166
+ }
167
+ this.state = LITERAL;
168
+ this.literalWaiting = literalSize;
169
+ return true;
170
+ }
171
+ return false;
172
+ }
173
+ return false;
174
+ }
175
+ /**
176
+ * Enforces the configured line-length cap for a projected line length. The projected length
177
+ * covers every byte of the line, the line terminator included, whether or not the line was
178
+ * split across input chunks. A line exactly at the limit is accepted.
179
+ *
180
+ * @param lineLength - Total length the current line would reach.
181
+ * @returns True if the line is within the limit, false if the stream was failed.
182
+ */
183
+ checkLineLength(lineLength) {
184
+ if (lineLength <= this.maxLineLength) {
185
+ return true;
186
+ }
187
+ const err = new Error(`Line length ${lineLength} exceeds maximum allowed size of ${this.maxLineLength} bytes`);
188
+ err.code = 'LineTooLarge';
189
+ err.lineLength = lineLength;
190
+ err.maxSize = this.maxLineLength;
191
+ return this.failStream(err);
192
+ }
193
+ /**
194
+ * Enforces the configured per-response size cap: the cumulative bytes of every line
195
+ * segment and declared literal of the response currently being assembled. Counting
196
+ * declared literal sizes at marker time means an oversized total is rejected before
197
+ * the literal bytes even arrive. The counter is reset when a response is emitted.
198
+ *
199
+ * @param additionalBytes - Bytes the next token would add to the response.
200
+ * @param peek - Measure only, without committing the bytes to the counter.
201
+ * Used for a line that is still being assembled: its bytes are committed once, when the
202
+ * line completes.
203
+ * @returns True if within the limit, false if the stream was failed.
204
+ */
205
+ checkResponseSize(additionalBytes, peek) {
206
+ let total = this.responseBytes + additionalBytes;
207
+ if (total <= this.maxResponseSize) {
208
+ if (!peek) {
209
+ this.responseBytes = total;
210
+ }
211
+ return true;
212
+ }
213
+ const err = new Error(`Response size ${total} exceeds maximum allowed size of ${this.maxResponseSize} bytes`);
214
+ err.code = 'ResponseTooLarge';
215
+ err.responseSize = total;
216
+ err.maxSize = this.maxResponseSize;
217
+ return this.failStream(err);
218
+ }
219
+ /**
220
+ * Processes a single input chunk of raw data. In LINE state, scans for LF-terminated
221
+ * lines and checks for literal markers. In LITERAL state, collects the expected number
222
+ * of literal bytes. When a complete command (with all its literals) is assembled, it is
223
+ * pushed downstream as a readable object.
224
+ *
225
+ * @param chunk - The raw data chunk to process.
226
+ * @param startPos - The byte offset within the chunk to start processing from.
227
+ */
228
+ async processInputChunk(chunk, startPos) {
229
+ startPos = startPos || 0;
230
+ if (this.destroyed || startPos >= chunk.length) {
231
+ return;
232
+ }
233
+ switch (this.state) {
234
+ case LINE: {
235
+ let lineStart = startPos;
236
+ for (let i = startPos, len = chunk.length; i < len; i++) {
237
+ if (chunk[i] === LF) {
238
+ // line end found. Measure the completed line (terminator included) before
239
+ // concatenating or emitting anything, so the cap does not depend on where
240
+ // TCP chunk boundaries happen to fall.
241
+ let segment = chunk.slice(lineStart, i + 1);
242
+ if (!this.checkLineLength(this.lineBytes + segment.length)) {
243
+ return;
244
+ }
245
+ this.lineBuffer.push(segment);
246
+ lineStart = i + 1;
247
+ let line = this.lineBuffer.length === 1 ? this.lineBuffer[0] : Buffer.concat(this.lineBuffer);
248
+ this.lineBuffer = [];
249
+ this.lineBytes = 0;
250
+ // try to detect if this is a literal start. An oversized literal fails the
251
+ // stream, so the marker line must not be buffered before the check - it
252
+ // would otherwise be emitted as part of the rejected command.
253
+ let isLiteralMarker = this.checkLiteralMarker(line);
254
+ if (this.destroyed) {
255
+ return;
256
+ }
257
+ // Count the line itself and, for a literal marker, the declared
258
+ // literal bytes against the cumulative per-response budget, so a
259
+ // response assembled from many tokens stays bounded as a whole
260
+ if (!this.checkResponseSize(line.length + (isLiteralMarker ? this.literalWaiting : 0))) {
261
+ return;
262
+ }
263
+ this.inputBuffer.push(line);
264
+ if (isLiteralMarker) {
265
+ // switch into literal mode and start over
266
+ return await this.processInputChunk(chunk, lineStart);
267
+ }
268
+ // reached end of command input, emit it
269
+ let payload = this.inputBuffer.length === 1 ? this.inputBuffer[0] : Buffer.concat(this.inputBuffer);
270
+ let literals = this.literals;
271
+ this.inputBuffer = [];
272
+ this.literals = [];
273
+ this.responseBytes = 0;
274
+ if (payload.length) {
275
+ // remove final line terminator (\n or \r\n)
276
+ if (payload[payload.length - 1] === LF) {
277
+ let end = payload.length - 1;
278
+ if (end > 0 && payload[end - 1] === CR) {
279
+ end--;
280
+ }
281
+ payload = payload.slice(0, end);
282
+ }
283
+ if (payload.length) {
284
+ // Whether more buffered input already followed this command on the
285
+ // wire - more bytes in this chunk or another queued chunk. Captured
286
+ // per emitted command (immutable on the pushed object) so a later
287
+ // command cannot overwrite it; consumers that care about pipelining
288
+ // boundaries can read it from the pushed object.
289
+ let trailingAfterLine = lineStart < chunk.length || this.inputQueue.length > 0;
290
+ await new Promise(resolve => {
291
+ // Tracked so destruction can settle the wait instead of leaving
292
+ // this loop (and the chunk's transform callback) pending forever
293
+ // when the consumer stops reading.
294
+ this.pendingPush = resolve;
295
+ const item = { payload, literals, next: resolve, trailingAfterLine };
296
+ this.push(item);
297
+ });
298
+ this.pendingPush = null;
299
+ if (this.destroyed) {
300
+ return;
301
+ }
302
+ }
303
+ }
304
+ }
305
+ }
306
+ if (lineStart < chunk.length) {
307
+ // No line terminator was found in the remaining bytes; carry the tail over to
308
+ // the next chunk after measuring the line it belongs to.
309
+ let tail = chunk.slice(lineStart);
310
+ // The response counter is only committed when a line completes, so an
311
+ // in-progress line is measured against the remaining budget separately.
312
+ // Without this a response cap lowered to bound parser memory buys nothing
313
+ // while a server streams a line that never terminates - only the much
314
+ // larger line cap would hold it back.
315
+ if (!this.checkLineLength(this.lineBytes + tail.length) || !this.checkResponseSize(this.lineBytes + tail.length, true)) {
316
+ return;
317
+ }
318
+ this.lineBytes += tail.length;
319
+ this.lineBuffer.push(tail);
320
+ }
321
+ break;
322
+ }
323
+ case LITERAL: {
324
+ const remainingInChunk = chunk.length - startPos;
325
+ const bytesToRead = Math.min(remainingInChunk, this.literalWaiting);
326
+ const partial = startPos === 0 && bytesToRead === chunk.length ? chunk : chunk.slice(startPos, startPos + bytesToRead);
327
+ this.literalBuffer.push(partial);
328
+ this.literalWaiting -= bytesToRead;
329
+ if (this.literalWaiting === 0) {
330
+ this.literals.push(Buffer.concat(this.literalBuffer));
331
+ this.literalBuffer = [];
332
+ this.state = LINE;
333
+ if (remainingInChunk > bytesToRead) {
334
+ return await this.processInputChunk(chunk, startPos + bytesToRead);
335
+ }
336
+ }
337
+ break;
338
+ }
339
+ }
340
+ }
341
+ /**
342
+ * Drains the input queue by processing each queued chunk sequentially.
343
+ * Yields to the event loop every 10 chunks to prevent CPU blocking on
344
+ * large bursts of incoming data.
345
+ */
346
+ async processInput() {
347
+ let data;
348
+ let processedCount = 0;
349
+ while (!this.destroyed && (data = this.inputQueue.shift())) {
350
+ this.activeInput = data;
351
+ await this.processInputChunk(data.chunk);
352
+ this.activeInput = null;
353
+ // mark chunk as processed
354
+ this.releaseInput(data);
355
+ // Yield to event loop every 10 chunks to prevent CPU blocking
356
+ processedCount++;
357
+ if (processedCount % 10 === 0) {
358
+ await new Promise(resolve => setImmediate(resolve));
359
+ }
360
+ }
361
+ }
362
+ /**
363
+ * Transform stream implementation. Receives raw data chunks from the writable side,
364
+ * converts strings to Buffers, tracks total bytes read, optionally logs raw data,
365
+ * and queues the chunk for asynchronous processing.
366
+ *
367
+ * @param chunk - The incoming data chunk.
368
+ * @param encoding - The encoding if chunk is a string.
369
+ * @param next - Callback to signal that this chunk has been consumed.
370
+ */
371
+ _transform(chunk, encoding, next) {
372
+ if (typeof chunk === 'string') {
373
+ chunk = Buffer.from(chunk, encoding);
374
+ }
375
+ if (!chunk || !chunk.length) {
376
+ return next();
377
+ }
378
+ this.readBytesCounter += chunk.length;
379
+ if (this.options.logRaw) {
380
+ this.log.trace({
381
+ src: 's',
382
+ msg: 'read from socket',
383
+ data: chunk.toString('base64'),
384
+ compress: !!this.compress,
385
+ secure: !!this.secureConnection,
386
+ cid: this.cid
387
+ });
388
+ }
389
+ // A terminal parser failure must not accept any more protocol input, even if the
390
+ // transport delivers a chunk that was already in flight.
391
+ if (this.destroyed) {
392
+ return next();
393
+ }
394
+ // Queue the chunk for async processing. The 'next' callback serves as
395
+ // backpressure: it is called only after this chunk is fully processed,
396
+ // which signals the writable side that more data can be accepted.
397
+ this.inputQueue.push({ chunk, next });
398
+ if (!this.processingInput) {
399
+ this.processingInput = true;
400
+ this.processInput()
401
+ .catch(err => this.failStream(err))
402
+ .finally(() => (this.processingInput = false));
403
+ }
404
+ }
405
+ /**
406
+ * Flush implementation called when the writable side ends. Signals completion immediately.
407
+ *
408
+ * @param next - Callback to signal flush completion.
409
+ */
410
+ _flush(next) {
411
+ next();
412
+ }
413
+ /**
414
+ * Destroy implementation for cleanup. Clears all internal buffers, drains the input queue
415
+ * by invoking pending callbacks, and forwards the error (if any) to the callback.
416
+ *
417
+ * @param err - The error that caused destruction, or null.
418
+ * @param callback - Callback to signal destruction completion.
419
+ */
420
+ _destroy(err, callback) {
421
+ // Destruction is the single release point for parser-owned callbacks, so a terminal
422
+ // failure can never leave the writable side or the processing loop waiting.
423
+ this.inputBuffer = [];
424
+ this.lineBuffer = [];
425
+ this.lineBytes = 0;
426
+ this.literalBuffer = [];
427
+ this.literals = [];
428
+ this.responseBytes = 0;
429
+ // Settle an in-flight push() wait so processInput() can unwind
430
+ if (typeof this.pendingPush === 'function') {
431
+ const resolve = this.pendingPush;
432
+ this.pendingPush = null;
433
+ resolve();
434
+ }
435
+ // Release the chunk currently being processed, then everything still queued.
436
+ // releaseInput() is idempotent, so the processing loop releasing the same chunk
437
+ // afterwards is a no-op.
438
+ this.releaseInput(this.activeInput);
439
+ this.activeInput = null;
440
+ while (this.inputQueue.length) {
441
+ this.releaseInput(this.inputQueue.shift());
442
+ }
443
+ callback(err);
444
+ }
445
+ }
446
+ exports.ImapStream = ImapStream;
@@ -0,0 +1,25 @@
1
+ import type { ImapFlowError } from '../errors.js';
2
+ export declare const MAX_LITERAL_SIZE: number;
3
+ export declare const MAX_LINE_SIZE: number;
4
+ export declare const MAX_RESPONSE_SIZE: number;
5
+ /**
6
+ * Normalizes a configured size limit. A non-negative integer is honored as-is (including 0, which
7
+ * means "reject anything non-empty"), and `Infinity` disables the limit; anything else falls back
8
+ * to the default, so an explicit 0 is not silently swallowed the way `value || DEFAULT` would
9
+ * swallow it.
10
+ *
11
+ * @param value - The configured value.
12
+ * @param defaultValue - Fallback when the value is not a usable limit.
13
+ * @returns The normalized limit.
14
+ */
15
+ export declare const normalizeLimit: (value: unknown, defaultValue: number) => number;
16
+ /**
17
+ * Builds the `LiteralTooLarge` error. One shape for every place a literal is refused, so callers
18
+ * can rely on `code`, `literalSize` and `maxSize` regardless of which parser rejected it.
19
+ *
20
+ * @param literalSize - The declared literal size.
21
+ * @param maxSize - The bound that was exceeded.
22
+ * @param reason - What the bound was, when it is not the configured maximum.
23
+ * @returns The error to emit or throw.
24
+ */
25
+ export declare const createLiteralTooLargeError: (literalSize: number, maxSize: number, reason?: string | null | undefined) => ImapFlowError;