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,156 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @help Adds ESEARCH [RFC4731] capability
|
|
5
|
+
* @help SEARCH and UID SEARCH take the RETURN (MIN MAX ALL COUNT) result options and answer with ESEARCH
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { buildEsearchResponse, selectReturned } = require('../esearch');
|
|
9
|
+
const { badError } = require('../commands/handlers/search');
|
|
10
|
+
|
|
11
|
+
// RFC 4466 section 2.6.1: search = "SEARCH" [SP "RETURN" SP "(" [search-return-opt *(SP search-return-opt)] ")"] SP search-program
|
|
12
|
+
const hasReturnOptions = attributes => {
|
|
13
|
+
const first = attributes && attributes[0];
|
|
14
|
+
return !!first && first.type === 'ATOM' && String(first.value).toUpperCase() === 'RETURN';
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
module.exports = function (server) {
|
|
18
|
+
// SEARCHRES loads ESEARCH as well, so the plugin may be called twice
|
|
19
|
+
if (server.searchReturnOptions) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
server.registerCapability('ESEARCH');
|
|
24
|
+
|
|
25
|
+
// RFC 4731 section 3.1 result options. Other plugins add their own: `data: false` for an option that
|
|
26
|
+
// does not add data to the ESEARCH response (SAVE of SEARCHRES), `response: true` if the ESEARCH
|
|
27
|
+
// response is sent even then (UPDATE of CONTEXT=SEARCH), `hint: true` for an option that is accepted
|
|
28
|
+
// and ignored (CONTEXT), `parse(item)` for an option with an argument and `once: true` if the option
|
|
29
|
+
// must not be repeated
|
|
30
|
+
server.searchReturnOptions = new Map(['MIN', 'MAX', 'ALL', 'COUNT'].map(name => [name, { data: true }]));
|
|
31
|
+
|
|
32
|
+
// Checks of the whole list of result options, `(options, connection, parsed, names)` returns an error message
|
|
33
|
+
// for an invalid combination. `names` also holds the hint options
|
|
34
|
+
server.searchReturnChecks = [];
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Parses a list of result options (RFC 4466 section 2.6.1: search-return-opts)
|
|
38
|
+
*
|
|
39
|
+
* @param {Array} list Parsed list of result options
|
|
40
|
+
* @param {Object} connection IMAP connection
|
|
41
|
+
* @param {Object} parsed Parsed command
|
|
42
|
+
* @return {Map} upper case option names to their parsed arguments (true for options without one)
|
|
43
|
+
* @throws {Error} BAD error for an invalid list
|
|
44
|
+
*/
|
|
45
|
+
server.parseSearchReturn = (list, connection, parsed) => {
|
|
46
|
+
if (!Array.isArray(list)) {
|
|
47
|
+
throw badError('RETURN expects a parenthesized list of result options');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const options = new Map();
|
|
51
|
+
// every option name, also the hints that are left out of `options`
|
|
52
|
+
const names = new Set();
|
|
53
|
+
for (let i = 0; i < list.length; i++) {
|
|
54
|
+
const item = list[i];
|
|
55
|
+
const name = item && item.type === 'ATOM' ? String(item.value).toUpperCase() : '';
|
|
56
|
+
const spec = server.searchReturnOptions.get(name);
|
|
57
|
+
// options that the server does not support must be rejected with BAD (RFC 4466 section 2.6.1)
|
|
58
|
+
if (!spec) {
|
|
59
|
+
throw badError('Unknown SEARCH result option' + (name ? ' ' + name : ''));
|
|
60
|
+
}
|
|
61
|
+
if (spec.once && options.has(name)) {
|
|
62
|
+
throw badError('SEARCH result option ' + name + ' can be used only once');
|
|
63
|
+
}
|
|
64
|
+
names.add(name);
|
|
65
|
+
const value = spec.parse ? spec.parse(list[++i]) : true;
|
|
66
|
+
if (!spec.hint) {
|
|
67
|
+
options.set(name, value);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
for (const check of server.searchReturnChecks) {
|
|
72
|
+
const error = check(options, connection, parsed, names);
|
|
73
|
+
if (error) {
|
|
74
|
+
throw badError(error);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return options;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// only result options without return data, like SAVE (RFC 5182 section 1), suppress the ESEARCH response
|
|
81
|
+
server.isSilentReturn = options =>
|
|
82
|
+
options.size > 0 &&
|
|
83
|
+
![...options.keys()].some(name => {
|
|
84
|
+
const spec = server.searchReturnOptions.get(name);
|
|
85
|
+
return spec.data || spec.response;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// The result options are taken off the arguments, so the SEARCH handler only sees the search program
|
|
89
|
+
const searchWrapper = (prevHandler, connection, parsed, data, callback) => {
|
|
90
|
+
if (!hasReturnOptions(parsed.attributes)) {
|
|
91
|
+
return prevHandler(connection, parsed, data, callback);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
parsed.searchReturn = server.parseSearchReturn(parsed.attributes[1], connection, parsed);
|
|
96
|
+
} catch (E) {
|
|
97
|
+
connection.sendStatus(parsed, data, 'BAD', E.message, false, (parsed.command || '').toUpperCase() + ' FAILED');
|
|
98
|
+
return callback();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
parsed.attributes = parsed.attributes.slice(2);
|
|
102
|
+
prevHandler(connection, parsed, data, callback);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// Lets a command take result options before its other arguments, parsed into `parsed.searchReturn`.
|
|
106
|
+
// ESORT uses it for SORT and UID SORT (RFC 5267 section 3)
|
|
107
|
+
server.acceptSearchReturn = command => {
|
|
108
|
+
const prevHandler = server.getCommandHandler(command);
|
|
109
|
+
server.setCommandHandler(command, (connection, parsed, data, callback) => searchWrapper(prevHandler, connection, parsed, data, callback));
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
server.acceptSearchReturn('SEARCH');
|
|
113
|
+
server.acceptSearchReturn('UID SEARCH');
|
|
114
|
+
|
|
115
|
+
// RFC 3501 section 5.5: only the search program can hold sequence numbers, the result options do not
|
|
116
|
+
server.connectionHandlers.push(connection => {
|
|
117
|
+
const usesSequenceNumbers = connection.usesSequenceNumbers;
|
|
118
|
+
connection.usesSequenceNumbers = function (parsed) {
|
|
119
|
+
if (hasReturnOptions(parsed.attributes)) {
|
|
120
|
+
parsed = Object.assign({}, parsed, { attributes: parsed.attributes.slice(2) });
|
|
121
|
+
}
|
|
122
|
+
return usesSequenceNumbers.call(this, parsed);
|
|
123
|
+
};
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Replaces the SEARCH response of an extended SEARCH with a single ESEARCH response (RFC 4731 section 3.1)
|
|
127
|
+
server.outputHandlers.push((connection, response, description, parsed, data, extra) => {
|
|
128
|
+
if (
|
|
129
|
+
!parsed ||
|
|
130
|
+
!parsed.searchReturn ||
|
|
131
|
+
response.tag !== '*' ||
|
|
132
|
+
response.command !== 'SEARCH' ||
|
|
133
|
+
(description !== 'SEARCH' && description !== 'UID SEARCH') ||
|
|
134
|
+
!extra ||
|
|
135
|
+
!Array.isArray(extra.list)
|
|
136
|
+
) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
response.skipResponse = true;
|
|
140
|
+
|
|
141
|
+
const options = parsed.searchReturn;
|
|
142
|
+
if (server.isSilentReturn(options)) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// like for SEARCH, the extra data is the search result, but `list` only holds the messages that the
|
|
147
|
+
// result options return (RFC 4731 section 3.2), for the MODSEQ of CONDSTORE
|
|
148
|
+
connection.send(
|
|
149
|
+
buildEsearchResponse(parsed.tag, description === 'UID SEARCH', extra, options),
|
|
150
|
+
'ESEARCH',
|
|
151
|
+
parsed,
|
|
152
|
+
data,
|
|
153
|
+
Object.assign({}, extra, { list: selectReturned(extra.list, options) })
|
|
154
|
+
);
|
|
155
|
+
});
|
|
156
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @help Adds ESORT [RFC5267] capability, loads SORT and ESEARCH as well
|
|
5
|
+
* @help SORT and UID SORT take the RETURN (MIN MAX ALL COUNT) result options and answer with ESEARCH, results are in sort order
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const sort = require('./sort');
|
|
9
|
+
const esearch = require('./esearch');
|
|
10
|
+
const { buildEsearchResponse, selectReturned } = require('../esearch');
|
|
11
|
+
|
|
12
|
+
const isSort = command => command === 'SORT' || command === 'UID SORT';
|
|
13
|
+
|
|
14
|
+
module.exports = function (server) {
|
|
15
|
+
if (server.esortLoaded) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
server.esortLoaded = true;
|
|
19
|
+
|
|
20
|
+
sort(server);
|
|
21
|
+
esearch(server);
|
|
22
|
+
|
|
23
|
+
server.registerCapability('ESORT');
|
|
24
|
+
|
|
25
|
+
// RFC 5267 section 3: extended-sort = ["UID" SP] "SORT" search-return-opts SP sort-criteria SP search-criteria
|
|
26
|
+
server.acceptSearchReturn('SORT');
|
|
27
|
+
server.acceptSearchReturn('UID SORT');
|
|
28
|
+
|
|
29
|
+
// RFC 5267 section 4.1: the CONTEXT, UPDATE and PARTIAL result options of SORT come with CONTEXT=SORT. PARTIAL
|
|
30
|
+
// is also defined for the PARTIAL capability (RFC 9394 section 3.1)
|
|
31
|
+
server.searchReturnChecks.push((options, connection, parsed, names) => {
|
|
32
|
+
if (!isSort((parsed.command || '').toUpperCase()) || server.contextSort) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
const name = ['CONTEXT', 'UPDATE', 'PARTIAL'].find(key => names.has(key) && (key !== 'PARTIAL' || !server.partialRangeLast));
|
|
36
|
+
return !!name && name + ' is not supported for SORT';
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Replaces the SORT response of an extended SORT with an ESEARCH response (RFC 5267 section 3)
|
|
40
|
+
server.outputHandlers.push((connection, response, description, parsed, data, extra) => {
|
|
41
|
+
if (!parsed || !parsed.searchReturn || response.tag !== '*' || response.command !== 'SORT' || !isSort(description) || !extra || !extra.sorted) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
response.skipResponse = true;
|
|
45
|
+
|
|
46
|
+
const options = parsed.searchReturn;
|
|
47
|
+
if (server.isSilentReturn(options)) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// the extra data lists the returned messages, for the MODSEQ of CONDSTORE (RFC 7162 section 3.1.10)
|
|
52
|
+
connection.send(
|
|
53
|
+
buildEsearchResponse(parsed.tag, description === 'UID SORT', extra, options, null, extra.sorted),
|
|
54
|
+
'ESEARCH',
|
|
55
|
+
parsed,
|
|
56
|
+
data,
|
|
57
|
+
Object.assign({}, extra, { list: selectReturned(extra.sorted, options) })
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { states } = require('../command-states');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @help Adds ID [RFC2971] capability
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
module.exports = function (server) {
|
|
10
|
+
// Register capability, always usable
|
|
11
|
+
server.registerCapability('ID');
|
|
12
|
+
|
|
13
|
+
// Add ID command
|
|
14
|
+
server.setCommandHandler(
|
|
15
|
+
'ID',
|
|
16
|
+
(connection, parsed, data, callback) => {
|
|
17
|
+
// keys are client supplied, so the object has no prototype
|
|
18
|
+
const clientList = Object.create(null);
|
|
19
|
+
let serverList = null;
|
|
20
|
+
let i;
|
|
21
|
+
let len;
|
|
22
|
+
let key;
|
|
23
|
+
|
|
24
|
+
// Require exactly 1 attribute (NIL or parameter list)
|
|
25
|
+
if (!parsed.attributes || parsed.attributes.length !== 1) {
|
|
26
|
+
return sendError('ID expects 1 attribute', connection, parsed, data, callback);
|
|
27
|
+
}
|
|
28
|
+
const list = parsed.attributes[0];
|
|
29
|
+
if ((list && !Array.isArray(list)) || (list && list.length % 2)) {
|
|
30
|
+
return sendError('ID expects valid parameter list', connection, parsed, data, callback);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// RFC 2971 section 3.3: at most 30 field-value pairs
|
|
34
|
+
if (list && list.length > 60) {
|
|
35
|
+
return sendError('ID allows at most 30 field-value pairs', connection, parsed, data, callback);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Build client ID object and check validity of the values
|
|
39
|
+
if (list && list.length) {
|
|
40
|
+
for (i = 0, len = list.length; i < len; i++) {
|
|
41
|
+
if (i % 2 === 0) {
|
|
42
|
+
// Handle keys (always strings)
|
|
43
|
+
if (list[i] && ['STRING', 'LITERAL'].indexOf(list[i].type) >= 0) {
|
|
44
|
+
key = list[i].value;
|
|
45
|
+
// RFC 2971 section 3.3: field names are at most 30 octets, case-insensitive and unique
|
|
46
|
+
if (key.length > 30) {
|
|
47
|
+
return sendError('ID field names can be at most 30 octets', connection, parsed, data, callback);
|
|
48
|
+
}
|
|
49
|
+
if (Object.keys(clientList).some(existing => existing.toLowerCase() === key.toLowerCase())) {
|
|
50
|
+
return sendError('ID field names must not repeat', connection, parsed, data, callback);
|
|
51
|
+
}
|
|
52
|
+
} else {
|
|
53
|
+
return sendError('ID expects valid parameter list', connection, parsed, data, callback);
|
|
54
|
+
}
|
|
55
|
+
} else {
|
|
56
|
+
// Handle values (string or NIL)
|
|
57
|
+
if (!list[i] || ['STRING', 'LITERAL'].indexOf(list[i].type) >= 0) {
|
|
58
|
+
if (list[i] && list[i].value.length > 1024) {
|
|
59
|
+
return sendError('ID values can be at most 1024 octets', connection, parsed, data, callback);
|
|
60
|
+
}
|
|
61
|
+
clientList[key] = (list[i] && list[i].value) || null;
|
|
62
|
+
} else {
|
|
63
|
+
return sendError('ID expects valid parameter list', connection, parsed, data, callback);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Build response object from server options
|
|
70
|
+
if (server.options.id) {
|
|
71
|
+
serverList = [];
|
|
72
|
+
Object.keys(server.options.id).forEach(key => {
|
|
73
|
+
serverList.push({
|
|
74
|
+
type: 'STRING',
|
|
75
|
+
value: key
|
|
76
|
+
});
|
|
77
|
+
serverList.push({
|
|
78
|
+
type: 'STRING',
|
|
79
|
+
value: (server.options.id[key] || '').toString()
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Send untagged ID response
|
|
85
|
+
connection.send(
|
|
86
|
+
{
|
|
87
|
+
tag: '*',
|
|
88
|
+
command: 'ID',
|
|
89
|
+
attributes: [serverList]
|
|
90
|
+
},
|
|
91
|
+
'ID',
|
|
92
|
+
parsed,
|
|
93
|
+
data,
|
|
94
|
+
clientList
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
// Send tagged response
|
|
98
|
+
connection.send(
|
|
99
|
+
{
|
|
100
|
+
tag: parsed.tag,
|
|
101
|
+
command: 'OK',
|
|
102
|
+
attributes: [
|
|
103
|
+
{
|
|
104
|
+
type: 'TEXT',
|
|
105
|
+
value: 'ID command completed'
|
|
106
|
+
}
|
|
107
|
+
]
|
|
108
|
+
},
|
|
109
|
+
'ID',
|
|
110
|
+
parsed,
|
|
111
|
+
data,
|
|
112
|
+
clientList
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
callback();
|
|
116
|
+
},
|
|
117
|
+
{ states: states.ANY }
|
|
118
|
+
);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
function sendError(message, connection, parsed, data, callback) {
|
|
122
|
+
connection.send(
|
|
123
|
+
{
|
|
124
|
+
tag: parsed.tag,
|
|
125
|
+
command: 'BAD',
|
|
126
|
+
attributes: [
|
|
127
|
+
{
|
|
128
|
+
type: 'TEXT',
|
|
129
|
+
value: message
|
|
130
|
+
}
|
|
131
|
+
]
|
|
132
|
+
},
|
|
133
|
+
'INVALID COMMAND',
|
|
134
|
+
parsed,
|
|
135
|
+
data
|
|
136
|
+
);
|
|
137
|
+
return callback();
|
|
138
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { states } = require('../command-states');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @help Adds IDLE [RFC2177] capability
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
module.exports = function (server) {
|
|
10
|
+
server.registerCapability('IDLE');
|
|
11
|
+
|
|
12
|
+
server.setCommandHandler(
|
|
13
|
+
'IDLE',
|
|
14
|
+
(connection, parsed, data, callback) => {
|
|
15
|
+
const idleTimer = setTimeout(
|
|
16
|
+
() => {
|
|
17
|
+
if (connection.socket && !connection.socket.destroyed) {
|
|
18
|
+
connection.send(
|
|
19
|
+
{
|
|
20
|
+
tag: '*',
|
|
21
|
+
command: 'BYE',
|
|
22
|
+
attributes: [
|
|
23
|
+
{
|
|
24
|
+
type: 'TEXT',
|
|
25
|
+
value: 'IDLE terminated'
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
'IDLE EXPIRED',
|
|
30
|
+
parsed,
|
|
31
|
+
data
|
|
32
|
+
);
|
|
33
|
+
connection.end();
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
30 * 60 * 1000
|
|
37
|
+
);
|
|
38
|
+
// An idling client should not keep the process alive
|
|
39
|
+
idleTimer.unref();
|
|
40
|
+
|
|
41
|
+
const clearIdleTimer = () => clearTimeout(idleTimer);
|
|
42
|
+
const socket = connection.socket;
|
|
43
|
+
if (socket) {
|
|
44
|
+
socket.once('close', clearIdleTimer);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
connection.directNotifications = true;
|
|
48
|
+
|
|
49
|
+
// Temporarily redirect client input to this function
|
|
50
|
+
connection.inputHandler = function (str) {
|
|
51
|
+
clearIdleTimer();
|
|
52
|
+
if (socket) {
|
|
53
|
+
socket.removeListener('close', clearIdleTimer);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Stop listening to any other user input
|
|
57
|
+
connection.inputHandler = false;
|
|
58
|
+
|
|
59
|
+
// Notifications are sent again only while a command is in progress
|
|
60
|
+
connection.directNotifications = false;
|
|
61
|
+
|
|
62
|
+
if (str.toUpperCase() === 'DONE') {
|
|
63
|
+
connection.send(
|
|
64
|
+
{
|
|
65
|
+
tag: parsed.tag,
|
|
66
|
+
command: 'OK',
|
|
67
|
+
attributes: [
|
|
68
|
+
{
|
|
69
|
+
type: 'TEXT',
|
|
70
|
+
value: 'IDLE terminated'
|
|
71
|
+
}
|
|
72
|
+
]
|
|
73
|
+
},
|
|
74
|
+
'IDLE',
|
|
75
|
+
parsed,
|
|
76
|
+
data
|
|
77
|
+
);
|
|
78
|
+
} else {
|
|
79
|
+
connection.send(
|
|
80
|
+
{
|
|
81
|
+
tag: parsed.tag,
|
|
82
|
+
command: 'BAD',
|
|
83
|
+
attributes: [
|
|
84
|
+
{
|
|
85
|
+
type: 'TEXT',
|
|
86
|
+
value: 'Invalid Idle continuation'
|
|
87
|
+
}
|
|
88
|
+
]
|
|
89
|
+
},
|
|
90
|
+
'INVALID IDLE',
|
|
91
|
+
parsed,
|
|
92
|
+
data
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
connection.write('+ idling\r\n');
|
|
98
|
+
|
|
99
|
+
connection.processNotifications();
|
|
100
|
+
|
|
101
|
+
return callback();
|
|
102
|
+
},
|
|
103
|
+
{ states: states.AUTHENTICATED, noArguments: true }
|
|
104
|
+
);
|
|
105
|
+
};
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { updateSession } = require('../utf8-session');
|
|
4
|
+
const literalMinus = require('./literalminus');
|
|
5
|
+
const { registerEnable, isEnabled } = require('./enable');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @help Adds IMAP4rev2 [RFC9051] capability next to IMAP4rev1 and loads
|
|
9
|
+
* @help the extensions folded into IMAP4rev2 (RFC 9051 Appendix E).
|
|
10
|
+
* @help A session follows RFC 9051 after ENABLE IMAP4rev2, others RFC 3501
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// RFC 9051 Appendix A: a client that wants IMAP4rev2 when both revisions are advertised MUST issue "ENABLE IMAP4rev2".
|
|
14
|
+
// The ENABLE plugin lists enabled extensions in this spelling
|
|
15
|
+
const REV2 = 'IMAP4rev2';
|
|
16
|
+
|
|
17
|
+
// RFC 9051 section 9 leaves these IMAP4rev1 items out of the search-key, status-att and fetch-att rules (Appendix E
|
|
18
|
+
// items 12 and 18), so an IMAP4rev2 session gets BAD for them
|
|
19
|
+
const REMOVED_SEARCH_KEYS = new Set(['NEW', 'OLD', 'RECENT', 'UNRECENT']);
|
|
20
|
+
const REMOVED_STATUS_ITEMS = new Set(['RECENT']);
|
|
21
|
+
// RFC 9051 section 6.3.11 and Appendix E item 3: STATUS DELETED is new in IMAP4rev2 (STATUS SIZE comes with STATUS=SIZE)
|
|
22
|
+
const ADDED_STATUS_ITEMS = new Set(['DELETED']);
|
|
23
|
+
const REMOVED_FETCH_ITEMS = new Set(['RFC822', 'RFC822.HEADER', 'RFC822.TEXT']);
|
|
24
|
+
|
|
25
|
+
// RFC 9051 section 2.3.2: keywords that SHOULD be allowed in SEARCH and preserved in APPEND, COPY and MOVE
|
|
26
|
+
const KEYWORDS = ['$Forwarded', '$MDNSent', '$Junk', '$NotJunk', '$Phishing'];
|
|
27
|
+
|
|
28
|
+
// set by setMode() below
|
|
29
|
+
const isRev2 = connection => !!connection.imap4rev2;
|
|
30
|
+
|
|
31
|
+
const atomName = attr => (attr && attr.type === 'ATOM' ? String(attr.value).toUpperCase() : '');
|
|
32
|
+
|
|
33
|
+
module.exports = function (server) {
|
|
34
|
+
// RFC 9051 Appendix A: advertised together with IMAP4rev1 (capability.js always lists it), so every session
|
|
35
|
+
// starts as IMAP4rev1 (section 7.2.2) and ENABLE switches it (section 6.3.1)
|
|
36
|
+
server.registerCapability('IMAP4rev2');
|
|
37
|
+
|
|
38
|
+
registerEnable(server, REV2);
|
|
39
|
+
|
|
40
|
+
// RFC 9051 section 4.3 and Appendix E item 2: non-synchronizing literals up to 4096 octets (LITERAL-). LITERAL+
|
|
41
|
+
// allows more, so it is kept if it is loaded already, and it can replace this LITERAL- when it is loaded later
|
|
42
|
+
if (!server.capabilities['LITERAL+'] && !server.capabilities['LITERAL-']) {
|
|
43
|
+
literalMinus(server);
|
|
44
|
+
server.impliedLiteralMinus = true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// RFC 9051 section 2.3.2: a mailbox that does not allow new keywords (no "\*" in PERMANENTFLAGS) still keeps these
|
|
48
|
+
server.mailboxHandlers.push((server, mailbox) => {
|
|
49
|
+
if (!mailbox.allowPermanentFlags) {
|
|
50
|
+
KEYWORDS.forEach(keyword => server.ensureFlag(mailbox.permanentFlags, keyword));
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// The session follows RFC 9051 once IMAP4rev2 is enabled
|
|
55
|
+
const setMode = connection => {
|
|
56
|
+
const rev2 = (connection.imap4rev2 = isEnabled(connection, REV2));
|
|
57
|
+
// UTF-8 in quoted strings and mailbox names (sections 4.3 and 5.1), SEARCH assumes UTF-8 (section 6.4.4)
|
|
58
|
+
updateSession(connection);
|
|
59
|
+
// message/global encapsulates a message like message/rfc822 (sections 6.4.5.1 and 7.5.2)
|
|
60
|
+
connection.messageGlobal = rev2;
|
|
61
|
+
connection.disabledSearchKeys = rev2 ? REMOVED_SEARCH_KEYS : null;
|
|
62
|
+
// literal sizes, partial ranges (parsed by imap-handler), LARGER and SMALLER are number64 (section 9, Appendix E item 1)
|
|
63
|
+
connection.number64 = connection.parserOptions.number64 = rev2;
|
|
64
|
+
connection.disabledStatusItems = rev2 ? REMOVED_STATUS_ITEMS : null;
|
|
65
|
+
// only for this session, an IMAP4rev1 session gets DELETED only from plugins that add it for everyone (QUOTA)
|
|
66
|
+
connection.addedStatusItems = rev2 ? ADDED_STATUS_ITEMS : null;
|
|
67
|
+
if (rev2) {
|
|
68
|
+
// \Recent is deprecated (section 2.3.2, Appendix E item 12), an IMAP4rev2 session does not see it
|
|
69
|
+
connection.getFlags = message => message.flags;
|
|
70
|
+
} else {
|
|
71
|
+
// back to IMAPConnection#getFlags
|
|
72
|
+
delete connection.getFlags;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
server.connectionHandlers.push(setMode);
|
|
76
|
+
// RFC 8437 section 4.1: UNAUTHENTICATE turns ENABLEd extensions off
|
|
77
|
+
server.resetHandlers.push(setMode);
|
|
78
|
+
|
|
79
|
+
const sendBad = (connection, parsed, data, callback, text) => {
|
|
80
|
+
connection.sendStatus(parsed, data, 'BAD', text);
|
|
81
|
+
return callback();
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// Wraps the handler of a command, `wrapper(prevHandler, connection, parsed, data, callback)` runs instead
|
|
85
|
+
const wrap = (command, wrapper) => {
|
|
86
|
+
const prevHandler = server.getCommandHandler(command);
|
|
87
|
+
server.setCommandHandler(command, (connection, parsed, data, callback) => wrapper(prevHandler, connection, parsed, data, callback));
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// Appendix E item 17: CHECK was removed, NOOP does the same. Item 19: LSUB was deprecated and left out of the
|
|
91
|
+
// section 9 grammar, LIST (SUBSCRIBED) replaces it
|
|
92
|
+
Object.entries({ CHECK: 'NOOP', LSUB: 'LIST (SUBSCRIBED)' }).forEach(([command, replacement]) => {
|
|
93
|
+
wrap(command, (prevHandler, connection, parsed, data, callback) => {
|
|
94
|
+
if (isRev2(connection)) {
|
|
95
|
+
return sendBad(connection, parsed, data, callback, command + ' is not part of IMAP4rev2, use ' + replacement + ' (RFC 9051 Appendix E)');
|
|
96
|
+
}
|
|
97
|
+
prevHandler(connection, parsed, data, callback);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Appendix E item 18: RFC822, RFC822.HEADER and RFC822.TEXT are not fetch-att items any more, BODY[], BODY.PEEK[HEADER]
|
|
102
|
+
// and BODY[TEXT] replace them. RFC822.SIZE stays
|
|
103
|
+
const fetchWrapper = (prevHandler, connection, parsed, data, callback) => {
|
|
104
|
+
const items = (parsed.attributes || [])[1];
|
|
105
|
+
if (isRev2(connection) && [].concat(items || []).some(item => REMOVED_FETCH_ITEMS.has(atomName(item)))) {
|
|
106
|
+
return sendBad(
|
|
107
|
+
connection,
|
|
108
|
+
parsed,
|
|
109
|
+
data,
|
|
110
|
+
callback,
|
|
111
|
+
'RFC822, RFC822.HEADER and RFC822.TEXT are not part of IMAP4rev2, use BODY[] (RFC 9051 Appendix E)'
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
prevHandler(connection, parsed, data, callback);
|
|
115
|
+
};
|
|
116
|
+
wrap('FETCH', fetchWrapper);
|
|
117
|
+
wrap('UID FETCH', fetchWrapper);
|
|
118
|
+
|
|
119
|
+
// Appendix E item 4 and section 6.4.4: SEARCH answers with ESEARCH, without result options it is RETURN (ALL).
|
|
120
|
+
// The ESEARCH plugin replaces the SEARCH response once `parsed.searchReturn` is set
|
|
121
|
+
const searchWrapper = (prevHandler, connection, parsed, data, callback) => {
|
|
122
|
+
if (isRev2(connection) && atomName((parsed.attributes || [])[0]) !== 'RETURN') {
|
|
123
|
+
parsed.searchReturn = server.parseSearchReturn([], connection, parsed);
|
|
124
|
+
}
|
|
125
|
+
prevHandler(connection, parsed, data, callback);
|
|
126
|
+
};
|
|
127
|
+
wrap('SEARCH', searchWrapper);
|
|
128
|
+
wrap('UID SEARCH', searchWrapper);
|
|
129
|
+
|
|
130
|
+
// Appendix E item 9 and section 6.3.2: SELECT and EXAMINE report CLOSED when they close the selected mailbox
|
|
131
|
+
server.closedChecks.push(isRev2);
|
|
132
|
+
|
|
133
|
+
// Untagged LIST response for the selected mailbox (section 6.3.2, Appendix E item 10), with accurate attributes.
|
|
134
|
+
// Not a "LIST ITEM" response, that ACL would hide for a mailbox the user may read but not list
|
|
135
|
+
const sendMailboxList = (connection, mailbox, parsed, data) => {
|
|
136
|
+
// the special-use attributes of SPECIAL-USE are mailbox attributes in IMAP4rev2 (section 7.3.1)
|
|
137
|
+
const attributes = mailbox.flags.concat(mailbox['special-use'] || []);
|
|
138
|
+
connection.send(
|
|
139
|
+
{
|
|
140
|
+
tag: '*',
|
|
141
|
+
command: 'LIST',
|
|
142
|
+
attributes: [
|
|
143
|
+
attributes.map(flag => ({ type: 'ATOM', value: flag })),
|
|
144
|
+
server.getSeparator(mailbox),
|
|
145
|
+
// the canonical name, converted for the session in IMAPConnection#send
|
|
146
|
+
{ type: 'MAILBOX', value: mailbox.path }
|
|
147
|
+
]
|
|
148
|
+
},
|
|
149
|
+
'SELECT LIST',
|
|
150
|
+
parsed,
|
|
151
|
+
data,
|
|
152
|
+
mailbox
|
|
153
|
+
);
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
server.outputHandlers.push((connection, response, description, parsed, data) => {
|
|
157
|
+
if (!parsed || !response || !isRev2(connection)) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const command = String(parsed.command || '').toUpperCase();
|
|
161
|
+
if (command !== 'SELECT' && command !== 'EXAMINE') {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Appendix E items 11 and 12: the RECENT response and the UNSEEN response code are deprecated
|
|
166
|
+
if (description === command + ' RECENT' || description === command + ' UNSEEN') {
|
|
167
|
+
response.skipResponse = true;
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (description === command && response.tag === parsed.tag && response.command === 'OK' && connection.selectedMailbox) {
|
|
172
|
+
sendMailboxList(connection, connection.selectedMailbox, parsed, data);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
server.outputHandlers.push((connection, response, description, parsed, data, extra) => {
|
|
177
|
+
if (description === 'ENABLED' && Array.isArray(extra) && extra.indexOf(REV2) >= 0) {
|
|
178
|
+
setMode(connection);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
// RFC 9051 Appendix E item 2 lists the extensions that IMAP4rev2 folds in. With them the plugins advertise their own
|
|
184
|
+
// capabilities, which RFC 9051 Appendix A suggests for a server that supports both revisions. AUTH=PLAIN is a MUST
|
|
185
|
+
// (section 6.1.1). LITERAL- is loaded by the plugin itself, unless LITERAL+ is loaded
|
|
186
|
+
module.exports.requires = [
|
|
187
|
+
'ENABLE',
|
|
188
|
+
'NAMESPACE',
|
|
189
|
+
'UNSELECT',
|
|
190
|
+
'UIDPLUS',
|
|
191
|
+
'ESEARCH',
|
|
192
|
+
'SEARCHRES',
|
|
193
|
+
'IDLE',
|
|
194
|
+
'SASL-IR',
|
|
195
|
+
'LIST-EXTENDED',
|
|
196
|
+
'LIST-STATUS',
|
|
197
|
+
'MOVE',
|
|
198
|
+
'BINARY',
|
|
199
|
+
'SPECIAL-USE',
|
|
200
|
+
'STATUS=SIZE',
|
|
201
|
+
'AUTH=PLAIN'
|
|
202
|
+
];
|