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,519 @@
1
+ 'use strict';
2
+
3
+ const { isUtf8 } = require('buffer');
4
+ const { getMessageData, render } = require('../../mimeparser');
5
+ const { monthIndex, dateKey, parseDateTime, parseHeaderDate } = require('../../dates');
6
+ const { MAX_NUMBER, MAX_NUMBER64, isNumber } = require('../../numbers');
7
+
8
+ // RFC 3501 6.4.4 search keys and their arguments
9
+ const searchKeys = {
10
+ ALL: [],
11
+ ANSWERED: [],
12
+ BCC: ['string'],
13
+ BEFORE: ['date'],
14
+ BODY: ['string'],
15
+ CC: ['string'],
16
+ DELETED: [],
17
+ DRAFT: [],
18
+ FLAGGED: [],
19
+ FROM: ['string'],
20
+ HEADER: ['string', 'string'],
21
+ KEYWORD: ['string'],
22
+ LARGER: ['number64'],
23
+ NEW: [],
24
+ NOT: ['key'],
25
+ OLD: [],
26
+ ON: ['date'],
27
+ OR: ['key', 'key'],
28
+ RECENT: [],
29
+ SEEN: [],
30
+ SENTBEFORE: ['date'],
31
+ SENTON: ['date'],
32
+ SENTSINCE: ['date'],
33
+ SINCE: ['date'],
34
+ SMALLER: ['number64'],
35
+ SUBJECT: ['string'],
36
+ TEXT: ['string'],
37
+ TO: ['string'],
38
+ UID: ['sequence'],
39
+ UNANSWERED: [],
40
+ UNDELETED: [],
41
+ UNDRAFT: [],
42
+ UNFLAGGED: [],
43
+ UNKEYWORD: ['string'],
44
+ UNSEEN: []
45
+ };
46
+
47
+ // Charsets accepted for the CHARSET argument (RFC 3501 6.4.4: US-ASCII must be supported)
48
+ const charsets = ['US-ASCII', 'UTF-8'];
49
+
50
+ /**
51
+ * Creates an error that is reported to the client as BAD
52
+ */
53
+ function badError(message) {
54
+ const err = new Error(message);
55
+ err.imapResponse = 'BAD';
56
+ return err;
57
+ }
58
+
59
+ /**
60
+ * Parses an RFC 3501 date argument (date-day "-" date-month "-" date-year) to a comparable YYYY-MM-DD string
61
+ */
62
+ function parseQueryDate(value) {
63
+ const match = (value || '').toString().match(/^(\d{1,2})-([A-Za-z]{3})-(\d{4})$/);
64
+ const date = match && dateKey(Number(match[1]), monthIndex(match[2]), Number(match[3]));
65
+ if (!date) {
66
+ throw badError('Invalid date argument ' + value);
67
+ }
68
+ return date;
69
+ }
70
+
71
+ /**
72
+ * Date of a date-time value, disregarding time and timezone, as a comparable YYYY-MM-DD string,
73
+ * or false when it can not be parsed
74
+ *
75
+ * @param {String} dateTime Date-time value, e.g. "14-Sep-2013 21:22:28 -0300"
76
+ * @return {String|Boolean} date or false
77
+ */
78
+ function getDateKey(dateTime) {
79
+ const date = parseDateTime(dateTime);
80
+ return date ? dateKey(date.day, date.month, date.year) : false;
81
+ }
82
+
83
+ /**
84
+ * Date of the internal date of a message, disregarding time and timezone, or false when it can not be parsed
85
+ */
86
+ function getInternalDate(message) {
87
+ return getDateKey(message.internaldate);
88
+ }
89
+
90
+ /**
91
+ * Date of the Date header of a message, disregarding time and timezone (RFC 3501 section 6.4.4, unlike the
92
+ * sent date of SORT it is not adjusted to UTC). Falls back to the internal date
93
+ */
94
+ function getSentDate(message) {
95
+ const date = parseHeaderDate(getMessageData(message).tree.parsedHeader.date);
96
+ return date ? dateKey(date.day, date.month, date.year) : getInternalDate(message);
97
+ }
98
+
99
+ /**
100
+ * Converts the parsed search criteria of a command to the values the search takes, parenthesized lists stay nested
101
+ *
102
+ * @param {Array} attributes Parsed arguments
103
+ * @return {Array} values
104
+ * @throws {Error} BAD error for an argument that can not be a search key or its value
105
+ */
106
+ function criteriaValues(attributes) {
107
+ const convert = (argument, i) => {
108
+ if (Array.isArray(argument)) {
109
+ return argument.map(convert);
110
+ }
111
+ if (!argument || ['STRING', 'ATOM', 'LITERAL', 'SEQUENCE'].indexOf(argument.type) < 0) {
112
+ throw badError('Invalid search criteria argument #' + (i + 1));
113
+ }
114
+ return argument.value;
115
+ };
116
+ return attributes.map(convert);
117
+ }
118
+
119
+ /**
120
+ * Answers a failed search with NO, or BAD for `err.imapResponse` "BAD": the BADCHARSET response code with the
121
+ * supported charsets (RFC 3501 section 7.1, RFC 5256 section 3), or the response code a search limit set
122
+ * (`err.responseCode`), and the text
123
+ *
124
+ * @param {Object} connection IMAP connection
125
+ * @param {Object} parsed Parsed command
126
+ * @param {String} data Raw command
127
+ * @param {Error} err Search error
128
+ * @param {String} description Description for output handlers
129
+ */
130
+ function sendSearchError(connection, parsed, data, err, description) {
131
+ const attributes = [];
132
+ if (err.code === 'BADCHARSET') {
133
+ attributes.push({
134
+ type: 'SECTION',
135
+ section: [{ type: 'ATOM', value: 'BADCHARSET' }, err.charsets.map(value => ({ type: 'ATOM', value }))]
136
+ });
137
+ } else if (err.responseCode) {
138
+ attributes.push({ type: 'SECTION', section: err.responseCode.map(value => ({ type: 'ATOM', value: String(value) })) });
139
+ }
140
+ attributes.push({ type: 'TEXT', value: err.message });
141
+ connection.send({ tag: parsed.tag, command: err.imapResponse === 'BAD' ? 'BAD' : 'NO', attributes }, description, parsed, data);
142
+ }
143
+
144
+ /**
145
+ * Lower cases ASCII letters only, so that 8-bit octets in binary strings stay intact
146
+ */
147
+ function asciiLowerCase(str) {
148
+ return str.replace(/[A-Z]+/g, chars => chars.toLowerCase());
149
+ }
150
+
151
+ /**
152
+ * Checks if a string contains another one, ignoring ASCII case
153
+ *
154
+ * @param {String} haystack String to search in
155
+ * @param {String} needle String to look for, already lower cased with asciiLowerCase
156
+ */
157
+ function contains(haystack, needle) {
158
+ return asciiLowerCase(haystack).indexOf(needle) >= 0;
159
+ }
160
+
161
+ /**
162
+ * Returns the search keys with the types of their arguments, including the keys that plugins
163
+ * define in `server.searchHandlers`. If a plugin handler takes more than 3 params
164
+ * (connection, message, index), the remaining ones are its arguments. A plugin handler can
165
+ * describe its arguments instead with an `argumentTypes(list)` method, that gets the criteria
166
+ * following the key and returns the list of types. A type can also be a function that gets the
167
+ * argument value and returns the parsed value, or throws for an invalid one.
168
+ *
169
+ * @param {Object} server IMAP server
170
+ * @return {Object} search key to list of argument types, or to a function that returns the list
171
+ */
172
+ function getSearchKeys(server) {
173
+ const keys = Object.assign({}, searchKeys);
174
+ const pluginHandlers = server.searchHandlers;
175
+ Object.keys(pluginHandlers).forEach(key => {
176
+ if (!(key in keys)) {
177
+ const handler = pluginHandlers[key];
178
+ keys[key] = typeof handler.argumentTypes === 'function' ? handler.argumentTypes : new Array(Math.max(handler.length - 3, 0)).fill('string');
179
+ }
180
+ });
181
+ return keys;
182
+ }
183
+
184
+ /**
185
+ * Checks if SEARCH criteria refer to messages by sequence number (RFC 3501 section 5.5), that is,
186
+ * if a sequence set is used as a search key. Arguments of search keys, like the UID set of UID
187
+ * or a string that looks like a number, are not search keys.
188
+ *
189
+ * @param {Object} server IMAP server
190
+ * @param {Array} attributes Parsed SEARCH arguments, nested lists are arrays
191
+ * @return {Boolean} true if a sequence set key occurs
192
+ */
193
+ function hasSequenceSetKey(server, attributes) {
194
+ const keys = getSearchKeys(server);
195
+ const walk = list => {
196
+ // arguments of the previous key that are values, not keys
197
+ let skip = 0;
198
+ return list.some((item, i) => {
199
+ if (Array.isArray(item)) {
200
+ return walk(item);
201
+ }
202
+ if (skip) {
203
+ skip--;
204
+ return false;
205
+ }
206
+ const value = ((item && item.value) || '').toString();
207
+ const key = value.toUpperCase();
208
+ if (key === 'CHARSET' && i === 0) {
209
+ skip = 1;
210
+ } else if (Object.hasOwn(keys, key)) {
211
+ // NOT and OR take keys as arguments, these are checked as keys
212
+ let types = keys[key];
213
+ if (typeof types === 'function') {
214
+ types = types(list.slice(i + 1).map(next => (Array.isArray(next) ? next : ((next && next.value) || '').toString())));
215
+ }
216
+ skip = types.filter(type => type !== 'key').length;
217
+ } else if (/^[\d*][\d*,:]*$/.test(value)) {
218
+ return true;
219
+ }
220
+ return false;
221
+ });
222
+ };
223
+ return walk([].concat(attributes || []));
224
+ }
225
+
226
+ /**
227
+ * Searches messages. Errors thrown for the client are answered with BAD when `imapResponse` is "BAD",
228
+ * otherwise NO. SORT and THREAD send `responseCode` (a list of atoms) as the response code
229
+ *
230
+ * @param {Object} connection IMAP connection
231
+ * @param {Array} messageSource Messages of the selected mailbox, as the session sees them
232
+ * @param {Array} params Search criteria: strings, and arrays for parenthesized lists
233
+ * @param {Function} [getMessageRange] Resolves the sequence sets of the criteria, `(range, isUid)`, defaults to
234
+ * the selected mailbox of the connection. Used to search a mailbox that is not selected
235
+ * @return {Object} `{ list, numbers, keys, matches }`, the matching messages, the sequence numbers by UID,
236
+ * the set of search keys used in the criteria and a `matches(message, index)` function that checks
237
+ * a message against the same criteria later. Sequence sets in the criteria stay as they were resolved
238
+ * now, they are not evaluated again
239
+ */
240
+ module.exports = function (connection, messageSource, params, getMessageRange) {
241
+ const resolveRange = getMessageRange || ((range, isUid) => connection.getMessageRange(range, isUid));
242
+ const numbers = {};
243
+ // search keys used in the criteria, other than sequence sets
244
+ const usedKeys = new Set();
245
+ const keys = getSearchKeys(connection.server);
246
+ const pluginHandlers = connection.server.searchHandlers;
247
+
248
+ params = [].concat(params || []);
249
+
250
+ // IMAP4rev1 search strings are US-ASCII unless a CHARSET is given (RFC 3501 6.4.4). A plugin can
251
+ // fix the charset of a session with connection.searchCharset, e.g. UTF-8 after ENABLE UTF8=ACCEPT,
252
+ // or change the default with connection.defaultSearchCharset (IMAP4rev2 assumes UTF-8, RFC 9051 6.4.4)
253
+ let searchCharset = connection.searchCharset || connection.defaultSearchCharset || 'US-ASCII';
254
+
255
+ if (typeof params[0] === 'string' && params[0].toUpperCase() === 'CHARSET') {
256
+ if (connection.searchCharset) {
257
+ // RFC 9755 section 3: a CHARSET conflicts with the charset of the session
258
+ throw badError('CHARSET is not allowed, search strings are always ' + connection.searchCharset + ' in this session');
259
+ }
260
+ params.shift();
261
+ const charset = params.shift();
262
+ if (typeof charset !== 'string') {
263
+ throw badError('CHARSET expects a charset name');
264
+ }
265
+ if (charsets.indexOf(charset.toUpperCase()) < 0) {
266
+ const err = new Error('Unsupported charset ' + charset);
267
+ err.imapResponse = 'NO';
268
+ err.code = 'BADCHARSET';
269
+ err.charsets = charsets;
270
+ throw err;
271
+ }
272
+ searchCharset = charset.toUpperCase();
273
+ }
274
+
275
+ // strings must be valid in the declared charset, a client must not send 8-bit text as US-ASCII
276
+ const checkString = (key, value) => {
277
+ if (searchCharset === 'US-ASCII' && /[\u0080-\u00ff]/.test(value)) {
278
+ throw badError(key + ' argument has 8-bit characters, use CHARSET UTF-8');
279
+ }
280
+ if (searchCharset === 'UTF-8' && !isUtf8(Buffer.from(value, 'binary'))) {
281
+ throw badError(key + ' argument is not valid UTF-8');
282
+ }
283
+ return value;
284
+ };
285
+
286
+ if (!params.length) {
287
+ throw badError('SEARCH expects search criteria, empty query given');
288
+ }
289
+
290
+ // Parses one search key and its arguments from a list of criteria into a node of the query tree
291
+ const parseKey = list => {
292
+ if (!list.length) {
293
+ throw badError('Unexpected end of search criteria');
294
+ }
295
+
296
+ const param = list.shift();
297
+
298
+ if (Array.isArray(param)) {
299
+ // a parenthesized list of keys that all must match
300
+ return { key: 'AND', args: parseList([].concat(param)) };
301
+ }
302
+
303
+ const key = param.toUpperCase();
304
+
305
+ // a plugin can remove keys from a session, e.g. NEW, OLD and RECENT are not in the IMAP4rev2 grammar
306
+ if (!Object.prototype.hasOwnProperty.call(keys, key) || (connection.disabledSearchKeys && connection.disabledSearchKeys.has(key))) {
307
+ // a sequence set, plugins may support other forms of it than numbers
308
+ let range;
309
+ try {
310
+ range = resolveRange(param, false);
311
+ } catch (E) {
312
+ throw /^[\d,:*]+$/.test(param) ? E : badError('Invalid search key ' + param);
313
+ }
314
+ return { key: '_SEQ', args: [range] };
315
+ }
316
+
317
+ usedKeys.add(key);
318
+ const types = typeof keys[key] === 'function' ? keys[key](list) : keys[key];
319
+ const args = types.map(type => {
320
+ if (type === 'key') {
321
+ return parseKey(list);
322
+ }
323
+
324
+ if (!list.length || typeof list[0] !== 'string') {
325
+ throw badError(key + ' expects ' + types.length + ' argument' + (types.length > 1 ? 's' : ''));
326
+ }
327
+
328
+ const value = list.shift();
329
+ if (typeof type === 'function') {
330
+ return type(value);
331
+ }
332
+ switch (type) {
333
+ case 'date':
334
+ return parseQueryDate(value);
335
+ case 'number':
336
+ case 'number64': {
337
+ // RFC 3501 section 9: number is 32-bit. LARGER and SMALLER take a number64 in IMAP4rev2 (RFC 9051
338
+ // section 9), so in a session with connection.number64 set
339
+ const number64 = type === 'number64' && connection.number64;
340
+ if (!isNumber(value, number64 ? MAX_NUMBER64 : MAX_NUMBER)) {
341
+ throw badError(key + ' expects a number' + (number64 ? '64' : ''));
342
+ }
343
+ return Number(value);
344
+ }
345
+ case 'sequence':
346
+ return resolveRange(value, true);
347
+ default:
348
+ return checkString(key, value);
349
+ }
350
+ });
351
+
352
+ return { key, args };
353
+ };
354
+
355
+ const parseList = list => {
356
+ if (!list.length) {
357
+ throw badError('Empty search criteria list');
358
+ }
359
+ const nodes = [];
360
+ while (list.length) {
361
+ nodes.push(parseKey(list));
362
+ }
363
+ return nodes;
364
+ };
365
+
366
+ const query = { key: 'AND', args: parseList(params) };
367
+
368
+ // a sequence set argument resolves to [number, message] pairs, turn these into a lookup set
369
+ const toMessageSet = range => new Set(range.map(item => item[1]));
370
+ const prepare = node => {
371
+ if (node.key === '_SEQ' || node.key === 'UID') {
372
+ node.set = toMessageSet(node.args[0]);
373
+ }
374
+ // lower case the string to look for once, not for every message
375
+ if (['BCC', 'BODY', 'CC', 'FROM', 'SUBJECT', 'TEXT', 'TO'].indexOf(node.key) >= 0) {
376
+ node.needle = asciiLowerCase(node.args[0]);
377
+ } else if (node.key === 'HEADER') {
378
+ node.needle = asciiLowerCase(node.args[1]);
379
+ }
380
+ node.args.forEach(arg => {
381
+ if (arg && typeof arg === 'object' && arg.key) {
382
+ prepare(arg);
383
+ }
384
+ });
385
+ };
386
+ prepare(query);
387
+
388
+ const hasFlag = (message, flag) => message.flags.indexOf(flag) >= 0;
389
+
390
+ // header lines as [lowercase name, unfolded value]
391
+ const getHeaders = message =>
392
+ (getMessageData(message).tree.header || []).map(line => {
393
+ const parts = line.split(':');
394
+ return [(parts.shift() || '').trim().toLowerCase(), parts.join(':').replace(/\r?\n(?=[ \t])/g, '')];
395
+ });
396
+
397
+ const matchHeader = (message, name, needle) => {
398
+ name = name.toLowerCase();
399
+ return getHeaders(message).some(header => header[0] === name && contains(header[1], needle));
400
+ };
401
+
402
+ const matches = (node, message, index) => {
403
+ const args = node.args;
404
+ if (Object.prototype.hasOwnProperty.call(pluginHandlers, node.key)) {
405
+ // plugin defined search key, which may also override a built-in one
406
+ return !!pluginHandlers[node.key].apply(null, [connection, message, index].concat(args));
407
+ }
408
+ switch (node.key) {
409
+ case 'AND':
410
+ return args.every(arg => matches(arg, message, index));
411
+ case '_SEQ':
412
+ case 'UID':
413
+ return node.set.has(message);
414
+ case 'ALL':
415
+ return true;
416
+ case 'ANSWERED':
417
+ return hasFlag(message, '\\Answered');
418
+ case 'BCC':
419
+ case 'CC':
420
+ case 'FROM':
421
+ case 'SUBJECT':
422
+ case 'TO':
423
+ return matchHeader(message, node.key, node.needle);
424
+ case 'HEADER':
425
+ return matchHeader(message, args[0], node.needle);
426
+ case 'BEFORE': {
427
+ const date = getInternalDate(message);
428
+ return !!date && date < args[0];
429
+ }
430
+ case 'ON':
431
+ return getInternalDate(message) === args[0];
432
+ case 'SINCE': {
433
+ const date = getInternalDate(message);
434
+ return !!date && date >= args[0];
435
+ }
436
+ case 'SENTBEFORE': {
437
+ const date = getSentDate(message);
438
+ return !!date && date < args[0];
439
+ }
440
+ case 'SENTON':
441
+ return getSentDate(message) === args[0];
442
+ case 'SENTSINCE': {
443
+ const date = getSentDate(message);
444
+ return !!date && date >= args[0];
445
+ }
446
+ case 'BODY':
447
+ return contains(render(getMessageData(message).tree, true), node.needle);
448
+ case 'TEXT':
449
+ return contains(getMessageData(message).raw, node.needle);
450
+ case 'DELETED':
451
+ return hasFlag(message, '\\Deleted');
452
+ case 'DRAFT':
453
+ return hasFlag(message, '\\Draft');
454
+ case 'FLAGGED':
455
+ return hasFlag(message, '\\Flagged');
456
+ case 'KEYWORD':
457
+ return hasFlag(message, args[0]);
458
+ case 'LARGER':
459
+ return getMessageData(message).raw.length > args[0];
460
+ case 'SMALLER':
461
+ return getMessageData(message).raw.length < args[0];
462
+ // \Recent is a session flag, see IMAPConnection#isRecent
463
+ case 'NEW':
464
+ return connection.isRecent(message) && !hasFlag(message, '\\Seen');
465
+ case 'OLD':
466
+ return !connection.isRecent(message);
467
+ case 'RECENT':
468
+ return connection.isRecent(message);
469
+ case 'SEEN':
470
+ return hasFlag(message, '\\Seen');
471
+ case 'NOT':
472
+ return !matches(args[0], message, index);
473
+ case 'OR':
474
+ return matches(args[0], message, index) || matches(args[1], message, index);
475
+ case 'UNANSWERED':
476
+ return !hasFlag(message, '\\Answered');
477
+ case 'UNDELETED':
478
+ return !hasFlag(message, '\\Deleted');
479
+ case 'UNDRAFT':
480
+ return !hasFlag(message, '\\Draft');
481
+ case 'UNFLAGGED':
482
+ return !hasFlag(message, '\\Flagged');
483
+ case 'UNKEYWORD':
484
+ return !hasFlag(message, args[0]);
485
+ case 'UNSEEN':
486
+ return !hasFlag(message, '\\Seen');
487
+ default:
488
+ return false;
489
+ }
490
+ };
491
+
492
+ // plugins can narrow down the messages that are looked at (server.searchLimits), sequence numbers stay as they are
493
+ const searched = connection.server.searchLimits.reduce((messages, limit) => limit(connection, messages, query) || messages, messageSource);
494
+ const searchedSet = searched !== messageSource && new Set(searched);
495
+
496
+ const list = [];
497
+ messageSource.forEach((message, i) => {
498
+ if (searchedSet && !searchedSet.has(message)) {
499
+ return;
500
+ }
501
+ if (matches(query, message, i + 1)) {
502
+ numbers[message.uid] = i + 1;
503
+ list.push(message);
504
+ }
505
+ });
506
+
507
+ return {
508
+ list,
509
+ numbers,
510
+ keys: usedKeys,
511
+ matches: (message, index) => matches(query, message, index)
512
+ };
513
+ };
514
+
515
+ module.exports.hasSequenceSetKey = hasSequenceSetKey;
516
+ module.exports.getDateKey = getDateKey;
517
+ module.exports.badError = badError;
518
+ module.exports.criteriaValues = criteriaValues;
519
+ module.exports.sendSearchError = sendSearchError;
@@ -0,0 +1,85 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * STATUS data items, shared by the STATUS command and the STATUS return option of LIST (LIST-STATUS,
5
+ * RFC 5819). A STATUS item can only be requested when it is listed in `server.allowedStatus`.
6
+ * Plugins add their items to `server.statusHandlers` (consulted before the built-in items) and to
7
+ * `server.allowedStatus`.
8
+ */
9
+
10
+ const statusHandlers = {
11
+ MESSAGES: (connection, mailbox) => mailbox.messages.length,
12
+ RECENT: (connection, mailbox, status) => status.recent,
13
+ UIDNEXT: (connection, mailbox) => mailbox.uidnext,
14
+ UIDVALIDITY: (connection, mailbox) => mailbox.uidvalidity,
15
+ UNSEEN: (connection, mailbox, status) => status.unseen || 0,
16
+ // RFC 9051 section 6.3.11. Not in RFC 3501, QUOTA lists it in allowedStatus, IMAP4rev2 allows it per session
17
+ DELETED: (connection, mailbox, status) => status.flags['\\Deleted'] || 0
18
+ };
19
+
20
+ // RFC 3501 section 9 atom = 1*ATOM-CHAR
21
+
22
+ /**
23
+ * Validates a list of STATUS data item names (RFC 3501 section 9, "(" status-att *(SP status-att) ")")
24
+ *
25
+ * @param {Object} server IMAPServer instance
26
+ * @param {Array} list Parsed list of status items
27
+ * @param {Object} [connection] IMAP connection, its `disabledStatusItems` set lists items the session can not use,
28
+ * `addedStatusItems` the items only this session can use (DELETED after ENABLE IMAP4rev2)
29
+ * @return {Array} upper case item names
30
+ * @throws {Error} if the list is empty or has an invalid item, the message is for the BAD response
31
+ */
32
+ function parseStatusItems(server, list, connection) {
33
+ if (!Array.isArray(list) || !list.length) {
34
+ throw new Error('Expecting a list of status items');
35
+ }
36
+ const disabled = connection && connection.disabledStatusItems;
37
+ const added = connection && connection.addedStatusItems;
38
+ return list.map((item, i) => {
39
+ const name = item && item.type === 'ATOM' && item.value.toUpperCase();
40
+ if (!name || (server.allowedStatus.indexOf(name) < 0 && !(added && added.has(name))) || (disabled && disabled.has(name))) {
41
+ throw new Error('Invalid status element (' + (i + 1) + ')');
42
+ }
43
+ return name;
44
+ });
45
+ }
46
+
47
+ /**
48
+ * Sends an untagged STATUS response
49
+ *
50
+ * @param {Object} connection IMAPConnection instance
51
+ * @param {String} path Storage name of the mailbox
52
+ * @param {Object} mailbox Mailbox object
53
+ * @param {Array} items Upper case item names from parseStatusItems
54
+ * @param {Object} parsed Parsed command
55
+ * @param {Object} data Command data
56
+ */
57
+ function sendStatus(connection, path, mailbox, items, parsed, data) {
58
+ connection.send(statusResponse(connection, path, mailbox, items), 'STATUS', parsed, data);
59
+ }
60
+
61
+ /**
62
+ * Builds an untagged STATUS response
63
+ *
64
+ * @param {Object} connection IMAPConnection instance
65
+ * @param {String} path Storage name of the mailbox
66
+ * @param {Object} mailbox Mailbox object
67
+ * @param {Array} items Upper case item names
68
+ * @return {Object} STATUS response
69
+ */
70
+ function statusResponse(connection, path, mailbox, items) {
71
+ const server = connection.server;
72
+ const status = server.getStatus(mailbox);
73
+ const list = [];
74
+ items.forEach(item => {
75
+ list.push({ type: 'ATOM', value: item }, (server.statusHandlers[item] || statusHandlers[item])(connection, mailbox, status));
76
+ });
77
+ return {
78
+ tag: '*',
79
+ command: 'STATUS',
80
+ // the mailbox name is converted for the session in IMAPConnection#send
81
+ attributes: [{ type: 'MAILBOX', value: path }, list]
82
+ };
83
+ }
84
+
85
+ module.exports = { parseStatusItems, sendStatus, statusResponse };