imapflow 1.0.187 → 1.0.189

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/imap-flow.js CHANGED
@@ -242,6 +242,13 @@ class ImapFlow extends EventEmitter {
242
242
  this.options.clientInfo || {}
243
243
  );
244
244
 
245
+ // remove diacritics
246
+ for (let key of Object.keys(this.clientInfo)) {
247
+ if (typeof this.clientInfo[key] === 'string') {
248
+ this.clientInfo[key] = this.clientInfo[key].normalize('NFD').replace(/\p{Diacritic}/gu, '');
249
+ }
250
+ }
251
+
245
252
  /**
246
253
  * Server identification info. Available after successful `connect()`.
247
254
  * If server does not provide identification info then this value is `null`.
@@ -408,7 +415,8 @@ class ImapFlow extends EventEmitter {
408
415
  }
409
416
 
410
417
  if (this.writeSocket.destroyed) {
411
- this.socket.emit('error', 'Write socket destroyed');
418
+ this.log.error({ msg: 'Write socket destroyed', cid: this.id });
419
+ this.close();
412
420
  return;
413
421
  }
414
422
 
@@ -509,6 +517,12 @@ class ImapFlow extends EventEmitter {
509
517
  }
510
518
 
511
519
  async exec(command, attributes, options) {
520
+ if (this.state === this.states.LOGOUT || this.isClosed) {
521
+ const error = new Error('Connection not available');
522
+ error.code = 'NoConnection';
523
+ throw error;
524
+ }
525
+
512
526
  if (!this.socket || this.socket.destroyed) {
513
527
  let error = new Error('Connection closed');
514
528
  error.code = 'EConnectionClosed';
@@ -758,22 +772,37 @@ class ImapFlow extends EventEmitter {
758
772
  this.close();
759
773
  });
760
774
 
775
+ /**
776
+ * Socket timeout event handler.
777
+ *
778
+ * When a socket timeout occurs during IDLE, the handler attempts to recover the connection
779
+ * by sending a NOOP command and then returning to IDLE state.
780
+ *
781
+ * @fires ImapFlow#error Emits error event unless the current command is IDLE
782
+ */
761
783
  this._socketTimeout =
762
784
  this._socketTimeout ||
763
785
  (() => {
786
+ const err = new Error('Socket timeout');
787
+ err.code = 'ETIMEOUT';
788
+
764
789
  if (this.idling) {
790
+ // Attempt to recover IDLE connections
765
791
  this.run('NOOP')
766
792
  .then(() => this.idle())
767
- .catch(this._socketError);
793
+ .catch(this._socketError); // Natural circuit breaker
768
794
  } else {
795
+ // Close immediately for non-IDLE operations
769
796
  this.log.debug({ msg: 'Socket timeout', cid: this.id });
797
+ this.emit('error', err);
770
798
  this.close();
771
799
  }
772
800
  });
773
801
 
774
- this.socket.on('error', this._socketError);
775
- this.socket.on('close', this._socketClose);
776
- this.socket.on('end', this._socketEnd);
802
+ this.socket.once('error', this._socketError);
803
+ this.socket.once('close', this._socketClose);
804
+ this.socket.once('end', this._socketEnd);
805
+
777
806
  this.socket.on('tlsClientError', this._socketError);
778
807
  this.socket.on('timeout', this._socketTimeout);
779
808
 
@@ -784,6 +813,10 @@ class ImapFlow extends EventEmitter {
784
813
  if (this._socketError) {
785
814
  this.socket.removeListener('error', this._socketError);
786
815
  this.socket.removeListener('tlsClientError', this._socketError);
816
+ this.writeSocket.removeListener('error', this._socketError);
817
+ }
818
+ if (this._socketTimeout) {
819
+ this.socket.removeListener('timeout', this._socketTimeout);
787
820
  }
788
821
  if (this._socketClose) {
789
822
  this.socket.removeListener('close', this._socketClose);
@@ -1130,7 +1163,11 @@ class ImapFlow extends EventEmitter {
1130
1163
  return reject(err);
1131
1164
  }
1132
1165
 
1133
- setImmediate(() => this.close());
1166
+ // ALWAYS emit the error so users can handle it
1167
+ setImmediate(() => {
1168
+ this.emit('error', err);
1169
+ this.close();
1170
+ });
1134
1171
  });
