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
package/lib/routes-ui.js CHANGED
@@ -23,10 +23,10 @@ const {
23
23
  formatByteSize,
24
24
  getDuration,
25
25
  parseSignedFormData,
26
- updatePublicInterfaces,
27
26
  hasEnvValue,
28
27
  retryAgent
29
28
  } = require('./tools');
29
+ const { updatePublicInterfaces } = require('./utils/network');
30
30
  const packageData = require('../package.json');
31
31
  const he = require('he');
32
32
  const crypto = require('crypto');
@@ -42,7 +42,6 @@ const os = require('os');
42
42
  const {
43
43
  ADDRESS_STRATEGIES,
44
44
  settingsSchema,
45
- templateSchemas,
46
45
  oauthCreateSchema,
47
46
  accountIdSchema,
48
47
  defaultAccountTypeSchema,
@@ -53,10 +52,7 @@ const fs = require('fs');
53
52
  const pathlib = require('path');
54
53
  const timezonesList = require('timezones-list').default;
55
54
  const { Client: ElasticSearch } = require('@elastic/elasticsearch');
56
- const { templates } = require('./templates');
57
- const { webhooks } = require('./webhooks');
58
55
  const { llmPreProcess } = require('./llm-pre-process');
59
- const wellKnownServices = require('nodemailer/lib/well-known/services.json');
60
56
  const { locales } = require('./translations');
61
57
  const capa = require('./capa');
62
58
  const exampleWebhookPayloads = require('./payload-examples-webhooks.json');
@@ -70,6 +66,8 @@ const base32 = require('base32.js');
70
66
  const { simpleParser } = require('mailparser');
71
67
  const libmime = require('libmime');
72
68
 
69
+ const adminEntitiesRoutes = require('./ui-routes/admin-entities-routes');
70
+
73
71
  const {
74
72
  DEFAULT_MAX_LOG_LINES,
75
73
  PDKDF2_ITERATIONS,
@@ -356,17 +354,6 @@ const OKTA_OAUTH2_CLIENT_ID = readEnvValue('OKTA_OAUTH2_CLIENT_ID');
356
354
  const OKTA_OAUTH2_CLIENT_SECRET = readEnvValue('OKTA_OAUTH2_CLIENT_SECRET');
357
355
  const USE_OKTA_AUTH = !!(OKTA_OAUTH2_ISSUER && OKTA_OAUTH2_CLIENT_ID && OKTA_OAUTH2_CLIENT_SECRET);
358
356
 
359
- const CODE_FORMATS = [
360
- {
361
- format: 'html',
362
- name: 'HTML'
363
- },
364
- {
365
- format: 'markdown',
366
- name: 'Markdown'
367
- }
368
- ];
369
-
370
357
  const oauthUpdateSchema = {
371
358
  app: Joi.string().empty('').max(255).example('gmail').label('Provider').required(),
372
359
 
@@ -870,6 +857,9 @@ async function getServerStatus(type) {
870
857
  }
871
858
 
872
859
  function applyRoutes(server, call) {
860
+ // Initialize admin entity routes (webhooks, templates, gateways, tokens)
861
+ adminEntitiesRoutes({ server, call });
862
+
873
863
  const getDefaultPrompt = async () =>
874
864
  await call({
875
865
  cmd: 'openAiDefaultPrompt'
@@ -1181,7 +1171,7 @@ function applyRoutes(server, call) {
1181
1171
 
1182
1172
  return h.redirect('/admin/config/webhooks');
1183
1173
  } catch (err) {
1184
- await request.flash({ type: 'danger', message: `Failed to update configuration` });
1174
+ await request.flash({ type: 'danger', message: `Couldn't save settings. Try again.` });
1185
1175
  request.logger.error({ msg: 'Failed to update configuration', err });
1186
1176
 
1187
1177
  return h.view(
@@ -1223,7 +1213,7 @@ function applyRoutes(server, call) {
1223
1213
  });
1224
1214
  }
1225
1215
 
1226
- await request.flash({ type: 'danger', message: `Failed to update configuration` });
1216
+ await request.flash({ type: 'danger', message: `Couldn't save settings. Try again.` });
1227
1217
  request.logger.error({ msg: 'Failed to update configuration', err });
1228
1218
 
1229
1219
  return h
@@ -1375,7 +1365,7 @@ function applyRoutes(server, call) {
1375
1365
 
1376
1366
  return h.redirect('/admin/config/service');
1377
1367
  } catch (err) {
1378
- await request.flash({ type: 'danger', message: `Failed to update configuration` });
1368
+ await request.flash({ type: 'danger', message: `Couldn't save settings. Try again.` });
1379
1369
  request.logger.error({ msg: 'Failed to update configuration', err });
1380
1370
 
1381
1371
  return h.view(
@@ -1427,7 +1417,7 @@ function applyRoutes(server, call) {
1427
1417
  });
1428
1418
  }
1429
1419
 
1430
- await request.flash({ type: 'danger', message: `Failed to update configuration` });
1420
+ await request.flash({ type: 'danger', message: `Couldn't save settings. Try again.` });
1431
1421
  request.logger.error({ msg: 'Failed to update configuration', err });
1432
1422
 
1433
1423
  return h
@@ -1624,7 +1614,7 @@ return true;`
1624
1614
 
1625
1615
  return h.redirect('/admin/config/ai');
1626
1616
  } catch (err) {
1627
- await request.flash({ type: 'danger', message: `Failed to update configuration` });
1617
+ await request.flash({ type: 'danger', message: `Couldn't save settings. Try again.` });
1628
1618
  request.logger.error({ msg: 'Failed to update configuration', err });
1629
1619
 
1630
1620
  let hasOpenAiAPIKey = !!(await settings.get('openAiAPIKey'));
@@ -1688,7 +1678,7 @@ return true;`
1688
1678
  });
1689
1679
  }
1690
1680
 
1691
- await request.flash({ type: 'danger', message: `Failed to update configuration` });
1681
+ await request.flash({ type: 'danger', message: `Couldn't save settings. Try again.` });
1692
1682
  request.logger.error({ msg: 'Failed to update configuration', err });
1693
1683
 
1694
1684
  let hasOpenAiAPIKey = !!(await settings.get('openAiAPIKey'));
@@ -2045,7 +2035,7 @@ return true;`
2045
2035
 
2046
2036
  return h.redirect('/admin/config/logging');
2047
2037
  } catch (err) {
2048
- await request.flash({ type: 'danger', message: `Failed to update configuration` });
2038
+ await request.flash({ type: 'danger', message: `Couldn't save settings. Try again.` });
2049
2039
  request.logger.error({ msg: 'Failed to update configuration', err });
2050
2040
 
2051
2041
  return h.view(
@@ -2080,7 +2070,7 @@ return true;`
2080
2070
  });
2081
2071
  }
2082
2072
 
2083
- await request.flash({ type: 'danger', message: `Failed to update configuration` });
2073
+ await request.flash({ type: 'danger', message: `Couldn't save settings. Try again.` });
2084
2074
  request.logger.error({ msg: 'Failed to update configuration', err });
2085
2075
 
2086
2076
  return h
@@ -2512,11 +2502,11 @@ return true;`
2512
2502
  try {
2513
2503
  await oauth2Apps.del(request.payload.app);
2514
2504
 
2515
- await request.flash({ type: 'info', message: `OAuth2 application was deleted` });
2505
+ await request.flash({ type: 'info', message: `OAuth2 app deleted` });
2516
2506
 
2517
2507
  return h.redirect('/admin/config/oauth');
2518
2508
  } catch (err) {
2519
- await request.flash({ type: 'danger', message: `Failed to delete the OAuth2 application` });
2509
+ await request.flash({ type: 'danger', message: `Couldn't delete OAuth2 app. Try again.` });
2520
2510
  request.logger.error({ msg: 'Failed to delete OAuth2 application', err, app: request.payload.app, remoteAddress: request.app.ip });
2521
2511
  return h.redirect(`/admin/config/oauth/app/${request.payload.app}`);
2522
2512
  }
@@ -2530,7 +2520,7 @@ return true;`
2530
2520
  },
2531
2521
 
2532
2522
  async failAction(request, h, err) {
2533
- await request.flash({ type: 'danger', message: `Failed to delete the OAuth2 application` });
2523
+ await request.flash({ type: 'danger', message: `Couldn't delete OAuth2 app. Try again.` });
2534
2524
  request.logger.error({ msg: 'Failed to delete delete the OAuth2 application', err });
2535
2525
 
2536
2526
  return h.redirect('/admin/config/oauth').takeover();
@@ -2650,10 +2640,10 @@ return true;`
2650
2640
  await call({ cmd: 'googlePubSub', app: oauth2App.id });
2651
2641
  }
2652
2642
 
2653
- await request.flash({ type: 'success', message: `OAuth2 application was registered` });
2643
+ await request.flash({ type: 'success', message: `OAuth2 app created` });
2654
2644
  return h.redirect(`/admin/config/oauth/app/${oauth2App.id}`);
2655
2645
  } catch (err) {
2656
- await request.flash({ type: 'danger', message: `Failed to register OAuth2 app` });
2646
+ await request.flash({ type: 'danger', message: `Couldn't register OAuth2 app. Try again.` });
2657
2647
  request.logger.error({ msg: 'Failed to register OAuth2 app', err });
2658
2648
 
2659
2649
  let { provider, baseScopes } = request.payload;
@@ -2733,7 +2723,7 @@ return true;`
2733
2723
  });
2734
2724
  }
2735
2725
 
2736
- await request.flash({ type: 'danger', message: `Failed to register OAuth2 app` });
2726
+ await request.flash({ type: 'danger', message: `Couldn't register OAuth2 app. Try again.` });
2737
2727
  request.logger.error({ msg: 'Failed to register OAuth2 app', err });
2738
2728
 
2739
2729
  let { provider, baseScopes } = request.payload;
@@ -2840,2420 +2830,38 @@ return true;`
2840
2830
 
2841
2831
  [`active${providerData.caseName}`]: true,
2842
2832
  providerData,
2843
- defaultRedirectUrl,
2844
-
2845
- appData,
2846
-
2847
- hasClientSecret: !!appData.clientSecret,
2848
- hasServiceKey: !!appData.serviceKey,
2849
-
2850
- pubSubApps:
2851
- pubSubApps &&
2852
- pubSubApps.apps &&
2853
- pubSubApps.apps.map(app => {
2854
- if (app.id === values.pubSubApp) {
2855
- app.selected = true;
2856
- }
2857
- return app;
2858
- }),
2859
-
2860
- values,
2861
-
2862
- baseScopesApi: values.baseScopes === 'api',
2863
- baseScopesImap: values.baseScopes === 'imap' || !values.baseScopes,
2864
- baseScopesPubsub: values.baseScopes === 'pubsub',
2865
-
2866
- azureClouds: structuredClone(AZURE_CLOUDS).map(entry => {
2867
- entry.selected = values.cloud === entry.id;
2868
- return entry;
2869
- }),
2870
-
2871
- authorityCommon: values.authority === 'common',
2872
- authorityOrganizations: values.authority === 'organizations',
2873
- authorityConsumers: values.authority === 'consumers',
2874
- authorityTenant: !!values.tenant
2875
- },
2876
- {
2877
- layout: 'app'
2878
- }
2879
- );
2880
- },
2881
-
2882
- options: {
2883
- validate: {
2884
- options: {
2885
- stripUnknown: true,
2886
- abortEarly: false,
2887
- convert: true
2888
- },
2889
-
2890
- async failAction(request, h /*, err*/) {
2891
- return h.redirect('/admin/config/oauth').takeover();
2892
- },
2893
-
2894
- params: Joi.object({
2895
- app: Joi.string().empty('').max(255).example('gmail').label('Provider').required()
2896
- })
2897
- }
2898
- }
2899
- });
2900
-
2901
- server.route({
2902
- method: 'POST',
2903
- path: '/admin/config/oauth/edit',
2904
- async handler(request, h) {
2905
- let appData = await oauth2Apps.get(request.payload.app);
2906
- if (!appData) {
2907
- let error = Boom.boomify(new Error('Application was not found.'), { statusCode: 404 });
2908
- throw error;
2909
- }
2910
-
2911
- try {
2912
- let updates = Object.assign({}, request.payload);
2913
- updates.extraScopes = updates.extraScopes
2914
- .split(/\s+/)
2915
- .map(scope => scope.trim())
2916
- .filter(scope => scope);
2917
-
2918
- updates.skipScopes = updates.skipScopes
2919
- .split(/\s+/)
2920
- .map(scope => scope.trim())
2921
- .filter(scope => scope);
2922
-
2923
- if (updates.authority === 'tenant') {
2924
- updates.authority = updates.tenant;
2925
- }
2926
- delete updates.tenant;
2927
-
2928
- let oauth2App = await oauth2Apps.update(appData.id, updates);
2929
- if (!oauth2App || !oauth2App.id) {
2930
- throw new Error('Unexpected result');
2931
- }
2932
-
2933
- if (oauth2App && oauth2App.pubsubUpdates && oauth2App.pubsubUpdates.pubSubSubscription) {
2934
- await call({ cmd: 'googlePubSub', app: oauth2App.id });
2935
- }
2936
-
2937
- await request.flash({ type: 'success', message: `OAuth2 application was updated` });
2938
- return h.redirect(`/admin/config/oauth/app/${oauth2App.id}`);
2939
- } catch (err) {
2940
- await request.flash({ type: 'danger', message: `Failed to update OAuth2 app` });
2941
- request.logger.error({ msg: 'Failed to update OAuth2 app', app: request.payload.app, err });
2942
-
2943
- let providerData = oauth2ProviderData(appData.provider, appData.cloud);
2944
-
2945
- let serviceUrl = await settings.get('serviceUrl');
2946
- let defaultRedirectUrl = `${serviceUrl}/oauth`;
2947
- if (appData.provider === 'outlook') {
2948
- defaultRedirectUrl = defaultRedirectUrl.replace(/^http:\/\/127\.0\.0\.1\b/i, 'http://localhost');
2949
- }
2950
-
2951
- let pubSubApps = await oauth2Apps.list(0, 1000, { pubsub: true });
2952
-
2953
- return h.view(
2954
- 'config/oauth/edit',
2955
- {
2956
- pageTitle: 'OAuth2',
2957
- menuConfig: true,
2958
- menuConfigOauth: true,
2959
-
2960
- [`active${providerData.caseName}`]: true,
2961
- providerData,
2962
- defaultRedirectUrl,
2963
- appData,
2964
-
2965
- hasClientSecret: !!appData.clientSecret,
2966
- hasServiceKey: !!appData.serviceKey,
2967
-
2968
- pubSubApps:
2969
- pubSubApps &&
2970
- pubSubApps.apps &&
2971
- pubSubApps.apps.map(app => {
2972
- if (app.id === request.payload.pubSubApp) {
2973
- app.selected = true;
2974
- }
2975
- return app;
2976
- }),
2977
-
2978
- baseScopesApi: request.payload.baseScopes === 'api',
2979
- baseScopesImap: request.payload.baseScopes === 'imap' || !request.payload.baseScopes,
2980
- baseScopesPubsub: request.payload.baseScopes === 'pubsub',
2981
-
2982
- azureClouds: structuredClone(AZURE_CLOUDS).map(entry => {
2983
- entry.selected = request.payload.cloud === entry.id;
2984
- return entry;
2985
- }),
2986
-
2987
- authorityCommon: request.payload.authority === 'common',
2988
- authorityOrganizations: request.payload.authority === 'organizations',
2989
- authorityConsumers: request.payload.authority === 'consumers',
2990
- authorityTenant: request.payload.authority === 'tenant'
2991
- },
2992
- {
2993
- layout: 'app'
2994
- }
2995
- );
2996
- }
2997
- },
2998
- options: {
2999
- validate: {
3000
- options: {
3001
- stripUnknown: true,
3002
- abortEarly: false,
3003
- convert: true
3004
- },
3005
-
3006
- async failAction(request, h, err) {
3007
- let errors = {};
3008
-
3009
- if (err.details) {
3010
- err.details.forEach(detail => {
3011
- if (!errors[detail.path]) {
3012
- errors[detail.path] = detail.message;
3013
- }
3014
- });
3015
- }
3016
-
3017
- let appData = await oauth2Apps.get(request.payload.app);
3018
- if (!appData) {
3019
- await request.flash({ type: 'danger', message: `Application was not found.` });
3020
- request.logger.error({ msg: 'Application was not found.', app: request.payload.app });
3021
- return h.redirect('/admin').takeover();
3022
- }
3023
-
3024
- await request.flash({ type: 'danger', message: `Failed to update OAuth2 app` });
3025
- request.logger.error({ msg: 'Failed to update OAuth2 app', err });
3026
-
3027
- let { provider } = request.payload;
3028
- if (!provider || !OAUTH_PROVIDERS.hasOwnProperty(provider)) {
3029
- return h.redirect('/admin').takeover();
3030
- }
3031
-
3032
- let providerData = oauth2ProviderData(provider);
3033
-
3034
- let serviceUrl = await settings.get('serviceUrl');
3035
- let defaultRedirectUrl = `${serviceUrl}/oauth`;
3036
- if (provider === 'outlook') {
3037
- defaultRedirectUrl = defaultRedirectUrl.replace(/^http:\/\/127\.0\.0\.1\b/i, 'http://localhost');
3038
- }
3039
-
3040
- let pubSubApps = await oauth2Apps.list(0, 1000, { pubsub: true });
3041
-
3042
- return h
3043
- .view(
3044
- 'config/oauth/edit',
3045
- {
3046
- pageTitle: 'OAuth2',
3047
- menuConfig: true,
3048
- menuConfigOauth: true,
3049
-
3050
- [`active${providerData.caseName}`]: true,
3051
- providerData,
3052
- defaultRedirectUrl,
3053
-
3054
- appData,
3055
-
3056
- hasClientSecret: !!appData.clientSecret,
3057
- hasServiceKey: !!appData.serviceKey,
3058
-
3059
- pubSubApps:
3060
- pubSubApps &&
3061
- pubSubApps.apps &&
3062
- pubSubApps.apps.map(app => {
3063
- if (app.id === request.payload.pubSubApp) {
3064
- app.selected = true;
3065
- }
3066
- return app;
3067
- }),
3068
-
3069
- baseScopesApi: request.payload.baseScopes === 'api',
3070
- baseScopesImap: request.payload.baseScopes === 'imap' || !request.payload.baseScopes,
3071
- baseScopesPubsub: request.payload.baseScopes === 'pubsub',
3072
-
3073
- azureClouds: structuredClone(AZURE_CLOUDS).map(entry => {
3074
- entry.selected = request.payload.cloud === entry.id;
3075
- return entry;
3076
- }),
3077
-
3078
- authorityCommon: request.payload.authority === 'common',
3079
- authorityOrganizations: request.payload.authority === 'organizations',
3080
- authorityConsumers: request.payload.authority === 'consumers',
3081
- authorityTenant: request.payload.authority === 'tenant',
3082
-
3083
- errors
3084
- },
3085
- {
3086
- layout: 'app'
3087
- }
3088
- )
3089
- .takeover();
3090
- },
3091
-
3092
- payload: Joi.object(oauthUpdateSchema)
3093
- }
3094
- }
3095
- });
3096
-
3097
- server.route({
3098
- method: 'GET',
3099
- path: '/admin/webhooks',
3100
- async handler(request, h) {
3101
- let data = await webhooks.list(request.query.page - 1, request.query.pageSize);
3102
-
3103
- let nextPage = false;
3104
- let prevPage = false;
3105
-
3106
- if (request.query.account) {
3107
- let accountObject = new Account({ redis, account: request.query.account });
3108
- data.account = await accountObject.loadAccountData();
3109
- }
3110
-
3111
- let getPagingUrl = page => {
3112
- let url = new URL(`admin/webhooks`, 'http://localhost');
3113
-
3114
- if (page) {
3115
- url.searchParams.append('page', page);
3116
- }
3117
-
3118
- if (request.query.pageSize !== DEFAULT_PAGE_SIZE) {
3119
- url.searchParams.append('pageSize', request.query.pageSize);
3120
- }
3121
-
3122
- return url.pathname + url.search;
3123
- };
3124
-
3125
- if (data.pages > data.page + 1) {
3126
- nextPage = getPagingUrl(data.page + 2);
3127
- }
3128
-
3129
- if (data.page > 0) {
3130
- prevPage = getPagingUrl(data.page);
3131
- }
3132
-
3133
- let newLink = new URL('/admin/webhooks/new', 'http://localhost');
3134
-
3135
- return h.view(
3136
- 'webhooks/index',
3137
- {
3138
- pageTitle: 'Webhook Routing',
3139
- menuWebhooks: true,
3140
-
3141
- newLink: newLink.pathname + newLink.search,
3142
-
3143
- showPaging: data.pages > 1,
3144
- nextPage,
3145
- prevPage,
3146
- firstPage: data.page === 0,
3147
- pageLinks: new Array(data.pages || 1).fill(0).map((z, i) => ({
3148
- url: getPagingUrl(i + 1),
3149
- title: i + 1,
3150
- active: i === data.page
3151
- })),
3152
-
3153
- webhooksEnabled: await settings.get('webhooksEnabled'),
3154
-
3155
- webhooks: data.webhooks
3156
- },
3157
- {
3158
- layout: 'app'
3159
- }
3160
- );
3161
- },
3162
-
3163
- options: {
3164
- validate: {
3165
- options: {
3166
- stripUnknown: true,
3167
- abortEarly: false,
3168
- convert: true
3169
- },
3170
-
3171
- async failAction(request, h /*, err*/) {
3172
- return h.redirect('/admin/webhooks').takeover();
3173
- },
3174
-
3175
- query: Joi.object({
3176
- page: Joi.number().integer().min(1).max(1000000).default(1),
3177
- pageSize: Joi.number().integer().min(1).max(250).default(DEFAULT_PAGE_SIZE)
3178
- })
3179
- }
3180
- }
3181
- });
3182
-
3183
- server.route({
3184
- method: 'GET',
3185
- path: '/admin/webhooks/new',
3186
- async handler(request, h) {
3187
- const values = {
3188
- name: '',
3189
- description: '',
3190
-
3191
- contentFnJson: JSON.stringify(`/*
3192
- // The following example passes webhooks for new emails that appear in the Inbox of the user "testaccount".
3193
- // NB! Gmail webhooks are always emitted from the "All Mail" folder, not the Inbox, so we need to check both the path and label values.
3194
-
3195
- const isInbox = payload.path === 'INBOX' || payload.data?.labels?.includes('\\\\Inbox');
3196
- if (payload.event === 'messageNew' && payload.account === 'testaccount' && isInbox) {
3197
- return true;
3198
- }
3199
- */`),
3200
- contentMapJson: JSON.stringify(`// By default the output payload is returned unmodified.
3201
-
3202
- return payload;`)
3203
- };
3204
-
3205
- return h.view(
3206
- 'webhooks/new',
3207
- {
3208
- pageTitle: 'Webhook Routing',
3209
- menuWebhooks: true,
3210
- values,
3211
-
3212
- examplePayloadsJson: JSON.stringify(await getExampleWebhookPayloads()),
3213
- notificationTypesJson: JSON.stringify(notificationTypes),
3214
- scriptEnvJson: JSON.stringify((await settings.get('scriptEnv')) || '{}')
3215
- },
3216
- {
3217
- layout: 'app'
3218
- }
3219
- );
3220
- },
3221
-
3222
- options: {
3223
- validate: {
3224
- options: {
3225
- stripUnknown: true,
3226
- abortEarly: false,
3227
- convert: true
3228
- },
3229
-
3230
- async failAction(request, h /*, err*/) {
3231
- return h.redirect('/admin/webhooks').takeover();
3232
- },
3233
-
3234
- query: Joi.object({})
3235
- }
3236
- }
3237
- });
3238
-
3239
- server.route({
3240
- method: 'POST',
3241
- path: '/admin/webhooks/new',
3242
- async handler(request, h) {
3243
- let contentFn, contentMap;
3244
- try {
3245
- if (request.payload.contentFnJson === '') {
3246
- contentFn = null;
3247
- } else {
3248
- contentFn = JSON.parse(request.payload.contentFnJson);
3249
- if (typeof contentFn !== 'string') {
3250
- throw new Error('Invalid Format');
3251
- }
3252
- }
3253
- } catch (err) {
3254
- err.details = {
3255
- contentFnJson: 'Invalid JSON'
3256
- };
3257
- throw err;
3258
- }
3259
-
3260
- try {
3261
- if (request.payload.contentMapJson === '') {
3262
- contentMap = null;
3263
- } else {
3264
- contentMap = JSON.parse(request.payload.contentMapJson);
3265
- if (typeof contentMap !== 'string') {
3266
- throw new Error('Invalid Format');
3267
- }
3268
- }
3269
- } catch (err) {
3270
- err.details = {
3271
- contentMapJson: 'Invalid JSON'
3272
- };
3273
- throw err;
3274
- }
3275
-
3276
- let customHeaders = request.payload.customHeaders
3277
- .split(/[\r\n]+/)
3278
- .map(header => header.trim())
3279
- .filter(header => header)
3280
- .map(line => {
3281
- let sep = line.indexOf(':');
3282
- if (sep >= 0) {
3283
- return {
3284
- key: line.substring(0, sep).trim(),
3285
- value: line.substring(sep + 1).trim()
3286
- };
3287
- }
3288
- return {
3289
- key: line,
3290
- value: ''
3291
- };
3292
- });
3293
-
3294
- try {
3295
- let createRequest = await webhooks.create(
3296
- {
3297
- name: request.payload.name,
3298
- description: request.payload.description,
3299
- targetUrl: request.payload.targetUrl,
3300
- enabled: request.payload.enabled,
3301
-
3302
- customHeaders
3303
- },
3304
- {
3305
- fn: contentFn,
3306
- map: contentMap
3307
- }
3308
- );
3309
-
3310
- await request.flash({ type: 'info', message: `Webhook routing was created` });
3311
- return h.redirect(`/admin/webhooks/webhook/${createRequest.id}`);
3312
- } catch (err) {
3313
- await request.flash({ type: 'danger', message: `Failed to create webhook routing` });
3314
- request.logger.error({ msg: 'Failed to create webhook routing', err });
3315
-
3316
- return h.view(
3317
- 'webhooks/new',
3318
- {
3319
- pageTitle: 'Webhook Routing',
3320
- menuWebhooks: true,
3321
- errors: err.details,
3322
-
3323
- examplePayloadsJson: JSON.stringify(await getExampleWebhookPayloads()),
3324
- notificationTypesJson: JSON.stringify(notificationTypes),
3325
- scriptEnvJson: JSON.stringify((await settings.get('scriptEnv')) || '{}')
3326
- },
3327
- {
3328
- layout: 'app'
3329
- }
3330
- );
3331
- }
3332
- },
3333
- options: {
3334
- validate: {
3335
- options: {
3336
- stripUnknown: true,
3337
- abortEarly: false,
3338
- convert: true
3339
- },
3340
-
3341
- async failAction(request, h, err) {
3342
- let errors = {};
3343
-
3344
- if (err.details) {
3345
- err.details.forEach(detail => {
3346
- if (!errors[detail.path]) {
3347
- errors[detail.path] = detail.message;
3348
- }
3349
- });
3350
- }
3351
-
3352
- await request.flash({ type: 'danger', message: `Failed to create webhook routing` });
3353
- request.logger.error({ msg: 'Failed to create webhook routing', err });
3354
-
3355
- return h
3356
- .view(
3357
- 'templates/new',
3358
- {
3359
- pageTitle: 'Templates',
3360
- menuTemplates: true,
3361
- errors,
3362
-
3363
- examplePayloadsJson: JSON.stringify(await getExampleWebhookPayloads()),
3364
- notificationTypesJson: JSON.stringify(notificationTypes)
3365
- },
3366
- {
3367
- layout: 'app'
3368
- }
3369
- )
3370
- .takeover();
3371
- },
3372
-
3373
- payload: Joi.object({
3374
- name: Joi.string().max(256).example('Transaction receipt').description('Name of the routing').label('RoutingName').required(),
3375
- description: Joi.string()
3376
- .allow('')
3377
- .max(1024)
3378
- .example('Something about the routing')
3379
- .description('Optional description of the webhook routing')
3380
- .label('RoutingDescription'),
3381
- targetUrl: Joi.string()
3382
- .uri({
3383
- scheme: ['http', 'https'],
3384
- allowRelative: false
3385
- })
3386
- .allow('')
3387
- .default('')
3388
- .example('https://myservice.com/imap/webhooks')
3389
- .description('Webhook target URL'),
3390
- enabled: Joi.boolean()
3391
- .truthy('Y', 'true', '1', 'on')
3392
- .falsy('N', 'false', 0, '')
3393
- .default(false)
3394
- .example(false)
3395
- .description('Is the routing enabled'),
3396
- customHeaders: Joi.string()
3397
- .allow('')
3398
- .trim()
3399
- .max(10 * 1024)
3400
- .description('Custom request headers'),
3401
- contentFnJson: Joi.string()
3402
- .max(1024 * 1024)
3403
- .default('')
3404
- .allow('')
3405
- .trim()
3406
- .description('Filter function'),
3407
- contentMapJson: Joi.string()
3408
- .max(1024 * 1024)
3409
- .default('')
3410
- .allow('')
3411
- .trim()
3412
- .description('Map function')
3413
- })
3414
- }
3415
- }
3416
- });
3417
-
3418
- server.route({
3419
- method: 'GET',
3420
- path: '/admin/webhooks/webhook/{webhook}',
3421
- async handler(request, h) {
3422
- let webhook = await webhooks.get(request.params.webhook);
3423
- if (!webhook) {
3424
- let error = Boom.boomify(new Error('Webhook Route was not found.'), { statusCode: 404 });
3425
- throw error;
3426
- }
3427
-
3428
- webhook.targetUrlShort = webhook.targetUrl ? new URL(webhook.targetUrl).hostname : false;
3429
-
3430
- const errorLog = ((await webhooks.getErrorLog(webhook.id)) || []).map(entry => {
3431
- if (entry.error && typeof entry.error === 'string') {
3432
- entry.error = entry.error
3433
- .replace(/\r?\n/g, '\n')
3434
- .replace(/^\s+at\s+.*$/gm, '')
3435
- .replace(/\n+/g, '\n')
3436
- .trim()
3437
- .replace(/(evalmachine.<anonymous>:)(\d+)/, (o, p, n) => p + (Number(n) - 1));
3438
- }
3439
- return entry;
3440
- });
3441
-
3442
- return h.view(
3443
- 'webhooks/webhook',
3444
- {
3445
- pageTitle: 'Webhook Routing',
3446
- menuWebhooks: true,
3447
- webhook,
3448
-
3449
- errorLog
3450
- },
3451
- {
3452
- layout: 'app'
3453
- }
3454
- );
3455
- },
3456
-
3457
- options: {
3458
- validate: {
3459
- options: {
3460
- stripUnknown: true,
3461
- abortEarly: false,
3462
- convert: true
3463
- },
3464
-
3465
- async failAction(request, h /*, err*/) {
3466
- return h.redirect('/admin/webhooks').takeover();
3467
- },
3468
-
3469
- params: Joi.object({
3470
- webhook: Joi.string()
3471
- .base64({ paddingRequired: false, urlSafe: true })
3472
- .max(512)
3473
- .example('AAAAAQAACnA')
3474
- .required()
3475
- .description('Webhook Route ID')
3476
- })
3477
- }
3478
- }
3479
- });
3480
-
3481
- server.route({
3482
- method: 'GET',
3483
- path: '/admin/webhooks/webhook/{webhook}/edit',
3484
- async handler(request, h) {
3485
- let webhook = await webhooks.get(request.params.webhook);
3486
- if (!webhook) {
3487
- let error = Boom.boomify(new Error('Webhook Route not found.'), { statusCode: 404 });
3488
- throw error;
3489
- }
3490
-
3491
- const values = {
3492
- webhook: webhook.id,
3493
- name: webhook.name,
3494
- description: webhook.description,
3495
- targetUrl: webhook.targetUrl,
3496
- enabled: webhook.enabled,
3497
- contentFnJson: JSON.stringify(webhook.content.fn || ''),
3498
- contentMapJson: JSON.stringify(webhook.content.map || ''),
3499
-
3500
- customHeaders: []
3501
- .concat(webhook.customHeaders || [])
3502
- .map(entry => `${entry.key}: ${entry.value}`.trim())
3503
- .join('\n')
3504
- };
3505
-
3506
- return h.view(
3507
- 'webhooks/edit',
3508
- {
3509
- pageTitle: 'Webhook Routing',
3510
- menuWebhooks: true,
3511
-
3512
- webhook,
3513
-
3514
- values,
3515
-
3516
- examplePayloadsJson: JSON.stringify(await getExampleWebhookPayloads()),
3517
- notificationTypesJson: JSON.stringify(notificationTypes),
3518
- scriptEnvJson: JSON.stringify((await settings.get('scriptEnv')) || '{}')
3519
- },
3520
- {
3521
- layout: 'app'
3522
- }
3523
- );
3524
- },
3525
-
3526
- options: {
3527
- validate: {
3528
- options: {
3529
- stripUnknown: true,
3530
- abortEarly: false,
3531
- convert: true
3532
- },
3533
-
3534
- async failAction(request, h /*, err*/) {
3535
- return h.redirect('/admin/webhooks').takeover();
3536
- },
3537
-
3538
- params: Joi.object({
3539
- webhook: Joi.string()
3540
- .base64({ paddingRequired: false, urlSafe: true })
3541
- .max(512)
3542
- .example('AAAAAQAACnA')
3543
- .required()
3544
- .description('Webhook Route ID')
3545
- })
3546
- }
3547
- }
3548
- });
3549
-
3550
- server.route({
3551
- method: 'POST',
3552
- path: '/admin/webhooks/edit',
3553
- async handler(request, h) {
3554
- let contentFn, contentMap;
3555
- try {
3556
- if (request.payload.contentFnJson === '') {
3557
- contentFn = null;
3558
- } else {
3559
- contentFn = JSON.parse(request.payload.contentFnJson);
3560
- if (typeof contentFn !== 'string') {
3561
- throw new Error('Invalid Format');
3562
- }
3563
- }
3564
- } catch (err) {
3565
- err.details = {
3566
- contentFnJson: 'Invalid JSON'
3567
- };
3568
- throw err;
3569
- }
3570
-
3571
- try {
3572
- if (request.payload.contentMapJson === '') {
3573
- contentMap = null;
3574
- } else {
3575
- contentMap = JSON.parse(request.payload.contentMapJson);
3576
- if (typeof contentMap !== 'string') {
3577
- throw new Error('Invalid Format');
3578
- }
3579
- }
3580
- } catch (err) {
3581
- err.details = {
3582
- contentMapJson: 'Invalid JSON'
3583
- };
3584
- throw err;
3585
- }
3586
-
3587
- let customHeaders = request.payload.customHeaders
3588
- .split(/[\r\n]+/)
3589
- .map(header => header.trim())
3590
- .filter(header => header)
3591
- .map(line => {
3592
- let sep = line.indexOf(':');
3593
- if (sep >= 0) {
3594
- return {
3595
- key: line.substring(0, sep).trim(),
3596
- value: line.substring(sep + 1).trim()
3597
- };
3598
- }
3599
- return {
3600
- key: line,
3601
- value: ''
3602
- };
3603
- });
3604
-
3605
- try {
3606
- await webhooks.update(
3607
- request.payload.webhook,
3608
- {
3609
- name: request.payload.name,
3610
- description: request.payload.description,
3611
- targetUrl: request.payload.targetUrl,
3612
- enabled: request.payload.enabled,
3613
-
3614
- customHeaders
3615
- },
3616
- {
3617
- fn: contentFn,
3618
- map: contentMap
3619
- }
3620
- );
3621
-
3622
- await request.flash({ type: 'info', message: `Webhook Route settings were updated` });
3623
- return h.redirect(`/admin/webhooks/webhook/${request.payload.webhook}`);
3624
- } catch (err) {
3625
- await request.flash({ type: 'danger', message: `Failed to update Webhook Route` });
3626
- request.logger.error({ msg: 'Failed to update Webhook Route', err });
3627
-
3628
- let webhook = await webhooks.get(request.payload.webhook);
3629
- if (!webhook) {
3630
- let error = Boom.boomify(new Error('Webhook Route not found.'), { statusCode: 404 });
3631
- throw error;
3632
- }
3633
-
3634
- return h.view(
3635
- 'webhooks/edit',
3636
- {
3637
- pageTitle: 'Webhook Routing',
3638
- menuWebhooks: true,
3639
-
3640
- webhook,
3641
-
3642
- errors: err.details,
3643
-
3644
- examplePayloadsJson: JSON.stringify(await getExampleWebhookPayloads()),
3645
- notificationTypesJson: JSON.stringify(notificationTypes),
3646
- scriptEnvJson: JSON.stringify((await settings.get('scriptEnv')) || '{}')
3647
- },
3648
- {
3649
- layout: 'app'
3650
- }
3651
- );
3652
- }
3653
- },
3654
- options: {
3655
- validate: {
3656
- options: {
3657
- stripUnknown: true,
3658
- abortEarly: false,
3659
- convert: true
3660
- },
3661
-
3662
- async failAction(request, h, err) {
3663
- let errors = {};
3664
-
3665
- if (err.details) {
3666
- err.details.forEach(detail => {
3667
- if (!errors[detail.path]) {
3668
- errors[detail.path] = detail.message;
3669
- }
3670
- });
3671
- }
3672
-
3673
- await request.flash({ type: 'danger', message: `Failed to update Webhook Route` });
3674
- request.logger.error({ msg: 'Failed to update Webhook Route', err });
3675
-
3676
- let webhook = await webhooks.get(request.payload.webhook);
3677
- if (!webhook) {
3678
- let error = Boom.boomify(new Error('Webhook Route not found.'), { statusCode: 404 });
3679
- throw error;
3680
- }
3681
-
3682
- return h
3683
- .view(
3684
- 'webhooks/edit',
3685
- {
3686
- pageTitle: 'Webhook Routing',
3687
- menuWebhooks: true,
3688
-
3689
- webhook,
3690
-
3691
- errors,
3692
-
3693
- examplePayloadsJson: JSON.stringify(await getExampleWebhookPayloads()),
3694
- notificationTypesJson: JSON.stringify(notificationTypes),
3695
- scriptEnvJson: JSON.stringify((await settings.get('scriptEnv')) || '{}')
3696
- },
3697
- {
3698
- layout: 'app'
3699
- }
3700
- )
3701
- .takeover();
3702
- },
3703
-
3704
- payload: Joi.object({
3705
- webhook: Joi.string()
3706
- .base64({ paddingRequired: false, urlSafe: true })
3707
- .max(512)
3708
- .example('AAAAAQAACnA')
3709
- .required()
3710
- .description('Webhook Route ID'),
3711
-
3712
- name: Joi.string().max(256).example('Transaction receipt').description('Name of the routing').label('RoutingName').required(),
3713
- description: Joi.string()
3714
- .allow('')
3715
- .max(1024)
3716
- .example('Something about the routing')
3717
- .description('Optional description of the webhook routing')
3718
- .label('RoutingDescription'),
3719
- targetUrl: Joi.string()
3720
- .uri({
3721
- scheme: ['http', 'https'],
3722
- allowRelative: false
3723
- })
3724
- .allow('')
3725
- .default('')
3726
- .example('https://myservice.com/imap/webhooks')
3727
- .description('Webhook target URL'),
3728
- enabled: Joi.boolean()
3729
- .truthy('Y', 'true', '1', 'on')
3730
- .falsy('N', 'false', 0, '')
3731
- .default(false)
3732
- .example(false)
3733
- .description('Is the routing enabled'),
3734
- customHeaders: Joi.string()
3735
- .allow('')
3736
- .trim()
3737
- .max(10 * 1024)
3738
- .description('Custom request headers'),
3739
- contentFnJson: Joi.string()
3740
- .max(1024 * 1024)
3741
- .default('')
3742
- .allow('')
3743
- .trim()
3744
- .description('Filter function'),
3745
- contentMapJson: Joi.string()
3746
- .max(1024 * 1024)
3747
- .default('')
3748
- .allow('')
3749
- .trim()
3750
- .description('Map function')
3751
- })
3752
- }
3753
- }
3754
- });
3755
-
3756
- server.route({
3757
- method: 'POST',
3758
- path: '/admin/webhooks/delete',
3759
- async handler(request, h) {
3760
- try {
3761
- await webhooks.del(request.payload.webhook);
3762
-
3763
- await request.flash({ type: 'info', message: `Webhook Route was deleted` });
3764
-
3765
- let accountWebhooksLink = new URL('/admin/webhooks', 'http://localhost');
3766
-
3767
- return h.redirect(accountWebhooksLink.pathname + accountWebhooksLink.search);
3768
- } catch (err) {
3769
- await request.flash({ type: 'danger', message: `Failed to delete the Webhook Route` });
3770
- request.logger.error({ msg: 'Failed to delete Webhook Route', err, webhook: request.payload.webhook, remoteAddress: request.app.ip });
3771
- return h.redirect(`/admin/webhooks/webhook/${request.payload.webhook}`);
3772
- }
3773
- },
3774
- options: {
3775
- validate: {
3776
- options: {
3777
- stripUnknown: true,
3778
- abortEarly: false,
3779
- convert: true
3780
- },
3781
-
3782
- async failAction(request, h, err) {
3783
- await request.flash({ type: 'danger', message: `Failed to delete Webhook Route` });
3784
- request.logger.error({ msg: 'Failed to delete delete Webhook Route', err });
3785
-
3786
- return h.redirect('/admin/webhooks').takeover();
3787
- },
3788
-
3789
- payload: Joi.object({
3790
- webhook: Joi.string()
3791
- .base64({ paddingRequired: false, urlSafe: true })
3792
- .max(512)
3793
- .example('AAAAAQAACnA')
3794
- .required()
3795
- .description('Webhook Route ID')
3796
- })
3797
- }
3798
- }
3799
- });
3800
-
3801
- server.route({
3802
- method: 'GET',
3803
- path: '/admin/templates',
3804
- async handler(request, h) {
3805
- let data = await templates.list(request.query.account, request.query.page - 1, request.query.pageSize);
3806
-
3807
- let nextPage = false;
3808
- let prevPage = false;
3809
-
3810
- if (request.query.account) {
3811
- let accountObject = new Account({ redis, account: request.query.account });
3812
- data.account = await accountObject.loadAccountData();
3813
- }
3814
-
3815
- let getPagingUrl = page => {
3816
- let url = new URL(`admin/templates`, 'http://localhost');
3817
- url.searchParams.append('page', page);
3818
-
3819
- if (request.query.account) {
3820
- url.searchParams.append('account', request.query.account);
3821
- }
3822
-
3823
- if (request.query.pageSize !== DEFAULT_PAGE_SIZE) {
3824
- url.searchParams.append('pageSize', request.query.pageSize);
3825
- }
3826
-
3827
- return url.pathname + url.search;
3828
- };
3829
-
3830
- if (data.pages > data.page + 1) {
3831
- nextPage = getPagingUrl(data.page + 2);
3832
- }
3833
-
3834
- if (data.page > 0) {
3835
- prevPage = getPagingUrl(data.page);
3836
- }
3837
-
3838
- let newLink = new URL('/admin/templates/new', 'http://localhost');
3839
- if (request.query.account) {
3840
- newLink.searchParams.append('account', request.query.account);
3841
- }
3842
-
3843
- return h.view(
3844
- 'templates/index',
3845
- {
3846
- pageTitle: 'Templates',
3847
- menuTemplates: true,
3848
-
3849
- account: data.account,
3850
- newLink: newLink.pathname + newLink.search,
3851
-
3852
- showPaging: data.pages > 1,
3853
- nextPage,
3854
- prevPage,
3855
- firstPage: data.page === 0,
3856
- pageLinks: new Array(data.pages || 1).fill(0).map((z, i) => ({
3857
- url: getPagingUrl(i + 1),
3858
- title: i + 1,
3859
- active: i === data.page
3860
- })),
3861
-
3862
- templates: data.templates
3863
- },
3864
- {
3865
- layout: 'app'
3866
- }
3867
- );
3868
- },
3869
-
3870
- options: {
3871
- validate: {
3872
- options: {
3873
- stripUnknown: true,
3874
- abortEarly: false,
3875
- convert: true
3876
- },
3877
-
3878
- async failAction(request, h /*, err*/) {
3879
- return h.redirect('/admin/templates').takeover();
3880
- },
3881
-
3882
- query: Joi.object({
3883
- account: accountIdSchema.default(null),
3884
- page: Joi.number().integer().min(1).max(1000000).default(1),
3885
- pageSize: Joi.number().integer().min(1).max(250).default(DEFAULT_PAGE_SIZE)
3886
- })
3887
- }
3888
- }
3889
- });
3890
-
3891
- server.route({
3892
- method: 'GET',
3893
- path: '/admin/templates/template/{template}',
3894
- async handler(request, h) {
3895
- let template = await templates.get(request.params.template);
3896
- if (!template) {
3897
- let error = Boom.boomify(new Error('Template not found.'), { statusCode: 404 });
3898
- throw error;
3899
- }
3900
-
3901
- let account;
3902
- if (template.account) {
3903
- let accountObject = new Account({ redis, account: template.account });
3904
- account = await accountObject.loadAccountData();
3905
- }
3906
-
3907
- let accountTemplatesLink = new URL('/admin/templates', 'http://localhost');
3908
- if (account) {
3909
- accountTemplatesLink.searchParams.append('account', account.account);
3910
- }
3911
-
3912
- return h.view(
3913
- 'templates/template',
3914
- {
3915
- pageTitle: 'Templates',
3916
- menuTemplates: true,
3917
-
3918
- account,
3919
-
3920
- accountTemplatesLink: accountTemplatesLink.pathname + accountTemplatesLink.search,
3921
-
3922
- format: CODE_FORMATS.find(entry => entry.format === template.format),
3923
-
3924
- template
3925
- },
3926
- {
3927
- layout: 'app'
3928
- }
3929
- );
3930
- },
3931
-
3932
- options: {
3933
- validate: {
3934
- options: {
3935
- stripUnknown: true,
3936
- abortEarly: false,
3937
- convert: true
3938
- },
3939
-
3940
- async failAction(request, h /*, err*/) {
3941
- return h.redirect('/admin/templates').takeover();
3942
- },
3943
-
3944
- params: Joi.object({
3945
- template: Joi.string()
3946
- .base64({ paddingRequired: false, urlSafe: true })
3947
- .max(512)
3948
- .example('AAAAAQAACnA')
3949
- .required()
3950
- .description('Template ID')
3951
- })
3952
- }
3953
- }
3954
- });
3955
-
3956
- server.route({
3957
- method: 'GET',
3958
- path: '/admin/templates/template/{template}/edit',
3959
- async handler(request, h) {
3960
- let template = await templates.get(request.params.template);
3961
- if (!template) {
3962
- let error = Boom.boomify(new Error('Template not found.'), { statusCode: 404 });
3963
- throw error;
3964
- }
3965
-
3966
- let account;
3967
- if (template.account) {
3968
- let accountObject = new Account({ redis, account: template.account });
3969
- account = await accountObject.loadAccountData();
3970
- }
3971
-
3972
- let accountTemplatesLink = new URL('/admin/templates', 'http://localhost');
3973
- if (account) {
3974
- accountTemplatesLink.searchParams.append('account', account.account);
3975
- }
3976
-
3977
- const values = {
3978
- template: template.id,
3979
- name: template.name,
3980
- description: template.description,
3981
- subject: template.content.subject,
3982
- format: template.format,
3983
- previewText: template.content.previewText
3984
- };
3985
-
3986
- return h.view(
3987
- 'templates/edit',
3988
- {
3989
- pageTitle: 'Templates',
3990
- menuTemplates: true,
3991
-
3992
- account,
3993
-
3994
- accountTemplatesLink: accountTemplatesLink.pathname + accountTemplatesLink.search,
3995
-
3996
- template,
3997
-
3998
- formats: CODE_FORMATS.map(format => Object.assign({ selected: format.format === values.format }, format)),
3999
-
4000
- values,
4001
-
4002
- contentHtmlJson: JSON.stringify(template.content.html || ''),
4003
- contentTextJson: JSON.stringify(template.content.text || '')
4004
- },
4005
- {
4006
- layout: 'app'
4007
- }
4008
- );
4009
- },
4010
-
4011
- options: {
4012
- validate: {
4013
- options: {
4014
- stripUnknown: true,
4015
- abortEarly: false,
4016
- convert: true
4017
- },
4018
-
4019
- async failAction(request, h /*, err*/) {
4020
- return h.redirect('/admin/templates').takeover();
4021
- },
4022
-
4023
- params: Joi.object({
4024
- template: Joi.string()
4025
- .base64({ paddingRequired: false, urlSafe: true })
4026
- .max(512)
4027
- .example('AAAAAQAACnA')
4028
- .required()
4029
- .description('Template ID')
4030
- })
4031
- }
4032
- }
4033
- });
4034
-
4035
- server.route({
4036
- method: 'POST',
4037
- path: '/admin/templates/edit',
4038
- async handler(request, h) {
4039
- try {
4040
- await templates.update(
4041
- request.payload.template,
4042
- {
4043
- name: request.payload.name,
4044
- description: request.payload.description,
4045
- format: request.payload.format
4046
- },
4047
- {
4048
- subject: request.payload.subject,
4049
- html: request.payload.contentHtml,
4050
- text: request.payload.contentText,
4051
- previewText: request.payload.previewText
4052
- }
4053
- );
4054
-
4055
- await request.flash({ type: 'info', message: `Template settings were updated` });
4056
- return h.redirect(`/admin/templates/template/${request.payload.template}`);
4057
- } catch (err) {
4058
- await request.flash({ type: 'danger', message: `Failed to update template` });
4059
- request.logger.error({ msg: 'Failed to update template', err });
4060
-
4061
- let template = await templates.get(request.payload.template);
4062
- if (!template) {
4063
- let error = Boom.boomify(new Error('Template not found.'), { statusCode: 404 });
4064
- throw error;
4065
- }
4066
-
4067
- let account;
4068
- if (template.account) {
4069
- let accountObject = new Account({ redis, account: template.account });
4070
- account = await accountObject.loadAccountData();
4071
- }
4072
-
4073
- let accountTemplatesLink = new URL('/admin/templates', 'http://localhost');
4074
- if (account) {
4075
- accountTemplatesLink.searchParams.append('account', account.account);
4076
- }
4077
-
4078
- return h.view(
4079
- 'templates/edit',
4080
- {
4081
- pageTitle: 'Templates',
4082
- menuTemplates: true,
4083
-
4084
- account,
4085
-
4086
- accountTemplatesLink: accountTemplatesLink.pathname + accountTemplatesLink.search,
4087
-
4088
- template,
4089
-
4090
- formats: CODE_FORMATS.map(format => Object.assign({ selected: format.format === request.payload.format }, format)),
4091
-
4092
- errors: err.details,
4093
-
4094
- contentHtmlJson: JSON.stringify(request.payload.contentHtml || ''),
4095
- contentTextJson: JSON.stringify(request.payload.contentText || '')
4096
- },
4097
- {
4098
- layout: 'app'
4099
- }
4100
- );
4101
- }
4102
- },
4103
- options: {
4104
- validate: {
4105
- options: {
4106
- stripUnknown: true,
4107
- abortEarly: false,
4108
- convert: true
4109
- },
4110
-
4111
- async failAction(request, h, err) {
4112
- let errors = {};
4113
-
4114
- if (err.details) {
4115
- err.details.forEach(detail => {
4116
- if (!errors[detail.path]) {
4117
- errors[detail.path] = detail.message;
4118
- }
4119
- });
4120
- }
4121
-
4122
- await request.flash({ type: 'danger', message: `Failed to update template` });
4123
- request.logger.error({ msg: 'Failed to update template', err });
4124
-
4125
- let template = await templates.get(request.payload.template);
4126
- if (!template) {
4127
- let error = Boom.boomify(new Error('Template not found.'), { statusCode: 404 });
4128
- throw error;
4129
- }
4130
-
4131
- let account;
4132
- if (template.account) {
4133
- let accountObject = new Account({ redis, account: template.account });
4134
- account = await accountObject.loadAccountData();
4135
- }
4136
-
4137
- let accountTemplatesLink = new URL('/admin/templates', 'http://localhost');
4138
- if (account) {
4139
- accountTemplatesLink.searchParams.append('account', account.account);
4140
- }
4141
-
4142
- return h
4143
- .view(
4144
- 'templates/edit',
4145
- {
4146
- pageTitle: 'Templates',
4147
- menuTemplates: true,
4148
-
4149
- account,
4150
-
4151
- accountTemplatesLink: accountTemplatesLink.pathname + accountTemplatesLink.search,
4152
-
4153
- template,
4154
-
4155
- formats: CODE_FORMATS.map(format => Object.assign({ selected: format.format === request.payload.format }, format)),
4156
-
4157
- errors,
4158
-
4159
- contentHtmlJson: JSON.stringify(request.payload.contentHtml || ''),
4160
- contentTextJson: JSON.stringify(request.payload.contentText || '')
4161
- },
4162
- {
4163
- layout: 'app'
4164
- }
4165
- )
4166
- .takeover();
4167
- },
4168
-
4169
- payload: Joi.object({
4170
- template: Joi.string()
4171
- .base64({ paddingRequired: false, urlSafe: true })
4172
- .max(512)
4173
- .example('AAAAAQAACnA')
4174
- .required()
4175
- .description('Template ID'),
4176
-
4177
- name: Joi.string().max(256).example('Transaction receipt').description('Name of the template').label('TemplateName').required(),
4178
- description: Joi.string()
4179
- .allow('')
4180
- .max(1024)
4181
- .example('Something about the template')
4182
- .description('Optional description of the template')
4183
- .label('TemplateDescription'),
4184
- format: Joi.string().valid('html', 'markdown').default('html').description('Markup language for HTML ("html" or "markdown")'),
4185
- subject: templateSchemas.subject,
4186
- contentText: templateSchemas.text,
4187
- contentHtml: templateSchemas.html,
4188
- previewText: templateSchemas.previewText
4189
- })
4190
- }
4191
- }
4192
- });
4193
-
4194
- server.route({
4195
- method: 'GET',
4196
- path: '/admin/templates/new',
4197
- async handler(request, h) {
4198
- let account;
4199
- if (request.query.account) {
4200
- let accountObject = new Account({ redis, account: request.query.account });
4201
- account = await accountObject.loadAccountData();
4202
- }
4203
-
4204
- let accountTemplatesLink = new URL('/admin/templates', 'http://localhost');
4205
- if (account) {
4206
- accountTemplatesLink.searchParams.append('account', account.account);
4207
- }
4208
-
4209
- const values = {
4210
- account: request.query.account,
4211
- name: '',
4212
- description: '',
4213
- subject: '',
4214
- format: 'html',
4215
- contentHtml: '',
4216
- contentText: '',
4217
- previewText: ''
4218
- };
4219
-
4220
- return h.view(
4221
- 'templates/new',
4222
- {
4223
- pageTitle: 'Templates',
4224
- menuTemplates: true,
4225
-
4226
- account,
4227
-
4228
- accountTemplatesLink: accountTemplatesLink.pathname + accountTemplatesLink.search,
4229
-
4230
- formats: CODE_FORMATS.map(format => Object.assign({ selected: format.format === values.format }, format)),
4231
-
4232
- values,
4233
-
4234
- contentHtmlJson: JSON.stringify(''),
4235
- contentTextJson: JSON.stringify('')
4236
- },
4237
- {
4238
- layout: 'app'
4239
- }
4240
- );
4241
- },
4242
-
4243
- options: {
4244
- validate: {
4245
- options: {
4246
- stripUnknown: true,
4247
- abortEarly: false,
4248
- convert: true
4249
- },
4250
-
4251
- async failAction(request, h /*, err*/) {
4252
- return h.redirect('/admin/templates').takeover();
4253
- },
4254
-
4255
- query: Joi.object({
4256
- account: accountIdSchema.default(null)
4257
- })
4258
- }
4259
- }
4260
- });
4261
-
4262
- server.route({
4263
- method: 'POST',
4264
- path: '/admin/templates/new',
4265
- async handler(request, h) {
4266
- try {
4267
- let createRequest = await templates.create(
4268
- request.payload.account,
4269
- {
4270
- name: request.payload.name,
4271
- description: request.payload.description,
4272
- format: request.payload.format
4273
- },
4274
- {
4275
- subject: request.payload.subject,
4276
- html: request.payload.contentHtml,
4277
- text: request.payload.contentText,
4278
- previewText: request.payload.previewText
4279
- }
4280
- );
4281
-
4282
- await request.flash({ type: 'info', message: `Template was created` });
4283
- return h.redirect(`/admin/templates/template/${createRequest.id}`);
4284
- } catch (err) {
4285
- await request.flash({ type: 'danger', message: `Failed to create template` });
4286
- request.logger.error({ msg: 'Failed to create template', err });
4287
-
4288
- let account;
4289
- if (request.payload.account) {
4290
- let accountObject = new Account({ redis, account: request.payload.account });
4291
- account = await accountObject.loadAccountData();
4292
- }
4293
-
4294
- let accountTemplatesLink = new URL('/admin/templates', 'http://localhost');
4295
- if (account) {
4296
- accountTemplatesLink.searchParams.append('account', account.account);
4297
- }
4298
-
4299
- return h.view(
4300
- 'templates/new',
4301
- {
4302
- pageTitle: 'Templates',
4303
- menuTemplates: true,
4304
-
4305
- account,
4306
-
4307
- accountTemplatesLink: accountTemplatesLink.pathname + accountTemplatesLink.search,
4308
-
4309
- formats: CODE_FORMATS.map(format => Object.assign({ selected: format.format === request.payload.format }, format)),
4310
-
4311
- errors: err.details,
4312
-
4313
- contentHtmlJson: JSON.stringify(request.payload.contentHtml || ''),
4314
- contentTextJson: JSON.stringify(request.payload.contentText || '')
4315
- },
4316
- {
4317
- layout: 'app'
4318
- }
4319
- );
4320
- }
4321
- },
4322
- options: {
4323
- validate: {
4324
- options: {
4325
- stripUnknown: true,
4326
- abortEarly: false,
4327
- convert: true
4328
- },
4329
-
4330
- async failAction(request, h, err) {
4331
- let errors = {};
4332
-
4333
- if (err.details) {
4334
- err.details.forEach(detail => {
4335
- if (!errors[detail.path]) {
4336
- errors[detail.path] = detail.message;
4337
- }
4338
- });
4339
- }
4340
-
4341
- await request.flash({ type: 'danger', message: `Failed to create template` });
4342
- request.logger.error({ msg: 'Failed to create template', err });
4343
-
4344
- let account;
4345
- if (request.payload.account) {
4346
- let accountObject = new Account({ redis, account: request.payload.account });
4347
- account = await accountObject.loadAccountData();
4348
- }
4349
-
4350
- let accountTemplatesLink = new URL('/admin/templates', 'http://localhost');
4351
- if (account) {
4352
- accountTemplatesLink.searchParams.append('account', account.account);
4353
- }
4354
-
4355
- return h
4356
- .view(
4357
- 'templates/new',
4358
- {
4359
- pageTitle: 'Templates',
4360
- menuTemplates: true,
4361
-
4362
- account,
4363
-
4364
- accountTemplatesLink: accountTemplatesLink.pathname + accountTemplatesLink.search,
4365
-
4366
- formats: CODE_FORMATS.map(format => Object.assign({ selected: format.format === request.payload.format }, format)),
4367
-
4368
- errors,
4369
-
4370
- contentHtmlJson: JSON.stringify(request.payload.contentHtml || ''),
4371
- contentTextJson: JSON.stringify(request.payload.contentText || '')
4372
- },
4373
- {
4374
- layout: 'app'
4375
- }
4376
- )
4377
- .takeover();
4378
- },
4379
-
4380
- payload: Joi.object({
4381
- account: accountIdSchema.default(null),
4382
-
4383
- name: Joi.string().max(256).example('Transaction receipt').description('Name of the template').label('TemplateName').required(),
4384
- description: Joi.string()
4385
- .allow('')
4386
- .max(1024)
4387
- .example('Something about the template')
4388
- .description('Optional description of the template')
4389
- .label('TemplateDescription'),
4390
- format: Joi.string().valid('html', 'markdown').default('html').description('Markup language for HTML ("html" or "markdown")'),
4391
- subject: templateSchemas.subject,
4392
- contentText: templateSchemas.text,
4393
- contentHtml: templateSchemas.html,
4394
- previewText: templateSchemas.previewText
4395
- })
4396
- }
4397
- }
4398
- });
4399
-
4400
- server.route({
4401
- method: 'POST',
4402
- path: '/admin/templates/delete',
4403
- async handler(request, h) {
4404
- try {
4405
- let templateResponse = await templates.del(request.payload.template);
4406
-
4407
- await request.flash({ type: 'info', message: `Template was deleted` });
4408
-
4409
- let accountTemplatesLink = new URL('/admin/templates', 'http://localhost');
4410
- if (templateResponse && templateResponse.account) {
4411
- accountTemplatesLink.searchParams.append('account', templateResponse.account);
4412
- }
4413
-
4414
- return h.redirect(accountTemplatesLink.pathname + accountTemplatesLink.search);
4415
- } catch (err) {
4416
- await request.flash({ type: 'danger', message: `Failed to delete the template` });
4417
- request.logger.error({ msg: 'Failed to delete the template', err, template: request.payload.template, remoteAddress: request.app.ip });
4418
- return h.redirect(`/admin/templates/template/${request.payload.template}`);
4419
- }
4420
- },
4421
- options: {
4422
- validate: {
4423
- options: {
4424
- stripUnknown: true,
4425
- abortEarly: false,
4426
- convert: true
4427
- },
4428
-
4429
- async failAction(request, h, err) {
4430
- await request.flash({ type: 'danger', message: `Failed to delete the account` });
4431
- request.logger.error({ msg: 'Failed to delete delete the account', err });
4432
-
4433
- return h.redirect('/admin/templates').takeover();
4434
- },
4435
-
4436
- payload: Joi.object({
4437
- template: Joi.string()
4438
- .base64({ paddingRequired: false, urlSafe: true })
4439
- .max(512)
4440
- .example('AAAAAQAACnA')
4441
- .required()
4442
- .description('Template ID')
4443
- })
4444
- }
4445
- }
4446
- });
4447
-
4448
- server.route({
4449
- method: 'POST',
4450
- path: '/admin/templates/test',
4451
- async handler(request) {
4452
- try {
4453
- request.logger.info({ msg: 'Trying to send test message', payload: request.payload });
4454
-
4455
- let template = await templates.get(request.payload.template);
4456
- if (!template) {
4457
- return {
4458
- error: 'Template was not found'
4459
- };
4460
- }
4461
-
4462
- let accountId = template.account || request.payload.account;
4463
- if (!accountId) {
4464
- return { error: 'Account ID not provided' };
4465
- }
4466
-
4467
- let accountObject = new Account({ redis, account: accountId, call, secret: await getSecret() });
4468
-
4469
- let account;
4470
- try {
4471
- account = await accountObject.loadAccountData();
4472
- } catch (err) {
4473
- return {
4474
- error: err.message
4475
- };
4476
- }
4477
-
4478
- try {
4479
- return await accountObject.queueMessage(
4480
- {
4481
- account: account.account,
4482
- template: template.id,
4483
- from: {
4484
- name: account.name,
4485
- address: account.email
4486
- },
4487
- to: [{ name: '', address: request.payload.to }],
4488
- render: {
4489
- params: request.payload.params || {}
4490
- },
4491
- copy: false,
4492
- deliveryAttempts: 0
4493
- },
4494
- { source: 'ui' }
4495
- );
4496
- } catch (err) {
4497
- return {
4498
- error: err.message
4499
- };
4500
- }
4501
- } catch (err) {
4502
- request.logger.error({ msg: 'Failed sending test message', err });
4503
- return {
4504
- success: false,
4505
- error: err.message
4506
- };
4507
- }
4508
- },
4509
- options: {
4510
- tags: ['test'],
4511
- validate: {
4512
- options: {
4513
- stripUnknown: true,
4514
- abortEarly: false,
4515
- convert: true
4516
- },
4517
-
4518
- failAction,
4519
-
4520
- payload: Joi.object({
4521
- account: accountIdSchema.default(null),
4522
- template: Joi.string()
4523
- .base64({ paddingRequired: false, urlSafe: true })
4524
- .max(512)
4525
- .example('AAAAAQAACnA')
4526
- .required()
4527
- .description('Template ID'),
4528
- to: Joi.string().email().required().description('Recipient address'),
4529
- params: Joi.object().description('Optional handlebars values').unknown()
4530
- })
4531
- }
4532
- }
4533
- });
4534
-
4535
- server.route({
4536
- method: 'GET',
4537
- path: '/admin/gateways',
4538
- async handler(request, h) {
4539
- let gatewayObject = new Gateway({ redis });
4540
-
4541
- let gateways = await gatewayObject.listGateways(request.query.page - 1, request.query.pageSize);
4542
-
4543
- if (gateways.pages < request.query.page) {
4544
- request.query.page = gateways.pages;
4545
- }
4546
-
4547
- let nextPage = false;
4548
- let prevPage = false;
4549
-
4550
- let getPagingUrl = page => {
4551
- let url = new URL(`admin/gateways`, 'http://localhost');
4552
- url.searchParams.append('page', page);
4553
- if (request.query.pageSize !== DEFAULT_PAGE_SIZE) {
4554
- url.searchParams.append('pageSize', request.query.pageSize);
4555
- }
4556
- return url.pathname + url.search;
4557
- };
4558
-
4559
- if (gateways.pages > gateways.page + 1) {
4560
- nextPage = getPagingUrl(gateways.page + 2);
4561
- }
4562
-
4563
- if (gateways.page > 0) {
4564
- prevPage = getPagingUrl(gateways.page);
4565
- }
4566
-
4567
- return h.view(
4568
- 'gateways/index',
4569
- {
4570
- pageTitle: 'Email Gateways',
4571
- menuGateways: true,
4572
-
4573
- showPaging: gateways.pages > 1,
4574
- nextPage,
4575
- prevPage,
4576
- firstPage: gateways.page === 0,
4577
- pageLinks: new Array(gateways.pages || 1).fill(0).map((z, i) => ({
4578
- url: getPagingUrl(i + 1),
4579
- title: i + 1,
4580
- active: i === gateways.page
4581
- })),
4582
-
4583
- gateways: gateways.gateways.map(entry => {
4584
- let label = {};
4585
- if (entry.deliveries && !entry.lastError) {
4586
- label.type = 'success';
4587
- label.name = 'Connected';
4588
- } else if (entry.lastError) {
4589
- label.type = 'danger';
4590
- label.name = 'Error';
4591
- label.error = entry.lastError.response;
4592
- } else {
4593
- label.type = 'info';
4594
- label.name = 'Not used';
4595
- }
4596
-
4597
- return Object.assign(entry, {
4598
- timeStr: entry.lastUse ? entry.lastUse.toISOString() : null,
4599
- label
4600
- });
4601
- })
4602
- },
4603
- {
4604
- layout: 'app'
4605
- }
4606
- );
4607
- },
4608
-
4609
- options: {
4610
- validate: {
4611
- options: {
4612
- stripUnknown: true,
4613
- abortEarly: false,
4614
- convert: true
4615
- },
4616
-
4617
- async failAction(request, h /*, err*/) {
4618
- return h.redirect('/admin/gateways').takeover();
4619
- },
4620
-
4621
- query: Joi.object({
4622
- page: Joi.number().integer().min(1).max(1000000).default(1),
4623
- pageSize: Joi.number().integer().min(1).max(250).default(DEFAULT_PAGE_SIZE)
4624
- })
4625
- }
4626
- }
4627
- });
4628
-
4629
- server.route({
4630
- method: 'GET',
4631
- path: '/admin/gateways/new',
4632
- async handler(request, h) {
4633
- return h.view(
4634
- 'gateways/new',
4635
- {
4636
- pageTitle: 'Email Gateways',
4637
- menuGateways: true,
4638
- wellKnownServices: JSON.stringify(Object.keys(wellKnownServices).map(key => Object.assign({ key }, wellKnownServices[key])))
4639
- },
4640
- {
4641
- layout: 'app'
4642
- }
4643
- );
4644
- }
4645
- });
4646
-
4647
- server.route({
4648
- method: 'GET',
4649
- path: '/admin/gateways/gateway/{gateway}',
4650
- async handler(request, h) {
4651
- let gatewayObject = new Gateway({ gateway: request.params.gateway, redis, secret: await getSecret() });
4652
- let gatewayData = await gatewayObject.loadGatewayData();
4653
-
4654
- let label = {};
4655
- if (gatewayData.deliveries && !gatewayData.lastError) {
4656
- label.type = 'success';
4657
- label.name = 'Connected';
4658
- } else if (gatewayData.lastError) {
4659
- label.type = 'danger';
4660
- label.name = 'Error';
4661
- label.error = gatewayData.lastError.response;
4662
- } else {
4663
- label.type = 'info';
4664
- label.name = 'Not used';
4665
- }
4666
-
4667
- return h.view(
4668
- 'gateways/gateway',
4669
- {
4670
- pageTitle: 'Email Gateways',
4671
- menuGateways: true,
4672
- wellKnownServices: JSON.stringify(Object.keys(wellKnownServices).map(key => Object.assign({ key }, wellKnownServices[key]))),
4673
-
4674
- gateway: gatewayData,
4675
- label
4676
- },
4677
- {
4678
- layout: 'app'
4679
- }
4680
- );
4681
- },
4682
-
4683
- options: {
4684
- validate: {
4685
- options: {
4686
- stripUnknown: true,
4687
- abortEarly: false,
4688
- convert: true
4689
- },
4690
-
4691
- async failAction(request, h, err) {
4692
- await request.flash({ type: 'danger', message: `Invalid gateway request: ${err.message}` });
4693
- return h.redirect('/admin/gateways').takeover();
4694
- },
4695
-
4696
- params: Joi.object({
4697
- gateway: Joi.string().max(256).required().example('sendgun').description('Gateway ID')
4698
- })
4699
- }
4700
- }
4701
- });
4702
-
4703
- server.route({
4704
- method: 'GET',
4705
- path: '/admin/gateways/edit/{gateway}',
4706
- async handler(request, h) {
4707
- let gatewayObject = new Gateway({ gateway: request.params.gateway, redis, secret: await getSecret() });
4708
- let gatewayData = await gatewayObject.loadGatewayData();
4709
-
4710
- let hasSMTPPass = !!gatewayData.pass;
4711
- delete gatewayData.pass;
4712
-
4713
- return h.view(
4714
- 'gateways/edit',
4715
- {
4716
- pageTitle: 'Email Gateways',
4717
- menuGateways: true,
4718
- wellKnownServices: JSON.stringify(Object.keys(wellKnownServices).map(key => Object.assign({ key }, wellKnownServices[key]))),
4719
- values: gatewayData,
4720
- gatewayData,
4721
- hasSMTPPass
4722
- },
4723
- {
4724
- layout: 'app'
4725
- }
4726
- );
4727
- },
4728
-
4729
- options: {
4730
- validate: {
4731
- options: {
4732
- stripUnknown: true,
4733
- abortEarly: false,
4734
- convert: true
4735
- },
4736
-
4737
- async failAction(request, h, err) {
4738
- await request.flash({ type: 'danger', message: `Invalid gateway request: ${err.message}` });
4739
- return h.redirect('/admin/gateways').takeover();
4740
- },
4741
-
4742
- params: Joi.object({
4743
- gateway: Joi.string().max(256).required().example('sendgun').description('Gateway ID')
4744
- })
4745
- }
4746
- }
4747
- });
4748
-
4749
- server.route({
4750
- method: 'POST',
4751
- path: '/admin/gateways/new',
4752
- async handler(request, h) {
4753
- try {
4754
- let gatewayData = {
4755
- gateway: request.payload.gateway || null,
4756
- name: request.payload.name || null,
4757
- host: request.payload.host || null,
4758
- port: request.payload.port || null,
4759
- secure: request.payload.secure || null,
4760
- user: request.payload.user || null,
4761
- pass: request.payload.pass || null,
4762
- tls: {}
4763
- };
4764
-
4765
- let gatewayObject = new Gateway({ redis, secret: await getSecret() });
4766
- let result = await gatewayObject.create(gatewayData);
4767
-
4768
- if (result.state === 'new') {
4769
- await request.flash({ type: 'success', message: `Added new SMTP gateway`, result });
4770
- } else {
4771
- await request.flash({ type: 'success', message: `Updated SMTP gateway`, result });
4772
- }
4773
-
4774
- return h.redirect(`/admin/gateways/gateway/${encodeURIComponent(result.gateway)}?state=${result.state}`);
4775
- } catch (err) {
4776
- await request.flash({ type: 'danger', message: `Failed to add new gateway` });
4777
- request.logger.error({ msg: 'Failed to add new gateway', err });
4778
-
4779
- return h.view(
4780
- 'gateways/new',
4781
- {
4782
- pageTitle: 'Email Gateways',
4783
- menuGateways: true,
4784
- wellKnownServices: JSON.stringify(Object.keys(wellKnownServices).map(key => Object.assign({ key }, wellKnownServices[key])))
4785
- },
4786
- {
4787
- layout: 'app'
4788
- }
4789
- );
4790
- }
4791
- },
4792
-
4793
- options: {
4794
- validate: {
4795
- options: {
4796
- stripUnknown: true,
4797
- abortEarly: false,
4798
- convert: true
4799
- },
4800
-
4801
- async failAction(request, h, err) {
4802
- let errors = {};
4803
-
4804
- if (err.details) {
4805
- err.details.forEach(detail => {
4806
- if (!errors[detail.path]) {
4807
- errors[detail.path] = detail.message;
4808
- }
4809
- });
4810
- }
4811
-
4812
- await request.flash({ type: 'danger', message: `Failed to add new gateway` });
4813
- request.logger.error({ msg: 'Failed to add new gateway', err });
4814
-
4815
- return h
4816
- .view(
4817
- 'gateways/new',
4818
- {
4819
- pageTitle: 'Email Gateways',
4820
- menuGateways: true,
4821
- wellKnownServices: JSON.stringify(Object.keys(wellKnownServices).map(key => Object.assign({ key }, wellKnownServices[key]))),
4822
-
4823
- errors
4824
- },
4825
- {
4826
- layout: 'app'
4827
- }
4828
- )
4829
- .takeover();
4830
- },
4831
-
4832
- payload: Joi.object({
4833
- gateway: Joi.string().empty('').trim().max(256).default(null).example('sendgun').description('Gateway ID').label('Gateway ID'),
4834
-
4835
- name: Joi.string().empty('').max(256).example('John Smith').description('Account Name').label('Gateway Name').required(),
4836
-
4837
- user: Joi.string().empty('').trim().max(1024).default(null).label('UserName'),
4838
- pass: Joi.string().empty('').max(1024).default(null).label('Password'),
4839
-
4840
- host: Joi.string().hostname().example('smtp.gmail.com').description('Hostname to connect to').label('Hostname').required(),
4841
- port: Joi.number()
4842
- .integer()
4843
- .min(1)
4844
- .max(64 * 1024)
4845
- .example(465)
4846
- .description('Service port number')
4847
- .label('Port')
4848
- .required(),
4849
-
4850
- secure: Joi.boolean()
4851
- .truthy('Y', 'true', '1', 'on')
4852
- .falsy('N', 'false', 0, '')
4853
- .default(false)
4854
- .example(true)
4855
- .description('Should connection use TLS. Usually true for port 465')
4856
- .label('TLS')
4857
- })
4858
- }
4859
- }
4860
- });
4861
-
4862
- server.route({
4863
- method: 'POST',
4864
- path: '/admin/gateways/edit',
4865
- async handler(request, h) {
4866
- try {
4867
- let gatewayData = {
4868
- gateway: request.payload.gateway || null,
4869
- name: request.payload.name || null,
4870
- host: request.payload.host || null,
4871
- port: request.payload.port || null,
4872
- secure: request.payload.secure || null,
4873
- user: request.payload.user || null
4874
- };
4875
-
4876
- if (request.payload.pass) {
4877
- gatewayData.pass = request.payload.pass;
4878
- }
4879
-
4880
- if (!request.payload.user && !request.payload.pass) {
4881
- gatewayData.pass = null;
4882
- }
4883
-
4884
- let gatewayObject = new Gateway({ gateway: request.payload.gateway, redis, secret: await getSecret() });
4885
- let result = await gatewayObject.update(gatewayData);
4886
-
4887
- await request.flash({ type: 'success', message: `Updated SMTP gateway`, result });
4888
-
4889
- return h.redirect(`/admin/gateways/gateway/${encodeURIComponent(result.gateway)}`);
4890
- } catch (err) {
4891
- await request.flash({ type: 'danger', message: `Failed to update gateway` });
4892
- request.logger.error({ msg: 'Failed to update gateway', err });
4893
-
4894
- let gatewayObject = new Gateway({ gateway: request.payload.gateway, redis, secret: await getSecret() });
4895
- let gatewayData = await gatewayObject.loadGatewayData();
4896
-
4897
- let hasSMTPPass = !!gatewayData.pass;
4898
-
4899
- return h.view(
4900
- 'gateways/edit',
4901
- {
4902
- pageTitle: 'Email Gateways',
4903
- menuGateways: true,
4904
- wellKnownServices: JSON.stringify(Object.keys(wellKnownServices).map(key => Object.assign({ key }, wellKnownServices[key]))),
4905
- hasSMTPPass,
4906
- gatewayData
4907
- },
4908
- {
4909
- layout: 'app'
4910
- }
4911
- );
4912
- }
4913
- },
4914
-
4915
- options: {
4916
- validate: {
4917
- options: {
4918
- stripUnknown: true,
4919
- abortEarly: false,
4920
- convert: true
4921
- },
4922
-
4923
- async failAction(request, h, err) {
4924
- let errors = {};
4925
-
4926
- if (err.details) {
4927
- err.details.forEach(detail => {
4928
- if (!errors[detail.path]) {
4929
- errors[detail.path] = detail.message;
4930
- }
4931
- });
4932
- }
4933
-
4934
- await request.flash({ type: 'danger', message: `Failed to update gateway` });
4935
- request.logger.error({ msg: 'Failed to update gateway', err });
4936
-
4937
- let gatewayObject = new Gateway({ gateway: request.payload.gateway, redis, secret: await getSecret() });
4938
- let gatewayData = await gatewayObject.loadGatewayData();
4939
-
4940
- let hasSMTPPass = !!gatewayData.pass;
4941
-
4942
- return h
4943
- .view(
4944
- 'gateways/edit',
4945
- {
4946
- pageTitle: 'Email Gateways',
4947
- menuGateways: true,
4948
- wellKnownServices: JSON.stringify(Object.keys(wellKnownServices).map(key => Object.assign({ key }, wellKnownServices[key]))),
4949
- hasSMTPPass,
4950
- gatewayData,
4951
-
4952
- errors
4953
- },
4954
- {
4955
- layout: 'app'
4956
- }
4957
- )
4958
- .takeover();
4959
- },
4960
-
4961
- payload: Joi.object({
4962
- gateway: Joi.string().empty('').trim().max(256).default(null).example('sendgun').description('Gateway ID').label('Gateway ID').required(),
4963
-
4964
- name: Joi.string().empty('').max(256).example('John Smith').description('Account Name').label('Gateway Name').required(),
4965
-
4966
- user: Joi.string().empty('').trim().max(1024).default(null).label('UserName'),
4967
- pass: Joi.string().empty('').max(1024).default(null).label('Password'),
4968
-
4969
- host: Joi.string().hostname().example('smtp.gmail.com').description('Hostname to connect to').label('Hostname').required(),
4970
- port: Joi.number()
4971
- .integer()
4972
- .min(1)
4973
- .max(64 * 1024)
4974
- .example(465)
4975
- .description('Service port number')
4976
- .label('Port')
4977
- .required(),
4978
-
4979
- secure: Joi.boolean()
4980
- .truthy('Y', 'true', '1', 'on')
4981
- .falsy('N', 'false', 0, '')
4982
- .default(false)
4983
- .example(true)
4984
- .description('Should connection use TLS. Usually true for port 465')
4985
- .label('TLS')
4986
- })
4987
- }
4988
- }
4989
- });
4990
-
4991
- server.route({
4992
- method: 'POST',
4993
- path: '/admin/gateways/test',
4994
- async handler(request) {
4995
- let { gateway, host, port, user, pass, secure } = request.payload;
4996
-
4997
- try {
4998
- if (user && !pass && gateway) {
4999
- let gatewayObject = new Gateway({ gateway, redis, secret: await getSecret() });
5000
- try {
5001
- let gatewayData = await gatewayObject.loadGatewayData();
5002
- if (gatewayData) {
5003
- pass = gatewayData.pass || '';
5004
- }
5005
- } catch (err) {
5006
- // ignore
5007
- }
5008
- }
5009
-
5010
- let accountData = {
5011
- smtp: {
5012
- host,
5013
- port,
5014
- secure,
5015
- auth:
5016
- user || pass
5017
- ? {
5018
- user,
5019
- pass: pass || ''
5020
- }
5021
- : false
5022
- }
5023
- };
5024
-
5025
- let verifyResult = await verifyAccountInfo(redis, accountData, request.logger.child({ gateway, action: 'verify-gateway' }));
5026
-
5027
- if (verifyResult) {
5028
- if (verifyResult.smtp && verifyResult.smtp.error && verifyResult.smtp.code) {
5029
- switch (verifyResult.smtp.code) {
5030
- case 'EDNS':
5031
- verifyResult.smtp.error = request.app.gt.gettext('Server hostname was not found');
5032
- break;
5033
- case 'EAUTH':
5034
- verifyResult.smtp.error = request.app.gt.gettext('Invalid username or password');
5035
- break;
5036
- case 'ESOCKET':
5037
- if (/openssl/.test(verifyResult.smtp.error)) {
5038
- verifyResult.smtp.error = request.app.gt.gettext('TLS protocol error');
5039
- }
5040
- break;
5041
- }
5042
- }
5043
- }
5044
-
5045
- return verifyResult.smtp;
5046
- } catch (err) {
5047
- request.logger.error({ msg: 'Failed posting request', host, port, user, pass: !!pass, err });
5048
- return {
5049
- success: false,
5050
- error: err.message
5051
- };
5052
- }
5053
- },
5054
- options: {
5055
- tags: ['test'],
5056
- validate: {
5057
- options: {
5058
- stripUnknown: true,
5059
- abortEarly: false,
5060
- convert: true
5061
- },
5062
-
5063
- failAction,
5064
-
5065
- payload: Joi.object({
5066
- gateway: Joi.string().empty('').trim().max(256).example('sendgun').description('Gateway ID'),
5067
- user: Joi.string().empty('').trim().max(1024).label('UserName'),
5068
- pass: Joi.string().empty('').max(1024).label('Password'),
5069
- host: Joi.string().hostname().example('smtp.gmail.com').description('Hostname to connect to').label('Hostname'),
5070
- port: Joi.number()
5071
- .integer()
5072
- .min(1)
5073
- .max(64 * 1024)
5074
- .example(465)
5075
- .description('Service port number')
5076
- .label('Port'),
5077
- secure: Joi.boolean()
5078
- .truthy('Y', 'true', '1', 'on')
5079
- .falsy('N', 'false', 0, '')
5080
- .default(false)
5081
- .example(true)
5082
- .description('Should connection use TLS. Usually true for port 465')
5083
- .label('TLS')
5084
- })
5085
- }
5086
- }
5087
- });
5088
-
5089
- server.route({
5090
- method: 'POST',
5091
- path: '/admin/gateways/delete/{gateway}',
5092
- async handler(request, h) {
5093
- try {
5094
- let gatewayObject = new Gateway({ redis, gateway: request.params.gateway, secret: await getSecret() });
5095
-
5096
- let deleted = await gatewayObject.delete();
5097
- if (deleted) {
5098
- await request.flash({ type: 'info', message: `Gateway was deleted` });
5099
- }
5100
-
5101
- return h.redirect('/admin/gateways');
5102
- } catch (err) {
5103
- await request.flash({ type: 'danger', message: `Failed to delete the gateway` });
5104
- request.logger.error({ msg: 'Failed to delete the gateway', err, gateway: request.payload.gateway, remoteAddress: request.app.ip });
5105
- return h.redirect(`/admin/gateways/${request.params.gateway}`);
5106
- }
5107
- },
5108
- options: {
5109
- validate: {
5110
- options: {
5111
- stripUnknown: true,
5112
- abortEarly: false,
5113
- convert: true
5114
- },
5115
-
5116
- async failAction(request, h, err) {
5117
- await request.flash({ type: 'danger', message: `Failed to delete the gateway` });
5118
- request.logger.error({ msg: 'Failed to delete delete the gateway', err });
5119
-
5120
- return h.redirect('/admin/gateways').takeover();
5121
- },
5122
-
5123
- params: Joi.object({
5124
- gateway: Joi.string().max(256).required().example('sendgun').description('Gateway ID')
5125
- })
5126
- }
5127
- }
5128
- });
5129
-
5130
- server.route({
5131
- method: 'GET',
5132
- path: '/admin/tokens',
5133
- async handler(request, h) {
5134
- let accountData;
5135
- if (request.query.account) {
5136
- let accountObject = new Account({ redis, account: request.query.account });
5137
- accountData = await accountObject.loadAccountData();
5138
- }
5139
-
5140
- const data = await tokens.list(request.query.account, request.query.page - 1, request.query.pageSize);
5141
-
5142
- data.tokens.forEach(entry => {
5143
- entry.access = entry.access || {};
5144
- entry.access.timeStr =
5145
- entry.access && entry.access.time && typeof entry.access.time.toISOString === 'function' ? entry.access.time.toISOString() : null;
5146
- entry.scopes = entry.scopes
5147
- ? entry.scopes.map((scope, i) => ({
5148
- name: scope === '*' ? 'all scopes' : scope,
5149
- first: !i
5150
- }))
5151
- : false;
5152
- });
5153
-
5154
- let nextPage = false;
5155
- let prevPage = false;
5156
-
5157
- let getPagingUrl = page => {
5158
- let url = new URL(`admin/tokens`, 'http://localhost');
5159
-
5160
- if (page) {
5161
- url.searchParams.append('page', page);
5162
- }
5163
-
5164
- if (request.query.pageSize !== DEFAULT_PAGE_SIZE) {
5165
- url.searchParams.append('pageSize', request.query.pageSize);
5166
- }
5167
-
5168
- return url.pathname + url.search;
5169
- };
5170
-
5171
- if (data.pages > data.page + 1) {
5172
- nextPage = getPagingUrl(data.page + 2);
5173
- }
5174
-
5175
- if (data.page > 0) {
5176
- prevPage = getPagingUrl(data.page);
5177
- }
5178
-
5179
- let newLink = new URL('/admin/tokens/new', 'http://localhost');
5180
- if (request.query.account) {
5181
- newLink.searchParams.append('account', request.query.account);
5182
- }
5183
-
5184
- return h.view(
5185
- 'tokens/index',
5186
- {
5187
- pageTitle: 'Access Tokens',
5188
- menuTokens: true,
5189
- data,
5190
-
5191
- account: accountData,
5192
-
5193
- showPaging: data.pages > 1,
5194
- nextPage,
5195
- prevPage,
5196
- firstPage: data.page === 0,
5197
- pageLinks: new Array(data.pages || 1).fill(0).map((z, i) => ({
5198
- url: getPagingUrl(i + 1, request.query.state, request.query.query),
5199
- title: i + 1,
5200
- active: i === data.page
5201
- })),
2833
+ defaultRedirectUrl,
5202
2834
 
5203
- newLink: newLink.pathname + newLink.search
5204
- },
5205
- {
5206
- layout: 'app'
5207
- }
5208
- );
5209
- },
2835
+ appData,
5210
2836
 
5211
- options: {
5212
- validate: {
5213
- options: {
5214
- stripUnknown: true,
5215
- abortEarly: false,
5216
- convert: true
5217
- },
2837
+ hasClientSecret: !!appData.clientSecret,
2838
+ hasServiceKey: !!appData.serviceKey,
5218
2839
 
5219
- async failAction(request, h /*, err*/) {
5220
- return h.redirect('/admin/tokens').takeover();
5221
- },
2840
+ pubSubApps:
2841
+ pubSubApps &&
2842
+ pubSubApps.apps &&
2843
+ pubSubApps.apps.map(app => {
2844
+ if (app.id === values.pubSubApp) {
2845
+ app.selected = true;
2846
+ }
2847
+ return app;
2848
+ }),
5222
2849
 
5223
- query: Joi.object({
5224
- account: accountIdSchema.default(null),
5225
- page: Joi.number().integer().min(1).max(1000000).default(1),
5226
- pageSize: Joi.number().integer().min(1).max(250).default(DEFAULT_PAGE_SIZE)
5227
- })
5228
- }
5229
- }
5230
- });
2850
+ values,
5231
2851
 
5232
- server.route({
5233
- method: 'GET',
5234
- path: '/admin/tokens/new',
5235
- async handler(request, h) {
5236
- let accountTokensLink = new URL('/admin/tokens', 'http://localhost');
2852
+ baseScopesApi: values.baseScopes === 'api',
2853
+ baseScopesImap: values.baseScopes === 'imap' || !values.baseScopes,
2854
+ baseScopesPubsub: values.baseScopes === 'pubsub',
5237
2855
 
5238
- let accountData;
5239
- if (request.query.account) {
5240
- let accountObject = new Account({ redis, account: request.query.account });
5241
- accountData = await accountObject.loadAccountData();
5242
- accountTokensLink.searchParams.append('account', request.query.account);
5243
- }
2856
+ azureClouds: structuredClone(AZURE_CLOUDS).map(entry => {
2857
+ entry.selected = values.cloud === entry.id;
2858
+ return entry;
2859
+ }),
5244
2860
 
5245
- return h.view(
5246
- 'tokens/new',
5247
- {
5248
- pageTitle: 'Access Tokens',
5249
- menuTokens: true,
5250
- values: {
5251
- scopesAll: true,
5252
- allAccounts: !request.query.account,
5253
- account: request.query.account
5254
- },
5255
- account: accountData,
5256
- accountTokensLink: accountTokensLink.pathname + accountTokensLink.search
2861
+ authorityCommon: values.authority === 'common',
2862
+ authorityOrganizations: values.authority === 'organizations',
2863
+ authorityConsumers: values.authority === 'consumers',
2864
+ authorityTenant: !!values.tenant
5257
2865
  },
5258
2866
  {
5259
2867
  layout: 'app'
@@ -5270,11 +2878,11 @@ return payload;`)
5270
2878
  },
5271
2879
 
5272
2880
  async failAction(request, h /*, err*/) {
5273
- return h.redirect('/admin/tokens').takeover();
2881
+ return h.redirect('/admin/config/oauth').takeover();
5274
2882
  },
5275
2883
 
5276
- query: Joi.object({
5277
- account: accountIdSchema.default(null)
2884
+ params: Joi.object({
2885
+ app: Joi.string().empty('').max(255).example('gmail').label('Provider').required()
5278
2886
  })
5279
2887
  }
5280
2888
  }
@@ -5282,74 +2890,99 @@ return payload;`)
5282
2890
 
5283
2891
  server.route({
5284
2892
  method: 'POST',
5285
- path: '/admin/tokens/new',
2893
+ path: '/admin/config/oauth/edit',
2894
+ async handler(request, h) {
2895
+ let appData = await oauth2Apps.get(request.payload.app);
2896
+ if (!appData) {
2897
+ let error = Boom.boomify(new Error('Application was not found.'), { statusCode: 404 });
2898
+ throw error;
2899
+ }
5286
2900
 
5287
- async handler(request) {
5288
2901
  try {
5289
- let data = {
5290
- ip: request.app.ip,
5291
- remoteAddress: request.app.ip,
5292
- description: request.payload.description,
5293
- scopes: request.payload.scopes
5294
- };
2902
+ let updates = Object.assign({}, request.payload);
2903
+ updates.extraScopes = updates.extraScopes
2904
+ .split(/\s+/)
2905
+ .map(scope => scope.trim())
2906
+ .filter(scope => scope);
2907
+
2908
+ updates.skipScopes = updates.skipScopes
2909
+ .split(/\s+/)
2910
+ .map(scope => scope.trim())
2911
+ .filter(scope => scope);
5295
2912
 
5296
- if (request.payload.account) {
5297
- let accountObject = new Account({ redis, account: request.payload.account });
5298
- await accountObject.loadAccountData();
5299
- data.account = request.payload.account;
2913
+ if (updates.authority === 'tenant') {
2914
+ updates.authority = updates.tenant;
5300
2915
  }
2916
+ delete updates.tenant;
5301
2917
 
5302
- let token = await tokens.provision(data);
2918
+ let oauth2App = await oauth2Apps.update(appData.id, updates);
2919
+ if (!oauth2App || !oauth2App.id) {
2920
+ throw new Error('Unexpected result');
2921
+ }
5303
2922
 
5304
- return {
5305
- success: true,
5306
- token
5307
- };
5308
- } catch (err) {
5309
- request.logger.error({ msg: 'Failed to generate token', err, remoteAddress: request.app.ip, description: request.payload.description });
5310
- if (Boom.isBoom(err)) {
5311
- return Object.assign({ success: false }, err.output.payload);
2923
+ if (oauth2App && oauth2App.pubsubUpdates && oauth2App.pubsubUpdates.pubSubSubscription) {
2924
+ await call({ cmd: 'googlePubSub', app: oauth2App.id });
5312
2925
  }
5313
- return { success: false, error: err.code || 'Error', message: err.message };
5314
- }
5315
- },
5316
- options: {
5317
- validate: {
5318
- options: {
5319
- stripUnknown: true,
5320
- abortEarly: false,
5321
- convert: true
5322
- },
5323
2926
 
5324
- failAction,
2927
+ await request.flash({ type: 'success', message: `OAuth2 app saved` });
2928
+ return h.redirect(`/admin/config/oauth/app/${oauth2App.id}`);
2929
+ } catch (err) {
2930
+ await request.flash({ type: 'danger', message: `Couldn't save OAuth2 app. Try again.` });
2931
+ request.logger.error({ msg: 'Failed to update OAuth2 app', app: request.payload.app, err });
5325
2932
 
5326
- payload: Joi.object({
5327
- description: Joi.string().empty('').trim().max(1024).required().example('Token description').description('Token description'),
5328
- scopes: Joi.array()
5329
- .items(Joi.string().valid('*', 'api', 'metrics', 'smtp', 'imap-proxy'))
5330
- .required()
5331
- .label('Scopes'),
5332
- account: accountIdSchema.default(null)
5333
- })
5334
- }
5335
- }
5336
- });
2933
+ let providerData = oauth2ProviderData(appData.provider, appData.cloud);
5337
2934
 
