emailengine-app 2.61.1 → 2.61.3

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 (136) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/data/google-crawlers.json +1 -1
  3. package/lib/account/account-state.js +248 -0
  4. package/lib/account.js +45 -193
  5. package/lib/api-routes/account-routes.js +1023 -0
  6. package/lib/api-routes/message-routes.js +1377 -0
  7. package/lib/consts.js +12 -2
  8. package/lib/email-client/base-client.js +282 -771
  9. package/lib/email-client/gmail/gmail-api.js +243 -0
  10. package/lib/email-client/gmail-client.js +145 -53
  11. package/lib/email-client/imap/mailbox.js +24 -698
  12. package/lib/email-client/imap/sync-operations.js +812 -0
  13. package/lib/email-client/imap-client.js +1 -1
  14. package/lib/email-client/message-builder.js +566 -0
  15. package/lib/email-client/notification-handler.js +314 -0
  16. package/lib/email-client/outlook/graph-api.js +326 -0
  17. package/lib/email-client/outlook-client.js +159 -113
  18. package/lib/email-client/smtp-pool-manager.js +196 -0
  19. package/lib/imapproxy/imap-server.js +3 -12
  20. package/lib/oauth/gmail.js +4 -4
  21. package/lib/oauth/mail-ru.js +30 -5
  22. package/lib/oauth/outlook.js +57 -3
  23. package/lib/oauth/pubsub/google.js +30 -11
  24. package/lib/oauth/scope-checker.js +202 -0
  25. package/lib/oauth2-apps.js +8 -4
  26. package/lib/redis-operations.js +484 -0
  27. package/lib/routes-ui.js +283 -2582
  28. package/lib/tools.js +4 -196
  29. package/lib/ui-routes/account-routes.js +1931 -0
  30. package/lib/ui-routes/admin-config-routes.js +1233 -0
  31. package/lib/ui-routes/admin-entities-routes.js +2367 -0
  32. package/lib/ui-routes/oauth-routes.js +992 -0
  33. package/lib/utils/network.js +237 -0
  34. package/package.json +10 -10
  35. package/sbom.json +1 -1
  36. package/static/js/app.js +5 -5
  37. package/static/licenses.html +79 -19
  38. package/translations/de.mo +0 -0
  39. package/translations/de.po +97 -86
  40. package/translations/en.mo +0 -0
  41. package/translations/en.po +80 -75
  42. package/translations/et.mo +0 -0
  43. package/translations/et.po +96 -86
  44. package/translations/fr.mo +0 -0
  45. package/translations/fr.po +97 -86
  46. package/translations/ja.mo +0 -0
  47. package/translations/ja.po +96 -86
  48. package/translations/messages.pot +105 -91
  49. package/translations/nl.mo +0 -0
  50. package/translations/nl.po +98 -86
  51. package/translations/pl.mo +0 -0
  52. package/translations/pl.po +96 -86
  53. package/views/account/security.hbs +4 -4
  54. package/views/accounts/account.hbs +13 -13
  55. package/views/accounts/register/imap-server.hbs +12 -12
  56. package/views/config/document-store/pre-processing/index.hbs +4 -2
  57. package/views/config/oauth/app.hbs +6 -7
  58. package/views/config/oauth/index.hbs +2 -2
  59. package/views/config/service.hbs +3 -4
  60. package/views/dashboard.hbs +5 -7
  61. package/views/error.hbs +22 -7
  62. package/views/gateways/gateway.hbs +2 -2
  63. package/views/partials/add_account_modal.hbs +7 -10
  64. package/views/partials/document_store_header.hbs +1 -1
  65. package/views/partials/editor_scope_info.hbs +0 -1
  66. package/views/partials/oauth_config_header.hbs +1 -1
  67. package/views/partials/side_menu.hbs +3 -3
  68. package/views/partials/webhook_form.hbs +2 -2
  69. package/views/templates/index.hbs +1 -1
  70. package/views/templates/template.hbs +8 -8
  71. package/views/tokens/index.hbs +6 -6
  72. package/views/tokens/new.hbs +1 -1
  73. package/views/webhooks/index.hbs +4 -4
  74. package/views/webhooks/webhook.hbs +7 -7
  75. package/workers/api.js +148 -2436
  76. package/workers/smtp.js +2 -1
  77. package/lib/imapproxy/imap-core/test/client.js +0 -46
  78. package/lib/imapproxy/imap-core/test/fixtures/append.eml +0 -1196
  79. package/lib/imapproxy/imap-core/test/fixtures/chunks.js +0 -44
  80. package/lib/imapproxy/imap-core/test/fixtures/fix1.eml +0 -6
  81. package/lib/imapproxy/imap-core/test/fixtures/fix2.eml +0 -599
  82. package/lib/imapproxy/imap-core/test/fixtures/fix3.eml +0 -32
  83. package/lib/imapproxy/imap-core/test/fixtures/fix4.eml +0 -6
  84. package/lib/imapproxy/imap-core/test/fixtures/mimetorture.eml +0 -599
  85. package/lib/imapproxy/imap-core/test/fixtures/mimetorture.js +0 -2740
  86. package/lib/imapproxy/imap-core/test/fixtures/mimetorture.json +0 -1411
  87. package/lib/imapproxy/imap-core/test/fixtures/mimetree.js +0 -85
  88. package/lib/imapproxy/imap-core/test/fixtures/nodemailer.eml +0 -582
  89. package/lib/imapproxy/imap-core/test/fixtures/ryan_finnie_mime_torture.eml +0 -599
  90. package/lib/imapproxy/imap-core/test/fixtures/simple.eml +0 -42
  91. package/lib/imapproxy/imap-core/test/fixtures/simple.json +0 -164
  92. package/lib/imapproxy/imap-core/test/imap-compile-stream-test.js +0 -671
  93. package/lib/imapproxy/imap-core/test/imap-compiler-test.js +0 -272
  94. package/lib/imapproxy/imap-core/test/imap-indexer-test.js +0 -236
  95. package/lib/imapproxy/imap-core/test/imap-parser-test.js +0 -922
  96. package/lib/imapproxy/imap-core/test/memory-notifier.js +0 -129
  97. package/lib/imapproxy/imap-core/test/prepare.sh +0 -74
  98. package/lib/imapproxy/imap-core/test/protocol-test.js +0 -1756
  99. package/lib/imapproxy/imap-core/test/search-test.js +0 -1356
  100. package/lib/imapproxy/imap-core/test/test-client.js +0 -152
  101. package/lib/imapproxy/imap-core/test/test-server.js +0 -623
  102. package/lib/imapproxy/imap-core/test/tools-test.js +0 -22
  103. package/test/api-test.js +0 -899
  104. package/test/autoreply-test.js +0 -327
  105. package/test/bounce-test.js +0 -151
  106. package/test/complaint-test.js +0 -256
  107. package/test/fixtures/autoreply/LICENSE +0 -27
  108. package/test/fixtures/autoreply/rfc3834-01.eml +0 -23
  109. package/test/fixtures/autoreply/rfc3834-02.eml +0 -24
  110. package/test/fixtures/autoreply/rfc3834-03.eml +0 -26
  111. package/test/fixtures/autoreply/rfc3834-04.eml +0 -48
  112. package/test/fixtures/autoreply/rfc3834-05.eml +0 -19
  113. package/test/fixtures/autoreply/rfc3834-06.eml +0 -59
  114. package/test/fixtures/bounces/163.eml +0 -2521
  115. package/test/fixtures/bounces/fastmail.eml +0 -242
  116. package/test/fixtures/bounces/gmail.eml +0 -252
  117. package/test/fixtures/bounces/hotmail.eml +0 -655
  118. package/test/fixtures/bounces/mailru.eml +0 -121
  119. package/test/fixtures/bounces/outlook.eml +0 -1107
  120. package/test/fixtures/bounces/postfix.eml +0 -101
  121. package/test/fixtures/bounces/rambler.eml +0 -116
  122. package/test/fixtures/bounces/workmail.eml +0 -142
  123. package/test/fixtures/bounces/yahoo.eml +0 -139
  124. package/test/fixtures/bounces/zoho.eml +0 -83
  125. package/test/fixtures/bounces/zonemta.eml +0 -100
  126. package/test/fixtures/complaints/LICENSE +0 -27
  127. package/test/fixtures/complaints/amazonses.eml +0 -72
  128. package/test/fixtures/complaints/dmarc.eml +0 -59
  129. package/test/fixtures/complaints/hotmail.eml +0 -49
  130. package/test/fixtures/complaints/optout.eml +0 -40
  131. package/test/fixtures/complaints/standard-arf.eml +0 -68
  132. package/test/fixtures/complaints/yahoo.eml +0 -68
  133. package/test/oauth2-apps-test.js +0 -301
  134. package/test/sendonly-test.js +0 -160
  135. package/test/test-config.js +0 -34
  136. package/test/webhooks-server.js +0 -39
