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.
Files changed (74) hide show
  1. package/README.md +51 -29
  2. package/bin/help.txt +3 -2
  3. package/bin/imapkit.js +15 -5
  4. package/dist/browser/imapkit.js +105 -0
  5. package/dist/cjs/commands/handlers/fetch.js +2 -1
  6. package/dist/cjs/commands/handlers/store.js +4 -4
  7. package/dist/cjs/commands/list.js +17 -3
  8. package/dist/cjs/control.d.ts +4 -9
  9. package/dist/cjs/control.js +1 -1
  10. package/dist/cjs/dates.d.ts +9 -1
  11. package/dist/cjs/dates.js +20 -0
  12. package/dist/cjs/envelope.d.ts +1 -1
  13. package/dist/cjs/envelope.js +26 -1
  14. package/dist/cjs/index.d.ts +3 -1
  15. package/dist/cjs/load-plugins.d.ts +3 -1
  16. package/dist/cjs/load-plugins.js +25 -6
  17. package/dist/cjs/memory-socket.d.ts +69 -0
  18. package/dist/cjs/memory-socket.js +187 -0
  19. package/dist/cjs/plugins/binary.js +2 -1
  20. package/dist/cjs/plugins/context-search.js +2 -2
  21. package/dist/cjs/plugins/create-special-use.d.ts +0 -5
  22. package/dist/cjs/plugins/create-special-use.js +11 -1
  23. package/dist/cjs/plugins/idle.js +2 -2
  24. package/dist/cjs/plugins/imap4rev2.js +5 -0
  25. package/dist/cjs/plugins/metadata-server.d.ts +6 -2
  26. package/dist/cjs/plugins/metadata-server.js +3 -1
  27. package/dist/cjs/plugins/metadata.d.ts +5 -1
  28. package/dist/cjs/plugins/metadata.js +3 -0
  29. package/dist/cjs/quirks.d.ts +2 -2
  30. package/dist/cjs/quirks.js +5 -4
  31. package/dist/cjs/rest.d.ts +7 -3
  32. package/dist/cjs/rest.js +11 -5
  33. package/dist/cjs/server.d.ts +67 -3
  34. package/dist/cjs/server.js +241 -93
  35. package/dist/cjs/storage-schema.js +12 -5
  36. package/dist/cjs/store-operations.d.ts +5 -2
  37. package/dist/cjs/store-operations.js +6 -3
  38. package/dist/cjs/types.d.ts +4 -0
  39. package/dist/esm/commands/handlers/fetch.js +2 -1
  40. package/dist/esm/commands/handlers/store.js +4 -4
  41. package/dist/esm/commands/list.js +17 -3
  42. package/dist/esm/control.d.ts +4 -9
  43. package/dist/esm/control.js +1 -1
  44. package/dist/esm/dates.d.ts +9 -1
  45. package/dist/esm/dates.js +20 -1
  46. package/dist/esm/envelope.d.ts +1 -1
  47. package/dist/esm/envelope.js +26 -1
  48. package/dist/esm/index.d.ts +3 -1
  49. package/dist/esm/load-plugins.d.ts +3 -1
  50. package/dist/esm/load-plugins.js +25 -6
  51. package/dist/esm/memory-socket.d.ts +69 -0
  52. package/dist/esm/memory-socket.js +182 -0
  53. package/dist/esm/plugins/binary.js +2 -1
  54. package/dist/esm/plugins/context-search.js +2 -2
  55. package/dist/esm/plugins/create-special-use.d.ts +0 -5
  56. package/dist/esm/plugins/create-special-use.js +8 -1
  57. package/dist/esm/plugins/idle.js +2 -2
  58. package/dist/esm/plugins/imap4rev2.js +5 -0
  59. package/dist/esm/plugins/metadata-server.d.ts +6 -2
  60. package/dist/esm/plugins/metadata-server.js +3 -1
  61. package/dist/esm/plugins/metadata.d.ts +5 -1
  62. package/dist/esm/plugins/metadata.js +3 -0
  63. package/dist/esm/quirks.d.ts +2 -2
  64. package/dist/esm/quirks.js +5 -4
  65. package/dist/esm/rest.d.ts +7 -3
  66. package/dist/esm/rest.js +11 -5
  67. package/dist/esm/server.d.ts +67 -3
  68. package/dist/esm/server.js +243 -95
  69. package/dist/esm/storage-schema.js +12 -5
  70. package/dist/esm/store-operations.d.ts +5 -2
  71. package/dist/esm/store-operations.js +6 -3
  72. package/dist/esm/types.d.ts +4 -0
  73. package/dist/plugin-help.json +3 -2
  74. package/package.json +8 -1
