imapflow 1.2.12 → 1.2.14
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/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +15 -0
- package/lib/charsets.js +1 -4
- package/lib/commands/append.js +1 -1
- package/lib/commands/copy.js +3 -24
- package/lib/commands/copyuid-parser.js +33 -0
- package/lib/commands/idle.js +5 -7
- package/lib/commands/list.js +1 -1
- package/lib/commands/move.js +4 -27
- package/lib/commands/store.js +1 -3
- package/lib/handler/imap-compiler.js +1 -3
- package/lib/handler/imap-formal-syntax.js +1 -7
- package/lib/handler/imap-parser.js +1 -1
- package/lib/handler/imap-stream.js +7 -13
- package/lib/handler/parser-instance.js +2 -2
- package/lib/handler/token-parser.js +9 -9
- package/lib/imap-flow.js +112 -124
- package/lib/search-compiler.js +4 -10
- package/lib/tools.js +29 -36
- package/package.json +4 -4
- package/test/charsets-test.js +161 -0
- package/test/commands-integration-test.js +61 -0
- package/test/connection-edge-cases-test.js +94 -0
- package/test/copyuid-parser-test.js +153 -0
- package/test/imap-compiler-test.js +66 -0
- package/test/imap-formal-syntax-test.js +275 -0
- package/test/imap-stream-edge-cases-test.js +268 -0
- package/test/imapflow-test.js +212 -0
- package/test/search-compiler-test.js +74 -0
- package/test/token-parser-test.js +374 -0
- package/test/tools-test.js +149 -2
- package/test/unhandled-rejection-test.js +327 -0
package/lib/imap-flow.js
CHANGED
|
@@ -40,6 +40,8 @@ const {
|
|
|
40
40
|
|
|
41
41
|
const imapCommands = require('./imap-commands.js');
|
|
42
42
|
|
|
43
|
+
const noop = () => {};
|
|
44
|
+
|
|
43
45
|
const CONNECT_TIMEOUT = 90 * 1000;
|
|
44
46
|
const GREETING_TIMEOUT = 16 * 1000;
|
|
45
47
|
const UPGRADE_TIMEOUT = 10 * 1000;
|
|
@@ -296,7 +298,6 @@ class ImapFlow extends EventEmitter {
|
|
|
296
298
|
this.state = this.states.NOT_AUTHENTICATED;
|
|
297
299
|
|
|
298
300
|
this.lockCounter = 0;
|
|
299
|
-
this.currentLock = false;
|
|
300
301
|
|
|
301
302
|
this.tagCounter = 0;
|
|
302
303
|
this.requestTagMap = new Map();
|
|
@@ -405,7 +406,8 @@ class ImapFlow extends EventEmitter {
|
|
|
405
406
|
let rid = BigInt('0x' + crypto.randomBytes(13).toString('hex')).toString(36);
|
|
406
407
|
if (rid.length < 20) {
|
|
407
408
|
rid = '0'.repeat(20 - rid.length) + rid;
|
|
408
|
-
}
|
|
409
|
+
}
|
|
410
|
+
if (rid.length > 20) {
|
|
409
411
|
rid = rid.substr(0, 20);
|
|
410
412
|
}
|
|
411
413
|
return rid;
|
|
@@ -552,24 +554,28 @@ class ImapFlow extends EventEmitter {
|
|
|
552
554
|
});
|
|
553
555
|
}
|
|
554
556
|
|
|
555
|
-
|
|
557
|
+
exec(command, attributes, options) {
|
|
556
558
|
if (this.state === this.states.LOGOUT || this.isClosed) {
|
|
557
559
|
const error = new Error('Connection not available');
|
|
558
560
|
error.code = 'NoConnection';
|
|
559
|
-
|
|
561
|
+
let p = Promise.reject(error);
|
|
562
|
+
p.catch(noop);
|
|
563
|
+
return p;
|
|
560
564
|
}
|
|
561
565
|
|
|
562
566
|
if (!this.socket || this.socket.destroyed) {
|
|
563
567
|
let error = new Error('Connection closed');
|
|
564
568
|
error.code = 'EConnectionClosed';
|
|
565
|
-
|
|
569
|
+
let p = Promise.reject(error);
|
|
570
|
+
p.catch(noop);
|
|
571
|
+
return p;
|
|
566
572
|
}
|
|
567
573
|
|
|
568
574
|
let tag = (++this.tagCounter).toString(16).toUpperCase();
|
|
569
575
|
|
|
570
576
|
options = options || {};
|
|
571
577
|
|
|
572
|
-
|
|
578
|
+
let promise = new Promise((resolve, reject) => {
|
|
573
579
|
this.requestTagMap.set(tag, { command, attributes, options, resolve, reject });
|
|
574
580
|
this.requestQueue.push({ tag, command, attributes, options });
|
|
575
581
|
this.trySend().catch(err => {
|
|
@@ -577,6 +583,13 @@ class ImapFlow extends EventEmitter {
|
|
|
577
583
|
reject(err);
|
|
578
584
|
});
|
|
579
585
|
});
|
|
586
|
+
|
|
587
|
+
// Prevent unhandled promise rejection if close() rejects this request
|
|
588
|
+
// via setImmediate before the caller's handler is attached. The rejection
|
|
589
|
+
// still propagates normally to the caller's await/.catch().
|
|
590
|
+
promise.catch(noop);
|
|
591
|
+
|
|
592
|
+
return promise;
|
|
580
593
|
}
|
|
581
594
|
|
|
582
595
|
// Resolves the handler for an untagged server response. IMAP untagged responses
|
|
@@ -1074,11 +1087,11 @@ class ImapFlow extends EventEmitter {
|
|
|
1074
1087
|
let err = new Error('Server does not support STARTTLS');
|
|
1075
1088
|
err.tlsFailed = true;
|
|
1076
1089
|
throw err;
|
|
1077
|
-
} else {
|
|
1078
|
-
// Opportunistic STARTTLS. But it's not possible right now.
|
|
1079
|
-
// Attention: Could be a downgrade attack.
|
|
1080
|
-
return false;
|
|
1081
1090
|
}
|
|
1091
|
+
|
|
1092
|
+
// Opportunistic STARTTLS. But it's not possible right now.
|
|
1093
|
+
// Attention: Could be a downgrade attack.
|
|
1094
|
+
return false;
|
|
1082
1095
|
}
|
|
1083
1096
|
|
|
1084
1097
|
/**
|
|
@@ -1497,7 +1510,7 @@ class ImapFlow extends EventEmitter {
|
|
|
1497
1510
|
return false;
|
|
1498
1511
|
}
|
|
1499
1512
|
|
|
1500
|
-
if (
|
|
1513
|
+
if (!this.mailbox || !comparePaths(this, this.mailbox.path, path)) {
|
|
1501
1514
|
return await this.mailboxOpen(path);
|
|
1502
1515
|
}
|
|
1503
1516
|
|
|
@@ -1619,7 +1632,7 @@ class ImapFlow extends EventEmitter {
|
|
|
1619
1632
|
}
|
|
1620
1633
|
}
|
|
1621
1634
|
|
|
1622
|
-
|
|
1635
|
+
let connectPromise = new Promise((resolve, reject) => {
|
|
1623
1636
|
this.connectTimeout = setTimeout(() => {
|
|
1624
1637
|
let err = new Error('Failed to establish connection in required time');
|
|
1625
1638
|
err.code = 'CONNECT_TIMEOUT';
|
|
@@ -1713,6 +1726,12 @@ class ImapFlow extends EventEmitter {
|
|
|
1713
1726
|
};
|
|
1714
1727
|
this.socket.on('error', this._connectErrorHandler);
|
|
1715
1728
|
});
|
|
1729
|
+
|
|
1730
|
+
// Prevent unhandled promise rejection if close() rejects the connect
|
|
1731
|
+
// promise via setImmediate. The rejection still propagates to the caller.
|
|
1732
|
+
connectPromise.catch(noop);
|
|
1733
|
+
|
|
1734
|
+
await connectPromise;
|
|
1716
1735
|
}
|
|
1717
1736
|
|
|
1718
1737
|
/**
|
|
@@ -2496,13 +2515,7 @@ class ImapFlow extends EventEmitter {
|
|
|
2496
2515
|
* await client.append('INBOX', rawMessageBuffer, ['\\Seen'], new Date(2000, 1, 1));
|
|
2497
2516
|
*/
|
|
2498
2517
|
async append(path, content, flags, idate) {
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
if (!response) {
|
|
2502
|
-
return false;
|
|
2503
|
-
}
|
|
2504
|
-
|
|
2505
|
-
return response;
|
|
2518
|
+
return (await this.run('APPEND', path, content, flags, idate)) || false;
|
|
2506
2519
|
}
|
|
2507
2520
|
|
|
2508
2521
|
/**
|
|
@@ -2589,13 +2602,7 @@ class ImapFlow extends EventEmitter {
|
|
|
2589
2602
|
return;
|
|
2590
2603
|
}
|
|
2591
2604
|
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
if (!response) {
|
|
2595
|
-
return false;
|
|
2596
|
-
}
|
|
2597
|
-
|
|
2598
|
-
return response;
|
|
2605
|
+
return (await this.run('SEARCH', query, options)) || false;
|
|
2599
2606
|
}
|
|
2600
2607
|
|
|
2601
2608
|
/**
|
|
@@ -2734,9 +2741,8 @@ class ImapFlow extends EventEmitter {
|
|
|
2734
2741
|
let entry = rowQueue.shift();
|
|
2735
2742
|
if (entry.err) {
|
|
2736
2743
|
return reject(entry.err);
|
|
2737
|
-
} else {
|
|
2738
|
-
return resolve(entry.value);
|
|
2739
2744
|
}
|
|
2745
|
+
return resolve(entry.value);
|
|
2740
2746
|
}
|
|
2741
2747
|
|
|
2742
2748
|
if (finished) {
|
|
@@ -3238,6 +3244,29 @@ class ImapFlow extends EventEmitter {
|
|
|
3238
3244
|
// stream and then fetch remaining chunks via fetchAllParts().
|
|
3239
3245
|
// setImmediate ensures the caller gets the {meta, content} return
|
|
3240
3246
|
// value before streaming begins.
|
|
3247
|
+
let runFetchAllParts = () => {
|
|
3248
|
+
fetchAllParts()
|
|
3249
|
+
.catch(err => {
|
|
3250
|
+
if (!fetchAborted && stream && !stream.destroyed) {
|
|
3251
|
+
stream.emit('error', err);
|
|
3252
|
+
} else {
|
|
3253
|
+
// Log when error cannot be emitted to stream
|
|
3254
|
+
this.log.warn({
|
|
3255
|
+
msg: 'Download error after stream closed',
|
|
3256
|
+
err,
|
|
3257
|
+
fetchAborted,
|
|
3258
|
+
streamDestroyed: stream?.destroyed,
|
|
3259
|
+
cid: this.id
|
|
3260
|
+
});
|
|
3261
|
+
}
|
|
3262
|
+
})
|
|
3263
|
+
.finally(() => {
|
|
3264
|
+
if (!fetchAborted && stream && !stream.destroyed) {
|
|
3265
|
+
stream.end();
|
|
3266
|
+
}
|
|
3267
|
+
});
|
|
3268
|
+
};
|
|
3269
|
+
|
|
3241
3270
|
setImmediate(() => {
|
|
3242
3271
|
let writeResult;
|
|
3243
3272
|
try {
|
|
@@ -3254,49 +3283,11 @@ class ImapFlow extends EventEmitter {
|
|
|
3254
3283
|
// Initial chunk filled the buffer, wait for drain
|
|
3255
3284
|
stream.once('drain', () => {
|
|
3256
3285
|
if (!fetchAborted) {
|
|
3257
|
-
|
|
3258
|
-
.catch(err => {
|
|
3259
|
-
if (!fetchAborted && stream && !stream.destroyed) {
|
|
3260
|
-
stream.emit('error', err);
|
|
3261
|
-
} else {
|
|
3262
|
-
// Log when error cannot be emitted to stream
|
|
3263
|
-
this.log.warn({
|
|
3264
|
-
msg: 'Download error after stream closed',
|
|
3265
|
-
err,
|
|
3266
|
-
fetchAborted,
|
|
3267
|
-
streamDestroyed: stream?.destroyed,
|
|
3268
|
-
cid: this.id
|
|
3269
|
-
});
|
|
3270
|
-
}
|
|
3271
|
-
})
|
|
3272
|
-
.finally(() => {
|
|
3273
|
-
if (!fetchAborted && stream && !stream.destroyed) {
|
|
3274
|
-
stream.end();
|
|
3275
|
-
}
|
|
3276
|
-
});
|
|
3286
|
+
runFetchAllParts();
|
|
3277
3287
|
}
|
|
3278
3288
|
});
|
|
3279
3289
|
} else {
|
|
3280
|
-
|
|
3281
|
-
.catch(err => {
|
|
3282
|
-
if (!fetchAborted && stream && !stream.destroyed) {
|
|
3283
|
-
stream.emit('error', err);
|
|
3284
|
-
} else {
|
|
3285
|
-
// Log when error cannot be emitted to stream
|
|
3286
|
-
this.log.warn({
|
|
3287
|
-
msg: 'Download error after stream closed',
|
|
3288
|
-
err,
|
|
3289
|
-
fetchAborted,
|
|
3290
|
-
streamDestroyed: stream?.destroyed,
|
|
3291
|
-
cid: this.id
|
|
3292
|
-
});
|
|
3293
|
-
}
|
|
3294
|
-
})
|
|
3295
|
-
.finally(() => {
|
|
3296
|
-
if (!fetchAborted && stream && !stream.destroyed) {
|
|
3297
|
-
stream.end();
|
|
3298
|
-
}
|
|
3299
|
-
});
|
|
3290
|
+
runFetchAllParts();
|
|
3300
3291
|
}
|
|
3301
3292
|
});
|
|
3302
3293
|
|
|
@@ -3495,14 +3486,6 @@ class ImapFlow extends EventEmitter {
|
|
|
3495
3486
|
// Process all locks in queue until empty
|
|
3496
3487
|
let processedCount = 0;
|
|
3497
3488
|
while (this.locks.length > 0) {
|
|
3498
|
-
if (!this.locks.length) {
|
|
3499
|
-
this.log.trace({
|
|
3500
|
-
msg: 'Mailbox locking queue processed',
|
|
3501
|
-
idling: this.idling
|
|
3502
|
-
});
|
|
3503
|
-
return;
|
|
3504
|
-
}
|
|
3505
|
-
|
|
3506
3489
|
// Yield to event loop periodically to prevent CPU blocking
|
|
3507
3490
|
processedCount++;
|
|
3508
3491
|
if (processedCount % 5 === 0) {
|
|
@@ -3549,46 +3532,46 @@ class ImapFlow extends EventEmitter {
|
|
|
3549
3532
|
this.currentLock = lock;
|
|
3550
3533
|
resolve({ path, release });
|
|
3551
3534
|
break; // Stop processing; next lock waits for release()
|
|
3552
|
-
}
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
})
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
} catch (E) {
|
|
3577
|
-
this.log.trace({ msg: 'Failed to verify failed mailbox', path, err: E });
|
|
3535
|
+
}
|
|
3536
|
+
|
|
3537
|
+
try {
|
|
3538
|
+
// Need to SELECT/EXAMINE a different mailbox
|
|
3539
|
+
await this.mailboxOpen(path, options);
|
|
3540
|
+
this.log.trace({
|
|
3541
|
+
msg: 'Mailbox lock acquired [selected]',
|
|
3542
|
+
path,
|
|
3543
|
+
lockId,
|
|
3544
|
+
idling: this.idling,
|
|
3545
|
+
...(options.description && { description: options.description })
|
|
3546
|
+
});
|
|
3547
|
+
this.currentLock = lock;
|
|
3548
|
+
resolve({ path, release });
|
|
3549
|
+
break; // Wait for this lock to be released
|
|
3550
|
+
} catch (err) {
|
|
3551
|
+
if (err.responseStatus === 'NO') {
|
|
3552
|
+
// SELECT failed with NO -- verify whether the mailbox exists
|
|
3553
|
+
// at all by running LIST. This sets mailboxMissing on the error
|
|
3554
|
+
// so the caller can distinguish "doesn't exist" from other failures.
|
|
3555
|
+
try {
|
|
3556
|
+
let folders = await this.run('LIST', '', path, { listOnly: true });
|
|
3557
|
+
if (!folders || !folders.length) {
|
|
3558
|
+
err.mailboxMissing = true;
|
|
3578
3559
|
}
|
|
3560
|
+
} catch (E) {
|
|
3561
|
+
this.log.trace({ msg: 'Failed to verify failed mailbox', path, err: E });
|
|
3579
3562
|
}
|
|
3580
|
-
|
|
3581
|
-
this.log.trace({
|
|
3582
|
-
msg: 'Failed to acquire mailbox lock',
|
|
3583
|
-
path,
|
|
3584
|
-
lockId,
|
|
3585
|
-
idling: this.idling,
|
|
3586
|
-
...(options.description && { description: options.description }),
|
|
3587
|
-
err
|
|
3588
|
-
});
|
|
3589
|
-
reject(err);
|
|
3590
|
-
// Continue to next lock in queue
|
|
3591
3563
|
}
|
|
3564
|
+
|
|
3565
|
+
this.log.trace({
|
|
3566
|
+
msg: 'Failed to acquire mailbox lock',
|
|
3567
|
+
path,
|
|
3568
|
+
lockId,
|
|
3569
|
+
idling: this.idling,
|
|
3570
|
+
...(options.description && { description: options.description }),
|
|
3571
|
+
err
|
|
3572
|
+
});
|
|
3573
|
+
reject(err);
|
|
3574
|
+
// Continue to next lock in queue
|
|
3592
3575
|
}
|
|
3593
3576
|
}
|
|
3594
3577
|
} finally {
|
|
@@ -3626,7 +3609,7 @@ class ImapFlow extends EventEmitter {
|
|
|
3626
3609
|
* lock.release();
|
|
3627
3610
|
* }
|
|
3628
3611
|
*/
|
|
3629
|
-
|
|
3612
|
+
getMailboxLock(path, options) {
|
|
3630
3613
|
options = options || {};
|
|
3631
3614
|
|
|
3632
3615
|
path = normalizePath(this, path);
|
|
@@ -3645,10 +3628,16 @@ class ImapFlow extends EventEmitter {
|
|
|
3645
3628
|
: null
|
|
3646
3629
|
});
|
|
3647
3630
|
|
|
3648
|
-
|
|
3631
|
+
let lockPromise = new Promise((resolve, reject) => {
|
|
3649
3632
|
this.locks.push({ resolve, reject, path, options, lockId });
|
|
3650
3633
|
this.processLocks().catch(err => reject(err));
|
|
3651
3634
|
});
|
|
3635
|
+
|
|
3636
|
+
// Prevent unhandled promise rejection if close() rejects this lock
|
|
3637
|
+
// via setImmediate. The rejection still propagates to the caller.
|
|
3638
|
+
lockPromise.catch(noop);
|
|
3639
|
+
|
|
3640
|
+
return lockPromise;
|
|
3652
3641
|
}
|
|
3653
3642
|
|
|
3654
3643
|
getLogger() {
|
|
@@ -3666,16 +3655,15 @@ class ImapFlow extends EventEmitter {
|
|
|
3666
3655
|
synteticLogger[level] = (...args) => {
|
|
3667
3656
|
// using {logger:false} disables logging
|
|
3668
3657
|
if (this.options.logger !== false) {
|
|
3669
|
-
if (
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
console.log(JSON.stringify(...args));
|
|
3675
|
-
}
|
|
3676
|
-
} else {
|
|
3677
|
-
mainLogger[level](...args);
|
|
3658
|
+
if (typeof mainLogger[level] !== 'function') {
|
|
3659
|
+
// we are checking to make sure the level is supported.
|
|
3660
|
+
// if it isn't supported but the level is error or fatal, log to console anyway.
|
|
3661
|
+
if (level === 'fatal' || level === 'error') {
|
|
3662
|
+
console.log(JSON.stringify(...args));
|
|
3678
3663
|
}
|
|
3664
|
+
} else {
|
|
3665
|
+
mainLogger[level](...args);
|
|
3666
|
+
}
|
|
3679
3667
|
}
|
|
3680
3668
|
|
|
3681
3669
|
if (this.emitLogs && args && args[0] && typeof args[0] === 'object') {
|
package/lib/search-compiler.js
CHANGED
|
@@ -316,16 +316,10 @@ module.exports.searchCompiler = (connection, query) => {
|
|
|
316
316
|
|
|
317
317
|
// NOT operator
|
|
318
318
|
case 'NOT':
|
|
319
|
-
{
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
if (typeof params[term] === 'object') {
|
|
325
|
-
attributes.push({ type: 'ATOM', value: 'NOT' });
|
|
326
|
-
// Recursively process NOT conditions
|
|
327
|
-
walk(params[term]);
|
|
328
|
-
}
|
|
319
|
+
if (params[term] && typeof params[term] === 'object') {
|
|
320
|
+
attributes.push({ type: 'ATOM', value: 'NOT' });
|
|
321
|
+
// Recursively process NOT conditions
|
|
322
|
+
walk(params[term]);
|
|
329
323
|
}
|
|
330
324
|
break;
|
|
331
325
|
|
package/lib/tools.js
CHANGED
|
@@ -77,7 +77,7 @@ const tools = {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
// ensure namespace prefix if needed
|
|
80
|
-
if (!skipNamespace && connection.namespace && connection.namespace.prefix && path.
|
|
80
|
+
if (!skipNamespace && connection.namespace && connection.namespace.prefix && !path.startsWith(connection.namespace.prefix)) {
|
|
81
81
|
path = connection.namespace.prefix + path;
|
|
82
82
|
}
|
|
83
83
|
|
|
@@ -111,7 +111,7 @@ const tools = {
|
|
|
111
111
|
if (list && Array.isArray(list)) {
|
|
112
112
|
list.forEach(val => {
|
|
113
113
|
if (typeof val.value !== 'string') {
|
|
114
|
-
return
|
|
114
|
+
return;
|
|
115
115
|
}
|
|
116
116
|
let capability = val.value.toUpperCase().trim();
|
|
117
117
|
|
|
@@ -120,7 +120,7 @@ const tools = {
|
|
|
120
120
|
return;
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
if (capability.
|
|
123
|
+
if (capability.startsWith('APPENDLIMIT=')) {
|
|
124
124
|
let splitPos = capability.indexOf('=');
|
|
125
125
|
let appendLimit = Number(capability.substr(splitPos + 1)) || 0;
|
|
126
126
|
map.set('APPENDLIMIT', appendLimit);
|
|
@@ -205,8 +205,6 @@ const tools = {
|
|
|
205
205
|
let cur = node.folders && node.folders.find(folder => folder.name === parent);
|
|
206
206
|
if (cur) {
|
|
207
207
|
node = cur;
|
|
208
|
-
} else {
|
|
209
|
-
// not yet set, will be created by the caller
|
|
210
208
|
}
|
|
211
209
|
}
|
|
212
210
|
|
|
@@ -294,7 +292,7 @@ const tools = {
|
|
|
294
292
|
|
|
295
293
|
const color = bit0 | bit1 | bit2; // eslint-disable-line no-bitwise
|
|
296
294
|
|
|
297
|
-
return FLAG_COLORS[color]
|
|
295
|
+
return FLAG_COLORS[color] ?? 'red'; // default to red for the unused \b111
|
|
298
296
|
},
|
|
299
297
|
|
|
300
298
|
/**
|
|
@@ -306,23 +304,20 @@ const tools = {
|
|
|
306
304
|
getColorFlags(color) {
|
|
307
305
|
// Reverse mapping from a color name to the Apple Mail $MailFlagBit0/1/2 flags.
|
|
308
306
|
// Returns an object with 'add' and 'remove' arrays so the caller can STORE +FLAGS/-FLAGS.
|
|
309
|
-
const colorCode = color ? FLAG_COLORS.indexOf(
|
|
310
|
-
if (colorCode
|
|
307
|
+
const colorCode = color ? FLAG_COLORS.indexOf(color.toString().toLowerCase().trim()) : null;
|
|
308
|
+
if (colorCode === null || colorCode < 0) {
|
|
309
|
+
if (colorCode === null) {
|
|
310
|
+
// Remove color: remove \Flagged and all MailFlagBit flags
|
|
311
|
+
return { add: [], remove: ['\\Flagged', '$MailFlagBit0', '$MailFlagBit1', '$MailFlagBit2'] };
|
|
312
|
+
}
|
|
311
313
|
return null;
|
|
312
314
|
}
|
|
313
315
|
|
|
314
316
|
// Decompose color index back into its 3-bit representation
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
// If colorCode is truthy (non-zero), add \Flagged; if zero/null, remove \Flagged
|
|
321
|
-
let result = { add: colorCode ? ['\\Flagged'] : [], remove: colorCode ? [] : ['\\Flagged'] };
|
|
322
|
-
|
|
323
|
-
// For each bit, add the corresponding $MailFlagBitN if set, remove it if unset
|
|
324
|
-
for (let i = 0; i < bits.length; i++) {
|
|
325
|
-
if (bits[i]) {
|
|
317
|
+
let result = { add: ['\\Flagged'], remove: [] };
|
|
318
|
+
for (let i = 0; i < 3; i++) {
|
|
319
|
+
// eslint-disable-next-line no-bitwise
|
|
320
|
+
if (colorCode & (1 << i)) {
|
|
326
321
|
result.add.push(`$MailFlagBit${i}`);
|
|
327
322
|
} else {
|
|
328
323
|
result.remove.push(`$MailFlagBit${i}`);
|
|
@@ -529,7 +524,7 @@ const tools = {
|
|
|
529
524
|
processName(name) {
|
|
530
525
|
name = (name || '').toString();
|
|
531
526
|
if (name.length > 2 && name.at(0) === '"' && name.at(-1) === '"') {
|
|
532
|
-
name = name.
|
|
527
|
+
name = name.slice(1, -1);
|
|
533
528
|
}
|
|
534
529
|
return name;
|
|
535
530
|
},
|
|
@@ -679,7 +674,7 @@ const tools = {
|
|
|
679
674
|
value = params[key];
|
|
680
675
|
|
|
681
676
|
// The first segment (*0*) may contain charset and language: charset'language'value
|
|
682
|
-
if (nr === 0 && match[0].
|
|
677
|
+
if (nr === 0 && match[0].at(-1) === '*' && (match = value.match(/^([^']*)'[^']*'(.*)$/))) {
|
|
683
678
|
params[actualKey].charset = match[1] || 'utf-8';
|
|
684
679
|
value = match[2];
|
|
685
680
|
}
|
|
@@ -713,12 +708,11 @@ const tools = {
|
|
|
713
708
|
value
|
|
714
709
|
// fix invalidly encoded chars
|
|
715
710
|
.replace(/[=?_\s]/g, s => {
|
|
716
|
-
let c = s.charCodeAt(0).toString(16);
|
|
717
711
|
if (s === ' ') {
|
|
718
712
|
return '_';
|
|
719
|
-
} else {
|
|
720
|
-
return '%' + (c.length < 2 ? '0' : '') + c;
|
|
721
713
|
}
|
|
714
|
+
let c = s.charCodeAt(0).toString(16);
|
|
715
|
+
return '%' + (c.length < 2 ? '0' : '') + c;
|
|
722
716
|
})
|
|
723
717
|
// change from urlencoding to percent encoding
|
|
724
718
|
.replace(/%/g, '=') +
|
|
@@ -788,25 +782,25 @@ const tools = {
|
|
|
788
782
|
|
|
789
783
|
// id
|
|
790
784
|
if (node[i]) {
|
|
791
|
-
curNode.id = (
|
|
785
|
+
curNode.id = (node[i].value || '').toString();
|
|
792
786
|
}
|
|
793
787
|
i++;
|
|
794
788
|
|
|
795
789
|
// description
|
|
796
790
|
if (node[i]) {
|
|
797
|
-
curNode.description = (
|
|
791
|
+
curNode.description = (node[i].value || '').toString();
|
|
798
792
|
}
|
|
799
793
|
i++;
|
|
800
794
|
|
|
801
795
|
// encoding
|
|
802
796
|
if (node[i]) {
|
|
803
|
-
curNode.encoding = (
|
|
797
|
+
curNode.encoding = (node[i].value || '').toString().toLowerCase();
|
|
804
798
|
}
|
|
805
799
|
i++;
|
|
806
800
|
|
|
807
801
|
// size
|
|
808
802
|
if (node[i]) {
|
|
809
|
-
curNode.size = Number(
|
|
803
|
+
curNode.size = Number(node[i].value || 0) || 0;
|
|
810
804
|
}
|
|
811
805
|
i++;
|
|
812
806
|
|
|
@@ -834,7 +828,7 @@ const tools = {
|
|
|
834
828
|
|
|
835
829
|
// line count
|
|
836
830
|
if (node[i]) {
|
|
837
|
-
curNode.lineCount = Number(
|
|
831
|
+
curNode.lineCount = Number(node[i].value || 0) || 0;
|
|
838
832
|
}
|
|
839
833
|
i++;
|
|
840
834
|
}
|
|
@@ -850,8 +844,7 @@ const tools = {
|
|
|
850
844
|
} else {
|
|
851
845
|
// correct structure, line count number is provided
|
|
852
846
|
if (node[i]) {
|
|
853
|
-
|
|
854
|
-
curNode.lineCount = Number((node[i] || {}).value || 0) || 0;
|
|
847
|
+
curNode.lineCount = Number(node[i].value || 0) || 0;
|
|
855
848
|
}
|
|
856
849
|
i++;
|
|
857
850
|
}
|
|
@@ -862,7 +855,7 @@ const tools = {
|
|
|
862
855
|
// md5
|
|
863
856
|
if (i < node.length - 1) {
|
|
864
857
|
if (node[i]) {
|
|
865
|
-
curNode.md5 = (
|
|
858
|
+
curNode.md5 = (node[i].value || '').toString().toLowerCase();
|
|
866
859
|
}
|
|
867
860
|
i++;
|
|
868
861
|
}
|
|
@@ -874,7 +867,7 @@ const tools = {
|
|
|
874
867
|
// body disposition
|
|
875
868
|
if (i < node.length - 1) {
|
|
876
869
|
if (Array.isArray(node[i]) && node[i].length) {
|
|
877
|
-
curNode.disposition = ((node[i][0]
|
|
870
|
+
curNode.disposition = ((node[i][0] && node[i][0].value) || '').toString().toLowerCase();
|
|
878
871
|
if (Array.isArray(node[i][1])) {
|
|
879
872
|
curNode.dispositionParameters = tools.getStructuredParams(node[i][1]);
|
|
880
873
|
}
|
|
@@ -895,7 +888,7 @@ const tools = {
|
|
|
895
888
|
// Errata: http://www.rfc-editor.org/errata_search.php?rfc=3501
|
|
896
889
|
if (i < node.length - 1) {
|
|
897
890
|
if (node[i]) {
|
|
898
|
-
curNode.location = (
|
|
891
|
+
curNode.location = (node[i].value || '').toString();
|
|
899
892
|
}
|
|
900
893
|
}
|
|
901
894
|
|
|
@@ -990,7 +983,7 @@ const tools = {
|
|
|
990
983
|
case '\\flagged':
|
|
991
984
|
case '\\deleted':
|
|
992
985
|
case '\\draft':
|
|
993
|
-
//
|
|
986
|
+
// normalize capitalization (e.g., "\\seen" -> "\\Seen")
|
|
994
987
|
return flag.toLowerCase().replace(/^\\./, c => c.toUpperCase());
|
|
995
988
|
}
|
|
996
989
|
return flag;
|
|
@@ -1048,7 +1041,7 @@ const tools = {
|
|
|
1048
1041
|
*/
|
|
1049
1042
|
getDecoder(charset) {
|
|
1050
1043
|
charset = (charset || 'ascii').toString().trim().toLowerCase();
|
|
1051
|
-
if (/^jis|^iso-?2022-?jp|^
|
|
1044
|
+
if (/^jis|^iso-?2022-?jp|^euc-?jp/.test(charset)) {
|
|
1052
1045
|
// special case not supported by iconv-lite
|
|
1053
1046
|
return new JPDecoder(charset);
|
|
1054
1047
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "imapflow",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.14",
|
|
4
4
|
"description": "IMAP Client for Node",
|
|
5
5
|
"main": "lib/imap-flow.js",
|
|
6
6
|
"types": "lib/imap-flow.d.ts",
|
|
@@ -28,9 +28,9 @@
|
|
|
28
28
|
"homepage": "https://imapflow.com/",
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@eslint/js": "10.0.1",
|
|
31
|
-
"@types/node": "25.
|
|
31
|
+
"@types/node": "25.5.0",
|
|
32
32
|
"c8": "11.0.0",
|
|
33
|
-
"eslint": "10.0.
|
|
33
|
+
"eslint": "10.0.3",
|
|
34
34
|
"eslint-config-nodemailer": "1.2.0",
|
|
35
35
|
"eslint-config-prettier": "10.1.8",
|
|
36
36
|
"grunt": "1.6.1",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"libbase64": "1.3.0",
|
|
49
49
|
"libmime": "5.3.7",
|
|
50
50
|
"libqp": "2.1.1",
|
|
51
|
-
"nodemailer": "8.0.
|
|
51
|
+
"nodemailer": "8.0.2",
|
|
52
52
|
"pino": "10.3.1",
|
|
53
53
|
"socks": "2.8.7"
|
|
54
54
|
}
|