imapkit 0.0.0-stage → 4.0.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/LICENSE +16 -0
- package/README.md +608 -2
- package/bin/help.txt +98 -0
- package/bin/imapkit.js +108 -0
- package/cert/server.crt +20 -0
- package/cert/server.key +28 -0
- package/lib/addressparser.js +283 -0
- package/lib/arguments.js +112 -0
- package/lib/bodystructure.js +149 -0
- package/lib/command-states.js +109 -0
- package/lib/commands/append.js +313 -0
- package/lib/commands/capability.js +47 -0
- package/lib/commands/check.js +21 -0
- package/lib/commands/close.js +30 -0
- package/lib/commands/copy.js +115 -0
- package/lib/commands/create.js +52 -0
- package/lib/commands/delete.js +64 -0
- package/lib/commands/examine.js +7 -0
- package/lib/commands/expunge.js +27 -0
- package/lib/commands/fetch.js +229 -0
- package/lib/commands/handlers/fetch.js +209 -0
- package/lib/commands/handlers/flags.js +42 -0
- package/lib/commands/handlers/search.js +519 -0
- package/lib/commands/handlers/status.js +85 -0
- package/lib/commands/handlers/store.js +127 -0
- package/lib/commands/list.js +100 -0
- package/lib/commands/login.js +67 -0
- package/lib/commands/logout.js +41 -0
- package/lib/commands/lsub.js +87 -0
- package/lib/commands/noop.js +21 -0
- package/lib/commands/rename.js +102 -0
- package/lib/commands/search.js +76 -0
- package/lib/commands/select.js +289 -0
- package/lib/commands/status.js +63 -0
- package/lib/commands/store.js +151 -0
- package/lib/commands/subscribe.js +53 -0
- package/lib/commands/uid copy.js +7 -0
- package/lib/commands/uid fetch.js +5 -0
- package/lib/commands/uid search.js +5 -0
- package/lib/commands/uid store.js +5 -0
- package/lib/commands/unsubscribe.js +50 -0
- package/lib/dates.js +123 -0
- package/lib/deflate-layer.js +232 -0
- package/lib/envelope.js +82 -0
- package/lib/esearch.js +208 -0
- package/lib/framing.js +102 -0
- package/lib/list-extensions.js +36 -0
- package/lib/load-plugins.js +109 -0
- package/lib/mailbox-name.js +133 -0
- package/lib/mimeparser.js +778 -0
- package/lib/mock-client.js +233 -0
- package/lib/numbers.js +52 -0
- package/lib/plugins/acl.js +964 -0
- package/lib/plugins/appendlimit.js +83 -0
- package/lib/plugins/auth-plain.js +94 -0
- package/lib/plugins/binary.js +256 -0
- package/lib/plugins/catenate.js +253 -0
- package/lib/plugins/compress.js +76 -0
- package/lib/plugins/condstore.js +563 -0
- package/lib/plugins/context-search.js +321 -0
- package/lib/plugins/context-sort.js +19 -0
- package/lib/plugins/create-special-use.js +108 -0
- package/lib/plugins/enable.js +155 -0
- package/lib/plugins/esearch.js +156 -0
- package/lib/plugins/esort.js +60 -0
- package/lib/plugins/id.js +138 -0
- package/lib/plugins/idle.js +105 -0
- package/lib/plugins/imap4rev2.js +202 -0
- package/lib/plugins/list-extended.js +258 -0
- package/lib/plugins/list-status.js +31 -0
- package/lib/plugins/literalminus.js +20 -0
- package/lib/plugins/literalplus.js +18 -0
- package/lib/plugins/logindisabled.js +50 -0
- package/lib/plugins/messagelimit.js +234 -0
- package/lib/plugins/metadata-server.js +13 -0
- package/lib/plugins/metadata.js +475 -0
- package/lib/plugins/move.js +110 -0
- package/lib/plugins/multiappend.js +26 -0
- package/lib/plugins/multisearch.js +269 -0
- package/lib/plugins/namespace.js +67 -0
- package/lib/plugins/notify.js +654 -0
- package/lib/plugins/oauthbearer.js +217 -0
- package/lib/plugins/objectid.js +243 -0
- package/lib/plugins/partial.js +68 -0
- package/lib/plugins/preview.js +400 -0
- package/lib/plugins/qresync.js +525 -0
- package/lib/plugins/quota.js +285 -0
- package/lib/plugins/replace.js +145 -0
- package/lib/plugins/sasl-ir.js +12 -0
- package/lib/plugins/savedate.js +59 -0
- package/lib/plugins/savelimit.js +18 -0
- package/lib/plugins/searchres.js +82 -0
- package/lib/plugins/sort-display.js +23 -0
- package/lib/plugins/sort.js +132 -0
- package/lib/plugins/special-use.js +95 -0
- package/lib/plugins/starttls.js +57 -0
- package/lib/plugins/status-size.js +19 -0
- package/lib/plugins/thread-orderedsubject.js +16 -0
- package/lib/plugins/thread-references.js +16 -0
- package/lib/plugins/uidonly.js +135 -0
- package/lib/plugins/uidplus.js +124 -0
- package/lib/plugins/unauthenticate.js +28 -0
- package/lib/plugins/unselect.js +36 -0
- package/lib/plugins/utf8-accept.js +68 -0
- package/lib/plugins/x-gm-ext-1.js +456 -0
- package/lib/plugins/xoauth2.js +188 -0
- package/lib/plugins/xtoybird.js +282 -0
- package/lib/server.js +2880 -0
- package/lib/smtp-listener.js +51 -0
- package/lib/sorting.js +373 -0
- package/lib/threading.js +357 -0
- package/lib/utf8-session.js +123 -0
- package/lib/vanished.js +57 -0
- package/package.json +61 -5
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { getPendingTarget } = require('../commands/append');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @help Adds APPENDLIMIT [RFC7889] capability
|
|
7
|
+
* @help Server option "appendLimit" sets the limit in octets for all
|
|
8
|
+
* @help mailboxes, a mailbox in storage can set its own "appendLimit"
|
|
9
|
+
* @help (null for no limit)
|
|
10
|
+
*
|
|
11
|
+
* APPENDLIMIT: https://www.rfc-editor.org/rfc/rfc7889
|
|
12
|
+
*
|
|
13
|
+
* The server option "appendLimit" (octets) sets a limit for all mailboxes, advertised as
|
|
14
|
+
* APPENDLIMIT=<n>. A mailbox in the storage can set its own "appendLimit" (a number, or null
|
|
15
|
+
* for no limit), then the capability is a plain APPENDLIMIT and clients read the limits with
|
|
16
|
+
* STATUS (APPENDLIMIT). A larger APPEND or REPLACE fails with NO [TOOBIG], a synchronizing
|
|
17
|
+
* literal is refused before it is sent.
|
|
18
|
+
*/
|
|
19
|
+
module.exports = function (server) {
|
|
20
|
+
const serverLimit = 'appendLimit' in server.options ? server.options.appendLimit : null;
|
|
21
|
+
if (serverLimit !== null && !isValidLimit(serverLimit)) {
|
|
22
|
+
throw new TypeError('Invalid appendLimit option, expecting a non-negative integer');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const hasOwnLimit = mailbox => Object.hasOwn(mailbox, 'appendLimit');
|
|
26
|
+
const hasMailboxLimits = () => Object.values(server.folderCache).some(hasOwnLimit);
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {Object} mailbox Mailbox object
|
|
30
|
+
* @return {Number|null} limit in octets, null if there is no limit
|
|
31
|
+
*/
|
|
32
|
+
const getLimit = mailbox => {
|
|
33
|
+
if (!hasOwnLimit(mailbox)) {
|
|
34
|
+
return serverLimit;
|
|
35
|
+
}
|
|
36
|
+
return isValidLimit(mailbox.appendLimit) ? mailbox.appendLimit : null;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// RFC 7889 section 5: an APPENDLIMIT of 0 means that nothing can be appended
|
|
40
|
+
const isTooBig = (mailbox, size) => {
|
|
41
|
+
const limit = getLimit(mailbox);
|
|
42
|
+
return limit !== null && (size > limit || limit === 0);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// RFC 7889 section 2: APPENDLIMIT=<n> for the same limit in every mailbox, otherwise a plain
|
|
46
|
+
// APPENDLIMIT and the limits come from STATUS
|
|
47
|
+
if (serverLimit !== null) {
|
|
48
|
+
server.registerCapability('APPENDLIMIT=' + serverLimit, () => !hasMailboxLimits());
|
|
49
|
+
}
|
|
50
|
+
server.registerCapability('APPENDLIMIT', () => serverLimit === null || hasMailboxLimits());
|
|
51
|
+
|
|
52
|
+
// RFC 7889 section 4: a message over the limit is refused with TOOBIG
|
|
53
|
+
server.appendChecks.push((connection, mailbox, messages, options) => {
|
|
54
|
+
// COPY and MOVE are not uploads (RFC 7889 section 1)
|
|
55
|
+
if (!options.command) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
if (messages.some(message => isTooBig(mailbox, message.raw.length))) {
|
|
59
|
+
return { code: 'TOOBIG', text: 'Message exceeds the APPENDLIMIT of the mailbox' };
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Refuse the literal of a message that is too large before the client sends it, by not
|
|
65
|
+
// sending a continuation request (RFC 3502 section 6.3.11 example A005)
|
|
66
|
+
server.literalFilters.push((connection, command, line, literalSize) => {
|
|
67
|
+
const path = getPendingTarget(connection, command, line);
|
|
68
|
+
const mailbox = path !== false && server.getMailbox(path);
|
|
69
|
+
if (mailbox && isTooBig(mailbox, literalSize)) {
|
|
70
|
+
return { command: 'NO', code: 'TOOBIG', text: 'Message exceeds the APPENDLIMIT of the mailbox' };
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// RFC 7889 section 3.1: status-att-val =/ "APPENDLIMIT" SP (number / nil)
|
|
76
|
+
server.allowedStatus.push('APPENDLIMIT');
|
|
77
|
+
// also used by the STATUS return option of LIST-STATUS (RFC 7889 section 3.2)
|
|
78
|
+
server.statusHandlers.APPENDLIMIT = (connection, mailbox) => getLimit(mailbox);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
function isValidLimit(value) {
|
|
82
|
+
return Number.isSafeInteger(value) && value >= 0;
|
|
83
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @help Adds AUTH=PLAIN capability
|
|
5
|
+
* @help Supports SALS-IR [RFC4959] as well
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const utf8 = new TextDecoder('utf-8', { fatal: true });
|
|
9
|
+
|
|
10
|
+
module.exports = function (server) {
|
|
11
|
+
// Register AUTH=PLAIN capability for non authenticated state
|
|
12
|
+
server.registerCapability('AUTH=PLAIN', connection => {
|
|
13
|
+
return connection.state === 'Not Authenticated';
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// Validates a base64 encoded "authzid NUL authcid NUL passwd" message (RFC 4616) and logs the user in
|
|
17
|
+
const authenticate = (connection, parsed, data, input) => {
|
|
18
|
+
const decoded = connection.decodeSaslResponse(input);
|
|
19
|
+
if (!decoded) {
|
|
20
|
+
return connection.sendStatus(parsed, data, 'BAD', 'Invalid base64 in SASL response', false, 'AUTHENTICATE PLAIN FAILED');
|
|
21
|
+
}
|
|
22
|
+
// RFC 4616 section 2: the message is UTF-8, user names are unicode strings like the keys of `users`
|
|
23
|
+
let message;
|
|
24
|
+
try {
|
|
25
|
+
message = utf8.decode(decoded);
|
|
26
|
+
} catch {
|
|
27
|
+
return connection.sendStatus(parsed, data, 'BAD', 'Invalid UTF-8 in SASL PLAIN message', false, 'AUTHENTICATE PLAIN FAILED');
|
|
28
|
+
}
|
|
29
|
+
const parts = message.split('\x00');
|
|
30
|
+
const authzid = parts[0] || '';
|
|
31
|
+
const username = parts[1] || '';
|
|
32
|
+
const password = parts[2] || '';
|
|
33
|
+
const user = connection.server.getUser(username);
|
|
34
|
+
|
|
35
|
+
if (parts.length !== 3 || !user || user.password !== password) {
|
|
36
|
+
// RFC 5530 section 3: unknown user or bad password
|
|
37
|
+
return connection.sendStatus(parsed, data, 'NO', 'Login failed: authentication failure', 'AUTHENTICATIONFAILED', 'AUTHENTICATE PLAIN FAILED');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (authzid && authzid !== username) {
|
|
41
|
+
// Acting as another user is not supported, authzid must be empty or the same as authcid. The credentials
|
|
42
|
+
// were fine, so this is AUTHORIZATIONFAILED (RFC 5530 section 3, RFC 9051 section 7.1)
|
|
43
|
+
return connection.sendStatus(parsed, data, 'NO', 'Can not act as ' + authzid, 'AUTHORIZATIONFAILED', 'AUTHENTICATE PLAIN FAILED');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
connection.state = 'Authenticated';
|
|
47
|
+
connection.username = username;
|
|
48
|
+
connection.sendStatus(parsed, data, 'OK', 'User logged in', false, 'AUTHENTICATE PLAIN SUCCESS');
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
server.setCommandHandler('AUTHENTICATE PLAIN', (connection, parsed, data, callback) => {
|
|
52
|
+
// If this is the old style api, send + and wait for password
|
|
53
|
+
if (!parsed.attributes) {
|
|
54
|
+
// Temporarily redirect client input to this function
|
|
55
|
+
connection.inputHandler = function (str) {
|
|
56
|
+
// Stop listening to any other user input
|
|
57
|
+
connection.inputHandler = false;
|
|
58
|
+
|
|
59
|
+
if (str === '*') {
|
|
60
|
+
// Client cancelled the exchange (RFC 3501 section 6.2.2)
|
|
61
|
+
return connection.sendStatus(parsed, data, 'BAD', 'Authentication cancelled', false, 'AUTHENTICATE PLAIN FAILED');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
authenticate(connection, parsed, data, str);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Send an empty continuation request to the client
|
|
68
|
+
connection.write('+ \r\n');
|
|
69
|
+
} else if (
|
|
70
|
+
parsed.attributes.length === 1 &&
|
|
71
|
+
// second argument must be Base64 string as ATOM
|
|
72
|
+
parsed.attributes[0].type === 'ATOM'
|
|
73
|
+
) {
|
|
74
|
+
if (!server.capabilities['SASL-IR'] || !server.capabilities['SASL-IR'](connection)) {
|
|
75
|
+
connection.sendStatus(
|
|
76
|
+
parsed,
|
|
77
|
+
data,
|
|
78
|
+
'BAD',
|
|
79
|
+
'SASL-IR must be enabled to send Initial Response with the request',
|
|
80
|
+
false,
|
|
81
|
+
'AUTHENTICATE PLAIN FAILED'
|
|
82
|
+
);
|
|
83
|
+
return callback();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
authenticate(connection, parsed, data, parsed.attributes[0].value);
|
|
87
|
+
} else {
|
|
88
|
+
// Not correct AUTH=PLAIN
|
|
89
|
+
connection.sendStatus(parsed, data, 'BAD', 'Invalid attributes for AUTHENTICATE PLAIN', false, 'AUTHENTICATE PLAIN FAILED');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return callback();
|
|
93
|
+
});
|
|
94
|
+
};
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { getMessageData, resolveNode, embeddedMessage, render, parseTree, normalizeLineBreaks, IDENTITY_ENCODINGS } = require('../mimeparser');
|
|
4
|
+
const { applyPartial } = require('../commands/handlers/fetch');
|
|
5
|
+
const { appendError } = require('../commands/append');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @help Adds BINARY [RFC3516] capability
|
|
9
|
+
*
|
|
10
|
+
* BINARY: https://www.rfc-editor.org/rfc/rfc3516
|
|
11
|
+
*
|
|
12
|
+
* Additional FETCH items:
|
|
13
|
+
* - BINARY[<part>]<<partial>>, BINARY.PEEK[<part>]<<partial>> and BINARY.SIZE[<part>]
|
|
14
|
+
*
|
|
15
|
+
* APPEND (and REPLACE) accept messages as a literal8 (~{n}), binary parts are stored base64 encoded
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
// RFC 3516 section 7: section-binary = "[" [section-part] "]", section-part = nz-number *("." nz-number)
|
|
19
|
+
const SECTION_PART = /^[1-9]\d*(?:\.[1-9]\d*)*$/;
|
|
20
|
+
|
|
21
|
+
// decoded leaf parts, the tree of a message is replaced when its source changes
|
|
22
|
+
const decodedCache = new WeakMap();
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Creates an error that fails a command with NO [UNKNOWN-CTE] (RFC 3516 sections 4.3 and 4.4)
|
|
26
|
+
*
|
|
27
|
+
* @param {String} message Human readable text
|
|
28
|
+
* @return {Error}
|
|
29
|
+
*/
|
|
30
|
+
function unknownCte(message) {
|
|
31
|
+
const err = new Error(message);
|
|
32
|
+
err.imapResponse = 'NO';
|
|
33
|
+
err.code = 'UNKNOWN-CTE';
|
|
34
|
+
return err;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Returns the media type of a tree node, e.g. "text/plain"
|
|
39
|
+
*/
|
|
40
|
+
function mediaType(node) {
|
|
41
|
+
const contentType = node.parsedHeader['content-type'];
|
|
42
|
+
return ((contentType && contentType.value) || '').toLowerCase();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Returns the lower case content transfer encoding of a node, 7bit if not set (RFC 2045 section 6.1)
|
|
47
|
+
*/
|
|
48
|
+
function transferEncoding(node) {
|
|
49
|
+
return (node.parsedHeader['content-transfer-encoding'] || '7bit').toString().trim().toLowerCase();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* RFC 9051 section 6.4.5: BINARY applies to leaf body parts only, not to multipart/*, message/rfc822
|
|
54
|
+
* or message/global parts. IMAP4rev1 describes message/global as a basic part (RFC 3501 body-type-basic)
|
|
55
|
+
*
|
|
56
|
+
* @param {Object} node Tree node
|
|
57
|
+
* @param {Boolean} global message/global is not a leaf, see IMAPConnection#messageGlobal
|
|
58
|
+
*/
|
|
59
|
+
function isLeaf(node, global) {
|
|
60
|
+
const type = mediaType(node);
|
|
61
|
+
return !node.boundary && !/^multipart\//.test(type) && type !== 'message/rfc822' && (!global || type !== 'message/global');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Decodes base64 data. RFC 2045 section 6.8: characters outside the base64 alphabet are ignored, and
|
|
66
|
+
* "=" marks the end of the data
|
|
67
|
+
*
|
|
68
|
+
* @param {String} str Encoded data
|
|
69
|
+
* @return {String} Decoded octets as a binary string
|
|
70
|
+
*/
|
|
71
|
+
function decodeBase64(str) {
|
|
72
|
+
let data = str.replace(/[^A-Za-z0-9+/=]/g, '').split('=')[0];
|
|
73
|
+
if (data.length % 4 === 1) {
|
|
74
|
+
// a single leftover character does not hold a whole octet
|
|
75
|
+
data = data.slice(0, -1);
|
|
76
|
+
}
|
|
77
|
+
return Buffer.from(data, 'base64').toString('binary');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Decodes quoted-printable data (RFC 2045 section 6.7). Trailing white space of a line is removed
|
|
82
|
+
* (rule 3), a line that ends with "=" continues on the next line (rule 5), and an "=" that is not
|
|
83
|
+
* followed by two hex digits is kept as it is, as the RFC suggests for robust implementations
|
|
84
|
+
*
|
|
85
|
+
* @param {String} str Encoded data with CRLF line breaks
|
|
86
|
+
* @return {String} Decoded octets as a binary string
|
|
87
|
+
*/
|
|
88
|
+
function decodeQuotedPrintable(str) {
|
|
89
|
+
return str
|
|
90
|
+
.replace(/[ \t]+(?=\r\n|$)/g, '')
|
|
91
|
+
.replace(/=(?:\r\n|$)/g, '')
|
|
92
|
+
.replace(/=([0-9A-Fa-f]{2})/g, (match, hex) => String.fromCharCode(parseInt(hex, 16)));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Returns the content of a BINARY section with the content transfer encoding removed
|
|
97
|
+
*
|
|
98
|
+
* @param {Object} connection IMAP connection
|
|
99
|
+
* @param {Object} message Message object
|
|
100
|
+
* @param {Object} query Parsed fetch item
|
|
101
|
+
* @return {String} Decoded section as a binary string
|
|
102
|
+
*/
|
|
103
|
+
function getDecodedSection(connection, message, query) {
|
|
104
|
+
const key = query.value.toUpperCase();
|
|
105
|
+
const section = query.section;
|
|
106
|
+
|
|
107
|
+
if (!section) {
|
|
108
|
+
throw new Error(key + ' requires a section');
|
|
109
|
+
}
|
|
110
|
+
if (!section.length) {
|
|
111
|
+
// the grammar allows BINARY[], but the whole message is not a leaf body part
|
|
112
|
+
throw new Error(key + '[] is not allowed, BINARY applies to leaf body parts only (RFC 9051 section 6.4.5)');
|
|
113
|
+
}
|
|
114
|
+
if (section.length !== 1 || section[0].type !== 'ATOM' || !SECTION_PART.test(section[0].value)) {
|
|
115
|
+
throw new Error('Invalid ' + key + ' section, expecting a part number (RFC 3516 section 7)');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const node = resolveNode(getMessageData(message).tree, section[0].value, connection.messageGlobal);
|
|
119
|
+
if (!node) {
|
|
120
|
+
// like BODY[<section>], a part that does not exist is empty
|
|
121
|
+
return '';
|
|
122
|
+
}
|
|
123
|
+
if (!isLeaf(node, connection.messageGlobal)) {
|
|
124
|
+
throw new Error(key + '[' + section[0].value + '] is not a leaf body part (RFC 9051 section 6.4.5)');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!decodedCache.has(node)) {
|
|
128
|
+
decodedCache.set(node, decodeNode(node));
|
|
129
|
+
}
|
|
130
|
+
return decodedCache.get(node);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Removes the content transfer encoding of a leaf part
|
|
135
|
+
*
|
|
136
|
+
* @param {Object} node Tree node
|
|
137
|
+
* @return {String} Decoded body as a binary string
|
|
138
|
+
*/
|
|
139
|
+
function decodeNode(node) {
|
|
140
|
+
const body = render(node, true);
|
|
141
|
+
const encoding = transferEncoding(node);
|
|
142
|
+
let decoded;
|
|
143
|
+
if (IDENTITY_ENCODINGS.includes(encoding)) {
|
|
144
|
+
decoded = body;
|
|
145
|
+
} else if (encoding === 'base64') {
|
|
146
|
+
decoded = decodeBase64(body);
|
|
147
|
+
} else if (encoding === 'quoted-printable') {
|
|
148
|
+
decoded = decodeQuotedPrintable(body);
|
|
149
|
+
} else {
|
|
150
|
+
throw unknownCte('Unknown Content-Transfer-Encoding ' + JSON.stringify(encoding).replace(/[^\x20-\x7e]/g, '?'));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// RFC 3516 section 6: textual sections are sent with CRLF line breaks
|
|
154
|
+
return /^text\//.test(mediaType(node)) ? normalizeLineBreaks(decoded) : decoded;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* BINARY[<section>]<<partial>> (RFC 3516 section 4.2)
|
|
159
|
+
*/
|
|
160
|
+
function fetchBinary(connection, message, query) {
|
|
161
|
+
// RFC 3516 section 4.2: the range applies to the decoded data
|
|
162
|
+
const value = applyPartial(getDecodedSection(connection, message, query), query.partial);
|
|
163
|
+
|
|
164
|
+
// RFC 3516 section 4.3: data without NUL is sent as a string, a literal8 only when it is needed
|
|
165
|
+
return {
|
|
166
|
+
type: value.indexOf('\x00') >= 0 ? 'LITERAL8' : 'LITERAL',
|
|
167
|
+
value
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
// BINARY sets \Seen like BODY[<section>] does
|
|
171
|
+
fetchBinary.setsSeen = true;
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* BINARY.SIZE[<section>] (RFC 3516 section 4.2), the size of the decoded section
|
|
175
|
+
*/
|
|
176
|
+
function fetchBinarySize(connection, message, query) {
|
|
177
|
+
if (query.partial) {
|
|
178
|
+
// RFC 3516 section 7: "BINARY.SIZE" section-binary, without a partial
|
|
179
|
+
throw new Error('BINARY.SIZE does not take a partial range');
|
|
180
|
+
}
|
|
181
|
+
return getDecodedSection(connection, message, query).length;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Base64 encodes the binary parts of a message that was appended as a literal8, as the IMAP4rev1
|
|
186
|
+
* part of the protocol can not carry them (RFC 3516 sections 4.4 and 6): parts with the binary
|
|
187
|
+
* encoding, and parts with an identity encoding that contain NUL octets anyway. Any other NUL octet
|
|
188
|
+
* (in a header, or in a part with another encoding) can not be stored.
|
|
189
|
+
*
|
|
190
|
+
* @param {String} raw Message as a binary string
|
|
191
|
+
* @return {String} Message to store
|
|
192
|
+
*/
|
|
193
|
+
function storeBinaryMessage(raw) {
|
|
194
|
+
const tree = parseTree(raw);
|
|
195
|
+
|
|
196
|
+
// returns true if the node or anything in it was converted
|
|
197
|
+
const convert = node => {
|
|
198
|
+
if (node.childNodes) {
|
|
199
|
+
// every part is converted, so no short circuit
|
|
200
|
+
return node.childNodes.map(convert).includes(true);
|
|
201
|
+
}
|
|
202
|
+
const message = embeddedMessage(node, true);
|
|
203
|
+
if (message) {
|
|
204
|
+
// RFC 2046 section 5.2.1: a message/rfc822 (or message/global) part keeps its identity encoding, its parts are converted
|
|
205
|
+
if (!convert(message)) {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
node.body = render(message);
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
const encoding = transferEncoding(node);
|
|
212
|
+
if (!isLeaf(node, true) || !node.body || !(encoding === 'binary' || (IDENTITY_ENCODINGS.includes(encoding) && node.body.indexOf('\x00') >= 0))) {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
// RFC 2045 section 6.8: encoded lines are at most 76 characters long
|
|
216
|
+
node.body = Buffer.from(node.body, 'binary')
|
|
217
|
+
.toString('base64')
|
|
218
|
+
.match(/.{1,76}/g)
|
|
219
|
+
.join('\r\n');
|
|
220
|
+
node.header = node.header.filter(line => !/^content-transfer-encoding\s*:/i.test(line)).concat('Content-Transfer-Encoding: base64');
|
|
221
|
+
return true;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const result = convert(tree) ? render(tree) : raw;
|
|
225
|
+
if (result.indexOf('\x00') >= 0) {
|
|
226
|
+
throw unknownCte('NUL octets are only allowed in the body of a part with the binary, 7bit or 8bit encoding');
|
|
227
|
+
}
|
|
228
|
+
return result;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
module.exports = function (server) {
|
|
232
|
+
server.registerCapability('BINARY');
|
|
233
|
+
|
|
234
|
+
// RFC 3516 section 7: literal8 and the BINARY fetch items with a section and a partial range
|
|
235
|
+
server.parserOptions.literal8 = true;
|
|
236
|
+
server.parserOptions.allowSection.push('BINARY', 'BINARY.PEEK', 'BINARY.SIZE');
|
|
237
|
+
|
|
238
|
+
server.fetchHandlers.BINARY = fetchBinary;
|
|
239
|
+
// the same without setting \Seen
|
|
240
|
+
server.fetchHandlers['BINARY.PEEK'] = (connection, message, query) => fetchBinary(connection, message, query);
|
|
241
|
+
server.fetchHandlers['BINARY.SIZE'] = fetchBinarySize;
|
|
242
|
+
|
|
243
|
+
// RFC 3516 section 4.4: APPEND (and REPLACE, MULTIAPPEND) take a message as a literal8, the commands
|
|
244
|
+
// allow it with the literal8: 'BINARY' option
|
|
245
|
+
server.appendLiteral8 = raw => {
|
|
246
|
+
try {
|
|
247
|
+
return storeBinaryMessage(raw);
|
|
248
|
+
} catch (err) {
|
|
249
|
+
throw appendError(err.message, err.code);
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
module.exports.decodeBase64 = decodeBase64;
|
|
255
|
+
module.exports.decodeQuotedPrintable = decodeQuotedPrintable;
|
|
256
|
+
module.exports.storeBinaryMessage = storeBinaryMessage;
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const imapHandler = require('imap-handler');
|
|
4
|
+
const { appendError, badArgument } = require('../commands/append');
|
|
5
|
+
const fetchHandlers = require('../commands/handlers/fetch');
|
|
6
|
+
const { getMessageData, resolveNode } = require('../mimeparser');
|
|
7
|
+
const { encodeMailboxName } = require('../mailbox-name');
|
|
8
|
+
const { isNumber, isNzNumber } = require('../numbers');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @help Adds CATENATE [RFC4469] and URL-PARTIAL [RFC5550] capabilities
|
|
12
|
+
* @help Only absolute-path URLs are accepted, eg. "/INBOX/;UID=1/;SECTION=1"
|
|
13
|
+
*
|
|
14
|
+
* CATENATE: https://www.rfc-editor.org/rfc/rfc4469
|
|
15
|
+
*
|
|
16
|
+
* APPEND (and REPLACE) can build a message from literals and IMAP URLs of messages or
|
|
17
|
+
* message parts on this server. Only absolute-path URLs are accepted, for example
|
|
18
|
+
* "/INBOX;UIDVALIDITY=1/;UID=2/;SECTION=1.MIME/;PARTIAL=0.100" (RFC 5092). Other URLs,
|
|
19
|
+
* and URLs that do not resolve, fail with NO [BADURL]. A message larger than the
|
|
20
|
+
* literal size limit fails with NO [TOOBIG].
|
|
21
|
+
*/
|
|
22
|
+
module.exports = function (server) {
|
|
23
|
+
server.registerCapability('CATENATE');
|
|
24
|
+
// RFC 5550 section 5.7.1: ;PARTIAL= is supported in the URLs of CATENATE
|
|
25
|
+
server.registerCapability('URL-PARTIAL');
|
|
26
|
+
|
|
27
|
+
// RFC 4469 section 5: append-data =/ "CATENATE" SP "(" cat-part *(SP cat-part) ")"
|
|
28
|
+
// cat-part = text-literal / url, text-literal = "TEXT" SP literal, url = "URL" SP astring
|
|
29
|
+
server.appendDataHandlers.CATENATE = (connection, list) => {
|
|
30
|
+
if (!Array.isArray(list) || !list.length || list.length % 2) {
|
|
31
|
+
throw badArgument('CATENATE expects a list of TEXT literals and URLs');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const parts = [];
|
|
35
|
+
for (let i = 0; i < list.length; i += 2) {
|
|
36
|
+
const label = list[i] && list[i].type === 'ATOM' ? String(list[i].value).toUpperCase() : '';
|
|
37
|
+
const value = list[i + 1];
|
|
38
|
+
if (label === 'TEXT' && value && value.type === 'LITERAL') {
|
|
39
|
+
parts.push({ text: value.value });
|
|
40
|
+
} else if (label === 'URL' && value && ['ATOM', 'STRING', 'LITERAL'].indexOf(value.type) >= 0) {
|
|
41
|
+
parts.push({ url: value.value });
|
|
42
|
+
} else {
|
|
43
|
+
throw badArgument('Invalid CATENATE part ' + (i / 2 + 1));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// the message is built once the target mailbox is known to exist
|
|
48
|
+
return () => {
|
|
49
|
+
const maxSize = connection.getMaxLiteralSize();
|
|
50
|
+
let raw = '';
|
|
51
|
+
for (const part of parts) {
|
|
52
|
+
raw += 'text' in part ? part.text : resolveUrl(connection, part.url);
|
|
53
|
+
// RFC 4469 section 4.2: TOOBIG for a message over the server's size limit
|
|
54
|
+
if (raw.length > maxSize) {
|
|
55
|
+
throw appendError('Catenated message is too large', 'TOOBIG');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return raw;
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// RFC 5092 section 11: bchar (achar / ":" / "@" / "/"), and ";" that starts the URL parameters
|
|
64
|
+
const URL_CHARS = /^[A-Za-z0-9\-._~!$'()*+,&=:@/;%]*$/;
|
|
65
|
+
|
|
66
|
+
// RFC 5092 section 11: iabsolute-path = "/" imessagepart, where
|
|
67
|
+
// imessagepart = enc-mailbox [uidvalidity] iuid [isection] [ipartial] (URLAUTH is not supported)
|
|
68
|
+
const MESSAGE_PART = /^\/([^;]+?)(?:;UIDVALIDITY=([^/;]*))?\/;UID=([^/;]*)(?:\/;SECTION=([^/;]*))?(?:\/;PARTIAL=([^/;]*))?$/i;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Creates a BADURL error (RFC 4469 section 4.1). url-resp-text is any TEXT-CHAR except "]", so other
|
|
72
|
+
* octets of an invalid URL are percent-encoded
|
|
73
|
+
*
|
|
74
|
+
* @param {String} url URL as the client sent it
|
|
75
|
+
* @param {String} text Human readable text
|
|
76
|
+
* @return {Error} Error object
|
|
77
|
+
*/
|
|
78
|
+
function badUrl(url, text) {
|
|
79
|
+
if (!url) {
|
|
80
|
+
// url-resp-text can not be empty
|
|
81
|
+
return appendError(text);
|
|
82
|
+
}
|
|
83
|
+
const value = url.replace(/[^\x20-\x5c\x5e-\x7e]/g, chr => '%' + chr.charCodeAt(0).toString(16).toUpperCase().padStart(2, '0'));
|
|
84
|
+
return appendError(text, 'BADURL', [{ type: 'TEXT', value }]);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Removes "." and ".." segments from a path (RFC 3986 section 5.2.4), as relative IMAP URLs are
|
|
89
|
+
* resolved by the generic URI rules (RFC 5092 section 7)
|
|
90
|
+
*
|
|
91
|
+
* @param {String} path Path that starts with "/"
|
|
92
|
+
* @return {String} Path without dot segments
|
|
93
|
+
*/
|
|
94
|
+
function removeDotSegments(path) {
|
|
95
|
+
const segments = path.split('/').slice(1);
|
|
96
|
+
const output = [];
|
|
97
|
+
segments.forEach((segment, i) => {
|
|
98
|
+
const last = i === segments.length - 1;
|
|
99
|
+
if (segment === '.' || segment === '..') {
|
|
100
|
+
if (segment === '..') {
|
|
101
|
+
output.pop();
|
|
102
|
+
}
|
|
103
|
+
if (last) {
|
|
104
|
+
output.push('');
|
|
105
|
+
}
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
output.push(segment);
|
|
109
|
+
});
|
|
110
|
+
return '/' + output.join('/');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Decodes percent-encoded octets
|
|
115
|
+
*
|
|
116
|
+
* @param {String} value Percent-encoded string
|
|
117
|
+
* @return {String} Binary string
|
|
118
|
+
*/
|
|
119
|
+
function percentDecode(value) {
|
|
120
|
+
return value.replace(/%([0-9a-f]{2})/gi, (match, hex) => String.fromCharCode(parseInt(hex, 16)));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Returns the octets of a message part as BODY.PEEK[<section>] would, or false if the section is
|
|
125
|
+
* invalid or the part does not exist
|
|
126
|
+
*
|
|
127
|
+
* @param {Object} connection IMAPConnection
|
|
128
|
+
* @param {Object} message Message object
|
|
129
|
+
* @param {String} section Decoded section-spec
|
|
130
|
+
* @return {String|Boolean} Section contents
|
|
131
|
+
*/
|
|
132
|
+
function getSection(connection, message, section) {
|
|
133
|
+
if (!section || !/^[\x20-\x7e]+$/.test(section)) {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// reuse the FETCH grammar for section-spec
|
|
138
|
+
let query;
|
|
139
|
+
try {
|
|
140
|
+
const parsed = imapHandler.parser('X FETCH 1 BODY.PEEK[' + section + ']');
|
|
141
|
+
query = parsed.attributes.length === 2 && !parsed.attributes[1].partial && parsed.attributes[1];
|
|
142
|
+
} catch {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
if (!query || !query.section || !query.section.length || query.section[0].type !== 'ATOM') {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// FETCH returns an empty string for a part that does not exist, a URL for it is invalid (RFC 4469 section 4.1)
|
|
150
|
+
const sectionId = String(query.section[0].value).toUpperCase();
|
|
151
|
+
const path = (sectionId.match(/^[1-9]\d*(?:\.[1-9]\d*)*/) || [''])[0];
|
|
152
|
+
if (path) {
|
|
153
|
+
const node = resolveNode(getMessageData(message).tree, path);
|
|
154
|
+
const key = sectionId.substr(path.length + 1);
|
|
155
|
+
// HEADER and TEXT with a part number refer to the message in a message/rfc822 part
|
|
156
|
+
if (!node || (key && key !== 'MIME' && !node.message)) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
return fetchHandlers.BODY(connection, message, { section: query.section }).value;
|
|
163
|
+
} catch {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Returns the octets an IMAP URL refers to. RFC 4469 section 3: only URLs of messages or message
|
|
170
|
+
* parts in the current session are supported, relative to "imap://user@server/". RFC 5092 section 7.2
|
|
171
|
+
* forbids relative-path references, so only absolute-path references ("/" imessagepart) are accepted
|
|
172
|
+
*
|
|
173
|
+
* @param {Object} connection IMAPConnection
|
|
174
|
+
* @param {String} url IMAP URL
|
|
175
|
+
* @return {String} Binary string
|
|
176
|
+
*/
|
|
177
|
+
function resolveUrl(connection, url) {
|
|
178
|
+
if (!url) {
|
|
179
|
+
throw badUrl(url, 'Empty URL does not refer to a message');
|
|
180
|
+
}
|
|
181
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(url) || url.substr(0, 2) === '//') {
|
|
182
|
+
throw badUrl(url, 'Only absolute-path URLs of this server are supported, e.g. /INBOX/;UID=1');
|
|
183
|
+
}
|
|
184
|
+
if (url.charAt(0) !== '/') {
|
|
185
|
+
throw badUrl(url, 'Relative-path URLs are not allowed (RFC 5092 section 7.2)');
|
|
186
|
+
}
|
|
187
|
+
if (!URL_CHARS.test(url) || /%(?![0-9a-f]{2})/i.test(url)) {
|
|
188
|
+
throw badUrl(url, 'Invalid characters in URL');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const match = removeDotSegments(url).match(MESSAGE_PART);
|
|
192
|
+
if (!match) {
|
|
193
|
+
throw badUrl(url, 'URL does not refer to a message or a message part');
|
|
194
|
+
}
|
|
195
|
+
const [, encMailbox, uidvalidity, uid, encSection, partial] = match;
|
|
196
|
+
|
|
197
|
+
// RFC 5092 section 11: uidvalidity and iuid-only are nz-number values (32-bit, RFC 3501 section 9)
|
|
198
|
+
if ((uidvalidity !== undefined && !isNzNumber(uidvalidity)) || !isNzNumber(uid)) {
|
|
199
|
+
throw badUrl(url, 'Invalid UIDVALIDITY or UID in URL');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// RFC 5092 section 8: mailbox names are percent-encoded UTF-8
|
|
203
|
+
let name;
|
|
204
|
+
try {
|
|
205
|
+
name = new TextDecoder('utf-8', { fatal: true }).decode(Buffer.from(percentDecode(encMailbox), 'binary'));
|
|
206
|
+
} catch {
|
|
207
|
+
throw badUrl(url, 'Mailbox name in URL is not valid UTF-8');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const mailbox = connection.server.getMailbox(encodeMailboxName(name));
|
|
211
|
+
if (!mailbox || mailbox.flags.indexOf('\\Noselect') >= 0) {
|
|
212
|
+
throw badUrl(url, 'Mailbox does not exist');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// plugins can refuse to read the mailbox, e.g. ACL without the "r" right
|
|
216
|
+
for (const check of connection.server.urlAccessChecks) {
|
|
217
|
+
const refusal = check(connection, mailbox, url);
|
|
218
|
+
if (refusal) {
|
|
219
|
+
throw badUrl(url, refusal.text || 'Permission denied');
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// RFC 5092 section 5: a stale URL behaves as if the mailbox does not exist
|
|
224
|
+
if (uidvalidity !== undefined && Number(uidvalidity) !== mailbox.uidvalidity) {
|
|
225
|
+
throw badUrl(url, 'UIDVALIDITY does not match');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const message = mailbox.messages.find(item => item.uid === Number(uid));
|
|
229
|
+
if (!message) {
|
|
230
|
+
throw badUrl(url, 'Message does not exist');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// RFC 5092 section 6: no section is the whole message, BODY.PEEK[], so \Seen is not set
|
|
234
|
+
let value = message.raw;
|
|
235
|
+
if (encSection !== undefined) {
|
|
236
|
+
value = getSection(connection, message, percentDecode(encSection));
|
|
237
|
+
if (value === false) {
|
|
238
|
+
throw badUrl(url, 'Invalid section or the message part does not exist');
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (partial !== undefined) {
|
|
243
|
+
// RFC 5092 section 11: partial-range = number ["." nz-number], offset and length in octets, both 32-bit
|
|
244
|
+
// (RFC 3501 section 9)
|
|
245
|
+
const range = partial.match(/^(\d+)(?:\.(\d+))?$/);
|
|
246
|
+
if (!range || !isNumber(range[1]) || (range[2] !== undefined && !isNzNumber(range[2]))) {
|
|
247
|
+
throw badUrl(url, 'Invalid partial range');
|
|
248
|
+
}
|
|
249
|
+
value = value.substr(Number(range[1]), range[2] ? Number(range[2]) : undefined);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return value;
|
|
253
|
+
}
|