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,637 @@
1
+ "use strict";
2
+ /**
3
+ * The control API, `server.control`: inspects and changes the server state from a test, without an IMAP session.
4
+ * Every change reaches the connected sessions as if another session had made it (EXISTS, EXPUNGE or VANISHED,
5
+ * unsolicited FETCH, BYE, NOTIFY events, CONDSTORE mod-sequences), with no session as its origin. The API is the
6
+ * operator: ACL does not apply, but every argument is checked. Mailboxes are addressed by their storage name
7
+ * (modified UTF-7), messages by mailbox and UID. Methods return plain data and throw an ImapKitError with a `code`:
8
+ * NONEXISTENT, ALREADYEXISTS, INVALID, or a RFC 5530 code of a failed mailbox operation (CANNOT, HASCHILDREN ...)
9
+ */
10
+ var __importDefault = (this && this.__importDefault) || function (mod) {
11
+ return (mod && mod.__esModule) ? mod : { "default": mod };
12
+ };
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.ImapKitError = exports.Control = void 0;
15
+ const mailbox_name_js_1 = __importDefault(require("./mailbox-name.js"));
16
+ const store_operations_js_1 = require("./store-operations.js");
17
+ Object.defineProperty(exports, "ImapKitError", { enumerable: true, get: function () { return store_operations_js_1.ImapKitError; } });
18
+ /**
19
+ * Copies a value of a plugin into the snapshot as JSON data, values that JSON can not hold are left out
20
+ *
21
+ * @param {*} value Any value
22
+ * @return {*} a JSON copy, or undefined
23
+ */
24
+ function jsonCopy(value) {
25
+ if (value === undefined || typeof value === 'function') {
26
+ return undefined;
27
+ }
28
+ // strings, numbers and booleans are immutable, the message source does not need a round trip
29
+ return value === null || typeof value !== 'object' ? value : JSON.parse(JSON.stringify(value));
30
+ }
31
+ /**
32
+ * Copies the properties of an object as JSON data, except the listed keys and values JSON can not hold
33
+ *
34
+ * @param {Object} source Object to copy
35
+ * @param {Set} skip Keys to leave out
36
+ * @return {Object} copy
37
+ */
38
+ function copyProperties(source, skip) {
39
+ const result = {};
40
+ Object.keys(source).forEach(key => {
41
+ const value = skip.has(key) ? undefined : jsonCopy(source[key]);
42
+ if (value !== undefined) {
43
+ result[key] = value;
44
+ }
45
+ });
46
+ return result;
47
+ }
48
+ const MESSAGE_SKIP = new Set(['ghost']);
49
+ const NAMESPACE_SKIP = new Set(['folders']);
50
+ // properties the server derives when it loads a mailbox, a snapshot leaves them out
51
+ const DERIVED_MAILBOX_KEYS = new Set(['path', 'namespace', 'messages', 'folders', 'subscribed', 'flags']);
52
+ const DERIVED_FLAGS = new Set(['\\HasChildren', '\\HasNoChildren']);
53
+ class Control {
54
+ constructor(server) {
55
+ this.server = server;
56
+ this.routes = [];
57
+ this.snapshotHandlers = [];
58
+ this.mailboxInfoHandlers = [];
59
+ this.messageInfoHandlers = [];
60
+ }
61
+ /**
62
+ * Adds an operation of a plugin, e.g. `server.control.setAcl()` of ACL, so that a plugin that is not loaded
63
+ * leaves no trace. The operation runs with no session as its origin, like the built-in ones, and throws
64
+ * ImapKitErrors (see storeError() in store-operations.ts)
65
+ *
66
+ * @param {String} name Method name on server.control
67
+ * @param {Function} fn Operation
68
+ * @param {Array} [routes] REST routes of the operation
69
+ */
70
+ register(name, fn, routes) {
71
+ if (name in this) {
72
+ throw new Error('Control operation ' + name + ' exists already');
73
+ }
74
+ this[name] = (...args) => this.run(() => fn(...args));
75
+ this.routes.push(...(routes || []));
76
+ }
77
+ /**
78
+ * Runs a change with no session as its origin, also when a listener of a command event calls the API
79
+ *
80
+ * @param {Function} fn Change
81
+ * @return {*} the result of fn
82
+ */
83
+ run(fn) {
84
+ return this.server.withOrigin(null, fn);
85
+ }
86
+ /**
87
+ * Finds a mailbox by its storage name
88
+ *
89
+ * @param {String} path Storage name, INBOX in any case
90
+ * @param {Boolean} [selectable] true if the mailbox must hold messages (not \Noselect)
91
+ * @return {Object} mailbox
92
+ * @throws {ImapKitError} NONEXISTENT
93
+ */
94
+ requireMailbox(path, selectable) {
95
+ const mailbox = typeof path === 'string' && path ? this.server.getMailbox(path) : undefined;
96
+ if (!mailbox || (selectable && mailbox.flags.indexOf('\\Noselect') >= 0)) {
97
+ throw new store_operations_js_1.ImapKitError('Mailbox ' + JSON.stringify(path) + ' does not exist', 'NONEXISTENT');
98
+ }
99
+ return mailbox;
100
+ }
101
+ /**
102
+ * Checks a new mailbox name
103
+ *
104
+ * @param {String} path Storage name
105
+ * @throws {ImapKitError} INVALID for a name that is not valid modified UTF-7
106
+ */
107
+ checkName(path) {
108
+ this.checkPath(path);
109
+ const problem = (0, mailbox_name_js_1.default)(path);
110
+ if (problem) {
111
+ throw new store_operations_js_1.ImapKitError(problem, 'INVALID');
112
+ }
113
+ }
114
+ /**
115
+ * Checks that a mailbox argument is a name, the store operations check the rest
116
+ *
117
+ * @param {String} path Storage name
118
+ * @throws {ImapKitError} INVALID
119
+ */
120
+ checkPath(path) {
121
+ if (typeof path !== 'string' || !path) {
122
+ throw new store_operations_js_1.ImapKitError('Mailbox name must be a non-empty string', 'INVALID');
123
+ }
124
+ }
125
+ /**
126
+ * Finds messages by UID
127
+ *
128
+ * @param {Object} mailbox Mailbox
129
+ * @param {Array} uids UIDs of the messages
130
+ * @return {Array} messages in the order of the UIDs, without duplicates
131
+ * @throws {ImapKitError} INVALID for a list that is not UIDs, NONEXISTENT for a UID that is not in the mailbox
132
+ */
133
+ requireMessages(mailbox, uids) {
134
+ if (!Array.isArray(uids) || uids.some(uid => !Number.isSafeInteger(uid) || uid < 1)) {
135
+ throw new store_operations_js_1.ImapKitError('UIDs must be an array of positive integers', 'INVALID');
136
+ }
137
+ const byUid = new Map(mailbox.messages.map(message => [message.uid, message]));
138
+ const missing = uids.filter(uid => !byUid.has(uid));
139
+ if (missing.length) {
140
+ throw new store_operations_js_1.ImapKitError('No message with UID ' + missing.join(', ') + ' in ' + mailbox.path, 'NONEXISTENT');
141
+ }
142
+ return [...new Set(uids)].map(uid => byUid.get(uid));
143
+ }
144
+ /**
145
+ * Finds the sessions a filter selects
146
+ *
147
+ * @param {Number|Object} filter A session number, or `{ session, user }`
148
+ * @return {Array} connections
149
+ */
150
+ findSessions(filter) {
151
+ const query = typeof filter === 'number' ? { session: filter } : filter || {};
152
+ if (query.session === undefined && query.user === undefined) {
153
+ throw new store_operations_js_1.ImapKitError('Select sessions by number or user', 'INVALID');
154
+ }
155
+ // a session that is closing already (BYE) is left out
156
+ return [...this.server.connections].filter(connection => connection.isOpen() &&
157
+ (query.session === undefined || connection.sessionNumber === query.session) &&
158
+ (query.user === undefined || connection.username === query.user));
159
+ }
160
+ // Inspection
161
+ /**
162
+ * Returns the storage as JSON data in the shape of the `storage` option, so that `imapkit({ storage })` with it
163
+ * starts from the same mailboxes, messages, UIDs, flags and subscriptions. Plugin data on mailboxes and messages
164
+ * is included as far as it is JSON data. Subscriptions of names that are not mailboxes are not part of it
165
+ *
166
+ * @return {Object} storage
167
+ */
168
+ snapshot() {
169
+ const server = this.server;
170
+ const copyMailbox = (mailbox) => {
171
+ const result = copyProperties(mailbox, DERIVED_MAILBOX_KEYS);
172
+ this.snapshotHandlers.forEach(handler => handler(mailbox, result));
173
+ result.flags = mailbox.flags.filter(flag => !DERIVED_FLAGS.has(flag));
174
+ result.subscribed = !!mailbox.subscribed;
175
+ result.messages = mailbox.messages.map(message => copyProperties(message, MESSAGE_SKIP));
176
+ if (mailbox.folders) {
177
+ result.folders = copyFolders(mailbox.folders);
178
+ }
179
+ return result;
180
+ };
181
+ const copyFolders = (folders) => {
182
+ const result = {};
183
+ Object.keys(folders).forEach(name => {
184
+ result[name] = copyMailbox(folders[name]);
185
+ });
186
+ return result;
187
+ };
188
+ const storage = {};
189
+ Object.keys(server.storage).forEach(key => {
190
+ if (key === 'INBOX') {
191
+ storage.INBOX = copyMailbox(server.storage.INBOX);
192
+ return;
193
+ }
194
+ const namespace = server.storage[key];
195
+ storage[key] = Object.assign(copyProperties(namespace, NAMESPACE_SKIP), { folders: copyFolders(namespace.folders || {}) });
196
+ });
197
+ return storage;
198
+ }
199
+ /**
200
+ * Describes every mailbox, including \Noselect hierarchy levels
201
+ *
202
+ * @return {Array} mailboxes ordered by name
203
+ */
204
+ listMailboxes() {
205
+ return Object.keys(this.server.folderCache)
206
+ .sort()
207
+ .map(path => this.describeMailbox(this.server.folderCache[path]));
208
+ }
209
+ /**
210
+ * Describes a mailbox
211
+ *
212
+ * @param {String} path Storage name
213
+ * @return {Object} mailbox info
214
+ */
215
+ getMailbox(path) {
216
+ return this.describeMailbox(this.requireMailbox(path));
217
+ }
218
+ describeMailbox(mailbox) {
219
+ const status = this.server.getStatus(mailbox);
220
+ const info = {
221
+ path: mailbox.path,
222
+ delimiter: this.server.getSeparator(mailbox),
223
+ flags: mailbox.flags.slice(),
224
+ selectable: mailbox.flags.indexOf('\\Noselect') < 0,
225
+ subscribed: !!mailbox.subscribed,
226
+ messages: mailbox.messages.length,
227
+ unseen: status ? status.unseen : 0,
228
+ uidnext: mailbox.uidnext,
229
+ uidvalidity: mailbox.uidvalidity,
230
+ permanentFlags: status ? status.permanentFlags.slice() : []
231
+ };
232
+ if (this.server.condstore && typeof mailbox.HIGHESTMODSEQ === 'number') {
233
+ info.highestModseq = mailbox.HIGHESTMODSEQ;
234
+ }
235
+ this.mailboxInfoHandlers.forEach(handler => handler(mailbox, info));
236
+ return info;
237
+ }
238
+ /**
239
+ * Describes the messages of a mailbox
240
+ *
241
+ * @param {String} path Storage name
242
+ * @param {Object} [options] `{ uids, raw }`: only these UIDs, include the message source
243
+ * @return {Array} messages ordered by UID
244
+ */
245
+ listMessages(path, options = {}) {
246
+ const mailbox = this.requireMailbox(path, true);
247
+ const messages = options.uids ? this.requireMessages(mailbox, options.uids).sort((a, b) => a.uid - b.uid) : mailbox.messages;
248
+ return messages.map(message => this.describeMessage(message, !!options.raw));
249
+ }
250
+ /**
251
+ * Describes a message
252
+ *
253
+ * @param {String} path Storage name
254
+ * @param {Number} uid UID of the message
255
+ * @param {Object} [options] `{ raw }`: include the message source, true by default
256
+ * @return {Object} message info
257
+ */
258
+ getMessage(path, uid, options = {}) {
259
+ const mailbox = this.requireMailbox(path, true);
260
+ return this.describeMessage(this.requireMessages(mailbox, [uid])[0], options.raw !== false);
261
+ }
262
+ describeMessage(message, raw) {
263
+ const info = {
264
+ uid: message.uid,
265
+ flags: message.flags.slice(),
266
+ internaldate: message.internaldate,
267
+ size: message.raw.length
268
+ };
269
+ if (this.server.condstore && typeof message.MODSEQ === 'number') {
270
+ info.modseq = message.MODSEQ;
271
+ }
272
+ if (raw) {
273
+ info.raw = Buffer.from(message.raw, 'binary');
274
+ }
275
+ this.messageInfoHandlers.forEach(handler => handler(message, info));
276
+ return info;
277
+ }
278
+ /**
279
+ * Describes the connected sessions
280
+ *
281
+ * @return {Array} sessions ordered by number
282
+ */
283
+ sessions() {
284
+ return [...this.server.connections].map(connection => connection.describe()).sort((a, b) => a.session - b.session);
285
+ }
286
+ // Messages
287
+ /**
288
+ * Adds a message to a mailbox, like a delivery from outside: sessions that have the mailbox selected get EXISTS,
289
+ * the first read-write one sees it as \Recent
290
+ *
291
+ * @param {String} path Storage name
292
+ * @param {Object} message `{ raw, flags, internaldate }`
293
+ * @param {Object} [options] `{ checks }`: true runs the checks APPEND runs (QUOTA, APPENDLIMIT ...), a failed
294
+ * one throws an ImapKitError with its response code, e.g. OVERQUOTA
295
+ * @return {Object} `{ uid, uidvalidity }`
296
+ */
297
+ addMessage(path, message, options = {}) {
298
+ const mailbox = this.requireMailbox(path, true);
299
+ const { raw, flags, internaldate } = message || {};
300
+ let source;
301
+ if (typeof raw === 'string') {
302
+ source = Buffer.from(raw, 'utf-8').toString('binary');
303
+ }
304
+ else if (raw instanceof Uint8Array) {
305
+ source = Buffer.from(raw).toString('binary');
306
+ }
307
+ else {
308
+ throw new store_operations_js_1.ImapKitError('Message source (raw) must be a string or a Buffer', 'INVALID');
309
+ }
310
+ if (!source.length) {
311
+ throw new store_operations_js_1.ImapKitError('Message source (raw) can not be empty', 'INVALID');
312
+ }
313
+ const list = (0, store_operations_js_1.checkFlags)(this.server, mailbox, flags || [], true);
314
+ if (internaldate !== undefined &&
315
+ !(internaldate instanceof Date && !isNaN(internaldate.getTime())) &&
316
+ !this.server.validateInternalDate(internaldate)) {
317
+ throw new store_operations_js_1.ImapKitError('Internal date must be a Date or a date-time string like "17-Jul-1996 02:44:25 -0700"', 'INVALID');
318
+ }
319
+ if (options.checks) {
320
+ const failure = this.server.appendChecks
321
+ .map(check => check(null, mailbox, [{ raw: source, flags: list, internaldate }], { command: 'APPEND' }))
322
+ .find(result => result && !result.soft);
323
+ if (failure) {
324
+ const code = Array.isArray(failure.code) ? failure.code[0] : failure.code;
325
+ throw new store_operations_js_1.ImapKitError(failure.text, typeof code === 'string' ? code : 'CANNOT');
326
+ }
327
+ }
328
+ const added = this.run(() => this.server.appendMessage(mailbox, list, internaldate || null, source));
329
+ return { uid: added.message.uid, uidvalidity: mailbox.uidvalidity };
330
+ }
331
+ /**
332
+ * Changes the flags of messages. Sessions that have the mailbox selected get unsolicited FETCH responses, with
333
+ * CONDSTORE the messages get new mod-sequences
334
+ *
335
+ * @param {String} path Storage name
336
+ * @param {Array} uids UIDs of the messages
337
+ * @param {Array} flags Flags
338
+ * @param {String} [mode] "set" (default), "add" or "remove"
339
+ * @return {Array} `{ uid, flags }` of every message
340
+ */
341
+ setFlags(path, uids, flags, mode = 'set') {
342
+ const mailbox = this.requireMailbox(path, true);
343
+ const messages = this.requireMessages(mailbox, uids);
344
+ this.run(() => (0, store_operations_js_1.changeFlags)(this.server, mailbox, messages, flags, mode));
345
+ return messages.map(message => ({ uid: message.uid, flags: message.flags.slice() }));
346
+ }
347
+ /**
348
+ * Removes messages. Sessions that have the mailbox selected get EXPUNGE (VANISHED with QRESYNC), following the
349
+ * RFC 2180 rules a session's EXPUNGE would
350
+ *
351
+ * @param {String} path Storage name
352
+ * @param {Array} uids UIDs of the messages
353
+ * @return {Array} the removed UIDs
354
+ */
355
+ expungeMessages(path, uids) {
356
+ const mailbox = this.requireMailbox(path, true);
357
+ const messages = this.requireMessages(mailbox, uids);
358
+ return this.run(() => (0, store_operations_js_1.expungeMessages)(this.server, mailbox, messages)).map(message => message.uid);
359
+ }
360
+ /**
361
+ * Copies messages to another mailbox, the copies keep flags and internal date and get new UIDs
362
+ *
363
+ * @param {String} path Storage name of the source
364
+ * @param {Array} uids UIDs of the messages
365
+ * @param {String} target Storage name of the target
366
+ * @return {Object} `{ uidvalidity, uids }`: UIDVALIDITY of the target, `{ uid, targetUid }` for every message
367
+ */
368
+ copyMessages(path, uids, target) {
369
+ return this.transfer(path, uids, target, false);
370
+ }
371
+ /**
372
+ * Moves messages to another mailbox: copies them and expunges them from the source
373
+ *
374
+ * @param {String} path Storage name of the source
375
+ * @param {Array} uids UIDs of the messages
376
+ * @param {String} target Storage name of the target
377
+ * @return {Object} `{ uidvalidity, uids }`: UIDVALIDITY of the target, `{ uid, targetUid }` for every message
378
+ */
379
+ moveMessages(path, uids, target) {
380
+ return this.transfer(path, uids, target, true);
381
+ }
382
+ transfer(path, uids, target, move) {
383
+ const mailbox = this.requireMailbox(path, true);
384
+ const targetMailbox = this.requireMailbox(target, true);
385
+ // in UID order, like COPY and MOVE
386
+ const messages = this.requireMessages(mailbox, uids).sort((a, b) => a.uid - b.uid);
387
+ return this.run(() => {
388
+ const result = messages.map(message => ({ uid: message.uid, targetUid: this.server.copyMessage(targetMailbox, message).message.uid }));
389
+ if (move) {
390
+ (0, store_operations_js_1.expungeMessages)(this.server, mailbox, messages);
391
+ }
392
+ return { uidvalidity: targetMailbox.uidvalidity, uids: result };
393
+ });
394
+ }
395
+ /**
396
+ * Replaces a message. The content of a UID never changes (RFC 9051 section 2.3.1.1), so this adds the new
397
+ * message and expunges the old one, the new message gets a new UID. Flags and internal date of the old
398
+ * message are kept unless given
399
+ *
400
+ * @param {String} path Storage name
401
+ * @param {Number} uid UID of the message to replace
402
+ * @param {Object} message `{ raw, flags, internaldate }`
403
+ * @return {Object} `{ uid, uidvalidity }` of the new message
404
+ */
405
+ replaceMessage(path, uid, message) {
406
+ const mailbox = this.requireMailbox(path, true);
407
+ const old = this.requireMessages(mailbox, [uid])[0];
408
+ const added = this.addMessage(path, {
409
+ raw: message && message.raw,
410
+ flags: message && message.flags ? message.flags : old.flags.slice(),
411
+ internaldate: message && message.internaldate ? message.internaldate : old.internaldate
412
+ });
413
+ this.run(() => (0, store_operations_js_1.expungeMessages)(this.server, mailbox, [old]));
414
+ return added;
415
+ }
416
+ // Mailboxes
417
+ /**
418
+ * Creates a mailbox, with any missing superior levels
419
+ *
420
+ * @param {String} path Storage name
421
+ * @param {Object} [options] `{ subscribed }`
422
+ * @return {Object} mailbox info
423
+ */
424
+ createMailbox(path, options = {}) {
425
+ this.checkName(path);
426
+ const mailbox = this.run(() => {
427
+ const created = (0, store_operations_js_1.createMailbox)(this.server, path);
428
+ if (options.subscribed) {
429
+ (0, store_operations_js_1.subscribeMailbox)(this.server, created.path);
430
+ }
431
+ return created;
432
+ });
433
+ return this.describeMailbox(mailbox);
434
+ }
435
+ /**
436
+ * Deletes a mailbox. Sessions that have it selected are disconnected with BYE, like after DELETE in another session
437
+ *
438
+ * @param {String} path Storage name
439
+ */
440
+ deleteMailbox(path) {
441
+ this.checkPath(path);
442
+ this.run(() => (0, store_operations_js_1.deleteMailbox)(this.server, path));
443
+ }
444
+ /**
445
+ * Renames a mailbox, with the RENAME rules (renaming INBOX moves its messages)
446
+ *
447
+ * @param {String} path Storage name
448
+ * @param {String} newPath New storage name
449
+ * @return {Object} info of the renamed mailbox
450
+ */
451
+ renameMailbox(path, newPath) {
452
+ this.checkPath(path);
453
+ this.checkName(newPath);
454
+ const result = this.run(() => (0, store_operations_js_1.renameMailbox)(this.server, path, newPath));
455
+ return this.getMailbox(result.path);
456
+ }
457
+ /**
458
+ * Gives a mailbox a new UIDVALIDITY, optionally with new UIDs ("keep", "renumber", "shuffle" or "offset"). Sessions
459
+ * that have the mailbox selected are disconnected with BYE, as a UID must not change during a session
460
+ *
461
+ * @param {String} path Storage name
462
+ * @param {Object} [options] `{ uidvalidity, uids, offset, seed }`
463
+ * @return {Object} `{ uidvalidity, uidnext, uids }`, `uids` lists `{ uid, newUid }` for every message
464
+ */
465
+ resetUidValidity(path, options = {}) {
466
+ const mailbox = this.requireMailbox(path, true);
467
+ return this.run(() => (0, store_operations_js_1.resetUidValidity)(this.server, mailbox, options || {}));
468
+ }
469
+ /**
470
+ * Subscribes a mailbox
471
+ *
472
+ * @param {String} path Storage name
473
+ * @return {Boolean} true if the subscription changed
474
+ */
475
+ subscribe(path) {
476
+ this.checkPath(path);
477
+ return this.run(() => (0, store_operations_js_1.subscribeMailbox)(this.server, path));
478
+ }
479
+ /**
480
+ * Unsubscribes a name, it does not have to be a mailbox
481
+ *
482
+ * @param {String} path Storage name
483
+ * @return {Boolean} true if the subscription changed
484
+ */
485
+ unsubscribe(path) {
486
+ this.checkPath(path);
487
+ return this.run(() => (0, store_operations_js_1.unsubscribeMailbox)(this.server, path));
488
+ }
489
+ // Users
490
+ /**
491
+ * Lists the users, without credentials
492
+ *
493
+ * @return {Array} `{ name, xoauth2 }`, xoauth2 is true for a user with an access token
494
+ */
495
+ listUsers() {
496
+ return Object.keys(this.server.users)
497
+ .sort()
498
+ .map(name => ({ name, xoauth2: !!(this.server.users[name].xoauth2 && this.server.users[name].xoauth2.accessToken) }));
499
+ }
500
+ /**
501
+ * Adds a user
502
+ *
503
+ * @param {String} name User name
504
+ * @param {Object} options `{ password, xoauth2: { accessToken, sessionTimeout } }`
505
+ */
506
+ addUser(name, options) {
507
+ if (typeof name !== 'string' || !name) {
508
+ throw new store_operations_js_1.ImapKitError('User name must be a non-empty string', 'INVALID');
509
+ }
510
+ if (Object.hasOwn(this.server.users, name)) {
511
+ throw new store_operations_js_1.ImapKitError('User ' + JSON.stringify(name) + ' already exists', 'ALREADYEXISTS');
512
+ }
513
+ this.server.users[name] = this.userData({}, options);
514
+ }
515
+ /**
516
+ * Changes the password or the XOAUTH2 token of a user
517
+ *
518
+ * @param {String} name User name
519
+ * @param {Object} options `{ password, xoauth2 }`, null xoauth2 removes the token
520
+ */
521
+ updateUser(name, options) {
522
+ this.server.users[name] = this.userData(this.requireUser(name), options);
523
+ }
524
+ /**
525
+ * Deletes a user, its sessions are disconnected with BYE unless `disconnect` is false
526
+ *
527
+ * @param {String} name User name
528
+ * @param {Object} [options] `{ disconnect }`
529
+ */
530
+ deleteUser(name, options = {}) {
531
+ this.requireUser(name);
532
+ delete this.server.users[name];
533
+ if (options.disconnect !== false) {
534
+ this.findSessions({ user: name }).forEach(connection => connection.bye('User was deleted', 'USER DELETED'));
535
+ }
536
+ }
537
+ requireUser(name) {
538
+ if (typeof name !== 'string' || !Object.hasOwn(this.server.users, name)) {
539
+ throw new store_operations_js_1.ImapKitError('User ' + JSON.stringify(name) + ' does not exist', 'NONEXISTENT');
540
+ }
541
+ return this.server.users[name];
542
+ }
543
+ userData(user, options) {
544
+ const { password, xoauth2 } = options || {};
545
+ const result = Object.assign({}, user);
546
+ if (password !== undefined) {
547
+ if (typeof password !== 'string') {
548
+ throw new store_operations_js_1.ImapKitError('Password must be a string', 'INVALID');
549
+ }
550
+ result.password = password;
551
+ }
552
+ if (xoauth2 === null) {
553
+ delete result.xoauth2;
554
+ }
555
+ else if (xoauth2 !== undefined) {
556
+ if (typeof xoauth2 !== 'object' || typeof xoauth2.accessToken !== 'string') {
557
+ throw new store_operations_js_1.ImapKitError('xoauth2 must be { accessToken, sessionTimeout }', 'INVALID');
558
+ }
559
+ result.xoauth2 = { accessToken: xoauth2.accessToken, sessionTimeout: Number(xoauth2.sessionTimeout) || store_operations_js_1.DEFAULT_SESSION_TIMEOUT };
560
+ }
561
+ return result;
562
+ }
563
+ // Server
564
+ /**
565
+ * Disconnects sessions
566
+ *
567
+ * @param {Number|Object} filter A session number, or `{ session, user }`
568
+ * @param {Object} [options] `{ text, reset }`: the text of the untagged BYE, or reset the TCP connection without BYE
569
+ * @return {Number} how many sessions were disconnected
570
+ */
571
+ disconnect(filter, options = {}) {
572
+ const sessions = this.findSessions(filter);
573
+ sessions.forEach(connection => {
574
+ if (options.reset) {
575
+ connection.closeNow('reset');
576
+ }
577
+ else {
578
+ connection.bye(options.text || 'Disconnected by the server', 'DISCONNECT');
579
+ }
580
+ });
581
+ return sessions.length;
582
+ }
583
+ /**
584
+ * Writes bytes to a session as they are, e.g. an untagged response between commands (ALERT, BYE, EXISTS).
585
+ * Nothing checks that they are valid IMAP
586
+ *
587
+ * @param {Number} session Session number
588
+ * @param {String|Buffer} data Bytes to send, a string is encoded as UTF-8
589
+ */
590
+ inject(session, data) {
591
+ const connection = this.findSessions({ session })[0];
592
+ if (!connection) {
593
+ throw new store_operations_js_1.ImapKitError('Session ' + session + ' does not exist', 'NONEXISTENT');
594
+ }
595
+ if (typeof data !== 'string' && !(data instanceof Uint8Array)) {
596
+ throw new store_operations_js_1.ImapKitError('Data must be a string or a Buffer', 'INVALID');
597
+ }
598
+ connection.write(Buffer.from(data));
599
+ }
600
+ /**
601
+ * Restores the mailboxes and users of the server options and disconnects every session with BYE, so that a
602
+ * long running server (the imapkit command) can be reused between tests. Plugins restore their own state on
603
+ * the `reset` event (QUOTA limits, server annotations). Script rules stay, `server.script.clear()` removes them
604
+ */
605
+ reset() {
606
+ this.server.connections.forEach(connection => {
607
+ if (connection.isOpen()) {
608
+ connection.bye('Server reset', 'RESET');
609
+ }
610
+ });
611
+ this.run(() => {
612
+ this.server.loadUsers();
613
+ this.server.loadStorage();
614
+ this.server.emit('reset');
615
+ });
616
+ }
617
+ /**
618
+ * Stops the server. A graceful shutdown stops accepting connections and waits until the clients disconnect,
619
+ * otherwise the sessions are closed right away
620
+ *
621
+ * @param {Object} [options] `{ graceful }`, true by default
622
+ * @return {Promise} resolves when the server is closed
623
+ */
624
+ shutdown(options = {}) {
625
+ return new Promise((resolve, reject) => {
626
+ const done = (err) => (err && err.code !== 'ERR_SERVER_NOT_RUNNING' ? reject(err) : resolve());
627
+ if (options.graceful === false) {
628
+ this.server.close(done);
629
+ }
630
+ else {
631
+ this.server.closeListeners(false);
632
+ this.server.server.close(done);
633
+ }
634
+ });
635
+ }
636
+ }
637
+ exports.Control = Control;
@@ -1,10 +1,17 @@
1
1
  import createServer, { TAG_REGEX, IMAPServer, IMAPConnection } from './server.js';
