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.
Files changed (114) hide show
  1. package/LICENSE +16 -0
  2. package/README.md +608 -2
  3. package/bin/help.txt +98 -0
  4. package/bin/imapkit.js +108 -0
  5. package/cert/server.crt +20 -0
  6. package/cert/server.key +28 -0
  7. package/lib/addressparser.js +283 -0
  8. package/lib/arguments.js +112 -0
  9. package/lib/bodystructure.js +149 -0
  10. package/lib/command-states.js +109 -0
  11. package/lib/commands/append.js +313 -0
  12. package/lib/commands/capability.js +47 -0
  13. package/lib/commands/check.js +21 -0
  14. package/lib/commands/close.js +30 -0
  15. package/lib/commands/copy.js +115 -0
  16. package/lib/commands/create.js +52 -0
  17. package/lib/commands/delete.js +64 -0
  18. package/lib/commands/examine.js +7 -0
  19. package/lib/commands/expunge.js +27 -0
  20. package/lib/commands/fetch.js +229 -0
  21. package/lib/commands/handlers/fetch.js +209 -0
  22. package/lib/commands/handlers/flags.js +42 -0
  23. package/lib/commands/handlers/search.js +519 -0
  24. package/lib/commands/handlers/status.js +85 -0
  25. package/lib/commands/handlers/store.js +127 -0
  26. package/lib/commands/list.js +100 -0
  27. package/lib/commands/login.js +67 -0
  28. package/lib/commands/logout.js +41 -0
  29. package/lib/commands/lsub.js +87 -0
  30. package/lib/commands/noop.js +21 -0
  31. package/lib/commands/rename.js +102 -0
  32. package/lib/commands/search.js +76 -0
  33. package/lib/commands/select.js +289 -0
  34. package/lib/commands/status.js +63 -0
  35. package/lib/commands/store.js +151 -0
  36. package/lib/commands/subscribe.js +53 -0
  37. package/lib/commands/uid copy.js +7 -0
  38. package/lib/commands/uid fetch.js +5 -0
  39. package/lib/commands/uid search.js +5 -0
  40. package/lib/commands/uid store.js +5 -0
  41. package/lib/commands/unsubscribe.js +50 -0
  42. package/lib/dates.js +123 -0
  43. package/lib/deflate-layer.js +232 -0
  44. package/lib/envelope.js +82 -0
  45. package/lib/esearch.js +208 -0
  46. package/lib/framing.js +102 -0
  47. package/lib/list-extensions.js +36 -0
  48. package/lib/load-plugins.js +109 -0
  49. package/lib/mailbox-name.js +133 -0
  50. package/lib/mimeparser.js +778 -0
  51. package/lib/mock-client.js +233 -0
  52. package/lib/numbers.js +52 -0
  53. package/lib/plugins/acl.js +964 -0
  54. package/lib/plugins/appendlimit.js +83 -0
  55. package/lib/plugins/auth-plain.js +94 -0
  56. package/lib/plugins/binary.js +256 -0
  57. package/lib/plugins/catenate.js +253 -0
  58. package/lib/plugins/compress.js +76 -0
  59. package/lib/plugins/condstore.js +563 -0
  60. package/lib/plugins/context-search.js +321 -0
  61. package/lib/plugins/context-sort.js +19 -0
  62. package/lib/plugins/create-special-use.js +108 -0
  63. package/lib/plugins/enable.js +155 -0
  64. package/lib/plugins/esearch.js +156 -0
  65. package/lib/plugins/esort.js +60 -0
  66. package/lib/plugins/id.js +138 -0
  67. package/lib/plugins/idle.js +105 -0
  68. package/lib/plugins/imap4rev2.js +202 -0
  69. package/lib/plugins/list-extended.js +258 -0
  70. package/lib/plugins/list-status.js +31 -0
  71. package/lib/plugins/literalminus.js +20 -0
  72. package/lib/plugins/literalplus.js +18 -0
  73. package/lib/plugins/logindisabled.js +50 -0
  74. package/lib/plugins/messagelimit.js +234 -0
  75. package/lib/plugins/metadata-server.js +13 -0
  76. package/lib/plugins/metadata.js +475 -0
  77. package/lib/plugins/move.js +110 -0
  78. package/lib/plugins/multiappend.js +26 -0
  79. package/lib/plugins/multisearch.js +269 -0
  80. package/lib/plugins/namespace.js +67 -0
  81. package/lib/plugins/notify.js +654 -0
  82. package/lib/plugins/oauthbearer.js +217 -0
  83. package/lib/plugins/objectid.js +243 -0
  84. package/lib/plugins/partial.js +68 -0
  85. package/lib/plugins/preview.js +400 -0
  86. package/lib/plugins/qresync.js +525 -0
  87. package/lib/plugins/quota.js +285 -0
  88. package/lib/plugins/replace.js +145 -0
  89. package/lib/plugins/sasl-ir.js +12 -0
  90. package/lib/plugins/savedate.js +59 -0
  91. package/lib/plugins/savelimit.js +18 -0
  92. package/lib/plugins/searchres.js +82 -0
  93. package/lib/plugins/sort-display.js +23 -0
  94. package/lib/plugins/sort.js +132 -0
  95. package/lib/plugins/special-use.js +95 -0
  96. package/lib/plugins/starttls.js +57 -0
  97. package/lib/plugins/status-size.js +19 -0
  98. package/lib/plugins/thread-orderedsubject.js +16 -0
  99. package/lib/plugins/thread-references.js +16 -0
  100. package/lib/plugins/uidonly.js +135 -0
  101. package/lib/plugins/uidplus.js +124 -0
  102. package/lib/plugins/unauthenticate.js +28 -0
  103. package/lib/plugins/unselect.js +36 -0
  104. package/lib/plugins/utf8-accept.js +68 -0
  105. package/lib/plugins/x-gm-ext-1.js +456 -0
  106. package/lib/plugins/xoauth2.js +188 -0
  107. package/lib/plugins/xtoybird.js +282 -0
  108. package/lib/server.js +2880 -0
  109. package/lib/smtp-listener.js +51 -0
  110. package/lib/sorting.js +373 -0
  111. package/lib/threading.js +357 -0
  112. package/lib/utf8-session.js +123 -0
  113. package/lib/vanished.js +57 -0
  114. 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 };