emailengine-app 2.61.1 → 2.61.2

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