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/control.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* unsolicited FETCH, BYE, NOTIFY events, CONDSTORE mod-sequences), with no session as its origin. The API is the
|
|
5
5
|
* operator: ACL does not apply, but every argument is checked. Mailboxes are addressed by their storage name
|
|
6
6
|
* (modified UTF-7), messages by mailbox and UID. Methods return plain data and throw an ImapKitError with a `code`:
|
|
7
|
-
* NONEXISTENT, ALREADYEXISTS, INVALID, or
|
|
7
|
+
* NONEXISTENT, ALREADYEXISTS, INVALID, or the response code of a failed mailbox operation (CANNOT, HASCHILDREN ...)
|
|
8
8
|
*/
|
|
9
9
|
import { ImapKitError } from './store-operations.js';
|
|
10
10
|
import type { FlagMode, UidValidityOptions } from './store-operations.js';
|
|
@@ -22,7 +22,7 @@ interface MailboxInfo {
|
|
|
22
22
|
unseen: number;
|
|
23
23
|
uidnext: number;
|
|
24
24
|
uidvalidity: number;
|
|
25
|
-
/** flags
|
|
25
|
+
/** the flags of the FLAGS response, SELECT adds \* to PERMANENTFLAGS when new keywords are allowed */
|
|
26
26
|
permanentFlags: string[];
|
|
27
27
|
/** only with CONDSTORE */
|
|
28
28
|
highestModseq?: number;
|
|
@@ -67,13 +67,8 @@ interface NewMessage {
|
|
|
67
67
|
/** a Date or a RFC 3501 date-time string, the current time if not set */
|
|
68
68
|
internaldate?: Date | string | undefined;
|
|
69
69
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
xoauth2?: {
|
|
73
|
-
accessToken?: string | undefined;
|
|
74
|
-
sessionTimeout?: number | undefined;
|
|
75
|
-
} | undefined;
|
|
76
|
-
}
|
|
70
|
+
/** The credentials of a user, see UserData */
|
|
71
|
+
type UserOptions = Pick<UserData, 'password' | 'xoauth2'>;
|
|
77
72
|
/** What a REST route handler gets, see src/rest.ts */
|
|
78
73
|
interface RouteRequest {
|
|
79
74
|
params: Record<string, string>;
|
package/dist/esm/control.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* unsolicited FETCH, BYE, NOTIFY events, CONDSTORE mod-sequences), with no session as its origin. The API is the
|
|
5
5
|
* operator: ACL does not apply, but every argument is checked. Mailboxes are addressed by their storage name
|
|
6
6
|
* (modified UTF-7), messages by mailbox and UID. Methods return plain data and throw an ImapKitError with a `code`:
|
|
7
|
-
* NONEXISTENT, ALREADYEXISTS, INVALID, or
|
|
7
|
+
* NONEXISTENT, ALREADYEXISTS, INVALID, or the response code of a failed mailbox operation (CANNOT, HASCHILDREN ...)
|
|
8
8
|
*/
|
|
9
9
|
import validateMailboxName from './mailbox-name.js';
|
|
10
10
|
import { DEFAULT_SESSION_TIMEOUT, ImapKitError, expungeMessages, checkFlags, changeFlags, createMailbox, deleteMailbox, renameMailbox, resetUidValidity, subscribeMailbox, unsubscribeMailbox } from './store-operations.js';
|
package/dist/esm/dates.d.ts
CHANGED
|
@@ -36,6 +36,14 @@ declare function isRealDate(day: number | string | undefined, month: number, yea
|
|
|
36
36
|
* @return {String|Boolean} the date, or false for an impossible date
|
|
37
37
|
*/
|
|
38
38
|
declare function dateKey(day: number | string, month: number, year: number | string): string | false;
|
|
39
|
+
/**
|
|
40
|
+
* Checks a date-time string of RFC 3501 section 9, like an INTERNALDATE ("14-Sep-2013 21:22:28 -0300"):
|
|
41
|
+
* date-time = DQUOTE date-day-fixed "-" date-month "-" date-year SP time SP zone DQUOTE
|
|
42
|
+
*
|
|
43
|
+
* @param {String} value Value to check
|
|
44
|
+
* @return {Boolean} true if the value is a date-time string of a real date and time
|
|
45
|
+
*/
|
|
46
|
+
declare function isDateTime(value: unknown): boolean;
|
|
39
47
|
/**
|
|
40
48
|
* Parses a date-time value of the RFC 3501 section 9 form, like an INTERNALDATE ("14-Sep-2013 21:22:28 -0300").
|
|
41
49
|
* A value with only the date part is accepted as well, its time fields are left undefined
|
|
@@ -61,4 +69,4 @@ declare function parseHeaderDate(header: string | undefined): DateParts | null;
|
|
|
61
69
|
* @return {Number} timestamp
|
|
62
70
|
*/
|
|
63
71
|
declare function toTimestamp(date: DateParts): number;
|
|
64
|
-
export { MONTHS, monthIndex, isRealDate, dateKey, parseDateTime, parseHeaderDate, toTimestamp };
|
|
72
|
+
export { MONTHS, monthIndex, isRealDate, isDateTime, dateKey, parseDateTime, parseHeaderDate, toTimestamp };
|
package/dist/esm/dates.js
CHANGED
|
@@ -40,6 +40,25 @@ function dateKey(day, month, year) {
|
|
|
40
40
|
}
|
|
41
41
|
return String(year).padStart(4, '0') + '-' + String(month + 1).padStart(2, '0') + '-' + String(day).padStart(2, '0');
|
|
42
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Checks a date-time string of RFC 3501 section 9, like an INTERNALDATE ("14-Sep-2013 21:22:28 -0300"):
|
|
45
|
+
* date-time = DQUOTE date-day-fixed "-" date-month "-" date-year SP time SP zone DQUOTE
|
|
46
|
+
*
|
|
47
|
+
* @param {String} value Value to check
|
|
48
|
+
* @return {Boolean} true if the value is a date-time string of a real date and time
|
|
49
|
+
*/
|
|
50
|
+
function isDateTime(value) {
|
|
51
|
+
if (!value || typeof value !== 'string') {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
// month names are case-insensitive like all ABNF strings
|
|
55
|
+
const match = value.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);
|
|
56
|
+
if (!match) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
// the values must also make a real date and time
|
|
60
|
+
return (isRealDate(match[1], monthIndex(match[2]), match[3]) && Number(match[4]) < 24 && Number(match[5]) < 60 && Number(match[6]) < 61 && Number(match[8]) < 60);
|
|
61
|
+
}
|
|
43
62
|
/**
|
|
44
63
|
* Parses a date-time value of the RFC 3501 section 9 form, like an INTERNALDATE ("14-Sep-2013 21:22:28 -0300").
|
|
45
64
|
* A value with only the date part is accepted as well, its time fields are left undefined
|
|
@@ -112,4 +131,4 @@ function toTimestamp(date) {
|
|
|
112
131
|
}
|
|
113
132
|
return Date.UTC(date.year, date.month, date.day, date.hours || 0, date.minutes || 0, date.seconds || 0) - offset * 60 * 1000;
|
|
114
133
|
}
|
|
115
|
-
export { MONTHS, monthIndex, isRealDate, dateKey, parseDateTime, parseHeaderDate, toTimestamp };
|
|
134
|
+
export { MONTHS, monthIndex, isRealDate, isDateTime, dateKey, parseDateTime, parseHeaderDate, toTimestamp };
|
package/dist/esm/envelope.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ParsedAddress } from './addressparser.js';
|
|
2
2
|
import type { ParsedHeader } from './mimeparser.js';
|
|
3
3
|
/** An address of the ENVELOPE (RFC 3501 section 9): name, source route, mailbox and host, all NIL for a group end */
|
|
4
|
-
export type EnvelopeAddress = [name: string | null, adl: null, mailbox: string | null, host: string | null];
|
|
4
|
+
export type EnvelopeAddress = [name: string | null, adl: string | null, mailbox: string | null, host: string | null];
|
|
5
5
|
/** The fields of the ENVELOPE (RFC 3501 section 7.4.2), in order */
|
|
6
6
|
export type Envelope = [
|
|
7
7
|
date: string | null,
|
package/dist/esm/envelope.js
CHANGED
|
@@ -1,4 +1,27 @@
|
|
|
1
1
|
// This module converts message structure into an ENVELOPE object
|
|
2
|
+
// RFC 5322 section 4.4: obs-angle-addr = [CFWS] "<" obs-route addr-spec ">" [CFWS], obs-route = obs-domain-list ":",
|
|
3
|
+
// obs-domain-list = *(CFWS / ",") "@" domain *("," [CFWS] ["@" domain]). A domain-literal can hold a colon
|
|
4
|
+
const OBS_ROUTE = /^[\s,]*(@(?:[^:[]|\[[^\]]*\])*):/;
|
|
5
|
+
/**
|
|
6
|
+
* Splits an obsolete source route from an address: the route goes to addr-adl, the rest is the addr-spec
|
|
7
|
+
* (RFC 9051 section 7.5.2: the at-domain-list is the "source route and obs-route ABNF production from [RFC5322]",
|
|
8
|
+
* the mailbox name the "local-part ABNF production"). The route is sent like Dovecot does, "@a,@b"
|
|
9
|
+
*
|
|
10
|
+
* @param {String} address Address from the angle brackets
|
|
11
|
+
* @return {Array} [route or null, addr-spec]
|
|
12
|
+
*/
|
|
13
|
+
function splitRoute(address) {
|
|
14
|
+
const match = address.match(OBS_ROUTE);
|
|
15
|
+
if (!match) {
|
|
16
|
+
return [null, address];
|
|
17
|
+
}
|
|
18
|
+
const route = match[1]
|
|
19
|
+
.split(',')
|
|
20
|
+
.map(domain => domain.trim())
|
|
21
|
+
.filter(domain => domain)
|
|
22
|
+
.join(',');
|
|
23
|
+
return [route, address.substr(match[0].length).trim()];
|
|
24
|
+
}
|
|
2
25
|
/**
|
|
3
26
|
* Convert a message object to an ENVELOPE object
|
|
4
27
|
*
|
|
@@ -57,12 +80,14 @@ function processAddress(arr, def) {
|
|
|
57
80
|
if (!address) {
|
|
58
81
|
return;
|
|
59
82
|
}
|
|
83
|
+
const [route, addrSpec] = splitRoute(address);
|
|
84
|
+
address = addrSpec;
|
|
60
85
|
const at = address.lastIndexOf('@');
|
|
61
86
|
const user = at >= 0 ? address.substr(0, at) : address;
|
|
62
87
|
// RFC 3501 7.4.2 reserves a NIL host for group markers, so an address without a domain gets
|
|
63
88
|
// the placeholder host that Dovecot uses for the same input
|
|
64
89
|
const domain = (at >= 0 ? address.substr(at + 1) : '') || 'MISSING_DOMAIN';
|
|
65
|
-
result.push([name,
|
|
90
|
+
result.push([name, route, user || null, domain]);
|
|
66
91
|
});
|
|
67
92
|
// env-from = "(" 1*address ")", there is no SP between the addresses (RFC 3501 section 9)
|
|
68
93
|
Object.defineProperty(result, 'adjacentLists', { value: true });
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -6,7 +6,8 @@ export { TAG_REGEX, IMAPServer, IMAPConnection, ImapKitError, quirks, storageSch
|
|
|
6
6
|
export type { Attribute, ParsedCommand, IMAPResponse, Notification, Callback, CommandHandler, CommandOptions, Plugin, IMAPError, Message, Mailbox, StorageNamespace, UserData, IMAPServerOptions } from './types.js';
|
|
7
7
|
export type { ScriptRule, ScriptContext, ScriptEvent, ScriptBytes, ScriptHandle } from './script.js';
|
|
8
8
|
export type { Quirk } from './quirks.js';
|
|
9
|
-
export type { Control, MailboxInfo, MessageInfo, SessionInfo, NewMessage, UserOptions, SessionFilter } from './control.js';
|
|
9
|
+
export type { Control, MailboxInfo, MessageInfo, SessionInfo, NewMessage, UserOptions, SessionFilter, ControlRoute, RouteRequest } from './control.js';
|
|
10
|
+
export type { FlagMode, UidMode, UidValidityOptions } from './store-operations.js';
|
|
10
11
|
declare const imapkit: typeof createServer & {
|
|
11
12
|
TAG_REGEX: RegExp;
|
|
12
13
|
IMAPServer: typeof IMAPServer;
|
|
@@ -15,7 +15,9 @@ declare function listPlugins(): string[];
|
|
|
15
15
|
*
|
|
16
16
|
* @param {Object} server IMAPServer instance
|
|
17
17
|
* @param {Array|String|Function} plugins List of plugins to load
|
|
18
|
+
* @param {Map} [exclude] Plugins a quirk preset leaves out, plugin name to quirk name
|
|
19
|
+
* @throws {Error} when a loaded plugin requires an excluded one
|
|
18
20
|
*/
|
|
19
|
-
declare function loadPlugins(server: IMAPServer, plugins: (string | Plugin)[] | string | Plugin | null | undefined, exclude?: string
|
|
21
|
+
declare function loadPlugins(server: IMAPServer, plugins: (string | Plugin)[] | string | Plugin | null | undefined, exclude?: Map<string, string>): void;
|
|
20
22
|
export default loadPlugins;
|
|
21
23
|
export { listPlugins };
|
package/dist/esm/load-plugins.js
CHANGED
|
@@ -46,12 +46,21 @@ function resolvePlugin(name) {
|
|
|
46
46
|
*
|
|
47
47
|
* @param {Object} server IMAPServer instance
|
|
48
48
|
* @param {Array|String|Function} plugins List of plugins to load
|
|
49
|
+
* @param {Map} [exclude] Plugins a quirk preset leaves out, plugin name to quirk name
|
|
50
|
+
* @throws {Error} when a loaded plugin requires an excluded one
|
|
49
51
|
*/
|
|
50
52
|
function loadPlugins(server, plugins, exclude) {
|
|
51
53
|
const loaded = new Set();
|
|
52
|
-
// plugins a quirk preset leaves out (no-move), they are not loaded even when listed
|
|
53
|
-
const excluded = new
|
|
54
|
-
|
|
54
|
+
// plugins a quirk preset leaves out (no-move), they are not loaded even when listed, and the quirk that removes them
|
|
55
|
+
const excluded = new Map();
|
|
56
|
+
(exclude || new Map()).forEach((quirk, name) => {
|
|
57
|
+
const resolved = resolvePlugin(name);
|
|
58
|
+
if (resolved) {
|
|
59
|
+
excluded.set(builtinPlugins[resolved], quirk);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
// `requiredBy` names the plugin whose `requires` lists this one
|
|
63
|
+
const load = (entry, requiredBy) => {
|
|
55
64
|
let plugin = entry;
|
|
56
65
|
if (typeof plugin === 'string') {
|
|
57
66
|
const name = resolvePlugin(plugin);
|
|
@@ -66,14 +75,24 @@ function loadPlugins(server, plugins, exclude) {
|
|
|
66
75
|
if (typeof plugin !== 'function') {
|
|
67
76
|
throw new TypeError('Invalid plugin, expecting a plugin name or a function');
|
|
68
77
|
}
|
|
69
|
-
|
|
78
|
+
const quirk = excluded.get(plugin);
|
|
79
|
+
if (quirk) {
|
|
80
|
+
if (requiredBy) {
|
|
81
|
+
// the plugin can not work without it (IMAP4rev2 folds in MOVE and UIDPLUS, RFC 9051 Appendix E), so
|
|
82
|
+
// advertising it without the required plugin would break its RFC
|
|
83
|
+
throw new Error(requiredBy + ' requires ' + entry + ', which the "' + quirk + '" quirk removes');
|
|
84
|
+
}
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (loaded.has(plugin)) {
|
|
70
88
|
return;
|
|
71
89
|
}
|
|
72
90
|
loaded.add(plugin);
|
|
73
|
-
|
|
91
|
+
const name = typeof entry === 'string' ? entry.trim() : plugin.name;
|
|
92
|
+
[].concat(plugin.requires || []).forEach(required => load(required, name));
|
|
74
93
|
plugin(server);
|
|
75
94
|
};
|
|
76
|
-
[].concat(plugins || []).forEach(load);
|
|
95
|
+
[].concat(plugins || []).forEach(entry => load(entry));
|
|
77
96
|
server.emit('pluginsLoaded');
|
|
78
97
|
}
|
|
79
98
|
export default loadPlugins;
|
|
@@ -103,7 +103,8 @@ function getDecodedSection(connection, message, query) {
|
|
|
103
103
|
}
|
|
104
104
|
const node = resolveNode(getMessageData(message).tree, section[0].value, connection.messageGlobal);
|
|
105
105
|
if (!node) {
|
|
106
|
-
// like BODY[<section>], a part that does not exist is empty
|
|
106
|
+
// like BODY[<section>], a part that does not exist is empty: RFC 3516 and RFC 9051 section 6.4.5 define no
|
|
107
|
+
// error for it, and BINARY follows the semantics of BODY (RFC 3516 section 4.2)
|
|
107
108
|
return '';
|
|
108
109
|
}
|
|
109
110
|
if (!isLeaf(node, connection.messageGlobal)) {
|
|
@@ -246,8 +246,8 @@ export default function contextSearchPlugin(server) {
|
|
|
246
246
|
// knows the current message list, so new matches can be reported after their EXISTS and FETCH responses
|
|
247
247
|
// (RFC 5267 section 4.3.2). A command that closes the mailbox gets no updates, its tagged response ends them
|
|
248
248
|
const processNotifications = connection.processNotifications;
|
|
249
|
-
connection.processNotifications = function (data) {
|
|
250
|
-
processNotifications.call(this, data);
|
|
249
|
+
connection.processNotifications = function (data, beforeCommand) {
|
|
250
|
+
processNotifications.call(this, data, beforeCommand);
|
|
251
251
|
const command = ((data && data.command) || '').toUpperCase();
|
|
252
252
|
if (this.searchContexts && this.searchContexts.size && !this.notificationQueue.length && !CLOSING_COMMANDS.has(command)) {
|
|
253
253
|
checkContexts(this);
|
|
@@ -1,7 +1,2 @@
|
|
|
1
1
|
import type { IMAPServer } from '../types.js';
|
|
2
|
-
/**
|
|
3
|
-
* @help Enables CREATE-SPECIAL-USE [RFC6154] capability
|
|
4
|
-
* @help Allowed special flags can be set with server
|
|
5
|
-
* @help option "special-use"
|
|
6
|
-
*/
|
|
7
2
|
export default function createSpecialUsePlugin(server: IMAPServer): void;
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
import formalSyntax from 'imap-handler/lib/formal';
|
|
2
|
+
import { isAtom } from '../arguments.js';
|
|
1
3
|
/**
|
|
2
4
|
* @help Enables CREATE-SPECIAL-USE [RFC6154] capability
|
|
3
5
|
* @help Allowed special flags can be set with server
|
|
4
6
|
* @help option "special-use"
|
|
5
7
|
*/
|
|
8
|
+
// use-attr-ext = "\" atom (RFC 6154 section 6), with the ATOM-CHAR of the imap-handler grammar (RFC 3501 section 9)
|
|
9
|
+
const USE_ATTR = new RegExp('^\\\\[' + formalSyntax['ATOM-CHAR']().replace(/[\\\]^-]/g, '\\$&') + ']+$');
|
|
6
10
|
export default function createSpecialUsePlugin(server) {
|
|
7
11
|
// Register capability
|
|
8
12
|
server.registerCapability('CREATE-SPECIAL-USE');
|
|
@@ -31,7 +35,10 @@ export default function createSpecialUsePlugin(server) {
|
|
|
31
35
|
}
|
|
32
36
|
if (specialUseList) {
|
|
33
37
|
for (i = 0, len = specialUseList.length; i < len; i++) {
|
|
34
|
-
|
|
38
|
+
// RFC 6154 section 6: use-attr-ext = "\" atom, so every entry is an atom that starts with a backslash,
|
|
39
|
+
// not NIL, a string, a literal, a number or a list
|
|
40
|
+
const entry = specialUseList[i];
|
|
41
|
+
if (!isAtom(entry) || !USE_ATTR.test(entry.value)) {
|
|
35
42
|
connection.send({
|
|
36
43
|
tag: parsed.tag,
|
|
37
44
|
command: 'BAD',
|
|
@@ -122,6 +122,11 @@ export default function imap4rev2Plugin(server) {
|
|
|
122
122
|
}, 'SELECT LIST', parsed, data, mailbox);
|
|
123
123
|
};
|
|
124
124
|
server.outputHandlers.push((connection, response, description, parsed, data) => {
|
|
125
|
+
if (description === 'RECENT NOTIFICATION' && isRev2(connection)) {
|
|
126
|
+
// Appendix E item 12: no RECENT response after the EXISTS of new messages either
|
|
127
|
+
response.skipResponse = true;
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
125
130
|
if (!parsed || !response || !isRev2(connection)) {
|
|
126
131
|
return;
|
|
127
132
|
}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import type { IMAPServer } from '../types.js';
|
|
2
2
|
/**
|
|
3
3
|
* @help Adds METADATA-SERVER [RFC5464] capability, like METADATA but
|
|
4
|
-
* @help only for server annotations (mailbox name ""). With METADATA
|
|
4
|
+
* @help only for server annotations (mailbox name ""), loads ENABLE. With METADATA
|
|
5
5
|
* @help also loaded, only METADATA is advertised
|
|
6
6
|
*/
|
|
7
|
-
|
|
7
|
+
declare function metadataServerPlugin(server: IMAPServer): void;
|
|
8
|
+
declare namespace metadataServerPlugin {
|
|
9
|
+
var requires: string[];
|
|
10
|
+
}
|
|
11
|
+
export default metadataServerPlugin;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { setup } from './metadata.js';
|
|
2
2
|
/**
|
|
3
3
|
* @help Adds METADATA-SERVER [RFC5464] capability, like METADATA but
|
|
4
|
-
* @help only for server annotations (mailbox name ""). With METADATA
|
|
4
|
+
* @help only for server annotations (mailbox name ""), loads ENABLE. With METADATA
|
|
5
5
|
* @help also loaded, only METADATA is advertised
|
|
6
6
|
*/
|
|
7
7
|
export default function metadataServerPlugin(server) {
|
|
8
8
|
setup(server, false);
|
|
9
9
|
}
|
|
10
|
+
// RFC 5464 section 4.1: a server that sends unsolicited METADATA responses "MUST support the ENABLE command"
|
|
11
|
+
metadataServerPlugin.requires = ['ENABLE'];
|
|
@@ -7,5 +7,9 @@ import type { IMAPServer } from '../types.js';
|
|
|
7
7
|
* @param {Boolean} mailboxes If true, mailbox annotations are supported, otherwise only server annotations
|
|
8
8
|
*/
|
|
9
9
|
declare function setup(server: IMAPServer, mailboxes: boolean): void;
|
|
10
|
-
|
|
10
|
+
declare function metadataPlugin(server: IMAPServer): void;
|
|
11
|
+
declare namespace metadataPlugin {
|
|
12
|
+
var requires: string[];
|
|
13
|
+
}
|
|
14
|
+
export default metadataPlugin;
|
|
11
15
|
export { setup };
|
|
@@ -11,6 +11,7 @@ import { registerEnable, isEnabled } from './enable.js';
|
|
|
11
11
|
* @help default 65536), "metadataMaxEntries" (per mailbox and for the
|
|
12
12
|
* @help server, default 100). "metadataPrivate": false turns /private
|
|
13
13
|
* @help entries off. ENABLE METADATA turns on unsolicited METADATA responses
|
|
14
|
+
* @help (loads the ENABLE plugin, RFC 5464 section 4.1)
|
|
14
15
|
*/
|
|
15
16
|
// Default limits, RFC 5464 section 4.1 requires at least 1024 octets and 10 entries
|
|
16
17
|
const DEFAULT_MAX_SIZE = 64 * 1024;
|
|
@@ -501,4 +502,6 @@ function setup(server, mailboxes) {
|
|
|
501
502
|
export default function metadataPlugin(server) {
|
|
502
503
|
setup(server, true);
|
|
503
504
|
}
|
|
505
|
+
// RFC 5464 section 4.1: a server that sends unsolicited METADATA responses "MUST support the ENABLE command"
|
|
506
|
+
metadataPlugin.requires = ['ENABLE'];
|
|
504
507
|
export { setup };
|
package/dist/esm/quirks.d.ts
CHANGED
|
@@ -19,10 +19,10 @@ export declare const quirks: Record<string, Quirk>;
|
|
|
19
19
|
* Resolves the `quirks` option
|
|
20
20
|
*
|
|
21
21
|
* @param {Array|String} names Quirk names
|
|
22
|
-
* @return {Object} `{ rules, removePlugins }` of all of them
|
|
22
|
+
* @return {Object} `{ rules, removePlugins }` of all of them, removePlugins maps a plugin name to the quirk that removes it
|
|
23
23
|
* @throws {Error} for an unknown name
|
|
24
24
|
*/
|
|
25
25
|
export declare function resolveQuirks(names: string[] | string | null | undefined): {
|
|
26
26
|
rules: ScriptRule[];
|
|
27
|
-
removePlugins:
|
|
27
|
+
removePlugins: Map<string, string>;
|
|
28
28
|
};
|
package/dist/esm/quirks.js
CHANGED
|
@@ -102,19 +102,20 @@ export const quirks = {
|
|
|
102
102
|
* Resolves the `quirks` option
|
|
103
103
|
*
|
|
104
104
|
* @param {Array|String} names Quirk names
|
|
105
|
-
* @return {Object} `{ rules, removePlugins }` of all of them
|
|
105
|
+
* @return {Object} `{ rules, removePlugins }` of all of them, removePlugins maps a plugin name to the quirk that removes it
|
|
106
106
|
* @throws {Error} for an unknown name
|
|
107
107
|
*/
|
|
108
108
|
export function resolveQuirks(names) {
|
|
109
109
|
const rules = [];
|
|
110
|
-
const removePlugins = new
|
|
110
|
+
const removePlugins = new Map();
|
|
111
111
|
[].concat(names || []).forEach(name => {
|
|
112
|
-
const
|
|
112
|
+
const key = String(name).toLowerCase();
|
|
113
|
+
const quirk = Object.hasOwn(quirks, key) ? quirks[key] : null;
|
|
113
114
|
if (!quirk) {
|
|
114
115
|
throw new Error('Unknown quirk "' + name + '". Available quirks: ' + Object.keys(quirks).join(', '));
|
|
115
116
|
}
|
|
116
117
|
rules.push(...(quirk.rules || []));
|
|
117
|
-
(quirk.removePlugins || []).forEach(plugin => removePlugins.
|
|
118
|
+
(quirk.removePlugins || []).forEach(plugin => removePlugins.set(plugin, key));
|
|
118
119
|
});
|
|
119
120
|
return { rules, removePlugins };
|
|
120
121
|
}
|
package/dist/esm/rest.d.ts
CHANGED
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
import http from 'node:http';
|
|
9
9
|
import type { IMAPServer } from './types.js';
|
|
10
10
|
import type { ControlRoute as Route } from './control.js';
|
|
11
|
+
/** The HTTP server of the REST API, `endStreams()` ends the open event streams */
|
|
12
|
+
type RestServer = http.Server & {
|
|
13
|
+
endStreams?: () => void;
|
|
14
|
+
};
|
|
11
15
|
/** Options of the REST API, the `rest` server option */
|
|
12
16
|
interface RestOptions {
|
|
13
17
|
port?: number | undefined;
|
|
@@ -29,7 +33,7 @@ declare function isLoopback(host: string): boolean;
|
|
|
29
33
|
* @param {Object} server IMAP server
|
|
30
34
|
* @return {Array} routes
|
|
31
35
|
*/
|
|
32
|
-
declare function getRoutes(server: IMAPServer): Route[];
|
|
36
|
+
declare function getRoutes(server: IMAPServer, streams?: Set<http.ServerResponse>): Route[];
|
|
33
37
|
/**
|
|
34
38
|
* Describes the routes as an OpenAPI 3.1 document
|
|
35
39
|
*
|
|
@@ -45,6 +49,6 @@ declare function openApi(routes: Route[]): Record<string, unknown>;
|
|
|
45
49
|
* @return {Object} HTTP server
|
|
46
50
|
* @throws {Error} for a host that is not a loopback address without a token
|
|
47
51
|
*/
|
|
48
|
-
declare function createRestServer(server: IMAPServer, options: RestOptions):
|
|
52
|
+
declare function createRestServer(server: IMAPServer, options: RestOptions): RestServer;
|
|
49
53
|
export { createRestServer, isLoopback, getRoutes, openApi };
|
|
50
|
-
export type { RestOptions };
|
|
54
|
+
export type { RestOptions, RestServer };
|
package/dist/esm/rest.js
CHANGED
|
@@ -98,8 +98,9 @@ function eventData(type, args) {
|
|
|
98
98
|
* @param {Array} types Event types to send
|
|
99
99
|
* @return {Function} writes the stream to a response
|
|
100
100
|
*/
|
|
101
|
-
function eventStream(server, types) {
|
|
101
|
+
function eventStream(server, types, streams) {
|
|
102
102
|
return (res, req) => {
|
|
103
|
+
streams.add(res);
|
|
103
104
|
res.writeHead(200, { 'Content-Type': 'text/event-stream; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
104
105
|
res.write(': connected\n\n');
|
|
105
106
|
const listeners = types.map(type => {
|
|
@@ -111,6 +112,7 @@ function eventStream(server, types) {
|
|
|
111
112
|
const ping = setInterval(() => res.write(': ping\n\n'), 15000);
|
|
112
113
|
ping.unref();
|
|
113
114
|
req.on('close', () => {
|
|
115
|
+
streams.delete(res);
|
|
114
116
|
clearInterval(ping);
|
|
115
117
|
listeners.forEach(({ type, listener }) => server.removeListener(type, listener));
|
|
116
118
|
});
|
|
@@ -122,7 +124,7 @@ function eventStream(server, types) {
|
|
|
122
124
|
* @param {Object} server IMAP server
|
|
123
125
|
* @return {Array} routes
|
|
124
126
|
*/
|
|
125
|
-
function getRoutes(server) {
|
|
127
|
+
function getRoutes(server, streams = new Set()) {
|
|
126
128
|
const control = server.control;
|
|
127
129
|
const ruleJson = (handle) => ({ id: handle.id, rule: handle.rule, matched: handle.matched, hits: handle.hits });
|
|
128
130
|
const routes = [
|
|
@@ -158,7 +160,7 @@ function getRoutes(server) {
|
|
|
158
160
|
if (unknown.length) {
|
|
159
161
|
throw storeError('Unknown event type ' + unknown.join(', ') + ', expected ' + EVENT_TYPES.join(', '), 'INVALID');
|
|
160
162
|
}
|
|
161
|
-
return { stream: eventStream(server, types) };
|
|
163
|
+
return { stream: eventStream(server, types, streams) };
|
|
162
164
|
}
|
|
163
165
|
},
|
|
164
166
|
{
|
|
@@ -499,7 +501,9 @@ function createRestServer(server, options) {
|
|
|
499
501
|
if (!token && !isLoopback(host)) {
|
|
500
502
|
throw new Error('The REST API controls the whole server, it needs a token (rest.token, --rest-token) to listen on ' + host);
|
|
501
503
|
}
|
|
502
|
-
|
|
504
|
+
// open event streams, a shutdown ends them so that the server can close
|
|
505
|
+
const streams = new Set();
|
|
506
|
+
const routes = getRoutes(server, streams);
|
|
503
507
|
const send = (res, status, body) => {
|
|
504
508
|
const json = JSON.stringify(body);
|
|
505
509
|
res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8', 'Content-Length': Buffer.byteLength(json), 'Cache-Control': 'no-store' });
|
|
@@ -539,7 +543,7 @@ function createRestServer(server, options) {
|
|
|
539
543
|
}
|
|
540
544
|
send(res, 200, result === undefined ? {} : result);
|
|
541
545
|
};
|
|
542
|
-
|
|
546
|
+
const rest = http.createServer((req, res) => {
|
|
543
547
|
handle(req, res).catch((err) => {
|
|
544
548
|
const code = err instanceof ImapKitError ? err.code : 'SERVERERROR';
|
|
545
549
|
// RFC 5530 codes of failed mailbox operations (CANNOT, HASCHILDREN ...) are conflicts with the state
|
|
@@ -549,5 +553,7 @@ function createRestServer(server, options) {
|
|
|
549
553
|
}
|
|
550
554
|
});
|
|
551
555
|
});
|
|
556
|
+
rest.endStreams = () => streams.forEach(res => res.end());
|
|
557
|
+
return rest;
|
|
552
558
|
}
|
|
553
559
|
export { createRestServer, isLoopback, getRoutes, openApi };
|
package/dist/esm/server.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { Stream } from 'node:stream';
|
|
2
2
|
import net from 'node:net';
|
|
3
|
-
import http from 'node:http';
|
|
4
3
|
import tls from 'node:tls';
|
|
5
4
|
import type { CompilerOptions, ParserOptions } from 'imap-handler';
|
|
6
5
|
import type { ResolvedCommandOptions } from './command-states.js';
|
|
7
6
|
import { Control } from './control.js';
|
|
7
|
+
import type { RestServer } from './rest.js';
|
|
8
8
|
import type { SessionInfo } from './control.js';
|
|
9
9
|
import { ServerScript } from './script.js';
|
|
10
10
|
import type { DeferredOutput, OutputOperation, ScriptContext, ScriptEvent, ScriptRule } from './script.js';
|
|
@@ -85,7 +85,7 @@ declare class IMAPServer extends Stream {
|
|
|
85
85
|
/** the SMTP server that `start()` runs for the `smtp` option */
|
|
86
86
|
smtpServer: SMTPListener | null;
|
|
87
87
|
/** the HTTP server of the REST API that `start()` runs for the `rest` option */
|
|
88
|
-
restServer:
|
|
88
|
+
restServer: RestServer | null;
|
|
89
89
|
constructor(options?: IMAPServerOptions);
|
|
90
90
|
/**
|
|
91
91
|
* Sets the users from the `users` option, or the default user. The users are deep copied, so that runtime changes
|
|
@@ -370,6 +370,25 @@ declare class IMAPServer extends Stream {
|
|
|
370
370
|
* @param {Array} flags Flags of a message
|
|
371
371
|
*/
|
|
372
372
|
rememberFlags(mailbox: Mailbox, flags: string[]): void;
|
|
373
|
+
/**
|
|
374
|
+
* Checks if a flag can be stored on messages of a mailbox: any flag when the mailbox allows new keywords
|
|
375
|
+
* (PERMANENTFLAGS has `\*`), otherwise only a flag in the PERMANENTFLAGS list that getStatus() builds, the
|
|
376
|
+
* `permanentFlags` of the mailbox and every flag its messages had. RFC 3501 section 7.1 (RFC 9051 section 7.1):
|
|
377
|
+
* PERMANENTFLAGS "indicates which of the known flags the client can change permanently", STORE ignores the others
|
|
378
|
+
*
|
|
379
|
+
* @param {Object} mailbox Mailbox object
|
|
380
|
+
* @param {String} flag Normalized flag
|
|
381
|
+
* @return {Boolean} true if the flag is a permanent flag of the mailbox
|
|
382
|
+
*/
|
|
383
|
+
isPermanentFlag(mailbox: Mailbox, flag: string): boolean;
|
|
384
|
+
/**
|
|
385
|
+
* The flags of the PERMANENTFLAGS list (without `\*`): the `permanentFlags` of the mailbox and every flag its
|
|
386
|
+
* messages have or had (`knownFlags`, see rememberFlags)
|
|
387
|
+
*
|
|
388
|
+
* @param {Object} mailbox Mailbox object
|
|
389
|
+
* @return {Array} flags, a new list
|
|
390
|
+
*/
|
|
391
|
+
permanentFlagList(mailbox: Mailbox): string[];
|
|
373
392
|
/**
|
|
374
393
|
* The current time for dates the server sets itself (INTERNALDATE of a message without one, SAVEDATE). The `now`
|
|
375
394
|
* option fixes it for repeatable tests: a Date, a timestamp, or a function that returns one
|
|
@@ -548,6 +567,8 @@ interface QueuedCommand {
|
|
|
548
567
|
rule: ScriptRule;
|
|
549
568
|
context: ScriptContext;
|
|
550
569
|
} | undefined;
|
|
570
|
+
/** handler for a command that has no registered one (AUTHENTICATE with an unknown mechanism) */
|
|
571
|
+
handler?: CommandHandler | undefined;
|
|
551
572
|
}
|
|
552
573
|
declare class IMAPConnection {
|
|
553
574
|
[key: string]: any;
|
|
@@ -921,7 +942,36 @@ declare class IMAPConnection {
|
|
|
921
942
|
* @return {Array} Notifications to send
|
|
922
943
|
*/
|
|
923
944
|
prepareNotifications(queue: Notification[]): Notification[];
|
|
924
|
-
|
|
945
|
+
/**
|
|
946
|
+
* Checks if EXPUNGE responses must wait while a command runs: during FETCH, STORE and SEARCH (RFC 3501 section
|
|
947
|
+
* 7.4.1), during the commands that extensions add to this list (see the noExpunge command option), and during UID
|
|
948
|
+
* SEARCH with message numbers in the search criteria (RFC 7162 section 3.2.10.2 for VANISHED, EXPUNGE may wait as
|
|
949
|
+
* well, RFC 3501 only allows it during UID commands)
|
|
950
|
+
*
|
|
951
|
+
* @param {Object} data Parsed command
|
|
952
|
+
* @return {Boolean} true if the command holds back EXPUNGE responses
|
|
953
|
+
*/
|
|
954
|
+
holdsExpunge(data: CommandContext): boolean;
|
|
955
|
+
/**
|
|
956
|
+
* Sends the queued notifications. During a command that holds back EXPUNGE responses (see holdsExpunge), or with
|
|
957
|
+
* `beforeCommand` before a command that refers to messages by sequence number, only the notifications queued before
|
|
958
|
+
* the first pending EXPUNGE go out: new messages (EXISTS) and flag changes. RFC 3501 section 5.2: "A server MUST send
|
|
959
|
+
* mailbox size updates automatically if a mailbox size change is observed during the processing of a command",
|
|
960
|
+
* section 7.4.1 forbids only EXPUNGE during FETCH, STORE and SEARCH. What was queued after the EXPUNGE waits with it,
|
|
961
|
+
* an EXISTS sent before it would describe a list that the client can not know yet
|
|
962
|
+
*
|
|
963
|
+
* @param {Object} [data] Parsed command that runs, or null between commands
|
|
964
|
+
* @param {Boolean} [beforeCommand] true when the command has not run yet, then a command with message sequence
|
|
965
|
+
* numbers (COPY, MOVE) also waits with the EXPUNGE responses, its numbers refer to the messages before them
|
|
966
|
+
*/
|
|
967
|
+
processNotifications(data?: CommandContext | null, beforeCommand?: boolean): void;
|
|
968
|
+
/**
|
|
969
|
+
* Sends an untagged RECENT response with the number of \Recent messages among the first `count` messages
|
|
970
|
+
*
|
|
971
|
+
* @param {Number} count Number of messages the client was told about
|
|
972
|
+
* @param {Map} sequence Message to the sequence number this session knows it by
|
|
973
|
+
*/
|
|
974
|
+
sendRecent(count: number, sequence: Map<Message, number>): void;
|
|
925
975
|
/**
|
|
926
976
|
* Compile a command object to a response string and write it to socket.
|
|
927
977
|
* If the command object has a skipResponse property, the command is
|
|
@@ -990,6 +1040,7 @@ declare class IMAPConnection {
|
|
|
990
1040
|
* @return {Boolean} true if the command uses message sequence numbers
|
|
991
1041
|
*/
|
|
992
1042
|
usesSequenceNumbers(parsed: CommandContext): boolean;
|
|
1043
|
+
findSequenceNumbers(parsed: CommandContext): boolean;
|
|
993
1044
|
/**
|
|
994
1045
|
* Decodes a SASL client response. It must be valid base64 by the RFC 3501 section 9 grammar,
|
|
995
1046
|
* "=" stands for an empty initial response (RFC 4959 section 3).
|