imapkit 4.3.1 → 5.0.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.
Files changed (93) hide show
  1. package/README.md +167 -51
  2. package/bin/help.txt +17 -3
  3. package/bin/imapkit.js +53 -23
  4. package/dist/cjs/commands/append.js +1 -1
  5. package/dist/cjs/commands/create.js +2 -2
  6. package/dist/cjs/commands/delete.js +2 -14
  7. package/dist/cjs/commands/handlers/flags.d.ts +3 -3
  8. package/dist/cjs/commands/handlers/flags.js +3 -3
  9. package/dist/cjs/commands/handlers/store.js +3 -3
  10. package/dist/cjs/commands/rename.js +6 -46
  11. package/dist/cjs/commands/select.js +1 -0
  12. package/dist/cjs/commands/subscribe.js +7 -8
  13. package/dist/cjs/commands/unsubscribe.js +3 -9
  14. package/dist/cjs/control.d.ts +430 -0
  15. package/dist/cjs/control.js +637 -0
  16. package/dist/cjs/index.d.ts +8 -1
  17. package/dist/cjs/index.js +9 -2
  18. package/dist/cjs/load-plugins.d.ts +1 -1
  19. package/dist/cjs/load-plugins.js +7 -2
  20. package/dist/cjs/plugins/acl.js +109 -42
  21. package/dist/cjs/plugins/condstore.js +7 -0
  22. package/dist/cjs/plugins/index.js +1 -3
  23. package/dist/cjs/plugins/metadata.js +140 -57
  24. package/dist/cjs/plugins/objectid.js +10 -0
  25. package/dist/cjs/plugins/quota.js +50 -2
  26. package/dist/cjs/plugins/savedate.js +1 -1
  27. package/dist/cjs/plugins/special-use.d.ts +0 -8
  28. package/dist/cjs/plugins/special-use.js +23 -0
  29. package/dist/cjs/quirks.d.ts +28 -0
  30. package/dist/cjs/quirks.js +124 -0
  31. package/dist/cjs/random.d.ts +10 -0
  32. package/dist/cjs/random.js +22 -0
  33. package/dist/cjs/rest.d.ts +50 -0
  34. package/dist/cjs/rest.js +561 -0
  35. package/dist/cjs/script.d.ts +33 -4
  36. package/dist/cjs/script.js +64 -4
  37. package/dist/cjs/server.d.ts +147 -8
  38. package/dist/cjs/server.js +398 -144
  39. package/dist/cjs/smtp-listener.d.ts +3 -2
  40. package/dist/cjs/smtp-listener.js +10 -6
  41. package/dist/cjs/storage-schema.d.ts +139 -0
  42. package/dist/cjs/storage-schema.js +215 -0
  43. package/dist/cjs/store-operations.d.ts +168 -0
  44. package/dist/cjs/store-operations.js +422 -0
  45. package/dist/cjs/types.d.ts +22 -1
  46. package/dist/esm/commands/append.js +1 -1
  47. package/dist/esm/commands/create.js +2 -2
  48. package/dist/esm/commands/delete.js +2 -14
  49. package/dist/esm/commands/handlers/flags.d.ts +3 -3
  50. package/dist/esm/commands/handlers/flags.js +3 -3
  51. package/dist/esm/commands/handlers/store.js +3 -3
  52. package/dist/esm/commands/rename.js +6 -46
  53. package/dist/esm/commands/select.js +1 -0
  54. package/dist/esm/commands/subscribe.js +7 -8
  55. package/dist/esm/commands/unsubscribe.js +3 -9
  56. package/dist/esm/control.d.ts +430 -0
  57. package/dist/esm/control.js +630 -0
  58. package/dist/esm/index.d.ts +8 -1
  59. package/dist/esm/index.js +5 -2
  60. package/dist/esm/load-plugins.d.ts +1 -1
  61. package/dist/esm/load-plugins.js +7 -2
  62. package/dist/esm/plugins/acl.js +109 -42
  63. package/dist/esm/plugins/condstore.js +7 -0
  64. package/dist/esm/plugins/index.js +1 -3
  65. package/dist/esm/plugins/metadata.js +140 -57
  66. package/dist/esm/plugins/objectid.js +10 -0
  67. package/dist/esm/plugins/quota.js +50 -2
  68. package/dist/esm/plugins/savedate.js +1 -1
  69. package/dist/esm/plugins/special-use.d.ts +0 -8
  70. package/dist/esm/plugins/special-use.js +23 -0
  71. package/dist/esm/quirks.d.ts +28 -0
  72. package/dist/esm/quirks.js +120 -0
  73. package/dist/esm/random.d.ts +10 -0
  74. package/dist/esm/random.js +19 -0
  75. package/dist/esm/rest.d.ts +50 -0
  76. package/dist/esm/rest.js +553 -0
  77. package/dist/esm/script.d.ts +33 -4
  78. package/dist/esm/script.js +63 -4
  79. package/dist/esm/server.d.ts +147 -8
  80. package/dist/esm/server.js +399 -145
  81. package/dist/esm/smtp-listener.d.ts +3 -2
  82. package/dist/esm/smtp-listener.js +10 -6
  83. package/dist/esm/storage-schema.d.ts +139 -0
  84. package/dist/esm/storage-schema.js +211 -0
  85. package/dist/esm/store-operations.d.ts +168 -0
  86. package/dist/esm/store-operations.js +407 -0
  87. package/dist/esm/types.d.ts +22 -1
  88. package/dist/plugin-help.json +0 -14
  89. package/package.json +11 -3
  90. package/dist/cjs/plugins/xtoybird.d.ts +0 -2
  91. package/dist/cjs/plugins/xtoybird.js +0 -236
  92. package/dist/esm/plugins/xtoybird.d.ts +0 -2
  93. package/dist/esm/plugins/xtoybird.js +0 -218
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.default = specialUsePlugin;
4
4
  const list_extensions_js_1 = require("../list-extensions.js");
