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.
- package/README.md +167 -51
- package/bin/help.txt +17 -3
- package/bin/imapkit.js +53 -23
- package/dist/cjs/commands/append.js +1 -1
- package/dist/cjs/commands/create.js +2 -2
- package/dist/cjs/commands/delete.js +2 -14
- package/dist/cjs/commands/handlers/flags.d.ts +3 -3
- package/dist/cjs/commands/handlers/flags.js +3 -3
- package/dist/cjs/commands/handlers/store.js +3 -3
- package/dist/cjs/commands/rename.js +6 -46
- package/dist/cjs/commands/select.js +1 -0
- package/dist/cjs/commands/subscribe.js +7 -8
- package/dist/cjs/commands/unsubscribe.js +3 -9
- package/dist/cjs/control.d.ts +430 -0
- package/dist/cjs/control.js +637 -0
- package/dist/cjs/index.d.ts +8 -1
- package/dist/cjs/index.js +9 -2
- package/dist/cjs/load-plugins.d.ts +1 -1
- package/dist/cjs/load-plugins.js +7 -2
- package/dist/cjs/plugins/acl.js +109 -42
- package/dist/cjs/plugins/condstore.js +7 -0
- package/dist/cjs/plugins/index.js +1 -3
- package/dist/cjs/plugins/metadata.js +140 -57
- package/dist/cjs/plugins/objectid.js +10 -0
- package/dist/cjs/plugins/quota.js +50 -2
- package/dist/cjs/plugins/savedate.js +1 -1
- package/dist/cjs/plugins/special-use.d.ts +0 -8
- package/dist/cjs/plugins/special-use.js +23 -0
- package/dist/cjs/quirks.d.ts +28 -0
- package/dist/cjs/quirks.js +124 -0
- package/dist/cjs/random.d.ts +10 -0
- package/dist/cjs/random.js +22 -0
- package/dist/cjs/rest.d.ts +50 -0
- package/dist/cjs/rest.js +561 -0
- package/dist/cjs/script.d.ts +33 -4
- package/dist/cjs/script.js +64 -4
- package/dist/cjs/server.d.ts +147 -8
- package/dist/cjs/server.js +398 -144
- package/dist/cjs/smtp-listener.d.ts +3 -2
- package/dist/cjs/smtp-listener.js +10 -6
- package/dist/cjs/storage-schema.d.ts +139 -0
- package/dist/cjs/storage-schema.js +215 -0
- package/dist/cjs/store-operations.d.ts +168 -0
- package/dist/cjs/store-operations.js +422 -0
- package/dist/cjs/types.d.ts +22 -1
- package/dist/esm/commands/append.js +1 -1
- package/dist/esm/commands/create.js +2 -2
- package/dist/esm/commands/delete.js +2 -14
- package/dist/esm/commands/handlers/flags.d.ts +3 -3
- package/dist/esm/commands/handlers/flags.js +3 -3
- package/dist/esm/commands/handlers/store.js +3 -3
- package/dist/esm/commands/rename.js +6 -46
- package/dist/esm/commands/select.js +1 -0
- package/dist/esm/commands/subscribe.js +7 -8
- package/dist/esm/commands/unsubscribe.js +3 -9
- package/dist/esm/control.d.ts +430 -0
- package/dist/esm/control.js +630 -0
- package/dist/esm/index.d.ts +8 -1
- package/dist/esm/index.js +5 -2
- package/dist/esm/load-plugins.d.ts +1 -1
- package/dist/esm/load-plugins.js +7 -2
- package/dist/esm/plugins/acl.js +109 -42
- package/dist/esm/plugins/condstore.js +7 -0
- package/dist/esm/plugins/index.js +1 -3
- package/dist/esm/plugins/metadata.js +140 -57
- package/dist/esm/plugins/objectid.js +10 -0
- package/dist/esm/plugins/quota.js +50 -2
- package/dist/esm/plugins/savedate.js +1 -1
- package/dist/esm/plugins/special-use.d.ts +0 -8
- package/dist/esm/plugins/special-use.js +23 -0
- package/dist/esm/quirks.d.ts +28 -0
- package/dist/esm/quirks.js +120 -0
- package/dist/esm/random.d.ts +10 -0
- package/dist/esm/random.js +19 -0
- package/dist/esm/rest.d.ts +50 -0
- package/dist/esm/rest.js +553 -0
- package/dist/esm/script.d.ts +33 -4
- package/dist/esm/script.js +63 -4
- package/dist/esm/server.d.ts +147 -8
- package/dist/esm/server.js +399 -145
- package/dist/esm/smtp-listener.d.ts +3 -2
- package/dist/esm/smtp-listener.js +10 -6
- package/dist/esm/storage-schema.d.ts +139 -0
- package/dist/esm/storage-schema.js +211 -0
- package/dist/esm/store-operations.d.ts +168 -0
- package/dist/esm/store-operations.js +407 -0
- package/dist/esm/types.d.ts +22 -1
- package/dist/plugin-help.json +0 -14
- package/package.json +11 -3
- package/dist/cjs/plugins/xtoybird.d.ts +0 -2
- package/dist/cjs/plugins/xtoybird.js +0 -236
- package/dist/esm/plugins/xtoybird.d.ts +0 -2
- package/dist/esm/plugins/xtoybird.js +0 -218
package/dist/esm/load-plugins.js
CHANGED
|
@@ -47,12 +47,17 @@ function resolvePlugin(name) {
|
|
|
47
47
|
* @param {Object} server IMAPServer instance
|
|
48
48
|
* @param {Array|String|Function} plugins List of plugins to load
|
|
49
49
|
*/
|
|
50
|
-
function loadPlugins(server, plugins) {
|
|
50
|
+
function loadPlugins(server, plugins, exclude) {
|
|
51
51
|
const loaded = new Set();
|
|
52
|
+
// plugins a quirk preset leaves out (no-move), they are not loaded even when listed
|
|
53
|
+
const excluded = new Set((exclude || []).map(name => resolvePlugin(name)).flatMap(name => (name ? [builtinPlugins[name]] : [])));
|
|
52
54
|
const load = (entry) => {
|
|
53
55
|
let plugin = entry;
|
|
54
56
|
if (typeof plugin === 'string') {
|
|
55
57
|
const name = resolvePlugin(plugin);
|
|
58
|
+
if (!name && plugin.toUpperCase() === 'XTOYBIRD') {
|
|
59
|
+
throw new Error('XTOYBIRD was removed in 5.0.0, use the control API (server.control.snapshot(), server.control.addUser() ...) instead, see the README');
|
|
60
|
+
}
|
|
56
61
|
if (!name) {
|
|
57
62
|
throw new Error('Unknown plugin "' + plugin + '". Available plugins: ' + listPlugins().join(', '));
|
|
58
63
|
}
|
|
@@ -61,7 +66,7 @@ function loadPlugins(server, plugins) {
|
|
|
61
66
|
if (typeof plugin !== 'function') {
|
|
62
67
|
throw new TypeError('Invalid plugin, expecting a plugin name or a function');
|
|
63
68
|
}
|
|
64
|
-
if (loaded.has(plugin)) {
|
|
69
|
+
if (loaded.has(plugin) || excluded.has(plugin)) {
|
|
65
70
|
return;
|
|
66
71
|
}
|
|
67
72
|
loaded.add(plugin);
|
package/dist/esm/plugins/acl.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
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
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
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
|
-
|
|
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
|
}
|
|
@@ -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);
|
|
@@ -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,3 +1,4 @@
|
|
|
1
|
+
import { storeError } from '../store-operations.js';
|
|
1
2
|
import { states } from '../command-states.js';
|
|
2
3
|
import { isNumber } from '../numbers.js';
|
|
3
4
|
import { isAstring } from '../arguments.js';
|
|
@@ -308,6 +309,60 @@ function setup(server, mailboxes) {
|
|
|
308
309
|
return callback();
|
|
309
310
|
}, { states: states.AUTHENTICATED });
|
|
310
311
|
// RFC 5464 section 4.3: setmetadata = "SETMETADATA" SP mailbox SP entry-values
|
|
312
|
+
/**
|
|
313
|
+
* Stores the changes of SETMETADATA (or of the control API) when all of them pass the checks, and tells the
|
|
314
|
+
* sessions that used ENABLE METADATA about the changed entries. The session that made the change
|
|
315
|
+
* (`server.activeConnection`) is not told, and without one (the control API) the read-only server entries can
|
|
316
|
+
* be set too
|
|
317
|
+
*
|
|
318
|
+
* @param {Object} holder Mailbox object or state.server
|
|
319
|
+
* @param {Map} changes Lower case entry name to the new value, null removes the entry
|
|
320
|
+
* @return {Object|null} `{ text, code }` when a change fails, nothing is changed then
|
|
321
|
+
*/
|
|
322
|
+
const storeChanges = (holder, changes) => {
|
|
323
|
+
const origin = server.activeConnection;
|
|
324
|
+
const entries = getEntries(holder);
|
|
325
|
+
const computed = getComputedEntries(holder);
|
|
326
|
+
// changes go to a copy that replaces the entries once every check passed
|
|
327
|
+
const next = Object.assign(Object.create(null), entries);
|
|
328
|
+
const changed = [];
|
|
329
|
+
for (const [name, value] of changes) {
|
|
330
|
+
if ((!holder.path && origin && READ_ONLY_SERVER_ENTRIES.has(name)) || name in computed) {
|
|
331
|
+
return { text: 'The ' + name + ' entry is read-only', code: 'CANNOT' };
|
|
332
|
+
}
|
|
333
|
+
if (!state.allowPrivate && name.startsWith('/private/')) {
|
|
334
|
+
return { text: 'Private annotations are not supported', code: ['METADATA', 'NOPRIVATE'] };
|
|
335
|
+
}
|
|
336
|
+
if (value !== null && value.length > state.maxSize) {
|
|
337
|
+
return { text: 'Value of ' + name + ' is too large', code: ['METADATA', 'MAXSIZE', state.maxSize] };
|
|
338
|
+
}
|
|
339
|
+
if ((entries[name] ?? null) !== value) {
|
|
340
|
+
changed.push(name);
|
|
341
|
+
}
|
|
342
|
+
if (value === null) {
|
|
343
|
+
delete next[name];
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
next[name] = value;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
// only adding entries can fail, even when the storage already holds more than the limit
|
|
350
|
+
const count = Object.keys(next).length;
|
|
351
|
+
if (count > state.maxEntries && count > Object.keys(entries).length) {
|
|
352
|
+
return { text: 'Too many annotations', code: ['METADATA', 'TOOMANY'] };
|
|
353
|
+
}
|
|
354
|
+
setEntries(holder, next);
|
|
355
|
+
if (changed.length) {
|
|
356
|
+
// RFC 5464 section 4.4.2: unsolicited responses only list the entry names. Section 4.1:
|
|
357
|
+
// only sessions that used ENABLE METADATA (or METADATA-SERVER) get them
|
|
358
|
+
server.notify({
|
|
359
|
+
tag: '*',
|
|
360
|
+
command: 'METADATA',
|
|
361
|
+
attributes: [mailboxAttribute(holder.path)].concat(changed.map(name => ({ type: 'ATOM', value: name })))
|
|
362
|
+
}, false, origin, isMetadataEnabled);
|
|
363
|
+
}
|
|
364
|
+
return null;
|
|
365
|
+
};
|
|
311
366
|
server.setCommandHandler('SETMETADATA', (connection, parsed, data, callback) => {
|
|
312
367
|
const attrs = parsed.attributes || [];
|
|
313
368
|
if (attrs.length !== 2 || !isAstring(attrs[0]) || !Array.isArray(attrs[1])) {
|
|
@@ -343,77 +398,105 @@ function setup(server, mailboxes) {
|
|
|
343
398
|
if (!holder) {
|
|
344
399
|
return callback();
|
|
345
400
|
}
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
const fail = (text, code) => {
|
|
401
|
+
const failure = storeChanges(holder, changes);
|
|
402
|
+
if (failure) {
|
|
349
403
|
// RFC 5464 section 4.3: when one entry fails, no entry is changed
|
|
350
|
-
connection.sendStatus(parsed, data, 'NO', text, code);
|
|
404
|
+
connection.sendStatus(parsed, data, 'NO', failure.text, failure.code);
|
|
351
405
|
return callback();
|
|
352
|
-
};
|
|
353
|
-
// changes go to a copy that replaces the entries once every check passed
|
|
354
|
-
const next = Object.assign(Object.create(null), entries);
|
|
355
|
-
const changed = [];
|
|
356
|
-
for (const [name, value] of changes) {
|
|
357
|
-
if ((!holder.path && READ_ONLY_SERVER_ENTRIES.has(name)) || name in computed) {
|
|
358
|
-
return fail('The ' + name + ' entry is read-only', 'CANNOT');
|
|
359
|
-
}
|
|
360
|
-
if (!state.allowPrivate && name.startsWith('/private/')) {
|
|
361
|
-
return fail('Private annotations are not supported', ['METADATA', 'NOPRIVATE']);
|
|
362
|
-
}
|
|
363
|
-
if (value !== null && value.length > state.maxSize) {
|
|
364
|
-
return fail('Value of ' + name + ' is too large', ['METADATA', 'MAXSIZE', state.maxSize]);
|
|
365
|
-
}
|
|
366
|
-
if ((entries[name] ?? null) !== value) {
|
|
367
|
-
changed.push(name);
|
|
368
|
-
}
|
|
369
|
-
if (value === null) {
|
|
370
|
-
delete next[name];
|
|
371
|
-
}
|
|
372
|
-
else {
|
|
373
|
-
next[name] = value;
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
// only adding entries can fail, even when the storage already holds more than the limit
|
|
377
|
-
const count = Object.keys(next).length;
|
|
378
|
-
if (count > state.maxEntries && count > Object.keys(entries).length) {
|
|
379
|
-
return fail('Too many annotations', ['METADATA', 'TOOMANY']);
|
|
380
|
-
}
|
|
381
|
-
setEntries(holder, next);
|
|
382
|
-
if (changed.length) {
|
|
383
|
-
// RFC 5464 section 4.4.2: unsolicited responses only list the entry names. Section 4.1:
|
|
384
|
-
// only sessions that used ENABLE METADATA (or METADATA-SERVER) get them
|
|
385
|
-
server.notify({
|
|
386
|
-
tag: '*',
|
|
387
|
-
command: 'METADATA',
|
|
388
|
-
attributes: [mailboxAttribute(holder.path)].concat(changed.map(name => ({ type: 'ATOM', value: name })))
|
|
389
|
-
}, false, connection, isMetadataEnabled);
|
|
390
406
|
}
|
|
391
407
|
connection.sendStatus(parsed, data, 'OK', 'SETMETADATA completed');
|
|
392
408
|
return callback();
|
|
393
409
|
},
|
|
394
410
|
// RFC 5464 section 5: values can be literal8, with or without BINARY
|
|
395
411
|
{ states: states.AUTHENTICATED, mailboxArguments: [0], literal8: true });
|
|
396
|
-
server.
|
|
397
|
-
if (
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
if (target && target !== extra && extra.metadata) {
|
|
405
|
-
setEntries(target, Object.assign(Object.create(null), getEntries(extra)));
|
|
412
|
+
server.on('mailbox', (event) => {
|
|
413
|
+
if (event.type === 'rename' && event.oldPath === 'INBOX') {
|
|
414
|
+
// RFC 5464 section 4.1: renaming INBOX copies its annotations, INBOX keeps them. Other mailboxes are
|
|
415
|
+
// moved as objects, so their annotations follow without any help
|
|
416
|
+
const inbox = server.getMailbox('INBOX');
|
|
417
|
+
const target = server.getMailbox(event.path);
|
|
418
|
+
if (inbox && target && target !== inbox && inbox.metadata) {
|
|
419
|
+
setEntries(target, Object.assign(Object.create(null), getEntries(inbox)));
|
|
406
420
|
}
|
|
407
421
|
}
|
|
408
|
-
else if (
|
|
409
|
-
// RFC 5464 section 4.1: a mailbox created later with the same name must not inherit the
|
|
410
|
-
//
|
|
411
|
-
const placeholder =
|
|
412
|
-
if (placeholder) {
|
|
422
|
+
else if (event.type === 'delete') {
|
|
423
|
+
// RFC 5464 section 4.1: a mailbox created later with the same name must not inherit the annotations.
|
|
424
|
+
// A mailbox with children stays as a \Noselect placeholder
|
|
425
|
+
const placeholder = server.getMailbox(event.path);
|
|
426
|
+
if (placeholder && placeholder !== event.mailbox) {
|
|
413
427
|
delete placeholder.metadata;
|
|
414
428
|
}
|
|
415
429
|
}
|
|
416
430
|
});
|
|
431
|
+
// Control API (README "Control API"): the stored annotations of a mailbox, or of the server for "". Values are
|
|
432
|
+
// UTF-8 text, the protocol keeps them as octets
|
|
433
|
+
// control.reset() restores the server annotations of the options, the mailboxes come with their storage
|
|
434
|
+
server.on('reset', () => {
|
|
435
|
+
state.server = { path: '', metadata: options.metadata };
|
|
436
|
+
});
|
|
437
|
+
const controlHolder = (path) => {
|
|
438
|
+
if (path === '') {
|
|
439
|
+
return state.server;
|
|
440
|
+
}
|
|
441
|
+
if (!state.mailboxes) {
|
|
442
|
+
throw storeError('Mailbox annotations need the METADATA plugin, METADATA-SERVER has server annotations only', 'INVALID');
|
|
443
|
+
}
|
|
444
|
+
return server.control.requireMailbox(path);
|
|
445
|
+
};
|
|
446
|
+
const getMetadataOperation = (path) => {
|
|
447
|
+
const entries = getEntries(controlHolder(path));
|
|
448
|
+
const result = {};
|
|
449
|
+
Object.keys(entries).forEach(name => {
|
|
450
|
+
result[name] = Buffer.from(entries[name], 'binary').toString('utf-8');
|
|
451
|
+
});
|
|
452
|
+
return result;
|
|
453
|
+
};
|
|
454
|
+
const setMetadataOperation = (path, values) => {
|
|
455
|
+
const holder = controlHolder(path);
|
|
456
|
+
if (!values || typeof values !== 'object' || Array.isArray(values)) {
|
|
457
|
+
throw storeError('setMetadata expects an object of entry names and values (null removes an entry)', 'INVALID');
|
|
458
|
+
}
|
|
459
|
+
const changes = new Map();
|
|
460
|
+
Object.keys(values).forEach(name => {
|
|
461
|
+
const value = values[name];
|
|
462
|
+
const error = checkEntryName(name, true);
|
|
463
|
+
if (error) {
|
|
464
|
+
throw storeError(error, 'INVALID');
|
|
465
|
+
}
|
|
466
|
+
if (value !== null && typeof value !== 'string') {
|
|
467
|
+
throw storeError('Value of ' + name + ' must be a string or null', 'INVALID');
|
|
468
|
+
}
|
|
469
|
+
changes.set(name.toLowerCase(), value === null ? null : Buffer.from(value, 'utf-8').toString('binary'));
|
|
470
|
+
});
|
|
471
|
+
const failure = storeChanges(holder, changes);
|
|
472
|
+
if (failure) {
|
|
473
|
+
throw storeError(failure.text, typeof failure.code === 'string' ? failure.code : 'INVALID');
|
|
474
|
+
}
|
|
475
|
+
return getMetadataOperation(path);
|
|
476
|
+
};
|
|
477
|
+
server.control.register('getMetadata', getMetadataOperation, [
|
|
478
|
+
{ method: 'GET', path: '/v1/metadata', summary: 'Server annotations (METADATA plugins)', handler: () => server.control.getMetadata('') },
|
|
479
|
+
{
|
|
480
|
+
method: 'GET',
|
|
481
|
+
path: '/v1/mailboxes/{path}/metadata',
|
|
482
|
+
summary: 'Mailbox annotations (METADATA plugin)',
|
|
483
|
+
handler: ({ params }) => server.control.getMetadata(params.path)
|
|
484
|
+
}
|
|
485
|
+
]);
|
|
486
|
+
server.control.register('setMetadata', setMetadataOperation, [
|
|
487
|
+
{
|
|
488
|
+
method: 'PUT',
|
|
489
|
+
path: '/v1/metadata',
|
|
490
|
+
summary: 'Sets server annotations ({ "/shared/comment": "text" }, null removes one, METADATA plugins)',
|
|
491
|
+
handler: ({ body }) => server.control.setMetadata('', body)
|
|
492
|
+
},
|
|
493
|
+
{
|
|
494
|
+
method: 'PUT',
|
|
495
|
+
path: '/v1/mailboxes/{path}/metadata',
|
|
496
|
+
summary: 'Sets mailbox annotations (METADATA plugin)',
|
|
497
|
+
handler: ({ params, body }) => server.control.setMetadata(params.path, body)
|
|
498
|
+
}
|
|
499
|
+
]);
|
|
417
500
|
}
|
|
418
501
|
export default function metadataPlugin(server) {
|
|
419
502
|
setup(server, true);
|
|
@@ -95,6 +95,16 @@ export default function objectidPlugin(server) {
|
|
|
95
95
|
}
|
|
96
96
|
getMailboxId(mailbox);
|
|
97
97
|
});
|
|
98
|
+
// Control API (README "Control API"): the object ids in getMailbox() and getMessage()
|
|
99
|
+
server.control.mailboxInfoHandlers.push((mailbox, info) => {
|
|
100
|
+
if (mailbox.flags.indexOf('\\Noselect') < 0) {
|
|
101
|
+
info.mailboxId = getMailboxId(mailbox);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
server.control.messageInfoHandlers.push((message, info) => {
|
|
105
|
+
info.emailId = message.EMAILID;
|
|
106
|
+
info.threadId = message.THREADID;
|
|
107
|
+
});
|
|
98
108
|
server.messageHandlers.push((server, message, mailbox) => {
|
|
99
109
|
const owner = 'a message of mailbox ' + mailbox.path;
|
|
100
110
|
if (message.EMAILID) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { storeError } from '../store-operations.js';
|
|
1
2
|
import { states } from '../command-states.js';
|
|
2
3
|
import { normalizeLineBreaks } from '../mimeparser.js';
|
|
3
4
|
import { MAX_NUMBER64, isNumber } from '../numbers.js';
|
|
@@ -29,6 +30,8 @@ export default function quotaPlugin(server) {
|
|
|
29
30
|
soft: !!config.soft,
|
|
30
31
|
limits: {}
|
|
31
32
|
};
|
|
33
|
+
// the limits of the quota option, control.reset() restores them
|
|
34
|
+
const initialLimits = {};
|
|
32
35
|
RESOURCES.forEach(resource => {
|
|
33
36
|
if (config[resource] === undefined || config[resource] === null) {
|
|
34
37
|
return;
|
|
@@ -37,8 +40,13 @@ export default function quotaPlugin(server) {
|
|
|
37
40
|
if (!Number.isSafeInteger(limit) || limit < 0) {
|
|
38
41
|
throw new Error('Invalid quota limit for ' + resource + ': ' + JSON.stringify(config[resource]));
|
|
39
42
|
}
|
|
40
|
-
|
|
43
|
+
initialLimits[resource] = limit;
|
|
41
44
|
});
|
|
45
|
+
const restoreLimits = () => {
|
|
46
|
+
root.limits = Object.assign({}, initialLimits);
|
|
47
|
+
};
|
|
48
|
+
restoreLimits();
|
|
49
|
+
server.on('reset', restoreLimits);
|
|
42
50
|
// RFC 9208 sections 3.1.1 and 5: every supported resource is advertised, SETQUOTA needs QUOTASET
|
|
43
51
|
server.registerCapability('QUOTA');
|
|
44
52
|
RESOURCES.forEach(resource => server.registerCapability('QUOTA=RES-' + resource));
|
|
@@ -111,7 +119,7 @@ export default function quotaPlugin(server) {
|
|
|
111
119
|
}
|
|
112
120
|
const added = { MESSAGE: messages.length, bytes: totalSize(messages) };
|
|
113
121
|
// RFC 8508 section 3.4: REPLACE counts only the net usage, the replaced message is removed
|
|
114
|
-
if (options.replaced && connection.selectedMailbox && inRoot(connection.selectedMailbox)) {
|
|
122
|
+
if (options.replaced && connection && connection.selectedMailbox && inRoot(connection.selectedMailbox)) {
|
|
115
123
|
added.MESSAGE--;
|
|
116
124
|
added.bytes -= totalSize([options.replaced]);
|
|
117
125
|
}
|
|
@@ -236,4 +244,44 @@ export default function quotaPlugin(server) {
|
|
|
236
244
|
}
|
|
237
245
|
});
|
|
238
246
|
server.statusHandlers['DELETED-STORAGE'] = (connection, mailbox) => totalSize(mailbox.messages.filter(message => message.flags.indexOf('\\Deleted') >= 0));
|
|
247
|
+
// Control API (README "Control API"): the quota root with its limits and usage, STORAGE in units of 1024 octets
|
|
248
|
+
const getQuotaOperation = () => {
|
|
249
|
+
const usage = getUsage();
|
|
250
|
+
return {
|
|
251
|
+
root: root.name,
|
|
252
|
+
limits: Object.assign({}, root.limits),
|
|
253
|
+
usage: { STORAGE: Math.ceil(usage.bytes / 1024), MESSAGE: usage.MESSAGE, MAILBOX: usage.MAILBOX }
|
|
254
|
+
};
|
|
255
|
+
};
|
|
256
|
+
// the new limits replace all earlier ones, like SETQUOTA (RFC 9208 section 4.1.3)
|
|
257
|
+
const setQuotaOperation = (limits) => {
|
|
258
|
+
if (!limits || typeof limits !== 'object' || Array.isArray(limits)) {
|
|
259
|
+
throw storeError('setQuota expects an object of resource limits, e.g. { STORAGE: 1024 }', 'INVALID');
|
|
260
|
+
}
|
|
261
|
+
const next = {};
|
|
262
|
+
Object.keys(limits).forEach(name => {
|
|
263
|
+
const resource = name.toUpperCase();
|
|
264
|
+
const value = limits[name];
|
|
265
|
+
if (RESOURCES.indexOf(resource) < 0) {
|
|
266
|
+
throw storeError('Unknown quota resource ' + name + ', expected ' + RESOURCES.join(', '), 'INVALID');
|
|
267
|
+
}
|
|
268
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0) {
|
|
269
|
+
throw storeError('Limit of ' + resource + ' must be a non-negative integer', 'INVALID');
|
|
270
|
+
}
|
|
271
|
+
next[resource] = value;
|
|
272
|
+
});
|
|
273
|
+
root.limits = next;
|
|
274
|
+
return getQuotaOperation();
|
|
275
|
+
};
|
|
276
|
+
server.control.register('getQuota', getQuotaOperation, [
|
|
277
|
+
{ method: 'GET', path: '/v1/quota', summary: 'Quota root, limits and usage (QUOTA plugin)', handler: () => server.control.getQuota() }
|
|
278
|
+
]);
|
|
279
|
+
server.control.register('setQuota', setQuotaOperation, [
|
|
280
|
+
{
|
|
281
|
+
method: 'PUT',
|
|
282
|
+
path: '/v1/quota',
|
|
283
|
+
summary: 'Replaces the quota limits ({ STORAGE, MESSAGE, MAILBOX }, QUOTA plugin)',
|
|
284
|
+
handler: ({ body }) => server.control.setQuota(body)
|
|
285
|
+
}
|
|
286
|
+
]);
|
|
239
287
|
}
|
|
@@ -25,7 +25,7 @@ export default function savedatePlugin(server) {
|
|
|
25
25
|
delete message.SAVEDATE;
|
|
26
26
|
return;
|
|
27
27
|
}
|
|
28
|
-
const savedate = server.normalizeDateTime(message.SAVEDATE ||
|
|
28
|
+
const savedate = server.normalizeDateTime(message.SAVEDATE || server.now());
|
|
29
29
|
if (!server.validateInternalDate(savedate)) {
|
|
30
30
|
throw new Error('Invalid SAVEDATE value ' + JSON.stringify(savedate) + ' in mailbox ' + mailbox.path);
|
|
31
31
|
}
|
|
@@ -1,10 +1,2 @@
|
|
|
1
1
|
import type { IMAPServer } from '../types.js';
|
|
2
|
-
/**
|
|
3
|
-
* @help Enables SPECIAL-USE [RFC6154] capability
|
|
4
|
-
* @help Mailboxes need to have a "special-use"
|
|
5
|
-
* @help property (String or Array) that will be used
|
|
6
|
-
* @help as extra flag for LIST and LSUB responses.
|
|
7
|
-
* @help With LIST-EXTENDED the SPECIAL-USE selection and
|
|
8
|
-
* @help return options work with the other LIST options
|
|
9
|
-
*/
|
|
10
2
|
export default function specialUsePlugin(server: IMAPServer): void;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getListExtensions } from '../list-extensions.js';
|
|
2
|
+
import { storeError } from '../store-operations.js';
|
|
2
3
|
/**
|
|
3
4
|
* @help Enables SPECIAL-USE [RFC6154] capability
|
|
4
5
|
* @help Mailboxes need to have a "special-use"
|
|
@@ -7,6 +8,8 @@ import { getListExtensions } from '../list-extensions.js';
|
|
|
7
8
|
* @help With LIST-EXTENDED the SPECIAL-USE selection and
|
|
8
9
|
* @help return options work with the other LIST options
|
|
9
10
|
*/
|
|
11
|
+
// RFC 6154 section 2 and RFC 8457 (\\Important)
|
|
12
|
+
const SPECIAL_USES = new Set(['\\All', '\\Archive', '\\Drafts', '\\Flagged', '\\Junk', '\\Sent', '\\Trash', '\\Important']);
|
|
10
13
|
export default function specialUsePlugin(server) {
|
|
11
14
|
// Register capability
|
|
12
15
|
server.registerCapability('SPECIAL-USE');
|
|
@@ -77,4 +80,24 @@ export default function specialUsePlugin(server) {
|
|
|
77
80
|
}
|
|
78
81
|
}
|
|
79
82
|
});
|
|
83
|
+
// Control API (README "Control API"): the special-use attributes of a mailbox (RFC 6154 section 2)
|
|
84
|
+
server.control.mailboxInfoHandlers.push((mailbox, info) => {
|
|
85
|
+
info.specialUse = getSpecialUse(mailbox);
|
|
86
|
+
});
|
|
87
|
+
const setSpecialUseOperation = (path, uses) => {
|
|
88
|
+
const mailbox = server.control.requireMailbox(path, true);
|
|
89
|
+
if (!Array.isArray(uses) || uses.some(use => typeof use !== 'string' || !SPECIAL_USES.has(use))) {
|
|
90
|
+
throw storeError('Special-use attributes must be a list of ' + [...SPECIAL_USES].join(', '), 'INVALID');
|
|
91
|
+
}
|
|
92
|
+
mailbox['special-use'] = [...new Set(uses)];
|
|
93
|
+
return server.control.getMailbox(mailbox.path);
|
|
94
|
+
};
|
|
95
|
+
server.control.register('setSpecialUse', setSpecialUseOperation, [
|
|
96
|
+
{
|
|
97
|
+
method: 'PUT',
|
|
98
|
+
path: '/v1/mailboxes/{path}/special-use',
|
|
99
|
+
summary: 'Sets the special-use attributes of a mailbox ({ specialUse: ["\\Sent"] }, SPECIAL-USE plugin)',
|
|
100
|
+
handler: ({ params, body }) => server.control.setSpecialUse(params.path, body.specialUse)
|
|
101
|
+
}
|
|
102
|
+
]);
|
|
80
103
|
}
|