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,654 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { states } = require('../command-states');
|
|
4
|
+
const { fetchResponse } = require('../commands/fetch');
|
|
5
|
+
const builtinFetchHandlers = require('../commands/handlers/fetch');
|
|
6
|
+
const { statusResponse } = require('../commands/handlers/status');
|
|
7
|
+
const { badError } = require('../commands/handlers/search');
|
|
8
|
+
const { isAtom, isAstring } = require('../arguments');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @help Adds NOTIFY [RFC5465] capability. Events: MessageNew (with
|
|
12
|
+
* @help fetch attributes for the selected mailbox), MessageExpunge,
|
|
13
|
+
* @help FlagChange, MailboxName, SubscriptionChange, and with METADATA
|
|
14
|
+
* @help MailboxMetadataChange and ServerMetadataChange.
|
|
15
|
+
* @help server.notifyOverflow([connection]) sends
|
|
16
|
+
* @help "* OK [NOTIFICATIONOVERFLOW]" and turns NOTIFY off
|
|
17
|
+
*
|
|
18
|
+
* NOTIFY: https://www.rfc-editor.org/rfc/rfc5465
|
|
19
|
+
*
|
|
20
|
+
* Additional commands:
|
|
21
|
+
* - NOTIFY SET [STATUS] event-groups, NOTIFY NONE
|
|
22
|
+
*
|
|
23
|
+
* After the first NOTIFY command the session gets only the events it asked for (RFC 5465 section 3.1),
|
|
24
|
+
* also between commands. Events caused by the session itself are not reported (section 5), the
|
|
25
|
+
* responses of its own commands are sent as usual. Fetch attributes of MessageNew never set \Seen,
|
|
26
|
+
* clients SHOULD NOT use such attributes anyway (section 5.2).
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
// RFC 5465 section 8, canonical names by upper case name
|
|
30
|
+
const MESSAGE_EVENTS = ['MessageNew', 'MessageExpunge', 'FlagChange', 'AnnotationChange'];
|
|
31
|
+
const EVENT_NAMES = new Map(
|
|
32
|
+
MESSAGE_EVENTS.concat(['MailboxName', 'SubscriptionChange', 'MailboxMetadataChange', 'ServerMetadataChange']).map(name => [name.toUpperCase(), name])
|
|
33
|
+
);
|
|
34
|
+
const SELECTED_FILTERS = ['SELECTED', 'SELECTED-DELAYED'];
|
|
35
|
+
const OTHER_FILTERS = ['INBOXES', 'PERSONAL', 'SUBSCRIBED', 'SUBTREE', 'MAILBOXES'];
|
|
36
|
+
// fetch items whose arguments are checked with a dry run when NOTIFY SET is parsed
|
|
37
|
+
const SECTION_ITEMS = ['BODY', 'BODY.PEEK', 'BINARY', 'BINARY.PEEK', 'BINARY.SIZE'];
|
|
38
|
+
|
|
39
|
+
module.exports = function (server) {
|
|
40
|
+
server.registerCapability('NOTIFY');
|
|
41
|
+
|
|
42
|
+
// NOTIFY needs the authenticated state, UNAUTHENTICATE clears the settings
|
|
43
|
+
const isActive = connection => !!connection.notifyState;
|
|
44
|
+
|
|
45
|
+
// RFC 5465 sections 5.6 and 5.7: the metadata events are supported (and REQUIRED) with METADATA or METADATA-SERVER
|
|
46
|
+
const getSupportedEvents = () => {
|
|
47
|
+
const supported = ['MessageNew', 'MessageExpunge', 'FlagChange', 'MailboxName', 'SubscriptionChange'];
|
|
48
|
+
if (server.capabilities.METADATA) {
|
|
49
|
+
supported.push('MailboxMetadataChange');
|
|
50
|
+
}
|
|
51
|
+
if (server.capabilities.METADATA || server.capabilities['METADATA-SERVER']) {
|
|
52
|
+
supported.push('ServerMetadataChange');
|
|
53
|
+
}
|
|
54
|
+
return supported;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Access checks, RFC 5465 section 3.1 and section 5: every event needs the "l" and "r" rights
|
|
58
|
+
const hasRights = (connection, mailbox, letters, acl) => {
|
|
59
|
+
const rights = server.acl && server.acl.getRights(connection, mailbox, acl);
|
|
60
|
+
return !rights || [...letters].every(letter => rights.has(letter));
|
|
61
|
+
};
|
|
62
|
+
const canList = (connection, mailbox, acl) => hasRights(connection, mailbox, 'l', acl);
|
|
63
|
+
const canRead = (connection, mailbox, acl) => hasRights(connection, mailbox, 'lr', acl);
|
|
64
|
+
|
|
65
|
+
const normalizePath = path => (path.toUpperCase() === 'INBOX' ? 'INBOX' : path);
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Checks if an event group of NOTIFY SET covers a mailbox name (RFC 5465 section 6)
|
|
69
|
+
*
|
|
70
|
+
* @param {Object} group Parsed event group
|
|
71
|
+
* @param {String} path Storage name
|
|
72
|
+
* @param {Object} [mailbox] Mailbox object, if the mailbox exists
|
|
73
|
+
* @param {Boolean} [subscription] true for a subscription change, the subscribed filter covers both states
|
|
74
|
+
*/
|
|
75
|
+
const covers = (group, path, mailbox, subscription) => {
|
|
76
|
+
switch (group.filter) {
|
|
77
|
+
case 'INBOXES':
|
|
78
|
+
// RFC 5465 section 6.3: a server that can not tell which mailboxes get mail treats it as "personal"
|
|
79
|
+
// falls through
|
|
80
|
+
case 'PERSONAL':
|
|
81
|
+
return server.isPersonal(path);
|
|
82
|
+
case 'SUBSCRIBED':
|
|
83
|
+
// RFC 5465 section 6.4: the list is reevaluated when subscriptions change
|
|
84
|
+
return !!subscription || !!(mailbox && mailbox.subscribed);
|
|
85
|
+
case 'SUBTREE':
|
|
86
|
+
return group.paths.indexOf(path) >= 0 || group.prefixes.some(prefix => path.substr(0, prefix.length) === prefix);
|
|
87
|
+
case 'MAILBOXES':
|
|
88
|
+
// RFC 5465 section 6.6: no wildcard expansion
|
|
89
|
+
return group.paths.indexOf(path) >= 0;
|
|
90
|
+
}
|
|
91
|
+
return false;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Lists the events a session asked for on a mailbox other than the selected one. Several groups
|
|
96
|
+
* can cover the same mailbox (RFC 5465 section 6), message events for the selected mailbox only
|
|
97
|
+
* come from SELECTED or SELECTED-DELAYED (section 3.1)
|
|
98
|
+
*
|
|
99
|
+
* @return {Set} event names
|
|
100
|
+
*/
|
|
101
|
+
const getEvents = (connection, path, mailbox, subscription) => {
|
|
102
|
+
const events = new Set();
|
|
103
|
+
connection.notifyState.groups.forEach(group => {
|
|
104
|
+
if (covers(group, path, mailbox, subscription)) {
|
|
105
|
+
group.events.forEach(event => events.add(event));
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
if (mailbox && mailbox === connection.selectedMailbox) {
|
|
109
|
+
MESSAGE_EVENTS.forEach(event => events.delete(event));
|
|
110
|
+
}
|
|
111
|
+
return events;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// RFC 5465 section 6.1.2: with SELECTED-DELAYED an expunge waits for a command that allows it
|
|
115
|
+
const isHeld = connection =>
|
|
116
|
+
!!(connection.notifyState.selected && connection.notifyState.selected.delayed) && !connection.directNotifications && connection.hasPendingExpunge();
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Sends the queued notifications of a session that is between commands (RFC 5465 section 3.1: the
|
|
120
|
+
* client listens all the time). During a command they wait for its tagged response
|
|
121
|
+
*/
|
|
122
|
+
const flush = connection => {
|
|
123
|
+
if (!connection.socket || !connection.notificationQueue.length || connection._runningCommand || !isActive(connection) || isHeld(connection)) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
connection.processNotifications();
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// the events of one command go out together, e.g. several expunges as one VANISHED response (QRESYNC)
|
|
130
|
+
const scheduleFlush = connection => {
|
|
131
|
+
if (!connection.notifyFlushPending) {
|
|
132
|
+
connection.notifyFlushPending = true;
|
|
133
|
+
setImmediate(() => {
|
|
134
|
+
connection.notifyFlushPending = false;
|
|
135
|
+
flush(connection);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Sends an event response about another mailbox or about the server
|
|
142
|
+
*
|
|
143
|
+
* @param {Object} connection IMAP connection
|
|
144
|
+
* @param {Object} response Untagged response
|
|
145
|
+
*/
|
|
146
|
+
const deliver = (connection, response) => {
|
|
147
|
+
response.tag = '*';
|
|
148
|
+
response.notification = true;
|
|
149
|
+
if (!connection._runningCommand && isHeld(connection)) {
|
|
150
|
+
// this does not change message sequence numbers, so it does not have to wait for the expunge
|
|
151
|
+
connection.send(response, 'NOTIFY EVENT');
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
connection.notificationQueue.push(response);
|
|
155
|
+
scheduleFlush(connection);
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// the sessions that use NOTIFY, other than the one that caused a change (RFC 5465 section 5: SHOULD omit)
|
|
159
|
+
const listeners = origin => [...server.connections].filter(connection => connection !== origin && isActive(connection));
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Builds an unsolicited STATUS response for a mailbox. With FlagChange the UNSEEN count is added
|
|
163
|
+
* when it changed since the session last heard of it (RFC 5465 section 5.1), with CONDSTORE (or
|
|
164
|
+
* QRESYNC) enabled HIGHESTMODSEQ as well (sections 5.1, 5.2 and 5.3)
|
|
165
|
+
*
|
|
166
|
+
* @return {Object|Boolean} response, or false if there is nothing to tell
|
|
167
|
+
*/
|
|
168
|
+
const eventStatus = (connection, mailbox, items, events) => {
|
|
169
|
+
items = items.slice();
|
|
170
|
+
if (events.has('FlagChange')) {
|
|
171
|
+
const unseen = server.getStatus(mailbox).unseen || 0;
|
|
172
|
+
if (connection.notifyState.unseen.get(mailbox) !== unseen) {
|
|
173
|
+
connection.notifyState.unseen.set(mailbox, unseen);
|
|
174
|
+
items.push('UNSEEN');
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (server.condstore && server.condstore.isEnabled(connection)) {
|
|
178
|
+
items.push('HIGHESTMODSEQ');
|
|
179
|
+
}
|
|
180
|
+
if (!items.length) {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
return statusResponse(connection, mailbox.path, mailbox, items);
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
// Reports a change of the messages of a mailbox that is not selected (RFC 5465 sections 5.1, 5.2 and 5.3)
|
|
187
|
+
const messageEvent = (mailbox, event, items, origin) => {
|
|
188
|
+
listeners(origin).forEach(connection => {
|
|
189
|
+
if (mailbox === connection.selectedMailbox || !canRead(connection, mailbox)) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const events = getEvents(connection, mailbox.path, mailbox);
|
|
193
|
+
if (!events.has(event)) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const response = eventStatus(
|
|
197
|
+
connection,
|
|
198
|
+
mailbox,
|
|
199
|
+
event === 'FlagChange' && server.condstore && server.condstore.isEnabled(connection) ? ['UIDVALIDITY'] : items,
|
|
200
|
+
events
|
|
201
|
+
);
|
|
202
|
+
if (response) {
|
|
203
|
+
deliver(connection, response);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
// Checks if any mailbox below a mailbox is visible to the session (RFC 3348 section 4)
|
|
209
|
+
const hasVisibleChildren = (connection, path) =>
|
|
210
|
+
server.hasDescendant(path, child => canList(connection, child) && child.flags.indexOf('\\NonExistent') < 0);
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Builds an unsolicited LIST response with accurate attributes (RFC 5465 sections 5.4 and 5.5)
|
|
214
|
+
*
|
|
215
|
+
* @param {Object} connection IMAP connection
|
|
216
|
+
* @param {String} path Storage name
|
|
217
|
+
* @param {Object} [options] `{ oldPath, noAccess, exists }`, `exists: false` reports the name as \NonExistent
|
|
218
|
+
*/
|
|
219
|
+
const listResponse = (connection, path, options) => {
|
|
220
|
+
options = options || {};
|
|
221
|
+
const mailbox = server.getMailbox(path);
|
|
222
|
+
const exists = options.exists !== false && !!mailbox && canList(connection, mailbox);
|
|
223
|
+
const flags = server.listAttributes(exists ? mailbox : null, {
|
|
224
|
+
exists,
|
|
225
|
+
subscribed: exists && mailbox.subscribed,
|
|
226
|
+
extra: options.noAccess ? ['\\NoAccess'] : [],
|
|
227
|
+
hasChildren: hasVisibleChildren(connection, path)
|
|
228
|
+
});
|
|
229
|
+
const attributes = [flags.map(flag => ({ type: 'ATOM', value: flag })), server.getSeparator(path), { type: 'MAILBOX', value: path }];
|
|
230
|
+
if (options.oldPath) {
|
|
231
|
+
// RFC 5465 section 5.4, the OLDNAME extended data item (mbox-list-extended)
|
|
232
|
+
attributes.push([{ type: 'STRING', value: 'OLDNAME' }, [{ type: 'STRING', value: connection.exportMailboxName(options.oldPath) }]]);
|
|
233
|
+
}
|
|
234
|
+
return { tag: '*', command: 'LIST', attributes };
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
// RFC 5465 section 5.4: a created or deleted mailbox and its direct parent are affected
|
|
238
|
+
const nameEvent = (connection, path, mailbox, exists) => {
|
|
239
|
+
[path, server.getParentPath(path)].forEach(name => {
|
|
240
|
+
if (!name) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const target = name === path ? mailbox : server.getMailbox(name);
|
|
244
|
+
if (getEvents(connection, name, target).has('MailboxName')) {
|
|
245
|
+
deliver(connection, listResponse(connection, name, name === path ? { exists } : {}));
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
// RFC 5465 section 5.1: with FlagChange the server MUST tell about UIDVALIDITY changes, a mailbox that
|
|
251
|
+
// gets a name that is watched has a new UIDVALIDITY
|
|
252
|
+
const uidvalidityEvent = (connection, mailbox) => {
|
|
253
|
+
if (mailbox !== connection.selectedMailbox && canRead(connection, mailbox) && getEvents(connection, mailbox.path, mailbox).has('FlagChange')) {
|
|
254
|
+
deliver(connection, eventStatus(connection, mailbox, ['MESSAGES', 'UIDNEXT', 'UIDVALIDITY'], new Set(['FlagChange'])));
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
server.on('mailbox', change => {
|
|
259
|
+
const mailbox = server.getMailbox(change.path);
|
|
260
|
+
listeners(change.origin).forEach(connection => {
|
|
261
|
+
switch (change.type) {
|
|
262
|
+
case 'create':
|
|
263
|
+
if (mailbox && canList(connection, mailbox)) {
|
|
264
|
+
nameEvent(connection, change.path, mailbox, true);
|
|
265
|
+
uidvalidityEvent(connection, mailbox);
|
|
266
|
+
}
|
|
267
|
+
break;
|
|
268
|
+
case 'delete':
|
|
269
|
+
if (change.mailbox && canList(connection, change.mailbox)) {
|
|
270
|
+
nameEvent(connection, change.path, mailbox, !!mailbox);
|
|
271
|
+
}
|
|
272
|
+
break;
|
|
273
|
+
case 'rename':
|
|
274
|
+
// RFC 5465 section 5.4: one LIST response for the new name, none for the children
|
|
275
|
+
if (mailbox && canList(connection, mailbox)) {
|
|
276
|
+
const events = getEvents(connection, change.path, mailbox);
|
|
277
|
+
getEvents(connection, change.oldPath).forEach(event => events.add(event));
|
|
278
|
+
if (events.has('MailboxName')) {
|
|
279
|
+
deliver(connection, listResponse(connection, change.path, { oldPath: change.oldPath }));
|
|
280
|
+
}
|
|
281
|
+
uidvalidityEvent(connection, mailbox);
|
|
282
|
+
}
|
|
283
|
+
break;
|
|
284
|
+
case 'subscribe':
|
|
285
|
+
case 'unsubscribe':
|
|
286
|
+
// RFC 5465 section 5.5
|
|
287
|
+
if (mailbox && canList(connection, mailbox) && getEvents(connection, change.path, mailbox, true).has('SubscriptionChange')) {
|
|
288
|
+
deliver(connection, listResponse(connection, change.path));
|
|
289
|
+
}
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// RFC 5465 section 5.4: granting or revoking the "l" right counts as creating or deleting the mailbox. Section 5.9:
|
|
296
|
+
// monitoring stops without the rights and starts again when they are granted, \NoAccess tells about the "r" right
|
|
297
|
+
server.on('acl', (mailbox, previous) => {
|
|
298
|
+
listeners(null).forEach(connection => {
|
|
299
|
+
const before = { list: canList(connection, mailbox, previous), read: canRead(connection, mailbox, previous) };
|
|
300
|
+
const after = { list: canList(connection, mailbox), read: canRead(connection, mailbox) };
|
|
301
|
+
if (before.list !== after.list) {
|
|
302
|
+
nameEvent(connection, mailbox.path, mailbox, after.list);
|
|
303
|
+
} else if (after.list && before.read !== after.read) {
|
|
304
|
+
const events = getEvents(connection, mailbox.path, mailbox);
|
|
305
|
+
if (MESSAGE_EVENTS.some(event => events.has(event))) {
|
|
306
|
+
deliver(connection, listResponse(connection, mailbox.path, { noAccess: !after.read }));
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
server.on('expunge', (mailbox, messages, origin) => messageEvent(mailbox, 'MessageExpunge', ['MESSAGES', 'UIDNEXT'], origin));
|
|
313
|
+
|
|
314
|
+
server.on('notify', notification => {
|
|
315
|
+
const command = notification.command || {};
|
|
316
|
+
const mailbox = typeof notification.mailbox === 'string' ? server.getMailbox(notification.mailbox) : notification.mailbox;
|
|
317
|
+
|
|
318
|
+
if (mailbox && command.flagUpdate) {
|
|
319
|
+
messageEvent(mailbox, 'FlagChange', [], notification.origin);
|
|
320
|
+
} else if (mailbox && command.message && isAtom(command.attributes && command.attributes[1], 'EXISTS')) {
|
|
321
|
+
messageEvent(mailbox, 'MessageNew', ['MESSAGES', 'UIDNEXT'], notification.origin);
|
|
322
|
+
} else if (command.command === 'METADATA' && command.attributes && command.attributes[0]) {
|
|
323
|
+
// RFC 5465 sections 5.6 and 5.7: METADATA responses without ENABLE METADATA
|
|
324
|
+
const path = command.attributes[0].value;
|
|
325
|
+
const target = path === '' ? null : server.getMailbox(path);
|
|
326
|
+
listeners(notification.origin).forEach(connection => {
|
|
327
|
+
let wanted;
|
|
328
|
+
if (path === '') {
|
|
329
|
+
wanted = connection.notifyState.groups.some(group => group.events.has('ServerMetadataChange'));
|
|
330
|
+
} else {
|
|
331
|
+
wanted = !!target && canRead(connection, target) && getEvents(connection, path, target).has('MailboxMetadataChange');
|
|
332
|
+
}
|
|
333
|
+
if (wanted) {
|
|
334
|
+
notification.notifyHandled = notification.notifyHandled || new Set();
|
|
335
|
+
notification.notifyHandled.add(connection);
|
|
336
|
+
deliver(connection, Object.assign({}, command));
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
// a METADATA response that NOTIFY sent already does not go out again for ENABLE METADATA
|
|
343
|
+
server.notifyFilters.push((connection, notification) => !(notification.notifyHandled && notification.notifyHandled.has(connection)));
|
|
344
|
+
|
|
345
|
+
// Which message event a notification for the selected mailbox is
|
|
346
|
+
const selectedEvent = command => {
|
|
347
|
+
if (command.flagUpdate) {
|
|
348
|
+
return 'FlagChange';
|
|
349
|
+
}
|
|
350
|
+
const type = command.attributes && command.attributes[1];
|
|
351
|
+
if (isAtom(type, 'EXPUNGE')) {
|
|
352
|
+
return 'MessageExpunge';
|
|
353
|
+
}
|
|
354
|
+
if (isAtom(type, 'EXISTS')) {
|
|
355
|
+
// the EXISTS after expunges carries a snapshot, the one of a new message the message
|
|
356
|
+
return command.message ? 'MessageNew' : 'MessageExpunge';
|
|
357
|
+
}
|
|
358
|
+
return false;
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
// Builds the FETCH response for a new message in the selected mailbox (RFC 5465 section 5.2)
|
|
362
|
+
const newMessageFetch = (connection, items, sequence, message) => {
|
|
363
|
+
const params = items.map(item => Object.assign({}, item));
|
|
364
|
+
let data;
|
|
365
|
+
try {
|
|
366
|
+
data = fetchResponse(connection, message, params);
|
|
367
|
+
} catch {
|
|
368
|
+
// the items were checked by NOTIFY SET, a failure here leaves out the FETCH response
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
371
|
+
// the message goes with the response for output handlers (UIDONLY reports its UID). Not as `message`, that
|
|
372
|
+
// marks an EXPUNGE notification
|
|
373
|
+
return { tag: '*', notification: true, attributes: [sequence, { type: 'ATOM', value: 'FETCH' }, data], fetchedMessage: message };
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
server.connectionHandlers.push(connection => {
|
|
377
|
+
connection.notifyState = null;
|
|
378
|
+
|
|
379
|
+
const queueNotification = connection.queueNotification;
|
|
380
|
+
connection.queueNotification = function (command, notification) {
|
|
381
|
+
const event = notification && notification.mailbox && selectedEvent(command);
|
|
382
|
+
if (!isActive(this) || !event || notification.origin === this) {
|
|
383
|
+
// responses to the own commands of the session are not NOTIFY events
|
|
384
|
+
return queueNotification.call(this, command, notification);
|
|
385
|
+
}
|
|
386
|
+
const selected = this.notifyState.selected;
|
|
387
|
+
if (!selected || !selected.events.has(event) || !canRead(this, this.selectedMailbox)) {
|
|
388
|
+
// RFC 5465 section 3.1: without SELECTED or SELECTED-DELAYED (or without the event) the client
|
|
389
|
+
// does not want to hear about it
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
this.notificationQueue.push(command);
|
|
393
|
+
if (event === 'MessageNew' && selected.fetch) {
|
|
394
|
+
// the EXISTS response gives the sequence number of the new message
|
|
395
|
+
const fetch = newMessageFetch(this, selected.fetch, command.attributes[0], command.message);
|
|
396
|
+
if (fetch) {
|
|
397
|
+
this.notificationQueue.push(fetch);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
scheduleFlush(this);
|
|
401
|
+
};
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
// RFC 8437 section 4.1: UNAUTHENTICATE forgets the NOTIFY settings
|
|
405
|
+
server.resetHandlers.push(connection => {
|
|
406
|
+
connection.notifyState = null;
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
// notifications held back during a command (FETCH, STORE, SEARCH) go out once it completed
|
|
410
|
+
server.outputHandlers.push((connection, response) => {
|
|
411
|
+
if (response.tag !== '*' && !response.notification && isActive(connection) && connection.notificationQueue.length) {
|
|
412
|
+
scheduleFlush(connection);
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* RFC 5465 section 5.8: disables notifications for a session (or all sessions that use NOTIFY),
|
|
418
|
+
* which get an untagged OK [NOTIFICATIONOVERFLOW] and behave as after NOTIFY NONE
|
|
419
|
+
*
|
|
420
|
+
* @param {Object} [target] IMAP connection, all sessions if not set
|
|
421
|
+
*/
|
|
422
|
+
server.notifyOverflow = target => {
|
|
423
|
+
listeners(null)
|
|
424
|
+
.filter(connection => !target || connection === target)
|
|
425
|
+
.forEach(connection => {
|
|
426
|
+
connection.notifyState = { selected: null, groups: [], unseen: new WeakMap() };
|
|
427
|
+
deliver(connection, {
|
|
428
|
+
command: 'OK',
|
|
429
|
+
attributes: [
|
|
430
|
+
{ type: 'SECTION', section: [{ type: 'ATOM', value: 'NOTIFICATIONOVERFLOW' }] },
|
|
431
|
+
{ type: 'TEXT', value: 'Too many notifications, NOTIFY is turned off' }
|
|
432
|
+
]
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Parses the fetch attributes of MessageNew (RFC 5465 section 8: "(" fetch-att *(SP fetch-att) ")")
|
|
439
|
+
*
|
|
440
|
+
* @param {Object} connection IMAP connection
|
|
441
|
+
* @param {Array} list Parsed list
|
|
442
|
+
* @return {Array} fetch items
|
|
443
|
+
*/
|
|
444
|
+
const parseFetchAtts = (connection, list) => {
|
|
445
|
+
if (!list.length) {
|
|
446
|
+
throw badError('MessageNew expects a list of fetch attributes');
|
|
447
|
+
}
|
|
448
|
+
return list.map(item => {
|
|
449
|
+
const key = isAtom(item) && item.value.toUpperCase();
|
|
450
|
+
// macros like ALL are not fetch-att values
|
|
451
|
+
if (!key || !(server.fetchHandlers[key] || builtinFetchHandlers[key])) {
|
|
452
|
+
throw badError('Invalid fetch attribute for MessageNew');
|
|
453
|
+
}
|
|
454
|
+
if ((item.section || item.partial) && SECTION_ITEMS.indexOf(key) >= 0) {
|
|
455
|
+
// check the section and the partial range against a message
|
|
456
|
+
const params = [Object.assign({}, item)];
|
|
457
|
+
try {
|
|
458
|
+
fetchResponse(connection, { raw: 'Subject: test\r\n\r\nTest\r\n', flags: [], uid: 1 }, params);
|
|
459
|
+
} catch (err) {
|
|
460
|
+
throw badError(err.message);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
return item;
|
|
464
|
+
});
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Parses the events of an event group (RFC 5465 section 8: events = ( "(" event *(SP event) ")" ) / "NONE")
|
|
469
|
+
*
|
|
470
|
+
* @param {Object} connection IMAP connection
|
|
471
|
+
* @param {Object|Array} value Parsed events
|
|
472
|
+
* @param {Boolean} selected true for SELECTED and SELECTED-DELAYED
|
|
473
|
+
* @param {Array} unknown Collects the unknown event names
|
|
474
|
+
* @return {Object} `{ events, fetch }`
|
|
475
|
+
*/
|
|
476
|
+
const parseEvents = (connection, value, selected, unknown) => {
|
|
477
|
+
const events = new Set();
|
|
478
|
+
let fetch = null;
|
|
479
|
+
if (isAtom(value, 'NONE')) {
|
|
480
|
+
return { events, fetch };
|
|
481
|
+
}
|
|
482
|
+
if (!Array.isArray(value) || !value.length) {
|
|
483
|
+
throw badError('Expecting a list of events or NONE');
|
|
484
|
+
}
|
|
485
|
+
for (let i = 0; i < value.length; i++) {
|
|
486
|
+
const item = value[i];
|
|
487
|
+
if (!isAtom(item) || item.section || item.partial) {
|
|
488
|
+
throw badError('Invalid event');
|
|
489
|
+
}
|
|
490
|
+
const name = EVENT_NAMES.get(item.value.toUpperCase());
|
|
491
|
+
if (Array.isArray(value[i + 1]) && name === 'MessageNew') {
|
|
492
|
+
// the fetch-att list is only allowed for the selected mailbox (RFC 5465 section 8)
|
|
493
|
+
if (!selected) {
|
|
494
|
+
throw badError('Fetch attributes for MessageNew are only allowed with SELECTED or SELECTED-DELAYED');
|
|
495
|
+
}
|
|
496
|
+
fetch = parseFetchAtts(connection, value[++i]);
|
|
497
|
+
}
|
|
498
|
+
if (!name) {
|
|
499
|
+
// event-ext: an event this server does not know, NO [BADEVENT] once the syntax is checked
|
|
500
|
+
unknown.push(item.value);
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
if (selected && MESSAGE_EVENTS.indexOf(name) < 0) {
|
|
504
|
+
// RFC 5465 section 6.1
|
|
505
|
+
throw badError(name + ' can not be used with SELECTED or SELECTED-DELAYED');
|
|
506
|
+
}
|
|
507
|
+
events.add(name);
|
|
508
|
+
}
|
|
509
|
+
// RFC 5465 section 5
|
|
510
|
+
if (events.has('MessageNew') !== events.has('MessageExpunge')) {
|
|
511
|
+
throw badError('MessageNew and MessageExpunge must be used together');
|
|
512
|
+
}
|
|
513
|
+
if ((events.has('FlagChange') || events.has('AnnotationChange')) && !events.has('MessageNew')) {
|
|
514
|
+
throw badError('FlagChange and AnnotationChange require MessageNew and MessageExpunge');
|
|
515
|
+
}
|
|
516
|
+
return { events, fetch };
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
// one-or-more-mailbox = mailbox / many-mailboxes, the names are converted to storage names
|
|
520
|
+
const parseMailboxes = (connection, value) => {
|
|
521
|
+
const list = Array.isArray(value) ? value : [value];
|
|
522
|
+
if (!list.length || !list.every(isAstring)) {
|
|
523
|
+
throw badError('Expecting a mailbox name or a list of mailbox names');
|
|
524
|
+
}
|
|
525
|
+
return list.map(item => normalizePath(connection.importMailboxName(item.value)));
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Parses the arguments of NOTIFY (RFC 5465 section 8)
|
|
530
|
+
*
|
|
531
|
+
* @return {Object} `{ state, status, unknown }`
|
|
532
|
+
*/
|
|
533
|
+
const parseNotify = (connection, args) => {
|
|
534
|
+
// `unseen` holds the UNSEEN count each mailbox had in the last STATUS response the session got
|
|
535
|
+
const state = { selected: null, groups: [], unseen: new WeakMap() };
|
|
536
|
+
const unknown = [];
|
|
537
|
+
if (isAtom(args[0], 'NONE')) {
|
|
538
|
+
if (args.length !== 1) {
|
|
539
|
+
throw badError('NOTIFY NONE does not take any arguments');
|
|
540
|
+
}
|
|
541
|
+
return { state, status: false, unknown };
|
|
542
|
+
}
|
|
543
|
+
if (!isAtom(args[0], 'SET')) {
|
|
544
|
+
throw badError('NOTIFY expects SET or NONE');
|
|
545
|
+
}
|
|
546
|
+
const status = isAtom(args[1], 'STATUS');
|
|
547
|
+
const groups = args.slice(status ? 2 : 1);
|
|
548
|
+
if (!groups.length) {
|
|
549
|
+
throw badError('NOTIFY SET expects event groups');
|
|
550
|
+
}
|
|
551
|
+
groups.forEach(group => {
|
|
552
|
+
if (!Array.isArray(group) || !isAtom(group[0])) {
|
|
553
|
+
throw badError('Invalid event group');
|
|
554
|
+
}
|
|
555
|
+
const filter = group[0].value.toUpperCase();
|
|
556
|
+
if (SELECTED_FILTERS.indexOf(filter) >= 0) {
|
|
557
|
+
if (group.length !== 2) {
|
|
558
|
+
throw badError('Invalid event group');
|
|
559
|
+
}
|
|
560
|
+
if (state.selected) {
|
|
561
|
+
// RFC 5465 section 6.1
|
|
562
|
+
throw badError('Only one of SELECTED and SELECTED-DELAYED can be used');
|
|
563
|
+
}
|
|
564
|
+
state.selected = Object.assign({ delayed: filter === 'SELECTED-DELAYED' }, parseEvents(connection, group[1], true, unknown));
|
|
565
|
+
} else if (OTHER_FILTERS.indexOf(filter) >= 0) {
|
|
566
|
+
const withNames = filter === 'SUBTREE' || filter === 'MAILBOXES';
|
|
567
|
+
if (group.length !== (withNames ? 3 : 2)) {
|
|
568
|
+
throw badError('Invalid event group');
|
|
569
|
+
}
|
|
570
|
+
const paths = withNames ? parseMailboxes(connection, group[1]) : [];
|
|
571
|
+
const { events } = parseEvents(connection, group[group.length - 1], false, unknown);
|
|
572
|
+
// the names below a SUBTREE root start with these
|
|
573
|
+
const prefixes = filter === 'SUBTREE' ? paths.map(root => root + server.getSeparator(root)) : [];
|
|
574
|
+
state.groups.push({ filter, paths, prefixes, events });
|
|
575
|
+
} else {
|
|
576
|
+
throw badError('Unknown mailbox filter ' + group[0].value);
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
return { state, status, unknown };
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
// RFC 5465 section 3.1: the initial STATUS responses and the \NoAccess LIST responses of NOTIFY SET
|
|
583
|
+
const sendInitialResponses = (connection, status, parsed, data) => {
|
|
584
|
+
const paths = Object.keys(server.folderCache).sort((a, b) => (a === 'INBOX' ? -1 : b === 'INBOX' ? 1 : a.localeCompare(b)));
|
|
585
|
+
paths.forEach(path => {
|
|
586
|
+
const mailbox = server.folderCache[path];
|
|
587
|
+
if (mailbox === connection.selectedMailbox || mailbox.flags.some(flag => /^\\(Noselect|NonExistent)$/i.test(flag))) {
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
const events = getEvents(connection, path, mailbox);
|
|
591
|
+
if (!events.size || !canList(connection, mailbox)) {
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
if (!canRead(connection, mailbox)) {
|
|
595
|
+
connection.send(listResponse(connection, path, { noAccess: true }), 'NOTIFY LIST', parsed, data);
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
if (!status || !events.has('MessageNew')) {
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
const items = ['MESSAGES', 'UIDNEXT', 'UIDVALIDITY'];
|
|
602
|
+
if (events.has('FlagChange')) {
|
|
603
|
+
items.push('UNSEEN');
|
|
604
|
+
connection.notifyState.unseen.set(mailbox, server.getStatus(mailbox).unseen || 0);
|
|
605
|
+
if (server.condstore) {
|
|
606
|
+
items.push('HIGHESTMODSEQ');
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
connection.send(statusResponse(connection, path, mailbox, items), 'NOTIFY STATUS', parsed, data);
|
|
610
|
+
});
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
server.setCommandHandler(
|
|
614
|
+
'NOTIFY',
|
|
615
|
+
(connection, parsed, data, callback) => {
|
|
616
|
+
let result;
|
|
617
|
+
try {
|
|
618
|
+
result = parseNotify(connection, parsed.attributes || []);
|
|
619
|
+
} catch (err) {
|
|
620
|
+
connection.sendStatus(parsed, data, 'BAD', err.imapResponse === 'BAD' ? err.message : 'Invalid NOTIFY arguments');
|
|
621
|
+
return callback();
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const supported = getSupportedEvents();
|
|
625
|
+
const used = [result.state.selected].concat(result.state.groups).filter(group => group);
|
|
626
|
+
const unsupported = result.unknown.concat(...used.map(group => [...group.events].filter(event => supported.indexOf(event) < 0)));
|
|
627
|
+
if (unsupported.length) {
|
|
628
|
+
// RFC 5465 section 3.1: NO with BADEVENT, which MUST list all supported events
|
|
629
|
+
connection.send(
|
|
630
|
+
{
|
|
631
|
+
tag: parsed.tag,
|
|
632
|
+
command: 'NO',
|
|
633
|
+
attributes: [
|
|
634
|
+
{ type: 'SECTION', section: [{ type: 'ATOM', value: 'BADEVENT' }, supported.map(event => ({ type: 'ATOM', value: event }))] },
|
|
635
|
+
{ type: 'TEXT', value: 'Unsupported NOTIFY events' }
|
|
636
|
+
]
|
|
637
|
+
},
|
|
638
|
+
'NOTIFY FAILED',
|
|
639
|
+
parsed,
|
|
640
|
+
data
|
|
641
|
+
);
|
|
642
|
+
return callback();
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
connection.notifyState = result.state;
|
|
646
|
+
sendInitialResponses(connection, result.status, parsed, data);
|
|
647
|
+
|
|
648
|
+
// the tagged response sends the changes of the selected mailbox, NOTIFY SET implies NOOP (section 3.1)
|
|
649
|
+
connection.sendStatus(parsed, data, 'OK', 'NOTIFY completed');
|
|
650
|
+
return callback();
|
|
651
|
+
},
|
|
652
|
+
{ states: states.AUTHENTICATED }
|
|
653
|
+
);
|
|
654
|
+
};
|