5
+ const store_operations_js_1 = require("../store-operations.js");
5
6
  /**
6
7
  * @help Enables SPECIAL-USE [RFC6154] capability
7
8
  * @help Mailboxes need to have a "special-use"
@@ -10,6 +11,8 @@ const list_extensions_js_1 = require("../list-extensions.js");
10
11
  * @help With LIST-EXTENDED the SPECIAL-USE selection and
11
12
  * @help return options work with the other LIST options
12
13
  */
14
+ // RFC 6154 section 2 and RFC 8457 (\\Important)
15
+ const SPECIAL_USES = new Set(['\\All', '\\Archive', '\\Drafts', '\\Flagged', '\\Junk', '\\Sent', '\\Trash', '\\Important']);
13
16
  function specialUsePlugin(server) {
14
17
  // Register capability
15
18
  server.registerCapability('SPECIAL-USE');
@@ -80,6 +83,26 @@ function specialUsePlugin(server) {
80
83
  }
81
84
  }
82
85
  });
86
+ // Control API (README "Control API"): the special-use attributes of a mailbox (RFC 6154 section 2)
87
+ server.control.mailboxInfoHandlers.push((mailbox, info) => {
88
+ info.specialUse = getSpecialUse(mailbox);
89
+ });
90
+ const setSpecialUseOperation = (path, uses) => {
91
+ const mailbox = server.control.requireMailbox(path, true);
92
+ if (!Array.isArray(uses) || uses.some(use => typeof use !== 'string' || !SPECIAL_USES.has(use))) {
93
+ throw (0, store_operations_js_1.storeError)('Special-use attributes must be a list of ' + [...SPECIAL_USES].join(', '), 'INVALID');
94
+ }
95
+ mailbox['special-use'] = [...new Set(uses)];
96
+ return server.control.getMailbox(mailbox.path);
97
+ };
98
+ server.control.register('setSpecialUse', setSpecialUseOperation, [
99
+ {
100
+ method: 'PUT',
101
+ path: '/v1/mailboxes/{path}/special-use',
102
+ summary: 'Sets the special-use attributes of a mailbox ({ specialUse: ["\\Sent"] }, SPECIAL-USE plugin)',
103
+ handler: ({ params, body }) => server.control.setSpecialUse(params.path, body.specialUse)
104
+ }
105
+ ]);
83
106
  }
