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,233 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const net = require('net');
|
|
4
|
+
const tls = require('tls');
|
|
5
|
+
const { splitResponses, splitAtLiterals } = require('./framing');
|
|
6
|
+
const DeflateLayer = require('./deflate-layer');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @namespace Mockup module
|
|
10
|
+
* @name mockup
|
|
11
|
+
*/
|
|
12
|
+
module.exports = runClientMockup;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* <p>Runs a batch of IMAP commands against a server, like a well behaved client would</p>
|
|
16
|
+
*
|
|
17
|
+
* <ul>
|
|
18
|
+
* <li>a command is sent only after the tagged response of the previous command arrived
|
|
19
|
+
* (RFC 3501 section 5.5 forbids pipelining in many cases)</li>
|
|
20
|
+
* <li>the data of a synchronizing literal ({n}) is sent only after the server's "+"
|
|
21
|
+
* continuation request (RFC 3501 section 4.3)</li>
|
|
22
|
+
* <li>when the server sends a "+" continuation request and the command has no literal
|
|
23
|
+
* data left, the next entry of the command list is sent as the continuation data
|
|
24
|
+
* (an AUTHENTICATE response, or DONE to end IDLE)</li>
|
|
25
|
+
* <li>after the tagged response of a STARTTLS command the connection is upgraded to TLS</li>
|
|
26
|
+
* <li>after the tagged OK of a COMPRESS DEFLATE command, data in both directions is compressed
|
|
27
|
+
* (RFC 4978). The client ends its compression after an UNAUTHENTICATE command, and expects
|
|
28
|
+
* the server to end its compression after the tagged OK (RFC 8437 section 4.1)</li>
|
|
29
|
+
* <li>the callback gets the decompressed data</li>
|
|
30
|
+
* <li>after the last command the connection is closed and the callback gets the transcript</li>
|
|
31
|
+
* </ul>
|
|
32
|
+
*
|
|
33
|
+
* <pre>
|
|
34
|
+
* var cmds = ["A1 CAPABILITY", "A2 STARTTLS", "A3 LOGIN username password", "A4 LOGOUT"];
|
|
35
|
+
* runClientMockup(143, "localhost", cmds, false, function(resp){
|
|
36
|
+
* console.log("Final:", resp.toString("utf-8").trim());
|
|
37
|
+
* });
|
|
38
|
+
* </pre>
|
|
39
|
+
*
|
|
40
|
+
* @memberOf mockup
|
|
41
|
+
* @param {Number} port Port number
|
|
42
|
+
* @param {String} host Hostname to connect to
|
|
43
|
+
* @param {Array} commands Command list to be sent to server, as binary strings
|
|
44
|
+
* @param {Boolean} [debug] if set to true log all input/output
|
|
45
|
+
* @param {Function} callback Callback function to run on completion, gets everything the server sent as a Buffer
|
|
46
|
+
*/
|
|
47
|
+
function runClientMockup(port, host, commands, debug, callback) {
|
|
48
|
+
host = host || 'localhost';
|
|
49
|
+
port = port || 143;
|
|
50
|
+
const queue = Array.isArray(commands) ? splitLiterals(commands) : [];
|
|
51
|
+
|
|
52
|
+
const responses = [];
|
|
53
|
+
let buffer = '';
|
|
54
|
+
// tag of the command waiting for its tagged response
|
|
55
|
+
let currentTag = null;
|
|
56
|
+
let currentCommand = '';
|
|
57
|
+
let lastResponse = '';
|
|
58
|
+
let greeted = false;
|
|
59
|
+
let finished = false;
|
|
60
|
+
|
|
61
|
+
let socket = net.connect(port, host);
|
|
62
|
+
// COMPRESS=DEFLATE layer, once the server accepted COMPRESS
|
|
63
|
+
let layer = null;
|
|
64
|
+
|
|
65
|
+
const log = (prefix, str) => {
|
|
66
|
+
if (debug) {
|
|
67
|
+
console.log(prefix + ' ' + str.replace(/\r?\n$/, ''));
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const write = text => {
|
|
72
|
+
log('C:', text);
|
|
73
|
+
if (layer) {
|
|
74
|
+
layer.write(Buffer.from(text, 'binary'));
|
|
75
|
+
} else {
|
|
76
|
+
socket.write(text, 'binary');
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const finish = () => {
|
|
81
|
+
if (finished) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
finished = true;
|
|
85
|
+
if (buffer) {
|
|
86
|
+
// an incomplete response
|
|
87
|
+
responses.push(Buffer.from(buffer, 'binary'));
|
|
88
|
+
}
|
|
89
|
+
if (layer) {
|
|
90
|
+
layer.destroy();
|
|
91
|
+
}
|
|
92
|
+
if (typeof callback === 'function') {
|
|
93
|
+
callback(Buffer.concat(responses));
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const sendNext = () => {
|
|
98
|
+
// literal data that the previous command did not get to send is dropped
|
|
99
|
+
while (queue.length && queue[0].continuation) {
|
|
100
|
+
queue.shift();
|
|
101
|
+
}
|
|
102
|
+
if (!queue.length) {
|
|
103
|
+
currentTag = null;
|
|
104
|
+
socket.end();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const next = queue.shift();
|
|
108
|
+
currentCommand = next.text.replace(/\r\n$/, '');
|
|
109
|
+
currentTag = currentCommand.split(' ').shift();
|
|
110
|
+
write(next.text);
|
|
111
|
+
if (layer && /^\S+ UNAUTHENTICATE$/i.test(currentCommand)) {
|
|
112
|
+
// the client ends its compression after the CRLF of UNAUTHENTICATE (RFC 8437 section 4.1)
|
|
113
|
+
layer.end();
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// literal data for the current command, or a continuation line such as DONE
|
|
118
|
+
const onContinuation = () => {
|
|
119
|
+
if (!queue.length) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
write(queue.shift().text);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const upgrade = () => {
|
|
126
|
+
const plain = socket;
|
|
127
|
+
plain.removeAllListeners('data');
|
|
128
|
+
plain.removeAllListeners('close');
|
|
129
|
+
socket = tls.connect({ socket: plain, host, rejectUnauthorized: false }, () => {
|
|
130
|
+
log('TLS', 'connection secured');
|
|
131
|
+
sendNext();
|
|
132
|
+
});
|
|
133
|
+
attach(socket);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// everything after the CRLF of the tagged OK is compressed (RFC 4978 section 3)
|
|
137
|
+
const startCompression = () => {
|
|
138
|
+
layer = new DeflateLayer({
|
|
139
|
+
writeRaw: chunk => socket.write(chunk),
|
|
140
|
+
onData: onData,
|
|
141
|
+
onError: err => {
|
|
142
|
+
log('Compression error:', err.message);
|
|
143
|
+
socket.destroy();
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// returns true if compression starts after this response
|
|
149
|
+
const onTagged = () => {
|
|
150
|
+
let compress = false;
|
|
151
|
+
if (/^\S+ OK/i.test(lastResponse)) {
|
|
152
|
+
if (/^\S+ STARTTLS$/i.test(currentCommand)) {
|
|
153
|
+
upgrade();
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
if (/^\S+ COMPRESS DEFLATE$/i.test(currentCommand)) {
|
|
157
|
+
startCompression();
|
|
158
|
+
compress = true;
|
|
159
|
+
} else if (layer && /^\S+ UNAUTHENTICATE$/i.test(currentCommand)) {
|
|
160
|
+
// the server ends its compression after the CRLF of the OK
|
|
161
|
+
layer.endInput();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
sendNext();
|
|
165
|
+
return compress;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// Splits the received data into complete responses, a response may contain literals
|
|
169
|
+
const processBuffer = () => {
|
|
170
|
+
const framed = splitResponses(buffer);
|
|
171
|
+
for (const response of framed.responses) {
|
|
172
|
+
lastResponse = buffer.slice(response.start, response.end);
|
|
173
|
+
responses.push(Buffer.from(lastResponse, 'binary'));
|
|
174
|
+
|
|
175
|
+
if (!greeted) {
|
|
176
|
+
greeted = true;
|
|
177
|
+
sendNext();
|
|
178
|
+
} else if (lastResponse.charAt(0) === '+') {
|
|
179
|
+
onContinuation();
|
|
180
|
+
} else if (currentTag && lastResponse.substr(0, currentTag.length + 1) === currentTag + ' ' && onTagged()) {
|
|
181
|
+
// compression started, the rest of the data is compressed
|
|
182
|
+
const rest = buffer.slice(response.end);
|
|
183
|
+
buffer = '';
|
|
184
|
+
if (rest) {
|
|
185
|
+
layer.receive(Buffer.from(rest, 'binary'));
|
|
186
|
+
}
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
buffer = buffer.slice(framed.end);
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
function onData(chunk) {
|
|
194
|
+
log('S:', chunk.toString('binary'));
|
|
195
|
+
buffer += chunk.toString('binary');
|
|
196
|
+
processBuffer();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function attach(sock) {
|
|
200
|
+
sock.on('data', chunk => {
|
|
201
|
+
if (layer) {
|
|
202
|
+
layer.receive(chunk);
|
|
203
|
+
} else {
|
|
204
|
+
onData(chunk);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
sock.on('error', err => {
|
|
208
|
+
log('Socket error:', err.message);
|
|
209
|
+
// the close event follows and returns what was received so far
|
|
210
|
+
});
|
|
211
|
+
// the last data may still be in the decompressor
|
|
212
|
+
sock.on('close', () => (layer ? layer.whenIdle(finish) : finish()));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
attach(socket);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Splits commands at synchronizing literals, so that literal data can wait for the
|
|
220
|
+
* continuation request
|
|
221
|
+
*
|
|
222
|
+
* @param {Array} commands Command strings
|
|
223
|
+
* @return {Array} Parts to send, as {text, continuation}, the last part of a command ends with CRLF
|
|
224
|
+
*/
|
|
225
|
+
function splitLiterals(commands) {
|
|
226
|
+
const parts = [];
|
|
227
|
+
commands.forEach(command => {
|
|
228
|
+
splitAtLiterals(String(command) + '\r\n').forEach((text, i) => {
|
|
229
|
+
parts.push({ text, continuation: i > 0 });
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
return parts;
|
|
233
|
+
}
|
package/lib/numbers.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Limits and checks of the numbers in the IMAP grammar
|
|
4
|
+
|
|
5
|
+
// RFC 3501 and RFC 9051 section 9: number and nz-number are unsigned 32-bit integers
|
|
6
|
+
const MAX_NUMBER = 4294967295;
|
|
7
|
+
// RFC 9051 section 9: number64 is an unsigned 63-bit integer, so is mod-sequence-value (RFC 7162 section 7)
|
|
8
|
+
const MAX_NUMBER64 = 2n ** 63n - 1n;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Checks if a value is a number by the grammar: 1*DIGIT, up to a limit
|
|
12
|
+
*
|
|
13
|
+
* @param {*} value Value to check, only strings can match
|
|
14
|
+
* @param {Number|BigInt} [max] Largest allowed value, MAX_NUMBER by default
|
|
15
|
+
* @return {Boolean} true if the value is a number within the limit
|
|
16
|
+
*/
|
|
17
|
+
function isNumber(value, max) {
|
|
18
|
+
const limit = String(max === undefined ? MAX_NUMBER : max);
|
|
19
|
+
// a longer string of digits is too large anyway, so no BigInt is built from it
|
|
20
|
+
if (typeof value !== 'string' || !/^\d+$/.test(value) || value.replace(/^0+(?=\d)/, '').length > limit.length) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
return BigInt(value) <= BigInt(limit);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Checks if a value is a non-zero number (nz-number, nz-number64): digit-nz *DIGIT, up to a limit
|
|
28
|
+
*
|
|
29
|
+
* @param {*} value Value to check, only strings can match
|
|
30
|
+
* @param {Number|BigInt} [max] Largest allowed value, MAX_NUMBER by default
|
|
31
|
+
* @return {Boolean} true if the value is a non-zero number within the limit
|
|
32
|
+
*/
|
|
33
|
+
function isNzNumber(value, max) {
|
|
34
|
+
return isNumber(value, max) && /^[1-9]/.test(value);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Checks if a value is a sequence set (RFC 3501 and RFC 9051 section 9): seq-number or seq-range items
|
|
39
|
+
* separated by commas, seq-number = nz-number / "*"
|
|
40
|
+
*
|
|
41
|
+
* @param {*} value Value to check, only strings can match
|
|
42
|
+
* @param {Boolean} [noStar] If true, "*" is not allowed (e.g. known-uids of RFC 7162 section 7)
|
|
43
|
+
* @return {Boolean} true if the value is a valid sequence set
|
|
44
|
+
*/
|
|
45
|
+
function isSequenceSet(value, noStar) {
|
|
46
|
+
if (typeof value !== 'string' || !/^[\d*]+(:[\d*]+)?(,[\d*]+(:[\d*]+)?)*$/.test(value)) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
return value.split(/[,:]/).every(part => (part === '*' ? !noStar : isNzNumber(part)));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = { MAX_NUMBER, MAX_NUMBER64, isNumber, isNzNumber, isSequenceSet };
|