@@ -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
- export default function metadataPlugin(server: IMAPServer): void;
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 };
@@ -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: Set<string>;
27
+ removePlugins: Map<string, string>;
28
28
  };
@@ -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 Set();
110
+ const removePlugins = new Map();
111
111
  [].concat(names || []).forEach(name => {
112
- const quirk = Object.hasOwn(quirks, String(name).toLowerCase()) ? quirks[String(name).toLowerCase()] : null;
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.add(plugin));
118
+ (quirk.removePlugins || []).forEach(plugin => removePlugins.set(plugin, key));
118
119
  });
119
120
  return { rules, removePlugins };
120
121
  }
@@ -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): http.Server;
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
- const routes = getRoutes(server);
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
- return http.createServer((req, res) => {
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 };
@@ -1,11 +1,12 @@
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
+ import type { MemorySocket } from './memory-socket.js';
9
10
  import { ServerScript } from './script.js';
10
11
  import type { DeferredOutput, OutputOperation, ScriptContext, ScriptEvent, ScriptRule } from './script.js';
11
12
  import type { AppendCheck, AppendCheckOptions, AppendDataHandler, AppendMessage, CapabilityCheck, CheckResult, ClosedCheck, CommandCheck, CommandContext, CommandOptions, CommandHandler, ConnectionHandler, ConnectionState, CopyHandler, FetchHandler, IMAPResponse, IMAPServerOptions, ListedMailbox, LiteralFilter, Mailbox, MailboxHandler, MailboxStatus, Message, MessageFilter, MessageHandler, MessageRange, Namespace, Notification, NotifyEvent, NotifyFilter, OutputHandler, ParsedCommand, RangeLimit, Refusal, SearchAccessCheck, SearchHandler, SearchLimit, ServerStorage, StatusHandler, StorageMailbox, StorageMessage, StoreHandler, SubscriptionStandIn, Transport, UrlAccessCheck, UserData } from './types.js';
@@ -85,7 +86,7 @@ declare class IMAPServer extends Stream {
85
86
  /** the SMTP server that `start()` runs for the `smtp` option */
86
87
  smtpServer: SMTPListener | null;
87
88
  /** the HTTP server of the REST API that `start()` runs for the `rest` option */
88
- restServer: http.Server | null;
89
+ restServer: RestServer | null;
89
90
  constructor(options?: IMAPServerOptions);
90
91
  /**
91
92
  * Sets the users from the `users` option, or the default user. The users are deep copied, so that runtime changes
@@ -158,6 +159,18 @@ declare class IMAPServer extends Stream {
158
159
  };
159
160
  address(): ReturnType<net.Server['address']>;
160
161
  createClient(socket: net.Socket): void;
162
+ /**
163
+ * Opens a connection to the server in memory, without a network socket (no `listen()` needed). This is how the
164
+ * server runs in a browser, see the README "Run in a browser"
165
+ *
166
+ * @param {Object} [options] `{ secure }`: false makes the connection a plaintext one, where STARTTLS turns on the
167
+ * encryption (nothing is encrypted, the session only counts as encrypted). By default the connection counts as
168
+ * one over TLS
169
+ * @return {MemorySocket} the client end, write commands to it and read the responses from its `data` events
170
+ */
171
+ connect(options?: {
172
+ secure?: boolean | undefined;
173
+ }): MemorySocket;
161
174
  registerCapability(keyword: string, handler?: CapabilityCheck | null): void;
162
175
  /**
163
176
  * Sets the handler of a command
@@ -370,6 +383,25 @@ declare class IMAPServer extends Stream {
370
383
  * @param {Array} flags Flags of a message
371
384
  */
372
385
  rememberFlags(mailbox: Mailbox, flags: string[]): void;
386
+ /**
387
+ * Checks if a flag can be stored on messages of a mailbox: any flag when the mailbox allows new keywords
388
+ * (PERMANENTFLAGS has `\*`), otherwise only a flag in the PERMANENTFLAGS list that getStatus() builds, the
389
+ * `permanentFlags` of the mailbox and every flag its messages had. RFC 3501 section 7.1 (RFC 9051 section 7.1):
390
+ * PERMANENTFLAGS "indicates which of the known flags the client can change permanently", STORE ignores the others
391
+ *
392
+ * @param {Object} mailbox Mailbox object
393
+ * @param {String} flag Normalized flag
394
+ * @return {Boolean} true if the flag is a permanent flag of the mailbox
395
+ */
396
+ isPermanentFlag(mailbox: Mailbox, flag: string): boolean;
397
+ /**
398
+ * The flags of the PERMANENTFLAGS list (without `\*`): the `permanentFlags` of the mailbox and every flag its
399
+ * messages have or had (`knownFlags`, see rememberFlags)
400
+ *
401
+ * @param {Object} mailbox Mailbox object
402
+ * @return {Array} flags, a new list
403
+ */
404
+ permanentFlagList(mailbox: Mailbox): string[];
373
405
  /**
374
406
  * The current time for dates the server sets itself (INTERNALDATE of a message without one, SAVEDATE). The `now`
375
407
  * option fixes it for repeatable tests: a Date, a timestamp, or a function that returns one
@@ -548,6 +580,8 @@ interface QueuedCommand {
548
580
  rule: ScriptRule;
549
581
  context: ScriptContext;
550
582
  } | undefined;
583
+ /** handler for a command that has no registered one (AUTHENTICATE with an unknown mechanism) */
584
+ handler?: CommandHandler | undefined;
551
585
  }
552
586
  declare class IMAPConnection {
553
587
  [key: string]: any;
@@ -921,7 +955,36 @@ declare class IMAPConnection {
921
955
  * @return {Array} Notifications to send
922
956
  */
923
957
  prepareNotifications(queue: Notification[]): Notification[];
924
- processNotifications(data?: CommandContext | null): void;
958
+ /**
959
+ * Checks if EXPUNGE responses must wait while a command runs: during FETCH, STORE and SEARCH (RFC 3501 section
960
+ * 7.4.1), during the commands that extensions add to this list (see the noExpunge command option), and during UID
961
+ * SEARCH with message numbers in the search criteria (RFC 7162 section 3.2.10.2 for VANISHED, EXPUNGE may wait as
962
+ * well, RFC 3501 only allows it during UID commands)
963
+ *
964
+ * @param {Object} data Parsed command
965
+ * @return {Boolean} true if the command holds back EXPUNGE responses
966
+ */
967
+ holdsExpunge(data: CommandContext): boolean;
968
+ /**
969
+ * Sends the queued notifications. During a command that holds back EXPUNGE responses (see holdsExpunge), or with
970
+ * `beforeCommand` before a command that refers to messages by sequence number, only the notifications queued before
971
+ * the first pending EXPUNGE go out: new messages (EXISTS) and flag changes. RFC 3501 section 5.2: "A server MUST send
972
+ * mailbox size updates automatically if a mailbox size change is observed during the processing of a command",
973
+ * section 7.4.1 forbids only EXPUNGE during FETCH, STORE and SEARCH. What was queued after the EXPUNGE waits with it,
974
+ * an EXISTS sent before it would describe a list that the client can not know yet
975
+ *
976
+ * @param {Object} [data] Parsed command that runs, or null between commands
977
+ * @param {Boolean} [beforeCommand] true when the command has not run yet, then a command with message sequence
978
+ * numbers (COPY, MOVE) also waits with the EXPUNGE responses, its numbers refer to the messages before them
979
+ */
980
+ processNotifications(data?: CommandContext | null, beforeCommand?: boolean): void;
981
+ /**
982
+ * Sends an untagged RECENT response with the number of \Recent messages among the first `count` messages
983
+ *
984
+ * @param {Number} count Number of messages the client was told about
985
+ * @param {Map} sequence Message to the sequence number this session knows it by
986
+ */
987
+ sendRecent(count: number, sequence: Map<Message, number>): void;
925
988
  /**
926
989
  * Compile a command object to a response string and write it to socket.
927
990
  * If the command object has a skipResponse property, the command is
@@ -990,6 +1053,7 @@ declare class IMAPConnection {
990
1053
  * @return {Boolean} true if the command uses message sequence numbers
991
1054
  */
992
1055
  usesSequenceNumbers(parsed: CommandContext): boolean;
1056
+ findSequenceNumbers(parsed: CommandContext): boolean;
993
1057
  /**
994
1058
  * Decodes a SASL client response. It must be valid base64 by the RFC 3501 section 9 grammar,
995
1059
  * "=" stands for an empty initial response (RFC 4959 section 3).