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,321 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @help Adds CONTEXT=SEARCH [RFC5267] capability, loads ESEARCH as well
|
|
5
|
+
* @help SEARCH RETURN (UPDATE) sends ADDTO and REMOVEFROM updates as the result changes, until CANCELUPDATE or the mailbox is closed
|
|
6
|
+
* @help SEARCH RETURN (PARTIAL 1:100) returns a window of the results, the CONTEXT hint is accepted and ignored
|
|
7
|
+
* @help Server option "maxSearchContexts" (default 10) limits the updating contexts of a session, above it NOUPDATE is sent
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const esearch = require('./esearch');
|
|
11
|
+
const { buildCorrelator, registerPartialOption, toSequenceSet, toOrderedSet } = require('../esearch');
|
|
12
|
+
const { states } = require('../command-states');
|
|
13
|
+
|
|
14
|
+
const DEFAULT_MAX_CONTEXTS = 10;
|
|
15
|
+
|
|
16
|
+
// commands that close the selected mailbox, updates cease with them (RFC 5267 section 4.3)
|
|
17
|
+
const CLOSING_COMMANDS = new Set(['SELECT', 'EXAMINE', 'CLOSE', 'UNSELECT']);
|
|
18
|
+
|
|
19
|
+
// searching commands that can create updating contexts, SORT and UID SORT only with CONTEXT=SORT (see the ESORT plugin)
|
|
20
|
+
const SEARCHING_COMMANDS = new Set(['SEARCH', 'UID SEARCH', 'SORT', 'UID SORT']);
|
|
21
|
+
|
|
22
|
+
const getContexts = connection => {
|
|
23
|
+
if (!connection.searchContexts) {
|
|
24
|
+
connection.searchContexts = new Map();
|
|
25
|
+
}
|
|
26
|
+
return connection.searchContexts;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Groups updates of one kind into context position and result set pairs (RFC 5267 sections 4.3.3 and
|
|
31
|
+
* 4.3.4). Updates of a searching command all use position 0 and form one set in mailbox order. Updates of
|
|
32
|
+
* a sorting command are merged while they continue each other: an insertion right after the previous
|
|
33
|
+
* inserted results, or a removal at the same position where the previous result was removed
|
|
34
|
+
*
|
|
35
|
+
* @param {Array} updates `[position, value]` pairs in the order they are applied
|
|
36
|
+
* @param {Boolean} isRemoval If true, the updates are REMOVEFROM updates
|
|
37
|
+
* @return {Array} payload of the ADDTO or REMOVEFROM return data item
|
|
38
|
+
*/
|
|
39
|
+
const groupUpdates = (updates, isRemoval) => {
|
|
40
|
+
const groups = [];
|
|
41
|
+
updates.forEach(([position, value]) => {
|
|
42
|
+
const last = groups[groups.length - 1];
|
|
43
|
+
const next = last && (isRemoval || !position ? last.position : last.position + last.values.length);
|
|
44
|
+
if (last && position === next) {
|
|
45
|
+
last.values.push(value);
|
|
46
|
+
} else {
|
|
47
|
+
groups.push({ position, values: [value] });
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
const payload = [];
|
|
51
|
+
groups.forEach(group => {
|
|
52
|
+
const set = group.position ? toOrderedSet(group.values) : toSequenceSet(group.values);
|
|
53
|
+
payload.push(group.position, { type: 'SEQUENCE', value: set });
|
|
54
|
+
});
|
|
55
|
+
return payload;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Sends ADDTO and REMOVEFROM updates of a context (RFC 5267 sections 4.3.3 and 4.3.4)
|
|
60
|
+
*
|
|
61
|
+
* @param {Object} connection IMAP connection
|
|
62
|
+
* @param {Object} context Updating context
|
|
63
|
+
* @param {Array} removed `[position, value]` pairs of removed results, applied first
|
|
64
|
+
* @param {Array} added `[position, value]` pairs of added results
|
|
65
|
+
*/
|
|
66
|
+
const sendUpdate = (connection, context, removed, added) => {
|
|
67
|
+
const attributes = [buildCorrelator(context.tag, context.correlatorMailbox)];
|
|
68
|
+
if (context.isUid) {
|
|
69
|
+
attributes.push({ type: 'ATOM', value: 'UID' });
|
|
70
|
+
}
|
|
71
|
+
if (removed.length) {
|
|
72
|
+
attributes.push({ type: 'ATOM', value: 'REMOVEFROM' }, groupUpdates(removed, true));
|
|
73
|
+
}
|
|
74
|
+
if (added.length) {
|
|
75
|
+
attributes.push({ type: 'ATOM', value: 'ADDTO' }, groupUpdates(added, false));
|
|
76
|
+
}
|
|
77
|
+
connection.send({ tag: '*', command: 'ESEARCH', attributes }, 'ESEARCH UPDATE', null, null, context);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Checks every updating context against the selected mailbox as the session sees it, and sends the
|
|
82
|
+
* changes. Expunged messages are handled when their EXPUNGE response is sent
|
|
83
|
+
*/
|
|
84
|
+
const checkContexts = connection => {
|
|
85
|
+
const contexts = connection.searchContexts;
|
|
86
|
+
const mailbox = connection.selectedMailbox;
|
|
87
|
+
const messages = mailbox ? mailbox.messages : [];
|
|
88
|
+
const numbers = new Map(messages.map((message, i) => [message, i + 1]));
|
|
89
|
+
contexts.forEach((context, tag) => {
|
|
90
|
+
if (context.mailbox !== mailbox) {
|
|
91
|
+
contexts.delete(tag);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const matching = messages.filter((message, i) => context.matches(message, i + 1));
|
|
95
|
+
const getValue = message => (context.isUid ? message.uid : numbers.get(message));
|
|
96
|
+
const current = new Set(matching);
|
|
97
|
+
const removed = [];
|
|
98
|
+
const added = [];
|
|
99
|
+
|
|
100
|
+
if (!context.compare) {
|
|
101
|
+
// a searching command, positions are 0 (RFC 5267 section 4.3.3)
|
|
102
|
+
context.results.forEach(message => {
|
|
103
|
+
// a message that is gone without an EXPUNGE response for this session is just dropped
|
|
104
|
+
if (!current.has(message) && numbers.has(message)) {
|
|
105
|
+
removed.push([0, getValue(message)]);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
matching.forEach(message => {
|
|
109
|
+
if (!context.results.has(message)) {
|
|
110
|
+
added.push([0, getValue(message)]);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
} else {
|
|
114
|
+
// a sorting command. The sort values of a message do not change, so the results that stay keep their order.
|
|
115
|
+
// Removals are counted on the list as it shrinks, insertions in ascending order of their final position
|
|
116
|
+
let kept = 0;
|
|
117
|
+
context.order.forEach(message => {
|
|
118
|
+
if (current.has(message)) {
|
|
119
|
+
kept++;
|
|
120
|
+
} else if (numbers.has(message)) {
|
|
121
|
+
removed.push([kept + 1, getValue(message)]);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
context.order = matching.sort(context.compare);
|
|
125
|
+
context.order.forEach((message, i) => {
|
|
126
|
+
if (!context.results.has(message)) {
|
|
127
|
+
added.push([i + 1, getValue(message)]);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
context.results = current;
|
|
133
|
+
if (removed.length || added.length) {
|
|
134
|
+
sendUpdate(connection, context, removed, added);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Removes expunged messages from the contexts that hold them, with one REMOVEFROM update per context
|
|
141
|
+
*
|
|
142
|
+
* @param {Object} connection IMAP connection
|
|
143
|
+
* @param {Array} messages The expunged messages
|
|
144
|
+
* @param {Number} seq Sequence number of the EXPUNGE response for a single message, null when there is none (VANISHED)
|
|
145
|
+
*/
|
|
146
|
+
const removeExpunged = (connection, messages, seq) => {
|
|
147
|
+
connection.searchContexts.forEach(context => {
|
|
148
|
+
if (context.mailbox !== connection.selectedMailbox) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const removed = [];
|
|
152
|
+
messages.forEach(message => {
|
|
153
|
+
if (!context.results.delete(message)) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
let position = 0;
|
|
157
|
+
if (context.order) {
|
|
158
|
+
position = context.order.indexOf(message) + 1;
|
|
159
|
+
context.order.splice(position - 1, 1);
|
|
160
|
+
}
|
|
161
|
+
if (context.isUid || seq) {
|
|
162
|
+
removed.push([position, context.isUid ? message.uid : seq]);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
if (removed.length) {
|
|
166
|
+
sendUpdate(connection, context, removed, []);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
module.exports = function (server) {
|
|
172
|
+
// CONTEXT=SORT loads this plugin as well, so it may be called twice
|
|
173
|
+
if (server.addSearchContext) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// the result options of CONTEXT=SEARCH extend the extended SEARCH command (RFC 5267 section 4.1)
|
|
178
|
+
esearch(server);
|
|
179
|
+
|
|
180
|
+
server.registerCapability('CONTEXT=SEARCH');
|
|
181
|
+
|
|
182
|
+
const maxContexts = Math.max(Number(server.options.maxSearchContexts) || DEFAULT_MAX_CONTEXTS, 1);
|
|
183
|
+
|
|
184
|
+
// RFC 5267 section 4.2: the server MAY ignore the CONTEXT hint
|
|
185
|
+
server.searchReturnOptions.set('CONTEXT', { hint: true });
|
|
186
|
+
// RFC 5267 section 4.3: UPDATE has no data of its own, but the ESEARCH response is still sent
|
|
187
|
+
server.searchReturnOptions.set('UPDATE', { data: false, response: true, once: true });
|
|
188
|
+
// RFC 5267 section 4.4
|
|
189
|
+
registerPartialOption(server);
|
|
190
|
+
|
|
191
|
+
// RFC 5267 section 4.3: UPDATE with the tag of an earlier searching command that still has an updating
|
|
192
|
+
// context SHALL be rejected with BAD
|
|
193
|
+
server.searchReturnChecks.push(
|
|
194
|
+
(options, connection, parsed) =>
|
|
195
|
+
options.has('UPDATE') && getContexts(connection).has(parsed.tag) && 'Tag ' + parsed.tag + ' is already in use by an updating search'
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Creates an updating context once a searching command with UPDATE has completed. Also used by
|
|
200
|
+
* CONTEXT=SORT and by the ESEARCH command of MULTISEARCH
|
|
201
|
+
*
|
|
202
|
+
* @param {Object} connection IMAP connection
|
|
203
|
+
* @param {Object} parsed Parsed searching command
|
|
204
|
+
* @param {String} data Raw command
|
|
205
|
+
* @param {Object} result Search result of the selected mailbox, with the `matches` function, and for a
|
|
206
|
+
* sorting command the `sorted` list and the `compare` function
|
|
207
|
+
* @param {Boolean} isUid If true, the updates list UIDs
|
|
208
|
+
* @param {Object} [correlatorMailbox] Mailbox for the MAILBOX and UIDVALIDITY correlators (RFC 7377)
|
|
209
|
+
*/
|
|
210
|
+
server.addSearchContext = (connection, parsed, data, result, isUid, correlatorMailbox) => {
|
|
211
|
+
const contexts = getContexts(connection);
|
|
212
|
+
if (contexts.size >= maxContexts) {
|
|
213
|
+
// RFC 5267 section 4.3.1: an untagged NO with NOUPDATE, the other result options are still honoured
|
|
214
|
+
connection.send(
|
|
215
|
+
{
|
|
216
|
+
tag: '*',
|
|
217
|
+
command: 'NO',
|
|
218
|
+
attributes: [
|
|
219
|
+
{
|
|
220
|
+
type: 'SECTION',
|
|
221
|
+
section: [
|
|
222
|
+
{ type: 'ATOM', value: 'NOUPDATE' },
|
|
223
|
+
{ type: 'STRING', value: parsed.tag }
|
|
224
|
+
]
|
|
225
|
+
},
|
|
226
|
+
{ type: 'TEXT', value: 'Too many updating contexts' }
|
|
227
|
+
]
|
|
228
|
+
},
|
|
229
|
+
'NOUPDATE',
|
|
230
|
+
parsed,
|
|
231
|
+
data
|
|
232
|
+
);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
contexts.set(parsed.tag, {
|
|
236
|
+
tag: parsed.tag,
|
|
237
|
+
isUid,
|
|
238
|
+
matches: result.matches,
|
|
239
|
+
results: new Set(result.list),
|
|
240
|
+
compare: result.compare || null,
|
|
241
|
+
order: result.compare ? result.sorted.slice() : null,
|
|
242
|
+
mailbox: connection.selectedMailbox,
|
|
243
|
+
correlatorMailbox: correlatorMailbox || null
|
|
244
|
+
});
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
// RFC 5267 section 4.3.5: command-select =/ "CANCELUPDATE" 1*(SP quoted)
|
|
248
|
+
server.setCommandHandler(
|
|
249
|
+
'CANCELUPDATE',
|
|
250
|
+
(connection, parsed, data, callback) => {
|
|
251
|
+
const tags = parsed.attributes || [];
|
|
252
|
+
if (!tags.length || tags.some(tag => !tag || tag.type !== 'STRING')) {
|
|
253
|
+
connection.sendStatus(parsed, data, 'BAD', 'CANCELUPDATE expects one or more quoted tags', false, 'CANCELUPDATE FAILED');
|
|
254
|
+
return callback();
|
|
255
|
+
}
|
|
256
|
+
const contexts = getContexts(connection);
|
|
257
|
+
const unknown = tags.find(tag => !contexts.has(tag.value));
|
|
258
|
+
if (unknown) {
|
|
259
|
+
// nothing is cancelled when a tag has no updating context
|
|
260
|
+
connection.sendStatus(parsed, data, 'NO', 'Unknown tag', false, 'CANCELUPDATE FAILED');
|
|
261
|
+
return callback();
|
|
262
|
+
}
|
|
263
|
+
tags.forEach(tag => contexts.delete(tag.value));
|
|
264
|
+
connection.sendStatus(parsed, data, 'OK', 'Updates cancelled', false, 'CANCELUPDATE');
|
|
265
|
+
return callback();
|
|
266
|
+
},
|
|
267
|
+
{ states: states.SELECTED }
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
// UNAUTHENTICATE (RFC 8437) closes the mailbox, so the updates end
|
|
271
|
+
server.resetHandlers.push(connection => {
|
|
272
|
+
connection.searchContexts = null;
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
server.connectionHandlers.push(connection => {
|
|
276
|
+
// Notifications from other sessions (and this one) are flushed here. Once none are pending, the session
|
|
277
|
+
// knows the current message list, so new matches can be reported after their EXISTS and FETCH responses
|
|
278
|
+
// (RFC 5267 section 4.3.2). A command that closes the mailbox gets no updates, its tagged response ends them
|
|
279
|
+
const processNotifications = connection.processNotifications;
|
|
280
|
+
connection.processNotifications = function (data) {
|
|
281
|
+
processNotifications.call(this, data);
|
|
282
|
+
const command = ((data && data.command) || '').toUpperCase();
|
|
283
|
+
if (this.searchContexts && this.searchContexts.size && !this.notificationQueue.length && !CLOSING_COMMANDS.has(command)) {
|
|
284
|
+
checkContexts(this);
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
server.outputHandlers.push((connection, response, description, parsed, data, extra) => {
|
|
290
|
+
// RFC 5267 section 4.3.4: REMOVEFROM for an expunged message MUST be sent before the EXPUNGE response. After
|
|
291
|
+
// ENABLE QRESYNC a VANISHED response reports the expunged messages (RFC 7162 section 3.2.10), it has no sequence
|
|
292
|
+
// numbers, so contexts with sequence numbers drop those messages without an update
|
|
293
|
+
const expunged = response.tag === '*' && (response.message ? [response.message] : response.command === 'VANISHED' && response.messages);
|
|
294
|
+
if (expunged && connection.searchContexts) {
|
|
295
|
+
removeExpunged(connection, expunged, response.message ? response.attributes[0] : null);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (!parsed) {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
const command = (parsed.command || '').toUpperCase();
|
|
303
|
+
|
|
304
|
+
if (response.tag === parsed.tag && CLOSING_COMMANDS.has(command)) {
|
|
305
|
+
connection.searchContexts = null;
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (!SEARCHING_COMMANDS.has(command) || !parsed.searchReturn || !parsed.searchReturn.has('UPDATE')) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
// the full search result, the context is created when the command succeeded
|
|
313
|
+
if (response.tag === '*' && command.endsWith(response.command) && description === command && extra && Array.isArray(extra.list)) {
|
|
314
|
+
parsed.contextResult = extra;
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
if (response.tag === parsed.tag && response.command === 'OK' && parsed.contextResult) {
|
|
318
|
+
server.addSearchContext(connection, parsed, data, parsed.contextResult, command.startsWith('UID '));
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @help Adds CONTEXT=SORT [RFC5267] capability, loads ESORT and CONTEXT=SEARCH as well
|
|
5
|
+
* @help SORT RETURN (UPDATE) sends ADDTO and REMOVEFROM updates with context positions in sort order, PARTIAL and CONTEXT work for SORT too
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const esort = require('./esort');
|
|
9
|
+
const contextSearch = require('./context-search');
|
|
10
|
+
|
|
11
|
+
module.exports = function (server) {
|
|
12
|
+
// RFC 5267 section 4.1: CONTEXT=SORT means SORT and the extended SORT syntax of section 3. The updating
|
|
13
|
+
// contexts are shared with CONTEXT=SEARCH, so that is loaded (and advertised) as well
|
|
14
|
+
esort(server);
|
|
15
|
+
contextSearch(server);
|
|
16
|
+
|
|
17
|
+
server.registerCapability('CONTEXT=SORT');
|
|
18
|
+
server.contextSort = true;
|
|
19
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @help Enables CREATE-SPECIAL-USE [RFC6154] capability
|
|
5
|
+
* @help Allowed special flags can be set with server
|
|
6
|
+
* @help option "special-use"
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
module.exports = function (server) {
|
|
10
|
+
// Register capability
|
|
11
|
+
server.registerCapability('CREATE-SPECIAL-USE');
|
|
12
|
+
|
|
13
|
+
const createHandler = server.getCommandHandler('CREATE');
|
|
14
|
+
const allowedList = [].concat(server.options['special-use'] || ['\\Archive', '\\Drafts', '\\Flagged', '\\Junk', '\\Sent', '\\Trash']);
|
|
15
|
+
|
|
16
|
+
server.setCommandHandler('CREATE', (connection, parsed, data, callback) => {
|
|
17
|
+
let i;
|
|
18
|
+
let len;
|
|
19
|
+
let specialUseList;
|
|
20
|
+
const mailboxSpecialUse = [];
|
|
21
|
+
if (parsed.attributes && Array.isArray(parsed.attributes[1])) {
|
|
22
|
+
for (i = 0; i < parsed.attributes[1].length; i += 2) {
|
|
23
|
+
if (
|
|
24
|
+
parsed.attributes[1][i] &&
|
|
25
|
+
parsed.attributes[1][i].type === 'ATOM' &&
|
|
26
|
+
parsed.attributes[1][i].value.toUpperCase() === 'USE' &&
|
|
27
|
+
Array.isArray(parsed.attributes[1][i + 1])
|
|
28
|
+
) {
|
|
29
|
+
specialUseList = parsed.attributes[1][i + 1];
|
|
30
|
+
parsed.attributes[1].splice(i, 2);
|
|
31
|
+
i -= 2;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Remove extra arguments if no members were left
|
|
36
|
+
if (!parsed.attributes[1].length) {
|
|
37
|
+
parsed.attributes.splice(1, 1);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (specialUseList) {
|
|
42
|
+
for (i = 0, len = specialUseList.length; i < len; i++) {
|
|
43
|
+
if (['ATOM', 'STRING', 'LITERAL'].indexOf(specialUseList[i].type) < 0) {
|
|
44
|
+
connection.send(
|
|
45
|
+
{
|
|
46
|
+
tag: parsed.tag,
|
|
47
|
+
command: 'BAD',
|
|
48
|
+
attributes: [
|
|
49
|
+
{
|
|
50
|
+
type: 'TEXT',
|
|
51
|
+
value: 'Invalid syntax for special use flag #' + (i + 1)
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
},
|
|
55
|
+
'CREATE-SPECIAL-USE FAILED',
|
|
56
|
+
parsed,
|
|
57
|
+
data
|
|
58
|
+
);
|
|
59
|
+
return callback();
|
|
60
|
+
}
|
|
61
|
+
// attribute names are case-insensitive, store the canonical form
|
|
62
|
+
const specialUse = allowedList.find(value => value.toLowerCase() === String(specialUseList[i].value).toLowerCase());
|
|
63
|
+
if (!specialUse) {
|
|
64
|
+
connection.send(
|
|
65
|
+
{
|
|
66
|
+
tag: parsed.tag,
|
|
67
|
+
command: 'NO',
|
|
68
|
+
attributes: [
|
|
69
|
+
{
|
|
70
|
+
type: 'SECTION',
|
|
71
|
+
section: [
|
|
72
|
+
{
|
|
73
|
+
type: 'ATOM',
|
|
74
|
+
value: 'USEATTR'
|
|
75
|
+
}
|
|
76
|
+
]
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
type: 'TEXT',
|
|
80
|
+
value: specialUseList[i].value + ' not supported'
|
|
81
|
+
}
|
|
82
|
+
]
|
|
83
|
+
},
|
|
84
|
+
'CREATE-SPECIAL-USE FAILED',
|
|
85
|
+
parsed,
|
|
86
|
+
data
|
|
87
|
+
);
|
|
88
|
+
return callback();
|
|
89
|
+
}
|
|
90
|
+
if (mailboxSpecialUse.indexOf(specialUse) < 0) {
|
|
91
|
+
mailboxSpecialUse.push(specialUse);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (mailboxSpecialUse.length) {
|
|
97
|
+
parsed.mailboxSpecialUse = mailboxSpecialUse;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
createHandler(connection, parsed, data, callback);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
server.outputHandlers.push((connection, response, description, parsed, data, folder) => {
|
|
104
|
+
if (description === 'CREATE' && folder && parsed.mailboxSpecialUse) {
|
|
105
|
+
folder['special-use'] = parsed.mailboxSpecialUse;
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
};
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { states } = require('../command-states');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @help Adds ENABLE capability [RFC5161]
|
|
7
|
+
* @help Plugins that can be enabled (eg. CONDSTORE)
|
|
8
|
+
* @help can be loaded in any order
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Makes an extension available for ENABLE. Plugins call it whether the ENABLE plugin is loaded before or after them
|
|
13
|
+
*
|
|
14
|
+
* @param {Object} server IMAPServer instance
|
|
15
|
+
* @param {String} name Capability name in the spelling it is advertised in, e.g. "IMAP4rev2"
|
|
16
|
+
*/
|
|
17
|
+
function registerEnable(server, name) {
|
|
18
|
+
server.enableAvailable = server.enableAvailable || [];
|
|
19
|
+
if (server.enableAvailable.indexOf(name) < 0) {
|
|
20
|
+
server.enableAvailable.push(name);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Checks if a session has enabled an extension with ENABLE. UNAUTHENTICATE clears the list (RFC 8437 section 4.1)
|
|
26
|
+
*
|
|
27
|
+
* @param {Object} connection IMAP connection
|
|
28
|
+
* @param {String} name Capability name in the spelling it was registered with, see registerEnable
|
|
29
|
+
* @return {Boolean} true if the extension is enabled
|
|
30
|
+
*/
|
|
31
|
+
function isEnabled(connection, name) {
|
|
32
|
+
return !!connection.enabled && connection.enabled.indexOf(name) >= 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = function (server) {
|
|
36
|
+
server.registerCapability('ENABLE');
|
|
37
|
+
|
|
38
|
+
// Shared with plugins that can be enabled, these might be loaded before or after this one
|
|
39
|
+
server.enableAvailable = server.enableAvailable || [];
|
|
40
|
+
|
|
41
|
+
server.connectionHandlers.push(connection => {
|
|
42
|
+
connection.enabled = connection.enabled || [];
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// RFC 8437 section 4.1: extensions enabled with ENABLE cease to be enabled after UNAUTHENTICATE
|
|
46
|
+
server.resetHandlers.push(connection => {
|
|
47
|
+
connection.enabled = [];
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
server.setCommandHandler(
|
|
51
|
+
'ENABLE',
|
|
52
|
+
(connection, parsed, data, callback) => {
|
|
53
|
+
let capability;
|
|
54
|
+
let i;
|
|
55
|
+
let len;
|
|
56
|
+
|
|
57
|
+
// RFC 5161 section 3.1: "Clients MUST NOT issue ENABLE once they SELECT/EXAMINE a mailbox".
|
|
58
|
+
// Servers do not have to check this, ImapKit does to catch the client bug.
|
|
59
|
+
if (connection.everSelected) {
|
|
60
|
+
connection.sendStatus(parsed, data, 'BAD', 'ENABLE is not allowed after SELECT or EXAMINE');
|
|
61
|
+
return callback();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!parsed.attributes) {
|
|
65
|
+
connection.send(
|
|
66
|
+
{
|
|
67
|
+
tag: parsed.tag,
|
|
68
|
+
command: 'BAD',
|
|
69
|
+
attributes: [
|
|
70
|
+
{
|
|
71
|
+
type: 'TEXT',
|
|
72
|
+
value: 'ENABLE expects capability list'
|
|
73
|
+
}
|
|
74
|
+
]
|
|
75
|
+
},
|
|
76
|
+
'INVALID COMMAND',
|
|
77
|
+
parsed,
|
|
78
|
+
data
|
|
79
|
+
);
|
|
80
|
+
return callback();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
for (i = 0, len = parsed.attributes.length; i < len; i++) {
|
|
84
|
+
if (!parsed.attributes[i] || parsed.attributes[i].type !== 'ATOM') {
|
|
85
|
+
connection.send(
|
|
86
|
+
{
|
|
87
|
+
tag: parsed.tag,
|
|
88
|
+
command: 'BAD',
|
|
89
|
+
attributes: [
|
|
90
|
+
{
|
|
91
|
+
type: 'TEXT',
|
|
92
|
+
value: 'Attribute nr ' + (i + 1) + ' is not an ATOM'
|
|
93
|
+
}
|
|
94
|
+
]
|
|
95
|
+
},
|
|
96
|
+
'INVALID COMMAND',
|
|
97
|
+
parsed,
|
|
98
|
+
data
|
|
99
|
+
);
|
|
100
|
+
return callback();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// The ENABLED response lists only the extensions enabled by this command
|
|
105
|
+
// Capability names are matched case-insensitively and listed in the spelling the server advertises them
|
|
106
|
+
// (`server.enableAvailable`), e.g. "IMAP4rev2", so plugins look them up in `connection.enabled` by that name
|
|
107
|
+
const enabled = [];
|
|
108
|
+
for (i = 0, len = parsed.attributes.length; i < len; i++) {
|
|
109
|
+
const requested = parsed.attributes[i].value.toUpperCase();
|
|
110
|
+
capability = server.enableAvailable.find(name => name.toUpperCase() === requested);
|
|
111
|
+
if (capability && connection.enabled.indexOf(capability) < 0) {
|
|
112
|
+
connection.enabled.push(capability);
|
|
113
|
+
enabled.push(capability);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
connection.send(
|
|
118
|
+
{
|
|
119
|
+
tag: '*',
|
|
120
|
+
command: 'ENABLED',
|
|
121
|
+
attributes: enabled.map(capability => ({
|
|
122
|
+
type: 'ATOM',
|
|
123
|
+
value: capability
|
|
124
|
+
}))
|
|
125
|
+
},
|
|
126
|
+
'ENABLED',
|
|
127
|
+
parsed,
|
|
128
|
+
data,
|
|
129
|
+
enabled
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
connection.send(
|
|
133
|
+
{
|
|
134
|
+
tag: parsed.tag,
|
|
135
|
+
command: 'OK',
|
|
136
|
+
attributes: [
|
|
137
|
+
{
|
|
138
|
+
type: 'TEXT',
|
|
139
|
+
value: 'ENABLE completed'
|
|
140
|
+
}
|
|
141
|
+
]
|
|
142
|
+
},
|
|
143
|
+
'ENABLE',
|
|
144
|
+
parsed,
|
|
145
|
+
data
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
return callback();
|
|
149
|
+
},
|
|
150
|
+
{ states: states.AUTHENTICATED }
|
|
151
|
+
);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
module.exports.registerEnable = registerEnable;
|
|
155
|
+
module.exports.isEnabled = isEnabled;
|