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,188 @@
1
+ 'use strict';
2
+
3
+ // https://developers.google.com/gmail/xoauth2_protocol
4
+
5
+ /**
6
+ * @help Enables XOAUTH2 capability
7
+ * @help Implementation difference - ImapKit requires
8
+ * @help SASL-IR enabled and used while Gmail does not
9
+ * @help Valid login info:
10
+ * @help Username: testuser
11
+ * @help Access Token: testtoken
12
+ */
13
+
14
+ const utf8 = new TextDecoder('utf-8', { fatal: true });
15
+
16
+ module.exports = function (server) {
17
+ // Register capability, usable for non authenticated users
18
+ server.registerCapability('AUTH=XOAUTH2', connection => {
19
+ return connection.state === 'Not Authenticated';
20
+ });
21
+
22
+ server.setCommandHandler('AUTHENTICATE XOAUTH2', (connection, parsed, data, callback) => {
23
+ if (!server.capabilities['SASL-IR'] || !server.capabilities['SASL-IR'](connection)) {
24
+ connection.send(
25
+ {
26
+ tag: parsed.tag,
27
+ command: 'BAD',
28
+ attributes: [
29
+ {
30
+ type: 'TEXT',
31
+ value: 'SASL-IR must be enabled to support XOAUTH2'
32
+ }
33
+ ]
34
+ },
35
+ 'AUTHENTICATE XOAUTH2 FAILED',
36
+ parsed,
37
+ data
38
+ );
39
+ return callback();
40
+ }
41
+
42
+ if (!parsed.attributes || parsed.attributes.length !== 1 || !parsed.attributes[0] || ['STRING', 'ATOM'].indexOf(parsed.attributes[0].type) < 0) {
43
+ connection.send(
44
+ {
45
+ tag: parsed.tag,
46
+ command: 'NO',
47
+ attributes: [
48
+ {
49
+ type: 'TEXT',
50
+ value: 'Invalid SASL argument'
51
+ }
52
+ ]
53
+ },
54
+ 'AUTHENTICATE XOAUTH2 FAILED',
55
+ parsed,
56
+ data
57
+ );
58
+ return callback();
59
+ }
60
+
61
+ const decoded = connection.decodeSaslResponse(parsed.attributes[0].value);
62
+ if (!decoded) {
63
+ connection.sendStatus(parsed, data, 'BAD', 'Invalid base64 in SASL response', false, 'AUTHENTICATE XOAUTH2 FAILED');
64
+ return callback();
65
+ }
66
+ let parts = [];
67
+ try {
68
+ // user names are unicode strings, like the keys of `users`
69
+ parts = utf8.decode(decoded).split('\x01');
70
+ } catch {
71
+ // invalid UTF-8 is a malformed payload like any other
72
+ }
73
+ const user = (parts[0] || '').substr(5);
74
+ const accessToken = (parts[1] || '').substr(12);
75
+
76
+ if (
77
+ parts.length !== 4 ||
78
+ !parts[0].match(/^user=/) ||
79
+ !parts[1].match(/^auth=Bearer /) ||
80
+ !user || // Must be present
81
+ !accessToken || // Must be present
82
+ parts[2] || // Must be empty
83
+ parts[3] // Must be empty
84
+ ) {
85
+ connection.send(
86
+ {
87
+ tag: parsed.tag,
88
+ command: 'NO',
89
+ attributes: [
90
+ {
91
+ type: 'TEXT',
92
+ value: 'Invalid SASL argument.'
93
+ }
94
+ ]
95
+ },
96
+ 'AUTHENTICATE XOAUTH2 FAILED',
97
+ parsed,
98
+ data
99
+ );
100
+ return callback();
101
+ }
102
+
103
+ const userData = connection.server.getUser(user);
104
+ if (!userData) {
105
+ connection.send(
106
+ {
107
+ tag: parsed.tag,
108
+ command: 'NO',
109
+ attributes: [
110
+ // RFC 5530 section 3: unknown user or bad token
111
+ { type: 'SECTION', section: [{ type: 'ATOM', value: 'AUTHENTICATIONFAILED' }] },
112
+ {
113
+ type: 'TEXT',
114
+ value: 'Invalid credentials'
115
+ }
116
+ ]
117
+ },
118
+ 'AUTHENTICATE XOAUTH2 FAILED',
119
+ parsed,
120
+ data
121
+ );
122
+ return callback();
123
+ }
124
+
125
+ if (!userData.xoauth2 || userData.xoauth2.accessToken !== accessToken) {
126
+ connection.send(
127
+ {
128
+ tag: '+',
129
+ attributes: [
130
+ {
131
+ type: 'ATOM',
132
+ value: Buffer.from(
133
+ JSON.stringify({
134
+ status: '400',
135
+ schemes: 'Bearer',
136
+ scope: 'https://mail.google.com/'
137
+ })
138
+ ).toString('base64')
139
+ }
140
+ ]
141
+ },
142
+ 'AUTHENTICATE XOAUTH2 FAILED',
143
+ parsed,
144
+ data
145
+ );
146
+
147
+ // wait for response
148
+ connection.inputHandler = function () {
149
+ connection.inputHandler = false;
150
+ connection.send(
151
+ {
152
+ tag: parsed.tag,
153
+ command: 'NO',
154
+ attributes: [
155
+ { type: 'SECTION', section: [{ type: 'ATOM', value: 'AUTHENTICATIONFAILED' }] },
156
+ {
157
+ type: 'TEXT',
158
+ value: 'SASL authentication failed'
159
+ }
160
+ ]
161
+ },
162
+ 'AUTHENTICATE XOAUTH2 FAILED',
163
+ parsed,
164
+ data
165
+ );
166
+ };
167
+ } else {
168
+ connection.state = 'Authenticated';
169
+ connection.username = user;
170
+ connection.send(
171
+ {
172
+ tag: parsed.tag,
173
+ command: 'OK',
174
+ attributes: [
175
+ {
176
+ type: 'TEXT',
177
+ value: 'User logged in'
178
+ }
179
+ ]
180
+ },
181
+ 'AUTHENTICATE XOAUTH2 SUCCESS',
182
+ parsed,
183
+ data
184
+ );
185
+ }
186
+ return callback();
187
+ });
188
+ };
@@ -0,0 +1,282 @@
1
+ 'use strict';
2
+
3
+ const { states } = require('../command-states');
4
+
5
+ /**
6
+ * @help Custom plugin to allow programmatic control of the server
7
+ * @help Available commands:
8
+ * @help XTOYBIRD SERVER dumps server internals
9
+ * @help XTOYBIRD CONNECTION dumps connection internals
10
+ * @help XTOYBIRD STORAGE dumps storage as JSON
11
+ * @help XTOYBIRD USERADD "username" "password" adds or updates user
12
+ * @help XTOYBIRD USERDEL "username" removes a user
13
+ * @help XTOYBIRD SHUTDOWN Closes the server after the last client
14
+ * @help disconnects. New connections are rejected.
15
+ * @help Commands are only allowed after login, and only for the owner
16
+ * @help (aclOwner option) when ACL is loaded. A test control plugin,
17
+ * @help any user it allows can read and change everything
18
+ */
19
+
20
+ const util = require('util');
21
+ const { isAstring } = require('../arguments');
22
+
23
+ module.exports = function (server) {
24
+ let username;
25
+ let password;
26
+ let message;
27
+
28
+ const sendError = (connection, parsed, data, callback, command, message) => {
29
+ connection.sendStatus(parsed, data, command, message, false, 'INVALID COMMAND');
30
+ return callback();
31
+ };
32
+
33
+ server.setCommandHandler(
34
+ 'XTOYBIRD',
35
+ (connection, parsed, data, callback) => {
36
+ // XTOYBIRD bypasses every access check (STORAGE dumps all mailboxes), so with ACL only the owner may use it
37
+ if (server.acl && !server.acl.isOwner(connection)) {
38
+ connection.sendStatus(parsed, data, 'NO', 'XTOYBIRD is only allowed for the owner', 'NOPERM', 'INVALID COMMAND');
39
+ return callback();
40
+ }
41
+
42
+ if (parsed.attributes) {
43
+ const subcommand = ((parsed.attributes[0] && parsed.attributes[0].value) || '').toString().toUpperCase();
44
+ if (subcommand === 'USERADD' && (parsed.attributes.length !== 3 || !isAstring(parsed.attributes[1]) || !isAstring(parsed.attributes[2]))) {
45
+ return sendError(connection, parsed, data, callback, 'BAD', 'USERADD expects username and password');
46
+ }
47
+ if (subcommand === 'USERDEL' && (parsed.attributes.length !== 2 || !isAstring(parsed.attributes[1]))) {
48
+ return sendError(connection, parsed, data, callback, 'BAD', 'USERDEL expects username');
49
+ }
50
+
51
+ switch (subcommand) {
52
+ case 'SERVER':
53
+ connection.send(
54
+ {
55
+ tag: '*',
56
+ command: 'XTOYBIRD',
57
+ attributes: [
58
+ {
59
+ type: 'SECTION',
60
+ section: [
61
+ {
62
+ type: 'ATOM',
63
+ value: 'XDUMPVAL'
64
+ }
65
+ ]
66
+ },
67
+ {
68
+ type: 'LITERAL',
69
+ value: util.inspect(server, false, 22)
70
+ }
71
+ ]
72
+ },
73
+ 'CONNECTION STATUS',
74
+ parsed,
75
+ data
76
+ );
77
+ break;
78
+
79
+ case 'CONNECTION':
80
+ connection.send(
81
+ {
82
+ tag: '*',
83
+ command: 'XTOYBIRD',
84
+ attributes: [
85
+ {
86
+ type: 'SECTION',
87
+ section: [
88
+ {
89
+ type: 'ATOM',
90
+ value: 'XDUMPVAL'
91
+ }
92
+ ]
93
+ },
94
+ {
95
+ type: 'LITERAL',
96
+ value: util.inspect(connection, false, 22)
97
+ }
98
+ ]
99
+ },
100
+ 'CONNECTION STATUS',
101
+ parsed,
102
+ data
103
+ );
104
+ break;
105
+
106
+ case 'STORAGE':
107
+ connection.send(
108
+ {
109
+ tag: '*',
110
+ command: 'XTOYBIRD',
111
+ attributes: [
112
+ {
113
+ type: 'SECTION',
114
+ section: [
115
+ {
116
+ type: 'ATOM',
117
+ value: 'XJSONDUMP'
118
+ }
119
+ ]
120
+ },
121
+ {
122
+ type: 'LITERAL',
123
+ value: JSON.stringify(connection.server.storage, false, 4)
124
+ }
125
+ ]
126
+ },
127
+ 'CONNECTION STATUS',
128
+ parsed,
129
+ data
130
+ );
131
+ break;
132
+
133
+ case 'USERADD':
134
+ username = parsed.attributes[1].value || '';
135
+ password = parsed.attributes[2].value || '';
136
+
137
+ if (connection.server.getUser(username)) {
138
+ connection.server.users[username].password = password;
139
+ message = 'updated';
140
+ } else {
141
+ connection.server.users[username] = {
142
+ password: password
143
+ };
144
+ message = 'added';
145
+ }
146
+
147
+ connection.send(
148
+ {
149
+ tag: '*',
150
+ command: 'XTOYBIRD',
151
+ attributes: [
152
+ {
153
+ type: 'SECTION',
154
+ section: [
155
+ {
156
+ type: 'ATOM',
157
+ value: 'XUSER'
158
+ }
159
+ ]
160
+ },
161
+ {
162
+ type: 'TEXT',
163
+ value: 'User ' + displayName(username) + message + ' successfully'
164
+ }
165
+ ]
166
+ },
167
+ 'CONNECTION USERADD',
168
+ parsed,
169
+ data
170
+ );
171
+ break;
172
+
173
+ case 'USERDEL':
174
+ username = parsed.attributes[1].value || '';
175
+
176
+ if (username in connection.server.users) {
177
+ delete connection.server.users[username];
178
+ message = 'succeeded';
179
+ } else {
180
+ message = 'failed (no such user)';
181
+ }
182
+
183
+ connection.send(
184
+ {
185
+ tag: '*',
186
+ command: 'XTOYBIRD',
187
+ attributes: [
188
+ {
189
+ type: 'SECTION',
190
+ section: [
191
+ {
192
+ type: 'ATOM',
193
+ value: 'XUSER'
194
+ }
195
+ ]
196
+ },
197
+ {
198
+ type: 'TEXT',
199
+ value: 'Removing user ' + displayName(username) + message
200
+ }
201
+ ]
202
+ },
203
+ 'CONNECTION USERDEL',
204
+ parsed,
205
+ data
206
+ );
207
+ break;
208
+
209
+ case 'SHUTDOWN':
210
+ connection.send(
211
+ {
212
+ tag: '*',
213
+ command: 'OK',
214
+ attributes: [
215
+ {
216
+ type: 'SECTION',
217
+ section: [
218
+ {
219
+ type: 'ATOM',
220
+ value: 'ALERT'
221
+ }
222
+ ]
223
+ },
224
+ {
225
+ type: 'TEXT',
226
+ value: 'System scheduled for shutdown'
227
+ }
228
+ ]
229
+ },
230
+ 'CONNECTION SHUTDOWN',
231
+ parsed,
232
+ data
233
+ );
234
+
235
+ connection.server.close();
236
+ break;
237
+
238
+ default:
239
+ connection.send(
240
+ {
241
+ tag: parsed.tag,
242
+ command: 'BAD',
243
+ attributes: [
244
+ {
245
+ type: 'TEXT',
246
+ value: 'Unknown command'
247
+ }
248
+ ]
249
+ },
250
+ 'INVALID COMMAND',
251
+ parsed,
252
+ data
253
+ );
254
+ return callback();
255
+ }
256
+ }
257
+
258
+ connection.send(
259
+ {
260
+ tag: parsed.tag,
261
+ command: 'OK',
262
+ attributes: [
263
+ {
264
+ type: 'TEXT',
265
+ value: 'XTOYBIRD Completed'
266
+ }
267
+ ]
268
+ },
269
+ 'XTOYBIRD',
270
+ parsed,
271
+ data
272
+ );
273
+ return callback();
274
+ },
275
+ { states: states.AUTHENTICATED }
276
+ );
277
+ };
278
+
279
+ // Response text is free text that some clients still tokenize, so only plain user names are echoed
280
+ function displayName(username) {
281
+ return /^[\w.@+-]+$/.test(username) ? "'" + username + "' " : '';
282
+ }