imapkit 0.0.0-stage → 4.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/LICENSE +16 -0
- package/README.md +608 -2
- package/bin/help.txt +98 -0
- package/bin/imapkit.js +108 -0
- package/cert/server.crt +20 -0
- package/cert/server.key +28 -0
- package/lib/addressparser.js +283 -0
- package/lib/arguments.js +112 -0
- package/lib/bodystructure.js +149 -0
- package/lib/command-states.js +109 -0
- package/lib/commands/append.js +313 -0
- package/lib/commands/capability.js +47 -0
- package/lib/commands/check.js +21 -0
- package/lib/commands/close.js +30 -0
- package/lib/commands/copy.js +115 -0
- package/lib/commands/create.js +52 -0
- package/lib/commands/delete.js +64 -0
- package/lib/commands/examine.js +7 -0
- package/lib/commands/expunge.js +27 -0
- package/lib/commands/fetch.js +229 -0
- package/lib/commands/handlers/fetch.js +209 -0
- package/lib/commands/handlers/flags.js +42 -0
- package/lib/commands/handlers/search.js +519 -0
- package/lib/commands/handlers/status.js +85 -0
- package/lib/commands/handlers/store.js +127 -0
- package/lib/commands/list.js +100 -0
- package/lib/commands/login.js +67 -0
- package/lib/commands/logout.js +41 -0
- package/lib/commands/lsub.js +87 -0
- package/lib/commands/noop.js +21 -0
- package/lib/commands/rename.js +102 -0
- package/lib/commands/search.js +76 -0
- package/lib/commands/select.js +289 -0
- package/lib/commands/status.js +63 -0
- package/lib/commands/store.js +151 -0
- package/lib/commands/subscribe.js +53 -0
- package/lib/commands/uid copy.js +7 -0
- package/lib/commands/uid fetch.js +5 -0
- package/lib/commands/uid search.js +5 -0
- package/lib/commands/uid store.js +5 -0
- package/lib/commands/unsubscribe.js +50 -0
- package/lib/dates.js +123 -0
- package/lib/deflate-layer.js +232 -0
- package/lib/envelope.js +82 -0
- package/lib/esearch.js +208 -0
- package/lib/framing.js +102 -0
- package/lib/list-extensions.js +36 -0
- package/lib/load-plugins.js +109 -0
- package/lib/mailbox-name.js +133 -0
- package/lib/mimeparser.js +778 -0
- package/lib/mock-client.js +233 -0
- package/lib/numbers.js +52 -0
- package/lib/plugins/acl.js +964 -0
- package/lib/plugins/appendlimit.js +83 -0
- package/lib/plugins/auth-plain.js +94 -0
- package/lib/plugins/binary.js +256 -0
- package/lib/plugins/catenate.js +253 -0
- package/lib/plugins/compress.js +76 -0
- package/lib/plugins/condstore.js +563 -0
- package/lib/plugins/context-search.js +321 -0
- package/lib/plugins/context-sort.js +19 -0
- package/lib/plugins/create-special-use.js +108 -0
- package/lib/plugins/enable.js +155 -0
- package/lib/plugins/esearch.js +156 -0
- package/lib/plugins/esort.js +60 -0
- package/lib/plugins/id.js +138 -0
- package/lib/plugins/idle.js +105 -0
- package/lib/plugins/imap4rev2.js +202 -0
- package/lib/plugins/list-extended.js +258 -0
- package/lib/plugins/list-status.js +31 -0
- package/lib/plugins/literalminus.js +20 -0
- package/lib/plugins/literalplus.js +18 -0
- package/lib/plugins/logindisabled.js +50 -0
- package/lib/plugins/messagelimit.js +234 -0
- package/lib/plugins/metadata-server.js +13 -0
- package/lib/plugins/metadata.js +475 -0
- package/lib/plugins/move.js +110 -0
- package/lib/plugins/multiappend.js +26 -0
- package/lib/plugins/multisearch.js +269 -0
- package/lib/plugins/namespace.js +67 -0
- package/lib/plugins/notify.js +654 -0
- package/lib/plugins/oauthbearer.js +217 -0
- package/lib/plugins/objectid.js +243 -0
- package/lib/plugins/partial.js +68 -0
- package/lib/plugins/preview.js +400 -0
- package/lib/plugins/qresync.js +525 -0
- package/lib/plugins/quota.js +285 -0
- package/lib/plugins/replace.js +145 -0
- package/lib/plugins/sasl-ir.js +12 -0
- package/lib/plugins/savedate.js +59 -0
- package/lib/plugins/savelimit.js +18 -0
- package/lib/plugins/searchres.js +82 -0
- package/lib/plugins/sort-display.js +23 -0
- package/lib/plugins/sort.js +132 -0
- package/lib/plugins/special-use.js +95 -0
- package/lib/plugins/starttls.js +57 -0
- package/lib/plugins/status-size.js +19 -0
- package/lib/plugins/thread-orderedsubject.js +16 -0
- package/lib/plugins/thread-references.js +16 -0
- package/lib/plugins/uidonly.js +135 -0
- package/lib/plugins/uidplus.js +124 -0
- package/lib/plugins/unauthenticate.js +28 -0
- package/lib/plugins/unselect.js +36 -0
- package/lib/plugins/utf8-accept.js +68 -0
- package/lib/plugins/x-gm-ext-1.js +456 -0
- package/lib/plugins/xoauth2.js +188 -0
- package/lib/plugins/xtoybird.js +282 -0
- package/lib/server.js +2880 -0
- package/lib/smtp-listener.js +51 -0
- package/lib/sorting.js +373 -0
- package/lib/threading.js +357 -0
- package/lib/utf8-session.js +123 -0
- package/lib/vanished.js +57 -0
- package/package.json +61 -5
|
@@ -0,0 +1,964 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { states } = require('../command-states');
|
|
4
|
+
const { normalizeSystemFlag, checkSystemFlags } = require('../commands/handlers/flags');
|
|
5
|
+
const builtinStoreHandlers = require('../commands/handlers/store');
|
|
6
|
+
const { getListExtensions } = require('../list-extensions');
|
|
7
|
+
const { beginSelect } = require('../commands/select');
|
|
8
|
+
const { getPendingTarget } = require('../commands/append');
|
|
9
|
+
const { isAstring } = require('../arguments');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @help Adds ACL [RFC4314] capability with RIGHTS=texk
|
|
13
|
+
* @help The owner (server option "aclOwner", default "testuser")
|
|
14
|
+
* @help has all rights on every mailbox. Other users get rights
|
|
15
|
+
* @help from the "acl" property of a mailbox in storage, e.g.
|
|
16
|
+
* @help {"otheruser": "lr", "anyone": "l"}, or from SETACL. With
|
|
17
|
+
* @help LIST-EXTENDED, LIST-MYRIGHTS [RFC8440] is supported as well
|
|
18
|
+
*
|
|
19
|
+
* ACL: https://www.rfc-editor.org/rfc/rfc4314
|
|
20
|
+
*
|
|
21
|
+
* LIST-MYRIGHTS: https://www.rfc-editor.org/rfc/rfc8440
|
|
22
|
+
*
|
|
23
|
+
* Additional commands:
|
|
24
|
+
* - SETACL, DELETEACL, GETACL, LISTRIGHTS, MYRIGHTS
|
|
25
|
+
* - the MYRIGHTS return option of LIST, when LIST-EXTENDED is loaded
|
|
26
|
+
*
|
|
27
|
+
* Users other than the owner can only do what their rights allow (RFC 4314 section 4).
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
// RFC 4314 section 2.1, in the order they are listed in responses
|
|
31
|
+
const RIGHTS = 'lrswipkxtea';
|
|
32
|
+
const ALL_RIGHTS = new Set(RIGHTS);
|
|
33
|
+
// RFC 4314 section 2.1.1: the obsolete "c" and "d" rights are macros. Like Dovecot, "c" is "kx"
|
|
34
|
+
// ("create" for servers that used "c" for DELETE) and "d" is "et"
|
|
35
|
+
const VIRTUAL_RIGHTS = { c: 'kx', d: 'et' };
|
|
36
|
+
// RFC 4314 section 5.2: with none of these the mailbox is READ-ONLY. All flags are shared between
|
|
37
|
+
// the users of a mailbox, so the "shared flag rights" are "s", "w" and "t"
|
|
38
|
+
const WRITE_RIGHTS = ['i', 'e', 's', 'w', 't'];
|
|
39
|
+
// RFC 4314 section 4: the rights for changing flags
|
|
40
|
+
const FLAG_RIGHTS = ['s', 'w', 't'];
|
|
41
|
+
// RFC 4314 section 4: MYRIGHTS needs any of these
|
|
42
|
+
const ANY_RIGHTS = ['l', 'r', 'i', 'k', 'x', 'a'];
|
|
43
|
+
const STORE_ITEMS = ['FLAGS', '+FLAGS', '-FLAGS', 'FLAGS.SILENT', '+FLAGS.SILENT', '-FLAGS.SILENT'];
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Formats an astring for a response. The compiler sends an ATOM value that is not a valid atom as a
|
|
47
|
+
* quoted string or a literal, only a leading "\\" (allowed for flags) and NIL have to be kept away from it
|
|
48
|
+
*
|
|
49
|
+
* @param {String} value
|
|
50
|
+
* @return {Object|String}
|
|
51
|
+
*/
|
|
52
|
+
function astring(value) {
|
|
53
|
+
// identifiers are unicode strings, sent as UTF-8 (RFC 4314 section 3)
|
|
54
|
+
value = Buffer.from(value, 'utf-8').toString('binary');
|
|
55
|
+
// an atom NIL would read back as NIL, not as an identifier
|
|
56
|
+
return value && value.charAt(0) !== '\\' && !/^NIL$/i.test(value) ? { type: 'ATOM', value } : value;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Creates an error that is answered with BAD
|
|
61
|
+
*
|
|
62
|
+
* @param {String} message Error text
|
|
63
|
+
* @return {Error}
|
|
64
|
+
*/
|
|
65
|
+
function badInput(message) {
|
|
66
|
+
const err = new Error(message);
|
|
67
|
+
err.imapResponse = 'BAD';
|
|
68
|
+
return err;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Parses a rights string, the virtual "c" and "d" rights are expanded (RFC 4314 section 2.1.1)
|
|
73
|
+
*
|
|
74
|
+
* @param {String} value Rights, e.g. "lrswd"
|
|
75
|
+
* @return {Set} rights
|
|
76
|
+
* @throws {Error} BAD error for unknown rights (RFC 4314 section 3.1)
|
|
77
|
+
*/
|
|
78
|
+
function parseRights(value) {
|
|
79
|
+
const rights = new Set();
|
|
80
|
+
for (const chr of value) {
|
|
81
|
+
if (ALL_RIGHTS.has(chr)) {
|
|
82
|
+
rights.add(chr);
|
|
83
|
+
} else if (Object.hasOwn(VIRTUAL_RIGHTS, chr)) {
|
|
84
|
+
VIRTUAL_RIGHTS[chr].split('').forEach(right => rights.add(right));
|
|
85
|
+
} else if (/^[A-Z]$/.test(chr)) {
|
|
86
|
+
throw badInput('Uppercase rights are not allowed');
|
|
87
|
+
} else {
|
|
88
|
+
throw badInput('The ' + (/^[\x21-\x7e]$/.test(chr) ? chr + ' ' : '') + 'right is not supported');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return rights;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Builds the attributes of an ACL, LISTRIGHTS or MYRIGHTS response. The mailbox name is a MAILBOX
|
|
96
|
+
* attribute, so each session gets it in the form it uses (modified UTF-7, or UTF-8 after ENABLE
|
|
97
|
+
* UTF8=ACCEPT)
|
|
98
|
+
*
|
|
99
|
+
* @param {Object} mailbox Mailbox object
|
|
100
|
+
* @param {Array} values Values that follow the mailbox name
|
|
101
|
+
* @return {Array} Response attributes
|
|
102
|
+
*/
|
|
103
|
+
function mailboxResponse(mailbox, values) {
|
|
104
|
+
return [{ type: 'MAILBOX', value: mailbox.path }].concat(values.map(astring));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Formats rights for ACL and MYRIGHTS responses. "c" and "d" are added when any of their
|
|
109
|
+
* member rights is set (RFC 4314 section 2.1.1)
|
|
110
|
+
*
|
|
111
|
+
* @param {Set} rights
|
|
112
|
+
* @return {String}
|
|
113
|
+
*/
|
|
114
|
+
function formatRights(rights) {
|
|
115
|
+
let result = RIGHTS.split('')
|
|
116
|
+
.filter(right => rights.has(right))
|
|
117
|
+
.join('');
|
|
118
|
+
Object.keys(VIRTUAL_RIGHTS).forEach(virtual => {
|
|
119
|
+
if (VIRTUAL_RIGHTS[virtual].split('').some(right => rights.has(right))) {
|
|
120
|
+
result += virtual;
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Checks an identifier argument. RFC 4314 section 3 asks for SASLprep, which is not implemented,
|
|
128
|
+
* but an identifier that SASLprep would refuse (control characters, invalid UTF-8) or that is
|
|
129
|
+
* empty gets BAD, as does a lone "-" (section 2: "-" prefixes the identifier of a negative right)
|
|
130
|
+
*
|
|
131
|
+
* @param {String} identifier Identifier as a binary string
|
|
132
|
+
* @return {String|Boolean} error text, or false if the identifier is fine
|
|
133
|
+
*/
|
|
134
|
+
function identifierError(identifier) {
|
|
135
|
+
if (!identifier || identifier === '-') {
|
|
136
|
+
return 'Empty identifier';
|
|
137
|
+
}
|
|
138
|
+
// eslint-disable-next-line no-control-regex
|
|
139
|
+
if (/[\x00-\x1f\x7f]/.test(identifier)) {
|
|
140
|
+
return 'Identifier contains control characters';
|
|
141
|
+
}
|
|
142
|
+
const decoded = Buffer.from(identifier, 'binary').toString('utf-8');
|
|
143
|
+
if (Buffer.from(decoded, 'utf-8').toString('binary') !== identifier) {
|
|
144
|
+
return 'Identifier is not valid UTF-8';
|
|
145
|
+
}
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
module.exports = function (server) {
|
|
150
|
+
server.registerCapability('ACL');
|
|
151
|
+
// RFC 4314 section 5.1.1: the rights not defined in RFC 2086, MUST include "t", "e", "x" and "k"
|
|
152
|
+
server.registerCapability('RIGHTS=texk');
|
|
153
|
+
|
|
154
|
+
const owner = typeof server.options.aclOwner === 'string' ? server.options.aclOwner : 'testuser';
|
|
155
|
+
|
|
156
|
+
const isOwner = connection => connection.username === owner;
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Returns the ACL of a mailbox as a Map of identifier to a Set of rights. The ACL from the
|
|
160
|
+
* storage (an object of identifier to rights string) is converted on first use
|
|
161
|
+
*
|
|
162
|
+
* @param {Object} mailbox Mailbox object
|
|
163
|
+
* @return {Map}
|
|
164
|
+
*/
|
|
165
|
+
const getAcl = mailbox => {
|
|
166
|
+
if (!(mailbox.acl instanceof Map)) {
|
|
167
|
+
const acl = new Map();
|
|
168
|
+
const source = mailbox.acl && typeof mailbox.acl === 'object' ? mailbox.acl : {};
|
|
169
|
+
Object.keys(source).forEach(identifier => {
|
|
170
|
+
try {
|
|
171
|
+
acl.set(identifier, parseRights(String(source[identifier])));
|
|
172
|
+
} catch (err) {
|
|
173
|
+
// not a client error, so this is answered with NO [SERVERBUG]
|
|
174
|
+
throw new Error('Invalid ACL in storage for ' + mailbox.path + ': ' + err.message, { cause: err });
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
mailbox.acl = acl;
|
|
178
|
+
}
|
|
179
|
+
return mailbox.acl;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Returns the rights a user has on a mailbox. The owner has all rights, other users get the
|
|
184
|
+
* union of the rights of their own identifier and "anyone", minus the negative rights of
|
|
185
|
+
* "-user" and "-anyone" (RFC 4314 section 2)
|
|
186
|
+
*
|
|
187
|
+
* @param {Object} connection IMAP connection of the user
|
|
188
|
+
* @param {Object} mailbox Mailbox object
|
|
189
|
+
* @param {Map} [acl] ACL to use instead of the current one of the mailbox
|
|
190
|
+
* @return {Set} rights
|
|
191
|
+
*/
|
|
192
|
+
const getRights = (connection, mailbox, acl) => {
|
|
193
|
+
const username = connection.username;
|
|
194
|
+
if (username === owner) {
|
|
195
|
+
return ALL_RIGHTS;
|
|
196
|
+
}
|
|
197
|
+
acl = acl || getAcl(mailbox);
|
|
198
|
+
const identifiers = ['anyone'].concat(typeof username === 'string' && username ? username : []);
|
|
199
|
+
const rights = new Set();
|
|
200
|
+
identifiers.forEach(identifier => (acl.get(identifier) || []).forEach(right => rights.add(right)));
|
|
201
|
+
identifiers.forEach(identifier => (acl.get('-' + identifier) || []).forEach(right => rights.delete(right)));
|
|
202
|
+
return rights;
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
// for plugins that report only what a user may see (e.g. NOTIFY)
|
|
206
|
+
server.acl = { getRights, isOwner };
|
|
207
|
+
|
|
208
|
+
// Tells plugins that the ACL of a mailbox changed, with an `acl` event `(mailbox, previousAcl)`
|
|
209
|
+
const changeAcl = (mailbox, change) => {
|
|
210
|
+
const acl = getAcl(mailbox);
|
|
211
|
+
const previous = new Map(acl);
|
|
212
|
+
change(acl);
|
|
213
|
+
server.emit('acl', mailbox, previous);
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
// CATENATE (RFC 4469 section 5): reading a message through an IMAP URL needs the "r" right, a
|
|
217
|
+
// mailbox without "l" is reported like one that does not exist (RFC 4314 section 6)
|
|
218
|
+
server.urlAccessChecks.push((connection, mailbox) => {
|
|
219
|
+
const rights = getRights(connection, mailbox);
|
|
220
|
+
if (!rights.has('r')) {
|
|
221
|
+
return { text: rights.has('l') ? 'Permission denied' : 'Mailbox does not exist' };
|
|
222
|
+
}
|
|
223
|
+
return false;
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// MULTISEARCH (RFC 7377 section 2.2): searching needs "r", and "l" for a mailbox that the client did not name.
|
|
227
|
+
// Other mailboxes are left out without an error
|
|
228
|
+
server.searchAccessChecks.push((connection, mailbox, named) => {
|
|
229
|
+
const rights = getRights(connection, mailbox);
|
|
230
|
+
return rights.has('r') && (named || rights.has('l'));
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// Rights on the selected mailbox are taken when it is selected (RFC 4314 section 5.1.1 allows caching
|
|
234
|
+
// them), so the FLAGS and PERMANENTFLAGS a session got stay correct until the next SELECT
|
|
235
|
+
const getSelectedRights = connection => connection.aclRights || ALL_RIGHTS;
|
|
236
|
+
|
|
237
|
+
// RFC 4314 section 4: "t" for \Deleted, "s" for \Seen and "w" for all other flags
|
|
238
|
+
const flagRight = flag => {
|
|
239
|
+
flag = normalizeSystemFlag(flag);
|
|
240
|
+
if (flag === '\\Deleted') {
|
|
241
|
+
return 't';
|
|
242
|
+
}
|
|
243
|
+
if (flag === '\\Seen') {
|
|
244
|
+
return 's';
|
|
245
|
+
}
|
|
246
|
+
return 'w';
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const flagValue = flag => (typeof flag === 'string' ? flag : String((flag && flag.value) || ''));
|
|
250
|
+
|
|
251
|
+
const canSetFlag = (rights, flag) => rights.has(flagRight(flagValue(flag)));
|
|
252
|
+
|
|
253
|
+
const mailboxArgument = (parsed, position) => {
|
|
254
|
+
const attr = (parsed.attributes || [])[position];
|
|
255
|
+
return isAstring(attr) ? attr.value : false;
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
// the mailbox named by an argument, false if there is no such mailbox or the argument is invalid
|
|
259
|
+
const argumentMailbox = (parsed, position) => {
|
|
260
|
+
const name = mailboxArgument(parsed, position);
|
|
261
|
+
return (name !== false && server.getMailbox(name)) || false;
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const sendNoPerm = (connection, parsed, data, description) => connection.sendStatus(parsed, data, 'NO', 'Permission denied', 'NOPERM', description);
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Refuses a command for lack of rights. Without the "l" right the user gets the same error
|
|
268
|
+
* as for a mailbox that does not exist, so the existence of the mailbox is not disclosed
|
|
269
|
+
* (RFC 4314 section 6), otherwise NO [NOPERM] (RFC 5530 section 3)
|
|
270
|
+
*/
|
|
271
|
+
const sendDenied = (connection, parsed, data, rights, description, notFound) => {
|
|
272
|
+
if (rights.has('l')) {
|
|
273
|
+
sendNoPerm(connection, parsed, data, description);
|
|
274
|
+
} else if (notFound === 'TRYCREATE') {
|
|
275
|
+
connection.sendStatus(parsed, data, 'NO', 'Target mailbox does not exist', 'TRYCREATE', description);
|
|
276
|
+
} else {
|
|
277
|
+
connection.sendStatus(parsed, data, 'NO', 'Mailbox does not exist', 'NONEXISTENT', description);
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Finds the nearest existing parent of a mailbox name (RFC 4314 section 4, CREATE)
|
|
283
|
+
*
|
|
284
|
+
* @param {String} path Mailbox path
|
|
285
|
+
* @return {Object|Boolean} parent mailbox or false if there is none
|
|
286
|
+
*/
|
|
287
|
+
const getParent = path => {
|
|
288
|
+
for (let parent = server.getParentPath(path); parent; parent = server.getParentPath(parent)) {
|
|
289
|
+
const mailbox = server.getMailbox(parent);
|
|
290
|
+
if (mailbox) {
|
|
291
|
+
return mailbox;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return false;
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
// Checks for the ACL commands, true if the command can go ahead
|
|
298
|
+
const checkAclCommand = (connection, parsed, data, mailbox, isAllowed) => {
|
|
299
|
+
if (!mailbox) {
|
|
300
|
+
connection.sendStatus(parsed, data, 'NO', 'Mailbox does not exist', 'NONEXISTENT');
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
const rights = getRights(connection, mailbox);
|
|
304
|
+
if (!isAllowed(rights)) {
|
|
305
|
+
sendDenied(connection, parsed, data, rights, false);
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
return true;
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
const hasAdmin = rights => rights.has('a');
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Validates the arguments of an ACL command: `count` astrings, the first one a mailbox name and
|
|
315
|
+
* the second one (if any) an identifier
|
|
316
|
+
*
|
|
317
|
+
* @return {Boolean} true if the arguments are fine, otherwise BAD was sent
|
|
318
|
+
*/
|
|
319
|
+
const checkArguments = (connection, parsed, data, count) => {
|
|
320
|
+
const args = parsed.attributes || [];
|
|
321
|
+
const command = parsed.command.toUpperCase();
|
|
322
|
+
if (args.length !== count || args.some(arg => !isAstring(arg))) {
|
|
323
|
+
connection.sendStatus(parsed, data, 'BAD', command + ' expects ' + (count === 1 ? 'a mailbox name' : count + ' string arguments'));
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
const error = count > 1 && identifierError(args[1].value);
|
|
327
|
+
if (error) {
|
|
328
|
+
// RFC 4314 section 3: an identifier that fails preparation is refused with BAD
|
|
329
|
+
connection.sendStatus(parsed, data, 'BAD', error);
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
if (count > 1) {
|
|
333
|
+
// user names and identifiers are unicode strings, whatever way the user logged in
|
|
334
|
+
args[1].value = Buffer.from(args[1].value, 'binary').toString('utf-8');
|
|
335
|
+
}
|
|
336
|
+
return true;
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
// RFC 4314 section 2: an implementation MAY force rights to always be granted to an identifier
|
|
340
|
+
const refuseOwnerIdentifier = (connection, parsed, data) => {
|
|
341
|
+
const identifier = parsed.attributes[1].value;
|
|
342
|
+
if (identifier !== owner && identifier !== '-' + owner) {
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
345
|
+
connection.sendStatus(parsed, data, 'NO', 'Rights of the mailbox owner can not be changed', 'CANNOT');
|
|
346
|
+
return true;
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
const aclCommand = (count, handler) => (connection, parsed, data, callback) => {
|
|
350
|
+
if (checkArguments(connection, parsed, data, count)) {
|
|
351
|
+
handler(connection, parsed, data, server.getMailbox(parsed.attributes[0].value));
|
|
352
|
+
}
|
|
353
|
+
callback();
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
const aclOptions = { states: states.AUTHENTICATED, mailboxArguments: [0] };
|
|
357
|
+
// RFC 4314 section 7: identifier and mod-rights are astrings
|
|
358
|
+
const identifierOptions = Object.assign({ astringArguments: [1] }, aclOptions);
|
|
359
|
+
|
|
360
|
+
// RFC 4314 section 3.8: myrights-data = "MYRIGHTS" SP mailbox SP rights
|
|
361
|
+
const sendMyRights = (connection, mailbox, parsed, data) =>
|
|
362
|
+
connection.send(
|
|
363
|
+
{ tag: '*', command: 'MYRIGHTS', attributes: mailboxResponse(mailbox, [formatRights(getRights(connection, mailbox))]) },
|
|
364
|
+
'MYRIGHTS',
|
|
365
|
+
parsed,
|
|
366
|
+
data,
|
|
367
|
+
mailbox
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
// RFC 8440 section 3: the MYRIGHTS return option of an extended LIST. The registry is shared with
|
|
371
|
+
// LIST-EXTENDED, which accepts the option only when it is loaded, in any load order
|
|
372
|
+
const listExtensions = getListExtensions(server);
|
|
373
|
+
server.registerCapability('LIST-MYRIGHTS', () => listExtensions.enabled);
|
|
374
|
+
listExtensions.returnOptions.MYRIGHTS = {
|
|
375
|
+
// a MYRIGHTS response follows the LIST response of every listed mailbox that matches the
|
|
376
|
+
// selection criteria and exists, not the ones listed only for CHILDINFO or as \NonExistent
|
|
377
|
+
onItem: (connection, folder, value, info, parsed, data) => {
|
|
378
|
+
if (info.matched && info.exists) {
|
|
379
|
+
sendMyRights(connection, folder, parsed, data);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
// RFC 4314 section 3.1
|
|
385
|
+
server.setCommandHandler(
|
|
386
|
+
'SETACL',
|
|
387
|
+
aclCommand(3, (connection, parsed, data, mailbox) => {
|
|
388
|
+
const identifier = parsed.attributes[1].value;
|
|
389
|
+
const modification = parsed.attributes[2].value;
|
|
390
|
+
const mode = /^[+-]/.test(modification) ? modification.charAt(0) : '';
|
|
391
|
+
|
|
392
|
+
// an unrecognized right MUST cause BAD, before anything else is checked
|
|
393
|
+
const rights = parseRights(modification.substr(mode.length));
|
|
394
|
+
|
|
395
|
+
if (!checkAclCommand(connection, parsed, data, mailbox, hasAdmin) || refuseOwnerIdentifier(connection, parsed, data)) {
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
changeAcl(mailbox, acl => {
|
|
400
|
+
const current = new Set(acl.get(identifier) || []);
|
|
401
|
+
if (mode === '+') {
|
|
402
|
+
rights.forEach(right => current.add(right));
|
|
403
|
+
} else if (mode === '-') {
|
|
404
|
+
rights.forEach(right => current.delete(right));
|
|
405
|
+
}
|
|
406
|
+
const updated = mode ? current : rights;
|
|
407
|
+
if (updated.size) {
|
|
408
|
+
acl.set(identifier, updated);
|
|
409
|
+
} else {
|
|
410
|
+
acl.delete(identifier);
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
connection.sendStatus(parsed, data, 'OK', 'Setacl complete');
|
|
414
|
+
}),
|
|
415
|
+
Object.assign({ astringArguments: [1, 2] }, aclOptions)
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
// RFC 4314 section 3.2
|
|
419
|
+
server.setCommandHandler(
|
|
420
|
+
'DELETEACL',
|
|
421
|
+
aclCommand(2, (connection, parsed, data, mailbox) => {
|
|
422
|
+
if (!checkAclCommand(connection, parsed, data, mailbox, hasAdmin) || refuseOwnerIdentifier(connection, parsed, data)) {
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
changeAcl(mailbox, acl => acl.delete(parsed.attributes[1].value));
|
|
426
|
+
connection.sendStatus(parsed, data, 'OK', 'Deleteacl complete');
|
|
427
|
+
}),
|
|
428
|
+
identifierOptions
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
// RFC 4314 sections 3.3 and 3.6
|
|
432
|
+
server.setCommandHandler(
|
|
433
|
+
'GETACL',
|
|
434
|
+
aclCommand(1, (connection, parsed, data, mailbox) => {
|
|
435
|
+
if (!checkAclCommand(connection, parsed, data, mailbox, hasAdmin)) {
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
const attributes = [owner, formatRights(ALL_RIGHTS)];
|
|
439
|
+
getAcl(mailbox).forEach((rights, identifier) => {
|
|
440
|
+
attributes.push(identifier, formatRights(rights));
|
|
441
|
+
});
|
|
442
|
+
connection.send({ tag: '*', command: 'ACL', attributes: mailboxResponse(mailbox, attributes) }, 'ACL', parsed, data, mailbox);
|
|
443
|
+
connection.sendStatus(parsed, data, 'OK', 'Getacl complete');
|
|
444
|
+
}),
|
|
445
|
+
aclOptions
|
|
446
|
+
);
|
|
447
|
+
|
|
448
|
+
// RFC 4314 sections 3.4 and 3.7
|
|
449
|
+
server.setCommandHandler(
|
|
450
|
+
'LISTRIGHTS',
|
|
451
|
+
aclCommand(2, (connection, parsed, data, mailbox) => {
|
|
452
|
+
if (!checkAclCommand(connection, parsed, data, mailbox, hasAdmin)) {
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
// the identifier is sent back in the same form as the client used
|
|
456
|
+
const identifier = parsed.attributes[1].value;
|
|
457
|
+
let attributes;
|
|
458
|
+
if (identifier === owner) {
|
|
459
|
+
// the owner always has every right, nothing is optional
|
|
460
|
+
attributes = [identifier, formatRights(ALL_RIGHTS)];
|
|
461
|
+
} else {
|
|
462
|
+
// nothing is granted always, every right can be granted on its own, including the virtual ones
|
|
463
|
+
attributes = [identifier, ''].concat(RIGHTS.split(''), Object.keys(VIRTUAL_RIGHTS));
|
|
464
|
+
}
|
|
465
|
+
connection.send({ tag: '*', command: 'LISTRIGHTS', attributes: mailboxResponse(mailbox, attributes) }, 'LISTRIGHTS', parsed, data, mailbox);
|
|
466
|
+
connection.sendStatus(parsed, data, 'OK', 'Listrights complete');
|
|
467
|
+
}),
|
|
468
|
+
identifierOptions
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
// RFC 4314 sections 3.5 and 3.8
|
|
472
|
+
server.setCommandHandler(
|
|
473
|
+
'MYRIGHTS',
|
|
474
|
+
aclCommand(1, (connection, parsed, data, mailbox) => {
|
|
475
|
+
if (!checkAclCommand(connection, parsed, data, mailbox, rights => ANY_RIGHTS.some(right => rights.has(right)))) {
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
sendMyRights(connection, mailbox, parsed, data);
|
|
479
|
+
connection.sendStatus(parsed, data, 'OK', 'Myrights complete');
|
|
480
|
+
}),
|
|
481
|
+
aclOptions
|
|
482
|
+
);
|
|
483
|
+
|
|
484
|
+
// Enforcement (RFC 4314 section 4). Commands are wrapped once every plugin is loaded, so that
|
|
485
|
+
// the commands of every plugin (MOVE, UID EXPUNGE, ...) are covered whatever the load order is
|
|
486
|
+
const wrappers = {};
|
|
487
|
+
// the handlers that the enforcement wraps
|
|
488
|
+
const prevHandlers = {};
|
|
489
|
+
const wrapCommands = () => {
|
|
490
|
+
Object.keys(wrappers).forEach(command => {
|
|
491
|
+
const prevHandler = server.getCommandHandler(command);
|
|
492
|
+
if (prevHandler) {
|
|
493
|
+
prevHandlers[command] = prevHandler;
|
|
494
|
+
server.setCommandHandler(command, (connection, parsed, data, callback) => {
|
|
495
|
+
if (isOwner(connection)) {
|
|
496
|
+
return prevHandler(connection, parsed, data, callback);
|
|
497
|
+
}
|
|
498
|
+
return wrappers[command](prevHandler, connection, parsed, data, callback);
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
// Return options of an extended LIST (LIST-STATUS, LIST-MYRIGHTS) add responses for the listed
|
|
504
|
+
// mailboxes, so they skip the mailboxes that are not listed for the user. STATUS also needs "r"
|
|
505
|
+
// (RFC 5819 section 2: the STATUS response MUST NOT be returned if the mailbox can not be selected)
|
|
506
|
+
Object.keys(listExtensions.returnOptions).forEach(name => {
|
|
507
|
+
const option = listExtensions.returnOptions[name];
|
|
508
|
+
if (typeof option.onItem !== 'function') {
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
const onItem = option.onItem;
|
|
512
|
+
const right = name === 'STATUS' ? 'r' : 'l';
|
|
513
|
+
option.onItem = (connection, folder, ...args) => {
|
|
514
|
+
if (isOwner(connection)) {
|
|
515
|
+
return onItem(connection, folder, ...args);
|
|
516
|
+
}
|
|
517
|
+
const rights = getRights(connection, folder);
|
|
518
|
+
if (rights.has('l') && rights.has(right)) {
|
|
519
|
+
return onItem(connection, folder, ...args);
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
// RFC 4314 section 4: STORE changes only the flags the user has rights for
|
|
525
|
+
STORE_ITEMS.forEach(item => {
|
|
526
|
+
const prevHandler = server.storeHandlers[item] || builtinStoreHandlers[item];
|
|
527
|
+
server.storeHandlers[item] = (connection, message, flags, ...args) => {
|
|
528
|
+
const rights = getSelectedRights(connection);
|
|
529
|
+
if (FLAG_RIGHTS.every(right => rights.has(right))) {
|
|
530
|
+
return prevHandler(connection, message, flags, ...args);
|
|
531
|
+
}
|
|
532
|
+
flags = [].concat(flags);
|
|
533
|
+
// invalid flags are still refused with BAD, even if the user could not set them anyway
|
|
534
|
+
flags.forEach(flag => checkSystemFlags(connection, normalizeSystemFlag(flagValue(flag))));
|
|
535
|
+
let allowed = flags.filter(flag => canSetFlag(rights, flag));
|
|
536
|
+
if (item.charAt(0) !== '+' && item.charAt(0) !== '-') {
|
|
537
|
+
// replacing the flags keeps the ones the user can not change
|
|
538
|
+
allowed = message.flags.filter(flag => !canSetFlag(rights, flag)).concat(allowed);
|
|
539
|
+
}
|
|
540
|
+
return prevHandler(connection, message, allowed, ...args);
|
|
541
|
+
};
|
|
542
|
+
});
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
server.once('pluginsLoaded', wrapCommands);
|
|
546
|
+
|
|
547
|
+
const selectWrapper = (prevHandler, connection, parsed, data, callback) => {
|
|
548
|
+
const mailbox = argumentMailbox(parsed, 0);
|
|
549
|
+
if (!mailbox) {
|
|
550
|
+
return prevHandler(connection, parsed, data, callback);
|
|
551
|
+
}
|
|
552
|
+
const command = parsed.command.toUpperCase();
|
|
553
|
+
const rights = getRights(connection, mailbox);
|
|
554
|
+
if (!rights.has('r')) {
|
|
555
|
+
// a failed SELECT leaves no mailbox selected (RFC 3501 section 6.3.1)
|
|
556
|
+
beginSelect(connection, parsed, data);
|
|
557
|
+
connection.closeMailbox();
|
|
558
|
+
sendDenied(connection, parsed, data, rights, command + ' FAILED');
|
|
559
|
+
return callback();
|
|
560
|
+
}
|
|
561
|
+
connection.aclRights = rights;
|
|
562
|
+
if (command === 'SELECT' && !WRITE_RIGHTS.some(right => rights.has(right))) {
|
|
563
|
+
// RFC 4314 section 5.2: the server MUST answer READ-ONLY, so the mailbox is opened like EXAMINE does
|
|
564
|
+
return prevHandlers.EXAMINE(connection, parsed, data, callback);
|
|
565
|
+
}
|
|
566
|
+
return prevHandler(connection, parsed, data, callback);
|
|
567
|
+
};
|
|
568
|
+
wrappers.SELECT = selectWrapper;
|
|
569
|
+
wrappers.EXAMINE = selectWrapper;
|
|
570
|
+
|
|
571
|
+
// Checks that the user has a right on the mailbox named by an argument. Unknown mailboxes and
|
|
572
|
+
// invalid arguments are left to the command handler
|
|
573
|
+
const hasMailboxRight = (right, position, notFound) => (connection, parsed, data) => {
|
|
574
|
+
const mailbox = argumentMailbox(parsed, position);
|
|
575
|
+
const rights = mailbox && getRights(connection, mailbox);
|
|
576
|
+
if (rights && !rights.has(right)) {
|
|
577
|
+
sendDenied(connection, parsed, data, rights, false, notFound);
|
|
578
|
+
return false;
|
|
579
|
+
}
|
|
580
|
+
return true;
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
const requireRight = (right, position, notFound) => {
|
|
584
|
+
const check = hasMailboxRight(right, position, notFound);
|
|
585
|
+
return (prevHandler, connection, parsed, data, callback) =>
|
|
586
|
+
check(connection, parsed, data) ? prevHandler(connection, parsed, data, callback) : callback();
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
wrappers.STATUS = requireRight('r', 0);
|
|
590
|
+
// SUBSCRIBE checks that the mailbox exists, so it needs "l". UNSUBSCRIBE needs no rights
|
|
591
|
+
wrappers.SUBSCRIBE = requireRight('l', 0);
|
|
592
|
+
|
|
593
|
+
wrappers.DELETE = requireRight('x', 0);
|
|
594
|
+
|
|
595
|
+
// CREATE needs "k" on the nearest existing parent, a mailbox at the top level can not be created
|
|
596
|
+
const checkCreateRight = (connection, parsed, data, path) => {
|
|
597
|
+
const parent = getParent(path);
|
|
598
|
+
if (!parent || !getRights(connection, parent).has('k')) {
|
|
599
|
+
sendNoPerm(connection, parsed, data);
|
|
600
|
+
return false;
|
|
601
|
+
}
|
|
602
|
+
return true;
|
|
603
|
+
};
|
|
604
|
+
|
|
605
|
+
wrappers.CREATE = (prevHandler, connection, parsed, data, callback) => {
|
|
606
|
+
const name = mailboxArgument(parsed, 0);
|
|
607
|
+
if (name !== false && !checkCreateRight(connection, parsed, data, name)) {
|
|
608
|
+
return callback();
|
|
609
|
+
}
|
|
610
|
+
return prevHandler(connection, parsed, data, callback);
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
// RENAME needs "x" on the mailbox and "k" on the new parent
|
|
614
|
+
const hasRenameRight = hasMailboxRight('x', 0);
|
|
615
|
+
wrappers.RENAME = (prevHandler, connection, parsed, data, callback) => {
|
|
616
|
+
if (!hasRenameRight(connection, parsed, data)) {
|
|
617
|
+
return callback();
|
|
618
|
+
}
|
|
619
|
+
const name = mailboxArgument(parsed, 1);
|
|
620
|
+
if (name !== false && argumentMailbox(parsed, 0) && !checkCreateRight(connection, parsed, data, name)) {
|
|
621
|
+
return callback();
|
|
622
|
+
}
|
|
623
|
+
return prevHandler(connection, parsed, data, callback);
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
// APPEND needs "i", and only the flags the user has rights for are stored
|
|
627
|
+
wrappers.APPEND = (prevHandler, connection, parsed, data, callback) => {
|
|
628
|
+
const mailbox = argumentMailbox(parsed, 0);
|
|
629
|
+
if (!mailbox) {
|
|
630
|
+
return prevHandler(connection, parsed, data, callback);
|
|
631
|
+
}
|
|
632
|
+
const rights = getRights(connection, mailbox);
|
|
633
|
+
if (!rights.has('i')) {
|
|
634
|
+
sendDenied(connection, parsed, data, rights, 'APPEND FAILED', 'TRYCREATE');
|
|
635
|
+
return callback();
|
|
636
|
+
}
|
|
637
|
+
const flags = parsed.attributes[1];
|
|
638
|
+
if (Array.isArray(flags) && flags.every(flag => flag && flag.type === 'ATOM')) {
|
|
639
|
+
try {
|
|
640
|
+
flags.forEach(flag => checkSystemFlags(connection, normalizeSystemFlag(flag.value)));
|
|
641
|
+
// the server MUST NOT fail APPEND for flags the user can not set
|
|
642
|
+
parsed.attributes[1] = flags.filter(flag => canSetFlag(rights, flag));
|
|
643
|
+
} catch {
|
|
644
|
+
// invalid flags are refused with BAD by the APPEND handler
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
return prevHandler(connection, parsed, data, callback);
|
|
648
|
+
};
|
|
649
|
+
|
|
650
|
+
// the refusal for a target mailbox without "i", see sendDenied
|
|
651
|
+
const insertRefusal = (connection, mailbox) => {
|
|
652
|
+
const rights = mailbox ? getRights(connection, mailbox) : false;
|
|
653
|
+
if (!rights || rights.has('i')) {
|
|
654
|
+
return false;
|
|
655
|
+
}
|
|
656
|
+
return rights.has('l')
|
|
657
|
+
? { command: 'NO', code: 'NOPERM', text: 'Permission denied' }
|
|
658
|
+
: { command: 'NO', code: 'TRYCREATE', text: 'Target mailbox does not exist' };
|
|
659
|
+
};
|
|
660
|
+
|
|
661
|
+
// COPY and MOVE need "i" on the target, MOVE also needs what UID STORE \Deleted and UID EXPUNGE
|
|
662
|
+
// need on the source, "t" and "e" (RFC 6851 section 4.2)
|
|
663
|
+
const requireInsert = requireRight('i', 1, 'TRYCREATE');
|
|
664
|
+
const copyWrapper = isMove => (prevHandler, connection, parsed, data, callback) => {
|
|
665
|
+
const command = parsed.command.toUpperCase();
|
|
666
|
+
if (isMove && !connection.readOnly) {
|
|
667
|
+
const rights = getSelectedRights(connection);
|
|
668
|
+
if (!rights.has('t') || !rights.has('e')) {
|
|
669
|
+
sendNoPerm(connection, parsed, data, command + ' FAIL');
|
|
670
|
+
return callback();
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
return requireInsert(prevHandler, connection, parsed, data, callback);
|
|
674
|
+
};
|
|
675
|
+
wrappers.COPY = copyWrapper(false);
|
|
676
|
+
wrappers['UID COPY'] = copyWrapper(false);
|
|
677
|
+
wrappers.MOVE = copyWrapper(true);
|
|
678
|
+
wrappers['UID MOVE'] = copyWrapper(true);
|
|
679
|
+
|
|
680
|
+
// RFC 8508 section 4.1: REPLACE needs the rights of UID STORE (\Deleted, "t") and UID EXPUNGE ("e") on the
|
|
681
|
+
// selected mailbox, and of APPEND ("i") on the target. A read-only session is refused by the command handler
|
|
682
|
+
const replaceRefusal = (connection, mailbox) => {
|
|
683
|
+
const selected = getSelectedRights(connection);
|
|
684
|
+
if (!connection.readOnly && (!selected.has('t') || !selected.has('e'))) {
|
|
685
|
+
return { command: 'NO', code: 'NOPERM', text: 'Permission denied' };
|
|
686
|
+
}
|
|
687
|
+
return insertRefusal(connection, mailbox);
|
|
688
|
+
};
|
|
689
|
+
const replaceWrapper = (prevHandler, connection, parsed, data, callback) => {
|
|
690
|
+
const refusal = replaceRefusal(connection, argumentMailbox(parsed, 1));
|
|
691
|
+
if (refusal) {
|
|
692
|
+
connection.sendStatus(parsed, data, refusal.command, refusal.text, refusal.code, parsed.command.toUpperCase() + ' FAILED');
|
|
693
|
+
return callback();
|
|
694
|
+
}
|
|
695
|
+
return prevHandler(connection, parsed, data, callback);
|
|
696
|
+
};
|
|
697
|
+
wrappers.REPLACE = replaceWrapper;
|
|
698
|
+
wrappers['UID REPLACE'] = replaceWrapper;
|
|
699
|
+
|
|
700
|
+
// The message literal of APPEND and REPLACE is refused before it is sent when the user may not add messages to the
|
|
701
|
+
// target, with the same answer as after the literal. A mailbox the user can not see ("l") is refused like a
|
|
702
|
+
// missing one, which the core literal filter refuses before the literal too, so the transcripts do not tell
|
|
703
|
+
// a hidden mailbox from a missing one (RFC 4314 section 6)
|
|
704
|
+
server.literalFilters.unshift((connection, command, line) => {
|
|
705
|
+
if (isOwner(connection) || connection.isBusy()) {
|
|
706
|
+
return false;
|
|
707
|
+
}
|
|
708
|
+
const path = getPendingTarget(connection, command, line);
|
|
709
|
+
const mailbox = path !== false && server.getMailbox(path);
|
|
710
|
+
if (!mailbox) {
|
|
711
|
+
return false;
|
|
712
|
+
}
|
|
713
|
+
return command === 'APPEND' ? insertRefusal(connection, mailbox) : replaceRefusal(connection, mailbox);
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
// RFC 5464 section 3.3: mailbox annotations need "l" and any of "r", "s", "w", "i" or "p". Server
|
|
717
|
+
// annotations (the empty mailbox name) are not covered by mailbox ACLs
|
|
718
|
+
const METADATA_RIGHTS = ['r', 's', 'w', 'i', 'p'];
|
|
719
|
+
const canUseMetadata = rights => rights.has('l') && METADATA_RIGHTS.some(right => rights.has(right));
|
|
720
|
+
const metadataWrapper = (prevHandler, connection, parsed, data, callback) => {
|
|
721
|
+
const args = parsed.attributes || [];
|
|
722
|
+
// GETMETADATA takes its options before the mailbox name (RFC 5464 errata 2785)
|
|
723
|
+
const position = parsed.command.toUpperCase() === 'GETMETADATA' && Array.isArray(args[0]) ? 1 : 0;
|
|
724
|
+
const mailbox = argumentMailbox(parsed, position);
|
|
725
|
+
if (mailbox && mailboxArgument(parsed, position) !== '') {
|
|
726
|
+
const rights = getRights(connection, mailbox);
|
|
727
|
+
if (!canUseMetadata(rights)) {
|
|
728
|
+
sendDenied(connection, parsed, data, rights, false);
|
|
729
|
+
return callback();
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
return prevHandler(connection, parsed, data, callback);
|
|
733
|
+
};
|
|
734
|
+
wrappers.GETMETADATA = metadataWrapper;
|
|
735
|
+
wrappers.SETMETADATA = metadataWrapper;
|
|
736
|
+
|
|
737
|
+
// Unsolicited METADATA responses (RFC 5464 section 4.4.2) only go to sessions that could read the
|
|
738
|
+
// annotations of the mailbox. Server annotations have an empty mailbox name
|
|
739
|
+
server.notifyFilters.push((connection, notification) => {
|
|
740
|
+
const command = notification.command || {};
|
|
741
|
+
if (command.command !== 'METADATA' || isOwner(connection)) {
|
|
742
|
+
return true;
|
|
743
|
+
}
|
|
744
|
+
const name = command.attributes && command.attributes[0];
|
|
745
|
+
const path = name && typeof name === 'object' ? name.value : name;
|
|
746
|
+
const mailbox = typeof path === 'string' && path !== '' && server.getMailbox(path);
|
|
747
|
+
return !mailbox || canUseMetadata(getRights(connection, mailbox));
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
// RFC 9208 section 6: GETQUOTA needs no rights. GETQUOTAROOT needs "r" for the MESSAGE and STORAGE
|
|
751
|
+
// resources, which tell about the messages of the mailbox, only MAILBOX needs no right. Without "r"
|
|
752
|
+
// (or for a mailbox that does not exist, so nothing is disclosed) the QUOTA responses only list MAILBOX
|
|
753
|
+
wrappers.GETQUOTAROOT = (prevHandler, connection, parsed, data, callback) => {
|
|
754
|
+
const mailbox = argumentMailbox(parsed, 0);
|
|
755
|
+
if (!mailbox || !getRights(connection, mailbox).has('r')) {
|
|
756
|
+
parsed.aclQuotaMailboxOnly = true;
|
|
757
|
+
}
|
|
758
|
+
return prevHandler(connection, parsed, data, callback);
|
|
759
|
+
};
|
|
760
|
+
|
|
761
|
+
// RFC 9208 section 6: SETQUOTA needs "a". The quota root is not a mailbox, so the user needs "a" on
|
|
762
|
+
// every mailbox the quota root applies to (all personal mailboxes)
|
|
763
|
+
wrappers.SETQUOTA = (prevHandler, connection, parsed, data, callback) => {
|
|
764
|
+
const args = parsed.attributes || [];
|
|
765
|
+
if (args.length === 2 && Array.isArray(args[1])) {
|
|
766
|
+
const isAdmin = Object.keys(server.folderCache).every(path => {
|
|
767
|
+
const mailbox = server.folderCache[path];
|
|
768
|
+
return !server.isPersonal(mailbox) || getRights(connection, mailbox).has('a');
|
|
769
|
+
});
|
|
770
|
+
if (!isAdmin) {
|
|
771
|
+
sendNoPerm(connection, parsed, data);
|
|
772
|
+
return callback();
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
return prevHandler(connection, parsed, data, callback);
|
|
776
|
+
};
|
|
777
|
+
|
|
778
|
+
// EXPUNGE needs "e". In a read-only session the command handler refuses it already
|
|
779
|
+
const expungeWrapper = (prevHandler, connection, parsed, data, callback) => {
|
|
780
|
+
if (!connection.readOnly && !getSelectedRights(connection).has('e')) {
|
|
781
|
+
sendNoPerm(connection, parsed, data);
|
|
782
|
+
return callback();
|
|
783
|
+
}
|
|
784
|
+
return prevHandler(connection, parsed, data, callback);
|
|
785
|
+
};
|
|
786
|
+
wrappers.EXPUNGE = expungeWrapper;
|
|
787
|
+
wrappers['UID EXPUNGE'] = expungeWrapper;
|
|
788
|
+
|
|
789
|
+
// UNAUTHENTICATE (RFC 8437): rights of the previous user must not stay with the session
|
|
790
|
+
server.resetHandlers.push(connection => {
|
|
791
|
+
connection.aclRights = null;
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
server.connectionHandlers.push(connection => {
|
|
795
|
+
// A FETCH that implies setting \Seen MUST NOT set it without "s", and without "e" CLOSE MUST
|
|
796
|
+
// close the mailbox without expunging (RFC 4314 section 4)
|
|
797
|
+
const canSetSeen = connection.canSetSeen;
|
|
798
|
+
const canExpunge = connection.canExpunge;
|
|
799
|
+
connection.canSetSeen = () => canSetSeen.call(connection) && getSelectedRights(connection).has('s');
|
|
800
|
+
connection.canExpunge = () => canExpunge.call(connection) && getSelectedRights(connection).has('e');
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
// STORE needs the right of at least one of the flags it changes, it SHOULD NOT fail otherwise.
|
|
804
|
+
// The flags themselves are filtered by the store handlers
|
|
805
|
+
const storeWrapper = (prevHandler, connection, parsed, data, callback) => {
|
|
806
|
+
const rights = getSelectedRights(connection);
|
|
807
|
+
const args = parsed.attributes || [];
|
|
808
|
+
const item = args.length >= 3 && args[args.length - 2] && args[args.length - 2].type === 'ATOM' && args[args.length - 2].value.toUpperCase();
|
|
809
|
+
if (connection.readOnly || !item) {
|
|
810
|
+
return prevHandler(connection, parsed, data, callback);
|
|
811
|
+
}
|
|
812
|
+
if (STORE_ITEMS.indexOf(item) < 0) {
|
|
813
|
+
// other items of plugins (e.g. X-GM-LABELS) change shared message data like keywords do, so they
|
|
814
|
+
// need "w". Unknown items are refused with BAD by the STORE handler
|
|
815
|
+
if (Object.hasOwn(server.storeHandlers, item) && !rights.has('w')) {
|
|
816
|
+
sendNoPerm(connection, parsed, data);
|
|
817
|
+
return callback();
|
|
818
|
+
}
|
|
819
|
+
return prevHandler(connection, parsed, data, callback);
|
|
820
|
+
}
|
|
821
|
+
const flags = [].concat(args[args.length - 1] || []);
|
|
822
|
+
try {
|
|
823
|
+
flags.forEach(flag => checkSystemFlags(connection, normalizeSystemFlag(flagValue(flag))));
|
|
824
|
+
} catch {
|
|
825
|
+
// invalid flags are refused with BAD by the STORE handler
|
|
826
|
+
return prevHandler(connection, parsed, data, callback);
|
|
827
|
+
}
|
|
828
|
+
let allowed;
|
|
829
|
+
if (item.charAt(0) === '+' || item.charAt(0) === '-') {
|
|
830
|
+
allowed = !flags.length || flags.some(flag => canSetFlag(rights, flag));
|
|
831
|
+
} else {
|
|
832
|
+
// replacing the flags changes every flag the user has rights for
|
|
833
|
+
allowed = FLAG_RIGHTS.some(right => rights.has(right));
|
|
834
|
+
}
|
|
835
|
+
if (!allowed) {
|
|
836
|
+
sendNoPerm(connection, parsed, data);
|
|
837
|
+
return callback();
|
|
838
|
+
}
|
|
839
|
+
return prevHandler(connection, parsed, data, callback);
|
|
840
|
+
};
|
|
841
|
+
wrappers.STORE = storeWrapper;
|
|
842
|
+
wrappers['UID STORE'] = storeWrapper;
|
|
843
|
+
|
|
844
|
+
// Checks if an extended LIST command asks for the STATUS return option (RFC 5819)
|
|
845
|
+
const hasStatusReturnOption = parsed => {
|
|
846
|
+
const args = (parsed && parsed.attributes) || [];
|
|
847
|
+
return args.some(
|
|
848
|
+
(arg, i) =>
|
|
849
|
+
arg &&
|
|
850
|
+
arg.type === 'ATOM' &&
|
|
851
|
+
/^RETURN$/i.test(arg.value) &&
|
|
852
|
+
Array.isArray(args[i + 1]) &&
|
|
853
|
+
args[i + 1].some(option => option && option.type === 'ATOM' && /^STATUS$/i.test(option.value))
|
|
854
|
+
);
|
|
855
|
+
};
|
|
856
|
+
|
|
857
|
+
// Checks if any mailbox below a mailbox is visible to the user
|
|
858
|
+
const hasVisibleChildren = (connection, mailbox) => server.hasDescendant(mailbox.path, child => getRights(connection, child).has('l'));
|
|
859
|
+
|
|
860
|
+
server.outputHandlers.push((connection, response, description, parsed, data, extra) => {
|
|
861
|
+
if (isOwner(connection)) {
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
if ((description === 'LIST ITEM' || description === 'LSUB ITEM') && extra && extra.path) {
|
|
866
|
+
// LIST and LSUB need "l", the mailbox is left out without it and never answered with NO
|
|
867
|
+
if (!getRights(connection, extra).has('l')) {
|
|
868
|
+
response.skipResponse = true;
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
const flags = response.attributes[0];
|
|
872
|
+
if (!Array.isArray(flags)) {
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
response.attributes[0] = flags.slice();
|
|
876
|
+
const index = flags.findIndex(flag => flag && flag.value === '\\HasChildren');
|
|
877
|
+
if (index >= 0 && !hasVisibleChildren(connection, extra)) {
|
|
878
|
+
// children that the user can not see do not count (RFC 3348 section 4)
|
|
879
|
+
response.attributes[0][index] = { type: 'ATOM', value: '\\HasNoChildren' };
|
|
880
|
+
}
|
|
881
|
+
// RFC 5819 section 2: a mailbox that gets no STATUS response because it can not be selected
|
|
882
|
+
// (no "r" right) MUST be listed with \Noselect
|
|
883
|
+
const isSelectable = !flags.some(flag => flag && /^\\(Noselect|NonExistent)$/i.test(flag.value));
|
|
884
|
+
if (isSelectable && hasStatusReturnOption(parsed) && !getRights(connection, extra).has('r')) {
|
|
885
|
+
response.attributes[0].push({ type: 'ATOM', value: '\\Noselect' });
|
|
886
|
+
}
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
if (description === 'QUOTA' && parsed && parsed.aclQuotaMailboxOnly && Array.isArray(response.attributes[1])) {
|
|
891
|
+
// quota-resource = resource-name SP resource-usage SP resource-limit
|
|
892
|
+
const list = response.attributes[1];
|
|
893
|
+
const filtered = [];
|
|
894
|
+
for (let i = 0; i < list.length; i += 3) {
|
|
895
|
+
if (list[i] && String(list[i].value).toUpperCase() === 'MAILBOX') {
|
|
896
|
+
filtered.push(list[i], list[i + 1], list[i + 2]);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
// quota-list needs at least one resource, so without a MAILBOX limit there is no QUOTA response
|
|
900
|
+
if (filtered.length) {
|
|
901
|
+
response.attributes[1] = filtered;
|
|
902
|
+
} else {
|
|
903
|
+
response.skipResponse = true;
|
|
904
|
+
}
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
if (description === 'SELECT PERMANENTFLAGS') {
|
|
909
|
+
// RFC 4314 section 5.1.1: PERMANENTFLAGS MUST reflect the rights of the user
|
|
910
|
+
const section = response.attributes[0] && response.attributes[0].section;
|
|
911
|
+
const rights = getSelectedRights(connection);
|
|
912
|
+
if (section && Array.isArray(section[1])) {
|
|
913
|
+
section[1] = section[1].filter(flag => (flag.value === '\\*' ? rights.has('w') : canSetFlag(rights, flag)));
|
|
914
|
+
}
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
if (extra && extra.targetUids && extra.mailbox && /^(UID )?(COPY|MOVE COPYUID)$/.test(description)) {
|
|
919
|
+
// RFC 4314 section 4: the copies only keep the flags the user has rights for in the target
|
|
920
|
+
const rights = getRights(connection, extra.mailbox);
|
|
921
|
+
const uids = new Set(extra.targetUids);
|
|
922
|
+
extra.mailbox.messages.forEach(message => {
|
|
923
|
+
if (uids.has(message.uid)) {
|
|
924
|
+
message.flags = message.flags.filter(flag => canSetFlag(rights, flag));
|
|
925
|
+
}
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
// The DELETE command MUST delete the ACL of the mailbox (RFC 4314 section 4), this matters when a
|
|
931
|
+
// \Noselect placeholder stays for the children of the mailbox
|
|
932
|
+
const prevDelete = server.getCommandHandler('DELETE');
|
|
933
|
+
server.setCommandHandler('DELETE', (connection, parsed, data, callback) => {
|
|
934
|
+
const mailbox = argumentMailbox(parsed, 0);
|
|
935
|
+
prevDelete(connection, parsed, data, () => {
|
|
936
|
+
const placeholder = argumentMailbox(parsed, 0);
|
|
937
|
+
if (placeholder && placeholder !== mailbox) {
|
|
938
|
+
delete placeholder.acl;
|
|
939
|
+
}
|
|
940
|
+
callback();
|
|
941
|
+
});
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
// A new mailbox inherits the ACL of its parent (RFC 4314 section 4, CREATE SHOULD)
|
|
945
|
+
const prevCreate = server.getCommandHandler('CREATE');
|
|
946
|
+
server.setCommandHandler('CREATE', (connection, parsed, data, callback) => {
|
|
947
|
+
const existing = new Set(Object.keys(server.folderCache));
|
|
948
|
+
prevCreate(connection, parsed, data, () => {
|
|
949
|
+
Object.keys(server.folderCache)
|
|
950
|
+
.filter(path => !existing.has(path))
|
|
951
|
+
.sort((a, b) => a.length - b.length)
|
|
952
|
+
.forEach(path => {
|
|
953
|
+
const parent = getParent(path);
|
|
954
|
+
const mailbox = server.folderCache[path];
|
|
955
|
+
if (parent && parent !== mailbox) {
|
|
956
|
+
const acl = new Map();
|
|
957
|
+
getAcl(parent).forEach((rights, identifier) => acl.set(identifier, new Set(rights)));
|
|
958
|
+
mailbox.acl = acl;
|
|
959
|
+
}
|
|
960
|
+
});
|
|
961
|
+
callback();
|
|
962
|
+
});
|
|
963
|
+
});
|
|
964
|
+
};
|