@@ -0,0 +1,1023 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+ const { redis } = require('../db');
5
+ const { Account } = require('../account');
6
+ const getSecret = require('../get-secret');
7
+ const { oauth2Apps } = require('../oauth2-apps');
8
+ const Boom = require('@hapi/boom');
9
+ const Joi = require('joi');
10
+ const { failAction } = require('../tools');
11
+
12
+ const {
13
+ settingsSchema,
14
+ accountSchemas,
15
+ accountIdSchema,
16
+ accountCountersSchema,
17
+ accountPathSchema,
18
+ lastErrorSchema,
19
+ imapSchema,
20
+ imapUpdateSchema,
21
+ smtpSchema,
22
+ smtpUpdateSchema,
23
+ oauth2Schema,
24
+ oauth2UpdateSchema
25
+ } = require('../schemas');
26
+
27
+ const { REDIS_PREFIX, MAX_FORM_TTL, NONCE_BYTES } = require('../consts');
28
+
29
+ /**
30
+ * Validates that delegation fields are only used with OAuth2 accounts.
31
+ */
32
+ function validateDelegationFields(payload) {
33
+ const auth = payload.oauth2?.auth;
34
+ const hasDelegation = auth?.delegatedUser || auth?.delegatedAccount;
35
+ if (hasDelegation && !payload.oauth2?.provider) {
36
+ throw Boom.badRequest('Delegation fields (delegatedUser, delegatedAccount) require oauth2.provider to be set');
37
+ }
38
+ }
39
+
40
+ async function init(args) {
41
+ const {
42
+ server,
43
+ call,
44
+ documentsQueue,
45
+ oauth2Schema: oauth2SchemaArg,
46
+ imapSchema: imapSchemaArg,
47
+ smtpSchema: smtpSchemaArg,
48
+ CORS_CONFIG,
49
+ AccountTypeSchema
50
+ } = args;
51
+
52
+ // POST /v1/account - Create account
53
+ server.route({
54
+ method: 'POST',
55
+ path: '/v1/account',
56
+
57
+ async handler(request) {
58
+ let accountObject = new Account({
59
+ redis,
60
+ call,
61
+ secret: await getSecret(),
62
+ timeout: request.headers['x-ee-timeout']
63
+ });
64
+
65
+ try {
66
+ if (request.payload.oauth2 && request.payload.oauth2.authorize) {
67
+ // redirect to OAuth2 consent screen
68
+
69
+ const oAuth2Client = await oauth2Apps.getClient(request.payload.oauth2.provider);
70
+ const nonce = crypto.randomBytes(NONCE_BYTES).toString('base64url');
71
+
72
+ const accountData = request.payload;
73
+
74
+ if (accountData.oauth2.redirectUrl) {
75
+ accountData._meta = {
76
+ redirectUrl: accountData.oauth2.redirectUrl
77
+ };
78
+ delete accountData.oauth2.redirectUrl;
79
+ }
80
+
81
+ delete accountData.oauth2.authorize; // do not store this property
82
+ // store account data
83
+ await redis
84
+ .multi()
85
+ .set(`${REDIS_PREFIX}account:add:${nonce}`, JSON.stringify(accountData))
86
+ .expire(`${REDIS_PREFIX}account:add:${nonce}`, Math.floor(MAX_FORM_TTL / 1000))
87
+ .exec();
88
+
89
+ // Generate the url that will be used for the consent dialog.
90
+ let authorizeUrl;
91
+ switch (oAuth2Client.provider) {
92
+ case 'gmail': {
93
+ let requestData = {
94
+ state: `account:add:${nonce}`
95
+ };
96
+
97
+ if (accountData.email) {
98
+ requestData.email = accountData.email;
99
+ }
100
+
101
+ authorizeUrl = oAuth2Client.generateAuthUrl(requestData);
102
+
103
+ break;
104
+ }
105
+
106
+ case 'outlook':
107
+ case 'mailRu':
108
+ authorizeUrl = oAuth2Client.generateAuthUrl({
109
+ state: `account:add:${nonce}`
110
+ });
111
+ break;
112
+
113
+ default: {
114
+ let error = Boom.boomify(new Error('Unknown OAuth provider'), { statusCode: 400 });
115
+ throw error;
116
+ }
117
+ }
118
+
119
+ return {
120
+ redirect: authorizeUrl
121
+ };
122
+ }
123
+
124
+ // Validate delegation fields are only used with OAuth2 provider
125
+ validateDelegationFields(request.payload);
126
+
127
+ let result = await accountObject.create(request.payload);
128
+ return result;
129
+ } catch (err) {
130
+ request.logger.error({ msg: 'API request failed', err });
131
+ if (Boom.isBoom(err)) {
132
+ throw err;
133
+ }
134
+ let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
135
+ if (err.code) {
136
+ error.output.payload.code = err.code;
137
+ }
138
+ throw error;
139
+ }
140
+ },
141
+
142
+ options: {
143
+ description: 'Register new account',
144
+ notes: 'Registers new IMAP account to be synced',
145
+ tags: ['api', 'Account'],
146
+
147
+ plugins: {},
148
+
149
+ auth: {
150
+ strategy: 'api-token',
151
+ mode: 'required'
152
+ },
153
+ cors: CORS_CONFIG,
154
+
155
+ validate: {
156
+ options: {
157
+ stripUnknown: false,
158
+ abortEarly: false,
159
+ convert: true
160
+ },
161
+ failAction,
162
+
163
+ payload: Joi.object({
164
+ account: Joi.string()
165
+ .empty('')
166
+ .trim()
167
+ .max(256)
168
+ .allow(null)
169
+ .example('example')
170
+ .description(
171
+ 'Account ID. If set to `null`, a unique ID will be generated automatically. If you provide an existing account ID, the settings for that account will be updated instead'
172
+ )
173
+ .required(),
174
+
175
+ name: Joi.string().max(256).required().example('My Email Account').description('Display name for the account'),
176
+ email: Joi.string().empty('').email().example('user@example.com').description('Default email address of the account'),
177
+
178
+ path: accountPathSchema.example(['*']).label('AccountPath'),
179
+
180
+ subconnections: accountSchemas.subconnections,
181
+
182
+ webhooks: Joi.string()
183
+ .uri({
184
+ scheme: ['http', 'https'],
185
+ allowRelative: false
186
+ })
187
+ .allow('')
188
+ .example('https://myservice.com/imap/webhooks')
189
+ .description('Account-specific webhook URL'),
190
+
191
+ copy: Joi.boolean()
192
+ .allow(null)
193
+ .example(null)
194
+ .description('Copy submitted messages to Sent folder. Set to `null` to unset and use provider specific default.'),
195
+
196
+ logs: Joi.boolean().example(false).description('Store recent logs').default(false),
197
+
198
+ notifyFrom: accountSchemas.notifyFrom.default('now'),
199
+ syncFrom: accountSchemas.syncFrom.default(null),
200
+
201
+ proxy: settingsSchema.proxyUrl,
202
+ smtpEhloName: settingsSchema.smtpEhloName,
203
+
204
+ imapIndexer: accountSchemas.imapIndexer,
205
+
206
+ imap: Joi.object(imapSchemaArg).allow(false).description('IMAP configuration').label('ImapConfiguration'),
207
+
208
+ smtp: Joi.object(smtpSchemaArg).allow(false).description('SMTP configuration').label('SmtpConfiguration'),
209
+
210
+ oauth2: Joi.object(oauth2SchemaArg).allow(false).description('OAuth2 configuration').label('OAuth2'),
211
+
212
+ webhooksCustomHeaders: settingsSchema.webhooksCustomHeaders.label('AccountWebhooksCustomHeaders'),
213
+
214
+ locale: Joi.string().empty('').max(100).example('fr').description('Optional locale'),
215
+ tz: Joi.string().empty('').max(100).example('Europe/Tallinn').description('Optional timezone')
216
+ })
217
+ .label('CreateAccount')
218
+ .example({
219
+ account: 'example',
220
+ name: 'Nyan Cat',
221
+ email: 'nyan.cat@example.com',
222
+ imap: {
223
+ auth: {
224
+ user: 'nyan.cat',
225
+ pass: 'sercretpass'
226
+ },
227
+ host: 'mail.example.com',
228
+ port: 993,
229
+ secure: true
230
+ },
231
+ smtp: {
232
+ auth: {
233
+ user: 'nyan.cat',
234
+ pass: 'secretpass'
235
+ },
236
+ host: 'mail.example.com',
237
+ port: 465,
238
+ secure: true
239
+ }
240
+ })
241
+ },
242
+
243
+ response: {
244
+ schema: Joi.object({
245
+ account: accountIdSchema.required(),
246
+ state: Joi.string()
247
+ .required()
248
+ .valid('existing', 'new')
249
+ .example('new')
250
+ .description('Is the account new or updated existing')
251
+ .label('CreateAccountState')
252
+ }).label('CreateAccountResponse'),
253
+ failAction: 'log'
254
+ }
255
+ }
256
+ });
257
+
258
+ // PUT /v1/account/{account} - Update account
259
+ server.route({
260
+ method: 'PUT',
261
+ path: '/v1/account/{account}',
262
+
263
+ async handler(request) {
264
+ let accountObject = new Account({
265
+ redis,
266
+ account: request.params.account,
267
+ call,
268
+ secret: await getSecret(),
269
+ timeout: request.headers['x-ee-timeout']
270
+ });
271
+
272
+ try {
273
+ // Validate delegation fields are only used with OAuth2 provider
274
+ validateDelegationFields(request.payload);
275
+
276
+ return await accountObject.update(request.payload);
277
+ } catch (err) {
278
+ request.logger.error({ msg: 'API request failed', err });
279
+ if (Boom.isBoom(err)) {
280
+ throw err;
281
+ }
282
+ let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
283
+ if (err.code) {
284
+ error.output.payload.code = err.code;
285
+ }
286
+ throw error;
287
+ }
288
+ },
289
+ options: {
290
+ description: 'Update account info',
291
+ notes: 'Updates account information',
292
+ tags: ['api', 'Account'],
293
+
294
+ plugins: {},
295
+
296
+ auth: {
297
+ strategy: 'api-token',
298
+ mode: 'required'
299
+ },
300
+ cors: CORS_CONFIG,
301
+
302
+ validate: {
303
+ options: {
304
+ stripUnknown: false,
305
+ abortEarly: false,
306
+ convert: true
307
+ },
308
+ failAction,
309
+
310
+ params: Joi.object({
311
+ account: accountIdSchema.required()
312
+ }),
313
+
314
+ payload: Joi.object({
315
+ name: Joi.string().max(256).example('My Email Account').description('Display name for the account'),
316
+ email: Joi.string().empty('').email().example('user@example.com').description('Default email address of the account'),
317
+
318
+ path: accountPathSchema.example(['*']).label('AccountPath'),
319
+
320
+ subconnections: accountSchemas.subconnections,
321
+
322
+ webhooks: Joi.string()
323
+ .uri({
324
+ scheme: ['http', 'https'],
325
+ allowRelative: false
326
+ })
327
+ .allow('')
328
+ .example('https://myservice.com/imap/webhooks')
329
+ .description('Account-specific webhook URL'),
330
+
331
+ copy: Joi.boolean()
332
+ .allow(null)
333
+ .example(null)
334
+ .description('Copy submitted messages to Sent folder. Set to `null` to unset and use provider specific default.'),
335
+
336
+ logs: Joi.boolean().example(false).description('Store recent logs'),
337
+
338
+ notifyFrom: accountSchemas.notifyFrom,
339
+ syncFrom: accountSchemas.syncFrom,
340
+
341
+ proxy: settingsSchema.proxyUrl,
342
+ smtpEhloName: settingsSchema.smtpEhloName,
343
+
344
+ imap: Joi.object(imapUpdateSchema).allow(false).description('IMAP configuration').label('IMAPUpdate'),
345
+ smtp: Joi.object(smtpUpdateSchema).allow(false).description('SMTP configuration').label('SMTPUpdate'),
346
+ oauth2: Joi.object(oauth2UpdateSchema).allow(false).description('OAuth2 configuration').label('OAuth2Update'),
347
+
348
+ webhooksCustomHeaders: settingsSchema.webhooksCustomHeaders.label('AccountWebhooksCustomHeaders'),
349
+
350
+ locale: Joi.string().empty('').max(100).example('fr').description('Optional locale'),
351
+ tz: Joi.string().empty('').max(100).example('Europe/Tallinn').description('Optional timezone')
352
+ })
353
+ .label('UpdateAccount')
354
+ .example({
355
+ name: 'Nyan Cat',
356
+ email: 'nyan.cat@example.com',
357
+ imap: {
358
+ partial: true,
359
+ disabled: true
360
+ },
361
+ smtp: {
362
+ partial: true,
363
+ host: 'mail.example.com'
364
+ }
365
+ })
366
+ },
367
+
368
+ response: {
369
+ schema: Joi.object({
370
+ account: accountIdSchema.required()
371
+ }),
372
+ failAction: 'log'
373
+ }
374
+ }
375
+ });
376
+
377
+ // PUT /v1/account/{account}/reconnect - Request reconnect
378
+ server.route({
379
+ method: 'PUT',
380
+ path: '/v1/account/{account}/reconnect',
381
+
382
+ async handler(request) {
383
+ let accountObject = new Account({
384
+ redis,
385
+ account: request.params.account,
386
+ call,
387
+ secret: await getSecret(),
388
+ timeout: request.headers['x-ee-timeout']
389
+ });
390
+
391
+ try {
392
+ return { reconnect: await accountObject.requestReconnect(request.payload) };
393
+ } catch (err) {
394
+ request.logger.error({ msg: 'API request failed', err });
395
+ if (Boom.isBoom(err)) {
396
+ throw err;
397
+ }
398
+ let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
399
+ if (err.code) {
400
+ error.output.payload.code = err.code;
401
+ }
402
+ throw error;
403
+ }
404
+ },
405
+ options: {
406
+ description: 'Request reconnect',
407
+ notes: 'Requests connection to be reconnected',
408
+ tags: ['api', 'Account'],
409
+
410
+ plugins: {},
411
+
412
+ auth: {
413
+ strategy: 'api-token',
414
+ mode: 'required'
415
+ },
416
+ cors: CORS_CONFIG,
417
+
418
+ validate: {
419
+ options: {
420
+ stripUnknown: false,
421
+ abortEarly: false,
422
+ convert: true
423
+ },
424
+ failAction,
425
+
426
+ params: Joi.object({
427
+ account: accountIdSchema.required()
428
+ }),
429
+
430
+ payload: Joi.object({
431
+ reconnect: Joi.boolean().truthy('Y', 'true', '1').falsy('N', 'false', 0).default(false).description('Only reconnect if true')
432
+ }).label('RequestReconnect')
433
+ },
434
+
435
+ response: {
436
+ schema: Joi.object({
437
+ reconnect: Joi.boolean().truthy('Y', 'true', '1').falsy('N', 'false', 0).default(false).description('Reconnection status')
438
+ }).label('RequestReconnectResponse'),
439
+ failAction: 'log'
440
+ }
441
+ }
442
+ });
443
+
444
+ // PUT /v1/account/{account}/sync - Request syncing
445
+ server.route({
446
+ method: 'PUT',
447
+ path: '/v1/account/{account}/sync',
448
+
449
+ async handler(request) {
450
+ let accountObject = new Account({
451
+ redis,
452
+ account: request.params.account,
453
+ call,
454
+ secret: await getSecret(),
455
+ timeout: request.headers['x-ee-timeout']
456
+ });
457
+
458
+ try {
459
+ return { sync: await accountObject.requestSync(request.payload) };
460
+ } catch (err) {
461
+ request.logger.error({ msg: 'API request failed', err });
462
+ if (Boom.isBoom(err)) {
463
+ throw err;
464
+ }
465
+ let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
466
+ if (err.code) {
467
+ error.output.payload.code = err.code;
468
+ }
469
+ throw error;
470
+ }
471
+ },
472
+ options: {
473
+ description: 'Request syncing',
474
+ notes: 'Immediately trigger account syncing for IMAP accounts',
475
+ tags: ['api', 'Account'],
476
+
477
+ plugins: {},
478
+
479
+ auth: {
480
+ strategy: 'api-token',
481
+ mode: 'required'
482
+ },
483
+ cors: CORS_CONFIG,
484
+
485
+ validate: {
486
+ options: {
487
+ stripUnknown: false,
488
+ abortEarly: false,
489
+ convert: true
490
+ },
491
+ failAction,
492
+
493
+ params: Joi.object({
494
+ account: accountIdSchema.required()
495
+ }),
496
+
497
+ payload: Joi.object({
498
+ sync: Joi.boolean().truthy('Y', 'true', '1').falsy('N', 'false', 0).default(false).description('Only sync if true')
499
+ }).label('RequestSync')
500
+ },
501
+
502
+ response: {
503
+ schema: Joi.object({
504
+ sync: Joi.boolean().truthy('Y', 'true', '1').falsy('N', 'false', 0).default(false).description('Sync status')
505
+ }).label('RequestSyncResponse'),
506
+ failAction: 'log'
507
+ }
508
+ }
509
+ });
510
+
511
+ // DELETE /v1/account/{account} - Remove account
512
+ server.route({
513
+ method: 'DELETE',
514
+ path: '/v1/account/{account}',
515
+
516
+ async handler(request) {
517
+ let accountObject = new Account({
518
+ redis,
519
+ account: request.params.account,
520
+ documentsQueue,
521
+ call,
522
+ secret: await getSecret(),
523
+ timeout: request.headers['x-ee-timeout']
524
+ });
525
+
526
+ try {
527
+ return await accountObject.delete();
528
+ } catch (err) {
529
+ request.logger.error({ msg: 'API request failed', err });
530
+ if (Boom.isBoom(err)) {
531
+ throw err;
532
+ }
533
+ let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
534
+ if (err.code) {
535
+ error.output.payload.code = err.code;
536
+ }
537
+ throw error;
538
+ }
539
+ },
540
+ options: {
541
+ description: 'Remove account',
542
+ notes: "Stop processing and clear the account's cache",
543
+
544
+ tags: ['api', 'Account'],
545
+
546
+ plugins: {},
547
+
548
+ auth: {
549
+ strategy: 'api-token',
550
+ mode: 'required'
551
+ },
552
+ cors: CORS_CONFIG,
553
+
554
+ validate: {
555
+ options: {
556
+ stripUnknown: false,
557
+ abortEarly: false,
558
+ convert: true
559
+ },
560
+ failAction,
561
+
562
+ params: Joi.object({
563
+ account: accountIdSchema.required()
564
+ })
565
+ },
566
+
567
+ response: {
568
+ schema: Joi.object({
569
+ account: accountIdSchema.required(),
570
+ deleted: Joi.boolean().truthy('Y', 'true', '1').falsy('N', 'false', 0).default(true).description('Was the account deleted')
571
+ }).label('DeleteRequestResponse'),
572
+ failAction: 'log'
573
+ }
574
+ }
575
+ });
576
+
577
+ // PUT /v1/account/{account}/flush - Request account flush
578
+ server.route({
579
+ method: 'PUT',
580
+ path: '/v1/account/{account}/flush',
581
+
582
+ async handler(request, h) {
583
+ let accountObject = new Account({
584
+ redis,
585
+ account: request.params.account,
586
+ call,
587
+ secret: await getSecret(),
588
+ esClient: await h.getESClient(request.logger),
589
+ timeout: request.headers['x-ee-timeout']
590
+ });
591
+
592
+ try {
593
+ return { flush: await accountObject.flush(request.payload) };
594
+ } catch (err) {
595
+ request.logger.error({ msg: 'API request failed', err });
596
+ if (Boom.isBoom(err)) {
597
+ throw err;
598
+ }
599
+ let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
600
+ if (err.code) {
601
+ error.output.payload.code = err.code;
602
+ }
603
+ throw error;
604
+ }
605
+ },
606
+ options: {
607
+ description: 'Request account flush',
608
+ notes: 'Deletes all email indexes from Redis and ElasticSearch and re-creates the index for that account. You can only run a single flush operation at a time, so you must wait until the previous flush has finished before initiating a new one.',
609
+ tags: ['api', 'Account'],
610
+
611
+ plugins: {},
612
+
613
+ auth: {
614
+ strategy: 'api-token',
615
+ mode: 'required'
616
+ },
617
+ cors: CORS_CONFIG,
618
+
619
+ validate: {
620
+ options: {
621
+ stripUnknown: false,
622
+ abortEarly: false,
623
+ convert: true
624
+ },
625
+ failAction,
626
+
627
+ params: Joi.object({
628
+ account: accountIdSchema.required()
629
+ }),
630
+
631
+ payload: Joi.object({
632
+ flush: Joi.boolean().truthy('Y', 'true', '1').falsy('N', 'false', 0).default(false).description('Only flush the account if true'),
633
+ notifyFrom: accountSchemas.notifyFrom.default('now'),
634
+ imapIndexer: accountSchemas.imapIndexer,
635
+ syncFrom: accountSchemas.syncFrom
636
+ }).label('RequestFlush')
637
+ },
638
+
639
+ response: {
640
+ schema: Joi.object({
641
+ flush: Joi.boolean().truthy('Y', 'true', '1').falsy('N', 'false', 0).default(false).description('Flush status')
642
+ }).label('RequestFlushResponse'),
643
+ failAction: 'log'
644
+ }
645
+ }
646
+ });
647
+
648
+ // GET /v1/accounts - List accounts
649
+ server.route({
650
+ method: 'GET',
651
+ path: '/v1/accounts',
652
+
653
+ async handler(request) {
654
+ try {
655
+ let accountObject = new Account({
656
+ redis,
657
+ account: request.params.account,
658
+ call,
659
+ secret: await getSecret(),
660
+ timeout: request.headers['x-ee-timeout']
661
+ });
662
+
663
+ return await accountObject.listAccounts(request.query.state, request.query.query, request.query.page, request.query.pageSize);
664
+ } catch (err) {
665
+ request.logger.error({ msg: 'API request failed', err });
666
+ if (Boom.isBoom(err)) {
667
+ throw err;
668
+ }
669
+ let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
670
+ if (err.code) {
671
+ error.output.payload.code = err.code;
672
+ }
673
+ throw error;
674
+ }
675
+ },
676
+
677
+ options: {
678
+ description: 'List accounts',
679
+ notes: 'Lists registered accounts',
680
+ tags: ['api', 'Account'],
681
+
682
+ plugins: {},
683
+
684
+ auth: {
685
+ strategy: 'api-token',
686
+ mode: 'required'
687
+ },
688
+ cors: CORS_CONFIG,
689
+
690
+ validate: {
691
+ options: {
692
+ stripUnknown: false,
693
+ abortEarly: false,
694
+ convert: true
695
+ },
696
+ failAction,
697
+
698
+ query: Joi.object({
699
+ page: Joi.number()
700
+ .integer()
701
+ .min(0)
702
+ .max(1024 * 1024)
703
+ .default(0)
704
+ .example(0)
705
+ .description('Page number (zero indexed, so use 0 for first page)')
706
+ .label('PageNumber'),
707
+ pageSize: Joi.number().integer().min(1).max(1000).default(20).example(20).description('How many entries per page').label('PageSize'),
708
+ state: Joi.string()
709
+ .valid('init', 'syncing', 'connecting', 'connected', 'authenticationError', 'connectError', 'unset', 'disconnected')
710
+ .example('connected')
711
+ .description('Filter accounts by state')
712
+ .label('AccountState'),
713
+ query: Joi.string().example('user@example.com').description('Filter accounts by string match').label('AccountQuery')
714
+ }).label('AccountsFilter')
715
+ },
716
+
717
+ response: {
718
+ schema: Joi.object({
719
+ total: Joi.number().integer().example(120).description('How many matching entries').label('TotalNumber'),
720
+ page: Joi.number().integer().example(0).description('Current page (0-based index)').label('PageNumber'),
721
+ pages: Joi.number().integer().example(24).description('Total page count').label('PagesNumber'),
722
+
723
+ accounts: Joi.array()
724
+ .items(
725
+ Joi.object({
726
+ account: accountIdSchema.required(),
727
+ name: Joi.string().max(256).example('My Email Account').description('Display name for the account'),
728
+ email: Joi.string().empty('').email().example('user@example.com').description('Default email address of the account'),
729
+ type: AccountTypeSchema,
730
+ app: Joi.string().max(256).example('AAABhaBPHscAAAAH').description('OAuth2 application ID'),
731
+ state: Joi.string()
732
+ .required()
733
+ .valid('init', 'syncing', 'connecting', 'connected', 'authenticationError', 'connectError', 'unset', 'disconnected')
734
+ .example('connected')
735
+ .description('Account state'),
736
+ webhooks: Joi.string()
737
+ .uri({
738
+ scheme: ['http', 'https'],
739
+ allowRelative: false
740
+ })
741
+ .example('https://myservice.com/imap/webhooks')
742
+ .description('Account-specific webhook URL'),
743
+ proxy: settingsSchema.proxyUrl,
744
+ smtpEhloName: settingsSchema.smtpEhloName,
745
+
746
+ counters: accountCountersSchema,
747
+
748
+ syncTime: Joi.date().iso().example('2021-02-17T13:43:18.860Z').description('Last sync time'),
749
+ lastError: lastErrorSchema.allow(null)
750
+ }).label('AccountResponseItem')
751
+ )
752
+ .label('AccountEntries')
753
+ }).label('AccountsFilterResponse'),
754
+ failAction: 'log'
755
+ }
756
+ }
757
+ });
758
+
759
+ // GET /v1/account/{account} - Get account info
760
+ server.route({
761
+ method: 'GET',
762
+ path: '/v1/account/{account}',
763
+
764
+ async handler(request) {
765
+ let accountObject = new Account({
766
+ redis,
767
+ account: request.params.account,
768
+ call,
769
+ secret: await getSecret(),
770
+ timeout: request.headers['x-ee-timeout']
771
+ });
772
+ try {
773
+ let accountData = await accountObject.loadAccountData();
774
+
775
+ // remove secrets
776
+ for (let type of ['imap', 'smtp', 'oauth2']) {
777
+ if (accountData[type] && accountData[type].auth) {
778
+ for (let key of ['pass', 'accessToken', 'refreshToken']) {
779
+ if (key in accountData[type].auth) {
780
+ accountData[type].auth[key] = '******';
781
+ }
782
+ }
783
+ }
784
+
785
+ if (accountData[type]) {
786
+ for (let key of ['accessToken', 'refreshToken']) {
787
+ if (key in accountData[type]) {
788
+ accountData[type][key] = '******';
789
+ }
790
+ }
791
+ }
792
+ }
793
+
794
+ let result = {};
795
+
796
+ for (let key of [
797
+ 'account',
798
+ 'name',
799
+ 'email',
800
+ 'copy',
801
+ 'logs',
802
+ 'notifyFrom',
803
+ 'syncFrom',
804
+ 'path',
805
+ 'subconnections',
806
+ 'webhooks',
807
+ 'proxy',
808
+ 'smtpEhloName',
809
+ 'imapIndexer',
810
+ 'imap',
811
+ 'smtp',
812
+ 'oauth2',
813
+ 'state',
814
+ 'smtpStatus',
815
+ 'syncError',
816
+ 'connections',
817
+ 'webhooksCustomHeaders',
818
+ 'locale',
819
+ 'tz'
820
+ ]) {
821
+ if (key in accountData) {
822
+ result[key] = accountData[key];
823
+ }
824
+ }
825
+
826
+ // default false
827
+ for (let key of ['logs']) {
828
+ result[key] = !!result[key];
829
+ }
830
+
831
+ // default null
832
+ for (let key of ['notifyFrom', 'syncFrom', 'lastError', 'smtpStatus']) {
833
+ result[key] = result[key] || null;
834
+ }
835
+
836
+ let oauth2App;
837
+ if (accountData.oauth2 && accountData.oauth2.provider) {
838
+ oauth2App = await oauth2Apps.get(accountData.oauth2.provider);
839
+
840
+ if (oauth2App) {
841
+ // Check if account is already marked as send-only
842
+ if (accountData.sendOnly) {
843
+ result.sendOnly = true;
844
+ } else {
845
+ result.type = oauth2App.provider;
846
+ }
847
+ if (oauth2App.id !== oauth2App.provider) {
848
+ result.app = oauth2App.id;
849
+ }
850
+ result.baseScopes = oauth2App.baseScope || 'imap';
851
+ } else {
852
+ result.type = 'oauth2';
853
+ }
854
+ } else if (accountData.oauth2 && accountData.oauth2.auth && accountData.oauth2.auth.delegatedAccount) {
855
+ result.type = 'delegated';
856
+ } else if (accountData.imap && !accountData.imap.disabled) {
857
+ result.type = 'imap';
858
+ } else {
859
+ result.type = 'sending';
860
+ result.sendOnly = true;
861
+ }
862
+
863
+ if ((accountData.imap || (oauth2App && (!oauth2App.baseScopes || oauth2App.baseScopes === 'imap'))) && !result.imapIndexer) {
864
+ result.imapIndexer = 'full';
865
+ }
866
+
867
+ if (accountData.sync) {
868
+ result.syncTime = accountData.sync;
869
+ }
870
+
871
+ if (accountData.state) {
872
+ result.lastError = accountData.state === 'connected' ? null : accountData.lastErrorState;
873
+ }
874
+
875
+ if (accountData.counters) {
876
+ result.counters = accountData.counters;
877
+ }
878
+
879
+ if (request.query.quota && !result.sendOnly) {
880
+ result.quota = await accountObject.getQuota();
881
+ }
882
+
883
+ return result;
884
+ } catch (err) {
885
+ request.logger.error({ msg: 'API request failed', err });
886
+ if (Boom.isBoom(err)) {
887
+ throw err;
888
+ }
889
+ let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
890
+ if (err.code) {
891
+ error.output.payload.code = err.code;
892
+ }
893
+ throw error;
894
+ }
895
+ },
896
+ options: {
897
+ description: 'Get account info',
898
+ notes: 'Returns stored information about the account. Passwords are not included.',
899
+ tags: ['api', 'Account'],
900
+
901
+ auth: {
902
+ strategy: 'api-token',
903
+ mode: 'required'
904
+ },
905
+ cors: CORS_CONFIG,
906
+
907
+ validate: {
908
+ options: {
909
+ stripUnknown: false,
910
+ abortEarly: false,
911
+ convert: true
912
+ },
913
+ failAction,
914
+
915
+ params: Joi.object({
916
+ account: accountIdSchema.required()
917
+ }),
918
+
919
+ query: Joi.object({
920
+ quota: Joi.boolean()
921
+ .truthy('Y', 'true', '1')
922
+ .falsy('N', 'false', 0)
923
+ .default(false)
924
+ .description('If true, then include quota information in the response')
925
+ .label('AccountQuota')
926
+ })
927
+ },
928
+
929
+ response: {
930
+ schema: Joi.object({
931
+ account: accountIdSchema.required(),
932
+
933
+ name: Joi.string().max(256).example('My Email Account').description('Display name for the account'),
934
+ email: Joi.string().empty('').email().example('user@example.com').description('Default email address of the account'),
935
+
936
+ copy: Joi.boolean().example(true).description('Copy submitted messages to Sent folder'),
937
+ logs: Joi.boolean().example(false).description('Store recent logs'),
938
+
939
+ notifyFrom: accountSchemas.notifyFrom,
940
+ syncFrom: accountSchemas.syncFrom,
941
+
942
+ path: accountPathSchema.example(['*']).label('AccountPath'),
943
+
944
+ imapIndexer: accountSchemas.imapIndexer,
945
+
946
+ subconnections: accountSchemas.subconnections,
947
+
948
+ webhooks: Joi.string()
949
+ .uri({
950
+ scheme: ['http', 'https'],
951
+ allowRelative: false
952
+ })
953
+ .example('https://myservice.com/imap/webhooks')
954
+ .description('Account-specific webhook URL'),
955
+ proxy: settingsSchema.proxyUrl,
956
+ smtpEhloName: settingsSchema.smtpEhloName,
957
+
958
+ imap: Joi.object(imapSchema).description('IMAP configuration').label('IMAPResponse'),
959
+
960
+ smtp: Joi.object(smtpSchema).description('SMTP configuration').label('SMTPResponse'),
961
+
962
+ oauth2: Joi.object(oauth2Schema).description('OAuth2 configuration').label('Oauth2Response'),
963
+
964
+ state: Joi.string()
965
+ .valid('init', 'syncing', 'connecting', 'connected', 'authenticationError', 'connectError', 'unset', 'disconnected')
966
+ .example('connected')
967
+ .description('Informational account state')
968
+ .label('AccountInfoState'),
969
+
970
+ smtpStatus: Joi.object({
971
+ created: Joi.date()
972
+ .iso()
973
+ .allow(null)
974
+ .example('2021-07-08T07:06:34.336Z')
975
+ .description('When was the status for SMTP connection last updated'),
976
+ status: Joi.string().valid('ok', 'error').description('Was the last SMTP attempt successful or not').label('SMTPStatusStatus'),
977
+ response: Joi.string().example('250 OK').description('SMTP response message for delivery attempt'),
978
+ description: Joi.string().example('Authentication failed').description('Error information'),
979
+ responseCode: Joi.number().integer().example(500).description('Error status code'),
980
+ code: Joi.string().example('EAUTH').description('Error type identifier'),
981
+ command: Joi.string().example('AUTH PLAIN').description('SMTP command that failed')
982
+ })
983
+ .description('Information about the last SMTP connection attempt')
984
+ .label('SMTPInfoStatus'),
985
+
986
+ webhooksCustomHeaders: settingsSchema.webhooksCustomHeaders.label('AccountWebhooksCustomHeaders'),
987
+
988
+ locale: Joi.string().empty('').max(100).example('fr').description('Optional locale'),
989
+ tz: Joi.string().empty('').max(100).example('Europe/Tallinn').description('Optional timezone'),
990
+
991
+ type: AccountTypeSchema,
992
+ app: Joi.string().max(256).example('AAABhaBPHscAAAAH').description('OAuth2 application ID'),
993
+ baseScopes: Joi.string()
994
+ .empty('')
995
+ .trim()
996
+ .valid(...['imap', 'api', 'pubsub'])
997
+ .example('imap')
998
+ .description('OAuth2 Base Scopes'),
999
+
1000
+ counters: accountCountersSchema,
1001
+
1002
+ quota: Joi.object({
1003
+ usage: Joi.number().integer().example(8547884032).description('How many bytes has the account stored in emails'),
1004
+ limit: Joi.number().integer().example(16106127360).description('How many bytes can the account store emails'),
1005
+ status: Joi.string().example('53%').description('Textual information about the usage')
1006
+ })
1007
+ .label('AccountQuota')
1008
+ .allow(false)
1009
+ .description(
1010
+ 'Account quota information if query argument quota=true. This value will be false if the server does not provide quota information.'
1011
+ ),
1012
+
1013
+ syncTime: Joi.date().iso().example('2021-02-17T13:43:18.860Z').description('Last sync time'),
1014
+
1015
+ lastError: lastErrorSchema.allow(null)
1016
+ }).label('AccountResponse'),
1017
+ failAction: 'log'
1018
+ }
1019
+ }
1020
+ });
1021
+ }
1022
+
1023
+ module.exports = init;