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,321 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @help Adds CONTEXT=SEARCH [RFC5267] capability, loads ESEARCH as well
5
+ * @help SEARCH RETURN (UPDATE) sends ADDTO and REMOVEFROM updates as the result changes, until CANCELUPDATE or the mailbox is closed
6
+ * @help SEARCH RETURN (PARTIAL 1:100) returns a window of the results, the CONTEXT hint is accepted and ignored
7
+ * @help Server option "maxSearchContexts" (default 10) limits the updating contexts of a session, above it NOUPDATE is sent
8
+ */
9
+
10
+ const esearch = require('./esearch');
11
+ const { buildCorrelator, registerPartialOption, toSequenceSet, toOrderedSet } = require('../esearch');
12
+ const { states } = require('../command-states');
13
+
14
+ const DEFAULT_MAX_CONTEXTS = 10;
15
+
16
+ // commands that close the selected mailbox, updates cease with them (RFC 5267 section 4.3)
17
+ const CLOSING_COMMANDS = new Set(['SELECT', 'EXAMINE', 'CLOSE', 'UNSELECT']);
18
+
19
+ // searching commands that can create updating contexts, SORT and UID SORT only with CONTEXT=SORT (see the ESORT plugin)
20
+ const SEARCHING_COMMANDS = new Set(['SEARCH', 'UID SEARCH', 'SORT', 'UID SORT']);
21
+
22
+ const getContexts = connection => {
23
+ if (!connection.searchContexts) {
24
+ connection.searchContexts = new Map();
25
+ }
26
+ return connection.searchContexts;
27
+ };
28
+
29
+ /**
30
+ * Groups updates of one kind into context position and result set pairs (RFC 5267 sections 4.3.3 and
31
+ * 4.3.4). Updates of a searching command all use position 0 and form one set in mailbox order. Updates of
32
+ * a sorting command are merged while they continue each other: an insertion right after the previous
33
+ * inserted results, or a removal at the same position where the previous result was removed
34
+ *
35
+ * @param {Array} updates `[position, value]` pairs in the order they are applied
36
+ * @param {Boolean} isRemoval If true, the updates are REMOVEFROM updates
37
+ * @return {Array} payload of the ADDTO or REMOVEFROM return data item
38
+ */
39
+ const groupUpdates = (updates, isRemoval) => {
40
+ const groups = [];
41
+ updates.forEach(([position, value]) => {
42
+ const last = groups[groups.length - 1];
43
+ const next = last && (isRemoval || !position ? last.position : last.position + last.values.length);
44
+ if (last && position === next) {
45
+ last.values.push(value);
46
+ } else {
47
+ groups.push({ position, values: [value] });
48
+ }
49
+ });
50
+ const payload = [];
51
+ groups.forEach(group => {
52
+ const set = group.position ? toOrderedSet(group.values) : toSequenceSet(group.values);
53
+ payload.push(group.position, { type: 'SEQUENCE', value: set });
54
+ });
55
+ return payload;
56
+ };
57
+
58
+ /**
59
+ * Sends ADDTO and REMOVEFROM updates of a context (RFC 5267 sections 4.3.3 and 4.3.4)
60
+ *
61
+ * @param {Object} connection IMAP connection
62
+ * @param {Object} context Updating context
63
+ * @param {Array} removed `[position, value]` pairs of removed results, applied first
64
+ * @param {Array} added `[position, value]` pairs of added results
65
+ */
66
+ const sendUpdate = (connection, context, removed, added) => {
67
+ const attributes = [buildCorrelator(context.tag, context.correlatorMailbox)];
68
+ if (context.isUid) {
69
+ attributes.push({ type: 'ATOM', value: 'UID' });
70
+ }
71
+ if (removed.length) {
72
+ attributes.push({ type: 'ATOM', value: 'REMOVEFROM' }, groupUpdates(removed, true));
73
+ }
74
+ if (added.length) {
75
+ attributes.push({ type: 'ATOM', value: 'ADDTO' }, groupUpdates(added, false));
76
+ }
77
+ connection.send({ tag: '*', command: 'ESEARCH', attributes }, 'ESEARCH UPDATE', null, null, context);
78
+ };
79
+
80
+ /**
81
+ * Checks every updating context against the selected mailbox as the session sees it, and sends the
82
+ * changes. Expunged messages are handled when their EXPUNGE response is sent
83
+ */
84
+ const checkContexts = connection => {
85
+ const contexts = connection.searchContexts;
86
+ const mailbox = connection.selectedMailbox;
87
+ const messages = mailbox ? mailbox.messages : [];
88
+ const numbers = new Map(messages.map((message, i) => [message, i + 1]));
89
+ contexts.forEach((context, tag) => {
90
+ if (context.mailbox !== mailbox) {
91
+ contexts.delete(tag);
92
+ return;
93
+ }
94
+ const matching = messages.filter((message, i) => context.matches(message, i + 1));
95
+ const getValue = message => (context.isUid ? message.uid : numbers.get(message));
96
+ const current = new Set(matching);
97
+ const removed = [];
98
+ const added = [];
99
+
100
+ if (!context.compare) {
101
+ // a searching command, positions are 0 (RFC 5267 section 4.3.3)
102
+ context.results.forEach(message => {
103
+ // a message that is gone without an EXPUNGE response for this session is just dropped
104
+ if (!current.has(message) && numbers.has(message)) {
105
+ removed.push([0, getValue(message)]);
106
+ }
107
+ });
108
+ matching.forEach(message => {
109
+ if (!context.results.has(message)) {
110
+ added.push([0, getValue(message)]);
111
+ }
112
+ });
113
+ } else {
114
+ // a sorting command. The sort values of a message do not change, so the results that stay keep their order.
115
+ // Removals are counted on the list as it shrinks, insertions in ascending order of their final position
116
+ let kept = 0;
117
+ context.order.forEach(message => {
118
+ if (current.has(message)) {
119
+ kept++;
120
+ } else if (numbers.has(message)) {
121
+ removed.push([kept + 1, getValue(message)]);
122
+ }
123
+ });
124
+ context.order = matching.sort(context.compare);
125
+ context.order.forEach((message, i) => {
126
+ if (!context.results.has(message)) {
127
+ added.push([i + 1, getValue(message)]);
128
+ }
129
+ });
130
+ }
131
+
132
+ context.results = current;
133
+ if (removed.length || added.length) {
134
+ sendUpdate(connection, context, removed, added);
135
+ }
136
+ });
137
+ };
138
+
139
+ /**
140
+ * Removes expunged messages from the contexts that hold them, with one REMOVEFROM update per context
141
+ *
142
+ * @param {Object} connection IMAP connection
143
+ * @param {Array} messages The expunged messages
144
+ * @param {Number} seq Sequence number of the EXPUNGE response for a single message, null when there is none (VANISHED)
145
+ */
146
+ const removeExpunged = (connection, messages, seq) => {
147
+ connection.searchContexts.forEach(context => {
148
+ if (context.mailbox !== connection.selectedMailbox) {
149
+ return;
150
+ }
151
+ const removed = [];
152
+ messages.forEach(message => {
153
+ if (!context.results.delete(message)) {
154
+ return;
155
+ }
156
+ let position = 0;
157
+ if (context.order) {
158
+ position = context.order.indexOf(message) + 1;
159
+ context.order.splice(position - 1, 1);
160
+ }
161
+ if (context.isUid || seq) {
162
+ removed.push([position, context.isUid ? message.uid : seq]);
163
+ }
164
+ });
165
+ if (removed.length) {
166
+ sendUpdate(connection, context, removed, []);
167
+ }
168
+ });
169
+ };
170
+
171
+ module.exports = function (server) {
172
+ // CONTEXT=SORT loads this plugin as well, so it may be called twice
173
+ if (server.addSearchContext) {
174
+ return;
175
+ }
176
+
177
+ // the result options of CONTEXT=SEARCH extend the extended SEARCH command (RFC 5267 section 4.1)
178
+ esearch(server);
179
+
180
+ server.registerCapability('CONTEXT=SEARCH');
181
+
182
+ const maxContexts = Math.max(Number(server.options.maxSearchContexts) || DEFAULT_MAX_CONTEXTS, 1);
183
+
184
+ // RFC 5267 section 4.2: the server MAY ignore the CONTEXT hint
185
+ server.searchReturnOptions.set('CONTEXT', { hint: true });
186
+ // RFC 5267 section 4.3: UPDATE has no data of its own, but the ESEARCH response is still sent
187
+ server.searchReturnOptions.set('UPDATE', { data: false, response: true, once: true });
188
+ // RFC 5267 section 4.4
189
+ registerPartialOption(server);
190
+
191
+ // RFC 5267 section 4.3: UPDATE with the tag of an earlier searching command that still has an updating
192
+ // context SHALL be rejected with BAD
193
+ server.searchReturnChecks.push(
194
+ (options, connection, parsed) =>
195
+ options.has('UPDATE') && getContexts(connection).has(parsed.tag) && 'Tag ' + parsed.tag + ' is already in use by an updating search'
196
+ );
197
+
198
+ /**
199
+ * Creates an updating context once a searching command with UPDATE has completed. Also used by
200
+ * CONTEXT=SORT and by the ESEARCH command of MULTISEARCH
201
+ *
202
+ * @param {Object} connection IMAP connection
203
+ * @param {Object} parsed Parsed searching command
204
+ * @param {String} data Raw command
205
+ * @param {Object} result Search result of the selected mailbox, with the `matches` function, and for a
206
+ * sorting command the `sorted` list and the `compare` function
207
+ * @param {Boolean} isUid If true, the updates list UIDs
208
+ * @param {Object} [correlatorMailbox] Mailbox for the MAILBOX and UIDVALIDITY correlators (RFC 7377)
209
+ */
210
+ server.addSearchContext = (connection, parsed, data, result, isUid, correlatorMailbox) => {
211
+ const contexts = getContexts(connection);
212
+ if (contexts.size >= maxContexts) {
213
+ // RFC 5267 section 4.3.1: an untagged NO with NOUPDATE, the other result options are still honoured
214
+ connection.send(
215
+ {
216
+ tag: '*',
217
+ command: 'NO',
218
+ attributes: [
219
+ {
220
+ type: 'SECTION',
221
+ section: [
222
+ { type: 'ATOM', value: 'NOUPDATE' },
223
+ { type: 'STRING', value: parsed.tag }
224
+ ]
225
+ },
226
+ { type: 'TEXT', value: 'Too many updating contexts' }
227
+ ]
228
+ },
229
+ 'NOUPDATE',
230
+ parsed,
231
+ data
232
+ );
233
+ return;
234
+ }
235
+ contexts.set(parsed.tag, {
236
+ tag: parsed.tag,
237
+ isUid,
238
+ matches: result.matches,
239
+ results: new Set(result.list),
240
+ compare: result.compare || null,
241
+ order: result.compare ? result.sorted.slice() : null,
242
+ mailbox: connection.selectedMailbox,
243
+ correlatorMailbox: correlatorMailbox || null
244
+ });
245
+ };
246
+
247
+ // RFC 5267 section 4.3.5: command-select =/ "CANCELUPDATE" 1*(SP quoted)
248
+ server.setCommandHandler(
249
+ 'CANCELUPDATE',
250
+ (connection, parsed, data, callback) => {
251
+ const tags = parsed.attributes || [];
252
+ if (!tags.length || tags.some(tag => !tag || tag.type !== 'STRING')) {
253
+ connection.sendStatus(parsed, data, 'BAD', 'CANCELUPDATE expects one or more quoted tags', false, 'CANCELUPDATE FAILED');
254
+ return callback();
255
+ }
256
+ const contexts = getContexts(connection);
257
+ const unknown = tags.find(tag => !contexts.has(tag.value));
258
+ if (unknown) {
259
+ // nothing is cancelled when a tag has no updating context
260
+ connection.sendStatus(parsed, data, 'NO', 'Unknown tag', false, 'CANCELUPDATE FAILED');
261
+ return callback();
262
+ }
263
+ tags.forEach(tag => contexts.delete(tag.value));
264
+ connection.sendStatus(parsed, data, 'OK', 'Updates cancelled', false, 'CANCELUPDATE');
265
+ return callback();
266
+ },
267
+ { states: states.SELECTED }
268
+ );
269
+
270
+ // UNAUTHENTICATE (RFC 8437) closes the mailbox, so the updates end
271
+ server.resetHandlers.push(connection => {
272
+ connection.searchContexts = null;
273
+ });
274
+
275
+ server.connectionHandlers.push(connection => {
276
+ // Notifications from other sessions (and this one) are flushed here. Once none are pending, the session
277
+ // knows the current message list, so new matches can be reported after their EXISTS and FETCH responses
278
+ // (RFC 5267 section 4.3.2). A command that closes the mailbox gets no updates, its tagged response ends them
279
+ const processNotifications = connection.processNotifications;
280
+ connection.processNotifications = function (data) {
281
+ processNotifications.call(this, data);
282
+ const command = ((data && data.command) || '').toUpperCase();
283
+ if (this.searchContexts && this.searchContexts.size && !this.notificationQueue.length && !CLOSING_COMMANDS.has(command)) {
284
+ checkContexts(this);
285
+ }
286
+ };
287
+ });
288
+
289
+ server.outputHandlers.push((connection, response, description, parsed, data, extra) => {
290
+ // RFC 5267 section 4.3.4: REMOVEFROM for an expunged message MUST be sent before the EXPUNGE response. After
291
+ // ENABLE QRESYNC a VANISHED response reports the expunged messages (RFC 7162 section 3.2.10), it has no sequence
292
+ // numbers, so contexts with sequence numbers drop those messages without an update
293
+ const expunged = response.tag === '*' && (response.message ? [response.message] : response.command === 'VANISHED' && response.messages);
294
+ if (expunged && connection.searchContexts) {
295
+ removeExpunged(connection, expunged, response.message ? response.attributes[0] : null);
296
+ return;
297
+ }
298
+
299
+ if (!parsed) {
300
+ return;
301
+ }
302
+ const command = (parsed.command || '').toUpperCase();
303
+
304
+ if (response.tag === parsed.tag && CLOSING_COMMANDS.has(command)) {
305
+ connection.searchContexts = null;
306
+ return;
307
+ }
308
+
309
+ if (!SEARCHING_COMMANDS.has(command) || !parsed.searchReturn || !parsed.searchReturn.has('UPDATE')) {
310
+ return;
311
+ }
312
+ // the full search result, the context is created when the command succeeded
313
+ if (response.tag === '*' && command.endsWith(response.command) && description === command && extra && Array.isArray(extra.list)) {
314
+ parsed.contextResult = extra;
315
+ return;
316
+ }
317
+ if (response.tag === parsed.tag && response.command === 'OK' && parsed.contextResult) {
318
+ server.addSearchContext(connection, parsed, data, parsed.contextResult, command.startsWith('UID '));
319
+ }
320
+ });
321
+ };
@@ -0,0 +1,19 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @help Adds CONTEXT=SORT [RFC5267] capability, loads ESORT and CONTEXT=SEARCH as well
5
+ * @help SORT RETURN (UPDATE) sends ADDTO and REMOVEFROM updates with context positions in sort order, PARTIAL and CONTEXT work for SORT too
6
+ */
7
+
8
+ const esort = require('./esort');
9
+ const contextSearch = require('./context-search');
10
+
11
+ module.exports = function (server) {
12
+ // RFC 5267 section 4.1: CONTEXT=SORT means SORT and the extended SORT syntax of section 3. The updating
13
+ // contexts are shared with CONTEXT=SEARCH, so that is loaded (and advertised) as well
14
+ esort(server);
15
+ contextSearch(server);
16
+
17
+ server.registerCapability('CONTEXT=SORT');
18
+ server.contextSort = true;
19
+ };
@@ -0,0 +1,108 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @help Enables CREATE-SPECIAL-USE [RFC6154] capability
5
+ * @help Allowed special flags can be set with server
6
+ * @help option "special-use"
7
+ */
8
+
9
+ module.exports = function (server) {
10
+ // Register capability
11
+ server.registerCapability('CREATE-SPECIAL-USE');
12
+
13
+ const createHandler = server.getCommandHandler('CREATE');
14
+ const allowedList = [].concat(server.options['special-use'] || ['\\Archive', '\\Drafts', '\\Flagged', '\\Junk', '\\Sent', '\\Trash']);
15
+
16
+ server.setCommandHandler('CREATE', (connection, parsed, data, callback) => {
17
+ let i;
18
+ let len;
19
+ let specialUseList;
20
+ const mailboxSpecialUse = [];
21
+ if (parsed.attributes && Array.isArray(parsed.attributes[1])) {
22
+ for (i = 0; i < parsed.attributes[1].length; i += 2) {
23
+ if (
24
+ parsed.attributes[1][i] &&
25
+ parsed.attributes[1][i].type === 'ATOM' &&
26
+ parsed.attributes[1][i].value.toUpperCase() === 'USE' &&
27
+ Array.isArray(parsed.attributes[1][i + 1])
28
+ ) {
29
+ specialUseList = parsed.attributes[1][i + 1];
30
+ parsed.attributes[1].splice(i, 2);
31
+ i -= 2;
32
+ }
33
+ }
34
+
35
+ // Remove extra arguments if no members were left
36
+ if (!parsed.attributes[1].length) {
37
+ parsed.attributes.splice(1, 1);
38
+ }
39
+ }
40
+
41
+ if (specialUseList) {
42
+ for (i = 0, len = specialUseList.length; i < len; i++) {
43
+ if (['ATOM', 'STRING', 'LITERAL'].indexOf(specialUseList[i].type) < 0) {
44
+ connection.send(
45
+ {
46
+ tag: parsed.tag,
47
+ command: 'BAD',
48
+ attributes: [
49
+ {
50
+ type: 'TEXT',
51
+ value: 'Invalid syntax for special use flag #' + (i + 1)
52
+ }
53
+ ]
54
+ },
55
+ 'CREATE-SPECIAL-USE FAILED',
56
+ parsed,
57
+ data
58
+ );
59
+ return callback();
60
+ }
61
+ // attribute names are case-insensitive, store the canonical form
62
+ const specialUse = allowedList.find(value => value.toLowerCase() === String(specialUseList[i].value).toLowerCase());
63
+ if (!specialUse) {
64
+ connection.send(
65
+ {
66
+ tag: parsed.tag,
67
+ command: 'NO',
68
+ attributes: [
69
+ {
70
+ type: 'SECTION',
71
+ section: [
72
+ {
73
+ type: 'ATOM',
74
+ value: 'USEATTR'
75
+ }
76
+ ]
77
+ },
78
+ {
79
+ type: 'TEXT',
80
+ value: specialUseList[i].value + ' not supported'
81
+ }
82
+ ]
83
+ },
84
+ 'CREATE-SPECIAL-USE FAILED',
85
+ parsed,
86
+ data
87
+ );
88
+ return callback();
89
+ }
90
+ if (mailboxSpecialUse.indexOf(specialUse) < 0) {
91
+ mailboxSpecialUse.push(specialUse);
92
+ }
93
+ }
94
+ }
95
+
96
+ if (mailboxSpecialUse.length) {
97
+ parsed.mailboxSpecialUse = mailboxSpecialUse;
98
+ }
99
+
100
+ createHandler(connection, parsed, data, callback);
101
+ });
102
+
103
+ server.outputHandlers.push((connection, response, description, parsed, data, folder) => {
104
+ if (description === 'CREATE' && folder && parsed.mailboxSpecialUse) {
105
+ folder['special-use'] = parsed.mailboxSpecialUse;
106
+ }
107
+ });
108
+ };
@@ -0,0 +1,155 @@
1
+ 'use strict';
2
+
3
+ const { states } = require('../command-states');
4
+
5
+ /**
6
+ * @help Adds ENABLE capability [RFC5161]
7
+ * @help Plugins that can be enabled (eg. CONDSTORE)
8
+ * @help can be loaded in any order
9
+ */
10
+
11
+ /**
12
+ * Makes an extension available for ENABLE. Plugins call it whether the ENABLE plugin is loaded before or after them
13
+ *
14
+ * @param {Object} server IMAPServer instance
15
+ * @param {String} name Capability name in the spelling it is advertised in, e.g. "IMAP4rev2"
16
+ */
17
+ function registerEnable(server, name) {
18
+ server.enableAvailable = server.enableAvailable || [];
19
+ if (server.enableAvailable.indexOf(name) < 0) {
20
+ server.enableAvailable.push(name);
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Checks if a session has enabled an extension with ENABLE. UNAUTHENTICATE clears the list (RFC 8437 section 4.1)
26
+ *
27
+ * @param {Object} connection IMAP connection
28
+ * @param {String} name Capability name in the spelling it was registered with, see registerEnable
29
+ * @return {Boolean} true if the extension is enabled
30
+ */
31
+ function isEnabled(connection, name) {
32
+ return !!connection.enabled && connection.enabled.indexOf(name) >= 0;
33
+ }
34
+
35
+ module.exports = function (server) {
36
+ server.registerCapability('ENABLE');
37
+
38
+ // Shared with plugins that can be enabled, these might be loaded before or after this one
39
+ server.enableAvailable = server.enableAvailable || [];
40
+
41
+ server.connectionHandlers.push(connection => {
42
+ connection.enabled = connection.enabled || [];
43
+ });
44
+
45
+ // RFC 8437 section 4.1: extensions enabled with ENABLE cease to be enabled after UNAUTHENTICATE
46
+ server.resetHandlers.push(connection => {
47
+ connection.enabled = [];
48
+ });
49
+
50
+ server.setCommandHandler(
51
+ 'ENABLE',
52
+ (connection, parsed, data, callback) => {
53
+ let capability;
54
+ let i;
55
+ let len;
56
+
57
+ // RFC 5161 section 3.1: "Clients MUST NOT issue ENABLE once they SELECT/EXAMINE a mailbox".
58
+ // Servers do not have to check this, ImapKit does to catch the client bug.
59
+ if (connection.everSelected) {
60
+ connection.sendStatus(parsed, data, 'BAD', 'ENABLE is not allowed after SELECT or EXAMINE');
61
+ return callback();
62
+ }
63
+
64
+ if (!parsed.attributes) {
65
+ connection.send(
66
+ {
67
+ tag: parsed.tag,
68
+ command: 'BAD',
69
+ attributes: [
70
+ {
71
+ type: 'TEXT',
72
+ value: 'ENABLE expects capability list'
73
+ }
74
+ ]
75
+ },
76
+ 'INVALID COMMAND',
77
+ parsed,
78
+ data
79
+ );
80
+ return callback();
81
+ }
82
+
83
+ for (i = 0, len = parsed.attributes.length; i < len; i++) {
84
+ if (!parsed.attributes[i] || parsed.attributes[i].type !== 'ATOM') {
85
+ connection.send(
86
+ {
87
+ tag: parsed.tag,
88
+ command: 'BAD',
89
+ attributes: [
90
+ {
91
+ type: 'TEXT',
92
+ value: 'Attribute nr ' + (i + 1) + ' is not an ATOM'
93
+ }
94
+ ]
95
+ },
96
+ 'INVALID COMMAND',
97
+ parsed,
98
+ data
99
+ );
100
+ return callback();
101
+ }
102
+ }
103
+
104
+ // The ENABLED response lists only the extensions enabled by this command
105
+ // Capability names are matched case-insensitively and listed in the spelling the server advertises them
106
+ // (`server.enableAvailable`), e.g. "IMAP4rev2", so plugins look them up in `connection.enabled` by that name
107
+ const enabled = [];
108
+ for (i = 0, len = parsed.attributes.length; i < len; i++) {
109
+ const requested = parsed.attributes[i].value.toUpperCase();
110
+ capability = server.enableAvailable.find(name => name.toUpperCase() === requested);
111
+ if (capability && connection.enabled.indexOf(capability) < 0) {
112
+ connection.enabled.push(capability);
113
+ enabled.push(capability);
114
+ }
115
+ }
116
+
117
+ connection.send(
118
+ {
119
+ tag: '*',
120
+ command: 'ENABLED',
121
+ attributes: enabled.map(capability => ({
122
+ type: 'ATOM',
123
+ value: capability
124
+ }))
125
+ },
126
+ 'ENABLED',
127
+ parsed,
128
+ data,
129
+ enabled
130
+ );
131
+
132
+ connection.send(
133
+ {
134
+ tag: parsed.tag,
135
+ command: 'OK',
136
+ attributes: [
137
+ {
138
+ type: 'TEXT',
139
+ value: 'ENABLE completed'
140
+ }
141
+ ]
142
+ },
143
+ 'ENABLE',
144
+ parsed,
145
+ data
146
+ );
147
+
148
+ return callback();
149
+ },
150
+ { states: states.AUTHENTICATED }
151
+ );
152
+ };
153
+
154
+ module.exports.registerEnable = registerEnable;
155
+ module.exports.isEnabled = isEnabled;