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,285 @@
1
+ 'use strict';
2
+
3
+ const { states } = require('../command-states');
4
+ const { normalizeLineBreaks } = require('../mimeparser');
5
+ const { MAX_NUMBER64, isNumber } = require('../numbers');
6
+ const { isAstring } = require('../arguments');
7
+
8
+ // Resource types of RFC 9208 section 5 that can be counted. ANNOTATION-STORAGE is left out,
9
+ // as there is no ANNOTATE support
10
+ const RESOURCES = ['STORAGE', 'MESSAGE', 'MAILBOX'];
11
+ const DEFAULT_ROOT = 'User quota';
12
+
13
+ /**
14
+ * @help Adds QUOTA [RFC9208] capability with the STORAGE, MESSAGE and MAILBOX
15
+ * @help resources and SETQUOTA. All personal mailboxes share one quota root, set
16
+ * @help with the "quota" server option, eg. {"root": "User quota",
17
+ * @help "STORAGE": 10240, "MESSAGE": 1000, "MAILBOX": 100, "soft": false}.
18
+ * @help STORAGE is in units of 1024 octets. Hard limits make APPEND, COPY, MOVE
19
+ * @help and CREATE fail with NO [OVERQUOTA], soft limits only send a warning
20
+ *
21
+ * QUOTA: https://www.rfc-editor.org/rfc/rfc9208.txt
22
+ *
23
+ * Additional commands:
24
+ * - GETQUOTA, GETQUOTAROOT, SETQUOTA
25
+ *
26
+ * Additional STATUS items:
27
+ * - DELETED, DELETED-STORAGE
28
+ */
29
+ module.exports = function (server) {
30
+ const config = server.options.quota || {};
31
+ const root = {
32
+ name: typeof config.root === 'string' ? config.root : DEFAULT_ROOT,
33
+ soft: !!config.soft,
34
+ limits: {}
35
+ };
36
+
37
+ RESOURCES.forEach(resource => {
38
+ if (config[resource] === undefined || config[resource] === null) {
39
+ return;
40
+ }
41
+ const limit = Number(config[resource]);
42
+ if (!Number.isSafeInteger(limit) || limit < 0) {
43
+ throw new Error('Invalid quota limit for ' + resource + ': ' + JSON.stringify(config[resource]));
44
+ }
45
+ root.limits[resource] = limit;
46
+ });
47
+
48
+ // RFC 9208 sections 3.1.1 and 5: every supported resource is advertised, SETQUOTA needs QUOTASET
49
+ server.registerCapability('QUOTA');
50
+ RESOURCES.forEach(resource => server.registerCapability('QUOTA=RES-' + resource));
51
+ server.registerCapability('QUOTASET');
52
+
53
+ // Personal mailboxes, INBOX included, belong to the quota root. Other namespaces have no quota root. A mailbox
54
+ // that does not exist yet belongs to the namespace of its prefix
55
+ const inRoot = path => server.isPersonal(path);
56
+
57
+ const isSelectable = mailbox => mailbox.flags.indexOf('\\Noselect') < 0;
58
+
59
+ // RFC822.SIZE, the message with CRLF line breaks
60
+ const totalSize = messages => messages.reduce((total, message) => total + normalizeLineBreaks(message.raw).length, 0);
61
+
62
+ const getUsage = () => {
63
+ const usage = { MESSAGE: 0, MAILBOX: 0, bytes: 0 };
64
+ Object.keys(server.folderCache).forEach(path => {
65
+ const mailbox = server.folderCache[path];
66
+ if (!isSelectable(mailbox) || !inRoot(mailbox)) {
67
+ return;
68
+ }
69
+ usage.MAILBOX++;
70
+ usage.MESSAGE += mailbox.messages.length;
71
+ usage.bytes += totalSize(mailbox.messages);
72
+ });
73
+ return usage;
74
+ };
75
+
76
+ // RFC 9208 section 4.2.1: quota-list, resources that are not listed are not limited
77
+ const sendQuota = (connection, parsed, data) => {
78
+ const usage = getUsage();
79
+ // RFC 9208 section 5.1: units of 1024 octets
80
+ usage.STORAGE = Math.ceil(usage.bytes / 1024);
81
+ const list = [];
82
+ RESOURCES.forEach(resource => {
83
+ if (resource in root.limits) {
84
+ list.push({ type: 'ATOM', value: resource }, usage[resource], root.limits[resource]);
85
+ }
86
+ });
87
+ connection.send(
88
+ {
89
+ tag: '*',
90
+ command: 'QUOTA',
91
+ attributes: [{ type: 'STRING', value: root.name }, list]
92
+ },
93
+ 'QUOTA',
94
+ parsed,
95
+ data
96
+ );
97
+ };
98
+
99
+ /**
100
+ * Checks if adding to the quota root goes over a limit
101
+ *
102
+ * @param {Object} added Added amounts, `{ MESSAGE, MAILBOX, bytes }`
103
+ * @return {Object|undefined} `{ code, text, soft }` for IMAPConnection#checkAppend, or nothing if within limits
104
+ */
105
+ const checkLimits = added => {
106
+ const limited = (added.MESSAGE && 'MESSAGE' in root.limits) || (added.bytes && 'STORAGE' in root.limits) || (added.MAILBOX && 'MAILBOX' in root.limits);
107
+ if (!limited) {
108
+ return;
109
+ }
110
+ const usage = getUsage();
111
+ const over =
112
+ (added.MESSAGE && 'MESSAGE' in root.limits && usage.MESSAGE + added.MESSAGE > root.limits.MESSAGE) ||
113
+ (added.bytes && 'STORAGE' in root.limits && usage.bytes + added.bytes > root.limits.STORAGE * 1024) ||
114
+ (added.MAILBOX && 'MAILBOX' in root.limits && usage.MAILBOX + added.MAILBOX > root.limits.MAILBOX);
115
+ if (!over) {
116
+ return;
117
+ }
118
+ // RFC 9208 section 4.3.1 and RFC 5530 section 3
119
+ return root.soft ? { code: 'OVERQUOTA', text: 'Soft quota has been exceeded', soft: true } : { code: 'OVERQUOTA', text: 'Quota exceeded' };
120
+ };
121
+
122
+ // APPEND, COPY, MOVE and REPLACE
123
+ server.appendChecks.push((connection, mailbox, messages, options) => {
124
+ if (!messages.length || !inRoot(mailbox)) {
125
+ return;
126
+ }
127
+ if (options.move && options.source && inRoot(options.source)) {
128
+ // moving within the quota root does not change the usage
129
+ return;
130
+ }
131
+ const added = { MESSAGE: messages.length, bytes: totalSize(messages) };
132
+ // RFC 8508 section 3.4: REPLACE counts only the net usage, the replaced message is removed
133
+ if (options.replaced && connection.selectedMailbox && inRoot(connection.selectedMailbox)) {
134
+ added.MESSAGE--;
135
+ added.bytes -= totalSize([options.replaced]);
136
+ }
137
+ return checkLimits(added);
138
+ });
139
+
140
+ // Number of mailboxes that CREATE adds, missing parent mailboxes are created as well
141
+ const countNewMailboxes = path => {
142
+ const target = server.getMailbox(server.stripSeparator(path, server.getSeparator(path)));
143
+ let count = !target || !isSelectable(target) ? 1 : 0;
144
+ for (let parent = server.getParentPath(path); parent; parent = server.getParentPath(parent)) {
145
+ if (!server.getMailbox(parent)) {
146
+ count++;
147
+ }
148
+ }
149
+ return count;
150
+ };
151
+
152
+ // The MAILBOX resource limits CREATE and RENAME INBOX (which creates a mailbox)
153
+ const wrapCreate = (command, getPath) => {
154
+ const prevHandler = server.getCommandHandler(command);
155
+ server.setCommandHandler(command, (connection, parsed, data, callback) => {
156
+ const path = getPath(parsed.attributes);
157
+ if (
158
+ typeof path === 'string' &&
159
+ path.toUpperCase() !== 'INBOX' &&
160
+ inRoot(path) &&
161
+ !connection.applyChecks([checkLimits({ MAILBOX: countNewMailboxes(path) })], parsed, data, command + ' FAILED')
162
+ ) {
163
+ return callback();
164
+ }
165
+ prevHandler(connection, parsed, data, callback);
166
+ });
167
+ };
168
+
169
+ const argValue = (attributes, i) => isAstring((attributes || [])[i]) && attributes[i].value;
170
+
171
+ wrapCreate('CREATE', attributes => (attributes && attributes.length === 1 ? argValue(attributes, 0) : false));
172
+ wrapCreate('RENAME', attributes => {
173
+ const source = argValue(attributes, 0);
174
+ // only RENAME INBOX adds a mailbox, other renames keep the count (RFC 3501 section 6.3.5)
175
+ return attributes && attributes.length === 2 && typeof source === 'string' && source.toUpperCase() === 'INBOX' ? argValue(attributes, 1) : false;
176
+ });
177
+
178
+ // RFC 9208 section 4.1.1: GETQUOTA quota-root-name
179
+ server.setCommandHandler(
180
+ 'GETQUOTA',
181
+ (connection, parsed, data, callback) => {
182
+ if (!parsed.attributes || parsed.attributes.length !== 1 || !isAstring(parsed.attributes[0])) {
183
+ connection.sendStatus(parsed, data, 'BAD', 'GETQUOTA expects a quota root name');
184
+ return callback();
185
+ }
186
+ if (parsed.attributes[0].value !== root.name) {
187
+ connection.sendStatus(parsed, data, 'NO', 'Quota root does not exist', 'NONEXISTENT');
188
+ return callback();
189
+ }
190
+ sendQuota(connection, parsed, data);
191
+ connection.sendStatus(parsed, data, 'OK', 'GETQUOTA completed');
192
+ return callback();
193
+ },
194
+ // quota-root = astring (RFC 9208 section 9)
195
+ { states: states.AUTHENTICATED, astringArguments: [0] }
196
+ );
197
+
198
+ // RFC 9208 section 4.1.2: GETQUOTAROOT mailbox, the mailbox does not have to exist
199
+ server.setCommandHandler(
200
+ 'GETQUOTAROOT',
201
+ (connection, parsed, data, callback) => {
202
+ if (!parsed.attributes || parsed.attributes.length !== 1 || !isAstring(parsed.attributes[0])) {
203
+ connection.sendStatus(parsed, data, 'BAD', 'GETQUOTAROOT expects a mailbox name');
204
+ return callback();
205
+ }
206
+ let path = parsed.attributes[0].value;
207
+ if (path.toUpperCase() === 'INBOX') {
208
+ path = 'INBOX';
209
+ }
210
+ const hasRoot = inRoot(path);
211
+ connection.send(
212
+ {
213
+ tag: '*',
214
+ command: 'QUOTAROOT',
215
+ attributes: [{ type: 'MAILBOX', value: path }].concat(hasRoot ? { type: 'STRING', value: root.name } : [])
216
+ },
217
+ 'QUOTAROOT',
218
+ parsed,
219
+ data
220
+ );
221
+ if (hasRoot) {
222
+ sendQuota(connection, parsed, data);
223
+ }
224
+ connection.sendStatus(parsed, data, 'OK', 'GETQUOTAROOT completed');
225
+ return callback();
226
+ },
227
+ { states: states.AUTHENTICATED, mailboxArguments: [0] }
228
+ );
229
+
230
+ // RFC 9208 section 4.1.3: SETQUOTA quota-root-name setquota-list, the new limits replace all earlier ones
231
+ server.setCommandHandler(
232
+ 'SETQUOTA',
233
+ (connection, parsed, data, callback) => {
234
+ const list = parsed.attributes && parsed.attributes[1];
235
+ if (!parsed.attributes || parsed.attributes.length !== 2 || !isAstring(parsed.attributes[0]) || !Array.isArray(list) || list.length % 2) {
236
+ connection.sendStatus(parsed, data, 'BAD', 'SETQUOTA expects a quota root name and a list of resource limits');
237
+ return callback();
238
+ }
239
+
240
+ const limits = {};
241
+ for (let i = 0; i < list.length; i += 2) {
242
+ const name = list[i];
243
+ const limit = list[i + 1];
244
+ // setquota-resource = resource-name SP resource-limit, resource-limit = number64
245
+ if (!name || name.type !== 'ATOM' || !limit || limit.type !== 'ATOM' || !isNumber(limit.value, MAX_NUMBER64)) {
246
+ connection.sendStatus(parsed, data, 'BAD', 'Invalid resource limit');
247
+ return callback();
248
+ }
249
+ const resource = name.value.toUpperCase();
250
+ if (RESOURCES.indexOf(resource) < 0 || resource in limits) {
251
+ connection.sendStatus(parsed, data, 'NO', 'Can not set a limit for ' + resource, 'CANNOT');
252
+ return callback();
253
+ }
254
+ const value = Number(limit.value);
255
+ if (!Number.isSafeInteger(value)) {
256
+ connection.sendStatus(parsed, data, 'NO', 'Limit is too large for ' + resource, 'LIMIT');
257
+ return callback();
258
+ }
259
+ limits[resource] = value;
260
+ }
261
+
262
+ // new quota roots can not be created
263
+ if (parsed.attributes[0].value !== root.name) {
264
+ connection.sendStatus(parsed, data, 'NO', 'Quota root does not exist', 'NONEXISTENT');
265
+ return callback();
266
+ }
267
+
268
+ root.limits = limits;
269
+ sendQuota(connection, parsed, data);
270
+ connection.sendStatus(parsed, data, 'OK', 'SETQUOTA completed');
271
+ return callback();
272
+ },
273
+ // quota-root = astring (RFC 9208 section 9)
274
+ { states: states.AUTHENTICATED, astringArguments: [0] }
275
+ );
276
+
277
+ // RFC 9208 section 4.1.4: DELETED (the built-in item, required with QUOTA=RES-MESSAGE) and DELETED-STORAGE
278
+ // (required with QUOTA=RES-STORAGE), the storage EXPUNGE would free as the sum of RFC822.SIZE
279
+ ['DELETED', 'DELETED-STORAGE'].forEach(item => {
280
+ if (server.allowedStatus.indexOf(item) < 0) {
281
+ server.allowedStatus.push(item);
282
+ }
283
+ });
284
+ server.statusHandlers['DELETED-STORAGE'] = (connection, mailbox) => totalSize(mailbox.messages.filter(message => message.flags.indexOf('\\Deleted') >= 0));
285
+ };
@@ -0,0 +1,145 @@
1
+ 'use strict';
2
+
3
+ const { prepareAppend, storeMessages } = require('../commands/append');
4
+ const { states } = require('../command-states');
5
+ const { isNzNumber } = require('../numbers');
6
+
7
+ // RFC 8508 section 5 and RFC 3501 section 9: seq-number = nz-number / "*"
8
+ const isSeqNumber = value => value === '*' || isNzNumber(value);
9
+
10
+ /**
11
+ * @help Adds REPLACE [RFC8508] capability (REPLACE and UID REPLACE commands)
12
+ *
13
+ * REPLACE: https://www.rfc-editor.org/rfc/rfc8508
14
+ *
15
+ * Additional commands:
16
+ * - REPLACE
17
+ * - UID REPLACE
18
+ *
19
+ * The new message is appended and the old one expunged as a single action. With UIDPLUS the
20
+ * APPENDUID response code is sent in an untagged OK before the EXPUNGE. Works with CATENATE.
21
+ */
22
+ module.exports = function (server) {
23
+ server.registerCapability('REPLACE');
24
+
25
+ const replaceHandler = function (uidMode, connection, parsed, data, callback) {
26
+ const command = uidMode ? 'UID REPLACE' : 'REPLACE';
27
+ const args = [].concat(parsed.attributes || []);
28
+ const id = args.shift();
29
+ const pathArg = args.shift();
30
+
31
+ // RFC 8508 section 5: replace = "REPLACE" SP seq-number SP mailbox append-message
32
+ if (!id || ['ATOM', 'SEQUENCE'].indexOf(id.type) < 0 || !isSeqNumber(String(id.value))) {
33
+ connection.sendStatus(parsed, data, 'BAD', command + ' expects a message number, a mailbox name and a message', false, 'INVALID COMMAND');
34
+ return callback();
35
+ }
36
+
37
+ const found = findMessage(connection, uidMode, id.value);
38
+ if (found.error) {
39
+ connection.sendStatus(
40
+ parsed,
41
+ data,
42
+ found.error.command,
43
+ found.error.text,
44
+ found.error.code || false,
45
+ found.error.command === 'BAD' ? 'INVALID COMMAND' : command + ' FAILED'
46
+ );
47
+ return callback();
48
+ }
49
+ const message = found.message;
50
+
51
+ // RFC 8508 section 4.7: REPLACE takes a single message even with MULTIAPPEND. Nothing is
52
+ // changed if the new message can not be appended (section 3.4)
53
+ const prepared = prepareAppend(connection, parsed, data, pathArg, args, { command, maxMessages: 1, replaced: message });
54
+ if (!prepared) {
55
+ return callback();
56
+ }
57
+
58
+ const result = storeMessages(connection, prepared);
59
+
60
+ // Hook for UIDPLUS to send APPENDUID in an untagged OK before the EXPUNGE
61
+ // (RFC 8508 section 4.3). If UIDPLUS is not loaded, this is not sent
62
+ connection.send(
63
+ {
64
+ tag: '*',
65
+ command: 'OK',
66
+ attributes: [
67
+ {
68
+ type: 'TEXT',
69
+ value: 'Replacement message saved'
70
+ }
71
+ ],
72
+ skipResponse: true
73
+ },
74
+ 'REPLACE APPENDUID',
75
+ parsed,
76
+ data,
77
+ result
78
+ );
79
+
80
+ // only the replaced message is expunged, not every \Deleted message (RFC 8508 section 3.4).
81
+ // When the new message went to the selected mailbox, it was already announced with EXISTS
82
+ connection.expungeSpecificMessages(connection.selectedMailbox, [message], false, true);
83
+
84
+ connection.sendStatus(parsed, data, 'OK', command + ' completed', false, command);
85
+ callback();
86
+ };
87
+
88
+ // Refuse the literal of the new message when the message to replace is already known to be
89
+ // invalid, unless earlier commands are still running and could change the selected mailbox
90
+ server.literalFilters.push((connection, command, line) => {
91
+ if ((command !== 'REPLACE' && command !== 'UID REPLACE') || connection.isBusy()) {
92
+ return false;
93
+ }
94
+ const match = line.match(/^\S+ (?:UID )?REPLACE ([^ ]+) /i);
95
+ return (match && isSeqNumber(match[1]) && findMessage(connection, command === 'UID REPLACE', match[1]).error) || false;
96
+ });
97
+
98
+ // RFC 8508 section 3.5: valid only in the selected state, the second argument is the target mailbox
99
+ // the message can be a literal8 with BINARY: RFC 8508 section 6 uses append-message, whose append-data
100
+ // is a literal8 too (RFC 4466 section 2.7)
101
+ // REPLACE takes a message sequence number, UID REPLACE a UID
102
+ const options = { states: states.SELECTED, mailboxArguments: [1], literal8: 'BINARY', appendMessage: true };
103
+ server.setCommandHandler('REPLACE', replaceHandler.bind(null, false), Object.assign({ sequenceSet: 0 }, options));
104
+ server.setCommandHandler('UID REPLACE', replaceHandler.bind(null, true), options);
105
+ };
106
+
107
+ /**
108
+ * Finds the message to replace in the selected mailbox
109
+ *
110
+ * @param {Object} connection IMAPConnection
111
+ * @param {Boolean} uidMode If true, the value is a UID, otherwise a sequence number
112
+ * @param {String} value seq-number, a number or "*"
113
+ * @return {Object} `{ message }`, or `{ error: { command, text } }` if there is no such message
114
+ */
115
+ function findMessage(connection, uidMode, value) {
116
+ // sequence numbers refer to the message list this session knows about
117
+ const messages = connection.getSessionMessages();
118
+ let message;
119
+ if (uidMode) {
120
+ // "*" is the UID of the last message (RFC 3501 section 9, seq-number)
121
+ const uid = value === '*' ? (messages.length ? messages[messages.length - 1].uid : 0) : Number(value);
122
+ message = messages.find(item => item.uid === uid);
123
+ } else {
124
+ const seq = value === '*' ? messages.length : Number(value);
125
+ // RFC 3501 section 9, seq-number: a sequence number greater than the number of messages
126
+ // (or "*" in an empty mailbox) gets a tagged BAD
127
+ if (!seq || seq > messages.length) {
128
+ return { error: { command: 'BAD', text: 'Invalid message sequence number' } };
129
+ }
130
+ message = messages[seq - 1];
131
+ }
132
+
133
+ // RFC 8508 section 3.2: NO - can't remove specified message
134
+ if (!message || message.ghost) {
135
+ return { error: { command: 'NO', text: 'No such message' } };
136
+ }
137
+
138
+ // the old message is expunged, which is not allowed after EXAMINE
139
+ const readOnly = connection.readOnlyRefusal();
140
+ if (readOnly) {
141
+ return { error: readOnly };
142
+ }
143
+
144
+ return { message };
145
+ }
@@ -0,0 +1,12 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @help Enables SASL-IR [RFC4959] capability
5
+ */
6
+
7
+ module.exports = function (server) {
8
+ // Register capability, usable for non authenticated users
9
+ server.registerCapability('SASL-IR', connection => {
10
+ return connection.state === 'Not Authenticated';
11
+ });
12
+ };
@@ -0,0 +1,59 @@
1
+ 'use strict';
2
+
3
+ const { getDateKey } = require('../commands/handlers/search');
4
+
5
+ /**
6
+ * @help Adds SAVEDATE [RFC8514] capability
7
+ * @help Every message gets a save date when it is added to a mailbox (APPEND,
8
+ * @help COPY, MOVE, and when the storage is loaded), messages in storage can
9
+ * @help set it with a "SAVEDATE" value. A mailbox with "SAVEDATE": false in
10
+ * @help storage does not support save dates, FETCH returns NIL for its messages
11
+ *
12
+ * SAVEDATE: https://www.rfc-editor.org/rfc/rfc8514.txt
13
+ *
14
+ * Additional FETCH items:
15
+ * - SAVEDATE
16
+ *
17
+ * Additional SEARCH keys:
18
+ * - SAVEDBEFORE, SAVEDON, SAVEDSINCE, SAVEDATESUPPORTED
19
+ */
20
+ module.exports = function (server) {
21
+ server.registerCapability('SAVEDATE');
22
+
23
+ // RFC 8514 section 3: storage may lack save dates for some mailboxes
24
+ const isSupported = mailbox => !!mailbox && mailbox.SAVEDATE !== false;
25
+
26
+ // RFC 8514 section 3: the save date is the current time when the message is saved in a mailbox, it is
27
+ // never copied from the source message. Messages from storage may bring their own value.
28
+ server.messageHandlers.push((server, message, mailbox) => {
29
+ if (!isSupported(mailbox)) {
30
+ delete message.SAVEDATE;
31
+ return;
32
+ }
33
+
34
+ const savedate = server.normalizeDateTime(message.SAVEDATE || new Date());
35
+ if (!server.validateInternalDate(savedate)) {
36
+ throw new Error('Invalid SAVEDATE value ' + JSON.stringify(savedate) + ' in mailbox ' + mailbox.path);
37
+ }
38
+ message.SAVEDATE = savedate;
39
+ });
40
+
41
+ // RFC 8514 section 4.2: date-time, or NIL if the mailbox does not support save dates
42
+ server.fetchHandlers.SAVEDATE = (connection, message) => message.SAVEDATE || null;
43
+
44
+ const dateKey = (name, compare) => {
45
+ const handler = (connection, message, index, date) => {
46
+ // RFC 8514 section 4.3: without save dates the internal date is used instead
47
+ const saved = getDateKey(message.SAVEDATE || message.internaldate);
48
+ return !!saved && compare(saved, date);
49
+ };
50
+ handler.argumentTypes = () => ['date'];
51
+ server.searchHandlers[name] = handler;
52
+ };
53
+
54
+ dateKey('SAVEDBEFORE', (saved, date) => saved < date);
55
+ dateKey('SAVEDON', (saved, date) => saved === date);
56
+ dateKey('SAVEDSINCE', (saved, date) => saved >= date);
57
+
58
+ server.searchHandlers.SAVEDATESUPPORTED = connection => isSupported(connection.selectedMailbox);
59
+ };
@@ -0,0 +1,18 @@
1
+ 'use strict';
2
+
3
+ const { getLimit, addSaveLimit } = require('./messagelimit');
4
+
5
+ /**
6
+ * @help Adds SAVELIMIT [RFC9738] capability, advertised as SAVELIMIT=<n>
7
+ * @help Server option "messageLimit" sets n (default 1000). COPY and APPEND (MULTIAPPEND)
8
+ * @help of more messages fail with NO [MESSAGELIMIT ...], other commands are not limited.
9
+ * @help Can not be loaded with MESSAGELIMIT
10
+ *
11
+ * SAVELIMIT: https://www.rfc-editor.org/rfc/rfc9738
12
+ */
13
+ module.exports = function (server) {
14
+ // RFC 9738 section 3: a server that only limits COPY and APPEND (and their UID variants) advertises SAVELIMIT
15
+ const limit = getLimit(server, 'SAVELIMIT');
16
+ server.registerCapability('SAVELIMIT=' + limit);
17
+ addSaveLimit(server, limit);
18
+ };
@@ -0,0 +1,82 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @help Adds SEARCHRES [RFC5182] capability, loads ESEARCH as well
5
+ * @help SEARCH RETURN (SAVE) stores the result, "$" refers to it in place of a sequence set
6
+ */
7
+
8
+ const esearch = require('./esearch');
9
+ const { selectReturned } = require('../esearch');
10
+
11
+ // Commands after which the search result variable is empty (RFC 5182 section 2.1). After CLOSE and
12
+ // UNSELECT no mailbox is selected, so "$" can not be used until the next SELECT or EXAMINE anyway
13
+ const RESET_COMMANDS = new Set(['SELECT', 'EXAMINE', 'CLOSE', 'UNSELECT']);
14
+
15
+ // SAVE also applies to commands based on SEARCH, like SORT (RFC 5182 section 1) with ESORT
16
+ // and to the ESEARCH command of MULTISEARCH, which leaves the messages to save in `parsed.searchresFound`
17
+ const isSearch = command => ['SEARCH', 'UID SEARCH', 'SORT', 'UID SORT', 'ESEARCH'].includes(command);
18
+
19
+ module.exports = function (server) {
20
+ // a SEARCHRES server MUST also implement ESEARCH (RFC 5182 section 2.1)
21
+ esearch(server);
22
+
23
+ server.registerCapability('SEARCHRES');
24
+ // SAVE adds no data to the ESEARCH response
25
+ server.searchReturnOptions.set('SAVE', { data: false });
26
+
27
+ server.connectionHandlers.push(connection => {
28
+ // RFC 5182 section 3: sequence-set =/ seq-last-command, seq-last-command = "$". Dovecot only accepts
29
+ // "$" as the whole sequence set, not combined with numbers like "1,$", and neither does ImapKit
30
+ const getMessageRange = connection.getMessageRange;
31
+ connection.getMessageRange = function (range, isUid) {
32
+ if (range !== '$') {
33
+ return getMessageRange.call(this, range, isUid);
34
+ }
35
+ // The search result variable holds message objects, so expunged messages drop out of it without
36
+ // renumbering, and it is the same set for UID and sequence number commands (RFC 5182 section 2.1).
37
+ // As "$" holds no sequence numbers, core lets it be pipelined (RFC 5182 section 2.3)
38
+ const saved = this.searchResult || new Set();
39
+ const result = [];
40
+ this.getSessionMessages().forEach((message, i) => {
41
+ if (saved.has(message)) {
42
+ result.push([i + 1, message]);
43
+ }
44
+ });
45
+ return result;
46
+ };
47
+ });
48
+
49
+ // RFC 8437 section 4.1: after UNAUTHENTICATE "$" represents the empty set
50
+ server.resetHandlers.push(connection => {
51
+ connection.searchResult = null;
52
+ });
53
+
54
+ server.outputHandlers.push((connection, response, description, parsed, data, extra) => {
55
+ if (!parsed || !response || (response.tag !== parsed.tag && response.command !== 'SEARCH' && response.command !== 'SORT')) {
56
+ return;
57
+ }
58
+ const command = (parsed.command || '').toUpperCase();
59
+ const save = isSearch(command) && parsed.searchReturn && parsed.searchReturn.has('SAVE');
60
+
61
+ // the result of SEARCH, with MIN and/or MAX only these messages are saved (RFC 5182 section 2.4).
62
+ // It is stored once the tagged response tells if the command succeeded
63
+ if (save && response.tag === '*' && ['SEARCH', 'SORT'].includes(response.command) && description === command && extra && Array.isArray(extra.list)) {
64
+ parsed.searchresFound = selectReturned(extra.sorted || extra.list, parsed.searchReturn);
65
+ return;
66
+ }
67
+
68
+ if (response.tag !== parsed.tag) {
69
+ return;
70
+ }
71
+
72
+ if (RESET_COMMANDS.has(command)) {
73
+ connection.searchResult = null;
74
+ return;
75
+ }
76
+
77
+ // RFC 5182 section 2.1: an OK sets the variable, a NO empties it and a BAD leaves it as it was
78
+ if (save && response.command !== 'BAD') {
79
+ connection.searchResult = new Set(response.command === 'OK' ? parsed.searchresFound : []);
80
+ }
81
+ });
82
+ };
@@ -0,0 +1,23 @@
1
+ 'use strict';
2
+
3
+ const { collationKey, displayAddress } = require('../sorting');
4
+ const sort = require('./sort');
5
+
6
+ /**
7
+ * @help Adds SORT=DISPLAY [RFC5957] capability, also loads SORT
8
+ *
9
+ * SORT=DISPLAY: https://tools.ietf.org/html/rfc5957
10
+ *
11
+ * Additional sort keys for SORT and UID SORT:
12
+ * - DISPLAYFROM
13
+ * - DISPLAYTO
14
+ */
15
+ module.exports = function (server) {
16
+ // RFC 5957 section 1: SORT=DISPLAY means the full SORT extension plus both sort keys
17
+ sort(server);
18
+ server.registerCapability('SORT=DISPLAY');
19
+
20
+ // RFC 5957 section 4: the DISPLAY sort value of the first address of env-from and env-to
21
+ server.sortKeys.DISPLAYFROM = message => collationKey(displayAddress(message, 'from'));
22
+ server.sortKeys.DISPLAYTO = message => collationKey(displayAddress(message, 'to'));
23
+ };