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,51 @@
1
+ 'use strict';
2
+
3
+ const { SMTPServer } = require('smtp-server');
4
+
5
+ /**
6
+ * Starts an SMTP server that appends every received message to the INBOX
7
+ * of the given ImapKit IMAP server. Any sender, recipient and credentials
8
+ * are accepted.
9
+ *
10
+ * @param {Number} smtpPort - port to listen on for SMTP commands
11
+ * @param {Object} imapServer - the ImapKit IMAP server
12
+ * @param {Function} [callback] - function executed when SMTP server is listening
13
+ * @return {SMTPServer} the SMTP server instance, use `close()` to stop it
14
+ */
15
+ exports.startSMTPServer = function startSMTPServer(smtpPort, imapServer, callback) {
16
+ const credentials = imapServer.getCredentials();
17
+
18
+ const server = new SMTPServer({
19
+ banner: 'ImapKit',
20
+ key: credentials.key,
21
+ cert: credentials.cert,
22
+ authOptional: true,
23
+ logger: false,
24
+ onAuth(auth, session, done) {
25
+ done(null, { user: auth.username });
26
+ },
27
+ onData(stream, session, done) {
28
+ const chunks = [];
29
+ stream.on('data', chunk => chunks.push(chunk));
30
+ stream.on('error', done);
31
+ stream.on('end', () => {
32
+ // smtp-server has already removed the dot-stuffing (RFC 5321 section 4.5.2)
33
+ imapServer.appendMessage('INBOX', [], false, Buffer.concat(chunks).toString('binary'));
34
+ done();
35
+ });
36
+ }
37
+ });
38
+
39
+ server.on('error', err => {
40
+ if (imapServer.options.debug) {
41
+ console.error('SMTP server error: %s', err.message);
42
+ }
43
+ });
44
+
45
+ server.listen(smtpPort, () => {
46
+ console.log('Incoming SMTP server up and running on port %s', server.server.address().port);
47
+ callback?.();
48
+ });
49
+
50
+ return server;
51
+ };
package/lib/sorting.js ADDED
@@ -0,0 +1,373 @@
1
+ 'use strict';
2
+
3
+ // Message values for SORT and THREAD (RFC 5256): string collation, base subject, sent date,
4
+ // addresses and Message IDs, and the search step that both commands start with
5
+
6
+ const { getMessageData } = require('./mimeparser');
7
+ const { processAddress } = require('./envelope');
8
+ const { parseDateTime, parseHeaderDate, toTimestamp } = require('./dates');
9
+ const makeSearch = require('./commands/handlers/search');
10
+ const { badError, criteriaValues, sendSearchError } = makeSearch;
11
+
12
+ // RFC 2047 section 2: encoded-word = "=?" charset "?" encoding "?" encoded-text "?=", RFC 2231 section 5
13
+ // adds an optional "*" language suffix to the charset
14
+ const ENCODED_WORD = /=\?([^?\s*]+)(?:\*[^?\s]*)?\?([BbQq])\?([^?\s]*)\?=/g;
15
+
16
+ const utf8Decoder = new TextDecoder('utf-8');
17
+
18
+ // decoders by lower case charset name, false for a charset that TextDecoder does not know
19
+ const decoders = new Map();
20
+
21
+ /**
22
+ * Decodes the octets of an encoded word, or returns false if the charset is unknown
23
+ */
24
+ function decodeCharset(charset, octets) {
25
+ charset = charset.toLowerCase();
26
+ if (!decoders.has(charset)) {
27
+ let decoder = false;
28
+ try {
29
+ decoder = new TextDecoder(charset);
30
+ } catch {
31
+ // unknown charset
32
+ }
33
+ decoders.set(charset, decoder);
34
+ }
35
+ const decoder = decoders.get(charset);
36
+ return decoder && decoder.decode(octets);
37
+ }
38
+
39
+ /**
40
+ * Decodes an RFC 2047 header value to a Unicode string. Text outside encoded words is read as UTF-8
41
+ * (invalid sequences become U+FFFD). Adjacent encoded words in the same charset are decoded together,
42
+ * so a multi-octet character may span them, and the white space between them is dropped (RFC 2047
43
+ * section 6.2). Encoded words in an unknown charset are kept as they are.
44
+ *
45
+ * @param {String} value Header value as a binary string
46
+ * @return {String} Decoded value
47
+ */
48
+ function decodeHeader(value) {
49
+ value = (value || '').toString();
50
+ let result = '';
51
+ let pending = null;
52
+ let lastIndex = 0;
53
+
54
+ const flush = () => {
55
+ if (pending) {
56
+ const decoded = decodeCharset(pending.charset, Buffer.concat(pending.octets));
57
+ result += decoded === false ? pending.source : decoded;
58
+ pending = null;
59
+ }
60
+ };
61
+
62
+ ENCODED_WORD.lastIndex = 0;
63
+ let match;
64
+ while ((match = ENCODED_WORD.exec(value))) {
65
+ const between = value.substring(lastIndex, match.index);
66
+ const adjacent = pending && /^\s*$/.test(between);
67
+ if (!adjacent) {
68
+ flush();
69
+ result += utf8Decoder.decode(Buffer.from(between, 'binary'));
70
+ }
71
+ lastIndex = ENCODED_WORD.lastIndex;
72
+
73
+ const octets =
74
+ match[2].toUpperCase() === 'B'
75
+ ? Buffer.from(match[3], 'base64')
76
+ : Buffer.from(
77
+ match[3].replace(/_/g, ' ').replace(/=([0-9a-fA-F]{2})/g, (m, hex) => String.fromCharCode(parseInt(hex, 16))),
78
+ 'binary'
79
+ );
80
+
81
+ if (pending && pending.charset.toLowerCase() !== match[1].toLowerCase()) {
82
+ flush();
83
+ }
84
+ if (!pending) {
85
+ pending = { charset: match[1], octets: [], source: '' };
86
+ }
87
+ pending.octets.push(octets);
88
+ pending.source += (pending.source ? between : '') + match[0];
89
+ }
90
+ flush();
91
+ return result + utf8Decoder.decode(Buffer.from(value.substr(lastIndex), 'binary'));
92
+ }
93
+
94
+ // UnicodeData.txt titlecase mappings that differ from the single code point uppercase mapping
95
+ // that String#toUpperCase gives: the Latin digraphs and the Greek letters with ypogegrammeni
96
+ const TITLECASE = new Map([
97
+ [0x01c4, 0x01c5],
98
+ [0x01c6, 0x01c5],
99
+ [0x01c7, 0x01c8],
100
+ [0x01c9, 0x01c8],
101
+ [0x01ca, 0x01cb],
102
+ [0x01cc, 0x01cb],
103
+ [0x01f1, 0x01f2],
104
+ [0x01f3, 0x01f2],
105
+ [0x1fb3, 0x1fbc],
106
+ [0x1fc3, 0x1fcc],
107
+ [0x1ff3, 0x1ffc]
108
+ ]);
109
+ for (const start of [0x1f80, 0x1f90, 0x1fa0]) {
110
+ for (let i = 0; i < 8; i++) {
111
+ TITLECASE.set(start + i, start + i + 8);
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Returns the i;unicode-casemap form of a string (RFC 5051 section 2): every character is titlecased
117
+ * and then decomposed (NFKD). Comparing the UTF-8 octets of the results is the collation that
118
+ * RFC 5256 section 7 requires for SORT and THREAD (I18NLEVEL=1, RFC 5255 section 4.5)
119
+ *
120
+ * @param {String} str Unicode string
121
+ * @return {Buffer} UTF-8 octets to compare with Buffer.compare
122
+ */
123
+ function collationKey(str) {
124
+ let result = '';
125
+ for (const char of str) {
126
+ const code = char.codePointAt(0);
127
+ if (TITLECASE.has(code)) {
128
+ result += String.fromCodePoint(TITLECASE.get(code));
129
+ continue;
130
+ }
131
+ // full case mappings (e.g. "ß" to "SS") are not the simple mapping of UnicodeData.txt
132
+ const upper = char.toUpperCase();
133
+ result += [...upper].length === 1 ? upper : char;
134
+ }
135
+ return Buffer.from(result.normalize('NFKD'), 'utf-8');
136
+ }
137
+
138
+ // RFC 5256 section 5: subj-refwd = ("re" / ("fw" ["d"])) *WSP [subj-blob] ":", subj-blob = "[" *BLOBCHAR "]" *WSP
139
+ const SUBJ_BLOB = '\\[[^[\\]]*\\] *';
140
+ const SUBJ_LEADER = new RegExp('^(?:' + SUBJ_BLOB + ')*(?:re|fwd?) *(?:' + SUBJ_BLOB + ')?:', 'i');
141
+ const SUBJ_BLOB_PREFIX = new RegExp('^' + SUBJ_BLOB);
142
+
143
+ /**
144
+ * Extracts the base subject (RFC 5256 section 2.1) and tells if the subject marks a reply or a
145
+ * forward (RFC 5256 section 3, REFERENCES): the extraction removed a subj-refwd, a "(fwd)" trailer
146
+ * or a subj-fwd-hdr and subj-fwd-trl pair.
147
+ *
148
+ * @param {String} subject Subject header value as a binary string
149
+ * @return {Object} `{ subject, isReply }`, subject is the base subject as a Unicode string
150
+ */
151
+ function baseSubject(subject) {
152
+ let isReply = false;
153
+
154
+ // (1) decode encoded words, tabs and continuations become a space, runs of spaces one space
155
+ let text = decodeHeader(subject)
156
+ .replace(/\r?\n(?=[ \t])/g, '')
157
+ .replace(/[\t\r\n]/g, ' ')
158
+ .replace(/ {2,}/g, ' ');
159
+
160
+ for (;;) {
161
+ // (2) remove subj-trailer text until there is none
162
+ let end = text.length;
163
+ for (;;) {
164
+ if (text.charAt(end - 1) === ' ') {
165
+ end--;
166
+ } else if (end >= 5 && text.substring(end - 5, end).toLowerCase() === '(fwd)') {
167
+ isReply = true;
168
+ end -= 5;
169
+ } else {
170
+ break;
171
+ }
172
+ }
173
+ text = text.substring(0, end);
174
+
175
+ // (3) remove subj-leader text, (4) remove a subj-blob prefix if a subj-base remains, (5) repeat
176
+ for (;;) {
177
+ let match;
178
+ if (text.charAt(0) === ' ') {
179
+ text = text.substr(1);
180
+ } else if ((match = text.match(SUBJ_LEADER))) {
181
+ isReply = true;
182
+ text = text.substr(match[0].length);
183
+ } else if ((match = text.match(SUBJ_BLOB_PREFIX)) && match[0].length < text.length) {
184
+ text = text.substr(match[0].length);
185
+ } else {
186
+ break;
187
+ }
188
+ }
189
+
190
+ // (6) unwrap "[fwd: ... ]" and start over from step (2)
191
+ if (/^\[fwd:/i.test(text) && /\]$/.test(text)) {
192
+ isReply = true;
193
+ text = text.slice(5, -1);
194
+ continue;
195
+ }
196
+
197
+ // (7) the remaining text is the base subject
198
+ return { subject: text, isReply };
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Internal date and time of a message in milliseconds (RFC 5256 ARRIVAL)
204
+ *
205
+ * @param {Object} message Message object
206
+ * @return {Number} timestamp, 0 if the internal date can not be parsed
207
+ */
208
+ function arrivalTime(message) {
209
+ const date = parseDateTime(message.internaldate);
210
+ return date ? toTimestamp(date) : 0;
211
+ }
212
+
213
+ /**
214
+ * Sent date of a message in milliseconds (RFC 5256 section 2.2): the Date header normalized to UTC.
215
+ * An invalid zone is read as UTC and an invalid time as 00:00:00, and if the header is missing or
216
+ * has no valid date, the internal date is used instead.
217
+ *
218
+ * @param {Object} message Message object
219
+ * @return {Number} timestamp
220
+ */
221
+ function sentTime(message) {
222
+ const date = parseHeaderDate(getMessageData(message).tree.parsedHeader.date);
223
+ if (!date) {
224
+ return arrivalTime(message);
225
+ }
226
+ if (date.hours === undefined || date.hours > 23 || date.minutes > 59 || date.seconds > 60) {
227
+ Object.assign(date, { hours: 0, minutes: 0, seconds: 0 });
228
+ }
229
+ return toTimestamp(date);
230
+ }
231
+
232
+ /**
233
+ * Returns the first address of an envelope address list, as [name, adl, mailbox, host]
234
+ */
235
+ function firstAddress(message, header) {
236
+ const list = processAddress(getMessageData(message).tree.parsedHeader[header]);
237
+ return (list && list[0]) || null;
238
+ }
239
+
240
+ /**
241
+ * addr-mailbox of the first address in a header (RFC 5256 CC, FROM and TO), empty if there is none
242
+ *
243
+ * @param {Object} message Message object
244
+ * @param {String} header "from", "to" or "cc"
245
+ * @return {String} Unicode string
246
+ */
247
+ function addressMailbox(message, header) {
248
+ const address = firstAddress(message, header);
249
+ return address && address[2] ? decodeHeader(address[2]) : '';
250
+ }
251
+
252
+ /**
253
+ * DISPLAY sort value of the first address in a header (RFC 5957 sections 3 and 4): the decoded
254
+ * addr-name, or addr-mailbox@addr-host, or addr-mailbox, or the empty string
255
+ *
256
+ * @param {Object} message Message object
257
+ * @param {String} header "from" or "to"
258
+ * @return {String} Unicode string
259
+ */
260
+ function displayAddress(message, header) {
261
+ const address = firstAddress(message, header);
262
+ const name = address && address[0] ? decodeHeader(address[0]) : '';
263
+ if (name || !address || !address[2]) {
264
+ return name;
265
+ }
266
+ return decodeHeader(address[3] ? address[2] + '@' + address[3] : address[2]);
267
+ }
268
+
269
+ /**
270
+ * Lists the Message IDs in a header value, normalized so that quoting does not matter (RFC 5256
271
+ * section 3, REFERENCES): comments and white space are dropped and a quoted id-left is unquoted.
272
+ * Only ids of the form "<" id-left "@" id-right ">" (RFC 5322 section 3.6.4) are valid.
273
+ *
274
+ * @param {String|Array} value Header value
275
+ * @return {Array} Message IDs
276
+ */
277
+ function parseMessageIds(value) {
278
+ const str = [].concat(value || []).join(' ');
279
+ const ids = [];
280
+ let depth = 0;
281
+ let quoted = false;
282
+ let current = null;
283
+
284
+ for (let i = 0; i < str.length; i++) {
285
+ const char = str.charAt(i);
286
+ if (quoted) {
287
+ if (char === '\\') {
288
+ if (current !== null) {
289
+ current += str.charAt(i + 1);
290
+ }
291
+ i++;
292
+ } else if (char === '"') {
293
+ quoted = false;
294
+ } else if (current !== null) {
295
+ current += char;
296
+ }
297
+ } else if (depth) {
298
+ if (char === '\\') {
299
+ i++;
300
+ } else if (char === '(') {
301
+ depth++;
302
+ } else if (char === ')') {
303
+ depth--;
304
+ }
305
+ } else if (char === '"') {
306
+ quoted = true;
307
+ } else if (char === '(') {
308
+ depth++;
309
+ } else if (char === '<') {
310
+ current = '';
311
+ } else if (char === '>') {
312
+ if (current !== null) {
313
+ const at = current.lastIndexOf('@');
314
+ if (at > 0 && at < current.length - 1) {
315
+ ids.push(current);
316
+ }
317
+ }
318
+ current = null;
319
+ } else if (current !== null && !/\s/.test(char)) {
320
+ current += char;
321
+ }
322
+ }
323
+ return ids;
324
+ }
325
+
326
+ /**
327
+ * Runs the search part of SORT and THREAD (RFC 5256 section 3): the charset is mandatory and the
328
+ * criteria follow it, both as in SEARCH. Sends a tagged BAD or NO response if the search fails.
329
+ *
330
+ * @param {Object} connection IMAP connection
331
+ * @param {Object} parsed Parsed command
332
+ * @param {String} data Raw command
333
+ * @param {Array} attributes Command arguments starting with the charset
334
+ * @return {Object|Boolean} `{ list, numbers }` like the SEARCH handler returns, or false after an error
335
+ */
336
+ function searchMessages(connection, parsed, data, attributes) {
337
+ const command = parsed.command.toUpperCase();
338
+ const charset = attributes[0];
339
+ try {
340
+ // RFC 5256 section 5: charset = atom / quoted
341
+ if (attributes.length < 2) {
342
+ throw badError(command + ' expects a charset and search criteria');
343
+ }
344
+ if (!charset || ['ATOM', 'STRING'].indexOf(charset.type) < 0) {
345
+ throw badError('Charset must be an atom or a quoted string');
346
+ }
347
+ const criteria = criteriaValues(attributes.slice(1));
348
+ if (connection.searchCharset) {
349
+ // RFC 9755 section 3: with a fixed session charset (UTF8=ACCEPT) other charsets are BAD
350
+ if (charset.value.toUpperCase() !== connection.searchCharset) {
351
+ throw badError('Charset must be ' + connection.searchCharset + ' in this session');
352
+ }
353
+ return makeSearch(connection, connection.getSessionMessages(), criteria);
354
+ }
355
+ const params = ['CHARSET', charset.value].concat(criteria);
356
+ return makeSearch(connection, connection.getSessionMessages(), params);
357
+ } catch (E) {
358
+ sendSearchError(connection, parsed, data, E, command + ' FAILED');
359
+ return false;
360
+ }
361
+ }
362
+
363
+ module.exports = {
364
+ decodeHeader,
365
+ collationKey,
366
+ baseSubject,
367
+ arrivalTime,
368
+ sentTime,
369
+ addressMailbox,
370
+ displayAddress,
371
+ parseMessageIds,
372
+ searchMessages
373
+ };