emailengine-app 2.68.1 → 2.70.0

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 (95) hide show
  1. package/.github/workflows/deploy.yml +8 -3
  2. package/.github/workflows/release.yaml +6 -0
  3. package/CHANGELOG.md +59 -0
  4. package/Gruntfile.js +3 -1
  5. package/config/default.toml +2 -0
  6. package/data/google-crawlers.json +7 -1
  7. package/getswagger.sh +40 -4
  8. package/gettext-extract.js +163 -0
  9. package/lib/account.js +135 -72
  10. package/lib/api-routes/account-routes.js +684 -106
  11. package/lib/api-routes/blocklist-routes.js +344 -0
  12. package/lib/api-routes/chat-routes.js +32 -14
  13. package/lib/api-routes/delivery-test-routes.js +346 -0
  14. package/lib/api-routes/export-routes.js +28 -14
  15. package/lib/api-routes/gateway-routes.js +427 -0
  16. package/lib/api-routes/license-routes.js +156 -0
  17. package/lib/api-routes/mailbox-routes.js +344 -0
  18. package/lib/api-routes/message-routes.js +221 -187
  19. package/lib/api-routes/oauth2-app-routes.js +697 -0
  20. package/lib/api-routes/outbox-routes.js +185 -0
  21. package/lib/api-routes/pubsub-routes.js +102 -0
  22. package/lib/api-routes/route-helpers.js +58 -0
  23. package/lib/api-routes/settings-routes.js +357 -0
  24. package/lib/api-routes/stats-routes.js +111 -0
  25. package/lib/api-routes/submit-routes.js +461 -0
  26. package/lib/api-routes/template-routes.js +60 -75
  27. package/lib/api-routes/token-routes.js +297 -0
  28. package/lib/api-routes/webhook-route-routes.js +181 -0
  29. package/lib/autodetect-imap-settings.js +0 -2
  30. package/lib/consts.js +5 -0
  31. package/lib/email-client/base-client.js +28 -6
  32. package/lib/email-client/gmail-client.js +133 -112
  33. package/lib/email-client/imap/mailbox.js +34 -11
  34. package/lib/email-client/imap/subconnection.js +20 -13
  35. package/lib/email-client/imap/sync-operations.js +131 -3
  36. package/lib/email-client/imap-client.js +152 -75
  37. package/lib/email-client/notification-handler.js +1 -4
  38. package/lib/email-client/outlook-client.js +134 -75
  39. package/lib/export.js +97 -20
  40. package/lib/feature-flags.js +2 -2
  41. package/lib/gateway.js +4 -9
  42. package/lib/get-raw-email.js +5 -5
  43. package/lib/imapproxy/imap-core/lib/commands/starttls.js +18 -0
  44. package/lib/imapproxy/imap-core/lib/imap-command.js +6 -1
  45. package/lib/imapproxy/imap-core/lib/imap-connection.js +106 -24
  46. package/lib/imapproxy/imap-core/lib/imap-server.js +24 -0
  47. package/lib/imapproxy/imap-core/lib/imap-stream.js +26 -0
  48. package/lib/logger.js +24 -21
  49. package/lib/message-port-stream.js +113 -16
  50. package/lib/metrics-collector.js +0 -2
  51. package/lib/oauth2-apps.js +13 -4
  52. package/lib/outbox.js +24 -40
  53. package/lib/redis-operations.js +1 -1
  54. package/lib/reject-worker-calls.js +42 -0
  55. package/lib/routes-ui.js +37 -8778
  56. package/lib/schemas.js +429 -84
  57. package/lib/sentry.js +139 -0
  58. package/lib/settings.js +9 -3
  59. package/lib/stream-encrypt.js +1 -1
  60. package/lib/templates.js +1 -1
  61. package/lib/tokens.js +5 -3
  62. package/lib/tools.js +70 -4
  63. package/lib/ui-routes/account-routes.js +45 -212
  64. package/lib/ui-routes/admin-config-routes.js +928 -489
  65. package/lib/ui-routes/admin-entities-routes.js +1 -0
  66. package/lib/ui-routes/auth-routes.js +1339 -0
  67. package/lib/ui-routes/dashboard-routes.js +188 -0
  68. package/lib/ui-routes/document-store-routes.js +800 -0
  69. package/lib/ui-routes/export-routes.js +217 -0
  70. package/lib/ui-routes/internals-routes.js +354 -0
  71. package/lib/ui-routes/network-config-routes.js +759 -0
  72. package/lib/ui-routes/{oauth-routes.js → oauth-config-routes.js} +369 -91
  73. package/lib/ui-routes/route-helpers.js +314 -0
  74. package/lib/ui-routes/smtp-test-routes.js +236 -0
  75. package/lib/ui-routes/unsubscribe-routes.js +232 -0
  76. package/lib/webhook-request.js +36 -0
  77. package/lib/webhooks.js +8 -4
  78. package/package.json +13 -12
  79. package/sbom.json +1 -1
  80. package/server.js +222 -39
  81. package/static/licenses.html +160 -300
  82. package/translations/messages.pot +112 -132
  83. package/update-info.sh +19 -1
  84. package/views/config/logging.hbs +48 -0
  85. package/views/dashboard.hbs +7 -26
  86. package/views/internals/index.hbs +15 -0
  87. package/views/tokens/index.hbs +9 -0
  88. package/workers/api.js +200 -4424
  89. package/workers/documents.js +2 -22
  90. package/workers/export.js +103 -104
  91. package/workers/imap-proxy.js +3 -23
  92. package/workers/imap.js +32 -36
  93. package/workers/smtp.js +2 -22
  94. package/workers/submit.js +26 -35
  95. package/workers/webhooks.js +9 -43
