imapkit 4.3.1 → 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.
Files changed (121) hide show
  1. package/README.md +187 -67
  2. package/bin/help.txt +20 -5
  3. package/bin/imapkit.js +64 -24
  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/fetch.js +2 -1
  8. package/dist/cjs/commands/handlers/flags.d.ts +3 -3
  9. package/dist/cjs/commands/handlers/flags.js +3 -3
  10. package/dist/cjs/commands/handlers/store.js +7 -7
  11. package/dist/cjs/commands/list.js +17 -3
  12. package/dist/cjs/commands/rename.js +6 -46
  13. package/dist/cjs/commands/select.js +1 -0
  14. package/dist/cjs/commands/subscribe.js +7 -8
  15. package/dist/cjs/commands/unsubscribe.js +3 -9
  16. package/dist/cjs/control.d.ts +425 -0
  17. package/dist/cjs/control.js +637 -0
  18. package/dist/cjs/dates.d.ts +9 -1
  19. package/dist/cjs/dates.js +20 -0
  20. package/dist/cjs/envelope.d.ts +1 -1
  21. package/dist/cjs/envelope.js +26 -1
  22. package/dist/cjs/index.d.ts +9 -1
  23. package/dist/cjs/index.js +9 -2
  24. package/dist/cjs/load-plugins.d.ts +3 -1
  25. package/dist/cjs/load-plugins.js +28 -4
  26. package/dist/cjs/plugins/acl.js +109 -42
  27. package/dist/cjs/plugins/binary.js +2 -1
  28. package/dist/cjs/plugins/condstore.js +7 -0
  29. package/dist/cjs/plugins/context-search.js +2 -2
  30. package/dist/cjs/plugins/create-special-use.d.ts +0 -5
  31. package/dist/cjs/plugins/create-special-use.js +11 -1
  32. package/dist/cjs/plugins/imap4rev2.js +5 -0
  33. package/dist/cjs/plugins/index.js +1 -3
  34. package/dist/cjs/plugins/metadata-server.d.ts +6 -2
  35. package/dist/cjs/plugins/metadata-server.js +3 -1
  36. package/dist/cjs/plugins/metadata.d.ts +5 -1
  37. package/dist/cjs/plugins/metadata.js +143 -57
  38. package/dist/cjs/plugins/objectid.js +10 -0
  39. package/dist/cjs/plugins/quota.js +50 -2
  40. package/dist/cjs/plugins/savedate.js +1 -1
  41. package/dist/cjs/plugins/special-use.d.ts +0 -8
  42. package/dist/cjs/plugins/special-use.js +23 -0
  43. package/dist/cjs/quirks.d.ts +28 -0
  44. package/dist/cjs/quirks.js +125 -0
  45. package/dist/cjs/random.d.ts +10 -0
  46. package/dist/cjs/random.js +22 -0
  47. package/dist/cjs/rest.d.ts +54 -0
  48. package/dist/cjs/rest.js +567 -0
  49. package/dist/cjs/script.d.ts +33 -4
  50. package/dist/cjs/script.js +64 -4
  51. package/dist/cjs/server.d.ts +199 -9
  52. package/dist/cjs/server.js +603 -227
  53. package/dist/cjs/smtp-listener.d.ts +3 -2
  54. package/dist/cjs/smtp-listener.js +10 -6
  55. package/dist/cjs/storage-schema.d.ts +139 -0
  56. package/dist/cjs/storage-schema.js +222 -0
  57. package/dist/cjs/store-operations.d.ts +171 -0
  58. package/dist/cjs/store-operations.js +425 -0
  59. package/dist/cjs/types.d.ts +26 -1
  60. package/dist/esm/commands/append.js +1 -1
  61. package/dist/esm/commands/create.js +2 -2
  62. package/dist/esm/commands/delete.js +2 -14
  63. package/dist/esm/commands/handlers/fetch.js +2 -1
  64. package/dist/esm/commands/handlers/flags.d.ts +3 -3
  65. package/dist/esm/commands/handlers/flags.js +3 -3
  66. package/dist/esm/commands/handlers/store.js +7 -7
  67. package/dist/esm/commands/list.js +17 -3
  68. package/dist/esm/commands/rename.js +6 -46
  69. package/dist/esm/commands/select.js +1 -0
  70. package/dist/esm/commands/subscribe.js +7 -8
  71. package/dist/esm/commands/unsubscribe.js +3 -9
  72. package/dist/esm/control.d.ts +425 -0
  73. package/dist/esm/control.js +630 -0
  74. package/dist/esm/dates.d.ts +9 -1
  75. package/dist/esm/dates.js +20 -1
  76. package/dist/esm/envelope.d.ts +1 -1
  77. package/dist/esm/envelope.js +26 -1
  78. package/dist/esm/index.d.ts +9 -1
  79. package/dist/esm/index.js +5 -2
  80. package/dist/esm/load-plugins.d.ts +3 -1
  81. package/dist/esm/load-plugins.js +28 -4
  82. package/dist/esm/plugins/acl.js +109 -42
  83. package/dist/esm/plugins/binary.js +2 -1
  84. package/dist/esm/plugins/condstore.js +7 -0
  85. package/dist/esm/plugins/context-search.js +2 -2
  86. package/dist/esm/plugins/create-special-use.d.ts +0 -5
  87. package/dist/esm/plugins/create-special-use.js +8 -1
  88. package/dist/esm/plugins/imap4rev2.js +5 -0
  89. package/dist/esm/plugins/index.js +1 -3
  90. package/dist/esm/plugins/metadata-server.d.ts +6 -2
  91. package/dist/esm/plugins/metadata-server.js +3 -1
  92. package/dist/esm/plugins/metadata.d.ts +5 -1
  93. package/dist/esm/plugins/metadata.js +143 -57
  94. package/dist/esm/plugins/objectid.js +10 -0
  95. package/dist/esm/plugins/quota.js +50 -2
  96. package/dist/esm/plugins/savedate.js +1 -1
  97. package/dist/esm/plugins/special-use.d.ts +0 -8
  98. package/dist/esm/plugins/special-use.js +23 -0
  99. package/dist/esm/quirks.d.ts +28 -0
  100. package/dist/esm/quirks.js +121 -0
  101. package/dist/esm/random.d.ts +10 -0
  102. package/dist/esm/random.js +19 -0
  103. package/dist/esm/rest.d.ts +54 -0
  104. package/dist/esm/rest.js +559 -0
  105. package/dist/esm/script.d.ts +33 -4
  106. package/dist/esm/script.js +63 -4
  107. package/dist/esm/server.d.ts +199 -9
  108. package/dist/esm/server.js +606 -230
  109. package/dist/esm/smtp-listener.d.ts +3 -2
  110. package/dist/esm/smtp-listener.js +10 -6
  111. package/dist/esm/storage-schema.d.ts +139 -0
  112. package/dist/esm/storage-schema.js +218 -0
  113. package/dist/esm/store-operations.d.ts +171 -0
  114. package/dist/esm/store-operations.js +410 -0
  115. package/dist/esm/types.d.ts +26 -1
  116. package/dist/plugin-help.json +3 -16
  117. package/package.json +11 -3
  118. package/dist/cjs/plugins/xtoybird.d.ts +0 -2
  119. package/dist/cjs/plugins/xtoybird.js +0 -236
  120. package/dist/esm/plugins/xtoybird.d.ts +0 -2
  121. package/dist/esm/plugins/xtoybird.js +0 -218