5338
- server.route({
5339
- method: 'POST',
5340
- path: '/admin/tokens/delete',
5341
- async handler(request, h) {
5342
- try {
5343
- let deleted = await tokens.delete(request.payload.token, { remoteAddress: request.app.ip });
5344
- if (deleted) {
5345
- await request.flash({ type: 'info', message: `Access token was deleted` });
2935
+ let serviceUrl = await settings.get('serviceUrl');
2936
+ let defaultRedirectUrl = `${serviceUrl}/oauth`;
2937
+ if (appData.provider === 'outlook') {
2938
+ defaultRedirectUrl = defaultRedirectUrl.replace(/^http:\/\/127\.0\.0\.1\b/i, 'http://localhost');
5346
2939
  }
5347
2940
 
5348
- return h.redirect('/admin/tokens');
5349
- } catch (err) {
5350
- await request.flash({ type: 'danger', message: `Failed to delete access token` });
5351
- request.logger.error({ msg: 'Failed to delete access token', err, token: request.payload.token, remoteAddress: request.app.ip });
5352
- return h.redirect('/admin/tokens');
2941
+ let pubSubApps = await oauth2Apps.list(0, 1000, { pubsub: true });
2942
+
2943
+ return h.view(
2944
+ 'config/oauth/edit',
2945
+ {
2946
+ pageTitle: 'OAuth2',
2947
+ menuConfig: true,
2948
+ menuConfigOauth: true,
2949
+
2950
+ [`active${providerData.caseName}`]: true,
2951
+ providerData,
2952
+ defaultRedirectUrl,
2953
+ appData,
2954
+
2955
+ hasClientSecret: !!appData.clientSecret,
2956
+ hasServiceKey: !!appData.serviceKey,
2957
+
2958
+ pubSubApps:
2959
+ pubSubApps &&
2960
+ pubSubApps.apps &&
2961
+ pubSubApps.apps.map(app => {
2962
+ if (app.id === request.payload.pubSubApp) {
2963
+ app.selected = true;
2964
+ }
2965
+ return app;
2966
+ }),
2967
+
2968
+ baseScopesApi: request.payload.baseScopes === 'api',
2969
+ baseScopesImap: request.payload.baseScopes === 'imap' || !request.payload.baseScopes,
2970
+ baseScopesPubsub: request.payload.baseScopes === 'pubsub',
2971
+
2972
+ azureClouds: structuredClone(AZURE_CLOUDS).map(entry => {
2973
+ entry.selected = request.payload.cloud === entry.id;
2974
+ return entry;
2975
+ }),
2976
+
2977
+ authorityCommon: request.payload.authority === 'common',
2978
+ authorityOrganizations: request.payload.authority === 'organizations',
2979
+ authorityConsumers: request.payload.authority === 'consumers',
2980
+ authorityTenant: request.payload.authority === 'tenant'
2981
+ },
2982
+ {
2983
+ layout: 'app'
2984
+ }
2985
+ );
5353
2986
  }
5354
2987
  },
5355
2988
  options: {
@@ -5361,17 +2994,98 @@ return payload;`)
5361
2994
  },
5362
2995
 
5363
2996
  async failAction(request, h, err) {
5364
- await request.flash({ type: 'danger', message: `Failed to delete access token` });
5365
- request.logger.error({ msg: 'Failed to delete access token', err });
2997
+ let errors = {};
2998
+
2999
+ if (err.details) {
3000
+ err.details.forEach(detail => {
3001
+ if (!errors[detail.path]) {
3002
+ errors[detail.path] = detail.message;
3003
+ }
3004
+ });
3005
+ }
3006
+
3007
+ let appData = await oauth2Apps.get(request.payload.app);
3008
+ if (!appData) {
3009
+ await request.flash({ type: 'danger', message: `Application not found` });
3010
+ request.logger.error({ msg: 'Application was not found.', app: request.payload.app });
3011
+ return h.redirect('/admin').takeover();
3012
+ }
3013
+
3014
+ await request.flash({ type: 'danger', message: `Couldn't save OAuth2 app. Try again.` });
3015
+ request.logger.error({ msg: 'Failed to update OAuth2 app', err });
3016
+
3017
+ let { provider } = request.payload;
3018
+ if (!provider || !OAUTH_PROVIDERS.hasOwnProperty(provider)) {
3019
+ return h.redirect('/admin').takeover();
3020
+ }
3021
+
3022
+ let providerData = oauth2ProviderData(provider);
3023
+
3024
+ let serviceUrl = await settings.get('serviceUrl');
3025
+ let defaultRedirectUrl = `${serviceUrl}/oauth`;
3026
+ if (provider === 'outlook') {
3027
+ defaultRedirectUrl = defaultRedirectUrl.replace(/^http:\/\/127\.0\.0\.1\b/i, 'http://localhost');
3028
+ }
3029
+
3030
+ let pubSubApps = await oauth2Apps.list(0, 1000, { pubsub: true });
3031
+
3032
+ return h
3033
+ .view(
3034
+ 'config/oauth/edit',
3035
+ {
3036
+ pageTitle: 'OAuth2',
3037
+ menuConfig: true,
3038
+ menuConfigOauth: true,
3039
+
3040
+ [`active${providerData.caseName}`]: true,
3041
+ providerData,
3042
+ defaultRedirectUrl,
3043
+
3044
+ appData,
3045
+
3046
+ hasClientSecret: !!appData.clientSecret,
3047
+ hasServiceKey: !!appData.serviceKey,
3048
+
3049
+ pubSubApps:
3050
+ pubSubApps &&
3051
+ pubSubApps.apps &&
3052
+ pubSubApps.apps.map(app => {
3053
+ if (app.id === request.payload.pubSubApp) {
3054
+ app.selected = true;
3055
+ }
3056
+ return app;
3057
+ }),
3058
+
3059
+ baseScopesApi: request.payload.baseScopes === 'api',
3060
+ baseScopesImap: request.payload.baseScopes === 'imap' || !request.payload.baseScopes,
3061
+ baseScopesPubsub: request.payload.baseScopes === 'pubsub',
3062
+
3063
+ azureClouds: structuredClone(AZURE_CLOUDS).map(entry => {
3064
+ entry.selected = request.payload.cloud === entry.id;
3065
+ return entry;
3066
+ }),
5366
3067
 
5367
- return h.redirect('/admin/tokens').takeover();
3068
+ authorityCommon: request.payload.authority === 'common',
3069
+ authorityOrganizations: request.payload.authority === 'organizations',
3070
+ authorityConsumers: request.payload.authority === 'consumers',
3071
+ authorityTenant: request.payload.authority === 'tenant',
3072
+
3073
+ errors
3074
+ },
3075
+ {
3076
+ layout: 'app'
3077
+ }
3078
+ )
3079
+ .takeover();
5368
3080
  },
5369
3081
 
5370
- payload: Joi.object({ token: Joi.string().length(64).hex().required().example('123456').description('Access token') })
3082
+ payload: Joi.object(oauthUpdateSchema)
5371
3083
  }
5372
3084
  }
5373
3085
  });
