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,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, null, user || null, domain]);
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 });
@@ -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/esm/index.js CHANGED
@@ -1,6 +1,9 @@
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
  // `imapkit(options)` creates a server. TAG_REGEX was a property of the CommonJS export, it and the
4
7
  // classes are properties of the factory with both module formats
5
- const imapkit = Object.assign(createServer, { TAG_REGEX, IMAPServer, IMAPConnection });
8
+ const imapkit = Object.assign(createServer, { TAG_REGEX, IMAPServer, IMAPConnection, ImapKitError, quirks });
6
9
  export default imapkit;
@@ -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 };
@@ -46,13 +46,27 @@ 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
- function loadPlugins(server, plugins) {
52
+ function loadPlugins(server, plugins, exclude) {
51
53
  const loaded = new Set();
52
- const load = (entry) => {
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) => {
53
64
  let plugin = entry;
54
65
  if (typeof plugin === 'string') {
55
66
  const name = resolvePlugin(plugin);
67
+ if (!name && plugin.toUpperCase() === 'XTOYBIRD') {
68
+ throw new Error('XTOYBIRD was removed in 5.0.0, use the control API (server.control.snapshot(), server.control.addUser() ...) instead, see the README');
69
+ }
56
70
  if (!name) {
57
71
  throw new Error('Unknown plugin "' + plugin + '". Available plugins: ' + listPlugins().join(', '));
58
72
  }
@@ -61,14 +75,24 @@ function loadPlugins(server, plugins) {
61
75
  if (typeof plugin !== 'function') {
62
76
  throw new TypeError('Invalid plugin, expecting a plugin name or a function');
63
77
  }
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
+ }
64
87
  if (loaded.has(plugin)) {
65
88
  return;
66
89
  }
67
90
  loaded.add(plugin);
68
- [].concat(plugin.requires || []).forEach(load);
91
+ const name = typeof entry === 'string' ? entry.trim() : plugin.name;
92
+ [].concat(plugin.requires || []).forEach(required => load(required, name));
69
93
  plugin(server);
70
94
  };
71
- [].concat(plugins || []).forEach(load);
95
+ [].concat(plugins || []).forEach(entry => load(entry));
72
96
  server.emit('pluginsLoaded');
73
97
  }
74
98
  export default loadPlugins;
@@ -5,6 +5,7 @@ import { getListExtensions } from '../list-extensions.js';
5
5
  import { beginSelect } from '../commands/select.js';
6
6
  import { getPendingTarget } from '../commands/append.js';
7
7
  import { isAstring } from '../arguments.js';
8
+ import { storeError } from '../store-operations.js';
8
9
  /**
9
10
  * @help Adds ACL [RFC4314] capability with RIGHTS=texk
10
11
  * @help The owner (server option "aclOwner", default "testuser")
@@ -199,6 +200,33 @@ export default function aclPlugin(server) {
199
200
  change(acl);
200
201
  server.emit('acl', mailbox, previous);
201
202
  };
203
+ /**
204
+ * Changes the rights of an identifier like SETACL (RFC 4314 section 3.1): "+" adds, "-" removes, no mode
205
+ * replaces. An identifier without rights is removed from the ACL
206
+ *
207
+ * @param {Object} mailbox Mailbox
208
+ * @param {String} identifier Identifier
209
+ * @param {String} mode "+", "-" or ""
210
+ * @param {Set} rights Parsed rights
211
+ */
212
+ const applyRights = (mailbox, identifier, mode, rights) => {
213
+ changeAcl(mailbox, acl => {
214
+ const current = new Set(acl.get(identifier) || []);
215
+ if (mode === '+') {
216
+ rights.forEach(right => current.add(right));
217
+ }
218
+ else if (mode === '-') {
219
+ rights.forEach(right => current.delete(right));
220
+ }
221
+ const updated = mode ? current : rights;
222
+ if (updated.size) {
223
+ acl.set(identifier, updated);
224
+ }
225
+ else {
226
+ acl.delete(identifier);
227
+ }
228
+ });
229
+ };
202
230
  // CATENATE (RFC 4469 section 5): reading a message through an IMAP URL needs the "r" right, a
203
231
  // mailbox without "l" is reported like one that does not exist (RFC 4314 section 6)
204
232
  server.urlAccessChecks.push((connection, mailbox) => {
@@ -354,22 +382,7 @@ export default function aclPlugin(server) {
354
382
  if (!checkAclCommand(connection, parsed, data, mailbox, hasAdmin) || refuseOwnerIdentifier(connection, parsed, data)) {
355
383
  return;
356
384
  }
357
- changeAcl(mailbox, acl => {
358
- const current = new Set(acl.get(identifier) || []);
359
- if (mode === '+') {
360
- rights.forEach(right => current.add(right));
361
- }
362
- else if (mode === '-') {
363
- rights.forEach(right => current.delete(right));
364
- }
365
- const updated = mode ? current : rights;
366
- if (updated.size) {
367
- acl.set(identifier, updated);
368
- }
369
- else {
370
- acl.delete(identifier);
371
- }
372
- });
385
+ applyRights(mailbox, identifier, mode, rights);
373
386
  connection.sendStatus(parsed, data, 'OK', 'Setacl complete');
374
387
  }), Object.assign({ astringArguments: [1, 2] }, aclOptions));
375
388
  // RFC 4314 section 3.2
@@ -467,7 +480,7 @@ export default function aclPlugin(server) {
467
480
  }
468
481
  flags = [].concat(flags);
469
482
  // invalid flags are still refused with BAD, even if the user could not set them anyway
470
- flags.forEach((flag) => checkSystemFlags(connection, normalizeSystemFlag(flagValue(flag))));
483
+ flags.forEach((flag) => checkSystemFlags(connection.server, normalizeSystemFlag(flagValue(flag))));
471
484
  let allowed = flags.filter((flag) => canSetFlag(rights, flag));
472
485
  if (item.charAt(0) !== '+' && item.charAt(0) !== '-') {
473
486
  // replacing the flags keeps the ones the user can not change
@@ -562,7 +575,7 @@ export default function aclPlugin(server) {
562
575
  const flags = parsed.attributes[1];
563
576
  if (Array.isArray(flags) && flags.every((flag) => flag && flag.type === 'ATOM')) {
564
577
  try {
565
- flags.forEach((flag) => checkSystemFlags(connection, normalizeSystemFlag(flag.value)));
578
+ flags.forEach((flag) => checkSystemFlags(connection.server, normalizeSystemFlag(flag.value)));
566
579
  // the server MUST NOT fail APPEND for flags the user can not set
567
580
  parsed.attributes[1] = flags.filter((flag) => canSetFlag(rights, flag));
568
581
  }
@@ -734,7 +747,7 @@ export default function aclPlugin(server) {
734
747
  }
735
748
  const flags = [].concat(args[args.length - 1] || []);
736
749
  try {
737
- flags.forEach(flag => checkSystemFlags(connection, normalizeSystemFlag(flagValue(flag))));
750
+ flags.forEach(flag => checkSystemFlags(connection.server, normalizeSystemFlag(flagValue(flag))));
738
751
  }
739
752
  catch {
740
753
  // invalid flags are refused with BAD by the STORE handler
@@ -833,38 +846,92 @@ export default function aclPlugin(server) {
833
846
  });
834
847
  }
835
848
  });
836
- // The DELETE command MUST delete the ACL of the mailbox (RFC 4314 section 4), this matters when a
837
- // \Noselect placeholder stays for the children of the mailbox
838
- // DELETE and CREATE are core commands, so the handlers exist
839
- const prevDelete = server.getCommandHandler('DELETE');
840
- server.setCommandHandler('DELETE', (connection, parsed, data, callback) => {
841
- const mailbox = argumentMailbox(parsed, 0);
842
- prevDelete(connection, parsed, data, () => {
843
- const placeholder = argumentMailbox(parsed, 0);
844
- if (placeholder && placeholder !== mailbox) {
849
+ server.on('mailbox', (event) => {
850
+ if (event.type === 'delete') {
851
+ // DELETE MUST delete the ACL of the mailbox (RFC 4314 section 4), this matters when a \Noselect
852
+ // placeholder stays for the children of the mailbox
853
+ const placeholder = server.getMailbox(event.path);
854
+ if (placeholder && placeholder !== event.mailbox) {
845
855
  delete placeholder.acl;
846
856
  }
847
- callback();
848
- });
849
- });
850
- // A new mailbox inherits the ACL of its parent (RFC 4314 section 4, CREATE SHOULD)
851
- const prevCreate = server.getCommandHandler('CREATE');
852
- server.setCommandHandler('CREATE', (connection, parsed, data, callback) => {
853
- const existing = new Set(Object.keys(server.folderCache));
854
- prevCreate(connection, parsed, data, () => {
855
- Object.keys(server.folderCache)
856
- .filter(path => !existing.has(path))
857
- .sort((a, b) => a.length - b.length)
858
- .forEach(path => {
857
+ }
858
+ else if (event.type === 'create') {
859
+ // A new mailbox inherits the ACL of its parent (RFC 4314 section 4, CREATE SHOULD)
860
+ (event.created || []).forEach((path) => {
859
861
  const parent = getParent(path);
860
862
  const mailbox = server.folderCache[path];
861
- if (parent && parent !== mailbox) {
863
+ if (parent && mailbox && parent !== mailbox) {
862
864
  const acl = new Map();
863
865
  getAcl(parent).forEach((rights, identifier) => acl.set(identifier, new Set(rights)));
864
866
  mailbox.acl = acl;
865
867
  }
866
868
  });
867
- callback();
869
+ }
870
+ });
871
+ // Control API (README "Control API"): ACLs as objects of identifier to rights, like the storage option
872
+ const aclObject = (mailbox) => {
873
+ const result = {};
874
+ getAcl(mailbox).forEach((rights, identifier) => {
875
+ result[identifier] = formatRights(rights);
868
876
  });
877
+ return result;
878
+ };
879
+ const controlMailbox = (path) => server.control.requireMailbox(path);
880
+ // the snapshot keeps ACLs in the form of the storage option, the parsed ACL is a Map
881
+ server.control.snapshotHandlers.push((mailbox, copy) => {
882
+ if (mailbox.acl instanceof Map) {
883
+ copy.acl = aclObject(mailbox);
884
+ }
869
885
  });
886
+ const getAclOperation = (path) => aclObject(controlMailbox(path));
887
+ // rights replace the rights of the identifier, "+rights" adds and "-rights" removes like SETACL (RFC 4314
888
+ // section 3.1), empty rights remove the identifier
889
+ const setAclOperation = (path, identifier, rights) => {
890
+ const mailbox = controlMailbox(path);
891
+ if (typeof identifier !== 'string' || !identifier || typeof rights !== 'string') {
892
+ throw storeError('setAcl expects an identifier and a rights string', 'INVALID');
893
+ }
894
+ const mode = /^[+-]/.test(rights) ? rights.charAt(0) : '';
895
+ let parsed;
896
+ try {
897
+ parsed = parseRights(rights.substr(mode.length));
898
+ }
899
+ catch (err) {
900
+ throw storeError(err.message, 'INVALID');
901
+ }
902
+ applyRights(mailbox, identifier, mode, parsed);
903
+ return aclObject(mailbox);
904
+ };
905
+ const deleteAclOperation = (path, identifier) => {
906
+ const mailbox = controlMailbox(path);
907
+ if (typeof identifier !== 'string' || !getAcl(mailbox).has(identifier)) {
908
+ throw storeError('No ACL entry for ' + JSON.stringify(identifier), 'NONEXISTENT');
909
+ }
910
+ changeAcl(mailbox, acl => acl.delete(identifier));
911
+ return aclObject(mailbox);
912
+ };
913
+ server.control.register('getAcl', getAclOperation, [
914
+ {
915
+ method: 'GET',
916
+ path: '/v1/mailboxes/{path}/acl',
917
+ summary: 'The ACL of a mailbox (ACL plugin)',
918
+ handler: ({ params }) => server.control.getAcl(params.path)
919
+ }
920
+ ]);
921
+ server.control.register('setAcl', setAclOperation, [
922
+ {
923
+ method: 'PUT',
924
+ path: '/v1/mailboxes/{path}/acl/{identifier}',
925
+ summary: 'Sets the rights of an identifier ({ rights }, "+" or "-" in front adds or removes, ACL plugin)',
926
+ handler: ({ params, body }) => server.control.setAcl(params.path, params.identifier, body.rights)
927
+ }
928
+ ]);
929
+ server.control.register('deleteAcl', deleteAclOperation, [
930
+ {
931
+ method: 'DELETE',
932
+ path: '/v1/mailboxes/{path}/acl/{identifier}',
933
+ summary: 'Removes an identifier from the ACL (ACL plugin)',
934
+ handler: ({ params }) => server.control.deleteAcl(params.path, params.identifier)
935
+ }
936
+ ]);
870
937
  }
@@ -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)) {
@@ -75,6 +75,13 @@ export default function condstorePlugin(server) {
75
75
  server.on('expunge', mailbox => {
76
76
  bumpModseq(mailbox);
77
77
  });
78
+ // Flags changed outside of STORE (the control API, see changeFlags() in store-operations.ts): every changed
79
+ // message gets a new mod-sequence (RFC 7162 section 3.1.1), before the sessions are told
80
+ server.on('flags', (mailbox, messages) => {
81
+ messages.forEach(message => {
82
+ message.MODSEQ = bumpModseq(mailbox);
83
+ });
84
+ });
78
85
  // RFC 7162 section 3.2.11: SELECT or EXAMINE closes the selected mailbox, a CONDSTORE server marks
79
86
  // where the responses for the new mailbox start
80
87
  server.closedChecks.push(() => true);
@@ -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
- if (['ATOM', 'STRING', 'LITERAL'].indexOf(specialUseList[i].type) < 0) {
38
+ // RFC 6154 section 6: use-attr-ext = "\" atom, so every entry is an atom that starts with a backslash,
39
+ // not NIL, a string, a literal, a number or a list
40
+ const entry = specialUseList[i];
41
+ if (!isAtom(entry) || !USE_ATTR.test(entry.value)) {
35
42
  connection.send({
36
43
  tag: parsed.tag,
37
44
  command: 'BAD',
@@ -122,6 +122,11 @@ export default function imap4rev2Plugin(server) {
122
122
  }, 'SELECT LIST', parsed, data, mailbox);
123
123
  };
124
124
  server.outputHandlers.push((connection, response, description, parsed, data) => {
125
+ if (description === 'RECENT NOTIFICATION' && isRev2(connection)) {
126
+ // Appendix E item 12: no RECENT response after the EXISTS of new messages either
127
+ response.skipResponse = true;
128
+ return;
129
+ }
125
130
  if (!parsed || !response || !isRev2(connection)) {
126
131
  return;
127
132
  }
@@ -53,7 +53,6 @@ import unselectPlugin from './unselect.js';
53
53
  import utf8AcceptPlugin from './utf8-accept.js';
54
54
  import xGmExt1Plugin from './x-gm-ext-1.js';
55
55
  import xoauth2Plugin from './xoauth2.js';
56
- import xtoybirdPlugin from './xtoybird.js';
57
56
  export const plugins = {
58
57
  acl: aclPlugin,
59
58
  appendlimit: appendlimitPlugin,
@@ -108,6 +107,5 @@ export const plugins = {
108
107
  unselect: unselectPlugin,
109
108
  'utf8-accept': utf8AcceptPlugin,
110
109
  'x-gm-ext-1': xGmExt1Plugin,
111
- xoauth2: xoauth2Plugin,
112
- xtoybird: xtoybirdPlugin
110
+ xoauth2: xoauth2Plugin
113
111
  };
@@ -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;
@@ -1,9 +1,11 @@
1
1
  import { setup } from './metadata.js';
2
2
  /**
3
3
  * @help Adds METADATA-SERVER [RFC5464] capability, like METADATA but
4
- * @help only for server annotations (mailbox name ""). With METADATA
4
+ * @help only for server annotations (mailbox name ""), loads ENABLE. With METADATA
5
5
  * @help also loaded, only METADATA is advertised
6
6
  */
7
7
  export default function metadataServerPlugin(server) {
8
8
  setup(server, false);
9
9
  }
10
+ // RFC 5464 section 4.1: a server that sends unsolicited METADATA responses "MUST support the ENABLE command"
11
+ metadataServerPlugin.requires = ['ENABLE'];
@@ -7,5 +7,9 @@ import type { IMAPServer } from '../types.js';
7
7
  * @param {Boolean} mailboxes If true, mailbox annotations are supported, otherwise only server annotations
8
8
  */
9
9
  declare function setup(server: IMAPServer, mailboxes: boolean): void;
10
- 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 };