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