imapkit 4.3.1 → 5.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/README.md +187 -67
- package/bin/help.txt +20 -5
- package/bin/imapkit.js +64 -24
- package/dist/cjs/commands/append.js +1 -1
- package/dist/cjs/commands/create.js +2 -2
- package/dist/cjs/commands/delete.js +2 -14
- package/dist/cjs/commands/handlers/fetch.js +2 -1
- package/dist/cjs/commands/handlers/flags.d.ts +3 -3
- package/dist/cjs/commands/handlers/flags.js +3 -3
- package/dist/cjs/commands/handlers/store.js +7 -7
- package/dist/cjs/commands/list.js +17 -3
- package/dist/cjs/commands/rename.js +6 -46
- package/dist/cjs/commands/select.js +1 -0
- package/dist/cjs/commands/subscribe.js +7 -8
- package/dist/cjs/commands/unsubscribe.js +3 -9
- package/dist/cjs/control.d.ts +425 -0
- package/dist/cjs/control.js +637 -0
- package/dist/cjs/dates.d.ts +9 -1
- package/dist/cjs/dates.js +20 -0
- package/dist/cjs/envelope.d.ts +1 -1
- package/dist/cjs/envelope.js +26 -1
- package/dist/cjs/index.d.ts +9 -1
- package/dist/cjs/index.js +9 -2
- package/dist/cjs/load-plugins.d.ts +3 -1
- package/dist/cjs/load-plugins.js +28 -4
- package/dist/cjs/plugins/acl.js +109 -42
- package/dist/cjs/plugins/binary.js +2 -1
- package/dist/cjs/plugins/condstore.js +7 -0
- package/dist/cjs/plugins/context-search.js +2 -2
- package/dist/cjs/plugins/create-special-use.d.ts +0 -5
- package/dist/cjs/plugins/create-special-use.js +11 -1
- package/dist/cjs/plugins/imap4rev2.js +5 -0
- package/dist/cjs/plugins/index.js +1 -3
- package/dist/cjs/plugins/metadata-server.d.ts +6 -2
- package/dist/cjs/plugins/metadata-server.js +3 -1
- package/dist/cjs/plugins/metadata.d.ts +5 -1
- package/dist/cjs/plugins/metadata.js +143 -57
- package/dist/cjs/plugins/objectid.js +10 -0
- package/dist/cjs/plugins/quota.js +50 -2
- package/dist/cjs/plugins/savedate.js +1 -1
- package/dist/cjs/plugins/special-use.d.ts +0 -8
- package/dist/cjs/plugins/special-use.js +23 -0
- package/dist/cjs/quirks.d.ts +28 -0
- package/dist/cjs/quirks.js +125 -0
- package/dist/cjs/random.d.ts +10 -0
- package/dist/cjs/random.js +22 -0
- package/dist/cjs/rest.d.ts +54 -0
- package/dist/cjs/rest.js +567 -0
- package/dist/cjs/script.d.ts +33 -4
- package/dist/cjs/script.js +64 -4
- package/dist/cjs/server.d.ts +199 -9
- package/dist/cjs/server.js +603 -227
- package/dist/cjs/smtp-listener.d.ts +3 -2
- package/dist/cjs/smtp-listener.js +10 -6
- package/dist/cjs/storage-schema.d.ts +139 -0
- package/dist/cjs/storage-schema.js +222 -0
- package/dist/cjs/store-operations.d.ts +171 -0
- package/dist/cjs/store-operations.js +425 -0
- package/dist/cjs/types.d.ts +26 -1
- package/dist/esm/commands/append.js +1 -1
- package/dist/esm/commands/create.js +2 -2
- package/dist/esm/commands/delete.js +2 -14
- package/dist/esm/commands/handlers/fetch.js +2 -1
- package/dist/esm/commands/handlers/flags.d.ts +3 -3
- package/dist/esm/commands/handlers/flags.js +3 -3
- package/dist/esm/commands/handlers/store.js +7 -7
- package/dist/esm/commands/list.js +17 -3
- package/dist/esm/commands/rename.js +6 -46
- package/dist/esm/commands/select.js +1 -0
- package/dist/esm/commands/subscribe.js +7 -8
- package/dist/esm/commands/unsubscribe.js +3 -9
- package/dist/esm/control.d.ts +425 -0
- package/dist/esm/control.js +630 -0
- package/dist/esm/dates.d.ts +9 -1
- package/dist/esm/dates.js +20 -1
- package/dist/esm/envelope.d.ts +1 -1
- package/dist/esm/envelope.js +26 -1
- package/dist/esm/index.d.ts +9 -1
- package/dist/esm/index.js +5 -2
- package/dist/esm/load-plugins.d.ts +3 -1
- package/dist/esm/load-plugins.js +28 -4
- package/dist/esm/plugins/acl.js +109 -42
- package/dist/esm/plugins/binary.js +2 -1
- package/dist/esm/plugins/condstore.js +7 -0
- package/dist/esm/plugins/context-search.js +2 -2
- package/dist/esm/plugins/create-special-use.d.ts +0 -5
- package/dist/esm/plugins/create-special-use.js +8 -1
- package/dist/esm/plugins/imap4rev2.js +5 -0
- package/dist/esm/plugins/index.js +1 -3
- package/dist/esm/plugins/metadata-server.d.ts +6 -2
- package/dist/esm/plugins/metadata-server.js +3 -1
- package/dist/esm/plugins/metadata.d.ts +5 -1
- package/dist/esm/plugins/metadata.js +143 -57
- package/dist/esm/plugins/objectid.js +10 -0
- package/dist/esm/plugins/quota.js +50 -2
- package/dist/esm/plugins/savedate.js +1 -1
- package/dist/esm/plugins/special-use.d.ts +0 -8
- package/dist/esm/plugins/special-use.js +23 -0
- package/dist/esm/quirks.d.ts +28 -0
- package/dist/esm/quirks.js +121 -0
- package/dist/esm/random.d.ts +10 -0
- package/dist/esm/random.js +19 -0
- package/dist/esm/rest.d.ts +54 -0
- package/dist/esm/rest.js +559 -0
- package/dist/esm/script.d.ts +33 -4
- package/dist/esm/script.js +63 -4
- package/dist/esm/server.d.ts +199 -9
- package/dist/esm/server.js +606 -230
- package/dist/esm/smtp-listener.d.ts +3 -2
- package/dist/esm/smtp-listener.js +10 -6
- package/dist/esm/storage-schema.d.ts +139 -0
- package/dist/esm/storage-schema.js +218 -0
- package/dist/esm/store-operations.d.ts +171 -0
- package/dist/esm/store-operations.js +410 -0
- package/dist/esm/types.d.ts +26 -1
- package/dist/plugin-help.json +3 -16
- package/package.json +11 -3
- package/dist/cjs/plugins/xtoybird.d.ts +0 -2
- package/dist/cjs/plugins/xtoybird.js +0 -236
- package/dist/esm/plugins/xtoybird.d.ts +0 -2
- package/dist/esm/plugins/xtoybird.js +0 -218
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Changes of the message store that IMAP commands and the control API share: expunge, flag changes, and the
|
|
4
|
+
* mailbox operations of DELETE, RENAME, SUBSCRIBE and UNSUBSCRIBE. Each one tells the sessions about the change
|
|
5
|
+
* the way another session's command would (EXISTS, EXPUNGE, unsolicited FETCH, BYE, `mailbox` events). The
|
|
6
|
+
* session that caused a change is `server.activeConnection` (null for the control API), the commands keep only
|
|
7
|
+
* what is specific to a session: argument checks, the selected mailbox, read-only state and the responses.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.ImapKitError = exports.DEFAULT_SESSION_TIMEOUT = void 0;
|
|
11
|
+
exports.storeError = storeError;
|
|
12
|
+
exports.expungeMessages = expungeMessages;
|
|
13
|
+
exports.notifyFlagChanges = notifyFlagChanges;
|
|
14
|
+
exports.checkFlags = checkFlags;
|
|
15
|
+
exports.changeFlags = changeFlags;
|
|
16
|
+
exports.createMailbox = createMailbox;
|
|
17
|
+
exports.deleteMailbox = deleteMailbox;
|
|
18
|
+
exports.renameMailbox = renameMailbox;
|
|
19
|
+
exports.resetUidValidity = resetUidValidity;
|
|
20
|
+
exports.subscribeMailbox = subscribeMailbox;
|
|
21
|
+
exports.unsubscribeMailbox = unsubscribeMailbox;
|
|
22
|
+
const flags_js_1 = require("./commands/handlers/flags.js");
|
|
23
|
+
const random_js_1 = require("./random.js");
|
|
24
|
+
const numbers_js_1 = require("./numbers.js");
|
|
25
|
+
/**
|
|
26
|
+
* the deprecated XOAUTH2 session timeout of the default user, and of a user the control API adds without one. It is
|
|
27
|
+
* stored and listed but never used, access tokens do not expire
|
|
28
|
+
*/
|
|
29
|
+
const DEFAULT_SESSION_TIMEOUT = 3600 * 1000;
|
|
30
|
+
exports.DEFAULT_SESSION_TIMEOUT = DEFAULT_SESSION_TIMEOUT;
|
|
31
|
+
/** An error of a failed store operation or control API call, `code` is a RFC 5530 response code or INVALID */
|
|
32
|
+
class ImapKitError extends Error {
|
|
33
|
+
constructor(message, code) {
|
|
34
|
+
super(message);
|
|
35
|
+
this.name = 'ImapKitError';
|
|
36
|
+
this.code = code;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
exports.ImapKitError = ImapKitError;
|
|
40
|
+
/**
|
|
41
|
+
* Creates an error for a failed operation, with a RFC 5530 response code
|
|
42
|
+
*
|
|
43
|
+
* @param {String} message Error message
|
|
44
|
+
* @param {String} code Response code, e.g. "NONEXISTENT"
|
|
45
|
+
* @return {Error} Error object
|
|
46
|
+
*/
|
|
47
|
+
function storeError(message, code) {
|
|
48
|
+
return new ImapKitError(message, code);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Removes messages from a mailbox and tells the sessions that have it selected with EXPUNGE responses
|
|
52
|
+
* (VANISHED with QRESYNC) and a new EXISTS. The `expunge` event goes out before the notifications
|
|
53
|
+
*
|
|
54
|
+
* @param {Object} server IMAP server
|
|
55
|
+
* @param {Object} mailbox Mailbox of the messages
|
|
56
|
+
* @param {Array|Function} messagesOrFilterFunc Messages to remove, or a function that returns true for them
|
|
57
|
+
* @param {Object} [options] `{ origin, skip, skipExists, highestFirst }`
|
|
58
|
+
* @return {Array} the removed messages
|
|
59
|
+
*/
|
|
60
|
+
function expungeMessages(server, mailbox, messagesOrFilterFunc, options = {}) {
|
|
61
|
+
let filterFunc;
|
|
62
|
+
if (Array.isArray(messagesOrFilterFunc)) {
|
|
63
|
+
const messageSet = new Set(messagesOrFilterFunc);
|
|
64
|
+
filterFunc = (message) => messageSet.has(message);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
filterFunc = messagesOrFilterFunc;
|
|
68
|
+
}
|
|
69
|
+
// sequence numbers of the removed messages, each one as it is after the earlier EXPUNGE responses. From the
|
|
70
|
+
// highest message down, the earlier responses do not change the sequence numbers of the later ones
|
|
71
|
+
const expunged = [];
|
|
72
|
+
const kept = [];
|
|
73
|
+
mailbox.messages.forEach((message, i) => {
|
|
74
|
+
if (filterFunc(message)) {
|
|
75
|
+
message.ghost = true;
|
|
76
|
+
expunged.push({ seq: options.highestFirst ? i + 1 : kept.length + 1, message });
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
kept.push(message);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
if (!expunged.length) {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
// old copy is required for those sessions that run FETCH before
|
|
86
|
+
// displaying the EXPUNGE notice
|
|
87
|
+
const mailboxCopy = mailbox.messages.slice();
|
|
88
|
+
// update the list in place, other code might hold a reference to it
|
|
89
|
+
kept.forEach((message, i) => {
|
|
90
|
+
mailbox.messages[i] = message;
|
|
91
|
+
});
|
|
92
|
+
mailbox.messages.length = kept.length;
|
|
93
|
+
const messages = expunged.map(entry => entry.message);
|
|
94
|
+
const origin = options.origin !== undefined ? options.origin : server.activeConnection;
|
|
95
|
+
// lets plugins track the removal (e.g. mod-sequences of CONDSTORE and QRESYNC) before any notification
|
|
96
|
+
server.emit('expunge', mailbox, messages, origin);
|
|
97
|
+
(options.highestFirst ? expunged.slice().reverse() : expunged).forEach(entry => {
|
|
98
|
+
server.notify({
|
|
99
|
+
tag: '*',
|
|
100
|
+
attributes: [
|
|
101
|
+
entry.seq,
|
|
102
|
+
{
|
|
103
|
+
type: 'ATOM',
|
|
104
|
+
value: 'EXPUNGE'
|
|
105
|
+
}
|
|
106
|
+
],
|
|
107
|
+
// the removed message, for plugins that report it differently (e.g. VANISHED of QRESYNC)
|
|
108
|
+
message: entry.message
|
|
109
|
+
}, mailbox, options.skip);
|
|
110
|
+
});
|
|
111
|
+
server.notify({
|
|
112
|
+
tag: '*',
|
|
113
|
+
attributes: [
|
|
114
|
+
mailbox.messages.length,
|
|
115
|
+
{
|
|
116
|
+
type: 'ATOM',
|
|
117
|
+
value: 'EXISTS'
|
|
118
|
+
}
|
|
119
|
+
],
|
|
120
|
+
// distribute the old mailbox data with the notification
|
|
121
|
+
mailboxCopy: mailboxCopy
|
|
122
|
+
}, mailbox, options.skip || options.skipExists);
|
|
123
|
+
return messages;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Tells the sessions that have a mailbox selected about changed flags, they get an untagged FETCH with the
|
|
127
|
+
* new flags (RFC 3501 section 5.2)
|
|
128
|
+
*
|
|
129
|
+
* @param {Object} server IMAP server
|
|
130
|
+
* @param {Object} mailbox Mailbox of the messages
|
|
131
|
+
* @param {Array} messages Messages with changed flags
|
|
132
|
+
* @param {Object} [ignoreConnection] A session that is not told, e.g. the one whose STORE changed the flags
|
|
133
|
+
*/
|
|
134
|
+
function notifyFlagChanges(server, mailbox, messages, ignoreConnection) {
|
|
135
|
+
if (messages.length) {
|
|
136
|
+
server.notify({ tag: '*', flagUpdate: messages }, mailbox, ignoreConnection);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Checks flags the way STORE does: system flags the server knows (never \Recent), valid keywords, and only
|
|
141
|
+
* permanent flags in a mailbox that does not allow new keywords
|
|
142
|
+
*
|
|
143
|
+
* @param {Object} server IMAP server
|
|
144
|
+
* @param {Object} mailbox Mailbox the flags are for
|
|
145
|
+
* @param {Array} flags Flags to check
|
|
146
|
+
* @param {Boolean} stored true if the flags are stored, so they must be permanent flags of the mailbox
|
|
147
|
+
* @return {Array} the normalized flags without duplicates
|
|
148
|
+
* @throws {Error} INVALID for a flag that can not be used
|
|
149
|
+
*/
|
|
150
|
+
function checkFlags(server, mailbox, flags, stored) {
|
|
151
|
+
if (!Array.isArray(flags)) {
|
|
152
|
+
throw storeError('Flags must be an array', 'INVALID');
|
|
153
|
+
}
|
|
154
|
+
const list = [];
|
|
155
|
+
flags.forEach((value) => {
|
|
156
|
+
if (typeof value !== 'string') {
|
|
157
|
+
throw storeError('Flags must be strings', 'INVALID');
|
|
158
|
+
}
|
|
159
|
+
const flag = (0, flags_js_1.normalizeSystemFlag)(value);
|
|
160
|
+
try {
|
|
161
|
+
(0, flags_js_1.checkSystemFlags)(server, flag);
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
throw storeError(err.message, 'INVALID');
|
|
165
|
+
}
|
|
166
|
+
if (stored && !server.isPermanentFlag(mailbox, flag)) {
|
|
167
|
+
throw storeError('Flag ' + flag + ' is not a permanent flag of ' + mailbox.path, 'INVALID');
|
|
168
|
+
}
|
|
169
|
+
if (list.indexOf(flag) < 0) {
|
|
170
|
+
list.push(flag);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
return list;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Changes the flags of messages outside of a STORE command. The flags follow the rules of STORE: system flags
|
|
177
|
+
* the server knows (never \Recent), valid keywords, and only permanent flags in a mailbox that does not allow
|
|
178
|
+
* new keywords. Plugins learn about the changed messages from the `flags` event `(mailbox, messages, origin)`
|
|
179
|
+
* before the sessions are told (CONDSTORE gives them new mod-sequences there)
|
|
180
|
+
*
|
|
181
|
+
* @param {Object} server IMAP server
|
|
182
|
+
* @param {Object} mailbox Mailbox of the messages
|
|
183
|
+
* @param {Array} messages Messages to change
|
|
184
|
+
* @param {Array} flags Flags to set, add or remove
|
|
185
|
+
* @param {String} mode "set", "add" or "remove"
|
|
186
|
+
* @return {Array} the messages whose flags changed
|
|
187
|
+
*/
|
|
188
|
+
function changeFlags(server, mailbox, messages, flags, mode) {
|
|
189
|
+
if (['set', 'add', 'remove'].indexOf(mode) < 0) {
|
|
190
|
+
throw storeError('Invalid flag mode ' + JSON.stringify(mode) + ', expected set, add or remove', 'INVALID');
|
|
191
|
+
}
|
|
192
|
+
const list = checkFlags(server, mailbox, flags, mode !== 'remove');
|
|
193
|
+
const changed = [];
|
|
194
|
+
messages.forEach(message => {
|
|
195
|
+
const before = message.flags.join(' ');
|
|
196
|
+
if (mode === 'set') {
|
|
197
|
+
message.flags = list.slice();
|
|
198
|
+
}
|
|
199
|
+
else if (mode === 'add') {
|
|
200
|
+
list.forEach(flag => server.ensureFlag(message.flags, flag));
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
list.forEach(flag => server.removeFlag(message.flags, flag));
|
|
204
|
+
}
|
|
205
|
+
server.rememberFlags(mailbox, message.flags);
|
|
206
|
+
if (message.flags.join(' ') !== before) {
|
|
207
|
+
changed.push(message);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
if (changed.length) {
|
|
211
|
+
server.emit('flags', mailbox, changed, server.activeConnection);
|
|
212
|
+
notifyFlagChanges(server, mailbox, changed, null);
|
|
213
|
+
}
|
|
214
|
+
return changed;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Creates a mailbox (CREATE), with any missing superior hierarchy levels
|
|
218
|
+
*
|
|
219
|
+
* @param {Object} server IMAP server
|
|
220
|
+
* @param {String} path Storage name of the mailbox
|
|
221
|
+
* @return {Object} the created mailbox
|
|
222
|
+
* @throws {Error} with a RFC 5530 `code` (ALREADYEXISTS, CANNOT ...)
|
|
223
|
+
*/
|
|
224
|
+
function createMailbox(server, path) {
|
|
225
|
+
const existing = new Set(Object.keys(server.folderCache));
|
|
226
|
+
const mailbox = server.createMailbox(path);
|
|
227
|
+
// the superior levels that did not exist are new mailboxes as well, plugins set them up like the target (ACL)
|
|
228
|
+
const created = Object.keys(server.folderCache)
|
|
229
|
+
.filter(name => !existing.has(name))
|
|
230
|
+
.sort((a, b) => a.length - b.length);
|
|
231
|
+
server.mailboxChanged('create', mailbox.path, { created });
|
|
232
|
+
return mailbox;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Deletes a mailbox (DELETE). Other sessions that have it selected are disconnected with an untagged BYE
|
|
236
|
+
* (RFC 2180 section 3.3, RFC 2683 section 3.1.2), as they can not be told about the deletion in any other way
|
|
237
|
+
*
|
|
238
|
+
* @param {Object} server IMAP server
|
|
239
|
+
* @param {String} path Storage name of the mailbox
|
|
240
|
+
* @throws {Error} with a response `code` (NONEXISTENT, CANNOT, HASCHILDREN ...)
|
|
241
|
+
*/
|
|
242
|
+
function deleteMailbox(server, path) {
|
|
243
|
+
const mailbox = server.getMailbox(path);
|
|
244
|
+
if (!mailbox) {
|
|
245
|
+
throw storeError('Mailbox does not exist', 'NONEXISTENT');
|
|
246
|
+
}
|
|
247
|
+
server.deleteMailbox(path);
|
|
248
|
+
server.mailboxChanged('delete', mailbox.path, { mailbox });
|
|
249
|
+
byeSelected(server, mailbox, 'Selected mailbox was deleted', 'MAILBOX DELETED');
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Disconnects the sessions that have a mailbox selected with an untagged BYE, except the one that made the change
|
|
253
|
+
*
|
|
254
|
+
* @param {Object} server IMAP server
|
|
255
|
+
* @param {Object} mailbox Mailbox
|
|
256
|
+
* @param {String} text Text of the BYE
|
|
257
|
+
* @param {String} description Description for output handlers
|
|
258
|
+
*/
|
|
259
|
+
function byeSelected(server, mailbox, text, description) {
|
|
260
|
+
const origin = server.activeConnection;
|
|
261
|
+
server.connections.forEach(connection => {
|
|
262
|
+
if (connection !== origin && connection.selectedMailbox === mailbox) {
|
|
263
|
+
connection.bye(text, description);
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Renames a mailbox (RENAME). Renaming INBOX moves its messages to the new mailbox and leaves INBOX
|
|
269
|
+
* empty (RFC 3501 section 6.3.5). Everything that can be checked is checked up front, so that a failed
|
|
270
|
+
* rename never loses the source
|
|
271
|
+
*
|
|
272
|
+
* @param {Object} server IMAP server
|
|
273
|
+
* @param {String} source Storage name of the mailbox
|
|
274
|
+
* @param {String} destination New storage name
|
|
275
|
+
* @return {Object} `{ path, mailbox }`: the new name, and the source mailbox object, which is INBOX itself for
|
|
276
|
+
* a rename of INBOX
|
|
277
|
+
* @throws {Error} with a RFC 5530 `code` (NONEXISTENT, ALREADYEXISTS, CANNOT ...)
|
|
278
|
+
*/
|
|
279
|
+
function renameMailbox(server, source, destination) {
|
|
280
|
+
const mailbox = server.getMailbox(source);
|
|
281
|
+
const target = server.getMailbox(destination);
|
|
282
|
+
if (!mailbox || mailbox.flags.indexOf('\\Noselect') >= 0) {
|
|
283
|
+
throw storeError('Mailbox does not exist', 'NONEXISTENT');
|
|
284
|
+
}
|
|
285
|
+
if (target && target.flags.indexOf('\\Noselect') < 0) {
|
|
286
|
+
throw storeError('Mailbox already exists', 'ALREADYEXISTS');
|
|
287
|
+
}
|
|
288
|
+
const separator = server.getSeparator(mailbox);
|
|
289
|
+
if (destination.substr(0, mailbox.path.length + separator.length) === mailbox.path + separator) {
|
|
290
|
+
throw storeError('Can not move a mailbox into itself', 'CANNOT');
|
|
291
|
+
}
|
|
292
|
+
const oldPath = mailbox.path;
|
|
293
|
+
let path;
|
|
294
|
+
if (source.toUpperCase() === 'INBOX') {
|
|
295
|
+
const renamed = server.createMailbox(destination);
|
|
296
|
+
path = renamed.path;
|
|
297
|
+
mailbox.messages.forEach((message) => {
|
|
298
|
+
server.copyMessage(renamed, message);
|
|
299
|
+
});
|
|
300
|
+
expungeMessages(server, mailbox, () => true);
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
server.deleteMailbox(source, true);
|
|
304
|
+
try {
|
|
305
|
+
server.createMailbox(destination, mailbox);
|
|
306
|
+
}
|
|
307
|
+
catch (E) {
|
|
308
|
+
// put the source mailbox back where it was
|
|
309
|
+
server.createMailbox(source, mailbox);
|
|
310
|
+
server.indexFolders();
|
|
311
|
+
throw E;
|
|
312
|
+
}
|
|
313
|
+
server.indexFolders();
|
|
314
|
+
path = mailbox.path;
|
|
315
|
+
}
|
|
316
|
+
server.mailboxChanged('rename', path, { oldPath });
|
|
317
|
+
return { path, mailbox };
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Gives a mailbox a new UIDVALIDITY, optionally with new UIDs. A UID never changes during a session (RFC 9051
|
|
321
|
+
* section 2.3.1.1), so the sessions that have the mailbox selected are disconnected with BYE, and the new value
|
|
322
|
+
* is greater than every earlier one, as UIDs that do not persist require. A client that keeps its cached UIDs
|
|
323
|
+
* without checking UIDVALIDITY gets other messages ("shuffle") or none ("offset") for them
|
|
324
|
+
*
|
|
325
|
+
* @param {Object} server IMAP server
|
|
326
|
+
* @param {Object} mailbox Mailbox, not \Noselect
|
|
327
|
+
* @param {Object} [options] `{ uidvalidity, uids, offset, seed }`
|
|
328
|
+
* @return {Object} `{ uidvalidity, uidnext, uids }`, `uids` maps every old UID to its new one
|
|
329
|
+
* @throws {Error} INVALID for a UIDVALIDITY that is not greater, or invalid options
|
|
330
|
+
*/
|
|
331
|
+
function resetUidValidity(server, mailbox, options = {}) {
|
|
332
|
+
const mode = options.uids || 'keep';
|
|
333
|
+
if (['keep', 'renumber', 'shuffle', 'offset'].indexOf(mode) < 0) {
|
|
334
|
+
throw storeError('Invalid UID mode ' + JSON.stringify(mode) + ', expected keep, renumber, shuffle or offset', 'INVALID');
|
|
335
|
+
}
|
|
336
|
+
const offset = options.offset === undefined ? 0 : options.offset;
|
|
337
|
+
if (!Number.isSafeInteger(offset) || offset < 0) {
|
|
338
|
+
throw storeError('Offset must be a non-negative integer', 'INVALID');
|
|
339
|
+
}
|
|
340
|
+
let uidvalidity = options.uidvalidity;
|
|
341
|
+
if (uidvalidity === undefined) {
|
|
342
|
+
uidvalidity = Math.max(mailbox.uidvalidity, server.uidvalidityCounter) + 1;
|
|
343
|
+
}
|
|
344
|
+
else if (!Number.isSafeInteger(uidvalidity) || uidvalidity <= mailbox.uidvalidity || uidvalidity > 0xffffffff) {
|
|
345
|
+
// nz-number of RFC 3501 section 9, a 32-bit value
|
|
346
|
+
throw storeError('UIDVALIDITY must be an integer above ' + mailbox.uidvalidity + ' and below 2^32', 'INVALID');
|
|
347
|
+
}
|
|
348
|
+
if (options.seed !== undefined && !Number.isSafeInteger(options.seed)) {
|
|
349
|
+
throw storeError('Seed must be an integer', 'INVALID');
|
|
350
|
+
}
|
|
351
|
+
byeSelected(server, mailbox, 'UIDVALIDITY of the selected mailbox changed', 'UIDVALIDITY CHANGED');
|
|
352
|
+
const messages = mailbox.messages;
|
|
353
|
+
const oldUids = messages.map(message => message.uid);
|
|
354
|
+
let newUids = oldUids;
|
|
355
|
+
if (mode === 'renumber' || mode === 'shuffle') {
|
|
356
|
+
newUids = messages.map((message, i) => i + 1);
|
|
357
|
+
if (mode === 'shuffle') {
|
|
358
|
+
const random = (0, random_js_1.seededRandom)(options.seed === undefined ? Math.floor(Math.random() * numbers_js_1.MAX_NUMBER) : options.seed);
|
|
359
|
+
// Fisher-Yates
|
|
360
|
+
for (let i = newUids.length - 1; i > 0; i--) {
|
|
361
|
+
const j = Math.floor(random() * (i + 1));
|
|
362
|
+
[newUids[i], newUids[j]] = [newUids[j], newUids[i]];
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
mailbox.uidnext = messages.length + 1;
|
|
366
|
+
}
|
|
367
|
+
else if (mode === 'offset') {
|
|
368
|
+
const shift = mailbox.uidnext - 1 + offset;
|
|
369
|
+
newUids = oldUids.map(uid => uid + shift);
|
|
370
|
+
mailbox.uidnext += shift;
|
|
371
|
+
}
|
|
372
|
+
messages.forEach((message, i) => {
|
|
373
|
+
message.uid = newUids[i];
|
|
374
|
+
});
|
|
375
|
+
if (mode === 'shuffle') {
|
|
376
|
+
// the other modes keep the UID order
|
|
377
|
+
messages.sort((a, b) => a.uid - b.uid);
|
|
378
|
+
}
|
|
379
|
+
mailbox.uidvalidity = uidvalidity;
|
|
380
|
+
server.uidvalidityCounter = Math.max(server.uidvalidityCounter, uidvalidity);
|
|
381
|
+
return {
|
|
382
|
+
uidvalidity,
|
|
383
|
+
uidnext: mailbox.uidnext,
|
|
384
|
+
uids: oldUids.map((uid, i) => ({ uid, newUid: newUids[i] }))
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Subscribes a mailbox (SUBSCRIBE), only an existing mailbox can be subscribed
|
|
389
|
+
*
|
|
390
|
+
* @param {Object} server IMAP server
|
|
391
|
+
* @param {String} path Storage name of the mailbox
|
|
392
|
+
* @return {Boolean} true if the subscription changed
|
|
393
|
+
* @throws {Error} NONEXISTENT for a name that is not a mailbox
|
|
394
|
+
*/
|
|
395
|
+
function subscribeMailbox(server, path) {
|
|
396
|
+
const mailbox = server.getMailbox(path);
|
|
397
|
+
if (!mailbox || mailbox.flags.indexOf('\\Noselect') >= 0) {
|
|
398
|
+
throw storeError('Mailbox does not exist', 'NONEXISTENT');
|
|
399
|
+
}
|
|
400
|
+
if (mailbox.subscribed) {
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
mailbox.subscribed = true;
|
|
404
|
+
server.mailboxChanged('subscribe', mailbox.path);
|
|
405
|
+
return true;
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Unsubscribes a name (UNSUBSCRIBE). The subscription list holds names, so a name stays removable after its
|
|
409
|
+
* mailbox is deleted (RFC 3501 section 6.3.6). Removing a name that is not subscribed is not an error
|
|
410
|
+
* (RFC 9051 section 6.3.8)
|
|
411
|
+
*
|
|
412
|
+
* @param {Object} server IMAP server
|
|
413
|
+
* @param {String} path Storage name
|
|
414
|
+
* @return {Boolean} true if the subscription changed
|
|
415
|
+
*/
|
|
416
|
+
function unsubscribeMailbox(server, path) {
|
|
417
|
+
if (path.toUpperCase() === 'INBOX') {
|
|
418
|
+
path = 'INBOX';
|
|
419
|
+
}
|
|
420
|
+
if (!server.subscriptions.delete(path)) {
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
server.mailboxChanged('unsubscribe', path);
|
|
424
|
+
return true;
|
|
425
|
+
}
|
package/dist/cjs/types.d.ts
CHANGED
|
@@ -72,6 +72,8 @@ export interface MailboxChangeEvent {
|
|
|
72
72
|
oldPath: string | null;
|
|
73
73
|
mailbox: Mailbox | null;
|
|
74
74
|
origin: IMAPConnection | null;
|
|
75
|
+
/** "create": every mailbox the change created, superior hierarchy levels first */
|
|
76
|
+
created?: string[] | undefined;
|
|
75
77
|
}
|
|
76
78
|
export type Callback = (err?: any, ...args: any[]) => void;
|
|
77
79
|
/** Handler of an IMAP command, it must send a tagged response and then call the callback */
|
|
@@ -210,6 +212,10 @@ export interface UserData {
|
|
|
210
212
|
password?: string | undefined;
|
|
211
213
|
xoauth2?: {
|
|
212
214
|
accessToken?: string | undefined;
|
|
215
|
+
/**
|
|
216
|
+
* @deprecated kept from hoodiecrow and ignored: access tokens never expire. Change the token with
|
|
217
|
+
* `control.updateUser()` to test a client against an expired one
|
|
218
|
+
*/
|
|
213
219
|
sessionTimeout?: number | undefined;
|
|
214
220
|
} | undefined;
|
|
215
221
|
[key: string]: any;
|
|
@@ -222,6 +228,18 @@ export interface IMAPServerOptions {
|
|
|
222
228
|
plugins?: (string | Plugin)[] | string | Plugin | undefined;
|
|
223
229
|
users?: Record<string, UserData> | undefined;
|
|
224
230
|
secureConnection?: boolean | undefined;
|
|
231
|
+
/** starts an SMTP server with `server.start()` that appends every received message to INBOX, needs the optional
|
|
232
|
+
* smtp-server package */
|
|
233
|
+
smtp?: {
|
|
234
|
+
port?: number | undefined;
|
|
235
|
+
host?: string | undefined;
|
|
236
|
+
} | undefined;
|
|
237
|
+
/** starts the REST API with `server.start()`, see src/rest.ts. A host other than a loopback address needs a token */
|
|
238
|
+
rest?: {
|
|
239
|
+
port?: number | undefined;
|
|
240
|
+
host?: string | undefined;
|
|
241
|
+
token?: string | undefined;
|
|
242
|
+
} | undefined;
|
|
225
243
|
credentials?: {
|
|
226
244
|
key: string | Buffer;
|
|
227
245
|
cert: string | Buffer;
|
|
@@ -232,6 +250,12 @@ export interface IMAPServerOptions {
|
|
|
232
250
|
maxLiteralSize?: number | undefined;
|
|
233
251
|
/** script rules that make the server misbehave on purpose, see src/script.ts and README "Scripted faults" */
|
|
234
252
|
script?: ScriptRule | ScriptRule[] | undefined;
|
|
253
|
+
/** the current time for the dates the server sets (INTERNALDATE, SAVEDATE): a Date, a timestamp or a function */
|
|
254
|
+
now?: Date | number | (() => Date | number) | undefined;
|
|
255
|
+
/** quirk presets that make the server behave like a known real server, see src/quirks.ts */
|
|
256
|
+
quirks?: string[] | string | undefined;
|
|
257
|
+
/** seed of the random numbers of script rules with `chance`, so that a run can be repeated */
|
|
258
|
+
scriptSeed?: number | undefined;
|
|
235
259
|
[key: string]: any;
|
|
236
260
|
}
|
|
237
261
|
/**
|
|
@@ -318,7 +342,8 @@ export type MessageHandler = (server: IMAPServer, message: Message, mailbox: Mai
|
|
|
318
342
|
export type MailboxHandler = (server: IMAPServer, mailbox: Mailbox) => void;
|
|
319
343
|
export type CopyHandler = (server: IMAPServer, source: Message, properties: Record<string, any>, mailbox: Mailbox) => void;
|
|
320
344
|
export type NotifyFilter = (connection: IMAPConnection, event: NotifyEvent) => boolean;
|
|
321
|
-
|
|
345
|
+
/** `server.appendChecks`, `connection` is null for the control API (`addMessage` with `checks`) */
|
|
346
|
+
export type AppendCheck = (connection: IMAPConnection | null, mailbox: Mailbox, messages: AppendMessage[], options: AppendCheckOptions) => CheckResult | false | null | undefined | void;
|
|
322
347
|
export type LiteralFilter = (connection: IMAPConnection, command: string, line: string, size: number) => Refusal | false | null | undefined;
|
|
323
348
|
export type UrlAccessCheck = (connection: IMAPConnection, mailbox: Mailbox, url: any) => {
|
|
324
349
|
text: string;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createMailbox } from '../store-operations.js';
|
|
1
2
|
export default function createCommand(connection, parsed, data, callback) {
|
|
2
3
|
if (!parsed.attributes || parsed.attributes.length !== 1 || !parsed.attributes[0] || ['STRING', 'LITERAL', 'ATOM'].indexOf(parsed.attributes[0].type) < 0) {
|
|
3
4
|
connection.send({
|
|
@@ -15,7 +16,7 @@ export default function createCommand(connection, parsed, data, callback) {
|
|
|
15
16
|
const path = parsed.attributes[0].value;
|
|
16
17
|
let mailbox;
|
|
17
18
|
try {
|
|
18
|
-
mailbox = connection.server
|
|
19
|
+
mailbox = createMailbox(connection.server, path);
|
|
19
20
|
}
|
|
20
21
|
catch (err) {
|
|
21
22
|
const E = err;
|
|
@@ -23,7 +24,6 @@ export default function createCommand(connection, parsed, data, callback) {
|
|
|
23
24
|
connection.sendStatus(parsed, data, 'NO', E.message, E.code, 'CREATE FAILED');
|
|
24
25
|
return callback();
|
|
25
26
|
}
|
|
26
|
-
connection.server.mailboxChanged('create', mailbox.path);
|
|
27
27
|
connection.send({
|
|
28
28
|
tag: parsed.tag,
|
|
29
29
|
command: 'OK',
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { deleteMailbox } from '../store-operations.js';
|
|
1
2
|
export default function deleteCommand(connection, parsed, data, callback) {
|
|
2
3
|
if (!parsed.attributes || parsed.attributes.length !== 1 || !parsed.attributes[0] || ['STRING', 'LITERAL', 'ATOM'].indexOf(parsed.attributes[0].type) < 0) {
|
|
3
4
|
connection.send({
|
|
@@ -13,27 +14,14 @@ export default function deleteCommand(connection, parsed, data, callback) {
|
|
|
13
14
|
return callback();
|
|
14
15
|
}
|
|
15
16
|
const path = parsed.attributes[0].value;
|
|
16
|
-
const mailbox = connection.server.getMailbox(path);
|
|
17
|
-
if (!mailbox) {
|
|
18
|
-
connection.sendStatus(parsed, data, 'NO', 'Mailbox does not exist', 'NONEXISTENT', 'DELETE FAILED');
|
|
19
|
-
return callback();
|
|
20
|
-
}
|
|
21
17
|
try {
|
|
22
|
-
connection.server
|
|
18
|
+
deleteMailbox(connection.server, path);
|
|
23
19
|
}
|
|
24
20
|
catch (err) {
|
|
25
21
|
const E = err;
|
|
26
22
|
connection.sendStatus(parsed, data, 'NO', E.message, E.code, 'DELETE FAILED');
|
|
27
23
|
return callback();
|
|
28
24
|
}
|
|
29
|
-
connection.server.mailboxChanged('delete', mailbox.path, { mailbox });
|
|
30
|
-
// RFC 2180 section 3.3: the other sessions that have the mailbox selected are disconnected with an untagged
|
|
31
|
-
// BYE (RFC 2683 section 3.1.2), as they can not be told about the deletion in any other way
|
|
32
|
-
connection.server.connections.forEach(other => {
|
|
33
|
-
if (other !== connection && other.selectedMailbox === mailbox) {
|
|
34
|
-
other.bye('Selected mailbox was deleted', 'MAILBOX DELETED');
|
|
35
|
-
}
|
|
36
|
-
});
|
|
37
25
|
connection.send({
|
|
38
26
|
tag: parsed.tag,
|
|
39
27
|
command: 'OK',
|
|
@@ -79,7 +79,8 @@ function getSection(data, query, global) {
|
|
|
79
79
|
else if (query.section.length > 1) {
|
|
80
80
|
throw new Error((key || 'Part number') + ' does not take any arguments');
|
|
81
81
|
}
|
|
82
|
-
//
|
|
82
|
+
// a part that does not exist is returned as an empty string, like Dovecot does: RFC 3501 and RFC 9051 section
|
|
83
|
+
// 6.4.5 define no error for it, only an empty string for a partial range that starts beyond the end of the text
|
|
83
84
|
const node = path ? resolveNode(data.tree, path, global) : data.tree;
|
|
84
85
|
if (!node) {
|
|
85
86
|
return '';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { IMAPServer } from '../../types.js';
|
|
2
2
|
declare const INVALID_KEYWORD_CHAR: RegExp;
|
|
3
3
|
/**
|
|
4
4
|
* System flags are case-insensitive, so "\seen" becomes "\Seen"
|
|
@@ -10,8 +10,8 @@ declare function normalizeSystemFlag(flag: string): string;
|
|
|
10
10
|
/**
|
|
11
11
|
* Throws if a flag can not be stored: an unknown system flag (including \Recent) or an invalid keyword
|
|
12
12
|
*
|
|
13
|
-
* @param {Object}
|
|
13
|
+
* @param {Object} server IMAP server, its systemFlags are the system flags that can be stored
|
|
14
14
|
* @param {String} flag Normalized flag value
|
|
15
15
|
*/
|
|
16
|
-
declare function checkSystemFlags(
|
|
16
|
+
declare function checkSystemFlags(server: IMAPServer, flag: string): void;
|
|
17
17
|
export { INVALID_KEYWORD_CHAR, normalizeSystemFlag, checkSystemFlags };
|
|
@@ -18,12 +18,12 @@ function normalizeSystemFlag(flag) {
|
|
|
18
18
|
/**
|
|
19
19
|
* Throws if a flag can not be stored: an unknown system flag (including \Recent) or an invalid keyword
|
|
20
20
|
*
|
|
21
|
-
* @param {Object}
|
|
21
|
+
* @param {Object} server IMAP server, its systemFlags are the system flags that can be stored
|
|
22
22
|
* @param {String} flag Normalized flag value
|
|
23
23
|
*/
|
|
24
|
-
function checkSystemFlags(
|
|
24
|
+
function checkSystemFlags(server, flag) {
|
|
25
25
|
if (flag.charAt(0) === '\\') {
|
|
26
|
-
if (
|
|
26
|
+
if (server.systemFlags.indexOf(flag) < 0) {
|
|
27
27
|
throw new Error('Invalid system flag ' + flag);
|
|
28
28
|
}
|
|
29
29
|
}
|
|
@@ -7,9 +7,9 @@ function setFlags(connection, message, flags) {
|
|
|
7
7
|
const messageFlags = [];
|
|
8
8
|
[].concat(flags).forEach(flag => {
|
|
9
9
|
flag = normalizeSystemFlag(typeof flag === 'string' ? flag : String(flag.value || ''));
|
|
10
|
-
checkSystemFlags(connection, flag);
|
|
11
|
-
//
|
|
12
|
-
if (
|
|
10
|
+
checkSystemFlags(connection.server, flag);
|
|
11
|
+
// a flag that is not in PERMANENTFLAGS is ignored (RFC 3501 section 7.1, RFC 9051 section 7.1)
|
|
12
|
+
if (!connection.server.isPermanentFlag(mailbox, flag)) {
|
|
13
13
|
return;
|
|
14
14
|
}
|
|
15
15
|
if (messageFlags.indexOf(flag) < 0) {
|
|
@@ -23,9 +23,9 @@ function addFlags(connection, message, flags) {
|
|
|
23
23
|
const mailbox = connection.selectedMailbox;
|
|
24
24
|
[].concat(flags).forEach(flag => {
|
|
25
25
|
flag = normalizeSystemFlag(typeof flag === 'string' ? flag : String(flag.value || ''));
|
|
26
|
-
checkSystemFlags(connection, flag);
|
|
27
|
-
//
|
|
28
|
-
if (
|
|
26
|
+
checkSystemFlags(connection.server, flag);
|
|
27
|
+
// a flag that is not in PERMANENTFLAGS is ignored (RFC 3501 section 7.1, RFC 9051 section 7.1)
|
|
28
|
+
if (!connection.server.isPermanentFlag(mailbox, flag)) {
|
|
29
29
|
return;
|
|
30
30
|
}
|
|
31
31
|
if (message.flags.indexOf(flag) < 0) {
|
|
@@ -37,7 +37,7 @@ function addFlags(connection, message, flags) {
|
|
|
37
37
|
function removeFlags(connection, message, flags) {
|
|
38
38
|
[].concat(flags).forEach(flag => {
|
|
39
39
|
flag = normalizeSystemFlag(typeof flag === 'string' ? flag : String(flag.value || ''));
|
|
40
|
-
checkSystemFlags(connection, flag);
|
|
40
|
+
checkSystemFlags(connection.server, flag);
|
|
41
41
|
if (message.flags.indexOf(flag) >= 0) {
|
|
42
42
|
for (let i = 0; i < message.flags.length; i++) {
|
|
43
43
|
if (message.flags[i] === flag) {
|
|
@@ -19,8 +19,22 @@ export default function listCommand(connection, parsed, data, callback) {
|
|
|
19
19
|
return callback();
|
|
20
20
|
}
|
|
21
21
|
if (!parsed.attributes[1].value) {
|
|
22
|
-
// empty
|
|
23
|
-
|
|
22
|
+
// RFC 3501 section 6.3.8: "An empty ("" string) mailbox name argument is a special request to return the
|
|
23
|
+
// hierarchy delimiter and the root name of the name given in the reference. The value returned as the root
|
|
24
|
+
// MAY be the empty string if the reference is non-rooted or is an empty string."
|
|
25
|
+
const server = connection.server;
|
|
26
|
+
const reference = parsed.attributes[0].value || '';
|
|
27
|
+
// the namespace of the reference, the personal one for a reference that is not a valid name
|
|
28
|
+
let key = server.referenceNamespace;
|
|
29
|
+
try {
|
|
30
|
+
key = server.getNamespace(connection.importMailboxName(reference)) || key;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// not a mailbox name, so not in another namespace
|
|
34
|
+
}
|
|
35
|
+
const namespace = key !== false ? server.storage[key] : undefined;
|
|
36
|
+
// the root of a name in another namespace is the prefix of that namespace, like "#news." in the RFC example
|
|
37
|
+
const root = key !== false && key !== server.referenceNamespace ? connection.exportMailboxName(key) : '';
|
|
24
38
|
if (namespace) {
|
|
25
39
|
connection.send({
|
|
26
40
|
tag: '*',
|
|
@@ -33,7 +47,7 @@ export default function listCommand(connection, parsed, data, callback) {
|
|
|
33
47
|
}
|
|
34
48
|
],
|
|
35
49
|
namespace.separator,
|
|
36
|
-
|
|
50
|
+
root
|
|
37
51
|
]
|
|
38
52
|
}, 'LIST ITEM', parsed, data);
|
|
39
53
|
}
|