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
package/lib/framing.js ADDED
@@ -0,0 +1,102 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * IMAP framing helpers shared by the mock client, the test helpers and the compare tool.
5
+ * Both functions take a Buffer or a binary string (one character per octet).
6
+ */
7
+
8
+ const LF = 0x0a;
9
+ const CR = 0x0d;
10
+
11
+ /**
12
+ * Splits received server data into responses. A response is one line, or a line that ends with a
13
+ * literal marker `{n}` or `~{n}` (RFC 3516 literal8), followed by n octets and the rest of the
14
+ * response. Lenient: lines end at LF, a CR before it is not required. Callers that need strict
15
+ * framing check the line ends themselves.
16
+ *
17
+ * @param {Buffer|String} data Received data, possibly ending with an incomplete response
18
+ * @return {Object} `{ responses, end, incomplete }`. Every response is `{ start, end, lines, literals }`,
19
+ * where `lines` lists `{ start, end, lf }` (end excludes the CR before the LF, `lf` is the
20
+ * offset of the LF) and `literals` lists `{ start, end, literal8 }`. `end` is the offset right
21
+ * after the last complete response. `incomplete` is false when all data was used, otherwise
22
+ * `{ reason, lines }` with the complete lines of the unfinished response, where reason is
23
+ * 'line' for a line without LF, 'literal' for literal data that is cut short (with `size`
24
+ * and `available`), or 'response' when the data ends right after literal data.
25
+ */
26
+ function splitResponses(data) {
27
+ const isBuffer = Buffer.isBuffer(data);
28
+ const length = data.length;
29
+ const charAt = isBuffer ? i => data[i] : i => data.charCodeAt(i);
30
+ const slice = isBuffer ? (start, end) => data.toString('binary', start, end) : (start, end) => data.slice(start, end);
31
+
32
+ const responses = [];
33
+ let pos = 0;
34
+ let end = 0;
35
+ let current = null;
36
+
37
+ while (pos < length) {
38
+ const lf = data.indexOf(isBuffer ? LF : '\n', pos);
39
+ if (lf < 0) {
40
+ return { responses, end, incomplete: { reason: 'line', lines: current ? current.lines : [] } };
41
+ }
42
+
43
+ const lineStart = pos;
44
+ const lineEnd = lf > pos && charAt(lf - 1) === CR ? lf - 1 : lf;
45
+ if (!current) {
46
+ current = { start: pos, end: 0, lines: [], literals: [] };
47
+ }
48
+ current.lines.push({ start: lineStart, end: lineEnd, lf });
49
+ pos = lf + 1;
50
+
51
+ // only lines that end with "}" can carry a literal marker, so most lines are not sliced
52
+ const marker = lineEnd > lineStart && charAt(lineEnd - 1) === 0x7d && slice(Math.max(lineEnd - 32, lineStart), lineEnd).match(/(~?)\{(\d+)\}$/);
53
+ if (marker) {
54
+ const size = Number(marker[2]);
55
+ if (pos + size > length) {
56
+ return { responses, end, incomplete: { reason: 'literal', size, available: length - pos, lines: current.lines } };
57
+ }
58
+ current.literals.push({ start: pos, end: pos + size, literal8: !!marker[1] });
59
+ pos += size;
60
+ continue;
61
+ }
62
+
63
+ current.end = end = pos;
64
+ responses.push(current);
65
+ current = null;
66
+ }
67
+
68
+ // the data ended right after a literal, the rest of the response is missing
69
+ return { responses, end, incomplete: current ? { reason: 'response', lines: current.lines } : false };
70
+ }
71
+
72
+ /**
73
+ * Splits an outgoing command after every synchronizing literal marker, as a client must wait for the
74
+ * continuation request before it sends the literal data (RFC 3501 section 4.3). The data of every
75
+ * literal, synchronizing or not ({n+}, RFC 7888), is skipped, so markers inside it are not matched.
76
+ *
77
+ * @param {Buffer|String} payload Command, as a Buffer or a binary string
78
+ * @return {Array} Chunks to send, of the same type as the payload
79
+ */
80
+ function splitAtLiterals(payload) {
81
+ const isBuffer = Buffer.isBuffer(payload);
82
+ const str = isBuffer ? payload.toString('binary') : String(payload);
83
+ const cut = (start, end) => (isBuffer ? payload.subarray(start, end) : str.slice(start, end));
84
+ const chunks = [];
85
+ const re = /~?\{(\d+)(\+?)\}\r\n/g;
86
+ let start = 0;
87
+ let match;
88
+
89
+ while ((match = re.exec(str))) {
90
+ const end = match.index + match[0].length;
91
+ if (!match[2]) {
92
+ chunks.push(cut(start, end));
93
+ start = end;
94
+ }
95
+ re.lastIndex = end + Number(match[1]);
96
+ }
97
+ chunks.push(cut(start));
98
+
99
+ return chunks.filter(chunk => chunk.length);
100
+ }
101
+
102
+ module.exports = { splitResponses, splitAtLiterals };
@@ -0,0 +1,36 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Returns the registry of extended LIST options (RFC 5258 section 3), shared by the LIST-EXTENDED
5
+ * plugin and the plugins that add their own options (SPECIAL-USE, LIST-STATUS). These can be loaded
6
+ * in any order, so whichever comes first creates the registry. Options are only accepted when the
7
+ * LIST-EXTENDED plugin is loaded (`enabled`).
8
+ *
9
+ * Selection options: `{ type, returnOption, includeNonExistent, match(folder, connection) }`
10
+ * - `type` is "base", "independent" or "modifier" (list-select-base-opt, list-select-independent-opt
11
+ * and list-select-mod-opt in the RFC 5258 section 6 grammar)
12
+ * - `returnOption` is the return option the selection option implies
13
+ * - `match` is the filter the mailboxes must pass, `includeNonExistent` if it can select mailbox
14
+ * names that do not exist (like SUBSCRIBED)
15
+ *
16
+ * Return options: `{ parse(list, connection), onItem(connection, folder, value, info, parsed, data) }`
17
+ * - `parse` is set for options that take a value (option-value), it returns the parsed value or
18
+ * throws an Error with a message for the BAD response
19
+ * - `onItem` runs after the LIST response of every listed mailbox. `info.matched` is true if the
20
+ * mailbox matched the selection criteria, `info.exists` if it is not \NonExistent
21
+ *
22
+ * @param {Object} server IMAPServer instance
23
+ * @return {Object} `{ enabled, selectionOptions, returnOptions }`
24
+ */
25
+ function getListExtensions(server) {
26
+ if (!server.listExtensions) {
27
+ server.listExtensions = {
28
+ enabled: false,
29
+ selectionOptions: Object.create(null),
30
+ returnOptions: Object.create(null)
31
+ };
32
+ }
33
+ return server.listExtensions;
34
+ }
35
+
36
+ module.exports = { getListExtensions };
@@ -0,0 +1,109 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const PLUGINS_DIR = path.join(__dirname, 'plugins');
7
+
8
+ // Capability spellings that do not match a plugin file name
9
+ const ALIASES = {
10
+ 'literal+': 'literalplus',
11
+ 'literal-': 'literalminus',
12
+ 'compress=deflate': 'compress',
13
+ 'auth=plain': 'auth-plain',
14
+ 'auth=xoauth2': 'xoauth2',
15
+ 'status=size': 'status-size',
16
+ 'sort=display': 'sort-display',
17
+ 'thread=orderedsubject': 'thread-orderedsubject',
18
+ 'thread=references': 'thread-references',
19
+ 'auth=oauthbearer': 'oauthbearer',
20
+ 'utf8=accept': 'utf8-accept',
21
+ 'context=search': 'context-search',
22
+ 'context=sort': 'context-sort'
23
+ };
24
+
25
+ let available = null;
26
+
27
+ /**
28
+ * Lists the modules of a directory (file names without the .js extension)
29
+ *
30
+ * @param {String} dir Directory path
31
+ * @return {Array} module names
32
+ */
33
+ function listModules(dir) {
34
+ return fs
35
+ .readdirSync(dir)
36
+ .filter(fileName => /\.js$/.test(fileName))
37
+ .map(fileName => fileName.replace(/\.js$/, ''));
38
+ }
39
+
40
+ /**
41
+ * Lists the names of the built-in plugins (file names without extension)
42
+ *
43
+ * @return {Array} plugin names
44
+ */
45
+ function listPlugins() {
46
+ if (!available) {
47
+ available = listModules(PLUGINS_DIR);
48
+ }
49
+ return available;
50
+ }
51
+
52
+ /**
53
+ * Resolves a plugin name, as given in options.plugins, to a built-in plugin
54
+ *
55
+ * @param {String} name Plugin name, eg. "IDLE" or "LITERAL+"
56
+ * @return {String|false} Resolved plugin name or false if no such plugin exists
57
+ */
58
+ function resolvePlugin(name) {
59
+ let key = String(name).trim().toLowerCase();
60
+ if (Object.prototype.hasOwnProperty.call(ALIASES, key)) {
61
+ key = ALIASES[key];
62
+ }
63
+ return listPlugins().includes(key) ? key : false;
64
+ }
65
+
66
+ /**
67
+ * Loads plugins for a server instance. Built-in plugins are referenced by name,
68
+ * custom plugins are functions. Unknown names throw, repeated plugins are loaded only once.
69
+ * A plugin that needs another plugin lists its name in `plugin.requires`, the required
70
+ * plugin is then loaded first. Once every plugin is loaded, the server emits `pluginsLoaded`: a plugin
71
+ * that has to wrap what other plugins set up (commands, output handlers), whatever the load order,
72
+ * does that in a `server.once('pluginsLoaded', ...)` listener. Listeners run in plugin load order.
73
+ *
74
+ * @param {Object} server IMAPServer instance
75
+ * @param {Array|String|Function} plugins List of plugins to load
76
+ */
77
+ function loadPlugins(server, plugins) {
78
+ const loaded = new Set();
79
+
80
+ const load = plugin => {
81
+ if (typeof plugin === 'string') {
82
+ const name = resolvePlugin(plugin);
83
+ if (!name) {
84
+ throw new Error('Unknown plugin "' + plugin + '". Available plugins: ' + listPlugins().join(', '));
85
+ }
86
+ plugin = require(path.join(PLUGINS_DIR, name));
87
+ }
88
+
89
+ if (typeof plugin !== 'function') {
90
+ throw new TypeError('Invalid plugin, expecting a plugin name or a function');
91
+ }
92
+
93
+ if (loaded.has(plugin)) {
94
+ return;
95
+ }
96
+ loaded.add(plugin);
97
+
98
+ [].concat(plugin.requires || []).forEach(load);
99
+
100
+ plugin(server);
101
+ };
102
+
103
+ [].concat(plugins || []).forEach(load);
104
+
105
+ server.emit('pluginsLoaded');
106
+ }
107
+
108
+ module.exports = loadPlugins;
109
+ module.exports.listModules = listModules;
@@ -0,0 +1,133 @@
1
+ 'use strict';
2
+
3
+ // Modified BASE64 alphabet of RFC 3501 section 5.1.3 ("," instead of "/")
4
+ const BASE64_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+,';
5
+
6
+ /**
7
+ * Checks a mailbox name against the modified UTF-7 rules of RFC 3501 section 5.1.3:
8
+ * printable US-ASCII represents itself, "&" is written as "&-", everything else
9
+ * is encoded in modified BASE64 between "&" and "-", without superfluous shifts
10
+ * and without encoding printable US-ASCII.
11
+ *
12
+ * @param {String} name Mailbox name as a binary string
13
+ * @return {String|Boolean} Description of the problem, or false if the name is valid
14
+ */
15
+ function validateMailboxName(name) {
16
+ if (typeof name !== 'string') {
17
+ return 'Invalid mailbox name';
18
+ }
19
+
20
+ let i = 0;
21
+ while (i < name.length) {
22
+ const code = name.charCodeAt(i);
23
+
24
+ if (code < 0x20 || code > 0x7e) {
25
+ return 'Mailbox name must use modified UTF-7 for non-ASCII characters (RFC 3501 section 5.1.3)';
26
+ }
27
+
28
+ if (name.charAt(i) !== '&') {
29
+ i++;
30
+ continue;
31
+ }
32
+
33
+ if (name.charAt(i + 1) === '-') {
34
+ // "&-" is the "&" character
35
+ i += 2;
36
+ continue;
37
+ }
38
+
39
+ const end = name.indexOf('-', i + 1);
40
+ if (end < 0) {
41
+ return 'Modified BASE64 in mailbox name must end with "-" (RFC 3501 section 5.1.3)';
42
+ }
43
+
44
+ const error = checkBase64(name.slice(i + 1, end));
45
+ if (error) {
46
+ return error;
47
+ }
48
+
49
+ if (name.charAt(end + 1) === '&' && name.charAt(end + 2) !== '-') {
50
+ return 'Mailbox name contains a superfluous shift (RFC 3501 section 5.1.3)';
51
+ }
52
+
53
+ i = end + 1;
54
+ }
55
+
56
+ return false;
57
+ }
58
+
59
+ function checkBase64(encoded) {
60
+ let bits = 0;
61
+ let bitCount = 0;
62
+ const units = [];
63
+
64
+ for (let i = 0; i < encoded.length; i++) {
65
+ const value = BASE64_CHARS.indexOf(encoded.charAt(i));
66
+ if (value < 0) {
67
+ return 'Invalid modified BASE64 in mailbox name (RFC 3501 section 5.1.3)';
68
+ }
69
+ bits = (bits << 6) | value;
70
+ bitCount += 6;
71
+ if (bitCount >= 16) {
72
+ bitCount -= 16;
73
+ units.push((bits >> bitCount) & 0xffff);
74
+ bits &= (1 << bitCount) - 1;
75
+ }
76
+ }
77
+
78
+ // leftover bits are padding and must be zero, and must not hold a partial character
79
+ if (!units.length || bitCount >= 6 || bits !== 0) {
80
+ return 'Invalid modified BASE64 in mailbox name (RFC 3501 section 5.1.3)';
81
+ }
82
+
83
+ for (let i = 0; i < units.length; i++) {
84
+ const unit = units[i];
85
+ if (unit >= 0x20 && unit <= 0x7e) {
86
+ return 'Modified BASE64 must not encode printable US-ASCII (RFC 3501 section 5.1.3)';
87
+ }
88
+ if (unit >= 0xd800 && unit <= 0xdbff) {
89
+ if (!(units[i + 1] >= 0xdc00 && units[i + 1] <= 0xdfff)) {
90
+ return 'Invalid UTF-16 in mailbox name';
91
+ }
92
+ i++;
93
+ } else if (unit >= 0xdc00 && unit <= 0xdfff) {
94
+ return 'Invalid UTF-16 in mailbox name';
95
+ }
96
+ }
97
+
98
+ return false;
99
+ }
100
+
101
+ /**
102
+ * Encodes a mailbox name in modified UTF-7 (RFC 3501 section 5.1.3)
103
+ *
104
+ * @param {String} name Mailbox name as a unicode string
105
+ * @return {String} Modified UTF-7 name
106
+ */
107
+ function encodeMailboxName(name) {
108
+ return name.replace(/&|[^\x20-\x7e]+/g, chunk => {
109
+ if (chunk === '&') {
110
+ return '&-';
111
+ }
112
+ return '&' + Buffer.from(chunk, 'utf16le').swap16().toString('base64').replace(/=+$/, '').replace(/\//g, ',') + '-';
113
+ });
114
+ }
115
+
116
+ /**
117
+ * Decodes a modified UTF-7 mailbox name (RFC 3501 section 5.1.3)
118
+ *
119
+ * @param {String} name Mailbox name as a binary string
120
+ * @return {String|Boolean} Mailbox name as a unicode string, or false if the name is not valid modified UTF-7
121
+ */
122
+ function decodeMailboxName(name) {
123
+ if (validateMailboxName(name)) {
124
+ return false;
125
+ }
126
+ return name.replace(/&([^-]*)-/g, (match, encoded) => (encoded ? Buffer.from(encoded.replace(/,/g, '/'), 'base64').swap16().toString('utf16le') : '&'));
127
+ }
128
+
129
+ module.exports = validateMailboxName;
130
+ module.exports.encode = encodeMailboxName;
131
+ module.exports.decode = decodeMailboxName;
132
+ // CATENATE resolves URL mailbox names with it
133
+ module.exports.encodeMailboxName = encodeMailboxName;