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,115 @@
1
+ 'use strict';
2
+
3
+ module.exports = function (connection, parsed, data, callback) {
4
+ return copyMessages(connection, parsed, data, callback, false);
5
+ };
6
+
7
+ /**
8
+ * Shared implementation of COPY and UID COPY
9
+ *
10
+ * @param {Boolean} isUid If true, the sequence set holds UID values
11
+ */
12
+ function copyMessages(connection, parsed, data, callback, isUid) {
13
+ const command = isUid ? 'UID COPY' : 'COPY';
14
+
15
+ if (
16
+ !parsed.attributes ||
17
+ parsed.attributes.length !== 2 ||
18
+ !parsed.attributes[0] ||
19
+ ['ATOM', 'SEQUENCE'].indexOf(parsed.attributes[0].type) < 0 ||
20
+ !parsed.attributes[1] ||
21
+ ['ATOM', 'STRING', 'LITERAL'].indexOf(parsed.attributes[1].type) < 0
22
+ ) {
23
+ connection.send(
24
+ {
25
+ tag: parsed.tag,
26
+ command: 'BAD',
27
+ attributes: [
28
+ {
29
+ type: 'TEXT',
30
+ value: command + ' expects sequence set and a mailbox name'
31
+ }
32
+ ]
33
+ },
34
+ 'INVALID COMMAND',
35
+ parsed,
36
+ data
37
+ );
38
+ return callback();
39
+ }
40
+
41
+ const result = copyToMailbox(connection, parsed, data, isUid, command + ' FAIL');
42
+ if (!result) {
43
+ return callback();
44
+ }
45
+
46
+ // the result is the extra context info for UIDPLUS
47
+ connection.send(
48
+ {
49
+ tag: parsed.tag,
50
+ command: 'OK',
51
+ attributes: [
52
+ {
53
+ type: 'TEXT',
54
+ value: command + ' Completed'
55
+ }
56
+ ]
57
+ },
58
+ command,
59
+ parsed,
60
+ data,
61
+ result
62
+ );
63
+ callback();
64
+ }
65
+
66
+ /**
67
+ * Copies messages from the selected mailbox to another one, the copy step of COPY and MOVE.
68
+ * Sends a tagged NO and returns false if the target mailbox can not be used.
69
+ *
70
+ * @param {Object} connection IMAP connection
71
+ * @param {Object} parsed Parsed command, with the sequence set and the mailbox name as arguments
72
+ * @param {String} data Raw command
73
+ * @param {Boolean} isUid If true, the sequence set holds UID values
74
+ * @param {String} failDescription Description for the failure response
75
+ * @param {Boolean} [isMove] If true, the messages are moved, the caller expunges them afterwards
76
+ * @return {Object|false} `{ mailbox, sourceUids, targetUids, messages }`, where messages lists the copied source messages
77
+ */
78
+ function copyToMailbox(connection, parsed, data, isUid, failDescription, isMove) {
79
+ const range = connection.limitRange(parsed, connection.getCommandRange(parsed.attributes[0].value, isUid));
80
+ const mailbox = connection.getTargetMailbox(parsed.attributes[1].value, parsed, data, failDescription);
81
+ if (!mailbox) {
82
+ return false;
83
+ }
84
+
85
+ const messages = range.map(rangeMessage => rangeMessage[1]);
86
+ if (messages.some(message => message.ghost)) {
87
+ // RFC 2180 section 4.4.1: nothing is copied when the set includes a message that another session expunged,
88
+ // the pending EXPUNGE responses go out before the NO
89
+ connection.sendStatus(parsed, data, 'NO', 'Some of the requested messages no longer exist', 'EXPUNGEISSUED', failDescription);
90
+ return false;
91
+ }
92
+
93
+ if (!connection.checkAppend(mailbox, messages, parsed, data, failDescription, { move: !!isMove, source: connection.selectedMailbox })) {
94
+ return false;
95
+ }
96
+
97
+ const sourceUids = [];
98
+ const targetUids = [];
99
+ // a range that is processed from the highest UID down (parsed.highestFirst) is still copied in UID order,
100
+ // so the new UIDs follow the order of the source UIDs (RFC 9738 section 3.1 MOVE example)
101
+ (parsed.highestFirst ? messages.slice().reverse() : messages).forEach(message => {
102
+ sourceUids.push(message.uid);
103
+ targetUids.push(connection.server.copyMessage(mailbox, message).message.uid);
104
+ });
105
+
106
+ return {
107
+ mailbox,
108
+ sourceUids,
109
+ targetUids,
110
+ messages
111
+ };
112
+ }
113
+
114
+ module.exports.copyMessages = copyMessages;
115
+ module.exports.copyToMailbox = copyToMailbox;
@@ -0,0 +1,52 @@
1
+ 'use strict';
2
+
3
+ module.exports = function (connection, parsed, data, callback) {
4
+ if (!parsed.attributes || parsed.attributes.length !== 1 || !parsed.attributes[0] || ['STRING', 'LITERAL', 'ATOM'].indexOf(parsed.attributes[0].type) < 0) {
5
+ connection.send(
6
+ {
7
+ tag: parsed.tag,
8
+ command: 'BAD',
9
+ attributes: [
10
+ {
11
+ type: 'TEXT',
12
+ value: 'CREATE expects mailbox name'
13
+ }
14
+ ]
15
+ },
16
+ 'INVALID COMMAND',
17
+ parsed,
18
+ data
19
+ );
20
+ return callback();
21
+ }
22
+
23
+ const path = parsed.attributes[0].value;
24
+
25
+ let mailbox;
26
+ try {
27
+ mailbox = connection.server.createMailbox(path);
28
+ } catch (E) {
29
+ // RFC 5530 response codes, e.g. [ALREADYEXISTS]
30
+ connection.sendStatus(parsed, data, 'NO', E.message, E.code, 'CREATE FAILED');
31
+ return callback();
32
+ }
33
+ connection.server.mailboxChanged('create', mailbox.path);
34
+
35
+ connection.send(
36
+ {
37
+ tag: parsed.tag,
38
+ command: 'OK',
39
+ attributes: [
40
+ {
41
+ type: 'TEXT',
42
+ value: 'CREATE completed'
43
+ }
44
+ ]
45
+ },
46
+ 'CREATE',
47
+ parsed,
48
+ data,
49
+ mailbox
50
+ );
51
+ return callback();
52
+ };
@@ -0,0 +1,64 @@
1
+ 'use strict';
2
+
3
+ module.exports = function (connection, parsed, data, callback) {
4
+ if (!parsed.attributes || parsed.attributes.length !== 1 || !parsed.attributes[0] || ['STRING', 'LITERAL', 'ATOM'].indexOf(parsed.attributes[0].type) < 0) {
5
+ connection.send(
6
+ {
7
+ tag: parsed.tag,
8
+ command: 'BAD',
9
+ attributes: [
10
+ {
11
+ type: 'TEXT',
12
+ value: 'DELETE expects mailbox name'
13
+ }
14
+ ]
15
+ },
16
+ 'INVALID COMMAND',
17
+ parsed,
18
+ data
19
+ );
20
+ return callback();
21
+ }
22
+
23
+ const path = parsed.attributes[0].value;
24
+
25
+ const mailbox = connection.server.getMailbox(path);
26
+
27
+ if (!mailbox) {
28
+ connection.sendStatus(parsed, data, 'NO', 'Mailbox does not exist', 'NONEXISTENT', 'DELETE FAILED');
29
+ return callback();
30
+ }
31
+
32
+ try {
33
+ connection.server.deleteMailbox(path);
34
+ } catch (E) {
35
+ connection.sendStatus(parsed, data, 'NO', E.message, E.code, 'DELETE FAILED');
36
+ return callback();
37
+ }
38
+ connection.server.mailboxChanged('delete', mailbox.path, { mailbox });
39
+
40
+ // RFC 2180 section 3.3: the other sessions that have the mailbox selected are disconnected with an untagged
41
+ // BYE (RFC 2683 section 3.1.2), as they can not be told about the deletion in any other way
42
+ connection.server.connections.forEach(other => {
43
+ if (other !== connection && other.selectedMailbox === mailbox) {
44
+ other.bye('Selected mailbox was deleted', 'MAILBOX DELETED');
45
+ }
46
+ });
47
+
48
+ connection.send(
49
+ {
50
+ tag: parsed.tag,
51
+ command: 'OK',
52
+ attributes: [
53
+ {
54
+ type: 'TEXT',
55
+ value: 'DELETE completed'
56
+ }
57
+ ]
58
+ },
59
+ 'DELETE',
60
+ parsed,
61
+ data
62
+ );
63
+ return callback();
64
+ };
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+
3
+ const { selectMailbox } = require('./select');
4
+
5
+ module.exports = function (connection, parsed, data, callback) {
6
+ return selectMailbox(connection, parsed, data, callback, true);
7
+ };
@@ -0,0 +1,27 @@
1
+ 'use strict';
2
+
3
+ module.exports = function (connection, parsed, data, callback) {
4
+ if (connection.refuseReadOnly(parsed, data, 'EXPUNGE FAILED')) {
5
+ return callback();
6
+ }
7
+
8
+ connection.expungeDeleted(connection.selectedMailbox, false, true);
9
+
10
+ connection.send(
11
+ {
12
+ tag: parsed.tag,
13
+ command: 'OK',
14
+ attributes: [
15
+ {
16
+ type: 'TEXT',
17
+ value: 'EXPUNGE Completed'
18
+ }
19
+ ]
20
+ },
21
+ 'EXPUNGE',
22
+ parsed,
23
+ data
24
+ );
25
+
26
+ callback();
27
+ };
@@ -0,0 +1,229 @@
1
+ 'use strict';
2
+
3
+ const fetchHandlers = require('./handlers/fetch');
4
+
5
+ const macros = {
6
+ ALL: ['FLAGS', 'INTERNALDATE', 'RFC822.SIZE', 'ENVELOPE'],
7
+ FAST: ['FLAGS', 'INTERNALDATE', 'RFC822.SIZE'],
8
+ FULL: ['FLAGS', 'INTERNALDATE', 'RFC822.SIZE', 'ENVELOPE', 'BODY']
9
+ };
10
+
11
+ /**
12
+ * Returns the item names as they appear in a FETCH response, without .PEEK and only with the origin
13
+ * octet of a partial range (RFC 3501 section 7.4.2)
14
+ *
15
+ * @param {Array} params Fetch items
16
+ * @return {Array} names
17
+ */
18
+ function responseNames(params) {
19
+ return params.map(param => {
20
+ const name = Object.assign({}, param, { value: param.value.replace(/\.PEEK\b/i, '') });
21
+ if (name.partial) {
22
+ name.partial = name.partial.slice(0, 1);
23
+ }
24
+ return name;
25
+ });
26
+ }
27
+
28
+ /**
29
+ * Returns the handlers of fetch items
30
+ *
31
+ * @param {Object} connection IMAP connection
32
+ * @param {Array} params Fetch items
33
+ * @return {Array} handlers
34
+ * @throws {Error} for an unknown item
35
+ */
36
+ function getHandlers(connection, params) {
37
+ return params.map((param, i) => {
38
+ const key = (param.value || '').toUpperCase();
39
+ const handler = connection.server.fetchHandlers[key] || fetchHandlers[key];
40
+ if (!handler) {
41
+ throw new Error('Invalid FETCH argument ' + (key ? ' ' + key : '#' + (i + 1)));
42
+ }
43
+ return handler;
44
+ });
45
+ }
46
+
47
+ /**
48
+ * Builds the data list of a FETCH response for a message, without any side effects like setting \Seen
49
+ *
50
+ * @param {Object} connection IMAP connection
51
+ * @param {Object} message Message object
52
+ * @param {Array} params Fetch items
53
+ * @param {Array} [handlers] Handlers from getHandlers
54
+ * @param {Array} [names] Names from responseNames
55
+ * @return {Array} FETCH data items
56
+ * @throws {Error} for an unknown item or invalid item arguments
57
+ */
58
+ function fetchResponse(connection, message, params, handlers, names) {
59
+ handlers = handlers || getHandlers(connection, params);
60
+ names = names || responseNames(params);
61
+ const response = [];
62
+ params.forEach((param, i) => response.push(names[i], handlers[i](connection, message, param)));
63
+ return response;
64
+ }
65
+
66
+ /**
67
+ * Shared implementation of FETCH and UID FETCH
68
+ *
69
+ * @param {Boolean} isUid If true, the sequence set lists UIDs and UID is always included in the response
70
+ */
71
+ function processFetch(isUid, connection, parsed, data, callback) {
72
+ const command = isUid ? 'UID FETCH' : 'FETCH';
73
+
74
+ if (
75
+ !parsed.attributes ||
76
+ parsed.attributes.length !== 2 ||
77
+ !parsed.attributes[0] ||
78
+ ['ATOM', 'SEQUENCE'].indexOf(parsed.attributes[0].type) < 0 ||
79
+ !parsed.attributes[1] ||
80
+ (['ATOM'].indexOf(parsed.attributes[1].type) < 0 && !Array.isArray(parsed.attributes[1]))
81
+ ) {
82
+ connection.send(
83
+ {
84
+ tag: parsed.tag,
85
+ command: 'BAD',
86
+ attributes: [
87
+ {
88
+ type: 'TEXT',
89
+ value: command + ' expects sequence set and message item names'
90
+ }
91
+ ]
92
+ },
93
+ 'INVALID COMMAND',
94
+ parsed,
95
+ data
96
+ );
97
+ return callback();
98
+ }
99
+
100
+ const range = connection.limitRange(parsed, connection.getCommandRange(parsed.attributes[0].value, isUid));
101
+ let params = [].concat(parsed.attributes[1] || []);
102
+
103
+ if (parsed.attributes[1].type === 'ATOM' && Object.prototype.hasOwnProperty.call(macros, parsed.attributes[1].value.toUpperCase())) {
104
+ params = macros[parsed.attributes[1].value.toUpperCase()].slice();
105
+ }
106
+
107
+ // messages that get \Seen set by this FETCH
108
+ const seen = [];
109
+ // untagged FETCH responses and their messages
110
+ const responses = [];
111
+ const messages = [];
112
+
113
+ try {
114
+ let uidExist = false;
115
+ let flagsExist = false;
116
+ let forceSeen = false;
117
+
118
+ params.forEach((param, i) => {
119
+ if (!param || (typeof param !== 'string' && param.type !== 'ATOM')) {
120
+ throw new Error('Invalid FETCH argument #' + (i + 1));
121
+ }
122
+
123
+ if (typeof param === 'string') {
124
+ param = params[i] = {
125
+ type: 'ATOM',
126
+ value: param
127
+ };
128
+ }
129
+
130
+ const key = param.value.toUpperCase();
131
+
132
+ if (key === 'FLAGS') {
133
+ flagsExist = true;
134
+ }
135
+
136
+ if (key === 'UID') {
137
+ uidExist = true;
138
+ }
139
+
140
+ // RFC 3501 6.4.5: BODY[<section>] and items whose handler is marked with setsSeen set \Seen
141
+ const handler = connection.server.fetchHandlers[key] || fetchHandlers[key];
142
+ if (connection.canSetSeen() && ((key === 'BODY' && param.section) || (handler && handler.setsSeen))) {
143
+ forceSeen = true;
144
+ }
145
+ });
146
+
147
+ if (forceSeen && !flagsExist) {
148
+ params.push({
149
+ type: 'ATOM',
150
+ value: 'FLAGS'
151
+ });
152
+ }
153
+
154
+ if (isUid && !uidExist) {
155
+ params.push({
156
+ type: 'ATOM',
157
+ value: 'UID'
158
+ });
159
+ }
160
+
161
+ const handlers = getHandlers(connection, params);
162
+
163
+ // item names as they appear in the response
164
+ const names = responseNames(params);
165
+
166
+ // nothing is sent before every message is done, so a failing item leaves no partial output
167
+ range.forEach(rangeMessage => {
168
+ for (let i = 0, len = connection.server.fetchFilters.length; i < len; i++) {
169
+ if (!connection.server.fetchFilters[i](connection, rangeMessage[1], parsed, rangeMessage[0])) {
170
+ return;
171
+ }
172
+ }
173
+
174
+ if (forceSeen && rangeMessage[1].flags.indexOf('\\Seen') < 0) {
175
+ rangeMessage[1].flags.push('\\Seen');
176
+ seen.push(rangeMessage[1]);
177
+ }
178
+
179
+ const response = fetchResponse(connection, rangeMessage[1], params, handlers, names);
180
+
181
+ messages.push(rangeMessage[1]);
182
+ responses.push({
183
+ tag: '*',
184
+ attributes: [
185
+ rangeMessage[0],
186
+ {
187
+ type: 'ATOM',
188
+ value: 'FETCH'
189
+ },
190
+ response
191
+ ]
192
+ });
193
+ });
194
+ } catch (E) {
195
+ // the failed FETCH did not set \Seen after all
196
+ seen.forEach(message => connection.server.removeFlag(message.flags, '\\Seen'));
197
+ // a handler can fail with NO and a response code, e.g. NO [UNKNOWN-CTE] (RFC 3516 section 4.3)
198
+ connection.sendStatus(parsed, data, E.imapResponse === 'NO' ? 'NO' : 'BAD', E.message, E.code, command + ' FAILED');
199
+ return callback();
200
+ }
201
+
202
+ // the message goes with its response, for output handlers
203
+ responses.forEach((response, i) => connection.send(response, command, parsed, data, messages[i]));
204
+
205
+ connection.send(
206
+ {
207
+ tag: parsed.tag,
208
+ command: 'OK',
209
+ attributes: [
210
+ {
211
+ type: 'TEXT',
212
+ value: command + ' Completed'
213
+ }
214
+ ]
215
+ },
216
+ command,
217
+ parsed,
218
+ data
219
+ );
220
+
221
+ // other sessions that have the mailbox selected learn about the new flags (RFC 3501 section 5.2)
222
+ connection.notifyFlagChanges(seen);
223
+ return callback();
224
+ }
225
+
226
+ module.exports = (connection, parsed, data, callback) => processFetch(false, connection, parsed, data, callback);
227
+ module.exports.processFetch = processFetch;
228
+ module.exports.macros = macros;
229
+ module.exports.fetchResponse = fetchResponse;