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