imapkit 5.0.0 → 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 +33 -29
- package/bin/help.txt +3 -2
- package/bin/imapkit.js +15 -5
- 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 +2 -1
- package/dist/cjs/load-plugins.d.ts +3 -1
- package/dist/cjs/load-plugins.js +25 -6
- 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/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 +54 -3
- package/dist/cjs/server.js +210 -88
- 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 +2 -1
- package/dist/esm/load-plugins.d.ts +3 -1
- package/dist/esm/load-plugins.js +25 -6
- 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/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 +54 -3
- package/dist/esm/server.js +212 -90
- 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 +1 -1
package/dist/esm/server.js
CHANGED
|
@@ -7,11 +7,11 @@ import loadPlugins from './load-plugins.js';
|
|
|
7
7
|
import { commands as builtinCommands } from './commands/index.js';
|
|
8
8
|
import { getCommandOptions, commandOptions } from './command-states.js';
|
|
9
9
|
import validateMailboxName from './mailbox-name.js';
|
|
10
|
-
import { MONTHS, monthIndex,
|
|
10
|
+
import { MONTHS, monthIndex, isDateTime } from './dates.js';
|
|
11
11
|
import fetchHandlers from './commands/handlers/fetch.js';
|
|
12
12
|
import { hasSequenceSetKey } from './commands/handlers/search.js';
|
|
13
13
|
import { isSequenceSet } from './numbers.js';
|
|
14
|
-
import { restoreNilAtoms } from './arguments.js';
|
|
14
|
+
import { restoreNilAtoms, isAtom } from './arguments.js';
|
|
15
15
|
import { refuseMissingTarget } from './commands/append.js';
|
|
16
16
|
import { DEFAULT_SESSION_TIMEOUT, storeError, expungeMessages, notifyFlagChanges } from './store-operations.js';
|
|
17
17
|
import { Control } from './control.js';
|
|
@@ -64,6 +64,36 @@ function getResponseTag(line) {
|
|
|
64
64
|
function stateError(command, state) {
|
|
65
65
|
return command + ' is not allowed in the ' + state + ' state';
|
|
66
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Checks if a queued notification changes message sequence numbers by removing messages: an EXPUNGE response, or the
|
|
69
|
+
* EXISTS that follows the EXPUNGE responses of another session (it carries the snapshot of the old message list)
|
|
70
|
+
*
|
|
71
|
+
* @param {Object} notification Queued notification
|
|
72
|
+
* @return {Boolean} true if the notification must wait while EXPUNGE responses are not allowed
|
|
73
|
+
*/
|
|
74
|
+
function isPendingExpunge(notification) {
|
|
75
|
+
return !!notification.mailboxCopy || isAtom(notification.attributes && notification.attributes[1], 'EXPUNGE');
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Checks if a notification is an untagged EXISTS response
|
|
79
|
+
*
|
|
80
|
+
* @param {Object} [notification] Notification to send
|
|
81
|
+
* @return {Boolean} true for an EXISTS response
|
|
82
|
+
*/
|
|
83
|
+
function isExists(notification) {
|
|
84
|
+
return !!notification && !notification.command && isAtom(notification.attributes && notification.attributes[1], 'EXISTS');
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Answers AUTHENTICATE with a mechanism no plugin supports: NO, not a syntax error (RFC 3501 section 6.2.2). It runs
|
|
88
|
+
* through the command queue, so the state check comes first (AUTHENTICATE is only valid in the Not Authenticated
|
|
89
|
+
* state, RFC 3501 section 6.2)
|
|
90
|
+
*/
|
|
91
|
+
// usesSequenceNumbers() of each running command
|
|
92
|
+
const sequenceNumberUse = new WeakMap();
|
|
93
|
+
const unsupportedMechanism = (connection, parsed, data, callback) => {
|
|
94
|
+
connection.sendStatus(parsed, data, 'NO', 'Unsupported authentication mechanism', false, 'UNKNOWN COMMAND');
|
|
95
|
+
callback();
|
|
96
|
+
};
|
|
67
97
|
/**
|
|
68
98
|
* Creates a new IMAP server, call `listen()` on it to start accepting connections
|
|
69
99
|
*
|
|
@@ -163,7 +193,7 @@ class IMAPServer extends Stream {
|
|
|
163
193
|
// plugins register their control operations
|
|
164
194
|
this.control = new Control(this);
|
|
165
195
|
// a quirk preset can leave plugins out (no-move, no-uidplus)
|
|
166
|
-
loadPlugins(this, this.options.plugins,
|
|
196
|
+
loadPlugins(this, this.options.plugins, quirks.removePlugins);
|
|
167
197
|
if (this.options.storage) {
|
|
168
198
|
// a typo in a fixture fails here with the path of the problem
|
|
169
199
|
validateStorage(this.options.storage);
|
|
@@ -292,6 +322,8 @@ class IMAPServer extends Stream {
|
|
|
292
322
|
this.smtpServer = null;
|
|
293
323
|
}
|
|
294
324
|
if (this.restServer) {
|
|
325
|
+
// event streams never end on their own, a graceful shutdown would wait for them forever
|
|
326
|
+
this.restServer.endStreams?.();
|
|
295
327
|
this.restServer.close();
|
|
296
328
|
// keep-alive connections would hold the server open
|
|
297
329
|
if (force) {
|
|
@@ -470,8 +502,7 @@ class IMAPServer extends Stream {
|
|
|
470
502
|
let seen = 0;
|
|
471
503
|
let unseen = 0;
|
|
472
504
|
// flags stay defined in the mailbox once a message had them, see rememberFlags
|
|
473
|
-
const permanentFlags =
|
|
474
|
-
(mailbox.knownFlags || []).forEach(flag => this.ensureFlag(permanentFlags, flag));
|
|
505
|
+
const permanentFlags = this.permanentFlagList(mailbox);
|
|
475
506
|
let recent = 0;
|
|
476
507
|
// \Recent sets of the sessions that have this mailbox selected
|
|
477
508
|
const recentSets = [];
|
|
@@ -517,20 +548,8 @@ class IMAPServer extends Stream {
|
|
|
517
548
|
* @return {Boolean} Returns true if the date string is in IMAP date-time format
|
|
518
549
|
*/
|
|
519
550
|
validateInternalDate(date) {
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
}
|
|
523
|
-
// date-time from RFC 3501 section 9, month names are case-insensitive like all ABNF strings
|
|
524
|
-
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);
|
|
525
|
-
if (!match) {
|
|
526
|
-
return false;
|
|
527
|
-
}
|
|
528
|
-
// the values must also make a real date and time
|
|
529
|
-
return (isRealDate(match[1], monthIndex(match[2]), match[3]) &&
|
|
530
|
-
Number(match[4]) < 24 &&
|
|
531
|
-
Number(match[5]) < 60 &&
|
|
532
|
-
Number(match[6]) < 61 &&
|
|
533
|
-
Number(match[8]) < 60);
|
|
551
|
+
// date-time from RFC 3501 section 9
|
|
552
|
+
return isDateTime(date);
|
|
534
553
|
}
|
|
535
554
|
/**
|
|
536
555
|
* Converts a date object to a valid date-time string format
|
|
@@ -961,6 +980,32 @@ class IMAPServer extends Stream {
|
|
|
961
980
|
const knownFlags = (mailbox.knownFlags = mailbox.knownFlags || []);
|
|
962
981
|
flags.forEach(flag => this.ensureFlag(knownFlags, flag));
|
|
963
982
|
}
|
|
983
|
+
/**
|
|
984
|
+
* Checks if a flag can be stored on messages of a mailbox: any flag when the mailbox allows new keywords
|
|
985
|
+
* (PERMANENTFLAGS has `\*`), otherwise only a flag in the PERMANENTFLAGS list that getStatus() builds, the
|
|
986
|
+
* `permanentFlags` of the mailbox and every flag its messages had. RFC 3501 section 7.1 (RFC 9051 section 7.1):
|
|
987
|
+
* PERMANENTFLAGS "indicates which of the known flags the client can change permanently", STORE ignores the others
|
|
988
|
+
*
|
|
989
|
+
* @param {Object} mailbox Mailbox object
|
|
990
|
+
* @param {String} flag Normalized flag
|
|
991
|
+
* @return {Boolean} true if the flag is a permanent flag of the mailbox
|
|
992
|
+
*/
|
|
993
|
+
isPermanentFlag(mailbox, flag) {
|
|
994
|
+
// the lists of permanentFlagList(), without building it: this runs for every flag STORE and APPEND set
|
|
995
|
+
return (mailbox.allowPermanentFlags || (mailbox.permanentFlags || []).indexOf(flag) >= 0 || (!!mailbox.knownFlags && mailbox.knownFlags.indexOf(flag) >= 0));
|
|
996
|
+
}
|
|
997
|
+
/**
|
|
998
|
+
* The flags of the PERMANENTFLAGS list (without `\*`): the `permanentFlags` of the mailbox and every flag its
|
|
999
|
+
* messages have or had (`knownFlags`, see rememberFlags)
|
|
1000
|
+
*
|
|
1001
|
+
* @param {Object} mailbox Mailbox object
|
|
1002
|
+
* @return {Array} flags, a new list
|
|
1003
|
+
*/
|
|
1004
|
+
permanentFlagList(mailbox) {
|
|
1005
|
+
const list = [].concat(mailbox.permanentFlags || []);
|
|
1006
|
+
(mailbox.knownFlags || []).forEach(flag => this.ensureFlag(list, flag));
|
|
1007
|
+
return list;
|
|
1008
|
+
}
|
|
964
1009
|
/**
|
|
965
1010
|
* The current time for dates the server sets itself (INTERNALDATE of a message without one, SAVEDATE). The `now`
|
|
966
1011
|
* option fixes it for repeatable tests: a Date, a timestamp, or a function that returns one
|
|
@@ -1032,9 +1077,10 @@ class IMAPServer extends Stream {
|
|
|
1032
1077
|
*/
|
|
1033
1078
|
appendMessage(path, flags, internaldate, raw, ignoreConnection, properties) {
|
|
1034
1079
|
const mailbox = typeof path === 'string' ? this.getMailbox(path) : path;
|
|
1035
|
-
// processMessage() below sets the UID
|
|
1080
|
+
// processMessage() below sets the UID. Flags the mailbox can not store are left out (APPEND, COPY: the flags
|
|
1081
|
+
// SHOULD be set, RFC 3501 sections 6.3.11 and 6.4.7), as STORE ignores them (RFC 3501 section 7.1, PERMANENTFLAGS)
|
|
1036
1082
|
const message = Object.assign({}, properties, {
|
|
1037
|
-
flags: flags,
|
|
1083
|
+
flags: flags.filter(flag => this.isPermanentFlag(mailbox, flag)),
|
|
1038
1084
|
internaldate: internaldate,
|
|
1039
1085
|
raw: raw,
|
|
1040
1086
|
recent: true
|
|
@@ -1220,49 +1266,48 @@ class IMAPServer extends Stream {
|
|
|
1220
1266
|
return flags;
|
|
1221
1267
|
}
|
|
1222
1268
|
matchFolders(referenceName, match, exportName, folders) {
|
|
1223
|
-
let includeINBOX = false;
|
|
1224
1269
|
const source = folders || this.folderCache;
|
|
1225
1270
|
const toName = exportName || ((name) => name);
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
//
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
const name = toName(key);
|
|
1236
|
-
if (key !== 'INBOX' && reference.substr(0, name.length) === name && (nsKey === false || name.length > nsName.length)) {
|
|
1237
|
-
nsKey = key;
|
|
1238
|
-
nsName = name;
|
|
1239
|
-
}
|
|
1240
|
-
}
|
|
1241
|
-
if (nsKey === false) {
|
|
1242
|
-
return [];
|
|
1243
|
-
}
|
|
1244
|
-
const namespace = this.storage[nsKey];
|
|
1245
|
-
const lookup = reference + match;
|
|
1246
|
-
const result = [];
|
|
1247
|
-
const pattern = '^' +
|
|
1271
|
+
// RFC 3501 section 6.3.8 (RFC 9051 section 6.3.9): "An empty ("" string) reference name argument
|
|
1272
|
+
// indicates that the mailbox name is interpreted as by SELECT", and without break out characters
|
|
1273
|
+
// "the canonical form is normally the reference name appended with the mailbox name". The pattern
|
|
1274
|
+
// matches full mailbox names, also in a personal namespace with a prefix such as "INBOX.". With the
|
|
1275
|
+
// namespace convention "#" is a break out character "and must be treated as such" (RFC 3501 section 6.3.8,
|
|
1276
|
+
// RFC 9051 section 6.3.9): a pattern that starts with it is a name of its own, the reference is ignored
|
|
1277
|
+
const lookup = match.charAt(0) === '#' ? match : (referenceName || '') + match;
|
|
1278
|
+
// "%" does not match the hierarchy delimiter, which is the one of the namespace a name belongs to
|
|
1279
|
+
const toRegExp = (separator, flags = '') => new RegExp('^' +
|
|
1248
1280
|
lookup
|
|
1249
1281
|
// escape regex symbols
|
|
1250
1282
|
.replace(/([\\^$+?!.():=[\]{}|,-])/g, '\\$1')
|
|
1251
1283
|
.replace(/[*]/g, '.*')
|
|
1252
|
-
.replace(/[%]/g, '[^' +
|
|
1253
|
-
'$';
|
|
1254
|
-
|
|
1255
|
-
//
|
|
1256
|
-
|
|
1284
|
+
.replace(/[%]/g, '[^' + separator.replace(/([\\^$+*?!.():=[\]{}|,-])/g, '\\$1') + ']*') +
|
|
1285
|
+
'$', flags);
|
|
1286
|
+
// RFC 3501 section 6.3.8 allows to "hide" otherwise accessible mailboxes from the wildcards: the
|
|
1287
|
+
// mailboxes of namespaces other than the personal one are only matched when the pattern names
|
|
1288
|
+
// the prefix of the namespace before any wildcard (LIST "" "user.%", LIST "#news." "*"). The pattern
|
|
1289
|
+
// of every namespace that is matched, null for a hidden one
|
|
1290
|
+
const fixedPrefix = lookup.replace(/[*%].*$/, '');
|
|
1291
|
+
const queries = new Map();
|
|
1292
|
+
Object.keys(this.storage).forEach(key => {
|
|
1293
|
+
const name = toName(key);
|
|
1294
|
+
const visible = key === this.referenceNamespace || fixedPrefix.substr(0, name.length) === name;
|
|
1295
|
+
queries.set(key, visible ? toRegExp(this.storage[key].separator) : null);
|
|
1296
|
+
});
|
|
1297
|
+
const result = [];
|
|
1298
|
+
// "The special name INBOX is included in the output from LIST, if [...] the uppercase string "INBOX"
|
|
1299
|
+
// matches the interpreted reference and mailbox name arguments", INBOX is case-insensitive
|
|
1300
|
+
if (source.INBOX && toRegExp((this.storage.INBOX && this.storage.INBOX.separator) || '/', 'i').test('INBOX')) {
|
|
1257
1301
|
result.push(source.INBOX);
|
|
1258
1302
|
}
|
|
1259
1303
|
Object.keys(source).forEach(path => {
|
|
1260
1304
|
const folder = source[path];
|
|
1261
|
-
|
|
1305
|
+
const query = path !== 'INBOX' && folder.namespace !== false && folder.namespace !== 'INBOX' ? queries.get(folder.namespace) : null;
|
|
1306
|
+
if (!query) {
|
|
1262
1307
|
return;
|
|
1263
1308
|
}
|
|
1264
1309
|
const name = toName(path);
|
|
1265
|
-
if (
|
|
1310
|
+
if (query.test(name) && (folder.flags.indexOf('\\NonExistent') < 0 || name === lookup)) {
|
|
1266
1311
|
result.push(folder);
|
|
1267
1312
|
}
|
|
1268
1313
|
});
|
|
@@ -2119,7 +2164,7 @@ class IMAPConnection {
|
|
|
2119
2164
|
* @return {Boolean} true if an EXPUNGE response is pending
|
|
2120
2165
|
*/
|
|
2121
2166
|
hasPendingExpunge() {
|
|
2122
|
-
return this.notificationQueue.some(
|
|
2167
|
+
return this.notificationQueue.some(isPendingExpunge);
|
|
2123
2168
|
}
|
|
2124
2169
|
/**
|
|
2125
2170
|
* Tells the other sessions that have the selected mailbox open about changed flags, they get
|
|
@@ -2326,22 +2371,52 @@ class IMAPConnection {
|
|
|
2326
2371
|
prepareNotifications(queue) {
|
|
2327
2372
|
return queue;
|
|
2328
2373
|
}
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2374
|
+
/**
|
|
2375
|
+
* Checks if EXPUNGE responses must wait while a command runs: during FETCH, STORE and SEARCH (RFC 3501 section
|
|
2376
|
+
* 7.4.1), during the commands that extensions add to this list (see the noExpunge command option), and during UID
|
|
2377
|
+
* SEARCH with message numbers in the search criteria (RFC 7162 section 3.2.10.2 for VANISHED, EXPUNGE may wait as
|
|
2378
|
+
* well, RFC 3501 only allows it during UID commands)
|
|
2379
|
+
*
|
|
2380
|
+
* @param {Object} data Parsed command
|
|
2381
|
+
* @return {Boolean} true if the command holds back EXPUNGE responses
|
|
2382
|
+
*/
|
|
2383
|
+
holdsExpunge(data) {
|
|
2384
|
+
const options = this.server.getCommandOptions(data.command);
|
|
2385
|
+
return options.noExpunge || (options.searchCriteria !== false && this.usesSequenceNumbers(data));
|
|
2386
|
+
}
|
|
2387
|
+
/**
|
|
2388
|
+
* Sends the queued notifications. During a command that holds back EXPUNGE responses (see holdsExpunge), or with
|
|
2389
|
+
* `beforeCommand` before a command that refers to messages by sequence number, only the notifications queued before
|
|
2390
|
+
* the first pending EXPUNGE go out: new messages (EXISTS) and flag changes. RFC 3501 section 5.2: "A server MUST send
|
|
2391
|
+
* mailbox size updates automatically if a mailbox size change is observed during the processing of a command",
|
|
2392
|
+
* section 7.4.1 forbids only EXPUNGE during FETCH, STORE and SEARCH. What was queued after the EXPUNGE waits with it,
|
|
2393
|
+
* an EXISTS sent before it would describe a list that the client can not know yet
|
|
2394
|
+
*
|
|
2395
|
+
* @param {Object} [data] Parsed command that runs, or null between commands
|
|
2396
|
+
* @param {Boolean} [beforeCommand] true when the command has not run yet, then a command with message sequence
|
|
2397
|
+
* numbers (COPY, MOVE) also waits with the EXPUNGE responses, its numbers refer to the messages before them
|
|
2398
|
+
*/
|
|
2399
|
+
processNotifications(data, beforeCommand) {
|
|
2338
2400
|
if (!this.notificationQueue.length) {
|
|
2339
2401
|
return;
|
|
2340
2402
|
}
|
|
2341
|
-
|
|
2342
|
-
this.notificationQueue = [];
|
|
2403
|
+
let queue = this.notificationQueue;
|
|
2343
2404
|
// Flag updates use the sequence numbers this session knows: before the EXPUNGE responses of
|
|
2344
2405
|
// the snapshot are sent, the snapshot, afterwards the current message list
|
|
2406
|
+
const snapshot = queue.find(notification => notification.mailboxCopy);
|
|
2407
|
+
let held = [];
|
|
2408
|
+
if (data && (this.holdsExpunge(data) || (beforeCommand && this.usesSequenceNumbers(data)))) {
|
|
2409
|
+
const first = queue.findIndex(isPendingExpunge);
|
|
2410
|
+
if (first >= 0) {
|
|
2411
|
+
held = queue.slice(first);
|
|
2412
|
+
queue = queue.slice(0, first);
|
|
2413
|
+
}
|
|
2414
|
+
if (!queue.length) {
|
|
2415
|
+
return;
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
this.notificationQueue = held;
|
|
2419
|
+
queue = this.prepareNotifications(queue);
|
|
2345
2420
|
const snapshotIndex = queue.findIndex(notification => notification.mailboxCopy);
|
|
2346
2421
|
const sequenceMaps = new Map();
|
|
2347
2422
|
const getSequence = (messages) => {
|
|
@@ -2355,16 +2430,58 @@ class IMAPConnection {
|
|
|
2355
2430
|
const current = this.selectedMailbox ? this.selectedMailbox.messages : [];
|
|
2356
2431
|
// a message changed several times is reported once, its FETCH response carries the current flags
|
|
2357
2432
|
const reported = new Set();
|
|
2433
|
+
// before the snapshot (or with all of it still held back, then the split at the first pending expunge leaves
|
|
2434
|
+
// no snapshot in the queue) the session knows the old list
|
|
2435
|
+
const sessionList = (i) => (snapshot && (snapshotIndex < 0 || i < snapshotIndex) ? snapshot.mailboxCopy : current);
|
|
2436
|
+
// the last EXISTS response of a run of EXISTS responses, and if the run announces new messages
|
|
2437
|
+
let lastExists = -1;
|
|
2438
|
+
let newMessages = false;
|
|
2358
2439
|
queue.forEach((notification, i) => {
|
|
2359
2440
|
if (notification.flagUpdate) {
|
|
2360
|
-
|
|
2361
|
-
this.sendFlagUpdate(notification.flagUpdate, getSequence(i < snapshotIndex ? queue[snapshotIndex].mailboxCopy : current), reported);
|
|
2441
|
+
this.sendFlagUpdate(notification.flagUpdate, getSequence(sessionList(i)), reported);
|
|
2362
2442
|
}
|
|
2363
2443
|
else {
|
|
2364
2444
|
this.send(notification);
|
|
2365
2445
|
}
|
|
2446
|
+
if (isExists(notification)) {
|
|
2447
|
+
lastExists = i;
|
|
2448
|
+
// the EXISTS after the EXPUNGE responses of another session carries the snapshot instead of a message
|
|
2449
|
+
newMessages = newMessages || !!notification.message;
|
|
2450
|
+
}
|
|
2451
|
+
const next = queue[i + 1];
|
|
2452
|
+
if (lastExists < 0 || isExists(next) || (next && next.fetchedMessage)) {
|
|
2453
|
+
// not the end of a run of EXISTS responses (and the FETCH responses NOTIFY sends after them)
|
|
2454
|
+
return;
|
|
2455
|
+
}
|
|
2456
|
+
// RFC 3501 section 7.3.2: the RECENT response "occurs as a result of a SELECT or EXAMINE command, and if the
|
|
2457
|
+
// size of the mailbox changes (e.g., new messages)". One for consecutive EXISTS responses that announce new
|
|
2458
|
+
// messages, with the number of \Recent messages among those the client was told about. After expunges the
|
|
2459
|
+
// EXPUNGE responses report the change (RFC 3501 section 7.4.1). It goes after the FETCH responses that NOTIFY
|
|
2460
|
+
// sends for new messages, RFC 5465 section 5.2: "an unsolicited EXISTS response, followed by an unsolicited
|
|
2461
|
+
// FETCH response [...] The server MAY also send a RECENT response"
|
|
2462
|
+
if (newMessages) {
|
|
2463
|
+
this.sendRecent(Number(queue[lastExists].attributes[0]), getSequence(sessionList(lastExists)));
|
|
2464
|
+
}
|
|
2465
|
+
lastExists = -1;
|
|
2466
|
+
newMessages = false;
|
|
2366
2467
|
});
|
|
2367
2468
|
}
|
|
2469
|
+
/**
|
|
2470
|
+
* Sends an untagged RECENT response with the number of \Recent messages among the first `count` messages
|
|
2471
|
+
*
|
|
2472
|
+
* @param {Number} count Number of messages the client was told about
|
|
2473
|
+
* @param {Map} sequence Message to the sequence number this session knows it by
|
|
2474
|
+
*/
|
|
2475
|
+
sendRecent(count, sequence) {
|
|
2476
|
+
let recent = 0;
|
|
2477
|
+
(this.recent || new Set()).forEach(message => {
|
|
2478
|
+
const seq = sequence.get(message);
|
|
2479
|
+
if (seq && seq <= count) {
|
|
2480
|
+
recent++;
|
|
2481
|
+
}
|
|
2482
|
+
});
|
|
2483
|
+
this.send({ tag: '*', notification: true, attributes: [recent, { type: 'ATOM', value: 'RECENT' }] }, 'RECENT NOTIFICATION');
|
|
2484
|
+
}
|
|
2368
2485
|
/**
|
|
2369
2486
|
* Compile a command object to a response string and write it to socket.
|
|
2370
2487
|
* If the command object has a skipResponse property, the command is
|
|
@@ -2407,10 +2524,11 @@ class IMAPConnection {
|
|
|
2407
2524
|
response.tag === parsed.tag &&
|
|
2408
2525
|
response.command === 'OK' &&
|
|
2409
2526
|
this.hasPendingExpunge() &&
|
|
2410
|
-
this.
|
|
2527
|
+
this.holdsExpunge(parsed) &&
|
|
2411
2528
|
!(Array.isArray(response.attributes) && response.attributes.some(attr => attr && attr.type === 'SECTION'))) {
|
|
2412
2529
|
// After the output handlers, they might add a response code of their own (MODIFIED of CONDSTORE).
|
|
2413
|
-
// FETCH, STORE, SEARCH and the like can not report the EXPUNGE of another session (RFC 3501 section 7.4.1),
|
|
2530
|
+
// FETCH, STORE, SEARCH and the like can not report the EXPUNGE of another session (RFC 3501 section 7.4.1), nor
|
|
2531
|
+
// can UID SEARCH with message numbers in the criteria (see holdsExpunge),
|
|
2414
2532
|
// EXPUNGEISSUED tells the client to issue NOOP soon (RFC 5530 section 3, RFC 9051 section 7.1)
|
|
2415
2533
|
response.attributes = [{ type: 'SECTION', section: [{ type: 'ATOM', value: 'EXPUNGEISSUED' }] }].concat(response.attributes || []);
|
|
2416
2534
|
}
|
|
@@ -2554,6 +2672,15 @@ class IMAPConnection {
|
|
|
2554
2672
|
* @return {Boolean} true if the command uses message sequence numbers
|
|
2555
2673
|
*/
|
|
2556
2674
|
usesSequenceNumbers(parsed) {
|
|
2675
|
+
// the notification checks ask several times per command, the answer does not change
|
|
2676
|
+
let cached = sequenceNumberUse.get(parsed);
|
|
2677
|
+
if (cached === undefined) {
|
|
2678
|
+
cached = this.findSequenceNumbers(parsed);
|
|
2679
|
+
sequenceNumberUse.set(parsed, cached);
|
|
2680
|
+
}
|
|
2681
|
+
return cached;
|
|
2682
|
+
}
|
|
2683
|
+
findSequenceNumbers(parsed) {
|
|
2557
2684
|
const { sequenceSet, searchCriteria } = this.server.getCommandOptions(parsed.command);
|
|
2558
2685
|
if (sequenceSet !== false) {
|
|
2559
2686
|
// other forms of sequence sets, like "$" of SEARCHRES (RFC 5182 section 2.3), do not use numbers
|
|
@@ -2732,12 +2859,14 @@ class IMAPConnection {
|
|
|
2732
2859
|
this.refusePipelined(parsed, data);
|
|
2733
2860
|
return;
|
|
2734
2861
|
}
|
|
2735
|
-
|
|
2862
|
+
// an unknown SASL mechanism is answered by its own handler, after the central checks (state, script rules)
|
|
2863
|
+
const fallback = /^AUTHENTICATE /i.test(parsed.command) && !this.server.getCommandHandler(parsed.command) ? unsupportedMechanism : undefined;
|
|
2864
|
+
if (fallback || this.server.getCommandHandler(parsed.command)) {
|
|
2736
2865
|
if (this.isAmbiguous(parsed)) {
|
|
2737
2866
|
this.sendStatus(parsed, data, 'BAD', 'Commands with message sequence numbers must wait for the completion of earlier commands');
|
|
2738
2867
|
return;
|
|
2739
2868
|
}
|
|
2740
|
-
const element = { parsed, data };
|
|
2869
|
+
const element = { parsed, data, handler: fallback };
|
|
2741
2870
|
if (scripted) {
|
|
2742
2871
|
// processQueue runs it once the script rule released the queue
|
|
2743
2872
|
this._commandQueue.unshift(element);
|
|
@@ -2747,19 +2876,6 @@ class IMAPConnection {
|
|
|
2747
2876
|
}
|
|
2748
2877
|
this.processQueue();
|
|
2749
2878
|
}
|
|
2750
|
-
else if (/^AUTHENTICATE /i.test(parsed.command)) {
|
|
2751
|
-
// an unsupported mechanism is a NO, not a syntax error (RFC 3501 section 6.2.2)
|
|
2752
|
-
this.send({
|
|
2753
|
-
tag: parsed.tag,
|
|
2754
|
-
command: 'NO',
|
|
2755
|
-
attributes: [
|
|
2756
|
-
{
|
|
2757
|
-
type: 'TEXT',
|
|
2758
|
-
value: 'Unsupported authentication mechanism'
|
|
2759
|
-
}
|
|
2760
|
-
]
|
|
2761
|
-
}, 'UNKNOWN COMMAND', parsed, data);
|
|
2762
|
-
}
|
|
2763
2879
|
else {
|
|
2764
2880
|
this.send({
|
|
2765
2881
|
tag: parsed.tag,
|
|
@@ -2927,19 +3043,25 @@ class IMAPConnection {
|
|
|
2927
3043
|
return next();
|
|
2928
3044
|
}
|
|
2929
3045
|
}
|
|
2930
|
-
if (
|
|
2931
|
-
|
|
2932
|
-
//
|
|
2933
|
-
//
|
|
2934
|
-
//
|
|
2935
|
-
//
|
|
2936
|
-
|
|
3046
|
+
if (this.state === 'Selected' &&
|
|
3047
|
+
(command.startsWith('UID ') || options.noExpunge || options.sequenceSet !== false || options.searchCriteria !== false)) {
|
|
3048
|
+
// A command that refers to messages runs on the message list the client was told about. Only these: CLOSE
|
|
3049
|
+
// must not send pending EXPUNGE responses (RFC 3501 section 6.4.2) and SELECT leaves the old mailbox. RFC 3501 section 5.2:
|
|
3050
|
+
// "A server MUST send mailbox size updates automatically if a mailbox size change is observed during the
|
|
3051
|
+
// processing of a command", so the new messages and flag changes of other sessions are reported first,
|
|
3052
|
+
// before the command resolves its sequence set or search criteria.
|
|
3053
|
+
// EXPUNGE responses may be sent during UID commands (RFC 3501 section 7.4.1), so these report the expunges of
|
|
3054
|
+
// other sessions first as well, then the command runs on the current mailbox, where the UIDs of the expunged
|
|
3055
|
+
// messages do not exist and are ignored (RFC 3501 section 6.4.8). The ghost handling of STORE, COPY and MOVE
|
|
3056
|
+
// (RFC 2180 section 4) only applies to their sequence number forms, these and UID SEARCH with message numbers
|
|
3057
|
+
// in the criteria keep the EXPUNGE responses for later, see processNotifications
|
|
3058
|
+
this.processNotifications(element.parsed, true);
|
|
2937
3059
|
}
|
|
2938
3060
|
// changes made while the handler runs are attributed to this session (the `origin` of notifications)
|
|
2939
3061
|
this.server.withOrigin(this, () => {
|
|
2940
3062
|
try {
|
|
2941
3063
|
const inputHandler = this.inputHandler;
|
|
2942
|
-
this.server.getCommandHandler(element.parsed.command)(this, element.parsed, element.data, next);
|
|
3064
|
+
(element.handler || this.server.getCommandHandler(element.parsed.command))(this, element.parsed, element.data, next);
|
|
2943
3065
|
if (this.inputHandler && this.inputHandler !== inputHandler) {
|
|
2944
3066
|
// the command reads the lines that follow (IDLE, AUTHENTICATE), script rules match them with it
|
|
2945
3067
|
this.inputCommand = { tag: element.parsed.tag, command: element.parsed.command };
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* messages (acl, metadata, MODSEQ ...), so other keys are allowed, only the types of the known keys are checked
|
|
6
6
|
*/
|
|
7
7
|
import { MAX_NUMBER } from './numbers.js';
|
|
8
|
+
import { isDateTime } from './dates.js';
|
|
8
9
|
// keys of plugins, an unknown key that looks like a typo of a known key is refused unless a plugin uses it
|
|
9
10
|
const PLUGIN_KEYS = [
|
|
10
11
|
'acl',
|
|
@@ -147,11 +148,17 @@ export function validateStorage(storage) {
|
|
|
147
148
|
if (message.flags !== undefined && typeof message.flags !== 'string' && !isFlagList(message.flags)) {
|
|
148
149
|
fail(path + '.flags', 'must be a flag or a list of flags');
|
|
149
150
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
151
|
+
// FETCH sends the internal date as it is and SORT ARRIVAL reads it, so it must be a date-time of RFC 3501
|
|
152
|
+
// section 9 (month names in any case, they are sent as "Jan", "Feb", ...) or a valid Date. The save date
|
|
153
|
+
// of the SAVEDATE plugin is a date-time too (RFC 8514 section 4.2)
|
|
154
|
+
for (const key of ['internaldate', 'SAVEDATE']) {
|
|
155
|
+
const value = message[key];
|
|
156
|
+
if (value === undefined || value === false || (key === 'SAVEDATE' && value === null)) {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (value instanceof Date ? isNaN(value.getTime()) : !isDateTime(value)) {
|
|
160
|
+
fail(path + '.' + key, 'must be a date-time string like "14-Sep-2013 21:22:28 -0300" or a Date, not ' + JSON.stringify(value));
|
|
161
|
+
}
|
|
155
162
|
}
|
|
156
163
|
if (message.recent !== undefined && typeof message.recent !== 'boolean') {
|
|
157
164
|
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
|
/**
|
|
@@ -8,7 +8,10 @@
|
|
|
8
8
|
import { normalizeSystemFlag, checkSystemFlags } from './commands/handlers/flags.js';
|
|
9
9
|
import { seededRandom } from './random.js';
|
|
10
10
|
import { MAX_NUMBER } from './numbers.js';
|
|
11
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* the deprecated XOAUTH2 session timeout of the default user, and of a user the control API adds without one. It is
|
|
13
|
+
* stored and listed but never used, access tokens do not expire
|
|
14
|
+
*/
|
|
12
15
|
const DEFAULT_SESSION_TIMEOUT = 3600 * 1000;
|
|
13
16
|
/** An error of a failed store operation or control API call, `code` is a RFC 5530 response code or INVALID */
|
|
14
17
|
class ImapKitError extends Error {
|
|
@@ -144,7 +147,7 @@ function checkFlags(server, mailbox, flags, stored) {
|
|
|
144
147
|
catch (err) {
|
|
145
148
|
throw storeError(err.message, 'INVALID');
|
|
146
149
|
}
|
|
147
|
-
if (stored &&
|
|
150
|
+
if (stored && !server.isPermanentFlag(mailbox, flag)) {
|
|
148
151
|
throw storeError('Flag ' + flag + ' is not a permanent flag of ' + mailbox.path, 'INVALID');
|
|
149
152
|
}
|
|
150
153
|
if (list.indexOf(flag) < 0) {
|
|
@@ -218,7 +221,7 @@ function createMailbox(server, path) {
|
|
|
218
221
|
*
|
|
219
222
|
* @param {Object} server IMAP server
|
|
220
223
|
* @param {String} path Storage name of the mailbox
|
|
221
|
-
* @throws {Error} with a
|
|
224
|
+
* @throws {Error} with a response `code` (NONEXISTENT, CANNOT, HASCHILDREN ...)
|
|
222
225
|
*/
|
|
223
226
|
function deleteMailbox(server, path) {
|
|
224
227
|
const mailbox = server.getMailbox(path);
|
package/dist/esm/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;
|
package/dist/plugin-help.json
CHANGED
|
@@ -104,7 +104,7 @@
|
|
|
104
104
|
],
|
|
105
105
|
"METADATA-SERVER": [
|
|
106
106
|
"Adds METADATA-SERVER [RFC5464] capability, like METADATA but",
|
|
107
|
-
"only for server annotations (mailbox name \"\"). With METADATA",
|
|
107
|
+
"only for server annotations (mailbox name \"\"), loads ENABLE. With METADATA",
|
|
108
108
|
"also loaded, only METADATA is advertised"
|
|
109
109
|
],
|
|
110
110
|
"METADATA": [
|
|
@@ -114,7 +114,8 @@
|
|
|
114
114
|
"\"metadata\" option. Limits: \"metadataMaxSize\" (octets per value,",
|
|
115
115
|
"default 65536), \"metadataMaxEntries\" (per mailbox and for the",
|
|
116
116
|
"server, default 100). \"metadataPrivate\": false turns /private",
|
|
117
|
-
"entries off. ENABLE METADATA turns on unsolicited METADATA responses"
|
|
117
|
+
"entries off. ENABLE METADATA turns on unsolicited METADATA responses",
|
|
118
|
+
"(loads the ENABLE plugin, RFC 5464 section 4.1)"
|
|
118
119
|
],
|
|
119
120
|
"MOVE": [
|
|
120
121
|
"Adds MOVE [RFC6851] capability"
|