imapflow 1.2.8 → 1.2.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/.ncurc.js +1 -1
  2. package/.release-please-manifest.json +1 -1
  3. package/CHANGELOG.md +14 -0
  4. package/README.md +36 -58
  5. package/eslint.config.js +18 -16
  6. package/lib/charsets.js +15 -0
  7. package/lib/commands/append.js +62 -54
  8. package/lib/commands/authenticate.js +99 -52
  9. package/lib/commands/capability.js +12 -2
  10. package/lib/commands/close.js +11 -1
  11. package/lib/commands/compress.js +10 -1
  12. package/lib/commands/copy.js +18 -1
  13. package/lib/commands/create.js +15 -2
  14. package/lib/commands/delete.js +10 -1
  15. package/lib/commands/enable.js +12 -1
  16. package/lib/commands/expunge.js +18 -2
  17. package/lib/commands/fetch.js +39 -4
  18. package/lib/commands/id.js +22 -3
  19. package/lib/commands/idle.js +39 -4
  20. package/lib/commands/list.js +86 -48
  21. package/lib/commands/login.js +12 -1
  22. package/lib/commands/logout.js +11 -2
  23. package/lib/commands/move.js +17 -1
  24. package/lib/commands/namespace.js +32 -2
  25. package/lib/commands/noop.js +6 -1
  26. package/lib/commands/quota.js +33 -14
  27. package/lib/commands/rename.js +13 -1
  28. package/lib/commands/search.js +16 -1
  29. package/lib/commands/select.js +76 -33
  30. package/lib/commands/starttls.js +6 -1
  31. package/lib/commands/status.js +64 -52
  32. package/lib/commands/store.js +27 -4
  33. package/lib/commands/subscribe.js +7 -1
  34. package/lib/commands/unsubscribe.js +7 -1
  35. package/lib/handler/imap-compiler.js +44 -2
  36. package/lib/handler/imap-formal-syntax.js +51 -3
  37. package/lib/handler/imap-handler.js +8 -0
  38. package/lib/handler/imap-parser.js +23 -2
  39. package/lib/handler/imap-stream.js +84 -31
  40. package/lib/handler/parser-instance.js +61 -1
  41. package/lib/handler/token-parser.js +66 -9
  42. package/lib/imap-commands.js +11 -0
  43. package/lib/imap-flow.d.ts +6 -0
  44. package/lib/imap-flow.js +175 -46
  45. package/lib/jp-decoder.js +10 -0
  46. package/lib/limited-passthrough.js +12 -5
  47. package/lib/proxy-connection.js +18 -12
  48. package/lib/search-compiler.js +3 -11
  49. package/lib/special-use.js +23 -16
  50. package/lib/tools.js +218 -13
  51. package/package.json +5 -17
  52. package/test/commands-integration-test.js +33 -0
  53. package/test/connection-edge-cases-test.js +105 -0
  54. package/test/special-use-test.js +32 -0
  55. package/.babelrc +0 -6
  56. package/.eslintrc +0 -16
  57. package/assets/favicon.ico +0 -0
  58. package/jsdoc.json +0 -28
package/lib/jp-decoder.js CHANGED
@@ -3,6 +3,11 @@
3
3
  const { Transform } = require('stream');
4
4
  const encodingJapanese = require('encoding-japanese');
5
5
 