84
107
  {
85
108
  const defaultExport = exports.default;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Quirk presets (#87): named sets of script rules (and plugins to leave out) that make the server behave like a known
3
+ * real server, so a client test reproduces the bug of that server in every run, without the server itself. The
4
+ * `quirks` option turns them on, the rules come after the rules of the `script` option. The probabilistic ones use
5
+ * the random numbers of the `scriptSeed` option, so a run can be repeated. The presets are exported as data, copy
6
+ * and adjust one as script rules when it does not fit
7
+ */
8
+ import type { ScriptRule } from './script.js';
9
+ /** A quirk preset */
10
+ export interface Quirk {
11
+ description: string;
12
+ /** script rules, see README "Scripted faults" */
13
+ rules?: ScriptRule[] | undefined;
14
+ /** plugins that are not loaded even when the `plugins` option lists them */
15
+ removePlugins?: string[] | undefined;
16
+ }
17
+ export declare const quirks: Record<string, Quirk>;
18
+ /**
19
+ * Resolves the `quirks` option
20
+ *
21
+ * @param {Array|String} names Quirk names
22
+ * @return {Object} `{ rules, removePlugins }` of all of them
23
+ * @throws {Error} for an unknown name
24
+ */
25
+ export declare function resolveQuirks(names: string[] | string | null | undefined): {
26
+ rules: ScriptRule[];
27
+ removePlugins: Set<string>;
28
+ };
@@ -0,0 +1,124 @@
1
+ "use strict";
2
+ /**
3
+ * Quirk presets (#87): named sets of script rules (and plugins to leave out) that make the server behave like a known
4
+ * real server, so a client test reproduces the bug of that server in every run, without the server itself. The
5
+ * `quirks` option turns them on, the rules come after the rules of the `script` option. The probabilistic ones use
6
+ * the random numbers of the `scriptSeed` option, so a run can be repeated. The presets are exported as data, copy
7
+ * and adjust one as script rules when it does not fit
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.quirks = void 0;
11
+ exports.resolveQuirks = resolveQuirks;
12
+ const FETCH_COMMANDS = ['FETCH', 'UID FETCH'];
13
+ /**
14
+ * The part a body section belongs to: "2.MIME", "2.HEADER" and "2" all belong to part 2, "HEADER" and "TEXT" to the
15
+ * message itself
16
+ *
17
+ * @param {Array} section Section of a BODY item, its first entry is the section spec
18
+ * @return {String} part path, "" for the message
19
+ */
20
+ function partPath(section) {
21
+ const spec = String((section[0] && section[0].value) || '').toUpperCase();
22
+ return spec.replace(/(^|\.)(MIME|HEADER(\.FIELDS(\.NOT)?)?|TEXT)$/, '');
23
+ }
24
+ /**
25
+ * Changes the BODY[...] values of an untagged FETCH response
26
+ *
27
+ * @param {Object} response Untagged FETCH response
28
+ * @param {Function} change `(name, value, index)` returns the new value
29
+ * @return {Object|undefined} the changed response, nothing for other responses
30
+ */
31
+ function changeBodyValues(response, change) {
32
+ const items = response.attributes && response.attributes[2];
33
+ if (!Array.isArray(items) || !response.attributes[1] || response.attributes[1].value !== 'FETCH') {
34
+ return undefined;
35
+ }
36
+ const changed = items.slice();
37
+ for (let i = 0; i < changed.length - 1; i += 2) {
38
+ const name = changed[i];
39
+ if (name && name.type === 'ATOM' && String(name.value).toUpperCase() === 'BODY' && Array.isArray(name.section)) {
40
+ changed[i + 1] = change(name, changed[i + 1]);
41
+ }
42
+ }
43
+ return Object.assign({}, response, { attributes: [response.attributes[0], response.attributes[1], changed] });
44
+ }
45
+ exports.quirks = {
46
+ 'james-fetchgroup': {
47
+ description: 'Apache James FetchGroup: only the first section asked for a part in one FETCH is answered, later ones for the same ' +
48
+ 'part come back empty. BODY[2.MIME] BODY[2] gives a zero-length body, the reverse order loses the headers',
49
+ rules: [
50
+ {
51
+ on: 'response',
52
+ command: FETCH_COMMANDS,
53
+ untagged: true,
54
+ mutate: response => {
55
+ const seen = new Set();
56
+ return changeBodyValues(response, (name, value) => {
57
+ const path = partPath(name.section);
58
+ if (seen.has(path)) {
59
+ return { type: 'LITERAL', value: '' };
60
+ }
61
+ seen.add(path);
62
+ return value;
63
+ });
64
+ }
65
+ }
66
+ ]
67
+ },
68
+ 'james-late-fetch': {
69
+ description: 'Apache James: now and then (1 in 4) a FETCH response comes after the tagged OK of its command',
70
+ rules: [{ on: 'response', command: FETCH_COMMANDS, untagged: true, chance: 0.25, defer: 'tagged' }]
71
+ },
72
+ 'yahoo-quoted-sections': {
73
+ description: 'Yahoo: short body sections (up to 100 octets without line breaks) are quoted strings instead of literals',
74
+ rules: [
75
+ {
76
+ on: 'response',
77
+ command: FETCH_COMMANDS,
78
+ untagged: true,
79
+ mutate: response => changeBodyValues(response, (name, value) => value && value.type === 'LITERAL' && typeof value.value === 'string' && value.value.length <= 100 && /^[\x20-\x7e]*$/.test(value.value)
80
+ ? { type: 'STRING', value: value.value }
81
+ : value)
82
+ }
83
+ ]
84
+ },
85
+ 'm365-throttle': {
86
+ description: 'Microsoft 365: 1 in 10 commands (not LOGOUT) is refused with "BAD Request is throttled. Suggested Backoff Time: 1000 milliseconds"',
87
+ rules: [
88
+ {
89
+ on: 'command',
90
+ chance: 0.1,
91
+ when: context => context.command !== 'LOGOUT',
92
+ send: '$TAG BAD Request is throttled. Suggested Backoff Time: 1000 milliseconds\r\n'
93
+ }
94
+ ]
95
+ },
96
+ 'no-uidplus': {
97
+ description: 'A server without UIDPLUS: no APPENDUID, COPYUID or UID EXPUNGE, even when the plugins list it',
98
+ removePlugins: ['UIDPLUS']
99
+ },
100
+ 'no-move': {
101
+ description: 'A server without MOVE, clients have to COPY, STORE \\Deleted and EXPUNGE',
102
+ removePlugins: ['MOVE']
103
+ }
104
+ };
105
+ /**
106
+ * Resolves the `quirks` option
107
+ *
108
+ * @param {Array|String} names Quirk names
109
+ * @return {Object} `{ rules, removePlugins }` of all of them
110
+ * @throws {Error} for an unknown name
111
+ */
112
+ function resolveQuirks(names) {
113
+ const rules = [];
114
+ const removePlugins = new Set();
115
+ [].concat(names || []).forEach(name => {
116
+ const quirk = Object.hasOwn(exports.quirks, String(name).toLowerCase()) ? exports.quirks[String(name).toLowerCase()] : null;
117
+ if (!quirk) {
118
+ throw new Error('Unknown quirk "' + name + '". Available quirks: ' + Object.keys(exports.quirks).join(', '));
119
+ }
120
+ rules.push(...(quirk.rules || []));
121
+ (quirk.removePlugins || []).forEach(plugin => removePlugins.add(plugin));
122
+ });
123
+ return { rules, removePlugins };
124
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Seeded random numbers, so that randomized test behavior (UID shuffles, script rule chances) can be repeated
3
+ */
4
+ /**
5
+ * A small seeded random number generator (mulberry32), for repeatable "shuffle" orders
6
+ *
7
+ * @param {Number} seed Seed
8
+ * @return {Function} returns numbers from 0 (inclusive) to 1 (exclusive)
9
+ */
10
+ export declare function seededRandom(seed: number): () => number;
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ /**
3
+ * Seeded random numbers, so that randomized test behavior (UID shuffles, script rule chances) can be repeated
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.seededRandom = seededRandom;
7
+ /**
8
+ * A small seeded random number generator (mulberry32), for repeatable "shuffle" orders
9
+ *
10
+ * @param {Number} seed Seed
11
+ * @return {Function} returns numbers from 0 (inclusive) to 1 (exclusive)
12
+ */
13
+ function seededRandom(seed) {
14
+ let state = seed >>> 0;
15
+ return () => {
16
+ state = (state + 0x6d2b79f5) >>> 0;
17
+ let t = state;
18
+ t = Math.imul(t ^ (t >>> 15), t | 1);
19
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
20
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
21
+ };
22
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * The REST API: the control API over HTTP, for tests written in any language. Off unless the `rest` option (or
3
+ * `--rest-port`) turns it on. It is full control over the server, including the mail of every user, so it listens
4
+ * on 127.0.0.1 by default, needs a bearer token on any other address, sends no CORS headers, takes JSON bodies
5
+ * only (a browser can not send them cross-origin without a CORS preflight) and, without a token, only answers
6
+ * requests for a loopback host name (DNS rebinding). Built on node:http only.
7
+ */
8
+ import http from 'node:http';
9
+ import type { IMAPServer } from './types.js';
10
+ import type { ControlRoute as Route } from './control.js';
11
+ /** Options of the REST API, the `rest` server option */
12
+ interface RestOptions {
13
+ port?: number | undefined;
14
+ /** address to listen on, 127.0.0.1 by default. Any other than a loopback address needs a token */
15
+ host?: string | undefined;
16
+ /** bearer token that every request must send in `Authorization: Bearer <token>` */
17
+ token?: string | undefined;
18
+ }
19
+ /**
20
+ * Checks if a host name or address is a loopback one
21
+ *
22
+ * @param {String} host Host name or address, can be in brackets
23
+ * @return {Boolean} true for localhost, 127.0.0.0/8 and ::1
24
+ */
25
+ declare function isLoopback(host: string): boolean;
26
+ /**
27
+ * Builds the route table of a server
28
+ *
29
+ * @param {Object} server IMAP server
30
+ * @return {Array} routes
31
+ */
32
+ declare function getRoutes(server: IMAPServer): Route[];
33
+ /**
34
+ * Describes the routes as an OpenAPI 3.1 document
35
+ *
36
+ * @param {Array} routes Route table
37
+ * @return {Object} OpenAPI document
38
+ */
39
+ declare function openApi(routes: Route[]): Record<string, unknown>;
40
+ /**
41
+ * Creates the HTTP server of the REST API, it does not listen yet
42
+ *
43
+ * @param {Object} server IMAP server
44
+ * @param {Object} options `{ host, token }`
45
+ * @return {Object} HTTP server
46
+ * @throws {Error} for a host that is not a loopback address without a token
47
+ */
48
+ declare function createRestServer(server: IMAPServer, options: RestOptions): http.Server;
49
+ export { createRestServer, isLoopback, getRoutes, openApi };
50
+ export type { RestOptions };