imapkit 5.0.0 → 5.1.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 +51 -29
- package/bin/help.txt +3 -2
- package/bin/imapkit.js +15 -5
- package/dist/browser/imapkit.js +105 -0
- package/dist/cjs/commands/handlers/fetch.js +2 -1
- package/dist/cjs/commands/handlers/store.js +4 -4
- package/dist/cjs/commands/list.js +17 -3
- package/dist/cjs/control.d.ts +4 -9
- package/dist/cjs/control.js +1 -1
- 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 +3 -1
- package/dist/cjs/load-plugins.d.ts +3 -1
- package/dist/cjs/load-plugins.js +25 -6
- package/dist/cjs/memory-socket.d.ts +69 -0
- package/dist/cjs/memory-socket.js +187 -0
- package/dist/cjs/plugins/binary.js +2 -1
- 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/idle.js +2 -2
- package/dist/cjs/plugins/imap4rev2.js +5 -0
- 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 +3 -0
- package/dist/cjs/quirks.d.ts +2 -2
- package/dist/cjs/quirks.js +5 -4
- package/dist/cjs/rest.d.ts +7 -3
- package/dist/cjs/rest.js +11 -5
- package/dist/cjs/server.d.ts +67 -3
- package/dist/cjs/server.js +241 -93
- package/dist/cjs/storage-schema.js +12 -5
- package/dist/cjs/store-operations.d.ts +5 -2
- package/dist/cjs/store-operations.js +6 -3
- package/dist/cjs/types.d.ts +4 -0
- package/dist/esm/commands/handlers/fetch.js +2 -1
- package/dist/esm/commands/handlers/store.js +4 -4
- package/dist/esm/commands/list.js +17 -3
- package/dist/esm/control.d.ts +4 -9
- package/dist/esm/control.js +1 -1
- 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 +3 -1
- package/dist/esm/load-plugins.d.ts +3 -1
- package/dist/esm/load-plugins.js +25 -6
- package/dist/esm/memory-socket.d.ts +69 -0
- package/dist/esm/memory-socket.js +182 -0
- package/dist/esm/plugins/binary.js +2 -1
- 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/idle.js +2 -2
- package/dist/esm/plugins/imap4rev2.js +5 -0
- 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 +3 -0
- package/dist/esm/quirks.d.ts +2 -2
- package/dist/esm/quirks.js +5 -4
- package/dist/esm/rest.d.ts +7 -3
- package/dist/esm/rest.js +11 -5
- package/dist/esm/server.d.ts +67 -3
- package/dist/esm/server.js +243 -95
- package/dist/esm/storage-schema.js +12 -5
- package/dist/esm/store-operations.d.ts +5 -2
- package/dist/esm/store-operations.js +6 -3
- package/dist/esm/types.d.ts +4 -0
- package/dist/plugin-help.json +3 -2
- package/package.json +8 -1
package/dist/cjs/server.js
CHANGED
|
@@ -59,6 +59,7 @@ const quirks_js_1 = require("./quirks.js");
|
|
|
59
59
|
const storage_schema_js_1 = require("./storage-schema.js");
|
|
60
60
|
const rest_js_1 = require("./rest.js");
|
|
61
61
|
const bundledCert = __importStar(require("./cert.js"));
|
|
62
|
+
const memory_socket_js_1 = require("./memory-socket.js");
|
|
62
63
|
const script_js_1 = require("./script.js");
|
|
63
64
|
// longest command line (not counting literals) accepted from a client
|
|
64
65
|
const MAX_LINE_LENGTH = 1024 * 1024;
|
|
@@ -105,6 +106,36 @@ function getResponseTag(line) {
|
|
|
105
106
|
function stateError(command, state) {
|
|
106
107
|
return command + ' is not allowed in the ' + state + ' state';
|
|
107
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* Checks if a queued notification changes message sequence numbers by removing messages: an EXPUNGE response, or the
|
|
111
|
+
* EXISTS that follows the EXPUNGE responses of another session (it carries the snapshot of the old message list)
|
|
112
|
+
*
|
|
113
|
+
* @param {Object} notification Queued notification
|
|
114
|
+
* @return {Boolean} true if the notification must wait while EXPUNGE responses are not allowed
|
|
115
|
+
*/
|
|
116
|
+
function isPendingExpunge(notification) {
|
|
117
|
+
return !!notification.mailboxCopy || (0, arguments_js_1.isAtom)(notification.attributes && notification.attributes[1], 'EXPUNGE');
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Checks if a notification is an untagged EXISTS response
|
|
121
|
+
*
|
|
122
|
+
* @param {Object} [notification] Notification to send
|
|
123
|
+
* @return {Boolean} true for an EXISTS response
|
|
124
|
+
*/
|
|
125
|
+
function isExists(notification) {
|
|
126
|
+
return !!notification && !notification.command && (0, arguments_js_1.isAtom)(notification.attributes && notification.attributes[1], 'EXISTS');
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Answers AUTHENTICATE with a mechanism no plugin supports: NO, not a syntax error (RFC 3501 section 6.2.2). It runs
|
|
130
|
+
* through the command queue, so the state check comes first (AUTHENTICATE is only valid in the Not Authenticated
|
|
131
|
+
* state, RFC 3501 section 6.2)
|
|
132
|
+
*/
|
|
133
|
+
// usesSequenceNumbers() of each running command
|
|
134
|
+
const sequenceNumberUse = new WeakMap();
|
|
135
|
+
const unsupportedMechanism = (connection, parsed, data, callback) => {
|
|
136
|
+
connection.sendStatus(parsed, data, 'NO', 'Unsupported authentication mechanism', false, 'UNKNOWN COMMAND');
|
|
137
|
+
callback();
|
|
138
|
+
};
|
|
108
139
|
/**
|
|
109
140
|
* Creates a new IMAP server, call `listen()` on it to start accepting connections
|
|
110
141
|
*
|
|
@@ -204,7 +235,7 @@ class IMAPServer extends node_stream_1.Stream {
|
|
|
204
235
|
// plugins register their control operations
|
|
205
236
|
this.control = new control_js_1.Control(this);
|
|
206
237
|
// a quirk preset can leave plugins out (no-move, no-uidplus)
|
|
207
|
-
(0, load_plugins_js_1.default)(this, this.options.plugins,
|
|
238
|
+
(0, load_plugins_js_1.default)(this, this.options.plugins, quirks.removePlugins);
|
|
208
239
|
if (this.options.storage) {
|
|
209
240
|
// a typo in a fixture fails here with the path of the problem
|
|
210
241
|
(0, storage_schema_js_1.validateStorage)(this.options.storage);
|
|
@@ -333,6 +364,8 @@ class IMAPServer extends node_stream_1.Stream {
|
|
|
333
364
|
this.smtpServer = null;
|
|
334
365
|
}
|
|
335
366
|
if (this.restServer) {
|
|
367
|
+
// event streams never end on their own, a graceful shutdown would wait for them forever
|
|
368
|
+
this.restServer.endStreams?.();
|
|
336
369
|
this.restServer.close();
|
|
337
370
|
// keep-alive connections would hold the server open
|
|
338
371
|
if (force) {
|
|
@@ -368,6 +401,21 @@ class IMAPServer extends node_stream_1.Stream {
|
|
|
368
401
|
handler(connection);
|
|
369
402
|
});
|
|
370
403
|
}
|
|
404
|
+
/**
|
|
405
|
+
* Opens a connection to the server in memory, without a network socket (no `listen()` needed). This is how the
|
|
406
|
+
* server runs in a browser, see the README "Run in a browser"
|
|
407
|
+
*
|
|
408
|
+
* @param {Object} [options] `{ secure }`: false makes the connection a plaintext one, where STARTTLS turns on the
|
|
409
|
+
* encryption (nothing is encrypted, the session only counts as encrypted). By default the connection counts as
|
|
410
|
+
* one over TLS
|
|
411
|
+
* @return {MemorySocket} the client end, write commands to it and read the responses from its `data` events
|
|
412
|
+
*/
|
|
413
|
+
connect(options) {
|
|
414
|
+
const [serverSocket, clientSocket] = (0, memory_socket_js_1.createSocketPair)();
|
|
415
|
+
serverSocket.encrypted = clientSocket.encrypted = !options || options.secure !== false;
|
|
416
|
+
this.createClient(serverSocket);
|
|
417
|
+
return clientSocket;
|
|
418
|
+
}
|
|
371
419
|
registerCapability(keyword, handler) {
|
|
372
420
|
this.capabilities[keyword] =
|
|
373
421
|
handler ||
|
|
@@ -511,8 +559,7 @@ class IMAPServer extends node_stream_1.Stream {
|
|
|
511
559
|
let seen = 0;
|
|
512
560
|
let unseen = 0;
|
|
513
561
|
// flags stay defined in the mailbox once a message had them, see rememberFlags
|
|
514
|
-
const permanentFlags =
|
|
515
|
-
(mailbox.knownFlags || []).forEach(flag => this.ensureFlag(permanentFlags, flag));
|
|
562
|
+
const permanentFlags = this.permanentFlagList(mailbox);
|
|
516
563
|
let recent = 0;
|
|
517
564
|
// \Recent sets of the sessions that have this mailbox selected
|
|
518
565
|
const recentSets = [];
|
|
@@ -558,20 +605,8 @@ class IMAPServer extends node_stream_1.Stream {
|
|
|
558
605
|
* @return {Boolean} Returns true if the date string is in IMAP date-time format
|
|
559
606
|
*/
|
|
560
607
|
validateInternalDate(date) {
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
}
|
|
564
|
-
// date-time from RFC 3501 section 9, month names are case-insensitive like all ABNF strings
|
|
565
|
-
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);
|
|
566
|
-
if (!match) {
|
|
567
|
-
return false;
|
|
568
|
-
}
|
|
569
|
-
// the values must also make a real date and time
|
|
570
|
-
return ((0, dates_js_1.isRealDate)(match[1], (0, dates_js_1.monthIndex)(match[2]), match[3]) &&
|
|
571
|
-
Number(match[4]) < 24 &&
|
|
572
|
-
Number(match[5]) < 60 &&
|
|
573
|
-
Number(match[6]) < 61 &&
|
|
574
|
-
Number(match[8]) < 60);
|
|
608
|
+
// date-time from RFC 3501 section 9
|
|
609
|
+
return (0, dates_js_1.isDateTime)(date);
|
|
575
610
|
}
|
|
576
611
|
/**
|
|
577
612
|
* Converts a date object to a valid date-time string format
|
|
@@ -1002,6 +1037,32 @@ class IMAPServer extends node_stream_1.Stream {
|
|
|
1002
1037
|
const knownFlags = (mailbox.knownFlags = mailbox.knownFlags || []);
|
|
1003
1038
|
flags.forEach(flag => this.ensureFlag(knownFlags, flag));
|
|
1004
1039
|
}
|
|
1040
|
+
/**
|
|
1041
|
+
* Checks if a flag can be stored on messages of a mailbox: any flag when the mailbox allows new keywords
|
|
1042
|
+
* (PERMANENTFLAGS has `\*`), otherwise only a flag in the PERMANENTFLAGS list that getStatus() builds, the
|
|
1043
|
+
* `permanentFlags` of the mailbox and every flag its messages had. RFC 3501 section 7.1 (RFC 9051 section 7.1):
|
|
1044
|
+
* PERMANENTFLAGS "indicates which of the known flags the client can change permanently", STORE ignores the others
|
|
1045
|
+
*
|
|
1046
|
+
* @param {Object} mailbox Mailbox object
|
|
1047
|
+
* @param {String} flag Normalized flag
|
|
1048
|
+
* @return {Boolean} true if the flag is a permanent flag of the mailbox
|
|
1049
|
+
*/
|
|
1050
|
+
isPermanentFlag(mailbox, flag) {
|
|
1051
|
+
// the lists of permanentFlagList(), without building it: this runs for every flag STORE and APPEND set
|
|
1052
|
+
return (mailbox.allowPermanentFlags || (mailbox.permanentFlags || []).indexOf(flag) >= 0 || (!!mailbox.knownFlags && mailbox.knownFlags.indexOf(flag) >= 0));
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* The flags of the PERMANENTFLAGS list (without `\*`): the `permanentFlags` of the mailbox and every flag its
|
|
1056
|
+
* messages have or had (`knownFlags`, see rememberFlags)
|
|
1057
|
+
*
|
|
1058
|
+
* @param {Object} mailbox Mailbox object
|
|
1059
|
+
* @return {Array} flags, a new list
|
|
1060
|
+
*/
|
|
1061
|
+
permanentFlagList(mailbox) {
|
|
1062
|
+
const list = [].concat(mailbox.permanentFlags || []);
|
|
1063
|
+
(mailbox.knownFlags || []).forEach(flag => this.ensureFlag(list, flag));
|
|
1064
|
+
return list;
|
|
1065
|
+
}
|
|
1005
1066
|
/**
|
|
1006
1067
|
* The current time for dates the server sets itself (INTERNALDATE of a message without one, SAVEDATE). The `now`
|
|
1007
1068
|
* option fixes it for repeatable tests: a Date, a timestamp, or a function that returns one
|
|
@@ -1073,9 +1134,10 @@ class IMAPServer extends node_stream_1.Stream {
|
|
|
1073
1134
|
*/
|
|
1074
1135
|
appendMessage(path, flags, internaldate, raw, ignoreConnection, properties) {
|
|
1075
1136
|
const mailbox = typeof path === 'string' ? this.getMailbox(path) : path;
|
|
1076
|
-
// processMessage() below sets the UID
|
|
1137
|
+
// processMessage() below sets the UID. Flags the mailbox can not store are left out (APPEND, COPY: the flags
|
|
1138
|
+
// SHOULD be set, RFC 3501 sections 6.3.11 and 6.4.7), as STORE ignores them (RFC 3501 section 7.1, PERMANENTFLAGS)
|
|
1077
1139
|
const message = Object.assign({}, properties, {
|
|
1078
|
-
flags: flags,
|
|
1140
|
+
flags: flags.filter(flag => this.isPermanentFlag(mailbox, flag)),
|
|
1079
1141
|
internaldate: internaldate,
|
|
1080
1142
|
raw: raw,
|
|
1081
1143
|
recent: true
|
|
@@ -1261,49 +1323,48 @@ class IMAPServer extends node_stream_1.Stream {
|
|
|
1261
1323
|
return flags;
|
|
1262
1324
|
}
|
|
1263
1325
|
matchFolders(referenceName, match, exportName, folders) {
|
|
1264
|
-
let includeINBOX = false;
|
|
1265
1326
|
const source = folders || this.folderCache;
|
|
1266
1327
|
const toName = exportName || ((name) => name);
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
//
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
const name = toName(key);
|
|
1277
|
-
if (key !== 'INBOX' && reference.substr(0, name.length) === name && (nsKey === false || name.length > nsName.length)) {
|
|
1278
|
-
nsKey = key;
|
|
1279
|
-
nsName = name;
|
|
1280
|
-
}
|
|
1281
|
-
}
|
|
1282
|
-
if (nsKey === false) {
|
|
1283
|
-
return [];
|
|
1284
|
-
}
|
|
1285
|
-
const namespace = this.storage[nsKey];
|
|
1286
|
-
const lookup = reference + match;
|
|
1287
|
-
const result = [];
|
|
1288
|
-
const pattern = '^' +
|
|
1328
|
+
// RFC 3501 section 6.3.8 (RFC 9051 section 6.3.9): "An empty ("" string) reference name argument
|
|
1329
|
+
// indicates that the mailbox name is interpreted as by SELECT", and without break out characters
|
|
1330
|
+
// "the canonical form is normally the reference name appended with the mailbox name". The pattern
|
|
1331
|
+
// matches full mailbox names, also in a personal namespace with a prefix such as "INBOX.". With the
|
|
1332
|
+
// namespace convention "#" is a break out character "and must be treated as such" (RFC 3501 section 6.3.8,
|
|
1333
|
+
// RFC 9051 section 6.3.9): a pattern that starts with it is a name of its own, the reference is ignored
|
|
1334
|
+
const lookup = match.charAt(0) === '#' ? match : (referenceName || '') + match;
|
|
1335
|
+
// "%" does not match the hierarchy delimiter, which is the one of the namespace a name belongs to
|
|
1336
|
+
const toRegExp = (separator, flags = '') => new RegExp('^' +
|
|
1289
1337
|
lookup
|
|
1290
1338
|
// escape regex symbols
|
|
1291
1339
|
.replace(/([\\^$+?!.():=[\]{}|,-])/g, '\\$1')
|
|
1292
1340
|
.replace(/[*]/g, '.*')
|
|
1293
|
-
.replace(/[%]/g, '[^' +
|
|
1294
|
-
'$';
|
|
1295
|
-
|
|
1296
|
-
//
|
|
1297
|
-
|
|
1341
|
+
.replace(/[%]/g, '[^' + separator.replace(/([\\^$+*?!.():=[\]{}|,-])/g, '\\$1') + ']*') +
|
|
1342
|
+
'$', flags);
|
|
1343
|
+
// RFC 3501 section 6.3.8 allows to "hide" otherwise accessible mailboxes from the wildcards: the
|
|
1344
|
+
// mailboxes of namespaces other than the personal one are only matched when the pattern names
|
|
1345
|
+
// the prefix of the namespace before any wildcard (LIST "" "user.%", LIST "#news." "*"). The pattern
|
|
1346
|
+
// of every namespace that is matched, null for a hidden one
|
|
1347
|
+
const fixedPrefix = lookup.replace(/[*%].*$/, '');
|
|
1348
|
+
const queries = new Map();
|
|
1349
|
+
Object.keys(this.storage).forEach(key => {
|
|
1350
|
+
const name = toName(key);
|
|
1351
|
+
const visible = key === this.referenceNamespace || fixedPrefix.substr(0, name.length) === name;
|
|
1352
|
+
queries.set(key, visible ? toRegExp(this.storage[key].separator) : null);
|
|
1353
|
+
});
|
|
1354
|
+
const result = [];
|
|
1355
|
+
// "The special name INBOX is included in the output from LIST, if [...] the uppercase string "INBOX"
|
|
1356
|
+
// matches the interpreted reference and mailbox name arguments", INBOX is case-insensitive
|
|
1357
|
+
if (source.INBOX && toRegExp((this.storage.INBOX && this.storage.INBOX.separator) || '/', 'i').test('INBOX')) {
|
|
1298
1358
|
result.push(source.INBOX);
|
|
1299
1359
|
}
|
|
1300
1360
|
Object.keys(source).forEach(path => {
|
|
1301
1361
|
const folder = source[path];
|
|
1302
|
-
|
|
1362
|
+
const query = path !== 'INBOX' && folder.namespace !== false && folder.namespace !== 'INBOX' ? queries.get(folder.namespace) : null;
|
|
1363
|
+
if (!query) {
|
|
1303
1364
|
return;
|
|
1304
1365
|
}
|
|
1305
1366
|
const name = toName(path);
|
|
1306
|
-
if (
|
|
1367
|
+
if (query.test(name) && (folder.flags.indexOf('\\NonExistent') < 0 || name === lookup)) {
|
|
1307
1368
|
result.push(folder);
|
|
1308
1369
|
}
|
|
1309
1370
|
});
|
|
@@ -1393,7 +1454,8 @@ class IMAPConnection {
|
|
|
1393
1454
|
this._outputQueue = null;
|
|
1394
1455
|
this._outputTimer = null;
|
|
1395
1456
|
this._deferredOutput = null;
|
|
1396
|
-
|
|
1457
|
+
// a TLS socket is encrypted, and so can be an in-memory connection of server.connect()
|
|
1458
|
+
this.secureConnection = !!this.options.secureConnection || socket.encrypted === true;
|
|
1397
1459
|
this._remainder = '';
|
|
1398
1460
|
this._command = '';
|
|
1399
1461
|
this._literalRemaining = 0;
|
|
@@ -1411,7 +1473,7 @@ class IMAPConnection {
|
|
|
1411
1473
|
this.messageGlobal = false;
|
|
1412
1474
|
this._commandQueue = [];
|
|
1413
1475
|
this._processing = false;
|
|
1414
|
-
if (this.options.debug) {
|
|
1476
|
+
if (this.options.debug && typeof this.socket.pipe === 'function') {
|
|
1415
1477
|
this.socket.pipe(process.stdout);
|
|
1416
1478
|
}
|
|
1417
1479
|
this.socket.on('data', this.receive.bind(this));
|
|
@@ -1650,7 +1712,8 @@ class IMAPConnection {
|
|
|
1650
1712
|
this.watchQuiet(quiet);
|
|
1651
1713
|
}
|
|
1652
1714
|
}, Math.max(0, next - (Date.now() - last)));
|
|
1653
|
-
|
|
1715
|
+
// browsers have no unref
|
|
1716
|
+
this._quietTimer.unref?.();
|
|
1654
1717
|
}
|
|
1655
1718
|
/**
|
|
1656
1719
|
* Handles data from the socket, through the transport layer if there is one
|
|
@@ -2161,7 +2224,7 @@ class IMAPConnection {
|
|
|
2161
2224
|
* @return {Boolean} true if an EXPUNGE response is pending
|
|
2162
2225
|
*/
|
|
2163
2226
|
hasPendingExpunge() {
|
|
2164
|
-
return this.notificationQueue.some(
|
|
2227
|
+
return this.notificationQueue.some(isPendingExpunge);
|
|
2165
2228
|
}
|
|
2166
2229
|
/**
|
|
2167
2230
|
* Tells the other sessions that have the selected mailbox open about changed flags, they get
|
|
@@ -2320,6 +2383,16 @@ class IMAPConnection {
|
|
|
2320
2383
|
// section 6.2.1). STARTTLS is refused when input is waiting (the noPipelining command option), this only
|
|
2321
2384
|
// guards against plugins that upgrade the connection otherwise
|
|
2322
2385
|
this.discardInput();
|
|
2386
|
+
// STARTTLS runs on a live connection
|
|
2387
|
+
const socket = this.socket;
|
|
2388
|
+
if (typeof socket.startTls === 'function') {
|
|
2389
|
+
// a socket without a TLS layer that can count as encrypted (MemorySocket of server.connect())
|
|
2390
|
+
return socket.startTls(() => {
|
|
2391
|
+
this.secureConnection = true;
|
|
2392
|
+
this.upgrading = false;
|
|
2393
|
+
callback();
|
|
2394
|
+
});
|
|
2395
|
+
}
|
|
2323
2396
|
const secureContext = node_tls_1.default.createSecureContext(this.server.getCredentials());
|
|
2324
2397
|
const socketOptions = {
|
|
2325
2398
|
secureContext: secureContext,
|
|
@@ -2330,8 +2403,6 @@ class IMAPConnection {
|
|
|
2330
2403
|
cb(null, secureContext);
|
|
2331
2404
|
}
|
|
2332
2405
|
};
|
|
2333
|
-
// STARTTLS runs on a live connection
|
|
2334
|
-
const socket = this.socket;
|
|
2335
2406
|
// the RST of a script rule goes to the TCP socket under the TLS layer, see resetSocket. Node also has it as the
|
|
2336
2407
|
// _parent of the TLS socket, Bun does not
|
|
2337
2408
|
this.tcpSocket = socket;
|
|
@@ -2368,22 +2439,52 @@ class IMAPConnection {
|
|
|
2368
2439
|
prepareNotifications(queue) {
|
|
2369
2440
|
return queue;
|
|
2370
2441
|
}
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2442
|
+
/**
|
|
2443
|
+
* Checks if EXPUNGE responses must wait while a command runs: during FETCH, STORE and SEARCH (RFC 3501 section
|
|
2444
|
+
* 7.4.1), during the commands that extensions add to this list (see the noExpunge command option), and during UID
|
|
2445
|
+
* SEARCH with message numbers in the search criteria (RFC 7162 section 3.2.10.2 for VANISHED, EXPUNGE may wait as
|
|
2446
|
+
* well, RFC 3501 only allows it during UID commands)
|
|
2447
|
+
*
|
|
2448
|
+
* @param {Object} data Parsed command
|
|
2449
|
+
* @return {Boolean} true if the command holds back EXPUNGE responses
|
|
2450
|
+
*/
|
|
2451
|
+
holdsExpunge(data) {
|
|
2452
|
+
const options = this.server.getCommandOptions(data.command);
|
|
2453
|
+
return options.noExpunge || (options.searchCriteria !== false && this.usesSequenceNumbers(data));
|
|
2454
|
+
}
|
|
2455
|
+
/**
|
|
2456
|
+
* Sends the queued notifications. During a command that holds back EXPUNGE responses (see holdsExpunge), or with
|
|
2457
|
+
* `beforeCommand` before a command that refers to messages by sequence number, only the notifications queued before
|
|
2458
|
+
* the first pending EXPUNGE go out: new messages (EXISTS) and flag changes. RFC 3501 section 5.2: "A server MUST send
|
|
2459
|
+
* mailbox size updates automatically if a mailbox size change is observed during the processing of a command",
|
|
2460
|
+
* section 7.4.1 forbids only EXPUNGE during FETCH, STORE and SEARCH. What was queued after the EXPUNGE waits with it,
|
|
2461
|
+
* an EXISTS sent before it would describe a list that the client can not know yet
|
|
2462
|
+
*
|
|
2463
|
+
* @param {Object} [data] Parsed command that runs, or null between commands
|
|
2464
|
+
* @param {Boolean} [beforeCommand] true when the command has not run yet, then a command with message sequence
|
|
2465
|
+
* numbers (COPY, MOVE) also waits with the EXPUNGE responses, its numbers refer to the messages before them
|
|
2466
|
+
*/
|
|
2467
|
+
processNotifications(data, beforeCommand) {
|
|
2380
2468
|
if (!this.notificationQueue.length) {
|
|
2381
2469
|
return;
|
|
2382
2470
|
}
|
|
2383
|
-
|
|
2384
|
-
this.notificationQueue = [];
|
|
2471
|
+
let queue = this.notificationQueue;
|
|
2385
2472
|
// Flag updates use the sequence numbers this session knows: before the EXPUNGE responses of
|
|
2386
2473
|
// the snapshot are sent, the snapshot, afterwards the current message list
|
|
2474
|
+
const snapshot = queue.find(notification => notification.mailboxCopy);
|
|
2475
|
+
let held = [];
|
|
2476
|
+
if (data && (this.holdsExpunge(data) || (beforeCommand && this.usesSequenceNumbers(data)))) {
|
|
2477
|
+
const first = queue.findIndex(isPendingExpunge);
|
|
2478
|
+
if (first >= 0) {
|
|
2479
|
+
held = queue.slice(first);
|
|
2480
|
+
queue = queue.slice(0, first);
|
|
2481
|
+
}
|
|
2482
|
+
if (!queue.length) {
|
|
2483
|
+
return;
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
this.notificationQueue = held;
|
|
2487
|
+
queue = this.prepareNotifications(queue);
|
|
2387
2488
|
const snapshotIndex = queue.findIndex(notification => notification.mailboxCopy);
|
|
2388
2489
|
const sequenceMaps = new Map();
|
|
2389
2490
|
const getSequence = (messages) => {
|
|
@@ -2397,16 +2498,58 @@ class IMAPConnection {
|
|
|
2397
2498
|
const current = this.selectedMailbox ? this.selectedMailbox.messages : [];
|
|
2398
2499
|
// a message changed several times is reported once, its FETCH response carries the current flags
|
|
2399
2500
|
const reported = new Set();
|
|
2501
|
+
// before the snapshot (or with all of it still held back, then the split at the first pending expunge leaves
|
|
2502
|
+
// no snapshot in the queue) the session knows the old list
|
|
2503
|
+
const sessionList = (i) => (snapshot && (snapshotIndex < 0 || i < snapshotIndex) ? snapshot.mailboxCopy : current);
|
|
2504
|
+
// the last EXISTS response of a run of EXISTS responses, and if the run announces new messages
|
|
2505
|
+
let lastExists = -1;
|
|
2506
|
+
let newMessages = false;
|
|
2400
2507
|
queue.forEach((notification, i) => {
|
|
2401
2508
|
if (notification.flagUpdate) {
|
|
2402
|
-
|
|
2403
|
-
this.sendFlagUpdate(notification.flagUpdate, getSequence(i < snapshotIndex ? queue[snapshotIndex].mailboxCopy : current), reported);
|
|
2509
|
+
this.sendFlagUpdate(notification.flagUpdate, getSequence(sessionList(i)), reported);
|
|
2404
2510
|
}
|
|
2405
2511
|
else {
|
|
2406
2512
|
this.send(notification);
|
|
2407
2513
|
}
|
|
2514
|
+
if (isExists(notification)) {
|
|
2515
|
+
lastExists = i;
|
|
2516
|
+
// the EXISTS after the EXPUNGE responses of another session carries the snapshot instead of a message
|
|
2517
|
+
newMessages = newMessages || !!notification.message;
|
|
2518
|
+
}
|
|
2519
|
+
const next = queue[i + 1];
|
|
2520
|
+
if (lastExists < 0 || isExists(next) || (next && next.fetchedMessage)) {
|
|
2521
|
+
// not the end of a run of EXISTS responses (and the FETCH responses NOTIFY sends after them)
|
|
2522
|
+
return;
|
|
2523
|
+
}
|
|
2524
|
+
// RFC 3501 section 7.3.2: the RECENT response "occurs as a result of a SELECT or EXAMINE command, and if the
|
|
2525
|
+
// size of the mailbox changes (e.g., new messages)". One for consecutive EXISTS responses that announce new
|
|
2526
|
+
// messages, with the number of \Recent messages among those the client was told about. After expunges the
|
|
2527
|
+
// EXPUNGE responses report the change (RFC 3501 section 7.4.1). It goes after the FETCH responses that NOTIFY
|
|
2528
|
+
// sends for new messages, RFC 5465 section 5.2: "an unsolicited EXISTS response, followed by an unsolicited
|
|
2529
|
+
// FETCH response [...] The server MAY also send a RECENT response"
|
|
2530
|
+
if (newMessages) {
|
|
2531
|
+
this.sendRecent(Number(queue[lastExists].attributes[0]), getSequence(sessionList(lastExists)));
|
|
2532
|
+
}
|
|
2533
|
+
lastExists = -1;
|
|
2534
|
+
newMessages = false;
|
|
2408
2535
|
});
|
|
2409
2536
|
}
|
|
2537
|
+
/**
|
|
2538
|
+
* Sends an untagged RECENT response with the number of \Recent messages among the first `count` messages
|
|
2539
|
+
*
|
|
2540
|
+
* @param {Number} count Number of messages the client was told about
|
|
2541
|
+
* @param {Map} sequence Message to the sequence number this session knows it by
|
|
2542
|
+
*/
|
|
2543
|
+
sendRecent(count, sequence) {
|
|
2544
|
+
let recent = 0;
|
|
2545
|
+
(this.recent || new Set()).forEach(message => {
|
|
2546
|
+
const seq = sequence.get(message);
|
|
2547
|
+
if (seq && seq <= count) {
|
|
2548
|
+
recent++;
|
|
2549
|
+
}
|
|
2550
|
+
});
|
|
2551
|
+
this.send({ tag: '*', notification: true, attributes: [recent, { type: 'ATOM', value: 'RECENT' }] }, 'RECENT NOTIFICATION');
|
|
2552
|
+
}
|
|
2410
2553
|
/**
|
|
2411
2554
|
* Compile a command object to a response string and write it to socket.
|
|
2412
2555
|
* If the command object has a skipResponse property, the command is
|
|
@@ -2449,10 +2592,11 @@ class IMAPConnection {
|
|
|
2449
2592
|
response.tag === parsed.tag &&
|
|
2450
2593
|
response.command === 'OK' &&
|
|
2451
2594
|
this.hasPendingExpunge() &&
|
|
2452
|
-
this.
|
|
2595
|
+
this.holdsExpunge(parsed) &&
|
|
2453
2596
|
!(Array.isArray(response.attributes) && response.attributes.some(attr => attr && attr.type === 'SECTION'))) {
|
|
2454
2597
|
// After the output handlers, they might add a response code of their own (MODIFIED of CONDSTORE).
|
|
2455
|
-
// FETCH, STORE, SEARCH and the like can not report the EXPUNGE of another session (RFC 3501 section 7.4.1),
|
|
2598
|
+
// FETCH, STORE, SEARCH and the like can not report the EXPUNGE of another session (RFC 3501 section 7.4.1), nor
|
|
2599
|
+
// can UID SEARCH with message numbers in the criteria (see holdsExpunge),
|
|
2456
2600
|
// EXPUNGEISSUED tells the client to issue NOOP soon (RFC 5530 section 3, RFC 9051 section 7.1)
|
|
2457
2601
|
response.attributes = [{ type: 'SECTION', section: [{ type: 'ATOM', value: 'EXPUNGEISSUED' }] }].concat(response.attributes || []);
|
|
2458
2602
|
}
|
|
@@ -2596,6 +2740,15 @@ class IMAPConnection {
|
|
|
2596
2740
|
* @return {Boolean} true if the command uses message sequence numbers
|
|
2597
2741
|
*/
|
|
2598
2742
|
usesSequenceNumbers(parsed) {
|
|
2743
|
+
// the notification checks ask several times per command, the answer does not change
|
|
2744
|
+
let cached = sequenceNumberUse.get(parsed);
|
|
2745
|
+
if (cached === undefined) {
|
|
2746
|
+
cached = this.findSequenceNumbers(parsed);
|
|
2747
|
+
sequenceNumberUse.set(parsed, cached);
|
|
2748
|
+
}
|
|
2749
|
+
return cached;
|
|
2750
|
+
}
|
|
2751
|
+
findSequenceNumbers(parsed) {
|
|
2599
2752
|
const { sequenceSet, searchCriteria } = this.server.getCommandOptions(parsed.command);
|
|
2600
2753
|
if (sequenceSet !== false) {
|
|
2601
2754
|
// other forms of sequence sets, like "$" of SEARCHRES (RFC 5182 section 2.3), do not use numbers
|
|
@@ -2774,12 +2927,14 @@ class IMAPConnection {
|
|
|
2774
2927
|
this.refusePipelined(parsed, data);
|
|
2775
2928
|
return;
|
|
2776
2929
|
}
|
|
2777
|
-
|
|
2930
|
+
// an unknown SASL mechanism is answered by its own handler, after the central checks (state, script rules)
|
|
2931
|
+
const fallback = /^AUTHENTICATE /i.test(parsed.command) && !this.server.getCommandHandler(parsed.command) ? unsupportedMechanism : undefined;
|
|
2932
|
+
if (fallback || this.server.getCommandHandler(parsed.command)) {
|
|
2778
2933
|
if (this.isAmbiguous(parsed)) {
|
|
2779
2934
|
this.sendStatus(parsed, data, 'BAD', 'Commands with message sequence numbers must wait for the completion of earlier commands');
|
|
2780
2935
|
return;
|
|
2781
2936
|
}
|
|
2782
|
-
const element = { parsed, data };
|
|
2937
|
+
const element = { parsed, data, handler: fallback };
|
|
2783
2938
|
if (scripted) {
|
|
2784
2939
|
// processQueue runs it once the script rule released the queue
|
|
2785
2940
|
this._commandQueue.unshift(element);
|
|
@@ -2789,19 +2944,6 @@ class IMAPConnection {
|
|
|
2789
2944
|
}
|
|
2790
2945
|
this.processQueue();
|
|
2791
2946
|
}
|
|
2792
|
-
else if (/^AUTHENTICATE /i.test(parsed.command)) {
|
|
2793
|
-
// an unsupported mechanism is a NO, not a syntax error (RFC 3501 section 6.2.2)
|
|
2794
|
-
this.send({
|
|
2795
|
-
tag: parsed.tag,
|
|
2796
|
-
command: 'NO',
|
|
2797
|
-
attributes: [
|
|
2798
|
-
{
|
|
2799
|
-
type: 'TEXT',
|
|
2800
|
-
value: 'Unsupported authentication mechanism'
|
|
2801
|
-
}
|
|
2802
|
-
]
|
|
2803
|
-
}, 'UNKNOWN COMMAND', parsed, data);
|
|
2804
|
-
}
|
|
2805
2947
|
else {
|
|
2806
2948
|
this.send({
|
|
2807
2949
|
tag: parsed.tag,
|
|
@@ -2969,19 +3111,25 @@ class IMAPConnection {
|
|
|
2969
3111
|
return next();
|
|
2970
3112
|
}
|
|
2971
3113
|
}
|
|
2972
|
-
if (
|
|
2973
|
-
|
|
2974
|
-
//
|
|
2975
|
-
//
|
|
2976
|
-
//
|
|
2977
|
-
//
|
|
2978
|
-
|
|
3114
|
+
if (this.state === 'Selected' &&
|
|
3115
|
+
(command.startsWith('UID ') || options.noExpunge || options.sequenceSet !== false || options.searchCriteria !== false)) {
|
|
3116
|
+
// A command that refers to messages runs on the message list the client was told about. Only these: CLOSE
|
|
3117
|
+
// must not send pending EXPUNGE responses (RFC 3501 section 6.4.2) and SELECT leaves the old mailbox. RFC 3501 section 5.2:
|
|
3118
|
+
// "A server MUST send mailbox size updates automatically if a mailbox size change is observed during the
|
|
3119
|
+
// processing of a command", so the new messages and flag changes of other sessions are reported first,
|
|
3120
|
+
// before the command resolves its sequence set or search criteria.
|
|
3121
|
+
// EXPUNGE responses may be sent during UID commands (RFC 3501 section 7.4.1), so these report the expunges of
|
|
3122
|
+
// other sessions first as well, then the command runs on the current mailbox, where the UIDs of the expunged
|
|
3123
|
+
// messages do not exist and are ignored (RFC 3501 section 6.4.8). The ghost handling of STORE, COPY and MOVE
|
|
3124
|
+
// (RFC 2180 section 4) only applies to their sequence number forms, these and UID SEARCH with message numbers
|
|
3125
|
+
// in the criteria keep the EXPUNGE responses for later, see processNotifications
|
|
3126
|
+
this.processNotifications(element.parsed, true);
|
|
2979
3127
|
}
|
|
2980
3128
|
// changes made while the handler runs are attributed to this session (the `origin` of notifications)
|
|
2981
3129
|
this.server.withOrigin(this, () => {
|
|
2982
3130
|
try {
|
|
2983
3131
|
const inputHandler = this.inputHandler;
|
|
2984
|
-
this.server.getCommandHandler(element.parsed.command)(this, element.parsed, element.data, next);
|
|
3132
|
+
(element.handler || this.server.getCommandHandler(element.parsed.command))(this, element.parsed, element.data, next);
|
|
2985
3133
|
if (this.inputHandler && this.inputHandler !== inputHandler) {
|
|
2986
3134
|
// the command reads the lines that follow (IDLE, AUTHENTICATE), script rules match them with it
|
|
2987
3135
|
this.inputCommand = { tag: element.parsed.tag, command: element.parsed.command };
|
|
@@ -9,6 +9,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
9
9
|
exports.storageSchema = void 0;
|
|
10
10
|
exports.validateStorage = validateStorage;
|
|
11
11
|
const numbers_js_1 = require("./numbers.js");
|
|
12
|
+
const dates_js_1 = require("./dates.js");
|
|
12
13
|
// keys of plugins, an unknown key that looks like a typo of a known key is refused unless a plugin uses it
|
|
13
14
|
const PLUGIN_KEYS = [
|
|
14
15
|
'acl',
|
|
@@ -151,11 +152,17 @@ function validateStorage(storage) {
|
|
|
151
152
|
if (message.flags !== undefined && typeof message.flags !== 'string' && !isFlagList(message.flags)) {
|
|
152
153
|
fail(path + '.flags', 'must be a flag or a list of flags');
|
|
153
154
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
155
|
+
// FETCH sends the internal date as it is and SORT ARRIVAL reads it, so it must be a date-time of RFC 3501
|
|
156
|
+
// section 9 (month names in any case, they are sent as "Jan", "Feb", ...) or a valid Date. The save date
|
|
157
|
+
// of the SAVEDATE plugin is a date-time too (RFC 8514 section 4.2)
|
|
158
|
+
for (const key of ['internaldate', 'SAVEDATE']) {
|
|
159
|
+
const value = message[key];
|
|
160
|
+
if (value === undefined || value === false || (key === 'SAVEDATE' && value === null)) {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (value instanceof Date ? isNaN(value.getTime()) : !(0, dates_js_1.isDateTime)(value)) {
|
|
164
|
+
fail(path + '.' + key, 'must be a date-time string like "14-Sep-2013 21:22:28 -0300" or a Date, not ' + JSON.stringify(value));
|
|
165
|
+
}
|
|
159
166
|
}
|
|
160
167
|
if (message.recent !== undefined && typeof message.recent !== 'boolean') {
|
|
161
168
|
fail(path + '.recent', 'must be true or false');
|
|
@@ -6,7 +6,10 @@
|
|
|
6
6
|
* what is specific to a session: argument checks, the selected mailbox, read-only state and the responses.
|
|
7
7
|
*/
|
|
8
8
|
import type { IMAPConnection, IMAPServer, Mailbox, Message } from './types.js';
|
|
9
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* the deprecated XOAUTH2 session timeout of the default user, and of a user the control API adds without one. It is
|
|
11
|
+
* stored and listed but never used, access tokens do not expire
|
|
12
|
+
*/
|
|
10
13
|
declare const DEFAULT_SESSION_TIMEOUT: number;
|
|
11
14
|
/** An error of a failed store operation or control API call, `code` is a RFC 5530 response code or INVALID */
|
|
12
15
|
declare class ImapKitError extends Error {
|
|
@@ -94,7 +97,7 @@ declare function createMailbox(server: IMAPServer, path: string): Mailbox;
|
|
|
94
97
|
*
|
|
95
98
|
* @param {Object} server IMAP server
|
|
96
99
|
* @param {String} path Storage name of the mailbox
|
|
97
|
-
* @throws {Error} with a
|
|
100
|
+
* @throws {Error} with a response `code` (NONEXISTENT, CANNOT, HASCHILDREN ...)
|
|
98
101
|
*/
|
|
99
102
|
declare function deleteMailbox(server: IMAPServer, path: string): void;
|
|
100
103
|
/**
|
|
@@ -22,7 +22,10 @@ exports.unsubscribeMailbox = unsubscribeMailbox;
|
|
|
22
22
|
const flags_js_1 = require("./commands/handlers/flags.js");
|
|
23
23
|
const random_js_1 = require("./random.js");
|
|
24
24
|
const numbers_js_1 = require("./numbers.js");
|
|
25
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* the deprecated XOAUTH2 session timeout of the default user, and of a user the control API adds without one. It is
|
|
27
|
+
* stored and listed but never used, access tokens do not expire
|
|
28
|
+
*/
|
|
26
29
|
const DEFAULT_SESSION_TIMEOUT = 3600 * 1000;
|
|
27
30
|
exports.DEFAULT_SESSION_TIMEOUT = DEFAULT_SESSION_TIMEOUT;
|
|
28
31
|
/** An error of a failed store operation or control API call, `code` is a RFC 5530 response code or INVALID */
|
|
@@ -160,7 +163,7 @@ function checkFlags(server, mailbox, flags, stored) {
|
|
|
160
163
|
catch (err) {
|
|
161
164
|
throw storeError(err.message, 'INVALID');
|
|
162
165
|
}
|
|
163
|
-
if (stored &&
|
|
166
|
+
if (stored && !server.isPermanentFlag(mailbox, flag)) {
|
|
164
167
|
throw storeError('Flag ' + flag + ' is not a permanent flag of ' + mailbox.path, 'INVALID');
|
|
165
168
|
}
|
|
166
169
|
if (list.indexOf(flag) < 0) {
|
|
@@ -234,7 +237,7 @@ function createMailbox(server, path) {
|
|
|
234
237
|
*
|
|
235
238
|
* @param {Object} server IMAP server
|
|
236
239
|
* @param {String} path Storage name of the mailbox
|
|
237
|
-
* @throws {Error} with a
|
|
240
|
+
* @throws {Error} with a response `code` (NONEXISTENT, CANNOT, HASCHILDREN ...)
|
|
238
241
|
*/
|
|
239
242
|
function deleteMailbox(server, path) {
|
|
240
243
|
const mailbox = server.getMailbox(path);
|
package/dist/cjs/types.d.ts
CHANGED
|
@@ -212,6 +212,10 @@ export interface UserData {
|
|
|
212
212
|
password?: string | undefined;
|
|
213
213
|
xoauth2?: {
|
|
214
214
|
accessToken?: string | undefined;
|
|
215
|
+
/**
|
|
216
|
+
* @deprecated kept from hoodiecrow and ignored: access tokens never expire. Change the token with
|
|
217
|
+
* `control.updateUser()` to test a client against an expired one
|
|
218
|
+
*/
|
|
215
219
|
sessionTimeout?: number | undefined;
|
|
216
220
|
} | undefined;
|
|
217
221
|
[key: string]: any;
|
|
@@ -79,7 +79,8 @@ function getSection(data, query, global) {
|
|
|
79
79
|
else if (query.section.length > 1) {
|
|
80
80
|
throw new Error((key || 'Part number') + ' does not take any arguments');
|
|
81
81
|
}
|
|
82
|
-
//
|
|
82
|
+
// a part that does not exist is returned as an empty string, like Dovecot does: RFC 3501 and RFC 9051 section
|
|
83
|
+
// 6.4.5 define no error for it, only an empty string for a partial range that starts beyond the end of the text
|
|
83
84
|
const node = path ? resolveNode(data.tree, path, global) : data.tree;
|
|
84
85
|
if (!node) {
|
|
85
86
|
return '';
|