@@ -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,
@@ -3,6 +3,29 @@
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.default = envelope;
5
5
  exports.processAddress = processAddress;
6
+ // RFC 5322 section 4.4: obs-angle-addr = [CFWS] "<" obs-route addr-spec ">" [CFWS], obs-route = obs-domain-list ":",
7
+ // obs-domain-list = *(CFWS / ",") "@" domain *("," [CFWS] ["@" domain]). A domain-literal can hold a colon
8
+ const OBS_ROUTE = /^[\s,]*(@(?:[^:[]|\[[^\]]*\])*):/;
9
+ /**
10
+ * Splits an obsolete source route from an address: the route goes to addr-adl, the rest is the addr-spec
11
+ * (RFC 9051 section 7.5.2: the at-domain-list is the "source route and obs-route ABNF production from [RFC5322]",
12
+ * the mailbox name the "local-part ABNF production"). The route is sent like Dovecot does, "@a,@b"
13
+ *
14
+ * @param {String} address Address from the angle brackets
15
+ * @return {Array} [route or null, addr-spec]
16
+ */
17
+ function splitRoute(address) {
18
+ const match = address.match(OBS_ROUTE);
19
+ if (!match) {
20
+ return [null, address];
21
+ }
22
+ const route = match[1]
23
+ .split(',')
24
+ .map(domain => domain.trim())
25
+ .filter(domain => domain)
26
+ .join(',');
27
+ return [route, address.substr(match[0].length).trim()];
28
+ }
6
29
  /**
7
30
  * Convert a message object to an ENVELOPE object
8
31
  *
@@ -61,12 +84,14 @@ function processAddress(arr, def) {
61
84
  if (!address) {
62
85
  return;
63
86
  }
87
+ const [route, addrSpec] = splitRoute(address);
88
+ address = addrSpec;
64
89
  const at = address.lastIndexOf('@');
65
90
  const user = at >= 0 ? address.substr(0, at) : address;
66
91
  // RFC 3501 7.4.2 reserves a NIL host for group markers, so an address without a domain gets
67
92
  // the placeholder host that Dovecot uses for the same input
68
93
  const domain = (at >= 0 ? address.substr(at + 1) : '') || 'MISSING_DOMAIN';
69
- result.push([name, null, user || null, domain]);
94
+ result.push([name, route, user || null, domain]);
70
95
  });
71
96
  // env-from = "(" 1*address ")", there is no SP between the addresses (RFC 3501 section 9)
72
97
  Object.defineProperty(result, 'adjacentLists', { value: true });
@@ -1,10 +1,18 @@
1
1
  import createServer, { TAG_REGEX, IMAPServer, IMAPConnection } from './server.js';
2
- export { TAG_REGEX, IMAPServer, IMAPConnection };
2
+ import { ImapKitError } from './control.js';
3
+ import { quirks } from './quirks.js';
4
+ import { storageSchema, validateStorage } from './storage-schema.js';
5
+ export { TAG_REGEX, IMAPServer, IMAPConnection, ImapKitError, quirks, storageSchema, validateStorage };
3
6
  export type { Attribute, ParsedCommand, IMAPResponse, Notification, Callback, CommandHandler, CommandOptions, Plugin, IMAPError, Message, Mailbox, StorageNamespace, UserData, IMAPServerOptions } from './types.js';
4
7
  export type { ScriptRule, ScriptContext, ScriptEvent, ScriptBytes, ScriptHandle } from './script.js';
8
+ export type { Quirk } from './quirks.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';
5
11
  declare const imapkit: typeof createServer & {
6
12
  TAG_REGEX: RegExp;
7
13
  IMAPServer: typeof IMAPServer;
8
14
  IMAPConnection: typeof IMAPConnection;
15
+ ImapKitError: typeof ImapKitError;
16
+ quirks: Record<string, import("./quirks.js").Quirk>;
9
17
  };
10
18
  export default imapkit;
package/dist/cjs/index.js CHANGED
@@ -33,14 +33,21 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.IMAPConnection = exports.IMAPServer = exports.TAG_REGEX = void 0;
36
+ exports.validateStorage = exports.storageSchema = exports.quirks = exports.ImapKitError = exports.IMAPConnection = exports.IMAPServer = exports.TAG_REGEX = void 0;
37
37
  const server_js_1 = __importStar(require("./server.js"));
38
38
  Object.defineProperty(exports, "TAG_REGEX", { enumerable: true, get: function () { return server_js_1.TAG_REGEX; } });
39
39
  Object.defineProperty(exports, "IMAPServer", { enumerable: true, get: function () { return server_js_1.IMAPServer; } });
40
40
  Object.defineProperty(exports, "IMAPConnection", { enumerable: true, get: function () { return server_js_1.IMAPConnection; } });
41
+ const control_js_1 = require("./control.js");
42
+ Object.defineProperty(exports, "ImapKitError", { enumerable: true, get: function () { return control_js_1.ImapKitError; } });
43
+ const quirks_js_1 = require("./quirks.js");
44
+ Object.defineProperty(exports, "quirks", { enumerable: true, get: function () { return quirks_js_1.quirks; } });
45
+ const storage_schema_js_1 = require("./storage-schema.js");
46
+ Object.defineProperty(exports, "storageSchema", { enumerable: true, get: function () { return storage_schema_js_1.storageSchema; } });
47
+ Object.defineProperty(exports, "validateStorage", { enumerable: true, get: function () { return storage_schema_js_1.validateStorage; } });
41
48
  // `imapkit(options)` creates a server. TAG_REGEX was a property of the CommonJS export, it and the
42
49
  // classes are properties of the factory with both module formats
43
- const imapkit = Object.assign(server_js_1.default, { TAG_REGEX: server_js_1.TAG_REGEX, IMAPServer: server_js_1.IMAPServer, IMAPConnection: server_js_1.IMAPConnection });
50
+ const imapkit = Object.assign(server_js_1.default, { TAG_REGEX: server_js_1.TAG_REGEX, IMAPServer: server_js_1.IMAPServer, IMAPConnection: server_js_1.IMAPConnection, ImapKitError: control_js_1.ImapKitError, quirks: quirks_js_1.quirks });
44
51
  exports.default = imapkit;
45
52
  {
46
53
  const defaultExport = exports.default;
@@ -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): void;
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 };
@@ -49,13 +49,27 @@ function resolvePlugin(name) {
49
49
  *
50
50
  * @param {Object} server IMAPServer instance
51
51
  * @param {Array|String|Function} plugins List of plugins to load
52
+ * @param {Map} [exclude] Plugins a quirk preset leaves out, plugin name to quirk name
53
+ * @throws {Error} when a loaded plugin requires an excluded one
52
54
  */
