emailengine-app 2.61.0 → 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 (137) hide show
  1. package/CHANGELOG.md +16 -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 +3 -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 +5 -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 +12 -12
  35. package/sbom.json +1 -1
  36. package/static/js/app.js +5 -5
  37. package/static/licenses.html +91 -21
  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 +67 -80
  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/workers/webhooks.js +6 -0
  78. package/lib/imapproxy/imap-core/test/client.js +0 -46
  79. package/lib/imapproxy/imap-core/test/fixtures/append.eml +0 -1196
  80. package/lib/imapproxy/imap-core/test/fixtures/chunks.js +0 -44
  81. package/lib/imapproxy/imap-core/test/fixtures/fix1.eml +0 -6
  82. package/lib/imapproxy/imap-core/test/fixtures/fix2.eml +0 -599
  83. package/lib/imapproxy/imap-core/test/fixtures/fix3.eml +0 -32
  84. package/lib/imapproxy/imap-core/test/fixtures/fix4.eml +0 -6
  85. package/lib/imapproxy/imap-core/test/fixtures/mimetorture.eml +0 -599
  86. package/lib/imapproxy/imap-core/test/fixtures/mimetorture.js +0 -2740
  87. package/lib/imapproxy/imap-core/test/fixtures/mimetorture.json +0 -1411
  88. package/lib/imapproxy/imap-core/test/fixtures/mimetree.js +0 -85
  89. package/lib/imapproxy/imap-core/test/fixtures/nodemailer.eml +0 -582
  90. package/lib/imapproxy/imap-core/test/fixtures/ryan_finnie_mime_torture.eml +0 -599
  91. package/lib/imapproxy/imap-core/test/fixtures/simple.eml +0 -42
  92. package/lib/imapproxy/imap-core/test/fixtures/simple.json +0 -164
  93. package/lib/imapproxy/imap-core/test/imap-compile-stream-test.js +0 -671
  94. package/lib/imapproxy/imap-core/test/imap-compiler-test.js +0 -272
  95. package/lib/imapproxy/imap-core/test/imap-indexer-test.js +0 -236
  96. package/lib/imapproxy/imap-core/test/imap-parser-test.js +0 -922
  97. package/lib/imapproxy/imap-core/test/memory-notifier.js +0 -129
  98. package/lib/imapproxy/imap-core/test/prepare.sh +0 -74
  99. package/lib/imapproxy/imap-core/test/protocol-test.js +0 -1756
  100. package/lib/imapproxy/imap-core/test/search-test.js +0 -1356
  101. package/lib/imapproxy/imap-core/test/test-client.js +0 -152
  102. package/lib/imapproxy/imap-core/test/test-server.js +0 -623
  103. package/lib/imapproxy/imap-core/test/tools-test.js +0 -22
  104. package/test/api-test.js +0 -899
  105. package/test/autoreply-test.js +0 -327
  106. package/test/bounce-test.js +0 -151
  107. package/test/complaint-test.js +0 -256
  108. package/test/fixtures/autoreply/LICENSE +0 -27
  109. package/test/fixtures/autoreply/rfc3834-01.eml +0 -23
  110. package/test/fixtures/autoreply/rfc3834-02.eml +0 -24
  111. package/test/fixtures/autoreply/rfc3834-03.eml +0 -26
  112. package/test/fixtures/autoreply/rfc3834-04.eml +0 -48
  113. package/test/fixtures/autoreply/rfc3834-05.eml +0 -19
  114. package/test/fixtures/autoreply/rfc3834-06.eml +0 -59
  115. package/test/fixtures/bounces/163.eml +0 -2521
  116. package/test/fixtures/bounces/fastmail.eml +0 -242
  117. package/test/fixtures/bounces/gmail.eml +0 -252
  118. package/test/fixtures/bounces/hotmail.eml +0 -655
  119. package/test/fixtures/bounces/mailru.eml +0 -121
  120. package/test/fixtures/bounces/outlook.eml +0 -1107
  121. package/test/fixtures/bounces/postfix.eml +0 -101
  122. package/test/fixtures/bounces/rambler.eml +0 -116
  123. package/test/fixtures/bounces/workmail.eml +0 -142
  124. package/test/fixtures/bounces/yahoo.eml +0 -139
  125. package/test/fixtures/bounces/zoho.eml +0 -83
  126. package/test/fixtures/bounces/zonemta.eml +0 -100
  127. package/test/fixtures/complaints/LICENSE +0 -27
  128. package/test/fixtures/complaints/amazonses.eml +0 -72
  129. package/test/fixtures/complaints/dmarc.eml +0 -59
  130. package/test/fixtures/complaints/hotmail.eml +0 -49
  131. package/test/fixtures/complaints/optout.eml +0 -40
  132. package/test/fixtures/complaints/standard-arf.eml +0 -68
  133. package/test/fixtures/complaints/yahoo.eml +0 -68
  134. package/test/oauth2-apps-test.js +0 -301
  135. package/test/sendonly-test.js +0 -160
  136. package/test/test-config.js +0 -34
  137. 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;