5374
3086
 
3087
+ // Webhook, template, gateway, and token routes are in admin-entities-routes.js
3088
+
5375
3089
  server.route({
5376
3090
  method: 'GET',
5377
3091
  path: '/admin/config/license',
@@ -5424,12 +3138,12 @@ return payload;`)
5424
3138
  }
5425
3139
 
5426
3140
  if (licenseInfo.active) {
5427
- await request.flash({ type: 'info', message: `License key was successfully registered` });
3141
+ await request.flash({ type: 'info', message: `License activated` });
5428
3142
  }
5429
3143
 
5430
3144
  return h.redirect('/admin/config/license');
5431
3145
  } catch (err) {
5432
- await request.flash({ type: 'danger', message: `Failed to register license key` });
3146
+ await request.flash({ type: 'danger', message: `Couldn't register license. Check the key and try again.` });
5433
3147
  request.logger.error({ msg: 'Failed to register license key', err });
5434
3148
 
5435
3149
  return h.view(
@@ -5471,7 +3185,7 @@ return payload;`)
5471
3185
  });
5472
3186
  }
5473
3187
 
5474
- await request.flash({ type: 'danger', message: `Failed to register license key` });
3188
+ await request.flash({ type: 'danger', message: `Couldn't register license. Check the key and try again.` });
5475
3189
  request.logger.error({ msg: 'Failed to register license key', err });
5476
3190
 
5477
3191
  return h
@@ -5519,12 +3233,12 @@ return payload;`)
5519
3233
  err.statusCode = 403;
5520
3234
  throw err;
5521
3235
  } else {
5522
- await request.flash({ type: 'info', message: `License key was unregistered` });
3236
+ await request.flash({ type: 'info', message: `License removed` });
5523
3237
  }
5524
3238
 
5525
3239
  return h.redirect('/admin/config/license');
5526
3240
  } catch (err) {
5527
- await request.flash({ type: 'danger', message: `Failed to unregister license key` });
3241
+ await request.flash({ type: 'danger', message: `Couldn't remove license. Try again.` });
5528
3242
  request.logger.error({ msg: 'Failed to unregister license key', err, token: request.payload.token, remoteAddress: request.app.ip });
5529
3243
  return h.redirect('/admin/config/license');
5530
3244
  }
@@ -5538,7 +3252,7 @@ return payload;`)
5538
3252
  },
5539
3253
 
5540
3254
  async failAction(request, h, err) {
5541
- await request.flash({ type: 'danger', message: `Failed to unregister license key` });
3255
+ await request.flash({ type: 'danger', message: `Couldn't remove license. Try again.` });
5542
3256
  request.logger.error({ msg: 'Failed to unregister license key', err });
5543
3257
 
5544
3258
  return h.redirect('/admin/config/license').takeover();
@@ -5601,8 +3315,8 @@ ${Buffer.from(data.content, 'base64url').toString('base64')}
5601
3315
  }
5602
3316
 
5603
3317
  if (licenseInfo.active) {
5604
- await request.flash({ type: 'info', message: `Trial license was activated` });
5605
- return { success: true, message: `Trial license was activated` };
3318
+ await request.flash({ type: 'info', message: `Trial activated` });
3319
+ return { success: true, message: `Trial activated` };
5606
3320
  }
5607
3321
 
5608
3322
  throw new Error('Failed to activate provisioned trial license');
@@ -5765,7 +3479,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')}
5765
3479
  return h.redirect('/admin');
5766
3480
  }
