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,132 @@
1
+ 'use strict';
2
+
3
+ const { states } = require('../command-states');
4
+ const { getMessageData } = require('../mimeparser');
5
+ const { collationKey, baseSubject, arrivalTime, sentTime, addressMailbox, searchMessages } = require('../sorting');
6
+
7
+ /**
8
+ * @help Adds SORT [RFC5256] capability
9
+ *
10
+ * SORT: https://tools.ietf.org/html/rfc5256
11
+ *
12
+ * Additional commands:
13
+ * - SORT
14
+ * - UID SORT
15
+ */
16
+ module.exports = function (server) {
17
+ if (server.sortKeys) {
18
+ // already loaded, e.g. by the SORT=DISPLAY plugin
19
+ return;
20
+ }
21
+
22
+ server.registerCapability('SORT');
23
+
24
+ // RFC 5256 section 3: sort keys and the value of a message for each. A value is a number or the
25
+ // collation key of a string, see sorting.collationKey. SORT=DISPLAY adds its keys here
26
+ server.sortKeys = {
27
+ ARRIVAL: message => arrivalTime(message),
28
+ CC: message => collationKey(addressMailbox(message, 'cc')),
29
+ DATE: message => sentTime(message),
30
+ FROM: message => collationKey(addressMailbox(message, 'from')),
31
+ SIZE: message => getMessageData(message).raw.length,
32
+ SUBJECT: message => collationKey(baseSubject(getMessageData(message).tree.parsedHeader.subject).subject),
33
+ TO: message => collationKey(addressMailbox(message, 'to'))
34
+ };
35
+
36
+ const compareValues = (a, b) => (Buffer.isBuffer(a) ? Buffer.compare(a, b) : a - b);
37
+
38
+ /**
39
+ * Returns a comparator of messages for parsed sort criteria: ascending order, REVERSE turns one
40
+ * criterion around, and mailbox order (the sequence number) breaks ties (RFC 5256 section 3).
41
+ * Every value is computed once per message. Plugins (ESORT, CONTEXT=SORT) use it to sort again later
42
+ *
43
+ * @param {Array} criteria List of `{ key, reverse }`
44
+ * @return {Function} comparator for two messages of the same mailbox
45
+ */
46
+ server.sortComparator = criteria => {
47
+ const cache = new WeakMap();
48
+ const getValues = message => {
49
+ if (!cache.has(message)) {
50
+ cache.set(
51
+ message,
52
+ criteria.map(criterion => server.sortKeys[criterion.key](message))
53
+ );
54
+ }
55
+ return cache.get(message);
56
+ };
57
+ return (a, b) => {
58
+ const aValues = getValues(a);
59
+ const bValues = getValues(b);
60
+ for (let i = 0; i < criteria.length; i++) {
61
+ const diff = compareValues(aValues[i], bValues[i]);
62
+ if (diff) {
63
+ return criteria[i].reverse ? -diff : diff;
64
+ }
65
+ }
66
+ // messages are kept in UID order, so this is the order of sequence numbers
67
+ return a.uid - b.uid;
68
+ };
69
+ };
70
+
71
+ const sortHandler = (isUid, connection, parsed, data, callback) => {
72
+ const command = isUid ? 'UID SORT' : 'SORT';
73
+ const attributes = parsed.attributes || [];
74
+
75
+ const fail = text => {
76
+ connection.sendStatus(parsed, data, 'BAD', text, false, command + ' FAILED');
77
+ return callback();
78
+ };
79
+
80
+ // RFC 5256 section 5: sort-criteria = "(" sort-criterion *(SP sort-criterion) ")",
81
+ // sort-criterion = ["REVERSE" SP] sort-key
82
+ if (!Array.isArray(attributes[0]) || !attributes[0].length) {
83
+ return fail(command + ' expects a list of sort criteria');
84
+ }
85
+ const criteria = [];
86
+ let reverse = false;
87
+ for (const item of attributes[0]) {
88
+ const key = item && item.type === 'ATOM' ? item.value.toUpperCase() : '';
89
+ if (key === 'REVERSE' && !reverse) {
90
+ reverse = true;
91
+ continue;
92
+ }
93
+ if (!Object.hasOwn(server.sortKeys, key)) {
94
+ return fail('Invalid sort criterion ' + (item && typeof item.value === 'string' ? item.value : ''));
95
+ }
96
+ criteria.push({ key, reverse });
97
+ reverse = false;
98
+ }
99
+ if (reverse) {
100
+ return fail('REVERSE must be followed by a sort key');
101
+ }
102
+
103
+ const result = searchMessages(connection, parsed, data, attributes.slice(1));
104
+ if (!result) {
105
+ return callback();
106
+ }
107
+
108
+ const compare = server.sortComparator(criteria);
109
+ const sorted = result.list.slice().sort(compare);
110
+
111
+ connection.send(
112
+ {
113
+ tag: '*',
114
+ command: 'SORT',
115
+ attributes: sorted.map(message => (isUid ? message.uid : result.numbers[message.uid]))
116
+ },
117
+ command,
118
+ parsed,
119
+ data,
120
+ // the search result, for CONDSTORE to append the highest mod-sequence (RFC 7162 section 3.1.9), with the
121
+ // sorted messages and the comparator for ESORT and CONTEXT=SORT
122
+ Object.assign(result, { sorted, compare })
123
+ );
124
+ connection.sendStatus(parsed, data, 'OK', command + ' completed', false, command);
125
+ return callback();
126
+ };
127
+
128
+ // RFC 5256 section 3: EXPUNGE responses are not permitted while responding to SORT, but are during UID SORT.
129
+ // The search criteria start after the sort criteria and the charset
130
+ server.setCommandHandler('SORT', sortHandler.bind(null, false), { states: states.SELECTED, searchCriteria: 2, noExpunge: true });
131
+ server.setCommandHandler('UID SORT', sortHandler.bind(null, true), { states: states.SELECTED, searchCriteria: 2 });
132
+ };
@@ -0,0 +1,95 @@
1
+ 'use strict';
2
+
3
+ const { getListExtensions } = require('../list-extensions');
4
+
5
+ /**
6
+ * @help Enables SPECIAL-USE [RFC6154] capability
7
+ * @help Mailboxes need to have a "special-use"
8
+ * @help property (String or Array) that will be used
9
+ * @help as extra flag for LIST and LSUB responses.
10
+ * @help With LIST-EXTENDED the SPECIAL-USE selection and
11
+ * @help return options work with the other LIST options
12
+ */
13
+
14
+ module.exports = function (server) {
15
+ // Register capability
16
+ server.registerCapability('SPECIAL-USE');
17
+
18
+ const getSpecialUse = folder => [].concat((folder && folder['special-use']) || []);
19
+
20
+ // RFC 6154 section 2: with LIST-EXTENDED, SPECIAL-USE is a list-select-independent-opt that
21
+ // implies the SPECIAL-USE return option. The LIST-EXTENDED plugin may be loaded before or after this one
22
+ const listExtensions = getListExtensions(server);
23
+ listExtensions.selectionOptions['SPECIAL-USE'] = {
24
+ type: 'independent',
25
+ returnOption: 'SPECIAL-USE',
26
+ match: folder => folder.flags.indexOf('\\Noselect') < 0 && getSpecialUse(folder).length > 0
27
+ };
28
+ // special-use attributes are always included, so the return option needs no extra handling
29
+ listExtensions.returnOptions['SPECIAL-USE'] = {};
30
+
31
+ const listHandler = server.getCommandHandler('LIST');
32
+
33
+ server.setCommandHandler('LIST', (connection, parsed, data, callback) => {
34
+ if (listExtensions.enabled) {
35
+ // LIST-EXTENDED handles the options
36
+ return listHandler(connection, parsed, data, callback);
37
+ }
38
+
39
+ // without LIST-EXTENDED only the SPECIAL-USE options are understood
40
+ let i;
41
+ if (parsed.attributes && Array.isArray(parsed.attributes[0])) {
42
+ for (i = parsed.attributes[0].length - 1; i >= 0; i--) {
43
+ if (parsed.attributes[0][i] && parsed.attributes[0][i].type === 'ATOM' && parsed.attributes[0][i].value.toUpperCase() === 'SPECIAL-USE') {
44
+ parsed.attributes[0].splice(i, 1);
45
+ parsed.listSpecialUseOnly = true;
46
+ }
47
+ }
48
+ // remove parameter if no other memebers were left
49
+ if (!parsed.attributes[0].length) {
50
+ parsed.attributes.splice(0, 1);
51
+ }
52
+ }
53
+
54
+ if (
55
+ parsed.attributes &&
56
+ parsed.attributes[2] &&
57
+ parsed.attributes[2].type === 'ATOM' &&
58
+ parsed.attributes[2].value.toUpperCase() === 'RETURN' &&
59
+ Array.isArray(parsed.attributes[3])
60
+ ) {
61
+ for (i = parsed.attributes[3].length - 1; i >= 0; i--) {
62
+ if (parsed.attributes[3][i] && parsed.attributes[3][i].type === 'ATOM' && parsed.attributes[3][i].value.toUpperCase() === 'SPECIAL-USE') {
63
+ // special-use attributes are always included, so the return option needs no extra handling
64
+ parsed.attributes[3].splice(i, 1);
65
+ }
66
+ }
67
+
68
+ // Remove RETURN (List) if no members were left
69
+ if (!parsed.attributes[3].length) {
70
+ parsed.attributes.splice(2, 2);
71
+ }
72
+ }
73
+
74
+ listHandler(connection, parsed, data, callback);
75
+ });
76
+
77
+ server.outputHandlers.push((connection, response, description, parsed, data, folder) => {
78
+ const specialUseList = getSpecialUse(folder).map(specialUse => {
79
+ return {
80
+ type: 'ATOM',
81
+ value: specialUse
82
+ };
83
+ });
84
+
85
+ if ((description === 'LIST ITEM' || description === 'LSUB ITEM') && folder && response.attributes && Array.isArray(response.attributes[0])) {
86
+ if (specialUseList.length) {
87
+ // special-use attributes are added to the other mailbox attributes, also with RETURN (SPECIAL-USE)
88
+ response.attributes[0] = response.attributes[0].concat(specialUseList);
89
+ } else if (parsed.listSpecialUseOnly) {
90
+ // Do not show this response
91
+ response.skipResponse = true;
92
+ }
93
+ }
94
+ });
95
+ };
@@ -0,0 +1,57 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @help Adds STARTTLS command
5
+ */
6
+
7
+ module.exports = function (server) {
8
+ // Register capability, usable with unsecure connection
9
+ server.registerCapability('STARTTLS', connection => {
10
+ return !connection.secureConnection;
11
+ });
12
+
13
+ // Add STARTTLS command
14
+ server.setCommandHandler('STARTTLS', (connection, parsed, data, callback) => {
15
+ // only works in insecure setting
16
+ if (connection.secureConnection) {
17
+ return sendError('Connection is already secured', connection, parsed, data, callback);
18
+ }
19
+
20
+ connection.send(
21
+ {
22
+ tag: parsed.tag,
23
+ command: 'OK',
24
+ attributes: [
25
+ {
26
+ type: 'TEXT',
27
+ value: 'Server ready to start TLS negotiation'
28
+ }
29
+ ]
30
+ },
31
+ 'STARTTLS INIT',
32
+ parsed,
33
+ data
34
+ );
35
+
36
+ connection.upgradeConnection(callback);
37
+ });
38
+ };
39
+
40
+ function sendError(message, connection, parsed, data, callback) {
41
+ connection.send(
42
+ {
43
+ tag: parsed.tag,
44
+ command: 'BAD',
45
+ attributes: [
46
+ {
47
+ type: 'TEXT',
48
+ value: message
49
+ }
50
+ ]
51
+ },
52
+ 'INVALID COMMAND',
53
+ parsed,
54
+ data
55
+ );
56
+ return callback();
57
+ }
@@ -0,0 +1,19 @@
1
+ 'use strict';
2
+
3
+ const fetchHandlers = require('../commands/handlers/fetch');
4
+
5
+ /**
6
+ * @help Adds STATUS=SIZE [RFC8438] capability. The SIZE status item
7
+ * @help is the sum of the RFC822.SIZE values of the messages, it can
8
+ * @help also be used with LIST-STATUS
9
+ */
10
+
11
+ module.exports = function (server) {
12
+ server.registerCapability('STATUS=SIZE');
13
+
14
+ server.allowedStatus.push('SIZE');
15
+
16
+ // RFC 8438 section 3: at least the sum of the RFC822.SIZE values of all messages in the mailbox
17
+ server.statusHandlers.SIZE = (connection, mailbox) =>
18
+ mailbox.messages.reduce((size, message) => size + fetchHandlers['RFC822.SIZE'](connection, message), 0);
19
+ };
@@ -0,0 +1,16 @@
1
+ 'use strict';
2
+
3
+ const { addThreadAlgorithm } = require('../threading');
4
+
5
+ /**
6
+ * @help Adds THREAD=ORDEREDSUBJECT [RFC5256] capability
7
+ *
8
+ * THREAD: https://tools.ietf.org/html/rfc5256
9
+ *
10
+ * Additional commands:
11
+ * - THREAD ORDEREDSUBJECT
12
+ * - UID THREAD ORDEREDSUBJECT
13
+ */
14
+ module.exports = function (server) {
15
+ addThreadAlgorithm(server, 'ORDEREDSUBJECT');
16
+ };
@@ -0,0 +1,16 @@
1
+ 'use strict';
2
+
3
+ const { addThreadAlgorithm } = require('../threading');
4
+
5
+ /**
6
+ * @help Adds THREAD=REFERENCES [RFC5256] capability
7
+ *
8
+ * THREAD: https://tools.ietf.org/html/rfc5256
9
+ *
10
+ * Additional commands:
11
+ * - THREAD REFERENCES
12
+ * - UID THREAD REFERENCES
13
+ */
14
+ module.exports = function (server) {
15
+ addThreadAlgorithm(server, 'REFERENCES');
16
+ };
@@ -0,0 +1,135 @@
1
+ 'use strict';
2
+
3
+ const { toVanished } = require('../vanished');
4
+ const { isAtom } = require('../arguments');
5
+ const { registerEnable, isEnabled } = require('./enable');
6
+
7
+ /**
8
+ * @help Adds UIDONLY [RFC9586] capability, loads ENABLE as well
9
+ * @help After ENABLE UIDONLY: FETCH, STORE, SEARCH, COPY, MOVE, SORT, THREAD, REPLACE and
10
+ * @help message numbers in UID SEARCH (SORT, THREAD) criteria are refused with BAD [UIDREQUIRED],
11
+ * @help UIDFETCH responses replace FETCH responses and VANISHED replaces EXPUNGE.
12
+ * @help Load UIDPLUS for COPYUID and UID EXPUNGE
13
+ *
14
+ * UIDONLY: https://www.rfc-editor.org/rfc/rfc9586
15
+ */
16
+
17
+ module.exports = function (server) {
18
+ server.registerCapability('UIDONLY');
19
+
20
+ registerEnable(server, 'UIDONLY');
21
+
22
+ // UNAUTHENTICATE clears the ENABLEd extensions (RFC 8437 section 4.1)
23
+ const isUidonly = connection => isEnabled(connection, 'UIDONLY');
24
+
25
+ const refuse = text => ({ command: 'BAD', code: 'UIDREQUIRED', text });
26
+
27
+ // RFC 9586 section 3: once UIDONLY is enabled, message numbers in any argument MUST be answered with a
28
+ // tagged BAD that has the UIDREQUIRED response code
29
+ const getRefusal = (connection, parsed) => {
30
+ const command = String(parsed.command || '').toUpperCase();
31
+ const options = server.getCommandOptions(command);
32
+ // the ESEARCH command of MULTISEARCH always returns UIDs (RFC 7377 section 2), like a UID command
33
+ const isUid = /^UID /.test(command) || command === 'ESEARCH';
34
+
35
+ // sections 3.2 and 3.8: FETCH, STORE, SEARCH, COPY, MOVE, SORT and THREAD are prohibited, also REPLACE
36
+ // (RFC 8508), its message argument is a sequence number. These have the sequenceSet or searchCriteria
37
+ // command option
38
+ if (!isUid && (options.sequenceSet !== false || options.searchCriteria !== false)) {
39
+ return refuse(command + ' is not allowed once UIDONLY is enabled, use UID ' + command);
40
+ }
41
+
42
+ // section 3.5: the <sequence set> search key is prohibited, also in UID SORT and UID THREAD
43
+ if (isUid && options.searchCriteria !== false && connection.usesSequenceNumbers(parsed)) {
44
+ return refuse('Message numbers are not allowed in the search criteria once UIDONLY is enabled, use UID <sequence set>');
45
+ }
46
+
47
+ // section 3.7: the fourth QRESYNC parameter (message sequence match data) MUST NOT be used
48
+ if ((command === 'SELECT' || command === 'EXAMINE') && Array.isArray(parsed.attributes && parsed.attributes[1])) {
49
+ const params = parsed.attributes[1];
50
+ const position = params.findIndex(param => isAtom(param, 'QRESYNC'));
51
+ const value = position >= 0 && params[position + 1];
52
+ if (Array.isArray(value) && value.slice(2).some(Array.isArray)) {
53
+ return refuse('QRESYNC message sequence match data is not allowed once UIDONLY is enabled');
54
+ }
55
+ }
56
+
57
+ return false;
58
+ };
59
+
60
+ server.commandChecks.push((connection, parsed) => isUidonly(connection) && getRefusal(connection, parsed));
61
+
62
+ // a prohibited command is refused before its literal is sent, before other checks that would answer NO
63
+ server.literalFilters.unshift((connection, command) => isUidonly(connection) && getRefusal(connection, { command }));
64
+
65
+ server.connectionHandlers.push(connection => {
66
+ // RFC 3501 section 5.5 is about commands with message numbers, these are all refused with
67
+ // BAD [UIDREQUIRED] once UIDONLY is enabled, as if they had waited
68
+ const isAmbiguous = connection.isAmbiguous;
69
+ connection.isAmbiguous = function (parsed) {
70
+ return !isUidonly(this) && isAmbiguous.call(this, parsed);
71
+ };
72
+
73
+ // RFC 9586 sections 3.4 and 3.6: expunges are announced with VANISHED instead of EXPUNGE, EXISTS and
74
+ // RECENT stay as they are
75
+ const prepareNotifications = connection.prepareNotifications;
76
+ connection.prepareNotifications = function (queue) {
77
+ queue = prepareNotifications.call(this, queue);
78
+ return isUidonly(this) ? toVanished(queue) : queue;
79
+ };
80
+ });
81
+
82
+ // RFC 9586 section 3: the server MUST NOT return message sequence numbers in any response
83
+ const outputHandler = (connection, response, description, parsed, data, extra) => {
84
+ if (!response || response.tag !== '*' || !Array.isArray(response.attributes) || !isUidonly(connection)) {
85
+ return;
86
+ }
87
+ const attributes = response.attributes;
88
+
89
+ // the UNSEEN response code of SELECT and EXAMINE is a message number (RFC 3501 section 7.1)
90
+ if (response.command === 'OK' && attributes[0] && attributes[0].type === 'SECTION' && isAtom(attributes[0].section[0], 'UNSEEN')) {
91
+ response.skipResponse = true;
92
+ return;
93
+ }
94
+
95
+ if (!isAtom(attributes[1], 'FETCH') || !Array.isArray(attributes[2])) {
96
+ return;
97
+ }
98
+
99
+ // section 3.3: uidfetch-resp = uniqueid SP "UIDFETCH" SP msg-att
100
+ // FETCH responses pass their message as `extra`, notifications like the MessageNew FETCH of NOTIFY carry it
101
+ const list = attributes[2];
102
+ const uidPos = list.findIndex((item, i) => !(i % 2) && isAtom(item, 'UID'));
103
+ const message = (extra && typeof extra.uid === 'number' && extra) || response.fetchedMessage;
104
+ const uid = message ? message.uid : uidPos >= 0 && list[uidPos + 1];
105
+ if (!uid) {
106
+ // never send a sequence number instead
107
+ response.skipResponse = true;
108
+ return;
109
+ }
110
+
111
+ // the UID data item is only included if the client asked for it, like in the examples of section 3.3
112
+ if (parsed && !('uidonlyKeepUid' in parsed)) {
113
+ parsed.uidonlyKeepUid = requestsUid(parsed);
114
+ }
115
+ let items = list;
116
+ if (uidPos >= 0 && list.length > 2 && !(parsed && parsed.uidonlyKeepUid)) {
117
+ items = list.slice(0, uidPos).concat(list.slice(uidPos + 2));
118
+ }
119
+ response.attributes = [uid, { type: 'ATOM', value: 'UIDFETCH' }, items];
120
+ };
121
+
122
+ // registered once every plugin is loaded, so that the other output handlers see the FETCH responses they
123
+ // know before these are turned into UIDFETCH responses
124
+ server.once('pluginsLoaded', () => server.outputHandlers.push(outputHandler));
125
+ };
126
+
127
+ // Checks if a UID FETCH command lists the UID data item
128
+ function requestsUid(parsed) {
129
+ if (String(parsed.command || '').toUpperCase() !== 'UID FETCH' || !parsed.attributes) {
130
+ return false;
131
+ }
132
+ return [].concat(parsed.attributes[1] || []).some(item => isAtom(item, 'UID'));
133
+ }
134
+
135
+ module.exports.requires = ['ENABLE'];
@@ -0,0 +1,124 @@
1
+ 'use strict';
2
+
3
+ const { toSequenceSet } = require('../esearch');
4
+
5
+ /**
6
+ * @help Adds UIDPLUS [RFC4315] capability
7
+ *
8
+ * UIDPLUS: http://tools.ietf.org/html/rfc4315
9
+ *
10
+ * Additional commands:
11
+ * - UID EXPUNGE
12
+ *
13
+ * Additional response codes:
14
+ * - APPENDUID (with a UID set for MULTIAPPEND, and in an untagged OK for REPLACE)
15
+ * - COPYUID
16
+ * - Not implemented: UIDNOTSTICKY
17
+ */
18
+ module.exports = function (server) {
19
+ server.registerCapability('UIDPLUS');
20
+
21
+ server.setCommandHandler('UID EXPUNGE', (connection, parsed, data, callback) => {
22
+ if (!parsed.attributes || parsed.attributes.length !== 1 || !parsed.attributes[0] || ['ATOM', 'SEQUENCE'].indexOf(parsed.attributes[0].type) < 0) {
23
+ connection.send(
24
+ {
25
+ tag: parsed.tag,
26
+ command: 'BAD',
27
+ attributes: [
28
+ {
29
+ type: 'TEXT',
30
+ value: 'UID EXPUNGE expects uid sequence set'
31
+ }
32
+ ]
33
+ },
34
+ 'INVALID COMMAND',
35
+ parsed,
36
+ data
37
+ );
38
+ return callback();
39
+ }
40
+
41
+ if (connection.refuseReadOnly(parsed, data, 'UID EXPUNGE FAILED')) {
42
+ return callback();
43
+ }
44
+
45
+ const sequence = parsed.attributes[0].value;
46
+ const range = connection.limitRange(parsed, connection.getMessageRange(sequence, true));
47
+ // Only messages with the \Deleted flag are removed (RFC 4315 section 2.1)
48
+ const rangeMessages = range.map(x => x[1]).filter(message => message.flags.indexOf('\\Deleted') >= 0);
49
+
50
+ connection.expungeSpecificMessages(connection.selectedMailbox, rangeMessages, false, true, !!parsed.highestFirst);
51
+
52
+ connection.send(
53
+ {
54
+ tag: parsed.tag,
55
+ command: 'OK',
56
+ attributes: [
57
+ {
58
+ type: 'TEXT',
59
+ value: 'UID EXPUNGE completed'
60
+ }
61
+ ]
62
+ },
63
+ 'UID EXPUNGE',
64
+ parsed,
65
+ data
66
+ );
67
+ callback();
68
+ });
69
+
70
+ server.outputHandlers.push((connection, response, description, parsed, data, extra) => {
71
+ if (description === 'APPEND' || description === 'REPLACE APPENDUID') {
72
+ // OK [APPENDUID <target-mailbox-uidvalidity> <uid>] APPEND Completed. With MULTIAPPEND the
73
+ // second value is a UID set, but never for a single message (RFC 4315 section 3)
74
+ const uids = extra.messages.map(message => message.uid);
75
+ response.attributes = [
76
+ {
77
+ type: 'SECTION',
78
+ section: [
79
+ {
80
+ type: 'ATOM',
81
+ value: 'APPENDUID'
82
+ },
83
+ extra.mailbox.uidvalidity,
84
+ uids.length > 1 ? { type: 'SEQUENCE', value: toSequenceSet(uids) } : uids[0]
85
+ ]
86
+ }
87
+ ].concat(response.attributes);
88
+ // REPLACE sends APPENDUID in an untagged OK before the EXPUNGE (RFC 8508 section 4.3)
89
+ response.skipResponse = false;
90
+ return;
91
+ }
92
+
93
+ if (description === 'COPY' || description === 'UID COPY' || description === 'MOVE COPYUID' || description === 'UID MOVE COPYUID') {
94
+ // Nothing was copied, an empty COPYUID would break the response grammar
95
+ if (!extra || !extra.sourceUids || !extra.sourceUids.length) {
96
+ return;
97
+ }
98
+ response.attributes = [
99
+ {
100
+ type: 'SECTION',
101
+ section: [
102
+ {
103
+ type: 'ATOM',
104
+ value: 'COPYUID'
105
+ },
106
+ extra.mailbox.uidvalidity,
107
+ // The range was interpreted in ascending order so these
108
+ // values are already in the right order.
109
+ {
110
+ type: 'SEQUENCE',
111
+ value: extra.sourceUids.join(',')
112
+ },
113
+ {
114
+ type: 'SEQUENCE',
115
+ value: extra.targetUids.join(',')
116
+ }
117
+ ]
118
+ }
119
+ ].concat(response.attributes);
120
+ response.skipResponse = false;
121
+ return;
122
+ }
123
+ });
124
+ };
@@ -0,0 +1,28 @@
1
+ 'use strict';
2
+
3
+ const { states } = require('../command-states');
4
+
5
+ /**
6
+ * @help Adds UNAUTHENTICATE [RFC8437] capability
7
+ * @help Returns to the Not Authenticated state, everything
8
+ * @help but TLS is reset (selected mailbox, ENABLE, COMPRESS)
9
+ */
10
+
11
+ module.exports = function (server) {
12
+ server.registerCapability('UNAUTHENTICATE');
13
+
14
+ server.setCommandHandler(
15
+ 'UNAUTHENTICATE',
16
+ (connection, parsed, data, callback) => {
17
+ // RFC 8437 section 3: a NO response is not permitted, the reset can not fail here
18
+ connection.sendStatus(parsed, data, 'OK', 'Completed, now in not authenticated state');
19
+
20
+ // RFC 8437 sections 3 and 4.1: the mailbox is closed without EXPUNGE responses, ENABLEd
21
+ // extensions and CONDSTORE are turned off, and compression ends after the CRLF of the OK
22
+ connection.resetSession();
23
+
24
+ return callback();
25
+ },
26
+ { states: states.AUTHENTICATED, noArguments: true }
27
+ );
28
+ };
@@ -0,0 +1,36 @@
1
+ 'use strict';
2
+
3
+ const { states } = require('../command-states');
4
+
5
+ /**
6
+ * @help Adds UNSELECT [RFC3691] capability
7
+ */
8
+
9
+ module.exports = function (server) {
10
+ server.registerCapability('UNSELECT');
11
+
12
+ server.setCommandHandler(
13
+ 'UNSELECT',
14
+ (connection, parsed, data, callback) => {
15
+ connection.send(
16
+ {
17
+ tag: parsed.tag,
18
+ command: 'OK',
19
+ attributes: [
20
+ {
21
+ type: 'TEXT',
22
+ value: 'Mailbox closed'
23
+ }
24
+ ]
25
+ },
26
+ 'CLOSE',
27
+ parsed,
28
+ data
29
+ );
30
+
31
+ connection.closeMailbox();
32
+ return callback();
33
+ },
34
+ { states: states.SELECTED, noArguments: true }
35
+ );
36
+ };