6
+ // A Transform stream for decoding Japanese character sets (Shift_JIS, EUC-JP, ISO-2022-JP).
7
+ // Unlike iconv-lite which can decode incrementally, encoding-japanese requires the complete
8
+ // input buffer for accurate charset detection and stateful decoding (especially ISO-2022-JP
9
+ // which uses escape sequences to switch between ASCII and multi-byte modes). Therefore,
10
+ // this stream buffers all input during _transform and performs the actual decoding in _flush.
6
11
  class JPDecoder extends Transform {
7
12
  constructor(charset) {
8
13
  super();
@@ -12,6 +17,8 @@ class JPDecoder extends Transform {
12
17
  this.chunklen = 0;
13
18
  }
14
19
 
20
+ // Buffer all incoming chunks; no decoding happens here because Japanese charsets
21
+ // require the complete input for accurate conversion.
15
22
  _transform(chunk, encoding, done) {
16
23
  if (typeof chunk === 'string') {
17
24
  chunk = Buffer.from(chunk, encoding);
@@ -22,6 +29,9 @@ class JPDecoder extends Transform {
22
29
  done();
23
30
  }
24
31
 
32
+ // Perform the actual charset conversion once all input has been received.
33
+ // Uses the encoding-japanese library to convert from the source charset to Unicode.
34
+ // On failure (corrupt or unrecognizable data), passes through the raw bytes unchanged.
25
35
  _flush(done) {
26
36
  let input = Buffer.concat(this.chunks, this.chunklen);
27
37
  try {
@@ -2,26 +2,33 @@
2
2
 
3
3
  const { Transform } = require('stream');
4
4
 
5
+ // A Transform stream that passes through data up to a maximum byte limit,
6
+ // then silently discards all subsequent chunks. Used to enforce download
7
+ // size limits when fetching message content from the IMAP server.
5
8
  class LimitedPassthrough extends Transform {
6
9
  constructor(options) {
7
10
  super();
8
11
  this.options = options || {};
9
12
  this.maxBytes = this.options.maxBytes || Infinity;
10
13
  this.processed = 0;
14
+ // Once set to true, all subsequent chunks are dropped without error
11
15
  this.limited = false;
12
16
  }
13
17
 
14
18
  _transform(chunk, encoding, done) {
19
+ // If the limit was already reached, discard the chunk immediately
15
20
  if (this.limited) {
16
21
  return done();
17
22
  }
18
23
 
19
- if (this.processed + chunk.length > this.maxBytes) {
20
- if (this.maxBytes - this.processed < 1) {
21
- return done();
22
- }
24
+ const remainingBytes = this.maxBytes - this.processed;
25
+ if (remainingBytes < 1) {
26
+ return done();
27
+ }
23
28
 
24
- chunk = chunk.slice(0, this.maxBytes - this.processed);
29
+ // Slice the chunk to fit within the remaining byte budget
30
+ if (chunk.length > remainingBytes) {
31
+ chunk = chunk.slice(0, remainingBytes);
25
32
  }
26
33
 
27
34
  this.processed += chunk.length;
@@ -7,11 +7,22 @@ const httpProxyClientAsync = util.promisify(httpProxyClient);
7
7
  const dns = require('dns').promises;
8
8
  const net = require('net');
9
9
 
10
+ // Redacts the password from a parsed URL object before it is logged,
11
+ // preventing credentials from appearing in log output.
12
+ const hidePassword = proxyUrl => {
13
+ if (proxyUrl.password) {
14
+ proxyUrl.password = '(hidden)';
15
+ }
16
+ };
17
+
10
18
  const proxyConnection = async (logger, connectionUrl, host, port) => {
11
19
  let proxyUrl = new URL(connectionUrl);
12
20
 
13
21
  let protocol = proxyUrl.protocol.replace(/:$/, '').toLowerCase();
14
22
 
23
+ // Pre-resolve the IMAP server hostname to an IP address before passing it to the proxy.
24
+ // Some proxy implementations (especially SOCKS4) do not support hostname resolution,
25
+ // so we resolve DNS on the client side to ensure compatibility.
15
26
  if (!net.isIP(host)) {
16
27
  let resolveResult = await dns.resolve(host);
17
28
  if (resolveResult && resolveResult.length) {
@@ -26,9 +37,7 @@ const proxyConnection = async (logger, connectionUrl, host, port) => {
26
37
  try {
27
38
  let socket = await httpProxyClientAsync(proxyUrl.href, port, host);
28
39
  if (socket) {
29
- if (proxyUrl.password) {
30
- proxyUrl.password = '(hidden)';
31
- }
40
+ hidePassword(proxyUrl);
32
41
  logger.info({
33
42
  msg: 'Established a socket via HTTP proxy',
34
43
  proxyUrl: proxyUrl.href,
@@ -38,9 +47,7 @@ const proxyConnection = async (logger, connectionUrl, host, port) => {
38
47
  }
39
48
  return socket;
40
49
  } catch (err) {
41
- if (proxyUrl.password) {
42
- proxyUrl.password = '(hidden)';
43
- }
50
+ hidePassword(proxyUrl);
44
51
  logger.error({
45
52
  msg: 'Failed to establish a socket via HTTP proxy',
46
53
  proxyUrl: proxyUrl.href,
@@ -59,6 +66,9 @@ const proxyConnection = async (logger, connectionUrl, host, port) => {
59
66
  case 'socks4a': {
60
67
  let proxyType = Number(protocol.replace(/\D/g, '')) || 5;
61
68
 
69
+ // targetHost here is the SOCKS proxy server's hostname (not the final IMAP destination).
70
+ // The SOCKS library needs a resolved IP for the proxy host it connects to.
71
+ // The final IMAP destination (host/port) is passed separately as 'destination'.
62
72
  let targetHost = proxyUrl.hostname;
63
73
  if (!net.isIP(targetHost)) {
64
74
  let resolveResult = await dns.resolve(targetHost);
@@ -89,9 +99,7 @@ const proxyConnection = async (logger, connectionUrl, host, port) => {
89
99
  try {
90
100
  const info = await SocksClient.createConnection(connectionOpts);
91
101
  if (info && info.socket) {
92
- if (proxyUrl.password) {
93
- proxyUrl.password = '(hidden)';
94
- }
102
+ hidePassword(proxyUrl);
95
103
  logger.info({
96
104
  msg: 'Established a socket via SOCKS proxy',
97
105
  proxyUrl: proxyUrl.href,
@@ -101,9 +109,7 @@ const proxyConnection = async (logger, connectionUrl, host, port) => {
101
109
  }
102
110
  return info.socket;
103
111
  } catch (err) {
104
- if (proxyUrl.password) {
105
- proxyUrl.password = '(hidden)';
106
- }
112
+ hidePassword(proxyUrl);
107
113
  logger.error({
108
114
  msg: 'Failed to establish a socket via SOCKS proxy',
109
115
  proxyUrl: proxyUrl.href,
@@ -105,8 +105,8 @@ let isUnicodeString = str => {
105
105
  * Supports standard IMAP search criteria and extensions like OBJECTID and Gmail extensions.
106
106
  *
107
107
  * @param {Object} connection - IMAP connection object
108
- * @param {Object} connection.capabilities - Set of server capabilities
109
- * @param {Object} connection.enabled - Set of enabled extensions
108
+ * @param {Map} connection.capabilities - Map of server capabilities
109
+ * @param {Set} connection.enabled - Set of enabled extensions
110
110
  * @param {Object} connection.mailbox - Current mailbox information
111
111
  * @param {Set} connection.mailbox.flags - Available flags in the mailbox
112
112
  * @param {Object} query - Search query object
@@ -259,15 +259,7 @@ module.exports.searchCompiler = (connection, query) => {
259
259
  // Convert to seconds ago from now
260
260
  const now = Date.now();
261
261
  const withinSeconds = Math.round(Math.max(0, now - params[term].getTime()) / 1000);
262
- let withinKeyword;
263
- switch (term.toUpperCase()) {
264
- case 'BEFORE':
265
- withinKeyword = 'OLDER';
266
- break;
267
- case 'SINCE':
268
- withinKeyword = 'YOUNGER';
269
- break;
270
- }
262
+ const withinKeyword = term.toUpperCase() === 'BEFORE' ? 'OLDER' : 'YOUNGER';
271
263
  setOpt(attributes, withinKeyword, withinSeconds.toString());
272
264
  break;
273
265
  }
@@ -1,5 +1,9 @@
1
1
  'use strict';
2
2
 
3
+ // Localized folder name mappings for detecting special-use mailboxes.
4
+ // When the server does not advertise the SPECIAL-USE extension (RFC 6154),
5
+ // we fall back to matching folder names against these lists of known
6
+ // translations in various languages (including non-Latin scripts).
3
7
  module.exports = {
4
8
  flags: ['\\All', '\\Archive', '\\Drafts', '\\Flagged', '\\Junk', '\\Sent', '\\Trash'],
5
9
  names: {
@@ -281,27 +285,30 @@ module.exports = {
281
285
  },
282
286
 
283
287
  specialUse(hasSpecialUseExtension, folder) {
284
- let result;
285
-
288
+ // If the server supports SPECIAL-USE (RFC 6154), check for special-use flags first.
289
+ // Extension-provided flags take precedence over name-based detection because they
290
+ // are authoritative -- the server explicitly marks the folder's role.
286
291
  if (hasSpecialUseExtension) {
287
- result = {
288
- flag: module.exports.flags.find(flag => folder.flags.has(flag)),
289
- source: 'extension'
290
- };
292
+ const flag = module.exports.flags.find(flag => folder.flags.has(flag));
293
+ if (flag) {
294
+ return { flag, source: 'extension' };
295
+ }
291
296
  }
292
297
 
293
- if (!result || !result.flag) {
294
- let name = folder.name
295
- .toLowerCase()
296
- .replace(/\u200e/g, '')
297
- .trim();
298
+ // Fallback: match folder name against known localized names.
299
+ // Remove U+200E (LEFT-TO-RIGHT MARK) which some mail clients (especially for
300
+ // RTL languages like Arabic, Hebrew) insert into folder names for display purposes.
301
+ // These invisible marks would otherwise prevent exact string matching.
302
+ let name = folder.name
303
+ .toLowerCase()
304
+ .replace(/\u200e/g, '')
305
+ .trim();
298
306
 
299
- result = {
300
- flag: Object.keys(module.exports.names).find(flag => module.exports.names[flag].includes(name)),
301
- source: 'name'
302
- };
307
+ const flag = Object.keys(module.exports.names).find(flag => module.exports.names[flag].includes(name));
308
+ if (flag) {
309
+ return { flag, source: 'name' };
303
310
  }
304
311
 
305
- return result && result.flag ? result : { flag: null };
312
+ return { flag: null };
306
313
  }
307
314
  };