5767
3481
  } catch (err) {
5768
- await request.flash({ type: 'danger', message: err.responseText || `Failed to authenticate` });
3482
+ await request.flash({ type: 'danger', message: err.responseText || `Sign-in failed. Check your password and try again.` });
5769
3483
  request.logger.error({ msg: 'Failed to authenticate', err });
5770
3484
 
5771
3485
  let errors = err.details;
@@ -5805,7 +3519,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')}
5805
3519
  });
5806
3520
  }
5807
3521
 
5808
- await request.flash({ type: 'danger', message: `Failed to authenticate` });
3522
+ await request.flash({ type: 'danger', message: `Sign-in failed. Check your password and try again.` });
5809
3523
  request.logger.error({ msg: 'Failed to authenticate', err });
5810
3524
 
5811
3525
  return h
@@ -5914,7 +3628,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')}
5914
3628
 
5915
3629
  let totpSeed = await settings.get('totpSeed');
5916
3630
  if (!totpSeed) {
5917
- await request.flash({ type: 'danger', message: `2FA setup not initiated` });
3631
+ await request.flash({ type: 'danger', message: `Start two-factor auth setup first` });
5918
3632
  return h.redirect(`/admin/login`);
5919
3633
  }
5920
3634
 
@@ -5954,7 +3668,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')}
5954
3668
  } catch (err) {
5955
3669
  if (!err.details || !err.details.code) {
5956
3670
  // skip error message if code is invalid
5957
- await request.flash({ type: 'danger', message: err.responseText || `Failed to verify login` });
3671
+ await request.flash({ type: 'danger', message: err.responseText || `Verification failed. Check your code and try again.` });
5958
3672
  }
5959
3673
 
5960
3674
  request.logger.error({ msg: 'Failed to verify login', err });
@@ -5993,7 +3707,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')}
5993
3707
  });