2
- export { TAG_REGEX, IMAPServer, IMAPConnection };
2
+ import { ImapKitError } from './control.js';
3
+ import { quirks } from './quirks.js';
4
+ import { storageSchema, validateStorage } from './storage-schema.js';
5
+ export { TAG_REGEX, IMAPServer, IMAPConnection, ImapKitError, quirks, storageSchema, validateStorage };
3
6
  export type { Attribute, ParsedCommand, IMAPResponse, Notification, Callback, CommandHandler, CommandOptions, Plugin, IMAPError, Message, Mailbox, StorageNamespace, UserData, IMAPServerOptions } from './types.js';
4
7
  export type { ScriptRule, ScriptContext, ScriptEvent, ScriptBytes, ScriptHandle } from './script.js';
8
+ export type { Quirk } from './quirks.js';
9
+ export type { Control, MailboxInfo, MessageInfo, SessionInfo, NewMessage, UserOptions, SessionFilter } from './control.js';
5
10
  declare const imapkit: typeof createServer & {
6
11
  TAG_REGEX: RegExp;
7
12
  IMAPServer: typeof IMAPServer;
8
13
  IMAPConnection: typeof IMAPConnection;
14
+ ImapKitError: typeof ImapKitError;
15
+ quirks: Record<string, import("./quirks.js").Quirk>;
9
16
  };
