imapkit 4.3.0 → 5.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 (93) hide show
  1. package/README.md +167 -51
  2. package/bin/help.txt +17 -3
  3. package/bin/imapkit.js +53 -23
  4. package/dist/cjs/commands/append.js +1 -1
  5. package/dist/cjs/commands/create.js +2 -2
  6. package/dist/cjs/commands/delete.js +2 -14
  7. package/dist/cjs/commands/handlers/flags.d.ts +3 -3
  8. package/dist/cjs/commands/handlers/flags.js +3 -3
  9. package/dist/cjs/commands/handlers/store.js +3 -3
  10. package/dist/cjs/commands/rename.js +6 -46
  11. package/dist/cjs/commands/select.js +1 -0
  12. package/dist/cjs/commands/subscribe.js +7 -8
  13. package/dist/cjs/commands/unsubscribe.js +3 -9
  14. package/dist/cjs/control.d.ts +430 -0
  15. package/dist/cjs/control.js +637 -0
  16. package/dist/cjs/index.d.ts +8 -1
  17. package/dist/cjs/index.js +9 -2
  18. package/dist/cjs/load-plugins.d.ts +1 -1
  19. package/dist/cjs/load-plugins.js +7 -2
  20. package/dist/cjs/plugins/acl.js +109 -42
  21. package/dist/cjs/plugins/condstore.js +7 -0
  22. package/dist/cjs/plugins/index.js +1 -3
  23. package/dist/cjs/plugins/metadata.js +140 -57
  24. package/dist/cjs/plugins/objectid.js +10 -0
  25. package/dist/cjs/plugins/quota.js +50 -2
  26. package/dist/cjs/plugins/savedate.js +1 -1
  27. package/dist/cjs/plugins/special-use.d.ts +0 -8
  28. package/dist/cjs/plugins/special-use.js +23 -0
  29. package/dist/cjs/quirks.d.ts +28 -0
  30. package/dist/cjs/quirks.js +124 -0
  31. package/dist/cjs/random.d.ts +10 -0
  32. package/dist/cjs/random.js +22 -0
  33. package/dist/cjs/rest.d.ts +50 -0
  34. package/dist/cjs/rest.js +561 -0
  35. package/dist/cjs/script.d.ts +33 -4
  36. package/dist/cjs/script.js +64 -4
  37. package/dist/cjs/server.d.ts +149 -8
  38. package/dist/cjs/server.js +429 -152
  39. package/dist/cjs/smtp-listener.d.ts +3 -2
  40. package/dist/cjs/smtp-listener.js +10 -6
  41. package/dist/cjs/storage-schema.d.ts +139 -0
  42. package/dist/cjs/storage-schema.js +215 -0
  43. package/dist/cjs/store-operations.d.ts +168 -0
  44. package/dist/cjs/store-operations.js +422 -0
  45. package/dist/cjs/types.d.ts +22 -1
  46. package/dist/esm/commands/append.js +1 -1
  47. package/dist/esm/commands/create.js +2 -2
  48. package/dist/esm/commands/delete.js +2 -14
  49. package/dist/esm/commands/handlers/flags.d.ts +3 -3
  50. package/dist/esm/commands/handlers/flags.js +3 -3
  51. package/dist/esm/commands/handlers/store.js +3 -3
  52. package/dist/esm/commands/rename.js +6 -46
  53. package/dist/esm/commands/select.js +1 -0
  54. package/dist/esm/commands/subscribe.js +7 -8
  55. package/dist/esm/commands/unsubscribe.js +3 -9
  56. package/dist/esm/control.d.ts +430 -0
  57. package/dist/esm/control.js +630 -0
  58. package/dist/esm/index.d.ts +8 -1
  59. package/dist/esm/index.js +5 -2
  60. package/dist/esm/load-plugins.d.ts +1 -1
  61. package/dist/esm/load-plugins.js +7 -2
  62. package/dist/esm/plugins/acl.js +109 -42
  63. package/dist/esm/plugins/condstore.js +7 -0
  64. package/dist/esm/plugins/index.js +1 -3
  65. package/dist/esm/plugins/metadata.js +140 -57
  66. package/dist/esm/plugins/objectid.js +10 -0
  67. package/dist/esm/plugins/quota.js +50 -2
  68. package/dist/esm/plugins/savedate.js +1 -1
  69. package/dist/esm/plugins/special-use.d.ts +0 -8
  70. package/dist/esm/plugins/special-use.js +23 -0
  71. package/dist/esm/quirks.d.ts +28 -0
  72. package/dist/esm/quirks.js +120 -0
  73. package/dist/esm/random.d.ts +10 -0
  74. package/dist/esm/random.js +19 -0
  75. package/dist/esm/rest.d.ts +50 -0
  76. package/dist/esm/rest.js +553 -0
  77. package/dist/esm/script.d.ts +33 -4
  78. package/dist/esm/script.js +63 -4
  79. package/dist/esm/server.d.ts +149 -8
  80. package/dist/esm/server.js +429 -152
  81. package/dist/esm/smtp-listener.d.ts +3 -2
  82. package/dist/esm/smtp-listener.js +10 -6
  83. package/dist/esm/storage-schema.d.ts +139 -0
  84. package/dist/esm/storage-schema.js +211 -0
  85. package/dist/esm/store-operations.d.ts +168 -0
  86. package/dist/esm/store-operations.js +407 -0
  87. package/dist/esm/types.d.ts +22 -1
  88. package/dist/plugin-help.json +0 -14
  89. package/package.json +11 -3
  90. package/dist/cjs/plugins/xtoybird.d.ts +0 -2
  91. package/dist/cjs/plugins/xtoybird.js +0 -236
  92. package/dist/esm/plugins/xtoybird.d.ts +0 -2
  93. package/dist/esm/plugins/xtoybird.js +0 -218