5994
3708
  }
5995
3709
 
5996
- await request.flash({ type: 'danger', message: `Failed to verify login` });
3710
+ await request.flash({ type: 'danger', message: `Verification failed. Check your code and try again.` });
5997
3711
  request.logger.error({ msg: 'Failed to verify login', err });
5998
3712
 
5999
3713
  return h
@@ -6112,7 +3826,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')}
6112
3826
  try {
6113
3827
  let totpSeed = await settings.get('totpSeed');
6114
3828
  if (!totpSeed) {
6115
- await request.flash({ type: 'danger', message: `2FA setup not initiated` });
3829
+ await request.flash({ type: 'danger', message: `Start two-factor auth setup first` });
6116
3830
  return h.redirect(`/admin/account/security`);
6117
3831
  }
6118
3832
 
@@ -6124,7 +3838,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')}
6124
3838
  });
6125
3839
 
6126
3840
  if (!verified) {
6127
- await request.flash({ type: 'danger', message: `TOTP code verification failed` });
3841
+ await request.flash({ type: 'danger', message: `Invalid verification code` });
6128
3842
  return h.redirect(`/admin/account/security`);
6129
3843
  }
6130
3844
 
@@ -6140,10 +3854,10 @@ ${Buffer.from(data.content, 'base64url').toString('base64')}
6140
3854
  }