53
- function loadPlugins(server, plugins) {
55
+ function loadPlugins(server, plugins, exclude) {
54
56
  const loaded = new Set();
55
- const load = (entry) => {
57
+ // plugins a quirk preset leaves out (no-move), they are not loaded even when listed, and the quirk that removes them
58
+ const excluded = new Map();
59
+ (exclude || new Map()).forEach((quirk, name) => {
60
+ const resolved = resolvePlugin(name);
61
+ if (resolved) {
62
+ excluded.set(index_js_1.plugins[resolved], quirk);
63
+ }
64
+ });
65
+ // `requiredBy` names the plugin whose `requires` lists this one
66
+ const load = (entry, requiredBy) => {
56
67
  let plugin = entry;
57
68
  if (typeof plugin === 'string') {
58
69
  const name = resolvePlugin(plugin);
70
+ if (!name && plugin.toUpperCase() === 'XTOYBIRD') {
71
+ throw new Error('XTOYBIRD was removed in 5.0.0, use the control API (server.control.snapshot(), server.control.addUser() ...) instead, see the README');
72
+ }
59
73
  if (!name) {
60
74
  throw new Error('Unknown plugin "' + plugin + '". Available plugins: ' + listPlugins().join(', '));
61
75
  }
@@ -64,14 +78,24 @@ function loadPlugins(server, plugins) {
64
78
  if (typeof plugin !== 'function') {
65
79
  throw new TypeError('Invalid plugin, expecting a plugin name or a function');
66
80
  }
81
+ const quirk = excluded.get(plugin);
82
+ if (quirk) {
83
+ if (requiredBy) {
84
+ // the plugin can not work without it (IMAP4rev2 folds in MOVE and UIDPLUS, RFC 9051 Appendix E), so
85
+ // advertising it without the required plugin would break its RFC
86
+ throw new Error(requiredBy + ' requires ' + entry + ', which the "' + quirk + '" quirk removes');
87
+ }
88
+ return;
89
+ }
67
90
  if (loaded.has(plugin)) {
68
91
  return;
69
92
  }
70
93
  loaded.add(plugin);
71
- [].concat(plugin.requires || []).forEach(load);
94
+ const name = typeof entry === 'string' ? entry.trim() : plugin.name;
95
+ [].concat(plugin.requires || []).forEach(required => load(required, name));
72
96
  plugin(server);
73
97
  };
74
- [].concat(plugins || []).forEach(load);
98
+ [].concat(plugins || []).forEach(entry => load(entry));
75
99
  server.emit('pluginsLoaded');
76
100
  }
77
101
  exports.default = loadPlugins;
@@ -11,6 +11,7 @@ const list_extensions_js_1 = require("../list-extensions.js");
11
11
  const select_js_1 = require("../commands/select.js");
12
12
  const append_js_1 = require("../commands/append.js");
13
13
  const arguments_js_1 = require("../arguments.js");
14
+ const store_operations_js_1 = require("../store-operations.js");
14
15
  /**
15
16
  * @help Adds ACL [RFC4314] capability with RIGHTS=texk
16
17
  * @help The owner (server option "aclOwner", default "testuser")
@@ -205,6 +206,33 @@ function aclPlugin(server) {
205
206
  change(acl);
206
207
  server.emit('acl', mailbox, previous);
207
208
  };
209
+ /**
210
+ * Changes the rights of an identifier like SETACL (RFC 4314 section 3.1): "+" adds, "-" removes, no mode
211
+ * replaces. An identifier without rights is removed from the ACL
212
+ *
213
+ * @param {Object} mailbox Mailbox
214
+ * @param {String} identifier Identifier
215
+ * @param {String} mode "+", "-" or ""
216
+ * @param {Set} rights Parsed rights
217
+ */
218
+ const applyRights = (mailbox, identifier, mode, rights) => {
219
+ changeAcl(mailbox, acl => {
220
+ const current = new Set(acl.get(identifier) || []);
221
+ if (mode === '+') {
222
+ rights.forEach(right => current.add(right));
223
+ }
224
+ else if (mode === '-') {
225
+ rights.forEach(right => current.delete(right));
226
+ }
227
+ const updated = mode ? current : rights;
228
+ if (updated.size) {
229
+ acl.set(identifier, updated);
230
+ }
231
+ else {
232
+ acl.delete(identifier);
233
+ }
234
+ });
235
+ };
208
236
  // CATENATE (RFC 4469 section 5): reading a message through an IMAP URL needs the "r" right, a
209
237
  // mailbox without "l" is reported like one that does not exist (RFC 4314 section 6)
210
238
  server.urlAccessChecks.push((connection, mailbox) => {
@@ -360,22 +388,7 @@ function aclPlugin(server) {
360
388
  if (!checkAclCommand(connection, parsed, data, mailbox, hasAdmin) || refuseOwnerIdentifier(connection, parsed, data)) {
361
389
  return;
362
390
  }
363
- changeAcl(mailbox, acl => {
364
- const current = new Set(acl.get(identifier) || []);
365
- if (mode === '+') {
366
- rights.forEach(right => current.add(right));
367
- }
368
- else if (mode === '-') {
369
- rights.forEach(right => current.delete(right));
370
- }
371
- const updated = mode ? current : rights;
372
- if (updated.size) {
373
- acl.set(identifier, updated);
374
- }
375
- else {
376
- acl.delete(identifier);
377
- }
378
- });
391
+ applyRights(mailbox, identifier, mode, rights);
379
392
  connection.sendStatus(parsed, data, 'OK', 'Setacl complete');
380
393
  }), Object.assign({ astringArguments: [1, 2] }, aclOptions));
381
394
  // RFC 4314 section 3.2
@@ -473,7 +486,7 @@ function aclPlugin(server) {
473
486
  }
474
487
  flags = [].concat(flags);
475
488
  // invalid flags are still refused with BAD, even if the user could not set them anyway
476
- flags.forEach((flag) => (0, flags_js_1.checkSystemFlags)(connection, (0, flags_js_1.normalizeSystemFlag)(flagValue(flag))));
489
+ flags.forEach((flag) => (0, flags_js_1.checkSystemFlags)(connection.server, (0, flags_js_1.normalizeSystemFlag)(flagValue(flag))));
477
490
  let allowed = flags.filter((flag) => canSetFlag(rights, flag));
478
491
  if (item.charAt(0) !== '+' && item.charAt(0) !== '-') {
479
492
  // replacing the flags keeps the ones the user can not change
@@ -568,7 +581,7 @@ function aclPlugin(server) {
568
581
  const flags = parsed.attributes[1];
569
582
  if (Array.isArray(flags) && flags.every((flag) => flag && flag.type === 'ATOM')) {
570
583
  try {
571
- flags.forEach((flag) => (0, flags_js_1.checkSystemFlags)(connection, (0, flags_js_1.normalizeSystemFlag)(flag.value)));
584
+ flags.forEach((flag) => (0, flags_js_1.checkSystemFlags)(connection.server, (0, flags_js_1.normalizeSystemFlag)(flag.value)));
572
585
  // the server MUST NOT fail APPEND for flags the user can not set
573
586
  parsed.attributes[1] = flags.filter((flag) => canSetFlag(rights, flag));
574
587
  }
@@ -740,7 +753,7 @@ function aclPlugin(server) {
740
753
  }
741
754
  const flags = [].concat(args[args.length - 1] || []);
742
755
  try {
743
- flags.forEach(flag => (0, flags_js_1.checkSystemFlags)(connection, (0, flags_js_1.normalizeSystemFlag)(flagValue(flag))));
756
+ flags.forEach(flag => (0, flags_js_1.checkSystemFlags)(connection.server, (0, flags_js_1.normalizeSystemFlag)(flagValue(flag))));
744
757
  }
745
758
  catch {
746
759
  // invalid flags are refused with BAD by the STORE handler
@@ -839,40 +852,94 @@ function aclPlugin(server) {
839
852
  });
840
853
  }
841
854
  });
842
- // The DELETE command MUST delete the ACL of the mailbox (RFC 4314 section 4), this matters when a
843
- // \Noselect placeholder stays for the children of the mailbox
844
- // DELETE and CREATE are core commands, so the handlers exist
845
- const prevDelete = server.getCommandHandler('DELETE');
846
- server.setCommandHandler('DELETE', (connection, parsed, data, callback) => {
847
- const mailbox = argumentMailbox(parsed, 0);
848
- prevDelete(connection, parsed, data, () => {
849
- const placeholder = argumentMailbox(parsed, 0);
850
- if (placeholder && placeholder !== mailbox) {
855
+ server.on('mailbox', (event) => {
856
+ if (event.type === 'delete') {
857
+ // DELETE MUST delete the ACL of the mailbox (RFC 4314 section 4), this matters when a \Noselect
858
+ // placeholder stays for the children of the mailbox
859
+ const placeholder = server.getMailbox(event.path);
860
+ if (placeholder && placeholder !== event.mailbox) {
851
861
  delete placeholder.acl;
852
862
  }
853
- callback();
854
- });
855
- });
856
- // A new mailbox inherits the ACL of its parent (RFC 4314 section 4, CREATE SHOULD)
857
- const prevCreate = server.getCommandHandler('CREATE');
858
- server.setCommandHandler('CREATE', (connection, parsed, data, callback) => {
859
- const existing = new Set(Object.keys(server.folderCache));
860
- prevCreate(connection, parsed, data, () => {
861
- Object.keys(server.folderCache)
862
- .filter(path => !existing.has(path))
863
- .sort((a, b) => a.length - b.length)
864
- .forEach(path => {
863
+ }
864
+ else if (event.type === 'create') {
865
+ // A new mailbox inherits the ACL of its parent (RFC 4314 section 4, CREATE SHOULD)
866
+ (event.created || []).forEach((path) => {
865
867
  const parent = getParent(path);
866
868
  const mailbox = server.folderCache[path];
867
- if (parent && parent !== mailbox) {
869
+ if (parent && mailbox && parent !== mailbox) {
868
870
  const acl = new Map();
869
871
  getAcl(parent).forEach((rights, identifier) => acl.set(identifier, new Set(rights)));
870
872
  mailbox.acl = acl;
871
873
  }
872
874
  });
873
- callback();
875
+ }
876
+ });
877
+ // Control API (README "Control API"): ACLs as objects of identifier to rights, like the storage option
878
+ const aclObject = (mailbox) => {
879
+ const result = {};
880
+ getAcl(mailbox).forEach((rights, identifier) => {
881
+ result[identifier] = formatRights(rights);
874
882
  });
883
+ return result;
884
+ };
885
+ const controlMailbox = (path) => server.control.requireMailbox(path);
886
+ // the snapshot keeps ACLs in the form of the storage option, the parsed ACL is a Map
887
+ server.control.snapshotHandlers.push((mailbox, copy) => {
888
+ if (mailbox.acl instanceof Map) {
889
+ copy.acl = aclObject(mailbox);
890
+ }
875
891
  });
892
+ const getAclOperation = (path) => aclObject(controlMailbox(path));
893
+ // rights replace the rights of the identifier, "+rights" adds and "-rights" removes like SETACL (RFC 4314
894
+ // section 3.1), empty rights remove the identifier
895
+ const setAclOperation = (path, identifier, rights) => {
896
+ const mailbox = controlMailbox(path);
897
+ if (typeof identifier !== 'string' || !identifier || typeof rights !== 'string') {
898
+ throw (0, store_operations_js_1.storeError)('setAcl expects an identifier and a rights string', 'INVALID');
899
+ }
900
+ const mode = /^[+-]/.test(rights) ? rights.charAt(0) : '';
901
+ let parsed;
902
+ try {
903
+ parsed = parseRights(rights.substr(mode.length));
904
+ }
905
+ catch (err) {
906
+ throw (0, store_operations_js_1.storeError)(err.message, 'INVALID');
907
+ }
908
+ applyRights(mailbox, identifier, mode, parsed);
909
+ return aclObject(mailbox);
910
+ };
911
+ const deleteAclOperation = (path, identifier) => {
912
+ const mailbox = controlMailbox(path);
913
+ if (typeof identifier !== 'string' || !getAcl(mailbox).has(identifier)) {
914
+ throw (0, store_operations_js_1.storeError)('No ACL entry for ' + JSON.stringify(identifier), 'NONEXISTENT');
915
+ }
916
+ changeAcl(mailbox, acl => acl.delete(identifier));
917
+ return aclObject(mailbox);
918
+ };
919
+ server.control.register('getAcl', getAclOperation, [
920
+ {
921
+ method: 'GET',
922
+ path: '/v1/mailboxes/{path}/acl',
923
+ summary: 'The ACL of a mailbox (ACL plugin)',
924
+ handler: ({ params }) => server.control.getAcl(params.path)
925
+ }
926
+ ]);
927
+ server.control.register('setAcl', setAclOperation, [
928
+ {
929
+ method: 'PUT',
930
+ path: '/v1/mailboxes/{path}/acl/{identifier}',
931
+ summary: 'Sets the rights of an identifier ({ rights }, "+" or "-" in front adds or removes, ACL plugin)',
932
+ handler: ({ params, body }) => server.control.setAcl(params.path, params.identifier, body.rights)
933
+ }
934
+ ]);
935
+ server.control.register('deleteAcl', deleteAclOperation, [
936
+ {
937
+ method: 'DELETE',
938
+ path: '/v1/mailboxes/{path}/acl/{identifier}',
939
+ summary: 'Removes an identifier from the ACL (ACL plugin)',
940
+ handler: ({ params }) => server.control.deleteAcl(params.path, params.identifier)
941
+ }
942
+ ]);
876
943
  }
877
944
  {
878
945
  const defaultExport = exports.default;
@@ -109,7 +109,8 @@ function getDecodedSection(connection, message, query) {
109
109
  }
110
110
  const node = (0, mimeparser_js_1.resolveNode)((0, mimeparser_js_1.getMessageData)(message).tree, section[0].value, connection.messageGlobal);
111
111
  if (!node) {
112
- // like BODY[<section>], a part that does not exist is empty
112
+ // like BODY[<section>], a part that does not exist is empty: RFC 3516 and RFC 9051 section 6.4.5 define no
113
+ // error for it, and BINARY follows the semantics of BODY (RFC 3516 section 4.2)
113
114
  return '';
114
115
  }
115
116
  if (!isLeaf(node, connection.messageGlobal)) {
@@ -78,6 +78,13 @@ function condstorePlugin(server) {
78
78
  server.on('expunge', mailbox => {
79
79
  bumpModseq(mailbox);
80
80
  });
81
+ // Flags changed outside of STORE (the control API, see changeFlags() in store-operations.ts): every changed
82
+ // message gets a new mod-sequence (RFC 7162 section 3.1.1), before the sessions are told
83
+ server.on('flags', (mailbox, messages) => {
84
+ messages.forEach(message => {
85
+ message.MODSEQ = bumpModseq(mailbox);
86
+ });
87
+ });
81
88
  // RFC 7162 section 3.2.11: SELECT or EXAMINE closes the selected mailbox, a CONDSTORE server marks
82
89
  // where the responses for the new mailbox start
83
90
  server.closedChecks.push(() => true);
@@ -252,8 +252,8 @@ function contextSearchPlugin(server) {
252
252
  // knows the current message list, so new matches can be reported after their EXISTS and FETCH responses
253
253
  // (RFC 5267 section 4.3.2). A command that closes the mailbox gets no updates, its tagged response ends them
254
254
  const processNotifications = connection.processNotifications;
255
- connection.processNotifications = function (data) {
256
- processNotifications.call(this, data);
255
+ connection.processNotifications = function (data, beforeCommand) {
256
+ processNotifications.call(this, data, beforeCommand);
257
257
  const command = ((data && data.command) || '').toUpperCase();
258
258
  if (this.searchContexts && this.searchContexts.size && !this.notificationQueue.length && !CLOSING_COMMANDS.has(command)) {
259
259
  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,11 +1,18 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.default = createSpecialUsePlugin;
7
+ const formal_1 = __importDefault(require("imap-handler/lib/formal"));
8
+ const arguments_js_1 = require("../arguments.js");
4
9
  /**
5
10
  * @help Enables CREATE-SPECIAL-USE [RFC6154] capability
6
11
  * @help Allowed special flags can be set with server
7
12
  * @help option "special-use"
8
13
  */
14
+ // use-attr-ext = "\" atom (RFC 6154 section 6), with the ATOM-CHAR of the imap-handler grammar (RFC 3501 section 9)
15
+ const USE_ATTR = new RegExp('^\\\\[' + formal_1.default['ATOM-CHAR']().replace(/[\\\]^-]/g, '\\$&') + ']+$');
9
16
  function createSpecialUsePlugin(server) {
10
17
  // Register capability
11
18
  server.registerCapability('CREATE-SPECIAL-USE');
@@ -34,7 +41,10 @@ function createSpecialUsePlugin(server) {
34
41
  }
35
42
  if (specialUseList) {
36
43
  for (i = 0, len = specialUseList.length; i < len; i++) {
37
- if (['ATOM', 'STRING', 'LITERAL'].indexOf(specialUseList[i].type) < 0) {
44
+ // RFC 6154 section 6: use-attr-ext = "\" atom, so every entry is an atom that starts with a backslash,
45
+ // not NIL, a string, a literal, a number or a list
46
+ const entry = specialUseList[i];
47
+ if (!(0, arguments_js_1.isAtom)(entry) || !USE_ATTR.test(entry.value)) {
38
48
  connection.send({
39
49
  tag: parsed.tag,
40
50
  command: 'BAD',
@@ -128,6 +128,11 @@ function imap4rev2Plugin(server) {
128
128
  }, 'SELECT LIST', parsed, data, mailbox);
129
129
  };
130
130
  server.outputHandlers.push((connection, response, description, parsed, data) => {
131
+ if (description === 'RECENT NOTIFICATION' && isRev2(connection)) {
132
+ // Appendix E item 12: no RECENT response after the EXISTS of new messages either
133
+ response.skipResponse = true;
134
+ return;
135
+ }
131
136
  if (!parsed || !response || !isRev2(connection)) {
132
137
  return;
133
138
  }
@@ -59,7 +59,6 @@ const unselect_js_1 = __importDefault(require("./unselect.js"));
59
59
  const utf8_accept_js_1 = __importDefault(require("./utf8-accept.js"));
60
60
  const x_gm_ext_1_js_1 = __importDefault(require("./x-gm-ext-1.js"));
61
61
  const xoauth2_js_1 = __importDefault(require("./xoauth2.js"));
62
- const xtoybird_js_1 = __importDefault(require("./xtoybird.js"));
63
62
  exports.plugins = {
64
63
  acl: acl_js_1.default,
65
64
  appendlimit: appendlimit_js_1.default,
@@ -114,6 +113,5 @@ exports.plugins = {
114
113
  unselect: unselect_js_1.default,
115
114
  'utf8-accept': utf8_accept_js_1.default,
116
115
  'x-gm-ext-1': x_gm_ext_1_js_1.default,
117
- xoauth2: xoauth2_js_1.default,
118
- xtoybird: xtoybird_js_1.default
116
+ xoauth2: xoauth2_js_1.default
119
117
  };
@@ -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
- export default function metadataServerPlugin(server: IMAPServer): void;
7
+ declare function metadataServerPlugin(server: IMAPServer): void;
8
+ declare namespace metadataServerPlugin {
9
+ var requires: string[];
10
+ }
11
+ export default metadataServerPlugin;
@@ -4,12 +4,14 @@ exports.default = metadataServerPlugin;
4
4
  const metadata_js_1 = require("./metadata.js");
5
5
  /**
6
6
  * @help Adds METADATA-SERVER [RFC5464] capability, like METADATA but
7
- * @help only for server annotations (mailbox name ""). With METADATA
7
+ * @help only for server annotations (mailbox name ""), loads ENABLE. With METADATA
8
8
  * @help also loaded, only METADATA is advertised
9
9
  */
10
10
  function metadataServerPlugin(server) {
11
11
  (0, metadata_js_1.setup)(server, false);
12
12
  }
13
+ // RFC 5464 section 4.1: a server that sends unsolicited METADATA responses "MUST support the ENABLE command"
14
+ metadataServerPlugin.requires = ['ENABLE'];
13
15
  {
14
16
  const defaultExport = exports.default;
15
17
  if (defaultExport && (typeof defaultExport === 'function' || typeof defaultExport === 'object')) {
@@ -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 };