imapflow 1.0.188 → 1.0.190

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,15 +772,29 @@ 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
  });
@@ -785,6 +813,10 @@ class ImapFlow extends EventEmitter {
785
813
  if (this._socketError) {
786
814
  this.socket.removeListener('error', this._socketError);
787
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);
788
820
  }
789
821
  if (this._socketClose) {
790
822
  this.socket.removeListener('close', this._socketClose);
@@ -1131,7 +1163,11 @@ class ImapFlow extends EventEmitter {
1131
1163
  return reject(err);
1132
1164
  }
1133
1165
 
1134
- setImmediate(() => this.close());
1166
+ // ALWAYS emit the error so users can handle it
1167
+ setImmediate(() => {
1168
+ this.emit('error', err);
1169
+ this.close();
1170
+ });
1135
1171
  });
1136
1172
  }
1137
1173
 
@@ -2787,6 +2823,7 @@ class ImapFlow extends EventEmitter {
2787
2823
 
2788
2824
  let stream;
2789
2825
  let output;
2826
+ let fetchAborted = false;
2790
2827
 
2791
2828
  switch (meta.encoding) {
2792
2829
  case 'base64':
@@ -2834,31 +2871,105 @@ class ImapFlow extends EventEmitter {
2834
2871
  });
2835
2872
  output = output.pipe(limiter);
2836
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
+
2837
2886
  let writeChunk = chunk => {
2838
- if (limiter.limited) {
2887
+ if (limiter.limited || fetchAborted || stream.destroyed) {
2839
2888
  return true;
2840
2889
  }
2841
2890
  return stream.write(chunk);
2842
2891
  };
2843
2892
 
2844
2893
  let fetchAllParts = async () => {
2845
- while (hasMore && !limiter.limited) {
2894
+ while (hasMore && !limiter.limited && !fetchAborted) {
2846
2895
  let { chunk } = await getNextPart();
2847
- if (!chunk) {
2896
+ if (!chunk || fetchAborted) {
2848
2897
  break;
2849
2898
  }
2850
2899
 
2900
+ // Handle backpressure
2851
2901
  if (writeChunk(chunk) === false) {
2852
- 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
+ }
2853
2938
  }
2854
2939
  }
2855
2940
  };
2856
2941
 
2857
2942
  setImmediate(() => {
2858
- writeChunk(chunk);
2859
- fetchAllParts()
2860
- .catch(err => stream.emit('error', err))
2861
- .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
+ }
2862
2973
  });
2863
2974
 
2864
2975
  return {
@@ -3029,76 +3140,73 @@ class ImapFlow extends EventEmitter {
3029
3140
  }
3030
3141
 
3031
3142
  async processLocks(force) {
3032
- if (!force && this.processingLock) {
3033
- this.log.trace({
3034
- msg: 'Mailbox locking queued',
3035
- path: this.mailbox && this.mailbox.path,
3036
- pending: this.locks.length,
3037
- idling: this.idling,
3038
- activeLock: this.currentLock
3039
- ? {
3040
- lockId: this.currentLock.lockId,
3041
- ...(this.currentLock.options?.description && { description: this.currentLock.options?.description })
3042
- }
3043
- : null
3044
- });
3045
- return;
3046
- }
3047
-
3048
- if (!this.locks.length) {
3049
- this.processingLock = false;
3050
- this.log.trace({
3051
- msg: 'Mailbox locking queue processed',
3052
- idling: this.idling
3053
- });
3054
- return;
3055
- }
3056
-
3057
- this.processingLock = true;
3058
-
3059
- const release = () => {
3060
- 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;
3061
3151
  this.log.trace({
3062
- msg: 'Mailbox lock released',
3063
- lockId: this.currentLock.lockId,
3152
+ msg: 'Mailbox locking queued',
3064
3153
  path: this.mailbox && this.mailbox.path,
3065
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',
3066
3173
  idling: this.idling
3067
3174
  });
3068
- this.currentLock = false;
3175
+ return;
3069
3176
  }
3070
- this.processLocks(true).catch(err => this.log.error({ err, cid: this.id }));
3071
- };
3072
3177
 
3073
- const lock = this.locks.shift();
3074
- 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
+ };
3075
3191
 
3076
- if (!this.usable || !this.socket || this.socket.destroyed) {
3077
- this.log.trace({ msg: 'Failed to acquire mailbox lock', path, lockId, idling: this.idling });
3078
- // reject all
3079
- let error = new Error('Connection not available');
3080
- error.code = 'NoConnection';
3081
- reject(error);
3082
- return await this.processLocks(true);
3083
- }
3192
+ const lock = this.locks.shift();
3193
+ const { resolve, reject, path, options, lockId } = lock;
3084
3194
 
3085
- if (this.mailbox && this.mailbox.path === path && !!this.mailbox.readOnly === !!options.readOnly) {
3086
- // nothing to do here, already selected
3087
- this.log.trace({
3088
- msg: 'Mailbox lock acquired [existing]',
3089
- path,
3090
- lockId,
3091
- idling: this.idling,
3092
- ...(options.description && { description: options.description })
3093
- });
3094
- this.currentLock = lock;
3095
- return resolve({ path, release });
3096
- } else {
3097
- try {
3098
- // Try to open. Throws if mailbox does not exists or can't open
3099
- 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
3100
3208
  this.log.trace({
3101
- msg: 'Mailbox lock acquired [selected]',
3209
+ msg: 'Mailbox lock acquired [existing]',
3102
3210
  path,
3103
3211
  lockId,
3104
3212
  idling: this.idling,
@@ -3106,28 +3214,51 @@ class ImapFlow extends EventEmitter {
3106
3214
  });
3107
3215
  this.currentLock = lock;
3108
3216
  return resolve({ path, release });
3109
- } catch (err) {
3110
- if (err.responseStatus === 'NO') {
3111
- try {
3112
- let folders = await this.run('LIST', '', path, { listOnly: true });
3113
- if (!folders || !folders.length) {
3114
- 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 });
3115
3239
  }
3116
- } catch (E) {
3117
- this.log.trace({ msg: 'Failed to verify failed mailbox', path, err: E });
3118
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);
3119
3253
  }
3254
+ }
3255
+ } finally {
3256
+ this.processingLock = false;
3120
3257
 
3121
- this.log.trace({
3122
- msg: 'Failed to acquire mailbox lock',
3123
- path,
3124
- lockId,
3125
- idling: this.idling,
3126
- ...(options.description && { description: options.description }),
3127
- err
3128
- });
3129
- reject(err);
3130
- 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 }));
3131
3262
  }
3132
3263
  }
3133
3264
  }