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,76 @@
1
+ 'use strict';
2
+
3
+ const { states } = require('../command-states');
4
+ const DeflateLayer = require('../deflate-layer');
5
+
6
+ /**
7
+ * @help Adds COMPRESS=DEFLATE [RFC4978] capability
8
+ * @help Raw DEFLATE in both directions after the tagged OK,
9
+ * @help every burst of responses ends with a sync flush
10
+ */
11
+
12
+ module.exports = function (server) {
13
+ server.registerCapability('COMPRESS=DEFLATE');
14
+
15
+ const isActive = connection => connection.transport instanceof DeflateLayer && connection.transport.active;
16
+
17
+ // RFC 8437 section 4.1: after UNAUTHENTICATE the server ends its compression after the CRLF of
18
+ // the OK, and the client after the CRLF of the command, so anything it compressed later is dropped
19
+ server.resetHandlers.push(connection => {
20
+ if (isActive(connection)) {
21
+ connection.discardInput();
22
+ connection.transport.endInput();
23
+ connection.transport.end();
24
+ }
25
+ });
26
+
27
+ server.setCommandHandler(
28
+ 'COMPRESS',
29
+ (connection, parsed, data, callback) => {
30
+ const args = parsed.attributes || [];
31
+
32
+ // RFC 4978 section 5: compress = "COMPRESS" SP algorithm, algorithm = "DEFLATE"
33
+ if (args.length !== 1 || !args[0] || args[0].type !== 'ATOM') {
34
+ connection.sendStatus(parsed, data, 'BAD', 'COMPRESS expects a compression mechanism');
35
+ return callback();
36
+ }
37
+
38
+ if (args[0].value.toUpperCase() !== 'DEFLATE') {
39
+ connection.sendStatus(parsed, data, 'BAD', 'Unknown compression mechanism');
40
+ return callback();
41
+ }
42
+
43
+ // RFC 4978 section 3: BAD if COMPRESS is already active, NO is for compression by another
44
+ // layer, such as TLS, which ImapKit never negotiates. Dovecot answers NO here
45
+ if (isActive(connection)) {
46
+ connection.sendStatus(parsed, data, 'BAD', 'DEFLATE active via COMPRESS', 'COMPRESSIONACTIVE');
47
+ return callback();
48
+ }
49
+
50
+ connection.sendStatus(parsed, data, 'OK', 'DEFLATE active');
51
+
52
+ if (connection.transport) {
53
+ // the layer of an earlier session, it passes data on as is since UNAUTHENTICATE
54
+ connection.transport.destroy();
55
+ }
56
+ // compression starts right after the CRLF of the tagged OK
57
+ connection.transport = new DeflateLayer({
58
+ writeRaw: chunk => connection.writeRaw(chunk),
59
+ onData: chunk => connection.onData(chunk),
60
+ onError: err => {
61
+ // the input can not be decompressed, so there is nothing left to talk about
62
+ if (connection.options.debug) {
63
+ console.log('COMPRESS error: %s', err.message);
64
+ }
65
+ connection.sendStatus({ tag: '*' }, false, 'BYE', 'Invalid compressed data', false, 'COMPRESS ERROR');
66
+ connection.end();
67
+ }
68
+ });
69
+
70
+ return callback();
71
+ },
72
+ // RFC 4978 section 3: the client MUST NOT send further commands until it has seen the result of COMPRESS.
73
+ // Those commands would be uncompressed, and after an OK the server expects compressed input
74
+ { states: states.AUTHENTICATED, noPipelining: true }
75
+ );
76
+ };
@@ -0,0 +1,563 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @help Adds CONDSTORE [RFC7162] capability
5
+ */
6
+
7
+ const { macros: FETCH_MACROS } = require('../commands/fetch');
8
+ const { badError } = require('../commands/handlers/search');
9
+ const { INVALID_KEYWORD_CHAR } = require('../commands/handlers/flags');
10
+ const { MAX_NUMBER64, isNumber } = require('../numbers');
11
+ const { registerEnable, isEnabled: isExtensionEnabled } = require('./enable');
12
+
13
+ module.exports = function (server) {
14
+ // Register capability, always usable
15
+ server.registerCapability('CONDSTORE');
16
+
17
+ registerEnable(server, 'CONDSTORE');
18
+
19
+ // HIGHESTMODSEQ must be a positive number, even for an empty mailbox
20
+ const getHighestModseq = mailbox => Math.max(Number(mailbox.HIGHESTMODSEQ) || 0, 1);
21
+
22
+ const bumpModseq = mailbox => {
23
+ mailbox.HIGHESTMODSEQ = getHighestModseq(mailbox) + 1;
24
+ return mailbox.HIGHESTMODSEQ;
25
+ };
26
+
27
+ // RFC 8437 section 4.1: after UNAUTHENTICATE the session behaves as if no CONDSTORE enabling command was issued
28
+ server.resetHandlers.push(connection => {
29
+ connection.condstoreEnabled = false;
30
+ });
31
+
32
+ // ENABLE QRESYNC is a CONDSTORE enabling command as well (RFC 7162 section 3.2.3)
33
+ const isEnabled = connection => !!connection.condstoreEnabled || isExtensionEnabled(connection, 'CONDSTORE') || isExtensionEnabled(connection, 'QRESYNC');
34
+
35
+ // shared with QRESYNC, which builds on CONDSTORE
36
+ server.condstore = { isEnabled, getHighestModseq };
37
+
38
+ const sendHighestModseq = (connection, mailbox, parsed, data) => {
39
+ connection.send(
40
+ {
41
+ tag: '*',
42
+ command: 'OK',
43
+ attributes: [
44
+ {
45
+ type: 'SECTION',
46
+ section: [
47
+ {
48
+ type: 'ATOM',
49
+ value: 'HIGHESTMODSEQ'
50
+ },
51
+ getHighestModseq(mailbox)
52
+ ]
53
+ },
54
+ {
55
+ type: 'TEXT',
56
+ value: 'Highest'
57
+ }
58
+ ]
59
+ },
60
+ 'CONDSTORE INFO',
61
+ parsed,
62
+ data
63
+ );
64
+ };
65
+
66
+ // Marks the session as CONDSTORE aware. The first CONDSTORE enabling command
67
+ // issued with a mailbox selected reports HIGHESTMODSEQ for that mailbox
68
+ const enableCondstore = (connection, parsed, data) => {
69
+ if (isEnabled(connection)) {
70
+ connection.condstoreEnabled = true;
71
+ return;
72
+ }
73
+ connection.condstoreEnabled = true;
74
+ if (connection.state === 'Selected' && connection.selectedMailbox) {
75
+ sendHighestModseq(connection, connection.selectedMailbox, parsed, data);
76
+ }
77
+ };
78
+
79
+ // Set modseq values when a message is created or initialized
80
+ server.messageHandlers.push((server, message, mailbox) => {
81
+ const modseq = Number(message.MODSEQ);
82
+ if (modseq > 0 && Number.isSafeInteger(modseq)) {
83
+ // keep the value from storage
84
+ message.MODSEQ = modseq;
85
+ mailbox.HIGHESTMODSEQ = Math.max(getHighestModseq(mailbox), modseq);
86
+ } else {
87
+ message.MODSEQ = bumpModseq(mailbox);
88
+ }
89
+ });
90
+
91
+ // Expunging messages (EXPUNGE, UID EXPUNGE, CLOSE, MOVE) changes the mailbox, all messages removed
92
+ // at once share the incremented mod-sequence (RFC 7162 section 3.2)
93
+ server.on('expunge', mailbox => {
94
+ bumpModseq(mailbox);
95
+ });
96
+
97
+ // RFC 7162 section 3.2.11: SELECT or EXAMINE closes the selected mailbox, a CONDSTORE server marks
98
+ // where the responses for the new mailbox start
99
+ server.closedChecks.push(() => true);
100
+
101
+ server.allowedStatus.push('HIGHESTMODSEQ');
102
+ server.statusHandlers.HIGHESTMODSEQ = (connection, mailbox) => getHighestModseq(mailbox);
103
+
104
+ // Wraps an existing command handler. Commands that are not available are left alone
105
+ const wrapHandler = (command, wrapper) => {
106
+ const prevHandler = server.getCommandHandler(command);
107
+ if (!prevHandler) {
108
+ return;
109
+ }
110
+ server.setCommandHandler(command, (connection, parsed, data, callback) => wrapper(prevHandler, connection, parsed, data, callback));
111
+ };
112
+
113
+ const sendBad = (connection, parsed, data, callback, message) => {
114
+ connection.sendStatus(parsed, data, 'BAD', message, false, 'CONDSTORE FAILED');
115
+ return callback();
116
+ };
117
+
118
+ // RFC 7162 section 7: chgsince-fetch-mod = "CHANGEDSINCE" SP mod-sequence-value (1 or more), store-modifier =/
119
+ // "UNCHANGEDSINCE" SP mod-sequence-valzer (0 or more). Both are 1*DIGIT, so not a quoted string, up to 63 bits
120
+ const isValidModseq = (value, allowZero) =>
121
+ !!value && value.type === 'ATOM' && isNumber(value.value, MAX_NUMBER64) && (allowZero || BigInt(value.value) > 0n);
122
+
123
+ const selectWrapper = (prevHandler, connection, parsed, data, callback) => {
124
+ if (hasCondstoreOption(parsed.attributes && parsed.attributes[1], parsed.attributes, 1)) {
125
+ // SELECT and EXAMINE always report HIGHESTMODSEQ, so no need to send it here
126
+ connection.condstoreEnabled = true;
127
+ parsed.condstoreOption = true;
128
+ }
129
+ prevHandler(connection, parsed, data, callback);
130
+ };
131
+
132
+ wrapHandler('SELECT', selectWrapper);
133
+ wrapHandler('EXAMINE', selectWrapper);
134
+
135
+ wrapHandler('STATUS', (prevHandler, connection, parsed, data, callback) => {
136
+ const items = parsed.attributes && parsed.attributes[1];
137
+ if (Array.isArray(items) && items.some(item => item && item.type === 'ATOM' && String(item.value).toUpperCase() === 'HIGHESTMODSEQ')) {
138
+ enableCondstore(connection, parsed, data);
139
+ }
140
+ prevHandler(connection, parsed, data, callback);
141
+ });
142
+
143
+ const fetchWrapper = (prevHandler, connection, parsed, data, callback) => {
144
+ // RFC 4466 2.4: fetch-modifiers are the last argument, a single fetch-att may take arguments of its own
145
+ const attributes = parsed.attributes || [];
146
+ const index = Math.max(attributes.length - 1, 2);
147
+ const changedsince = getCondstoreValue(attributes[index], 'CHANGEDSINCE', attributes, index);
148
+
149
+ if (changedsince) {
150
+ if (!isValidModseq(changedsince)) {
151
+ return sendBad(connection, parsed, data, callback, 'Invalid syntax for CHANGEDSINCE, number expected');
152
+ }
153
+ parsed.changedsince = Number(changedsince.value);
154
+ // CHANGEDSINCE implicitly adds the MODSEQ data item
155
+ addModseqItem(parsed);
156
+ }
157
+
158
+ if (changedsince || hasModseqItem(parsed)) {
159
+ enableCondstore(connection, parsed, data);
160
+ }
161
+
162
+ prevHandler(connection, parsed, data, callback);
163
+ };
164
+
165
+ wrapHandler('FETCH', fetchWrapper);
166
+ wrapHandler('UID FETCH', fetchWrapper);
167
+
168
+ const storeWrapper = (prevHandler, connection, parsed, data, callback) => {
169
+ const unchangedsince = getCondstoreValue(parsed.attributes && parsed.attributes[1], 'UNCHANGEDSINCE', parsed.attributes, 1);
170
+
171
+ if (unchangedsince) {
172
+ if (!isValidModseq(unchangedsince, true)) {
173
+ return sendBad(connection, parsed, data, callback, 'Invalid syntax for UNCHANGEDSINCE, number expected');
174
+ }
175
+ parsed.unchangedsince = Number(unchangedsince.value);
176
+ enableCondstore(connection, parsed, data);
177
+ }
178
+
179
+ // Per command state for tracking changed messages
180
+ parsed.condstoreStore = {
181
+ isUid: (parsed.command || '').toUpperCase() === 'UID STORE',
182
+ messages: new Map(),
183
+ modified: [],
184
+ done: false
185
+ };
186
+
187
+ prevHandler(connection, parsed, data, callback);
188
+ };
189
+
190
+ wrapHandler('STORE', storeWrapper);
191
+ wrapHandler('UID STORE', storeWrapper);
192
+
193
+ // RFC 7162 section 3.1.5: MODSEQ [<entry-name> <entry-type-req>] <mod-sequence-valzer>. Mod-sequences
194
+ // are not stored per flag, so the entry name and type are checked but otherwise ignored
195
+ const searchModseq = (connection, message, index, ...args) => message.MODSEQ >= args[args.length - 1];
196
+ searchModseq.argumentTypes = list =>
197
+ typeof list[0] === 'string' && /^\d+$/.test(list[0]) ? [parseModseqValzer] : [parseEntryName, parseEntryType, parseModseqValzer];
198
+ server.searchHandlers.MODSEQ = searchModseq;
199
+
200
+ // highest mod-sequence of a list of messages
201
+ const highestModseq = messages => messages.reduce((highest, message) => Math.max(highest, message.MODSEQ), 0);
202
+
203
+ server.fetchHandlers.MODSEQ = function (connection, message) {
204
+ return [message.MODSEQ]; // Must be a list
205
+ };
206
+
207
+ server.fetchFilters.push((connection, message, parsed, index) => {
208
+ if ('changedsince' in parsed && !(parsed.changedsince < message.MODSEQ)) {
209
+ return false;
210
+ }
211
+
212
+ // remember \Seen state, FETCH might set it implicitly
213
+ if (!parsed.condstoreFetch) {
214
+ parsed.condstoreFetch = new Map();
215
+ }
216
+ parsed.condstoreFetch.set(index, {
217
+ message,
218
+ seen: message.flags.indexOf('\\Seen') >= 0
219
+ });
220
+
221
+ return true;
222
+ });
223
+
224
+ server.storeFilters.push((connection, message, parsed, index) => {
225
+ const state = parsed.condstoreStore;
226
+ if (!state) {
227
+ return true;
228
+ }
229
+
230
+ if ('unchangedsince' in parsed && message.MODSEQ > parsed.unchangedsince) {
231
+ // failed the UNCHANGEDSINCE test, reported with the MODIFIED response code
232
+ state.modified.push(state.isUid ? message.uid : index);
233
+ return false;
234
+ }
235
+
236
+ state.messages.set(message, {
237
+ index,
238
+ before: snapshot(message),
239
+ bumped: false,
240
+ reported: false
241
+ });
242
+
243
+ return true;
244
+ });
245
+
246
+ // Bumps MODSEQ if the message metadata was changed by the current STORE
247
+ const checkStored = (connection, message, entry) => {
248
+ if (!entry.bumped && snapshot(message) !== entry.before) {
249
+ entry.bumped = true;
250
+ message.MODSEQ = bumpModseq(connection.selectedMailbox);
251
+ }
252
+ };
253
+
254
+ server.outputHandlers.push((connection, response, description, parsed, data, extra) => {
255
+ // Flag changes made by another session (RFC 7162 section 3.1), these are not responses to a command
256
+ if (description === 'FLAG NOTIFICATION' && response && extra && isEnabled(connection)) {
257
+ setModseqValue(response.attributes[2], extra.MODSEQ);
258
+ return;
259
+ }
260
+
261
+ if (!parsed || !response) {
262
+ return;
263
+ }
264
+
265
+ // Untagged FETCH responses caused by STORE
266
+ if (description === 'FLAG UPDATE' && parsed.condstoreStore && extra && parsed.condstoreStore.messages.has(extra)) {
267
+ const entry = parsed.condstoreStore.messages.get(extra);
268
+ checkStored(connection, extra, entry);
269
+ entry.reported = true;
270
+ if (isEnabled(connection)) {
271
+ setModseqValue(response.attributes[2], extra.MODSEQ);
272
+ setUidValue(response.attributes[2], extra.uid);
273
+ }
274
+ return;
275
+ }
276
+
277
+ // SEARCH, ESEARCH, SORT or THREAD with the MODSEQ search key, a CONDSTORE enabling command (RFC 7162 section 3.1).
278
+ // `extra.list` holds the messages the response is about
279
+ const isSearch = ['SEARCH', 'SORT', 'THREAD'].includes(response.command) && [response.command, 'UID ' + response.command].includes(description);
280
+ if ((isSearch || description === 'ESEARCH') && response.tag === '*' && extra && extra.keys && extra.keys.has('MODSEQ')) {
281
+ enableCondstore(connection, parsed, data);
282
+ // RFC 7162 section 3.1.9: THREAD responses are unchanged
283
+ if (extra.list.length && response.command !== 'THREAD') {
284
+ const modseq = [{ type: 'ATOM', value: 'MODSEQ' }, highestModseq(extra.list)];
285
+ // RFC 7162 sections 3.1.6 and 3.1.9 append "(MODSEQ n)" to SEARCH and SORT, RFC 4731 section 3.2 (and RFC 7162 section 3.1.10)
286
+ // adds "MODSEQ n" to ESEARCH
287
+ response.attributes.push(...(isSearch ? [modseq] : modseq));
288
+ }
289
+ return;
290
+ }
291
+
292
+ // Untagged FETCH responses of FETCH, check for implicitly set \Seen
293
+ if (
294
+ response.tag === '*' &&
295
+ parsed.condstoreFetch &&
296
+ response.attributes &&
297
+ response.attributes[1] &&
298
+ response.attributes[1].value === 'FETCH' &&
299
+ parsed.condstoreFetch.has(response.attributes[0])
300
+ ) {
301
+ const entry = parsed.condstoreFetch.get(response.attributes[0]);
302
+ const message = entry.message;
303
+ let changed = false;
304
+ if (!entry.seen && message.flags.indexOf('\\Seen') >= 0 && connection.selectedMailbox) {
305
+ entry.seen = true;
306
+ message.MODSEQ = bumpModseq(connection.selectedMailbox);
307
+ changed = true;
308
+ }
309
+ const list = response.attributes[2];
310
+ if (Array.isArray(list) && (findItem(list, 'MODSEQ') >= 0 || (changed && isEnabled(connection)))) {
311
+ setModseqValue(list, message.MODSEQ);
312
+ }
313
+ // RFC 7162 section 3.1: a FETCH that implicitly set \Seen includes UID and MODSEQ
314
+ if (Array.isArray(list) && changed && isEnabled(connection)) {
315
+ setUidValue(list, message.uid);
316
+ }
317
+ return;
318
+ }
319
+
320
+ // Tagged response of STORE
321
+ if (response.tag === parsed.tag && parsed.condstoreStore && !parsed.condstoreStore.done) {
322
+ const state = parsed.condstoreStore;
323
+ state.done = true;
324
+
325
+ state.messages.forEach((entry, message) => {
326
+ checkStored(connection, message, entry);
327
+ // .SILENT stores send no FETCH responses, CONDSTORE aware clients still need the new MODSEQ values
328
+ if (!entry.reported && isEnabled(connection) && (entry.bumped || 'unchangedsince' in parsed)) {
329
+ const attributes = [
330
+ {
331
+ type: 'ATOM',
332
+ value: 'MODSEQ'
333
+ },
334
+ [message.MODSEQ]
335
+ ];
336
+ // RFC 7162 section 3.1: UID and MODSEQ in every untagged FETCH caused by STORE
337
+ attributes.unshift({ type: 'ATOM', value: 'UID' }, message.uid);
338
+ connection.send(
339
+ {
340
+ tag: '*',
341
+ attributes: [
342
+ entry.index,
343
+ {
344
+ type: 'ATOM',
345
+ value: 'FETCH'
346
+ },
347
+ attributes
348
+ ]
349
+ },
350
+ 'CONDSTORE MODSEQ UPDATE',
351
+ parsed,
352
+ data,
353
+ message
354
+ );
355
+ }
356
+ });
357
+
358
+ // RFC 7162 section 3.1.3 example 11: MODIFIED can also go with the tagged NO of a STORE on expunged messages
359
+ // (RFC 2180 section 4.2.3), it replaces EXPUNGEISSUED there since a response has only one response code
360
+ if ((response.command === 'OK' || response.command === 'NO') && state.modified.length) {
361
+ response.attributes = [
362
+ {
363
+ type: 'SECTION',
364
+ section: [
365
+ {
366
+ type: 'ATOM',
367
+ value: 'MODIFIED'
368
+ },
369
+ {
370
+ type: 'SEQUENCE',
371
+ value: state.modified.sort((a, b) => a - b).join(',')
372
+ }
373
+ ]
374
+ }
375
+ ].concat((response.attributes || []).filter(attr => response.command === 'OK' || !attr || attr.type !== 'SECTION'));
376
+ }
377
+ return;
378
+ }
379
+
380
+ if (description === 'ENABLED' && Array.isArray(extra) && extra.indexOf('CONDSTORE') >= 0) {
381
+ // ENABLE CONDSTORE was issued
382
+ if (!connection.condstoreEnabled) {
383
+ connection.condstoreEnabled = true;
384
+ if (connection.state === 'Selected' && connection.selectedMailbox) {
385
+ parsed.condstoreReportHighest = true;
386
+ }
387
+ }
388
+ return;
389
+ }
390
+
391
+ if (description === 'ENABLE' && parsed.condstoreReportHighest && connection.selectedMailbox) {
392
+ parsed.condstoreReportHighest = false;
393
+ sendHighestModseq(connection, connection.selectedMailbox, parsed, data);
394
+ return;
395
+ }
396
+
397
+ // Add CONDSTORE info to SELECT and EXAMINE
398
+ if ((description === 'EXAMINE' || description === 'SELECT') && response.tag === parsed.tag && connection.selectedMailbox) {
399
+ // (CONDSTORE) option was used, show notice
400
+ if (parsed.condstoreOption) {
401
+ const last = response.attributes && response.attributes.slice(-1)[0];
402
+ if (!last || last.type !== 'TEXT') {
403
+ response.attributes = (response.attributes || []).concat({
404
+ type: 'TEXT',
405
+ value: 'CONDSTORE is now enabled'
406
+ });
407
+ } else {
408
+ last.value += ', CONDSTORE is now enabled';
409
+ }
410
+ }
411
+
412
+ // Send untagged info about highest modseq
413
+ sendHighestModseq(connection, connection.selectedMailbox, parsed, data);
414
+ }
415
+ });
416
+ };
417
+
418
+ // RFC 7162 section 7: mod-sequence-valzer = "0" / mod-sequence-value. Dovecot also accepts values above 63 bits
419
+ function parseModseqValzer(value) {
420
+ if (!isNumber(value, MAX_NUMBER64)) {
421
+ throw badError('MODSEQ expects a mod-sequence value');
422
+ }
423
+ return Number(value);
424
+ }
425
+
426
+ // RFC 7162 section 7: entry-flag-name = DQUOTE "/flags/" attr-flag DQUOTE, attr-flag is a flag without "\Recent"
427
+ function parseEntryName(value) {
428
+ const match = value.match(/^\/flags\/(\\?)(.+)$/i);
429
+ if (!match || INVALID_KEYWORD_CHAR.test(match[2]) || (match[1] && match[2].toUpperCase() === 'RECENT')) {
430
+ throw badError('MODSEQ expects an entry name like "/flags/\\\\seen"');
431
+ }
432
+ return value;
433
+ }
434
+
435
+ // RFC 7162 section 7: entry-type-req = "priv" / "shared" / "all"
436
+ function parseEntryType(value) {
437
+ if (!['PRIV', 'SHARED', 'ALL'].includes(value.toUpperCase())) {
438
+ throw badError('MODSEQ expects entry type priv, shared or all');
439
+ }
440
+ return value;
441
+ }
442
+
443
+ // Serialized message metadata, used to detect changes
444
+ function snapshot(message) {
445
+ return JSON.stringify([message.flags || [], message['X-GM-LABELS'] || []]);
446
+ }
447
+
448
+ function findItem(list, name) {
449
+ if (!Array.isArray(list)) {
450
+ return -1;
451
+ }
452
+ for (let i = 0; i < list.length; i += 2) {
453
+ if (list[i] && list[i].type === 'ATOM' && String(list[i].value).toUpperCase() === name) {
454
+ return i;
455
+ }
456
+ }
457
+ return -1;
458
+ }
459
+
460
+ // Sets or adds the MODSEQ value in a FETCH response attribute list
461
+ function setModseqValue(list, modseq) {
462
+ if (!Array.isArray(list)) {
463
+ return;
464
+ }
465
+ const pos = findItem(list, 'MODSEQ');
466
+ if (pos >= 0) {
467
+ list[pos + 1] = [modseq];
468
+ } else {
469
+ list.push(
470
+ {
471
+ type: 'ATOM',
472
+ value: 'MODSEQ'
473
+ },
474
+ [modseq]
475
+ );
476
+ }
477
+ }
478
+
479
+ // Adds the UID to a FETCH response attribute list unless it is there already
480
+ function setUidValue(list, uid) {
481
+ if (Array.isArray(list) && findItem(list, 'UID') < 0) {
482
+ list.push(
483
+ {
484
+ type: 'ATOM',
485
+ value: 'UID'
486
+ },
487
+ uid
488
+ );
489
+ }
490
+ }
491
+
492
+ function isModseqAtom(item) {
493
+ return !!item && item.type === 'ATOM' && String(item.value).toUpperCase() === 'MODSEQ';
494
+ }
495
+
496
+ function hasModseqItem(parsed) {
497
+ const items = parsed.attributes && parsed.attributes[1];
498
+ return Array.isArray(items) ? items.some(isModseqAtom) : isModseqAtom(items);
499
+ }
500
+
501
+ // Adds MODSEQ to the list of FETCH data items, unless it is already there
502
+ function addModseqItem(parsed) {
503
+ if (!parsed.attributes || !parsed.attributes[1] || hasModseqItem(parsed)) {
504
+ return;
505
+ }
506
+ let items = parsed.attributes[1];
507
+ if (!Array.isArray(items)) {
508
+ const name = String(items.value).toUpperCase();
509
+ const macro = items.type === 'ATOM' && Object.hasOwn(FETCH_MACROS, name) && FETCH_MACROS[name];
510
+ // a single fetch-att keeps the arguments that follow it, eg. RFC 8970 PREVIEW (LAZY)
511
+ items = macro ? macro.map(value => ({ type: 'ATOM', value })) : [items].concat(parsed.attributes.splice(2));
512
+ }
513
+ parsed.attributes[1] = items.concat({
514
+ type: 'ATOM',
515
+ value: 'MODSEQ'
516
+ });
517
+ }
518
+
519
+ function hasCondstoreOption(attributes, parent, index) {
520
+ if (!attributes) {
521
+ return false;
522
+ }
523
+ let condstoreOption = false;
524
+ if (Array.isArray(attributes)) {
525
+ for (let i = attributes.length - 1; i >= 0; i--) {
526
+ if (attributes[i] && attributes[i].type === 'ATOM' && attributes[i].value.toUpperCase() === 'CONDSTORE') {
527
+ attributes.splice(i, 1);
528
+ condstoreOption = true;
529
+ break;
530
+ }
531
+ }
532
+
533
+ // remove parameter if no other memebers were left
534
+ if (!attributes.length) {
535
+ parent.splice(index, 1);
536
+ }
537
+ }
538
+ return !!condstoreOption;
539
+ }
540
+
541
+ function getCondstoreValue(attributes, name, parent, index) {
542
+ if (!attributes) {
543
+ return false;
544
+ }
545
+ let condstoreValue = false;
546
+ if (Array.isArray(attributes)) {
547
+ for (let i = 0; i < attributes.length; i += 2) {
548
+ if (attributes[i] && attributes[i].type === 'ATOM' && attributes[i].value.toUpperCase() === name.toUpperCase()) {
549
+ // a missing value is reported as a syntax error
550
+ condstoreValue = attributes[i + 1] || { type: 'NIL' };
551
+ attributes.splice(i, 2);
552
+ break;
553
+ }
554
+ }
555
+
556
+ // remove parameter if no other memebers were left
557
+ if (!attributes.length) {
558
+ parent.splice(index, 1);
559
+ }
560
+ }
561
+
562
+ return condstoreValue;
563
+ }