6141
3855
  }
6142
3856
 
6143
- await request.flash({ type: 'success', message: `Two-factor authentication was enabled` });
3857
+ await request.flash({ type: 'success', message: `Two-factor auth enabled` });
6144
3858
  return h.redirect(`/admin/account/security`);
6145
3859
  } catch (err) {
6146
- await request.flash({ type: 'danger', message: `Failed to enable 2FA` });
3860
+ await request.flash({ type: 'danger', message: `Couldn't enable two-factor auth. Try again.` });
6147
3861
  request.logger.error({ msg: 'Failed to enable 2FA', err, remoteAddress: request.app.ip });
6148
3862
  return h.redirect(`/admin/account/security`);
6149
3863
  }
@@ -6157,7 +3871,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')}
6157
3871
  },
6158
3872
 
6159
3873
  async failAction(request, h, err) {
6160
- await request.flash({ type: 'danger', message: `Failed to enable 2FA` });
3874
+ await request.flash({ type: 'danger', message: `Couldn't enable two-factor auth. Try again.` });
6161
3875
  request.logger.error({ msg: 'Failed to enable 2FA', err });
6162
3876
 
6163
3877
  return h.redirect('/admin').takeover();
@@ -6183,10 +3897,10 @@ ${Buffer.from(data.content, 'base64url').toString('base64')}
6183
3897
  await settings.set('totpEnabled', false);
6184
3898
  await settings.set('totpSeed', false);
6185
3899
 
6186
- await request.flash({ type: 'info', message: `Two-factor authentication was disabled` });
3900
+ await request.flash({ type: 'info', message: `Two-factor auth disabled` });
6187
3901
  return h.redirect(`/admin/account/security`);
6188
3902
  } catch (err) {
6189
- await request.flash({ type: 'danger', message: `Failed to disable 2FA` });
3903
+ await request.flash({ type: 'danger', message: `Couldn't disable two-factor auth. Try again.` });
6190
3904
  request.logger.error({ msg: 'Failed to enable 2FA', err, remoteAddress: request.app.ip });
6191
3905
  return h.redirect(`/admin/account/security`);
6192
3906
  }