@@ -0,0 +1,422 @@
1
+ "use strict";
2
+ /**
3
+ * Changes of the message store that IMAP commands and the control API share: expunge, flag changes, and the
4
+ * mailbox operations of DELETE, RENAME, SUBSCRIBE and UNSUBSCRIBE. Each one tells the sessions about the change
5
+ * the way another session's command would (EXISTS, EXPUNGE, unsolicited FETCH, BYE, `mailbox` events). The
6
+ * session that caused a change is `server.activeConnection` (null for the control API), the commands keep only
7
+ * what is specific to a session: argument checks, the selected mailbox, read-only state and the responses.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.ImapKitError = exports.DEFAULT_SESSION_TIMEOUT = void 0;
11
+ exports.storeError = storeError;
12
+ exports.expungeMessages = expungeMessages;
13
+ exports.notifyFlagChanges = notifyFlagChanges;
14
+ exports.checkFlags = checkFlags;
15
+ exports.changeFlags = changeFlags;
16
+ exports.createMailbox = createMailbox;
17
+ exports.deleteMailbox = deleteMailbox;
18
+ exports.renameMailbox = renameMailbox;
19
+ exports.resetUidValidity = resetUidValidity;
20
+ exports.subscribeMailbox = subscribeMailbox;
21
+ exports.unsubscribeMailbox = unsubscribeMailbox;
22
+ const flags_js_1 = require("./commands/handlers/flags.js");
23
+ const random_js_1 = require("./random.js");
24
+ const numbers_js_1 = require("./numbers.js");
25
+ /** the XOAUTH2 session timeout of the default user, and of a user the control API adds without one */
26
+ const DEFAULT_SESSION_TIMEOUT = 3600 * 1000;
27
+ exports.DEFAULT_SESSION_TIMEOUT = DEFAULT_SESSION_TIMEOUT;
28
+ /** An error of a failed store operation or control API call, `code` is a RFC 5530 response code or INVALID */
29
+ class ImapKitError extends Error {
30
+ constructor(message, code) {
31
+ super(message);
32
+ this.name = 'ImapKitError';
33
+ this.code = code;
34
+ }
35
+ }
36
+ exports.ImapKitError = ImapKitError;
37
+ /**
38
+ * Creates an error for a failed operation, with a RFC 5530 response code
39
+ *
40
+ * @param {String} message Error message
41
+ * @param {String} code Response code, e.g. "NONEXISTENT"
42
+ * @return {Error} Error object
43
+ */
44
+ function storeError(message, code) {
45
+ return new ImapKitError(message, code);
46
+ }
47
+ /**
48
+ * Removes messages from a mailbox and tells the sessions that have it selected with EXPUNGE responses
49
+ * (VANISHED with QRESYNC) and a new EXISTS. The `expunge` event goes out before the notifications
50
+ *
51
+ * @param {Object} server IMAP server
52
+ * @param {Object} mailbox Mailbox of the messages
53
+ * @param {Array|Function} messagesOrFilterFunc Messages to remove, or a function that returns true for them
54
+ * @param {Object} [options] `{ origin, skip, skipExists, highestFirst }`
55
+ * @return {Array} the removed messages
56
+ */
57
+ function expungeMessages(server, mailbox, messagesOrFilterFunc, options = {}) {
58
+ let filterFunc;
59
+ if (Array.isArray(messagesOrFilterFunc)) {
60
+ const messageSet = new Set(messagesOrFilterFunc);
61
+ filterFunc = (message) => messageSet.has(message);
62
+ }
63
+ else {
64
+ filterFunc = messagesOrFilterFunc;
65
+ }
66
+ // sequence numbers of the removed messages, each one as it is after the earlier EXPUNGE responses. From the
67
+ // highest message down, the earlier responses do not change the sequence numbers of the later ones
68
+ const expunged = [];
69
+ const kept = [];
70
+ mailbox.messages.forEach((message, i) => {
71
+ if (filterFunc(message)) {
72
+ message.ghost = true;
73
+ expunged.push({ seq: options.highestFirst ? i + 1 : kept.length + 1, message });
74
+ }
75
+ else {
76
+ kept.push(message);
77
+ }
78
+ });
79
+ if (!expunged.length) {
80
+ return [];
81
+ }
82
+ // old copy is required for those sessions that run FETCH before
83
+ // displaying the EXPUNGE notice
84
+ const mailboxCopy = mailbox.messages.slice();
85
+ // update the list in place, other code might hold a reference to it
86
+ kept.forEach((message, i) => {
87
+ mailbox.messages[i] = message;
88
+ });
89
+ mailbox.messages.length = kept.length;
90
+ const messages = expunged.map(entry => entry.message);
91
+ const origin = options.origin !== undefined ? options.origin : server.activeConnection;
92
+ // lets plugins track the removal (e.g. mod-sequences of CONDSTORE and QRESYNC) before any notification
93
+ server.emit('expunge', mailbox, messages, origin);
94
+ (options.highestFirst ? expunged.slice().reverse() : expunged).forEach(entry => {
95
+ server.notify({
96
+ tag: '*',
97
+ attributes: [
98
+ entry.seq,
99
+ {
100
+ type: 'ATOM',
101
+ value: 'EXPUNGE'
102
+ }
103
+ ],
104
+ // the removed message, for plugins that report it differently (e.g. VANISHED of QRESYNC)
105
+ message: entry.message
106
+ }, mailbox, options.skip);
107
+ });
108
+ server.notify({
109
+ tag: '*',
110
+ attributes: [
111
+ mailbox.messages.length,
112
+ {
113
+ type: 'ATOM',
114
+ value: 'EXISTS'
115
+ }
116
+ ],
117
+ // distribute the old mailbox data with the notification
118
+ mailboxCopy: mailboxCopy
119
+ }, mailbox, options.skip || options.skipExists);
120
+ return messages;
121
+ }
122
+ /**
123
+ * Tells the sessions that have a mailbox selected about changed flags, they get an untagged FETCH with the
124
+ * new flags (RFC 3501 section 5.2)
125
+ *
126
+ * @param {Object} server IMAP server
127
+ * @param {Object} mailbox Mailbox of the messages
128
+ * @param {Array} messages Messages with changed flags
129
+ * @param {Object} [ignoreConnection] A session that is not told, e.g. the one whose STORE changed the flags
130
+ */
131
+ function notifyFlagChanges(server, mailbox, messages, ignoreConnection) {
132
+ if (messages.length) {
133
+ server.notify({ tag: '*', flagUpdate: messages }, mailbox, ignoreConnection);
134
+ }
135
+ }
136
+ /**
137
+ * Checks flags the way STORE does: system flags the server knows (never \Recent), valid keywords, and only
138
+ * permanent flags in a mailbox that does not allow new keywords
139
+ *
140
+ * @param {Object} server IMAP server
141
+ * @param {Object} mailbox Mailbox the flags are for
142
+ * @param {Array} flags Flags to check
143
+ * @param {Boolean} stored true if the flags are stored, so they must be permanent flags of the mailbox
144
+ * @return {Array} the normalized flags without duplicates
145
+ * @throws {Error} INVALID for a flag that can not be used
146
+ */
147
+ function checkFlags(server, mailbox, flags, stored) {
148
+ if (!Array.isArray(flags)) {
149
+ throw storeError('Flags must be an array', 'INVALID');
150
+ }
151
+ const list = [];
152
+ flags.forEach((value) => {
153
+ if (typeof value !== 'string') {
154
+ throw storeError('Flags must be strings', 'INVALID');
155
+ }
156
+ const flag = (0, flags_js_1.normalizeSystemFlag)(value);
157
+ try {
158
+ (0, flags_js_1.checkSystemFlags)(server, flag);
159
+ }
160
+ catch (err) {
161
+ throw storeError(err.message, 'INVALID');
162
+ }
163
+ if (stored && mailbox.permanentFlags.indexOf(flag) < 0 && !mailbox.allowPermanentFlags) {
164
+ throw storeError('Flag ' + flag + ' is not a permanent flag of ' + mailbox.path, 'INVALID');
165
+ }
166
+ if (list.indexOf(flag) < 0) {
167
+ list.push(flag);
168
+ }
169
+ });
170
+ return list;
171
+ }
172
+ /**
173
+ * Changes the flags of messages outside of a STORE command. The flags follow the rules of STORE: system flags
174
+ * the server knows (never \Recent), valid keywords, and only permanent flags in a mailbox that does not allow
175
+ * new keywords. Plugins learn about the changed messages from the `flags` event `(mailbox, messages, origin)`
176
+ * before the sessions are told (CONDSTORE gives them new mod-sequences there)
177
+ *
178
+ * @param {Object} server IMAP server
179
+ * @param {Object} mailbox Mailbox of the messages
180
+ * @param {Array} messages Messages to change
181
+ * @param {Array} flags Flags to set, add or remove
182
+ * @param {String} mode "set", "add" or "remove"
183
+ * @return {Array} the messages whose flags changed
184
+ */
185
+ function changeFlags(server, mailbox, messages, flags, mode) {
186
+ if (['set', 'add', 'remove'].indexOf(mode) < 0) {
187
+ throw storeError('Invalid flag mode ' + JSON.stringify(mode) + ', expected set, add or remove', 'INVALID');
188
+ }
189
+ const list = checkFlags(server, mailbox, flags, mode !== 'remove');
190
+ const changed = [];
191
+ messages.forEach(message => {
192
+ const before = message.flags.join(' ');
193
+ if (mode === 'set') {
194
+ message.flags = list.slice();
195
+ }
196
+ else if (mode === 'add') {
197
+ list.forEach(flag => server.ensureFlag(message.flags, flag));
198
+ }
199
+ else {
200
+ list.forEach(flag => server.removeFlag(message.flags, flag));
201
+ }
202
+ server.rememberFlags(mailbox, message.flags);
203
+ if (message.flags.join(' ') !== before) {
204
+ changed.push(message);
205
+ }
206
+ });
207
+ if (changed.length) {
208
+ server.emit('flags', mailbox, changed, server.activeConnection);
209
+ notifyFlagChanges(server, mailbox, changed, null);
210
+ }
211
+ return changed;
212
+ }
213
+ /**
214
+ * Creates a mailbox (CREATE), with any missing superior hierarchy levels
215
+ *
216
+ * @param {Object} server IMAP server
217
+ * @param {String} path Storage name of the mailbox
218
+ * @return {Object} the created mailbox
219
+ * @throws {Error} with a RFC 5530 `code` (ALREADYEXISTS, CANNOT ...)
220
+ */
221
+ function createMailbox(server, path) {
222
+ const existing = new Set(Object.keys(server.folderCache));
223
+ const mailbox = server.createMailbox(path);
224
+ // the superior levels that did not exist are new mailboxes as well, plugins set them up like the target (ACL)
225
+ const created = Object.keys(server.folderCache)
226
+ .filter(name => !existing.has(name))
227
+ .sort((a, b) => a.length - b.length);
228
+ server.mailboxChanged('create', mailbox.path, { created });
229
+ return mailbox;
230
+ }
231
+ /**
232
+ * Deletes a mailbox (DELETE). Other sessions that have it selected are disconnected with an untagged BYE
233
+ * (RFC 2180 section 3.3, RFC 2683 section 3.1.2), as they can not be told about the deletion in any other way
234
+ *
235
+ * @param {Object} server IMAP server
236
+ * @param {String} path Storage name of the mailbox
237
+ * @throws {Error} with a RFC 5530 `code` (NONEXISTENT, CANNOT, HASCHILDREN ...)
238
+ */
239
+ function deleteMailbox(server, path) {
240
+ const mailbox = server.getMailbox(path);
241
+ if (!mailbox) {
242
+ throw storeError('Mailbox does not exist', 'NONEXISTENT');
243
+ }
244
+ server.deleteMailbox(path);
245
+ server.mailboxChanged('delete', mailbox.path, { mailbox });
246
+ byeSelected(server, mailbox, 'Selected mailbox was deleted', 'MAILBOX DELETED');
247
+ }
248
+ /**
249
+ * Disconnects the sessions that have a mailbox selected with an untagged BYE, except the one that made the change
250
+ *
251
+ * @param {Object} server IMAP server
252
+ * @param {Object} mailbox Mailbox
253
+ * @param {String} text Text of the BYE
254
+ * @param {String} description Description for output handlers
255
+ */
256
+ function byeSelected(server, mailbox, text, description) {
257
+ const origin = server.activeConnection;
258
+ server.connections.forEach(connection => {
259
+ if (connection !== origin && connection.selectedMailbox === mailbox) {
260
+ connection.bye(text, description);
261
+ }
262
+ });
263
+ }
264
+ /**
265
+ * Renames a mailbox (RENAME). Renaming INBOX moves its messages to the new mailbox and leaves INBOX
266
+ * empty (RFC 3501 section 6.3.5). Everything that can be checked is checked up front, so that a failed
267
+ * rename never loses the source
268
+ *
269
+ * @param {Object} server IMAP server
270
+ * @param {String} source Storage name of the mailbox
271
+ * @param {String} destination New storage name
272
+ * @return {Object} `{ path, mailbox }`: the new name, and the source mailbox object, which is INBOX itself for
273
+ * a rename of INBOX
274
+ * @throws {Error} with a RFC 5530 `code` (NONEXISTENT, ALREADYEXISTS, CANNOT ...)
275
+ */
276
+ function renameMailbox(server, source, destination) {
277
+ const mailbox = server.getMailbox(source);
278
+ const target = server.getMailbox(destination);
279
+ if (!mailbox || mailbox.flags.indexOf('\\Noselect') >= 0) {
280
+ throw storeError('Mailbox does not exist', 'NONEXISTENT');
281
+ }
282
+ if (target && target.flags.indexOf('\\Noselect') < 0) {
283
+ throw storeError('Mailbox already exists', 'ALREADYEXISTS');
284
+ }
285
+ const separator = server.getSeparator(mailbox);
286
+ if (destination.substr(0, mailbox.path.length + separator.length) === mailbox.path + separator) {
287
+ throw storeError('Can not move a mailbox into itself', 'CANNOT');
288
+ }
289
+ const oldPath = mailbox.path;
290
+ let path;
291
+ if (source.toUpperCase() === 'INBOX') {
292
+ const renamed = server.createMailbox(destination);
293
+ path = renamed.path;
294
+ mailbox.messages.forEach((message) => {
295
+ server.copyMessage(renamed, message);
296
+ });
297
+ expungeMessages(server, mailbox, () => true);
298
+ }
299
+ else {
300
+ server.deleteMailbox(source, true);
301
+ try {
302
+ server.createMailbox(destination, mailbox);
303
+ }
304
+ catch (E) {
305
+ // put the source mailbox back where it was
306
+ server.createMailbox(source, mailbox);
307
+ server.indexFolders();
308
+ throw E;
309
+ }
310
+ server.indexFolders();
311
+ path = mailbox.path;
312
+ }
313
+ server.mailboxChanged('rename', path, { oldPath });
314
+ return { path, mailbox };
315
+ }
316
+ /**
317
+ * Gives a mailbox a new UIDVALIDITY, optionally with new UIDs. A UID never changes during a session (RFC 9051
318
+ * section 2.3.1.1), so the sessions that have the mailbox selected are disconnected with BYE, and the new value
319
+ * is greater than every earlier one, as UIDs that do not persist require. A client that keeps its cached UIDs
320
+ * without checking UIDVALIDITY gets other messages ("shuffle") or none ("offset") for them
321
+ *
322
+ * @param {Object} server IMAP server
323
+ * @param {Object} mailbox Mailbox, not \Noselect
324
+ * @param {Object} [options] `{ uidvalidity, uids, offset, seed }`
325
+ * @return {Object} `{ uidvalidity, uidnext, uids }`, `uids` maps every old UID to its new one
326
+ * @throws {Error} INVALID for a UIDVALIDITY that is not greater, or invalid options
327
+ */
328
+ function resetUidValidity(server, mailbox, options = {}) {
329
+ const mode = options.uids || 'keep';
330
+ if (['keep', 'renumber', 'shuffle', 'offset'].indexOf(mode) < 0) {
331
+ throw storeError('Invalid UID mode ' + JSON.stringify(mode) + ', expected keep, renumber, shuffle or offset', 'INVALID');
332
+ }
333
+ const offset = options.offset === undefined ? 0 : options.offset;
334
+ if (!Number.isSafeInteger(offset) || offset < 0) {
335
+ throw storeError('Offset must be a non-negative integer', 'INVALID');
336
+ }
337
+ let uidvalidity = options.uidvalidity;
338
+ if (uidvalidity === undefined) {
339
+ uidvalidity = Math.max(mailbox.uidvalidity, server.uidvalidityCounter) + 1;
340
+ }
341
+ else if (!Number.isSafeInteger(uidvalidity) || uidvalidity <= mailbox.uidvalidity || uidvalidity > 0xffffffff) {
342
+ // nz-number of RFC 3501 section 9, a 32-bit value
343
+ throw storeError('UIDVALIDITY must be an integer above ' + mailbox.uidvalidity + ' and below 2^32', 'INVALID');
344
+ }
345
+ if (options.seed !== undefined && !Number.isSafeInteger(options.seed)) {
346
+ throw storeError('Seed must be an integer', 'INVALID');
347
+ }
348
+ byeSelected(server, mailbox, 'UIDVALIDITY of the selected mailbox changed', 'UIDVALIDITY CHANGED');
349
+ const messages = mailbox.messages;
350
+ const oldUids = messages.map(message => message.uid);
351
+ let newUids = oldUids;
352
+ if (mode === 'renumber' || mode === 'shuffle') {
353
+ newUids = messages.map((message, i) => i + 1);
354
+ if (mode === 'shuffle') {
355
+ const random = (0, random_js_1.seededRandom)(options.seed === undefined ? Math.floor(Math.random() * numbers_js_1.MAX_NUMBER) : options.seed);
356
+ // Fisher-Yates
357
+ for (let i = newUids.length - 1; i > 0; i--) {
358
+ const j = Math.floor(random() * (i + 1));
359
+ [newUids[i], newUids[j]] = [newUids[j], newUids[i]];
360
+ }
361
+ }
362
+ mailbox.uidnext = messages.length + 1;
363
+ }
364
+ else if (mode === 'offset') {
365
+ const shift = mailbox.uidnext - 1 + offset;
366
+ newUids = oldUids.map(uid => uid + shift);
367
+ mailbox.uidnext += shift;
368
+ }
369
+ messages.forEach((message, i) => {
370
+ message.uid = newUids[i];
371
+ });
372
+ if (mode === 'shuffle') {
373
+ // the other modes keep the UID order
374
+ messages.sort((a, b) => a.uid - b.uid);
375
+ }
376
+ mailbox.uidvalidity = uidvalidity;
377
+ server.uidvalidityCounter = Math.max(server.uidvalidityCounter, uidvalidity);
378
+ return {
379
+ uidvalidity,
380
+ uidnext: mailbox.uidnext,
381
+ uids: oldUids.map((uid, i) => ({ uid, newUid: newUids[i] }))
382
+ };
383
+ }
384
+ /**
385
+ * Subscribes a mailbox (SUBSCRIBE), only an existing mailbox can be subscribed
386
+ *
387
+ * @param {Object} server IMAP server
388
+ * @param {String} path Storage name of the mailbox
389
+ * @return {Boolean} true if the subscription changed
390
+ * @throws {Error} NONEXISTENT for a name that is not a mailbox
391
+ */
392
+ function subscribeMailbox(server, path) {
393
+ const mailbox = server.getMailbox(path);
394
+ if (!mailbox || mailbox.flags.indexOf('\\Noselect') >= 0) {
395
+ throw storeError('Mailbox does not exist', 'NONEXISTENT');
396
+ }
397
+ if (mailbox.subscribed) {
398
+ return false;
399
+ }
400
+ mailbox.subscribed = true;
401
+ server.mailboxChanged('subscribe', mailbox.path);
402
+ return true;
403
+ }
404
+ /**
405
+ * Unsubscribes a name (UNSUBSCRIBE). The subscription list holds names, so a name stays removable after its
406
+ * mailbox is deleted (RFC 3501 section 6.3.6). Removing a name that is not subscribed is not an error
407
+ * (RFC 9051 section 6.3.8)
408
+ *
409
+ * @param {Object} server IMAP server
410
+ * @param {String} path Storage name
411
+ * @return {Boolean} true if the subscription changed
412
+ */
413
+ function unsubscribeMailbox(server, path) {
414
+ if (path.toUpperCase() === 'INBOX') {
415
+ path = 'INBOX';
416
+ }
417
+ if (!server.subscriptions.delete(path)) {
418
+ return false;
419
+ }
420
+ server.mailboxChanged('unsubscribe', path);
421
+ return true;
422
+ }
@@ -72,6 +72,8 @@ export interface MailboxChangeEvent {
72
72
  oldPath: string | null;
73
73
  mailbox: Mailbox | null;
74
74
  origin: IMAPConnection | null;
75
+ /** "create": every mailbox the change created, superior hierarchy levels first */
76
+ created?: string[] | undefined;
75
77
  }
