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,149 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Converts a parsed MIME tree into BODY and BODYSTRUCTURE data. Ported from WildDuck
|
|
4
|
+
// (imap-core/lib/indexer/body-structure.js)
|
|
5
|
+
|
|
6
|
+
const envelope = require('./envelope');
|
|
7
|
+
const { partsOf, embeddedMessage } = require('./mimeparser');
|
|
8
|
+
|
|
9
|
+
// Expose to the world
|
|
10
|
+
module.exports = createBodystructure;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Generates an object out of parsed mime tree, that can be
|
|
14
|
+
* serialized into a BODYSTRUCTURE string
|
|
15
|
+
*
|
|
16
|
+
* @param {Object} tree Parsed mime tree (see mimeparser.js for input)
|
|
17
|
+
* @param {Object} [options] Optional options object
|
|
18
|
+
* @param {Boolean} [options.upperCaseKeys] If true, use only upper case key names
|
|
19
|
+
* @param {Boolean} [options.skipContentLocation] If true, do not include Content-Location in the output
|
|
20
|
+
* @param {Boolean} [options.body] If true, skip extension fields (needed for BODY)
|
|
21
|
+
* @param {Boolean} [options.messageGlobal] If true, describe message/global like message/rfc822 (IMAP4rev2, RFC 9051 section 7.5.2)
|
|
22
|
+
* @return {Array} Object structure in the form of BODYSTRUCTURE
|
|
23
|
+
*/
|
|
24
|
+
function createBodystructure(tree, options) {
|
|
25
|
+
options = options || {};
|
|
26
|
+
|
|
27
|
+
const key = name => (options.upperCaseKeys && typeof name === 'string' ? name.toUpperCase() : name);
|
|
28
|
+
|
|
29
|
+
// Parameter list of a parsed structured header value, as the flat `(key value key value)` list
|
|
30
|
+
const paramList = parsed => {
|
|
31
|
+
if (!parsed || !parsed.hasParams) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const list = [];
|
|
35
|
+
Object.keys(parsed.params).forEach(name => {
|
|
36
|
+
list.push(key(name), parsed.params[name]);
|
|
37
|
+
});
|
|
38
|
+
return list.length ? list : null;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const getBasicFields = node => {
|
|
42
|
+
const contentType = node.parsedHeader['content-type'] || {};
|
|
43
|
+
let bodyType = contentType.type || null;
|
|
44
|
+
let bodySubtype = contentType.subtype || null;
|
|
45
|
+
|
|
46
|
+
if (!bodyType || !bodySubtype) {
|
|
47
|
+
// prevent strange content types like (NIL "/ms-word") that may break some clients
|
|
48
|
+
if (bodyType === 'text' || bodySubtype === 'plain') {
|
|
49
|
+
bodyType = 'text';
|
|
50
|
+
bodySubtype = 'plain';
|
|
51
|
+
} else {
|
|
52
|
+
bodyType = 'application';
|
|
53
|
+
bodySubtype = 'octet-stream';
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return [
|
|
58
|
+
key(bodyType),
|
|
59
|
+
key(bodySubtype),
|
|
60
|
+
// body parameter parenthesized list
|
|
61
|
+
paramList(contentType),
|
|
62
|
+
// body id
|
|
63
|
+
node.parsedHeader['content-id'] || null,
|
|
64
|
+
// body description
|
|
65
|
+
node.parsedHeader['content-description'] || null,
|
|
66
|
+
// body encoding
|
|
67
|
+
key(node.parsedHeader['content-transfer-encoding'] || '7bit'),
|
|
68
|
+
// body size
|
|
69
|
+
node.size
|
|
70
|
+
];
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// The extension fields every part has (a non-multipart part also has an MD5 before them)
|
|
74
|
+
const getExtensionFields = node => {
|
|
75
|
+
const languageString = node.parsedHeader['content-language'] && node.parsedHeader['content-language'].replace(/[ ,]+/g, ',').replace(/^,+|,+$/g, '');
|
|
76
|
+
const language = (languageString && languageString.split(',')) || null;
|
|
77
|
+
const disposition = node.parsedHeader['content-disposition'];
|
|
78
|
+
|
|
79
|
+
const data = [
|
|
80
|
+
// body disposition
|
|
81
|
+
(disposition && [key(disposition.value), paramList(disposition)]) || null,
|
|
82
|
+
// body language
|
|
83
|
+
language
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
// NB! RFC3501 has an errata with content-location type, it is described as
|
|
87
|
+
// "A string list" (eg. an array) in RFC but the errata page states
|
|
88
|
+
// that it is a string (http://www.rfc-editor.org/errata_search.php?rfc=3501)
|
|
89
|
+
// see note for "Section 7.4.2, page 75"
|
|
90
|
+
if (!options.skipContentLocation) {
|
|
91
|
+
data.push(node.parsedHeader['content-location'] || null);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return data;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// A non-multipart node: the basic fields, the fields of its type (line count for text, envelope and
|
|
98
|
+
// structure for message/rfc822), and the extension fields unless BODY was asked for
|
|
99
|
+
const processLeaf = (node, extra) => {
|
|
100
|
+
let data = getBasicFields(node).concat(extra);
|
|
101
|
+
if (!options.body) {
|
|
102
|
+
data = data.concat([node.parsedHeader['content-md5'] || null], getExtensionFields(node));
|
|
103
|
+
}
|
|
104
|
+
return data;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const processMultipartNode = node => {
|
|
108
|
+
let data = partsOf(node)
|
|
109
|
+
.map(walker)
|
|
110
|
+
.concat([key(node.multipart)]);
|
|
111
|
+
|
|
112
|
+
if (!options.body) {
|
|
113
|
+
// RFC 3501 7.4.2: BODY is BODYSTRUCTURE without extension data, and for a multipart the
|
|
114
|
+
// parameter list is the first extension field (body-ext-mpart)
|
|
115
|
+
data = data.concat([paramList(node.parsedHeader['content-type'])], getExtensionFields(node));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// body-type-mpart = 1*body SP media-subtype, there is no SP between the bodies (RFC 3501 section 9)
|
|
119
|
+
Object.defineProperty(data, 'adjacentLists', { value: true });
|
|
120
|
+
return data;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const walker = node => {
|
|
124
|
+
const contentType = node.parsedHeader['content-type'] || {};
|
|
125
|
+
switch (contentType.type) {
|
|
126
|
+
case 'multipart':
|
|
127
|
+
// a multipart without a boundary parameter has no parts and is described with the
|
|
128
|
+
// basic fields (RFC 3501 body-type-mpart needs at least one body)
|
|
129
|
+
return partsOf(node) ? processMultipartNode(node) : processLeaf(node, []);
|
|
130
|
+
case 'text':
|
|
131
|
+
return processLeaf(node, [node.lineCount]);
|
|
132
|
+
case 'message': {
|
|
133
|
+
// RFC 2045 5.1: the subtype is not case sensitive. Other message subtypes, such as
|
|
134
|
+
// message/delivery-status, are described as basic parts. RFC 9051 section 9 media-message
|
|
135
|
+
// adds message/global, RFC 3501 body-type-basic describes it as a basic part
|
|
136
|
+
// (embeddedMessage() only returns the message of these types)
|
|
137
|
+
const embedded = embeddedMessage(node, options.messageGlobal);
|
|
138
|
+
if (embedded) {
|
|
139
|
+
return processLeaf(node, [envelope(embedded.parsedHeader), walker(embedded), node.lineCount]);
|
|
140
|
+
}
|
|
141
|
+
return processLeaf(node, []);
|
|
142
|
+
}
|
|
143
|
+
default:
|
|
144
|
+
return processLeaf(node, []);
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
return walker(tree);
|
|
149
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Connection states in which each command is valid (RFC 3501 sections 3 and 6). Commands valid
|
|
4
|
+
// in the authenticated state are also valid in the selected state.
|
|
5
|
+
const ANY = ['Not Authenticated', 'Authenticated', 'Selected'];
|
|
6
|
+
const NOT_AUTHENTICATED = ['Not Authenticated'];
|
|
7
|
+
const AUTHENTICATED = ['Authenticated', 'Selected'];
|
|
8
|
+
const SELECTED = ['Selected'];
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Normalizes command options
|
|
12
|
+
*
|
|
13
|
+
* @param {Object|Array} options `{ states, noArguments, mailboxArguments, astringArguments, searchCriteria, sequenceSet, noExpunge,
|
|
14
|
+
* literal8, noPipelining }`, or just the list of states
|
|
15
|
+
* @return {Object} options with all keys set
|
|
16
|
+
*/
|
|
17
|
+
function commandOptions(options) {
|
|
18
|
+
if (Array.isArray(options)) {
|
|
19
|
+
options = { states: options };
|
|
20
|
+
}
|
|
21
|
+
options = options || {};
|
|
22
|
+
return {
|
|
23
|
+
// false means any state
|
|
24
|
+
states: options.states ? [].concat(options.states) : false,
|
|
25
|
+
// "Arguments: none" in the command description
|
|
26
|
+
noArguments: !!options.noArguments,
|
|
27
|
+
// positions of the arguments that are mailbox names, checked against RFC 3501 section 5.1.3
|
|
28
|
+
mailboxArguments: [].concat(options.mailboxArguments || []),
|
|
29
|
+
// positions of other arguments that are astrings (RFC 3501 section 9), like user names. In these, in the mailbox
|
|
30
|
+
// name arguments and in search criteria a NIL atom is a string, not the nil of an nstring
|
|
31
|
+
astringArguments: [].concat(options.astringArguments || []),
|
|
32
|
+
// position of the first search key argument, for commands that take search criteria (RFC 3501 section 6.4.4)
|
|
33
|
+
searchCriteria: typeof options.searchCriteria === 'number' ? options.searchCriteria : false,
|
|
34
|
+
// position of the argument that holds message sequence numbers (a sequence set or a seq-number, not UIDs), for
|
|
35
|
+
// the RFC 3501 section 5.5 ambiguity check and for UIDONLY (RFC 9586 section 3)
|
|
36
|
+
sequenceSet: typeof options.sequenceSet === 'number' ? options.sequenceSet : false,
|
|
37
|
+
// EXPUNGE responses must not be sent while the command runs, so a client does not have to wait for its
|
|
38
|
+
// completion before sending a command with sequence numbers (RFC 3501 sections 5.5 and 7.4.1)
|
|
39
|
+
noExpunge: !!options.noExpunge,
|
|
40
|
+
// if the command accepts literal8 arguments `~{n}`: true, or the capability that allows them, like
|
|
41
|
+
// "BINARY" for the APPEND message (RFC 3516 section 4.4)
|
|
42
|
+
literal8: options.literal8 === true || typeof options.literal8 === 'string' ? options.literal8 : false,
|
|
43
|
+
// the client must not send anything after the command before it has seen the result, because the command changes
|
|
44
|
+
// the layers below the protocol (STARTTLS, RFC 9051 section 6.2.1, COMPRESS, RFC 4978 section 3). The command is
|
|
45
|
+
// refused with BAD if more input is waiting
|
|
46
|
+
noPipelining: !!options.noPipelining,
|
|
47
|
+
// the command takes a message after its mailbox argument like APPEND does (REPLACE, RFC 8508 section 3.4), so a
|
|
48
|
+
// message literal to a missing mailbox is refused before it is sent
|
|
49
|
+
appendMessage: !!options.appendMessage
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// RFC 3501 core commands. Plugins pass the options of their commands to setCommandHandler
|
|
54
|
+
const CORE_COMMANDS = {
|
|
55
|
+
CAPABILITY: commandOptions({ states: ANY, noArguments: true }),
|
|
56
|
+
NOOP: commandOptions({ states: ANY, noArguments: true }),
|
|
57
|
+
LOGOUT: commandOptions({ states: ANY, noArguments: true }),
|
|
58
|
+
|
|
59
|
+
STARTTLS: commandOptions({ states: NOT_AUTHENTICATED, noArguments: true, noPipelining: true }),
|
|
60
|
+
// login = "LOGIN" SP userid SP password, both astrings
|
|
61
|
+
LOGIN: commandOptions({ states: NOT_AUTHENTICATED, astringArguments: [0, 1] }),
|
|
62
|
+
|
|
63
|
+
SELECT: commandOptions({ states: AUTHENTICATED, mailboxArguments: [0] }),
|
|
64
|
+
EXAMINE: commandOptions({ states: AUTHENTICATED, mailboxArguments: [0] }),
|
|
65
|
+
CREATE: commandOptions({ states: AUTHENTICATED, mailboxArguments: [0] }),
|
|
66
|
+
DELETE: commandOptions({ states: AUTHENTICATED, mailboxArguments: [0] }),
|
|
67
|
+
RENAME: commandOptions({ states: AUTHENTICATED, mailboxArguments: [0, 1] }),
|
|
68
|
+
SUBSCRIBE: commandOptions({ states: AUTHENTICATED, mailboxArguments: [0] }),
|
|
69
|
+
UNSUBSCRIBE: commandOptions({ states: AUTHENTICATED, mailboxArguments: [0] }),
|
|
70
|
+
// the reference is a mailbox name, the pattern a list-mailbox, an atom NIL is a valid name for both
|
|
71
|
+
LIST: commandOptions({ states: AUTHENTICATED, astringArguments: [0, 1] }),
|
|
72
|
+
LSUB: commandOptions({ states: AUTHENTICATED, astringArguments: [0, 1] }),
|
|
73
|
+
STATUS: commandOptions({ states: AUTHENTICATED, mailboxArguments: [0] }),
|
|
74
|
+
APPEND: commandOptions({ states: AUTHENTICATED, mailboxArguments: [0], literal8: 'BINARY', appendMessage: true }),
|
|
75
|
+
|
|
76
|
+
CHECK: commandOptions({ states: SELECTED, noArguments: true }),
|
|
77
|
+
CLOSE: commandOptions({ states: SELECTED, noArguments: true }),
|
|
78
|
+
EXPUNGE: commandOptions({ states: SELECTED, noArguments: true }),
|
|
79
|
+
SEARCH: commandOptions({ states: SELECTED, searchCriteria: 0, noExpunge: true }),
|
|
80
|
+
FETCH: commandOptions({ states: SELECTED, sequenceSet: 0, noExpunge: true }),
|
|
81
|
+
STORE: commandOptions({ states: SELECTED, sequenceSet: 0, noExpunge: true }),
|
|
82
|
+
COPY: commandOptions({ states: SELECTED, sequenceSet: 0, mailboxArguments: [1] }),
|
|
83
|
+
'UID COPY': commandOptions({ states: SELECTED, mailboxArguments: [1] }),
|
|
84
|
+
'UID SEARCH': commandOptions({ states: SELECTED, searchCriteria: 0 })
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const UID_COMMAND = commandOptions({ states: SELECTED });
|
|
88
|
+
const AUTHENTICATE_COMMAND = commandOptions({ states: NOT_AUTHENTICATED });
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Returns the options of a core command
|
|
92
|
+
*
|
|
93
|
+
* @param {String} command Upper case command name, e.g. "UID FETCH"
|
|
94
|
+
* @return {Object|Boolean} command options (see commandOptions) or false if the command is not a core command
|
|
95
|
+
*/
|
|
96
|
+
function getCommandOptions(command) {
|
|
97
|
+
if (Object.hasOwn(CORE_COMMANDS, command)) {
|
|
98
|
+
return CORE_COMMANDS[command];
|
|
99
|
+
}
|
|
100
|
+
if (/^UID /.test(command)) {
|
|
101
|
+
return UID_COMMAND;
|
|
102
|
+
}
|
|
103
|
+
if (/^AUTHENTICATE /.test(command)) {
|
|
104
|
+
return AUTHENTICATE_COMMAND;
|
|
105
|
+
}
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = { getCommandOptions, commandOptions, states: { ANY, NOT_AUTHENTICATED, AUTHENTICATED, SELECTED } };
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const imapHandler = require('imap-handler');
|
|
4
|
+
const { normalizeSystemFlag, checkSystemFlags } = require('./handlers/flags');
|
|
5
|
+
const { restoreNilAtoms, isAstring } = require('../arguments');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Creates an error that refuses an APPEND like command
|
|
9
|
+
*
|
|
10
|
+
* @param {String} text Human readable text
|
|
11
|
+
* @param {String} [code] Response code, e.g. "TOOBIG"
|
|
12
|
+
* @param {Array} [codeArgs] Arguments of the response code, as response attributes
|
|
13
|
+
* @param {String} [response] "NO" (default) or "BAD"
|
|
14
|
+
* @return {Error} Error object
|
|
15
|
+
*/
|
|
16
|
+
function appendError(text, code, codeArgs, response) {
|
|
17
|
+
const err = new Error(text);
|
|
18
|
+
err.imapResponse = response || 'NO';
|
|
19
|
+
err.responseCode = code || false;
|
|
20
|
+
err.responseCodeArgs = codeArgs || [];
|
|
21
|
+
return err;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const badArgument = text => appendError(text, false, false, 'BAD');
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Parses a flag list, flags are atoms, and \Recent or unknown system flags can not be set (RFC 3501 section 9)
|
|
28
|
+
*
|
|
29
|
+
* @param {Object} connection IMAPConnection
|
|
30
|
+
* @param {Array} list Parsed flag list
|
|
31
|
+
* @return {Array} Flags
|
|
32
|
+
*/
|
|
33
|
+
function parseFlags(connection, list) {
|
|
34
|
+
try {
|
|
35
|
+
return list.map(flag => {
|
|
36
|
+
if (!flag || flag.type !== 'ATOM') {
|
|
37
|
+
throw new Error('Invalid flags argument');
|
|
38
|
+
}
|
|
39
|
+
const value = normalizeSystemFlag(flag.value);
|
|
40
|
+
checkSystemFlags(connection, value);
|
|
41
|
+
return value;
|
|
42
|
+
});
|
|
43
|
+
} catch {
|
|
44
|
+
throw badArgument('Invalid flags argument');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Parses the messages of an APPEND like command. RFC 3502 and RFC 4466 section 3:
|
|
50
|
+
* append-message = [SP flag-list] [SP date-time] SP append-data, where append-data is a literal
|
|
51
|
+
* or an extension (label SP value) from `server.appendDataHandlers`, e.g. CATENATE (RFC 4469)
|
|
52
|
+
*
|
|
53
|
+
* @param {Object} connection IMAPConnection
|
|
54
|
+
* @param {Array} args Parsed arguments after the mailbox name
|
|
55
|
+
* @param {Number} maxMessages Largest number of messages allowed
|
|
56
|
+
* @return {Array} Messages `{ flags, internaldate, raw, resolve, literal8 }`, resolve builds the message source
|
|
57
|
+
* of an append-data extension. Throws a BAD error if the arguments break the grammar
|
|
58
|
+
*/
|
|
59
|
+
function parseMessages(connection, args, maxMessages) {
|
|
60
|
+
const server = connection.server;
|
|
61
|
+
const messages = [];
|
|
62
|
+
|
|
63
|
+
for (let i = 0; i < args.length;) {
|
|
64
|
+
const message = { flags: [], internaldate: false, raw: false };
|
|
65
|
+
|
|
66
|
+
if (Array.isArray(args[i])) {
|
|
67
|
+
message.flags = parseFlags(connection, args[i++]);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// date-time is always a quoted string, a literal here is the message
|
|
71
|
+
if (args[i] && args[i].type === 'STRING') {
|
|
72
|
+
if (!server.validateInternalDate(args[i].value)) {
|
|
73
|
+
throw badArgument('Invalid internaldate argument');
|
|
74
|
+
}
|
|
75
|
+
message.internaldate = args[i++].value;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const appendData = args[i++];
|
|
79
|
+
const label = appendData && appendData.type === 'ATOM' ? String(appendData.value).toUpperCase() : '';
|
|
80
|
+
if (appendData && appendData.type === 'LITERAL') {
|
|
81
|
+
message.raw = appendData.value;
|
|
82
|
+
} else if (appendData && appendData.type === 'LITERAL8' && server.appendLiteral8) {
|
|
83
|
+
// RFC 3516 section 4.4: the message as a literal8, server.appendLiteral8 (BINARY) stores it
|
|
84
|
+
message.raw = appendData.value;
|
|
85
|
+
message.literal8 = true;
|
|
86
|
+
} else if (label && Object.hasOwn(server.appendDataHandlers, label) && i < args.length) {
|
|
87
|
+
message.resolve = server.appendDataHandlers[label](connection, args[i++]);
|
|
88
|
+
} else {
|
|
89
|
+
throw badArgument('Invalid message source argument');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
messages.push(message);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!messages.length) {
|
|
96
|
+
throw badArgument('Missing message argument');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (messages.length > maxMessages) {
|
|
100
|
+
// only a single message unless MULTIAPPEND is supported (RFC 4466 section 3)
|
|
101
|
+
throw badArgument('Only a single message can be appended');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return messages;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Sends the response for an error from appendError()
|
|
109
|
+
*
|
|
110
|
+
* @param {Object} connection IMAPConnection
|
|
111
|
+
* @param {Object} parsed Parsed command
|
|
112
|
+
* @param {String} data Raw command
|
|
113
|
+
* @param {Error} err Error object
|
|
114
|
+
* @param {String} description Description of a NO response
|
|
115
|
+
*/
|
|
116
|
+
function sendAppendError(connection, parsed, data, err, description) {
|
|
117
|
+
const attributes = [];
|
|
118
|
+
if (err.responseCode) {
|
|
119
|
+
attributes.push({
|
|
120
|
+
type: 'SECTION',
|
|
121
|
+
section: [{ type: 'ATOM', value: err.responseCode }].concat(err.responseCodeArgs)
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
attributes.push({ type: 'TEXT', value: err.message });
|
|
125
|
+
|
|
126
|
+
connection.send(
|
|
127
|
+
{
|
|
128
|
+
tag: parsed.tag,
|
|
129
|
+
command: err.imapResponse,
|
|
130
|
+
attributes
|
|
131
|
+
},
|
|
132
|
+
err.imapResponse === 'BAD' ? 'INVALID COMMAND' : description,
|
|
133
|
+
parsed,
|
|
134
|
+
data
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Finds the target mailbox name of an APPEND or REPLACE command when a literal of a message (or of a CATENATE TEXT
|
|
140
|
+
* part) is announced. Only the start of the command up to the mailbox argument is parsed, so the data of earlier
|
|
141
|
+
* literals is not parsed again for every message of a MULTIAPPEND
|
|
142
|
+
*
|
|
143
|
+
* @param {Object} connection IMAPConnection
|
|
144
|
+
* @param {String} command Upper case command name
|
|
145
|
+
* @param {String} line Command received so far, up to the literal size marker
|
|
146
|
+
* @return {String|Boolean} storage name of the mailbox, or false if the literal is not a message or the name is not
|
|
147
|
+
* known yet or not valid
|
|
148
|
+
*/
|
|
149
|
+
function getPendingTarget(connection, command, line) {
|
|
150
|
+
const options = connection.server.getCommandOptions(command);
|
|
151
|
+
if (!options.appendMessage) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
const mailboxPosition = options.mailboxArguments[0];
|
|
155
|
+
|
|
156
|
+
// the literal of a CATENATE URL is not a message part
|
|
157
|
+
if (/[ (]URL $/i.test(line.slice(-5))) {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// an open CATENATE list is closed for parsing
|
|
162
|
+
const parse = text => {
|
|
163
|
+
text = text.replace(/ $/, '');
|
|
164
|
+
for (const suffix of ['', ')']) {
|
|
165
|
+
try {
|
|
166
|
+
const parsed = imapHandler.parser(text + suffix);
|
|
167
|
+
restoreNilAtoms(parsed, text, path => path.length === 1 && path[0] === mailboxPosition);
|
|
168
|
+
return parsed.attributes || [];
|
|
169
|
+
} catch {
|
|
170
|
+
// try the next suffix
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return [];
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// the command up to the first literal, or up to the end of it when that literal is the mailbox name
|
|
177
|
+
const literal = line.match(/(~?)\{(\d+)\+?\}\r\n/);
|
|
178
|
+
let attributes = parse(literal ? line.substr(0, literal.index) : line);
|
|
179
|
+
if (attributes.length <= mailboxPosition && literal && !literal[1]) {
|
|
180
|
+
attributes = parse(line.substr(0, literal.index + literal[0].length + Number(literal[2])));
|
|
181
|
+
}
|
|
182
|
+
// a literal8 can not be a mailbox name, the command handler refuses it
|
|
183
|
+
const pathArg = attributes[mailboxPosition];
|
|
184
|
+
if (!isAstring(pathArg)) {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
try {
|
|
188
|
+
return connection.importMailboxName(pathArg.value);
|
|
189
|
+
} catch {
|
|
190
|
+
// the command handler refuses the name
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* A literal filter (`server.literalFilters`) that refuses the message of an APPEND or REPLACE to a mailbox that does
|
|
197
|
+
* not exist before the client sends it, by not sending a continuation request (RFC 3502 section 6.3.11 example A004,
|
|
198
|
+
* RFC 3501 section 6.3.11: TRYCREATE). Not while earlier commands are still waiting, these could create the mailbox
|
|
199
|
+
*
|
|
200
|
+
* @param {Object} connection IMAPConnection
|
|
201
|
+
* @param {String} command Upper case command name
|
|
202
|
+
* @param {String} line Command received so far, up to the literal size marker
|
|
203
|
+
* @return {Object|Boolean} `{ command, code, text }` to refuse the literal, or false
|
|
204
|
+
*/
|
|
205
|
+
function refuseMissingTarget(connection, command, line) {
|
|
206
|
+
const path = !connection.isBusy() && getPendingTarget(connection, command, line);
|
|
207
|
+
return path !== false && connection.server.targetRefusal(path);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Prepares the messages of an APPEND like command (APPEND, REPLACE): parses the arguments, checks
|
|
212
|
+
* the target mailbox, builds the message sources and runs `server.appendChecks` through
|
|
213
|
+
* IMAPConnection#checkAppend. Nothing is stored yet, so a failure leaves every mailbox as it was
|
|
214
|
+
* (RFC 3502 section 6.3.11). The error response is sent here.
|
|
215
|
+
*
|
|
216
|
+
* The checks get the built messages (with `raw`) of the whole command and the options as
|
|
217
|
+
* `{ command, replaced }`, `replaced` is the message that REPLACE removes.
|
|
218
|
+
*
|
|
219
|
+
* @param {Object} connection IMAPConnection
|
|
220
|
+
* @param {Object} parsed Parsed command
|
|
221
|
+
* @param {String} data Raw command
|
|
222
|
+
* @param {Object} pathArg Parsed mailbox name argument
|
|
223
|
+
* @param {Array} args Parsed arguments after the mailbox name
|
|
224
|
+
* @param {Object} options `{ command, maxMessages, replaced }`
|
|
225
|
+
* @return {Object|Boolean} `{ mailbox, messages }` or false if an error response was sent
|
|
226
|
+
*/
|
|
227
|
+
function prepareAppend(connection, parsed, data, pathArg, args, options) {
|
|
228
|
+
const description = options.command + ' FAILED';
|
|
229
|
+
const maxMessages = options.maxMessages || (connection.server.multiAppend ? Infinity : 1);
|
|
230
|
+
try {
|
|
231
|
+
if (!pathArg || ['STRING', 'ATOM', 'LITERAL'].indexOf(pathArg.type) < 0) {
|
|
232
|
+
throw badArgument('Invalid mailbox argument');
|
|
233
|
+
}
|
|
234
|
+
const messages = parseMessages(connection, args, maxMessages);
|
|
235
|
+
|
|
236
|
+
// getTargetMailbox sends the NO response itself
|
|
237
|
+
const mailbox = connection.getTargetMailbox(pathArg.value, parsed, data, description);
|
|
238
|
+
if (!mailbox) {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
messages.forEach(message => {
|
|
243
|
+
if (message.resolve) {
|
|
244
|
+
message.raw = message.resolve();
|
|
245
|
+
} else if (message.literal8) {
|
|
246
|
+
message.raw = connection.server.appendLiteral8(message.raw);
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// checkAppend sends the NO response itself
|
|
251
|
+
if (!connection.checkAppend(mailbox, messages, parsed, data, description, { command: options.command, replaced: options.replaced })) {
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return { mailbox, messages };
|
|
256
|
+
} catch (err) {
|
|
257
|
+
if (!err.imapResponse) {
|
|
258
|
+
throw err;
|
|
259
|
+
}
|
|
260
|
+
sendAppendError(connection, parsed, data, err, description);
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Stores prepared messages. The sessions that have the target mailbox selected get EXISTS updates
|
|
267
|
+
*
|
|
268
|
+
* @param {Object} connection IMAPConnection
|
|
269
|
+
* @param {Object} prepared Result of prepareAppend()
|
|
270
|
+
* @return {Object} `{ mailbox, message, messages }`, message is the last stored message
|
|
271
|
+
*/
|
|
272
|
+
function storeMessages(connection, prepared) {
|
|
273
|
+
const messages = prepared.messages.map(
|
|
274
|
+
message => connection.server.appendMessage(prepared.mailbox, message.flags, message.internaldate, message.raw).message
|
|
275
|
+
);
|
|
276
|
+
return { mailbox: prepared.mailbox, message: messages[messages.length - 1], messages };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
module.exports = function (connection, parsed, data, callback) {
|
|
280
|
+
const args = [].concat(parsed.attributes || []);
|
|
281
|
+
|
|
282
|
+
const prepared = prepareAppend(connection, parsed, data, args.shift(), args, { command: 'APPEND' });
|
|
283
|
+
if (!prepared) {
|
|
284
|
+
return callback();
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const appendResult = storeMessages(connection, prepared);
|
|
288
|
+
|
|
289
|
+
connection.send(
|
|
290
|
+
{
|
|
291
|
+
tag: parsed.tag,
|
|
292
|
+
command: 'OK',
|
|
293
|
+
attributes: [
|
|
294
|
+
{
|
|
295
|
+
type: 'TEXT',
|
|
296
|
+
value: 'APPEND Completed'
|
|
297
|
+
}
|
|
298
|
+
]
|
|
299
|
+
},
|
|
300
|
+
'APPEND',
|
|
301
|
+
parsed,
|
|
302
|
+
data,
|
|
303
|
+
appendResult
|
|
304
|
+
);
|
|
305
|
+
callback();
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
module.exports.appendError = appendError;
|
|
309
|
+
module.exports.badArgument = badArgument;
|
|
310
|
+
module.exports.prepareAppend = prepareAppend;
|
|
311
|
+
module.exports.storeMessages = storeMessages;
|
|
312
|
+
module.exports.getPendingTarget = getPendingTarget;
|
|
313
|
+
module.exports.refuseMissingTarget = refuseMissingTarget;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
module.exports = function (connection, parsed, data, callback) {
|
|
4
|
+
const capabilities = ['IMAP4rev1'];
|
|
5
|
+
|
|
6
|
+
Object.keys(connection.server.capabilities).forEach(key => {
|
|
7
|
+
if (connection.server.capabilities[key](connection)) {
|
|
8
|
+
capabilities.push(key);
|
|
9
|
+
}
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
connection.send(
|
|
13
|
+
{
|
|
14
|
+
tag: '*',
|
|
15
|
+
command: 'CAPABILITY',
|
|
16
|
+
attributes: capabilities.map(capability => {
|
|
17
|
+
return {
|
|
18
|
+
type: 'TEXT',
|
|
19
|
+
value: capability
|
|
20
|
+
};
|
|
21
|
+
})
|
|
22
|
+
},
|
|
23
|
+
'CAPABILITY LIST',
|
|
24
|
+
parsed,
|
|
25
|
+
data,
|
|
26
|
+
capabilities
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
connection.send(
|
|
30
|
+
{
|
|
31
|
+
tag: parsed.tag,
|
|
32
|
+
command: 'OK',
|
|
33
|
+
attributes: [
|
|
34
|
+
{
|
|
35
|
+
type: 'TEXT',
|
|
36
|
+
value: 'Completed'
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
},
|
|
40
|
+
'CAPABILITY COMPLETED',
|
|
41
|
+
parsed,
|
|
42
|
+
data,
|
|
43
|
+
capabilities
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
callback();
|
|
47
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
module.exports = function (connection, parsed, data, callback) {
|
|
4
|
+
connection.send(
|
|
5
|
+
{
|
|
6
|
+
tag: parsed.tag,
|
|
7
|
+
command: 'OK',
|
|
8
|
+
attributes: [
|
|
9
|
+
{
|
|
10
|
+
type: 'TEXT',
|
|
11
|
+
value: 'Completed'
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
},
|
|
15
|
+
'CHECK',
|
|
16
|
+
parsed,
|
|
17
|
+
data
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
callback();
|
|
21
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
module.exports = function (connection, parsed, data, callback) {
|
|
4
|
+
// a read-only mailbox is closed without removing anything
|
|
5
|
+
if (connection.canExpunge()) {
|
|
6
|
+
connection.expungeDeleted(connection.selectedMailbox, true);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// CLOSE does not send pending EXPUNGE or EXISTS responses (RFC 3501 section 6.4.2)
|
|
10
|
+
connection.notificationQueue = [];
|
|
11
|
+
|
|
12
|
+
connection.send(
|
|
13
|
+
{
|
|
14
|
+
tag: parsed.tag,
|
|
15
|
+
command: 'OK',
|
|
16
|
+
attributes: [
|
|
17
|
+
{
|
|
18
|
+
type: 'TEXT',
|
|
19
|
+
value: 'Mailbox closed'
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
'CLOSE',
|
|
24
|
+
parsed,
|
|
25
|
+
data
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
connection.closeMailbox();
|
|
29
|
+
return callback();
|
|
30
|
+
};
|