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/esm/server.js
CHANGED
|
@@ -13,8 +13,13 @@ import { hasSequenceSetKey } from './commands/handlers/search.js';
|
|
|
13
13
|
import { isSequenceSet } from './numbers.js';
|
|
14
14
|
import { restoreNilAtoms } from './arguments.js';
|
|
15
15
|
import { refuseMissingTarget } from './commands/append.js';
|
|
16
|
+
import { DEFAULT_SESSION_TIMEOUT, storeError, expungeMessages, notifyFlagChanges } from './store-operations.js';
|
|
17
|
+
import { Control } from './control.js';
|
|
18
|
+
import { resolveQuirks } from './quirks.js';
|
|
19
|
+
import { validateStorage } from './storage-schema.js';
|
|
20
|
+
import { createRestServer } from './rest.js';
|
|
16
21
|
import * as bundledCert from './cert.js';
|
|
17
|
-
import { ServerScript, sendOutput, handleLine, literalResponse, releaseDeferred } from './script.js';
|
|
22
|
+
import { ServerScript, sendOutput, handleLine, handleQuiet, literalResponse, releaseDeferred } from './script.js';
|
|
18
23
|
// longest command line (not counting literals) accepted from a client
|
|
19
24
|
const MAX_LINE_LENGTH = 1024 * 1024;
|
|
20
25
|
// milliseconds between the last output and the RST of a script rule with `close: 'reset'`
|
|
@@ -59,18 +64,6 @@ function getResponseTag(line) {
|
|
|
59
64
|
function stateError(command, state) {
|
|
60
65
|
return command + ' is not allowed in the ' + state + ' state';
|
|
61
66
|
}
|
|
62
|
-
/**
|
|
63
|
-
* Creates an error for a failed mailbox operation, with a RFC 5530 response code
|
|
64
|
-
*
|
|
65
|
-
* @param {String} message Error message
|
|
66
|
-
* @param {String} code Response code, e.g. "ALREADYEXISTS"
|
|
67
|
-
* @return {Error} Error object
|
|
68
|
-
*/
|
|
69
|
-
function mailboxError(message, code) {
|
|
70
|
-
const err = new Error(message);
|
|
71
|
-
err.code = code;
|
|
72
|
-
return err;
|
|
73
|
-
}
|
|
74
67
|
/**
|
|
75
68
|
* Creates a new IMAP server, call `listen()` on it to start accepting connections
|
|
76
69
|
*
|
|
@@ -159,11 +152,33 @@ class IMAPServer extends Stream {
|
|
|
159
152
|
// the session whose command is running, see IMAPServer#notify
|
|
160
153
|
this.activeConnection = null;
|
|
161
154
|
this.sessionCounter = 0;
|
|
162
|
-
// rules that make the server misbehave on purpose, from the script option or server.script.add()
|
|
155
|
+
// rules that make the server misbehave on purpose, from the script option or server.script.add(), and the rules
|
|
156
|
+
// of the quirk presets after them
|
|
163
157
|
this.script = new ServerScript(this, this.options.script);
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
158
|
+
const quirks = resolveQuirks(this.options.quirks);
|
|
159
|
+
if (quirks.rules.length) {
|
|
160
|
+
this.script.add(quirks.rules);
|
|
161
|
+
}
|
|
162
|
+
this.loadUsers();
|
|
163
|
+
// plugins register their control operations
|
|
164
|
+
this.control = new Control(this);
|
|
165
|
+
// a quirk preset can leave plugins out (no-move, no-uidplus)
|
|
166
|
+
loadPlugins(this, this.options.plugins, [...quirks.removePlugins]);
|
|
167
|
+
if (this.options.storage) {
|
|
168
|
+
// a typo in a fixture fails here with the path of the problem
|
|
169
|
+
validateStorage(this.options.storage);
|
|
170
|
+
}
|
|
171
|
+
this.systemFlags = [].concat(this.options.systemFlags || ['\\Answered', '\\Flagged', '\\Draft', '\\Deleted', '\\Seen']);
|
|
172
|
+
this.loadStorage();
|
|
173
|
+
this.smtpServer = null;
|
|
174
|
+
this.restServer = null;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Sets the users from the `users` option, or the default user. The users are deep copied, so that runtime changes
|
|
178
|
+
* never leak into the caller's objects or into other servers built from the same fixture. Without a prototype,
|
|
179
|
+
* user names like "__proto__" or "toString" are plain keys
|
|
180
|
+
*/
|
|
181
|
+
loadUsers() {
|
|
167
182
|
this.users = Object.assign(Object.create(null), this.options.users
|
|
168
183
|
? structuredClone(this.options.users)
|
|
169
184
|
: {
|
|
@@ -171,12 +186,16 @@ class IMAPServer extends Stream {
|
|
|
171
186
|
password: 'testpass',
|
|
172
187
|
xoauth2: {
|
|
173
188
|
accessToken: 'testtoken',
|
|
174
|
-
sessionTimeout:
|
|
189
|
+
sessionTimeout: DEFAULT_SESSION_TIMEOUT
|
|
175
190
|
}
|
|
176
191
|
}
|
|
177
192
|
});
|
|
178
|
-
|
|
179
|
-
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Builds the mailboxes from the `storage` option, a deep copy of it. Message and mailbox handlers of plugins run
|
|
196
|
+
* on every message and mailbox
|
|
197
|
+
*/
|
|
198
|
+
loadStorage() {
|
|
180
199
|
// indexFolders() below turns the storage option into namespaces and mailboxes in place
|
|
181
200
|
this.storage = (this.options.storage
|
|
182
201
|
? structuredClone(this.options.storage)
|
|
@@ -184,6 +203,7 @@ class IMAPServer extends Stream {
|
|
|
184
203
|
INBOX: {},
|
|
185
204
|
'': {}
|
|
186
205
|
});
|
|
206
|
+
this.referenceNamespace = false;
|
|
187
207
|
this.uidvalidityCounter = 0; // highest UIDVALIDITY in use, new mailboxes get a higher one
|
|
188
208
|
// subscribed mailbox names (RFC 3501 section 6.3.6). Names, not mailboxes: a subscription outlives
|
|
189
209
|
// DELETE and stays with the old name on RENAME (RFC 9051 section 6.3.6), see trackSubscription
|
|
@@ -191,10 +211,67 @@ class IMAPServer extends Stream {
|
|
|
191
211
|
this.folderCache = Object.create(null);
|
|
192
212
|
this.indexFolders(true);
|
|
193
213
|
}
|
|
214
|
+
/**
|
|
215
|
+
* Starts accepting connections
|
|
216
|
+
*
|
|
217
|
+
* @param {Number} [port] Port to listen on, a free port if not set
|
|
218
|
+
* @param {String} [host] Address to listen on, all addresses if not set
|
|
219
|
+
* @return {Promise<Number>} resolves with the port the server listens on
|
|
220
|
+
*/
|
|
221
|
+
async start(port, host) {
|
|
222
|
+
const imapPort = await listenOn(this.server, port, host);
|
|
223
|
+
await Promise.all([this.options.smtp ? this.startSmtp() : null, this.options.rest ? this.startRest() : null]);
|
|
224
|
+
return imapPort;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Starts the REST API of the `rest` option, see src/rest.ts
|
|
228
|
+
*
|
|
229
|
+
* @return {Promise<Number>} resolves with the HTTP port
|
|
230
|
+
*/
|
|
231
|
+
async startRest() {
|
|
232
|
+
const { port, host, token } = this.options.rest || {};
|
|
233
|
+
this.restServer = createRestServer(this, { host, token });
|
|
234
|
+
return listenOn(this.restServer, port, host || '127.0.0.1');
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Starts the SMTP server of the `smtp` option. smtp-server is an optional dependency, loaded only here
|
|
238
|
+
*
|
|
239
|
+
* @return {Promise<Number>} resolves with the SMTP port
|
|
240
|
+
*/
|
|
241
|
+
startSmtp() {
|
|
242
|
+
const { port, host } = this.options.smtp || {};
|
|
243
|
+
return this.loadSmtpListener().then(({ startSMTPServer }) => new Promise((resolve, reject) => {
|
|
244
|
+
const smtp = startSMTPServer(port || 0, this, smtpPort => {
|
|
245
|
+
smtp.removeListener('error', reject);
|
|
246
|
+
resolve(smtpPort);
|
|
247
|
+
}, host);
|
|
248
|
+
smtp.once('error', reject);
|
|
249
|
+
this.smtpServer = smtp;
|
|
250
|
+
}));
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Loads the SMTP listener, which needs the smtp-server package
|
|
254
|
+
*
|
|
255
|
+
* @return {Promise<Object>} the smtp-listener module
|
|
256
|
+
*/
|
|
257
|
+
loadSmtpListener() {
|
|
258
|
+
return import('./smtp-listener.js').catch((err) => {
|
|
259
|
+
throw new Error('The smtp option needs the smtp-server package, install it with: npm install smtp-server (' + err.message + ')');
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Stops the server and closes every connection
|
|
264
|
+
*
|
|
265
|
+
* @return {Promise} resolves when the server is closed
|
|
266
|
+
*/
|
|
267
|
+
stop() {
|
|
268
|
+
return this.control.shutdown({ graceful: false });
|
|
269
|
+
}
|
|
194
270
|
listen(...args) {
|
|
195
271
|
this.server.listen.apply(this.server, args);
|
|
196
272
|
}
|
|
197
273
|
close(callback) {
|
|
274
|
+
this.closeListeners(true);
|
|
198
275
|
this.server.close(callback);
|
|
199
276
|
// close() only completes once all connections are gone
|
|
200
277
|
this.connections.forEach((connection) => {
|
|
@@ -203,6 +280,29 @@ class IMAPServer extends Stream {
|
|
|
203
280
|
}
|
|
204
281
|
});
|
|
205
282
|
}
|
|
283
|
+
/**
|
|
284
|
+
* Stops the SMTP and REST listeners of `start()`
|
|
285
|
+
*
|
|
286
|
+
* @param {Boolean} force true closes the open REST connections too, otherwise only the idle ones (a request that
|
|
287
|
+
* is answered, like POST /v1/shutdown, finishes first)
|
|
288
|
+
*/
|
|
289
|
+
closeListeners(force) {
|
|
290
|
+
if (this.smtpServer) {
|
|
291
|
+
this.smtpServer.close(() => false);
|
|
292
|
+
this.smtpServer = null;
|
|
293
|
+
}
|
|
294
|
+
if (this.restServer) {
|
|
295
|
+
this.restServer.close();
|
|
296
|
+
// keep-alive connections would hold the server open
|
|
297
|
+
if (force) {
|
|
298
|
+
this.restServer.closeAllConnections?.();
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
this.restServer.closeIdleConnections?.();
|
|
302
|
+
}
|
|
303
|
+
this.restServer = null;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
206
306
|
/**
|
|
207
307
|
* Returns TLS key and certificate. Without credentials in the options the
|
|
208
308
|
* bundled self-signed certificate for localhost is used.
|
|
@@ -322,13 +422,31 @@ class IMAPServer extends Stream {
|
|
|
322
422
|
*
|
|
323
423
|
* @param {String} type Kind of change
|
|
324
424
|
* @param {String} path Storage name of the mailbox
|
|
325
|
-
* @param {Object} [details] `{ oldPath, mailbox }`: the earlier name of a renamed mailbox, the mailbox
|
|
326
|
-
* object that a DELETE removed
|
|
425
|
+
* @param {Object} [details] `{ oldPath, mailbox, created }`: the earlier name of a renamed mailbox, the mailbox
|
|
426
|
+
* object that a DELETE removed, the mailboxes a CREATE made (superior levels first)
|
|
327
427
|
*/
|
|
328
428
|
mailboxChanged(type, path, details) {
|
|
329
429
|
const event = Object.assign({ type, path, oldPath: null, mailbox: null }, details, { origin: this.activeConnection });
|
|
330
430
|
this.emit('mailbox', event);
|
|
331
431
|
}
|
|
432
|
+
/**
|
|
433
|
+
* Runs a change with `origin` as the session that caused it, the `origin` of the notifications and events it
|
|
434
|
+
* makes. Commands run with their session, the control API with null
|
|
435
|
+
*
|
|
436
|
+
* @param {Object|null} origin Session, or null for a change from outside
|
|
437
|
+
* @param {Function} fn Change
|
|
438
|
+
* @return {*} the result of fn
|
|
439
|
+
*/
|
|
440
|
+
withOrigin(origin, fn) {
|
|
441
|
+
const previous = this.activeConnection;
|
|
442
|
+
this.activeConnection = origin;
|
|
443
|
+
try {
|
|
444
|
+
return fn();
|
|
445
|
+
}
|
|
446
|
+
finally {
|
|
447
|
+
this.activeConnection = previous;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
332
450
|
/**
|
|
333
451
|
* Retrieves the handler of an IMAP command
|
|
334
452
|
*
|
|
@@ -461,22 +579,22 @@ class IMAPServer extends Stream {
|
|
|
461
579
|
*/
|
|
462
580
|
createMailbox(path, defaultMailbox) {
|
|
463
581
|
if (!path) {
|
|
464
|
-
throw
|
|
582
|
+
throw storeError('Invalid mailbox name', 'CANNOT');
|
|
465
583
|
}
|
|
466
584
|
// Ensure case insensitive INBOX
|
|
467
585
|
if (path.toUpperCase() === 'INBOX') {
|
|
468
|
-
throw
|
|
586
|
+
throw storeError('INBOX can not be modified', 'ALREADYEXISTS');
|
|
469
587
|
}
|
|
470
588
|
const { namespace, storage } = this.getPersonalNamespace(path);
|
|
471
589
|
path = this.stripSeparator(path, storage.separator);
|
|
472
590
|
if (this.folderCache[path] && this.folderCache[path].flags.indexOf('\\Noselect') < 0) {
|
|
473
|
-
throw
|
|
591
|
+
throw storeError('Mailbox already exists', 'ALREADYEXISTS');
|
|
474
592
|
}
|
|
475
593
|
const folderPath = path.substr(namespace.length).split(storage.separator);
|
|
476
594
|
if (folderPath.some(name => !name)) {
|
|
477
595
|
// an empty hierarchy level ("foo//bar", "/foo", "foo//"). RFC 5530 section 3 has this very case as the
|
|
478
596
|
// example of CANNOT, Dovecot refuses it too
|
|
479
|
-
throw
|
|
597
|
+
throw storeError('Mailbox names can not have empty hierarchy levels', 'CANNOT');
|
|
480
598
|
}
|
|
481
599
|
let parent = storage;
|
|
482
600
|
let curPath = namespace;
|
|
@@ -487,7 +605,7 @@ class IMAPServer extends Stream {
|
|
|
487
605
|
curPath += (curPath.length ? storage.separator : '') + folderName;
|
|
488
606
|
let folder = this.getMailbox(curPath) || false;
|
|
489
607
|
if (folder && folder.flags && folder.flags.indexOf('\\Noinferiors') >= 0) {
|
|
490
|
-
throw
|
|
608
|
+
throw storeError('Can not create subfolders for ' + folder.path, 'CANNOT');
|
|
491
609
|
}
|
|
492
610
|
// a \Noselect placeholder that is created again is replaced with a new mailbox that only keeps
|
|
493
611
|
// the children, nothing else of a deleted mailbox may come back (RFC 3501 section 6.3.3)
|
|
@@ -529,17 +647,17 @@ class IMAPServer extends Stream {
|
|
|
529
647
|
deleteMailbox(path, keepContents) {
|
|
530
648
|
// Ensure case insensitive INBOX
|
|
531
649
|
if (path.toUpperCase() === 'INBOX') {
|
|
532
|
-
throw
|
|
650
|
+
throw storeError('INBOX can not be modified', 'CANNOT');
|
|
533
651
|
}
|
|
534
652
|
const { namespace, storage } = this.getPersonalNamespace(path);
|
|
535
653
|
const mailbox = this.folderCache[this.stripSeparator(path, storage.separator)];
|
|
536
654
|
if (!mailbox) {
|
|
537
|
-
throw
|
|
655
|
+
throw storeError('Mailbox does not exist', 'NONEXISTENT');
|
|
538
656
|
}
|
|
539
657
|
if (mailbox.flags.indexOf('\\Noselect') >= 0 && Object.keys(mailbox.folders || {}).length) {
|
|
540
658
|
// RFC 9051 section 6.3.5: deleting a \Noselect name that has inferior names is an error, the RFC 5530
|
|
541
659
|
// section 3 HASCHILDREN response code tells the client to delete the children first
|
|
542
|
-
throw
|
|
660
|
+
throw storeError('Mailbox has children, delete them first', 'HASCHILDREN');
|
|
543
661
|
}
|
|
544
662
|
const levels = mailbox.path.split(storage.separator);
|
|
545
663
|
const folderName = levels.pop();
|
|
@@ -589,7 +707,7 @@ class IMAPServer extends Stream {
|
|
|
589
707
|
const prefix = key.length ? key.substr(0, key.length - this.storage[key].separator.length) : key;
|
|
590
708
|
if (key.length && (path === prefix || path.substr(0, key.length) === key)) {
|
|
591
709
|
if (path === prefix) {
|
|
592
|
-
throw
|
|
710
|
+
throw storeError('Used mailbox name is a namespace value', 'CANNOT');
|
|
593
711
|
}
|
|
594
712
|
namespace = key;
|
|
595
713
|
}
|
|
@@ -599,10 +717,10 @@ class IMAPServer extends Stream {
|
|
|
599
717
|
});
|
|
600
718
|
const storage = this.storage[namespace];
|
|
601
719
|
if (!storage) {
|
|
602
|
-
throw
|
|
720
|
+
throw storeError('Unknown namespace', 'CANNOT');
|
|
603
721
|
}
|
|
604
722
|
if (storage.type !== 'personal') {
|
|
605
|
-
throw
|
|
723
|
+
throw storeError('Permission denied', 'NOPERM');
|
|
606
724
|
}
|
|
607
725
|
return { namespace, storage };
|
|
608
726
|
}
|
|
@@ -843,6 +961,17 @@ class IMAPServer extends Stream {
|
|
|
843
961
|
const knownFlags = (mailbox.knownFlags = mailbox.knownFlags || []);
|
|
844
962
|
flags.forEach(flag => this.ensureFlag(knownFlags, flag));
|
|
845
963
|
}
|
|
964
|
+
/**
|
|
965
|
+
* The current time for dates the server sets itself (INTERNALDATE of a message without one, SAVEDATE). The `now`
|
|
966
|
+
* option fixes it for repeatable tests: a Date, a timestamp, or a function that returns one
|
|
967
|
+
*
|
|
968
|
+
* @return {Date} current time
|
|
969
|
+
*/
|
|
970
|
+
now() {
|
|
971
|
+
const now = this.options.now;
|
|
972
|
+
const value = typeof now === 'function' ? now() : now;
|
|
973
|
+
return value === undefined || value === null ? new Date() : new Date(value);
|
|
974
|
+
}
|
|
846
975
|
normalizeDateTime(value) {
|
|
847
976
|
if (value instanceof Date) {
|
|
848
977
|
return this.formatInternalDate(value);
|
|
@@ -861,7 +990,7 @@ class IMAPServer extends Stream {
|
|
|
861
990
|
* @param mailbox Mailbox of the message
|
|
862
991
|
*/
|
|
863
992
|
processMessage(message, mailbox) {
|
|
864
|
-
message.internaldate = this.normalizeDateTime(message.internaldate ||
|
|
993
|
+
message.internaldate = this.normalizeDateTime(message.internaldate || this.now());
|
|
865
994
|
message.flags = [].concat(message.flags || []);
|
|
866
995
|
if (message.flags.indexOf('\\Recent') >= 0) {
|
|
867
996
|
// \Recent is not a stored flag, it belongs to the first session that selects the mailbox
|
|
@@ -1251,6 +1380,7 @@ class IMAPConnection {
|
|
|
1251
1380
|
this.notificationQueue = [];
|
|
1252
1381
|
this.server.on('notify', this._notificationCallback);
|
|
1253
1382
|
this.server.connections.add(this);
|
|
1383
|
+
this.emitSession('open');
|
|
1254
1384
|
this.scriptOutput('greeting', '* OK ImapKit ready for rumble\r\n', {});
|
|
1255
1385
|
}
|
|
1256
1386
|
/**
|
|
@@ -1285,7 +1415,7 @@ class IMAPConnection {
|
|
|
1285
1415
|
}
|
|
1286
1416
|
this._outputQueue = this._outputQueue || [];
|
|
1287
1417
|
this._outputQueue.push(Object.assign({}, operation, { transport: this.transport }));
|
|
1288
|
-
if (!this._outputTimer) {
|
|
1418
|
+
if (!this._outputTimer && !this._outputWaiting) {
|
|
1289
1419
|
this.flushOutput();
|
|
1290
1420
|
}
|
|
1291
1421
|
}
|
|
@@ -1306,10 +1436,28 @@ class IMAPConnection {
|
|
|
1306
1436
|
return;
|
|
1307
1437
|
}
|
|
1308
1438
|
if (operation.data && operation.chunk && operation.data.length > operation.chunk) {
|
|
1309
|
-
// the rest waits for chunkDelay, like a delay of its own
|
|
1310
|
-
|
|
1439
|
+
// the rest waits for chunkDelay, like a delay of its own. Without a delay each piece waits for the next
|
|
1440
|
+
// event loop turn, and Nagle's algorithm must not hold it back to merge it with the next one
|
|
1441
|
+
const piece = operation.data.subarray(0, operation.chunk);
|
|
1311
1442
|
operation.data = operation.data.subarray(operation.chunk);
|
|
1443
|
+
if (!operation.chunkDelay || operation.chunkDelay === 'tick') {
|
|
1444
|
+
// the next piece waits until this one was handed to the system, and one more event loop turn, so
|
|
1445
|
+
// the pieces leave as separate segments without a wall clock delay (#89)
|
|
1446
|
+
this.socket?.setNoDelay?.(true);
|
|
1447
|
+
this._outputWaiting = true;
|
|
1448
|
+
this.writeLayer(piece, operation.transport || null, () => {
|
|
1449
|
+
if (this._outputWaiting && this._outputQueue === queue) {
|
|
1450
|
+
this._outputTimer = nextTurn(() => {
|
|
1451
|
+
this._outputTimer = null;
|
|
1452
|
+
this._outputWaiting = false;
|
|
1453
|
+
this.flushOutput();
|
|
1454
|
+
});
|
|
1455
|
+
}
|
|
1456
|
+
});
|
|
1457
|
+
return;
|
|
1458
|
+
}
|
|
1312
1459
|
operation.delay = operation.chunkDelay;
|
|
1460
|
+
this.writeLayer(piece, operation.transport || null);
|
|
1313
1461
|
continue;
|
|
1314
1462
|
}
|
|
1315
1463
|
if (operation.data) {
|
|
@@ -1329,8 +1477,10 @@ class IMAPConnection {
|
|
|
1329
1477
|
clearOutputQueue() {
|
|
1330
1478
|
if (this._outputTimer) {
|
|
1331
1479
|
clearTimeout(this._outputTimer);
|
|
1480
|
+
clearImmediate(this._outputTimer);
|
|
1332
1481
|
this._outputTimer = null;
|
|
1333
1482
|
}
|
|
1483
|
+
this._outputWaiting = false;
|
|
1334
1484
|
this._outputQueue = null;
|
|
1335
1485
|
this._deferredOutput = null;
|
|
1336
1486
|
}
|
|
@@ -1339,13 +1489,17 @@ class IMAPConnection {
|
|
|
1339
1489
|
*
|
|
1340
1490
|
* @param {Buffer} data Output
|
|
1341
1491
|
* @param {Object|null} transport Transport layer
|
|
1492
|
+
* @param {Function} [callback] Called once the data was handed to the system (a transport: on the next turn)
|
|
1342
1493
|
*/
|
|
1343
|
-
writeLayer(data, transport) {
|
|
1494
|
+
writeLayer(data, transport, callback) {
|
|
1344
1495
|
if (transport) {
|
|
1345
1496
|
transport.write(data);
|
|
1497
|
+
if (callback) {
|
|
1498
|
+
setImmediate(callback);
|
|
1499
|
+
}
|
|
1346
1500
|
}
|
|
1347
1501
|
else {
|
|
1348
|
-
this.writeRaw(data);
|
|
1502
|
+
this.writeRaw(data, callback);
|
|
1349
1503
|
}
|
|
1350
1504
|
}
|
|
1351
1505
|
/**
|
|
@@ -1399,17 +1553,70 @@ class IMAPConnection {
|
|
|
1399
1553
|
*
|
|
1400
1554
|
* @param {Buffer} data Data to send
|
|
1401
1555
|
*/
|
|
1402
|
-
writeRaw(data) {
|
|
1556
|
+
writeRaw(data, callback) {
|
|
1403
1557
|
if (this.socket && !this.socket.destroyed) {
|
|
1404
|
-
this.socket.write(data);
|
|
1558
|
+
this.socket.write(data, callback);
|
|
1559
|
+
this.touch();
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
/**
|
|
1563
|
+
* Notes input or output for the quiet events of script rules (#88)
|
|
1564
|
+
*/
|
|
1565
|
+
touch() {
|
|
1566
|
+
if (!this.server.script.watches('quiet')) {
|
|
1567
|
+
// nothing waits for quiet sessions, a rule added later counts from then on
|
|
1568
|
+
this._lastActivity = undefined;
|
|
1569
|
+
return;
|
|
1570
|
+
}
|
|
1571
|
+
this._lastActivity = Date.now();
|
|
1572
|
+
if (!this._quietTimer) {
|
|
1573
|
+
this.watchQuiet();
|
|
1405
1574
|
}
|
|
1406
1575
|
}
|
|
1576
|
+
/**
|
|
1577
|
+
* Starts waiting for the next quiet event, if a rule waits for one and the session is not waiting already. When
|
|
1578
|
+
* the time is up, the first rule that matches handles the event (`send`, `close`), the output it sends starts
|
|
1579
|
+
* the next quiet time
|
|
1580
|
+
*
|
|
1581
|
+
* @param {Number} [checked] Quiet time the rules were checked for already, only longer ones are waited for
|
|
1582
|
+
*/
|
|
1583
|
+
watchQuiet(checked = 0) {
|
|
1584
|
+
if (this._quietTimer || !this.socket || this._closing) {
|
|
1585
|
+
return;
|
|
1586
|
+
}
|
|
1587
|
+
const last = this._lastActivity || Date.now();
|
|
1588
|
+
this._lastActivity = last;
|
|
1589
|
+
// the time is measured from the last activity, a rule whose time is up already fires right away
|
|
1590
|
+
const next = this.server.script.nextQuiet(checked);
|
|
1591
|
+
if (next === null) {
|
|
1592
|
+
return;
|
|
1593
|
+
}
|
|
1594
|
+
this._quietTimer = setTimeout(() => {
|
|
1595
|
+
this._quietTimer = null;
|
|
1596
|
+
if (!this.isOpen() || this._lastActivity !== last) {
|
|
1597
|
+
// there was activity in between, it started a new wait
|
|
1598
|
+
return this.watchQuiet();
|
|
1599
|
+
}
|
|
1600
|
+
// a timer can fire a millisecond before Date.now() says the time is up
|
|
1601
|
+
const quiet = Math.max(Date.now() - last, next);
|
|
1602
|
+
const found = this.server.script.check(this, 'quiet', { data: '', quiet });
|
|
1603
|
+
if (found) {
|
|
1604
|
+
handleQuiet(this, found.rule, found.context);
|
|
1605
|
+
}
|
|
1606
|
+
if (this._lastActivity === last) {
|
|
1607
|
+
// nothing was sent, wait for the rules with a longer quiet time
|
|
1608
|
+
this.watchQuiet(quiet);
|
|
1609
|
+
}
|
|
1610
|
+
}, Math.max(0, next - (Date.now() - last)));
|
|
1611
|
+
this._quietTimer.unref();
|
|
1612
|
+
}
|
|
1407
1613
|
/**
|
|
1408
1614
|
* Handles data from the socket, through the transport layer if there is one
|
|
1409
1615
|
*
|
|
1410
1616
|
* @param {Buffer} chunk Received data
|
|
1411
1617
|
*/
|
|
1412
1618
|
receive(chunk) {
|
|
1619
|
+
this.touch();
|
|
1413
1620
|
if (this.transport) {
|
|
1414
1621
|
this.transport.receive(chunk);
|
|
1415
1622
|
}
|
|
@@ -1542,11 +1749,15 @@ class IMAPConnection {
|
|
|
1542
1749
|
* notifications that were not sent yet are dropped. Sends nothing, the caller answers the command
|
|
1543
1750
|
*/
|
|
1544
1751
|
closeMailbox() {
|
|
1752
|
+
const wasSelected = !!this.selectedMailbox;
|
|
1545
1753
|
this.state = 'Authenticated';
|
|
1546
1754
|
this.selectedMailbox = false;
|
|
1547
1755
|
this.readOnly = false;
|
|
1548
1756
|
this.recent = null;
|
|
1549
1757
|
this.notificationQueue = [];
|
|
1758
|
+
if (wasSelected) {
|
|
1759
|
+
this.emitSession('unselect');
|
|
1760
|
+
}
|
|
1550
1761
|
}
|
|
1551
1762
|
onClose() {
|
|
1552
1763
|
if (this.socket) {
|
|
@@ -1558,8 +1769,14 @@ class IMAPConnection {
|
|
|
1558
1769
|
this.transport = null;
|
|
1559
1770
|
}
|
|
1560
1771
|
this.clearOutputQueue();
|
|
1772
|
+
if (this._quietTimer) {
|
|
1773
|
+
clearTimeout(this._quietTimer);
|
|
1774
|
+
this._quietTimer = null;
|
|
1775
|
+
}
|
|
1561
1776
|
this.server.removeListener('notify', this._notificationCallback);
|
|
1562
|
-
this.server.connections.delete(this)
|
|
1777
|
+
if (this.server.connections.delete(this)) {
|
|
1778
|
+
this.emitSession('close');
|
|
1779
|
+
}
|
|
1563
1780
|
}
|
|
1564
1781
|
onError(err) {
|
|
1565
1782
|
if (this.options.debug) {
|
|
@@ -1911,8 +2128,8 @@ class IMAPConnection {
|
|
|
1911
2128
|
* @param {Array} messages Messages with changed flags
|
|
1912
2129
|
*/
|
|
1913
2130
|
notifyFlagChanges(messages) {
|
|
1914
|
-
if (
|
|
1915
|
-
this.server
|
|
2131
|
+
if (this.selectedMailbox) {
|
|
2132
|
+
notifyFlagChanges(this.server, this.selectedMailbox, messages, this);
|
|
1916
2133
|
}
|
|
1917
2134
|
}
|
|
1918
2135
|
/**
|
|
@@ -2168,7 +2385,7 @@ class IMAPConnection {
|
|
|
2168
2385
|
*/
|
|
2169
2386
|
send(response, description, parsed, data, ...extra) {
|
|
2170
2387
|
// nothing goes out once the connection is closing (RFC 3501 section 7.1.5)
|
|
2171
|
-
if (!this.
|
|
2388
|
+
if (!this.isOpen()) {
|
|
2172
2389
|
return;
|
|
2173
2390
|
}
|
|
2174
2391
|
if (!response.notification && response.tag !== '*') {
|
|
@@ -2178,6 +2395,9 @@ class IMAPConnection {
|
|
|
2178
2395
|
this.server.outputHandlers.forEach(handler => {
|
|
2179
2396
|
handler(this, response, description, parsed, data, ...extra);
|
|
2180
2397
|
});
|
|
2398
|
+
if (this._commandStart && parsed && response.tag === this._commandStart.tag && parsed.tag === response.tag) {
|
|
2399
|
+
this.commandCompleted(String(response.command || '').toUpperCase());
|
|
2400
|
+
}
|
|
2181
2401
|
// No need to display this response to user
|
|
2182
2402
|
if (response.skipResponse) {
|
|
2183
2403
|
return;
|
|
@@ -2553,6 +2773,64 @@ class IMAPConnection {
|
|
|
2553
2773
|
}, 'UNKNOWN COMMAND', parsed, data);
|
|
2554
2774
|
}
|
|
2555
2775
|
}
|
|
2776
|
+
/**
|
|
2777
|
+
* Emits the `command` event when the tagged response of a command goes out, and the `session` events "login"
|
|
2778
|
+
* (the command authenticated the session) and "logout" (back to Not Authenticated, UNAUTHENTICATE). Every command
|
|
2779
|
+
* that changes these does so before its tagged response. "select" and "unselect" come from SELECT and
|
|
2780
|
+
* closeMailbox(). Tests wait for these instead of polling
|
|
2781
|
+
*
|
|
2782
|
+
* @param {String} status OK, NO or BAD
|
|
2783
|
+
*/
|
|
2784
|
+
commandCompleted(status) {
|
|
2785
|
+
const start = this._commandStart;
|
|
2786
|
+
this._commandStart = null;
|
|
2787
|
+
const server = this.server;
|
|
2788
|
+
if (!server.listenerCount('command') && !server.listenerCount('session')) {
|
|
2789
|
+
return;
|
|
2790
|
+
}
|
|
2791
|
+
server.emit('command', { session: this.sessionNumber, tag: start.tag, command: start.command, status, user: this.username || null });
|
|
2792
|
+
const authenticated = (state) => state === 'Authenticated' || state === 'Selected';
|
|
2793
|
+
if (authenticated(start.state) !== authenticated(this.state) && this.state !== 'Logout') {
|
|
2794
|
+
this.emitSession(authenticated(this.state) ? 'login' : 'logout');
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
/**
|
|
2798
|
+
* Emits a `session` event, `{ type, session, ...fields }`
|
|
2799
|
+
*
|
|
2800
|
+
* @param {String} type Event type
|
|
2801
|
+
* @param {Object} [fields] More fields of the event
|
|
2802
|
+
*/
|
|
2803
|
+
emitSession(type, fields) {
|
|
2804
|
+
if (this.server.listenerCount('session')) {
|
|
2805
|
+
this.server.emit('session', Object.assign({ type, session: this.describe() }, fields));
|
|
2806
|
+
}
|
|
2807
|
+
}
|
|
2808
|
+
/**
|
|
2809
|
+
* Describes the session without credentials or sockets, for the control API and the `session` events
|
|
2810
|
+
*
|
|
2811
|
+
* @return {Object} `{ session, user, state, mailbox, readOnly, enabled, secure, compressed, remoteAddress }`
|
|
2812
|
+
*/
|
|
2813
|
+
describe() {
|
|
2814
|
+
return {
|
|
2815
|
+
session: this.sessionNumber,
|
|
2816
|
+
user: this.username || null,
|
|
2817
|
+
state: this.state,
|
|
2818
|
+
mailbox: this.selectedMailbox ? this.selectedMailbox.path : null,
|
|
2819
|
+
readOnly: !!this.selectedMailbox && !!this.readOnly,
|
|
2820
|
+
enabled: Array.isArray(this.enabled) ? this.enabled.slice() : [],
|
|
2821
|
+
secure: !!this.secureConnection,
|
|
2822
|
+
compressed: !!this.transport,
|
|
2823
|
+
remoteAddress: (this.socket && this.socket.remoteAddress) || null
|
|
2824
|
+
};
|
|
2825
|
+
}
|
|
2826
|
+
/**
|
|
2827
|
+
* Checks if output can still be sent: the socket is open and the connection is not closing (BYE, LOGOUT)
|
|
2828
|
+
*
|
|
2829
|
+
* @return {Boolean} true for a live session
|
|
2830
|
+
*/
|
|
2831
|
+
isOpen() {
|
|
2832
|
+
return !!this.socket && !this.socket.destroyed && !this._closing;
|
|
2833
|
+
}
|
|
2556
2834
|
/**
|
|
2557
2835
|
* Handles a command line with the script rule that matched it, after the rule's delay. With `run` the line
|
|
2558
2836
|
* goes through the parser and the command handler as usual afterwards
|
|
@@ -2593,6 +2871,8 @@ class IMAPConnection {
|
|
|
2593
2871
|
const command = element.parsed.command.toUpperCase();
|
|
2594
2872
|
this._runningCommand = element;
|
|
2595
2873
|
const options = this.server.getCommandOptions(command);
|
|
2874
|
+
// the session events of the command are worked out when its tagged response goes out, see commandCompleted
|
|
2875
|
+
this._commandStart = { tag: element.parsed.tag, command, state: this.state };
|
|
2596
2876
|
let done = false;
|
|
2597
2877
|
const next = () => {
|
|
2598
2878
|
if (done) {
|
|
@@ -2655,53 +2935,52 @@ class IMAPConnection {
|
|
|
2655
2935
|
// criteria, processNotifications knows when EXPUNGE must wait
|
|
2656
2936
|
this.processNotifications(element.parsed);
|
|
2657
2937
|
}
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2938
|
+
// changes made while the handler runs are attributed to this session (the `origin` of notifications)
|
|
2939
|
+
this.server.withOrigin(this, () => {
|
|
2940
|
+
try {
|
|
2941
|
+
const inputHandler = this.inputHandler;
|
|
2942
|
+
this.server.getCommandHandler(element.parsed.command)(this, element.parsed, element.data, next);
|
|
2943
|
+
if (this.inputHandler && this.inputHandler !== inputHandler) {
|
|
2944
|
+
// the command reads the lines that follow (IDLE, AUTHENTICATE), script rules match them with it
|
|
2945
|
+
this.inputCommand = { tag: element.parsed.tag, command: element.parsed.command };
|
|
2946
|
+
this.emitSession('waiting', { command });
|
|
2947
|
+
}
|
|
2666
2948
|
}
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2949
|
+
catch (E) {
|
|
2950
|
+
const ex = E;
|
|
2951
|
+
const badInput = ex.imapResponse === 'BAD';
|
|
2952
|
+
if (!badInput && this.options.debug) {
|
|
2953
|
+
console.error('Error processing command:', ex, '\n', ex.stack);
|
|
2954
|
+
}
|
|
2955
|
+
this.send({
|
|
2956
|
+
tag: element.parsed.tag,
|
|
2957
|
+
command: badInput ? 'BAD' : 'NO',
|
|
2958
|
+
attributes: [].concat(badInput
|
|
2959
|
+
? []
|
|
2960
|
+
: {
|
|
2961
|
+
type: 'SECTION',
|
|
2962
|
+
section: [
|
|
2963
|
+
{
|
|
2964
|
+
type: 'ATOM',
|
|
2965
|
+
value: 'SERVERBUG'
|
|
2966
|
+
}
|
|
2967
|
+
]
|
|
2968
|
+
}, {
|
|
2969
|
+
type: 'TEXT',
|
|
2970
|
+
value: badInput ? ex.message : 'Server error: ' + ex.message
|
|
2971
|
+
})
|
|
2972
|
+
}, badInput ? 'INVALID COMMAND' : 'SERVER ERROR', element.parsed, element.data);
|
|
2973
|
+
// keep the connection usable, otherwise every later command would hang
|
|
2974
|
+
next();
|
|
2673
2975
|
}
|
|
2674
|
-
|
|
2675
|
-
tag: element.parsed.tag,
|
|
2676
|
-
command: badInput ? 'BAD' : 'NO',
|
|
2677
|
-
attributes: [].concat(badInput
|
|
2678
|
-
? []
|
|
2679
|
-
: {
|
|
2680
|
-
type: 'SECTION',
|
|
2681
|
-
section: [
|
|
2682
|
-
{
|
|
2683
|
-
type: 'ATOM',
|
|
2684
|
-
value: 'SERVERBUG'
|
|
2685
|
-
}
|
|
2686
|
-
]
|
|
2687
|
-
}, {
|
|
2688
|
-
type: 'TEXT',
|
|
2689
|
-
value: badInput ? ex.message : 'Server error: ' + ex.message
|
|
2690
|
-
})
|
|
2691
|
-
}, badInput ? 'INVALID COMMAND' : 'SERVER ERROR', element.parsed, element.data);
|
|
2692
|
-
// keep the connection usable, otherwise every later command would hang
|
|
2693
|
-
next();
|
|
2694
|
-
}
|
|
2695
|
-
finally {
|
|
2696
|
-
this.server.activeConnection = null;
|
|
2697
|
-
}
|
|
2976
|
+
});
|
|
2698
2977
|
}
|
|
2699
2978
|
/**
|
|
2700
2979
|
* Removes messages with \Deleted flag
|
|
2701
2980
|
*
|
|
2702
2981
|
* @param {Object} mailbox Mailbox to check for
|
|
2703
2982
|
* @param {Boolean} [ignoreSelf] If set to true, does not send any notices to itself
|
|
2704
|
-
* @param {Boolean} [
|
|
2983
|
+
* @param {Boolean} [ignoreExists] If set to true, does not send EXISTS notice to itself
|
|
2705
2984
|
*/
|
|
2706
2985
|
expungeDeleted(mailbox, ignoreSelf, ignoreExists) {
|
|
2707
2986
|
this.expungeSpecificMessages(mailbox, (message) => {
|
|
@@ -2710,80 +2989,55 @@ class IMAPConnection {
|
|
|
2710
2989
|
}
|
|
2711
2990
|
/**
|
|
2712
2991
|
* Given a set of messages in a mailbox (possibly via getMessageRange), remove
|
|
2713
|
-
* them from the mailbox and generate EXPUNGE notifications.
|
|
2992
|
+
* them from the mailbox and generate EXPUNGE notifications, see expungeMessages() in store-operations.ts
|
|
2714
2993
|
*
|
|
2715
2994
|
* @param {Object} mailbox Mailbox to check for
|
|
2716
2995
|
* @param {Function|Array} messagesOrFilterFunc An Array of messages in the
|
|
2717
2996
|
* folder that should be removed or a filtering function that indicates
|
|
2718
2997
|
* messages to be removed by returning true.
|
|
2719
2998
|
* @param {Boolean} [ignoreSelf] If set to true, does not send any notices to itself
|
|
2720
|
-
* @param {Boolean} [
|
|
2999
|
+
* @param {Boolean} [ignoreExists] If set to true, does not send EXISTS notice to itself
|
|
2721
3000
|
* @param {Boolean} [highestFirst] If set to true, the EXPUNGE responses go from the highest UID to the lowest
|
|
2722
3001
|
* (MESSAGELIMIT, RFC 9738 section 3.1), otherwise from the lowest
|
|
2723
3002
|
*/
|
|
2724
3003
|
expungeSpecificMessages(mailbox, messagesOrFilterFunc, ignoreSelf, ignoreExists, highestFirst) {
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
else {
|
|
2731
|
-
filterFunc = messagesOrFilterFunc;
|
|
2732
|
-
}
|
|
2733
|
-
// sequence numbers of the removed messages, each one as it is after the earlier EXPUNGE responses. From the
|
|
2734
|
-
// highest message down, the earlier responses do not change the sequence numbers of the later ones
|
|
2735
|
-
const expunged = [];
|
|
2736
|
-
const kept = [];
|
|
2737
|
-
mailbox.messages.forEach((message, i) => {
|
|
2738
|
-
if (filterFunc(message)) {
|
|
2739
|
-
message.ghost = true;
|
|
2740
|
-
expunged.push({ seq: highestFirst ? i + 1 : kept.length + 1, message });
|
|
2741
|
-
}
|
|
2742
|
-
else {
|
|
2743
|
-
kept.push(message);
|
|
2744
|
-
}
|
|
2745
|
-
});
|
|
2746
|
-
if (!expunged.length) {
|
|
2747
|
-
return;
|
|
2748
|
-
}
|
|
2749
|
-
// old copy is required for those sessions that run FETCH before
|
|
2750
|
-
// displaying the EXPUNGE notice
|
|
2751
|
-
const mailboxCopy = mailbox.messages.slice();
|
|
2752
|
-
// update the list in place, other code might hold a reference to it
|
|
2753
|
-
kept.forEach((message, i) => {
|
|
2754
|
-
mailbox.messages[i] = message;
|
|
2755
|
-
});
|
|
2756
|
-
mailbox.messages.length = kept.length;
|
|
2757
|
-
// lets plugins track the removal (e.g. mod-sequences of CONDSTORE and QRESYNC) before any notification
|
|
2758
|
-
this.server.emit('expunge', mailbox, expunged.map(entry => entry.message), this);
|
|
2759
|
-
(highestFirst ? expunged.slice().reverse() : expunged).forEach(entry => {
|
|
2760
|
-
this.server.notify({
|
|
2761
|
-
tag: '*',
|
|
2762
|
-
attributes: [
|
|
2763
|
-
entry.seq,
|
|
2764
|
-
{
|
|
2765
|
-
type: 'ATOM',
|
|
2766
|
-
value: 'EXPUNGE'
|
|
2767
|
-
}
|
|
2768
|
-
],
|
|
2769
|
-
// the removed message, for plugins that report it differently (e.g. VANISHED of QRESYNC)
|
|
2770
|
-
message: entry.message
|
|
2771
|
-
}, mailbox, ignoreSelf ? this : false);
|
|
3004
|
+
expungeMessages(this.server, mailbox, messagesOrFilterFunc, {
|
|
3005
|
+
origin: this,
|
|
3006
|
+
skip: ignoreSelf ? this : null,
|
|
3007
|
+
skipExists: ignoreExists ? this : null,
|
|
3008
|
+
highestFirst: !!highestFirst
|
|
2772
3009
|
});
|
|
2773
|
-
this.server.notify({
|
|
2774
|
-
tag: '*',
|
|
2775
|
-
attributes: [
|
|
2776
|
-
mailbox.messages.length,
|
|
2777
|
-
{
|
|
2778
|
-
type: 'ATOM',
|
|
2779
|
-
value: 'EXISTS'
|
|
2780
|
-
}
|
|
2781
|
-
],
|
|
2782
|
-
// distribute the old mailbox data with the notification
|
|
2783
|
-
mailboxCopy: mailboxCopy
|
|
2784
|
-
}, mailbox, ignoreSelf || ignoreExists ? this : false);
|
|
2785
3010
|
}
|
|
2786
3011
|
}
|
|
3012
|
+
// setImmediate does not let Deno send what sockets were given, see nextTurn
|
|
3013
|
+
const IS_DENO = 'Deno' in globalThis;
|
|
3014
|
+
/**
|
|
3015
|
+
* Runs a function on the next event loop turn that also handles network I/O: setImmediate on Node and Bun, a zero
|
|
3016
|
+
* timer on Deno, where setImmediate runs before the sockets send what was written (#89)
|
|
3017
|
+
*
|
|
3018
|
+
* @param {Function} fn Function to run
|
|
3019
|
+
* @return {Object} the timer, clearOutputQueue clears it
|
|
3020
|
+
*/
|
|
3021
|
+
function nextTurn(fn) {
|
|
3022
|
+
return IS_DENO ? setTimeout(fn, 0) : setImmediate(fn);
|
|
3023
|
+
}
|
|
3024
|
+
/**
|
|
3025
|
+
* Starts listening, resolves with the port or rejects with the listen error (EADDRINUSE)
|
|
3026
|
+
*
|
|
3027
|
+
* @param {Object} server net, tls or http server
|
|
3028
|
+
* @param {Number} [port] Port, a free one if not set
|
|
3029
|
+
* @param {String} [host] Address, all addresses if not set
|
|
3030
|
+
* @return {Promise<Number>} the port
|
|
3031
|
+
*/
|
|
3032
|
+
function listenOn(server, port, host) {
|
|
3033
|
+
return new Promise((resolve, reject) => {
|
|
3034
|
+
server.once('error', reject);
|
|
3035
|
+
server.listen({ port: port || 0, host }, () => {
|
|
3036
|
+
server.removeListener('error', reject);
|
|
3037
|
+
resolve(server.address().port);
|
|
3038
|
+
});
|
|
3039
|
+
});
|
|
3040
|
+
}
|
|
2787
3041
|
/**
|
|
2788
3042
|
* Destroys a socket with a TCP RST (`close: 'reset'` of a script rule). resetAndDestroy (Node 16.17) needs a socket with
|
|
2789
3043
|
* a TCP handle, a TLS socket throws ERR_INVALID_HANDLE_TYPE (#84), so the RST goes to the TCP socket under the TLS
|