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,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
+ };