76
78
  export type Callback = (err?: any, ...args: any[]) => void;
77
79
  /** Handler of an IMAP command, it must send a tagged response and then call the callback */
@@ -222,6 +224,18 @@ export interface IMAPServerOptions {
222
224
  plugins?: (string | Plugin)[] | string | Plugin | undefined;
223
225
  users?: Record<string, UserData> | undefined;
224
226
  secureConnection?: boolean | undefined;
227
+ /** starts an SMTP server with `server.start()` that appends every received message to INBOX, needs the optional
228
+ * smtp-server package */
229
+ smtp?: {
230
+ port?: number | undefined;
231
+ host?: string | undefined;
232
+ } | undefined;
233
+ /** starts the REST API with `server.start()`, see src/rest.ts. A host other than a loopback address needs a token */
234
+ rest?: {
235
+ port?: number | undefined;
236
+ host?: string | undefined;
237
+ token?: string | undefined;
238
+ } | undefined;
225
239
  credentials?: {
226
240
  key: string | Buffer;
227
241
  cert: string | Buffer;
@@ -232,6 +246,12 @@ export interface IMAPServerOptions {
232
246
  maxLiteralSize?: number | undefined;
233
247
  /** script rules that make the server misbehave on purpose, see src/script.ts and README "Scripted faults" */
234
248
  script?: ScriptRule | ScriptRule[] | undefined;
249
+ /** the current time for the dates the server sets (INTERNALDATE, SAVEDATE): a Date, a timestamp or a function */
250
+ now?: Date | number | (() => Date | number) | undefined;
251
+ /** quirk presets that make the server behave like a known real server, see src/quirks.ts */
252
+ quirks?: string[] | string | undefined;
253
+ /** seed of the random numbers of script rules with `chance`, so that a run can be repeated */
254
+ scriptSeed?: number | undefined;
235
255
  [key: string]: any;
236
256
  }
237
257
  /**
@@ -318,7 +338,8 @@ export type MessageHandler = (server: IMAPServer, message: Message, mailbox: Mai
318
338
  export type MailboxHandler = (server: IMAPServer, mailbox: Mailbox) => void;
319
339
  export type CopyHandler = (server: IMAPServer, source: Message, properties: Record<string, any>, mailbox: Mailbox) => void;
320
340
  export type NotifyFilter = (connection: IMAPConnection, event: NotifyEvent) => boolean;
321
- export type AppendCheck = (connection: IMAPConnection, mailbox: Mailbox, messages: AppendMessage[], options: AppendCheckOptions) => CheckResult | false | null | undefined | void;
341
+ /** `server.appendChecks`, `connection` is null for the control API (`addMessage` with `checks`) */
342
+ export type AppendCheck = (connection: IMAPConnection | null, mailbox: Mailbox, messages: AppendMessage[], options: AppendCheckOptions) => CheckResult | false | null | undefined | void;
322
343
  export type LiteralFilter = (connection: IMAPConnection, command: string, line: string, size: number) => Refusal | false | null | undefined;
323
344
  export type UrlAccessCheck = (connection: IMAPConnection, mailbox: Mailbox, url: any) => {
324
345
  text: string;
@@ -32,7 +32,7 @@ function parseFlags(connection, list) {
32
32
  throw new Error('Invalid flags argument');
33
33
  }
34
34
  const value = normalizeSystemFlag(flag.value);
35
- checkSystemFlags(connection, value);
35
+ checkSystemFlags(connection.server, value);
36
36
  return value;
37
37
  });
38
38
  }
