imapflow 1.0.188 → 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/CHANGELOG.md +8 -0
- package/lib/handler/imap-parser.js +8 -3
- package/lib/imap-flow.d.ts +786 -0
- package/lib/imap-flow.js +220 -89
- package/package.json +9 -9
- package/lib/types.d.ts +0 -1123
- package/types.js +0 -56
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
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
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
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
|
|
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
|
-
|
|
3175
|
+
return;
|
|
3069
3176
|
}
|
|
3070
|
-
this.processLocks(true).catch(err => this.log.error({ err, cid: this.id }));
|
|
3071
|
-
};
|
|
3072
3177
|
|
|
3073
|
-
|
|
3074
|
-
|
|
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
|
-
|
|
3077
|
-
|
|
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
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
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 [
|
|
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
|
-
}
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
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
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
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
|
}
|
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "imapflow",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.189",
|
|
4
4
|
"description": "IMAP Client for Node",
|
|
5
|
-
"main": "
|
|
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
|
-
"
|
|
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
|
},
|
|
@@ -32,7 +32,7 @@
|
|
|
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": "24.0.
|
|
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",
|
|
@@ -43,16 +43,16 @@
|
|
|
43
43
|
"imapflow-jsdoc-template": "3.4.0-imapflow.2",
|
|
44
44
|
"jsdoc": "3.6.11",
|
|
45
45
|
"st": "3.0.2",
|
|
46
|
-
"
|
|
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.
|
|
52
|
+
"libmime": "5.3.7",
|
|
53
53
|
"libqp": "2.1.1",
|
|
54
|
-
"mailsplit": "5.4.
|
|
55
|
-
"nodemailer": "7.0.
|
|
54
|
+
"mailsplit": "5.4.5",
|
|
55
|
+
"nodemailer": "7.0.4",
|
|
56
56
|
"pino": "9.7.0",
|
|
57
57
|
"socks": "2.8.5"
|
|
58
58
|
}
|