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,132 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { states } = require('../command-states');
|
|
4
|
+
const { getMessageData } = require('../mimeparser');
|
|
5
|
+
const { collationKey, baseSubject, arrivalTime, sentTime, addressMailbox, searchMessages } = require('../sorting');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @help Adds SORT [RFC5256] capability
|
|
9
|
+
*
|
|
10
|
+
* SORT: https://tools.ietf.org/html/rfc5256
|
|
11
|
+
*
|
|
12
|
+
* Additional commands:
|
|
13
|
+
* - SORT
|
|
14
|
+
* - UID SORT
|
|
15
|
+
*/
|
|
16
|
+
module.exports = function (server) {
|
|
17
|
+
if (server.sortKeys) {
|
|
18
|
+
// already loaded, e.g. by the SORT=DISPLAY plugin
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
server.registerCapability('SORT');
|
|
23
|
+
|
|
24
|
+
// RFC 5256 section 3: sort keys and the value of a message for each. A value is a number or the
|
|
25
|
+
// collation key of a string, see sorting.collationKey. SORT=DISPLAY adds its keys here
|
|
26
|
+
server.sortKeys = {
|
|
27
|
+
ARRIVAL: message => arrivalTime(message),
|
|
28
|
+
CC: message => collationKey(addressMailbox(message, 'cc')),
|
|
29
|
+
DATE: message => sentTime(message),
|
|
30
|
+
FROM: message => collationKey(addressMailbox(message, 'from')),
|
|
31
|
+
SIZE: message => getMessageData(message).raw.length,
|
|
32
|
+
SUBJECT: message => collationKey(baseSubject(getMessageData(message).tree.parsedHeader.subject).subject),
|
|
33
|
+
TO: message => collationKey(addressMailbox(message, 'to'))
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const compareValues = (a, b) => (Buffer.isBuffer(a) ? Buffer.compare(a, b) : a - b);
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Returns a comparator of messages for parsed sort criteria: ascending order, REVERSE turns one
|
|
40
|
+
* criterion around, and mailbox order (the sequence number) breaks ties (RFC 5256 section 3).
|
|
41
|
+
* Every value is computed once per message. Plugins (ESORT, CONTEXT=SORT) use it to sort again later
|
|
42
|
+
*
|
|
43
|
+
* @param {Array} criteria List of `{ key, reverse }`
|
|
44
|
+
* @return {Function} comparator for two messages of the same mailbox
|
|
45
|
+
*/
|
|
46
|
+
server.sortComparator = criteria => {
|
|
47
|
+
const cache = new WeakMap();
|
|
48
|
+
const getValues = message => {
|
|
49
|
+
if (!cache.has(message)) {
|
|
50
|
+
cache.set(
|
|
51
|
+
message,
|
|
52
|
+
criteria.map(criterion => server.sortKeys[criterion.key](message))
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
return cache.get(message);
|
|
56
|
+
};
|
|
57
|
+
return (a, b) => {
|
|
58
|
+
const aValues = getValues(a);
|
|
59
|
+
const bValues = getValues(b);
|
|
60
|
+
for (let i = 0; i < criteria.length; i++) {
|
|
61
|
+
const diff = compareValues(aValues[i], bValues[i]);
|
|
62
|
+
if (diff) {
|
|
63
|
+
return criteria[i].reverse ? -diff : diff;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// messages are kept in UID order, so this is the order of sequence numbers
|
|
67
|
+
return a.uid - b.uid;
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const sortHandler = (isUid, connection, parsed, data, callback) => {
|
|
72
|
+
const command = isUid ? 'UID SORT' : 'SORT';
|
|
73
|
+
const attributes = parsed.attributes || [];
|
|
74
|
+
|
|
75
|
+
const fail = text => {
|
|
76
|
+
connection.sendStatus(parsed, data, 'BAD', text, false, command + ' FAILED');
|
|
77
|
+
return callback();
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// RFC 5256 section 5: sort-criteria = "(" sort-criterion *(SP sort-criterion) ")",
|
|
81
|
+
// sort-criterion = ["REVERSE" SP] sort-key
|
|
82
|
+
if (!Array.isArray(attributes[0]) || !attributes[0].length) {
|
|
83
|
+
return fail(command + ' expects a list of sort criteria');
|
|
84
|
+
}
|
|
85
|
+
const criteria = [];
|
|
86
|
+
let reverse = false;
|
|
87
|
+
for (const item of attributes[0]) {
|
|
88
|
+
const key = item && item.type === 'ATOM' ? item.value.toUpperCase() : '';
|
|
89
|
+
if (key === 'REVERSE' && !reverse) {
|
|
90
|
+
reverse = true;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (!Object.hasOwn(server.sortKeys, key)) {
|
|
94
|
+
return fail('Invalid sort criterion ' + (item && typeof item.value === 'string' ? item.value : ''));
|
|
95
|
+
}
|
|
96
|
+
criteria.push({ key, reverse });
|
|
97
|
+
reverse = false;
|
|
98
|
+
}
|
|
99
|
+
if (reverse) {
|
|
100
|
+
return fail('REVERSE must be followed by a sort key');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const result = searchMessages(connection, parsed, data, attributes.slice(1));
|
|
104
|
+
if (!result) {
|
|
105
|
+
return callback();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const compare = server.sortComparator(criteria);
|
|
109
|
+
const sorted = result.list.slice().sort(compare);
|
|
110
|
+
|
|
111
|
+
connection.send(
|
|
112
|
+
{
|
|
113
|
+
tag: '*',
|
|
114
|
+
command: 'SORT',
|
|
115
|
+
attributes: sorted.map(message => (isUid ? message.uid : result.numbers[message.uid]))
|
|
116
|
+
},
|
|
117
|
+
command,
|
|
118
|
+
parsed,
|
|
119
|
+
data,
|
|
120
|
+
// the search result, for CONDSTORE to append the highest mod-sequence (RFC 7162 section 3.1.9), with the
|
|
121
|
+
// sorted messages and the comparator for ESORT and CONTEXT=SORT
|
|
122
|
+
Object.assign(result, { sorted, compare })
|
|
123
|
+
);
|
|
124
|
+
connection.sendStatus(parsed, data, 'OK', command + ' completed', false, command);
|
|
125
|
+
return callback();
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// RFC 5256 section 3: EXPUNGE responses are not permitted while responding to SORT, but are during UID SORT.
|
|
129
|
+
// The search criteria start after the sort criteria and the charset
|
|
130
|
+
server.setCommandHandler('SORT', sortHandler.bind(null, false), { states: states.SELECTED, searchCriteria: 2, noExpunge: true });
|
|
131
|
+
server.setCommandHandler('UID SORT', sortHandler.bind(null, true), { states: states.SELECTED, searchCriteria: 2 });
|
|
132
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { getListExtensions } = require('../list-extensions');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @help Enables SPECIAL-USE [RFC6154] capability
|
|
7
|
+
* @help Mailboxes need to have a "special-use"
|
|
8
|
+
* @help property (String or Array) that will be used
|
|
9
|
+
* @help as extra flag for LIST and LSUB responses.
|
|
10
|
+
* @help With LIST-EXTENDED the SPECIAL-USE selection and
|
|
11
|
+
* @help return options work with the other LIST options
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
module.exports = function (server) {
|
|
15
|
+
// Register capability
|
|
16
|
+
server.registerCapability('SPECIAL-USE');
|
|
17
|
+
|
|
18
|
+
const getSpecialUse = folder => [].concat((folder && folder['special-use']) || []);
|
|
19
|
+
|
|
20
|
+
// RFC 6154 section 2: with LIST-EXTENDED, SPECIAL-USE is a list-select-independent-opt that
|
|
21
|
+
// implies the SPECIAL-USE return option. The LIST-EXTENDED plugin may be loaded before or after this one
|
|
22
|
+
const listExtensions = getListExtensions(server);
|
|
23
|
+
listExtensions.selectionOptions['SPECIAL-USE'] = {
|
|
24
|
+
type: 'independent',
|
|
25
|
+
returnOption: 'SPECIAL-USE',
|
|
26
|
+
match: folder => folder.flags.indexOf('\\Noselect') < 0 && getSpecialUse(folder).length > 0
|
|
27
|
+
};
|
|
28
|
+
// special-use attributes are always included, so the return option needs no extra handling
|
|
29
|
+
listExtensions.returnOptions['SPECIAL-USE'] = {};
|
|
30
|
+
|
|
31
|
+
const listHandler = server.getCommandHandler('LIST');
|
|
32
|
+
|
|
33
|
+
server.setCommandHandler('LIST', (connection, parsed, data, callback) => {
|
|
34
|
+
if (listExtensions.enabled) {
|
|
35
|
+
// LIST-EXTENDED handles the options
|
|
36
|
+
return listHandler(connection, parsed, data, callback);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// without LIST-EXTENDED only the SPECIAL-USE options are understood
|
|
40
|
+
let i;
|
|
41
|
+
if (parsed.attributes && Array.isArray(parsed.attributes[0])) {
|
|
42
|
+
for (i = parsed.attributes[0].length - 1; i >= 0; i--) {
|
|
43
|
+
if (parsed.attributes[0][i] && parsed.attributes[0][i].type === 'ATOM' && parsed.attributes[0][i].value.toUpperCase() === 'SPECIAL-USE') {
|
|
44
|
+
parsed.attributes[0].splice(i, 1);
|
|
45
|
+
parsed.listSpecialUseOnly = true;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// remove parameter if no other memebers were left
|
|
49
|
+
if (!parsed.attributes[0].length) {
|
|
50
|
+
parsed.attributes.splice(0, 1);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (
|
|
55
|
+
parsed.attributes &&
|
|
56
|
+
parsed.attributes[2] &&
|
|
57
|
+
parsed.attributes[2].type === 'ATOM' &&
|
|
58
|
+
parsed.attributes[2].value.toUpperCase() === 'RETURN' &&
|
|
59
|
+
Array.isArray(parsed.attributes[3])
|
|
60
|
+
) {
|
|
61
|
+
for (i = parsed.attributes[3].length - 1; i >= 0; i--) {
|
|
62
|
+
if (parsed.attributes[3][i] && parsed.attributes[3][i].type === 'ATOM' && parsed.attributes[3][i].value.toUpperCase() === 'SPECIAL-USE') {
|
|
63
|
+
// special-use attributes are always included, so the return option needs no extra handling
|
|
64
|
+
parsed.attributes[3].splice(i, 1);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Remove RETURN (List) if no members were left
|
|
69
|
+
if (!parsed.attributes[3].length) {
|
|
70
|
+
parsed.attributes.splice(2, 2);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
listHandler(connection, parsed, data, callback);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
server.outputHandlers.push((connection, response, description, parsed, data, folder) => {
|
|
78
|
+
const specialUseList = getSpecialUse(folder).map(specialUse => {
|
|
79
|
+
return {
|
|
80
|
+
type: 'ATOM',
|
|
81
|
+
value: specialUse
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
if ((description === 'LIST ITEM' || description === 'LSUB ITEM') && folder && response.attributes && Array.isArray(response.attributes[0])) {
|
|
86
|
+
if (specialUseList.length) {
|
|
87
|
+
// special-use attributes are added to the other mailbox attributes, also with RETURN (SPECIAL-USE)
|
|
88
|
+
response.attributes[0] = response.attributes[0].concat(specialUseList);
|
|
89
|
+
} else if (parsed.listSpecialUseOnly) {
|
|
90
|
+
// Do not show this response
|
|
91
|
+
response.skipResponse = true;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @help Adds STARTTLS command
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
module.exports = function (server) {
|
|
8
|
+
// Register capability, usable with unsecure connection
|
|
9
|
+
server.registerCapability('STARTTLS', connection => {
|
|
10
|
+
return !connection.secureConnection;
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
// Add STARTTLS command
|
|
14
|
+
server.setCommandHandler('STARTTLS', (connection, parsed, data, callback) => {
|
|
15
|
+
// only works in insecure setting
|
|
16
|
+
if (connection.secureConnection) {
|
|
17
|
+
return sendError('Connection is already secured', connection, parsed, data, callback);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
connection.send(
|
|
21
|
+
{
|
|
22
|
+
tag: parsed.tag,
|
|
23
|
+
command: 'OK',
|
|
24
|
+
attributes: [
|
|
25
|
+
{
|
|
26
|
+
type: 'TEXT',
|
|
27
|
+
value: 'Server ready to start TLS negotiation'
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
},
|
|
31
|
+
'STARTTLS INIT',
|
|
32
|
+
parsed,
|
|
33
|
+
data
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
connection.upgradeConnection(callback);
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function sendError(message, connection, parsed, data, callback) {
|
|
41
|
+
connection.send(
|
|
42
|
+
{
|
|
43
|
+
tag: parsed.tag,
|
|
44
|
+
command: 'BAD',
|
|
45
|
+
attributes: [
|
|
46
|
+
{
|
|
47
|
+
type: 'TEXT',
|
|
48
|
+
value: message
|
|
49
|
+
}
|
|
50
|
+
]
|
|
51
|
+
},
|
|
52
|
+
'INVALID COMMAND',
|
|
53
|
+
parsed,
|
|
54
|
+
data
|
|
55
|
+
);
|
|
56
|
+
return callback();
|
|
57
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fetchHandlers = require('../commands/handlers/fetch');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @help Adds STATUS=SIZE [RFC8438] capability. The SIZE status item
|
|
7
|
+
* @help is the sum of the RFC822.SIZE values of the messages, it can
|
|
8
|
+
* @help also be used with LIST-STATUS
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
module.exports = function (server) {
|
|
12
|
+
server.registerCapability('STATUS=SIZE');
|
|
13
|
+
|
|
14
|
+
server.allowedStatus.push('SIZE');
|
|
15
|
+
|
|
16
|
+
// RFC 8438 section 3: at least the sum of the RFC822.SIZE values of all messages in the mailbox
|
|
17
|
+
server.statusHandlers.SIZE = (connection, mailbox) =>
|
|
18
|
+
mailbox.messages.reduce((size, message) => size + fetchHandlers['RFC822.SIZE'](connection, message), 0);
|
|
19
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { addThreadAlgorithm } = require('../threading');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @help Adds THREAD=ORDEREDSUBJECT [RFC5256] capability
|
|
7
|
+
*
|
|
8
|
+
* THREAD: https://tools.ietf.org/html/rfc5256
|
|
9
|
+
*
|
|
10
|
+
* Additional commands:
|
|
11
|
+
* - THREAD ORDEREDSUBJECT
|
|
12
|
+
* - UID THREAD ORDEREDSUBJECT
|
|
13
|
+
*/
|
|
14
|
+
module.exports = function (server) {
|
|
15
|
+
addThreadAlgorithm(server, 'ORDEREDSUBJECT');
|
|
16
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { addThreadAlgorithm } = require('../threading');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @help Adds THREAD=REFERENCES [RFC5256] capability
|
|
7
|
+
*
|
|
8
|
+
* THREAD: https://tools.ietf.org/html/rfc5256
|
|
9
|
+
*
|
|
10
|
+
* Additional commands:
|
|
11
|
+
* - THREAD REFERENCES
|
|
12
|
+
* - UID THREAD REFERENCES
|
|
13
|
+
*/
|
|
14
|
+
module.exports = function (server) {
|
|
15
|
+
addThreadAlgorithm(server, 'REFERENCES');
|
|
16
|
+
};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { toVanished } = require('../vanished');
|
|
4
|
+
const { isAtom } = require('../arguments');
|
|
5
|
+
const { registerEnable, isEnabled } = require('./enable');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @help Adds UIDONLY [RFC9586] capability, loads ENABLE as well
|
|
9
|
+
* @help After ENABLE UIDONLY: FETCH, STORE, SEARCH, COPY, MOVE, SORT, THREAD, REPLACE and
|
|
10
|
+
* @help message numbers in UID SEARCH (SORT, THREAD) criteria are refused with BAD [UIDREQUIRED],
|
|
11
|
+
* @help UIDFETCH responses replace FETCH responses and VANISHED replaces EXPUNGE.
|
|
12
|
+
* @help Load UIDPLUS for COPYUID and UID EXPUNGE
|
|
13
|
+
*
|
|
14
|
+
* UIDONLY: https://www.rfc-editor.org/rfc/rfc9586
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
module.exports = function (server) {
|
|
18
|
+
server.registerCapability('UIDONLY');
|
|
19
|
+
|
|
20
|
+
registerEnable(server, 'UIDONLY');
|
|
21
|
+
|
|
22
|
+
// UNAUTHENTICATE clears the ENABLEd extensions (RFC 8437 section 4.1)
|
|
23
|
+
const isUidonly = connection => isEnabled(connection, 'UIDONLY');
|
|
24
|
+
|
|
25
|
+
const refuse = text => ({ command: 'BAD', code: 'UIDREQUIRED', text });
|
|
26
|
+
|
|
27
|
+
// RFC 9586 section 3: once UIDONLY is enabled, message numbers in any argument MUST be answered with a
|
|
28
|
+
// tagged BAD that has the UIDREQUIRED response code
|
|
29
|
+
const getRefusal = (connection, parsed) => {
|
|
30
|
+
const command = String(parsed.command || '').toUpperCase();
|
|
31
|
+
const options = server.getCommandOptions(command);
|
|
32
|
+
// the ESEARCH command of MULTISEARCH always returns UIDs (RFC 7377 section 2), like a UID command
|
|
33
|
+
const isUid = /^UID /.test(command) || command === 'ESEARCH';
|
|
34
|
+
|
|
35
|
+
// sections 3.2 and 3.8: FETCH, STORE, SEARCH, COPY, MOVE, SORT and THREAD are prohibited, also REPLACE
|
|
36
|
+
// (RFC 8508), its message argument is a sequence number. These have the sequenceSet or searchCriteria
|
|
37
|
+
// command option
|
|
38
|
+
if (!isUid && (options.sequenceSet !== false || options.searchCriteria !== false)) {
|
|
39
|
+
return refuse(command + ' is not allowed once UIDONLY is enabled, use UID ' + command);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// section 3.5: the <sequence set> search key is prohibited, also in UID SORT and UID THREAD
|
|
43
|
+
if (isUid && options.searchCriteria !== false && connection.usesSequenceNumbers(parsed)) {
|
|
44
|
+
return refuse('Message numbers are not allowed in the search criteria once UIDONLY is enabled, use UID <sequence set>');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// section 3.7: the fourth QRESYNC parameter (message sequence match data) MUST NOT be used
|
|
48
|
+
if ((command === 'SELECT' || command === 'EXAMINE') && Array.isArray(parsed.attributes && parsed.attributes[1])) {
|
|
49
|
+
const params = parsed.attributes[1];
|
|
50
|
+
const position = params.findIndex(param => isAtom(param, 'QRESYNC'));
|
|
51
|
+
const value = position >= 0 && params[position + 1];
|
|
52
|
+
if (Array.isArray(value) && value.slice(2).some(Array.isArray)) {
|
|
53
|
+
return refuse('QRESYNC message sequence match data is not allowed once UIDONLY is enabled');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return false;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
server.commandChecks.push((connection, parsed) => isUidonly(connection) && getRefusal(connection, parsed));
|
|
61
|
+
|
|
62
|
+
// a prohibited command is refused before its literal is sent, before other checks that would answer NO
|
|
63
|
+
server.literalFilters.unshift((connection, command) => isUidonly(connection) && getRefusal(connection, { command }));
|
|
64
|
+
|
|
65
|
+
server.connectionHandlers.push(connection => {
|
|
66
|
+
// RFC 3501 section 5.5 is about commands with message numbers, these are all refused with
|
|
67
|
+
// BAD [UIDREQUIRED] once UIDONLY is enabled, as if they had waited
|
|
68
|
+
const isAmbiguous = connection.isAmbiguous;
|
|
69
|
+
connection.isAmbiguous = function (parsed) {
|
|
70
|
+
return !isUidonly(this) && isAmbiguous.call(this, parsed);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// RFC 9586 sections 3.4 and 3.6: expunges are announced with VANISHED instead of EXPUNGE, EXISTS and
|
|
74
|
+
// RECENT stay as they are
|
|
75
|
+
const prepareNotifications = connection.prepareNotifications;
|
|
76
|
+
connection.prepareNotifications = function (queue) {
|
|
77
|
+
queue = prepareNotifications.call(this, queue);
|
|
78
|
+
return isUidonly(this) ? toVanished(queue) : queue;
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// RFC 9586 section 3: the server MUST NOT return message sequence numbers in any response
|
|
83
|
+
const outputHandler = (connection, response, description, parsed, data, extra) => {
|
|
84
|
+
if (!response || response.tag !== '*' || !Array.isArray(response.attributes) || !isUidonly(connection)) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const attributes = response.attributes;
|
|
88
|
+
|
|
89
|
+
// the UNSEEN response code of SELECT and EXAMINE is a message number (RFC 3501 section 7.1)
|
|
90
|
+
if (response.command === 'OK' && attributes[0] && attributes[0].type === 'SECTION' && isAtom(attributes[0].section[0], 'UNSEEN')) {
|
|
91
|
+
response.skipResponse = true;
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!isAtom(attributes[1], 'FETCH') || !Array.isArray(attributes[2])) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// section 3.3: uidfetch-resp = uniqueid SP "UIDFETCH" SP msg-att
|
|
100
|
+
// FETCH responses pass their message as `extra`, notifications like the MessageNew FETCH of NOTIFY carry it
|
|
101
|
+
const list = attributes[2];
|
|
102
|
+
const uidPos = list.findIndex((item, i) => !(i % 2) && isAtom(item, 'UID'));
|
|
103
|
+
const message = (extra && typeof extra.uid === 'number' && extra) || response.fetchedMessage;
|
|
104
|
+
const uid = message ? message.uid : uidPos >= 0 && list[uidPos + 1];
|
|
105
|
+
if (!uid) {
|
|
106
|
+
// never send a sequence number instead
|
|
107
|
+
response.skipResponse = true;
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// the UID data item is only included if the client asked for it, like in the examples of section 3.3
|
|
112
|
+
if (parsed && !('uidonlyKeepUid' in parsed)) {
|
|
113
|
+
parsed.uidonlyKeepUid = requestsUid(parsed);
|
|
114
|
+
}
|
|
115
|
+
let items = list;
|
|
116
|
+
if (uidPos >= 0 && list.length > 2 && !(parsed && parsed.uidonlyKeepUid)) {
|
|
117
|
+
items = list.slice(0, uidPos).concat(list.slice(uidPos + 2));
|
|
118
|
+
}
|
|
119
|
+
response.attributes = [uid, { type: 'ATOM', value: 'UIDFETCH' }, items];
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// registered once every plugin is loaded, so that the other output handlers see the FETCH responses they
|
|
123
|
+
// know before these are turned into UIDFETCH responses
|
|
124
|
+
server.once('pluginsLoaded', () => server.outputHandlers.push(outputHandler));
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// Checks if a UID FETCH command lists the UID data item
|
|
128
|
+
function requestsUid(parsed) {
|
|
129
|
+
if (String(parsed.command || '').toUpperCase() !== 'UID FETCH' || !parsed.attributes) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
return [].concat(parsed.attributes[1] || []).some(item => isAtom(item, 'UID'));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
module.exports.requires = ['ENABLE'];
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { toSequenceSet } = require('../esearch');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @help Adds UIDPLUS [RFC4315] capability
|
|
7
|
+
*
|
|
8
|
+
* UIDPLUS: http://tools.ietf.org/html/rfc4315
|
|
9
|
+
*
|
|
10
|
+
* Additional commands:
|
|
11
|
+
* - UID EXPUNGE
|
|
12
|
+
*
|
|
13
|
+
* Additional response codes:
|
|
14
|
+
* - APPENDUID (with a UID set for MULTIAPPEND, and in an untagged OK for REPLACE)
|
|
15
|
+
* - COPYUID
|
|
16
|
+
* - Not implemented: UIDNOTSTICKY
|
|
17
|
+
*/
|
|
18
|
+
module.exports = function (server) {
|
|
19
|
+
server.registerCapability('UIDPLUS');
|
|
20
|
+
|
|
21
|
+
server.setCommandHandler('UID EXPUNGE', (connection, parsed, data, callback) => {
|
|
22
|
+
if (!parsed.attributes || parsed.attributes.length !== 1 || !parsed.attributes[0] || ['ATOM', 'SEQUENCE'].indexOf(parsed.attributes[0].type) < 0) {
|
|
23
|
+
connection.send(
|
|
24
|
+
{
|
|
25
|
+
tag: parsed.tag,
|
|
26
|
+
command: 'BAD',
|
|
27
|
+
attributes: [
|
|
28
|
+
{
|
|
29
|
+
type: 'TEXT',
|
|
30
|
+
value: 'UID EXPUNGE expects uid sequence set'
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
},
|
|
34
|
+
'INVALID COMMAND',
|
|
35
|
+
parsed,
|
|
36
|
+
data
|
|
37
|
+
);
|
|
38
|
+
return callback();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (connection.refuseReadOnly(parsed, data, 'UID EXPUNGE FAILED')) {
|
|
42
|
+
return callback();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const sequence = parsed.attributes[0].value;
|
|
46
|
+
const range = connection.limitRange(parsed, connection.getMessageRange(sequence, true));
|
|
47
|
+
// Only messages with the \Deleted flag are removed (RFC 4315 section 2.1)
|
|
48
|
+
const rangeMessages = range.map(x => x[1]).filter(message => message.flags.indexOf('\\Deleted') >= 0);
|
|
49
|
+
|
|
50
|
+
connection.expungeSpecificMessages(connection.selectedMailbox, rangeMessages, false, true, !!parsed.highestFirst);
|
|
51
|
+
|
|
52
|
+
connection.send(
|
|
53
|
+
{
|
|
54
|
+
tag: parsed.tag,
|
|
55
|
+
command: 'OK',
|
|
56
|
+
attributes: [
|
|
57
|
+
{
|
|
58
|
+
type: 'TEXT',
|
|
59
|
+
value: 'UID EXPUNGE completed'
|
|
60
|
+
}
|
|
61
|
+
]
|
|
62
|
+
},
|
|
63
|
+
'UID EXPUNGE',
|
|
64
|
+
parsed,
|
|
65
|
+
data
|
|
66
|
+
);
|
|
67
|
+
callback();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
server.outputHandlers.push((connection, response, description, parsed, data, extra) => {
|
|
71
|
+
if (description === 'APPEND' || description === 'REPLACE APPENDUID') {
|
|
72
|
+
// OK [APPENDUID <target-mailbox-uidvalidity> <uid>] APPEND Completed. With MULTIAPPEND the
|
|
73
|
+
// second value is a UID set, but never for a single message (RFC 4315 section 3)
|
|
74
|
+
const uids = extra.messages.map(message => message.uid);
|
|
75
|
+
response.attributes = [
|
|
76
|
+
{
|
|
77
|
+
type: 'SECTION',
|
|
78
|
+
section: [
|
|
79
|
+
{
|
|
80
|
+
type: 'ATOM',
|
|
81
|
+
value: 'APPENDUID'
|
|
82
|
+
},
|
|
83
|
+
extra.mailbox.uidvalidity,
|
|
84
|
+
uids.length > 1 ? { type: 'SEQUENCE', value: toSequenceSet(uids) } : uids[0]
|
|
85
|
+
]
|
|
86
|
+
}
|
|
87
|
+
].concat(response.attributes);
|
|
88
|
+
// REPLACE sends APPENDUID in an untagged OK before the EXPUNGE (RFC 8508 section 4.3)
|
|
89
|
+
response.skipResponse = false;
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (description === 'COPY' || description === 'UID COPY' || description === 'MOVE COPYUID' || description === 'UID MOVE COPYUID') {
|
|
94
|
+
// Nothing was copied, an empty COPYUID would break the response grammar
|
|
95
|
+
if (!extra || !extra.sourceUids || !extra.sourceUids.length) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
response.attributes = [
|
|
99
|
+
{
|
|
100
|
+
type: 'SECTION',
|
|
101
|
+
section: [
|
|
102
|
+
{
|
|
103
|
+
type: 'ATOM',
|
|
104
|
+
value: 'COPYUID'
|
|
105
|
+
},
|
|
106
|
+
extra.mailbox.uidvalidity,
|
|
107
|
+
// The range was interpreted in ascending order so these
|
|
108
|
+
// values are already in the right order.
|
|
109
|
+
{
|
|
110
|
+
type: 'SEQUENCE',
|
|
111
|
+
value: extra.sourceUids.join(',')
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
type: 'SEQUENCE',
|
|
115
|
+
value: extra.targetUids.join(',')
|
|
116
|
+
}
|
|
117
|
+
]
|
|
118
|
+
}
|
|
119
|
+
].concat(response.attributes);
|
|
120
|
+
response.skipResponse = false;
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { states } = require('../command-states');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @help Adds UNAUTHENTICATE [RFC8437] capability
|
|
7
|
+
* @help Returns to the Not Authenticated state, everything
|
|
8
|
+
* @help but TLS is reset (selected mailbox, ENABLE, COMPRESS)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
module.exports = function (server) {
|
|
12
|
+
server.registerCapability('UNAUTHENTICATE');
|
|
13
|
+
|
|
14
|
+
server.setCommandHandler(
|
|
15
|
+
'UNAUTHENTICATE',
|
|
16
|
+
(connection, parsed, data, callback) => {
|
|
17
|
+
// RFC 8437 section 3: a NO response is not permitted, the reset can not fail here
|
|
18
|
+
connection.sendStatus(parsed, data, 'OK', 'Completed, now in not authenticated state');
|
|
19
|
+
|
|
20
|
+
// RFC 8437 sections 3 and 4.1: the mailbox is closed without EXPUNGE responses, ENABLEd
|
|
21
|
+
// extensions and CONDSTORE are turned off, and compression ends after the CRLF of the OK
|
|
22
|
+
connection.resetSession();
|
|
23
|
+
|
|
24
|
+
return callback();
|
|
25
|
+
},
|
|
26
|
+
{ states: states.AUTHENTICATED, noArguments: true }
|
|
27
|
+
);
|
|
28
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { states } = require('../command-states');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @help Adds UNSELECT [RFC3691] capability
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
module.exports = function (server) {
|
|
10
|
+
server.registerCapability('UNSELECT');
|
|
11
|
+
|
|
12
|
+
server.setCommandHandler(
|
|
13
|
+
'UNSELECT',
|
|
14
|
+
(connection, parsed, data, callback) => {
|
|
15
|
+
connection.send(
|
|
16
|
+
{
|
|
17
|
+
tag: parsed.tag,
|
|
18
|
+
command: 'OK',
|
|
19
|
+
attributes: [
|
|
20
|
+
{
|
|
21
|
+
type: 'TEXT',
|
|
22
|
+
value: 'Mailbox closed'
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
'CLOSE',
|
|
27
|
+
parsed,
|
|
28
|
+
data
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
connection.closeMailbox();
|
|
32
|
+
return callback();
|
|
33
|
+
},
|
|
34
|
+
{ states: states.SELECTED, noArguments: true }
|
|
35
|
+
);
|
|
36
|
+
};
|