@@ -1,3 +1,4 @@
1
+ import { createMailbox } from '../store-operations.js';
1
2
  export default function createCommand(connection, parsed, data, callback) {
2
3
  if (!parsed.attributes || parsed.attributes.length !== 1 || !parsed.attributes[0] || ['STRING', 'LITERAL', 'ATOM'].indexOf(parsed.attributes[0].type) < 0) {
3
4
  connection.send({
@@ -15,7 +16,7 @@ export default function createCommand(connection, parsed, data, callback) {
15
16
  const path = parsed.attributes[0].value;
16
17
  let mailbox;
17
18
  try {
18
- mailbox = connection.server.createMailbox(path);
19
+ mailbox = createMailbox(connection.server, path);
19
20
  }
20
21
  catch (err) {
21
22
  const E = err;
@@ -23,7 +24,6 @@ export default function createCommand(connection, parsed, data, callback) {
23
24
  connection.sendStatus(parsed, data, 'NO', E.message, E.code, 'CREATE FAILED');
24
25
  return callback();
25
26
  }
26
- connection.server.mailboxChanged('create', mailbox.path);
27
27
  connection.send({
28
28
  tag: parsed.tag,
29
29
  command: 'OK',
@@ -1,3 +1,4 @@
1
+ import { deleteMailbox } from '../store-operations.js';
1
2
  export default function deleteCommand(connection, parsed, data, callback) {
2
3
  if (!parsed.attributes || parsed.attributes.length !== 1 || !parsed.attributes[0] || ['STRING', 'LITERAL', 'ATOM'].indexOf(parsed.attributes[0].type) < 0) {
3
4
  connection.send({
@@ -13,27 +14,14 @@ export default function deleteCommand(connection, parsed, data, callback) {
13
14
  return callback();
14
15
  }
15
16
  const path = parsed.attributes[0].value;
16
- const mailbox = connection.server.getMailbox(path);
17
- if (!mailbox) {
18
- connection.sendStatus(parsed, data, 'NO', 'Mailbox does not exist', 'NONEXISTENT', 'DELETE FAILED');
19
- return callback();
20
- }
21
17
  try {
22
- connection.server.deleteMailbox(path);
18
+ deleteMailbox(connection.server, path);
23
19
  }
24
20
  catch (err) {
25
21
  const E = err;
26
22
  connection.sendStatus(parsed, data, 'NO', E.message, E.code, 'DELETE FAILED');
27
23
  return callback();
28
24
  }
29
- connection.server.mailboxChanged('delete', mailbox.path, { mailbox });
30
- // RFC 2180 section 3.3: the other sessions that have the mailbox selected are disconnected with an untagged
31
- // BYE (RFC 2683 section 3.1.2), as they can not be told about the deletion in any other way
32
- connection.server.connections.forEach(other => {
33
- if (other !== connection && other.selectedMailbox === mailbox) {
34
- other.bye('Selected mailbox was deleted', 'MAILBOX DELETED');
35
- }
36
- });
37
25
  connection.send({
38
26
  tag: parsed.tag,
39
27
  command: 'OK',
@@ -1,4 +1,4 @@
1
- import type { IMAPConnection } from '../../types.js';
1
+ import type { IMAPServer } from '../../types.js';
2
2
  declare const INVALID_KEYWORD_CHAR: RegExp;
3
3
  /**
4
4
  * System flags are case-insensitive, so "\seen" becomes "\Seen"
@@ -10,8 +10,8 @@ declare function normalizeSystemFlag(flag: string): string;
10
10
  /**
11
11
  * Throws if a flag can not be stored: an unknown system flag (including \Recent) or an invalid keyword
12
12
  *
13
- * @param {Object} connection IMAP connection
13
+ * @param {Object} server IMAP server, its systemFlags are the system flags that can be stored
14
14
  * @param {String} flag Normalized flag value
15
15
  */
16
- declare function checkSystemFlags(connection: IMAPConnection, flag: string): void;
16
+ declare function checkSystemFlags(server: IMAPServer, flag: string): void;
17
17
  export { INVALID_KEYWORD_CHAR, normalizeSystemFlag, checkSystemFlags };
@@ -18,12 +18,12 @@ function normalizeSystemFlag(flag) {
18
18
  /**
19
19
  * Throws if a flag can not be stored: an unknown system flag (including \Recent) or an invalid keyword
20
20
  *
21
- * @param {Object} connection IMAP connection
21
+ * @param {Object} server IMAP server, its systemFlags are the system flags that can be stored
22
22
  * @param {String} flag Normalized flag value
23
23
  */
24
- function checkSystemFlags(connection, flag) {
24
+ function checkSystemFlags(server, flag) {
25
25
  if (flag.charAt(0) === '\\') {
26
- if (connection.server.systemFlags.indexOf(flag) < 0) {
26
+ if (server.systemFlags.indexOf(flag) < 0) {
27
27
  throw new Error('Invalid system flag ' + flag);
28
28
  }
29
29
  }
@@ -7,7 +7,7 @@ function setFlags(connection, message, flags) {
7
7
  const messageFlags = [];
8
8
  [].concat(flags).forEach(flag => {
9
9
  flag = normalizeSystemFlag(typeof flag === 'string' ? flag : String(flag.value || ''));
10
- checkSystemFlags(connection, flag);
10
+ checkSystemFlags(connection.server, flag);
11
11
  // Ignore if it is not in allowed list and only permament flags are allowed to use
12
12
  if (mailbox.permanentFlags.indexOf(flag) < 0 && !mailbox.allowPermanentFlags) {
13
13
  return;
@@ -23,7 +23,7 @@ function addFlags(connection, message, flags) {
23
23
  const mailbox = connection.selectedMailbox;
24
24
  [].concat(flags).forEach(flag => {
25
25
  flag = normalizeSystemFlag(typeof flag === 'string' ? flag : String(flag.value || ''));
26
- checkSystemFlags(connection, flag);
26
+ checkSystemFlags(connection.server, flag);
27
27
  // Ignore if it is not in allowed list and only permament flags are allowed to use
28
28
  if (mailbox.permanentFlags.indexOf(flag) < 0 && !mailbox.allowPermanentFlags) {
29
29
  return;
@@ -37,7 +37,7 @@ function addFlags(connection, message, flags) {
37
37
  function removeFlags(connection, message, flags) {
38
38
  [].concat(flags).forEach(flag => {
39
39
  flag = normalizeSystemFlag(typeof flag === 'string' ? flag : String(flag.value || ''));
40
- checkSystemFlags(connection, flag);
40
+ checkSystemFlags(connection.server, flag);
41
41
  if (message.flags.indexOf(flag) >= 0) {
42
42
  for (let i = 0; i < message.flags.length; i++) {
43
43
  if (message.flags[i] === flag) {
@@ -1,3 +1,4 @@
1
+ import { renameMailbox } from '../store-operations.js';
1
2
  export default function renameCommand(connection, parsed, data, callback) {
2
3
  if (!parsed.attributes ||
3
4
  parsed.attributes.length !== 2 ||
@@ -19,56 +20,15 @@ export default function renameCommand(connection, parsed, data, callback) {
19
20
  }
20
21
  const source = parsed.attributes[0].value;
21
22
  const destination = parsed.attributes[1].value;
22
- const server = connection.server;
23
- const mailbox = server.getMailbox(source);
24
- const target = server.getMailbox(destination);
25
- const sendError = (code, message) => {
26
- connection.sendStatus(parsed, data, 'NO', message, code, 'RENAME FAILED');
27
- return callback();
28
- };
29
- // check everything that can be checked up front, so that a failed RENAME never loses the source
30
- if (!mailbox || mailbox.flags.indexOf('\\Noselect') >= 0) {
31
- return sendError('NONEXISTENT', 'Mailbox does not exist');
32
- }
33
- if (target && target.flags.indexOf('\\Noselect') < 0) {
34
- return sendError('ALREADYEXISTS', 'Mailbox already exists');
35
- }
36
- const separator = server.getSeparator(mailbox);
37
- if (destination.substr(0, mailbox.path.length + separator.length) === mailbox.path + separator) {
38
- return sendError('CANNOT', 'Can not move a mailbox into itself');
39
- }
40
- const oldPath = mailbox.path;
41
- let newPath;
23
+ let renamed;
42
24
  try {
43
- if (source.toUpperCase() === 'INBOX') {
44
- // Renaming INBOX moves its messages to the new mailbox and leaves INBOX empty (RFC 3501 section 6.3.5)
45
- const newMailbox = server.createMailbox(destination);
46
- newPath = newMailbox.path;
47
- mailbox.messages.forEach((message) => {
48
- server.copyMessage(newMailbox, message);
49
- });
50
- connection.expungeSpecificMessages(mailbox, () => true);
51
- }
52
- else {
53
- server.deleteMailbox(source, true);
54
- try {
55
- server.createMailbox(destination, mailbox);
56
- }
57
- catch (E) {
58
- // put the source mailbox back where it was
59
- server.createMailbox(source, mailbox);
60
- server.indexFolders();
61
- throw E;
62
- }
63
- server.indexFolders();
64
- newPath = mailbox.path;
65
- }
25
+ renamed = renameMailbox(connection.server, source, destination);
66
26
  }
67
27
  catch (err) {
68
28
  const E = err;
69
- return sendError(E.code, E.message);
29
+ connection.sendStatus(parsed, data, 'NO', E.message, E.code, 'RENAME FAILED');
30
+ return callback();
70
31
  }
71
- server.mailboxChanged('rename', newPath, { oldPath });
72
32
  connection.send({
73
33
  tag: parsed.tag,
74
34
  command: 'OK',
@@ -78,6 +38,6 @@ export default function renameCommand(connection, parsed, data, callback) {
78
38
  value: 'RENAME completed'
79
39
  }
80
40
  ]
81
- }, 'RENAME', parsed, data, mailbox);
41
+ }, 'RENAME', parsed, data, renamed.mailbox);
82
42
  return callback();
83
43
  }