imapkit 0.0.0-stage → 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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,258 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { isDeepStrictEqual } = require('util');
|
|
4
|
+
const { getListExtensions } = require('../list-extensions');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @help Adds LIST-EXTENDED [RFC5258] capability: selection options
|
|
8
|
+
* @help SUBSCRIBED, REMOTE and RECURSIVEMATCH, return options SUBSCRIBED
|
|
9
|
+
* @help and CHILDREN, multiple mailbox patterns and CHILDINFO.
|
|
10
|
+
* @help \Noselect mailboxes are listed as \NonExistent
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const { isAstring } = require('../arguments');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Checks if a LIST command uses the extended syntax (RFC 5258 section 1): selection options before
|
|
17
|
+
* the reference, a list of patterns, or more than 2 arguments (return options)
|
|
18
|
+
*
|
|
19
|
+
* @param {Array} args Command arguments
|
|
20
|
+
* @return {Boolean} true for an extended LIST command
|
|
21
|
+
*/
|
|
22
|
+
const isExtended = args => Array.isArray(args[0]) || Array.isArray(args[1]) || args.length > 2;
|
|
23
|
+
|
|
24
|
+
module.exports = function (server) {
|
|
25
|
+
const extensions = getListExtensions(server);
|
|
26
|
+
if (extensions.enabled) {
|
|
27
|
+
// already loaded by LIST-STATUS
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
extensions.enabled = true;
|
|
31
|
+
|
|
32
|
+
server.registerCapability('LIST-EXTENDED');
|
|
33
|
+
|
|
34
|
+
// RFC 5258 section 3.1. There are no remote mailboxes, so REMOTE changes nothing
|
|
35
|
+
extensions.selectionOptions.SUBSCRIBED = {
|
|
36
|
+
type: 'base',
|
|
37
|
+
returnOption: 'SUBSCRIBED',
|
|
38
|
+
includeNonExistent: true,
|
|
39
|
+
match: folder => !!folder.subscribed
|
|
40
|
+
};
|
|
41
|
+
extensions.selectionOptions.REMOTE = { type: 'independent' };
|
|
42
|
+
extensions.selectionOptions.RECURSIVEMATCH = { type: 'modifier' };
|
|
43
|
+
|
|
44
|
+
// RFC 5258 section 3.2. Children attributes are always returned, so CHILDREN needs no handling
|
|
45
|
+
extensions.returnOptions.SUBSCRIBED = {};
|
|
46
|
+
extensions.returnOptions.CHILDREN = {};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Parses selection or return options (RFC 5258 section 6, option-extension = tag [SP option-value])
|
|
50
|
+
*
|
|
51
|
+
* @param {Array} list Parsed option list
|
|
52
|
+
* @param {Object} registry Known options
|
|
53
|
+
* @param {String} kind "selection" or "return", for error messages
|
|
54
|
+
* @param {Object} connection IMAPConnection instance
|
|
55
|
+
* @return {Map} option name to value (true for options without a value)
|
|
56
|
+
*/
|
|
57
|
+
const parseOptions = (list, registry, kind, connection) => {
|
|
58
|
+
const options = new Map();
|
|
59
|
+
for (let i = 0; i < list.length; i++) {
|
|
60
|
+
const item = list[i];
|
|
61
|
+
if (!item || item.type !== 'ATOM') {
|
|
62
|
+
throw new Error('Invalid ' + kind + ' option');
|
|
63
|
+
}
|
|
64
|
+
const name = item.value.toUpperCase();
|
|
65
|
+
const option = registry[name];
|
|
66
|
+
if (!option) {
|
|
67
|
+
// RFC 5258 section 3: "A server MUST respond to options it does not recognize with a BAD response"
|
|
68
|
+
throw new Error('Unknown ' + kind + ' option ' + name);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let value = true;
|
|
72
|
+
if (option.parse) {
|
|
73
|
+
if (!Array.isArray(list[i + 1])) {
|
|
74
|
+
throw new Error(name + ' ' + kind + ' option requires a value');
|
|
75
|
+
}
|
|
76
|
+
value = option.parse(list[++i], connection);
|
|
77
|
+
} else if (Array.isArray(list[i + 1])) {
|
|
78
|
+
throw new Error(name + ' ' + kind + ' option takes no value');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// RFC 5258 section 3: a repeated option counts once, so it can not have different values
|
|
82
|
+
if (options.has(name) && !isDeepStrictEqual(options.get(name), value)) {
|
|
83
|
+
throw new Error(name + ' ' + kind + ' option is repeated with a different value');
|
|
84
|
+
}
|
|
85
|
+
options.set(name, value);
|
|
86
|
+
}
|
|
87
|
+
return options;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Parses the arguments of an extended LIST command:
|
|
92
|
+
* list = "LIST" [SP list-select-opts] SP mailbox SP mbox-or-pat [SP list-return-opts]
|
|
93
|
+
*
|
|
94
|
+
* @param {Array} args Command arguments
|
|
95
|
+
* @param {Object} connection IMAPConnection instance
|
|
96
|
+
* @return {Object} `{ selection, reference, patterns, returns }`
|
|
97
|
+
*/
|
|
98
|
+
const parseArguments = (args, connection) => {
|
|
99
|
+
let selection = new Map();
|
|
100
|
+
let returns = new Map();
|
|
101
|
+
let pos = 0;
|
|
102
|
+
|
|
103
|
+
if (Array.isArray(args[0])) {
|
|
104
|
+
selection = parseOptions(args[0], extensions.selectionOptions, 'selection', connection);
|
|
105
|
+
pos++;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const reference = args[pos];
|
|
109
|
+
let patterns = args[pos + 1];
|
|
110
|
+
const rest = args.slice(pos + 2);
|
|
111
|
+
|
|
112
|
+
if (!isAstring(reference)) {
|
|
113
|
+
throw new Error('LIST expects a reference name');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// patterns = "(" list-mailbox *(SP list-mailbox) ")"
|
|
117
|
+
patterns = Array.isArray(patterns) ? patterns : [patterns];
|
|
118
|
+
if (!patterns.length || !patterns.every(isAstring)) {
|
|
119
|
+
throw new Error('LIST expects a mailbox pattern or a list of patterns');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// list-return-opts = "RETURN" SP "(" [return-option *(SP return-option)] ")"
|
|
123
|
+
if (rest.length) {
|
|
124
|
+
if (rest.length !== 2 || !rest[0] || rest[0].type !== 'ATOM' || rest[0].value.toUpperCase() !== 'RETURN' || !Array.isArray(rest[1])) {
|
|
125
|
+
throw new Error('LIST expects RETURN and a list of return options after the patterns');
|
|
126
|
+
}
|
|
127
|
+
returns = parseOptions(rest[1], extensions.returnOptions, 'return', connection);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// RFC 5258 section 3.1: RECURSIVEMATCH (any list-select-mod-opt) needs a list-select-base-opt
|
|
131
|
+
const types = [...selection.keys()].map(name => extensions.selectionOptions[name].type);
|
|
132
|
+
if (types.includes('modifier') && !types.includes('base')) {
|
|
133
|
+
throw new Error('RECURSIVEMATCH must be used together with a selection option like SUBSCRIBED');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// a selection option implies its return option, eg. SUBSCRIBED
|
|
137
|
+
selection.forEach((value, name) => {
|
|
138
|
+
const implied = extensions.selectionOptions[name].returnOption;
|
|
139
|
+
if (implied && !returns.has(implied)) {
|
|
140
|
+
returns.set(implied, true);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
selection,
|
|
146
|
+
reference: reference.value,
|
|
147
|
+
// RFC 5258 section 3: an empty pattern is ignored in an extended LIST command
|
|
148
|
+
patterns: patterns.map(pattern => pattern.value).filter(pattern => pattern),
|
|
149
|
+
returns
|
|
150
|
+
};
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const exists = folder => folder.flags.indexOf('\\Noselect') < 0;
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Mailbox attributes for an extended LIST response. \NonExistent replaces \Noselect (RFC 5258
|
|
157
|
+
* section 3, \NonExistent implies \Noselect), children attributes are computed (RFC 5258 section 4),
|
|
158
|
+
* \Subscribed is only listed with the SUBSCRIBED return option (RFC 5258 section 3.2)
|
|
159
|
+
*/
|
|
160
|
+
const getAttributes = (folder, isExisting, hasChildren, returns) =>
|
|
161
|
+
server
|
|
162
|
+
.listAttributes(folder, { exists: isExisting, subscribed: returns.has('SUBSCRIBED') && folder.subscribed, hasChildren })
|
|
163
|
+
.map(flag => ({ type: 'ATOM', value: flag }));
|
|
164
|
+
|
|
165
|
+
const listHandler = server.getCommandHandler('LIST');
|
|
166
|
+
|
|
167
|
+
server.setCommandHandler('LIST', (connection, parsed, data, callback) => {
|
|
168
|
+
const args = parsed.attributes || [];
|
|
169
|
+
if (!isExtended(args)) {
|
|
170
|
+
// RFC 3501 LIST
|
|
171
|
+
return listHandler(connection, parsed, data, callback);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
let request;
|
|
175
|
+
try {
|
|
176
|
+
request = parseArguments(args, connection);
|
|
177
|
+
} catch (err) {
|
|
178
|
+
connection.sendStatus(parsed, data, 'BAD', err.message, false, 'INVALID COMMAND');
|
|
179
|
+
return callback();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const selection = [...request.selection.keys()].map(name => ({ name, ...extensions.selectionOptions[name] }));
|
|
183
|
+
const filters = selection.filter(option => option.match);
|
|
184
|
+
const includeNonExistent = selection.some(option => option.includeNonExistent);
|
|
185
|
+
const recursive = request.selection.has('RECURSIVEMATCH');
|
|
186
|
+
// RFC 5258 section 6: CHILDINFO lists the list-select-base-opt options, always quoted
|
|
187
|
+
const childInfo = selection.filter(option => option.type === 'base').map(option => ({ type: 'STRING', value: option.name }));
|
|
188
|
+
|
|
189
|
+
// does a mailbox satisfy the selection criteria. Without options that means an existing mailbox
|
|
190
|
+
const matches = folder => (includeNonExistent || exists(folder)) && filters.every(option => option.match(folder, connection));
|
|
191
|
+
|
|
192
|
+
// RFC 5258 section 3.1: SUBSCRIBED lists subscribed names, also the ones that are not mailboxes
|
|
193
|
+
let folders = server.folderCache;
|
|
194
|
+
if (includeNonExistent) {
|
|
195
|
+
folders = Object.assign(Object.create(null), folders, server.getSubscriptionTree());
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// RFC 5258 section 3: a mailbox that matches several patterns is listed once
|
|
199
|
+
const candidates = new Set();
|
|
200
|
+
request.patterns.forEach(pattern => {
|
|
201
|
+
server.matchFolders(request.reference, pattern, path => connection.exportMailboxName(path), folders).forEach(folder => candidates.add(folder));
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
candidates.forEach(folder => {
|
|
205
|
+
const isExisting = exists(folder);
|
|
206
|
+
const isMatch = matches(folder);
|
|
207
|
+
const descendants = server.getDescendants(folder.path, folders);
|
|
208
|
+
const hasChildren = descendants.some(exists);
|
|
209
|
+
|
|
210
|
+
let hasChildInfo = false;
|
|
211
|
+
if (recursive) {
|
|
212
|
+
// RFC 5258 section 3.5: a matching mailbox gets CHILDINFO if a descendant matches as well.
|
|
213
|
+
// A mailbox that does not match is only listed for a matching descendant that is not
|
|
214
|
+
// listed itself (redundant CHILDINFO SHOULD be suppressed)
|
|
215
|
+
hasChildInfo = descendants.some(descendant => matches(descendant) && (isMatch || !candidates.has(descendant)));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// RFC 5258 section 3.5: without selection filters a mailbox that does not exist but has
|
|
219
|
+
// existing descendants is listed as "\NonExistent \HasChildren"
|
|
220
|
+
const listed = isMatch || hasChildInfo || (!filters.length && !isExisting && hasChildren);
|
|
221
|
+
if (!listed) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const attributes = [
|
|
226
|
+
getAttributes(folder, isExisting, hasChildren, request.returns),
|
|
227
|
+
server.getSeparator(folder),
|
|
228
|
+
connection.exportMailboxName(folder.path)
|
|
229
|
+
];
|
|
230
|
+
if (hasChildInfo) {
|
|
231
|
+
// mbox-list-extended = "(" mbox-list-extended-item *(SP mbox-list-extended-item) ")"
|
|
232
|
+
attributes.push([{ type: 'STRING', value: 'CHILDINFO' }, childInfo]);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
connection.send(
|
|
236
|
+
{
|
|
237
|
+
tag: '*',
|
|
238
|
+
command: 'LIST',
|
|
239
|
+
attributes
|
|
240
|
+
},
|
|
241
|
+
'LIST ITEM',
|
|
242
|
+
parsed,
|
|
243
|
+
data,
|
|
244
|
+
folder
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
request.returns.forEach((value, name) => {
|
|
248
|
+
const option = extensions.returnOptions[name];
|
|
249
|
+
if (option.onItem) {
|
|
250
|
+
option.onItem(connection, folder, value, { matched: isMatch, exists: isExisting }, parsed, data);
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
connection.sendStatus(parsed, data, 'OK', 'Completed', false, 'LIST');
|
|
256
|
+
return callback();
|
|
257
|
+
});
|
|
258
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { getListExtensions } = require('../list-extensions');
|
|
4
|
+
const { parseStatusItems, sendStatus } = require('../commands/handlers/status');
|
|
5
|
+
const listExtended = require('./list-extended');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @help Adds LIST-STATUS [RFC5819] capability, the STATUS return option
|
|
9
|
+
* @help of LIST. Loads LIST-EXTENDED as well
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
module.exports = function (server) {
|
|
13
|
+
// the STATUS return option needs the extended LIST syntax (RFC 5819 section 4)
|
|
14
|
+
listExtended(server);
|
|
15
|
+
|
|
16
|
+
server.registerCapability('LIST-STATUS');
|
|
17
|
+
|
|
18
|
+
// status-option = "STATUS" SP "(" status-att *(SP status-att) ")"
|
|
19
|
+
getListExtensions(server).returnOptions.STATUS = {
|
|
20
|
+
parse: (list, connection) => parseStatusItems(server, list, connection),
|
|
21
|
+
|
|
22
|
+
// RFC 5819 section 2: a STATUS response follows the LIST response of every selectable mailbox
|
|
23
|
+
// that matches the selection criteria. Mailboxes listed only for CHILDINFO or as \NonExistent
|
|
24
|
+
// get none. The selected mailbox is no exception (RFC 9051 section 6.3.11)
|
|
25
|
+
onItem: (connection, folder, items, info, parsed, data) => {
|
|
26
|
+
if (info.matched && info.exists) {
|
|
27
|
+
sendStatus(connection, folder.path, folder, items, parsed, data);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @help Enables LITERAL- [RFC7888] capability
|
|
5
|
+
* @help Non-synchronizing literals up to 4096 octets, a larger
|
|
6
|
+
* @help one is dropped and answered with BAD [TOOBIG]
|
|
7
|
+
* @help Can not be loaded with LITERAL+
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
module.exports = function (server) {
|
|
11
|
+
// RFC 7888 section 5: servers MUST NOT advertise both LITERAL+ and LITERAL-
|
|
12
|
+
if (server.capabilities['LITERAL+']) {
|
|
13
|
+
throw new Error('LITERAL- can not be enabled together with LITERAL+');
|
|
14
|
+
}
|
|
15
|
+
server.registerCapability('LITERAL-');
|
|
16
|
+
// loaded by name, LITERAL+ can not replace it any more (see IMAP4rev2)
|
|
17
|
+
server.impliedLiteralMinus = false;
|
|
18
|
+
server.literalPlus = true;
|
|
19
|
+
server.nonSyncLiteralLimit = 4096;
|
|
20
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @help Enables LITERAL+ [RFC7888] capability
|
|
5
|
+
* @help Can not be loaded with LITERAL-
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
module.exports = function (server) {
|
|
9
|
+
// RFC 7888 section 5: servers MUST NOT advertise both LITERAL+ and LITERAL-. IMAP4rev2 adds LITERAL- only
|
|
10
|
+
// when LITERAL+ is not loaded, LITERAL+ replaces it then, as it allows more (RFC 9051 section 4.3)
|
|
11
|
+
if (server.capabilities['LITERAL-'] && !server.impliedLiteralMinus) {
|
|
12
|
+
throw new Error('LITERAL+ can not be enabled together with LITERAL-');
|
|
13
|
+
}
|
|
14
|
+
delete server.capabilities['LITERAL-'];
|
|
15
|
+
server.registerCapability('LITERAL+');
|
|
16
|
+
server.literalPlus = true;
|
|
17
|
+
server.nonSyncLiteralLimit = Infinity;
|
|
18
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @help Disables LOGIN support for unencrypted connections
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
module.exports = function (server) {
|
|
8
|
+
server.registerCapability('LOGINDISABLED', connection => {
|
|
9
|
+
return !connection.secureConnection && connection.state === 'Not Authenticated';
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
// Retrieve actual LOGIN handler
|
|
13
|
+
// Will be run if conditions are met
|
|
14
|
+
const oldHandler = server.getCommandHandler('LOGIN');
|
|
15
|
+
|
|
16
|
+
// Override LOGIN
|
|
17
|
+
server.setCommandHandler('LOGIN', (connection, parsed, data, callback) => {
|
|
18
|
+
// If the connection is unsecure, do not allow LOGIN
|
|
19
|
+
if (!connection.secureConnection) {
|
|
20
|
+
connection.send(
|
|
21
|
+
{
|
|
22
|
+
tag: parsed.tag,
|
|
23
|
+
command: 'NO',
|
|
24
|
+
attributes: [
|
|
25
|
+
{
|
|
26
|
+
type: 'SECTION',
|
|
27
|
+
section: [
|
|
28
|
+
{
|
|
29
|
+
type: 'ATOM',
|
|
30
|
+
value: 'PRIVACYREQUIRED'
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
type: 'TEXT',
|
|
36
|
+
value: 'Run STARTTLS first'
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
},
|
|
40
|
+
'LOGIN FAILED',
|
|
41
|
+
parsed,
|
|
42
|
+
data
|
|
43
|
+
);
|
|
44
|
+
return callback();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Reroute command to actual LOGIN handler
|
|
48
|
+
oldHandler(connection, parsed, data, callback);
|
|
49
|
+
});
|
|
50
|
+
};
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { badError } = require('../commands/handlers/search');
|
|
4
|
+
const { parsePartialRange } = require('../esearch');
|
|
5
|
+
const { MAX_NUMBER, isNzNumber } = require('../numbers');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @help Adds MESSAGELIMIT [RFC9738] capability, advertised as MESSAGELIMIT=<n>
|
|
9
|
+
* @help Server option "messageLimit" sets n (default 1000). FETCH, STORE, SEARCH, MOVE, UID EXPUNGE
|
|
10
|
+
* @help and their UID variants only work on the n messages with the highest UIDs and add
|
|
11
|
+
* @help [MESSAGELIMIT n uid] to the tagged OK. COPY, APPEND (MULTIAPPEND), SORT and THREAD of
|
|
12
|
+
* @help more messages fail with NO [MESSAGELIMIT ...]. Adds the UIDAFTER and UIDBEFORE search keys.
|
|
13
|
+
* @help Can not be loaded with SAVELIMIT
|
|
14
|
+
*
|
|
15
|
+
* MESSAGELIMIT: https://www.rfc-editor.org/rfc/rfc9738
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
// RFC 9738 section 3: the advertised limit SHOULD NOT be lower than 1000
|
|
19
|
+
const DEFAULT_LIMIT = 1000;
|
|
20
|
+
|
|
21
|
+
// RFC 9738 section 3.1: commands that operate on the messages with the highest UIDs and return the MESSAGELIMIT response code.
|
|
22
|
+
// EXPUNGE, CLOSE and STATUS UNSEEN MUST NOT be limited
|
|
23
|
+
const PARTIAL_COMMANDS = new Set(['FETCH', 'UID FETCH', 'STORE', 'UID STORE', 'MOVE', 'UID MOVE', 'UID EXPUNGE']);
|
|
24
|
+
const SEARCH_COMMANDS = new Set(['SEARCH', 'UID SEARCH']);
|
|
25
|
+
// RFC 9738 section 3.3: SORT and THREAD can not be run on more messages than the limit
|
|
26
|
+
const SORT_COMMANDS = new Set(['SORT', 'UID SORT', 'THREAD', 'UID THREAD']);
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Reads the limit from the "messageLimit" server option
|
|
30
|
+
*
|
|
31
|
+
* @param {Object} server IMAPServer
|
|
32
|
+
* @param {String} name Capability name for the error message
|
|
33
|
+
* @return {Number} limit
|
|
34
|
+
*/
|
|
35
|
+
function getLimit(server, name) {
|
|
36
|
+
if (server.messageLimit) {
|
|
37
|
+
// RFC 9738 section 3: SAVELIMIT is advertised instead of MESSAGELIMIT, never both
|
|
38
|
+
throw new Error(name + ' can not be enabled together with ' + server.messageLimit.name);
|
|
39
|
+
}
|
|
40
|
+
const limit = 'messageLimit' in server.options ? server.options.messageLimit : DEFAULT_LIMIT;
|
|
41
|
+
// message-limit = nz-number
|
|
42
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_NUMBER) {
|
|
43
|
+
throw new TypeError('Invalid messageLimit option, expecting a positive integer');
|
|
44
|
+
}
|
|
45
|
+
server.messageLimit = { name, limit };
|
|
46
|
+
return limit;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Refuses COPY and APPEND of more messages than the limit, shared with the SAVELIMIT plugin. COPY and
|
|
51
|
+
* MULTIAPPEND APPEND are atomic, so nothing is copied or appended (RFC 9738 section 3.1)
|
|
52
|
+
*
|
|
53
|
+
* @param {Object} server IMAPServer
|
|
54
|
+
* @param {Number} limit Message limit
|
|
55
|
+
*/
|
|
56
|
+
function addSaveLimit(server, limit) {
|
|
57
|
+
server.appendChecks.push((connection, mailbox, messages, options) => {
|
|
58
|
+
if (messages.length <= limit) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
if (options.command) {
|
|
62
|
+
return { code: ['MESSAGELIMIT', limit], text: options.command + ' exceeds the limit of ' + limit + ' messages, nothing was appended' };
|
|
63
|
+
}
|
|
64
|
+
// MOVE is not atomic, it is cut to the limit before it gets here
|
|
65
|
+
if (!options.move) {
|
|
66
|
+
// the lowest UID the command would have processed, the messages are in UID order
|
|
67
|
+
const lastUid = messages[messages.length - limit].uid;
|
|
68
|
+
return { code: ['MESSAGELIMIT', limit, lastUid], text: 'Too many messages to copy, try a smaller subset' };
|
|
69
|
+
}
|
|
70
|
+
return false;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = function (server) {
|
|
75
|
+
const limit = getLimit(server, 'MESSAGELIMIT');
|
|
76
|
+
server.registerCapability('MESSAGELIMIT=' + limit);
|
|
77
|
+
addSaveLimit(server, limit);
|
|
78
|
+
|
|
79
|
+
const commandName = parsed => String(parsed.command || '').toUpperCase();
|
|
80
|
+
|
|
81
|
+
// FETCH, STORE, MOVE and UID EXPUNGE only operate on the messages with the highest UIDs, "the server is REQUIRED
|
|
82
|
+
// to process messages from highest to lowest UID". COPY is refused by addSaveLimit instead
|
|
83
|
+
server.rangeLimits.push((connection, parsed, range) => {
|
|
84
|
+
const command = commandName(parsed);
|
|
85
|
+
// a FETCH with the PARTIAL modifier was checked before it ran, its range is not cut
|
|
86
|
+
if (!PARTIAL_COMMANDS.has(command) || parsed.partialFetch) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
// UID EXPUNGE only operates on the \Deleted messages of the set
|
|
90
|
+
const operated = command === 'UID EXPUNGE' ? range.filter(entry => entry[1].flags.indexOf('\\Deleted') >= 0) : range;
|
|
91
|
+
if (operated.length <= limit) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
const dropped = new Set(operated.slice(0, -limit));
|
|
95
|
+
parsed.messageLimitUid = operated[operated.length - limit][1].uid;
|
|
96
|
+
// section 3.1: "when the MESSAGELIMIT response code is returned, the server is REQUIRED to process messages
|
|
97
|
+
// from highest to lowest UID", the FETCH, STORE, MOVE and UID EXPUNGE examples respond in that order
|
|
98
|
+
parsed.highestFirst = true;
|
|
99
|
+
return range.filter(entry => !dropped.has(entry)).reverse();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// RFC 9738 section 3.1: with the PARTIAL FETCH modifier (RFC 9394 section 3.3), the PARTIAL range is the message
|
|
103
|
+
// count, a larger range is refused without doing any work. The PARTIAL plugin answers an invalid modifier with BAD
|
|
104
|
+
server.commandChecks.push((connection, parsed) => {
|
|
105
|
+
const modifiers = server.capabilities.PARTIAL && parsed.attributes && parsed.attributes[2];
|
|
106
|
+
const command = commandName(parsed);
|
|
107
|
+
if (!Array.isArray(modifiers) || (command !== 'FETCH' && command !== 'UID FETCH')) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
const position = modifiers.findIndex((item, i) => !(i % 2) && item && item.type === 'ATOM' && String(item.value).toUpperCase() === 'PARTIAL');
|
|
111
|
+
let range;
|
|
112
|
+
try {
|
|
113
|
+
range = position >= 0 && parsePartialRange(modifiers[position + 1], true);
|
|
114
|
+
} catch {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
if (range && range.to - range.from + 1 > limit) {
|
|
118
|
+
return { command: 'NO', code: ['MESSAGELIMIT', limit], text: command + ' exceeds the limit of ' + limit + ' messages' };
|
|
119
|
+
}
|
|
120
|
+
return false;
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// RFC 9738 section 3.1: SEARCH counts the searched messages, not the matching ones. The messages a search
|
|
124
|
+
// looks at are the ones its top level sequence set, UID, UIDAFTER and UIDBEFORE keys allow
|
|
125
|
+
server.searchLimits.push((connection, messages, query) => {
|
|
126
|
+
// the running command, SEARCH and SORT do not pass it to the search
|
|
127
|
+
const parsed = connection._runningCommand && connection._runningCommand.parsed;
|
|
128
|
+
const command = parsed && commandName(parsed);
|
|
129
|
+
if (messages.length <= limit || (!SEARCH_COMMANDS.has(command) && !SORT_COMMANDS.has(command))) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
const candidates = messages.filter(message => isCandidate(query, message));
|
|
133
|
+
if (candidates.length <= limit) {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
if (SORT_COMMANDS.has(command)) {
|
|
137
|
+
const err = new Error(command + ' exceeds the limit of ' + limit + ' messages, narrow it down with UIDAFTER or UIDBEFORE');
|
|
138
|
+
err.imapResponse = 'NO';
|
|
139
|
+
err.responseCode = ['MESSAGELIMIT', limit];
|
|
140
|
+
throw err;
|
|
141
|
+
}
|
|
142
|
+
// with SEARCHRES, only these results are saved in "$" (RFC 9738 section 3.4)
|
|
143
|
+
parsed.messageLimitUid = candidates[candidates.length - limit].uid;
|
|
144
|
+
return candidates.slice(-limit);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// RFC 9738 section 3.2: UIDAFTER <uid> is "UID <uid>+1:*", UIDBEFORE <uid> is "UID 1:<uid>-1"
|
|
148
|
+
const parseUniqueId = value => {
|
|
149
|
+
// uniqueid = nz-number
|
|
150
|
+
if (!isNzNumber(value)) {
|
|
151
|
+
throw badError('UIDAFTER and UIDBEFORE expect a UID');
|
|
152
|
+
}
|
|
153
|
+
return Number(value);
|
|
154
|
+
};
|
|
155
|
+
const uidAfter = (connection, message, index, uid) => message.uid > uid;
|
|
156
|
+
uidAfter.argumentTypes = () => [parseUniqueId];
|
|
157
|
+
const uidBefore = (connection, message, index, uid) => message.uid < uid;
|
|
158
|
+
uidBefore.argumentTypes = () => [parseUniqueId];
|
|
159
|
+
server.searchHandlers.UIDAFTER = uidAfter;
|
|
160
|
+
server.searchHandlers.UIDBEFORE = uidBefore;
|
|
161
|
+
|
|
162
|
+
const outputHandler = (connection, response, description, parsed, data) => {
|
|
163
|
+
// section 3.1 UID SEARCH example: the results go from the highest UID down, (MODSEQ n) of CONDSTORE stays last
|
|
164
|
+
if (parsed && parsed.messageLimitUid && response.tag === '*' && response.command === 'SEARCH' && Array.isArray(response.attributes)) {
|
|
165
|
+
const modseq = response.attributes.filter(Array.isArray);
|
|
166
|
+
response.attributes = response.attributes
|
|
167
|
+
.filter(attr => !Array.isArray(attr))
|
|
168
|
+
.reverse()
|
|
169
|
+
.concat(modseq);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (!parsed || !parsed.messageLimitUid || response.tag !== parsed.tag) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const lastUid = parsed.messageLimitUid;
|
|
176
|
+
parsed.messageLimitUid = false;
|
|
177
|
+
if (response.command !== 'OK') {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// resp-text-code =/ "MESSAGELIMIT" SP message-limit [SP uniqueid]
|
|
182
|
+
const code = { type: 'SECTION', section: [{ type: 'ATOM', value: 'MESSAGELIMIT' }, limit, lastUid] };
|
|
183
|
+
const attributes = response.attributes || [];
|
|
184
|
+
if (attributes[0] && attributes[0].type === 'SECTION') {
|
|
185
|
+
// RFC 9738 section 3.1: when the tagged OK carries another response code (EXPUNGEISSUED there, and here
|
|
186
|
+
// also HIGHESTMODSEQ, MODIFIED), MESSAGELIMIT is sent in an untagged NO
|
|
187
|
+
connection.send(
|
|
188
|
+
{
|
|
189
|
+
tag: '*',
|
|
190
|
+
command: 'NO',
|
|
191
|
+
attributes: [code, { type: 'TEXT', value: 'Only the last ' + limit + ' messages were processed' }]
|
|
192
|
+
},
|
|
193
|
+
'MESSAGELIMIT',
|
|
194
|
+
parsed,
|
|
195
|
+
data
|
|
196
|
+
);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
response.attributes = [code].concat(attributes);
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
// registered once every plugin is loaded, so that the response codes other plugins add to the tagged OK
|
|
203
|
+
// are already there
|
|
204
|
+
server.once('pluginsLoaded', () => server.outputHandlers.push(outputHandler));
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Checks if a message passes the keys of a search query that every match must pass: sequence sets,
|
|
209
|
+
* UID, UIDAFTER and UIDBEFORE at the top level, or in parenthesized lists at the top level
|
|
210
|
+
*
|
|
211
|
+
* @param {Object} node AND node of the query tree, see commands/handlers/search.js
|
|
212
|
+
* @param {Object} message Message
|
|
213
|
+
* @return {Boolean} true if the message has to be searched
|
|
214
|
+
*/
|
|
215
|
+
function isCandidate(node, message) {
|
|
216
|
+
return node.args.every(arg => {
|
|
217
|
+
switch (arg.key) {
|
|
218
|
+
case 'AND':
|
|
219
|
+
return isCandidate(arg, message);
|
|
220
|
+
case '_SEQ':
|
|
221
|
+
case 'UID':
|
|
222
|
+
return arg.set.has(message);
|
|
223
|
+
case 'UIDAFTER':
|
|
224
|
+
return message.uid > arg.args[0];
|
|
225
|
+
case 'UIDBEFORE':
|
|
226
|
+
return message.uid < arg.args[0];
|
|
227
|
+
default:
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
module.exports.getLimit = getLimit;
|
|
234
|
+
module.exports.addSaveLimit = addSaveLimit;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { setup } = require('./metadata');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @help Adds METADATA-SERVER [RFC5464] capability, like METADATA but
|
|
7
|
+
* @help only for server annotations (mailbox name ""). With METADATA
|
|
8
|
+
* @help also loaded, only METADATA is advertised
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
module.exports = function (server) {
|
|
12
|
+
setup(server, false);
|
|
13
|
+
};
|