imapkit 0.0.0-stage → 4.0.0
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
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;
|