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,964 @@
1
+ 'use strict';
2
+
3
+ const { states } = require('../command-states');
4
+ const { normalizeSystemFlag, checkSystemFlags } = require('../commands/handlers/flags');
5
+ const builtinStoreHandlers = require('../commands/handlers/store');
6
+ const { getListExtensions } = require('../list-extensions');
7
+ const { beginSelect } = require('../commands/select');
8
+ const { getPendingTarget } = require('../commands/append');
9
+ const { isAstring } = require('../arguments');
10
+
11
+ /**
12
+ * @help Adds ACL [RFC4314] capability with RIGHTS=texk
13
+ * @help The owner (server option "aclOwner", default "testuser")
14
+ * @help has all rights on every mailbox. Other users get rights
15
+ * @help from the "acl" property of a mailbox in storage, e.g.
16
+ * @help {"otheruser": "lr", "anyone": "l"}, or from SETACL. With
17
+ * @help LIST-EXTENDED, LIST-MYRIGHTS [RFC8440] is supported as well
18
+ *
19
+ * ACL: https://www.rfc-editor.org/rfc/rfc4314
20
+ *
21
+ * LIST-MYRIGHTS: https://www.rfc-editor.org/rfc/rfc8440
22
+ *
23
+ * Additional commands:
24
+ * - SETACL, DELETEACL, GETACL, LISTRIGHTS, MYRIGHTS
25
+ * - the MYRIGHTS return option of LIST, when LIST-EXTENDED is loaded
26
+ *
27
+ * Users other than the owner can only do what their rights allow (RFC 4314 section 4).
28
+ */
29
+
30
+ // RFC 4314 section 2.1, in the order they are listed in responses
31
+ const RIGHTS = 'lrswipkxtea';
32
+ const ALL_RIGHTS = new Set(RIGHTS);
33
+ // RFC 4314 section 2.1.1: the obsolete "c" and "d" rights are macros. Like Dovecot, "c" is "kx"
34
+ // ("create" for servers that used "c" for DELETE) and "d" is "et"
35
+ const VIRTUAL_RIGHTS = { c: 'kx', d: 'et' };
36
+ // RFC 4314 section 5.2: with none of these the mailbox is READ-ONLY. All flags are shared between
37
+ // the users of a mailbox, so the "shared flag rights" are "s", "w" and "t"
38
+ const WRITE_RIGHTS = ['i', 'e', 's', 'w', 't'];
39
+ // RFC 4314 section 4: the rights for changing flags
40
+ const FLAG_RIGHTS = ['s', 'w', 't'];
41
+ // RFC 4314 section 4: MYRIGHTS needs any of these
42
+ const ANY_RIGHTS = ['l', 'r', 'i', 'k', 'x', 'a'];
43
+ const STORE_ITEMS = ['FLAGS', '+FLAGS', '-FLAGS', 'FLAGS.SILENT', '+FLAGS.SILENT', '-FLAGS.SILENT'];
44
+
45
+ /**
46
+ * Formats an astring for a response. The compiler sends an ATOM value that is not a valid atom as a
47
+ * quoted string or a literal, only a leading "\\" (allowed for flags) and NIL have to be kept away from it
48
+ *
49
+ * @param {String} value
50
+ * @return {Object|String}
51
+ */
52
+ function astring(value) {
53
+ // identifiers are unicode strings, sent as UTF-8 (RFC 4314 section 3)
54
+ value = Buffer.from(value, 'utf-8').toString('binary');
55
+ // an atom NIL would read back as NIL, not as an identifier
56
+ return value && value.charAt(0) !== '\\' && !/^NIL$/i.test(value) ? { type: 'ATOM', value } : value;
57
+ }
58
+
59
+ /**
60
+ * Creates an error that is answered with BAD
61
+ *
62
+ * @param {String} message Error text
63
+ * @return {Error}
64
+ */
65
+ function badInput(message) {
66
+ const err = new Error(message);
67
+ err.imapResponse = 'BAD';
68
+ return err;
69
+ }
70
+
71
+ /**
72
+ * Parses a rights string, the virtual "c" and "d" rights are expanded (RFC 4314 section 2.1.1)
73
+ *
74
+ * @param {String} value Rights, e.g. "lrswd"
75
+ * @return {Set} rights
76
+ * @throws {Error} BAD error for unknown rights (RFC 4314 section 3.1)
77
+ */
78
+ function parseRights(value) {
79
+ const rights = new Set();
80
+ for (const chr of value) {
81
+ if (ALL_RIGHTS.has(chr)) {
82
+ rights.add(chr);
83
+ } else if (Object.hasOwn(VIRTUAL_RIGHTS, chr)) {
84
+ VIRTUAL_RIGHTS[chr].split('').forEach(right => rights.add(right));
85
+ } else if (/^[A-Z]$/.test(chr)) {
86
+ throw badInput('Uppercase rights are not allowed');
87
+ } else {
88
+ throw badInput('The ' + (/^[\x21-\x7e]$/.test(chr) ? chr + ' ' : '') + 'right is not supported');
89
+ }
90
+ }
91
+ return rights;
92
+ }
93
+
94
+ /**
95
+ * Builds the attributes of an ACL, LISTRIGHTS or MYRIGHTS response. The mailbox name is a MAILBOX
96
+ * attribute, so each session gets it in the form it uses (modified UTF-7, or UTF-8 after ENABLE
97
+ * UTF8=ACCEPT)
98
+ *
99
+ * @param {Object} mailbox Mailbox object
100
+ * @param {Array} values Values that follow the mailbox name
101
+ * @return {Array} Response attributes
102
+ */
103
+ function mailboxResponse(mailbox, values) {
104
+ return [{ type: 'MAILBOX', value: mailbox.path }].concat(values.map(astring));
105
+ }
106
+
107
+ /**
108
+ * Formats rights for ACL and MYRIGHTS responses. "c" and "d" are added when any of their
109
+ * member rights is set (RFC 4314 section 2.1.1)
110
+ *
111
+ * @param {Set} rights
112
+ * @return {String}
113
+ */
114
+ function formatRights(rights) {
115
+ let result = RIGHTS.split('')
116
+ .filter(right => rights.has(right))
117
+ .join('');
118
+ Object.keys(VIRTUAL_RIGHTS).forEach(virtual => {
119
+ if (VIRTUAL_RIGHTS[virtual].split('').some(right => rights.has(right))) {
120
+ result += virtual;
121
+ }
122
+ });
123
+ return result;
124
+ }
125
+
126
+ /**
127
+ * Checks an identifier argument. RFC 4314 section 3 asks for SASLprep, which is not implemented,
128
+ * but an identifier that SASLprep would refuse (control characters, invalid UTF-8) or that is
129
+ * empty gets BAD, as does a lone "-" (section 2: "-" prefixes the identifier of a negative right)
130
+ *
131
+ * @param {String} identifier Identifier as a binary string
132
+ * @return {String|Boolean} error text, or false if the identifier is fine
133
+ */
134
+ function identifierError(identifier) {
135
+ if (!identifier || identifier === '-') {
136
+ return 'Empty identifier';
137
+ }
138
+ // eslint-disable-next-line no-control-regex
139
+ if (/[\x00-\x1f\x7f]/.test(identifier)) {
140
+ return 'Identifier contains control characters';
141
+ }
142
+ const decoded = Buffer.from(identifier, 'binary').toString('utf-8');
143
+ if (Buffer.from(decoded, 'utf-8').toString('binary') !== identifier) {
144
+ return 'Identifier is not valid UTF-8';
145
+ }
146
+ return false;
147
+ }
148
+
149
+ module.exports = function (server) {
150
+ server.registerCapability('ACL');
151
+ // RFC 4314 section 5.1.1: the rights not defined in RFC 2086, MUST include "t", "e", "x" and "k"
152
+ server.registerCapability('RIGHTS=texk');
153
+
154
+ const owner = typeof server.options.aclOwner === 'string' ? server.options.aclOwner : 'testuser';
155
+
156
+ const isOwner = connection => connection.username === owner;
157
+
158
+ /**
159
+ * Returns the ACL of a mailbox as a Map of identifier to a Set of rights. The ACL from the
160
+ * storage (an object of identifier to rights string) is converted on first use
161
+ *
162
+ * @param {Object} mailbox Mailbox object
163
+ * @return {Map}
164
+ */
165
+ const getAcl = mailbox => {
166
+ if (!(mailbox.acl instanceof Map)) {
167
+ const acl = new Map();
168
+ const source = mailbox.acl && typeof mailbox.acl === 'object' ? mailbox.acl : {};
169
+ Object.keys(source).forEach(identifier => {
170
+ try {
171
+ acl.set(identifier, parseRights(String(source[identifier])));
172
+ } catch (err) {
173
+ // not a client error, so this is answered with NO [SERVERBUG]
174
+ throw new Error('Invalid ACL in storage for ' + mailbox.path + ': ' + err.message, { cause: err });
175
+ }
176
+ });
177
+ mailbox.acl = acl;
178
+ }
179
+ return mailbox.acl;
180
+ };
181
+
182
+ /**
183
+ * Returns the rights a user has on a mailbox. The owner has all rights, other users get the
184
+ * union of the rights of their own identifier and "anyone", minus the negative rights of
185
+ * "-user" and "-anyone" (RFC 4314 section 2)
186
+ *
187
+ * @param {Object} connection IMAP connection of the user
188
+ * @param {Object} mailbox Mailbox object
189
+ * @param {Map} [acl] ACL to use instead of the current one of the mailbox
190
+ * @return {Set} rights
191
+ */
192
+ const getRights = (connection, mailbox, acl) => {
193
+ const username = connection.username;
194
+ if (username === owner) {
195
+ return ALL_RIGHTS;
196
+ }
197
+ acl = acl || getAcl(mailbox);
198
+ const identifiers = ['anyone'].concat(typeof username === 'string' && username ? username : []);
199
+ const rights = new Set();
200
+ identifiers.forEach(identifier => (acl.get(identifier) || []).forEach(right => rights.add(right)));
201
+ identifiers.forEach(identifier => (acl.get('-' + identifier) || []).forEach(right => rights.delete(right)));
202
+ return rights;
203
+ };
204
+
205
+ // for plugins that report only what a user may see (e.g. NOTIFY)
206
+ server.acl = { getRights, isOwner };
207
+
208
+ // Tells plugins that the ACL of a mailbox changed, with an `acl` event `(mailbox, previousAcl)`
209
+ const changeAcl = (mailbox, change) => {
210
+ const acl = getAcl(mailbox);
211
+ const previous = new Map(acl);
212
+ change(acl);
213
+ server.emit('acl', mailbox, previous);
214
+ };
215
+
216
+ // CATENATE (RFC 4469 section 5): reading a message through an IMAP URL needs the "r" right, a
217
+ // mailbox without "l" is reported like one that does not exist (RFC 4314 section 6)
218
+ server.urlAccessChecks.push((connection, mailbox) => {
219
+ const rights = getRights(connection, mailbox);
220
+ if (!rights.has('r')) {
221
+ return { text: rights.has('l') ? 'Permission denied' : 'Mailbox does not exist' };
222
+ }
223
+ return false;
224
+ });
225
+
226
+ // MULTISEARCH (RFC 7377 section 2.2): searching needs "r", and "l" for a mailbox that the client did not name.
227
+ // Other mailboxes are left out without an error
228
+ server.searchAccessChecks.push((connection, mailbox, named) => {
229
+ const rights = getRights(connection, mailbox);
230
+ return rights.has('r') && (named || rights.has('l'));
231
+ });
232
+
233
+ // Rights on the selected mailbox are taken when it is selected (RFC 4314 section 5.1.1 allows caching
234
+ // them), so the FLAGS and PERMANENTFLAGS a session got stay correct until the next SELECT
235
+ const getSelectedRights = connection => connection.aclRights || ALL_RIGHTS;
236
+
237
+ // RFC 4314 section 4: "t" for \Deleted, "s" for \Seen and "w" for all other flags
238
+ const flagRight = flag => {
239
+ flag = normalizeSystemFlag(flag);
240
+ if (flag === '\\Deleted') {
241
+ return 't';
242
+ }
243
+ if (flag === '\\Seen') {
244
+ return 's';
245
+ }
246
+ return 'w';
247
+ };
248
+
249
+ const flagValue = flag => (typeof flag === 'string' ? flag : String((flag && flag.value) || ''));
250
+
251
+ const canSetFlag = (rights, flag) => rights.has(flagRight(flagValue(flag)));
252
+
253
+ const mailboxArgument = (parsed, position) => {
254
+ const attr = (parsed.attributes || [])[position];
255
+ return isAstring(attr) ? attr.value : false;
256
+ };
257
+
258
+ // the mailbox named by an argument, false if there is no such mailbox or the argument is invalid
259
+ const argumentMailbox = (parsed, position) => {
260
+ const name = mailboxArgument(parsed, position);
261
+ return (name !== false && server.getMailbox(name)) || false;
262
+ };
263
+
264
+ const sendNoPerm = (connection, parsed, data, description) => connection.sendStatus(parsed, data, 'NO', 'Permission denied', 'NOPERM', description);
265
+
266
+ /**
267
+ * Refuses a command for lack of rights. Without the "l" right the user gets the same error
268
+ * as for a mailbox that does not exist, so the existence of the mailbox is not disclosed
269
+ * (RFC 4314 section 6), otherwise NO [NOPERM] (RFC 5530 section 3)
270
+ */
271
+ const sendDenied = (connection, parsed, data, rights, description, notFound) => {
272
+ if (rights.has('l')) {
273
+ sendNoPerm(connection, parsed, data, description);
274
+ } else if (notFound === 'TRYCREATE') {
275
+ connection.sendStatus(parsed, data, 'NO', 'Target mailbox does not exist', 'TRYCREATE', description);
276
+ } else {
277
+ connection.sendStatus(parsed, data, 'NO', 'Mailbox does not exist', 'NONEXISTENT', description);
278
+ }
279
+ };
280
+
281
+ /**
282
+ * Finds the nearest existing parent of a mailbox name (RFC 4314 section 4, CREATE)
283
+ *
284
+ * @param {String} path Mailbox path
285
+ * @return {Object|Boolean} parent mailbox or false if there is none
286
+ */
287
+ const getParent = path => {
288
+ for (let parent = server.getParentPath(path); parent; parent = server.getParentPath(parent)) {
289
+ const mailbox = server.getMailbox(parent);
290
+ if (mailbox) {
291
+ return mailbox;
292
+ }
293
+ }
294
+ return false;
295
+ };
296
+
297
+ // Checks for the ACL commands, true if the command can go ahead
298
+ const checkAclCommand = (connection, parsed, data, mailbox, isAllowed) => {
299
+ if (!mailbox) {
300
+ connection.sendStatus(parsed, data, 'NO', 'Mailbox does not exist', 'NONEXISTENT');
301
+ return false;
302
+ }
303
+ const rights = getRights(connection, mailbox);
304
+ if (!isAllowed(rights)) {
305
+ sendDenied(connection, parsed, data, rights, false);
306
+ return false;
307
+ }
308
+ return true;
309
+ };
310
+
311
+ const hasAdmin = rights => rights.has('a');
312
+
313
+ /**
314
+ * Validates the arguments of an ACL command: `count` astrings, the first one a mailbox name and
315
+ * the second one (if any) an identifier
316
+ *
317
+ * @return {Boolean} true if the arguments are fine, otherwise BAD was sent
318
+ */
319
+ const checkArguments = (connection, parsed, data, count) => {
320
+ const args = parsed.attributes || [];
321
+ const command = parsed.command.toUpperCase();
322
+ if (args.length !== count || args.some(arg => !isAstring(arg))) {
323
+ connection.sendStatus(parsed, data, 'BAD', command + ' expects ' + (count === 1 ? 'a mailbox name' : count + ' string arguments'));
324
+ return false;
325
+ }
326
+ const error = count > 1 && identifierError(args[1].value);
327
+ if (error) {
328
+ // RFC 4314 section 3: an identifier that fails preparation is refused with BAD
329
+ connection.sendStatus(parsed, data, 'BAD', error);
330
+ return false;
331
+ }
332
+ if (count > 1) {
333
+ // user names and identifiers are unicode strings, whatever way the user logged in
334
+ args[1].value = Buffer.from(args[1].value, 'binary').toString('utf-8');
335
+ }
336
+ return true;
337
+ };
338
+
339
+ // RFC 4314 section 2: an implementation MAY force rights to always be granted to an identifier
340
+ const refuseOwnerIdentifier = (connection, parsed, data) => {
341
+ const identifier = parsed.attributes[1].value;
342
+ if (identifier !== owner && identifier !== '-' + owner) {
343
+ return false;
344
+ }
345
+ connection.sendStatus(parsed, data, 'NO', 'Rights of the mailbox owner can not be changed', 'CANNOT');
346
+ return true;
347
+ };
348
+
349
+ const aclCommand = (count, handler) => (connection, parsed, data, callback) => {
350
+ if (checkArguments(connection, parsed, data, count)) {
351
+ handler(connection, parsed, data, server.getMailbox(parsed.attributes[0].value));
352
+ }
353
+ callback();
354
+ };
355
+
356
+ const aclOptions = { states: states.AUTHENTICATED, mailboxArguments: [0] };
357
+ // RFC 4314 section 7: identifier and mod-rights are astrings
358
+ const identifierOptions = Object.assign({ astringArguments: [1] }, aclOptions);
359
+
360
+ // RFC 4314 section 3.8: myrights-data = "MYRIGHTS" SP mailbox SP rights
361
+ const sendMyRights = (connection, mailbox, parsed, data) =>
362
+ connection.send(
363
+ { tag: '*', command: 'MYRIGHTS', attributes: mailboxResponse(mailbox, [formatRights(getRights(connection, mailbox))]) },
364
+ 'MYRIGHTS',
365
+ parsed,
366
+ data,
367
+ mailbox
368
+ );
369
+
370
+ // RFC 8440 section 3: the MYRIGHTS return option of an extended LIST. The registry is shared with
371
+ // LIST-EXTENDED, which accepts the option only when it is loaded, in any load order
372
+ const listExtensions = getListExtensions(server);
373
+ server.registerCapability('LIST-MYRIGHTS', () => listExtensions.enabled);
374
+ listExtensions.returnOptions.MYRIGHTS = {
375
+ // a MYRIGHTS response follows the LIST response of every listed mailbox that matches the
376
+ // selection criteria and exists, not the ones listed only for CHILDINFO or as \NonExistent
377
+ onItem: (connection, folder, value, info, parsed, data) => {
378
+ if (info.matched && info.exists) {
379
+ sendMyRights(connection, folder, parsed, data);
380
+ }
381
+ }
382
+ };
383
+
384
+ // RFC 4314 section 3.1
385
+ server.setCommandHandler(
386
+ 'SETACL',
387
+ aclCommand(3, (connection, parsed, data, mailbox) => {
388
+ const identifier = parsed.attributes[1].value;
389
+ const modification = parsed.attributes[2].value;
390
+ const mode = /^[+-]/.test(modification) ? modification.charAt(0) : '';
391
+
392
+ // an unrecognized right MUST cause BAD, before anything else is checked
393
+ const rights = parseRights(modification.substr(mode.length));
394
+
395
+ if (!checkAclCommand(connection, parsed, data, mailbox, hasAdmin) || refuseOwnerIdentifier(connection, parsed, data)) {
396
+ return;
397
+ }
398
+
399
+ changeAcl(mailbox, acl => {
400
+ const current = new Set(acl.get(identifier) || []);
401
+ if (mode === '+') {
402
+ rights.forEach(right => current.add(right));
403
+ } else if (mode === '-') {
404
+ rights.forEach(right => current.delete(right));
405
+ }
406
+ const updated = mode ? current : rights;
407
+ if (updated.size) {
408
+ acl.set(identifier, updated);
409
+ } else {
410
+ acl.delete(identifier);
411
+ }
412
+ });
413
+ connection.sendStatus(parsed, data, 'OK', 'Setacl complete');
414
+ }),
415
+ Object.assign({ astringArguments: [1, 2] }, aclOptions)
416
+ );
417
+
418
+ // RFC 4314 section 3.2
419
+ server.setCommandHandler(
420
+ 'DELETEACL',
421
+ aclCommand(2, (connection, parsed, data, mailbox) => {
422
+ if (!checkAclCommand(connection, parsed, data, mailbox, hasAdmin) || refuseOwnerIdentifier(connection, parsed, data)) {
423
+ return;
424
+ }
425
+ changeAcl(mailbox, acl => acl.delete(parsed.attributes[1].value));
426
+ connection.sendStatus(parsed, data, 'OK', 'Deleteacl complete');
427
+ }),
428
+ identifierOptions
429
+ );
430
+
431
+ // RFC 4314 sections 3.3 and 3.6
432
+ server.setCommandHandler(
433
+ 'GETACL',
434
+ aclCommand(1, (connection, parsed, data, mailbox) => {
435
+ if (!checkAclCommand(connection, parsed, data, mailbox, hasAdmin)) {
436
+ return;
437
+ }
438
+ const attributes = [owner, formatRights(ALL_RIGHTS)];
439
+ getAcl(mailbox).forEach((rights, identifier) => {
440
+ attributes.push(identifier, formatRights(rights));
441
+ });
442
+ connection.send({ tag: '*', command: 'ACL', attributes: mailboxResponse(mailbox, attributes) }, 'ACL', parsed, data, mailbox);
443
+ connection.sendStatus(parsed, data, 'OK', 'Getacl complete');
444
+ }),
445
+ aclOptions
446
+ );
447
+
448
+ // RFC 4314 sections 3.4 and 3.7
449
+ server.setCommandHandler(
450
+ 'LISTRIGHTS',
451
+ aclCommand(2, (connection, parsed, data, mailbox) => {
452
+ if (!checkAclCommand(connection, parsed, data, mailbox, hasAdmin)) {
453
+ return;
454
+ }
455
+ // the identifier is sent back in the same form as the client used
456
+ const identifier = parsed.attributes[1].value;
457
+ let attributes;
458
+ if (identifier === owner) {
459
+ // the owner always has every right, nothing is optional
460
+ attributes = [identifier, formatRights(ALL_RIGHTS)];
461
+ } else {
462
+ // nothing is granted always, every right can be granted on its own, including the virtual ones
463
+ attributes = [identifier, ''].concat(RIGHTS.split(''), Object.keys(VIRTUAL_RIGHTS));
464
+ }
465
+ connection.send({ tag: '*', command: 'LISTRIGHTS', attributes: mailboxResponse(mailbox, attributes) }, 'LISTRIGHTS', parsed, data, mailbox);
466
+ connection.sendStatus(parsed, data, 'OK', 'Listrights complete');
467
+ }),
468
+ identifierOptions
469
+ );
470
+
471
+ // RFC 4314 sections 3.5 and 3.8
472
+ server.setCommandHandler(
473
+ 'MYRIGHTS',
474
+ aclCommand(1, (connection, parsed, data, mailbox) => {
475
+ if (!checkAclCommand(connection, parsed, data, mailbox, rights => ANY_RIGHTS.some(right => rights.has(right)))) {
476
+ return;
477
+ }
478
+ sendMyRights(connection, mailbox, parsed, data);
479
+ connection.sendStatus(parsed, data, 'OK', 'Myrights complete');
480
+ }),
481
+ aclOptions
482
+ );
483
+
484
+ // Enforcement (RFC 4314 section 4). Commands are wrapped once every plugin is loaded, so that
485
+ // the commands of every plugin (MOVE, UID EXPUNGE, ...) are covered whatever the load order is
486
+ const wrappers = {};
487
+ // the handlers that the enforcement wraps
488
+ const prevHandlers = {};
489
+ const wrapCommands = () => {
490
+ Object.keys(wrappers).forEach(command => {
491
+ const prevHandler = server.getCommandHandler(command);
492
+ if (prevHandler) {
493
+ prevHandlers[command] = prevHandler;
494
+ server.setCommandHandler(command, (connection, parsed, data, callback) => {
495
+ if (isOwner(connection)) {
496
+ return prevHandler(connection, parsed, data, callback);
497
+ }
498
+ return wrappers[command](prevHandler, connection, parsed, data, callback);
499
+ });
500
+ }
501
+ });
502
+
503
+ // Return options of an extended LIST (LIST-STATUS, LIST-MYRIGHTS) add responses for the listed
504
+ // mailboxes, so they skip the mailboxes that are not listed for the user. STATUS also needs "r"
505
+ // (RFC 5819 section 2: the STATUS response MUST NOT be returned if the mailbox can not be selected)
506
+ Object.keys(listExtensions.returnOptions).forEach(name => {
507
+ const option = listExtensions.returnOptions[name];
508
+ if (typeof option.onItem !== 'function') {
509
+ return;
510
+ }
511
+ const onItem = option.onItem;
512
+ const right = name === 'STATUS' ? 'r' : 'l';
513
+ option.onItem = (connection, folder, ...args) => {
514
+ if (isOwner(connection)) {
515
+ return onItem(connection, folder, ...args);
516
+ }
517
+ const rights = getRights(connection, folder);
518
+ if (rights.has('l') && rights.has(right)) {
519
+ return onItem(connection, folder, ...args);
520
+ }
521
+ };
522
+ });
523
+
524
+ // RFC 4314 section 4: STORE changes only the flags the user has rights for
525
+ STORE_ITEMS.forEach(item => {
526
+ const prevHandler = server.storeHandlers[item] || builtinStoreHandlers[item];
527
+ server.storeHandlers[item] = (connection, message, flags, ...args) => {
528
+ const rights = getSelectedRights(connection);
529
+ if (FLAG_RIGHTS.every(right => rights.has(right))) {
530
+ return prevHandler(connection, message, flags, ...args);
531
+ }
532
+ flags = [].concat(flags);
533
+ // invalid flags are still refused with BAD, even if the user could not set them anyway
534
+ flags.forEach(flag => checkSystemFlags(connection, normalizeSystemFlag(flagValue(flag))));
535
+ let allowed = flags.filter(flag => canSetFlag(rights, flag));
536
+ if (item.charAt(0) !== '+' && item.charAt(0) !== '-') {
537
+ // replacing the flags keeps the ones the user can not change
538
+ allowed = message.flags.filter(flag => !canSetFlag(rights, flag)).concat(allowed);
539
+ }
540
+ return prevHandler(connection, message, allowed, ...args);
541
+ };
542
+ });
543
+ };
544
+
545
+ server.once('pluginsLoaded', wrapCommands);
546
+
547
+ const selectWrapper = (prevHandler, connection, parsed, data, callback) => {
548
+ const mailbox = argumentMailbox(parsed, 0);
549
+ if (!mailbox) {
550
+ return prevHandler(connection, parsed, data, callback);
551
+ }
552
+ const command = parsed.command.toUpperCase();
553
+ const rights = getRights(connection, mailbox);
554
+ if (!rights.has('r')) {
555
+ // a failed SELECT leaves no mailbox selected (RFC 3501 section 6.3.1)
556
+ beginSelect(connection, parsed, data);
557
+ connection.closeMailbox();
558
+ sendDenied(connection, parsed, data, rights, command + ' FAILED');
559
+ return callback();
560
+ }
561
+ connection.aclRights = rights;
562
+ if (command === 'SELECT' && !WRITE_RIGHTS.some(right => rights.has(right))) {
563
+ // RFC 4314 section 5.2: the server MUST answer READ-ONLY, so the mailbox is opened like EXAMINE does
564
+ return prevHandlers.EXAMINE(connection, parsed, data, callback);
565
+ }
566
+ return prevHandler(connection, parsed, data, callback);
567
+ };
568
+ wrappers.SELECT = selectWrapper;
569
+ wrappers.EXAMINE = selectWrapper;
570
+
571
+ // Checks that the user has a right on the mailbox named by an argument. Unknown mailboxes and
572
+ // invalid arguments are left to the command handler
573
+ const hasMailboxRight = (right, position, notFound) => (connection, parsed, data) => {
574
+ const mailbox = argumentMailbox(parsed, position);
575
+ const rights = mailbox && getRights(connection, mailbox);
576
+ if (rights && !rights.has(right)) {
577
+ sendDenied(connection, parsed, data, rights, false, notFound);
578
+ return false;
579
+ }
580
+ return true;
581
+ };
582
+
583
+ const requireRight = (right, position, notFound) => {
584
+ const check = hasMailboxRight(right, position, notFound);
585
+ return (prevHandler, connection, parsed, data, callback) =>
586
+ check(connection, parsed, data) ? prevHandler(connection, parsed, data, callback) : callback();
587
+ };
588
+
589
+ wrappers.STATUS = requireRight('r', 0);
590
+ // SUBSCRIBE checks that the mailbox exists, so it needs "l". UNSUBSCRIBE needs no rights
591
+ wrappers.SUBSCRIBE = requireRight('l', 0);
592
+
593
+ wrappers.DELETE = requireRight('x', 0);
594
+
595
+ // CREATE needs "k" on the nearest existing parent, a mailbox at the top level can not be created
596
+ const checkCreateRight = (connection, parsed, data, path) => {
597
+ const parent = getParent(path);
598
+ if (!parent || !getRights(connection, parent).has('k')) {
599
+ sendNoPerm(connection, parsed, data);
600
+ return false;
601
+ }
602
+ return true;
603
+ };
604
+
605
+ wrappers.CREATE = (prevHandler, connection, parsed, data, callback) => {
606
+ const name = mailboxArgument(parsed, 0);
607
+ if (name !== false && !checkCreateRight(connection, parsed, data, name)) {
608
+ return callback();
609
+ }
610
+ return prevHandler(connection, parsed, data, callback);
611
+ };
612
+
613
+ // RENAME needs "x" on the mailbox and "k" on the new parent
614
+ const hasRenameRight = hasMailboxRight('x', 0);
615
+ wrappers.RENAME = (prevHandler, connection, parsed, data, callback) => {
616
+ if (!hasRenameRight(connection, parsed, data)) {
617
+ return callback();
618
+ }
619
+ const name = mailboxArgument(parsed, 1);
620
+ if (name !== false && argumentMailbox(parsed, 0) && !checkCreateRight(connection, parsed, data, name)) {
621
+ return callback();
622
+ }
623
+ return prevHandler(connection, parsed, data, callback);
624
+ };
625
+
626
+ // APPEND needs "i", and only the flags the user has rights for are stored
627
+ wrappers.APPEND = (prevHandler, connection, parsed, data, callback) => {
628
+ const mailbox = argumentMailbox(parsed, 0);
629
+ if (!mailbox) {
630
+ return prevHandler(connection, parsed, data, callback);
631
+ }
632
+ const rights = getRights(connection, mailbox);
633
+ if (!rights.has('i')) {
634
+ sendDenied(connection, parsed, data, rights, 'APPEND FAILED', 'TRYCREATE');
635
+ return callback();
636
+ }
637
+ const flags = parsed.attributes[1];
638
+ if (Array.isArray(flags) && flags.every(flag => flag && flag.type === 'ATOM')) {
639
+ try {
640
+ flags.forEach(flag => checkSystemFlags(connection, normalizeSystemFlag(flag.value)));
641
+ // the server MUST NOT fail APPEND for flags the user can not set
642
+ parsed.attributes[1] = flags.filter(flag => canSetFlag(rights, flag));
643
+ } catch {
644
+ // invalid flags are refused with BAD by the APPEND handler
645
+ }
646
+ }
647
+ return prevHandler(connection, parsed, data, callback);
648
+ };
649
+
650
+ // the refusal for a target mailbox without "i", see sendDenied
651
+ const insertRefusal = (connection, mailbox) => {
652
+ const rights = mailbox ? getRights(connection, mailbox) : false;
653
+ if (!rights || rights.has('i')) {
654
+ return false;
655
+ }
656
+ return rights.has('l')
657
+ ? { command: 'NO', code: 'NOPERM', text: 'Permission denied' }
658
+ : { command: 'NO', code: 'TRYCREATE', text: 'Target mailbox does not exist' };
659
+ };
660
+
661
+ // COPY and MOVE need "i" on the target, MOVE also needs what UID STORE \Deleted and UID EXPUNGE
662
+ // need on the source, "t" and "e" (RFC 6851 section 4.2)
663
+ const requireInsert = requireRight('i', 1, 'TRYCREATE');
664
+ const copyWrapper = isMove => (prevHandler, connection, parsed, data, callback) => {
665
+ const command = parsed.command.toUpperCase();
666
+ if (isMove && !connection.readOnly) {
667
+ const rights = getSelectedRights(connection);
668
+ if (!rights.has('t') || !rights.has('e')) {
669
+ sendNoPerm(connection, parsed, data, command + ' FAIL');
670
+ return callback();
671
+ }
672
+ }
673
+ return requireInsert(prevHandler, connection, parsed, data, callback);
674
+ };
675
+ wrappers.COPY = copyWrapper(false);
676
+ wrappers['UID COPY'] = copyWrapper(false);
677
+ wrappers.MOVE = copyWrapper(true);
678
+ wrappers['UID MOVE'] = copyWrapper(true);
679
+
680
+ // RFC 8508 section 4.1: REPLACE needs the rights of UID STORE (\Deleted, "t") and UID EXPUNGE ("e") on the
681
+ // selected mailbox, and of APPEND ("i") on the target. A read-only session is refused by the command handler
682
+ const replaceRefusal = (connection, mailbox) => {
683
+ const selected = getSelectedRights(connection);
684
+ if (!connection.readOnly && (!selected.has('t') || !selected.has('e'))) {
685
+ return { command: 'NO', code: 'NOPERM', text: 'Permission denied' };
686
+ }
687
+ return insertRefusal(connection, mailbox);
688
+ };
689
+ const replaceWrapper = (prevHandler, connection, parsed, data, callback) => {
690
+ const refusal = replaceRefusal(connection, argumentMailbox(parsed, 1));
691
+ if (refusal) {
692
+ connection.sendStatus(parsed, data, refusal.command, refusal.text, refusal.code, parsed.command.toUpperCase() + ' FAILED');
693
+ return callback();
694
+ }
695
+ return prevHandler(connection, parsed, data, callback);
696
+ };
697
+ wrappers.REPLACE = replaceWrapper;
698
+ wrappers['UID REPLACE'] = replaceWrapper;
699
+
700
+ // The message literal of APPEND and REPLACE is refused before it is sent when the user may not add messages to the
701
+ // target, with the same answer as after the literal. A mailbox the user can not see ("l") is refused like a
702
+ // missing one, which the core literal filter refuses before the literal too, so the transcripts do not tell
703
+ // a hidden mailbox from a missing one (RFC 4314 section 6)
704
+ server.literalFilters.unshift((connection, command, line) => {
705
+ if (isOwner(connection) || connection.isBusy()) {
706
+ return false;
707
+ }
708
+ const path = getPendingTarget(connection, command, line);
709
+ const mailbox = path !== false && server.getMailbox(path);
710
+ if (!mailbox) {
711
+ return false;
712
+ }
713
+ return command === 'APPEND' ? insertRefusal(connection, mailbox) : replaceRefusal(connection, mailbox);
714
+ });
715
+
716
+ // RFC 5464 section 3.3: mailbox annotations need "l" and any of "r", "s", "w", "i" or "p". Server
717
+ // annotations (the empty mailbox name) are not covered by mailbox ACLs
718
+ const METADATA_RIGHTS = ['r', 's', 'w', 'i', 'p'];
719
+ const canUseMetadata = rights => rights.has('l') && METADATA_RIGHTS.some(right => rights.has(right));
720
+ const metadataWrapper = (prevHandler, connection, parsed, data, callback) => {
721
+ const args = parsed.attributes || [];
722
+ // GETMETADATA takes its options before the mailbox name (RFC 5464 errata 2785)
723
+ const position = parsed.command.toUpperCase() === 'GETMETADATA' && Array.isArray(args[0]) ? 1 : 0;
724
+ const mailbox = argumentMailbox(parsed, position);
725
+ if (mailbox && mailboxArgument(parsed, position) !== '') {
726
+ const rights = getRights(connection, mailbox);
727
+ if (!canUseMetadata(rights)) {
728
+ sendDenied(connection, parsed, data, rights, false);
729
+ return callback();
730
+ }
731
+ }
732
+ return prevHandler(connection, parsed, data, callback);
733
+ };
734
+ wrappers.GETMETADATA = metadataWrapper;
735
+ wrappers.SETMETADATA = metadataWrapper;
736
+
737
+ // Unsolicited METADATA responses (RFC 5464 section 4.4.2) only go to sessions that could read the
738
+ // annotations of the mailbox. Server annotations have an empty mailbox name
739
+ server.notifyFilters.push((connection, notification) => {
740
+ const command = notification.command || {};
741
+ if (command.command !== 'METADATA' || isOwner(connection)) {
742
+ return true;
743
+ }
744
+ const name = command.attributes && command.attributes[0];
745
+ const path = name && typeof name === 'object' ? name.value : name;
746
+ const mailbox = typeof path === 'string' && path !== '' && server.getMailbox(path);
747
+ return !mailbox || canUseMetadata(getRights(connection, mailbox));
748
+ });
749
+
750
+ // RFC 9208 section 6: GETQUOTA needs no rights. GETQUOTAROOT needs "r" for the MESSAGE and STORAGE
751
+ // resources, which tell about the messages of the mailbox, only MAILBOX needs no right. Without "r"
752
+ // (or for a mailbox that does not exist, so nothing is disclosed) the QUOTA responses only list MAILBOX
753
+ wrappers.GETQUOTAROOT = (prevHandler, connection, parsed, data, callback) => {
754
+ const mailbox = argumentMailbox(parsed, 0);
755
+ if (!mailbox || !getRights(connection, mailbox).has('r')) {
756
+ parsed.aclQuotaMailboxOnly = true;
757
+ }
758
+ return prevHandler(connection, parsed, data, callback);
759
+ };
760
+
761
+ // RFC 9208 section 6: SETQUOTA needs "a". The quota root is not a mailbox, so the user needs "a" on
762
+ // every mailbox the quota root applies to (all personal mailboxes)
763
+ wrappers.SETQUOTA = (prevHandler, connection, parsed, data, callback) => {
764
+ const args = parsed.attributes || [];
765
+ if (args.length === 2 && Array.isArray(args[1])) {
766
+ const isAdmin = Object.keys(server.folderCache).every(path => {
767
+ const mailbox = server.folderCache[path];
768
+ return !server.isPersonal(mailbox) || getRights(connection, mailbox).has('a');
769
+ });
770
+ if (!isAdmin) {
771
+ sendNoPerm(connection, parsed, data);
772
+ return callback();
773
+ }
774
+ }
775
+ return prevHandler(connection, parsed, data, callback);
776
+ };
777
+
778
+ // EXPUNGE needs "e". In a read-only session the command handler refuses it already
779
+ const expungeWrapper = (prevHandler, connection, parsed, data, callback) => {
780
+ if (!connection.readOnly && !getSelectedRights(connection).has('e')) {
781
+ sendNoPerm(connection, parsed, data);
782
+ return callback();
783
+ }
784
+ return prevHandler(connection, parsed, data, callback);
785
+ };
786
+ wrappers.EXPUNGE = expungeWrapper;
787
+ wrappers['UID EXPUNGE'] = expungeWrapper;
788
+
789
+ // UNAUTHENTICATE (RFC 8437): rights of the previous user must not stay with the session
790
+ server.resetHandlers.push(connection => {
791
+ connection.aclRights = null;
792
+ });
793
+
794
+ server.connectionHandlers.push(connection => {
795
+ // A FETCH that implies setting \Seen MUST NOT set it without "s", and without "e" CLOSE MUST
796
+ // close the mailbox without expunging (RFC 4314 section 4)
797
+ const canSetSeen = connection.canSetSeen;
798
+ const canExpunge = connection.canExpunge;
799
+ connection.canSetSeen = () => canSetSeen.call(connection) && getSelectedRights(connection).has('s');
800
+ connection.canExpunge = () => canExpunge.call(connection) && getSelectedRights(connection).has('e');
801
+ });
802
+
803
+ // STORE needs the right of at least one of the flags it changes, it SHOULD NOT fail otherwise.
804
+ // The flags themselves are filtered by the store handlers
805
+ const storeWrapper = (prevHandler, connection, parsed, data, callback) => {
806
+ const rights = getSelectedRights(connection);
807
+ const args = parsed.attributes || [];
808
+ const item = args.length >= 3 && args[args.length - 2] && args[args.length - 2].type === 'ATOM' && args[args.length - 2].value.toUpperCase();
809
+ if (connection.readOnly || !item) {
810
+ return prevHandler(connection, parsed, data, callback);
811
+ }
812
+ if (STORE_ITEMS.indexOf(item) < 0) {
813
+ // other items of plugins (e.g. X-GM-LABELS) change shared message data like keywords do, so they
814
+ // need "w". Unknown items are refused with BAD by the STORE handler
815
+ if (Object.hasOwn(server.storeHandlers, item) && !rights.has('w')) {
816
+ sendNoPerm(connection, parsed, data);
817
+ return callback();
818
+ }
819
+ return prevHandler(connection, parsed, data, callback);
820
+ }
821
+ const flags = [].concat(args[args.length - 1] || []);
822
+ try {
823
+ flags.forEach(flag => checkSystemFlags(connection, normalizeSystemFlag(flagValue(flag))));
824
+ } catch {
825
+ // invalid flags are refused with BAD by the STORE handler
826
+ return prevHandler(connection, parsed, data, callback);
827
+ }
828
+ let allowed;
829
+ if (item.charAt(0) === '+' || item.charAt(0) === '-') {
830
+ allowed = !flags.length || flags.some(flag => canSetFlag(rights, flag));
831
+ } else {
832
+ // replacing the flags changes every flag the user has rights for
833
+ allowed = FLAG_RIGHTS.some(right => rights.has(right));
834
+ }
835
+ if (!allowed) {
836
+ sendNoPerm(connection, parsed, data);
837
+ return callback();
838
+ }
839
+ return prevHandler(connection, parsed, data, callback);
840
+ };
841
+ wrappers.STORE = storeWrapper;
842
+ wrappers['UID STORE'] = storeWrapper;
843
+
844
+ // Checks if an extended LIST command asks for the STATUS return option (RFC 5819)
845
+ const hasStatusReturnOption = parsed => {
846
+ const args = (parsed && parsed.attributes) || [];
847
+ return args.some(
848
+ (arg, i) =>
849
+ arg &&
850
+ arg.type === 'ATOM' &&
851
+ /^RETURN$/i.test(arg.value) &&
852
+ Array.isArray(args[i + 1]) &&
853
+ args[i + 1].some(option => option && option.type === 'ATOM' && /^STATUS$/i.test(option.value))
854
+ );
855
+ };
856
+
857
+ // Checks if any mailbox below a mailbox is visible to the user
858
+ const hasVisibleChildren = (connection, mailbox) => server.hasDescendant(mailbox.path, child => getRights(connection, child).has('l'));
859
+
860
+ server.outputHandlers.push((connection, response, description, parsed, data, extra) => {
861
+ if (isOwner(connection)) {
862
+ return;
863
+ }
864
+
865
+ if ((description === 'LIST ITEM' || description === 'LSUB ITEM') && extra && extra.path) {
866
+ // LIST and LSUB need "l", the mailbox is left out without it and never answered with NO
867
+ if (!getRights(connection, extra).has('l')) {
868
+ response.skipResponse = true;
869
+ return;
870
+ }
871
+ const flags = response.attributes[0];
872
+ if (!Array.isArray(flags)) {
873
+ return;
874
+ }
875
+ response.attributes[0] = flags.slice();
876
+ const index = flags.findIndex(flag => flag && flag.value === '\\HasChildren');
877
+ if (index >= 0 && !hasVisibleChildren(connection, extra)) {
878
+ // children that the user can not see do not count (RFC 3348 section 4)
879
+ response.attributes[0][index] = { type: 'ATOM', value: '\\HasNoChildren' };
880
+ }
881
+ // RFC 5819 section 2: a mailbox that gets no STATUS response because it can not be selected
882
+ // (no "r" right) MUST be listed with \Noselect
883
+ const isSelectable = !flags.some(flag => flag && /^\\(Noselect|NonExistent)$/i.test(flag.value));
884
+ if (isSelectable && hasStatusReturnOption(parsed) && !getRights(connection, extra).has('r')) {
885
+ response.attributes[0].push({ type: 'ATOM', value: '\\Noselect' });
886
+ }
887
+ return;
888
+ }
889
+
890
+ if (description === 'QUOTA' && parsed && parsed.aclQuotaMailboxOnly && Array.isArray(response.attributes[1])) {
891
+ // quota-resource = resource-name SP resource-usage SP resource-limit
892
+ const list = response.attributes[1];
893
+ const filtered = [];
894
+ for (let i = 0; i < list.length; i += 3) {
895
+ if (list[i] && String(list[i].value).toUpperCase() === 'MAILBOX') {
896
+ filtered.push(list[i], list[i + 1], list[i + 2]);
897
+ }
898
+ }
899
+ // quota-list needs at least one resource, so without a MAILBOX limit there is no QUOTA response
900
+ if (filtered.length) {
901
+ response.attributes[1] = filtered;
902
+ } else {
903
+ response.skipResponse = true;
904
+ }
905
+ return;
906
+ }
907
+
908
+ if (description === 'SELECT PERMANENTFLAGS') {
909
+ // RFC 4314 section 5.1.1: PERMANENTFLAGS MUST reflect the rights of the user
910
+ const section = response.attributes[0] && response.attributes[0].section;
911
+ const rights = getSelectedRights(connection);
912
+ if (section && Array.isArray(section[1])) {
913
+ section[1] = section[1].filter(flag => (flag.value === '\\*' ? rights.has('w') : canSetFlag(rights, flag)));
914
+ }
915
+ return;
916
+ }
917
+
918
+ if (extra && extra.targetUids && extra.mailbox && /^(UID )?(COPY|MOVE COPYUID)$/.test(description)) {
919
+ // RFC 4314 section 4: the copies only keep the flags the user has rights for in the target
920
+ const rights = getRights(connection, extra.mailbox);
921
+ const uids = new Set(extra.targetUids);
922
+ extra.mailbox.messages.forEach(message => {
923
+ if (uids.has(message.uid)) {
924
+ message.flags = message.flags.filter(flag => canSetFlag(rights, flag));
925
+ }
926
+ });
927
+ }
928
+ });
929
+
930
+ // The DELETE command MUST delete the ACL of the mailbox (RFC 4314 section 4), this matters when a
931
+ // \Noselect placeholder stays for the children of the mailbox
932
+ const prevDelete = server.getCommandHandler('DELETE');
933
+ server.setCommandHandler('DELETE', (connection, parsed, data, callback) => {
934
+ const mailbox = argumentMailbox(parsed, 0);
935
+ prevDelete(connection, parsed, data, () => {
936
+ const placeholder = argumentMailbox(parsed, 0);
937
+ if (placeholder && placeholder !== mailbox) {
938
+ delete placeholder.acl;
939
+ }
940
+ callback();
941
+ });
942
+ });
943
+
944
+ // A new mailbox inherits the ACL of its parent (RFC 4314 section 4, CREATE SHOULD)
945
+ const prevCreate = server.getCommandHandler('CREATE');
946
+ server.setCommandHandler('CREATE', (connection, parsed, data, callback) => {
947
+ const existing = new Set(Object.keys(server.folderCache));
948
+ prevCreate(connection, parsed, data, () => {
949
+ Object.keys(server.folderCache)
950
+ .filter(path => !existing.has(path))
951
+ .sort((a, b) => a.length - b.length)
952
+ .forEach(path => {
953
+ const parent = getParent(path);
954
+ const mailbox = server.folderCache[path];
955
+ if (parent && parent !== mailbox) {
956
+ const acl = new Map();
957
+ getAcl(parent).forEach((rights, identifier) => acl.set(identifier, new Set(rights)));
958
+ mailbox.acl = acl;
959
+ }
960
+ });
961
+ callback();
962
+ });
963
+ });
964
+ };