@@ -1,26 +1,27 @@
1
1
  'use strict';
2
2
 
3
- const Boom = require('@hapi/boom');
3
+ // Admin UI routes for OAuth2 application config (/admin/config/oauth*): listing apps,
4
+ // per-app view/edit/delete, creating apps, adding accounts, provider subscriptions, and
5
+ // app verification. Extracted verbatim from lib/routes-ui.js. AZURE_CLOUDS and the
6
+ // authMethodContext/getPubSubAppsForSelect render helpers move with the routes (only the
7
+ // OAuth app config pages use them).
8
+
4
9
  const Joi = require('joi');
10
+ const util = require('util');
11
+ const Boom = require('@hapi/boom');
5
12
 
6
13
  const settings = require('../settings');
14
+ const consts = require('../consts');
7
15
  const { redis } = require('../db');
8
- const { oauth2Apps, OAUTH_PROVIDERS, SERVICE_ACCOUNT_PROVIDERS, oauth2ProviderData } = require('../oauth2-apps');
9
16
  const getSecret = require('../get-secret');
10
17
  const { Account } = require('../account');
11
- const { oauthCreateSchema, oauthUpdateSchema } = require('../schemas');
12
- const consts = require('../consts');
18
+ const { oauth2Apps, OAUTH_PROVIDERS, oauth2ProviderData, SERVICE_ACCOUNT_PROVIDERS } = require('../oauth2-apps');
19
+ const { verifyOAuth2App } = require('../oauth/verify-app');
20
+ const { oauthCreateSchema, oauthUpdateSchema, accountIdSchema } = require('../schemas');
21
+ const { throwAsBoom } = require('./route-helpers');
13
22
 
14
23
  const { DEFAULT_PAGE_SIZE } = consts;
15
24
 
