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,3949 @@
1
+ "use strict";
2
+ /**
3
+ * @module imapflow
4
+ */
5
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
6
+ if (k2 === undefined) k2 = k;
7
+ var desc = Object.getOwnPropertyDescriptor(m, k);
8
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
9
+ desc = { enumerable: true, get: function() { return m[k]; } };
10
+ }
11
+ Object.defineProperty(o, k2, desc);
12
+ }) : (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ o[k2] = m[k];
15
+ }));
16
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
17
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
18
+ }) : function(o, v) {
19
+ o["default"] = v;
20
+ });
21
+ var __importStar = (this && this.__importStar) || (function () {
22
+ var ownKeys = function(o) {
23
+ ownKeys = Object.getOwnPropertyNames || function (o) {
24
+ var ar = [];
25
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
26
+ return ar;
27
+ };
28
+ return ownKeys(o);
29
+ };
30
+ return function (mod) {
31
+ if (mod && mod.__esModule) return mod;
32
+ var result = {};
33
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
34
+ __setModuleDefault(result, mod);
35
+ return result;
36
+ };
37
+ })();
38
+ var __importDefault = (this && this.__importDefault) || function (mod) {
39
+ return (mod && mod.__esModule) ? mod : { "default": mod };
40
+ };
41
+ Object.defineProperty(exports, "__esModule", { value: true });
42
+ exports.ImapFlow = exports.AuthenticationFailure = void 0;
43
+ const node_tls_1 = __importDefault(require("node:tls"));
44
+ const node_net_1 = __importDefault(require("node:net"));
45
+ const node_crypto_1 = __importDefault(require("node:crypto"));
46
+ const node_zlib_1 = __importDefault(require("node:zlib"));
47
+ const node_events_1 = require("node:events");
48
+ const node_stream_1 = require("node:stream");
49
+ const libmime_1 = __importDefault(require("libmime"));
50
+ const libqp_1 = __importDefault(require("libqp"));
51
+ const libbase64_1 = __importDefault(require("libbase64"));
52
+ const mailsplit_1 = require("@zone-eu/mailsplit");
53
+ const flowed_decoder_js_1 = __importDefault(require("@zone-eu/mailsplit/lib/flowed-decoder.js"));
54
+ const logger_js_1 = __importDefault(require("./logger.js"));
55
+ const packageInfo = __importStar(require("./package-info.js"));
56
+ const limited_passthrough_js_1 = require("./limited-passthrough.js");
57
+ const imap_stream_js_1 = require("./handler/imap-stream.js");
58
+ const imap_handler_js_1 = require("./handler/imap-handler.js");
59
+ const proxy_connection_js_1 = require("./proxy-connection.js");
60
+ const connection_deadline_js_1 = require("./connection-deadline.js");
61
+ const errors_js_1 = require("./errors.js");
62
+ const imap_commands_js_1 = __importDefault(require("./imap-commands.js"));
63
+ const tools_js_1 = require("./tools.js");
64
+ var errors_js_2 = require("./errors.js");
65
+ Object.defineProperty(exports, "AuthenticationFailure", { enumerable: true, get: function () { return errors_js_2.AuthenticationFailure; } });
66
+ const GREETING_TIMEOUT = 16 * 1000;
67
+ const UPGRADE_TIMEOUT = 10 * 1000;
68
+ const SOCKET_TIMEOUT = 5 * 60 * 1000;
69
+ // Ceiling for any throttle back-off wait. Both the connection-level back-off and the per-command
70
+ // retries derive their delay from server-supplied hints, which are unbounded.
71
+ const MAX_THROTTLE_DELAY = 5 * 60 * 1000;
72
+ // Default threshold for warning that a mailbox lock has been held for a long
73
+ // time. Intended to catch forgotten release() calls, not legitimate long ops
74
+ // (e.g. fetching hundreds of thousands of messages). Configurable via the
75
+ // ImapFlow constructor option `maxLockHoldTime`. Set to 0 or false to disable.
76
+ const HELD_LOCK_WARN_MS = 30 * 60 * 1000;
77
+ // How long the connection has to stay inactive before auto-IDLE starts. Long enough that a caller
78
+ // running a sequence of commands is not interrupted by an IDLE it immediately has to break.
79
+ // Configurable via the ImapFlow constructor option `autoIdleDelay`.
80
+ const AUTO_IDLE_DELAY = 15 * 1000;
81
+ // Headroom kept between the auto-IDLE delay and the socket inactivity watchdog, so IDLE reaches
82
+ // the wire before the watchdog can fire. See normalizeAutoIdleDelay().
83
+ const AUTO_IDLE_SOCKET_MARGIN = 1000;
84
+ // Commands whose client frames carry credentials; the raw traffic log withholds frame content
85
+ // while one of these is in flight. See the logRaw branch in write().
86
+ const RAW_SENSITIVE_COMMANDS = new Set(['LOGIN', 'AUTHENTICATE']);
87
+ // Stand-in payload for a withheld raw client frame. Fixed width, so the entry says nothing
88
+ // about the length of what it replaced.
89
+ const RAW_HIDDEN_PLACEHOLDER = Buffer.from('(* value hidden *)\r\n').toString('base64');
90
+ // Whether any attribute of a command is marked as a secret. Recurses into nested lists because
91
+ // the command compiler honors `sensitive` at any depth, and the two must agree on what counts.
92
+ function hasSensitiveAttribute(attributes) {
93
+ return []
94
+ .concat(attributes || [])
95
+ .some(node => (Array.isArray(node) ? hasSensitiveAttribute(node) : !!node && typeof node === 'object' && !Buffer.isBuffer(node) && !!node.sensitive));
96
+ }
97
+ // How deep flattenLoggedError() follows a chain of errors. Bounded because the chain comes from
98
+ // whatever failed, not from this library: a cause chain can be arbitrarily long, and the cycle
99
+ // check below only catches errors that repeat.
100
+ const MAX_ERROR_FLATTEN_DEPTH = 4;
101
+ // Recognizes an Error without instanceof, which fails for an error that crossed a realm boundary
102
+ // (worker thread, vm context) even though it serializes exactly the same way.
103
+ function isErrorLike(value) {
104
+ return (value instanceof Error ||
105
+ (!!value && typeof value === 'object' && typeof value.message === 'string' && typeof value.stack === 'string'));
106
+ }
107
+ // An Error carries `message` and `stack` on its prototype rather than as own enumerable
108
+ // properties, so JSON.stringify() renders one as `{}` and both logger fallback paths (the console
109
+ // fallback and emitLogs) would drop everything identifying it. Flattening happens here for both,
110
+ // so their shapes cannot drift apart.
111
+ //
112
+ // Nested errors are flattened too, because the top level is often not where the answer is: this
113
+ // library attaches the underlying failure as an enumerable `_err` (proxy setup, response
114
+ // processing, normalized connection deadlines), and Node reports a multi-address connect failure
115
+ // as an AggregateError whose members hold the per-address causes.
116
+ function flattenLoggedError(value, depth = 0, seen = new Set()) {
117
+ if (depth >= MAX_ERROR_FLATTEN_DEPTH) {
118
+ return isErrorLike(value) ? value.message : value;
119
+ }
120
+ if (Array.isArray(value)) {
121
+ return value.map(entry => flattenLoggedError(entry, depth + 1, seen));
122
+ }
123
+ if (!isErrorLike(value)) {
124
+ // Anything else is left alone: exploding a Buffer would produce one key per byte, and a
125
+ // Date would become a pair of undefined fields.
126
+ return value;
127
+ }
128
+ // A repeat renders as its message alone, so a chain that loops back does not restate a full
129
+ // stack for every level down to the depth cap
130
+ if (seen.has(value)) {
131
+ return value.message;
132
+ }
133
+ seen.add(value);
134
+ let flatErr = {
135
+ message: value.message,
136
+ stack: value.stack
137
+ };
138
+ // `cause` (passed through the Error options argument) and the AggregateError members are own
139
+ // properties but not enumerable, so Object.keys does not list them
140
+ for (let key of new Set([...Object.keys(value), 'cause', 'errors'])) {
141
+ if (key in value) {
142
+ flatErr[key] = flattenLoggedError(value[key], depth + 1, seen);
143
+ }
144
+ }
145
+ return flatErr;
146
+ }
147
+ // The largest delay setTimeout can honor (2^31 - 1 ms). Anything above fires after 1 ms instead,
148
+ // so the auto-IDLE delay cap has to stay inside this range even when socketTimeout is not.
149
+ const MAX_TIMER_DELAY = 2 ** 31 - 1;
150
+ const stateValues = {
151
+ NOT_AUTHENTICATED: 0x01,
152
+ AUTHENTICATED: 0x02,
153
+ SELECTED: 0x03,
154
+ LOGOUT: 0x04
155
+ };
156
+ const states = stateValues;
157
+ /**
158
+ * Normalizes the configured auto-IDLE delay into a value `setTimeout` can honor. Anything Node
159
+ * would silently turn into a 1ms timer - NaN, a negative number, a value above the 32-bit range -
160
+ * falls back to the default instead, because a 1ms delay means an IDLE/DONE round trip around
161
+ * every single command. The delay is also capped below `socketTimeout`, see AUTO_IDLE_SOCKET_MARGIN.
162
+ *
163
+ * @param value - The configured `autoIdleDelay` option.
164
+ * @param socketTimeout - The normalized socket inactivity timeout.
165
+ * @param log - Logger, used to report a value that could not be used as given.
166
+ * @param cid - Connection id for the log entry.
167
+ * @returns Delay in milliseconds.
168
+ */
169
+ const normalizeAutoIdleDelay = (value, socketTimeout, log, cid) => {
170
+ const maxDelay = Math.max(0, Math.min(socketTimeout, MAX_TIMER_DELAY) - AUTO_IDLE_SOCKET_MARGIN);
171
+ const configured = value !== undefined && value !== null;
172
+ // Numeric strings are accepted, because configuration usually arrives from an environment
173
+ // variable or a JSON file. Booleans and blank strings are not: Number() would read them as 0,
174
+ // i.e. "IDLE around every command", the opposite of the "off" they suggest.
175
+ let delay = typeof value === 'number' || (typeof value === 'string' && value.trim()) ? Number(value) : NaN;
176
+ let reason = null;
177
+ if (!Number.isFinite(delay) || delay < 0) {
178
+ reason = 'not a non-negative finite number';
179
+ delay = AUTO_IDLE_DELAY;
180
+ }
181
+ if (delay > maxDelay) {
182
+ // An invalid value keeps its own reason: the cap then applies to the fallback default,
183
+ // not to anything the caller asked for.
184
+ reason = reason || `above socketTimeout (${socketTimeout} ms)`;
185
+ delay = maxDelay;
186
+ }
187
+ // Only an explicitly configured value is worth warning about. Capping the default because the
188
+ // caller picked a short socketTimeout is expected behavior, not a misconfiguration.
189
+ if (configured && reason) {
190
+ log.warn({ msg: 'Adjusted unusable autoIdleDelay option', requested: value, autoIdleDelay: delay, reason, cid });
191
+ }
192
+ return Math.floor(delay);
193
+ };
194
+ /**
195
+ * IMAP client class for accessing IMAP mailboxes
196
+ */
197
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
198
+ class ImapFlow extends node_events_1.EventEmitter {
199
+ /**
200
+ * Current module version as a static class property
201
+ */
202
+ static { this.version = packageInfo.version; }
203
+ constructor(options) {
204
+ super({ captureRejections: true });
205
+ this.options = options || {};
206
+ this.id = this.options.id || this.getRandomId();
207
+ this.clientInfo = Object.assign({
208
+ name: packageInfo.name,
209
+ version: packageInfo.version,
210
+ vendor: 'Postal Systems',
211
+ 'support-url': 'https://github.com/postalsys/imapflow/issues'
212
+ }, this.options.clientInfo || {});
213
+ // remove diacritics
214
+ for (let key of Object.keys(this.clientInfo)) {
215
+ if (typeof this.clientInfo[key] === 'string') {
216
+ this.clientInfo[key] = this.clientInfo[key].normalize('NFD').replace(/\p{Diacritic}/gu, '');
217
+ }
218
+ }
219
+ this.serverInfo = null; //updated by ID
220
+ this.log = this.getLogger();
221
+ this.secureConnection = !!this.options.secure;
222
+ // 993 is IMAPS, 143 is IMAP over cleartext/STARTTLS. The non-secure default used to be 110,
223
+ // which is POP3 - a client created without an explicit port could never connect.
224
+ this.port = Number(this.options.port) || (this.secureConnection ? 993 : 143);
225
+ this.host = this.options.host || 'localhost';
226
+ this.servername = this.options.servername ? this.options.servername : !node_net_1.default.isIP(this.host) ? this.host : false;
227
+ if (typeof this.options.secure === 'undefined' && this.port === 993) {
228
+ // if secure option is not set but port is 993, then default to secure
229
+ this.secureConnection = true;
230
+ }
231
+ // Normalized once so direct TLS, cleartext, proxied and STARTTLS-upgraded transports
232
+ // cannot end up with different inactivity watchdogs. As documented, 0 (and any other
233
+ // falsy or invalid value) means "use the default", not "disable".
234
+ this.socketTimeout = Number(this.options.socketTimeout) || SOCKET_TIMEOUT;
235
+ this.logRaw = this.options.logRaw;
236
+ this.streamer = new imap_stream_js_1.ImapStream({
237
+ logger: this.log,
238
+ cid: this.id,
239
+ logRaw: this.logRaw,
240
+ secureConnection: this.secureConnection,
241
+ maxLineLength: this.options.maxLineLength,
242
+ maxLiteralSize: this.options.maxLiteralSize,
243
+ maxResponseSize: this.options.maxResponseSize
244
+ });
245
+ this.reading = false;
246
+ this.socket = false;
247
+ this.writeSocket = false;
248
+ this._throttleWaits = new Set();
249
+ this._upgradeReject = null;
250
+ this.isClosed = false;
251
+ this.states = states;
252
+ this.state = this.states.NOT_AUTHENTICATED;
253
+ this.lockCounter = 0;
254
+ this.tagCounter = 0;
255
+ this.requestTagMap = new Map();
256
+ this.requestQueue = [];
257
+ this.currentRequest = false;
258
+ this._unknownTagCount = 0;
259
+ this._nextUnknownTagWarn = 1;
260
+ this.writeBytesCounter = 0;
261
+ this.commandParts = [];
262
+ this.rawSensitiveCommand = true;
263
+ this.capabilities = new Map();
264
+ this.authCapabilities = new Map();
265
+ this.rawCapabilities = null;
266
+ this.expectCapabilityUpdate = false; // force CAPABILITY after LOGIN
267
+ this._starttlsHadTrailingData = false;
268
+ this.enabled = new Set();
269
+ this.usable = false;
270
+ this.authenticated = false;
271
+ this.mailbox = false;
272
+ this.currentSelectCommand = false;
273
+ this.idling = false;
274
+ this.emitLogs = !!this.options.emitLogs;
275
+ this.lo = 0;
276
+ this.untaggedHandlers = {};
277
+ this.sectionHandlers = {};
278
+ this.commands = imap_commands_js_1.default;
279
+ this.folders = new Map();
280
+ this.currentLock = false;
281
+ this.locks = [];
282
+ this.idRequested = false;
283
+ this.maxIdleTime = this.options.maxIdleTime || false;
284
+ this.autoIdleDelay = normalizeAutoIdleDelay(this.options.autoIdleDelay, this.socketTimeout, this.log, this.id);
285
+ this._lastPollAt = 0;
286
+ this._openDownloads = 0;
287
+ this.missingIdleCommand = (this.options.missingIdleCommand || '').toString().toUpperCase().trim() || 'NOOP';
288
+ this.disableBinary = !!this.options.disableBinary;
289
+ this.skipListSubscribedArg = false;
290
+ this.skipListStatusArgs = false;
291
+ this.skipListAuxArgs = false;
292
+ this.skipLsub = false;
293
+ // Named error handler for proper cleanup. Certain error codes represent
294
+ // expected socket/network issues (buffer exhaustion, connection reset, broken pipe,
295
+ // timeout, unreachable host) that just need a silent connection close rather
296
+ // than emitting an error event to the caller.
297
+ this._streamerErrorHandler = (err) => {
298
+ if (['Z_BUF_ERROR', 'ECONNRESET', 'EPIPE', 'ETIMEDOUT', 'EHOSTUNREACH'].includes(err.code)) {
299
+ this.closeAfter();
300
+ return;
301
+ }
302
+ this.log.error({ err, cid: this.id });
303
+ this.emitError(err);
304
+ };
305
+ this.streamer.on('error', this._streamerErrorHandler);
306
+ this._connectCalled = false;
307
+ }
308
+ /** @internal */
309
+ emitError(err) {
310
+ if (!err) {
311
+ return;
312
+ }
313
+ err._connId = err._connId || this.id;
314
+ // During a STARTTLS handshake the upgrade owns the single error path (its settle()
315
+ // helper). Route the error there so a streamer-originated failure is surfaced with its
316
+ // real code (instead of a generic ClosedAfterConnect*) and cannot hang a verifyOnly
317
+ // connect() waiting on a 'close' that never rejects. Fall back to closing if the upgrade
318
+ // has no pending rejector.
319
+ if (this.upgrading) {
320
+ let reject = this._upgradeReject;
321
+ this._upgradeReject = null;
322
+ if (typeof reject === 'function') {
323
+ // settle() clears the upgrade timer and flags, and closes the connection
324
+ reject(err);
325
+ return;
326
+ }
327
+ this.upgrading = false;
328
+ this.closeAfter();
329
+ return;
330
+ }
331
+ // While the initial connect promise is still pending it owns error reporting:
332
+ // reject it once instead of emitting a duplicate 'error' event (which would also
333
+ // throw if the caller has not attached an 'error' listener yet).
334
+ if (typeof this.initialReject === 'function') {
335
+ let reject = this.initialReject;
336
+ this.initialResolve = false;
337
+ this.initialReject = false;
338
+ this.closeAfter();
339
+ reject(err);
340
+ return;
341
+ }
342
+ this.closeAfter();
343
+ this.emit('error', err);
344
+ }
345
+ /** @internal */
346
+ getRandomId() {
347
+ let rid = BigInt('0x' + node_crypto_1.default.randomBytes(13).toString('hex')).toString(36);
348
+ if (rid.length < 20) {
349
+ rid = '0'.repeat(20 - rid.length) + rid;
350
+ }
351
+ if (rid.length > 20) {
352
+ rid = rid.substr(0, 20);
353
+ }
354
+ return rid;
355
+ }
356
+ /** @internal */
357
+ write(chunk) {
358
+ if (!this.socket || this.socket.destroyed) {
359
+ // do not write after connection end or logout
360
+ throw this.createConnectionError('NoConnection', 'Socket is already closed', { rejectedFrom: 'writeNoSocket' });
361
+ }
362
+ if (this.state === this.states.LOGOUT) {
363
+ // should not happen
364
+ throw this.createConnectionError('StateLogout', 'Can not send data after logged out', { rejectedFrom: 'writeAfterLogout' });
365
+ }
366
+ if (this.writeSocket.destroyed) {
367
+ this.log.error({ msg: 'Write socket destroyed', cid: this.id });
368
+ this.close();
369
+ return;
370
+ }
371
+ // Append CRLF only to the final part of a command. When sending literals,
372
+ // commandParts holds the remaining parts (literal data, continuation); the CRLF
373
+ // delimiter is only added when no more parts remain (the command is complete).
374
+ let addLineBreak = !this.commandParts.length;
375
+ let data;
376
+ if (typeof chunk === 'string') {
377
+ if (addLineBreak) {
378
+ chunk += '\r\n';
379
+ }
380
+ data = Buffer.from(chunk, 'binary');
381
+ }
382
+ else if (Buffer.isBuffer(chunk)) {
383
+ if (addLineBreak) {
384
+ data = Buffer.concat([chunk, Buffer.from('\r\n')]);
385
+ }
386
+ else {
387
+ data = chunk;
388
+ }
389
+ }
390
+ else {
391
+ return false;
392
+ }
393
+ if (this.logRaw) {
394
+ // Client frames of an authentication exchange carry credentials: the LOGIN
395
+ // arguments, and for AUTHENTICATE also the continuation writes (SASL PLAIN
396
+ // response, AUTH=LOGIN password, OAuth token payload) that bypass send(). The
397
+ // parsed command log masks these, so the raw log must withhold them too, but
398
+ // `data` still carries the placeholder rather than being dropped - the field is
399
+ // part of the documented log format and consumers decode it unconditionally.
400
+ this.log.trace({
401
+ src: 'c',
402
+ msg: 'write to socket',
403
+ data: this.rawSensitiveCommand ? RAW_HIDDEN_PLACEHOLDER : data.toString('base64'),
404
+ ...(this.rawSensitiveCommand ? { hidden: true } : {}),
405
+ compress: !!this._deflate,
406
+ secure: !!this.secureConnection,
407
+ cid: this.id
408
+ });
409
+ }
410
+ this.writeBytesCounter += data.length;
411
+ this.writeSocket.write(data);
412
+ }
413
+ /**
414
+ * Returns byte counters for the current connection.
415
+ *
416
+ * @param reset If `true` then resets the byte counters after returning the current values
417
+ * @returns Byte counters: bytes sent to and received from the server
418
+ */
419
+ stats(reset) {
420
+ let result = {
421
+ sent: this.writeBytesCounter || 0,
422
+ received: (this.streamer && this.streamer.readBytesCounter) || 0
423
+ };
424
+ if (reset) {
425
+ this.writeBytesCounter = 0;
426
+ if (this.streamer) {
427
+ this.streamer.readBytesCounter = 0;
428
+ }
429
+ }
430
+ return result;
431
+ }
432
+ // Compiles and sends an IMAP command to the server. The command is compiled
433
+ // twice: once as an array (for sending, with literal data split into parts)
434
+ // and once as a string (for logging, with sensitive data masked).
435
+ // When LITERAL- or LITERAL+ extensions are available, the compiler can use
436
+ // non-synchronizing literals to avoid waiting for server "+" continuation.
437
+ /** @internal */
438
+ async send(data) {
439
+ if (this.state === this.states.LOGOUT) {
440
+ // already logged out
441
+ if (data.tag) {
442
+ let request = this.requestTagMap.get(data.tag);
443
+ if (request) {
444
+ this.requestTagMap.delete(data.tag);
445
+ request.reject(this.createNoConnectionError(false, { rejectedFrom: 'sendAfterLogout', command: request.command }));
446
+ }
447
+ }
448
+ return;
449
+ }
450
+ // Classify before the first await. Every frame of this command - the command line and
451
+ // any continuation write that follows it - belongs to it until the next send(), because
452
+ // trySend() keeps one command in flight at a time. Reading currentRequest inside write()
453
+ // instead would be racy: rejectCurrentRequest() can clear it while the two compiler
454
+ // awaits below are pending, and the credential frame would then be logged in the clear.
455
+ // Uppercased because the wire protocol is case-insensitive and exec() passes the
456
+ // caller's spelling through unchanged. The command list covers the mechanisms whose
457
+ // secret arrives in a continuation frame, which carries no attributes of its own; the
458
+ // `sensitive` marker catches anything that instead puts a secret on the command line,
459
+ // so marking an attribute is enough to keep a new command out of the raw log too.
460
+ this.rawSensitiveCommand =
461
+ RAW_SENSITIVE_COMMANDS.has(typeof data.command === 'string' ? data.command.toUpperCase() : '') || hasSensitiveAttribute(data.attributes);
462
+ // Compile with asArray=true: splits output into parts for literal handling.
463
+ // First part is the command text up to the first literal, remaining parts
464
+ // are stored in this.commandParts and sent after server "+" continuations.
465
+ let compiled = await (0, imap_handler_js_1.compiler)(data, {
466
+ asArray: true,
467
+ // LITERAL- is part of base IMAP4rev2
468
+ literalMinus: (0, tools_js_1.hasCapability)(this, 'LITERAL-') || this.capabilities.has('LITERAL+')
469
+ });
470
+ this.commandParts = compiled;
471
+ // Compile again for logging with isLogging=true: masks sensitive values
472
+ // like passwords while producing a human-readable command string
473
+ let logCompiled = await (0, imap_handler_js_1.compiler)(data, {
474
+ isLogging: true
475
+ });
476
+ /* c8 ignore next */ // send() is always invoked with a request object carrying options, so the {} fallback is unreachable
477
+ let options = data.options || {};
478
+ this.log.debug({ src: 'c', msg: logCompiled.toString(), cid: this.id, comment: options.comment });
479
+ // Send the first part (command text). If there are literal parts,
480
+ // the server will respond with "+" continuations and reader() will
481
+ // send each remaining part from this.commandParts.
482
+ this.write(this.commandParts.shift());
483
+ // The command is on the wire now. Tagged-response correlation requires this, so a server
484
+ // that guesses the next (sequential) tag cannot settle a command during the window between
485
+ // it becoming current and actually being written.
486
+ if (this.currentRequest && this.currentRequest.tag === data.tag) {
487
+ this.currentRequest.sent = true;
488
+ }
489
+ if (typeof options.onSend === 'function') {
490
+ // The command is already on the wire, so a throwing onSend callback must not
491
+ // reach trySend()'s catch - that would reject the request and dispatch the
492
+ // next command into the server's pending state for this one.
493
+ try {
494
+ options.onSend();
495
+ }
496
+ catch (err) {
497
+ this.log.warn({ err, cid: this.id });
498
+ }
499
+ }
500
+ }
501
+ /** @internal */
502
+ async trySend() {
503
+ while (!this.currentRequest && this.requestQueue.length) {
504
+ this.currentRequest = this.requestQueue.shift();
505
+ try {
506
+ await this.send({
507
+ tag: this.currentRequest.tag,
508
+ command: this.currentRequest.command,
509
+ attributes: this.currentRequest.attributes,
510
+ options: this.currentRequest.options
511
+ });
512
+ return;
513
+ }
514
+ catch (err) {
515
+ // A failure here (most likely the compiler refusing an invalid
516
+ // user-supplied value) belongs to the command that was being dispatched.
517
+ // Without this the shifted request would stay currentRequest forever:
518
+ // nothing reached the wire, so no tagged response ever clears it, and
519
+ // every later command would queue behind it until the socket timeout.
520
+ // Reject the failed command and keep draining the queue.
521
+ this.commandParts = [];
522
+ this.rejectCurrentRequest(err);
523
+ }
524
+ }
525
+ }
526
+ /** @internal */
527
+ exec(command, attributes, options) {
528
+ if (this.state === this.states.LOGOUT || this.isClosed) {
529
+ return (0, tools_js_1.guardedReject)(this.createNoConnectionError(false, { rejectedFrom: 'execClosed', command }));
530
+ }
531
+ if (!this.socket || this.socket.destroyed) {
532
+ return (0, tools_js_1.guardedReject)(this.createConnectionError('EConnectionClosed', 'Connection closed', { rejectedFrom: 'execNoSocket', command }));
533
+ }
534
+ let tag = (++this.tagCounter).toString(16).toUpperCase();
535
+ let execOptions = options || {};
536
+ // Guarded: close() rejects this request synchronously, possibly before the caller has
537
+ // attached its handler. See guardedPromise().
538
+ return (0, tools_js_1.guardedPromise)((resolve, reject) => {
539
+ this.requestTagMap.set(tag, { command, attributes, options: execOptions, resolve, reject });
540
+ this.requestQueue.push({ tag, command, attributes, options: execOptions });
541
+ // trySend() settles dispatch failures itself, by rejecting the affected
542
+ // command through requestTagMap; this catch exists only so a throw from the
543
+ // dispatch machinery itself can never surface as a floating rejection.
544
+ this.trySend().catch(err => (0, tools_js_1.logConnectionError)(this, 'Failed to dispatch command', err));
545
+ });
546
+ }
547
+ // Resolves an untagged server response to the keyword it is dispatched on. IMAP untagged
548
+ // responses come in two forms:
549
+ // * CAPABILITY ... (keyword as command)
550
+ // * 42 FETCH (...) (numeric prefix + keyword)
551
+ // For numeric-prefixed responses the keyword sits in the first attribute, because `command`
552
+ // holds the sequence number. Also used for logging, so a failure reports FETCH rather than
553
+ // the message number that happened to precede it.
554
+ /** @internal */
555
+ normalizeUntaggedCommand(command, attributes) {
556
+ if (/^[0-9]+$/.test(command)) {
557
+ let type = attributes && attributes.length && typeof attributes[0].value === 'string'
558
+ ? attributes[0].value.toUpperCase()
559
+ : false;
560
+ if (type) {
561
+ command = type;
562
+ }
563
+ }
564
+ return command.toUpperCase().trim();
565
+ }
566
+ // Handler priority: command-specific handlers (registered per exec() call) take
567
+ // precedence over global handlers (registered on the connection).
568
+ /** @internal */
569
+ getUntaggedHandler(command, attributes) {
570
+ command = this.normalizeUntaggedCommand(command, attributes);
571
+ // Check command-specific handler first (registered in exec() options.untagged)
572
+ if (this.currentRequest && this.currentRequest.options && this.currentRequest.options.untagged && this.currentRequest.options.untagged[command]) {
573
+ return this.currentRequest.options.untagged[command];
574
+ }
575
+ // Fall back to global handler (e.g., for CAPABILITY, BYE, etc.)
576
+ let handler = this.untaggedHandlers[command];
577
+ if (handler) {
578
+ return handler;
579
+ }
580
+ }
581
+ /** @internal */
582
+ getSectionHandler(key) {
583
+ if (this.sectionHandlers[key]) {
584
+ return this.sectionHandlers[key];
585
+ }
586
+ }
587
+ // Releases a readable stream item exactly once. The item's `next` callback is the parser's
588
+ // backpressure token: until it is called, ImapStream stops feeding the connection. Every
589
+ // path out of response handling - success, handled error, or unexpected throw - has to go
590
+ // through here, otherwise the parser stalls permanently.
591
+ /** @internal */
592
+ releaseStreamData(data) {
593
+ if (!data || data.released) {
594
+ return;
595
+ }
596
+ data.released = true;
597
+ if (typeof data.next === 'function') {
598
+ data.next();
599
+ }
600
+ }
601
+ // Records a tagged response whose tag was never issued by this connection. ImapFlow talks
602
+ // to a wide range of non-conforming servers, so this is tolerated rather than terminal, but
603
+ // it must not pass silently. Warnings are emitted for the first occurrence and then at
604
+ // powers of two so a server spraying stray tagged lines cannot flood the log, while the
605
+ // counter itself stays exact and is reported when the connection closes.
606
+ /** @internal */
607
+ countUnknownTag(tag) {
608
+ if (this.isClosed) {
609
+ // teardown crossover, not a server compatibility signal
610
+ return;
611
+ }
612
+ this._unknownTagCount++;
613
+ if (this._unknownTagCount === this._nextUnknownTagWarn) {
614
+ this._nextUnknownTagWarn *= 2;
615
+ this.log.warn({
616
+ msg: 'Tagged response for an unknown tag',
617
+ tag,
618
+ unknownTagCount: this._unknownTagCount,
619
+ cid: this.id
620
+ });
621
+ }
622
+ }
623
+ // Terminally fails the connection on a protocol violation: stop parsing, then report. Both
624
+ // steps are explicit here rather than destroying the parser *with* the error and relying on
625
+ // its error listener to report, so the reporting path does not depend on teardown ordering or
626
+ // on the streamer error handler's suppression list.
627
+ /** @internal */
628
+ failProtocol(err) {
629
+ if (this.streamer && !this.streamer.destroyed) {
630
+ // Destroyed without an error: nothing after a protocol violation may reach
631
+ // application state, and emitError() below owns reporting.
632
+ this.streamer.destroy();
633
+ }
634
+ this.emitError(err);
635
+ }
636
+ // Rejects the in-flight request, if any, exactly once. Used when response handling fails in
637
+ // a way that leaves the command's outcome unknown.
638
+ /** @internal */
639
+ rejectCurrentRequest(err) {
640
+ if (!this.currentRequest) {
641
+ return;
642
+ }
643
+ let tag = this.currentRequest.tag;
644
+ this.currentRequest = false;
645
+ let request = this.requestTagMap.get(tag);
646
+ if (request) {
647
+ this.requestTagMap.delete(tag);
648
+ request.reject(err);
649
+ }
650
+ }
651
+ /**
652
+ * Waits out a throttle back-off.
653
+ *
654
+ * The delay is capped at MAX_THROTTLE_DELAY because it can come straight from a server hint
655
+ * (a Microsoft 365 "Suggested Backoff Time", say) and an uncapped hint would park the caller
656
+ * for weeks. The timer is unref'd and tracked so it can never outlive the client: a bare
657
+ * setTimeout here keeps a short-lived process alive for the full delay after close(), and
658
+ * leaves the caller waiting on a connection that is already gone.
659
+ *
660
+ * @param delay - Requested delay in milliseconds.
661
+ * @returns True if close() aborted the wait, false on normal expiry.
662
+ * @internal
663
+ */
664
+ async throttleWait(delay) {
665
+ delay = Math.min(Math.max(Number(delay) || 0, 0), MAX_THROTTLE_DELAY);
666
+ return await new Promise(resolve => {
667
+ let entry = { resolve };
668
+ entry.timer = setTimeout(() => {
669
+ this._throttleWaits.delete(entry);
670
+ resolve(false);
671
+ }, delay);
672
+ (0, tools_js_1.unrefTimer)(entry.timer);
673
+ this._throttleWaits.add(entry);
674
+ });
675
+ }
676
+ /** @internal */
677
+ async reader() {
678
+ let data;
679
+ let processedCount = 0;
680
+ while ((data = this.streamer.read()) !== null) {
681
+ let keepReading;
682
+ try {
683
+ keepReading = await this.handleResponse(data);
684
+ }
685
+ catch (err) {
686
+ // Response handling past the parse step (log compilation, response shape
687
+ // assumptions, an untagged handler bug) must never throw out of this loop: the
688
+ // parser would keep waiting on its backpressure callback forever, which is a
689
+ // silent permanent hang. Fail closed instead.
690
+ keepReading = false;
691
+ let error = new Error('Failed to process server response');
692
+ error.code = 'ResponseProcessingFailed';
693
+ error._err = err;
694
+ this.log.error({ msg: 'Failed to process server response', err, cid: this.id });
695
+ this.rejectCurrentRequest(error);
696
+ this.failProtocol(error);
697
+ }
698
+ finally {
699
+ this.releaseStreamData(data);
700
+ }
701
+ if (!keepReading) {
702
+ return;
703
+ }
704
+ // Yield to event loop every 10 processed messages to prevent CPU blocking
705
+ processedCount++;
706
+ if (processedCount % 10 === 0) {
707
+ await new Promise(resolve => setImmediate(resolve));
708
+ }
709
+ }
710
+ }
711
+ /**
712
+ * Fails the in-flight command when a line that could not be parsed was addressed to its tag.
713
+ * Only the leading tag is read from the raw payload - the rest of the line is by definition
714
+ * not trustworthy - and only the command that is actually on the wire may be settled this way,
715
+ * the same invariant the parsed tagged-response path enforces.
716
+ *
717
+ * @param payload - Raw bytes of the line that failed to parse.
718
+ * @param parserError - The error the parser raised.
719
+ * @internal
720
+ */
721
+ rejectUnparsedCompletion(payload, parserError) {
722
+ if (!this.currentRequest || !this.currentRequest.sent) {
723
+ return;
724
+ }
725
+ // Prefer the tag the parser had already extracted before it failed - it went
726
+ // through the same leading-NUL workaround as every parsed response. Fall back
727
+ // to the raw bytes for lines whose tag itself was unparseable: skip the NUL
728
+ // padding buggy servers prepend and stop at the first byte a tag cannot contain.
729
+ let tag = parserError && parserError.parsedTag;
730
+ if (!tag) {
731
+ let match = payload.toString('latin1', 0, 64).match(/^\0*([^\s\x00-\x1f\x7f]+)/);
732
+ tag = match && match[1];
733
+ }
734
+ if (!tag || tag !== this.currentRequest.tag) {
735
+ return;
736
+ }
737
+ let err = new Error('Failed to parse the server response for this command');
738
+ err.code = parserError.code || 'ParserError';
739
+ err.parserError = parserError;
740
+ this.rejectCurrentRequest(err);
741
+ this.trySend().catch(sendErr => (0, tools_js_1.logConnectionError)(this, 'Failed to dispatch command', sendErr));
742
+ }
743
+ /**
744
+ * Handles a single parsed server response: telemetry, continuation requests, response-code
745
+ * section handlers, untagged handlers and tagged command completion.
746
+ *
747
+ * @param data - Readable item from the parser stream.
748
+ * @returns `true` to keep reading, `false` to stop (connection is failing).
749
+ * @internal
750
+ */
751
+ async handleResponse(data) {
752
+ let parsed;
753
+ try {
754
+ parsed = await (0, imap_handler_js_1.parser)(data.payload, { literals: data.literals });
755
+ }
756
+ catch (err) {
757
+ // can not make sense of this. The payload can be up to the configured line
758
+ // cap (1GB by default), so log only a bounded prefix: a server looping
759
+ // unparseable garbage would otherwise turn this error log into a disk filler.
760
+ this.log.error({ src: 's', msg: data.payload.toString('latin1', 0, 1024), payloadBytes: data.payload.length, err, cid: this.id });
761
+ // An unparseable untagged line is junk that can be skipped, but the line may
762
+ // have been the in-flight command's tagged completion. Dropping that one
763
+ // silently strands the command: currentRequest is never cleared, so trySend()
764
+ // stops dispatching and every later command queues behind it until the socket
765
+ // timeout fires. The tag is recovered from the raw bytes (a tag is
766
+ // ASTRING-CHAR only, so it survives whatever made the rest unparseable) and
767
+ // the command is failed with the parser error instead of hanging.
768
+ this.rejectUnparsedCompletion(data.payload, err);
769
+ return true;
770
+ }
771
+ if (parsed.tag && !['*', '+'].includes(parsed.tag) && parsed.command) {
772
+ let payload = { response: parsed.command };
773
+ if (parsed.attributes &&
774
+ parsed.attributes[0] &&
775
+ parsed.attributes[0].section &&
776
+ parsed.attributes[0].section[0] &&
777
+ parsed.attributes[0].section[0].type === 'ATOM') {
778
+ payload.code = parsed.attributes[0].section[0].value;
779
+ }
780
+ // Outside the parse try/catch on purpose: a throwing user 'response' listener
781
+ // is not a parse failure and must not settle the in-flight command or fail the
782
+ // connection - the same contract untagged handlers get.
783
+ try {
784
+ this.emit('response', payload);
785
+ }
786
+ catch (err) {
787
+ this.log.warn({ err, cid: this.id });
788
+ }
789
+ }
790
+ let logCompiled = await (0, imap_handler_js_1.compiler)(parsed, {
791
+ isLogging: true
792
+ });
793
+ if (/^\d+$/.test(parsed.command || '') && parsed.attributes && parsed.attributes[0] && parsed.attributes[0].value === 'FETCH') {
794
+ // too many FETCH responses, might want to filter these out
795
+ this.log.trace({ src: 's', msg: logCompiled.toString(), cid: this.id, nullBytesRemoved: parsed.nullBytesRemoved });
796
+ }
797
+ else {
798
+ this.log.debug({ src: 's', msg: logCompiled.toString(), cid: this.id, nullBytesRemoved: parsed.nullBytesRemoved });
799
+ }
800
+ // IMAP "+" (continuation request) handling. The server sends "+" in two cases:
801
+ // 1. During IDLE or AUTHENTICATE, where a custom handler (onPlusTag) processes it
802
+ // 2. During literal data transfer, where we send the next queued literal chunk
803
+ if (parsed.tag === '+' && this.currentRequest && this.currentRequest.options && typeof this.currentRequest.options.onPlusTag === 'function') {
804
+ try {
805
+ await this.currentRequest.options.onPlusTag(parsed);
806
+ }
807
+ catch (err) {
808
+ // The handler ran across an await and may have closed the connection, which
809
+ // clears currentRequest, so the command name is read defensively
810
+ this.log.warn({
811
+ msg: 'Failed to process continuation response',
812
+ command: this.currentRequest ? this.currentRequest.command : undefined,
813
+ err,
814
+ cid: this.id
815
+ });
816
+ }
817
+ return true;
818
+ }
819
+ // Server acknowledged our literal size with "+", send the actual literal data
820
+ if (parsed.tag === '+' && this.commandParts.length) {
821
+ let content = this.commandParts.shift();
822
+ // A write() failure here (e.g. socket closed mid-command) must not fail the whole
823
+ // connection; the command's own tagged response or the close path reports it.
824
+ try {
825
+ this.write(content);
826
+ this.log.debug({ src: 'c', msg: `(* ${content.length}B continuation *)`, cid: this.id });
827
+ }
828
+ catch (err) {
829
+ (0, tools_js_1.logConnectionError)(this, 'Failed to send literal continuation', err);
830
+ }
831
+ return true;
832
+ }
833
+ let section = parsed.attributes && parsed.attributes.length && parsed.attributes[0] && !parsed.attributes[0].value && parsed.attributes[0].section;
834
+ // section[0] can be a parsed NIL (null), e.g. from a "[NIL]" response code - the
835
+ // dereference must be guarded or one such line tears down the whole connection
836
+ if (section && section.length && section[0] && section[0].type === 'ATOM' && typeof section[0].value === 'string') {
837
+ let sectionKey = section[0].value.toUpperCase().trim();
838
+ let sectionHandler = this.getSectionHandler(sectionKey);
839
+ if (sectionHandler) {
840
+ try {
841
+ await sectionHandler(section.slice(1));
842
+ }
843
+ catch (err) {
844
+ this.log.warn({ msg: 'Failed to process response section', section: sectionKey, err, cid: this.id });
845
+ }
846
+ }
847
+ }
848
+ if (parsed.tag === '*' && parsed.command) {
849
+ let untaggedHandler = this.getUntaggedHandler(parsed.command, parsed.attributes);
850
+ if (untaggedHandler) {
851
+ try {
852
+ await untaggedHandler(parsed);
853
+ }
854
+ catch (err) {
855
+ // Normalized only here: this runs for every untagged response, including
856
+ // every FETCH, and the keyword is needed only to describe a failure
857
+ this.log.warn({
858
+ msg: 'Failed to process untagged response',
859
+ command: this.normalizeUntaggedCommand(parsed.command, parsed.attributes),
860
+ err,
861
+ cid: this.id
862
+ });
863
+ return true;
864
+ }
865
+ }
866
+ }
867
+ // Tagged response correlation. A tagged response may only complete the command that was
868
+ // actually written to the socket (invariant 2), so the three cases below are kept apart:
869
+ // the active command completes, a command that has not been written yet is proof of
870
+ // desynchronization (queued behind another command, or current but not yet on the wire),
871
+ // and an entirely unknown tag is recorded but tolerated.
872
+ if (parsed.tag && !['*', '+'].includes(parsed.tag)) {
873
+ if (this.currentRequest && this.currentRequest.tag === parsed.tag && this.currentRequest.sent) {
874
+ let request = this.requestTagMap.get(parsed.tag);
875
+ this.requestTagMap.delete(parsed.tag);
876
+ this.currentRequest = false;
877
+ if (request) {
878
+ await this.settleRequest(request, parsed, !!data.trailingAfterLine);
879
+ }
880
+ // Send the next queued command only after the completed command's handler has
881
+ // applied its own state (e.g. select.ts publishing the new mailbox), so the next
882
+ // command cannot reach the wire against half-updated state. A failure here must
883
+ // not propagate, or the whole connection would be failed over a send error that
884
+ // the command's own promise already reports.
885
+ // Note: on a rejected command the handler's catch block runs on its own microtask
886
+ // chain, so only the success path is fully ordered.
887
+ try {
888
+ await this.trySend();
889
+ }
890
+ catch (err) {
891
+ this.log.warn({ err, cid: this.id });
892
+ }
893
+ }
894
+ else if (this.requestTagMap.has(parsed.tag)) {
895
+ // The server answered a command that has not been written to the socket yet.
896
+ // Continuing would report unsent mutations as successful and leave every later
897
+ // response ambiguous, so reject this request and fail the connection closed.
898
+ let request = this.requestTagMap.get(parsed.tag);
899
+ this.requestTagMap.delete(parsed.tag);
900
+ let err = new Error('Server sent a tagged response for a command that was not in flight');
901
+ err.code = 'UnexpectedTag';
902
+ err.details = {
903
+ received: parsed.tag,
904
+ expected: this.currentRequest ? this.currentRequest.tag : null
905
+ };
906
+ this.log.error({ msg: 'Protocol desynchronization', err, cid: this.id });
907
+ request.reject(err);
908
+ this.failProtocol(err);
909
+ return false;
910
+ }
911
+ else {
912
+ this.countUnknownTag(parsed.tag);
913
+ }
914
+ }
915
+ return true;
916
+ }
917
+ /**
918
+ * Settles a request with its tagged completion response.
919
+ *
920
+ * On success the returned promise stays pending until the command handler calls `next()` on
921
+ * the response, which is what orders state application before the next queued command is
922
+ * dispatched. A command handler must therefore always release its own response before
923
+ * awaiting another command on the same connection.
924
+ *
925
+ * @param request - Pending request entry (resolve/reject and the compiled command).
926
+ * @param parsed - Parsed tagged response.
927
+ * @param hasTrailingData - Whether more input was already buffered after this line.
928
+ * @internal
929
+ */
930
+ async settleRequest(request, parsed, hasTrailingData) {
931
+ switch ((parsed.command || '').toUpperCase()) {
932
+ case 'OK':
933
+ case 'BYE':
934
+ // hasTrailingData is forwarded so STARTTLS can detect a plaintext
935
+ // injection (data buffered after the tagged OK, before the handshake).
936
+ await new Promise(resolve => request.resolve({ response: parsed, next: resolve, hasTrailingData }));
937
+ break;
938
+ case 'NO':
939
+ case 'BAD': {
940
+ let txt = parsed.attributes &&
941
+ parsed.attributes
942
+ .filter(val => val.type === 'TEXT')
943
+ .map(val => val.value.trim())
944
+ .join(' ');
945
+ let err = new Error('Command failed');
946
+ err.response = parsed;
947
+ err.responseStatus = parsed.command.toUpperCase();
948
+ try {
949
+ err.executedCommand =
950
+ parsed.tag +
951
+ (await (0, imap_handler_js_1.compiler)(request, {
952
+ isLogging: true
953
+ })).toString();
954
+ }
955
+ catch {
956
+ // ignore
957
+ }
958
+ if (txt) {
959
+ err.responseText = txt;
960
+ if (err.responseStatus === 'NO' && txt.includes('Some of the requested messages no longer exist')) {
961
+ // Treat as successful response
962
+ // Kept at warn: the caller is handed fewer messages than it asked for and
963
+ // is told nothing else about it, so this entry is the only record that
964
+ // the response was truncated.
965
+ this.log.warn({ msg: 'Partial FETCH response', cid: this.id, err });
966
+ await new Promise(resolve => request.resolve({ response: parsed, next: resolve }));
967
+ break;
968
+ }
969
+ let throttleDelay = false;
970
+ // MS365 throttling detection: Office 365 returns BAD with a human-readable
971
+ // backoff time when rate limits are hit. Parse the delay from the response text.
972
+ // Example: "tag BAD Request is throttled. Suggested Backoff Time: 92415 milliseconds"
973
+ if (/Request is throttled/i.test(txt) && /Backoff Time/i.test(txt)) {
974
+ let throttlingMatch = txt.match(/Backoff Time[:=\s]+(\d+)/i);
975
+ if (throttlingMatch && throttlingMatch[1] && !isNaN(throttlingMatch[1])) {
976
+ throttleDelay = Number(throttlingMatch[1]);
977
+ }
978
+ }
979
+ // Wait and return a throttling error
980
+ if (throttleDelay) {
981
+ err.code = 'ETHROTTLE';
982
+ err.throttleReset = throttleDelay;
983
+ // The server-suggested delay can be very large, so throttleWait() caps it
984
+ let delayResponse = Math.min(throttleDelay, MAX_THROTTLE_DELAY);
985
+ this.log.warn({ msg: 'Throttling detected', cid: this.id, throttleDelay, delayResponse, err });
986
+ let aborted = await this.throttleWait(delayResponse);
987
+ if (aborted) {
988
+ // Connection closed during back-off: reject promptly with a
989
+ // connection error (carrying any server BYE reason) instead of
990
+ // waiting out the throttle delay.
991
+ request.reject(this.createNoConnectionError(this.byeReason, { rejectedFrom: 'throttleAbort', command: request.command }));
992
+ break;
993
+ }
994
+ }
995
+ }
996
+ request.reject(err);
997
+ break;
998
+ }
999
+ default: {
1000
+ let err = new Error('Invalid server response');
1001
+ err.code = 'InvalidResponse';
1002
+ err.response = parsed;
1003
+ request.reject(err);
1004
+ break;
1005
+ }
1006
+ }
1007
+ }
1008
+ /** @internal */
1009
+ setEventHandlers() {
1010
+ // Bind the 'readable' event to kick off the reader loop.
1011
+ // The `this.reading` flag acts as a concurrency guard: if reader()
1012
+ // is already running, new 'readable' events are ignored. The reader
1013
+ // loop will keep draining data until the stream returns null.
1014
+ const onReadable = () => {
1015
+ if (!this.reading) {
1016
+ this.reading = true;
1017
+ this.reader()
1018
+ .catch(err => this.log.error({ err, cid: this.id }))
1019
+ .finally(() => {
1020
+ this.reading = false;
1021
+ // A 'readable' event that fired while the loop was winding down was
1022
+ // ignored by the guard above. Node emits the event on the next tick,
1023
+ // after this handler has run, but a runtime that implements nextTick
1024
+ // as a microtask (Cloudflare Workers) emits it before, and the response
1025
+ // the parser had pushed in the meantime would then sit unread until the
1026
+ // next chunk arrives - or, for the last response of an exchange, until
1027
+ // the socket times out. Anything already buffered is picked up here.
1028
+ if (this.streamer && !this.streamer.destroyed && this.streamer.readableLength > 0) {
1029
+ onReadable();
1030
+ }
1031
+ });
1032
+ }
1033
+ };
1034
+ this.socketReadable = onReadable;
1035
+ this.streamer.on('readable', onReadable);
1036
+ }
1037
+ /**
1038
+ * Applies the transport options every established application socket needs: TCP keepalive and
1039
+ * the inactivity watchdog. Called for direct TLS, cleartext, proxied and STARTTLS-upgraded
1040
+ * sockets, so the watchdog cannot silently differ between transports (a STARTTLS session used
1041
+ * to end up with no armed timer at all).
1042
+ *
1043
+ * @param socket - The socket that now carries the IMAP session.
1044
+ * @internal
1045
+ */
1046
+ configureSocket(socket) {
1047
+ /* c8 ignore next 3 */ // defensive: connect() only calls this with an established socket
1048
+ if (!socket) {
1049
+ return;
1050
+ }
1051
+ if (typeof socket.setKeepAlive === 'function') {
1052
+ socket.setKeepAlive(true, 5 * 1000);
1053
+ }
1054
+ if (typeof socket.setTimeout === 'function') {
1055
+ socket.setTimeout(this.socketTimeout);
1056
+ }
1057
+ }
1058
+ /** @internal */
1059
+ setSocketHandlers() {
1060
+ // Clear any existing handlers first to prevent duplicates
1061
+ this.clearSocketHandlers();
1062
+ this._socketError =
1063
+ this._socketError ||
1064
+ ((err) => {
1065
+ this.log.error({ err, cid: this.id });
1066
+ this.emitError(err);
1067
+ });
1068
+ this._socketClose = this._socketClose || (() => this.close());
1069
+ this._socketEnd = this._socketEnd || (() => this.close());
1070
+ /**
1071
+ * Socket timeout event handler.
1072
+ *
1073
+ * A quiet socket is only a dead connection when something was supposed to be talking. An
1074
+ * idling session, a download whose consumer stopped draining, and a held mailbox lock
1075
+ * whose owner is busy between commands are all expected to go quiet, so the handler keeps
1076
+ * such a connection alive with a NOOP instead of tearing it down. An in-flight command is
1077
+ * the opposite: its reply is overdue, a recovery NOOP would only queue up behind it and
1078
+ * never reach the wire, so the timeout is reported as an error. The IDLE command itself is
1079
+ * the one exception - it stays in flight for as long as idling lasts, and run() breaks it
1080
+ * through preCheck() before the NOOP is dispatched.
1081
+ *
1082
+ * IDLE is not restarted here: run() re-arms auto-IDLE once the NOOP settles, and
1083
+ * autoidle() knows whether the connection is actually free for IDLE - an open download or
1084
+ * a held lock keeps just the keepalive, and with disableAutoIdle nothing restarts at all.
1085
+ * If the server is dead the NOOP never settles, and the next timeout fires with the NOOP
1086
+ * as the stuck in-flight command, which lands in the error branch below.
1087
+ *
1088
+ * Emits the error event if the connection cannot be recovered
1089
+ */
1090
+ this._socketTimeout =
1091
+ this._socketTimeout ||
1092
+ (() => {
1093
+ const err = new Error('Socket timeout');
1094
+ err.code = 'ETIMEOUT';
1095
+ const quietExpected = this.idling || this._openDownloads || this.currentLock;
1096
+ const commandStuck = this.currentRequest && !(this.idling && this.currentRequest.command === 'IDLE');
1097
+ if (quietExpected && !commandStuck) {
1098
+ if (!this.usable || !this.socket || this.socket.destroyed) {
1099
+ this.emitError(err);
1100
+ return;
1101
+ }
1102
+ this.run('NOOP').catch(err => {
1103
+ this.log.warn({ msg: 'Connection recovery failed after timeout', err, cid: this.id });
1104
+ if (!this.isClosed) {
1105
+ this.close();
1106
+ }
1107
+ });
1108
+ }
1109
+ else {
1110
+ this.log.debug({ msg: 'Socket timeout', cid: this.id });
1111
+ this.emitError(err);
1112
+ }
1113
+ });
1114
+ const socket = this.socket;
1115
+ socket.once('error', this._socketError);
1116
+ socket.once('close', this._socketClose);
1117
+ socket.once('end', this._socketEnd);
1118
+ socket.on('tlsClientError', this._socketError);
1119
+ socket.on('timeout', this._socketTimeout);
1120
+ if (this.writeSocket && this.writeSocket !== this.socket) {
1121
+ this.writeSocket.on('error', this._socketError);
1122
+ }
1123
+ }
1124
+ /** @internal */
1125
+ clearSocketHandlers() {
1126
+ if (!this.socket) {
1127
+ return;
1128
+ }
1129
+ // Remove temporary connection error handler if still present
1130
+ if (this._connectErrorHandler) {
1131
+ this.socket.removeListener('error', this._connectErrorHandler);
1132
+ this._connectErrorHandler = null;
1133
+ }
1134
+ if (this._socketError) {
1135
+ this.socket.removeListener('error', this._socketError);
1136
+ this.socket.removeListener('tlsClientError', this._socketError);
1137
+ if (this.writeSocket && this.writeSocket !== this.socket) {
1138
+ this.writeSocket.removeListener('error', this._socketError);
1139
+ }
1140
+ }
1141
+ if (this._socketTimeout) {
1142
+ this.socket.removeListener('timeout', this._socketTimeout);
1143
+ }
1144
+ if (this._socketClose) {
1145
+ this.socket.removeListener('close', this._socketClose);
1146
+ }
1147
+ if (this._socketEnd) {
1148
+ this.socket.removeListener('end', this._socketEnd);
1149
+ }
1150
+ }
1151
+ /** @internal */
1152
+ async startSession() {
1153
+ await this.run('CAPABILITY');
1154
+ if (this.capabilities.has('ID')) {
1155
+ this.idRequested = await this.run('ID', this.clientInfo);
1156
+ }
1157
+ await this.upgradeToSTARTTLS();
1158
+ await this.authenticate();
1159
+ if ((!this.idRequested || Object.keys(this.idRequested).length < 2) && this.capabilities.has('ID')) {
1160
+ // re-request ID after LOGIN
1161
+ this.idRequested = await this.run('ID', this.clientInfo);
1162
+ }
1163
+ // Make sure we have namespace set. This should also throw if Exchange actually failed authentication
1164
+ let nsResponse = await this.run('NAMESPACE');
1165
+ if (nsResponse && nsResponse.error && nsResponse.status === 'BAD' && /User is authenticated but not connected/i.test(nsResponse.text)) {
1166
+ // Not a NAMESPACE failure but authentication failure, so report as
1167
+ this.authenticated = false;
1168
+ let err = new errors_js_1.AuthenticationFailure('Authentication failed');
1169
+ err.response = nsResponse.text;
1170
+ throw err;
1171
+ }
1172
+ if (this.options.verifyOnly) {
1173
+ // List all folders and logout
1174
+ if (this.options.includeMailboxes) {
1175
+ this._mailboxList = await this.list();
1176
+ }
1177
+ return await this.logout();
1178
+ }
1179
+ // try to use compression (if supported)
1180
+ if (!this.options.disableCompression) {
1181
+ await this.compress();
1182
+ }
1183
+ if (!this.options.disableAutoEnable) {
1184
+ await this.autoEnable();
1185
+ }
1186
+ this.usable = true;
1187
+ }
1188
+ // Enable extensions if possible. IMAP4rev2 must be enabled explicitly on
1189
+ // servers that advertise both rev1 and rev2 (RFC 9051 Appendix A); a single
1190
+ // ENABLE call is used so the enabled set is built in one round trip.
1191
+ /** @internal */
1192
+ async autoEnable() {
1193
+ let enableList = ['CONDSTORE', 'UTF8=ACCEPT'].concat(this.options.qresync ? 'QRESYNC' : []).concat(this.options.disableIMAP4rev2 ? [] : 'IMAP4rev2');
1194
+ let enableResult = await this.run('ENABLE', enableList);
1195
+ if (enableResult === false && enableList.includes('IMAP4rev2')) {
1196
+ // RFC 5161 requires servers to ignore unknown ENABLE arguments, but a
1197
+ // broken implementation may reject the whole command over IMAP4rev2 -
1198
+ // retry without it so CONDSTORE/QRESYNC are not lost as collateral
1199
+ await this.run('ENABLE', enableList.filter(extension => extension !== 'IMAP4rev2'));
1200
+ }
1201
+ }
1202
+ /** @internal */
1203
+ async compress() {
1204
+ if (!(await this.run('COMPRESS'))) {
1205
+ return; // was not able to negotiate compression
1206
+ }
1207
+ // Set up DEFLATE compression (RFC 4978). After COMPRESS is negotiated,
1208
+ // all data in both directions is wrapped in a zlib DEFLATE stream.
1209
+ // The incoming pipeline becomes: socket -> inflate -> streamer (parser).
1210
+ // The outgoing pipeline uses a manual pump (see readNext below) instead
1211
+ // of a normal pipe, because we need to flush after every IMAP command
1212
+ // to ensure the server receives complete commands promptly.
1213
+ this._deflate = node_zlib_1.default.createDeflateRaw({
1214
+ windowBits: 15,
1215
+ level: node_zlib_1.default.constants.Z_DEFAULT_COMPRESSION, // Use default compression level (6)
1216
+ memLevel: 8, // Memory usage level (8 is default)
1217
+ strategy: node_zlib_1.default.constants.Z_DEFAULT_STRATEGY,
1218
+ chunkSize: 16 * 1024 // Process in 16KB chunks to prevent CPU blocking
1219
+ });
1220
+ this._inflate = node_zlib_1.default.createInflateRaw({
1221
+ chunkSize: 16 * 1024 // Process in 16KB chunks to prevent CPU blocking
1222
+ });
1223
+ const socket = this.socket;
1224
+ // Reroute incoming data through inflate: socket -> inflate -> streamer.
1225
+ // The streamer's compress flag tells it to expect deflated framing.
1226
+ socket.unpipe(this.streamer);
1227
+ this.streamer.compress = true;
1228
+ socket.pipe(this._inflate).pipe(this.streamer);
1229
+ this._inflate.on('error', err => {
1230
+ // Only forward into the streamer while it is alive and still has an error
1231
+ // listener. After close() the streamer is destroyed and its listener removed,
1232
+ // so emitting 'error' would throw an unhandled error and crash the process.
1233
+ // (this.streamer is assigned once in the constructor and never nulled.)
1234
+ if (!this.streamer.destroyed && this.streamer.listenerCount('error')) {
1235
+ this.streamer.emit('error', err);
1236
+ }
1237
+ });
1238
+ // For outgoing data, replace the writeSocket with a PassThrough buffer.
1239
+ // We can't pipe writeSocket -> deflate -> socket directly because we need
1240
+ // to call deflate.flush() after each IMAP command to push all pending
1241
+ // compressed bytes to the server immediately (IMAP is request-response).
1242
+ const writeSocket = new node_stream_1.PassThrough({
1243
+ highWaterMark: 64 * 1024 // 64KB buffer limit to prevent excessive memory usage
1244
+ });
1245
+ this.writeSocket = writeSocket;
1246
+ /* c8 ignore start */ // destroySoon override is never invoked by ImapFlow (close() calls destroy()); kept for stream API completeness
1247
+ writeSocket.destroySoon = () => {
1248
+ try {
1249
+ if (this.socket) {
1250
+ this.socket.destroy();
1251
+ }
1252
+ writeSocket.end();
1253
+ }
1254
+ catch (err) {
1255
+ this.log.error({ err, msg: 'Failed to destroy PassThrough socket', cid: this.id });
1256
+ throw err;
1257
+ }
1258
+ };
1259
+ /* c8 ignore stop */
1260
+ // The PassThrough reports its own `destroyed` state. It used to proxy the raw socket's
1261
+ // instead, which made close() skip destroying it and left the second raw-socket teardown
1262
+ // branch unreachable. write() checks the raw socket separately, so nothing depends on the
1263
+ // two states being conflated.
1264
+ // Manual pump loop: reads chunks from writeSocket, pushes them into
1265
+ // deflate, and flushes when the buffer is drained. This ensures each
1266
+ // IMAP command is fully compressed and flushed to the socket immediately.
1267
+ let reading = false;
1268
+ let processedChunks = 0;
1269
+ let readNext = async () => {
1270
+ try {
1271
+ reading = true;
1272
+ processedChunks = 0;
1273
+ let chunk;
1274
+ while (this.writeSocket && (chunk = this.writeSocket.read()) !== null) {
1275
+ if (this._deflate && this._deflate.write(chunk) === false) {
1276
+ this._deflate.once('drain', readNext);
1277
+ return;
1278
+ }
1279
+ // Yield to event loop every 100 chunks to prevent CPU blocking
1280
+ processedChunks++;
1281
+ /* c8 ignore next 6 */ // requires 100+ queued chunks in a single pump pass; not reproducible deterministically
1282
+ if (processedChunks % 100 === 0) {
1283
+ await new Promise(resolve => setImmediate(resolve));
1284
+ if (!this.writeSocket) {
1285
+ break;
1286
+ }
1287
+ }
1288
+ }
1289
+ // flush data to socket
1290
+ if (this._deflate) {
1291
+ this._deflate.flush();
1292
+ }
1293
+ reading = false;
1294
+ /* c8 ignore next 3 */ // defensive: the pump body does not throw under normal operation
1295
+ }
1296
+ catch (ex) {
1297
+ this.emitError(ex);
1298
+ }
1299
+ };
1300
+ writeSocket.on('readable', () => {
1301
+ if (!reading && this.writeSocket) {
1302
+ readNext();
1303
+ }
1304
+ });
1305
+ writeSocket.on('error', err => {
1306
+ if (this.socket) {
1307
+ this.socket.emit('error', err);
1308
+ }
1309
+ });
1310
+ this._deflate.pipe(socket);
1311
+ this._deflate.on('error', err => {
1312
+ if (this.socket) {
1313
+ this.socket.emit('error', err);
1314
+ }
1315
+ });
1316
+ }
1317
+ /** @internal */
1318
+ _failSTARTTLS() {
1319
+ if (this.options.doSTARTTLS === true) {
1320
+ // STARTTLS configured as requirement
1321
+ let err = new Error('Server does not support STARTTLS');
1322
+ err.tlsFailed = true;
1323
+ throw err;
1324
+ }
1325
+ // Opportunistic STARTTLS. But it's not possible right now.
1326
+ // Attention: Could be a downgrade attack.
1327
+ return false;
1328
+ }
1329
+ /**
1330
+ * Tries to upgrade the connection to TLS using STARTTLS.
1331
+ * @throws if STARTTLS is required, but not possible.
1332
+ * @returns true, if the connection is now protected by TLS, either direct TLS or STARTTLS.
1333
+ */
1334
+ async upgradeToSTARTTLS() {
1335
+ if (this.options.doSTARTTLS === true && this.options.secure === true) {
1336
+ throw new Error('Misconfiguration: Cannot set both secure=true for TLS and doSTARTTLS=true for STARTTLS.');
1337
+ }
1338
+ if (this.secureConnection) {
1339
+ // Already using direct TLS. No need for STARTTLS.
1340
+ return true;
1341
+ }
1342
+ if (this.options.doSTARTTLS === false) {
1343
+ // STARTTLS explictly disabled by config
1344
+ return false;
1345
+ }
1346
+ if (!this.capabilities.has('STARTTLS')) {
1347
+ return this._failSTARTTLS();
1348
+ }
1349
+ this.expectCapabilityUpdate = true;
1350
+ let canUpgrade = await this.run('STARTTLS');
1351
+ if (!canUpgrade) {
1352
+ return this._failSTARTTLS();
1353
+ }
1354
+ // STARTTLS plaintext-injection guard (RFC 3501 section 6.2.1): a compliant server stays
1355
+ // silent after the tagged STARTTLS OK until the TLS handshake, so any data that
1356
+ // followed the OK was injected by a MITM and must not be treated as if it arrived
1357
+ // over TLS. Two complementary best-effort checks fail closed before wrapping the
1358
+ // socket; injection that still races in afterwards corrupts the TLS handshake and
1359
+ // is rejected there instead (with a generic TLS error rather than STARTTLS_INJECTION).
1360
+ const failSTARTTLSInjection = () => {
1361
+ let err = new Error('Server sent data after the STARTTLS response and before the TLS handshake; possible plaintext-injection attack');
1362
+ err.code = 'STARTTLS_INJECTION';
1363
+ err.tlsFailed = true;
1364
+ this.closeAfter();
1365
+ return err;
1366
+ };
1367
+ // Check 1: the parser saw more input already buffered right after the tagged OK
1368
+ // (same TCP segment, or an already-queued chunk) - see hasTrailingData / starttls.ts.
1369
+ if (this._starttlsHadTrailingData) {
1370
+ throw failSTARTTLSInjection();
1371
+ }
1372
+ const socketPlain = this.socket;
1373
+ // STARTTLS upgrade sequence: detach the plain socket from the parser,
1374
+ // wrap it in a TLS socket, then reconnect the new TLS socket to the
1375
+ // parser. The plain socket becomes the underlying transport for TLS.
1376
+ socketPlain.unpipe(this.streamer);
1377
+ // Check 2: now that the parser is detached, any bytes still buffered on the plain
1378
+ // socket arrived after the OK and were not consumed by the handshake - i.e. injected.
1379
+ // This catches late/fragmented injection that the parse-time snapshot cannot see.
1380
+ let injectedTail = typeof socketPlain.read === 'function' ? socketPlain.read() : null;
1381
+ /* c8 ignore next 3 */ // late/fragmented post-OK injection is timing-dependent and not deterministically reproducible
1382
+ if (injectedTail && injectedTail.length) {
1383
+ throw failSTARTTLSInjection();
1384
+ }
1385
+ let upgraded = await new Promise((resolve, reject) => {
1386
+ let opts = Object.assign({
1387
+ socket: socketPlain,
1388
+ // host is required even though the socket is already connected: without
1389
+ // it, a connection made to an IP literal (servername=false) has its
1390
+ // certificate verified against Node's fallback name "localhost" instead
1391
+ // of the IP - accepting any "localhost" certificate for any IP-hosted
1392
+ // server, and rejecting legitimate IP-SAN certificates.
1393
+ host: this.host,
1394
+ servername: this.servername,
1395
+ port: this.port
1396
+ }, this.options.tls || {});
1397
+ this.clearSocketHandlers();
1398
+ let settled = false;
1399
+ // Single settlement path for the upgrade. Every terminal outcome - handshake
1400
+ // success, an error on the plain or the TLS socket, the upgrade timeout, an
1401
+ // explicit close(), or a streamer error routed here by emitError() - goes through
1402
+ // this helper exactly once. It owns clearing the upgrade timer, the exposed
1403
+ // rejector, the `upgrading` flag and the temporary handshake handlers, so a late
1404
+ // socket event cannot re-enter an already settled upgrade or leave state behind.
1405
+ const settle = (err, result) => {
1406
+ if (settled) {
1407
+ return;
1408
+ }
1409
+ settled = true;
1410
+ (0, tools_js_1.clearTimer)(this.upgradeTimeout);
1411
+ this.upgradeTimeout = null;
1412
+ this.upgrading = false;
1413
+ this._upgradeReject = null;
1414
+ socketPlain.removeListener('error', settle);
1415
+ if (this.socket && this.socket !== socketPlain) {
1416
+ this.socket.removeListener('error', settle);
1417
+ }
1418
+ if (err) {
1419
+ (0, tools_js_1.clearTimer)(this.connectTimeout);
1420
+ // Preserve the original error, marked as a TLS failure so callers can tell
1421
+ // an upgrade failure from an ordinary command failure.
1422
+ err.tlsFailed = true;
1423
+ this.closeAfter();
1424
+ return reject(err);
1425
+ }
1426
+ resolve(result);
1427
+ };
1428
+ // Exposed so emitError() and close() can settle the upgrade through the same path.
1429
+ this._upgradeReject = settle;
1430
+ // An error on either socket settles the upgrade, so settle() is the listener itself:
1431
+ // one function, one settlement, and removeListener() in settle() needs no separate
1432
+ // handler references. A TLS handshake failure (bad certificate, protocol mismatch)
1433
+ // is emitted on the new TLS socket rather than on the plain one, so both are covered.
1434
+ socketPlain.once('error', settle);
1435
+ /* c8 ignore start */ // UPGRADE_TIMEOUT is 10s; firing it deterministically would make the test suite hang
1436
+ this.upgradeTimeout = setTimeout(() => {
1437
+ let err = new Error('Failed to upgrade connection in required time');
1438
+ err.code = 'UPGRADE_TIMEOUT';
1439
+ settle(err);
1440
+ }, UPGRADE_TIMEOUT);
1441
+ /* c8 ignore stop */
1442
+ this.upgrading = true;
1443
+ let tlsSocket;
1444
+ try {
1445
+ tlsSocket = node_tls_1.default.connect(opts, () => {
1446
+ try {
1447
+ /* c8 ignore start */ // race: connection closed during the TLS handshake window
1448
+ if (this.isClosed) {
1449
+ return settle(this.createNoConnectionError(false, { rejectedFrom: 'tlsUpgrade' }));
1450
+ }
1451
+ /* c8 ignore stop */
1452
+ // TLS handshake complete. Reconnect the now-encrypted socket
1453
+ // to the IMAP parser stream and record the cipher details.
1454
+ this.secureConnection = true;
1455
+ this.streamer.secureConnection = true;
1456
+ tlsSocket.pipe(this.streamer);
1457
+ // Cloudflare Workers expose getCipher() but return null from it, so the
1458
+ // result is normalized to the documented `false`
1459
+ /* c8 ignore next */ // on Node an upgraded TLS socket always answers getCipher(), so the false fallback is unreachable
1460
+ this.tls = (typeof tlsSocket.getCipher === 'function' && tlsSocket.getCipher()) || false;
1461
+ if (this.tls) {
1462
+ this.tls.authorized = tlsSocket.authorized;
1463
+ this.log.info({
1464
+ src: 'tls',
1465
+ msg: 'Established TLS session',
1466
+ cid: this.id,
1467
+ authorized: this.tls.authorized,
1468
+ /* c8 ignore next */ // cipher.standardName is present on modern Node, so the .name fallback rarely runs
1469
+ algo: this.tls.standardName || this.tls.name,
1470
+ version: this.tls.version
1471
+ });
1472
+ }
1473
+ // The plain socket is now only the TLS transport: drop its superseded
1474
+ // inactivity timer so no armed timer is left behind without a listener.
1475
+ if (typeof socketPlain.setTimeout === 'function') {
1476
+ socketPlain.setTimeout(0);
1477
+ }
1478
+ // Install the normal socket handlers only now that the handshake
1479
+ // succeeded. Doing this during the handshake would leave both settle() and
1480
+ // the generic _socketError on the socket; a handshake 'error' would then fire
1481
+ // BOTH (EventEmitter clones its listener array on emit), causing a duplicate
1482
+ // error and a possible unhandled 'error' crash. Keeping settle() as the sole
1483
+ // listener until here guarantees a single error path for the upgrade.
1484
+ this.setSocketHandlers();
1485
+ // Arm the inactivity watchdog on the socket that now carries the session.
1486
+ // Without this a STARTTLS-upgraded connection has no watchdog at all: the
1487
+ // timer was armed on the plain socket, while the timeout listener lives on
1488
+ // the TLS socket.
1489
+ this.configureSocket(this.socket);
1490
+ // settle() also removes the temporary handshake handlers
1491
+ settle(null, true);
1492
+ /* c8 ignore next 3 */ // defensive: the success callback body does not throw under normal operation
1493
+ }
1494
+ catch (ex) {
1495
+ this.emitError(ex);
1496
+ }
1497
+ });
1498
+ }
1499
+ catch (err) {
1500
+ // tls.connect() refused the upgrade before any handshake (an option the runtime
1501
+ // does not implement, a socket it can not wrap). Settled through the same path
1502
+ // as a handshake failure, so the upgrade state and its timer are cleared and
1503
+ // the error is marked as a TLS failure rather than escaping the executor.
1504
+ settle(err);
1505
+ return;
1506
+ }
1507
+ this.socket = tlsSocket;
1508
+ // Registered after tls.connect (the TLS socket now exists). This is the ONLY
1509
+ // error listener during the handshake window; the generic handlers are installed
1510
+ // by setSocketHandlers() inside the success callback above, so a handshake error
1511
+ // has a single error path.
1512
+ tlsSocket.once('error', settle);
1513
+ this.writeSocket = tlsSocket;
1514
+ });
1515
+ if (upgraded) {
1516
+ // RFC 9051 section 6.2.1: once TLS is started the client MUST discard the
1517
+ // cached capabilities and reissue CAPABILITY, because everything learned
1518
+ // before the handshake was plaintext an active attacker could rewrite.
1519
+ // Unconditional on purpose: a server that stamps [CAPABILITY ...] on the
1520
+ // STARTTLS OK itself clears expectCapabilityUpdate, so keying the discard
1521
+ // on that flag would keep exactly the pre-TLS list an attacker controls -
1522
+ // the list that then picks the AUTH mechanism and answers LOGINDISABLED.
1523
+ this.clearCapabilities();
1524
+ await this.run('CAPABILITY');
1525
+ }
1526
+ return upgraded;
1527
+ }
1528
+ /** @internal */
1529
+ async setAuthenticationState() {
1530
+ this.state = this.states.AUTHENTICATED;
1531
+ this.authenticated = true;
1532
+ if (this.expectCapabilityUpdate) {
1533
+ // update capabilities
1534
+ await this.run('CAPABILITY');
1535
+ }
1536
+ }
1537
+ /** @internal */
1538
+ async authenticate() {
1539
+ if (this.state === this.states.LOGOUT) {
1540
+ throw new errors_js_1.AuthenticationFailure('Already logged out');
1541
+ }
1542
+ if (this.state !== this.states.NOT_AUTHENTICATED) {
1543
+ // nothing to do here, usually happens with PREAUTH greeting
1544
+ return true;
1545
+ }
1546
+ if (!this.options.auth) {
1547
+ throw new errors_js_1.AuthenticationFailure('Please configure the login');
1548
+ }
1549
+ this.expectCapabilityUpdate = true;
1550
+ let loginMethod = (this.options.auth.loginMethod || '').toString().trim().toUpperCase();
1551
+ if (!loginMethod && /\\|\//.test(this.options.auth.user)) {
1552
+ // Special override for MS Exchange when authenticating as some other user or non-email account
1553
+ loginMethod = 'LOGIN';
1554
+ }
1555
+ if (this.options.auth.accessToken) {
1556
+ this.authenticated = await this.run('AUTHENTICATE', this.options.auth.user, { accessToken: this.options.auth.accessToken });
1557
+ }
1558
+ else if (this.options.auth.pass) {
1559
+ if ((this.capabilities.has('AUTH=LOGIN') || this.capabilities.has('AUTH=PLAIN')) && loginMethod !== 'LOGIN') {
1560
+ this.authenticated = await this.run('AUTHENTICATE', this.options.auth.user, {
1561
+ password: this.options.auth.pass,
1562
+ loginMethod,
1563
+ authzid: this.options.auth.authzid
1564
+ });
1565
+ }
1566
+ else {
1567
+ if (this.capabilities.has('LOGINDISABLED')) {
1568
+ throw new errors_js_1.AuthenticationFailure('Login is disabled');
1569
+ }
1570
+ this.authenticated = await this.run('LOGIN', this.options.auth.user, this.options.auth.pass);
1571
+ }
1572
+ }
1573
+ else {
1574
+ throw new errors_js_1.AuthenticationFailure('No password configured');
1575
+ }
1576
+ if (this.authenticated) {
1577
+ this.log.info({
1578
+ src: 'auth',
1579
+ msg: 'User authenticated',
1580
+ cid: this.id,
1581
+ user: this.options.auth.user
1582
+ });
1583
+ await this.setAuthenticationState();
1584
+ return true;
1585
+ }
1586
+ throw new errors_js_1.AuthenticationFailure('No matching authentication method');
1587
+ }
1588
+ /** @internal */
1589
+ beginSession(onUnhandledError) {
1590
+ (0, tools_js_1.clearTimer)(this.greetingTimeout);
1591
+ this.untaggedHandlers.OK = null;
1592
+ this.untaggedHandlers.PREAUTH = null;
1593
+ if (this.isClosed) {
1594
+ return;
1595
+ }
1596
+ // get out of current parsing "thread", so do not await for startSession
1597
+ this.startSession()
1598
+ .then(() => {
1599
+ if (typeof this.initialResolve === 'function') {
1600
+ let resolve = this.initialResolve;
1601
+ this.initialResolve = false;
1602
+ this.initialReject = false;
1603
+ return resolve();
1604
+ }
1605
+ })
1606
+ .catch(err => {
1607
+ this.log.error({ err, cid: this.id });
1608
+ if (typeof this.initialReject === 'function') {
1609
+ (0, tools_js_1.clearTimer)(this.greetingTimeout);
1610
+ let reject = this.initialReject;
1611
+ this.initialResolve = false;
1612
+ this.initialReject = false;
1613
+ return reject(err);
1614
+ }
1615
+ onUnhandledError(err);
1616
+ });
1617
+ }
1618
+ /** @internal */
1619
+ async initialOK(message) {
1620
+ this.greeting = (message.attributes || [])
1621
+ .filter(entry => entry.type === 'TEXT')
1622
+ .map(entry => entry.value)
1623
+ .filter(entry => entry)
1624
+ .join('');
1625
+ // ALWAYS emit the error so users can handle it
1626
+ this.beginSession(err => this.emitError(err));
1627
+ }
1628
+ /** @internal */
1629
+ async initialPREAUTH() {
1630
+ if (this.isClosed) {
1631
+ return;
1632
+ }
1633
+ this.state = this.states.AUTHENTICATED;
1634
+ // documented contract for the `authenticated` property: `true` when the
1635
+ // connection was authenticated by a PREAUTH greeting (no credentials known)
1636
+ this.authenticated = true;
1637
+ this.beginSession(err => {
1638
+ this.log.error({ err, cid: this.id });
1639
+ this.closeAfter();
1640
+ });
1641
+ }
1642
+ /** @internal */
1643
+ async serverBye(parsed) {
1644
+ // Extract BYE reason from response for better error messages
1645
+ let reason = parsed &&
1646
+ parsed.attributes &&
1647
+ parsed.attributes
1648
+ .filter(val => val.type === 'TEXT')
1649
+ .map(val => val.value.trim())
1650
+ .join(' ');
1651
+ this.byeReason = reason || 'Server closed connection';
1652
+ this.untaggedHandlers.BYE = null;
1653
+ this.state = this.states.LOGOUT;
1654
+ }
1655
+ // Drops every capability-derived field together - the counterpart of
1656
+ // updateCapabilitiesFromRaw() below, which sets them together. rawCapabilities is
1657
+ // public surface external consumers read, so a discard (RFC 9051 6.2.1 requires
1658
+ // one after STARTTLS) that missed it would leave the stale list visible if the
1659
+ // re-fetch fails.
1660
+ /** @internal */
1661
+ clearCapabilities() {
1662
+ this.capabilities.clear();
1663
+ this.authCapabilities.clear();
1664
+ this.rawCapabilities = null;
1665
+ }
1666
+ /** @internal */
1667
+ updateCapabilitiesFromRaw(rawCapabilities) {
1668
+ this.rawCapabilities = rawCapabilities;
1669
+ this.capabilities = (0, tools_js_1.updateCapabilities)(rawCapabilities);
1670
+ if (this.capabilities) {
1671
+ for (let [capa] of this.capabilities) {
1672
+ if (/^AUTH=/i.test(capa) && !this.authCapabilities.has(capa.toUpperCase())) {
1673
+ this.authCapabilities.set(capa.toUpperCase(), false);
1674
+ }
1675
+ }
1676
+ }
1677
+ if (this.expectCapabilityUpdate) {
1678
+ this.expectCapabilityUpdate = false;
1679
+ }
1680
+ }
1681
+ /** @internal */
1682
+ async sectionCapability(section) {
1683
+ this.updateCapabilitiesFromRaw(section);
1684
+ }
1685
+ /** @internal */
1686
+ async untaggedCapability(untagged) {
1687
+ this.updateCapabilitiesFromRaw(untagged.attributes);
1688
+ }
1689
+ /** @internal */
1690
+ async untaggedExists(untagged) {
1691
+ if (!this.mailbox) {
1692
+ // mailbox closed, ignore
1693
+ return;
1694
+ }
1695
+ if (!untagged) {
1696
+ return;
1697
+ }
1698
+ // Not a usable count: anything but a bounded digit run. A digit run long enough
1699
+ // coerces to Infinity, which would corrupt mailbox state (resolveRange('*') would
1700
+ // compile to the literal "Infinity" and every range-based command would fail until
1701
+ // the next SELECT)
1702
+ let count = (0, tools_js_1.parseUintValue)(untagged.command, tools_js_1.MAX_UINT32_DIGITS);
1703
+ if (count === false) {
1704
+ return;
1705
+ }
1706
+ if (count === this.mailbox.exists) {
1707
+ // nothing changed?
1708
+ return;
1709
+ }
1710
+ // keep exists up to date
1711
+ let prevCount = this.mailbox.exists;
1712
+ this.mailbox.exists = count;
1713
+ this.emit('exists', {
1714
+ path: this.mailbox.path,
1715
+ count,
1716
+ prevCount
1717
+ });
1718
+ }
1719
+ // Reports one expunged message, either through the caller's expungeHandler or as an
1720
+ // 'expunge' event. Shared by the EXPUNGE and VANISHED paths so the two cannot drift.
1721
+ /** @internal */
1722
+ async notifyExpunge(payload) {
1723
+ if (typeof this.options.expungeHandler !== 'function') {
1724
+ this.emit('expunge', payload);
1725
+ return;
1726
+ }
1727
+ try {
1728
+ await this.options.expungeHandler(payload);
1729
+ }
1730
+ catch (err) {
1731
+ // The throw comes from the caller's own handler, not from this library
1732
+ this.log.error({ msg: 'Failed to notify expunge event', payload, err, cid: this.id });
1733
+ }
1734
+ }
1735
+ /** @internal */
1736
+ async untaggedExpunge(untagged) {
1737
+ if (!this.mailbox) {
1738
+ // mailbox closed, ignore
1739
+ return;
1740
+ }
1741
+ if (!untagged) {
1742
+ return;
1743
+ }
1744
+ // Same bound untaggedExists() applies: only a bounded decimal run is a usable sequence number
1745
+ let seq = (0, tools_js_1.parseUintValue)(untagged.command, tools_js_1.MAX_UINT32_DIGITS);
1746
+ if (seq && seq <= this.mailbox.exists) {
1747
+ this.mailbox.exists--;
1748
+ let payload = {
1749
+ path: this.mailbox.path,
1750
+ seq,
1751
+ vanished: false
1752
+ };
1753
+ await this.notifyExpunge(payload);
1754
+ }
1755
+ }
1756
+ /** @internal */
1757
+ async untaggedVanished(untagged, mailbox) {
1758
+ mailbox = mailbox || this.mailbox;
1759
+ if (!mailbox) {
1760
+ // mailbox closed, ignore
1761
+ return;
1762
+ }
1763
+ let tags = [];
1764
+ let uids = false;
1765
+ // A malformed VANISHED can carry no attributes at all, and one carrying only the
1766
+ // (EARLIER) tag leaves `uids` false - expandRange() handles that and yields nothing
1767
+ if (!untagged.attributes || !untagged.attributes.length) {
1768
+ return;
1769
+ }
1770
+ if (untagged.attributes.length > 1 && Array.isArray(untagged.attributes[0])) {
1771
+ tags = (0, tools_js_1.getStringList)(untagged.attributes[0]).map(value => value.toUpperCase());
1772
+ untagged.attributes.shift();
1773
+ }
1774
+ if (untagged.attributes[0] && typeof untagged.attributes[0].value === 'string') {
1775
+ uids = untagged.attributes[0].value;
1776
+ }
1777
+ let uidList = (0, tools_js_1.expandRange)(uids);
1778
+ for (let uid of uidList) {
1779
+ let payload = {
1780
+ path: mailbox.path,
1781
+ uid,
1782
+ vanished: true,
1783
+ earlier: tags.includes('EARLIER')
1784
+ };
1785
+ await this.notifyExpunge(payload);
1786
+ }
1787
+ }
1788
+ /** @internal */
1789
+ async untaggedFetch(untagged, mailbox) {
1790
+ mailbox = mailbox || this.mailbox;
1791
+ if (!mailbox) {
1792
+ // mailbox closed, ignore
1793
+ return;
1794
+ }
1795
+ let message = await (0, tools_js_1.formatMessageResponse)(untagged, mailbox);
1796
+ if (message.flags) {
1797
+ let updateEvent = {
1798
+ path: mailbox.path,
1799
+ seq: message.seq
1800
+ };
1801
+ if (message.uid) {
1802
+ updateEvent.uid = message.uid;
1803
+ }
1804
+ if (message.modseq) {
1805
+ updateEvent.modseq = message.modseq;
1806
+ }
1807
+ updateEvent.flags = message.flags;
1808
+ if (message.flagColor) {
1809
+ updateEvent.flagColor = message.flagColor;
1810
+ }
1811
+ this.emit('flags', updateEvent);
1812
+ }
1813
+ }
1814
+ /** @internal */
1815
+ async ensureSelectedMailbox(path) {
1816
+ if (!path) {
1817
+ return false;
1818
+ }
1819
+ if (!this.mailbox || !(0, tools_js_1.comparePaths)(this, this.mailbox.path, Array.isArray(path) ? (0, tools_js_1.normalizePath)(this, path) : path)) {
1820
+ return await this.mailboxOpen(path);
1821
+ }
1822
+ return true;
1823
+ }
1824
+ // Normalizes a message range from various input formats into an IMAP-compatible
1825
+ // sequence string (e.g., "1:5,7,10:*"). Handles: numbers, "*", {all:true},
1826
+ // {uid:value}, search query objects (resolved via SEARCH), and arrays of numbers.
1827
+ /** @internal */
1828
+ async resolveRange(range, options) {
1829
+ let value = range;
1830
+ if (typeof value === 'number' || typeof value === 'bigint') {
1831
+ value = value.toString();
1832
+ }
1833
+ // Replace "*" with the actual message count. Some servers reject bare "*"
1834
+ // in certain commands, and this also forces a sequence query (not UID).
1835
+ if (value === '*') {
1836
+ if (!this.mailbox.exists) {
1837
+ return false;
1838
+ }
1839
+ value = this.mailbox.exists.toString();
1840
+ options.uid = false; // sequence query
1841
+ }
1842
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
1843
+ if (value.all && Object.keys(value).length === 1) {
1844
+ value = '1:*';
1845
+ }
1846
+ else if (value.uid && Object.keys(value).length === 1) {
1847
+ value = value.uid;
1848
+ options.uid = true;
1849
+ }
1850
+ else {
1851
+ // Arbitrary search query object: run SEARCH to resolve it into
1852
+ // a set of UIDs, then pack into a compact range string.
1853
+ options.uid = true; // force UIDs instead of sequence numbers
1854
+ value = await this.run('SEARCH', value, options);
1855
+ if (value && value.length) {
1856
+ value = (0, tools_js_1.packMessageRange)(value);
1857
+ }
1858
+ }
1859
+ }
1860
+ if (Array.isArray(value)) {
1861
+ value = value.join(',');
1862
+ }
1863
+ if (!value) {
1864
+ return false;
1865
+ }
1866
+ return value;
1867
+ }
1868
+ // The single definition of "the connection is not free". A held or queued mailbox lock, a
1869
+ // command in flight or queued, and an open download stream all mean a caller is
1870
+ // mid-sequence: starting IDLE there injects an IDLE/DONE round trip - or, with
1871
+ // `missingIdleCommand` set to SELECT or STATUS, a mailbox poll - between two of that
1872
+ // caller's own commands. Every one of those states ends by calling autoidle() again, so
1873
+ // declining while busy postpones IDLE, it never cancels it.
1874
+ /** @internal */
1875
+ connectionBusy() {
1876
+ return !!(this.currentLock || this.locks.length || this.currentRequest || this.requestQueue.length || this._openDownloads);
1877
+ }
1878
+ // Timer process-liveness policy: connection establishment and greeting deadlines keep the
1879
+ // process alive, because a caller is waiting on connect() to settle. Background timers
1880
+ // (auto-IDLE, IDLE restart, fallback polling, throttle back-off, the held-lock diagnostic) are
1881
+ // unref'd, so an otherwise idle process is not held open by them. Every timer is still cleared
1882
+ // explicitly on close().
1883
+ /** @internal */
1884
+ autoidle() {
1885
+ (0, tools_js_1.clearTimer)(this.idleStartTimer);
1886
+ if (this.options.disableAutoIdle || this.state !== this.states.SELECTED) {
1887
+ return;
1888
+ }
1889
+ if (this.connectionBusy()) {
1890
+ return;
1891
+ }
1892
+ this.idleStartTimer = setTimeout(() => {
1893
+ // Re-checked at fire time: paths that take ownership of the connection clear this
1894
+ // timer, but the guard must not depend on every one of them doing so - a single
1895
+ // missed clearTimeout would inject IDLE between a caller's own commands. Declining
1896
+ // postpones rather than cancels: whatever made the connection busy calls autoidle()
1897
+ // again when it finishes.
1898
+ if (this.state !== this.states.SELECTED || this.connectionBusy()) {
1899
+ return;
1900
+ }
1901
+ this.idle().catch(err => (0, tools_js_1.logConnectionError)(this, 'Auto-IDLE failed', err));
1902
+ }, this.autoIdleDelay);
1903
+ (0, tools_js_1.unrefTimer)(this.idleStartTimer);
1904
+ }
1905
+ // PUBLIC API METHODS
1906
+ /**
1907
+ * Initiates a connection against IMAP server. Throws if anything goes wrong. This is something you have to call before you can run any IMAP commands
1908
+ *
1909
+ * @throws Will throw an error if connection or authentication fails
1910
+ * @example
1911
+ * let client = new ImapFlow({...});
1912
+ * await client.connect();
1913
+ */
1914
+ async connect() {
1915
+ if (this._connectCalled) {
1916
+ // Prevent re-using ImapFlow instances by allowing to call connect just once.
1917
+ throw new Error('Can not re-use ImapFlow instance');
1918
+ }
1919
+ this._connectCalled = true;
1920
+ // One deadline for the whole attempt, started before anything is resolved or negotiated.
1921
+ // Proxy DNS and proxy negotiation used to run entirely outside the timer, so a stalled
1922
+ // proxy could hang far beyond the documented connectionTimeout.
1923
+ let deadline = new connection_deadline_js_1.ConnectionDeadline(this.options.connectionTimeout);
1924
+ let connector = this.secureConnection ? node_tls_1.default : node_net_1.default;
1925
+ let opts = Object.assign({
1926
+ host: this.host,
1927
+ servername: this.servername,
1928
+ port: this.port
1929
+ }, this.options.tls || {});
1930
+ this.untaggedHandlers.OK = (...args) => this.initialOK(...args);
1931
+ this.untaggedHandlers.BYE = (...args) => this.serverBye(...args);
1932
+ this.untaggedHandlers.PREAUTH = () => this.initialPREAUTH();
1933
+ this.untaggedHandlers.CAPABILITY = (...args) => this.untaggedCapability(...args);
1934
+ this.sectionHandlers.CAPABILITY = (...args) => this.sectionCapability(...args);
1935
+ this.untaggedHandlers.EXISTS = (...args) => this.untaggedExists(...args);
1936
+ this.untaggedHandlers.EXPUNGE = (...args) => this.untaggedExpunge(...args);
1937
+ // these methods take an optional second argument, so make sure that some random IMAP tag is not used as the second argument
1938
+ this.untaggedHandlers.FETCH = untagged => this.untaggedFetch(untagged);
1939
+ this.untaggedHandlers.VANISHED = untagged => this.untaggedVanished(untagged);
1940
+ let socket = false;
1941
+ if (this.options.proxy) {
1942
+ try {
1943
+ socket = await (0, proxy_connection_js_1.proxyConnection)(this.log, this.options.proxy, this.host, this.port, { deadline });
1944
+ if (!socket) {
1945
+ throw new Error('Failed to setup proxy connection');
1946
+ }
1947
+ }
1948
+ catch (err) {
1949
+ // Logged here rather than relying on proxy-connection.ts, which only reports
1950
+ // failures from inside the two connect helpers. An unsupported scheme, a proxy URL
1951
+ // that will not parse and a deadline that expired before the connect started all
1952
+ // reject before any logging happens there, so this is the one place that sees
1953
+ // every way proxy setup can fail.
1954
+ this.log.error({ msg: 'Failed to setup proxy connection', err, cid: this.id });
1955
+ if (err.code === 'CONNECT_TIMEOUT') {
1956
+ // The shared deadline expired during proxy setup. Report it as the documented
1957
+ // connection timeout rather than as a generic proxy failure.
1958
+ throw err;
1959
+ }
1960
+ let error = new Error('Failed to setup proxy connection');
1961
+ error.code = err.code || 'ProxyError';
1962
+ error._err = err;
1963
+ throw error;
1964
+ }
1965
+ }
1966
+ // Guarded: close() rejects a pending connect() synchronously. See guardedPromise().
1967
+ let connectPromise = (0, tools_js_1.guardedPromise)((resolve, reject) => {
1968
+ // Whatever the proxy phase already used is gone from the budget
1969
+ this.connectTimeout = setTimeout(() => {
1970
+ let err = deadline.error();
1971
+ this.log.error({ err, cid: this.id });
1972
+ this.closeAfter();
1973
+ reject(err);
1974
+ }, deadline.remaining());
1975
+ let onConnect = () => {
1976
+ try {
1977
+ (0, tools_js_1.clearTimer)(this.connectTimeout);
1978
+ // ImapFlow now owns the socket; drop the proxy's early error handler
1979
+ // (its "before connection setup" message no longer applies).
1980
+ (0, proxy_connection_js_1.detachEarlyErrorHandler)(socket);
1981
+ this.configureSocket(this.socket);
1982
+ this.greetingTimeout = setTimeout(() => {
1983
+ let err = new Error(
1984
+ /* c8 ignore next */ // the greeting-timeout test uses a plaintext socket; the secure-socket branch of this hint is not separately exercised
1985
+ `Failed to receive greeting from server in required time${!this.secureConnection ? '. Maybe should use TLS?' : ''}`);
1986
+ err.code = 'GREETING_TIMEOUT';
1987
+ err.details = {
1988
+ /* c8 ignore next */ // firing the timeout with the default (large) value would hang the suite, so only the explicit-option path is tested
1989
+ greetingTimeout: this.options.greetingTimeout || GREETING_TIMEOUT
1990
+ };
1991
+ this.log.error({ err, cid: this.id });
1992
+ this.closeAfter();
1993
+ reject(err);
1994
+ }, this.options.greetingTimeout || GREETING_TIMEOUT);
1995
+ const connected = this.socket;
1996
+ this.tls = (typeof connected.getCipher === 'function' && connected.getCipher()) || false;
1997
+ let logInfo = {
1998
+ src: 'connection',
1999
+ msg: `Established ${this.tls ? 'secure ' : ''}TCP connection`,
2000
+ cid: this.id,
2001
+ secure: !!this.tls,
2002
+ host: this.host,
2003
+ servername: this.servername,
2004
+ port: connected.remotePort,
2005
+ address: connected.remoteAddress,
2006
+ localAddress: connected.localAddress,
2007
+ localPort: connected.localPort
2008
+ };
2009
+ if (this.tls) {
2010
+ logInfo.authorized = this.tls.authorized = connected.authorized;
2011
+ /* c8 ignore next */ // cipher.standardName is present on modern Node, so the .name fallback rarely runs
2012
+ logInfo.algo = this.tls.standardName || this.tls.name;
2013
+ logInfo.version = this.tls.version;
2014
+ }
2015
+ this.log.info(logInfo);
2016
+ this.setSocketHandlers();
2017
+ this.setEventHandlers();
2018
+ connected.pipe(this.streamer);
2019
+ // executed by initial "* OK"
2020
+ this.initialResolve = resolve;
2021
+ this.initialReject = reject;
2022
+ /* c8 ignore next 4 */ // defensive: the onConnect setup body does not throw under normal operation
2023
+ }
2024
+ catch (ex) {
2025
+ // connect failed
2026
+ reject(ex);
2027
+ }
2028
+ };
2029
+ if (socket) {
2030
+ // socket is already established via proxy
2031
+ if (this.secureConnection) {
2032
+ // TLS socket requires a handshake
2033
+ opts.socket = socket;
2034
+ this.socket = connector.connect(opts, onConnect);
2035
+ }
2036
+ else {
2037
+ // cleartext socket is already usable
2038
+ this.socket = socket;
2039
+ setImmediate(onConnect);
2040
+ }
2041
+ }
2042
+ else {
2043
+ this.socket = connector.connect(opts, onConnect);
2044
+ }
2045
+ this.writeSocket = this.socket;
2046
+ // Store connection error handler for cleanup
2047
+ this._connectErrorHandler = (err) => {
2048
+ (0, tools_js_1.clearTimer)(this.connectTimeout);
2049
+ (0, tools_js_1.clearTimer)(this.greetingTimeout);
2050
+ this.closeAfter();
2051
+ this.log.error({ err, cid: this.id });
2052
+ reject(err);
2053
+ };
2054
+ this.socket.on('error', this._connectErrorHandler);
2055
+ });
2056
+ await connectPromise;
2057
+ }
2058
+ /**
2059
+ * Graceful connection close by sending logout command to server. TCP connection is closed once command is finished.
2060
+ *
2061
+ * @example
2062
+ * let client = new ImapFlow({...});
2063
+ * await client.connect();
2064
+ * ...
2065
+ * await client.logout();
2066
+ */
2067
+ async logout() {
2068
+ return await this.run('LOGOUT');
2069
+ }
2070
+ /**
2071
+ * Close the TCP connection.
2072
+ * Unlike `close()`, return immediately from this function, allowing the
2073
+ * caller function to proceed, and run `close()` function afterwards.
2074
+ */
2075
+ closeAfter() {
2076
+ setImmediate(() => this.close());
2077
+ }
2078
+ // Connection-scoped wrapper around the shared stamping helper; see buildConnectionError().
2079
+ /** @internal */
2080
+ createConnectionError(code, message, meta) {
2081
+ return (0, tools_js_1.buildConnectionError)(this.id, code, message, meta);
2082
+ }
2083
+ // The standard "connection not available" error, optionally annotated with the server's BYE
2084
+ // reason. Single source of truth so every NoConnection rejection is consistent.
2085
+ /** @internal */
2086
+ createNoConnectionError(byeReason, meta) {
2087
+ const error = this.createConnectionError('NoConnection', 'Connection not available', meta);
2088
+ if (byeReason) {
2089
+ error.reason = byeReason;
2090
+ }
2091
+ return error;
2092
+ }
2093
+ /**
2094
+ * Closes TCP connection without notifying the server.
2095
+ *
2096
+ * @example
2097
+ * let client = new ImapFlow({...});
2098
+ * await client.connect();
2099
+ * ...
2100
+ * client.close();
2101
+ */
2102
+ close() {
2103
+ try {
2104
+ // clear pending timers
2105
+ (0, tools_js_1.clearTimer)(this.idleStartTimer);
2106
+ (0, tools_js_1.clearTimer)(this.upgradeTimeout);
2107
+ (0, tools_js_1.clearTimer)(this.connectTimeout);
2108
+ (0, tools_js_1.clearTimer)(this.greetingTimeout);
2109
+ // Abort every in-flight throttle back-off so each waiter unblocks and its request is
2110
+ // settled promptly rather than after the full delay.
2111
+ for (let entry of this._throttleWaits) {
2112
+ (0, tools_js_1.clearTimer)(entry.timer);
2113
+ entry.resolve(true);
2114
+ }
2115
+ this._throttleWaits.clear();
2116
+ this.usable = false;
2117
+ // close() takes over ownership of the idling state: dropping the session token means a
2118
+ // poll or IDLE that unwinds after this point sees that it no longer owns the flag and
2119
+ // leaves it alone (see claimIdling() in commands/idle.ts).
2120
+ this._idleSession = null;
2121
+ this.idling = false;
2122
+ // An in-flight STARTTLS upgrade has to be settled through its own single settlement
2123
+ // path, otherwise the upgrade promise (and the session it belongs to) stays pending
2124
+ // for the lifetime of the process.
2125
+ if (typeof this._upgradeReject === 'function') {
2126
+ let reject = this._upgradeReject;
2127
+ this._upgradeReject = null;
2128
+ reject(this.createNoConnectionError(false, { rejectedFrom: 'upgrade' }));
2129
+ }
2130
+ if (typeof this.initialReject === 'function' && !this.options.verifyOnly) {
2131
+ (0, tools_js_1.clearTimer)(this.greetingTimeout);
2132
+ let reject = this.initialReject;
2133
+ this.initialResolve = false;
2134
+ this.initialReject = false;
2135
+ let err = new Error('Unexpected close');
2136
+ /* c8 ignore next */ // closing a pending connect over an already-secure socket (the TLS branch) is not separately exercised
2137
+ err.code = `ClosedAfterConnect${this.secureConnection ? 'TLS' : 'Text'}`;
2138
+ // Surface the server's BYE reason (e.g. "Too many connections") when the
2139
+ // connection was closed by an untagged BYE, so the caller sees why.
2140
+ if (this.byeReason) {
2141
+ err.reason = this.byeReason;
2142
+ }
2143
+ // Synchronous rejection is safe: connectPromise was built by guardedPromise(),
2144
+ // so the rejection is already observed. close() is synchronous, so all cleanup
2145
+ // completes before any microtask rejection handler runs.
2146
+ reject(err);
2147
+ }
2148
+ if (typeof this.preCheck === 'function') {
2149
+ // Runs while the connection is being torn down, so the rejection this sees is
2150
+ // almost always the NoConnection close() is about to raise itself.
2151
+ this.preCheck().catch(err => (0, tools_js_1.logConnectionError)(this, 'Failed to break IDLE while closing', err));
2152
+ }
2153
+ // Session-only public state must not survive the connection it describes: callers read
2154
+ // these properties in reconnect logic and would otherwise mistake cached objects for
2155
+ // live server state. Cleared during the first close only, so repeated close() calls
2156
+ // stay idempotent and cannot emit an event twice.
2157
+ // `byeReason` is deliberately kept: it explains why the session ended.
2158
+ //
2159
+ // `authenticated` is kept for a verifyOnly connection, where it is the result rather
2160
+ // than live state. That mode authenticates, optionally lists, and logs out before
2161
+ // connect() resolves, so clearing it here left every caller reading `false` off a
2162
+ // connection that had just authenticated successfully - there is no later moment at
2163
+ // which the answer could be read, and such a client is never reconnected.
2164
+ let closedMailbox = false;
2165
+ if (!this.isClosed) {
2166
+ closedMailbox = this.mailbox;
2167
+ this.mailbox = false;
2168
+ this.currentSelectCommand = false;
2169
+ if (!this.options.verifyOnly) {
2170
+ this.authenticated = false;
2171
+ }
2172
+ this.preCheck = false;
2173
+ }
2174
+ // Collect all pending requests to reject
2175
+ let pendingRequests = [];
2176
+ // reject command that is currently processed
2177
+ if (this.currentRequest && this.requestTagMap.has(this.currentRequest.tag)) {
2178
+ let tag = this.currentRequest.tag;
2179
+ let request = this.requestTagMap.get(tag);
2180
+ if (request) {
2181
+ this.requestTagMap.delete(tag);
2182
+ pendingRequests.push(request);
2183
+ }
2184
+ this.currentRequest = false;
2185
+ }
2186
+ // reject all other pending commands
2187
+ while (this.requestQueue.length) {
2188
+ let req = this.requestQueue.shift();
2189
+ if (req && this.requestTagMap.has(req.tag)) {
2190
+ let request = this.requestTagMap.get(req.tag);
2191
+ if (request) {
2192
+ this.requestTagMap.delete(req.tag);
2193
+ pendingRequests.push(request);
2194
+ }
2195
+ }
2196
+ }
2197
+ // Reject pending requests and locks synchronously. Every promise rejected here was
2198
+ // built by guardedPromise(), so its rejection is already observed and cannot trigger
2199
+ // unhandledRejection. close() is synchronous, so all remaining cleanup runs before
2200
+ // any microtask rejection handler fires.
2201
+ //
2202
+ // The error travels on, though, through await chains and .then() links that
2203
+ // guardedPromise() knows nothing about. Read a crash stack ending here as "this is
2204
+ // the value that escaped", never as "this is the promise that escaped".
2205
+ let byeReason = this.byeReason;
2206
+ for (let request of pendingRequests) {
2207
+ request.reject(this.createNoConnectionError(byeReason, { rejectedFrom: 'pendingRequest', command: request.command }));
2208
+ }
2209
+ // Clear current lock - holder will see errors when they try operations.
2210
+ // Also clear the held-lock diagnostic timer so it doesn't fire post-close.
2211
+ if (this.currentLock && this.currentLock.heldWarnTimer) {
2212
+ (0, tools_js_1.clearTimer)(this.currentLock.heldWarnTimer);
2213
+ this.currentLock.heldWarnTimer = null;
2214
+ }
2215
+ this.currentLock = false;
2216
+ if (this.locks && this.locks.length) {
2217
+ let pendingLocks = this.locks.splice(0); // Take all locks and clear the array
2218
+ for (let lock of pendingLocks) {
2219
+ if (lock.acquireTimer) {
2220
+ (0, tools_js_1.clearTimer)(lock.acquireTimer);
2221
+ lock.acquireTimer = null;
2222
+ }
2223
+ if (typeof lock.reject === 'function') {
2224
+ lock.reject(this.createNoConnectionError(byeReason, { rejectedFrom: 'mailboxLock', path: lock.path }));
2225
+ }
2226
+ }
2227
+ }
2228
+ // cleanup compression streams if they exist
2229
+ if (this._inflate) {
2230
+ try {
2231
+ this._inflate.unpipe();
2232
+ this._inflate.destroy();
2233
+ this._inflate = null;
2234
+ }
2235
+ catch (err) {
2236
+ this.log.error({ err, msg: 'Failed to destroy inflate stream', cid: this.id });
2237
+ }
2238
+ }
2239
+ if (this._deflate) {
2240
+ try {
2241
+ this._deflate.unpipe();
2242
+ this._deflate.destroy();
2243
+ this._deflate = null;
2244
+ }
2245
+ catch (err) {
2246
+ this.log.error({ err, msg: 'Failed to destroy deflate stream', cid: this.id });
2247
+ }
2248
+ }
2249
+ // cleanup streamer
2250
+ if (this.streamer) {
2251
+ try {
2252
+ // remove our listeners explicitly by reference
2253
+ if (this.socketReadable) {
2254
+ this.streamer.removeListener('readable', this.socketReadable);
2255
+ }
2256
+ if (this._streamerErrorHandler) {
2257
+ this.streamer.removeListener('error', this._streamerErrorHandler);
2258
+ }
2259
+ if (!this.streamer.destroyed) {
2260
+ this.streamer.destroy();
2261
+ }
2262
+ }
2263
+ catch (err) {
2264
+ this.log.error({ err, msg: 'Failed to cleanup streamer', cid: this.id });
2265
+ }
2266
+ }
2267
+ // clear socket handlers
2268
+ this.clearSocketHandlers();
2269
+ // clear cached data
2270
+ this.folders.clear();
2271
+ this.requestTagMap.clear();
2272
+ this.state = this.states.LOGOUT;
2273
+ if (this.isClosed) {
2274
+ return;
2275
+ }
2276
+ // Set before teardown so a socket event that re-enters close() during destruction
2277
+ // cannot run this block a second time.
2278
+ this.isClosed = true;
2279
+ // Socket teardown, in one documented order. Each stream owns and reports its own
2280
+ // lifecycle, so each is destroyed exactly once:
2281
+ // 1. the compression PassThrough (writeSocket), if compression replaced it
2282
+ // 2. the raw socket, which is also writeSocket when compression is not active
2283
+ // The compression streams themselves were destroyed above.
2284
+ if (this.writeSocket && this.writeSocket !== this.socket && !this.writeSocket.destroyed) {
2285
+ try {
2286
+ this.writeSocket.destroy();
2287
+ }
2288
+ catch (err) {
2289
+ this.log.error({ err, cid: this.id });
2290
+ }
2291
+ }
2292
+ if (this.socket && !this.socket.destroyed) {
2293
+ try {
2294
+ this.socket.destroy();
2295
+ }
2296
+ catch (err) {
2297
+ this.log.error({ err, cid: this.id });
2298
+ }
2299
+ }
2300
+ // Null out all socket and handler references so the GC can collect
2301
+ // them even if the ImapFlow instance itself is still referenced.
2302
+ this.socket = null;
2303
+ this.writeSocket = null;
2304
+ this._inflate = null;
2305
+ this._deflate = null;
2306
+ this._streamerErrorHandler = null;
2307
+ this._connectErrorHandler = null;
2308
+ this._socketError = null;
2309
+ this._socketClose = null;
2310
+ this._socketEnd = null;
2311
+ this._socketTimeout = null;
2312
+ this.log.debug({
2313
+ msg: 'Connection closed',
2314
+ cid: this.id,
2315
+ ...(this._unknownTagCount ? { unknownTagCount: this._unknownTagCount } : {})
2316
+ });
2317
+ // A mailbox that was still selected is now closed, so the transition is reported once,
2318
+ // whether the session ended with a clean logout or a lost transport. Emitted before
2319
+ // 'close' and only from the first close(), so no consumer sees it twice.
2320
+ if (closedMailbox) {
2321
+ this.emit('mailboxClose', closedMailbox);
2322
+ }
2323
+ this.emit('close');
2324
+ }
2325
+ catch (ex) {
2326
+ // close failed
2327
+ this.log.error({ err: ex, cid: this.id });
2328
+ }
2329
+ }
2330
+ /**
2331
+ * Returns current quota
2332
+ *
2333
+ * @param path Optional mailbox path if you want to check quota for specific folder. If value is an array then it is joined using current delimiter symbols. Namespace prefix is added automatically if required.
2334
+ * @returns Quota information or `false` if QUOTA extension is not supported or requested path does not exist
2335
+ *
2336
+ * @example
2337
+ * let quota = await client.getQuota();
2338
+ * console.log(quota.storage.used, quota.storage.limit)
2339
+ */
2340
+ async getQuota(path) {
2341
+ path = path || 'INBOX';
2342
+ return await this.run('QUOTA', path);
2343
+ }
2344
+ /**
2345
+ * Lists available mailboxes as an Array
2346
+ *
2347
+ * @param options defines additional listing options
2348
+ * @returns An array of ListResponse objects
2349
+ *
2350
+ * @example
2351
+ * let list = await client.list();
2352
+ * list.forEach(mailbox=>console.log(mailbox.path));
2353
+ */
2354
+ async list(options) {
2355
+ options = options || {};
2356
+ let folders = await this.run('LIST', '', '*', options);
2357
+ this.folders = new Map(folders.map(folder => [folder.path, folder]));
2358
+ return folders;
2359
+ }
2360
+ /**
2361
+ * Lists available mailboxes as a tree structured object
2362
+ *
2363
+ * @param options defines additional listing options
2364
+ * @returns Tree structured object
2365
+ *
2366
+ * @example
2367
+ * let tree = await client.listTree();
2368
+ * tree.folders.forEach(mailbox=>console.log(mailbox.path));
2369
+ */
2370
+ async listTree(options) {
2371
+ options = options || {};
2372
+ let folders = await this.run('LIST', '', '*', options);
2373
+ this.folders = new Map(folders.map(folder => [folder.path, folder]));
2374
+ return (0, tools_js_1.getFolderTree)(folders);
2375
+ }
2376
+ /**
2377
+ * Performs a no-op call against server
2378
+ */
2379
+ async noop() {
2380
+ await this.run('NOOP');
2381
+ }
2382
+ /**
2383
+ * Creates a new mailbox folder and sets up subscription for the created mailbox. Throws on error.
2384
+ *
2385
+ * @param path Full mailbox path. Unicode is allowed. If value is an array then it is joined using current delimiter symbols. Namespace prefix is added automatically if required.
2386
+ * @returns Mailbox info
2387
+ * @throws Will throw an error if mailbox can not be created
2388
+ *
2389
+ * @example
2390
+ * let info = await client.mailboxCreate(['parent', 'child']);
2391
+ * console.log(info.path);
2392
+ * // "INBOX.parent.child" // assumes "INBOX." as namespace prefix and "." as delimiter
2393
+ */
2394
+ async mailboxCreate(path) {
2395
+ return await this.run('CREATE', path);
2396
+ }
2397
+ /**
2398
+ * Renames a mailbox. Throws on error.
2399
+ *
2400
+ * @param path Path for the mailbox to rename. Unicode is allowed. If value is an array then it is joined using current delimiter symbols. Namespace prefix is added automatically if required.
2401
+ * @param newPath New path for the mailbox
2402
+ * @returns Mailbox info
2403
+ * @throws Will throw an error if mailbox does not exist or can not be renamed
2404
+ *
2405
+ * @example
2406
+ * let info = await client.mailboxRename('parent.child', 'Important stuff');
2407
+ * console.log(info.newPath);
2408
+ * // "INBOX.Important stuff" // assumes "INBOX." as namespace prefix
2409
+ */
2410
+ async mailboxRename(path, newPath) {
2411
+ return await this.run('RENAME', path, newPath);
2412
+ }
2413
+ /**
2414
+ * Deletes a mailbox. Throws on error.
2415
+ *
2416
+ * @param path Path for the mailbox to delete. Unicode is allowed. If value is an array then it is joined using current delimiter symbols. Namespace prefix is added automatically if required.
2417
+ * @returns Mailbox info
2418
+ * @throws Will throw an error if mailbox does not exist or can not be deleted
2419
+ *
2420
+ * @example
2421
+ * let info = await client.mailboxDelete('Important stuff');
2422
+ * console.log(info.path);
2423
+ * // "INBOX.Important stuff" // assumes "INBOX." as namespace prefix
2424
+ */
2425
+ async mailboxDelete(path) {
2426
+ return await this.run('DELETE', path);
2427
+ }
2428
+ /**
2429
+ * Subscribes to a mailbox
2430
+ *
2431
+ * @param path Path for the mailbox to subscribe to. Unicode is allowed. If value is an array then it is joined using current delimiter symbols. Namespace prefix is added automatically if required.
2432
+ * @returns `true` if subscription operation succeeded, `false` otherwise
2433
+ *
2434
+ * @example
2435
+ * await client.mailboxSubscribe('Important stuff');
2436
+ */
2437
+ async mailboxSubscribe(path) {
2438
+ return await this.run('SUBSCRIBE', path);
2439
+ }
2440
+ /**
2441
+ * Unsubscribes from a mailbox
2442
+ *
2443
+ * @param path **Path for the mailbox** to unsubscribe from. Unicode is allowed. If value is an array then it is joined using current delimiter symbols. Namespace prefix is added automatically if required.
2444
+ * @returns `true` if unsubscription operation succeeded, `false` otherwise
2445
+ *
2446
+ * @example
2447
+ * await client.mailboxUnsubscribe('Important stuff');
2448
+ */
2449
+ async mailboxUnsubscribe(path) {
2450
+ return await this.run('UNSUBSCRIBE', path);
2451
+ }
2452
+ /**
2453
+ * Opens a mailbox to access messages. You can perform message operations only against an opened mailbox.
2454
+ * Using {@link ImapFlow#getMailboxLock} instead of `mailboxOpen()` is preferred. Both do the same thing
2455
+ * but next `getMailboxLock()` call is not executed until previous one is released.
2456
+ *
2457
+ * @param path **Path for the mailbox** to open
2458
+ * @param options optional options
2459
+ * @returns Mailbox info
2460
+ * @throws Will throw an error if mailbox does not exist or can not be opened
2461
+ *
2462
+ * @example
2463
+ * let mailbox = await client.mailboxOpen('Important stuff');
2464
+ * console.log(mailbox.exists);
2465
+ * // 125
2466
+ */
2467
+ async mailboxOpen(path, options) {
2468
+ return await this.run('SELECT', path, options);
2469
+ }
2470
+ /**
2471
+ * Closes a previously opened mailbox
2472
+ *
2473
+ * @returns Did the operation succeed or not
2474
+ *
2475
+ * @example
2476
+ * let mailbox = await client.mailboxOpen('INBOX');
2477
+ * await client.mailboxClose();
2478
+ */
2479
+ async mailboxClose() {
2480
+ return await this.run('CLOSE');
2481
+ }
2482
+ /**
2483
+ * Requests the status of the indicated mailbox. Only requested status values will be returned.
2484
+ *
2485
+ * @param path mailbox path to check for (unicode string). If value is an array then it is joined using current delimiter symbols. Namespace prefix is added automatically if required.
2486
+ * @param query defines requested status items
2487
+ * @returns status of the indicated mailbox
2488
+ *
2489
+ * @example
2490
+ * let status = await client.status('INBOX', {unseen: true});
2491
+ * console.log(status.unseen);
2492
+ * // 123
2493
+ */
2494
+ async status(path, query) {
2495
+ return await this.run('STATUS', path, query);
2496
+ }
2497
+ /**
2498
+ * Starts listening for new or deleted messages from the currently opened mailbox. Only required if `disableAutoIdle` is set to `true`
2499
+ * otherwise IDLE is started by default on connection inactivity. NB! If `idle()` is called manually then it does not
2500
+ * return until IDLE is finished which means you would have to call some other command out of scope.
2501
+ *
2502
+ * @returns Did the operation succeed or not
2503
+ *
2504
+ * @example
2505
+ * let mailbox = await client.mailboxOpen('INBOX');
2506
+ *
2507
+ * await client.idle();
2508
+ */
2509
+ async idle() {
2510
+ if (!this.idling) {
2511
+ return await this.run('IDLE', this.maxIdleTime);
2512
+ }
2513
+ }
2514
+ /**
2515
+ * Sets flags for a message or message range
2516
+ *
2517
+ * @param range Range to filter the messages
2518
+ * @param flags Array of flags to set. Only flags that are permitted to set are used, other flags are ignored
2519
+ * @param options Store options
2520
+ * @returns Did the operation succeed or not
2521
+ *
2522
+ * @example
2523
+ * let mailbox = await client.mailboxOpen('INBOX');
2524
+ * // mark all unseen messages as seen (and remove other flags)
2525
+ * await client.messageFlagsSet({seen: false}, ['\Seen]);
2526
+ */
2527
+ async messageFlagsSet(range, flags, options) {
2528
+ options = options || {};
2529
+ let resolved = await this.resolveRange(range, options);
2530
+ if (!resolved) {
2531
+ return false;
2532
+ }
2533
+ let queryOpts = Object.assign({
2534
+ operation: 'set'
2535
+ }, options);
2536
+ return await this.run('STORE', resolved, flags, queryOpts);
2537
+ }
2538
+ /**
2539
+ * Adds flags for a message or message range
2540
+ *
2541
+ * @param range Range to filter the messages
2542
+ * @param flags Array of flags to set. Only flags that are permitted to set are used, other flags are ignored
2543
+ * @param options Store options
2544
+ * @returns Did the operation succeed or not
2545
+ *
2546
+ * @example
2547
+ * let mailbox = await client.mailboxOpen('INBOX');
2548
+ * // mark all unseen messages as seen (and keep other flags as is)
2549
+ * await client.messageFlagsAdd({seen: false}, ['\Seen]);
2550
+ */
2551
+ async messageFlagsAdd(range, flags, options) {
2552
+ options = options || {};
2553
+ let resolved = await this.resolveRange(range, options);
2554
+ if (!resolved) {
2555
+ return false;
2556
+ }
2557
+ let queryOpts = Object.assign({
2558
+ operation: 'add'
2559
+ }, options);
2560
+ return await this.run('STORE', resolved, flags, queryOpts);
2561
+ }
2562
+ /**
2563
+ * Remove specific flags from a message or message range
2564
+ *
2565
+ * @param range Range to filter the messages
2566
+ * @param flags Array of flags to remove. Only flags that are permitted to set are used, other flags are ignored
2567
+ * @param options Store options
2568
+ * @returns Did the operation succeed or not
2569
+ *
2570
+ * @example
2571
+ * let mailbox = await client.mailboxOpen('INBOX');
2572
+ * // mark all seen messages as unseen by removing \\Seen flag
2573
+ * await client.messageFlagsRemove({seen: true}, ['\Seen]);
2574
+ */
2575
+ async messageFlagsRemove(range, flags, options) {
2576
+ options = options || {};
2577
+ let resolved = await this.resolveRange(range, options);
2578
+ if (!resolved) {
2579
+ return false;
2580
+ }
2581
+ let queryOpts = Object.assign({
2582
+ operation: 'remove'
2583
+ }, options);
2584
+ return await this.run('STORE', resolved, flags, queryOpts);
2585
+ }
2586
+ /**
2587
+ * Sets a colored flag for an email. Only supported by mail clients like Apple Mail
2588
+ *
2589
+ * @param range Range to filter the messages
2590
+ * @param color The color to set. One of 'red', 'orange', 'yellow', 'green', 'blue', 'purple', and 'grey'
2591
+ * @param options Store options
2592
+ * @returns Did the operation succeed or not
2593
+ *
2594
+ * @example
2595
+ * let mailbox = await client.mailboxOpen('INBOX');
2596
+ * // add a purple flag for all emails
2597
+ * await client.setFlagColor('1:*', 'Purple');
2598
+ */
2599
+ async setFlagColor(range, color, options) {
2600
+ options = options || {};
2601
+ let resolved = await this.resolveRange(range, options);
2602
+ if (!resolved) {
2603
+ return false;
2604
+ }
2605
+ let flagChanges = (0, tools_js_1.getColorFlags)(color);
2606
+ if (!flagChanges) {
2607
+ return false;
2608
+ }
2609
+ let addResults;
2610
+ let removeResults;
2611
+ if (flagChanges.add && flagChanges.add.length) {
2612
+ let queryOpts = Object.assign({
2613
+ operation: 'add'
2614
+ }, options, {
2615
+ useLabels: false, // override if set
2616
+ // prevent triggering a premature Flags change notification
2617
+ silent: flagChanges.remove && flagChanges.remove.length
2618
+ });
2619
+ addResults = await this.run('STORE', resolved, flagChanges.add, queryOpts);
2620
+ }
2621
+ if (flagChanges.remove && flagChanges.remove.length) {
2622
+ let queryOpts = Object.assign({
2623
+ operation: 'remove'
2624
+ }, options, { useLabels: false } // override if set
2625
+ );
2626
+ removeResults = await this.run('STORE', resolved, flagChanges.remove, queryOpts);
2627
+ }
2628
+ return addResults || removeResults || false;
2629
+ }
2630
+ /**
2631
+ * Delete messages from the currently opened mailbox. Method does not indicate info about deleted messages,
2632
+ * instead you should be using the `expunge` event for this
2633
+ *
2634
+ * @param range Range to filter the messages
2635
+ * @param options Range options
2636
+ * @returns Did the operation succeed or not
2637
+ *
2638
+ * @example
2639
+ * let mailbox = await client.mailboxOpen('INBOX');
2640
+ * // delete all seen messages
2641
+ * await client.messageDelete({seen: true});
2642
+ */
2643
+ async messageDelete(range, options) {
2644
+ options = options || {};
2645
+ let resolved = await this.resolveRange(range, options);
2646
+ if (!resolved) {
2647
+ return false;
2648
+ }
2649
+ return await this.run('EXPUNGE', resolved, options);
2650
+ }
2651
+ /**
2652
+ * Appends a new message to a mailbox
2653
+ *
2654
+ * @param path Mailbox path to upload the message to (unicode string). If value is an array then it is joined using current delimiter symbols. Namespace prefix is added automatically if required.
2655
+ * @param content RFC822 formatted email message
2656
+ * @param flags an array of flags to be set for the uploaded message
2657
+ * @param idate internal date to be set for the message
2658
+ * @returns info about uploaded message
2659
+ *
2660
+ * @example
2661
+ * await client.append('INBOX', rawMessageBuffer, ['\\Seen'], new Date(2000, 1, 1));
2662
+ */
2663
+ async append(path, content, flags, idate) {
2664
+ return (await this.run('APPEND', path, content, flags, idate)) || false;
2665
+ }
2666
+ /**
2667
+ * Copies messages from current mailbox to destination mailbox
2668
+ *
2669
+ * @param range Range of messages to copy
2670
+ * @param destination Mailbox path to copy the messages to. If value is an array then it is joined using current delimiter symbols. Namespace prefix is added automatically if required.
2671
+ * @param options Range options
2672
+ * @returns info about copies messages
2673
+ *
2674
+ * @example
2675
+ * await client.mailboxOpen('INBOX');
2676
+ * // copy all messages to a mailbox called "Backup" (must exist)
2677
+ * let result = await client.messageCopy('1:*', 'Backup');
2678
+ * console.log('Copied %s messages', result.uidMap.size);
2679
+ */
2680
+ async messageCopy(range, destination, options) {
2681
+ options = options || {};
2682
+ let resolved = await this.resolveRange(range, options);
2683
+ if (!resolved) {
2684
+ return false;
2685
+ }
2686
+ return await this.run('COPY', resolved, destination, options);
2687
+ }
2688
+ /**
2689
+ * Moves messages from current mailbox to destination mailbox
2690
+ *
2691
+ * @param range Range of messages to move
2692
+ * @param destination Mailbox path to move the messages to. If value is an array then it is joined using current delimiter symbols. Namespace prefix is added automatically if required.
2693
+ * @param options Range options
2694
+ * @returns info about moved messages
2695
+ *
2696
+ * @example
2697
+ * await client.mailboxOpen('INBOX');
2698
+ * // move all messages to a mailbox called "Trash" (must exist)
2699
+ * let result = await client.messageMove('1:*', 'Trash');
2700
+ * console.log('Moved %s messages', result.uidMap.size);
2701
+ */
2702
+ async messageMove(range, destination, options) {
2703
+ options = options || {};
2704
+ let resolved = await this.resolveRange(range, options);
2705
+ if (!resolved) {
2706
+ return false;
2707
+ }
2708
+ return await this.run('MOVE', resolved, destination, options);
2709
+ }
2710
+ async search(query, options) {
2711
+ if (!this.mailbox) {
2712
+ // no mailbox selected, nothing to do
2713
+ return;
2714
+ }
2715
+ const result = (await this.run('SEARCH', query, options)) || false;
2716
+ // When returnOptions was requested but server lacked ESEARCH capability,
2717
+ // search.ts returns a plain number[]. Derive ESearchResult client-side.
2718
+ if (options && options.returnOptions && Array.isArray(result)) {
2719
+ const arr = result;
2720
+ // Normalize to uppercase so callers can use mixed-case strings like 'count'
2721
+ const normalizedOptions = options.returnOptions.map(o => (typeof o === 'string' ? o.toUpperCase() : o));
2722
+ const esearch = {};
2723
+ if (normalizedOptions.includes('COUNT')) {
2724
+ esearch.count = arr.length;
2725
+ }
2726
+ if (normalizedOptions.includes('MIN') && arr.length) {
2727
+ esearch.min = arr[0]; // already sorted ascending by search.ts
2728
+ }
2729
+ if (normalizedOptions.includes('MAX') && arr.length) {
2730
+ esearch.max = arr[arr.length - 1];
2731
+ }
2732
+ if (normalizedOptions.includes('ALL') && arr.length) {
2733
+ esearch.all = (0, tools_js_1.packMessageRange)(arr);
2734
+ }
2735
+ // PARTIAL cannot be derived client-side, omit it.
2736
+ // When returnOptions contains only { partial: ... } items and the server
2737
+ // lacks ESEARCH, PARTIAL cannot be derived client-side. Return the raw
2738
+ // number[] so the caller has actionable data. Note: this is an edge case,
2739
+ // callers targeting no-ESEARCH servers should avoid requesting PARTIAL
2740
+ // without COUNT or ALL.
2741
+ if (Object.keys(esearch).length === 0) {
2742
+ return result;
2743
+ }
2744
+ return esearch;
2745
+ }
2746
+ return result;
2747
+ }
2748
+ /**
2749
+ * Fetch messages from the currently opened mailbox
2750
+ *
2751
+ * @param range Range of messages to fetch
2752
+ * @param query Fetch query
2753
+ * @param options Fetch options
2754
+ * @yields Message data object
2755
+ *
2756
+ * @example
2757
+ * let mailbox = await client.mailboxOpen('INBOX');
2758
+ * // fetch UID for all messages in a mailbox
2759
+ * for await (let msg of client.fetch('1:*', {uid: true})){
2760
+ * console.log(msg.uid);
2761
+ * // NB! You can not run any IMAP commands in this loop
2762
+ * // otherwise you will end up in a deadloop
2763
+ * }
2764
+ */
2765
+ async *fetch(range, query, options) {
2766
+ options = options || {};
2767
+ if (!this.mailbox) {
2768
+ // no mailbox selected, nothing to do
2769
+ return;
2770
+ }
2771
+ let resolved = await this.resolveRange(range, options);
2772
+ if (!resolved) {
2773
+ return false;
2774
+ }
2775
+ // Push/pull coordination for the async generator pattern:
2776
+ // The FETCH command handler pushes results into rowQueue via onUntaggedFetch.
2777
+ // The generator consumer pulls via getNext(). The `push` callback bridges the
2778
+ // two: when the consumer is waiting and the queue is empty, `push` is set to
2779
+ // a function that wakes up the consumer when new data arrives.
2780
+ let finished = false;
2781
+ let aborted = false;
2782
+ let push = false;
2783
+ let rowQueue = [];
2784
+ let getNext = () => new Promise((resolve, reject) => {
2785
+ let check = () => {
2786
+ if (rowQueue.length) {
2787
+ let entry = rowQueue.shift();
2788
+ if (entry.err) {
2789
+ return reject(entry.err);
2790
+ }
2791
+ return resolve(entry.value);
2792
+ }
2793
+ if (finished) {
2794
+ return resolve(null);
2795
+ }
2796
+ // No data available yet; register a wakeup callback
2797
+ push = () => {
2798
+ push = false;
2799
+ check();
2800
+ };
2801
+ };
2802
+ check();
2803
+ });
2804
+ // Fire-and-forget the FETCH command. It runs in the background while
2805
+ // the generator yields results. Each untagged FETCH response is paired
2806
+ // with a `next` callback that acts as backpressure: the FETCH handler
2807
+ // won't process the next response until the consumer calls next().
2808
+ this.run('FETCH', resolved, query, {
2809
+ uid: !!options.uid,
2810
+ binary: options.binary,
2811
+ changedSince: options.changedSince,
2812
+ onUntaggedFetch: (untagged, next) => {
2813
+ if (aborted) {
2814
+ next();
2815
+ return;
2816
+ }
2817
+ rowQueue.push({
2818
+ value: {
2819
+ response: untagged,
2820
+ next
2821
+ }
2822
+ });
2823
+ if (typeof push === 'function') {
2824
+ push();
2825
+ }
2826
+ }
2827
+ })
2828
+ .then(() => {
2829
+ finished = true;
2830
+ if (typeof push === 'function') {
2831
+ push();
2832
+ }
2833
+ })
2834
+ .catch(err => {
2835
+ rowQueue.push({ err });
2836
+ if (typeof push === 'function') {
2837
+ push();
2838
+ }
2839
+ });
2840
+ let lastRes = null;
2841
+ try {
2842
+ let res;
2843
+ while ((res = await getNext())) {
2844
+ lastRes = res;
2845
+ if (this.isClosed || !this.socket || this.socket.destroyed) {
2846
+ throw this.createConnectionError('EConnectionClosed', 'Connection closed', { rejectedFrom: 'fetchStream', command: 'FETCH' });
2847
+ }
2848
+ yield res.response;
2849
+ // Signal the FETCH handler to process the next untagged response
2850
+ res.next();
2851
+ lastRes = null;
2852
+ }
2853
+ }
2854
+ finally {
2855
+ aborted = true;
2856
+ // Release backpressure for the item that was yielded but whose
2857
+ // next() was not yet called (happens on break/return/throw)
2858
+ if (lastRes && typeof lastRes.next === 'function') {
2859
+ lastRes.next();
2860
+ }
2861
+ while (rowQueue.length) {
2862
+ let entry = rowQueue.shift();
2863
+ if (entry.value && typeof entry.value.next === 'function') {
2864
+ entry.value.next();
2865
+ }
2866
+ }
2867
+ }
2868
+ }
2869
+ /**
2870
+ * Fetch messages from the currently opened mailbox.
2871
+ *
2872
+ * This method will fetch all messages before resolving the promise, unlike .fetch(), which
2873
+ * is an async generator. Do not use large ranges like 1:*, as this might exhaust all available
2874
+ * memory if the mailbox contains a large number of emails.
2875
+ * @param range Range of messages to fetch
2876
+ * @param query Fetch query
2877
+ * @param options Fetch options
2878
+ * @returns Array of Message data object
2879
+ *
2880
+ * @example
2881
+ * let mailbox = await client.mailboxOpen('INBOX');
2882
+ * // fetch UID for all messages in a mailbox
2883
+ * const messages = await client.fetchAll('1:*', {uid: true});
2884
+ * for (let msg of messages){
2885
+ * console.log(msg.uid);
2886
+ * }
2887
+ */
2888
+ async fetchAll(range, query, options) {
2889
+ const results = [];
2890
+ const generator = this.fetch(range, query, options);
2891
+ for await (const message of generator) {
2892
+ results.push(message);
2893
+ }
2894
+ return results;
2895
+ }
2896
+ /**
2897
+ * Fetch a single message from the currently opened mailbox
2898
+ *
2899
+ * @param seq Single UID or sequence number of the message to fetch for
2900
+ * @param query Fetch query
2901
+ * @param options Fetch options
2902
+ * @returns Message data object
2903
+ *
2904
+ * @example
2905
+ * let mailbox = await client.mailboxOpen('INBOX');
2906
+ * // fetch UID for the last email in the selected mailbox
2907
+ * let lastMsg = await client.fetchOne('*', {uid: true})
2908
+ * console.log(lastMsg.uid);
2909
+ */
2910
+ async fetchOne(seq, query, options) {
2911
+ if (!this.mailbox) {
2912
+ // no mailbox selected, nothing to do
2913
+ return;
2914
+ }
2915
+ if (seq === '*') {
2916
+ if (!this.mailbox.exists) {
2917
+ return false;
2918
+ }
2919
+ seq = this.mailbox.exists.toString();
2920
+ options = Object.assign({}, options || {}, { uid: false }); // force into a sequence query
2921
+ }
2922
+ let response = await this.run('FETCH', (seq || '').toString(), query, options);
2923
+ if (!response || !response.list || !response.list.length) {
2924
+ return false;
2925
+ }
2926
+ return response.list[0];
2927
+ }
2928
+ /**
2929
+ * Download either full rfc822 formatted message or a specific bodystructure part as a Stream.
2930
+ * Bodystructure parts are decoded so the resulting stream is a binary file. Text content
2931
+ * is automatically converted to UTF-8 charset.
2932
+ *
2933
+ * @param range UID or sequence number for the message to fetch
2934
+ * @param part If not set then downloads entire rfc822 formatted message, otherwise downloads specific bodystructure part
2935
+ * @param options Download options
2936
+ * @returns Download data object. Resolves with an empty object when no mailbox is selected or the message or part was not found
2937
+ *
2938
+ * @example
2939
+ * let mailbox = await client.mailboxOpen('INBOX');
2940
+ * // download body part nr '1.2' from latest message
2941
+ * let {meta, content} = await client.download('*', '1.2');
2942
+ * content.pipe(fs.createWriteStream(meta.filename));
2943
+ */
2944
+ async download(range, part, options) {
2945
+ if (!this.mailbox) {
2946
+ // no mailbox selected, nothing to do
2947
+ return {};
2948
+ }
2949
+ let downloadOptions = Object.assign({
2950
+ chunkSize: 64 * 1024,
2951
+ maxBytes: Infinity
2952
+ }, options || {});
2953
+ let hasMore = true;
2954
+ let processed = 0;
2955
+ let chunkSize = Number(downloadOptions.chunkSize) || 64 * 1024;
2956
+ // Normalized once here so every bounded stage of the pipeline below agrees on the budget
2957
+ let maxBytes = (0, limited_passthrough_js_1.normalizeByteLimit)(downloadOptions.maxBytes);
2958
+ let uid = false;
2959
+ if (part === '1') {
2960
+ // Special handling for part "1": in single-node emails (no childNodes),
2961
+ // the body is accessed via "TEXT" rather than "1", and headers via
2962
+ // "HEADER" instead of "1.MIME". Check bodyStructure to detect this.
2963
+ let response = await this.fetchOne(range, { uid: true, bodyStructure: true }, downloadOptions);
2964
+ if (!response) {
2965
+ return { response: false, chunk: false };
2966
+ }
2967
+ if (!uid && response.uid) {
2968
+ uid = response.uid;
2969
+ // force UID from now on even if first range was a sequence number
2970
+ range = uid;
2971
+ downloadOptions.uid = true;
2972
+ }
2973
+ if (!response.bodyStructure.childNodes) {
2974
+ // single text message
2975
+ part = 'TEXT';
2976
+ }
2977
+ }
2978
+ let getNextPart = async (query) => {
2979
+ query = query || {};
2980
+ let mimeKey;
2981
+ if (!part) {
2982
+ query.source = {
2983
+ start: processed,
2984
+ maxLength: chunkSize
2985
+ };
2986
+ }
2987
+ else {
2988
+ part = part.toString().toLowerCase().trim();
2989
+ if (!query.bodyParts) {
2990
+ query.bodyParts = [];
2991
+ }
2992
+ if (query.size) {
2993
+ if (/^[\d.]+$/.test(part)) {
2994
+ // fetch meta as well
2995
+ mimeKey = part + '.mime';
2996
+ query.bodyParts.push(mimeKey);
2997
+ }
2998
+ else if (part === 'text') {
2999
+ mimeKey = 'header';
3000
+ query.bodyParts.push(mimeKey);
3001
+ }
3002
+ }
3003
+ query.bodyParts.push({
3004
+ key: part,
3005
+ start: processed,
3006
+ maxLength: chunkSize
3007
+ });
3008
+ }
3009
+ let response = await this.fetchOne(range, query, downloadOptions);
3010
+ if (!response) {
3011
+ return { response: false, chunk: false };
3012
+ }
3013
+ if (!uid && response.uid) {
3014
+ uid = response.uid;
3015
+ // force UID from now on even if first range was a sequence number
3016
+ range = uid;
3017
+ downloadOptions.uid = true;
3018
+ }
3019
+ let chunk = !part ? response.source : response.bodyParts && response.bodyParts.get(part);
3020
+ if (!chunk) {
3021
+ return {};
3022
+ }
3023
+ processed += chunk.length;
3024
+ hasMore = chunk.length >= chunkSize;
3025
+ let result = { chunk };
3026
+ if (query.size) {
3027
+ result.response = response;
3028
+ }
3029
+ if (query.bodyParts) {
3030
+ if (mimeKey === 'header') {
3031
+ result.mime = response.headers;
3032
+ }
3033
+ else {
3034
+ result.mime = response.bodyParts && mimeKey ? response.bodyParts.get(mimeKey) : undefined;
3035
+ }
3036
+ }
3037
+ return result;
3038
+ };
3039
+ let { response, chunk, mime } = await getNextPart({
3040
+ size: true,
3041
+ uid: true
3042
+ });
3043
+ if (!response || !chunk) {
3044
+ // ???
3045
+ return {};
3046
+ }
3047
+ let meta = {
3048
+ expectedSize: response.size
3049
+ };
3050
+ if (!part) {
3051
+ meta.contentType = 'message/rfc822';
3052
+ }
3053
+ else if (mime) {
3054
+ let headers = new mailsplit_1.Headers(mime);
3055
+ let contentType = libmime_1.default.parseHeaderValue(headers.getFirst('Content-Type'));
3056
+ let transferEncoding = libmime_1.default.parseHeaderValue(headers.getFirst('Content-Transfer-Encoding'));
3057
+ let disposition = libmime_1.default.parseHeaderValue(headers.getFirst('Content-Disposition'));
3058
+ if (contentType.value.toLowerCase().trim()) {
3059
+ meta.contentType = contentType.value.toLowerCase().trim();
3060
+ }
3061
+ if (contentType.params.charset) {
3062
+ meta.charset = contentType.params.charset.toLowerCase().trim();
3063
+ }
3064
+ if (transferEncoding.value) {
3065
+ meta.encoding = transferEncoding.value
3066
+ .replace(/\(.*\)/g, '')
3067
+ .toLowerCase()
3068
+ .trim();
3069
+ }
3070
+ if (disposition.value) {
3071
+ /* c8 ignore next */ // a parsed disposition value is never all-whitespace, so the `false` fallback is unreachable
3072
+ meta.disposition = disposition.value.toLowerCase().trim() || false;
3073
+ try {
3074
+ meta.disposition = libmime_1.default.decodeWords(meta.disposition);
3075
+ }
3076
+ catch {
3077
+ // failed to parse disposition, keep as is (most probably an unknown charset is used)
3078
+ }
3079
+ }
3080
+ if (contentType.params.format && contentType.params.format.toLowerCase().trim() === 'flowed') {
3081
+ meta.flowed = true;
3082
+ if (contentType.params.delsp && contentType.params.delsp.toLowerCase().trim() === 'yes') {
3083
+ meta.delSp = true;
3084
+ }
3085
+ }
3086
+ let filename = disposition.params.filename || contentType.params.name || false;
3087
+ if (filename) {
3088
+ try {
3089
+ filename = libmime_1.default.decodeWords(filename);
3090
+ }
3091
+ catch {
3092
+ // failed to parse filename, keep as is (most probably an unknown charset is used)
3093
+ }
3094
+ meta.filename = filename;
3095
+ }
3096
+ }
3097
+ let stream;
3098
+ let output;
3099
+ let fetchAborted = false;
3100
+ // Build a decoder pipeline that progressively transforms the raw FETCH data:
3101
+ // 1. Transfer-encoding decoder (base64 or quoted-printable -> binary)
3102
+ // 2. Format decoder (format=flowed -> plain text, if applicable)
3103
+ // 3. Charset decoder (non-UTF-8 -> UTF-8, for text parts only)
3104
+ // 4. Byte limiter (enforces maxBytes cap)
3105
+ // `stream` is the head of the pipeline (where raw chunks are written),
3106
+ // `output` is the tail (what the caller reads from).
3107
+ // Parts that arrived via FETCH BINARY (response.binaryParts) are already
3108
+ // decoded by the server - decoding again would corrupt the data, so stage 1
3109
+ // is skipped for them.
3110
+ let clientEncoding = response.binaryParts && part && response.binaryParts.has(part) ? false : meta.encoding;
3111
+ switch (clientEncoding) {
3112
+ case 'base64':
3113
+ output = stream = new libbase64_1.default.Decoder();
3114
+ break;
3115
+ case 'quoted-printable':
3116
+ output = stream = new libqp_1.default.Decoder();
3117
+ break;
3118
+ default:
3119
+ output = stream = new node_stream_1.PassThrough();
3120
+ }
3121
+ // Every byte-bounded stage of the pipeline. The fetch loop below stops as soon as any of
3122
+ // them has taken all it will accept. The limiter at the tail is not enough on its own: a
3123
+ // transform in the middle that buffers its whole input before emitting anything (the
3124
+ // format=flowed decoder, the Japanese charset decoder) leaves the tail limiter reporting
3125
+ // `limited === false` however much the server sends, so a download with a small maxBytes
3126
+ // would still pull the entire part off the wire.
3127
+ let limiters = [];
3128
+ let isLimited = () => limiters.some(entry => entry.limited);
3129
+ // Appending a stage means forwarding the current tail's errors to it before piping, so a
3130
+ // failure anywhere reaches the stream the caller is reading
3131
+ let pipeStage = (stage) => {
3132
+ output.on('error', err => {
3133
+ stage.emit('error', err);
3134
+ });
3135
+ output = output.pipe(stage);
3136
+ return stage;
3137
+ };
3138
+ let isTextNode = ['text/html', 'text/plain', 'text/x-amp-html'].includes(meta.contentType) || (part === '1' && !meta.contentType);
3139
+ if ((!meta.disposition || meta.disposition === 'inline') && isTextNode) {
3140
+ // RFC 3676 format=flowed text: unwrap soft line breaks
3141
+ if (meta.flowed) {
3142
+ // FlowedDecoder buffers its whole input before emitting, and being third party it
3143
+ // carries no bound of its own, so bound what it can ever be handed. Unwrapping only
3144
+ // removes bytes, so capping its input at maxBytes cannot push the delivered output
3145
+ // above the cap either.
3146
+ limiters.push(pipeStage(new limited_passthrough_js_1.LimitedPassthrough({ maxBytes })));
3147
+ pipeStage(new flowed_decoder_js_1.default(meta.delSp ? { delSp: true } : {}));
3148
+ }
3149
+ // Convert non-UTF-8 charsets to UTF-8 via a streaming decoder.
3150
+ // ASCII and UTF-8 need no conversion. Unknown charsets are left as-is.
3151
+ if (meta.charset && !['ascii', 'usascii', 'utf8'].includes(meta.charset.toLowerCase().replace(/[^a-z0-9]+/g, ''))) {
3152
+ try {
3153
+ let decoder = (0, tools_js_1.getDecoder)(meta.charset, maxBytes);
3154
+ // Safety listener attached first so the decoder always has at least
3155
+ // one 'error' listener. Prevents Node.js from throwing
3156
+ // ERR_UNHANDLED_ERROR if a later pipe setup step throws and leaves
3157
+ // the source-forwarding closure attached without a downstream
3158
+ // listener wired up. Any real listener the caller attaches still
3159
+ // fires in addition to this one.
3160
+ decoder.on('error', err => {
3161
+ this.log.warn({ err, charset: meta.charset, cid: this.id });
3162
+ });
3163
+ // The Japanese decoder buffers its whole input as well, and reports the same
3164
+ // `limited` flag the limiters do so the fetch loop can stop once it is full.
3165
+ // A streaming decoder has no such flag, which reads as false and is correct.
3166
+ limiters.push(pipeStage(decoder));
3167
+ // force to utf-8 for output
3168
+ meta.charset = 'utf-8';
3169
+ }
3170
+ catch {
3171
+ // do not decode charset
3172
+ }
3173
+ }
3174
+ }
3175
+ let limiter = pipeStage(new limited_passthrough_js_1.LimitedPassthrough({ maxBytes }));
3176
+ limiters.push(limiter);
3177
+ // Cleanup function
3178
+ const cleanup = () => {
3179
+ fetchAborted = true;
3180
+ if (stream && !stream.destroyed) {
3181
+ stream.destroy();
3182
+ }
3183
+ };
3184
+ // Listen for stream destruction
3185
+ output.once('error', cleanup);
3186
+ output.once('close', cleanup);
3187
+ let writeChunk = (chunk) => {
3188
+ if (isLimited() || fetchAborted || stream.destroyed) {
3189
+ return true;
3190
+ }
3191
+ return stream.write(chunk);
3192
+ };
3193
+ // Fetch remaining chunks in a loop, writing each to the decoder stream.
3194
+ // Stops when the server returns a short chunk (< chunkSize), the byte
3195
+ // limiter is satisfied, or the consumer destroys the output stream.
3196
+ let fetchAllParts = async () => {
3197
+ while (hasMore && !isLimited() && !fetchAborted) {
3198
+ let { chunk } = await getNextPart();
3199
+ if (!chunk || fetchAborted) {
3200
+ break;
3201
+ }
3202
+ // Handle backpressure
3203
+ if (writeChunk(chunk) === false) {
3204
+ // Wait for drain event before continuing
3205
+ try {
3206
+ await new Promise((resolve, reject) => {
3207
+ // finish() is the listener itself, as settle() is for the TLS upgrade:
3208
+ // 'drain' and 'close' emit no arguments, 'error' emits the error, and
3209
+ // removal needs no separate handler references. It removes only the
3210
+ // three listeners this wait installed - removeAllListeners('error')
3211
+ // also took off the forwarder pipeStage() attached to the head stream
3212
+ // when the pipeline was built, and the head must keep that forwarder
3213
+ // for the life of the download or a chunk failure has nowhere to go.
3214
+ const finish = (err) => {
3215
+ for (let event of ['drain', 'error', 'close']) {
3216
+ stream.removeListener(event, finish);
3217
+ }
3218
+ /* c8 ignore next 2 */ // stream error during a backpressure drain wait is timing-dependent
3219
+ if (err) {
3220
+ reject(err);
3221
+ }
3222
+ else {
3223
+ resolve();
3224
+ }
3225
+ };
3226
+ stream.once('drain', finish);
3227
+ stream.once('error', finish);
3228
+ stream.once('close', finish);
3229
+ });
3230
+ /* c8 ignore start */ // re-throw path only triggers on a stream error mid-drain, which is timing-dependent
3231
+ }
3232
+ catch (err) {
3233
+ // Re-throw only if not aborted
3234
+ if (!fetchAborted) {
3235
+ throw err;
3236
+ }
3237
+ }
3238
+ /* c8 ignore stop */
3239
+ // Check if we should abort after waiting
3240
+ if (fetchAborted) {
3241
+ break;
3242
+ }
3243
+ }
3244
+ }
3245
+ };
3246
+ // A download is a sequence of chunk FETCHes with a backpressure wait in between. Those
3247
+ // gaps look exactly like an inactive connection, so without this auto-IDLE would start
3248
+ // between chunks and the next chunk would have to break it again - two extra round
3249
+ // trips per chunk, for as long as the consumer is slow. Counted before control returns
3250
+ // to the event loop: the head chunk's own FETCH already armed the auto-IDLE timer, and
3251
+ // with a very short autoIdleDelay that timer could otherwise fire before the deferred
3252
+ // chunk loop below has marked the download open.
3253
+ this._openDownloads++;
3254
+ let downloadDone = false;
3255
+ let finishDownload = () => {
3256
+ if (!downloadDone) {
3257
+ downloadDone = true;
3258
+ this._openDownloads--;
3259
+ this.autoidle();
3260
+ }
3261
+ };
3262
+ // Kick off the download pipeline asynchronously. The first chunk was
3263
+ // already fetched above (to get metadata); write it to the decoder
3264
+ // stream and then fetch remaining chunks via fetchAllParts().
3265
+ // setImmediate ensures the caller gets the {meta, content} return
3266
+ // value before streaming begins.
3267
+ let runFetchAllParts = () => {
3268
+ fetchAllParts()
3269
+ .catch(err => {
3270
+ if (!fetchAborted && stream && !stream.destroyed) {
3271
+ stream.emit('error', err);
3272
+ /* c8 ignore start */ // the else logs when a fetch error arrives after the stream was already torn down (timing-dependent)
3273
+ }
3274
+ else {
3275
+ // Log when error cannot be emitted to stream
3276
+ this.log.warn({
3277
+ msg: 'Download error after stream closed',
3278
+ err,
3279
+ fetchAborted,
3280
+ streamDestroyed: stream?.destroyed,
3281
+ cid: this.id
3282
+ });
3283
+ }
3284
+ /* c8 ignore stop */
3285
+ })
3286
+ .finally(() => {
3287
+ finishDownload();
3288
+ if (!fetchAborted && stream && !stream.destroyed) {
3289
+ stream.end();
3290
+ }
3291
+ })
3292
+ // Terminal guard: nothing consumes this chain, so a throw from either handler
3293
+ // above rejects a promise nobody holds and takes the process down on
3294
+ // unhandledRejection. Reaching it always means an invariant broke - the head
3295
+ // stream kept pipeStage()'s error forwarder for the life of the download, so
3296
+ // emit('error') above has somewhere to go - which is why it logs at error even
3297
+ // for a routine-looking connection code.
3298
+ .catch(err => this.log.error({ msg: 'Failed to fail the download stream', err, cid: this.id }));
3299
+ };
3300
+ setImmediate(() => {
3301
+ let writeResult;
3302
+ try {
3303
+ writeResult = writeChunk(chunk);
3304
+ }
3305
+ catch (err) {
3306
+ stream.emit('error', err);
3307
+ finishDownload();
3308
+ /* c8 ignore next 3 */ // emitting the error above triggers cleanup (fetchAborted=true), so this end() guard is already false here
3309
+ if (!fetchAborted && stream && !stream.destroyed) {
3310
+ stream.end();
3311
+ }
3312
+ return;
3313
+ }
3314
+ /* c8 ignore next 9 */ // `stream` is piped to the limiter before this runs, so the head write drains synchronously and always returns true (verified for chunkSize up to 8MB); the drain-wait branch is unreachable
3315
+ if (!writeResult) {
3316
+ // Initial chunk filled the buffer, wait for drain
3317
+ stream.once('drain', () => {
3318
+ if (!fetchAborted) {
3319
+ runFetchAllParts();
3320
+ }
3321
+ else {
3322
+ finishDownload();
3323
+ }
3324
+ });
3325
+ }
3326
+ else {
3327
+ runFetchAllParts();
3328
+ }
3329
+ });
3330
+ return {
3331
+ meta,
3332
+ content: output
3333
+ };
3334
+ }
3335
+ /**
3336
+ * Fetch multiple attachments as Buffer values
3337
+ *
3338
+ * @param range UID or sequence number for the message to fetch
3339
+ * @param parts A list of bodystructure parts
3340
+ * @param options Download options
3341
+ * @returns Download data object, keyed by part
3342
+ *
3343
+ * @example
3344
+ * let mailbox = await client.mailboxOpen('INBOX');
3345
+ * // download body parts '2', and '3' from all messages in the selected mailbox
3346
+ * let response = await client.downloadMany('*', ['2', '3']);
3347
+ * process.stdout.write(response[2].content)
3348
+ * process.stdout.write(response[3].content)
3349
+ */
3350
+ async downloadMany(range, parts, options) {
3351
+ if (!this.mailbox) {
3352
+ // no mailbox selected, nothing to do
3353
+ return {};
3354
+ }
3355
+ let downloadOptions = Object.assign({
3356
+ chunkSize: 64 * 1024,
3357
+ maxBytes: Infinity
3358
+ }, options || {});
3359
+ let query = { bodyParts: [] };
3360
+ for (let part of parts) {
3361
+ query.bodyParts.push(part + '.mime');
3362
+ query.bodyParts.push(part);
3363
+ }
3364
+ let response = await this.fetchOne(range, query, downloadOptions);
3365
+ if (!response || !response.bodyParts) {
3366
+ return { response: false };
3367
+ }
3368
+ let data = {};
3369
+ for (let [part, content] of response.bodyParts) {
3370
+ let keyParts = part.split('.mime');
3371
+ // The server chooses the BODY[...] keys it answers with: never let one be a
3372
+ // prototype-chain name, or the assignments below write onto Object.prototype
3373
+ // (process-wide pollution) instead of the result object.
3374
+ if ((0, tools_js_1.isUnsafeKey)(keyParts[0])) {
3375
+ continue;
3376
+ }
3377
+ if (keyParts.length === 1) {
3378
+ // content
3379
+ let key = keyParts[0];
3380
+ if (!data[key]) {
3381
+ data[key] = { content };
3382
+ }
3383
+ else {
3384
+ data[key].content = content;
3385
+ }
3386
+ }
3387
+ else if (keyParts.length === 2) {
3388
+ // header
3389
+ let key = keyParts[0];
3390
+ if (!data[key]) {
3391
+ data[key] = {};
3392
+ }
3393
+ let entry = data[key];
3394
+ if (!entry.meta) {
3395
+ entry.meta = {};
3396
+ }
3397
+ let meta = entry.meta;
3398
+ let headers = new mailsplit_1.Headers(content);
3399
+ let contentType = libmime_1.default.parseHeaderValue(headers.getFirst('Content-Type'));
3400
+ let transferEncoding = libmime_1.default.parseHeaderValue(headers.getFirst('Content-Transfer-Encoding'));
3401
+ let disposition = libmime_1.default.parseHeaderValue(headers.getFirst('Content-Disposition'));
3402
+ if (contentType.value.toLowerCase().trim()) {
3403
+ meta.contentType = contentType.value.toLowerCase().trim();
3404
+ }
3405
+ if (contentType.params.charset) {
3406
+ meta.charset = contentType.params.charset.toLowerCase().trim();
3407
+ }
3408
+ if (transferEncoding.value) {
3409
+ meta.encoding = transferEncoding.value
3410
+ .replace(/\(.*\)/g, '')
3411
+ .toLowerCase()
3412
+ .trim();
3413
+ }
3414
+ if (disposition.value) {
3415
+ /* c8 ignore next */ // a parsed disposition value is never all-whitespace, so the `false` fallback is unreachable
3416
+ meta.disposition = disposition.value.toLowerCase().trim() || false;
3417
+ try {
3418
+ meta.disposition = libmime_1.default.decodeWords(meta.disposition);
3419
+ }
3420
+ catch {
3421
+ // failed to parse disposition, keep as is (most probably an unknown charset is used)
3422
+ }
3423
+ }
3424
+ if (contentType.params.format && contentType.params.format.toLowerCase().trim() === 'flowed') {
3425
+ meta.flowed = true;
3426
+ if (contentType.params.delsp && contentType.params.delsp.toLowerCase().trim() === 'yes') {
3427
+ meta.delSp = true;
3428
+ }
3429
+ }
3430
+ let filename = disposition.params.filename || contentType.params.name || false;
3431
+ if (filename) {
3432
+ try {
3433
+ filename = libmime_1.default.decodeWords(filename);
3434
+ }
3435
+ catch {
3436
+ // failed to parse filename, keep as is (most probably an unknown charset is used)
3437
+ }
3438
+ meta.filename = filename;
3439
+ }
3440
+ }
3441
+ }
3442
+ for (let part of Object.keys(data)) {
3443
+ let entry = data[part];
3444
+ // `meta` is only built from the companion BODY[<part>.MIME] item. A server may
3445
+ // legally answer with fewer items than were requested, and one part arriving
3446
+ // without its MIME headers must not cost the caller the whole download.
3447
+ let meta = entry.meta || {};
3448
+ entry.meta = meta;
3449
+ // parts that arrived via FETCH BINARY (response.binaryParts) are already
3450
+ // decoded by the server - decoding again would corrupt the data
3451
+ let clientEncoding = response.binaryParts && response.binaryParts.has(part) ? false : meta.encoding;
3452
+ switch (clientEncoding) {
3453
+ case 'base64':
3454
+ entry.content = entry.content ? libbase64_1.default.decode(entry.content.toString()) : null;
3455
+ break;
3456
+ case 'quoted-printable':
3457
+ entry.content = entry.content ? libqp_1.default.decode(entry.content.toString()) : null;
3458
+ break;
3459
+ default:
3460
+ // keep as is, already a buffer
3461
+ }
3462
+ }
3463
+ return data;
3464
+ }
3465
+ /** @internal */
3466
+ async run(command, ...args) {
3467
+ command = command.toUpperCase();
3468
+ if (!this.commands.has(command)) {
3469
+ return false;
3470
+ }
3471
+ if (!this.socket || this.socket.destroyed) {
3472
+ throw this.createNoConnectionError(false, { rejectedFrom: 'noSocket', command });
3473
+ }
3474
+ (0, tools_js_1.clearTimer)(this.idleStartTimer);
3475
+ try {
3476
+ // The preCheck (breaking an active IDLE) sits inside the try on purpose: the
3477
+ // clearTimeout above is unconditional, so every exit - a failed command or a
3478
+ // preCheck that rejects - must still reach the finally, or auto-IDLE would stay
3479
+ // disarmed on an otherwise healthy connection until some later command succeeded.
3480
+ if (typeof this.preCheck === 'function') {
3481
+ await this.preCheck();
3482
+ }
3483
+ return await this.runInternal(command, ...args);
3484
+ }
3485
+ finally {
3486
+ // Re-arm auto-IDLE after every command, IDLE included. autoidle() clears any prior
3487
+ // timer and declines while the connection is busy or not SELECTED, so calling it
3488
+ // unconditionally is safe and is the single place the invariant lives. IDLE was once
3489
+ // carved out here on the theory that a command which broke it re-arms on its own way
3490
+ // out - but that only holds when a command broke it. When an IDLE or poll session ends
3491
+ // on its own (the server refused IDLE, ended it unsolicited, or a poll failed) this is
3492
+ // the only thing that re-arms it; without it such a connection would go dark until the
3493
+ // socket watchdog tore it down. When a command really did break IDLE, that command is
3494
+ // still in flight at this point, so autoidle() declines here and re-arms once it ends.
3495
+ this.autoidle();
3496
+ }
3497
+ }
3498
+ /**
3499
+ * Dispatches a command without the IDLE handshake that `run()` performs.
3500
+ *
3501
+ * Used by callers that already own the connection's idle state - fallback polling issues its
3502
+ * commands through here, because `run()` would await `preCheck()`, and the preCheck it would
3503
+ * await belongs to the very polling session making the call, so the session would cancel
3504
+ * itself. Auto-IDLE is not restarted either, for the same reason: the caller is the idle loop.
3505
+ *
3506
+ * @param command Command name, as registered in the command registry.
3507
+ * @param args Arguments forwarded to the command implementation.
3508
+ * @returns Whatever the command implementation returns, or `false` for an
3509
+ * unknown command.
3510
+ * @internal
3511
+ */
3512
+ async runInternal(command, ...args) {
3513
+ command = command.toUpperCase();
3514
+ if (!this.commands.has(command)) {
3515
+ return false;
3516
+ }
3517
+ if (!this.socket || this.socket.destroyed) {
3518
+ throw this.createNoConnectionError(false, { rejectedFrom: 'noSocket', command });
3519
+ }
3520
+ let handler = this.commands.get(command);
3521
+ return await handler(this, ...args);
3522
+ }
3523
+ // Mailbox lock queue processor. Implements a mutex pattern: only one lock
3524
+ // is active at a time. When the active lock is released, the next queued
3525
+ // lock is processed. The `processingLock` flag prevents concurrent runs
3526
+ // of this method (which could happen via setImmediate re-entry from release()).
3527
+ /** @internal */
3528
+ async processLocks() {
3529
+ const wasProcessing = this.processingLock;
3530
+ if (wasProcessing) {
3531
+ // Another processor is already running; it will pick up new locks
3532
+ this.log.trace({
3533
+ msg: 'Mailbox locking queued',
3534
+ path: this.mailbox && this.mailbox.path,
3535
+ pending: this.locks.length,
3536
+ idling: this.idling,
3537
+ activeLock: this.currentLock
3538
+ ? {
3539
+ lockId: this.currentLock.lockId,
3540
+ ...(this.currentLock.options?.description && { description: this.currentLock.options?.description })
3541
+ }
3542
+ : null
3543
+ });
3544
+ return;
3545
+ }
3546
+ this.processingLock = true;
3547
+ try {
3548
+ // Process all locks in queue until empty
3549
+ let processedCount = 0;
3550
+ while (this.locks.length > 0) {
3551
+ // Mutex invariant: at most one lock may be held at a time.
3552
+ // If a lock is already granted, stop processing; release() will
3553
+ // clear currentLock and reschedule us to pick up the next queued lock.
3554
+ if (this.currentLock) {
3555
+ break;
3556
+ }
3557
+ // Yield to event loop periodically to prevent CPU blocking
3558
+ processedCount++;
3559
+ if (processedCount % 5 === 0) {
3560
+ await new Promise(resolve => setImmediate(resolve));
3561
+ }
3562
+ const lock = this.locks.shift();
3563
+ const { resolve, reject, path, options, lockId } = lock;
3564
+ // From here on the grant/reject path owns the outcome; the acquire
3565
+ // timer must not race with resolution.
3566
+ if (lock.acquireTimer) {
3567
+ (0, tools_js_1.clearTimer)(lock.acquireTimer);
3568
+ lock.acquireTimer = null;
3569
+ }
3570
+ const armHeldTimer = () => {
3571
+ let threshold = Number(options.maxLockHoldTime ?? this.options.maxLockHoldTime ?? HELD_LOCK_WARN_MS);
3572
+ if (!threshold || threshold <= 0) {
3573
+ return;
3574
+ }
3575
+ lock.heldAt = Date.now();
3576
+ // Background diagnostic: must not keep the process alive on its own
3577
+ lock.heldWarnTimer = setTimeout(() => {
3578
+ lock.heldWarnTimer = null;
3579
+ this.log.warn({
3580
+ msg: 'Mailbox lock held for a long time',
3581
+ lockId: lock.lockId,
3582
+ path,
3583
+ heldFor: Date.now() - lock.heldAt,
3584
+ /* c8 ignore next */ // the held-lock-warning diagnostic with a description set is a timing-dependent log detail
3585
+ ...(options.description && { description: options.description }),
3586
+ cid: this.id
3587
+ });
3588
+ }, threshold);
3589
+ (0, tools_js_1.unrefTimer)(lock.heldWarnTimer);
3590
+ };
3591
+ // release() is captured per-lock. It must only clear this.currentLock
3592
+ // if the caller still owns it - otherwise a stale release (after a
3593
+ // disconnect replaced the lock, or a double-release from user code)
3594
+ // would clear the new holder's lock and allow concurrent access.
3595
+ const release = () => {
3596
+ if (this.currentLock === lock) {
3597
+ if (lock.heldWarnTimer) {
3598
+ (0, tools_js_1.clearTimer)(lock.heldWarnTimer);
3599
+ lock.heldWarnTimer = null;
3600
+ }
3601
+ this.log.trace({
3602
+ msg: 'Mailbox lock released',
3603
+ lockId: lock.lockId,
3604
+ path: this.mailbox && this.mailbox.path,
3605
+ pending: this.locks.length,
3606
+ idling: this.idling
3607
+ });
3608
+ this.currentLock = false;
3609
+ // autoidle() will not arm while a lock is held, so the release is what
3610
+ // restarts it. It re-checks the queue itself, so a lock waiting behind
3611
+ // this one still keeps IDLE off.
3612
+ this.autoidle();
3613
+ // Use setImmediate to avoid stack overflow
3614
+ setImmediate(() => {
3615
+ this.processLocks().catch(err => this.log.error({ err, cid: this.id }));
3616
+ });
3617
+ }
3618
+ else {
3619
+ this.log.trace({
3620
+ msg: 'Ignoring stale lock release',
3621
+ lockId: lock.lockId,
3622
+ cid: this.id
3623
+ });
3624
+ }
3625
+ };
3626
+ if (!this.usable || !this.socket || this.socket.destroyed) {
3627
+ this.log.trace({ msg: 'Failed to acquire mailbox lock', path, lockId, idling: this.idling });
3628
+ reject(this.createNoConnectionError(false, { rejectedFrom: 'mailboxLock', path }));
3629
+ continue; // Process next lock in queue
3630
+ }
3631
+ // Both grant paths finish the same way. autoidle() is re-checked because a stale
3632
+ // auto-IDLE timer may still be armed at this point: on the SELECT path run()
3633
+ // re-arms auto-IDLE when the SELECT settles - a moment before currentLock is set -
3634
+ // and the fast path can inherit a timer from an earlier command. Either way the
3635
+ // timer must not fire inside the lock.
3636
+ const grantLock = () => {
3637
+ this.currentLock = lock;
3638
+ armHeldTimer();
3639
+ this.autoidle();
3640
+ resolve({ path, release });
3641
+ };
3642
+ if (this.mailbox && this.mailbox.path === path && !!this.mailbox.readOnly === !!options.readOnly) {
3643
+ // Fast path: mailbox is already selected with the right access mode
3644
+ this.log.trace({
3645
+ msg: 'Mailbox lock acquired [existing]',
3646
+ path,
3647
+ lockId,
3648
+ idling: this.idling,
3649
+ ...(options.description && { description: options.description })
3650
+ });
3651
+ grantLock();
3652
+ break; // Stop processing; next lock waits for release()
3653
+ }
3654
+ try {
3655
+ // Need to SELECT/EXAMINE a different mailbox
3656
+ await this.mailboxOpen(path, options);
3657
+ this.log.trace({
3658
+ msg: 'Mailbox lock acquired [selected]',
3659
+ path,
3660
+ lockId,
3661
+ idling: this.idling,
3662
+ ...(options.description && { description: options.description })
3663
+ });
3664
+ grantLock();
3665
+ break; // Wait for this lock to be released
3666
+ }
3667
+ catch (err) {
3668
+ if (err.responseStatus === 'NO') {
3669
+ // SELECT failed with NO: verify whether the mailbox exists
3670
+ // at all by running LIST. This sets mailboxMissing on the error
3671
+ // so the caller can distinguish "doesn't exist" from other failures.
3672
+ try {
3673
+ let folders = await this.run('LIST', '', path, { listOnly: true });
3674
+ if (!folders || !folders.length) {
3675
+ err.mailboxMissing = true;
3676
+ }
3677
+ }
3678
+ catch (E) {
3679
+ this.log.trace({ msg: 'Failed to verify failed mailbox', path, err: E });
3680
+ }
3681
+ }
3682
+ this.log.trace({
3683
+ msg: 'Failed to acquire mailbox lock',
3684
+ path,
3685
+ lockId,
3686
+ idling: this.idling,
3687
+ ...(options.description && { description: options.description }),
3688
+ err
3689
+ });
3690
+ reject(err);
3691
+ // Continue to next lock in queue
3692
+ }
3693
+ }
3694
+ }
3695
+ finally {
3696
+ this.processingLock = false;
3697
+ // New locks may have been queued while we were processing (e.g.,
3698
+ // a lock that failed immediately and the next getMailboxLock call
3699
+ // arrived before we finished). Schedule another run if needed.
3700
+ /* c8 ignore start */ // requires a lock to be enqueued during an in-flight processLocks pass; not reproducible deterministically
3701
+ if (this.locks.length && !this.currentLock) {
3702
+ setImmediate(() => {
3703
+ this.processLocks().catch(err => this.log.error({ err, cid: this.id }));
3704
+ });
3705
+ }
3706
+ /* c8 ignore stop */
3707
+ }
3708
+ }
3709
+ /**
3710
+ * Opens a mailbox if not already open and returns a lock. Next call to `getMailboxLock()` is queued
3711
+ * until previous lock is released. This is suggested over {@link ImapFlow#mailboxOpen} as
3712
+ * `getMailboxLock()` gives you a weak transaction while `mailboxOpen()` has no guarantees whatsoever that another
3713
+ * mailbox is opened while you try to call multiple fetch or store commands.
3714
+ *
3715
+ * @param path **Path for the mailbox** to open
3716
+ * @param options optional options
3717
+ * @returns Mailbox lock
3718
+ * @throws Will throw an error if mailbox does not exist or can not be opened
3719
+ *
3720
+ * @example
3721
+ * let lock = await client.getMailboxLock('INBOX');
3722
+ * try {
3723
+ * // do something in the mailbox
3724
+ * } finally {
3725
+ * // use finally{} to make sure lock is released even if exception occurs
3726
+ * lock.release();
3727
+ * }
3728
+ */
3729
+ getMailboxLock(path, options) {
3730
+ options = options || {};
3731
+ let lockPath = (0, tools_js_1.normalizePath)(this, path);
3732
+ let lockId = ++this.lockCounter;
3733
+ this.log.trace({
3734
+ msg: 'Requesting lock',
3735
+ path: lockPath,
3736
+ lockId,
3737
+ ...(options.description && { description: options.description }),
3738
+ activeLock: this.currentLock
3739
+ ? {
3740
+ lockId: this.currentLock.lockId,
3741
+ ...(this.currentLock.options?.description && { description: this.currentLock.options?.description })
3742
+ }
3743
+ : null
3744
+ });
3745
+ const lockOptions = options;
3746
+ // Guarded: close() rejects every queued lock synchronously. See guardedPromise().
3747
+ let lockPromise = (0, tools_js_1.guardedPromise)((resolve, reject) => {
3748
+ let lockEntry = { resolve, reject, path: lockPath, options: lockOptions, lockId };
3749
+ this.locks.push(lockEntry);
3750
+ // Opt-in acquire timeout: if the lock has not been granted within
3751
+ // acquireTimeout ms, remove it from the queue and reject. Only
3752
+ // affects queued (pending) locks - once granted, the timer is cleared.
3753
+ if (Number(lockOptions.acquireTimeout) > 0) {
3754
+ lockEntry.acquireTimer = setTimeout(() => {
3755
+ lockEntry.acquireTimer = null;
3756
+ const idx = this.locks.indexOf(lockEntry);
3757
+ if (idx !== -1) {
3758
+ this.locks.splice(idx, 1);
3759
+ let err = new Error('Timed out waiting for mailbox lock');
3760
+ err.code = 'LockTimeout';
3761
+ err.lockId = lockEntry.lockId;
3762
+ reject(err);
3763
+ }
3764
+ }, Number(lockOptions.acquireTimeout));
3765
+ }
3766
+ this.processLocks().catch(err => reject(err));
3767
+ });
3768
+ return lockPromise;
3769
+ }
3770
+ /** @internal */
3771
+ getLogger() {
3772
+ let mainLogger = this.options.logger && typeof this.options.logger === 'object'
3773
+ ? this.options.logger
3774
+ : logger_js_1.default.child({
3775
+ component: 'imap-connection',
3776
+ cid: this.id
3777
+ });
3778
+ let synteticLogger = {};
3779
+ let levels = ['trace', 'debug', 'info', 'warn', 'error', 'fatal'];
3780
+ for (let level of levels) {
3781
+ synteticLogger[level] = (...args) => {
3782
+ // using {logger:false} disables logging
3783
+ if (this.options.logger !== false) {
3784
+ const logMethod = mainLogger[level];
3785
+ if (typeof logMethod !== 'function') {
3786
+ // we are checking to make sure the level is supported.
3787
+ // if it isn't supported but the level is error or fatal, log to console anyway.
3788
+ if (level === 'fatal' || level === 'error') {
3789
+ let entry = args[0];
3790
+ try {
3791
+ if (entry && typeof entry === 'object' && entry.err) {
3792
+ entry = Object.assign({}, entry, { err: flattenLoggedError(entry.err) });
3793
+ }
3794
+ console.error(JSON.stringify(entry));
3795
+ }
3796
+ catch {
3797
+ // Serializing failed (a circular structure, a BigInt, a throwing
3798
+ // getter). This fallback exists so an error is never lost, so hand
3799
+ // the entry to console.error itself - it inspects rather than
3800
+ // serializes, and handles all three - instead of dropping it.
3801
+ console.error(entry);
3802
+ }
3803
+ }
3804
+ }
3805
+ else {
3806
+ logMethod.apply(mainLogger, args);
3807
+ }
3808
+ }
3809
+ if (this.emitLogs && args && args[0] && typeof args[0] === 'object') {
3810
+ // Guarded for the same reason as the console fallback above: a log call must
3811
+ // never throw. Most of these run inside catch blocks in the protocol
3812
+ // machinery, where a throw would escape the handler that was recovering from
3813
+ // something else and strand the connection. A throwing property getter on the
3814
+ // logged error and a throwing 'log' listener both end up here.
3815
+ try {
3816
+ let logEntry = Object.assign({ level, t: Date.now(), cid: this.id, lo: ++this.lo }, args[0]);
3817
+ if (logEntry.err) {
3818
+ logEntry.err = flattenLoggedError(logEntry.err);
3819
+ }
3820
+ this.emit('log', logEntry);
3821
+ }
3822
+ catch {
3823
+ // Nothing to do with it: reporting the failure would re-enter this
3824
+ // same path
3825
+ }
3826
+ }
3827
+ };
3828
+ }
3829
+ return synteticLogger;
3830
+ }
3831
+ /**
3832
+ * Detaches sockets from the IMAP pipeline. Useful for upgrading the connection
3833
+ * (e.g., STARTTLS) or transferring socket ownership.
3834
+ *
3835
+ * @returns Socket objects: `readSocket` is the read socket (inflated socket if compression is enabled, raw socket otherwise),
3836
+ * `writeSocket` the write socket and `socket` the raw underlying socket (same as readSocket/writeSocket when compression is disabled)
3837
+ */
3838
+ unbind() {
3839
+ const socket = this.socket;
3840
+ socket.unpipe(this.streamer);
3841
+ if (this._inflate) {
3842
+ this._inflate.unpipe(this.streamer);
3843
+ }
3844
+ // Detach all of ImapFlow's socket listeners - the raw socket plus, when
3845
+ // compression is active, the PassThrough writeSocket - so the connection
3846
+ // is fully released to the caller.
3847
+ this.clearSocketHandlers();
3848
+ const readSocket = this._inflate || socket;
3849
+ const writeSocket = this.writeSocket || socket;
3850
+ // Defense-in-depth: when compression is active the raw socket is orphaned
3851
+ // (neither readSocket nor writeSocket) yet still live and still the target
3852
+ // of the deflate/writeSocket error forwarders. We just stripped our own
3853
+ // error listener, so any post-unbind error (e.g. an upstream ECONNRESET)
3854
+ // would become an unhandled 'error' that crashes the host process. Attach
3855
+ // a benign listener so the orphaned socket can never throw after handoff.
3856
+ // Non-compression path: socket === readSocket === writeSocket and the
3857
+ // caller owns it directly, so leave it untouched (no behavior change).
3858
+ if (socket !== readSocket && socket !== writeSocket) {
3859
+ socket.on('error', err => {
3860
+ this.log.debug({ msg: 'Suppressed error on unbound socket', err, cid: this.id });
3861
+ });
3862
+ }
3863
+ return {
3864
+ readSocket,
3865
+ writeSocket,
3866
+ socket
3867
+ };
3868
+ }
3869
+ }
3870
+ exports.ImapFlow = ImapFlow;
3871
+ /**
3872
+ * Connection close event. **NB!** ImapFlow does not handle reconnects automatically.
3873
+ * So whenever a 'close' event occurs you must create a new connection yourself.
3874
+ *
3875
+ * @event ImapFlow#close
3876
+ */
3877
+ /**
3878
+ * Error event. In most cases getting an error event also means that connection is closed
3879
+ * and pending operations should return with a failure.
3880
+ *
3881
+ * @event ImapFlow#error
3882
+ * @example
3883
+ * client.on('error', err=>{
3884
+ * console.log(`Error occurred: ${err.message}`);
3885
+ * });
3886
+ */
3887
+ /**
3888
+ * Message count in currently opened mailbox changed
3889
+ *
3890
+ * @event ImapFlow#exists
3891
+ * @example
3892
+ * client.on('exists', data=>{
3893
+ * console.log(`Message count in "${data.path}" is ${data.count}`);
3894
+ * });
3895
+ */
3896
+ /**
3897
+ * Deleted message sequence number in currently opened mailbox. One event is fired for every deleted email.
3898
+ *
3899
+ * @event ImapFlow#expunge
3900
+ * @example
3901
+ * client.on('expunge', data=>{
3902
+ * console.log(`Message #${data.seq} was deleted from "${data.path}"`);
3903
+ * });
3904
+ */
3905
+ /**
3906
+ * Flags were updated for a message. Not all servers fire this event.
3907
+ *
3908
+ * @event ImapFlow#flags
3909
+ * @example
3910
+ * client.on('flags', data=>{
3911
+ * console.log(`Flag set for #${data.seq} is now "${Array.from(data.flags).join(', ')}"`);
3912
+ * });
3913
+ */
3914
+ /**
3915
+ * Mailbox was opened
3916
+ *
3917
+ * @event ImapFlow#mailboxOpen
3918
+ * @example
3919
+ * client.on('mailboxOpen', mailbox => {
3920
+ * console.log(`Mailbox ${mailbox.path} was opened`);
3921
+ * });
3922
+ */
3923
+ /**
3924
+ * Mailbox was closed
3925
+ *
3926
+ * Emitted both when a selected mailbox is closed explicitly, by `mailboxClose()` or by
3927
+ * selecting a different mailbox, and when the connection itself goes away while a mailbox
3928
+ * was still selected, whether through a clean logout or a lost transport. The transition is
3929
+ * reported once per selected mailbox, before the `close` event.
3930
+ *
3931
+ * @event ImapFlow#mailboxClose
3932
+ * @example
3933
+ * client.on('mailboxClose', mailbox => {
3934
+ * console.log(`Mailbox ${mailbox.path} was closed`);
3935
+ * });
3936
+ */
3937
+ /**
3938
+ * Log event if `emitLogs=true`
3939
+ *
3940
+ * @event ImapFlow#log
3941
+ * @example
3942
+ * client.on('log', entry => {
3943
+ * console.log(`${entry.cid} ${entry.msg}`);
3944
+ * });
3945
+ */
3946
+ // Both `import { ImapFlow } from 'imapflow'` and `import imapflow from 'imapflow'` work, the
3947
+ // latter matching the shape `require('imapflow')` has always had
3948
+ const imapflow = { ImapFlow, AuthenticationFailure: errors_js_1.AuthenticationFailure };
3949
+ exports.default = imapflow;