@@ -6200,7 +3914,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')}
6200
3914
  },
6201
3915
 
6202
3916
  async failAction(request, h, err) {
6203
- await request.flash({ type: 'danger', message: `Failed to disable 2FA` });
3917
+ await request.flash({ type: 'danger', message: `Couldn't disable two-factor auth. Try again.` });
6204
3918
  request.logger.error({ msg: 'Failed to disable 2FA', err });
6205
3919
 
6206
3920
  return h.redirect('/admin').takeover();
@@ -6233,7 +3947,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')}
6233
3947
  await request.flash({ type: 'info', message: `User logged out` });
6234
3948
  return h.redirect('/');
6235
3949
  } catch (err) {
6236
- await request.flash({ type: 'danger', message: `Failed to log out user sessions` });
3950
+ await request.flash({ type: 'danger', message: `Couldn't log out sessions. Try again.` });
6237
3951
  request.logger.error({ msg: 'Failed to log out user sessions', err, remoteAddress: request.app.ip });
6238
3952
  return h.redirect(`/admin/account/security`);
6239
3953
  }
@@ -6247,7 +3961,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')}
6247
3961
  },
6248
3962
 
6249
3963
  async failAction(request, h, err) {
6250
- await request.flash({ type: 'danger', message: `Failed to log out user sessions` });
3964
+ await request.flash({ type: 'danger', message: `Couldn't log out sessions. Try again.` });
6251
3965
  request.logger.error({ msg: 'Failed to log out user sessions', err });
6252
3966
 
6253
3967
  return h.redirect('/admin').takeover();
@@ -6336,16 +4050,16 @@ ${Buffer.from(data.content, 'base64url').toString('base64')}
6336
4050
  }
6337
4051
 
6338
4052
  if (!hasExistingPassword) {
6339
- await request.flash({ type: 'info', message: `Authentication password set` });
4053
+ await request.flash({ type: 'info', message: `Password saved` });
6340
4054
 
6341
4055
  return h.redirect('/admin');
6342
4056
  }
6343
4057
 
6344
- await request.flash({ type: 'info', message: `Authentication password updated` });
4058
+ await request.flash({ type: 'info', message: `Password updated` });
6345
4059
 
6346
4060
  return h.redirect('/admin/account/password');
6347
4061
  } catch (err) {
6348
- await request.flash({ type: 'danger', message: `Failed to update password` });
4062
+ await request.flash({ type: 'danger', message: `Couldn't update password. Try again.` });
6349
4063
  request.logger.error({ msg: 'Failed to update password', err });
6350
4064
 
6351
4065
  let username = (request.auth && request.auth.credentials && request.auth.credentials.user) || 'admin';
@@ -6386,7 +4100,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')}
6386
4100
  });
6387
4101
  }
6388
4102
 
6389
- await request.flash({ type: 'danger', message: `Failed to update account password` });
4103
+ await request.flash({ type: 'danger', message: `Couldn't update password. Try again.` });
6390
4104
  request.logger.error({ msg: 'Failed to update account password', err });
6391
4105
 
6392
4106
  let username = (request.auth && request.auth.credentials && request.auth.credentials.user) || 'admin';
@@ -6705,12 +4419,8 @@ ${Buffer.from(data.content, 'base64url').toString('base64')}
6705
4419
 
6706
4420
  const nonce = data.n || crypto.randomBytes(NONCE_BYTES).toString('base64url');
6707
4421
 
6708
- // store account data
6709
- await redis
6710
- .multi()
6711
- .set(`${REDIS_PREFIX}account:add:${nonce}`, JSON.stringify(accountData))
6712
- .expire(`${REDIS_PREFIX}account:add:${nonce}`, Math.floor(MAX_FORM_TTL / 1000))
6713
- .exec();
4422
+ // store account data with atomic SET + EX
4423
+ await redis.set(`${REDIS_PREFIX}account:add:${nonce}`, JSON.stringify(accountData), 'EX', Math.floor(MAX_FORM_TTL / 1000));
6714
4424
 
6715
4425
  // Generate the url that will be used for the consent dialog.
6716
4426
 
@@ -6790,7 +4500,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')}
6790
4500
 
6791
4501
  async failAction(request, h, err) {
6792
4502
  request.logger.error({ msg: 'Failed to validate request arguments', err });
6793
- let error = Boom.boomify(new Error(request.app.gt.gettext('Failed to validate request arguments')), { statusCode: 400 });
4503
+ let error = Boom.boomify(new Error(request.app.gt.gettext('Invalid request. Check your input and try again.')), { statusCode: 400 });
6794
4504
  if (err.code) {
6795
4505
  error.output.payload.code = err.code;
6796
4506
  }
@@ -6823,7 +4533,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')}
6823
4533
 
6824
4534
  async failAction(request, h, err) {
6825
4535
  request.logger.error({ msg: 'Failed to validate request arguments', err });
6826
- let error = Boom.boomify(new Error(request.app.gt.gettext('Failed to validate request arguments')), { statusCode: 400 });
4536
+ let error = Boom.boomify(new Error(request.app.gt.gettext('Invalid request. Check your input and try again.')), { statusCode: 400 });
6827
4537
  if (err.code) {
6828
4538
  error.output.payload.code = err.code;
6829
4539
  }
@@ -6920,7 +4630,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')}
6920
4630
  });
6921
4631
  }
6922
4632
 
6923
- await request.flash({ type: 'danger', message: request.app.gt.gettext('Failed to process account') });
4633
+ await request.flash({ type: 'danger', message: request.app.gt.gettext(`Couldn't set up account. Try again.`) });
6924
4634
  request.logger.error({ msg: 'Failed to process account', err });
6925
4635
 
6926
4636
  return h
@@ -7185,7 +4895,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')}
7185
4895
  });
7186
4896
  }
7187
4897
 
7188
- await request.flash({ type: 'danger', message: request.app.gt.gettext('Failed to process account') });
4898
+ await request.flash({ type: 'danger', message: request.app.gt.gettext(`Couldn't set up account. Try again.`) });
7189
4899
  request.logger.error({ msg: 'Failed to process account', err });
7190
4900
 
7191
4901
  return h
@@ -7491,12 +5201,12 @@ ${Buffer.from(data.content, 'base64url').toString('base64')}
7491
5201
 
7492
5202
  let deleted = await accountObject.delete();
7493
5203
  if (deleted) {
7494
- await request.flash({ type: 'info', message: `Account was deleted` });
5204
+ await request.flash({ type: 'info', message: `Account deleted` });
7495
5205
  }
7496
5206
 
7497
5207
  return h.redirect('/admin/accounts');
7498
5208
  } catch (err) {
7499
- await request.flash({ type: 'danger', message: `Failed to delete the account` });
5209
+ await request.flash({ type: 'danger', message: `Couldn't delete account. Try again.` });
7500
5210
  request.logger.error({ msg: 'Failed to delete the account', err, account: request.payload.account, remoteAddress: request.app.ip });
7501
5211
  return h.redirect(`/admin/accounts/${request.params.account}`);
7502
5212
  }
@@ -7510,7 +5220,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')}
7510
5220
  },
7511
5221
 