16
- // Microsoft Entra requires 'localhost' rather than '127.0.0.1' in redirect URIs
17
- function normalizeOutlookRedirectUrl(redirectUrl, provider) {
18
- if (provider === 'outlook') {
19
- return redirectUrl.replace(/^http:\/\/127\.0\.0\.1\b/i, 'http://localhost');
20
- }
21
- return redirectUrl;
22
- }
23
-
24
25
  const AZURE_CLOUDS = [
25
26
  {
26
27
  id: 'global',
@@ -47,8 +48,35 @@ const AZURE_CLOUDS = [
47
48
  }
48
49
  ];
49
50
 
50
- function init({ server, call }) {
51
- // GET /admin/config/oauth - OAuth applications list
51
+ function init(args) {
52
+ const { server, call } = args;
53
+
54
+ // Render-context booleans for the gmailService authMethod tab selector.
55
+ // When `locked` is set the selector is shown but cannot be switched - the
56
+ // authentication method is fixed once an app has been created.
57
+ function authMethodContext(authMethod, locked) {
58
+ return {
59
+ authMethodIsServiceKey: !authMethod || authMethod === 'serviceKey',
60
+ authMethodIsExternalAccount: authMethod === 'externalAccount',
61
+ authMethodLocked: !!locked
62
+ };
63
+ }
64
+
65
+ /**
66
+ * Fetch the list of Pub/Sub apps and mark the one matching selectedId as selected.
67
+ * Returns the apps array ready for template rendering.
68
+ */
69
+ async function getPubSubAppsForSelect(selectedId) {
70
+ let result = await oauth2Apps.list(0, 1000, { pubsub: true });
71
+ let apps = (result && result.apps) || [];
72
+ for (let app of apps) {
73
+ if (app.id === selectedId) {
74
+ app.selected = true;
75
+ }
76
+ }
77
+ return apps;
78
+ }
79
+
52
80
  server.route({
53
81
  method: 'GET',
54
82
  path: '/admin/config/oauth',
@@ -108,11 +136,12 @@ function init({ server, call }) {
108
136
  pageTitle: 'OAuth2',
109
137
  menuConfig: true,
110
138
  menuConfigOauth: true,
139
+ activeApplications: true,
111
140
 
112
141
  newLink: newLink.pathname + newLink.search,
113
142
 
114
143
  searchTarget: '/admin/config/oauth',
115
- searchPlaceholder: 'Search for OAuth2 applications...',
144
+ searchPlaceholder: 'Search for OAuth2 applications',
116
145
  query: request.query.query,
117
146
 
118
147
  showPaging: data.pages > 1,
@@ -154,7 +183,160 @@ function init({ server, call }) {
154
183
  }
155
184
  });
156
185
 
157
- // GET /admin/config/oauth/app/{app} - View OAuth application details
186
+ // GET /admin/config/oauth/subscriptions - Gmail Pub/Sub subscriptions list
187
+ server.route({
188
+ method: 'GET',
189
+ path: '/admin/config/oauth/subscriptions',
190
+ async handler(request, h) {
191
+ try {
192
+ let data = await oauth2Apps.list(request.query.page - 1, request.query.pageSize, { pubsub: true });
193
+
194
+ let gmailSubscriptionTtl = await settings.get('gmailSubscriptionTtl');
195
+
196
+ // Compute human-readable expiration for each app
197
+ // meta.subscriptionExpiration is:
198
+ // undefined - no data yet (app predates this feature or ensurePubsub hasn't run)
199
+ // null - indefinite (no TTL set, ensurePubsub confirmed this)
200
+ // "Ns" - TTL in seconds (e.g. "2678400s" for 31 days)
201
+ let gt = request.app.gt;
202
+ for (let app of data.apps) {
203
+ if (!app.pubSubSubscription) {
204
+ app.expirationLabel = '';
205
+ continue;
206
+ }
207
+
208
+ let meta = app.meta || {};
209
+ if (!('subscriptionExpiration' in meta)) {
210
+ app.expirationLabel = gt.gettext('Unknown');
211
+ continue;
212
+ }
213
+
214
+ let seconds = parseInt(meta.subscriptionExpiration, 10);
215
+ if (seconds > 0) {
216
+ let days = Math.round(seconds / 86400);
217
+ app.expirationLabel = util.format(gt.ngettext('%d day', '%d days', days), days);
218
+ } else {
219
+ app.expirationLabel = gt.gettext('Indefinite');
220
+ }
221
+ }
222
+
223
+ let nextPage = false;
224
+ let prevPage = false;
225
+
226
+ let getPagingUrl = page => {
227
+ let url = new URL(`admin/config/oauth/subscriptions`, 'http://localhost');
228
+
229
+ if (page) {
230
+ url.searchParams.append('page', page);
231
+ }
232
+
233
+ if (request.query.pageSize !== DEFAULT_PAGE_SIZE) {
234
+ url.searchParams.append('pageSize', request.query.pageSize);
235
+ }
236
+
237
+ return url.pathname + url.search;
238
+ };
239
+
240
+ if (data.pages > data.page + 1) {
241
+ nextPage = getPagingUrl(data.page + 2);
242
+ }
243
+
244
+ if (data.page > 0) {
245
+ prevPage = getPagingUrl(data.page);
246
+ }
247
+
248
+ return h.view(
249
+ 'config/oauth/subscriptions',
250
+ {
251
+ pageTitle: 'OAuth2',
252
+ menuConfig: true,
253
+ menuConfigOauth: true,
254
+ activeSubscriptions: true,
255
+
256
+ showPaging: data.pages > 1,
257
+ nextPage,
258
+ prevPage,
259
+ firstPage: data.page === 0,
260
+ pageLinks: new Array(data.pages || 1).fill(0).map((z, i) => ({
261
+ url: getPagingUrl(i + 1),
262
+ title: i + 1,
263
+ active: i === data.page
264
+ })),
265
+
266
+ apps: data.apps,
267
+
268
+ values: {
269
+ gmailSubscriptionTtl: typeof gmailSubscriptionTtl === 'number' ? gmailSubscriptionTtl : ''
270
+ }
271
+ },
272
+ {
273
+ layout: 'app'
274
+ }
275
+ );
276
+ } catch (err) {
277
+ request.logger.error({ msg: 'Failed to load subscriptions page', err });
278
+ throwAsBoom(err);
279
+ }
280
+ },
281
+
282
+ options: {
283
+ validate: {
284
+ options: {
285
+ stripUnknown: true,
286
+ abortEarly: false,
287
+ convert: true
288
+ },
289
+
290
+ async failAction(request, h /*, err*/) {
291
+ return h.redirect('/admin/config/oauth/subscriptions').takeover();
292
+ },
293
+
294
+ query: Joi.object({
295
+ page: Joi.number().integer().min(1).max(1000000).default(1),
296
+ pageSize: Joi.number().integer().min(1).max(250).default(DEFAULT_PAGE_SIZE)
297
+ })
298
+ }
299
+ }
300
+ });
301
+
302
+ server.route({
303
+ method: 'POST',
304
+ path: '/admin/config/oauth/subscriptions',
305
+ async handler(request, h) {
306
+ try {
307
+ // Joi .empty('').allow(null) ensures this is either null or a number
308
+ let ttl = request.payload.gmailSubscriptionTtl != null ? request.payload.gmailSubscriptionTtl : null;
309
+ await settings.set('gmailSubscriptionTtl', ttl);
310
+
311
+ await request.flash({ type: 'info', message: 'Configuration updated' });
312
+ return h.redirect('/admin/config/oauth/subscriptions');
313
+ } catch (err) {
314
+ await request.flash({ type: 'danger', message: 'Failed to save settings' });
315
+ request.logger.error({ msg: 'Failed to save subscription settings', err });
316
+ return h.redirect('/admin/config/oauth/subscriptions');
317
+ }
318
+ },
319
+ options: {
320
+ validate: {
321
+ options: {
322
+ stripUnknown: true,
323
+ abortEarly: false,
324
+ convert: true
325
+ },
326
+
327
+ async failAction(request, h /*, err*/) {
328
+ await request.flash({ type: 'danger', message: 'Invalid setting value' });
329
+ return h.redirect('/admin/config/oauth/subscriptions').takeover();
330
+ },
331
+
332
+ payload: Joi.object({
333
+ gmailSubscriptionTtl: Joi.number().integer().empty('').allow(null).min(0).max(365),
334
+ crumb: Joi.string().max(256)
335
+ })
336
+ }
337
+ }
338
+ });
339
+
158
340
  server.route({
159
341
  method: 'GET',
160
342
  path: '/admin/config/oauth/app/{app}',
@@ -213,8 +395,10 @@ function init({ server, call }) {
213
395
  app.cloudData = AZURE_CLOUDS.find(entry => entry.id === app.cloud);
214
396
  }
215
397
 
216
- let isServiceAccount = SERVICE_ACCOUNT_PROVIDERS.has(app.provider);
217
- let serviceUrl = (await settings.get('serviceUrl')) || '';
398
+ // Service account apps scoped for email access (IMAP/SMTP or Gmail/Graph API) can register
399
+ // accounts directly without an interactive consent flow. Pub/Sub-scoped service apps are for
400
+ // webhook notifications only, so they must not offer the direct add-account shortcut.
401
+ let canAddServiceAccount = SERVICE_ACCOUNT_PROVIDERS.has(app.provider) && app.enabled && app.baseScopes !== 'pubsub';
218
402
 
219
403
  return h.view(
220
404
  'config/oauth/app',
@@ -231,10 +415,13 @@ function init({ server, call }) {
231
415
  baseScopesImap: app.baseScopes === 'imap' || !app.baseScopes,
232
416
  baseScopesPubsub: app.baseScopes === 'pubsub',
233
417
 
418
+ appShowAuthMethod: app.provider === 'gmailService',
419
+ authMethodIsExternalAccount: app.authMethod === 'externalAccount',
420
+
421
+ canAddServiceAccount,
422
+
234
423
  disabledScopes,
235
424
  isSendOnlyGmail,
236
- isServiceAccount,
237
- serviceUrl,
238
425
 
239
426
  providerData
240
427
  },
@@ -263,7 +450,6 @@ function init({ server, call }) {
263
450
  }
264
451
  });
265
452
 
266
- // POST /admin/config/oauth/delete - Delete OAuth application
267
453
  server.route({
268
454
  method: 'POST',
269
455
  path: '/admin/config/oauth/delete',
@@ -308,7 +494,119 @@ function init({ server, call }) {
308
494
  }
309
495
  });
310
496
 
311
- // GET /admin/config/oauth/new - New OAuth application form
497
+ server.route({
498
+ method: 'POST',
499
+ path: '/admin/config/oauth/app/{app}/add-account',
500
+ async handler(request, h) {
501
+ const appId = request.params.app;
502
+ try {
503
+ const app = await oauth2Apps.get(appId);
504
+ if (!app) {
505
+ let error = Boom.boomify(new Error('Application was not found.'), { statusCode: 404 });
506
+ throw error;
507
+ }
508
+
509
+ // Direct account registration is only valid for email-scoped service account apps. Interactive
510
+ // providers must use the hosted consent flow, and Pub/Sub-scoped apps grant no mailbox access.
511
+ if (!SERVICE_ACCOUNT_PROVIDERS.has(app.provider) || !app.enabled || app.baseScopes === 'pubsub') {
512
+ let error = Boom.boomify(new Error('This application can not register accounts directly.'), { statusCode: 400 });
513
+ throw error;
514
+ }
515
+
516
+ let accountData = {
517
+ account: request.payload.account || null,
518
+ email: request.payload.email,
519
+ oauth2: {
520
+ provider: app.id,
521
+ auth: {
522
+ user: request.payload.email
523
+ }
524
+ }
525
+ };
526
+
527
+ if (request.payload.name) {
528
+ accountData.name = request.payload.name;
529
+ }
530
+
531
+ const accountObject = new Account({ redis, call, secret: await getSecret() });
532
+ const result = await accountObject.create(accountData);
533
+
534
+ await request.flash({ type: 'info', message: `Account ${result.state === 'existing' ? 'updated' : 'added'}` });
535
+
536
+ return h.redirect(`/admin/accounts/${result.account}`);
537
+ } catch (err) {
538
+ request.logger.error({ msg: 'Failed to register service account', err, app: appId, remoteAddress: request.app.ip });
539
+ await request.flash({ type: 'danger', message: `Failed to add account${err.message ? `: ${err.message}` : ''}` });
540
+ return h.redirect(`/admin/config/oauth/app/${appId}`);
541
+ }
542
+ },
543
+ options: {
544
+ validate: {
545
+ options: {
546
+ stripUnknown: true,
547
+ abortEarly: false,
548
+ convert: true
549
+ },
550
+
551
+ async failAction(request, h, err) {
552
+ request.logger.error({ msg: 'Failed to register service account', err, app: request.params.app });
553
+ await request.flash({ type: 'danger', message: `Failed to add account. Provide a valid email address.` });
554
+ return h.redirect(`/admin/config/oauth/app/${request.params.app}`).takeover();
555
+ },
556
+
557
+ params: Joi.object({
558
+ app: Joi.string().empty('').max(255).example('gmail').label('Provider').required()
559
+ }),
560
+
561
+ payload: Joi.object({
562
+ account: accountIdSchema.default(null),
563
+ name: Joi.string().empty('').max(256).example('John Smith').description('Account Name'),
564
+ email: Joi.string().email().required().example('user@example.com').label('Email').description('Mailbox email address')
565
+ })
566
+ }
567
+ }
568
+ });
569
+
570
+ server.route({
571
+ method: 'POST',
572
+ path: '/admin/config/oauth/verify/{app}',
573
+ async handler(request, h) {
574
+ try {
575
+ return await verifyOAuth2App(request.params.app, {
576
+ account: request.payload.account,
577
+ testConnection: request.payload.testConnection
578
+ });
579
+ } catch (err) {
580
+ request.logger.error({ msg: 'Failed to verify OAuth2 application', err, app: request.params.app });
581
+ return h.response({ error: err.message, code: err.code || null }).code(err.statusCode || 500);
582
+ }
583
+ },
584
+ options: {
585
+ validate: {
586
+ options: {
587
+ stripUnknown: true,
588
+ abortEarly: false,
589
+ convert: true
590
+ },
591
+
592
+ async failAction(request, h, err) {
593
+ request.logger.error({ msg: 'Invalid verify request', err, app: request.params.app });
594
+ return h.response({ error: 'Invalid request' }).code(400).takeover();
595
+ },
596
+
597
+ params: Joi.object({
598
+ app: Joi.string().empty('').max(255).required().label('Provider')
599
+ }),
600
+
601
+ payload: Joi.object({
602
+ crumb: Joi.string().optional(),
603
+ account: Joi.string().trim().empty('').max(256).optional(),
604
+ testConnection: Joi.boolean().truthy('Y', 'true', '1', 'on').falsy('N', 'false', 0, '').default(true)
605
+ })
606
+ }
607
+ }
608
+ });
609
+
312
610
  server.route({
313
611
  method: 'GET',
314
612
  path: '/admin/config/oauth/new',
@@ -317,9 +615,10 @@ function init({ server, call }) {
317
615
  let providerData = oauth2ProviderData(provider);
318
616
 
319
617
  let serviceUrl = await settings.get('serviceUrl');
320
- let defaultRedirectUrl = normalizeOutlookRedirectUrl(`${serviceUrl}/oauth`, provider);
321
-
322
- let pubSubApps = await oauth2Apps.list(0, 1000, { pubsub: true });
618
+ let defaultRedirectUrl = `${serviceUrl}/oauth`;
619
+ if (provider === 'outlook') {
620
+ defaultRedirectUrl = defaultRedirectUrl.replace(/^http:\/\/127\.0\.0\.1\b/i, 'http://localhost');
621
+ }
323
622
 
324
623
  return h.view(
325
624
  'config/oauth/new',
@@ -338,7 +637,9 @@ function init({ server, call }) {
338
637
  baseScopesApi: false,
339
638
  baseScopesPubsub: false,
340
639
 
341
- pubSubApps: pubSubApps && pubSubApps.apps,
640
+ ...authMethodContext('serviceKey'),
641
+
642
+ pubSubApps: await getPubSubAppsForSelect(null),
342
643
 
343
644
  azureClouds: structuredClone(AZURE_CLOUDS).map(entry => {
344
645
  if (entry.id === 'global') {
@@ -349,10 +650,11 @@ function init({ server, call }) {
349
650
 
350
651
  values: {
351
652
  provider,
352
- ...(provider !== 'outlookService' ? { redirectUrl: defaultRedirectUrl } : {})
653
+ redirectUrl: defaultRedirectUrl,
654
+ authMethod: 'serviceKey'
353
655
  },
354
656
 
355
- authorityCommon: provider !== 'outlookService'
657
+ authorityCommon: true
356
658
  },
357
659
  {
358
660
  layout: 'app'
@@ -383,7 +685,6 @@ function init({ server, call }) {
383
685
  }
384
686
  });
385
687
 
386
- // POST /admin/config/oauth/new - Create OAuth application
387
688
  server.route({
388
689
  method: 'POST',
389
690
  path: '/admin/config/oauth/new',
@@ -428,9 +729,10 @@ function init({ server, call }) {
428
729
  let providerData = oauth2ProviderData(provider);
429
730
 
430
731
  let serviceUrl = await settings.get('serviceUrl');
431
- let defaultRedirectUrl = normalizeOutlookRedirectUrl(`${serviceUrl}/oauth`, provider);
432
-
433
- let pubSubApps = await oauth2Apps.list(0, 1000, { pubsub: true });
732
+ let defaultRedirectUrl = `${serviceUrl}/oauth`;
733
+ if (provider === 'outlook') {
734
+ defaultRedirectUrl = defaultRedirectUrl.replace(/^http:\/\/127\.0\.0\.1\b/i, 'http://localhost');
735
+ }
434
736
 
435
737
  return h.view(
436
738
  'config/oauth/new',
@@ -445,20 +747,14 @@ function init({ server, call }) {
445
747
  providerData,
446
748
  defaultRedirectUrl,
447
749
 
448
- pubSubApps:
449
- pubSubApps &&
450
- pubSubApps.apps &&
451
- pubSubApps.apps.map(app => {
452
- if (app.id === request.payload.pubSubApp) {
453
- app.selected = true;
454
- }
455
- return app;
456
- }),
750
+ pubSubApps: await getPubSubAppsForSelect(request.payload.pubSubApp),
457
751
 
458
752
  baseScopesApi: baseScopes === 'api',
459
753
  baseScopesImap: baseScopes === 'imap' || !baseScopes,
460
754
  baseScopesPubsub: baseScopes === 'pubsub',
461
755
 
756
+ ...authMethodContext(request.payload.authMethod),
757
+
462
758
  azureClouds: structuredClone(AZURE_CLOUDS).map(entry => {
463
759
  entry.selected = request.payload.cloud === entry.id;
464
760
  return entry;
@@ -505,9 +801,10 @@ function init({ server, call }) {
505
801
  let providerData = oauth2ProviderData(provider);
506
802
 
507
803
  let serviceUrl = await settings.get('serviceUrl');
508
- let defaultRedirectUrl = normalizeOutlookRedirectUrl(`${serviceUrl}/oauth`, provider);
509
-
510
- let pubSubApps = await oauth2Apps.list(0, 1000, { pubsub: true });
804
+ let defaultRedirectUrl = `${serviceUrl}/oauth`;
805
+ if (provider === 'outlook') {
806
+ defaultRedirectUrl = defaultRedirectUrl.replace(/^http:\/\/127\.0\.0\.1\b/i, 'http://localhost');
807
+ }
511
808
 
512
809
  return h
513
810
  .view(
@@ -523,20 +820,14 @@ function init({ server, call }) {
523
820
  providerData,
524
821
  defaultRedirectUrl,
525
822
 
526
- pubSubApps:
527
- pubSubApps &&
528
- pubSubApps.apps &&
529
- pubSubApps.apps.map(app => {
530
- if (app.id === request.payload.pubSubApp) {
531
- app.selected = true;
532
- }
533
- return app;
534
- }),
823
+ pubSubApps: await getPubSubAppsForSelect(request.payload.pubSubApp),
535
824
 
536
825
  baseScopesApi: baseScopes === 'api',
537
826
  baseScopesImap: baseScopes === 'imap' || !baseScopes,
538
827
  baseScopesPubsub: baseScopes === 'pubsub',
539
828
 
829
+ ...authMethodContext(request.payload.authMethod),
830
+
540
831
  azureClouds: structuredClone(AZURE_CLOUDS).map(entry => {
541
832
  entry.selected = request.payload.cloud === entry.id;
542
833
  return entry;
@@ -561,7 +852,6 @@ function init({ server, call }) {
561
852
  }
562
853
  });
563
854
 
564
- // GET /admin/config/oauth/edit/{app} - Edit OAuth application form
565
855
  server.route({
566
856
  method: 'GET',
567
857
  path: '/admin/config/oauth/edit/{app}',
@@ -574,19 +864,21 @@ function init({ server, call }) {
574
864
 
575
865
  let providerData = oauth2ProviderData(appData.provider, appData.cloud);
576
866
  let serviceUrl = await settings.get('serviceUrl');
577
- let defaultRedirectUrl = normalizeOutlookRedirectUrl(`${serviceUrl}/oauth`, providerData.provider);
867
+ let defaultRedirectUrl = `${serviceUrl}/oauth`;
868
+ if (providerData.provider === 'outlook') {
869
+ defaultRedirectUrl = defaultRedirectUrl.replace(/^http:\/\/127\.0\.0\.1\b/i, 'http://localhost');
870
+ }
578
871
 
579
872
  let values = Object.assign({}, appData, {
580
873
  clientSecret: '',
581
874
  serviceKey: '',
875
+ externalAccount: '',
582
876
  extraScopes: [].concat(appData.extraScopes || []).join('\n'),
583
877
  skipScopes: [].concat(appData.skipScopes || []).join('\n'),
584
878
 
585
879
  tenant: appData.authority && !['common', 'organizations', 'consumers'].includes(appData.authority) ? appData.authority : ''
586
880
  });
587
881
 
588
- let pubSubApps = await oauth2Apps.list(0, 1000, { pubsub: true });
589
-
590
882
  return h.view(
591
883
  'config/oauth/edit',
592
884
  {
@@ -602,16 +894,11 @@ function init({ server, call }) {
602
894
 
603
895
  hasClientSecret: !!appData.clientSecret,
604
896
  hasServiceKey: !!appData.serviceKey,
897
+ hasExternalAccount: !!appData.externalAccount,
605
898
 
606
- pubSubApps:
607
- pubSubApps &&
608
- pubSubApps.apps &&
609
- pubSubApps.apps.map(app => {
610
- if (app.id === values.pubSubApp) {
611
- app.selected = true;
612
- }
613
- return app;
614
- }),
899
+ ...authMethodContext(appData.authMethod, !!appData.serviceKey || !!appData.externalAccount),
900
+
901
+ pubSubApps: await getPubSubAppsForSelect(values.pubSubApp),
615
902
 
616
903
  values,
617
904
 
@@ -654,7 +941,6 @@ function init({ server, call }) {
654
941
  }
655
942
  });
656
943
 
657
- // POST /admin/config/oauth/edit - Update OAuth application
658
944
  server.route({
659
945
  method: 'POST',
660
946
  path: '/admin/config/oauth/edit',
@@ -700,9 +986,10 @@ function init({ server, call }) {
700
986
  let providerData = oauth2ProviderData(appData.provider, appData.cloud);
701
987
 
702
988
  let serviceUrl = await settings.get('serviceUrl');
703
- let defaultRedirectUrl = normalizeOutlookRedirectUrl(`${serviceUrl}/oauth`, appData.provider);
704
-
705
- let pubSubApps = await oauth2Apps.list(0, 1000, { pubsub: true });
989
+ let defaultRedirectUrl = `${serviceUrl}/oauth`;
990
+ if (appData.provider === 'outlook') {
991
+ defaultRedirectUrl = defaultRedirectUrl.replace(/^http:\/\/127\.0\.0\.1\b/i, 'http://localhost');
992
+ }
706
993
 
707
994
  return h.view(
708
995
  'config/oauth/edit',
@@ -718,16 +1005,11 @@ function init({ server, call }) {
718
1005
 
719
1006
  hasClientSecret: !!appData.clientSecret,
720
1007
  hasServiceKey: !!appData.serviceKey,
1008
+ hasExternalAccount: !!appData.externalAccount,
1009
+
1010
+ ...authMethodContext(appData.authMethod, !!appData.serviceKey || !!appData.externalAccount),
721
1011
 
722
- pubSubApps:
723
- pubSubApps &&
724
- pubSubApps.apps &&
725
- pubSubApps.apps.map(app => {
726
- if (app.id === request.payload.pubSubApp) {
727
- app.selected = true;
728
- }
729
- return app;
730
- }),
1012
+ pubSubApps: await getPubSubAppsForSelect(request.payload.pubSubApp),
731
1013
 
732
1014
  baseScopesApi: request.payload.baseScopes === 'api',
733
1015
  baseScopesImap: request.payload.baseScopes === 'imap' || !request.payload.baseScopes,
@@ -786,9 +1068,10 @@ function init({ server, call }) {
786
1068
  let providerData = oauth2ProviderData(provider);
787
1069
 
788
1070
  let serviceUrl = await settings.get('serviceUrl');
789
- let defaultRedirectUrl = normalizeOutlookRedirectUrl(`${serviceUrl}/oauth`, provider);
790
-
791
- let pubSubApps = await oauth2Apps.list(0, 1000, { pubsub: true });
1071
+ let defaultRedirectUrl = `${serviceUrl}/oauth`;
1072
+ if (provider === 'outlook') {
1073
+ defaultRedirectUrl = defaultRedirectUrl.replace(/^http:\/\/127\.0\.0\.1\b/i, 'http://localhost');
1074
+ }
792
1075
 
793
1076
  return h
794
1077
  .view(
@@ -806,16 +1089,11 @@ function init({ server, call }) {
806
1089
 
807
1090
  hasClientSecret: !!appData.clientSecret,
808
1091
  hasServiceKey: !!appData.serviceKey,
1092
+ hasExternalAccount: !!appData.externalAccount,
1093
+
1094
+ ...authMethodContext(request.payload.authMethod),
809
1095
 
810
- pubSubApps:
811
- pubSubApps &&
812
- pubSubApps.apps &&
813
- pubSubApps.apps.map(app => {
814
- if (app.id === request.payload.pubSubApp) {
815
- app.selected = true;
816
- }
817
- return app;
818
- }),
1096
+ pubSubApps: await getPubSubAppsForSelect(request.payload.pubSubApp),
819
1097
 
820
1098
  baseScopesApi: request.payload.baseScopes === 'api',
821
1099
  baseScopesImap: request.payload.baseScopes === 'imap' || !request.payload.baseScopes,