imapflow 1.4.9 → 1.5.0
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/.github/workflows/test.yml +20 -0
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +7 -0
- package/CLAUDE.md +2 -4
- package/lib/commands/fetch.js +18 -14
- package/lib/commands/list.js +4 -1
- package/lib/commands/status.js +6 -1
- package/lib/handler/imap-compiler.js +12 -9
- package/lib/handler/token-parser.js +7 -0
- package/lib/imap-flow.d.ts +14 -0
- package/lib/imap-flow.js +19 -2
- package/lib/tools.js +35 -3
- package/package.json +1 -1
- package/test/commands-integration-test.js +329 -0
- package/test/connection-edge-cases-test.js +4 -40
- package/test/handler-branches-test.js +4 -3
- package/test/imap-compiler-test.js +85 -0
- package/test/imap-flow-fetch-download-test.js +56 -0
- package/test/imap-flow-secure-test.js +25 -11
- package/test/imap-flow-server-test.js +80 -0
- package/test/imap-parser-test.js +112 -1
- package/test/imap-stream-test.js +46 -0
- package/test/integration/README.md +17 -5
- package/test/integration/rev2-live-test.js +125 -0
- package/test/integration/run-rev2-tests.sh +14 -0
- package/test/search-compiler-test.js +17 -0
- package/test/tools-test.js +42 -4
|
@@ -29,3 +29,23 @@ jobs:
|
|
|
29
29
|
cache: npm
|
|
30
30
|
- run: npm install
|
|
31
31
|
- run: npm test
|
|
32
|
+
|
|
33
|
+
test-rev2:
|
|
34
|
+
# Live IMAP4rev2 integration tests against a real Dovecot 2.4 server in
|
|
35
|
+
# Docker on linux/amd64 (ubuntu runners are amd64 with Docker preinstalled).
|
|
36
|
+
# This is the only place the suite runs on amd64 - Apple Silicon dev
|
|
37
|
+
# machines cannot run the amd64 image under Rosetta.
|
|
38
|
+
name: Live IMAP4rev2 tests (Dovecot, linux/amd64)
|
|
39
|
+
timeout-minutes: 15
|
|
40
|
+
runs-on: ubuntu-latest
|
|
41
|
+
steps:
|
|
42
|
+
- uses: actions/checkout@v6
|
|
43
|
+
- name: Use Node.js 24.x
|
|
44
|
+
uses: actions/setup-node@v6
|
|
45
|
+
with:
|
|
46
|
+
node-version: 24.x
|
|
47
|
+
cache: npm
|
|
48
|
+
- run: npm install
|
|
49
|
+
- run: npm run test:rev2
|
|
50
|
+
env:
|
|
51
|
+
IMAPFLOW_DOVECOT_PLATFORM: linux/amd64
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.5.0](https://github.com/postalsys/imapflow/compare/v1.4.9...v1.5.0) (2026-07-23)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* add STATUS SIZE/DELETED and rev2 BINARY fetch support, fix protocol bugs found in an RFC 9051 review ([04f5bf6](https://github.com/postalsys/imapflow/commit/04f5bf6c1a49e155dd23b2af062e570530b814b1))
|
|
9
|
+
|
|
3
10
|
## [1.4.9](https://github.com/postalsys/imapflow/compare/v1.4.8...v1.4.9) (2026-07-22)
|
|
4
11
|
|
|
5
12
|
|
package/CLAUDE.md
CHANGED
|
@@ -81,7 +81,7 @@ CommonJS-compatible:
|
|
|
81
81
|
1. Run `npm run format` and `npm run lint`
|
|
82
82
|
2. Run `npm test` and keep it green
|
|
83
83
|
3. For non-trivial changes, run `/simplify` to review changed code and `/security-review` to check for security issues before committing
|
|
84
|
-
- After pushing, check the GitHub Actions runs for the push (e.g. `gh run list --branch master`) and report their status
|
|
84
|
+
- After pushing, check the GitHub Actions runs for the push (e.g. `gh run list --branch master`) and report their status. If a run fails for a strange or unrelated reason (for example a checkout step reporting "account suspended", HTTP 403, or other auth/infrastructure errors that have nothing to do with the change), check <https://www.githubstatus.com/> for an active GitHub incident before assuming the failure is caused by the change.
|
|
85
85
|
|
|
86
86
|
## Relationship to EmailEngine
|
|
87
87
|
|
|
@@ -97,9 +97,7 @@ Packaging Constraints).
|
|
|
97
97
|
## Security
|
|
98
98
|
|
|
99
99
|
Security policy and private reporting channels are documented in
|
|
100
|
-
[`SECURITY.md`](SECURITY.md) / [`SECURITY.txt`](SECURITY.txt).
|
|
101
|
-
through the "CodeQL Advanced" GitHub Actions workflow
|
|
102
|
-
(`.github/workflows/codeql.yml`, config in `.github/codeql/codeql-config.yml`).
|
|
100
|
+
[`SECURITY.md`](SECURITY.md) / [`SECURITY.txt`](SECURITY.txt).
|
|
103
101
|
|
|
104
102
|
## Release Process
|
|
105
103
|
|
package/lib/commands/fetch.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { formatMessageResponse } = require('../tools');
|
|
3
|
+
const { formatMessageResponse, isRev2Active } = require('../tools');
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Fetches emails from the server.
|
|
@@ -25,8 +25,12 @@ module.exports = async (connection, range, query, options) => {
|
|
|
25
25
|
|
|
26
26
|
let mailbox = connection.mailbox;
|
|
27
27
|
|
|
28
|
-
// Use BINARY extension for fetching if supported and requested, otherwise fall back to BODY
|
|
29
|
-
|
|
28
|
+
// Use BINARY extension for fetching if supported and requested, otherwise fall back to BODY.
|
|
29
|
+
// RFC 9051 folds the FETCH side of the BINARY extension into base IMAP4rev2, so an active
|
|
30
|
+
// rev2 session can use it even without the BINARY capability token (the APPEND side is NOT
|
|
31
|
+
// folded in and stays gated on the token in append.js)
|
|
32
|
+
const canUseBinary = connection.capabilities.has('BINARY') || isRev2Active(connection);
|
|
33
|
+
const commandKey = canUseBinary && options.binary && !connection.disableBinary ? 'BINARY' : 'BODY';
|
|
30
34
|
|
|
31
35
|
// Retry logic for ETHROTTLE errors (server rate limiting) with exponential backoff
|
|
32
36
|
let retryCount = 0;
|
|
@@ -50,21 +54,21 @@ module.exports = async (connection, range, query, options) => {
|
|
|
50
54
|
// PEEK avoids marking messages as \Seen. Section identifies what to fetch (HEADER, specific part, etc.)
|
|
51
55
|
// Partial is an optional byte range [start, maxLength].
|
|
52
56
|
let setBodyPeek = (attributes, partial) => {
|
|
57
|
+
let section = [].concat(attributes || []);
|
|
58
|
+
|
|
59
|
+
// BINARY may only address the empty section or a numeric part specifier
|
|
60
|
+
// (RFC 3516 / RFC 9051 section-binary) - HEADER, HEADER.FIELDS, TEXT and
|
|
61
|
+
// n.MIME are invalid after BINARY and must stay BODY fetches
|
|
62
|
+
let binaryAddressable =
|
|
63
|
+
!section.length || (section.length === 1 && typeof section[0].value === 'string' && /^\d+(\.\d+)*$/.test(section[0].value));
|
|
64
|
+
|
|
53
65
|
let bodyPeek = {
|
|
54
66
|
type: 'ATOM',
|
|
55
|
-
value: `${commandKey}.PEEK`,
|
|
56
|
-
section
|
|
67
|
+
value: `${binaryAddressable ? commandKey : 'BODY'}.PEEK`,
|
|
68
|
+
section,
|
|
57
69
|
partial
|
|
58
70
|
};
|
|
59
71
|
|
|
60
|
-
if (Array.isArray(attributes)) {
|
|
61
|
-
attributes.forEach(attribute => {
|
|
62
|
-
bodyPeek.section.push(attribute);
|
|
63
|
-
});
|
|
64
|
-
} else if (attributes) {
|
|
65
|
-
bodyPeek.section.push(attributes);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
72
|
queryStructure.push(bodyPeek);
|
|
69
73
|
};
|
|
70
74
|
|
|
@@ -88,7 +92,7 @@ module.exports = async (connection, range, query, options) => {
|
|
|
88
92
|
partial.push(Number(query.source.maxLength));
|
|
89
93
|
}
|
|
90
94
|
}
|
|
91
|
-
|
|
95
|
+
setBodyPeek(null, partial);
|
|
92
96
|
}
|
|
93
97
|
|
|
94
98
|
// Always request a unique email ID for message deduplication.
|
package/lib/commands/list.js
CHANGED
|
@@ -235,7 +235,10 @@ module.exports = async (connection, reference, mailbox, options) => {
|
|
|
235
235
|
UIDNEXT: { key: 'uidNext', parser: Number },
|
|
236
236
|
UIDVALIDITY: { key: 'uidValidity', parser: BigInt },
|
|
237
237
|
UNSEEN: { key: 'unseen', parser: Number },
|
|
238
|
-
HIGHESTMODSEQ: { key: 'highestModseq', parser: BigInt }
|
|
238
|
+
HIGHESTMODSEQ: { key: 'highestModseq', parser: BigInt },
|
|
239
|
+
// IMAP4rev2 additions (RFC 9051): mailbox size and \Deleted count
|
|
240
|
+
SIZE: { key: 'size', parser: Number },
|
|
241
|
+
DELETED: { key: 'deleted', parser: Number }
|
|
239
242
|
};
|
|
240
243
|
|
|
241
244
|
let key;
|
package/lib/commands/status.js
CHANGED
|
@@ -86,7 +86,12 @@ module.exports = async (connection, path, query) => {
|
|
|
86
86
|
updateMailbox: (val, conn) => {
|
|
87
87
|
conn.mailbox.highestModseq = val;
|
|
88
88
|
}
|
|
89
|
-
}
|
|
89
|
+
},
|
|
90
|
+
// IMAP4rev2 additions (RFC 9051): total mailbox size in octets
|
|
91
|
+
// (number64, exact as a JS number up to 2^53-1) and count of
|
|
92
|
+
// messages with the \Deleted flag
|
|
93
|
+
SIZE: { key: 'size', parser: Number },
|
|
94
|
+
DELETED: { key: 'deleted', parser: Number }
|
|
90
95
|
};
|
|
91
96
|
|
|
92
97
|
let key;
|
|
@@ -130,15 +130,18 @@ module.exports = async (response, options) => {
|
|
|
130
130
|
if (isLogging) {
|
|
131
131
|
resp.push(formatRespEntry('"(* ' + node.value.length + 'B literal *)"'));
|
|
132
132
|
} else {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
133
|
+
// The literal size marker counts octets - string values are written as
|
|
134
|
+
// UTF-8, so their UTF-16 .length would undercount multi-byte characters
|
|
135
|
+
let literalLength = !node.value ? 0 : Buffer.isBuffer(node.value) ? node.value.length : Buffer.byteLength(node.value.toString());
|
|
136
|
+
|
|
137
|
+
// Append '+' to the size marker only when the extension actually permits a
|
|
138
|
+
// non-synchronizing literal of this size (RFC 7888): LITERAL+ always,
|
|
139
|
+
// LITERAL- only up to 4096 bytes
|
|
140
|
+
let usePlus = literalPlus || (literalMinus && literalLength <= 4096);
|
|
141
|
+
// canAppend: whether the literal data can be sent in the same buffer segment -
|
|
142
|
+
// non-synchronizing literals always, and everything in single-buffer mode
|
|
143
|
+
// (asArray false), which has no continuation flow
|
|
144
|
+
let canAppend = !asArray || usePlus;
|
|
142
145
|
|
|
143
146
|
// Emit the literal header: optional '~' prefix for literal8, then {size[+]}\r\n
|
|
144
147
|
resp.push(formatRespEntry(`${node.isLiteral8 ? '~' : ''}{${literalLength}${usePlus ? '+' : ''}}\r\n`));
|
|
@@ -580,6 +580,13 @@ class TokenParser {
|
|
|
580
580
|
if (!this.currentNode.literalLength) {
|
|
581
581
|
// special case where literal content length is 0
|
|
582
582
|
// close the node right away, do not wait for additional input
|
|
583
|
+
if (this.options.literals && this.options.literals.length) {
|
|
584
|
+
// ImapStream queues a Buffer for every literal marker it
|
|
585
|
+
// extracts, including {0} - consume the queue entry so
|
|
586
|
+
// subsequent literals in the same response stay aligned
|
|
587
|
+
// with their markers instead of shifting by one
|
|
588
|
+
this.currentNode.value = this.options.literals.shift();
|
|
589
|
+
}
|
|
583
590
|
this.currentNode.endPos = this.pos + i;
|
|
584
591
|
this.currentNode.isClosed = true;
|
|
585
592
|
this.currentNode = this.currentNode.parentNode;
|
package/lib/imap-flow.d.ts
CHANGED
|
@@ -207,6 +207,10 @@ export interface ListOptions {
|
|
|
207
207
|
unseen?: boolean;
|
|
208
208
|
/** If true request last known modseq value */
|
|
209
209
|
highestModseq?: boolean;
|
|
210
|
+
/** If true request total mailbox size in octets (requires STATUS=SIZE or IMAP4rev2) */
|
|
211
|
+
size?: boolean;
|
|
212
|
+
/** If true request count of messages with \Deleted flag (requires IMAP4rev2) */
|
|
213
|
+
deleted?: boolean;
|
|
210
214
|
};
|
|
211
215
|
/** Set specific paths as special use folders */
|
|
212
216
|
specialUseHints?: {
|
|
@@ -282,6 +286,10 @@ export interface StatusObject {
|
|
|
282
286
|
unseen?: number;
|
|
283
287
|
/** Last known modseq value (if CONDSTORE extension is enabled) */
|
|
284
288
|
highestModseq?: bigint;
|
|
289
|
+
/** Total size of the mailbox in octets (only if requested and the server supports STATUS=SIZE or IMAP4rev2) */
|
|
290
|
+
size?: number;
|
|
291
|
+
/** Count of messages with \Deleted flag (only if requested and IMAP4rev2 is active) */
|
|
292
|
+
deleted?: number;
|
|
285
293
|
}
|
|
286
294
|
|
|
287
295
|
export type SequenceString = string | number | bigint;
|
|
@@ -493,6 +501,8 @@ export interface FetchMessageObject {
|
|
|
493
501
|
internalDate?: Date | string;
|
|
494
502
|
/** A Map of message body parts where key is requested part identifier and value is a Buffer */
|
|
495
503
|
bodyParts?: Map<string, Buffer>;
|
|
504
|
+
/** Part identifiers from bodyParts that arrived via FETCH BINARY, i.e. with the content-transfer-encoding already decoded by the server */
|
|
505
|
+
binaryParts?: Set<string>;
|
|
496
506
|
/** Requested header lines as Buffer */
|
|
497
507
|
headers?: Buffer;
|
|
498
508
|
/** Account unique ID for this email */
|
|
@@ -755,6 +765,10 @@ export class ImapFlow extends EventEmitter {
|
|
|
755
765
|
uidValidity?: boolean;
|
|
756
766
|
unseen?: boolean;
|
|
757
767
|
highestModseq?: boolean;
|
|
768
|
+
/** Requires STATUS=SIZE or IMAP4rev2 */
|
|
769
|
+
size?: boolean;
|
|
770
|
+
/** Requires IMAP4rev2 */
|
|
771
|
+
deleted?: boolean;
|
|
758
772
|
}
|
|
759
773
|
): Promise<StatusObject>;
|
|
760
774
|
|
package/lib/imap-flow.js
CHANGED
|
@@ -1541,6 +1541,9 @@ class ImapFlow extends EventEmitter {
|
|
|
1541
1541
|
return;
|
|
1542
1542
|
}
|
|
1543
1543
|
this.state = this.states.AUTHENTICATED;
|
|
1544
|
+
// documented contract for the `authenticated` property: `true` when the
|
|
1545
|
+
// connection was authenticated by a PREAUTH greeting (no credentials known)
|
|
1546
|
+
this.authenticated = true;
|
|
1544
1547
|
this.beginSession(err => {
|
|
1545
1548
|
this.log.error({ err, cid: this.id });
|
|
1546
1549
|
this.closeAfter();
|
|
@@ -2258,6 +2261,8 @@ class ImapFlow extends EventEmitter {
|
|
|
2258
2261
|
* @property {Boolean} [statusQuery.uidValidity] if `true` request mailbox `UIDVALIDITY` value
|
|
2259
2262
|
* @property {Boolean} [statusQuery.unseen] if `true` request count of unseen messages
|
|
2260
2263
|
* @property {Boolean} [statusQuery.highestModseq] if `true` request last known modseq value
|
|
2264
|
+
* @property {Boolean} [statusQuery.size] if `true` request total mailbox size in octets (requires STATUS=SIZE or IMAP4rev2)
|
|
2265
|
+
* @property {Boolean} [statusQuery.deleted] if `true` request count of messages with \\Deleted flag (requires IMAP4rev2)
|
|
2261
2266
|
* @property {Object} [specialUseHints] set specific paths as special use folders, this would override special use flags provided from the server
|
|
2262
2267
|
* @property {String} [specialUseHints.sent] Path to "Sent Mail" folder
|
|
2263
2268
|
* @property {String} [specialUseHints.trash] Path to "Trash" folder
|
|
@@ -2462,6 +2467,8 @@ class ImapFlow extends EventEmitter {
|
|
|
2462
2467
|
* @property {BigInt} [uidValidity] Mailbox `UIDVALIDITY` value
|
|
2463
2468
|
* @property {Number} [unseen] Count of unseen messages
|
|
2464
2469
|
* @property {BigInt} [highestModseq] Last known modseq value (if CONDSTORE extension is enabled)
|
|
2470
|
+
* @property {Number} [size] Total size of the mailbox in octets (only if requested and the server supports STATUS=SIZE or IMAP4rev2)
|
|
2471
|
+
* @property {Number} [deleted] Count of messages with \\Deleted flag (only if requested and IMAP4rev2 is active)
|
|
2465
2472
|
*/
|
|
2466
2473
|
|
|
2467
2474
|
/**
|
|
@@ -2475,6 +2482,8 @@ class ImapFlow extends EventEmitter {
|
|
|
2475
2482
|
* @param {Boolean} query.uidValidity if `true` request mailbox `UIDVALIDITY` value
|
|
2476
2483
|
* @param {Boolean} query.unseen if `true` request count of unseen messages
|
|
2477
2484
|
* @param {Boolean} query.highestModseq if `true` request last known modseq value
|
|
2485
|
+
* @param {Boolean} query.size if `true` request total mailbox size in octets (requires STATUS=SIZE or IMAP4rev2)
|
|
2486
|
+
* @param {Boolean} query.deleted if `true` request count of messages with \\Deleted flag (requires IMAP4rev2)
|
|
2478
2487
|
* @returns {Promise<StatusObject>} status of the indicated mailbox
|
|
2479
2488
|
*
|
|
2480
2489
|
* @example
|
|
@@ -2969,6 +2978,7 @@ class ImapFlow extends EventEmitter {
|
|
|
2969
2978
|
* @property {MessageStructureObject} [bodyStructure] message body structure
|
|
2970
2979
|
* @property {Date} [internalDate] message internal date
|
|
2971
2980
|
* @property {Map<string, Buffer>} [bodyParts] a Map of message body parts where key is requested part identifier and value is a Buffer
|
|
2981
|
+
* @property {Set<string>} [binaryParts] part identifiers from `bodyParts` that arrived via FETCH BINARY, i.e. with the content-transfer-encoding already decoded by the server
|
|
2972
2982
|
* @property {Buffer} [headers] Requested header lines as Buffer
|
|
2973
2983
|
*/
|
|
2974
2984
|
|
|
@@ -3403,7 +3413,11 @@ class ImapFlow extends EventEmitter {
|
|
|
3403
3413
|
// 4. Byte limiter (enforces maxBytes cap)
|
|
3404
3414
|
// `stream` is the head of the pipeline (where raw chunks are written),
|
|
3405
3415
|
// `output` is the tail (what the caller reads from).
|
|
3406
|
-
|
|
3416
|
+
// Parts that arrived via FETCH BINARY (response.binaryParts) are already
|
|
3417
|
+
// decoded by the server - decoding again would corrupt the data, so stage 1
|
|
3418
|
+
// is skipped for them.
|
|
3419
|
+
let clientEncoding = response.binaryParts && response.binaryParts.has(part) ? false : meta.encoding;
|
|
3420
|
+
switch (clientEncoding) {
|
|
3407
3421
|
case 'base64':
|
|
3408
3422
|
output = stream = new libbase64.Decoder();
|
|
3409
3423
|
break;
|
|
@@ -3713,7 +3727,10 @@ class ImapFlow extends EventEmitter {
|
|
|
3713
3727
|
for (let part of Object.keys(data)) {
|
|
3714
3728
|
let meta = data[part].meta;
|
|
3715
3729
|
|
|
3716
|
-
|
|
3730
|
+
// parts that arrived via FETCH BINARY (response.binaryParts) are already
|
|
3731
|
+
// decoded by the server - decoding again would corrupt the data
|
|
3732
|
+
let clientEncoding = response.binaryParts && response.binaryParts.has(part) ? false : meta.encoding;
|
|
3733
|
+
switch (clientEncoding) {
|
|
3717
3734
|
case 'base64':
|
|
3718
3735
|
data[part].content = data[part].content ? libbase64.decode(data[part].content.toString()) : null;
|
|
3719
3736
|
break;
|
package/lib/tools.js
CHANGED
|
@@ -19,9 +19,10 @@ const EXPANDED_RANGE_LIMIT = 0x1000000;
|
|
|
19
19
|
// Extensions that RFC 9051 (IMAP4rev2) folds into the base protocol (Appendix E).
|
|
20
20
|
// When IMAP4rev2 is active, these are available even without their own capability
|
|
21
21
|
// token. BINARY is deliberately excluded - RFC 9051 only folds in the FETCH side,
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
22
|
+
// which fetch.js handles with its own isRev2Active check, while the APPEND side
|
|
23
|
+
// stays gated on the BINARY token. The set mirrors the Appendix E list in full,
|
|
24
|
+
// including entries no call site consults yet, so any future capability check
|
|
25
|
+
// gets the rev2 folding for free.
|
|
25
26
|
const IMAP4REV2_FOLDED_CAPABILITIES = new Set([
|
|
26
27
|
'ENABLE',
|
|
27
28
|
'ESEARCH',
|
|
@@ -115,6 +116,24 @@ const tools = {
|
|
|
115
116
|
attributes.push({ type: 'ATOM', value: key.toUpperCase() });
|
|
116
117
|
}
|
|
117
118
|
break;
|
|
119
|
+
|
|
120
|
+
case 'SIZE':
|
|
121
|
+
// STATUS SIZE requires the STATUS=SIZE extension (RFC 8438), which
|
|
122
|
+
// RFC 9051 folds into base IMAP4rev2
|
|
123
|
+
if (tools.hasCapability(connection, 'STATUS=SIZE')) {
|
|
124
|
+
attributes.push({ type: 'ATOM', value: key.toUpperCase() });
|
|
125
|
+
}
|
|
126
|
+
break;
|
|
127
|
+
|
|
128
|
+
case 'DELETED':
|
|
129
|
+
// STATUS DELETED is a base IMAP4rev2 addition (RFC 9051 Appendix E
|
|
130
|
+
// item 3) with no standalone capability - requesting it from a plain
|
|
131
|
+
// rev1 server would get the whole STATUS request rejected. RFC 9208
|
|
132
|
+
// additionally makes it mandatory when QUOTA=RES-MESSAGE is advertised.
|
|
133
|
+
if (tools.isRev2Active(connection) || connection.capabilities.has('QUOTA=RES-MESSAGE')) {
|
|
134
|
+
attributes.push({ type: 'ATOM', value: key.toUpperCase() });
|
|
135
|
+
}
|
|
136
|
+
break;
|
|
118
137
|
}
|
|
119
138
|
});
|
|
120
139
|
|
|
@@ -591,6 +610,19 @@ const tools = {
|
|
|
591
610
|
map.bodyParts = new Map();
|
|
592
611
|
}
|
|
593
612
|
map.bodyParts.set(partKey, value);
|
|
613
|
+
|
|
614
|
+
if (match[1].toLowerCase() === 'binary') {
|
|
615
|
+
// The part arrived via FETCH BINARY (RFC 3516, FETCH side folded
|
|
616
|
+
// into IMAP4rev2), so the server has already removed the
|
|
617
|
+
// content-transfer-encoding - consumers must not decode it again.
|
|
618
|
+
// Recorded from the actual response, not predicted from the
|
|
619
|
+
// request, so it stays correct even if a server answers a BINARY
|
|
620
|
+
// request with a BODY response or vice versa.
|
|
621
|
+
if (!map.binaryParts) {
|
|
622
|
+
map.binaryParts = new Set();
|
|
623
|
+
}
|
|
624
|
+
map.binaryParts.add(partKey);
|
|
625
|
+
}
|
|
594
626
|
break;
|
|
595
627
|
}
|
|
596
628
|
break;
|