imapkit 0.0.0-stage → 4.0.1

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,525 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @help Adds QRESYNC [RFC7162] capability, loads CONDSTORE and ENABLE as well
5
+ * @help After ENABLE QRESYNC: SELECT/EXAMINE (QRESYNC (...)), UID FETCH (CHANGEDSINCE n VANISHED),
6
+ * @help and VANISHED responses instead of EXPUNGE
7
+ */
8
+
9
+ const fetchHandlers = require('../commands/handlers/fetch');
10
+ const { isExpungeNotification, toVanished } = require('../vanished');
11
+ const { MAX_NUMBER64, isNumber, isNzNumber, isSequenceSet } = require('../numbers');
12
+ const { isAtom } = require('../arguments');
13
+ const { registerEnable, isEnabled } = require('./enable');
14
+
15
+ // commands that report expunged messages, their tagged OK carries the new HIGHESTMODSEQ (RFC 7162 sections 3.2.7
16
+ // and 3.2.9, RFC 6851 section 4.4). CLOSE MUST NOT (RFC 7162 section 3.2.8)
17
+ // REPLACE removes the replaced message as if with UID EXPUNGE (RFC 8508 section 4.5)
18
+ const EXPUNGING_COMMANDS = new Set(['EXPUNGE', 'UID EXPUNGE', 'MOVE', 'UID MOVE', 'REPLACE', 'UID REPLACE']);
19
+
20
+ module.exports = function (server) {
21
+ server.registerCapability('QRESYNC');
22
+ registerEnable(server, 'QRESYNC');
23
+
24
+ // RFC 8437 section 4.1: UNAUTHENTICATE ends what ENABLE QRESYNC enabled (the ENABLE plugin clears the list)
25
+ server.resetHandlers.push(connection => {
26
+ connection.qresyncExpunged = false;
27
+ });
28
+
29
+ // mod-sequence each expunged message was removed with
30
+ const expungeModseq = new WeakMap();
31
+
32
+ const isQresync = connection => isEnabled(connection, 'QRESYNC');
33
+ const { isEnabled: isCondstore, getHighestModseq } = server.condstore;
34
+
35
+ // Expunged UID ranges with their mod-sequences, oldest first (RFC 7162 section 5.3), and the mod-sequence after which
36
+ // all expunges are remembered. Messages missing from the initial storage were expunged at an unknown time
37
+ // before. The record belongs to one UIDVALIDITY, as nothing needs to be kept once it changes
38
+ const getExpunged = (mailbox, since) => {
39
+ if (!mailbox.qresyncExpunged || mailbox.qresyncExpunged.uidvalidity !== mailbox.uidvalidity) {
40
+ mailbox.qresyncExpunged = { uidvalidity: mailbox.uidvalidity, since: since || getHighestModseq(mailbox), list: [] };
41
+ }
42
+ return mailbox.qresyncExpunged;
43
+ };
44
+
45
+ // start remembering expunges for the mailboxes of the storage once their messages have mod-sequences
46
+ const indexFolders = server.indexFolders;
47
+ server.indexFolders = function () {
48
+ const result = indexFolders.apply(this, arguments);
49
+ Object.keys(this.folderCache).forEach(path => getExpunged(this.folderCache[path]));
50
+ return result;
51
+ };
52
+
53
+ // CONDSTORE has already incremented HIGHESTMODSEQ for this removal, the expunged UIDs get that value
54
+ // (RFC 7162 section 3.2)
55
+ server.on('expunge', (mailbox, messages, connection) => {
56
+ const modseq = getHighestModseq(mailbox);
57
+ const list = getExpunged(mailbox, modseq - 1).list;
58
+ messages.forEach(message => {
59
+ // consecutive UIDs expunged together are kept as one range, so the history stays small even in long test
60
+ // runs. It is never trimmed: the server keeps everything since it started (RFC 7162 section 5.3)
61
+ const last = list[list.length - 1];
62
+ if (last && last.modseq === modseq && last.to + 1 === message.uid) {
63
+ last.to = message.uid;
64
+ } else {
65
+ list.push({ from: message.uid, to: message.uid, modseq });
66
+ }
67
+ expungeModseq.set(message, modseq);
68
+ });
69
+ if (connection) {
70
+ connection.qresyncExpunged = true;
71
+ }
72
+ });
73
+
74
+ /**
75
+ * Lists the UIDs of a set that were expunged after a mod-sequence (RFC 7162 sections 3.2.5.1 and 3.2.6)
76
+ *
77
+ * @param {Object} mailbox Mailbox object
78
+ * @param {Array} messages Messages of the mailbox as the session sees them, these are not reported
79
+ * @param {Array} ranges Ascending UID ranges as [from, to] pairs
80
+ * @param {Number} modseq Mod-sequence the client knows
81
+ * @param {Number} [knownUpTo] The client knows about all expunges up to this UID (message sequence match data)
82
+ * @return {String} sequence set of the UIDs, empty if there are none
83
+ */
84
+ const getVanished = (mailbox, messages, ranges, modseq, knownUpTo) => {
85
+ const { since, list } = getExpunged(mailbox);
86
+
87
+ let candidates;
88
+ if (modseq >= since) {
89
+ // all expunges after the client's mod-sequence are remembered. Message sequence match data only helps
90
+ // when they are not, so it is not used here (RFC 7162 section 5.3)
91
+ candidates = intersectRanges(mergeRanges(list.filter(entry => entry.modseq > modseq).map(entry => [entry.from, entry.to])), ranges);
92
+ } else {
93
+ // RFC 7162 section 3.2.6: for a mod-sequence older than the remembered expunges, the server MUST behave as
94
+ // if asked to report all expunged messages of the set: every UID below UIDNEXT that is not in the mailbox,
95
+ // except what the sequence match data shows the client knows
96
+ candidates = intersectRanges(ranges, [[(knownUpTo || 0) + 1, mailbox.uidnext - 1]]);
97
+ }
98
+
99
+ // messages the session still sees are left out, an expunge that the session has not been told about yet is
100
+ // reported with VANISHED without EARLIER
101
+ return subtractUids(candidates, messages)
102
+ .map(range => (range[0] === range[1] ? String(range[0]) : range[0] + ':' + range[1]))
103
+ .join(',');
104
+ };
105
+
106
+ const sendBad = (connection, parsed, data, callback, message) => {
107
+ connection.sendStatus(parsed, data, 'BAD', message, false, 'QRESYNC FAILED');
108
+ return callback();
109
+ };
110
+
111
+ // Wraps an existing command handler. Commands that are not available are left alone
112
+ const wrapHandler = (command, wrapper) => {
113
+ const prevHandler = server.getCommandHandler(command);
114
+ if (!prevHandler) {
115
+ return;
116
+ }
117
+ server.setCommandHandler(command, (connection, parsed, data, callback) => wrapper(prevHandler, connection, parsed, data, callback));
118
+ };
119
+
120
+ // RFC 7162 section 3.2.5: SELECT/EXAMINE mailbox (QRESYNC (uidvalidity mod-sequence-value [known-uids] [seq-match-data]))
121
+ const selectWrapper = (prevHandler, connection, parsed, data, callback) => {
122
+ const params = parsed.attributes && parsed.attributes[1];
123
+ const isQresyncParam = param => isAtom(param, 'QRESYNC');
124
+ const position = Array.isArray(params) ? params.findIndex(isQresyncParam) : -1;
125
+ if (position < 0) {
126
+ return prevHandler(connection, parsed, data, callback);
127
+ }
128
+
129
+ if (!isQresync(connection)) {
130
+ return sendBad(connection, parsed, data, callback, 'QRESYNC parameter requires ENABLE QRESYNC');
131
+ }
132
+ if (params.findLastIndex(isQresyncParam) !== position) {
133
+ return sendBad(connection, parsed, data, callback, 'QRESYNC parameter can be used only once');
134
+ }
135
+
136
+ let qresync;
137
+ try {
138
+ qresync = parseQresyncParam(params[position + 1]);
139
+ } catch (err) {
140
+ return sendBad(connection, parsed, data, callback, err.message);
141
+ }
142
+
143
+ params.splice(position, 2);
144
+ if (!params.length) {
145
+ parsed.attributes.splice(1, 1);
146
+ }
147
+ parsed.qresync = qresync;
148
+
149
+ prevHandler(connection, parsed, data, callback);
150
+ };
151
+
152
+ wrapHandler('SELECT', selectWrapper);
153
+ wrapHandler('EXAMINE', selectWrapper);
154
+
155
+ // RFC 7162 section 3.2.6: the VANISHED UID FETCH modifier
156
+ const fetchWrapper = (isUid, prevHandler, connection, parsed, data, callback) => {
157
+ const modifiers = parsed.attributes && parsed.attributes[2];
158
+ const isVanished = modifier => isAtom(modifier, 'VANISHED');
159
+ const position = Array.isArray(modifiers) ? modifiers.findIndex(isVanished) : -1;
160
+ if (position < 0) {
161
+ return prevHandler(connection, parsed, data, callback);
162
+ }
163
+
164
+ if (!isUid) {
165
+ return sendBad(connection, parsed, data, callback, 'VANISHED is only allowed with UID FETCH');
166
+ }
167
+ if (!isQresync(connection)) {
168
+ return sendBad(connection, parsed, data, callback, 'VANISHED requires ENABLE QRESYNC');
169
+ }
170
+ if (modifiers.findLastIndex(isVanished) !== position) {
171
+ return sendBad(connection, parsed, data, callback, 'VANISHED can be used only once');
172
+ }
173
+ if (!modifiers.some(modifier => isAtom(modifier, 'CHANGEDSINCE'))) {
174
+ return sendBad(connection, parsed, data, callback, 'VANISHED requires CHANGEDSINCE');
175
+ }
176
+
177
+ modifiers.splice(position, 1);
178
+ parsed.qresyncVanished = true;
179
+
180
+ prevHandler(connection, parsed, data, callback);
181
+ };
182
+
183
+ wrapHandler('FETCH', fetchWrapper.bind(null, false));
184
+ wrapHandler('UID FETCH', fetchWrapper.bind(null, true));
185
+
186
+ // RFC 7162 section 3.2.10: after ENABLE QRESYNC, expunges are reported with VANISHED instead of EXPUNGE.
187
+ // Consecutive EXPUNGE notifications become one VANISHED response
188
+ server.connectionHandlers.push(connection => {
189
+ const prepareNotifications = connection.prepareNotifications;
190
+ connection.prepareNotifications = function (queue) {
191
+ queue = prepareNotifications.call(this, queue);
192
+ return isQresync(this) ? toVanished(queue) : queue;
193
+ };
194
+ });
195
+
196
+ // Sends a VANISHED (EARLIER) response, if there is anything to report
197
+ const sendVanishedEarlier = (connection, uids, parsed, data) => {
198
+ if (!uids) {
199
+ return;
200
+ }
201
+ connection.send(
202
+ {
203
+ tag: '*',
204
+ command: 'VANISHED',
205
+ attributes: [[{ type: 'ATOM', value: 'EARLIER' }], { type: 'SEQUENCE', value: uids }]
206
+ },
207
+ 'VANISHED EARLIER',
208
+ parsed,
209
+ data
210
+ );
211
+ };
212
+
213
+ // RFC 7162 section 3.2.5.1: after SELECT/EXAMINE (QRESYNC), expunges and flag changes since the given mod-sequence
214
+ const sendQresyncChanges = (connection, parsed, data) => {
215
+ const mailbox = connection.selectedMailbox;
216
+ const qresync = parsed.qresync;
217
+ if (qresync.uidvalidity !== mailbox.uidvalidity) {
218
+ // RFC 7162 section 3.2.5: ignore the remaining parameters if UIDVALIDITY does not match
219
+ return;
220
+ }
221
+
222
+ // without known UIDs, the client is treated as if it sent 1:<UIDNEXT - 1>, nothing for an unused mailbox
223
+ const ranges = qresync.knownUids || (mailbox.uidnext > 1 ? [[1, mailbox.uidnext - 1]] : []);
224
+
225
+ // RFC 7162 section 3.2.5.2: the last pair of sequence number and UID that still match tells up to which
226
+ // UID the client knows about all expunges, the first pair that does not match ends the comparison
227
+ let knownUpTo = 0;
228
+ if (qresync.seqMatch) {
229
+ const uids = iterateRanges(qresync.seqMatch.uids);
230
+ for (const seq of iterateRanges(qresync.seqMatch.sequences)) {
231
+ const uid = uids.next().value;
232
+ const message = mailbox.messages[seq - 1];
233
+ if (!message || message.uid !== uid) {
234
+ break;
235
+ }
236
+ knownUpTo = uid;
237
+ }
238
+ }
239
+
240
+ // VANISHED (EARLIER) MUST come before the FETCH responses (RFC 7162 section 3.2.6)
241
+ sendVanishedEarlier(connection, getVanished(mailbox, mailbox.messages, ranges, qresync.modseq, knownUpTo), parsed, data);
242
+
243
+ const getFlags = server.fetchHandlers.FLAGS || fetchHandlers.FLAGS;
244
+ mailbox.messages.forEach((message, i) => {
245
+ if (message.MODSEQ <= qresync.modseq || !inRanges(ranges, message.uid)) {
246
+ return;
247
+ }
248
+ // flag changes MUST include the UID (RFC 7162 section 3.2.5.1)
249
+ connection.send(
250
+ {
251
+ tag: '*',
252
+ attributes: [
253
+ i + 1,
254
+ { type: 'ATOM', value: 'FETCH' },
255
+ [
256
+ { type: 'ATOM', value: 'UID' },
257
+ message.uid,
258
+ { type: 'ATOM', value: 'FLAGS' },
259
+ getFlags(connection, message, { type: 'ATOM', value: 'FLAGS' }),
260
+ { type: 'ATOM', value: 'MODSEQ' },
261
+ [message.MODSEQ]
262
+ ]
263
+ ]
264
+ },
265
+ 'QRESYNC FETCH',
266
+ parsed,
267
+ data,
268
+ message
269
+ );
270
+ });
271
+ };
272
+
273
+ const highestModseqCode = modseq => ({ type: 'SECTION', section: [{ type: 'ATOM', value: 'HIGHESTMODSEQ' }, modseq] });
274
+
275
+ server.outputHandlers.push((connection, response, description, parsed, data) => {
276
+ if (!parsed || !response || response.tag === '+') {
277
+ return;
278
+ }
279
+ const isTagged = response.tag !== '*' && response.tag === parsed.tag;
280
+
281
+ // UID FETCH (VANISHED): VANISHED (EARLIER) comes before the first FETCH response, or before the tagged OK
282
+ if (parsed.qresyncVanished && ((response.tag === '*' && description === 'UID FETCH') || (isTagged && response.command === 'OK'))) {
283
+ parsed.qresyncVanished = false;
284
+ const mailbox = connection.selectedMailbox;
285
+ const ranges = parseUidSet(parsed.attributes[0].value, mailbox.uidnext - 1);
286
+ if (ranges) {
287
+ sendVanishedEarlier(connection, getVanished(mailbox, connection.getSessionMessages(), ranges, parsed.changedsince), parsed, data);
288
+ }
289
+ return;
290
+ }
291
+
292
+ if (!isTagged) {
293
+ return;
294
+ }
295
+
296
+ if (parsed.qresync && response.command === 'OK' && (description === 'SELECT' || description === 'EXAMINE') && connection.selectedMailbox) {
297
+ sendQresyncChanges(connection, parsed, data);
298
+ return;
299
+ }
300
+
301
+ const expunged = connection.qresyncExpunged;
302
+ connection.qresyncExpunged = false;
303
+
304
+ // HIGHESTMODSEQ in the tagged OK once messages were expunged, RFC 7162 sections 3.2.7 and 3.2.9 require it
305
+ // with QRESYNC enabled, RFC 6851 section 4.4 for MOVE
306
+ if (
307
+ expunged &&
308
+ response.command === 'OK' &&
309
+ EXPUNGING_COMMANDS.has(String(parsed.command).toUpperCase()) &&
310
+ connection.selectedMailbox &&
311
+ isCondstore(connection)
312
+ ) {
313
+ const hasCode = response.attributes && response.attributes[0] && response.attributes[0].type === 'SECTION';
314
+ if (!hasCode) {
315
+ response.attributes = [highestModseqCode(getHighestModseq(connection.selectedMailbox))].concat(response.attributes || []);
316
+ }
317
+ return;
318
+ }
319
+
320
+ // RFC 7162 section 3.2: while expunges are held back (FETCH, STORE, SEARCH), the client must not take a
321
+ // MODSEQ it got as the new HIGHESTMODSEQ, so tell it a value below the mod-sequence of the pending expunges
322
+ if (isCondstore(connection)) {
323
+ let lowest = 0;
324
+ connection.notificationQueue.forEach(notification => {
325
+ const modseq = isExpungeNotification(notification) && expungeModseq.get(notification.message);
326
+ if (modseq && (!lowest || modseq < lowest)) {
327
+ lowest = modseq;
328
+ }
329
+ });
330
+ if (lowest > 1) {
331
+ connection.send(
332
+ {
333
+ tag: '*',
334
+ command: 'OK',
335
+ attributes: [highestModseqCode(lowest - 1), { type: 'TEXT', value: 'Expunges not reported yet' }]
336
+ },
337
+ 'QRESYNC HIGHESTMODSEQ',
338
+ parsed,
339
+ data
340
+ );
341
+ }
342
+ }
343
+ });
344
+ };
345
+
346
+ /**
347
+ * Parses a sequence set without "*" into [from, to] ranges as written
348
+ *
349
+ * @param {Object} value Parsed argument
350
+ * @param {String} name Name of the argument for the error message
351
+ * @return {Array} ranges
352
+ */
353
+ function parseSequenceSet(value, name) {
354
+ if (!value || Array.isArray(value) || ['ATOM', 'SEQUENCE'].indexOf(value.type) < 0 || !isSequenceSet(value.value, true)) {
355
+ throw new Error('Invalid QRESYNC ' + name + ', a sequence set without "*" expected');
356
+ }
357
+ return value.value.split(',').map(part => {
358
+ const range = part.split(':').map(Number);
359
+ return [range[0], range.length > 1 ? range[1] : range[0]];
360
+ });
361
+ }
362
+
363
+ /**
364
+ * Parses the UID set of UID FETCH, "*" stands for the highest UID the mailbox may have used
365
+ *
366
+ * @param {String} value Sequence set
367
+ * @param {Number} star Value for "*"
368
+ * @return {Array|false} ranges ordered as [low, high], or false for other forms (e.g. "$" of SEARCHRES)
369
+ */
370
+ function parseUidSet(value, star) {
371
+ if (!/^[\d*:,]+$/.test(String(value))) {
372
+ return false;
373
+ }
374
+ return mergeRanges(
375
+ String(value)
376
+ .split(',')
377
+ .map(part => part.split(':').map(number => (number === '*' ? star : Number(number))))
378
+ );
379
+ }
380
+
381
+ // Intersection of two lists of ascending, non-overlapping ranges
382
+ function intersectRanges(a, b) {
383
+ const result = [];
384
+ let i = 0;
385
+ let j = 0;
386
+ while (i < a.length && j < b.length) {
387
+ const from = Math.max(a[i][0], b[j][0]);
388
+ const to = Math.min(a[i][1], b[j][1]);
389
+ if (from <= to) {
390
+ result.push([from, to]);
391
+ }
392
+ if (a[i][1] < b[j][1]) {
393
+ i++;
394
+ } else {
395
+ j++;
396
+ }
397
+ }
398
+ return result;
399
+ }
400
+
401
+ // Removes the UIDs of messages (ordered by UID) from ascending ranges, walking both lists once
402
+ function subtractUids(ranges, messages) {
403
+ const result = [];
404
+ let i = 0;
405
+ ranges.forEach(([from, to]) => {
406
+ let next = from;
407
+ while (i < messages.length && messages[i].uid < from) {
408
+ i++;
409
+ }
410
+ for (; i < messages.length && messages[i].uid <= to; i++) {
411
+ if (messages[i].uid > next) {
412
+ result.push([next, messages[i].uid - 1]);
413
+ }
414
+ next = messages[i].uid + 1;
415
+ }
416
+ if (next <= to) {
417
+ result.push([next, to]);
418
+ }
419
+ });
420
+ return result;
421
+ }
422
+
423
+ /**
424
+ * Orders the ends of every range and merges overlapping or adjacent ranges, so that a UID is listed once
425
+ *
426
+ * @param {Array} ranges List of [from, to] or [number] ranges
427
+ * @return {Array} ascending [low, high] ranges
428
+ */
429
+ function mergeRanges(ranges) {
430
+ const sorted = ranges.map(range => [Math.min(...range), Math.max(...range)]).sort((a, b) => a[0] - b[0]);
431
+ const result = [];
432
+ sorted.forEach(range => {
433
+ const last = result[result.length - 1];
434
+ if (last && range[0] <= last[1] + 1) {
435
+ last[1] = Math.max(last[1], range[1]);
436
+ } else {
437
+ result.push(range);
438
+ }
439
+ });
440
+ return result;
441
+ }
442
+
443
+ // Lists the numbers of ascending ranges one by one
444
+ function* iterateRanges(ranges) {
445
+ for (const [from, to] of ranges) {
446
+ for (let i = from; i <= to; i++) {
447
+ yield i;
448
+ }
449
+ }
450
+ }
451
+
452
+ /**
453
+ * Checks that a sequence set lists its numbers in ascending order and counts them
454
+ */
455
+ function countAscending(ranges, name) {
456
+ let last = 0;
457
+ let count = 0;
458
+ ranges.forEach(([from, to]) => {
459
+ if (from > to || from <= last) {
460
+ throw new Error('Invalid QRESYNC ' + name + ', numbers must be in ascending order');
461
+ }
462
+ last = to;
463
+ count += to - from + 1;
464
+ });
465
+ return count;
466
+ }
467
+
468
+ /**
469
+ * Validates the value of the QRESYNC SELECT/EXAMINE parameter (RFC 7162 section 7):
470
+ * "(" uidvalidity SP mod-sequence-value [SP known-uids] [SP seq-match-data] ")"
471
+ *
472
+ * @param {Array} value Parsed parameter value
473
+ * @return {Object} `{ uidvalidity, modseq, knownUids, seqMatch }`
474
+ */
475
+ function parseQresyncParam(value) {
476
+ if (!Array.isArray(value) || value.length < 2 || value.length > 4) {
477
+ throw new Error('QRESYNC expects (uidvalidity modseq [known-uids] [(known-sequence-set known-uid-set)])');
478
+ }
479
+
480
+ const [uidvalidity, modseq] = value;
481
+ // uidvalidity = nz-number
482
+ if (!isAtom(uidvalidity) || !isNzNumber(uidvalidity.value)) {
483
+ throw new Error('Invalid QRESYNC uidvalidity, a non-zero number expected');
484
+ }
485
+ // mod-sequence-value = 1*DIGIT, 1 <= n <= 9,223,372,036,854,775,807
486
+ if (!isAtom(modseq) || !isNumber(modseq.value, MAX_NUMBER64) || BigInt(modseq.value) < 1n) {
487
+ throw new Error('Invalid QRESYNC mod-sequence, a positive number expected');
488
+ }
489
+
490
+ const result = {
491
+ uidvalidity: Number(uidvalidity.value),
492
+ modseq: Number(modseq.value),
493
+ knownUids: false,
494
+ seqMatch: false
495
+ };
496
+
497
+ const rest = value.slice(2);
498
+ if (rest.length && !Array.isArray(rest[0])) {
499
+ result.knownUids = mergeRanges(parseSequenceSet(rest.shift(), 'known-uids'));
500
+ }
501
+
502
+ if (rest.length) {
503
+ const seqMatch = rest.shift();
504
+ if (!Array.isArray(seqMatch) || seqMatch.length !== 2 || rest.length) {
505
+ throw new Error('Invalid QRESYNC parameters, (known-sequence-set known-uid-set) expected last');
506
+ }
507
+ // RFC 7162 section 3.2.5.2: both sets MUST be in ascending order, the Nth number of one matches the Nth of the other
508
+ const sequences = parseSequenceSet(seqMatch[0], 'known-sequence-set');
509
+ const uids = parseSequenceSet(seqMatch[1], 'known-uid-set');
510
+ const count = countAscending(sequences, 'known-sequence-set');
511
+ if (countAscending(uids, 'known-uid-set') !== count) {
512
+ throw new Error('Invalid QRESYNC sets, known-sequence-set and known-uid-set must have as many numbers');
513
+ }
514
+ result.seqMatch = { sequences, uids };
515
+ }
516
+
517
+ return result;
518
+ }
519
+
520
+ function inRanges(ranges, uid) {
521
+ return ranges.some(range => uid >= range[0] && uid <= range[1]);
522
+ }
523
+
524
+ // RFC 7162 section 3.2.3: a QRESYNC server implements CONDSTORE and ENABLE, these are loaded first
525
+ module.exports.requires = ['ENABLE', 'CONDSTORE'];