1135
1172
  }
1136
1173
 
@@ -2786,6 +2823,7 @@ class ImapFlow extends EventEmitter {
2786
2823
 
2787
2824
  let stream;
2788
2825
  let output;
2826
+ let fetchAborted = false;
2789
2827
 
2790
2828
  switch (meta.encoding) {
2791
2829
  case 'base64':
@@ -2833,31 +2871,105 @@ class ImapFlow extends EventEmitter {
2833
2871
  });
2834
2872
  output = output.pipe(limiter);
2835
2873
 
2874
+ // Cleanup function
2875
+ const cleanup = () => {
2876
+ fetchAborted = true;
2877
+ if (stream && !stream.destroyed) {
2878
+ stream.destroy();
2879
+ }
2880
+ };
2881
+
2882
+ // Listen for stream destruction
2883
+ output.once('error', cleanup);
2884
+ output.once('close', cleanup);
2885
+
2836
2886
  let writeChunk = chunk => {
2837
- if (limiter.limited) {
2887
+ if (limiter.limited || fetchAborted || stream.destroyed) {
2838
2888
  return true;
2839
2889
  }
2840
2890
  return stream.write(chunk);
2841
2891
  };
2842
2892
 
2843
2893
  let fetchAllParts = async () => {
2844
- while (hasMore && !limiter.limited) {
2894
+ while (hasMore && !limiter.limited && !fetchAborted) {
2845
2895
  let { chunk } = await getNextPart();
2846
- if (!chunk) {
2896
+ if (!chunk || fetchAborted) {
2847
2897
  break;
2848
2898
  }
2849
2899
 
2900
+ // Handle backpressure
2850
2901
  if (writeChunk(chunk) === false) {
2851
- await new Promise(resolve => stream.once('drain', resolve));
2902
+ // Wait for drain event before continuing
2903
+ try {
2904
+ await new Promise((resolve, reject) => {
2905
+ let resolved = false;
2906
+
2907
+ const finish = err => {
2908
+ if (resolved) return;
2909
+ resolved = true;
2910
+
2911
+ // Remove all listeners
2912
+ stream.removeAllListeners('drain');
2913
+ stream.removeAllListeners('error');
2914
+ stream.removeAllListeners('close');
2915
+
2916
+ if (err) {
2917
+ reject(err);
2918
+ } else {
2919
+ resolve();
2920
+ }
2921
+ };
2922
+
2923
+ stream.once('drain', () => finish());
2924
+ stream.once('error', err => finish(err));
2925
+ stream.once('close', () => finish());
2926
+ });
2927
+ } catch (err) {
2928
+ // Re-throw only if not aborted
2929
+ if (!fetchAborted) {
2930
+ throw err;
2931
+ }
2932
+ }
2933
+
2934
+ // Check if we should abort after waiting
2935
+ if (fetchAborted) {
2936
+ break;
2937
+ }
2852
2938
  }
2853
2939
  }
2854
2940
  };
2855
2941
 
2856
2942
  setImmediate(() => {
2857
- writeChunk(chunk);
2858
- fetchAllParts()
2859
- .catch(err => stream.emit('error', err))
2860
- .finally(() => stream.end());
2943
+ if (!writeChunk(chunk)) {
2944
+ // Initial chunk filled the buffer, wait for drain
2945
+ stream.once('drain', () => {
2946
+ if (!fetchAborted) {
2947
+ fetchAllParts()
2948
+ .catch(err => {
2949
+ if (!fetchAborted && stream && !stream.destroyed) {
2950
+ stream.emit('error', err);
2951
+ }
2952
+ })
2953
+ .finally(() => {
2954
+ if (!fetchAborted && stream && !stream.destroyed) {
2955
+ stream.end();
2956
+ }
2957
+ });
2958
+ }
2959
+ });
2960
+ } else {
2961
+ fetchAllParts()
2962
+ .catch(err => {
2963
+ if (!fetchAborted && stream && !stream.destroyed) {
2964
+ stream.emit('error', err);
2965
+ }
2966
+ })
2967
+ .finally(() => {
2968
+ if (!fetchAborted && stream && !stream.destroyed) {
2969
+ stream.end();
2970
+ }
2971
+ });
2972
+ }
2861
2973
  });
2862
2974
 
2863
2975
  return {
@@ -3028,76 +3140,73 @@ class ImapFlow extends EventEmitter {
3028
3140
  }
3029
3141
 
3030
3142
  async processLocks(force) {
3031
- if (!force && this.processingLock) {
3032
- this.log.trace({
3033
- msg: 'Mailbox locking queued',
3034
- path: this.mailbox && this.mailbox.path,
3035
- pending: this.locks.length,
3036
- idling: this.idling,
3037
- activeLock: this.currentLock
3038
- ? {
3039
- lockId: this.currentLock.lockId,
3040
- ...(this.currentLock.options?.description && { description: this.currentLock.options?.description })
3041
- }
3042
- : null
3043
- });
3044
- return;
3045
- }
3046
-
3047
- if (!this.locks.length) {
3048
- this.processingLock = false;
3049
- this.log.trace({
3050
- msg: 'Mailbox locking queue processed',
3051
- idling: this.idling
3052
- });
3053
- return;
3054
- }
3055
-
3056
- this.processingLock = true;
3057
-
3058
- const release = () => {
3059
- if (this.currentLock) {
3143
+ if (!force) {
3144
+ // Atomic test-and-set to prevent race condition
3145
+ const wasProcessing = this.processingLock;
3146
+ this.processingLock = true;
3147
+
3148
+ if (wasProcessing) {
3149
+ // Another processor is already running, reset and exit
3150
+ this.processingLock = wasProcessing;
3060
3151
  this.log.trace({
3061
- msg: 'Mailbox lock released',
3062
- lockId: this.currentLock.lockId,
3152
+ msg: 'Mailbox locking queued',
3063
3153
  path: this.mailbox && this.mailbox.path,
3064
3154
  pending: this.locks.length,
3155
+ idling: this.idling,
3156
+ activeLock: this.currentLock
3157
+ ? {
3158
+ lockId: this.currentLock.lockId,
3159
+ ...(this.currentLock.options?.description && { description: this.currentLock.options?.description })
3160
+ }
3161
+ : null
3162
+ });
3163
+ return;
3164
+ }
3165
+ } else {
3166
+ this.processingLock = true;
3167
+ }
3168
+
3169
+ try {
3170
+ if (!this.locks.length) {
3171
+ this.log.trace({
3172
+ msg: 'Mailbox locking queue processed',
3065
3173
  idling: this.idling
3066
3174
  });
3067
- this.currentLock = false;
3175
+ return;
3068
3176
  }
3069
- this.processLocks(true).catch(err => this.log.error({ err, cid: this.id }));
3070
- };
3071
3177
 
3072
- const lock = this.locks.shift();
3073
- const { resolve, reject, path, options, lockId } = lock;
3178
+ const release = () => {
3179
+ if (this.currentLock) {
3180
+ this.log.trace({
3181
+ msg: 'Mailbox lock released',
3182
+ lockId: this.currentLock.lockId,
3183
+ path: this.mailbox && this.mailbox.path,
3184
+ pending: this.locks.length,
3185
+ idling: this.idling
3186
+ });
3187
+ this.currentLock = false;
3188
+ }
3189
+ this.processLocks(true).catch(err => this.log.error({ err, cid: this.id }));
3190
+ };
3074
3191
 
3075
- if (!this.usable || !this.socket || this.socket.destroyed) {
3076
- this.log.trace({ msg: 'Failed to acquire mailbox lock', path, lockId, idling: this.idling });
3077
- // reject all
3078
- let error = new Error('Connection not available');
3079
- error.code = 'NoConnection';
3080
- reject(error);
3081
- return await this.processLocks(true);
3082
- }
3192
+ const lock = this.locks.shift();
3193
+ const { resolve, reject, path, options, lockId } = lock;
3083
3194
 
3084
- if (this.mailbox && this.mailbox.path === path && !!this.mailbox.readOnly === !!options.readOnly) {
3085
- // nothing to do here, already selected
3086
- this.log.trace({
3087
- msg: 'Mailbox lock acquired [existing]',
3088
- path,
3089
- lockId,
3090
- idling: this.idling,
3091
- ...(options.description && { description: options.description })
3092
- });
3093
- this.currentLock = lock;
3094
- return resolve({ path, release });
3095
- } else {
3096
- try {
3097
- // Try to open. Throws if mailbox does not exists or can't open
3098
- await this.mailboxOpen(path, options);
3195
+ if (!this.usable || !this.socket || this.socket.destroyed) {
3196
+ this.log.trace({ msg: 'Failed to acquire mailbox lock', path, lockId, idling: this.idling });
3197
+ // reject all
3198
+ let error = new Error('Connection not available');
3199
+ error.code = 'NoConnection';
3200
+ reject(error);
3201
+ // Process remaining locks
3202
+ await this.processLocks(true);
3203
+ return;
3204
+ }
3205
+
3206
+ if (this.mailbox && this.mailbox.path === path && !!this.mailbox.readOnly === !!options.readOnly) {
3207
+ // nothing to do here, already selected
3099
3208
  this.log.trace({
3100
- msg: 'Mailbox lock acquired [selected]',
3209
+ msg: 'Mailbox lock acquired [existing]',
3101
3210
  path,
3102
3211
  lockId,
3103
3212
  idling: this.idling,
@@ -3105,28 +3214,51 @@ class ImapFlow extends EventEmitter {
3105
3214
  });
3106
3215
  this.currentLock = lock;
3107
3216
  return resolve({ path, release });
3108
- } catch (err) {
3109
- if (err.responseStatus === 'NO') {
3110
- try {
3111
- let folders = await this.run('LIST', '', path, { listOnly: true });
3112
- if (!folders || !folders.length) {
3113
- err.mailboxMissing = true;
3217
+ } else {
3218
+ try {
3219
+ // Try to open. Throws if mailbox does not exists or can't open
3220
+ await this.mailboxOpen(path, options);
3221
+ this.log.trace({
3222
+ msg: 'Mailbox lock acquired [selected]',
3223
+ path,
3224
+ lockId,
3225
+ idling: this.idling,
3226
+ ...(options.description && { description: options.description })
3227
+ });
3228
+ this.currentLock = lock;
3229
+ return resolve({ path, release });
3230
+ } catch (err) {
3231
+ if (err.responseStatus === 'NO') {
3232
+ try {
3233
+ let folders = await this.run('LIST', '', path, { listOnly: true });
3234
+ if (!folders || !folders.length) {
3235
+ err.mailboxMissing = true;
3236
+ }
3237
+ } catch (E) {
3238
+ this.log.trace({ msg: 'Failed to verify failed mailbox', path, err: E });
3114
3239
  }
3115
- } catch (E) {
3116
- this.log.trace({ msg: 'Failed to verify failed mailbox', path, err: E });
3117
3240
  }
3241
+
3242
+ this.log.trace({
3243
+ msg: 'Failed to acquire mailbox lock',
3244
+ path,
3245
+ lockId,
3246
+ idling: this.idling,
3247
+ ...(options.description && { description: options.description }),
3248
+ err
3249
+ });
3250
+ reject(err);
3251
+ // Process remaining locks
3252
+ await this.processLocks(true);
3118
3253
  }
3254
+ }
3255
+ } finally {
3256
+ this.processingLock = false;
3119
3257
 
3120
- this.log.trace({
3121
- msg: 'Failed to acquire mailbox lock',
3122
- path,
3123
- lockId,
3124
- idling: this.idling,
3125
- ...(options.description && { description: options.description }),
3126
- err
3127
- });
3128
- reject(err);
3129
- await this.processLocks(true);
3258
+ // Check if new locks were added while we were processing
3259
+ if (this.locks.length && !this.processingLock) {
3260
+ // Recursively process any new locks that were added
3261
+ this.processLocks(true).catch(err => this.log.error({ err, cid: this.id }));
3130
3262
  }
3131
3263
  }
3132
3264
  }
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "imapflow",
3
- "version": "1.0.187",
3
+ "version": "1.0.189",
4
4
  "description": "IMAP Client for Node",
5
- "main": "./lib/imap-flow.js",
5
+ "main": "lib/imap-flow.js",
6
+ "types": "lib/imap-flow.d.ts",
6
7
  "scripts": {
7
8
  "test": "grunt",
8
9
  "prepare": "npm run build",
9
10
  "docs": "rm -rf docs && mkdir -p docs && jsdoc lib/imap-flow.js -c jsdoc.json -R README.md --destination docs/ && cp assets/favicon.ico docs",
10
- "dst": "node types.js",
11
- "build": "npm run docs && npm run dst",
11
+ "build": "npm run docs",
12
12
  "st": "npm run docs && st -d docs -i index.html",
13
13
  "update": "rm -rf node_modules package-lock.json && ncu -u && npm install"
14
14
  },
@@ -28,11 +28,11 @@
28
28
  },
29
29
  "homepage": "https://imapflow.com/",
30
30
  "devDependencies": {
31
- "@babel/eslint-parser": "7.27.1",
31
+ "@babel/eslint-parser": "7.27.5",
32
32
  "@babel/eslint-plugin": "7.27.1",
33
33
  "@babel/plugin-syntax-class-properties": "7.12.13",
34
34
  "@babel/preset-env": "7.27.2",
35
- "@types/node": "22.15.19",
35
+ "@types/node": "24.0.7",
36
36
  "eslint": "8.57.0",
37
37
  "eslint-config-nodemailer": "1.2.0",
38
38
  "eslint-config-prettier": "9.1.0",
@@ -42,18 +42,18 @@
42
42
  "grunt-eslint": "24.3.0",
43
43
  "imapflow-jsdoc-template": "3.4.0-imapflow.2",
44
44
  "jsdoc": "3.6.11",
45
- "st": "3.0.1",
46
- "tsd-jsdoc": "2.5.0"
45
+ "st": "3.0.2",
46
+ "typescript": "5.8.3"
47
47
  },
48
48
  "dependencies": {
49
49
  "encoding-japanese": "2.2.0",
50
50
  "iconv-lite": "0.6.3",
51
51
  "libbase64": "1.3.0",
52
- "libmime": "5.3.6",
52
+ "libmime": "5.3.7",
53
53
  "libqp": "2.1.1",
54
- "mailsplit": "5.4.3",
55
- "nodemailer": "7.0.3",
54
+ "mailsplit": "5.4.5",
55
+ "nodemailer": "7.0.4",
56
56
  "pino": "9.7.0",
57
- "socks": "2.8.4"
57
+ "socks": "2.8.5"
58
58
  }
59
59
  }
@@ -439,8 +439,6 @@ module.exports['IMAP Parser: Section: allow trailing ws'] = test =>
439
439
  ])
440
440
  );
441
441
 
442
- module.exports['IMAP Parser: Tags: fail unknown section'] = test => asyncWrapperFail(test, async test => test.ok(await parser('TAG1 CMD KODY[]')));
443
-
444
442
  module.exports['IMAP Parser: Readable: simple'] = test =>
445
443
  asyncWrapper(test, async test =>
446
444
  test.deepEqual(await parser('* OK Hello world!'), {
@@ -817,8 +815,6 @@ module.exports['IMAP Parser: Partial: Section'] = test =>
817
815
  ])
818
816
  );
819
817
 
820
- module.exports['IMAP Parser: Partial: fail unknown section'] = test => asyncWrapperFail(test, async test => test.ok(await parser('TAG1 CMD KODY<0.123>')));
821
-
822
818
  module.exports['IMAP Parser: Partial: fail zero prefix for start'] = test => asyncWrapperFail(test, async test => test.ok(await parser('TAG1 CMD BODY[]<01>')));
823
819
 
824
820
  module.exports['IMAP Parser: Partial: fail zero prefix for end'] = test => asyncWrapperFail(test, async test => test.ok(await parser('TAG1 CMD BODY[]<0.01>')));
@@ -1249,3 +1245,17 @@ module.exports['IMAP Parser, FETCH with BODYSTRUCTURE'] = test =>
1249
1245
  }
1250
1246
  }
1251
1247
  });
1248
+
1249
+ module.exports['IMAP Parser, ATOM with <, [, ]'] = test =>
1250
+ asyncWrapper(test, async test => {
1251
+ let password = `<[+=</$]`;
1252
+ let parsed = await parser(`3 LOGIN user@domain ${password}`);
1253
+ test.deepEqual(parsed, {
1254
+ tag: '3',
1255
+ command: 'LOGIN',
1256
+ attributes: [
1257
+ { type: 'ATOM', value: 'user@domain' },
1258
+ { type: 'ATOM', value: password }
1259
+ ]
1260
+ });
1261
+ });