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
|
@@ -8,8 +8,8 @@ function setFlags(connection, message, flags) {
|
|
|
8
8
|
[].concat(flags).forEach(flag => {
|
|
9
9
|
flag = normalizeSystemFlag(typeof flag === 'string' ? flag : String(flag.value || ''));
|
|
10
10
|
checkSystemFlags(connection.server, flag);
|
|
11
|
-
//
|
|
12
|
-
if (
|
|
11
|
+
// a flag that is not in PERMANENTFLAGS is ignored (RFC 3501 section 7.1, RFC 9051 section 7.1)
|
|
12
|
+
if (!connection.server.isPermanentFlag(mailbox, flag)) {
|
|
13
13
|
return;
|
|
14
14
|
}
|
|
15
15
|
if (messageFlags.indexOf(flag) < 0) {
|
|
@@ -24,8 +24,8 @@ function addFlags(connection, message, flags) {
|
|
|
24
24
|
[].concat(flags).forEach(flag => {
|
|
25
25
|
flag = normalizeSystemFlag(typeof flag === 'string' ? flag : String(flag.value || ''));
|
|
26
26
|
checkSystemFlags(connection.server, flag);
|
|
27
|
-
//
|
|
28
|
-
if (
|
|
27
|
+
// a flag that is not in PERMANENTFLAGS is ignored (RFC 3501 section 7.1, RFC 9051 section 7.1)
|
|
28
|
+
if (!connection.server.isPermanentFlag(mailbox, flag)) {
|
|
29
29
|
return;
|
|
30
30
|
}
|
|
31
31
|
if (message.flags.indexOf(flag) < 0) {
|
|
@@ -19,8 +19,22 @@ export default function listCommand(connection, parsed, data, callback) {
|
|
|
19
19
|
return callback();
|
|
20
20
|
}
|
|
21
21
|
if (!parsed.attributes[1].value) {
|
|
22
|
-
// empty
|
|
23
|
-
|
|
22
|
+
// RFC 3501 section 6.3.8: "An empty ("" string) mailbox name argument is a special request to return the
|
|
23
|
+
// hierarchy delimiter and the root name of the name given in the reference. The value returned as the root
|
|
24
|
+
// MAY be the empty string if the reference is non-rooted or is an empty string."
|
|
25
|
+
const server = connection.server;
|
|
26
|
+
const reference = parsed.attributes[0].value || '';
|
|
27
|
+
// the namespace of the reference, the personal one for a reference that is not a valid name
|
|
28
|
+
let key = server.referenceNamespace;
|
|
29
|
+
try {
|
|
30
|
+
key = server.getNamespace(connection.importMailboxName(reference)) || key;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// not a mailbox name, so not in another namespace
|
|
34
|
+
}
|
|
35
|
+
const namespace = key !== false ? server.storage[key] : undefined;
|
|
36
|
+
// the root of a name in another namespace is the prefix of that namespace, like "#news." in the RFC example
|
|
37
|
+
const root = key !== false && key !== server.referenceNamespace ? connection.exportMailboxName(key) : '';
|
|
24
38
|
if (namespace) {
|
|
25
39
|
connection.send({
|
|
26
40
|
tag: '*',
|
|
@@ -33,7 +47,7 @@ export default function listCommand(connection, parsed, data, callback) {
|
|
|
33
47
|
}
|
|
34
48
|
],
|
|
35
49
|
namespace.separator,
|
|
36
|
-
|
|
50
|
+
root
|
|
37
51
|
]
|
|
38
52
|
}, 'LIST ITEM', parsed, data);
|
|
39
53
|
}
|
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,9 @@ 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 {
|
|
9
|
+
export type { MemorySocket } from './memory-socket.js';
|
|
10
|
+
export type { Control, MailboxInfo, MessageInfo, SessionInfo, NewMessage, UserOptions, SessionFilter, ControlRoute, RouteRequest } from './control.js';
|
|
11
|
+
export type { FlagMode, UidMode, UidValidityOptions } from './store-operations.js';
|
|
10
12
|
declare const imapkit: typeof createServer & {
|
|
11
13
|
TAG_REGEX: RegExp;
|
|
12
14
|
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;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
/**
|
|
3
|
+
* One end of an in-memory connection, see `server.connect()`. It has the parts of `net.Socket` that the server and a
|
|
4
|
+
* client use: `write(data, [encoding], [callback])`, `end()`, `destroy()`, `pause()`, `resume()`, `setEncoding()`,
|
|
5
|
+
* the `data`, `end`, `close` and `error` events, and `encrypted`, which makes the server treat the connection like
|
|
6
|
+
* one over TLS (as `tls.TLSSocket#encrypted` does). Written data reaches the other end on a later event loop turn,
|
|
7
|
+
* one `data` event per write
|
|
8
|
+
*/
|
|
9
|
+
export declare class MemorySocket extends EventEmitter {
|
|
10
|
+
/** the other end of the connection */
|
|
11
|
+
peer: MemorySocket | null;
|
|
12
|
+
destroyed: boolean;
|
|
13
|
+
/** true if the connection counts as encrypted (implicit TLS, or after STARTTLS) */
|
|
14
|
+
encrypted: boolean;
|
|
15
|
+
_paused: boolean;
|
|
16
|
+
_ending: boolean;
|
|
17
|
+
_encoding: BufferEncoding | null;
|
|
18
|
+
/** received data that waits while the socket is paused */
|
|
19
|
+
_received: Buffer[];
|
|
20
|
+
constructor();
|
|
21
|
+
/**
|
|
22
|
+
* Sends data to the other end
|
|
23
|
+
*
|
|
24
|
+
* @param data Data to send, a string is encoded with `encoding` (utf8 by default)
|
|
25
|
+
* @param encoding Encoding of a string, or the callback
|
|
26
|
+
* @param callback Called once the other end received the data
|
|
27
|
+
* @return true, there is no backpressure
|
|
28
|
+
*/
|
|
29
|
+
write(data: Buffer | Uint8Array | string, encoding?: BufferEncoding | (() => void), callback?: () => void): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Ends the connection after the data written so far reached the other end, both ends close then
|
|
32
|
+
*
|
|
33
|
+
* @param data Data to send before the end
|
|
34
|
+
*/
|
|
35
|
+
end(data?: Buffer | Uint8Array | string): this;
|
|
36
|
+
/**
|
|
37
|
+
* Closes both ends right away, data that did not reach the other end yet is lost
|
|
38
|
+
*
|
|
39
|
+
* @param error Error to emit on this end
|
|
40
|
+
*/
|
|
41
|
+
destroy(error?: Error): this;
|
|
42
|
+
/**
|
|
43
|
+
* Closes the connection like a TCP RST: the other end gets an ECONNRESET error (`close: 'reset'` of script rules)
|
|
44
|
+
*/
|
|
45
|
+
resetAndDestroy(): this;
|
|
46
|
+
/**
|
|
47
|
+
* STARTTLS without TLS: both ends count as encrypted from now on (`upgradeConnection` in src/server.ts)
|
|
48
|
+
*
|
|
49
|
+
* @param callback Called once the connection counts as encrypted
|
|
50
|
+
*/
|
|
51
|
+
startTls(callback: () => void): void;
|
|
52
|
+
pause(): this;
|
|
53
|
+
resume(): this;
|
|
54
|
+
/**
|
|
55
|
+
* Makes the `data` events carry strings instead of Buffers
|
|
56
|
+
*
|
|
57
|
+
* @param encoding Encoding of the strings, e.g. "utf8" or "binary"
|
|
58
|
+
*/
|
|
59
|
+
setEncoding(encoding: BufferEncoding): this;
|
|
60
|
+
deliver(chunk: Buffer): void;
|
|
61
|
+
private emitData;
|
|
62
|
+
close(error?: Error): void;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Creates the two ends of an in-memory connection
|
|
66
|
+
*
|
|
67
|
+
* @return `[serverSocket, clientSocket]`
|
|
68
|
+
*/
|
|
69
|
+
export declare function createSocketPair(): [MemorySocket, MemorySocket];
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
/**
|
|
3
|
+
* One end of an in-memory connection, see `server.connect()`. It has the parts of `net.Socket` that the server and a
|
|
4
|
+
* client use: `write(data, [encoding], [callback])`, `end()`, `destroy()`, `pause()`, `resume()`, `setEncoding()`,
|
|
5
|
+
* the `data`, `end`, `close` and `error` events, and `encrypted`, which makes the server treat the connection like
|
|
6
|
+
* one over TLS (as `tls.TLSSocket#encrypted` does). Written data reaches the other end on a later event loop turn,
|
|
7
|
+
* one `data` event per write
|
|
8
|
+
*/
|
|
9
|
+
export class MemorySocket extends EventEmitter {
|
|
10
|
+
constructor() {
|
|
11
|
+
super();
|
|
12
|
+
this.peer = null;
|
|
13
|
+
this.destroyed = false;
|
|
14
|
+
this.encrypted = false;
|
|
15
|
+
this._paused = false;
|
|
16
|
+
this._ending = false;
|
|
17
|
+
this._encoding = null;
|
|
18
|
+
this._received = [];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Sends data to the other end
|
|
22
|
+
*
|
|
23
|
+
* @param data Data to send, a string is encoded with `encoding` (utf8 by default)
|
|
24
|
+
* @param encoding Encoding of a string, or the callback
|
|
25
|
+
* @param callback Called once the other end received the data
|
|
26
|
+
* @return true, there is no backpressure
|
|
27
|
+
*/
|
|
28
|
+
write(data, encoding, callback) {
|
|
29
|
+
if (typeof encoding === 'function') {
|
|
30
|
+
callback = encoding;
|
|
31
|
+
encoding = undefined;
|
|
32
|
+
}
|
|
33
|
+
if (this.destroyed || this._ending) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
// no copy, like a real socket the data is not changed after it was written
|
|
37
|
+
const chunk = typeof data === 'string' ? Buffer.from(data, encoding || 'utf8') : Buffer.from(data.buffer, data.byteOffset, data.byteLength);
|
|
38
|
+
nextTurn(() => {
|
|
39
|
+
this.peer?.deliver(chunk);
|
|
40
|
+
callback?.();
|
|
41
|
+
});
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Ends the connection after the data written so far reached the other end, both ends close then
|
|
46
|
+
*
|
|
47
|
+
* @param data Data to send before the end
|
|
48
|
+
*/
|
|
49
|
+
end(data) {
|
|
50
|
+
if (data !== undefined) {
|
|
51
|
+
this.write(data);
|
|
52
|
+
}
|
|
53
|
+
if (this.destroyed || this._ending) {
|
|
54
|
+
return this;
|
|
55
|
+
}
|
|
56
|
+
this._ending = true;
|
|
57
|
+
// queued after the writes, so the other end gets all data first
|
|
58
|
+
nextTurn(() => {
|
|
59
|
+
const peer = this.peer;
|
|
60
|
+
if (peer && !peer.destroyed) {
|
|
61
|
+
peer.emit('end');
|
|
62
|
+
peer.close();
|
|
63
|
+
}
|
|
64
|
+
this.close();
|
|
65
|
+
});
|
|
66
|
+
return this;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Closes both ends right away, data that did not reach the other end yet is lost
|
|
70
|
+
*
|
|
71
|
+
* @param error Error to emit on this end
|
|
72
|
+
*/
|
|
73
|
+
destroy(error) {
|
|
74
|
+
if (this.destroyed) {
|
|
75
|
+
return this;
|
|
76
|
+
}
|
|
77
|
+
const peer = this.peer;
|
|
78
|
+
this.close(error);
|
|
79
|
+
if (peer && !peer.destroyed) {
|
|
80
|
+
peer.close();
|
|
81
|
+
}
|
|
82
|
+
return this;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Closes the connection like a TCP RST: the other end gets an ECONNRESET error (`close: 'reset'` of script rules)
|
|
86
|
+
*/
|
|
87
|
+
resetAndDestroy() {
|
|
88
|
+
const peer = this.peer;
|
|
89
|
+
this.destroy();
|
|
90
|
+
if (peer) {
|
|
91
|
+
const error = new Error('read ECONNRESET');
|
|
92
|
+
error.code = 'ECONNRESET';
|
|
93
|
+
nextTurn(() => peer.emit('error', error));
|
|
94
|
+
}
|
|
95
|
+
return this;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* STARTTLS without TLS: both ends count as encrypted from now on (`upgradeConnection` in src/server.ts)
|
|
99
|
+
*
|
|
100
|
+
* @param callback Called once the connection counts as encrypted
|
|
101
|
+
*/
|
|
102
|
+
startTls(callback) {
|
|
103
|
+
this.encrypted = true;
|
|
104
|
+
if (this.peer) {
|
|
105
|
+
this.peer.encrypted = true;
|
|
106
|
+
}
|
|
107
|
+
callback();
|
|
108
|
+
}
|
|
109
|
+
pause() {
|
|
110
|
+
this._paused = true;
|
|
111
|
+
return this;
|
|
112
|
+
}
|
|
113
|
+
resume() {
|
|
114
|
+
this._paused = false;
|
|
115
|
+
while (this._received.length && !this._paused && !this.destroyed) {
|
|
116
|
+
this.emitData(this._received.shift());
|
|
117
|
+
}
|
|
118
|
+
return this;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Makes the `data` events carry strings instead of Buffers
|
|
122
|
+
*
|
|
123
|
+
* @param encoding Encoding of the strings, e.g. "utf8" or "binary"
|
|
124
|
+
*/
|
|
125
|
+
setEncoding(encoding) {
|
|
126
|
+
this._encoding = encoding;
|
|
127
|
+
return this;
|
|
128
|
+
}
|
|
129
|
+
deliver(chunk) {
|
|
130
|
+
if (this.destroyed) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (this._paused) {
|
|
134
|
+
this._received.push(chunk);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
this.emitData(chunk);
|
|
138
|
+
}
|
|
139
|
+
emitData(chunk) {
|
|
140
|
+
this.emit('data', this._encoding ? chunk.toString(this._encoding) : chunk);
|
|
141
|
+
}
|
|
142
|
+
close(error) {
|
|
143
|
+
if (this.destroyed) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
this.destroyed = true;
|
|
147
|
+
this._received = [];
|
|
148
|
+
nextTurn(() => {
|
|
149
|
+
if (error) {
|
|
150
|
+
this.emit('error', error);
|
|
151
|
+
}
|
|
152
|
+
this.emit('close', !!error);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Creates the two ends of an in-memory connection
|
|
158
|
+
*
|
|
159
|
+
* @return `[serverSocket, clientSocket]`
|
|
160
|
+
*/
|
|
161
|
+
export function createSocketPair() {
|
|
162
|
+
const serverSocket = new MemorySocket();
|
|
163
|
+
const clientSocket = new MemorySocket();
|
|
164
|
+
serverSocket.peer = clientSocket;
|
|
165
|
+
clientSocket.peer = serverSocket;
|
|
166
|
+
return [serverSocket, clientSocket];
|
|
167
|
+
}
|
|
168
|
+
// one ordered queue for both ends, so the data and events of a connection keep the order they were sent in
|
|
169
|
+
const queue = [];
|
|
170
|
+
function nextTurn(fn) {
|
|
171
|
+
queue.push(fn);
|
|
172
|
+
if (queue.length === 1) {
|
|
173
|
+
setImmediate(runQueue);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function runQueue() {
|
|
177
|
+
// what runs now may queue more, that waits for the next turn
|
|
178
|
+
const batch = queue.splice(0, queue.length);
|
|
179
|
+
for (const fn of batch) {
|
|
180
|
+
fn();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -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',
|
package/dist/esm/plugins/idle.js
CHANGED
|
@@ -20,8 +20,8 @@ export default function idlePlugin(server) {
|
|
|
20
20
|
connection.end();
|
|
21
21
|
}
|
|
22
22
|
}, 30 * 60 * 1000);
|
|
23
|
-
// An idling client should not keep the process alive
|
|
24
|
-
idleTimer.unref();
|
|
23
|
+
// An idling client should not keep the process alive (browsers have no unref)
|
|
24
|
+
idleTimer.unref?.();
|
|
25
25
|
const clearIdleTimer = () => clearTimeout(idleTimer);
|
|
26
26
|
const socket = connection.socket;
|
|
27
27
|
if (socket) {
|
|
@@ -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'];
|