10
17
  export default imapkit;
package/dist/cjs/index.js CHANGED
@@ -33,14 +33,21 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.IMAPConnection = exports.IMAPServer = exports.TAG_REGEX = void 0;
36
+ exports.validateStorage = exports.storageSchema = exports.quirks = exports.ImapKitError = exports.IMAPConnection = exports.IMAPServer = exports.TAG_REGEX = void 0;
37
37
  const server_js_1 = __importStar(require("./server.js"));
38
38
  Object.defineProperty(exports, "TAG_REGEX", { enumerable: true, get: function () { return server_js_1.TAG_REGEX; } });
39
39
  Object.defineProperty(exports, "IMAPServer", { enumerable: true, get: function () { return server_js_1.IMAPServer; } });
40
40
  Object.defineProperty(exports, "IMAPConnection", { enumerable: true, get: function () { return server_js_1.IMAPConnection; } });
41
+ const control_js_1 = require("./control.js");
42
+ Object.defineProperty(exports, "ImapKitError", { enumerable: true, get: function () { return control_js_1.ImapKitError; } });
43
+ const quirks_js_1 = require("./quirks.js");
44
+ Object.defineProperty(exports, "quirks", { enumerable: true, get: function () { return quirks_js_1.quirks; } });
45
+ const storage_schema_js_1 = require("./storage-schema.js");
46
+ Object.defineProperty(exports, "storageSchema", { enumerable: true, get: function () { return storage_schema_js_1.storageSchema; } });
47
+ Object.defineProperty(exports, "validateStorage", { enumerable: true, get: function () { return storage_schema_js_1.validateStorage; } });
41
48
  // `imapkit(options)` creates a server. TAG_REGEX was a property of the CommonJS export, it and the
42
49
  // classes are properties of the factory with both module formats
43
- const imapkit = Object.assign(server_js_1.default, { TAG_REGEX: server_js_1.TAG_REGEX, IMAPServer: server_js_1.IMAPServer, IMAPConnection: server_js_1.IMAPConnection });
50
+ const imapkit = Object.assign(server_js_1.default, { TAG_REGEX: server_js_1.TAG_REGEX, IMAPServer: server_js_1.IMAPServer, IMAPConnection: server_js_1.IMAPConnection, ImapKitError: control_js_1.ImapKitError, quirks: quirks_js_1.quirks });
44
51
  exports.default = imapkit;
45
52
  {
46
53
  const defaultExport = exports.default;