imapkit 4.3.1 → 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.
- package/README.md +167 -51
- package/bin/help.txt +17 -3
- package/bin/imapkit.js +53 -23
- package/dist/cjs/commands/append.js +1 -1
- package/dist/cjs/commands/create.js +2 -2
- package/dist/cjs/commands/delete.js +2 -14
- package/dist/cjs/commands/handlers/flags.d.ts +3 -3
- package/dist/cjs/commands/handlers/flags.js +3 -3
- package/dist/cjs/commands/handlers/store.js +3 -3
- package/dist/cjs/commands/rename.js +6 -46
- package/dist/cjs/commands/select.js +1 -0
- package/dist/cjs/commands/subscribe.js +7 -8
- package/dist/cjs/commands/unsubscribe.js +3 -9
- package/dist/cjs/control.d.ts +430 -0
- package/dist/cjs/control.js +637 -0
- package/dist/cjs/index.d.ts +8 -1
- package/dist/cjs/index.js +9 -2
- package/dist/cjs/load-plugins.d.ts +1 -1
- package/dist/cjs/load-plugins.js +7 -2
- package/dist/cjs/plugins/acl.js +109 -42
- package/dist/cjs/plugins/condstore.js +7 -0
- package/dist/cjs/plugins/index.js +1 -3
- package/dist/cjs/plugins/metadata.js +140 -57
- package/dist/cjs/plugins/objectid.js +10 -0
- package/dist/cjs/plugins/quota.js +50 -2
- package/dist/cjs/plugins/savedate.js +1 -1
- package/dist/cjs/plugins/special-use.d.ts +0 -8
- package/dist/cjs/plugins/special-use.js +23 -0
- package/dist/cjs/quirks.d.ts +28 -0
- package/dist/cjs/quirks.js +124 -0
- package/dist/cjs/random.d.ts +10 -0
- package/dist/cjs/random.js +22 -0
- package/dist/cjs/rest.d.ts +50 -0
- package/dist/cjs/rest.js +561 -0
- package/dist/cjs/script.d.ts +33 -4
- package/dist/cjs/script.js +64 -4
- package/dist/cjs/server.d.ts +147 -8
- package/dist/cjs/server.js +398 -144
- package/dist/cjs/smtp-listener.d.ts +3 -2
- package/dist/cjs/smtp-listener.js +10 -6
- package/dist/cjs/storage-schema.d.ts +139 -0
- package/dist/cjs/storage-schema.js +215 -0
- package/dist/cjs/store-operations.d.ts +168 -0
- package/dist/cjs/store-operations.js +422 -0
- package/dist/cjs/types.d.ts +22 -1
- package/dist/esm/commands/append.js +1 -1
- package/dist/esm/commands/create.js +2 -2
- package/dist/esm/commands/delete.js +2 -14
- package/dist/esm/commands/handlers/flags.d.ts +3 -3
- package/dist/esm/commands/handlers/flags.js +3 -3
- package/dist/esm/commands/handlers/store.js +3 -3
- package/dist/esm/commands/rename.js +6 -46
- package/dist/esm/commands/select.js +1 -0
- package/dist/esm/commands/subscribe.js +7 -8
- package/dist/esm/commands/unsubscribe.js +3 -9
- package/dist/esm/control.d.ts +430 -0
- package/dist/esm/control.js +630 -0
- package/dist/esm/index.d.ts +8 -1
- package/dist/esm/index.js +5 -2
- package/dist/esm/load-plugins.d.ts +1 -1
- package/dist/esm/load-plugins.js +7 -2
- package/dist/esm/plugins/acl.js +109 -42
- package/dist/esm/plugins/condstore.js +7 -0
- package/dist/esm/plugins/index.js +1 -3
- package/dist/esm/plugins/metadata.js +140 -57
- package/dist/esm/plugins/objectid.js +10 -0
- package/dist/esm/plugins/quota.js +50 -2
- package/dist/esm/plugins/savedate.js +1 -1
- package/dist/esm/plugins/special-use.d.ts +0 -8
- package/dist/esm/plugins/special-use.js +23 -0
- package/dist/esm/quirks.d.ts +28 -0
- package/dist/esm/quirks.js +120 -0
- package/dist/esm/random.d.ts +10 -0
- package/dist/esm/random.js +19 -0
- package/dist/esm/rest.d.ts +50 -0
- package/dist/esm/rest.js +553 -0
- package/dist/esm/script.d.ts +33 -4
- package/dist/esm/script.js +63 -4
- package/dist/esm/server.d.ts +147 -8
- package/dist/esm/server.js +399 -145
- package/dist/esm/smtp-listener.d.ts +3 -2
- package/dist/esm/smtp-listener.js +10 -6
- package/dist/esm/storage-schema.d.ts +139 -0
- package/dist/esm/storage-schema.js +211 -0
- package/dist/esm/store-operations.d.ts +168 -0
- package/dist/esm/store-operations.js +407 -0
- package/dist/esm/types.d.ts +22 -1
- package/dist/plugin-help.json +0 -14
- package/package.json +11 -3
- package/dist/cjs/plugins/xtoybird.d.ts +0 -2
- package/dist/cjs/plugins/xtoybird.js +0 -236
- package/dist/esm/plugins/xtoybird.d.ts +0 -2
- package/dist/esm/plugins/xtoybird.js +0 -218
package/dist/cjs/server.js
CHANGED
|
@@ -53,6 +53,11 @@ const search_js_1 = require("./commands/handlers/search.js");
|
|
|
53
53
|
const numbers_js_1 = require("./numbers.js");
|
|
54
54
|
const arguments_js_1 = require("./arguments.js");
|
|
55
55
|
const append_js_1 = require("./commands/append.js");
|
|
56
|
+
const store_operations_js_1 = require("./store-operations.js");
|
|
57
|
+
const control_js_1 = require("./control.js");
|
|
58
|
+
const quirks_js_1 = require("./quirks.js");
|
|
59
|
+
const storage_schema_js_1 = require("./storage-schema.js");
|
|
60
|
+
const rest_js_1 = require("./rest.js");
|
|
56
61
|
const bundledCert = __importStar(require("./cert.js"));
|
|
57
62
|
const script_js_1 = require("./script.js");
|
|
58
63
|
// longest command line (not counting literals) accepted from a client
|
|
@@ -100,18 +105,6 @@ function getResponseTag(line) {
|
|
|
100
105
|
function stateError(command, state) {
|
|
101
106
|
return command + ' is not allowed in the ' + state + ' state';
|
|
102
107
|
}
|
|
103
|
-
/**
|
|
104
|
-
* Creates an error for a failed mailbox operation, with a RFC 5530 response code
|
|
105
|
-
*
|
|
106
|
-
* @param {String} message Error message
|
|
107
|
-
* @param {String} code Response code, e.g. "ALREADYEXISTS"
|
|
108
|
-
* @return {Error} Error object
|
|
109
|
-
*/
|
|
110
|
-
function mailboxError(message, code) {
|
|
111
|
-
const err = new Error(message);
|
|
112
|
-
err.code = code;
|
|
113
|
-
return err;
|
|
114
|
-
}
|
|
115
108
|
/**
|
|
116
109
|
* Creates a new IMAP server, call `listen()` on it to start accepting connections
|
|
117
110
|
*
|
|
@@ -200,11 +193,33 @@ class IMAPServer extends node_stream_1.Stream {
|
|
|
200
193
|
// the session whose command is running, see IMAPServer#notify
|
|
201
194
|
this.activeConnection = null;
|
|
202
195
|
this.sessionCounter = 0;
|
|
203
|
-
// rules that make the server misbehave on purpose, from the script option or server.script.add()
|
|
196
|
+
// rules that make the server misbehave on purpose, from the script option or server.script.add(), and the rules
|
|
197
|
+
// of the quirk presets after them
|
|
204
198
|
this.script = new script_js_1.ServerScript(this, this.options.script);
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
199
|
+
const quirks = (0, quirks_js_1.resolveQuirks)(this.options.quirks);
|
|
200
|
+
if (quirks.rules.length) {
|
|
201
|
+
this.script.add(quirks.rules);
|
|
202
|
+
}
|
|
203
|
+
this.loadUsers();
|
|
204
|
+
// plugins register their control operations
|
|
205
|
+
this.control = new control_js_1.Control(this);
|
|
206
|
+
// a quirk preset can leave plugins out (no-move, no-uidplus)
|
|
207
|
+
(0, load_plugins_js_1.default)(this, this.options.plugins, [...quirks.removePlugins]);
|
|
208
|
+
if (this.options.storage) {
|
|
209
|
+
// a typo in a fixture fails here with the path of the problem
|
|
210
|
+
(0, storage_schema_js_1.validateStorage)(this.options.storage);
|
|
211
|
+
}
|
|
212
|
+
this.systemFlags = [].concat(this.options.systemFlags || ['\\Answered', '\\Flagged', '\\Draft', '\\Deleted', '\\Seen']);
|
|
213
|
+
this.loadStorage();
|
|
214
|
+
this.smtpServer = null;
|
|
215
|
+
this.restServer = null;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Sets the users from the `users` option, or the default user. The users are deep copied, so that runtime changes
|
|
219
|
+
* never leak into the caller's objects or into other servers built from the same fixture. Without a prototype,
|
|
220
|
+
* user names like "__proto__" or "toString" are plain keys
|
|
221
|
+
*/
|
|
222
|
+
loadUsers() {
|
|
208
223
|
this.users = Object.assign(Object.create(null), this.options.users
|
|
209
224
|
? structuredClone(this.options.users)
|
|
210
225
|
: {
|
|
@@ -212,12 +227,16 @@ class IMAPServer extends node_stream_1.Stream {
|
|
|
212
227
|
password: 'testpass',
|
|
213
228
|
xoauth2: {
|
|
214
229
|
accessToken: 'testtoken',
|
|
215
|
-
sessionTimeout:
|
|
230
|
+
sessionTimeout: store_operations_js_1.DEFAULT_SESSION_TIMEOUT
|
|
216
231
|
}
|
|
217
232
|
}
|
|
218
233
|
});
|
|
219
|
-
|
|
220
|
-
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Builds the mailboxes from the `storage` option, a deep copy of it. Message and mailbox handlers of plugins run
|
|
237
|
+
* on every message and mailbox
|
|
238
|
+
*/
|
|
239
|
+
loadStorage() {
|
|
221
240
|
// indexFolders() below turns the storage option into namespaces and mailboxes in place
|
|
222
241
|
this.storage = (this.options.storage
|
|
223
242
|
? structuredClone(this.options.storage)
|
|
@@ -225,6 +244,7 @@ class IMAPServer extends node_stream_1.Stream {
|
|
|
225
244
|
INBOX: {},
|
|
226
245
|
'': {}
|
|
227
246
|
});
|
|
247
|
+
this.referenceNamespace = false;
|
|
228
248
|
this.uidvalidityCounter = 0; // highest UIDVALIDITY in use, new mailboxes get a higher one
|
|
229
249
|
// subscribed mailbox names (RFC 3501 section 6.3.6). Names, not mailboxes: a subscription outlives
|
|
230
250
|
// DELETE and stays with the old name on RENAME (RFC 9051 section 6.3.6), see trackSubscription
|
|
@@ -232,10 +252,67 @@ class IMAPServer extends node_stream_1.Stream {
|
|
|
232
252
|
this.folderCache = Object.create(null);
|
|
233
253
|
this.indexFolders(true);
|
|
234
254
|
}
|
|
255
|
+
/**
|
|
256
|
+
* Starts accepting connections
|
|
257
|
+
*
|
|
258
|
+
* @param {Number} [port] Port to listen on, a free port if not set
|
|
259
|
+
* @param {String} [host] Address to listen on, all addresses if not set
|
|
260
|
+
* @return {Promise<Number>} resolves with the port the server listens on
|
|
261
|
+
*/
|
|
262
|
+
async start(port, host) {
|
|
263
|
+
const imapPort = await listenOn(this.server, port, host);
|
|
264
|
+
await Promise.all([this.options.smtp ? this.startSmtp() : null, this.options.rest ? this.startRest() : null]);
|
|
265
|
+
return imapPort;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Starts the REST API of the `rest` option, see src/rest.ts
|
|
269
|
+
*
|
|
270
|
+
* @return {Promise<Number>} resolves with the HTTP port
|
|
271
|
+
*/
|
|
272
|
+
async startRest() {
|
|
273
|
+
const { port, host, token } = this.options.rest || {};
|
|
274
|
+
this.restServer = (0, rest_js_1.createRestServer)(this, { host, token });
|
|
275
|
+
return listenOn(this.restServer, port, host || '127.0.0.1');
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Starts the SMTP server of the `smtp` option. smtp-server is an optional dependency, loaded only here
|
|
279
|
+
*
|
|
280
|
+
* @return {Promise<Number>} resolves with the SMTP port
|
|
281
|
+
*/
|
|
282
|
+
startSmtp() {
|
|
283
|
+
const { port, host } = this.options.smtp || {};
|
|
284
|
+
return this.loadSmtpListener().then(({ startSMTPServer }) => new Promise((resolve, reject) => {
|
|
285
|
+
const smtp = startSMTPServer(port || 0, this, smtpPort => {
|
|
286
|
+
smtp.removeListener('error', reject);
|
|
287
|
+
resolve(smtpPort);
|
|
288
|
+
}, host);
|
|
289
|
+
smtp.once('error', reject);
|
|
290
|
+
this.smtpServer = smtp;
|
|
291
|
+
}));
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Loads the SMTP listener, which needs the smtp-server package
|
|
295
|
+
*
|
|
296
|
+
* @return {Promise<Object>} the smtp-listener module
|
|
297
|
+
*/
|
|
298
|
+
loadSmtpListener() {
|
|
299
|
+
return Promise.resolve().then(() => __importStar(require('./smtp-listener.js'))).catch((err) => {
|
|
300
|
+
throw new Error('The smtp option needs the smtp-server package, install it with: npm install smtp-server (' + err.message + ')');
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Stops the server and closes every connection
|
|
305
|
+
*
|
|
306
|
+
* @return {Promise} resolves when the server is closed
|
|
307
|
+
*/
|
|
308
|
+
stop() {
|
|
309
|
+
return this.control.shutdown({ graceful: false });
|
|
310
|
+
}
|
|
235
311
|
listen(...args) {
|
|
236
312
|
this.server.listen.apply(this.server, args);
|
|
237
313
|
}
|
|
238
314
|
close(callback) {
|
|
315
|
+
this.closeListeners(true);
|
|
239
316
|
this.server.close(callback);
|
|
240
317
|
// close() only completes once all connections are gone
|
|
241
318
|
this.connections.forEach((connection) => {
|
|
@@ -244,6 +321,29 @@ class IMAPServer extends node_stream_1.Stream {
|
|
|
244
321
|
}
|
|
245
322
|
});
|
|
246
323
|
}
|
|
324
|
+
/**
|
|
325
|
+
* Stops the SMTP and REST listeners of `start()`
|
|
326
|
+
*
|
|
327
|
+
* @param {Boolean} force true closes the open REST connections too, otherwise only the idle ones (a request that
|
|
328
|
+
* is answered, like POST /v1/shutdown, finishes first)
|
|
329
|
+
*/
|
|
330
|
+
closeListeners(force) {
|
|
331
|
+
if (this.smtpServer) {
|
|
332
|
+
this.smtpServer.close(() => false);
|
|
333
|
+
this.smtpServer = null;
|
|
334
|
+
}
|
|
335
|
+
if (this.restServer) {
|
|
336
|
+
this.restServer.close();
|
|
337
|
+
// keep-alive connections would hold the server open
|
|
338
|
+
if (force) {
|
|
339
|
+
this.restServer.closeAllConnections?.();
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
this.restServer.closeIdleConnections?.();
|
|
343
|
+
}
|
|
344
|
+
this.restServer = null;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
247
347
|
/**
|
|
248
348
|
* Returns TLS key and certificate. Without credentials in the options the
|
|
249
349
|
* bundled self-signed certificate for localhost is used.
|
|
@@ -363,13 +463,31 @@ class IMAPServer extends node_stream_1.Stream {
|
|
|
363
463
|
*
|
|
364
464
|
* @param {String} type Kind of change
|
|
365
465
|
* @param {String} path Storage name of the mailbox
|
|
366
|
-
* @param {Object} [details] `{ oldPath, mailbox }`: the earlier name of a renamed mailbox, the mailbox
|
|
367
|
-
* object that a DELETE removed
|
|
466
|
+
* @param {Object} [details] `{ oldPath, mailbox, created }`: the earlier name of a renamed mailbox, the mailbox
|
|
467
|
+
* object that a DELETE removed, the mailboxes a CREATE made (superior levels first)
|
|
368
468
|
*/
|
|
369
469
|
mailboxChanged(type, path, details) {
|
|
370
470
|
const event = Object.assign({ type, path, oldPath: null, mailbox: null }, details, { origin: this.activeConnection });
|
|
371
471
|
this.emit('mailbox', event);
|
|
372
472
|
}
|
|
473
|
+
/**
|
|
474
|
+
* Runs a change with `origin` as the session that caused it, the `origin` of the notifications and events it
|
|
475
|
+
* makes. Commands run with their session, the control API with null
|
|
476
|
+
*
|
|
477
|
+
* @param {Object|null} origin Session, or null for a change from outside
|
|
478
|
+
* @param {Function} fn Change
|
|
479
|
+
* @return {*} the result of fn
|
|
480
|
+
*/
|
|
481
|
+
withOrigin(origin, fn) {
|
|
482
|
+
const previous = this.activeConnection;
|
|
483
|
+
this.activeConnection = origin;
|
|
484
|
+
try {
|
|
485
|
+
return fn();
|
|
486
|
+
}
|
|
487
|
+
finally {
|
|
488
|
+
this.activeConnection = previous;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
373
491
|
/**
|
|
374
492
|
* Retrieves the handler of an IMAP command
|
|
375
493
|
*
|
|
@@ -502,22 +620,22 @@ class IMAPServer extends node_stream_1.Stream {
|
|
|
502
620
|
*/
|
|
503
621
|
createMailbox(path, defaultMailbox) {
|
|
504
622
|
if (!path) {
|
|
505
|
-
throw
|
|
623
|
+
throw (0, store_operations_js_1.storeError)('Invalid mailbox name', 'CANNOT');
|
|
506
624
|
}
|
|
507
625
|
// Ensure case insensitive INBOX
|
|
508
626
|
if (path.toUpperCase() === 'INBOX') {
|
|
509
|
-
throw
|
|
627
|
+
throw (0, store_operations_js_1.storeError)('INBOX can not be modified', 'ALREADYEXISTS');
|
|
510
628
|
}
|
|
511
629
|
const { namespace, storage } = this.getPersonalNamespace(path);
|
|
512
630
|
path = this.stripSeparator(path, storage.separator);
|
|
513
631
|
if (this.folderCache[path] && this.folderCache[path].flags.indexOf('\\Noselect') < 0) {
|
|
514
|
-
throw
|
|
632
|
+
throw (0, store_operations_js_1.storeError)('Mailbox already exists', 'ALREADYEXISTS');
|
|
515
633
|
}
|
|
516
634
|
const folderPath = path.substr(namespace.length).split(storage.separator);
|
|
517
635
|
if (folderPath.some(name => !name)) {
|
|
518
636
|
// an empty hierarchy level ("foo//bar", "/foo", "foo//"). RFC 5530 section 3 has this very case as the
|
|
519
637
|
// example of CANNOT, Dovecot refuses it too
|
|
520
|
-
throw
|
|
638
|
+
throw (0, store_operations_js_1.storeError)('Mailbox names can not have empty hierarchy levels', 'CANNOT');
|
|
521
639
|
}
|
|
522
640
|
let parent = storage;
|
|
523
641
|
let curPath = namespace;
|
|
@@ -528,7 +646,7 @@ class IMAPServer extends node_stream_1.Stream {
|
|
|
528
646
|
curPath += (curPath.length ? storage.separator : '') + folderName;
|
|
529
647
|
let folder = this.getMailbox(curPath) || false;
|
|
530
648
|
if (folder && folder.flags && folder.flags.indexOf('\\Noinferiors') >= 0) {
|
|
531
|
-
throw
|
|
649
|
+
throw (0, store_operations_js_1.storeError)('Can not create subfolders for ' + folder.path, 'CANNOT');
|
|
532
650
|
}
|
|
533
651
|
// a \Noselect placeholder that is created again is replaced with a new mailbox that only keeps
|
|
534
652
|
// the children, nothing else of a deleted mailbox may come back (RFC 3501 section 6.3.3)
|
|
@@ -570,17 +688,17 @@ class IMAPServer extends node_stream_1.Stream {
|
|
|
570
688
|
deleteMailbox(path, keepContents) {
|
|
571
689
|
// Ensure case insensitive INBOX
|
|
572
690
|
if (path.toUpperCase() === 'INBOX') {
|
|
573
|
-
throw
|
|
691
|
+
throw (0, store_operations_js_1.storeError)('INBOX can not be modified', 'CANNOT');
|
|
574
692
|
}
|
|
575
693
|
const { namespace, storage } = this.getPersonalNamespace(path);
|
|
576
694
|
const mailbox = this.folderCache[this.stripSeparator(path, storage.separator)];
|
|
577
695
|
if (!mailbox) {
|
|
578
|
-
throw
|
|
696
|
+
throw (0, store_operations_js_1.storeError)('Mailbox does not exist', 'NONEXISTENT');
|
|
579
697
|
}
|
|
580
698
|
if (mailbox.flags.indexOf('\\Noselect') >= 0 && Object.keys(mailbox.folders || {}).length) {
|
|
581
699
|
// RFC 9051 section 6.3.5: deleting a \Noselect name that has inferior names is an error, the RFC 5530
|
|
582
700
|
// section 3 HASCHILDREN response code tells the client to delete the children first
|
|
583
|
-
throw
|
|
701
|
+
throw (0, store_operations_js_1.storeError)('Mailbox has children, delete them first', 'HASCHILDREN');
|
|
584
702
|
}
|
|
585
703
|
const levels = mailbox.path.split(storage.separator);
|
|
586
704
|
const folderName = levels.pop();
|
|
@@ -630,7 +748,7 @@ class IMAPServer extends node_stream_1.Stream {
|
|
|
630
748
|
const prefix = key.length ? key.substr(0, key.length - this.storage[key].separator.length) : key;
|
|
631
749
|
if (key.length && (path === prefix || path.substr(0, key.length) === key)) {
|
|
632
750
|
if (path === prefix) {
|
|
633
|
-
throw
|
|
751
|
+
throw (0, store_operations_js_1.storeError)('Used mailbox name is a namespace value', 'CANNOT');
|
|
634
752
|
}
|
|
635
753
|
namespace = key;
|
|
636
754
|
}
|
|
@@ -640,10 +758,10 @@ class IMAPServer extends node_stream_1.Stream {
|
|
|
640
758
|
});
|
|
641
759
|
const storage = this.storage[namespace];
|
|
642
760
|
if (!storage) {
|
|
643
|
-
throw
|
|
761
|
+
throw (0, store_operations_js_1.storeError)('Unknown namespace', 'CANNOT');
|
|
644
762
|
}
|
|
645
763
|
if (storage.type !== 'personal') {
|
|
646
|
-
throw
|
|
764
|
+
throw (0, store_operations_js_1.storeError)('Permission denied', 'NOPERM');
|
|
647
765
|
}
|
|
648
766
|
return { namespace, storage };
|
|
649
767
|
}
|
|
@@ -884,6 +1002,17 @@ class IMAPServer extends node_stream_1.Stream {
|
|
|
884
1002
|
const knownFlags = (mailbox.knownFlags = mailbox.knownFlags || []);
|
|
885
1003
|
flags.forEach(flag => this.ensureFlag(knownFlags, flag));
|
|
886
1004
|
}
|
|
1005
|
+
/**
|
|
1006
|
+
* The current time for dates the server sets itself (INTERNALDATE of a message without one, SAVEDATE). The `now`
|
|
1007
|
+
* option fixes it for repeatable tests: a Date, a timestamp, or a function that returns one
|
|
1008
|
+
*
|
|
1009
|
+
* @return {Date} current time
|
|
1010
|
+
*/
|
|
1011
|
+
now() {
|
|
1012
|
+
const now = this.options.now;
|
|
1013
|
+
const value = typeof now === 'function' ? now() : now;
|
|
1014
|
+
return value === undefined || value === null ? new Date() : new Date(value);
|
|
1015
|
+
}
|
|
887
1016
|
normalizeDateTime(value) {
|
|
888
1017
|
if (value instanceof Date) {
|
|
889
1018
|
return this.formatInternalDate(value);
|
|
@@ -902,7 +1031,7 @@ class IMAPServer extends node_stream_1.Stream {
|
|
|
902
1031
|
* @param mailbox Mailbox of the message
|
|
903
1032
|
*/
|
|
904
1033
|
processMessage(message, mailbox) {
|
|
905
|
-
message.internaldate = this.normalizeDateTime(message.internaldate ||
|
|
1034
|
+
message.internaldate = this.normalizeDateTime(message.internaldate || this.now());
|
|
906
1035
|
message.flags = [].concat(message.flags || []);
|
|
907
1036
|
if (message.flags.indexOf('\\Recent') >= 0) {
|
|
908
1037
|
// \Recent is not a stored flag, it belongs to the first session that selects the mailbox
|
|
@@ -1293,6 +1422,7 @@ class IMAPConnection {
|
|
|
1293
1422
|
this.notificationQueue = [];
|
|
1294
1423
|
this.server.on('notify', this._notificationCallback);
|
|
1295
1424
|
this.server.connections.add(this);
|
|
1425
|
+
this.emitSession('open');
|
|
1296
1426
|
this.scriptOutput('greeting', '* OK ImapKit ready for rumble\r\n', {});
|
|
1297
1427
|
}
|
|
1298
1428
|
/**
|
|
@@ -1327,7 +1457,7 @@ class IMAPConnection {
|
|
|
1327
1457
|
}
|
|
1328
1458
|
this._outputQueue = this._outputQueue || [];
|
|
1329
1459
|
this._outputQueue.push(Object.assign({}, operation, { transport: this.transport }));
|
|
1330
|
-
if (!this._outputTimer) {
|
|
1460
|
+
if (!this._outputTimer && !this._outputWaiting) {
|
|
1331
1461
|
this.flushOutput();
|
|
1332
1462
|
}
|
|
1333
1463
|
}
|
|
@@ -1348,10 +1478,28 @@ class IMAPConnection {
|
|
|
1348
1478
|
return;
|
|
1349
1479
|
}
|
|
1350
1480
|
if (operation.data && operation.chunk && operation.data.length > operation.chunk) {
|
|
1351
|
-
// the rest waits for chunkDelay, like a delay of its own
|
|
1352
|
-
|
|
1481
|
+
// the rest waits for chunkDelay, like a delay of its own. Without a delay each piece waits for the next
|
|
1482
|
+
// event loop turn, and Nagle's algorithm must not hold it back to merge it with the next one
|
|
1483
|
+
const piece = operation.data.subarray(0, operation.chunk);
|
|
1353
1484
|
operation.data = operation.data.subarray(operation.chunk);
|
|
1485
|
+
if (!operation.chunkDelay || operation.chunkDelay === 'tick') {
|
|
1486
|
+
// the next piece waits until this one was handed to the system, and one more event loop turn, so
|
|
1487
|
+
// the pieces leave as separate segments without a wall clock delay (#89)
|
|
1488
|
+
this.socket?.setNoDelay?.(true);
|
|
1489
|
+
this._outputWaiting = true;
|
|
1490
|
+
this.writeLayer(piece, operation.transport || null, () => {
|
|
1491
|
+
if (this._outputWaiting && this._outputQueue === queue) {
|
|
1492
|
+
this._outputTimer = nextTurn(() => {
|
|
1493
|
+
this._outputTimer = null;
|
|
1494
|
+
this._outputWaiting = false;
|
|
1495
|
+
this.flushOutput();
|
|
1496
|
+
});
|
|
1497
|
+
}
|
|
1498
|
+
});
|
|
1499
|
+
return;
|
|
1500
|
+
}
|
|
1354
1501
|
operation.delay = operation.chunkDelay;
|
|
1502
|
+
this.writeLayer(piece, operation.transport || null);
|
|
1355
1503
|
continue;
|
|
1356
1504
|
}
|
|
1357
1505
|
if (operation.data) {
|
|
@@ -1371,8 +1519,10 @@ class IMAPConnection {
|
|
|
1371
1519
|
clearOutputQueue() {
|
|
1372
1520
|
if (this._outputTimer) {
|
|
1373
1521
|
clearTimeout(this._outputTimer);
|
|
1522
|
+
clearImmediate(this._outputTimer);
|
|
1374
1523
|
this._outputTimer = null;
|
|
1375
1524
|
}
|
|
1525
|
+
this._outputWaiting = false;
|
|
1376
1526
|
this._outputQueue = null;
|
|
1377
1527
|
this._deferredOutput = null;
|
|
1378
1528
|
}
|
|
@@ -1381,13 +1531,17 @@ class IMAPConnection {
|
|
|
1381
1531
|
*
|
|
1382
1532
|
* @param {Buffer} data Output
|
|
1383
1533
|
* @param {Object|null} transport Transport layer
|
|
1534
|
+
* @param {Function} [callback] Called once the data was handed to the system (a transport: on the next turn)
|
|
1384
1535
|
*/
|
|
1385
|
-
writeLayer(data, transport) {
|
|
1536
|
+
writeLayer(data, transport, callback) {
|
|
1386
1537
|
if (transport) {
|
|
1387
1538
|
transport.write(data);
|
|
1539
|
+
if (callback) {
|
|
1540
|
+
setImmediate(callback);
|
|
1541
|
+
}
|
|
1388
1542
|
}
|
|
1389
1543
|
else {
|
|
1390
|
-
this.writeRaw(data);
|
|
1544
|
+
this.writeRaw(data, callback);
|
|
1391
1545
|
}
|
|
1392
1546
|
}
|
|
1393
1547
|
/**
|
|
@@ -1441,17 +1595,70 @@ class IMAPConnection {
|
|
|
1441
1595
|
*
|
|
1442
1596
|
* @param {Buffer} data Data to send
|
|
1443
1597
|
*/
|
|
1444
|
-
writeRaw(data) {
|
|
1598
|
+
writeRaw(data, callback) {
|
|
1445
1599
|
if (this.socket && !this.socket.destroyed) {
|
|
1446
|
-
this.socket.write(data);
|
|
1600
|
+
this.socket.write(data, callback);
|
|
1601
|
+
this.touch();
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
/**
|
|
1605
|
+
* Notes input or output for the quiet events of script rules (#88)
|
|
1606
|
+
*/
|
|
1607
|
+
touch() {
|
|
1608
|
+
if (!this.server.script.watches('quiet')) {
|
|
1609
|
+
// nothing waits for quiet sessions, a rule added later counts from then on
|
|
1610
|
+
this._lastActivity = undefined;
|
|
1611
|
+
return;
|
|
1612
|
+
}
|
|
1613
|
+
this._lastActivity = Date.now();
|
|
1614
|
+
if (!this._quietTimer) {
|
|
1615
|
+
this.watchQuiet();
|
|
1447
1616
|
}
|
|
1448
1617
|
}
|
|
1618
|
+
/**
|
|
1619
|
+
* Starts waiting for the next quiet event, if a rule waits for one and the session is not waiting already. When
|
|
1620
|
+
* the time is up, the first rule that matches handles the event (`send`, `close`), the output it sends starts
|
|
1621
|
+
* the next quiet time
|
|
1622
|
+
*
|
|
1623
|
+
* @param {Number} [checked] Quiet time the rules were checked for already, only longer ones are waited for
|
|
1624
|
+
*/
|
|
1625
|
+
watchQuiet(checked = 0) {
|
|
1626
|
+
if (this._quietTimer || !this.socket || this._closing) {
|
|
1627
|
+
return;
|
|
1628
|
+
}
|
|
1629
|
+
const last = this._lastActivity || Date.now();
|
|
1630
|
+
this._lastActivity = last;
|
|
1631
|
+
// the time is measured from the last activity, a rule whose time is up already fires right away
|
|
1632
|
+
const next = this.server.script.nextQuiet(checked);
|
|
1633
|
+
if (next === null) {
|
|
1634
|
+
return;
|
|
1635
|
+
}
|
|
1636
|
+
this._quietTimer = setTimeout(() => {
|
|
1637
|
+
this._quietTimer = null;
|
|
1638
|
+
if (!this.isOpen() || this._lastActivity !== last) {
|
|
1639
|
+
// there was activity in between, it started a new wait
|
|
1640
|
+
return this.watchQuiet();
|
|
1641
|
+
}
|
|
1642
|
+
// a timer can fire a millisecond before Date.now() says the time is up
|
|
1643
|
+
const quiet = Math.max(Date.now() - last, next);
|
|
1644
|
+
const found = this.server.script.check(this, 'quiet', { data: '', quiet });
|
|
1645
|
+
if (found) {
|
|
1646
|
+
(0, script_js_1.handleQuiet)(this, found.rule, found.context);
|
|
1647
|
+
}
|
|
1648
|
+
if (this._lastActivity === last) {
|
|
1649
|
+
// nothing was sent, wait for the rules with a longer quiet time
|
|
1650
|
+
this.watchQuiet(quiet);
|
|
1651
|
+
}
|
|
1652
|
+
}, Math.max(0, next - (Date.now() - last)));
|
|
1653
|
+
this._quietTimer.unref();
|
|
1654
|
+
}
|
|
1449
1655
|
/**
|
|
1450
1656
|
* Handles data from the socket, through the transport layer if there is one
|
|
1451
1657
|
*
|
|
1452
1658
|
* @param {Buffer} chunk Received data
|
|
1453
1659
|
*/
|
|
1454
1660
|
receive(chunk) {
|
|
1661
|
+
this.touch();
|
|
1455
1662
|
if (this.transport) {
|
|
1456
1663
|
this.transport.receive(chunk);
|
|
1457
1664
|
}
|
|
@@ -1584,11 +1791,15 @@ class IMAPConnection {
|
|
|
1584
1791
|
* notifications that were not sent yet are dropped. Sends nothing, the caller answers the command
|
|
1585
1792
|
*/
|
|
1586
1793
|
closeMailbox() {
|
|
1794
|
+
const wasSelected = !!this.selectedMailbox;
|
|
1587
1795
|
this.state = 'Authenticated';
|
|
1588
1796
|
this.selectedMailbox = false;
|
|
1589
1797
|
this.readOnly = false;
|
|
1590
1798
|
this.recent = null;
|
|
1591
1799
|
this.notificationQueue = [];
|
|
1800
|
+
if (wasSelected) {
|
|
1801
|
+
this.emitSession('unselect');
|
|
1802
|
+
}
|
|
1592
1803
|
}
|
|
1593
1804
|
onClose() {
|
|
1594
1805
|
if (this.socket) {
|
|
@@ -1600,8 +1811,14 @@ class IMAPConnection {
|
|
|
1600
1811
|
this.transport = null;
|
|
1601
1812
|
}
|
|
1602
1813
|
this.clearOutputQueue();
|
|
1814
|
+
if (this._quietTimer) {
|
|
1815
|
+
clearTimeout(this._quietTimer);
|
|
1816
|
+
this._quietTimer = null;
|
|
1817
|
+
}
|
|
1603
1818
|
this.server.removeListener('notify', this._notificationCallback);
|
|
1604
|
-
this.server.connections.delete(this)
|
|
1819
|
+
if (this.server.connections.delete(this)) {
|
|
1820
|
+
this.emitSession('close');
|
|
1821
|
+
}
|
|
1605
1822
|
}
|
|
1606
1823
|
onError(err) {
|
|
1607
1824
|
if (this.options.debug) {
|
|
@@ -1953,8 +2170,8 @@ class IMAPConnection {
|
|
|
1953
2170
|
* @param {Array} messages Messages with changed flags
|
|
1954
2171
|
*/
|
|
1955
2172
|
notifyFlagChanges(messages) {
|
|
1956
|
-
if (
|
|
1957
|
-
this.server
|
|
2173
|
+
if (this.selectedMailbox) {
|
|
2174
|
+
(0, store_operations_js_1.notifyFlagChanges)(this.server, this.selectedMailbox, messages, this);
|
|
1958
2175
|
}
|
|
1959
2176
|
}
|
|
1960
2177
|
/**
|
|
@@ -2210,7 +2427,7 @@ class IMAPConnection {
|
|
|
2210
2427
|
*/
|
|
2211
2428
|
send(response, description, parsed, data, ...extra) {
|
|
2212
2429
|
// nothing goes out once the connection is closing (RFC 3501 section 7.1.5)
|
|
2213
|
-
if (!this.
|
|
2430
|
+
if (!this.isOpen()) {
|
|
2214
2431
|
return;
|
|
2215
2432
|
}
|
|
2216
2433
|
if (!response.notification && response.tag !== '*') {
|
|
@@ -2220,6 +2437,9 @@ class IMAPConnection {
|
|
|
2220
2437
|
this.server.outputHandlers.forEach(handler => {
|
|
2221
2438
|
handler(this, response, description, parsed, data, ...extra);
|
|
2222
2439
|
});
|
|
2440
|
+
if (this._commandStart && parsed && response.tag === this._commandStart.tag && parsed.tag === response.tag) {
|
|
2441
|
+
this.commandCompleted(String(response.command || '').toUpperCase());
|
|
2442
|
+
}
|
|
2223
2443
|
// No need to display this response to user
|
|
2224
2444
|
if (response.skipResponse) {
|
|
2225
2445
|
return;
|
|
@@ -2595,6 +2815,64 @@ class IMAPConnection {
|
|
|
2595
2815
|
}, 'UNKNOWN COMMAND', parsed, data);
|
|
2596
2816
|
}
|
|
2597
2817
|
}
|
|
2818
|
+
/**
|
|
2819
|
+
* Emits the `command` event when the tagged response of a command goes out, and the `session` events "login"
|
|
2820
|
+
* (the command authenticated the session) and "logout" (back to Not Authenticated, UNAUTHENTICATE). Every command
|
|
2821
|
+
* that changes these does so before its tagged response. "select" and "unselect" come from SELECT and
|
|
2822
|
+
* closeMailbox(). Tests wait for these instead of polling
|
|
2823
|
+
*
|
|
2824
|
+
* @param {String} status OK, NO or BAD
|
|
2825
|
+
*/
|
|
2826
|
+
commandCompleted(status) {
|
|
2827
|
+
const start = this._commandStart;
|
|
2828
|
+
this._commandStart = null;
|
|
2829
|
+
const server = this.server;
|
|
2830
|
+
if (!server.listenerCount('command') && !server.listenerCount('session')) {
|
|
2831
|
+
return;
|
|
2832
|
+
}
|
|
2833
|
+
server.emit('command', { session: this.sessionNumber, tag: start.tag, command: start.command, status, user: this.username || null });
|
|
2834
|
+
const authenticated = (state) => state === 'Authenticated' || state === 'Selected';
|
|
2835
|
+
if (authenticated(start.state) !== authenticated(this.state) && this.state !== 'Logout') {
|
|
2836
|
+
this.emitSession(authenticated(this.state) ? 'login' : 'logout');
|
|
2837
|
+
}
|
|
2838
|
+
}
|
|
2839
|
+
/**
|
|
2840
|
+
* Emits a `session` event, `{ type, session, ...fields }`
|
|
2841
|
+
*
|
|
2842
|
+
* @param {String} type Event type
|
|
2843
|
+
* @param {Object} [fields] More fields of the event
|
|
2844
|
+
*/
|
|
2845
|
+
emitSession(type, fields) {
|
|
2846
|
+
if (this.server.listenerCount('session')) {
|
|
2847
|
+
this.server.emit('session', Object.assign({ type, session: this.describe() }, fields));
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2850
|
+
/**
|
|
2851
|
+
* Describes the session without credentials or sockets, for the control API and the `session` events
|
|
2852
|
+
*
|
|
2853
|
+
* @return {Object} `{ session, user, state, mailbox, readOnly, enabled, secure, compressed, remoteAddress }`
|
|
2854
|
+
*/
|
|
2855
|
+
describe() {
|
|
2856
|
+
return {
|
|
2857
|
+
session: this.sessionNumber,
|
|
2858
|
+
user: this.username || null,
|
|
2859
|
+
state: this.state,
|
|
2860
|
+
mailbox: this.selectedMailbox ? this.selectedMailbox.path : null,
|
|
2861
|
+
readOnly: !!this.selectedMailbox && !!this.readOnly,
|
|
2862
|
+
enabled: Array.isArray(this.enabled) ? this.enabled.slice() : [],
|
|
2863
|
+
secure: !!this.secureConnection,
|
|
2864
|
+
compressed: !!this.transport,
|
|
2865
|
+
remoteAddress: (this.socket && this.socket.remoteAddress) || null
|
|
2866
|
+
};
|
|
2867
|
+
}
|
|
2868
|
+
/**
|
|
2869
|
+
* Checks if output can still be sent: the socket is open and the connection is not closing (BYE, LOGOUT)
|
|
2870
|
+
*
|
|
2871
|
+
* @return {Boolean} true for a live session
|
|
2872
|
+
*/
|
|
2873
|
+
isOpen() {
|
|
2874
|
+
return !!this.socket && !this.socket.destroyed && !this._closing;
|
|
2875
|
+
}
|
|
2598
2876
|
/**
|
|
2599
2877
|
* Handles a command line with the script rule that matched it, after the rule's delay. With `run` the line
|
|
2600
2878
|
* goes through the parser and the command handler as usual afterwards
|
|
@@ -2635,6 +2913,8 @@ class IMAPConnection {
|
|
|
2635
2913
|
const command = element.parsed.command.toUpperCase();
|
|
2636
2914
|
this._runningCommand = element;
|
|
2637
2915
|
const options = this.server.getCommandOptions(command);
|
|
2916
|
+
// the session events of the command are worked out when its tagged response goes out, see commandCompleted
|
|
2917
|
+
this._commandStart = { tag: element.parsed.tag, command, state: this.state };
|
|
2638
2918
|
let done = false;
|
|
2639
2919
|
const next = () => {
|
|
2640
2920
|
if (done) {
|
|
@@ -2697,53 +2977,52 @@ class IMAPConnection {
|
|
|
2697
2977
|
// criteria, processNotifications knows when EXPUNGE must wait
|
|
2698
2978
|
this.processNotifications(element.parsed);
|
|
2699
2979
|
}
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2980
|
+
// changes made while the handler runs are attributed to this session (the `origin` of notifications)
|
|
2981
|
+
this.server.withOrigin(this, () => {
|
|
2982
|
+
try {
|
|
2983
|
+
const inputHandler = this.inputHandler;
|
|
2984
|
+
this.server.getCommandHandler(element.parsed.command)(this, element.parsed, element.data, next);
|
|
2985
|
+
if (this.inputHandler && this.inputHandler !== inputHandler) {
|
|
2986
|
+
// the command reads the lines that follow (IDLE, AUTHENTICATE), script rules match them with it
|
|
2987
|
+
this.inputCommand = { tag: element.parsed.tag, command: element.parsed.command };
|
|
2988
|
+
this.emitSession('waiting', { command });
|
|
2989
|
+
}
|
|
2708
2990
|
}
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2991
|
+
catch (E) {
|
|
2992
|
+
const ex = E;
|
|
2993
|
+
const badInput = ex.imapResponse === 'BAD';
|
|
2994
|
+
if (!badInput && this.options.debug) {
|
|
2995
|
+
console.error('Error processing command:', ex, '\n', ex.stack);
|
|
2996
|
+
}
|
|
2997
|
+
this.send({
|
|
2998
|
+
tag: element.parsed.tag,
|
|
2999
|
+
command: badInput ? 'BAD' : 'NO',
|
|
3000
|
+
attributes: [].concat(badInput
|
|
3001
|
+
? []
|
|
3002
|
+
: {
|
|
3003
|
+
type: 'SECTION',
|
|
3004
|
+
section: [
|
|
3005
|
+
{
|
|
3006
|
+
type: 'ATOM',
|
|
3007
|
+
value: 'SERVERBUG'
|
|
3008
|
+
}
|
|
3009
|
+
]
|
|
3010
|
+
}, {
|
|
3011
|
+
type: 'TEXT',
|
|
3012
|
+
value: badInput ? ex.message : 'Server error: ' + ex.message
|
|
3013
|
+
})
|
|
3014
|
+
}, badInput ? 'INVALID COMMAND' : 'SERVER ERROR', element.parsed, element.data);
|
|
3015
|
+
// keep the connection usable, otherwise every later command would hang
|
|
3016
|
+
next();
|
|
2715
3017
|
}
|
|
2716
|
-
|
|
2717
|
-
tag: element.parsed.tag,
|
|
2718
|
-
command: badInput ? 'BAD' : 'NO',
|
|
2719
|
-
attributes: [].concat(badInput
|
|
2720
|
-
? []
|
|
2721
|
-
: {
|
|
2722
|
-
type: 'SECTION',
|
|
2723
|
-
section: [
|
|
2724
|
-
{
|
|
2725
|
-
type: 'ATOM',
|
|
2726
|
-
value: 'SERVERBUG'
|
|
2727
|
-
}
|
|
2728
|
-
]
|
|
2729
|
-
}, {
|
|
2730
|
-
type: 'TEXT',
|
|
2731
|
-
value: badInput ? ex.message : 'Server error: ' + ex.message
|
|
2732
|
-
})
|
|
2733
|
-
}, badInput ? 'INVALID COMMAND' : 'SERVER ERROR', element.parsed, element.data);
|
|
2734
|
-
// keep the connection usable, otherwise every later command would hang
|
|
2735
|
-
next();
|
|
2736
|
-
}
|
|
2737
|
-
finally {
|
|
2738
|
-
this.server.activeConnection = null;
|
|
2739
|
-
}
|
|
3018
|
+
});
|
|
2740
3019
|
}
|
|
2741
3020
|
/**
|
|
2742
3021
|
* Removes messages with \Deleted flag
|
|
2743
3022
|
*
|
|
2744
3023
|
* @param {Object} mailbox Mailbox to check for
|
|
2745
3024
|
* @param {Boolean} [ignoreSelf] If set to true, does not send any notices to itself
|
|
2746
|
-
* @param {Boolean} [
|
|
3025
|
+
* @param {Boolean} [ignoreExists] If set to true, does not send EXISTS notice to itself
|
|
2747
3026
|
*/
|
|
2748
3027
|
expungeDeleted(mailbox, ignoreSelf, ignoreExists) {
|
|
2749
3028
|
this.expungeSpecificMessages(mailbox, (message) => {
|
|
@@ -2752,81 +3031,56 @@ class IMAPConnection {
|
|
|
2752
3031
|
}
|
|
2753
3032
|
/**
|
|
2754
3033
|
* Given a set of messages in a mailbox (possibly via getMessageRange), remove
|
|
2755
|
-
* them from the mailbox and generate EXPUNGE notifications.
|
|
3034
|
+
* them from the mailbox and generate EXPUNGE notifications, see expungeMessages() in store-operations.ts
|
|
2756
3035
|
*
|
|
2757
3036
|
* @param {Object} mailbox Mailbox to check for
|
|
2758
3037
|
* @param {Function|Array} messagesOrFilterFunc An Array of messages in the
|
|
2759
3038
|
* folder that should be removed or a filtering function that indicates
|
|
2760
3039
|
* messages to be removed by returning true.
|
|
2761
3040
|
* @param {Boolean} [ignoreSelf] If set to true, does not send any notices to itself
|
|
2762
|
-
* @param {Boolean} [
|
|
3041
|
+
* @param {Boolean} [ignoreExists] If set to true, does not send EXISTS notice to itself
|
|
2763
3042
|
* @param {Boolean} [highestFirst] If set to true, the EXPUNGE responses go from the highest UID to the lowest
|
|
2764
3043
|
* (MESSAGELIMIT, RFC 9738 section 3.1), otherwise from the lowest
|
|
2765
3044
|
*/
|
|
2766
3045
|
expungeSpecificMessages(mailbox, messagesOrFilterFunc, ignoreSelf, ignoreExists, highestFirst) {
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
else {
|
|
2773
|
-
filterFunc = messagesOrFilterFunc;
|
|
2774
|
-
}
|
|
2775
|
-
// sequence numbers of the removed messages, each one as it is after the earlier EXPUNGE responses. From the
|
|
2776
|
-
// highest message down, the earlier responses do not change the sequence numbers of the later ones
|
|
2777
|
-
const expunged = [];
|
|
2778
|
-
const kept = [];
|
|
2779
|
-
mailbox.messages.forEach((message, i) => {
|
|
2780
|
-
if (filterFunc(message)) {
|
|
2781
|
-
message.ghost = true;
|
|
2782
|
-
expunged.push({ seq: highestFirst ? i + 1 : kept.length + 1, message });
|
|
2783
|
-
}
|
|
2784
|
-
else {
|
|
2785
|
-
kept.push(message);
|
|
2786
|
-
}
|
|
2787
|
-
});
|
|
2788
|
-
if (!expunged.length) {
|
|
2789
|
-
return;
|
|
2790
|
-
}
|
|
2791
|
-
// old copy is required for those sessions that run FETCH before
|
|
2792
|
-
// displaying the EXPUNGE notice
|
|
2793
|
-
const mailboxCopy = mailbox.messages.slice();
|
|
2794
|
-
// update the list in place, other code might hold a reference to it
|
|
2795
|
-
kept.forEach((message, i) => {
|
|
2796
|
-
mailbox.messages[i] = message;
|
|
2797
|
-
});
|
|
2798
|
-
mailbox.messages.length = kept.length;
|
|
2799
|
-
// lets plugins track the removal (e.g. mod-sequences of CONDSTORE and QRESYNC) before any notification
|
|
2800
|
-
this.server.emit('expunge', mailbox, expunged.map(entry => entry.message), this);
|
|
2801
|
-
(highestFirst ? expunged.slice().reverse() : expunged).forEach(entry => {
|
|
2802
|
-
this.server.notify({
|
|
2803
|
-
tag: '*',
|
|
2804
|
-
attributes: [
|
|
2805
|
-
entry.seq,
|
|
2806
|
-
{
|
|
2807
|
-
type: 'ATOM',
|
|
2808
|
-
value: 'EXPUNGE'
|
|
2809
|
-
}
|
|
2810
|
-
],
|
|
2811
|
-
// the removed message, for plugins that report it differently (e.g. VANISHED of QRESYNC)
|
|
2812
|
-
message: entry.message
|
|
2813
|
-
}, mailbox, ignoreSelf ? this : false);
|
|
3046
|
+
(0, store_operations_js_1.expungeMessages)(this.server, mailbox, messagesOrFilterFunc, {
|
|
3047
|
+
origin: this,
|
|
3048
|
+
skip: ignoreSelf ? this : null,
|
|
3049
|
+
skipExists: ignoreExists ? this : null,
|
|
3050
|
+
highestFirst: !!highestFirst
|
|
2814
3051
|
});
|
|
2815
|
-
this.server.notify({
|
|
2816
|
-
tag: '*',
|
|
2817
|
-
attributes: [
|
|
2818
|
-
mailbox.messages.length,
|
|
2819
|
-
{
|
|
2820
|
-
type: 'ATOM',
|
|
2821
|
-
value: 'EXISTS'
|
|
2822
|
-
}
|
|
2823
|
-
],
|
|
2824
|
-
// distribute the old mailbox data with the notification
|
|
2825
|
-
mailboxCopy: mailboxCopy
|
|
2826
|
-
}, mailbox, ignoreSelf || ignoreExists ? this : false);
|
|
2827
3052
|
}
|
|
2828
3053
|
}
|
|
2829
3054
|
exports.IMAPConnection = IMAPConnection;
|
|
3055
|
+
// setImmediate does not let Deno send what sockets were given, see nextTurn
|
|
3056
|
+
const IS_DENO = 'Deno' in globalThis;
|
|
3057
|
+
/**
|
|
3058
|
+
* Runs a function on the next event loop turn that also handles network I/O: setImmediate on Node and Bun, a zero
|
|
3059
|
+
* timer on Deno, where setImmediate runs before the sockets send what was written (#89)
|
|
3060
|
+
*
|
|
3061
|
+
* @param {Function} fn Function to run
|
|
3062
|
+
* @return {Object} the timer, clearOutputQueue clears it
|
|
3063
|
+
*/
|
|
3064
|
+
function nextTurn(fn) {
|
|
3065
|
+
return IS_DENO ? setTimeout(fn, 0) : setImmediate(fn);
|
|
3066
|
+
}
|
|
3067
|
+
/**
|
|
3068
|
+
* Starts listening, resolves with the port or rejects with the listen error (EADDRINUSE)
|
|
3069
|
+
*
|
|
3070
|
+
* @param {Object} server net, tls or http server
|
|
3071
|
+
* @param {Number} [port] Port, a free one if not set
|
|
3072
|
+
* @param {String} [host] Address, all addresses if not set
|
|
3073
|
+
* @return {Promise<Number>} the port
|
|
3074
|
+
*/
|
|
3075
|
+
function listenOn(server, port, host) {
|
|
3076
|
+
return new Promise((resolve, reject) => {
|
|
3077
|
+
server.once('error', reject);
|
|
3078
|
+
server.listen({ port: port || 0, host }, () => {
|
|
3079
|
+
server.removeListener('error', reject);
|
|
3080
|
+
resolve(server.address().port);
|
|
3081
|
+
});
|
|
3082
|
+
});
|
|
3083
|
+
}
|
|
2830
3084
|
/**
|
|
2831
3085
|
* Destroys a socket with a TCP RST (`close: 'reset'` of a script rule). resetAndDestroy (Node 16.17) needs a socket with
|
|
2832
3086
|
* a TCP handle, a TLS socket throws ERR_INVALID_HANDLE_TYPE (#84), so the RST goes to the TCP socket under the TLS
|