imapkit 4.3.1 → 5.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +187 -67
- package/bin/help.txt +20 -5
- package/bin/imapkit.js +64 -24
- 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/fetch.js +2 -1
- 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 +7 -7
- package/dist/cjs/commands/list.js +17 -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 +425 -0
- package/dist/cjs/control.js +637 -0
- package/dist/cjs/dates.d.ts +9 -1
- package/dist/cjs/dates.js +20 -0
- package/dist/cjs/envelope.d.ts +1 -1
- package/dist/cjs/envelope.js +26 -1
- package/dist/cjs/index.d.ts +9 -1
- package/dist/cjs/index.js +9 -2
- package/dist/cjs/load-plugins.d.ts +3 -1
- package/dist/cjs/load-plugins.js +28 -4
- package/dist/cjs/plugins/acl.js +109 -42
- package/dist/cjs/plugins/binary.js +2 -1
- package/dist/cjs/plugins/condstore.js +7 -0
- package/dist/cjs/plugins/context-search.js +2 -2
- package/dist/cjs/plugins/create-special-use.d.ts +0 -5
- package/dist/cjs/plugins/create-special-use.js +11 -1
- package/dist/cjs/plugins/imap4rev2.js +5 -0
- package/dist/cjs/plugins/index.js +1 -3
- package/dist/cjs/plugins/metadata-server.d.ts +6 -2
- package/dist/cjs/plugins/metadata-server.js +3 -1
- package/dist/cjs/plugins/metadata.d.ts +5 -1
- package/dist/cjs/plugins/metadata.js +143 -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 +125 -0
- package/dist/cjs/random.d.ts +10 -0
- package/dist/cjs/random.js +22 -0
- package/dist/cjs/rest.d.ts +54 -0
- package/dist/cjs/rest.js +567 -0
- package/dist/cjs/script.d.ts +33 -4
- package/dist/cjs/script.js +64 -4
- package/dist/cjs/server.d.ts +199 -9
- package/dist/cjs/server.js +603 -227
- 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 +222 -0
- package/dist/cjs/store-operations.d.ts +171 -0
- package/dist/cjs/store-operations.js +425 -0
- package/dist/cjs/types.d.ts +26 -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/fetch.js +2 -1
- 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 +7 -7
- package/dist/esm/commands/list.js +17 -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 +425 -0
- package/dist/esm/control.js +630 -0
- package/dist/esm/dates.d.ts +9 -1
- package/dist/esm/dates.js +20 -1
- package/dist/esm/envelope.d.ts +1 -1
- package/dist/esm/envelope.js +26 -1
- package/dist/esm/index.d.ts +9 -1
- package/dist/esm/index.js +5 -2
- package/dist/esm/load-plugins.d.ts +3 -1
- package/dist/esm/load-plugins.js +28 -4
- package/dist/esm/plugins/acl.js +109 -42
- package/dist/esm/plugins/binary.js +2 -1
- package/dist/esm/plugins/condstore.js +7 -0
- package/dist/esm/plugins/context-search.js +2 -2
- package/dist/esm/plugins/create-special-use.d.ts +0 -5
- package/dist/esm/plugins/create-special-use.js +8 -1
- package/dist/esm/plugins/imap4rev2.js +5 -0
- package/dist/esm/plugins/index.js +1 -3
- package/dist/esm/plugins/metadata-server.d.ts +6 -2
- package/dist/esm/plugins/metadata-server.js +3 -1
- package/dist/esm/plugins/metadata.d.ts +5 -1
- package/dist/esm/plugins/metadata.js +143 -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 +121 -0
- package/dist/esm/random.d.ts +10 -0
- package/dist/esm/random.js +19 -0
- package/dist/esm/rest.d.ts +54 -0
- package/dist/esm/rest.js +559 -0
- package/dist/esm/script.d.ts +33 -4
- package/dist/esm/script.js +63 -4
- package/dist/esm/server.d.ts +199 -9
- package/dist/esm/server.js +606 -230
- 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 +218 -0
- package/dist/esm/store-operations.d.ts +171 -0
- package/dist/esm/store-operations.js +410 -0
- package/dist/esm/types.d.ts +26 -1
- package/dist/plugin-help.json +3 -16
- 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
|
|
@@ -101,17 +106,35 @@ function stateError(command, state) {
|
|
|
101
106
|
return command + ' is not allowed in the ' + state + ' state';
|
|
102
107
|
}
|
|
103
108
|
/**
|
|
104
|
-
*
|
|
109
|
+
* Checks if a queued notification changes message sequence numbers by removing messages: an EXPUNGE response, or the
|
|
110
|
+
* EXISTS that follows the EXPUNGE responses of another session (it carries the snapshot of the old message list)
|
|
105
111
|
*
|
|
106
|
-
* @param {
|
|
107
|
-
* @
|
|
108
|
-
* @return {Error} Error object
|
|
112
|
+
* @param {Object} notification Queued notification
|
|
113
|
+
* @return {Boolean} true if the notification must wait while EXPUNGE responses are not allowed
|
|
109
114
|
*/
|
|
110
|
-
function
|
|
111
|
-
|
|
112
|
-
err.code = code;
|
|
113
|
-
return err;
|
|
115
|
+
function isPendingExpunge(notification) {
|
|
116
|
+
return !!notification.mailboxCopy || (0, arguments_js_1.isAtom)(notification.attributes && notification.attributes[1], 'EXPUNGE');
|
|
114
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* Checks if a notification is an untagged EXISTS response
|
|
120
|
+
*
|
|
121
|
+
* @param {Object} [notification] Notification to send
|
|
122
|
+
* @return {Boolean} true for an EXISTS response
|
|
123
|
+
*/
|
|
124
|
+
function isExists(notification) {
|
|
125
|
+
return !!notification && !notification.command && (0, arguments_js_1.isAtom)(notification.attributes && notification.attributes[1], 'EXISTS');
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Answers AUTHENTICATE with a mechanism no plugin supports: NO, not a syntax error (RFC 3501 section 6.2.2). It runs
|
|
129
|
+
* through the command queue, so the state check comes first (AUTHENTICATE is only valid in the Not Authenticated
|
|
130
|
+
* state, RFC 3501 section 6.2)
|
|
131
|
+
*/
|
|
132
|
+
// usesSequenceNumbers() of each running command
|
|
133
|
+
const sequenceNumberUse = new WeakMap();
|
|
134
|
+
const unsupportedMechanism = (connection, parsed, data, callback) => {
|
|
135
|
+
connection.sendStatus(parsed, data, 'NO', 'Unsupported authentication mechanism', false, 'UNKNOWN COMMAND');
|
|
136
|
+
callback();
|
|
137
|
+
};
|
|
115
138
|
/**
|
|
116
139
|
* Creates a new IMAP server, call `listen()` on it to start accepting connections
|
|
117
140
|
*
|
|
@@ -200,11 +223,33 @@ class IMAPServer extends node_stream_1.Stream {
|
|
|
200
223
|
// the session whose command is running, see IMAPServer#notify
|
|
201
224
|
this.activeConnection = null;
|
|
202
225
|
this.sessionCounter = 0;
|
|
203
|
-
// rules that make the server misbehave on purpose, from the script option or server.script.add()
|
|
226
|
+
// rules that make the server misbehave on purpose, from the script option or server.script.add(), and the rules
|
|
227
|
+
// of the quirk presets after them
|
|
204
228
|
this.script = new script_js_1.ServerScript(this, this.options.script);
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
229
|
+
const quirks = (0, quirks_js_1.resolveQuirks)(this.options.quirks);
|
|
230
|
+
if (quirks.rules.length) {
|
|
231
|
+
this.script.add(quirks.rules);
|
|
232
|
+
}
|
|
233
|
+
this.loadUsers();
|
|
234
|
+
// plugins register their control operations
|
|
235
|
+
this.control = new control_js_1.Control(this);
|
|
236
|
+
// a quirk preset can leave plugins out (no-move, no-uidplus)
|
|
237
|
+
(0, load_plugins_js_1.default)(this, this.options.plugins, quirks.removePlugins);
|
|
238
|
+
if (this.options.storage) {
|
|
239
|
+
// a typo in a fixture fails here with the path of the problem
|
|
240
|
+
(0, storage_schema_js_1.validateStorage)(this.options.storage);
|
|
241
|
+
}
|
|
242
|
+
this.systemFlags = [].concat(this.options.systemFlags || ['\\Answered', '\\Flagged', '\\Draft', '\\Deleted', '\\Seen']);
|
|
243
|
+
this.loadStorage();
|
|
244
|
+
this.smtpServer = null;
|
|
245
|
+
this.restServer = null;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Sets the users from the `users` option, or the default user. The users are deep copied, so that runtime changes
|
|
249
|
+
* never leak into the caller's objects or into other servers built from the same fixture. Without a prototype,
|
|
250
|
+
* user names like "__proto__" or "toString" are plain keys
|
|
251
|
+
*/
|
|
252
|
+
loadUsers() {
|
|
208
253
|
this.users = Object.assign(Object.create(null), this.options.users
|
|
209
254
|
? structuredClone(this.options.users)
|
|
210
255
|
: {
|
|
@@ -212,12 +257,16 @@ class IMAPServer extends node_stream_1.Stream {
|
|
|
212
257
|
password: 'testpass',
|
|
213
258
|
xoauth2: {
|
|
214
259
|
accessToken: 'testtoken',
|
|
215
|
-
sessionTimeout:
|
|
260
|
+
sessionTimeout: store_operations_js_1.DEFAULT_SESSION_TIMEOUT
|
|
216
261
|
}
|
|
217
262
|
}
|
|
218
263
|
});
|
|
219
|
-
|
|
220
|
-
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Builds the mailboxes from the `storage` option, a deep copy of it. Message and mailbox handlers of plugins run
|
|
267
|
+
* on every message and mailbox
|
|
268
|
+
*/
|
|
269
|
+
loadStorage() {
|
|
221
270
|
// indexFolders() below turns the storage option into namespaces and mailboxes in place
|
|
222
271
|
this.storage = (this.options.storage
|
|
223
272
|
? structuredClone(this.options.storage)
|
|
@@ -225,6 +274,7 @@ class IMAPServer extends node_stream_1.Stream {
|
|
|
225
274
|
INBOX: {},
|
|
226
275
|
'': {}
|
|
227
276
|
});
|
|
277
|
+
this.referenceNamespace = false;
|
|
228
278
|
this.uidvalidityCounter = 0; // highest UIDVALIDITY in use, new mailboxes get a higher one
|
|
229
279
|
// subscribed mailbox names (RFC 3501 section 6.3.6). Names, not mailboxes: a subscription outlives
|
|
230
280
|
// DELETE and stays with the old name on RENAME (RFC 9051 section 6.3.6), see trackSubscription
|
|
@@ -232,10 +282,67 @@ class IMAPServer extends node_stream_1.Stream {
|
|
|
232
282
|
this.folderCache = Object.create(null);
|
|
233
283
|
this.indexFolders(true);
|
|
234
284
|
}
|
|
285
|
+
/**
|
|
286
|
+
* Starts accepting connections
|
|
287
|
+
*
|
|
288
|
+
* @param {Number} [port] Port to listen on, a free port if not set
|
|
289
|
+
* @param {String} [host] Address to listen on, all addresses if not set
|
|
290
|
+
* @return {Promise<Number>} resolves with the port the server listens on
|
|
291
|
+
*/
|
|
292
|
+
async start(port, host) {
|
|
293
|
+
const imapPort = await listenOn(this.server, port, host);
|
|
294
|
+
await Promise.all([this.options.smtp ? this.startSmtp() : null, this.options.rest ? this.startRest() : null]);
|
|
295
|
+
return imapPort;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Starts the REST API of the `rest` option, see src/rest.ts
|
|
299
|
+
*
|
|
300
|
+
* @return {Promise<Number>} resolves with the HTTP port
|
|
301
|
+
*/
|
|
302
|
+
async startRest() {
|
|
303
|
+
const { port, host, token } = this.options.rest || {};
|
|
304
|
+
this.restServer = (0, rest_js_1.createRestServer)(this, { host, token });
|
|
305
|
+
return listenOn(this.restServer, port, host || '127.0.0.1');
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Starts the SMTP server of the `smtp` option. smtp-server is an optional dependency, loaded only here
|
|
309
|
+
*
|
|
310
|
+
* @return {Promise<Number>} resolves with the SMTP port
|
|
311
|
+
*/
|
|
312
|
+
startSmtp() {
|
|
313
|
+
const { port, host } = this.options.smtp || {};
|
|
314
|
+
return this.loadSmtpListener().then(({ startSMTPServer }) => new Promise((resolve, reject) => {
|
|
315
|
+
const smtp = startSMTPServer(port || 0, this, smtpPort => {
|
|
316
|
+
smtp.removeListener('error', reject);
|
|
317
|
+
resolve(smtpPort);
|
|
318
|
+
}, host);
|
|
319
|
+
smtp.once('error', reject);
|
|
320
|
+
this.smtpServer = smtp;
|
|
321
|
+
}));
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Loads the SMTP listener, which needs the smtp-server package
|
|
325
|
+
*
|
|
326
|
+
* @return {Promise<Object>} the smtp-listener module
|
|
327
|
+
*/
|
|
328
|
+
loadSmtpListener() {
|
|
329
|
+
return Promise.resolve().then(() => __importStar(require('./smtp-listener.js'))).catch((err) => {
|
|
330
|
+
throw new Error('The smtp option needs the smtp-server package, install it with: npm install smtp-server (' + err.message + ')');
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Stops the server and closes every connection
|
|
335
|
+
*
|
|
336
|
+
* @return {Promise} resolves when the server is closed
|
|
337
|
+
*/
|
|
338
|
+
stop() {
|
|
339
|
+
return this.control.shutdown({ graceful: false });
|
|
340
|
+
}
|
|
235
341
|
listen(...args) {
|
|
236
342
|
this.server.listen.apply(this.server, args);
|
|
237
343
|
}
|
|
238
344
|
close(callback) {
|
|
345
|
+
this.closeListeners(true);
|
|
239
346
|
this.server.close(callback);
|
|
240
347
|
// close() only completes once all connections are gone
|
|
241
348
|
this.connections.forEach((connection) => {
|
|
@@ -244,6 +351,31 @@ class IMAPServer extends node_stream_1.Stream {
|
|
|
244
351
|
}
|
|
245
352
|
});
|
|
246
353
|
}
|
|
354
|
+
/**
|
|
355
|
+
* Stops the SMTP and REST listeners of `start()`
|
|
356
|
+
*
|
|
357
|
+
* @param {Boolean} force true closes the open REST connections too, otherwise only the idle ones (a request that
|
|
358
|
+
* is answered, like POST /v1/shutdown, finishes first)
|
|
359
|
+
*/
|
|
360
|
+
closeListeners(force) {
|
|
361
|
+
if (this.smtpServer) {
|
|
362
|
+
this.smtpServer.close(() => false);
|
|
363
|
+
this.smtpServer = null;
|
|
364
|
+
}
|
|
365
|
+
if (this.restServer) {
|
|
366
|
+
// event streams never end on their own, a graceful shutdown would wait for them forever
|
|
367
|
+
this.restServer.endStreams?.();
|
|
368
|
+
this.restServer.close();
|
|
369
|
+
// keep-alive connections would hold the server open
|
|
370
|
+
if (force) {
|
|
371
|
+
this.restServer.closeAllConnections?.();
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
this.restServer.closeIdleConnections?.();
|
|
375
|
+
}
|
|
376
|
+
this.restServer = null;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
247
379
|
/**
|
|
248
380
|
* Returns TLS key and certificate. Without credentials in the options the
|
|
249
381
|
* bundled self-signed certificate for localhost is used.
|
|
@@ -363,13 +495,31 @@ class IMAPServer extends node_stream_1.Stream {
|
|
|
363
495
|
*
|
|
364
496
|
* @param {String} type Kind of change
|
|
365
497
|
* @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
|
|
498
|
+
* @param {Object} [details] `{ oldPath, mailbox, created }`: the earlier name of a renamed mailbox, the mailbox
|
|
499
|
+
* object that a DELETE removed, the mailboxes a CREATE made (superior levels first)
|
|
368
500
|
*/
|
|
369
501
|
mailboxChanged(type, path, details) {
|
|
370
502
|
const event = Object.assign({ type, path, oldPath: null, mailbox: null }, details, { origin: this.activeConnection });
|
|
371
503
|
this.emit('mailbox', event);
|
|
372
504
|
}
|
|
505
|
+
/**
|
|
506
|
+
* Runs a change with `origin` as the session that caused it, the `origin` of the notifications and events it
|
|
507
|
+
* makes. Commands run with their session, the control API with null
|
|
508
|
+
*
|
|
509
|
+
* @param {Object|null} origin Session, or null for a change from outside
|
|
510
|
+
* @param {Function} fn Change
|
|
511
|
+
* @return {*} the result of fn
|
|
512
|
+
*/
|
|
513
|
+
withOrigin(origin, fn) {
|
|
514
|
+
const previous = this.activeConnection;
|
|
515
|
+
this.activeConnection = origin;
|
|
516
|
+
try {
|
|
517
|
+
return fn();
|
|
518
|
+
}
|
|
519
|
+
finally {
|
|
520
|
+
this.activeConnection = previous;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
373
523
|
/**
|
|
374
524
|
* Retrieves the handler of an IMAP command
|
|
375
525
|
*
|
|
@@ -393,8 +543,7 @@ class IMAPServer extends node_stream_1.Stream {
|
|
|
393
543
|
let seen = 0;
|
|
394
544
|
let unseen = 0;
|
|
395
545
|
// flags stay defined in the mailbox once a message had them, see rememberFlags
|
|
396
|
-
const permanentFlags =
|
|
397
|
-
(mailbox.knownFlags || []).forEach(flag => this.ensureFlag(permanentFlags, flag));
|
|
546
|
+
const permanentFlags = this.permanentFlagList(mailbox);
|
|
398
547
|
let recent = 0;
|
|
399
548
|
// \Recent sets of the sessions that have this mailbox selected
|
|
400
549
|
const recentSets = [];
|
|
@@ -440,20 +589,8 @@ class IMAPServer extends node_stream_1.Stream {
|
|
|
440
589
|
* @return {Boolean} Returns true if the date string is in IMAP date-time format
|
|
441
590
|
*/
|
|
442
591
|
validateInternalDate(date) {
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
}
|
|
446
|
-
// date-time from RFC 3501 section 9, month names are case-insensitive like all ABNF strings
|
|
447
|
-
const match = date.match(/^( \d|\d\d)-(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-(\d{4}) (\d{2}):(\d{2}):(\d{2}) [-+](\d{2})(\d{2})$/i);
|
|
448
|
-
if (!match) {
|
|
449
|
-
return false;
|
|
450
|
-
}
|
|
451
|
-
// the values must also make a real date and time
|
|
452
|
-
return ((0, dates_js_1.isRealDate)(match[1], (0, dates_js_1.monthIndex)(match[2]), match[3]) &&
|
|
453
|
-
Number(match[4]) < 24 &&
|
|
454
|
-
Number(match[5]) < 60 &&
|
|
455
|
-
Number(match[6]) < 61 &&
|
|
456
|
-
Number(match[8]) < 60);
|
|
592
|
+
// date-time from RFC 3501 section 9
|
|
593
|
+
return (0, dates_js_1.isDateTime)(date);
|
|
457
594
|
}
|
|
458
595
|
/**
|
|
459
596
|
* Converts a date object to a valid date-time string format
|
|
@@ -502,22 +639,22 @@ class IMAPServer extends node_stream_1.Stream {
|
|
|
502
639
|
*/
|
|
503
640
|
createMailbox(path, defaultMailbox) {
|
|
504
641
|
if (!path) {
|
|
505
|
-
throw
|
|
642
|
+
throw (0, store_operations_js_1.storeError)('Invalid mailbox name', 'CANNOT');
|
|
506
643
|
}
|
|
507
644
|
// Ensure case insensitive INBOX
|
|
508
645
|
if (path.toUpperCase() === 'INBOX') {
|
|
509
|
-
throw
|
|
646
|
+
throw (0, store_operations_js_1.storeError)('INBOX can not be modified', 'ALREADYEXISTS');
|
|
510
647
|
}
|
|
511
648
|
const { namespace, storage } = this.getPersonalNamespace(path);
|
|
512
649
|
path = this.stripSeparator(path, storage.separator);
|
|
513
650
|
if (this.folderCache[path] && this.folderCache[path].flags.indexOf('\\Noselect') < 0) {
|
|
514
|
-
throw
|
|
651
|
+
throw (0, store_operations_js_1.storeError)('Mailbox already exists', 'ALREADYEXISTS');
|
|
515
652
|
}
|
|
516
653
|
const folderPath = path.substr(namespace.length).split(storage.separator);
|
|
517
654
|
if (folderPath.some(name => !name)) {
|
|
518
655
|
// an empty hierarchy level ("foo//bar", "/foo", "foo//"). RFC 5530 section 3 has this very case as the
|
|
519
656
|
// example of CANNOT, Dovecot refuses it too
|
|
520
|
-
throw
|
|
657
|
+
throw (0, store_operations_js_1.storeError)('Mailbox names can not have empty hierarchy levels', 'CANNOT');
|
|
521
658
|
}
|
|
522
659
|
let parent = storage;
|
|
523
660
|
let curPath = namespace;
|
|
@@ -528,7 +665,7 @@ class IMAPServer extends node_stream_1.Stream {
|
|
|
528
665
|
curPath += (curPath.length ? storage.separator : '') + folderName;
|
|
529
666
|
let folder = this.getMailbox(curPath) || false;
|
|
530
667
|
if (folder && folder.flags && folder.flags.indexOf('\\Noinferiors') >= 0) {
|
|
531
|
-
throw
|
|
668
|
+
throw (0, store_operations_js_1.storeError)('Can not create subfolders for ' + folder.path, 'CANNOT');
|
|
532
669
|
}
|
|
533
670
|
// a \Noselect placeholder that is created again is replaced with a new mailbox that only keeps
|
|
534
671
|
// the children, nothing else of a deleted mailbox may come back (RFC 3501 section 6.3.3)
|
|
@@ -570,17 +707,17 @@ class IMAPServer extends node_stream_1.Stream {
|
|
|
570
707
|
deleteMailbox(path, keepContents) {
|
|
571
708
|
// Ensure case insensitive INBOX
|
|
572
709
|
if (path.toUpperCase() === 'INBOX') {
|
|
573
|
-
throw
|
|
710
|
+
throw (0, store_operations_js_1.storeError)('INBOX can not be modified', 'CANNOT');
|
|
574
711
|
}
|
|
575
712
|
const { namespace, storage } = this.getPersonalNamespace(path);
|
|
576
713
|
const mailbox = this.folderCache[this.stripSeparator(path, storage.separator)];
|
|
577
714
|
if (!mailbox) {
|
|
578
|
-
throw
|
|
715
|
+
throw (0, store_operations_js_1.storeError)('Mailbox does not exist', 'NONEXISTENT');
|
|
579
716
|
}
|
|
580
717
|
if (mailbox.flags.indexOf('\\Noselect') >= 0 && Object.keys(mailbox.folders || {}).length) {
|
|
581
718
|
// RFC 9051 section 6.3.5: deleting a \Noselect name that has inferior names is an error, the RFC 5530
|
|
582
719
|
// section 3 HASCHILDREN response code tells the client to delete the children first
|
|
583
|
-
throw
|
|
720
|
+
throw (0, store_operations_js_1.storeError)('Mailbox has children, delete them first', 'HASCHILDREN');
|
|
584
721
|
}
|
|
585
722
|
const levels = mailbox.path.split(storage.separator);
|
|
586
723
|
const folderName = levels.pop();
|
|
@@ -630,7 +767,7 @@ class IMAPServer extends node_stream_1.Stream {
|
|
|
630
767
|
const prefix = key.length ? key.substr(0, key.length - this.storage[key].separator.length) : key;
|
|
631
768
|
if (key.length && (path === prefix || path.substr(0, key.length) === key)) {
|
|
632
769
|
if (path === prefix) {
|
|
633
|
-
throw
|
|
770
|
+
throw (0, store_operations_js_1.storeError)('Used mailbox name is a namespace value', 'CANNOT');
|
|
634
771
|
}
|
|
635
772
|
namespace = key;
|
|
636
773
|
}
|
|
@@ -640,10 +777,10 @@ class IMAPServer extends node_stream_1.Stream {
|
|
|
640
777
|
});
|
|
641
778
|
const storage = this.storage[namespace];
|
|
642
779
|
if (!storage) {
|
|
643
|
-
throw
|
|
780
|
+
throw (0, store_operations_js_1.storeError)('Unknown namespace', 'CANNOT');
|
|
644
781
|
}
|
|
645
782
|
if (storage.type !== 'personal') {
|
|
646
|
-
throw
|
|
783
|
+
throw (0, store_operations_js_1.storeError)('Permission denied', 'NOPERM');
|
|
647
784
|
}
|
|
648
785
|
return { namespace, storage };
|
|
649
786
|
}
|
|
@@ -884,6 +1021,43 @@ class IMAPServer extends node_stream_1.Stream {
|
|
|
884
1021
|
const knownFlags = (mailbox.knownFlags = mailbox.knownFlags || []);
|
|
885
1022
|
flags.forEach(flag => this.ensureFlag(knownFlags, flag));
|
|
886
1023
|
}
|
|
1024
|
+
/**
|
|
1025
|
+
* Checks if a flag can be stored on messages of a mailbox: any flag when the mailbox allows new keywords
|
|
1026
|
+
* (PERMANENTFLAGS has `\*`), otherwise only a flag in the PERMANENTFLAGS list that getStatus() builds, the
|
|
1027
|
+
* `permanentFlags` of the mailbox and every flag its messages had. RFC 3501 section 7.1 (RFC 9051 section 7.1):
|
|
1028
|
+
* PERMANENTFLAGS "indicates which of the known flags the client can change permanently", STORE ignores the others
|
|
1029
|
+
*
|
|
1030
|
+
* @param {Object} mailbox Mailbox object
|
|
1031
|
+
* @param {String} flag Normalized flag
|
|
1032
|
+
* @return {Boolean} true if the flag is a permanent flag of the mailbox
|
|
1033
|
+
*/
|
|
1034
|
+
isPermanentFlag(mailbox, flag) {
|
|
1035
|
+
// the lists of permanentFlagList(), without building it: this runs for every flag STORE and APPEND set
|
|
1036
|
+
return (mailbox.allowPermanentFlags || (mailbox.permanentFlags || []).indexOf(flag) >= 0 || (!!mailbox.knownFlags && mailbox.knownFlags.indexOf(flag) >= 0));
|
|
1037
|
+
}
|
|
1038
|
+
/**
|
|
1039
|
+
* The flags of the PERMANENTFLAGS list (without `\*`): the `permanentFlags` of the mailbox and every flag its
|
|
1040
|
+
* messages have or had (`knownFlags`, see rememberFlags)
|
|
1041
|
+
*
|
|
1042
|
+
* @param {Object} mailbox Mailbox object
|
|
1043
|
+
* @return {Array} flags, a new list
|
|
1044
|
+
*/
|
|
1045
|
+
permanentFlagList(mailbox) {
|
|
1046
|
+
const list = [].concat(mailbox.permanentFlags || []);
|
|
1047
|
+
(mailbox.knownFlags || []).forEach(flag => this.ensureFlag(list, flag));
|
|
1048
|
+
return list;
|
|
1049
|
+
}
|
|
1050
|
+
/**
|
|
1051
|
+
* The current time for dates the server sets itself (INTERNALDATE of a message without one, SAVEDATE). The `now`
|
|
1052
|
+
* option fixes it for repeatable tests: a Date, a timestamp, or a function that returns one
|
|
1053
|
+
*
|
|
1054
|
+
* @return {Date} current time
|
|
1055
|
+
*/
|
|
1056
|
+
now() {
|
|
1057
|
+
const now = this.options.now;
|
|
1058
|
+
const value = typeof now === 'function' ? now() : now;
|
|
1059
|
+
return value === undefined || value === null ? new Date() : new Date(value);
|
|
1060
|
+
}
|
|
887
1061
|
normalizeDateTime(value) {
|
|
888
1062
|
if (value instanceof Date) {
|
|
889
1063
|
return this.formatInternalDate(value);
|
|
@@ -902,7 +1076,7 @@ class IMAPServer extends node_stream_1.Stream {
|
|
|
902
1076
|
* @param mailbox Mailbox of the message
|
|
903
1077
|
*/
|
|
904
1078
|
processMessage(message, mailbox) {
|
|
905
|
-
message.internaldate = this.normalizeDateTime(message.internaldate ||
|
|
1079
|
+
message.internaldate = this.normalizeDateTime(message.internaldate || this.now());
|
|
906
1080
|
message.flags = [].concat(message.flags || []);
|
|
907
1081
|
if (message.flags.indexOf('\\Recent') >= 0) {
|
|
908
1082
|
// \Recent is not a stored flag, it belongs to the first session that selects the mailbox
|
|
@@ -944,9 +1118,10 @@ class IMAPServer extends node_stream_1.Stream {
|
|
|
944
1118
|
*/
|
|
945
1119
|
appendMessage(path, flags, internaldate, raw, ignoreConnection, properties) {
|
|
946
1120
|
const mailbox = typeof path === 'string' ? this.getMailbox(path) : path;
|
|
947
|
-
// processMessage() below sets the UID
|
|
1121
|
+
// processMessage() below sets the UID. Flags the mailbox can not store are left out (APPEND, COPY: the flags
|
|
1122
|
+
// SHOULD be set, RFC 3501 sections 6.3.11 and 6.4.7), as STORE ignores them (RFC 3501 section 7.1, PERMANENTFLAGS)
|
|
948
1123
|
const message = Object.assign({}, properties, {
|
|
949
|
-
flags: flags,
|
|
1124
|
+
flags: flags.filter(flag => this.isPermanentFlag(mailbox, flag)),
|
|
950
1125
|
internaldate: internaldate,
|
|
951
1126
|
raw: raw,
|
|
952
1127
|
recent: true
|
|
@@ -1132,49 +1307,48 @@ class IMAPServer extends node_stream_1.Stream {
|
|
|
1132
1307
|
return flags;
|
|
1133
1308
|
}
|
|
1134
1309
|
matchFolders(referenceName, match, exportName, folders) {
|
|
1135
|
-
let includeINBOX = false;
|
|
1136
1310
|
const source = folders || this.folderCache;
|
|
1137
1311
|
const toName = exportName || ((name) => name);
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
//
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
const name = toName(key);
|
|
1148
|
-
if (key !== 'INBOX' && reference.substr(0, name.length) === name && (nsKey === false || name.length > nsName.length)) {
|
|
1149
|
-
nsKey = key;
|
|
1150
|
-
nsName = name;
|
|
1151
|
-
}
|
|
1152
|
-
}
|
|
1153
|
-
if (nsKey === false) {
|
|
1154
|
-
return [];
|
|
1155
|
-
}
|
|
1156
|
-
const namespace = this.storage[nsKey];
|
|
1157
|
-
const lookup = reference + match;
|
|
1158
|
-
const result = [];
|
|
1159
|
-
const pattern = '^' +
|
|
1312
|
+
// RFC 3501 section 6.3.8 (RFC 9051 section 6.3.9): "An empty ("" string) reference name argument
|
|
1313
|
+
// indicates that the mailbox name is interpreted as by SELECT", and without break out characters
|
|
1314
|
+
// "the canonical form is normally the reference name appended with the mailbox name". The pattern
|
|
1315
|
+
// matches full mailbox names, also in a personal namespace with a prefix such as "INBOX.". With the
|
|
1316
|
+
// namespace convention "#" is a break out character "and must be treated as such" (RFC 3501 section 6.3.8,
|
|
1317
|
+
// RFC 9051 section 6.3.9): a pattern that starts with it is a name of its own, the reference is ignored
|
|
1318
|
+
const lookup = match.charAt(0) === '#' ? match : (referenceName || '') + match;
|
|
1319
|
+
// "%" does not match the hierarchy delimiter, which is the one of the namespace a name belongs to
|
|
1320
|
+
const toRegExp = (separator, flags = '') => new RegExp('^' +
|
|
1160
1321
|
lookup
|
|
1161
1322
|
// escape regex symbols
|
|
1162
1323
|
.replace(/([\\^$+?!.():=[\]{}|,-])/g, '\\$1')
|
|
1163
1324
|
.replace(/[*]/g, '.*')
|
|
1164
|
-
.replace(/[%]/g, '[^' +
|
|
1165
|
-
'$';
|
|
1166
|
-
|
|
1167
|
-
//
|
|
1168
|
-
|
|
1325
|
+
.replace(/[%]/g, '[^' + separator.replace(/([\\^$+*?!.():=[\]{}|,-])/g, '\\$1') + ']*') +
|
|
1326
|
+
'$', flags);
|
|
1327
|
+
// RFC 3501 section 6.3.8 allows to "hide" otherwise accessible mailboxes from the wildcards: the
|
|
1328
|
+
// mailboxes of namespaces other than the personal one are only matched when the pattern names
|
|
1329
|
+
// the prefix of the namespace before any wildcard (LIST "" "user.%", LIST "#news." "*"). The pattern
|
|
1330
|
+
// of every namespace that is matched, null for a hidden one
|
|
1331
|
+
const fixedPrefix = lookup.replace(/[*%].*$/, '');
|
|
1332
|
+
const queries = new Map();
|
|
1333
|
+
Object.keys(this.storage).forEach(key => {
|
|
1334
|
+
const name = toName(key);
|
|
1335
|
+
const visible = key === this.referenceNamespace || fixedPrefix.substr(0, name.length) === name;
|
|
1336
|
+
queries.set(key, visible ? toRegExp(this.storage[key].separator) : null);
|
|
1337
|
+
});
|
|
1338
|
+
const result = [];
|
|
1339
|
+
// "The special name INBOX is included in the output from LIST, if [...] the uppercase string "INBOX"
|
|
1340
|
+
// matches the interpreted reference and mailbox name arguments", INBOX is case-insensitive
|
|
1341
|
+
if (source.INBOX && toRegExp((this.storage.INBOX && this.storage.INBOX.separator) || '/', 'i').test('INBOX')) {
|
|
1169
1342
|
result.push(source.INBOX);
|
|
1170
1343
|
}
|
|
1171
1344
|
Object.keys(source).forEach(path => {
|
|
1172
1345
|
const folder = source[path];
|
|
1173
|
-
|
|
1346
|
+
const query = path !== 'INBOX' && folder.namespace !== false && folder.namespace !== 'INBOX' ? queries.get(folder.namespace) : null;
|
|
1347
|
+
if (!query) {
|
|
1174
1348
|
return;
|
|
1175
1349
|
}
|
|
1176
1350
|
const name = toName(path);
|
|
1177
|
-
if (
|
|
1351
|
+
if (query.test(name) && (folder.flags.indexOf('\\NonExistent') < 0 || name === lookup)) {
|
|
1178
1352
|
result.push(folder);
|
|
1179
1353
|
}
|
|
1180
1354
|
});
|
|
@@ -1293,6 +1467,7 @@ class IMAPConnection {
|
|
|
1293
1467
|
this.notificationQueue = [];
|
|
1294
1468
|
this.server.on('notify', this._notificationCallback);
|
|
1295
1469
|
this.server.connections.add(this);
|
|
1470
|
+
this.emitSession('open');
|
|
1296
1471
|
this.scriptOutput('greeting', '* OK ImapKit ready for rumble\r\n', {});
|
|
1297
1472
|
}
|
|
1298
1473
|
/**
|
|
@@ -1327,7 +1502,7 @@ class IMAPConnection {
|
|
|
1327
1502
|
}
|
|
1328
1503
|
this._outputQueue = this._outputQueue || [];
|
|
1329
1504
|
this._outputQueue.push(Object.assign({}, operation, { transport: this.transport }));
|
|
1330
|
-
if (!this._outputTimer) {
|
|
1505
|
+
if (!this._outputTimer && !this._outputWaiting) {
|
|
1331
1506
|
this.flushOutput();
|
|
1332
1507
|
}
|
|
1333
1508
|
}
|
|
@@ -1348,10 +1523,28 @@ class IMAPConnection {
|
|
|
1348
1523
|
return;
|
|
1349
1524
|
}
|
|
1350
1525
|
if (operation.data && operation.chunk && operation.data.length > operation.chunk) {
|
|
1351
|
-
// the rest waits for chunkDelay, like a delay of its own
|
|
1352
|
-
|
|
1526
|
+
// the rest waits for chunkDelay, like a delay of its own. Without a delay each piece waits for the next
|
|
1527
|
+
// event loop turn, and Nagle's algorithm must not hold it back to merge it with the next one
|
|
1528
|
+
const piece = operation.data.subarray(0, operation.chunk);
|
|
1353
1529
|
operation.data = operation.data.subarray(operation.chunk);
|
|
1530
|
+
if (!operation.chunkDelay || operation.chunkDelay === 'tick') {
|
|
1531
|
+
// the next piece waits until this one was handed to the system, and one more event loop turn, so
|
|
1532
|
+
// the pieces leave as separate segments without a wall clock delay (#89)
|
|
1533
|
+
this.socket?.setNoDelay?.(true);
|
|
1534
|
+
this._outputWaiting = true;
|
|
1535
|
+
this.writeLayer(piece, operation.transport || null, () => {
|
|
1536
|
+
if (this._outputWaiting && this._outputQueue === queue) {
|
|
1537
|
+
this._outputTimer = nextTurn(() => {
|
|
1538
|
+
this._outputTimer = null;
|
|
1539
|
+
this._outputWaiting = false;
|
|
1540
|
+
this.flushOutput();
|
|
1541
|
+
});
|
|
1542
|
+
}
|
|
1543
|
+
});
|
|
1544
|
+
return;
|
|
1545
|
+
}
|
|
1354
1546
|
operation.delay = operation.chunkDelay;
|
|
1547
|
+
this.writeLayer(piece, operation.transport || null);
|
|
1355
1548
|
continue;
|
|
1356
1549
|
}
|
|
1357
1550
|
if (operation.data) {
|
|
@@ -1371,8 +1564,10 @@ class IMAPConnection {
|
|
|
1371
1564
|
clearOutputQueue() {
|
|
1372
1565
|
if (this._outputTimer) {
|
|
1373
1566
|
clearTimeout(this._outputTimer);
|
|
1567
|
+
clearImmediate(this._outputTimer);
|
|
1374
1568
|
this._outputTimer = null;
|
|
1375
1569
|
}
|
|
1570
|
+
this._outputWaiting = false;
|
|
1376
1571
|
this._outputQueue = null;
|
|
1377
1572
|
this._deferredOutput = null;
|
|
1378
1573
|
}
|
|
@@ -1381,13 +1576,17 @@ class IMAPConnection {
|
|
|
1381
1576
|
*
|
|
1382
1577
|
* @param {Buffer} data Output
|
|
1383
1578
|
* @param {Object|null} transport Transport layer
|
|
1579
|
+
* @param {Function} [callback] Called once the data was handed to the system (a transport: on the next turn)
|
|
1384
1580
|
*/
|
|
1385
|
-
writeLayer(data, transport) {
|
|
1581
|
+
writeLayer(data, transport, callback) {
|
|
1386
1582
|
if (transport) {
|
|
1387
1583
|
transport.write(data);
|
|
1584
|
+
if (callback) {
|
|
1585
|
+
setImmediate(callback);
|
|
1586
|
+
}
|
|
1388
1587
|
}
|
|
1389
1588
|
else {
|
|
1390
|
-
this.writeRaw(data);
|
|
1589
|
+
this.writeRaw(data, callback);
|
|
1391
1590
|
}
|
|
1392
1591
|
}
|
|
1393
1592
|
/**
|
|
@@ -1441,17 +1640,70 @@ class IMAPConnection {
|
|
|
1441
1640
|
*
|
|
1442
1641
|
* @param {Buffer} data Data to send
|
|
1443
1642
|
*/
|
|
1444
|
-
writeRaw(data) {
|
|
1643
|
+
writeRaw(data, callback) {
|
|
1445
1644
|
if (this.socket && !this.socket.destroyed) {
|
|
1446
|
-
this.socket.write(data);
|
|
1645
|
+
this.socket.write(data, callback);
|
|
1646
|
+
this.touch();
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
/**
|
|
1650
|
+
* Notes input or output for the quiet events of script rules (#88)
|
|
1651
|
+
*/
|
|
1652
|
+
touch() {
|
|
1653
|
+
if (!this.server.script.watches('quiet')) {
|
|
1654
|
+
// nothing waits for quiet sessions, a rule added later counts from then on
|
|
1655
|
+
this._lastActivity = undefined;
|
|
1656
|
+
return;
|
|
1657
|
+
}
|
|
1658
|
+
this._lastActivity = Date.now();
|
|
1659
|
+
if (!this._quietTimer) {
|
|
1660
|
+
this.watchQuiet();
|
|
1447
1661
|
}
|
|
1448
1662
|
}
|
|
1663
|
+
/**
|
|
1664
|
+
* Starts waiting for the next quiet event, if a rule waits for one and the session is not waiting already. When
|
|
1665
|
+
* the time is up, the first rule that matches handles the event (`send`, `close`), the output it sends starts
|
|
1666
|
+
* the next quiet time
|
|
1667
|
+
*
|
|
1668
|
+
* @param {Number} [checked] Quiet time the rules were checked for already, only longer ones are waited for
|
|
1669
|
+
*/
|
|
1670
|
+
watchQuiet(checked = 0) {
|
|
1671
|
+
if (this._quietTimer || !this.socket || this._closing) {
|
|
1672
|
+
return;
|
|
1673
|
+
}
|
|
1674
|
+
const last = this._lastActivity || Date.now();
|
|
1675
|
+
this._lastActivity = last;
|
|
1676
|
+
// the time is measured from the last activity, a rule whose time is up already fires right away
|
|
1677
|
+
const next = this.server.script.nextQuiet(checked);
|
|
1678
|
+
if (next === null) {
|
|
1679
|
+
return;
|
|
1680
|
+
}
|
|
1681
|
+
this._quietTimer = setTimeout(() => {
|
|
1682
|
+
this._quietTimer = null;
|
|
1683
|
+
if (!this.isOpen() || this._lastActivity !== last) {
|
|
1684
|
+
// there was activity in between, it started a new wait
|
|
1685
|
+
return this.watchQuiet();
|
|
1686
|
+
}
|
|
1687
|
+
// a timer can fire a millisecond before Date.now() says the time is up
|
|
1688
|
+
const quiet = Math.max(Date.now() - last, next);
|
|
1689
|
+
const found = this.server.script.check(this, 'quiet', { data: '', quiet });
|
|
1690
|
+
if (found) {
|
|
1691
|
+
(0, script_js_1.handleQuiet)(this, found.rule, found.context);
|
|
1692
|
+
}
|
|
1693
|
+
if (this._lastActivity === last) {
|
|
1694
|
+
// nothing was sent, wait for the rules with a longer quiet time
|
|
1695
|
+
this.watchQuiet(quiet);
|
|
1696
|
+
}
|
|
1697
|
+
}, Math.max(0, next - (Date.now() - last)));
|
|
1698
|
+
this._quietTimer.unref();
|
|
1699
|
+
}
|
|
1449
1700
|
/**
|
|
1450
1701
|
* Handles data from the socket, through the transport layer if there is one
|
|
1451
1702
|
*
|
|
1452
1703
|
* @param {Buffer} chunk Received data
|
|
1453
1704
|
*/
|
|
1454
1705
|
receive(chunk) {
|
|
1706
|
+
this.touch();
|
|
1455
1707
|
if (this.transport) {
|
|
1456
1708
|
this.transport.receive(chunk);
|
|
1457
1709
|
}
|
|
@@ -1584,11 +1836,15 @@ class IMAPConnection {
|
|
|
1584
1836
|
* notifications that were not sent yet are dropped. Sends nothing, the caller answers the command
|
|
1585
1837
|
*/
|
|
1586
1838
|
closeMailbox() {
|
|
1839
|
+
const wasSelected = !!this.selectedMailbox;
|
|
1587
1840
|
this.state = 'Authenticated';
|
|
1588
1841
|
this.selectedMailbox = false;
|
|
1589
1842
|
this.readOnly = false;
|
|
1590
1843
|
this.recent = null;
|
|
1591
1844
|
this.notificationQueue = [];
|
|
1845
|
+
if (wasSelected) {
|
|
1846
|
+
this.emitSession('unselect');
|
|
1847
|
+
}
|
|
1592
1848
|
}
|
|
1593
1849
|
onClose() {
|
|
1594
1850
|
if (this.socket) {
|
|
@@ -1600,8 +1856,14 @@ class IMAPConnection {
|
|
|
1600
1856
|
this.transport = null;
|
|
1601
1857
|
}
|
|
1602
1858
|
this.clearOutputQueue();
|
|
1859
|
+
if (this._quietTimer) {
|
|
1860
|
+
clearTimeout(this._quietTimer);
|
|
1861
|
+
this._quietTimer = null;
|
|
1862
|
+
}
|
|
1603
1863
|
this.server.removeListener('notify', this._notificationCallback);
|
|
1604
|
-
this.server.connections.delete(this)
|
|
1864
|
+
if (this.server.connections.delete(this)) {
|
|
1865
|
+
this.emitSession('close');
|
|
1866
|
+
}
|
|
1605
1867
|
}
|
|
1606
1868
|
onError(err) {
|
|
1607
1869
|
if (this.options.debug) {
|
|
@@ -1944,7 +2206,7 @@ class IMAPConnection {
|
|
|
1944
2206
|
* @return {Boolean} true if an EXPUNGE response is pending
|
|
1945
2207
|
*/
|
|
1946
2208
|
hasPendingExpunge() {
|
|
1947
|
-
return this.notificationQueue.some(
|
|
2209
|
+
return this.notificationQueue.some(isPendingExpunge);
|
|
1948
2210
|
}
|
|
1949
2211
|
/**
|
|
1950
2212
|
* Tells the other sessions that have the selected mailbox open about changed flags, they get
|
|
@@ -1953,8 +2215,8 @@ class IMAPConnection {
|
|
|
1953
2215
|
* @param {Array} messages Messages with changed flags
|
|
1954
2216
|
*/
|
|
1955
2217
|
notifyFlagChanges(messages) {
|
|
1956
|
-
if (
|
|
1957
|
-
this.server
|
|
2218
|
+
if (this.selectedMailbox) {
|
|
2219
|
+
(0, store_operations_js_1.notifyFlagChanges)(this.server, this.selectedMailbox, messages, this);
|
|
1958
2220
|
}
|
|
1959
2221
|
}
|
|
1960
2222
|
/**
|
|
@@ -2151,22 +2413,52 @@ class IMAPConnection {
|
|
|
2151
2413
|
prepareNotifications(queue) {
|
|
2152
2414
|
return queue;
|
|
2153
2415
|
}
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2416
|
+
/**
|
|
2417
|
+
* Checks if EXPUNGE responses must wait while a command runs: during FETCH, STORE and SEARCH (RFC 3501 section
|
|
2418
|
+
* 7.4.1), during the commands that extensions add to this list (see the noExpunge command option), and during UID
|
|
2419
|
+
* SEARCH with message numbers in the search criteria (RFC 7162 section 3.2.10.2 for VANISHED, EXPUNGE may wait as
|
|
2420
|
+
* well, RFC 3501 only allows it during UID commands)
|
|
2421
|
+
*
|
|
2422
|
+
* @param {Object} data Parsed command
|
|
2423
|
+
* @return {Boolean} true if the command holds back EXPUNGE responses
|
|
2424
|
+
*/
|
|
2425
|
+
holdsExpunge(data) {
|
|
2426
|
+
const options = this.server.getCommandOptions(data.command);
|
|
2427
|
+
return options.noExpunge || (options.searchCriteria !== false && this.usesSequenceNumbers(data));
|
|
2428
|
+
}
|
|
2429
|
+
/**
|
|
2430
|
+
* Sends the queued notifications. During a command that holds back EXPUNGE responses (see holdsExpunge), or with
|
|
2431
|
+
* `beforeCommand` before a command that refers to messages by sequence number, only the notifications queued before
|
|
2432
|
+
* the first pending EXPUNGE go out: new messages (EXISTS) and flag changes. RFC 3501 section 5.2: "A server MUST send
|
|
2433
|
+
* mailbox size updates automatically if a mailbox size change is observed during the processing of a command",
|
|
2434
|
+
* section 7.4.1 forbids only EXPUNGE during FETCH, STORE and SEARCH. What was queued after the EXPUNGE waits with it,
|
|
2435
|
+
* an EXISTS sent before it would describe a list that the client can not know yet
|
|
2436
|
+
*
|
|
2437
|
+
* @param {Object} [data] Parsed command that runs, or null between commands
|
|
2438
|
+
* @param {Boolean} [beforeCommand] true when the command has not run yet, then a command with message sequence
|
|
2439
|
+
* numbers (COPY, MOVE) also waits with the EXPUNGE responses, its numbers refer to the messages before them
|
|
2440
|
+
*/
|
|
2441
|
+
processNotifications(data, beforeCommand) {
|
|
2163
2442
|
if (!this.notificationQueue.length) {
|
|
2164
2443
|
return;
|
|
2165
2444
|
}
|
|
2166
|
-
|
|
2167
|
-
this.notificationQueue = [];
|
|
2445
|
+
let queue = this.notificationQueue;
|
|
2168
2446
|
// Flag updates use the sequence numbers this session knows: before the EXPUNGE responses of
|
|
2169
2447
|
// the snapshot are sent, the snapshot, afterwards the current message list
|
|
2448
|
+
const snapshot = queue.find(notification => notification.mailboxCopy);
|
|
2449
|
+
let held = [];
|
|
2450
|
+
if (data && (this.holdsExpunge(data) || (beforeCommand && this.usesSequenceNumbers(data)))) {
|
|
2451
|
+
const first = queue.findIndex(isPendingExpunge);
|
|
2452
|
+
if (first >= 0) {
|
|
2453
|
+
held = queue.slice(first);
|
|
2454
|
+
queue = queue.slice(0, first);
|
|
2455
|
+
}
|
|
2456
|
+
if (!queue.length) {
|
|
2457
|
+
return;
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
2460
|
+
this.notificationQueue = held;
|
|
2461
|
+
queue = this.prepareNotifications(queue);
|
|
2170
2462
|
const snapshotIndex = queue.findIndex(notification => notification.mailboxCopy);
|
|
2171
2463
|
const sequenceMaps = new Map();
|
|
2172
2464
|
const getSequence = (messages) => {
|
|
@@ -2180,15 +2472,57 @@ class IMAPConnection {
|
|
|
2180
2472
|
const current = this.selectedMailbox ? this.selectedMailbox.messages : [];
|
|
2181
2473
|
// a message changed several times is reported once, its FETCH response carries the current flags
|
|
2182
2474
|
const reported = new Set();
|
|
2475
|
+
// before the snapshot (or with all of it still held back, then the split at the first pending expunge leaves
|
|
2476
|
+
// no snapshot in the queue) the session knows the old list
|
|
2477
|
+
const sessionList = (i) => (snapshot && (snapshotIndex < 0 || i < snapshotIndex) ? snapshot.mailboxCopy : current);
|
|
2478
|
+
// the last EXISTS response of a run of EXISTS responses, and if the run announces new messages
|
|
2479
|
+
let lastExists = -1;
|
|
2480
|
+
let newMessages = false;
|
|
2183
2481
|
queue.forEach((notification, i) => {
|
|
2184
2482
|
if (notification.flagUpdate) {
|
|
2185
|
-
|
|
2186
|
-
this.sendFlagUpdate(notification.flagUpdate, getSequence(i < snapshotIndex ? queue[snapshotIndex].mailboxCopy : current), reported);
|
|
2483
|
+
this.sendFlagUpdate(notification.flagUpdate, getSequence(sessionList(i)), reported);
|
|
2187
2484
|
}
|
|
2188
2485
|
else {
|
|
2189
2486
|
this.send(notification);
|
|
2190
2487
|
}
|
|
2488
|
+
if (isExists(notification)) {
|
|
2489
|
+
lastExists = i;
|
|
2490
|
+
// the EXISTS after the EXPUNGE responses of another session carries the snapshot instead of a message
|
|
2491
|
+
newMessages = newMessages || !!notification.message;
|
|
2492
|
+
}
|
|
2493
|
+
const next = queue[i + 1];
|
|
2494
|
+
if (lastExists < 0 || isExists(next) || (next && next.fetchedMessage)) {
|
|
2495
|
+
// not the end of a run of EXISTS responses (and the FETCH responses NOTIFY sends after them)
|
|
2496
|
+
return;
|
|
2497
|
+
}
|
|
2498
|
+
// RFC 3501 section 7.3.2: the RECENT response "occurs as a result of a SELECT or EXAMINE command, and if the
|
|
2499
|
+
// size of the mailbox changes (e.g., new messages)". One for consecutive EXISTS responses that announce new
|
|
2500
|
+
// messages, with the number of \Recent messages among those the client was told about. After expunges the
|
|
2501
|
+
// EXPUNGE responses report the change (RFC 3501 section 7.4.1). It goes after the FETCH responses that NOTIFY
|
|
2502
|
+
// sends for new messages, RFC 5465 section 5.2: "an unsolicited EXISTS response, followed by an unsolicited
|
|
2503
|
+
// FETCH response [...] The server MAY also send a RECENT response"
|
|
2504
|
+
if (newMessages) {
|
|
2505
|
+
this.sendRecent(Number(queue[lastExists].attributes[0]), getSequence(sessionList(lastExists)));
|
|
2506
|
+
}
|
|
2507
|
+
lastExists = -1;
|
|
2508
|
+
newMessages = false;
|
|
2509
|
+
});
|
|
2510
|
+
}
|
|
2511
|
+
/**
|
|
2512
|
+
* Sends an untagged RECENT response with the number of \Recent messages among the first `count` messages
|
|
2513
|
+
*
|
|
2514
|
+
* @param {Number} count Number of messages the client was told about
|
|
2515
|
+
* @param {Map} sequence Message to the sequence number this session knows it by
|
|
2516
|
+
*/
|
|
2517
|
+
sendRecent(count, sequence) {
|
|
2518
|
+
let recent = 0;
|
|
2519
|
+
(this.recent || new Set()).forEach(message => {
|
|
2520
|
+
const seq = sequence.get(message);
|
|
2521
|
+
if (seq && seq <= count) {
|
|
2522
|
+
recent++;
|
|
2523
|
+
}
|
|
2191
2524
|
});
|
|
2525
|
+
this.send({ tag: '*', notification: true, attributes: [recent, { type: 'ATOM', value: 'RECENT' }] }, 'RECENT NOTIFICATION');
|
|
2192
2526
|
}
|
|
2193
2527
|
/**
|
|
2194
2528
|
* Compile a command object to a response string and write it to socket.
|
|
@@ -2210,7 +2544,7 @@ class IMAPConnection {
|
|
|
2210
2544
|
*/
|
|
2211
2545
|
send(response, description, parsed, data, ...extra) {
|
|
2212
2546
|
// nothing goes out once the connection is closing (RFC 3501 section 7.1.5)
|
|
2213
|
-
if (!this.
|
|
2547
|
+
if (!this.isOpen()) {
|
|
2214
2548
|
return;
|
|
2215
2549
|
}
|
|
2216
2550
|
if (!response.notification && response.tag !== '*') {
|
|
@@ -2220,6 +2554,9 @@ class IMAPConnection {
|
|
|
2220
2554
|
this.server.outputHandlers.forEach(handler => {
|
|
2221
2555
|
handler(this, response, description, parsed, data, ...extra);
|
|
2222
2556
|
});
|
|
2557
|
+
if (this._commandStart && parsed && response.tag === this._commandStart.tag && parsed.tag === response.tag) {
|
|
2558
|
+
this.commandCompleted(String(response.command || '').toUpperCase());
|
|
2559
|
+
}
|
|
2223
2560
|
// No need to display this response to user
|
|
2224
2561
|
if (response.skipResponse) {
|
|
2225
2562
|
return;
|
|
@@ -2229,10 +2566,11 @@ class IMAPConnection {
|
|
|
2229
2566
|
response.tag === parsed.tag &&
|
|
2230
2567
|
response.command === 'OK' &&
|
|
2231
2568
|
this.hasPendingExpunge() &&
|
|
2232
|
-
this.
|
|
2569
|
+
this.holdsExpunge(parsed) &&
|
|
2233
2570
|
!(Array.isArray(response.attributes) && response.attributes.some(attr => attr && attr.type === 'SECTION'))) {
|
|
2234
2571
|
// After the output handlers, they might add a response code of their own (MODIFIED of CONDSTORE).
|
|
2235
|
-
// FETCH, STORE, SEARCH and the like can not report the EXPUNGE of another session (RFC 3501 section 7.4.1),
|
|
2572
|
+
// FETCH, STORE, SEARCH and the like can not report the EXPUNGE of another session (RFC 3501 section 7.4.1), nor
|
|
2573
|
+
// can UID SEARCH with message numbers in the criteria (see holdsExpunge),
|
|
2236
2574
|
// EXPUNGEISSUED tells the client to issue NOOP soon (RFC 5530 section 3, RFC 9051 section 7.1)
|
|
2237
2575
|
response.attributes = [{ type: 'SECTION', section: [{ type: 'ATOM', value: 'EXPUNGEISSUED' }] }].concat(response.attributes || []);
|
|
2238
2576
|
}
|
|
@@ -2376,6 +2714,15 @@ class IMAPConnection {
|
|
|
2376
2714
|
* @return {Boolean} true if the command uses message sequence numbers
|
|
2377
2715
|
*/
|
|
2378
2716
|
usesSequenceNumbers(parsed) {
|
|
2717
|
+
// the notification checks ask several times per command, the answer does not change
|
|
2718
|
+
let cached = sequenceNumberUse.get(parsed);
|
|
2719
|
+
if (cached === undefined) {
|
|
2720
|
+
cached = this.findSequenceNumbers(parsed);
|
|
2721
|
+
sequenceNumberUse.set(parsed, cached);
|
|
2722
|
+
}
|
|
2723
|
+
return cached;
|
|
2724
|
+
}
|
|
2725
|
+
findSequenceNumbers(parsed) {
|
|
2379
2726
|
const { sequenceSet, searchCriteria } = this.server.getCommandOptions(parsed.command);
|
|
2380
2727
|
if (sequenceSet !== false) {
|
|
2381
2728
|
// other forms of sequence sets, like "$" of SEARCHRES (RFC 5182 section 2.3), do not use numbers
|
|
@@ -2554,12 +2901,14 @@ class IMAPConnection {
|
|
|
2554
2901
|
this.refusePipelined(parsed, data);
|
|
2555
2902
|
return;
|
|
2556
2903
|
}
|
|
2557
|
-
|
|
2904
|
+
// an unknown SASL mechanism is answered by its own handler, after the central checks (state, script rules)
|
|
2905
|
+
const fallback = /^AUTHENTICATE /i.test(parsed.command) && !this.server.getCommandHandler(parsed.command) ? unsupportedMechanism : undefined;
|
|
2906
|
+
if (fallback || this.server.getCommandHandler(parsed.command)) {
|
|
2558
2907
|
if (this.isAmbiguous(parsed)) {
|
|
2559
2908
|
this.sendStatus(parsed, data, 'BAD', 'Commands with message sequence numbers must wait for the completion of earlier commands');
|
|
2560
2909
|
return;
|
|
2561
2910
|
}
|
|
2562
|
-
const element = { parsed, data };
|
|
2911
|
+
const element = { parsed, data, handler: fallback };
|
|
2563
2912
|
if (scripted) {
|
|
2564
2913
|
// processQueue runs it once the script rule released the queue
|
|
2565
2914
|
this._commandQueue.unshift(element);
|
|
@@ -2569,19 +2918,6 @@ class IMAPConnection {
|
|
|
2569
2918
|
}
|
|
2570
2919
|
this.processQueue();
|
|
2571
2920
|
}
|
|
2572
|
-
else if (/^AUTHENTICATE /i.test(parsed.command)) {
|
|
2573
|
-
// an unsupported mechanism is a NO, not a syntax error (RFC 3501 section 6.2.2)
|
|
2574
|
-
this.send({
|
|
2575
|
-
tag: parsed.tag,
|
|
2576
|
-
command: 'NO',
|
|
2577
|
-
attributes: [
|
|
2578
|
-
{
|
|
2579
|
-
type: 'TEXT',
|
|
2580
|
-
value: 'Unsupported authentication mechanism'
|
|
2581
|
-
}
|
|
2582
|
-
]
|
|
2583
|
-
}, 'UNKNOWN COMMAND', parsed, data);
|
|
2584
|
-
}
|
|
2585
2921
|
else {
|
|
2586
2922
|
this.send({
|
|
2587
2923
|
tag: parsed.tag,
|
|
@@ -2595,6 +2931,64 @@ class IMAPConnection {
|
|
|
2595
2931
|
}, 'UNKNOWN COMMAND', parsed, data);
|
|
2596
2932
|
}
|
|
2597
2933
|
}
|
|
2934
|
+
/**
|
|
2935
|
+
* Emits the `command` event when the tagged response of a command goes out, and the `session` events "login"
|
|
2936
|
+
* (the command authenticated the session) and "logout" (back to Not Authenticated, UNAUTHENTICATE). Every command
|
|
2937
|
+
* that changes these does so before its tagged response. "select" and "unselect" come from SELECT and
|
|
2938
|
+
* closeMailbox(). Tests wait for these instead of polling
|
|
2939
|
+
*
|
|
2940
|
+
* @param {String} status OK, NO or BAD
|
|
2941
|
+
*/
|
|
2942
|
+
commandCompleted(status) {
|
|
2943
|
+
const start = this._commandStart;
|
|
2944
|
+
this._commandStart = null;
|
|
2945
|
+
const server = this.server;
|
|
2946
|
+
if (!server.listenerCount('command') && !server.listenerCount('session')) {
|
|
2947
|
+
return;
|
|
2948
|
+
}
|
|
2949
|
+
server.emit('command', { session: this.sessionNumber, tag: start.tag, command: start.command, status, user: this.username || null });
|
|
2950
|
+
const authenticated = (state) => state === 'Authenticated' || state === 'Selected';
|
|
2951
|
+
if (authenticated(start.state) !== authenticated(this.state) && this.state !== 'Logout') {
|
|
2952
|
+
this.emitSession(authenticated(this.state) ? 'login' : 'logout');
|
|
2953
|
+
}
|
|
2954
|
+
}
|
|
2955
|
+
/**
|
|
2956
|
+
* Emits a `session` event, `{ type, session, ...fields }`
|
|
2957
|
+
*
|
|
2958
|
+
* @param {String} type Event type
|
|
2959
|
+
* @param {Object} [fields] More fields of the event
|
|
2960
|
+
*/
|
|
2961
|
+
emitSession(type, fields) {
|
|
2962
|
+
if (this.server.listenerCount('session')) {
|
|
2963
|
+
this.server.emit('session', Object.assign({ type, session: this.describe() }, fields));
|
|
2964
|
+
}
|
|
2965
|
+
}
|
|
2966
|
+
/**
|
|
2967
|
+
* Describes the session without credentials or sockets, for the control API and the `session` events
|
|
2968
|
+
*
|
|
2969
|
+
* @return {Object} `{ session, user, state, mailbox, readOnly, enabled, secure, compressed, remoteAddress }`
|
|
2970
|
+
*/
|
|
2971
|
+
describe() {
|
|
2972
|
+
return {
|
|
2973
|
+
session: this.sessionNumber,
|
|
2974
|
+
user: this.username || null,
|
|
2975
|
+
state: this.state,
|
|
2976
|
+
mailbox: this.selectedMailbox ? this.selectedMailbox.path : null,
|
|
2977
|
+
readOnly: !!this.selectedMailbox && !!this.readOnly,
|
|
2978
|
+
enabled: Array.isArray(this.enabled) ? this.enabled.slice() : [],
|
|
2979
|
+
secure: !!this.secureConnection,
|
|
2980
|
+
compressed: !!this.transport,
|
|
2981
|
+
remoteAddress: (this.socket && this.socket.remoteAddress) || null
|
|
2982
|
+
};
|
|
2983
|
+
}
|
|
2984
|
+
/**
|
|
2985
|
+
* Checks if output can still be sent: the socket is open and the connection is not closing (BYE, LOGOUT)
|
|
2986
|
+
*
|
|
2987
|
+
* @return {Boolean} true for a live session
|
|
2988
|
+
*/
|
|
2989
|
+
isOpen() {
|
|
2990
|
+
return !!this.socket && !this.socket.destroyed && !this._closing;
|
|
2991
|
+
}
|
|
2598
2992
|
/**
|
|
2599
2993
|
* Handles a command line with the script rule that matched it, after the rule's delay. With `run` the line
|
|
2600
2994
|
* goes through the parser and the command handler as usual afterwards
|
|
@@ -2635,6 +3029,8 @@ class IMAPConnection {
|
|
|
2635
3029
|
const command = element.parsed.command.toUpperCase();
|
|
2636
3030
|
this._runningCommand = element;
|
|
2637
3031
|
const options = this.server.getCommandOptions(command);
|
|
3032
|
+
// the session events of the command are worked out when its tagged response goes out, see commandCompleted
|
|
3033
|
+
this._commandStart = { tag: element.parsed.tag, command, state: this.state };
|
|
2638
3034
|
let done = false;
|
|
2639
3035
|
const next = () => {
|
|
2640
3036
|
if (done) {
|
|
@@ -2689,61 +3085,66 @@ class IMAPConnection {
|
|
|
2689
3085
|
return next();
|
|
2690
3086
|
}
|
|
2691
3087
|
}
|
|
2692
|
-
if (
|
|
2693
|
-
|
|
2694
|
-
//
|
|
2695
|
-
//
|
|
2696
|
-
//
|
|
2697
|
-
//
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
//
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
this.
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
3088
|
+
if (this.state === 'Selected' &&
|
|
3089
|
+
(command.startsWith('UID ') || options.noExpunge || options.sequenceSet !== false || options.searchCriteria !== false)) {
|
|
3090
|
+
// A command that refers to messages runs on the message list the client was told about. Only these: CLOSE
|
|
3091
|
+
// must not send pending EXPUNGE responses (RFC 3501 section 6.4.2) and SELECT leaves the old mailbox. RFC 3501 section 5.2:
|
|
3092
|
+
// "A server MUST send mailbox size updates automatically if a mailbox size change is observed during the
|
|
3093
|
+
// processing of a command", so the new messages and flag changes of other sessions are reported first,
|
|
3094
|
+
// before the command resolves its sequence set or search criteria.
|
|
3095
|
+
// EXPUNGE responses may be sent during UID commands (RFC 3501 section 7.4.1), so these report the expunges of
|
|
3096
|
+
// other sessions first as well, then the command runs on the current mailbox, where the UIDs of the expunged
|
|
3097
|
+
// messages do not exist and are ignored (RFC 3501 section 6.4.8). The ghost handling of STORE, COPY and MOVE
|
|
3098
|
+
// (RFC 2180 section 4) only applies to their sequence number forms, these and UID SEARCH with message numbers
|
|
3099
|
+
// in the criteria keep the EXPUNGE responses for later, see processNotifications
|
|
3100
|
+
this.processNotifications(element.parsed, true);
|
|
3101
|
+
}
|
|
3102
|
+
// changes made while the handler runs are attributed to this session (the `origin` of notifications)
|
|
3103
|
+
this.server.withOrigin(this, () => {
|
|
3104
|
+
try {
|
|
3105
|
+
const inputHandler = this.inputHandler;
|
|
3106
|
+
(element.handler || this.server.getCommandHandler(element.parsed.command))(this, element.parsed, element.data, next);
|
|
3107
|
+
if (this.inputHandler && this.inputHandler !== inputHandler) {
|
|
3108
|
+
// the command reads the lines that follow (IDLE, AUTHENTICATE), script rules match them with it
|
|
3109
|
+
this.inputCommand = { tag: element.parsed.tag, command: element.parsed.command };
|
|
3110
|
+
this.emitSession('waiting', { command });
|
|
3111
|
+
}
|
|
2708
3112
|
}
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
3113
|
+
catch (E) {
|
|
3114
|
+
const ex = E;
|
|
3115
|
+
const badInput = ex.imapResponse === 'BAD';
|
|
3116
|
+
if (!badInput && this.options.debug) {
|
|
3117
|
+
console.error('Error processing command:', ex, '\n', ex.stack);
|
|
3118
|
+
}
|
|
3119
|
+
this.send({
|
|
3120
|
+
tag: element.parsed.tag,
|
|
3121
|
+
command: badInput ? 'BAD' : 'NO',
|
|
3122
|
+
attributes: [].concat(badInput
|
|
3123
|
+
? []
|
|
3124
|
+
: {
|
|
3125
|
+
type: 'SECTION',
|
|
3126
|
+
section: [
|
|
3127
|
+
{
|
|
3128
|
+
type: 'ATOM',
|
|
3129
|
+
value: 'SERVERBUG'
|
|
3130
|
+
}
|
|
3131
|
+
]
|
|
3132
|
+
}, {
|
|
3133
|
+
type: 'TEXT',
|
|
3134
|
+
value: badInput ? ex.message : 'Server error: ' + ex.message
|
|
3135
|
+
})
|
|
3136
|
+
}, badInput ? 'INVALID COMMAND' : 'SERVER ERROR', element.parsed, element.data);
|
|
3137
|
+
// keep the connection usable, otherwise every later command would hang
|
|
3138
|
+
next();
|
|
2715
3139
|
}
|
|
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
|
-
}
|
|
3140
|
+
});
|
|
2740
3141
|
}
|
|
2741
3142
|
/**
|
|
2742
3143
|
* Removes messages with \Deleted flag
|
|
2743
3144
|
*
|
|
2744
3145
|
* @param {Object} mailbox Mailbox to check for
|
|
2745
3146
|
* @param {Boolean} [ignoreSelf] If set to true, does not send any notices to itself
|
|
2746
|
-
* @param {Boolean} [
|
|
3147
|
+
* @param {Boolean} [ignoreExists] If set to true, does not send EXISTS notice to itself
|
|
2747
3148
|
*/
|
|
2748
3149
|
expungeDeleted(mailbox, ignoreSelf, ignoreExists) {
|
|
2749
3150
|
this.expungeSpecificMessages(mailbox, (message) => {
|
|
@@ -2752,81 +3153,56 @@ class IMAPConnection {
|
|
|
2752
3153
|
}
|
|
2753
3154
|
/**
|
|
2754
3155
|
* Given a set of messages in a mailbox (possibly via getMessageRange), remove
|
|
2755
|
-
* them from the mailbox and generate EXPUNGE notifications.
|
|
3156
|
+
* them from the mailbox and generate EXPUNGE notifications, see expungeMessages() in store-operations.ts
|
|
2756
3157
|
*
|
|
2757
3158
|
* @param {Object} mailbox Mailbox to check for
|
|
2758
3159
|
* @param {Function|Array} messagesOrFilterFunc An Array of messages in the
|
|
2759
3160
|
* folder that should be removed or a filtering function that indicates
|
|
2760
3161
|
* messages to be removed by returning true.
|
|
2761
3162
|
* @param {Boolean} [ignoreSelf] If set to true, does not send any notices to itself
|
|
2762
|
-
* @param {Boolean} [
|
|
3163
|
+
* @param {Boolean} [ignoreExists] If set to true, does not send EXISTS notice to itself
|
|
2763
3164
|
* @param {Boolean} [highestFirst] If set to true, the EXPUNGE responses go from the highest UID to the lowest
|
|
2764
3165
|
* (MESSAGELIMIT, RFC 9738 section 3.1), otherwise from the lowest
|
|
2765
3166
|
*/
|
|
2766
3167
|
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);
|
|
3168
|
+
(0, store_operations_js_1.expungeMessages)(this.server, mailbox, messagesOrFilterFunc, {
|
|
3169
|
+
origin: this,
|
|
3170
|
+
skip: ignoreSelf ? this : null,
|
|
3171
|
+
skipExists: ignoreExists ? this : null,
|
|
3172
|
+
highestFirst: !!highestFirst
|
|
2814
3173
|
});
|
|
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
3174
|
}
|
|
2828
3175
|
}
|
|
2829
3176
|
exports.IMAPConnection = IMAPConnection;
|
|
3177
|
+
// setImmediate does not let Deno send what sockets were given, see nextTurn
|
|
3178
|
+
const IS_DENO = 'Deno' in globalThis;
|
|
3179
|
+
/**
|
|
3180
|
+
* Runs a function on the next event loop turn that also handles network I/O: setImmediate on Node and Bun, a zero
|
|
3181
|
+
* timer on Deno, where setImmediate runs before the sockets send what was written (#89)
|
|
3182
|
+
*
|
|
3183
|
+
* @param {Function} fn Function to run
|
|
3184
|
+
* @return {Object} the timer, clearOutputQueue clears it
|
|
3185
|
+
*/
|
|
3186
|
+
function nextTurn(fn) {
|
|
3187
|
+
return IS_DENO ? setTimeout(fn, 0) : setImmediate(fn);
|
|
3188
|
+
}
|
|
3189
|
+
/**
|
|
3190
|
+
* Starts listening, resolves with the port or rejects with the listen error (EADDRINUSE)
|
|
3191
|
+
*
|
|
3192
|
+
* @param {Object} server net, tls or http server
|
|
3193
|
+
* @param {Number} [port] Port, a free one if not set
|
|
3194
|
+
* @param {String} [host] Address, all addresses if not set
|
|
3195
|
+
* @return {Promise<Number>} the port
|
|
3196
|
+
*/
|
|
3197
|
+
function listenOn(server, port, host) {
|
|
3198
|
+
return new Promise((resolve, reject) => {
|
|
3199
|
+
server.once('error', reject);
|
|
3200
|
+
server.listen({ port: port || 0, host }, () => {
|
|
3201
|
+
server.removeListener('error', reject);
|
|
3202
|
+
resolve(server.address().port);
|
|
3203
|
+
});
|
|
3204
|
+
});
|
|
3205
|
+
}
|
|
2830
3206
|
/**
|
|
2831
3207
|
* Destroys a socket with a TCP RST (`close: 'reset'` of a script rule). resetAndDestroy (Node 16.17) needs a socket with
|
|
2832
3208
|
* a TCP handle, a TLS socket throws ERR_INVALID_HANDLE_TYPE (#84), so the RST goes to the TCP socket under the TLS
|