7512
5222
  async failAction(request, h, err) {
7513
- await request.flash({ type: 'danger', message: `Failed to delete the account` });
5223
+ await request.flash({ type: 'danger', message: `Couldn't delete account. Try again.` });
7514
5224
  request.logger.error({ msg: 'Failed to delete delete the account', err });
7515
5225
 
7516
5226
  return h.redirect('/admin/accounts').takeover();
@@ -7712,7 +5422,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')}
7712
5422
  let authData = await settings.get('authData');
7713
5423
  let hasExistingPassword = !!(authData && authData.password);
7714
5424
  if (!hasExistingPassword) {
7715
- await request.flash({ type: 'info', message: `Authorization required to access messages` });
5425
+ await request.flash({ type: 'info', message: `Set a password to access messages` });
7716
5426
  return h.redirect('/admin/account/password');
7717
5427
  }
7718
5428
 
@@ -7721,7 +5431,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')}
7721
5431
  if (request.cookieAuth) {
7722
5432
  request.cookieAuth.clear();
7723
5433
  }
7724
- await request.flash({ type: 'info', message: `Re-authentication required` });
5434
+ await request.flash({ type: 'info', message: `Sign in again to continue` });
7725
5435
  return h.redirect('/admin/login?next=' + encodeURIComponent('/admin/accounts/{account}/browse'));
7726
5436
  }
7727
5437
 
@@ -7743,7 +5453,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')}
7743
5453
 
7744
5454
  const canReadMail = (accountData.imap || accountData.oauth2) && !(accountData.imap && accountData.imap.disabled) && !DISABLE_MESSAGE_BROWSER;
7745
5455
  if (!canReadMail) {
7746
- await request.flash({ type: 'danger', message: `Mail access is disabled for the selected account` });
5456
+ await request.flash({ type: 'danger', message: `Mail access is disabled for this account` });
7747
5457
  return h.redirect(`/admin/accounts/${request.params.account}`);
7748
5458
  }
7749
5459
 
@@ -7948,7 +5658,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')}
7948
5658
 
7949
5659
  return h.redirect(`/admin/accounts/${request.params.account}`);
7950
5660
  } catch (err) {
7951
- await request.flash({ type: 'danger', message: `Failed to update account settings` });
5661
+ await request.flash({ type: 'danger', message: `Couldn't save account settings. Try again.` });
7952
5662
  request.logger.error({ msg: 'Failed to update account settings', err, account: request.params.account });
7953
5663
 
7954
5664
  let accountObject = new Account({ redis, account: request.params.account, call, secret: await getSecret() });
@@ -7997,7 +5707,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')}
7997
5707
  });
7998
5708
  }
7999
5709
 
8000
- await request.flash({ type: 'danger', message: `Failed to update configuration` });
5710
+ await request.flash({ type: 'danger', message: `Couldn't save settings. Try again.` });
8001
5711
  request.logger.error({ msg: 'Failed to update configuration', err });
8002
5712
 
8003
5713
  let accountObject = new Account({ redis, account: request.params.account, call, secret: await getSecret() });
@@ -8171,7 +5881,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')}
8171
5881
 
8172
5882
  return h.redirect('/admin/config/document-store');
8173
5883
  } catch (err) {
8174
- await request.flash({ type: 'danger', message: `Failed to update configuration` });
5884
+ await request.flash({ type: 'danger', message: `Couldn't save settings. Try again.` });
8175
5885
  request.logger.error({ msg: 'Failed to update configuration', err });
8176
5886
 
8177
5887
  let hasDocumentStorePassword = !!(await settings.get('documentStorePassword'));
@@ -8211,7 +5921,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')}
8211
5921
  });
8212
5922
  }
8213
5923
 
8214
- await request.flash({ type: 'danger', message: `Failed to update configuration` });
5924
+ await request.flash({ type: 'danger', message: `Couldn't save settings. Try again.` });
8215
5925
  request.logger.error({ msg: 'Failed to update configuration', err });
8216
5926
 
8217
5927
  let hasDocumentStorePassword = !!(await settings.get('documentStorePassword'));
@@ -8284,7 +5994,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')}
8284
5994
 
8285
5995
  return h.redirect('/admin/config/document-store/chat');
8286
5996
  } catch (err) {
8287
- await request.flash({ type: 'danger', message: `Failed to update configuration` });
5997
+ await request.flash({ type: 'danger', message: `Couldn't save settings. Try again.` });
8288
5998
  request.logger.error({ msg: 'Failed to update configuration', err });
8289
5999
 
8290
6000
  return h.view(
@@ -8325,7 +6035,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')}
8325
6035
  });
8326
6036
  }
8327
6037
 
8328
- await request.flash({ type: 'danger', message: `Failed to update configuration` });
6038
+ await request.flash({ type: 'danger', message: `Couldn't save settings. Try again.` });
8329
6039
  request.logger.error({ msg: 'Failed to update configuration', err });
8330
6040
 
8331
6041
  return h
@@ -8440,10 +6150,10 @@ return payload;`
8440
6150
  documentStorePreProcessingMap: contentMap
8441
6151
  });
8442
6152
 
8443
- await request.flash({ type: 'info', message: `Pre-processing rules for the Document Store were updated` });
6153
+ await request.flash({ type: 'info', message: `Document Store rules saved` });
8444
6154
  return h.redirect(`/admin/config/document-store/pre-processing`);
8445
6155
  } catch (err) {
8446
- await request.flash({ type: 'danger', message: `Failed to update Document Store pre-processing rules` });
6156
+ await request.flash({ type: 'danger', message: `Couldn't save Document Store rules. Try again.` });
8447
6157
  request.logger.error({ msg: 'Failed to update Document Store pre-processing rules', err });
8448
6158
 
8449
6159
  return h.view(
@@ -8483,7 +6193,7 @@ return payload;`
8483
6193
  });
8484
6194
  }
8485
6195
 
8486
- await request.flash({ type: 'danger', message: `Failed to update Document Store pre-processing rules` });
6196
+ await request.flash({ type: 'danger', message: `Couldn't save Document Store rules. Try again.` });
8487
6197
  request.logger.error({ msg: 'Failed to update Document Store pre-processing rules', err });
8488
6198
 
8489
6199
  return h
@@ -8653,7 +6363,7 @@ return payload;`
8653
6363
  if (Boom.isBoom(err)) {
8654
6364
  await request.flash({ type: 'danger', message: err.message });
8655
6365
  } else {
8656
- await request.flash({ type: 'danger', message: err.responseText || `Failed to create mapping` });
6366
+ await request.flash({ type: 'danger', message: err.responseText || `Couldn't create mapping. Try again.` });
8657
6367
  }
8658
6368
  request.logger.error({ msg: 'Failed to create mapping', err });
8659
6369
 
@@ -8691,7 +6401,7 @@ return payload;`
8691
6401
  });
8692
6402
  }
8693
6403
 
8694
- await request.flash({ type: 'danger', message: `Failed to create mapping` });
6404
+ await request.flash({ type: 'danger', message: `Couldn't create mapping. Try again.` });
8695
6405
  request.logger.error({ msg: 'Failed to create mapping', err });
8696
6406
 
8697
6407
  return h
@@ -8901,7 +6611,7 @@ return payload;`
8901
6611
 
8902
6612
  return h.redirect('/admin/config/network');
8903
6613
  } catch (err) {
8904
- await request.flash({ type: 'danger', message: `Failed to update configuration` });
6614
+ await request.flash({ type: 'danger', message: `Couldn't save settings. Try again.` });
8905
6615
  request.logger.error({ msg: 'Failed to update configuration', err });
8906
6616
 
8907
6617
  let smtpStrategies = ADDRESS_STRATEGIES.map(entry => Object.assign({ selected: request.payload.smtpStrategy === entry.key }, entry));
@@ -8945,7 +6655,7 @@ return payload;`
8945
6655
  });
8946
6656
  }
8947
6657
 
8948
- await request.flash({ type: 'danger', message: `Failed to update configuration` });
6658
+ await request.flash({ type: 'danger', message: `Couldn't save settings. Try again.` });
8949
6659
  request.logger.error({ msg: 'Failed to update configuration', err });
8950
6660
 
8951
6661
  let smtpStrategies = ADDRESS_STRATEGIES.map(entry => Object.assign({ selected: request.payload.smtpStrategy === entry.key }, entry));
@@ -9002,10 +6712,10 @@ return payload;`
9002
6712
 
9003
6713
  await redis.hdel(`${REDIS_PREFIX}interfaces`, localAddress);
9004
6714
 
9005
- await request.flash({ type: 'info', message: `Address was removed from the list` });
6715
+ await request.flash({ type: 'info', message: `Address removed` });
9006
6716
  return h.redirect('/admin/config/network');
9007
6717
  } catch (err) {
9008
- await request.flash({ type: 'danger', message: `Failed to delete address` });
6718
+ await request.flash({ type: 'danger', message: `Couldn't delete address. Try again.` });
9009
6719
  request.logger.error({ msg: 'Failed to delete address', err, localAddress: request.payload.localAddress, remoteAddress: request.app.ip });
9010
6720
  return h.redirect('/admin/config/network');
9011
6721
  }
@@ -9019,7 +6729,7 @@ return payload;`
9019
6729
  },
9020
6730
 
9021
6731
  async failAction(request, h, err) {
9022
- await request.flash({ type: 'danger', message: `Failed to delete address` });
6732
+ await request.flash({ type: 'danger', message: `Couldn't delete address. Try again.` });
9023
6733
  request.logger.error({ msg: 'Failed to delete address', err });
9024
6734
 
9025
6735
  return h.redirect('/admin/config/network').takeover();
@@ -9114,7 +6824,7 @@ return payload;`
9114
6824
 
9115
6825
  return h.redirect('/admin/config/imap-proxy');
9116
6826
  } catch (err) {
9117
- await request.flash({ type: 'danger', message: `Failed to update configuration` });
6827
+ await request.flash({ type: 'danger', message: `Couldn't save settings. Try again.` });
9118
6828
  request.logger.error({ msg: 'Failed to update configuration', err });
9119
6829
 
9120
6830
  let availableAddresses = new Set(
@@ -9166,7 +6876,7 @@ return payload;`
9166
6876
  });
9167
6877
  }
9168
6878
 
9169
- await request.flash({ type: 'danger', message: `Failed to update configuration` });
6879
+ await request.flash({ type: 'danger', message: `Couldn't save settings. Try again.` });
9170
6880
  request.logger.error({ msg: 'Failed to update configuration', err });
9171
6881
 
9172
6882
  let availableAddresses = new Set(
@@ -9288,7 +6998,7 @@ return payload;`
9288
6998
 
9289
6999
  return h.redirect('/admin/config/smtp');
9290
7000
  } catch (err) {
9291
- await request.flash({ type: 'danger', message: `Failed to update configuration` });
7001
+ await request.flash({ type: 'danger', message: `Couldn't save settings. Try again.` });
9292
7002
  request.logger.error({ msg: 'Failed to update configuration', err });
9293
7003
 
9294
7004
  let availableAddresses = new Set(
@@ -9340,7 +7050,7 @@ return payload;`
9340
7050
  });
9341
7051
  }
9342
7052
 
9343
- await request.flash({ type: 'danger', message: `Failed to update configuration` });
7053
+ await request.flash({ type: 'danger', message: `Couldn't save settings. Try again.` });
9344
7054
  request.logger.error({ msg: 'Failed to update configuration', err });
9345
7055
 
9346
7056
  let availableAddresses = new Set(
@@ -9742,7 +7452,7 @@ ${now}`,
9742
7452
 
9743
7453
  async failAction(request, h, err) {
9744
7454
  request.logger.error({ msg: 'Failed to validate request arguments', err });
9745
- let error = Boom.boomify(new Error(request.app.gt.gettext('Failed to validate request arguments')), { statusCode: 400 });
7455
+ let error = Boom.boomify(new Error(request.app.gt.gettext('Invalid request. Check your input and try again.')), { statusCode: 400 });
9746
7456
  if (err.code) {
9747
7457
  error.output.payload.code = err.code;
9748
7458
  }
@@ -9844,7 +7554,7 @@ ${now}`,
9844
7554
  }
9845
7555
  );
9846
7556
  } catch (err) {
9847
- await request.flash({ type: 'danger', message: request.app.gt.gettext('Failed to process request') });
7557
+ await request.flash({ type: 'danger', message: request.app.gt.gettext(`Couldn't process request. Try again.`) });
9848
7558
  request.logger.error({ msg: 'Failed to process subscription request', err });
9849
7559
 
9850
7560
  return h.view(
@@ -9879,7 +7589,7 @@ ${now}`,
9879
7589
  });
9880
7590
  }
9881
7591
 
9882
- await request.flash({ type: 'danger', message: request.app.gt.gettext('Failed to process request') });
7592
+ await request.flash({ type: 'danger', message: request.app.gt.gettext(`Couldn't process request. Try again.`) });
9883
7593
  request.logger.error({ msg: 'Failed to process subscription request', err });
9884
7594
 
9885
7595
  return h
@@ -9916,15 +7626,8 @@ ${now}`,
9916
7626
 
9917
7627
  let defaultLocale = (await settings.get('locale')) || 'en';
9918
7628
 
9919
- let percentFormatter;
9920
7629
  let bytesFormatter;
9921
7630
 
9922
- let percentFormatterOpts = {
9923
- style: 'percent',
9924
- minimumFractionDigits: 2,
9925
- maximumFractionDigits: 2
9926
- };
9927
-
9928
7631
  let bytesFormatterOpts = {
9929
7632
  style: 'unit',
9930
7633
  unit: 'byte',
@@ -9933,10 +7636,8 @@ ${now}`,
9933
7636
  };
9934
7637
 
9935
7638
  try {
9936
- percentFormatter = new Intl.NumberFormat(defaultLocale, percentFormatterOpts);
9937
7639
  bytesFormatter = new Intl.NumberFormat(defaultLocale, bytesFormatterOpts);
9938
7640
  } catch (err) {
9939
- percentFormatter = new Intl.NumberFormat('en-US', percentFormatterOpts);
9940
7641
  bytesFormatter = new Intl.NumberFormat('en-US', bytesFormatterOpts);
9941
7642
  }
9942
7643
 
@@ -10029,12 +7730,12 @@ ${now}`,
10029
7730
  try {
10030
7731
  let killed = await call({ cmd: 'kill-thread', thread: request.payload.thread });
10031
7732
  if (killed) {
10032
- await request.flash({ type: 'info', message: `Thread was killed` });
7733
+ await request.flash({ type: 'info', message: `Worker stopped` });
10033
7734
  }
10034
7735
 
10035
7736
  return h.redirect('/admin/internals');
10036
7737
  } catch (err) {
10037
- await request.flash({ type: 'danger', message: `Failed to kill thread` });
7738
+ await request.flash({ type: 'danger', message: `Couldn't stop worker. Try again.` });
10038
7739
  request.logger.error({ msg: 'Failed to kill thread', err, thread: request.payload.thread, remoteAddress: request.app.ip });
10039
7740
  return h.redirect('/admin/internals');
10040
7741
  }
@@ -10048,7 +7749,7 @@ ${now}`,
10048
7749
  },
10049
7750
 
10050
7751
  async failAction(request, h, err) {
10051
- await request.flash({ type: 'danger', message: `Failed to kill thread` });
7752
+ await request.flash({ type: 'danger', message: `Couldn't stop worker. Try again.` });
10052
7753
  request.logger.error({ msg: 'Failed to kill thread', err });
10053
7754
 
10054
7755
  return h.redirect('/admin/internals').takeover();
@@ -10086,7 +7787,7 @@ ${now}`,
10086
7787
  .header('Pragma', 'no-cache')
10087
7788
  .code(200);
10088
7789
  } catch (err) {
10089
- await request.flash({ type: 'danger', message: `Failed to generate snapshot` });
7790
+ await request.flash({ type: 'danger', message: `Couldn't create snapshot. Try again.` });
10090
7791
  request.logger.error({ msg: 'Failed to generate snapshot', err, thread: request.payload.thread, remoteAddress: request.app.ip });
10091
7792
  return h.redirect('/admin/internals');
10092
7793
  }
@@ -10100,7 +7801,7 @@ ${now}`,
10100
7801
  },
10101
7802
 
10102
7803
  async failAction(request, h, err) {
10103
- await request.flash({ type: 'danger', message: `Failed to generate snapshot` });
7804
+ await request.flash({ type: 'danger', message: `Couldn't create snapshot. Try again.` });
10104
7805
  request.logger.error({ msg: 'Failed to generate snapshot', err });
10105
7806
 
10106
7807
  return h.redirect('/admin/internals').takeover();
@@ -10124,12 +7825,12 @@ ${now}`,
10124
7825
  const threadInfo = threads.find(t => t.threadId === threadId);
10125
7826
 
10126
7827
  if (!threadInfo) {
10127
- await request.flash({ type: 'danger', message: 'Thread not found' });
7828
+ await request.flash({ type: 'danger', message: `Worker not found` });
10128
7829
  return h.redirect('/admin/internals');
10129
7830
  }
10130
7831
 
10131
7832
  if (threadInfo.type !== 'imap') {
10132
- await request.flash({ type: 'warning', message: 'Only email worker threads have assigned accounts' });
7833
+ await request.flash({ type: 'warning', message: `Only email workers have assigned accounts` });
10133
7834
  return h.redirect('/admin/internals');
10134
7835
  }
10135
7836