imapflow 1.7.8 → 2.0.1

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