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,992 @@
1
+ 'use strict';
2
+
3
+ const Boom = require('@hapi/boom');
4
+ const Joi = require('joi');
5
+
6
+ const settings = require('../settings');
7
+ const { redis } = require('../db');
8
+ const { oauth2Apps, LEGACY_KEYS, OAUTH_PROVIDERS, oauth2ProviderData } = require('../oauth2-apps');
9
+ const getSecret = require('../get-secret');
10
+ const { Account } = require('../account');
11
+ const { oauthCreateSchema, googleProjectIdSchema, googleWorkspaceAccountsSchema } = require('../schemas');
12
+ const consts = require('../consts');
13
+
14
+ const { DEFAULT_PAGE_SIZE } = consts;
15
+
16
+ const AZURE_CLOUDS = [
17
+ {
18
+ id: 'global',
19
+ name: 'Azure global service',
20
+ description: 'Regular Microsoft cloud accounts'
21
+ },
22
+
23
+ {
24
+ id: 'gcc-high',
25
+ name: 'GCC High',
26
+ description: 'Microsoft Graph for US Government L4'
27
+ },
28
+
29
+ {
30
+ id: 'dod',
31
+ name: 'DoD',
32
+ description: 'Microsoft Graph for US Government L5 (DOD)'
33
+ },
34
+
35
+ {
36
+ id: 'china',
37
+ name: 'Azure China',
38
+ description: 'Microsoft Graph China operated by 21Vianet'
39
+ }
40
+ ];
41
+
42
+ const oauthUpdateSchema = {
43
+ app: Joi.string().empty('').max(255).example('gmail').label('Provider').required(),
44
+
45
+ provider: Joi.string()
46
+ .trim()
47
+ .empty('')
48
+ .max(256)
49
+ .valid(...Object.keys(OAUTH_PROVIDERS))
50
+ .example('gmail')
51
+ .required()
52
+ .description('OAuth2 provider'),
53
+
54
+ name: Joi.string()
55
+ .trim()
56
+ .empty('')
57
+ .max(256)
58
+ .example('My Gmail App')
59
+ .description('Application name')
60
+ .when('app', {
61
+ not: Joi.string().valid(...LEGACY_KEYS),
62
+ then: Joi.required(),
63
+ otherwise: Joi.optional().valid(false, null)
64
+ }),
65
+ description: Joi.string().trim().allow('').max(1024).example('My cool app').description('Application description'),
66
+
67
+ title: Joi.string().allow('').trim().max(256).example('App title').description('Title for the application button'),
68
+
69
+ enabled: Joi.boolean().truthy('Y', 'true', '1', 'on').falsy('N', 'false', 0, '').default(false).description('Enable this app'),
70
+
71
+ clientId: Joi.string()
72
+ .trim()
73
+ .allow('')
74
+ .max(256)
75
+ .when('provider', {
76
+ not: 'gmailService',
77
+ then: Joi.required(),
78
+ otherwise: Joi.optional().valid(false, null)
79
+ })
80
+ .description('OAuth2 Client ID'),
81
+
82
+ clientSecret: Joi.string()
83
+ .trim()
84
+ .empty('', false, null)
85
+ .max(256)
86
+ .when('provider', {
87
+ not: 'gmailService',
88
+ then: Joi.optional(),
89
+ otherwise: Joi.forbidden()
90
+ })
91
+ .description('OAuth2 Client Secret'),
92
+
93
+ pubSubApp: Joi.string()
94
+ .empty('')
95
+ .base64({ paddingRequired: false, urlSafe: true })
96
+ .max(512)
97
+ .example('AAAAAQAACnA')
98
+ .description('Cloud Pub/Sub app for Gmail API webhooks'),
99
+
100
+ extraScopes: Joi.string()
101
+ .allow('')
102
+ .trim()
103
+ .max(10 * 1024)
104
+ .description('OAuth2 Extra Scopes'),
105
+
106
+ skipScopes: Joi.string()
107
+ .allow('')
108
+ .trim()
109
+ .max(10 * 1024)
110
+ .description('OAuth2 scopes to skip from the base set'),
111
+
112
+ serviceClient: Joi.string()
113
+ .trim()
114
+ .allow('')
115
+ .max(256)
116
+ .when('provider', {
117
+ is: 'gmailService',
118
+ then: Joi.required(),
119
+ otherwise: Joi.optional().valid(false, null)
120
+ })
121
+ .description('OAuth2 Service Client ID'),
122
+
123
+ googleProjectId: googleProjectIdSchema,
124
+
125
+ googleWorkspaceAccounts: googleWorkspaceAccountsSchema.when('provider', {
126
+ is: 'gmail',
127
+ then: Joi.optional().default(false)
128
+ }),
129
+
130
+ serviceClientEmail: Joi.string()
131
+ .trim()
132
+ .allow('')
133
+ .email()
134
+ .when('provider', {
135
+ is: 'gmailService',
136
+ then: Joi.required(),
137
+ otherwise: Joi.optional().valid(false, null)
138
+ })
139
+ .example('name@project-123.iam.gserviceaccount.com')
140
+ .description('Service Client Email for 2-legged OAuth2 applications'),
141
+
142
+ serviceKey: Joi.string()
143
+ .trim()
144
+ .empty('', false, null)
145
+ .max(100 * 1024)
146
+ .when('provider', {
147
+ is: 'gmailService',
148
+ then: Joi.optional(),
149
+ otherwise: Joi.forbidden()
150
+ })
151
+ .description('OAuth2 Secret Service Key'),
152
+
153
+ authority: Joi.string()
154
+ .trim()
155
+ .empty('')
156
+ .max(1024)
157
+ .when('provider', {
158
+ is: 'outlook',
159
+ then: Joi.required(),
160
+ otherwise: Joi.optional().valid(false, null)
161
+ })
162
+ .example(false)
163
+ .label('SupportedAccountTypes'),
164
+
165
+ cloud: Joi.string()
166
+ .trim()
167
+ .empty('')
168
+ .valid('global', 'gcc-high', 'dod', 'china')
169
+ .example('global')
170
+ .description('Azure cloud type for Outlook OAuth2 applications')
171
+ .label('AzureCloud'),
172
+
173
+ tenant: Joi.string().trim().empty('').max(1024).example('f8cdef31-a31e-4b4a-93e4-5f571e91255a').label('Directorytenant'),
174
+
175
+ redirectUrl: Joi.string()
176
+ .allow('')
177
+ .uri({ scheme: ['http', 'https'], allowRelative: false })
178
+ .when('provider', {
179
+ not: 'gmailService',
180
+ then: Joi.required(),
181
+ otherwise: Joi.optional().valid(false, null)
182
+ })
183
+ .description('OAuth2 Callback URL')
184
+ };
185
+
186
+ function init({ server, call }) {
187
+ // GET /admin/config/oauth - OAuth applications list
188
+ server.route({
189
+ method: 'GET',
190
+ path: '/admin/config/oauth',
191
+ async handler(request, h) {
192
+ let data = await oauth2Apps.list(request.query.page - 1, request.query.pageSize, { query: request.query.query });
193
+
194
+ let nextPage = false;
195
+ let prevPage = false;
196
+
197
+ let getPagingUrl = (page, query) => {
198
+ let url = new URL(`admin/config/oauth`, 'http://localhost');
199
+
200
+ if (page) {
201
+ url.searchParams.append('page', page);
202
+ }
203
+
204
+ if (request.query.pageSize !== DEFAULT_PAGE_SIZE) {
205
+ url.searchParams.append('pageSize', request.query.pageSize);
206
+ }
207
+
208
+ if (query) {
209
+ url.searchParams.append('query', query);
210
+ }
211
+
212
+ return url.pathname + url.search;
213
+ };
214
+
215
+ if (data.pages > data.page + 1) {
216
+ nextPage = getPagingUrl(data.page + 2, request.query.query);
217
+ }
218
+
219
+ if (data.page > 0) {
220
+ prevPage = getPagingUrl(data.page, request.query.query);
221
+ }
222
+
223
+ data.apps.forEach(app => {
224
+ app.providerData = oauth2ProviderData(app.provider);
225
+ switch (app.baseScopes) {
226
+ case 'api':
227
+ app.baseScopesText = 'API';
228
+ break;
229
+ case 'pubsub':
230
+ app.baseScopesText = 'Cloud Pub/Sub';
231
+ break;
232
+ case 'imap':
233
+ default:
234
+ app.baseScopesText = 'IMAP and SMTP';
235
+ break;
236
+ }
237
+ });
238
+
239
+ let newLink = new URL('/admin/config/oauth/new', 'http://localhost');
240
+
241
+ return h.view(
242
+ 'config/oauth/index',
243
+ {
244
+ pageTitle: 'OAuth2',
245
+ menuConfig: true,
246
+ menuConfigOauth: true,
247
+
248
+ newLink: newLink.pathname + newLink.search,
249
+
250
+ searchTarget: '/admin/config/oauth',
251
+ searchPlaceholder: 'Search for OAuth2 applications...',
252
+ query: request.query.query,
253
+
254
+ showPaging: data.pages > 1,
255
+ nextPage,
256
+ prevPage,
257
+ firstPage: data.page === 0,
258
+ pageLinks: new Array(data.pages || 1).fill(0).map((z, i) => ({
259
+ url: getPagingUrl(i + 1, request.query.query),
260
+ title: i + 1,
261
+ active: i === data.page
262
+ })),
263
+
264
+ apps: data.apps
265
+ },
266
+ {
267
+ layout: 'app'
268
+ }
269
+ );
270
+ },
271
+
272
+ options: {
273
+ validate: {
274
+ options: {
275
+ stripUnknown: true,
276
+ abortEarly: false,
277
+ convert: true
278
+ },
279
+
280
+ async failAction(request, h /*, err*/) {
281
+ return h.redirect('/admin/config/oauth').takeover();
282
+ },
283
+
284
+ query: Joi.object({
285
+ page: Joi.number().integer().min(1).max(1000000).default(1),
286
+ pageSize: Joi.number().integer().min(1).max(250).default(DEFAULT_PAGE_SIZE),
287
+ query: Joi.string().example('Gmail').description('Filter accounts by search term').label('AppQuery')
288
+ })
289
+ }
290
+ }
291
+ });
292
+
293
+ // GET /admin/config/oauth/app/{app} - View OAuth application details
294
+ server.route({
295
+ method: 'GET',
296
+ path: '/admin/config/oauth/app/{app}',
297
+ async handler(request, h) {
298
+ let app = await oauth2Apps.get(request.params.app);
299
+ if (!app) {
300
+ let error = Boom.boomify(new Error('Application was not found.'), { statusCode: 404 });
301
+ throw error;
302
+ }
303
+
304
+ let providerData = oauth2ProviderData(app.provider);
305
+
306
+ let disabledScopes = {};
307
+ if (
308
+ (app.skipScopes && app.skipScopes.includes('SMTP.Send')) ||
309
+ (app.skipScopes && app.skipScopes.includes('https://outlook.office.com/SMTP.Send'))
310
+ ) {
311
+ disabledScopes.SMTP_Send = true;
312
+ }
313
+
314
+ if (
315
+ (app.skipScopes && app.skipScopes.includes('Mail.Send')) ||
316
+ (app.skipScopes && app.skipScopes.includes('https://graph.microsoft.com/Mail.Send'))
317
+ ) {
318
+ disabledScopes.Mail_Send = true;
319
+ }
320
+
321
+ if (
322
+ (app.skipScopes && app.skipScopes.includes('gmail.modify')) ||
323
+ (app.skipScopes && app.skipScopes.includes('https://www.googleapis.com/auth/gmail.modify'))
324
+ ) {
325
+ disabledScopes.Gmail_Modify = true;
326
+ }
327
+
328
+ // Detect send-only Gmail configuration
329
+ let isSendOnlyGmail = false;
330
+ if (app.provider === 'gmail' && app.baseScopes === 'api') {
331
+ // Build scope list from extraScopes (short form or full URLs)
332
+ const scopes = (app.extraScopes || []).map(scope => (scope.startsWith('https://') ? scope : `https://www.googleapis.com/auth/${scope}`));
333
+
334
+ // Use Account class helper to check scopes - no need to create full Account instance
335
+ const accountHelper = new Account({ redis, secret: await getSecret() });
336
+ const { hasSendScope, hasReadScope } = accountHelper.checkAccountScopes('gmail', scopes);
337
+
338
+ if (hasSendScope && !hasReadScope) {
339
+ isSendOnlyGmail = true;
340
+ }
341
+ }
342
+
343
+ if (app.pubSubApp) {
344
+ let pubSubApp = await oauth2Apps.get(app.pubSubApp);
345
+ app.pubSubAppData = pubSubApp;
346
+ }
347
+
348
+ if (app.cloud) {
349
+ app.cloudData = AZURE_CLOUDS.find(entry => entry.id === app.cloud);
350
+ }
351
+
352
+ return h.view(
353
+ 'config/oauth/app',
354
+ {
355
+ pageTitle: 'OAuth2',
356
+ menuConfig: true,
357
+ menuConfigOauth: true,
358
+
359
+ [`active${providerData.caseName}`]: true,
360
+
361
+ app,
362
+
363
+ baseScopesApi: app.baseScopes === 'api',
364
+ baseScopesImap: app.baseScopes === 'imap' || !app.baseScopes,
365
+ baseScopesPubsub: app.baseScopes === 'pubsub',
366
+
367
+ disabledScopes,
368
+ isSendOnlyGmail,
369
+
370
+ providerData
371
+ },
372
+ {
373
+ layout: 'app'
374
+ }
375
+ );
376
+ },
377
+
378
+ options: {
379
+ validate: {
380
+ options: {
381
+ stripUnknown: true,
382
+ abortEarly: false,
383
+ convert: true
384
+ },
385
+
386
+ async failAction(request, h /*, err*/) {
387
+ return h.redirect('/admin/config/oauth').takeover();
388
+ },
389
+
390
+ params: Joi.object({
391
+ app: Joi.string().empty('').max(255).example('gmail').label('Provider').required()
392
+ })
393
+ }
394
+ }
395
+ });
396
+
397
+ // POST /admin/config/oauth/delete - Delete OAuth application
398
+ server.route({
399
+ method: 'POST',
400
+ path: '/admin/config/oauth/delete',
401
+ async handler(request, h) {
402
+ try {
403
+ await oauth2Apps.del(request.payload.app);
404
+
405
+ await request.flash({ type: 'info', message: `OAuth2 app deleted` });
406
+
407
+ return h.redirect('/admin/config/oauth');
408
+ } catch (err) {
409
+ await request.flash({ type: 'danger', message: `Couldn't delete OAuth2 app. Try again.` });
410
+ request.logger.error({ msg: 'Failed to delete OAuth2 application', err, app: request.payload.app, remoteAddress: request.app.ip });
411
+ return h.redirect(`/admin/config/oauth/app/${request.payload.app}`);
412
+ }
413
+ },
414
+ options: {
415
+ validate: {
416
+ options: {
417
+ stripUnknown: true,
418
+ abortEarly: false,
419
+ convert: true
420
+ },
421
+
422
+ async failAction(request, h, err) {
423
+ await request.flash({ type: 'danger', message: `Couldn't delete OAuth2 app. Try again.` });
424
+ request.logger.error({ msg: 'Failed to delete delete the OAuth2 application', err });
425
+
426
+ return h.redirect('/admin/config/oauth').takeover();
427
+ },
428
+
429
+ payload: Joi.object({
430
+ app: Joi.string().empty('').max(255).example('gmail').label('Provider').required()
431
+ })
432
+ }
433
+ }
434
+ });
435
+
436
+ // GET /admin/config/oauth/new - New OAuth application form
437
+ server.route({
438
+ method: 'GET',
439
+ path: '/admin/config/oauth/new',
440
+ async handler(request, h) {
441
+ let { provider } = request.query;
442
+ let providerData = oauth2ProviderData(provider);
443
+
444
+ let serviceUrl = await settings.get('serviceUrl');
445
+ let defaultRedirectUrl = `${serviceUrl}/oauth`;
446
+ if (provider === 'outlook') {
447
+ defaultRedirectUrl = defaultRedirectUrl.replace(/^http:\/\/127\.0\.0\.1\b/i, 'http://localhost');
448
+ }
449
+
450
+ let pubSubApps = await oauth2Apps.list(0, 1000, { pubsub: true });
451
+
452
+ return h.view(
453
+ 'config/oauth/new',
454
+ {
455
+ pageTitle: 'OAuth2',
456
+ menuConfig: true,
457
+ menuConfigOauth: true,
458
+
459
+ actionCreate: true,
460
+
461
+ [`active${providerData.caseName}`]: true,
462
+ providerData,
463
+ defaultRedirectUrl,
464
+
465
+ baseScopesImap: true,
466
+ baseScopesApi: false,
467
+ baseScopesPubsub: false,
468
+
469
+ pubSubApps: pubSubApps && pubSubApps.apps,
470
+
471
+ azureClouds: structuredClone(AZURE_CLOUDS).map(entry => {
472
+ if (entry.id === 'global') {
473
+ entry.selected = true;
474
+ }
475
+ return entry;
476
+ }),
477
+
478
+ values: {
479
+ provider,
480
+ redirectUrl: defaultRedirectUrl
481
+ },
482
+
483
+ authorityCommon: true
484
+ },
485
+ {
486
+ layout: 'app'
487
+ }
488
+ );
489
+ },
490
+
491
+ options: {
492
+ validate: {
493
+ options: {
494
+ stripUnknown: true,
495
+ abortEarly: false,
496
+ convert: true
497
+ },
498
+
499
+ async failAction(request, h /*, err*/) {
500
+ return h.redirect('/admin/config/oauth').takeover();
501
+ },
502
+
503
+ query: Joi.object({
504
+ provider: Joi.string()
505
+ .empty('')
506
+ .valid(...Object.keys(OAUTH_PROVIDERS))
507
+ .label('Provider')
508
+ .required()
509
+ })
510
+ }
511
+ }
512
+ });
513
+
514
+ // POST /admin/config/oauth/new - Create OAuth application
515
+ server.route({
516
+ method: 'POST',
517
+ path: '/admin/config/oauth/new',
518
+ async handler(request, h) {
519
+ try {
520
+ let appData = Object.assign({}, request.payload);
521
+ appData.extraScopes = appData.extraScopes
522
+ .split(/\s+/)
523
+ .map(scope => scope.trim())
524
+ .filter(scope => scope);
525
+
526
+ appData.skipScopes = appData.skipScopes
527
+ .split(/\s+/)
528
+ .map(scope => scope.trim())
529
+ .filter(scope => scope);
530
+
531
+ if (appData.authority === 'tenant') {
532
+ appData.authority = appData.tenant;
533
+ }
534
+ delete appData.tenant;
535
+
536
+ let oauth2App = await oauth2Apps.create(appData);
537
+ if (!oauth2App || !oauth2App.id) {
538
+ throw new Error('Unexpected result');
539
+ }
540
+
541
+ if (oauth2App && oauth2App.pubsubUpdates && oauth2App.pubsubUpdates.pubSubSubscription) {
542
+ await call({ cmd: 'googlePubSub', app: oauth2App.id });
543
+ }
544
+
545
+ await request.flash({ type: 'success', message: `OAuth2 app created` });
546
+ return h.redirect(`/admin/config/oauth/app/${oauth2App.id}`);
547
+ } catch (err) {
548
+ await request.flash({ type: 'danger', message: `Couldn't register OAuth2 app. Try again.` });
549
+ request.logger.error({ msg: 'Failed to register OAuth2 app', err });
550
+
551
+ let { provider, baseScopes } = request.payload;
552
+ if (!provider || !OAUTH_PROVIDERS.hasOwnProperty(provider)) {
553
+ return h.redirect('/admin');
554
+ }
555
+
556
+ let providerData = oauth2ProviderData(provider);
557
+
558
+ let serviceUrl = await settings.get('serviceUrl');
559
+ let defaultRedirectUrl = `${serviceUrl}/oauth`;
560
+ if (provider === 'outlook') {
561
+ defaultRedirectUrl = defaultRedirectUrl.replace(/^http:\/\/127\.0\.0\.1\b/i, 'http://localhost');
562
+ }
563
+
564
+ let pubSubApps = await oauth2Apps.list(0, 1000, { pubsub: true });
565
+
566
+ return h.view(
567
+ 'config/oauth/new',
568
+ {
569
+ pageTitle: 'OAuth2',
570
+ menuConfig: true,
571
+ menuConfigOauth: true,
572
+
573
+ actionCreate: true,
574
+
575
+ [`active${providerData.caseName}`]: true,
576
+ providerData,
577
+ defaultRedirectUrl,
578
+
579
+ pubSubApps:
580
+ pubSubApps &&
581
+ pubSubApps.apps &&
582
+ pubSubApps.apps.map(app => {
583
+ if (app.id === request.payload.pubSubApp) {
584
+ app.selected = true;
585
+ }
586
+ return app;
587
+ }),
588
+
589
+ baseScopesApi: baseScopes === 'api',
590
+ baseScopesImap: baseScopes === 'imap' || !baseScopes,
591
+ baseScopesPubsub: baseScopes === 'pubsub',
592
+
593
+ azureClouds: structuredClone(AZURE_CLOUDS).map(entry => {
594
+ entry.selected = request.payload.cloud === entry.id;
595
+ return entry;
596
+ }),
597
+
598
+ authorityCommon: request.payload.authority === 'common',
599
+ authorityOrganizations: request.payload.authority === 'organizations',
600
+ authorityConsumers: request.payload.authority === 'consumers',
601
+ authorityTenant: request.payload.authority === 'tenant'
602
+ },
603
+ {
604
+ layout: 'app'
605
+ }
606
+ );
607
+ }
608
+ },
609
+ options: {
610
+ validate: {
611
+ options: {
612
+ stripUnknown: true,
613
+ abortEarly: false,
614
+ convert: true
615
+ },
616
+
617
+ async failAction(request, h, err) {
618
+ let errors = {};
619
+
620
+ if (err.details) {
621
+ err.details.forEach(detail => {
622
+ if (!errors[detail.path]) {
623
+ errors[detail.path] = detail.message;
624
+ }
625
+ });
626
+ }
627
+
628
+ await request.flash({ type: 'danger', message: `Couldn't register OAuth2 app. Try again.` });
629
+ request.logger.error({ msg: 'Failed to register OAuth2 app', err });
630
+
631
+ let { provider, baseScopes } = request.payload;
632
+ if (!provider || !OAUTH_PROVIDERS.hasOwnProperty(provider)) {
633
+ return h.redirect('/admin').takeover();
634
+ }
635
+
636
+ let providerData = oauth2ProviderData(provider);
637
+
638
+ let serviceUrl = await settings.get('serviceUrl');
639
+ let defaultRedirectUrl = `${serviceUrl}/oauth`;
640
+ if (provider === 'outlook') {
641
+ defaultRedirectUrl = defaultRedirectUrl.replace(/^http:\/\/127\.0\.0\.1\b/i, 'http://localhost');
642
+ }
643
+
644
+ let pubSubApps = await oauth2Apps.list(0, 1000, { pubsub: true });
645
+
646
+ return h
647
+ .view(
648
+ 'config/oauth/new',
649
+ {
650
+ pageTitle: 'OAuth2',
651
+ menuConfig: true,
652
+ menuConfigOauth: true,
653
+
654
+ actionCreate: true,
655
+
656
+ [`active${providerData.caseName}`]: true,
657
+ providerData,
658
+ defaultRedirectUrl,
659
+
660
+ pubSubApps:
661
+ pubSubApps &&
662
+ pubSubApps.apps &&
663
+ pubSubApps.apps.map(app => {
664
+ if (app.id === request.payload.pubSubApp) {
665
+ app.selected = true;
666
+ }
667
+ return app;
668
+ }),
669
+
670
+ baseScopesApi: baseScopes === 'api',
671
+ baseScopesImap: baseScopes === 'imap' || !baseScopes,
672
+ baseScopesPubsub: baseScopes === 'pubsub',
673
+
674
+ azureClouds: structuredClone(AZURE_CLOUDS).map(entry => {
675
+ entry.selected = request.payload.cloud === entry.id;
676
+ return entry;
677
+ }),
678
+
679
+ authorityCommon: request.payload.authority === 'common',
680
+ authorityOrganizations: request.payload.authority === 'organizations',
681
+ authorityConsumers: request.payload.authority === 'consumers',
682
+ authorityTenant: request.payload.authority === 'tenant',
683
+
684
+ errors
685
+ },
686
+ {
687
+ layout: 'app'
688
+ }
689
+ )
690
+ .takeover();
691
+ },
692
+
693
+ payload: Joi.object(oauthCreateSchema).tailor('web')
694
+ }
695
+ }
696
+ });
697
+
698
+ // GET /admin/config/oauth/edit/{app} - Edit OAuth application form
699
+ server.route({
700
+ method: 'GET',
701
+ path: '/admin/config/oauth/edit/{app}',
702
+ async handler(request, h) {
703
+ let appData = await oauth2Apps.get(request.params.app);
704
+ if (!appData) {
705
+ let error = Boom.boomify(new Error('Application was not found.'), { statusCode: 404 });
706
+ throw error;
707
+ }
708
+
709
+ let providerData = oauth2ProviderData(appData.provider, appData.cloud);
710
+ let serviceUrl = await settings.get('serviceUrl');
711
+ let defaultRedirectUrl = `${serviceUrl}/oauth`;
712
+ if (providerData.provider === 'outlook') {
713
+ defaultRedirectUrl = defaultRedirectUrl.replace(/^http:\/\/127\.0\.0\.1\b/i, 'http://localhost');
714
+ }
715
+
716
+ let values = Object.assign({}, appData, {
717
+ clientSecret: '',
718
+ serviceKey: '',
719
+ extraScopes: [].concat(appData.extraScopes || []).join('\n'),
720
+ skipScopes: [].concat(appData.skipScopes || []).join('\n'),
721
+
722
+ tenant: appData.authority && !['common', 'organizations', 'consumers'].includes(appData.authority) ? appData.authority : ''
723
+ });
724
+
725
+ let pubSubApps = await oauth2Apps.list(0, 1000, { pubsub: true });
726
+
727
+ return h.view(
728
+ 'config/oauth/edit',
729
+ {
730
+ pageTitle: 'OAuth2',
731
+ menuConfig: true,
732
+ menuConfigOauth: true,
733
+
734
+ [`active${providerData.caseName}`]: true,
735
+ providerData,
736
+ defaultRedirectUrl,
737
+
738
+ appData,
739
+
740
+ hasClientSecret: !!appData.clientSecret,
741
+ hasServiceKey: !!appData.serviceKey,
742
+
743
+ pubSubApps:
744
+ pubSubApps &&
745
+ pubSubApps.apps &&
746
+ pubSubApps.apps.map(app => {
747
+ if (app.id === values.pubSubApp) {
748
+ app.selected = true;
749
+ }
750
+ return app;
751
+ }),
752
+
753
+ values,
754
+
755
+ baseScopesApi: values.baseScopes === 'api',
756
+ baseScopesImap: values.baseScopes === 'imap' || !values.baseScopes,
757
+ baseScopesPubsub: values.baseScopes === 'pubsub',
758
+
759
+ azureClouds: structuredClone(AZURE_CLOUDS).map(entry => {
760
+ entry.selected = values.cloud === entry.id;
761
+ return entry;
762
+ }),
763
+
764
+ authorityCommon: values.authority === 'common',
765
+ authorityOrganizations: values.authority === 'organizations',
766
+ authorityConsumers: values.authority === 'consumers',
767
+ authorityTenant: !!values.tenant
768
+ },
769
+ {
770
+ layout: 'app'
771
+ }
772
+ );
773
+ },
774
+
775
+ options: {
776
+ validate: {
777
+ options: {
778
+ stripUnknown: true,
779
+ abortEarly: false,
780
+ convert: true
781
+ },
782
+
783
+ async failAction(request, h /*, err*/) {
784
+ return h.redirect('/admin/config/oauth').takeover();
785
+ },
786
+
787
+ params: Joi.object({
788
+ app: Joi.string().empty('').max(255).example('gmail').label('Provider').required()
789
+ })
790
+ }
791
+ }
792
+ });
793
+
794
+ // POST /admin/config/oauth/edit - Update OAuth application
795
+ server.route({
796
+ method: 'POST',
797
+ path: '/admin/config/oauth/edit',
798
+ async handler(request, h) {
799
+ let appData = await oauth2Apps.get(request.payload.app);
800
+ if (!appData) {
801
+ let error = Boom.boomify(new Error('Application was not found.'), { statusCode: 404 });
802
+ throw error;
803
+ }
804
+
805
+ try {
806
+ let updates = Object.assign({}, request.payload);
807
+ updates.extraScopes = updates.extraScopes
808
+ .split(/\s+/)
809
+ .map(scope => scope.trim())
810
+ .filter(scope => scope);
811
+
812
+ updates.skipScopes = updates.skipScopes
813
+ .split(/\s+/)
814
+ .map(scope => scope.trim())
815
+ .filter(scope => scope);
816
+
817
+ if (updates.authority === 'tenant') {
818
+ updates.authority = updates.tenant;
819
+ }
820
+ delete updates.tenant;
821
+
822
+ let oauth2App = await oauth2Apps.update(appData.id, updates);
823
+ if (!oauth2App || !oauth2App.id) {
824
+ throw new Error('Unexpected result');
825
+ }
826
+
827
+ if (oauth2App && oauth2App.pubsubUpdates && oauth2App.pubsubUpdates.pubSubSubscription) {
828
+ await call({ cmd: 'googlePubSub', app: oauth2App.id });
829
+ }
830
+
831
+ await request.flash({ type: 'success', message: `OAuth2 app saved` });
832
+ return h.redirect(`/admin/config/oauth/app/${oauth2App.id}`);
833
+ } catch (err) {
834
+ await request.flash({ type: 'danger', message: `Couldn't save OAuth2 app. Try again.` });
835
+ request.logger.error({ msg: 'Failed to update OAuth2 app', app: request.payload.app, err });
836
+
837
+ let providerData = oauth2ProviderData(appData.provider, appData.cloud);
838
+
839
+ let serviceUrl = await settings.get('serviceUrl');
840
+ let defaultRedirectUrl = `${serviceUrl}/oauth`;
841
+ if (appData.provider === 'outlook') {
842
+ defaultRedirectUrl = defaultRedirectUrl.replace(/^http:\/\/127\.0\.0\.1\b/i, 'http://localhost');
843
+ }
844
+
845
+ let pubSubApps = await oauth2Apps.list(0, 1000, { pubsub: true });
846
+
847
+ return h.view(
848
+ 'config/oauth/edit',
849
+ {
850
+ pageTitle: 'OAuth2',
851
+ menuConfig: true,
852
+ menuConfigOauth: true,
853
+
854
+ [`active${providerData.caseName}`]: true,
855
+ providerData,
856
+ defaultRedirectUrl,
857
+ appData,
858
+
859
+ hasClientSecret: !!appData.clientSecret,
860
+ hasServiceKey: !!appData.serviceKey,
861
+
862
+ pubSubApps:
863
+ pubSubApps &&
864
+ pubSubApps.apps &&
865
+ pubSubApps.apps.map(app => {
866
+ if (app.id === request.payload.pubSubApp) {
867
+ app.selected = true;
868
+ }
869
+ return app;
870
+ }),
871
+
872
+ baseScopesApi: request.payload.baseScopes === 'api',
873
+ baseScopesImap: request.payload.baseScopes === 'imap' || !request.payload.baseScopes,
874
+ baseScopesPubsub: request.payload.baseScopes === 'pubsub',
875
+
876
+ azureClouds: structuredClone(AZURE_CLOUDS).map(entry => {
877
+ entry.selected = request.payload.cloud === entry.id;
878
+ return entry;
879
+ }),
880
+
881
+ authorityCommon: request.payload.authority === 'common',
882
+ authorityOrganizations: request.payload.authority === 'organizations',
883
+ authorityConsumers: request.payload.authority === 'consumers',
884
+ authorityTenant: request.payload.authority === 'tenant'
885
+ },
886
+ {
887
+ layout: 'app'
888
+ }
889
+ );
890
+ }
891
+ },
892
+ options: {
893
+ validate: {
894
+ options: {
895
+ stripUnknown: true,
896
+ abortEarly: false,
897
+ convert: true
898
+ },
899
+
900
+ async failAction(request, h, err) {
901
+ let errors = {};
902
+
903
+ if (err.details) {
904
+ err.details.forEach(detail => {
905
+ if (!errors[detail.path]) {
906
+ errors[detail.path] = detail.message;
907
+ }
908
+ });
909
+ }
910
+
911
+ let appData = await oauth2Apps.get(request.payload.app);
912
+ if (!appData) {
913
+ await request.flash({ type: 'danger', message: `Application not found` });
914
+ request.logger.error({ msg: 'Application was not found.', app: request.payload.app });
915
+ return h.redirect('/admin').takeover();
916
+ }
917
+
918
+ await request.flash({ type: 'danger', message: `Couldn't save OAuth2 app. Try again.` });
919
+ request.logger.error({ msg: 'Failed to update OAuth2 app', err });
920
+
921
+ let { provider } = request.payload;
922
+ if (!provider || !OAUTH_PROVIDERS.hasOwnProperty(provider)) {
923
+ return h.redirect('/admin').takeover();
924
+ }
925
+
926
+ let providerData = oauth2ProviderData(provider);
927
+
928
+ let serviceUrl = await settings.get('serviceUrl');
929
+ let defaultRedirectUrl = `${serviceUrl}/oauth`;
930
+ if (provider === 'outlook') {
931
+ defaultRedirectUrl = defaultRedirectUrl.replace(/^http:\/\/127\.0\.0\.1\b/i, 'http://localhost');
932
+ }
933
+
934
+ let pubSubApps = await oauth2Apps.list(0, 1000, { pubsub: true });
935
+
936
+ return h
937
+ .view(
938
+ 'config/oauth/edit',
939
+ {
940
+ pageTitle: 'OAuth2',
941
+ menuConfig: true,
942
+ menuConfigOauth: true,
943
+
944
+ [`active${providerData.caseName}`]: true,
945
+ providerData,
946
+ defaultRedirectUrl,
947
+
948
+ appData,
949
+
950
+ hasClientSecret: !!appData.clientSecret,
951
+ hasServiceKey: !!appData.serviceKey,
952
+
953
+ pubSubApps:
954
+ pubSubApps &&
955
+ pubSubApps.apps &&
956
+ pubSubApps.apps.map(app => {
957
+ if (app.id === request.payload.pubSubApp) {
958
+ app.selected = true;
959
+ }
960
+ return app;
961
+ }),
962
+
963
+ baseScopesApi: request.payload.baseScopes === 'api',
964
+ baseScopesImap: request.payload.baseScopes === 'imap' || !request.payload.baseScopes,
965
+ baseScopesPubsub: request.payload.baseScopes === 'pubsub',
966
+
967
+ azureClouds: structuredClone(AZURE_CLOUDS).map(entry => {
968
+ entry.selected = request.payload.cloud === entry.id;
969
+ return entry;
970
+ }),
971
+
972
+ authorityCommon: request.payload.authority === 'common',
973
+ authorityOrganizations: request.payload.authority === 'organizations',
974
+ authorityConsumers: request.payload.authority === 'consumers',
975
+ authorityTenant: request.payload.authority === 'tenant',
976
+
977
+ errors
978
+ },
979
+ {
980
+ layout: 'app'
981
+ }
982
+ )
983
+ .takeover();
984
+ },
985
+
986
+ payload: Joi.object(oauthUpdateSchema)
987
+ }
988
+ }
989
+ });
990
+ }
991
+
992
+ module.exports = init;