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.
Files changed (114) hide show
  1. package/LICENSE +16 -0
  2. package/README.md +608 -2
  3. package/bin/help.txt +98 -0
  4. package/bin/imapkit.js +108 -0
  5. package/cert/server.crt +20 -0
  6. package/cert/server.key +28 -0
  7. package/lib/addressparser.js +283 -0
  8. package/lib/arguments.js +112 -0
  9. package/lib/bodystructure.js +149 -0
  10. package/lib/command-states.js +109 -0
  11. package/lib/commands/append.js +313 -0
  12. package/lib/commands/capability.js +47 -0
  13. package/lib/commands/check.js +21 -0
  14. package/lib/commands/close.js +30 -0
  15. package/lib/commands/copy.js +115 -0
  16. package/lib/commands/create.js +52 -0
  17. package/lib/commands/delete.js +64 -0
  18. package/lib/commands/examine.js +7 -0
  19. package/lib/commands/expunge.js +27 -0
  20. package/lib/commands/fetch.js +229 -0
  21. package/lib/commands/handlers/fetch.js +209 -0
  22. package/lib/commands/handlers/flags.js +42 -0
  23. package/lib/commands/handlers/search.js +519 -0
  24. package/lib/commands/handlers/status.js +85 -0
  25. package/lib/commands/handlers/store.js +127 -0
  26. package/lib/commands/list.js +100 -0
  27. package/lib/commands/login.js +67 -0
  28. package/lib/commands/logout.js +41 -0
  29. package/lib/commands/lsub.js +87 -0
  30. package/lib/commands/noop.js +21 -0
  31. package/lib/commands/rename.js +102 -0
  32. package/lib/commands/search.js +76 -0
  33. package/lib/commands/select.js +289 -0
  34. package/lib/commands/status.js +63 -0
  35. package/lib/commands/store.js +151 -0
  36. package/lib/commands/subscribe.js +53 -0
  37. package/lib/commands/uid copy.js +7 -0
  38. package/lib/commands/uid fetch.js +5 -0
  39. package/lib/commands/uid search.js +5 -0
  40. package/lib/commands/uid store.js +5 -0
  41. package/lib/commands/unsubscribe.js +50 -0
  42. package/lib/dates.js +123 -0
  43. package/lib/deflate-layer.js +232 -0
  44. package/lib/envelope.js +82 -0
  45. package/lib/esearch.js +208 -0
  46. package/lib/framing.js +102 -0
  47. package/lib/list-extensions.js +36 -0
  48. package/lib/load-plugins.js +109 -0
  49. package/lib/mailbox-name.js +133 -0
  50. package/lib/mimeparser.js +778 -0
  51. package/lib/mock-client.js +233 -0
  52. package/lib/numbers.js +52 -0
  53. package/lib/plugins/acl.js +964 -0
  54. package/lib/plugins/appendlimit.js +83 -0
  55. package/lib/plugins/auth-plain.js +94 -0
  56. package/lib/plugins/binary.js +256 -0
  57. package/lib/plugins/catenate.js +253 -0
  58. package/lib/plugins/compress.js +76 -0
  59. package/lib/plugins/condstore.js +563 -0
  60. package/lib/plugins/context-search.js +321 -0
  61. package/lib/plugins/context-sort.js +19 -0
  62. package/lib/plugins/create-special-use.js +108 -0
  63. package/lib/plugins/enable.js +155 -0
  64. package/lib/plugins/esearch.js +156 -0
  65. package/lib/plugins/esort.js +60 -0
  66. package/lib/plugins/id.js +138 -0
  67. package/lib/plugins/idle.js +105 -0
  68. package/lib/plugins/imap4rev2.js +202 -0
  69. package/lib/plugins/list-extended.js +258 -0
  70. package/lib/plugins/list-status.js +31 -0
  71. package/lib/plugins/literalminus.js +20 -0
  72. package/lib/plugins/literalplus.js +18 -0
  73. package/lib/plugins/logindisabled.js +50 -0
  74. package/lib/plugins/messagelimit.js +234 -0
  75. package/lib/plugins/metadata-server.js +13 -0
  76. package/lib/plugins/metadata.js +475 -0
  77. package/lib/plugins/move.js +110 -0
  78. package/lib/plugins/multiappend.js +26 -0
  79. package/lib/plugins/multisearch.js +269 -0
  80. package/lib/plugins/namespace.js +67 -0
  81. package/lib/plugins/notify.js +654 -0
  82. package/lib/plugins/oauthbearer.js +217 -0
  83. package/lib/plugins/objectid.js +243 -0
  84. package/lib/plugins/partial.js +68 -0
  85. package/lib/plugins/preview.js +400 -0
  86. package/lib/plugins/qresync.js +525 -0
  87. package/lib/plugins/quota.js +285 -0
  88. package/lib/plugins/replace.js +145 -0
  89. package/lib/plugins/sasl-ir.js +12 -0
  90. package/lib/plugins/savedate.js +59 -0
  91. package/lib/plugins/savelimit.js +18 -0
  92. package/lib/plugins/searchres.js +82 -0
  93. package/lib/plugins/sort-display.js +23 -0
  94. package/lib/plugins/sort.js +132 -0
  95. package/lib/plugins/special-use.js +95 -0
  96. package/lib/plugins/starttls.js +57 -0
  97. package/lib/plugins/status-size.js +19 -0
  98. package/lib/plugins/thread-orderedsubject.js +16 -0
  99. package/lib/plugins/thread-references.js +16 -0
  100. package/lib/plugins/uidonly.js +135 -0
  101. package/lib/plugins/uidplus.js +124 -0
  102. package/lib/plugins/unauthenticate.js +28 -0
  103. package/lib/plugins/unselect.js +36 -0
  104. package/lib/plugins/utf8-accept.js +68 -0
  105. package/lib/plugins/x-gm-ext-1.js +456 -0
  106. package/lib/plugins/xoauth2.js +188 -0
  107. package/lib/plugins/xtoybird.js +282 -0
  108. package/lib/server.js +2880 -0
  109. package/lib/smtp-listener.js +51 -0
  110. package/lib/sorting.js +373 -0
  111. package/lib/threading.js +357 -0
  112. package/lib/utf8-session.js +123 -0
  113. package/lib/vanished.js +57 -0
  114. 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
+ }