emailengine-app 2.68.1 → 2.69.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/.github/workflows/deploy.yml +2 -0
  2. package/.github/workflows/release.yaml +4 -0
  3. package/CHANGELOG.md +40 -0
  4. package/config/default.toml +2 -0
  5. package/data/google-crawlers.json +7 -1
  6. package/lib/account.js +62 -25
  7. package/lib/api-routes/account-routes.js +493 -75
  8. package/lib/api-routes/blocklist-routes.js +337 -0
  9. package/lib/api-routes/delivery-test-routes.js +321 -0
  10. package/lib/api-routes/export-routes.js +1 -12
  11. package/lib/api-routes/gateway-routes.js +376 -0
  12. package/lib/api-routes/license-routes.js +142 -0
  13. package/lib/api-routes/mailbox-routes.js +318 -0
  14. package/lib/api-routes/message-routes.js +21 -129
  15. package/lib/api-routes/oauth2-app-routes.js +631 -0
  16. package/lib/api-routes/outbox-routes.js +173 -0
  17. package/lib/api-routes/pubsub-routes.js +98 -0
  18. package/lib/api-routes/route-helpers.js +45 -0
  19. package/lib/api-routes/settings-routes.js +331 -0
  20. package/lib/api-routes/stats-routes.js +77 -0
  21. package/lib/api-routes/submit-routes.js +472 -0
  22. package/lib/api-routes/template-routes.js +7 -55
  23. package/lib/api-routes/token-routes.js +297 -0
  24. package/lib/api-routes/webhook-route-routes.js +152 -0
  25. package/lib/email-client/gmail-client.js +14 -0
  26. package/lib/email-client/imap/mailbox.js +34 -11
  27. package/lib/email-client/imap/subconnection.js +20 -12
  28. package/lib/email-client/imap/sync-operations.js +130 -2
  29. package/lib/email-client/imap-client.js +116 -58
  30. package/lib/email-client/outlook-client.js +85 -13
  31. package/lib/export.js +60 -19
  32. package/lib/imapproxy/imap-core/lib/commands/starttls.js +18 -0
  33. package/lib/imapproxy/imap-core/lib/imap-command.js +6 -1
  34. package/lib/imapproxy/imap-core/lib/imap-connection.js +106 -23
  35. package/lib/imapproxy/imap-core/lib/imap-server.js +24 -0
  36. package/lib/imapproxy/imap-core/lib/imap-stream.js +26 -0
  37. package/lib/message-port-stream.js +113 -16
  38. package/lib/reject-worker-calls.js +42 -0
  39. package/lib/routes-ui.js +37 -8778
  40. package/lib/schemas.js +26 -1
  41. package/lib/tools.js +68 -0
  42. package/lib/ui-routes/account-routes.js +40 -210
  43. package/lib/ui-routes/admin-config-routes.js +913 -487
  44. package/lib/ui-routes/admin-entities-routes.js +1 -0
  45. package/lib/ui-routes/auth-routes.js +1339 -0
  46. package/lib/ui-routes/dashboard-routes.js +188 -0
  47. package/lib/ui-routes/document-store-routes.js +800 -0
  48. package/lib/ui-routes/export-routes.js +217 -0
  49. package/lib/ui-routes/internals-routes.js +354 -0
  50. package/lib/ui-routes/network-config-routes.js +759 -0
  51. package/lib/ui-routes/{oauth-routes.js → oauth-config-routes.js} +371 -91
  52. package/lib/ui-routes/route-helpers.js +316 -0
  53. package/lib/ui-routes/smtp-test-routes.js +236 -0
  54. package/lib/ui-routes/unsubscribe-routes.js +234 -0
  55. package/lib/webhook-request.js +36 -0
  56. package/package.json +8 -8
  57. package/sbom.json +1 -1
  58. package/server.js +214 -16
  59. package/static/licenses.html +12 -12
  60. package/translations/messages.pot +129 -149
  61. package/views/dashboard.hbs +7 -26
  62. package/views/internals/index.hbs +15 -0
  63. package/views/tokens/index.hbs +9 -0
  64. package/workers/api.js +198 -4401
  65. package/workers/export.js +87 -54
  66. package/workers/imap.js +29 -13
  67. package/workers/submit.js +20 -11
  68. package/workers/webhooks.js +6 -20
package/workers/api.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // NB! This file is processed by gettext parser and can not use newer syntax like ?.
4
4
 
5
- const { parentPort } = require('worker_threads');
5
+ const { parentPort, workerData } = require('worker_threads');
6
6
 
7
7
  const packageData = require('../package.json');
8
8
  const config = require('@zone-eu/wild-config');
@@ -24,23 +24,18 @@ const eulaText = marked.parse(
24
24
  const {
25
25
  getByteSize,
26
26
  getDuration,
27
- getStats,
28
27
  flash,
29
28
  failAction,
30
- verifyAccountInfo,
31
29
  isEmail,
32
- getLogs,
33
30
  getWorkerCount,
34
31
  runPrechecks,
35
32
  matcher,
36
33
  readEnvValue,
37
- getSignedFormData,
38
34
  threadStats,
39
35
  hasEnvValue,
40
36
  getBoolean,
41
37
  loadTlsConfig,
42
- httpAgent,
43
- reloadHttpProxyAgent,
38
+ maybeReloadHttpProxyAgent,
44
39
  resolveOAuthErrorStatus
45
40
  } = require('../lib/tools');
46
41
  const { matchIp, detectAutomatedRequest } = require('../lib/utils/network');
@@ -84,20 +79,13 @@ const pathlib = require('path');
84
79
  const crypto = require('crypto');
85
80
  const { Transform, finished } = require('stream');
86
81
  const { oauth2Apps, OAUTH_PROVIDERS } = require('../lib/oauth2-apps');
87
- const { verifyOAuth2App } = require('../lib/oauth/verify-app');
88
82
 
89
83
  const handlebars = require('handlebars');
90
84
  const AuthBearer = require('hapi-auth-bearer-token');
91
85
  const tokens = require('../lib/tokens');
92
- const { autodetectImapSettings } = require('../lib/autodetect-imap-settings');
93
86
 
94
- const outbox = require('../lib/outbox');
95
-
96
- const { lists } = require('../lib/lists');
97
-
98
- const { redis, documentsQueue, notifyQueue, submitQueue } = require('../lib/db');
87
+ const { redis, documentsQueue } = require('../lib/db');
99
88
  const { Account } = require('../lib/account');
100
- const { Gateway } = require('../lib/gateway');
101
89
  const settings = require('../lib/settings');
102
90
 
103
91
  const getSecret = require('../lib/get-secret');
@@ -114,7 +102,6 @@ const {
114
102
  TRACK_OPEN_NOTIFY,
115
103
  TRACK_CLICK_NOTIFY,
116
104
  REDIS_PREFIX,
117
- MAX_DAYS_STATS,
118
105
  RENEW_TLS_AFTER,
119
106
  BLOCK_TLS_RENEW,
120
107
  TLS_RENEW_CHECK_INTERVAL,
@@ -128,45 +115,27 @@ const {
128
115
  NONCE_BYTES
129
116
  } = consts;
130
117
 
131
- const { fetch: fetchCmd } = require('undici');
132
-
133
118
  const templateRoutes = require('../lib/api-routes/template-routes');
134
119
  const chatRoutes = require('../lib/api-routes/chat-routes');
135
120
  const bullBoardRoutes = require('../lib/api-routes/bull-board-routes');
136
121
  const accountRoutes = require('../lib/api-routes/account-routes');
137
122
  const messageRoutes = require('../lib/api-routes/message-routes');
138
123
  const exportRoutes = require('../lib/api-routes/export-routes');
139
-
140
- const {
141
- settingsSchema,
142
- addressSchema,
143
- settingsQuerySchema,
144
- imapSchema,
145
- smtpSchema,
146
- oauth2Schema,
147
- mailboxesSchema,
148
- shortMailboxesSchema,
149
- licenseSchema,
150
- lastErrorSchema,
151
- templateSchemas,
152
- accountSchemas,
153
- oauthCreateSchema,
154
- tokenRestrictionsSchema,
155
- accountIdSchema,
156
- ipSchema,
157
- accountPathSchema,
158
- defaultAccountTypeSchema,
159
- fromAddressSchema,
160
- outboxEntrySchema,
161
- googleProjectIdSchema,
162
- googleWorkspaceAccountsSchema,
163
- googleTopicNameSchema,
164
- googleSubscriptionNameSchema,
165
- messageReferenceSchema,
166
- idempotencyKeySchema,
167
- headerTimeoutSchema,
168
- pubSubErrorSchema
169
- } = require('../lib/schemas');
124
+ const pubsubRoutes = require('../lib/api-routes/pubsub-routes');
125
+ const tokenRoutes = require('../lib/api-routes/token-routes');
126
+ const mailboxRoutes = require('../lib/api-routes/mailbox-routes');
127
+ const settingsRoutes = require('../lib/api-routes/settings-routes');
128
+ const statsRoutes = require('../lib/api-routes/stats-routes');
129
+ const licenseRoutes = require('../lib/api-routes/license-routes');
130
+ const outboxRoutes = require('../lib/api-routes/outbox-routes');
131
+ const webhookRouteRoutes = require('../lib/api-routes/webhook-route-routes');
132
+ const oauth2AppRoutes = require('../lib/api-routes/oauth2-app-routes');
133
+ const gatewayRoutes = require('../lib/api-routes/gateway-routes');
134
+ const deliveryTestRoutes = require('../lib/api-routes/delivery-test-routes');
135
+ const blocklistRoutes = require('../lib/api-routes/blocklist-routes');
136
+ const submitRoutes = require('../lib/api-routes/submit-routes');
137
+
138
+ const { imapSchema, smtpSchema, oauth2Schema, accountIdSchema, headerTimeoutSchema } = require('../lib/schemas');
170
139
 
171
140
  const OAuth2ProviderSchema = Joi.string()
172
141
  .valid(...Object.keys(OAUTH_PROVIDERS))
@@ -182,21 +151,6 @@ const AccountTypeSchema = Joi.string()
182
151
  .required()
183
152
  .label('AccountType');
184
153
 
185
- function flattenOAuthAppMeta(app) {
186
- if (!app.meta) {
187
- return;
188
- }
189
- let authFlag = app.meta.authFlag;
190
- let pubSubFlag = app.meta.pubSubFlag;
191
- delete app.meta;
192
- if (authFlag && authFlag.message) {
193
- app.lastError = { response: authFlag.message };
194
- }
195
- if (pubSubFlag && pubSubFlag.message) {
196
- app.pubSubError = { message: pubSubFlag.message, description: pubSubFlag.description || null };
197
- }
198
- }
199
-
200
154
  const SUPPORTED_LOCALES = locales.map(locale => locale.locale);
201
155
 
202
156
  const FLAG_SORT_ORDER = ['\\Inbox', '\\Flagged', '\\Sent', '\\Drafts', '\\All', '\\Archive', '\\Junk', '\\Trash'];
@@ -260,6 +214,15 @@ const API_TLS = hasEnvValue('EENGINE_API_TLS') ? getBoolean(readEnvValue('EENGIN
260
214
  // Merge TLS settings from config params and environment
261
215
  loadTlsConfig(API_TLS, 'EENGINE_API_TLS_');
262
216
 
217
+ // Per-worker thread metadata. With multiple API workers (EENGINE_WORKERS_API > 1) the
218
+ // main thread assigns each one an index and whether to bind with SO_REUSEPORT. Only
219
+ // worker 0 runs singleton maintenance tasks (e.g. TLS certificate renewal).
220
+ const WORKER_INDEX = (workerData && workerData.workerIndex) || 0;
221
+ const USE_REUSE_PORT = !!(workerData && workerData.reusePort);
222
+ // Worker 0 is the primary; it runs singleton maintenance tasks (e.g. TLS certificate renewal)
223
+ // that must execute exactly once across all API workers.
224
+ const IS_PRIMARY_API_WORKER = WORKER_INDEX === 0;
225
+
263
226
  const ADMIN_ACCESS_ADDRESSES = hasEnvValue('EENGINE_ADMIN_ACCESS_ADDRESSES')
264
227
  ? readEnvValue('EENGINE_ADMIN_ACCESS_ADDRESSES')
265
228
  .split(',')
@@ -542,6 +505,11 @@ parentPort.on('message', message => {
542
505
  if (message && message.cmd === 'change') {
543
506
  publishChangeEvent(message);
544
507
  }
508
+
509
+ if (message && message.cmd === 'settings') {
510
+ // Keep this worker's in-memory HTTP proxy agent in sync when proxy settings change
511
+ maybeReloadHttpProxyAgent(message.data);
512
+ }
545
513
  });
546
514
 
547
515
  const init = async () => {
@@ -620,11 +588,8 @@ const init = async () => {
620
588
  return formatter.format(intVal);
621
589
  });
622
590
 
623
- const server = Hapi.server({
624
- port: API_PORT,
625
- host: API_HOST,
626
- tls: API_TLS,
627
-
591
+ // Base Hapi options shared by both the default and SO_REUSEPORT binding paths
592
+ const serverOptions = {
628
593
  state: {
629
594
  strictHeader: false
630
595
  },
@@ -646,7 +611,27 @@ const init = async () => {
646
611
  }).unknown()
647
612
  }
648
613
  }
649
- });
614
+ };
615
+
616
+ // With multiple API workers we provide our own listener and bind it ourselves with
617
+ // SO_REUSEPORT so the kernel load-balances connections. Hapi forbids port/host when
618
+ // autoListen is false and needs a truthy `tls` flag to treat a provided HTTPS
619
+ // listener correctly. The single-worker path keeps Hapi's default binding unchanged.
620
+ let reusePortListener = null;
621
+ if (USE_REUSE_PORT) {
622
+ const http = require('http');
623
+ const https = require('https');
624
+ reusePortListener = API_TLS ? https.createServer(API_TLS) : http.createServer();
625
+ serverOptions.listener = reusePortListener;
626
+ serverOptions.tls = !!API_TLS;
627
+ serverOptions.autoListen = false;
628
+ } else {
629
+ serverOptions.port = API_PORT;
630
+ serverOptions.host = API_HOST;
631
+ serverOptions.tls = API_TLS;
632
+ }
633
+
634
+ const server = Hapi.server(serverOptions);
650
635
 
651
636
  let assertPreconditionResult;
652
637
  server.decorate('toolkit', 'getESClient', async (...args) => await getESClient(...args));
@@ -1132,6 +1117,12 @@ Include your token in requests using one of these methods:
1132
1117
  };
1133
1118
  }
1134
1119
 
1120
+ // Bind the token hash (id) to the request logger so it is included in the per-request
1121
+ // log entry, allowing API requests to be correlated to the token that made them.
1122
+ if (request.logger && typeof request.logger.setBindings === 'function') {
1123
+ request.logger.setBindings({ tokenId: tokenData.id, tokenAccount: tokenData.account || null });
1124
+ }
1125
+
1135
1126
  if (scope && tokenData.scopes && !tokenData.scopes.includes(scope) && !tokenData.scopes.includes('*')) {
1136
1127
  // failed scope validation
1137
1128
  logger.error({
@@ -1620,99 +1611,6 @@ Include your token in requests using one of these methods:
1620
1611
  }
1621
1612
  });
1622
1613
 
1623
- server.route({
1624
- method: 'GET',
1625
- path: '/v1/pubsub/status',
1626
-
1627
- async handler(request) {
1628
- try {
1629
- let response = await oauth2Apps.list(request.query.page, request.query.pageSize, { pubsub: true });
1630
-
1631
- let apps = response.apps.map(app => {
1632
- flattenOAuthAppMeta(app);
1633
- return { id: app.id, name: app.name || null, lastError: app.lastError || null, pubSubError: app.pubSubError || null };
1634
- });
1635
-
1636
- return {
1637
- total: response.total,
1638
- page: response.page,
1639
- pages: response.pages,
1640
- apps
1641
- };
1642
- } catch (err) {
1643
- request.logger.error({ msg: 'API request failed', err });
1644
- if (Boom.isBoom(err)) {
1645
- throw err;
1646
- }
1647
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
1648
- if (err.code) {
1649
- error.output.payload.code = err.code;
1650
- }
1651
- throw error;
1652
- }
1653
- },
1654
-
1655
- options: {
1656
- description: 'List Pub/Sub status',
1657
- notes: 'Lists Pub/Sub enabled OAuth2 applications and their subscription status',
1658
- tags: ['api', 'OAuth2 Applications'],
1659
-
1660
- plugins: {},
1661
-
1662
- auth: {
1663
- strategy: 'api-token',
1664
- mode: 'required'
1665
- },
1666
- cors: CORS_CONFIG,
1667
-
1668
- validate: {
1669
- options: {
1670
- stripUnknown: false,
1671
- abortEarly: false,
1672
- convert: true
1673
- },
1674
- failAction,
1675
-
1676
- query: Joi.object({
1677
- page: Joi.number()
1678
- .integer()
1679
- .min(0)
1680
- .max(1024 * 1024)
1681
- .default(0)
1682
- .example(0)
1683
- .description('Page number (zero indexed, so use 0 for first page)')
1684
- .label('PageNumber'),
1685
- pageSize: Joi.number().integer().min(1).max(1000).default(20).example(20).description('How many entries per page').label('PageSize')
1686
- }).label('PubSubStatusFilter')
1687
- },
1688
-
1689
- response: {
1690
- schema: Joi.object({
1691
- total: Joi.number().integer().example(120).description('How many matching entries').label('TotalNumber'),
1692
- page: Joi.number().integer().example(0).description('Current page (0-based index)').label('PageNumber'),
1693
- pages: Joi.number().integer().example(24).description('Total page count').label('PagesNumber'),
1694
-
1695
- apps: Joi.array()
1696
- .items(
1697
- Joi.object({
1698
- id: Joi.string().max(256).required().example('AAABhaBPHscAAAAH').description('OAuth2 application ID'),
1699
- name: Joi.string().allow(null).max(256).example('My Gmail App').description('Display name for the app'),
1700
- lastError: Joi.object({
1701
- response: Joi.string().example('Enable the Cloud Pub/Sub API').description('Setup error message')
1702
- })
1703
- .allow(null)
1704
- .description('Setup error from ensurePubsub, if any')
1705
- .label('PubSubSetupError'),
1706
- pubSubError: pubSubErrorSchema.allow(null)
1707
- }).label('PubSubAppStatus')
1708
- )
1709
- .label('PubSubAppStatusList')
1710
- }).label('PubSubStatusResponse'),
1711
- failAction: 'log'
1712
- }
1713
- }
1714
- });
1715
-
1716
1614
  server.route({
1717
1615
  method: 'GET',
1718
1616
  path: '/redirect',
@@ -2657,4290 +2555,159 @@ Include your token in requests using one of these methods:
2657
2555
  }
2658
2556
  });
2659
2557
 
2660
- server.route({
2661
- method: 'POST',
2662
- path: '/v1/token',
2663
-
2664
- async handler(request) {
2665
- let accountObject = new Account({
2666
- redis,
2667
- account: request.payload.account,
2668
- call,
2669
- secret: await getSecret(),
2670
- timeout: request.headers['x-ee-timeout']
2671
- });
2672
-
2673
- try {
2674
- // throws if account does not exist
2675
- await accountObject.loadAccountData();
2676
-
2677
- let token = await tokens.provision(Object.assign({}, request.payload, { remoteAddress: request.app.ip }));
2678
-
2679
- return { token };
2680
- } catch (err) {
2681
- request.logger.error({ msg: 'API request failed', err });
2682
- if (Boom.isBoom(err)) {
2683
- throw err;
2684
- }
2685
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
2686
- if (err.code) {
2687
- error.output.payload.code = err.code;
2688
- }
2689
- throw error;
2690
- }
2691
- },
2692
-
2693
- options: {
2694
- description: 'Provision an access token',
2695
- notes: 'Provisions a new access token for an account',
2696
- tags: ['api', 'Access Tokens'],
2558
+ // setup template routes
2559
+ await templateRoutes({ server, call, CORS_CONFIG });
2697
2560
 
2698
- plugins: {},
2561
+ // setup "chat with email" routes
2562
+ await chatRoutes({ server, call, CORS_CONFIG });
2699
2563
 
2700
- auth: {
2701
- strategy: 'api-token',
2702
- mode: 'required'
2703
- },
2704
- cors: CORS_CONFIG,
2564
+ // setup account CRUD routes
2565
+ await accountRoutes({
2566
+ server,
2567
+ call,
2568
+ documentsQueue,
2569
+ oauth2Schema,
2570
+ imapSchema,
2571
+ smtpSchema,
2572
+ CORS_CONFIG,
2573
+ AccountTypeSchema,
2574
+ OAuth2ProviderSchema,
2575
+ metrics
2576
+ });
2705
2577
 
2706
- validate: {
2707
- options: {
2708
- stripUnknown: false,
2709
- abortEarly: false,
2710
- convert: true
2711
- },
2712
- failAction,
2578
+ // setup message routes
2579
+ await messageRoutes({
2580
+ server,
2581
+ call,
2582
+ CORS_CONFIG,
2583
+ MAX_ATTACHMENT_SIZE,
2584
+ MAX_BODY_SIZE,
2585
+ MAX_PAYLOAD_TIMEOUT
2586
+ });
2713
2587
 
2714
- payload: Joi.object({
2715
- account: accountIdSchema.required(),
2588
+ // setup export routes
2589
+ await exportRoutes({
2590
+ server,
2591
+ CORS_CONFIG
2592
+ });
2716
2593
 
2717
- description: Joi.string().empty('').trim().max(1024).required().example('Token description').description('Token description'),
2594
+ // setup Pub/Sub status route
2595
+ await pubsubRoutes({ server, CORS_CONFIG });
2718
2596
 
2719
- scopes: Joi.array()
2720
- .items(Joi.string().valid('api', 'smtp', 'imap-proxy').label('TokenScope'))
2721
- .single()
2722
- .default(['api'])
2723
- .required()
2724
- .description(
2725
- 'Token permission scopes: "api" for REST API access, "smtp" for SMTP submission, "imap-proxy" for IMAP proxy authentication'
2726
- )
2727
- .label('Scopes'),
2597
+ // setup access token routes
2598
+ await tokenRoutes({ server, call, CORS_CONFIG });
2728
2599
 
2729
- metadata: Joi.string()
2730
- .empty('')
2731
- .max(1024 * 1024)
2732
- .custom((value, helpers) => {
2733
- try {
2734
- // check if parsing fails
2735
- JSON.parse(value);
2736
- return value;
2737
- } catch (err) {
2738
- return helpers.message('Metadata must be a valid JSON string');
2739
- }
2740
- })
2741
- .example('{"example": "value"}')
2742
- .description('Related metadata in JSON format')
2743
- .label('JsonMetaData'),
2600
+ // setup mailbox routes
2601
+ await mailboxRoutes({ server, call, CORS_CONFIG, FLAG_SORT_ORDER });
2744
2602
 
2745
- restrictions: tokenRestrictionsSchema,
2603
+ // setup settings routes
2604
+ await settingsRoutes({ server, notify, CORS_CONFIG });
2746
2605
 
2747
- ip: ipSchema.description('IP address of the requester').label('TokenIP')
2748
- }).label('CreateToken')
2749
- },
2606
+ // setup stats route
2607
+ await statsRoutes({ server, call, CORS_CONFIG });
2750
2608
 
2751
- response: {
2752
- schema: Joi.object({
2753
- token: Joi.string().length(64).hex().required().example('123456').description('Access token')
2754
- }).label('CreateTokenResponse'),
2755
- failAction: 'log'
2756
- }
2757
- }
2758
- });
2609
+ // setup license routes
2610
+ await licenseRoutes({ server, call, CORS_CONFIG });
2759
2611
 
2760
- server.route({
2761
- method: 'DELETE',
2762
- path: '/v1/token/{token}',
2612
+ // setup outbox routes
2613
+ await outboxRoutes({ server, CORS_CONFIG });
2763
2614
 
2764
- async handler(request) {
2765
- try {
2766
- return { deleted: await tokens.delete(request.params.token, { remoteAddress: request.app.ip }) };
2767
- } catch (err) {
2768
- request.logger.error({ msg: 'API request failed', err });
2769
- if (Boom.isBoom(err)) {
2770
- throw err;
2771
- }
2772
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
2773
- if (err.code) {
2774
- error.output.payload.code = err.code;
2775
- }
2776
- throw error;
2777
- }
2778
- },
2779
- options: {
2780
- description: 'Remove a token',
2781
- notes: 'Delete an access token',
2782
- tags: ['api', 'Access Tokens'],
2615
+ // setup webhook route management routes
2616
+ await webhookRouteRoutes({ server, CORS_CONFIG });
2783
2617
 
2784
- plugins: {},
2618
+ // setup OAuth2 application routes
2619
+ await oauth2AppRoutes({ server, call, CORS_CONFIG, OAuth2ProviderSchema });
2785
2620
 
2786
- auth: {
2787
- strategy: 'api-token',
2788
- mode: 'required'
2789
- },
2790
- cors: CORS_CONFIG,
2621
+ // setup SMTP gateway routes
2622
+ await gatewayRoutes({ server, call, CORS_CONFIG });
2791
2623
 
2792
- validate: {
2793
- options: {
2794
- stripUnknown: false,
2795
- abortEarly: false,
2796
- convert: true
2797
- },
2798
- failAction,
2624
+ // setup delivery test routes
2625
+ await deliveryTestRoutes({ server, call, CORS_CONFIG, SMTP_TEST_HOST });
2799
2626
 
2800
- params: Joi.object({
2801
- token: Joi.string().length(64).hex().required().example('123456').description('Access token')
2802
- }).label('DeleteTokenRequest')
2803
- },
2627
+ // setup blocklist routes
2628
+ await blocklistRoutes({ server, call, CORS_CONFIG });
2804
2629
 
2805
- response: {
2806
- schema: Joi.object({
2807
- deleted: Joi.boolean().truthy('Y', 'true', '1').falsy('N', 'false', 0).default(true).description('Was the token deleted')
2808
- }).label('DeleteTokenRequestResponse'),
2809
- failAction: 'log'
2810
- }
2811
- }
2812
- });
2630
+ // setup message submit route
2631
+ await submitRoutes({ server, call, CORS_CONFIG, MAX_ATTACHMENT_SIZE, MAX_BODY_SIZE, MAX_PAYLOAD_TIMEOUT });
2813
2632
 
2814
2633
  server.route({
2815
2634
  method: 'GET',
2816
- path: '/v1/tokens',
2635
+ path: '/v1/changes',
2817
2636
 
2818
- async handler(request) {
2819
- try {
2820
- // TODO: allow paging
2821
- return { tokens: (await tokens.list(null, 0, 1000)).tokens };
2822
- } catch (err) {
2823
- request.logger.error({ msg: 'API request failed', err });
2824
- if (Boom.isBoom(err)) {
2825
- throw err;
2826
- }
2827
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
2828
- if (err.code) {
2829
- error.output.payload.code = err.code;
2637
+ async handler(request, h) {
2638
+ request.app.stream = new ResponseStream();
2639
+ finished(request.app.stream, err => request.app.stream.finalize(err));
2640
+ setImmediate(() => {
2641
+ try {
2642
+ request.app.stream.write(`: EmailEngine v${packageData.version}\n\n`);
2643
+ } catch (err) {
2644
+ // ignore
2830
2645
  }
2831
- throw error;
2832
- }
2646
+ });
2647
+ return h
2648
+ .response(request.app.stream)
2649
+ .header('X-Accel-Buffering', 'no')
2650
+ .header('Connection', 'keep-alive')
2651
+ .header('Cache-Control', 'no-cache')
2652
+ .type('text/event-stream');
2833
2653
  },
2834
2654
 
2835
2655
  options: {
2836
- description: 'List root tokens',
2837
- notes: 'Lists access tokens registered for root access',
2838
- tags: ['api', 'Access Tokens'],
2656
+ description: 'Stream state changes',
2657
+ notes: 'Stream account state changes as an EventSource',
2658
+ tags: ['api', 'Account'],
2839
2659
 
2840
- plugins: {},
2660
+ plugins: {
2661
+ 'hapi-swagger': {
2662
+ produces: ['text/event-stream']
2663
+ }
2664
+ },
2841
2665
 
2842
2666
  auth: {
2843
2667
  strategy: 'api-token',
2844
2668
  mode: 'required'
2845
2669
  },
2846
- cors: CORS_CONFIG,
2847
-
2848
- validate: {
2849
- options: {
2850
- stripUnknown: false,
2851
- abortEarly: false,
2852
- convert: true
2853
- },
2854
- failAction
2855
- },
2856
-
2857
- response: {
2858
- schema: Joi.object({
2859
- tokens: Joi.array()
2860
- .items(
2861
- Joi.object({
2862
- account: accountIdSchema.required(),
2863
- description: Joi.string().empty('').trim().max(1024).required().example('Token description').description('Token description'),
2864
- metadata: Joi.string()
2865
- .empty('')
2866
- .max(1024 * 1024)
2867
- .custom((value, helpers) => {
2868
- try {
2869
- // check if parsing fails
2870
- JSON.parse(value);
2871
- return value;
2872
- } catch (err) {
2873
- return helpers.message('Metadata must be a valid JSON string');
2874
- }
2875
- })
2876
- .example('{"example": "value"}')
2877
- .description('Related metadata in JSON format')
2878
- .label('JsonMetaData'),
2879
- ip: ipSchema.description('IP address of the requester').label('TokenIP')
2880
- }).label('RootTokensItem')
2881
- )
2882
- .label('RootTokensEntries')
2883
- }).label('RootTokensResponse'),
2884
- failAction: 'log'
2885
- }
2670
+ cors: CORS_CONFIG
2886
2671
  }
2887
2672
  });
2888
2673
 
2889
- server.route({
2890
- method: 'GET',
2891
- path: '/v1/tokens/account/{account}',
2674
+ // Web UI routes
2892
2675
 
2893
- async handler(request) {
2894
- try {
2895
- // TODO: allow paging
2896
- return { tokens: (await tokens.list(request.params.account, 0, 1000)).tokens };
2897
- } catch (err) {
2898
- request.logger.error({ msg: 'API request failed', err });
2899
- if (Boom.isBoom(err)) {
2900
- throw err;
2901
- }
2902
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
2903
- if (err.code) {
2904
- error.output.payload.code = err.code;
2905
- }
2906
- throw error;
2907
- }
2908
- },
2676
+ await server.register({
2677
+ plugin: Crumb,
2909
2678
 
2910
2679
  options: {
2911
- description: 'List account tokens',
2912
- notes: 'Lists access tokens registered for an account',
2913
- tags: ['api', 'Access Tokens'],
2914
-
2915
- plugins: {},
2916
-
2917
- auth: {
2918
- strategy: 'api-token',
2919
- mode: 'required'
2680
+ cookieOptions: {
2681
+ isSecure: secureCookie
2920
2682
  },
2921
- cors: CORS_CONFIG,
2922
2683
 
2923
- validate: {
2924
- options: {
2925
- stripUnknown: false,
2926
- abortEarly: false,
2927
- convert: true
2928
- },
2929
- failAction,
2930
- params: Joi.object({
2931
- account: accountIdSchema.required()
2932
- })
2933
- },
2684
+ skip: (request /*, h*/) => {
2685
+ let tags = (request.route && request.route.settings && request.route.settings.tags) || [];
2686
+
2687
+ if (tags.includes('api') || tags.includes('metrics') || tags.includes('external')) {
2688
+ return true;
2689
+ }
2934
2690
 
2935
- response: {
2936
- schema: Joi.object({
2937
- tokens: Joi.array()
2938
- .items(
2939
- Joi.object({
2940
- account: accountIdSchema.required(),
2941
- description: Joi.string().empty('').trim().max(1024).required().example('Token description').description('Token description'),
2942
- metadata: Joi.string()
2943
- .empty('')
2944
- .max(1024 * 1024)
2945
- .custom((value, helpers) => {
2946
- try {
2947
- // check if parsing fails
2948
- JSON.parse(value);
2949
- return value;
2950
- } catch (err) {
2951
- return helpers.message('Metadata must be a valid JSON string');
2952
- }
2953
- })
2954
- .example('{"example": "value"}')
2955
- .description('Related metadata in JSON format')
2956
- .label('JsonMetaData'),
2957
-
2958
- restrictions: tokenRestrictionsSchema,
2959
-
2960
- ip: ipSchema.description('IP address of the requester').label('TokenIP')
2961
- }).label('AccountTokensItem')
2962
- )
2963
- .label('AccountTokensEntries')
2964
- }).label('AccountsTokensResponse'),
2965
- failAction: 'log'
2691
+ return false;
2966
2692
  }
2967
2693
  }
2968
2694
  });
2969
2695
 
2970
- server.route({
2971
- method: 'POST',
2972
- path: '/v1/authentication/form',
2696
+ server.views({
2697
+ engines: {
2698
+ hbs: handlebars
2699
+ },
2700
+ compileOptions: {
2701
+ preventIndent: true
2702
+ },
2973
2703
 
2974
- async handler(request) {
2975
- try {
2976
- let { data, signature } = await getSignedFormData({
2977
- account: request.payload.account,
2978
- name: request.payload.name,
2979
- email: request.payload.email,
2980
- syncFrom: request.payload.syncFrom,
2981
- notifyFrom: request.payload.notifyFrom,
2982
- subconnections: request.payload.subconnections,
2983
- redirectUrl: request.payload.redirectUrl,
2984
- delegated: request.payload.delegated,
2985
- path: request.payload.path && !request.payload.path.includes('*') ? request.payload.path : null,
2986
- // identify request
2987
- n: crypto.randomBytes(NONCE_BYTES).toString('base64url'),
2988
- t: Date.now()
2989
- });
2704
+ relativeTo: pathlib.join(__dirname, '..'),
2705
+ path: './views',
2706
+ layout: 'app',
2707
+ layoutPath: './views/layout',
2708
+ partialsPath: './views/partials',
2990
2709
 
2991
- let serviceUrl = await settings.get('serviceUrl');
2992
- if (!serviceUrl) {
2993
- let err = new Error('Service URL not set up');
2994
- err.code = 'MissingServiceURLSetup';
2995
- throw err;
2996
- }
2997
-
2998
- let url = new URL(`accounts/new`, serviceUrl);
2999
-
3000
- url.searchParams.append('data', data);
3001
- if (signature) {
3002
- url.searchParams.append('sig', signature);
3003
- }
3004
-
3005
- let type = request.payload.type;
3006
-
3007
- if (type && type !== 'imap') {
3008
- let oauth2app = await oauth2Apps.get(type);
3009
- if (!oauth2app || !oauth2app.enabled) {
3010
- type = false;
3011
- }
3012
- }
3013
-
3014
- if (!type) {
3015
- let oauth2apps = (await oauth2Apps.list(0, 100)).apps.filter(app => app.includeInListing);
3016
- if (!oauth2apps.length) {
3017
- type = 'imap';
3018
- }
3019
- }
3020
-
3021
- if (type) {
3022
- url.searchParams.append('type', type);
3023
- }
3024
-
3025
- return {
3026
- url: url.href
3027
- };
3028
- } catch (err) {
3029
- request.logger.error({ msg: 'API request failed', err });
3030
- if (Boom.isBoom(err)) {
3031
- throw err;
3032
- }
3033
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
3034
- if (err.code) {
3035
- error.output.payload.code = err.code;
3036
- }
3037
- throw error;
3038
- }
3039
- },
3040
-
3041
- options: {
3042
- description: 'Generate authentication link',
3043
- notes: 'Generates a redirect link to the hosted authentication form',
3044
- tags: ['api', 'Account'],
3045
-
3046
- plugins: {},
3047
-
3048
- auth: {
3049
- strategy: 'api-token',
3050
- mode: 'required'
3051
- },
3052
- cors: CORS_CONFIG,
3053
-
3054
- validate: {
3055
- options: {
3056
- stripUnknown: false,
3057
- abortEarly: false,
3058
- convert: true
3059
- },
3060
- failAction,
3061
-
3062
- payload: Joi.object({
3063
- account: Joi.string()
3064
- .empty('')
3065
- .trim()
3066
- .max(256)
3067
- .allow(null)
3068
- .example('example')
3069
- .default(null)
3070
- .description(
3071
- 'Account ID. If set to `null`, a unique ID will be generated automatically. If you provide an existing account ID, the settings for that account will be updated instead'
3072
- ),
3073
-
3074
- name: Joi.string().empty('').max(256).example('My Email Account').description('Display name for the account'),
3075
-
3076
- email: Joi.string()
3077
- .empty('')
3078
- .email()
3079
- .example('user@example.com')
3080
- .description('Specifies the default email address for this account. Users can change it if needed.'),
3081
-
3082
- delegated: Joi.boolean()
3083
- .empty('')
3084
- .truthy('Y', 'true', '1')
3085
- .falsy('N', 'false', 0)
3086
- .default(false)
3087
- .description('If true, configures this account as a shared mailbox. Currently supported by MS365 OAuth2 accounts'),
3088
-
3089
- syncFrom: accountSchemas.syncFrom,
3090
- notifyFrom: accountSchemas.notifyFrom,
3091
-
3092
- subconnections: accountSchemas.subconnections,
3093
-
3094
- path: accountPathSchema.example(['*']).label('AccountFormPath'),
3095
- redirectUrl: Joi.string()
3096
- .empty('')
3097
- .uri({ scheme: ['http', 'https'], allowRelative: false })
3098
- .required()
3099
- .example('https://myapp/account/settings.php')
3100
- .description('After the authentication process is completed, the user is redirected to this URL'),
3101
-
3102
- type: defaultAccountTypeSchema
3103
- }).label('RequestAuthForm')
3104
- },
3105
-
3106
- response: {
3107
- schema: Joi.object({
3108
- url: Joi.string()
3109
- .empty('')
3110
- .uri({ scheme: ['http', 'https'], allowRelative: false })
3111
- .required()
3112
- .example('https://ee.example.com/accounts/new?data=eyJhY2NvdW50IjoiZXhh...L0W_BkFH5HW6Krwmr7c&type=imap')
3113
- .description('Generated URL to the hosted authentication form')
3114
- }).label('RequestAuthFormResponse'),
3115
- failAction: 'log'
3116
- }
3117
- }
3118
- });
3119
-
3120
- server.route({
3121
- method: 'GET',
3122
- path: '/v1/account/{account}/mailboxes',
3123
-
3124
- async handler(request) {
3125
- let accountObject = new Account({
3126
- redis,
3127
- account: request.params.account,
3128
- call,
3129
- secret: await getSecret(),
3130
- timeout: request.headers['x-ee-timeout']
3131
- });
3132
-
3133
- try {
3134
- let mailboxes = await accountObject.getMailboxListing(request.query);
3135
-
3136
- if (mailboxes && Array.isArray(mailboxes)) {
3137
- mailboxes = mailboxes.sort((a, b) => {
3138
- if (a.specialUse && !b.specialUse) {
3139
- return -1;
3140
- }
3141
- if (!a.specialUse && b.specialUse) {
3142
- return 1;
3143
- }
3144
- if (a.specialUse && b.specialUse) {
3145
- return FLAG_SORT_ORDER.indexOf(a.specialUse) - FLAG_SORT_ORDER.indexOf(b.specialUse);
3146
- }
3147
-
3148
- return a.path.localeCompare(b.path);
3149
- });
3150
- }
3151
-
3152
- return { mailboxes };
3153
- } catch (err) {
3154
- request.logger.error({ msg: 'API request failed', err });
3155
- if (Boom.isBoom(err)) {
3156
- throw err;
3157
- }
3158
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
3159
- if (err.code) {
3160
- error.output.payload.code = err.code;
3161
- }
3162
- throw error;
3163
- }
3164
- },
3165
-
3166
- options: {
3167
- description: 'List mailboxes',
3168
- notes: 'Lists all available mailboxes',
3169
- tags: ['api', 'Mailbox'],
3170
-
3171
- auth: {
3172
- strategy: 'api-token',
3173
- mode: 'required'
3174
- },
3175
- cors: CORS_CONFIG,
3176
-
3177
- validate: {
3178
- options: {
3179
- stripUnknown: false,
3180
- abortEarly: false,
3181
- convert: true
3182
- },
3183
- failAction,
3184
-
3185
- params: Joi.object({
3186
- account: accountIdSchema.required()
3187
- }),
3188
-
3189
- query: Joi.object({
3190
- counters: Joi.boolean()
3191
- .truthy('Y', 'true', '1')
3192
- .falsy('N', 'false', 0)
3193
- .default(false)
3194
- .description('If true, then includes message counters in the response')
3195
- .label('MailboxCounters')
3196
- }).label('MailboxListQuery')
3197
- },
3198
-
3199
- response: {
3200
- schema: Joi.object({
3201
- mailboxes: mailboxesSchema
3202
- }).label('MailboxesFilterResponse'),
3203
- failAction: 'log'
3204
- }
3205
- }
3206
- });
3207
-
3208
- server.route({
3209
- method: 'POST',
3210
- path: '/v1/account/{account}/mailbox',
3211
-
3212
- async handler(request) {
3213
- let accountObject = new Account({
3214
- redis,
3215
- account: request.params.account,
3216
- call,
3217
- secret: await getSecret(),
3218
- timeout: request.headers['x-ee-timeout']
3219
- });
3220
-
3221
- try {
3222
- return await accountObject.createMailbox(request.payload.path);
3223
- } catch (err) {
3224
- request.logger.error({ msg: 'API request failed', err });
3225
- if (Boom.isBoom(err)) {
3226
- throw err;
3227
- }
3228
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
3229
- if (err.code) {
3230
- error.output.payload.code = err.code;
3231
- }
3232
- if (err.info) {
3233
- error.output.payload.details = err.info;
3234
- }
3235
- throw error;
3236
- }
3237
- },
3238
-
3239
- options: {
3240
- description: 'Create mailbox',
3241
- notes: 'Create new mailbox folder',
3242
- tags: ['api', 'Mailbox'],
3243
-
3244
- plugins: {},
3245
-
3246
- auth: {
3247
- strategy: 'api-token',
3248
- mode: 'required'
3249
- },
3250
- cors: CORS_CONFIG,
3251
-
3252
- validate: {
3253
- options: {
3254
- stripUnknown: false,
3255
- abortEarly: false,
3256
- convert: true
3257
- },
3258
- failAction,
3259
-
3260
- params: Joi.object({
3261
- account: accountIdSchema.required()
3262
- }),
3263
-
3264
- payload: Joi.object({
3265
- path: Joi.array()
3266
- .items(Joi.string().max(256))
3267
- .single()
3268
- .example(['Parent folder', 'Subfolder'])
3269
- .description('Mailbox path as an array or a string. If account is namespaced then namespace prefix is added by default.')
3270
- .label('MailboxPath')
3271
- }).label('CreateMailbox')
3272
- },
3273
-
3274
- response: {
3275
- schema: Joi.object({
3276
- path: Joi.string().required().example('Kalender/S&APw-nnip&AOQ-evad').description('Full path to mailbox').label('MailboxPath'),
3277
- mailboxId: Joi.string().example('1439876283476').description('Mailbox ID (if server has support)').label('MailboxId'),
3278
- created: Joi.boolean().example(true).description('Was the mailbox created')
3279
- }).label('CreateMailboxResponse'),
3280
- failAction: 'log'
3281
- }
3282
- }
3283
- });
3284
-
3285
- server.route({
3286
- method: 'PUT',
3287
- path: '/v1/account/{account}/mailbox',
3288
-
3289
- async handler(request) {
3290
- let accountObject = new Account({
3291
- redis,
3292
- account: request.params.account,
3293
- call,
3294
- secret: await getSecret(),
3295
- timeout: request.headers['x-ee-timeout']
3296
- });
3297
-
3298
- try {
3299
- return await accountObject.modifyMailbox(request.payload.path, request.payload.newPath, request.payload.subscribed);
3300
- } catch (err) {
3301
- request.logger.error({ msg: 'API request failed', err });
3302
- if (Boom.isBoom(err)) {
3303
- throw err;
3304
- }
3305
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
3306
- if (err.code) {
3307
- error.output.payload.code = err.code;
3308
- }
3309
- if (err.info) {
3310
- error.output.payload.details = err.info;
3311
- }
3312
- throw error;
3313
- }
3314
- },
3315
-
3316
- options: {
3317
- description: 'Modify mailbox',
3318
- notes: 'Modify an existing mailbox folder (rename or change subscription status)',
3319
- tags: ['api', 'Mailbox'],
3320
-
3321
- plugins: {},
3322
-
3323
- auth: {
3324
- strategy: 'api-token',
3325
- mode: 'required'
3326
- },
3327
- cors: CORS_CONFIG,
3328
-
3329
- validate: {
3330
- options: {
3331
- stripUnknown: false,
3332
- abortEarly: false,
3333
- convert: true
3334
- },
3335
- failAction,
3336
-
3337
- params: Joi.object({
3338
- account: accountIdSchema.required()
3339
- }),
3340
-
3341
- payload: Joi.object({
3342
- path: Joi.string().required().example('Folder Name').description('Mailbox folder path to modify').label('ExistingMailboxPath'),
3343
- newPath: Joi.array()
3344
- .items(Joi.string().max(256))
3345
- .single()
3346
- .example(['Parent folder', 'Subfolder'])
3347
- .description('New mailbox path as an array or a string. If account is namespaced then namespace prefix is added by default. Optional.')
3348
- .label('TargetMailboxPath'),
3349
- subscribed: Joi.boolean()
3350
- .example(true)
3351
- .description('Change mailbox subscription status. Only applies to IMAP accounts, ignored for Gmail and Outlook.')
3352
- .label('SubscriptionStatus')
3353
- })
3354
- .or('newPath', 'subscribed')
3355
- .label('ModifyMailbox')
3356
- },
3357
-
3358
- response: {
3359
- schema: Joi.object({
3360
- path: Joi.string().required().example('Mail').description('Mailbox folder path').label('ExistingMailboxPath'),
3361
- newPath: Joi.string().example('Kalender/S&APw-nnip&AOQ-evad').description('Full path to mailbox if renamed').label('NewMailboxPath'),
3362
- renamed: Joi.boolean().example(true).description('Was the mailbox renamed'),
3363
- subscribed: Joi.boolean().example(true).description('Subscription status after modification')
3364
- }).label('ModifyMailboxResponse'),
3365
- failAction: 'log'
3366
- }
3367
- }
3368
- });
3369
-
3370
- server.route({
3371
- method: 'DELETE',
3372
- path: '/v1/account/{account}/mailbox',
3373
-
3374
- async handler(request) {
3375
- let accountObject = new Account({
3376
- redis,
3377
- account: request.params.account,
3378
- call,
3379
- secret: await getSecret(),
3380
- timeout: request.headers['x-ee-timeout']
3381
- });
3382
-
3383
- try {
3384
- return await accountObject.deleteMailbox(request.query.path);
3385
- } catch (err) {
3386
- request.logger.error({ msg: 'API request failed', err });
3387
- if (Boom.isBoom(err)) {
3388
- throw err;
3389
- }
3390
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
3391
- if (err.code) {
3392
- error.output.payload.code = err.code;
3393
- }
3394
- throw error;
3395
- }
3396
- },
3397
-
3398
- options: {
3399
- description: 'Delete mailbox',
3400
- notes: 'Delete existing mailbox folder',
3401
- tags: ['api', 'Mailbox'],
3402
-
3403
- plugins: {},
3404
-
3405
- auth: {
3406
- strategy: 'api-token',
3407
- mode: 'required'
3408
- },
3409
- cors: CORS_CONFIG,
3410
-
3411
- validate: {
3412
- options: {
3413
- stripUnknown: false,
3414
- abortEarly: false,
3415
- convert: true
3416
- },
3417
- failAction,
3418
-
3419
- params: Joi.object({
3420
- account: accountIdSchema.required()
3421
- }),
3422
-
3423
- query: Joi.object({
3424
- path: Joi.string().required().example('My Outdated Mail').description('Mailbox folder path to delete').label('MailboxPath')
3425
- }).label('DeleteMailbox')
3426
- },
3427
-
3428
- response: {
3429
- schema: Joi.object({
3430
- path: Joi.string().required().example('Kalender/S&APw-nnip&AOQ-evad').description('Full path to mailbox').label('MailboxPath'),
3431
- deleted: Joi.boolean().example(true).description('Was the mailbox deleted')
3432
- }).label('DeleteMailboxResponse'),
3433
- failAction: 'log'
3434
- }
3435
- }
3436
- });
3437
-
3438
- server.route({
3439
- method: 'POST',
3440
- path: '/v1/account/{account}/submit',
3441
-
3442
- async handler(request) {
3443
- let accountObject = new Account({
3444
- redis,
3445
- account: request.params.account,
3446
- call,
3447
- secret: await getSecret(),
3448
- timeout: request.headers['x-ee-timeout']
3449
- });
3450
-
3451
- try {
3452
- return await accountObject.queueMessage(request.payload, {
3453
- source: 'api',
3454
- idempotencyKey: request.headers['idempotency-key'],
3455
- useStructuredFormat: request.query.useStructuredFormat
3456
- });
3457
- } catch (err) {
3458
- request.logger.error({ msg: 'API request failed', err });
3459
- if (Boom.isBoom(err)) {
3460
- throw err;
3461
- }
3462
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
3463
- if (err.code) {
3464
- error.output.payload.code = err.code;
3465
- }
3466
- if (err.info) {
3467
- error.output.payload.info = err.info;
3468
- }
3469
- throw error;
3470
- }
3471
- },
3472
- options: {
3473
- payload: {
3474
- maxBytes: MAX_BODY_SIZE,
3475
- timeout: MAX_PAYLOAD_TIMEOUT
3476
- },
3477
-
3478
- description: 'Submit message for delivery',
3479
- notes: 'Submit message for delivery. If reference message ID is provided then EmailEngine adds all headers and flags required for a reply/forward automatically.',
3480
- tags: ['api', 'Submit'],
3481
-
3482
- plugins: {},
3483
-
3484
- auth: {
3485
- strategy: 'api-token',
3486
- mode: 'required'
3487
- },
3488
- cors: CORS_CONFIG,
3489
-
3490
- validate: {
3491
- options: {
3492
- stripUnknown: false,
3493
- abortEarly: false,
3494
- convert: true
3495
- },
3496
- failAction,
3497
-
3498
- params: Joi.object({
3499
- account: accountIdSchema.required()
3500
- }),
3501
-
3502
- query: Joi.object({
3503
- documentStore: Joi.boolean()
3504
- .truthy('Y', 'true', '1')
3505
- .falsy('N', 'false', 0)
3506
- .default(false)
3507
- .description('If enabled then fetch email used as a reference template from the Document Store'),
3508
- useStructuredFormat: Joi.boolean()
3509
- .truthy('Y', 'true', '1')
3510
- .falsy('N', 'false', 0)
3511
- .default(false)
3512
- .description(
3513
- 'For MS Graph accounts: If true, uses structured JSON format (respects from field for shared mailboxes, breaks calendar invites and special MIME types). If false, sends as raw MIME (preserves calendar invites, ignores from field). Default is false (raw MIME).'
3514
- )
3515
- }).label('SubmitQuery'),
3516
-
3517
- headers: Joi.object({
3518
- 'x-ee-timeout': headerTimeoutSchema,
3519
- 'idempotency-key': idempotencyKeySchema
3520
- }).unknown(),
3521
-
3522
- payload: Joi.object({
3523
- reference: messageReferenceSchema,
3524
-
3525
- envelope: Joi.object({
3526
- from: Joi.string().email().allow('').example('sender@example.com'),
3527
- to: Joi.array().items(Joi.string().email().required().example('recipient@example.com')).single().label('SmtpEnvelopeTo')
3528
- })
3529
- .description(
3530
- "An optional object specifying the SMTP envelope used during email transmission. If not provided, the envelope is automatically derived from the email's message headers. This is useful when you need the envelope addresses to differ from those in the email headers."
3531
- )
3532
- .label('SMTPEnvelope')
3533
- .when('mailMerge', {
3534
- is: Joi.exist().not(false, null),
3535
- then: Joi.forbidden('y')
3536
- }),
3537
-
3538
- raw: Joi.string()
3539
- .base64()
3540
- .max(MAX_ATTACHMENT_SIZE)
3541
- .example('TUlNRS1WZXJzaW9uOiAxLjANClN1YmplY3Q6IGhlbGxvIHdvcmxkDQoNCkhlbGxvIQ0K')
3542
- .description(
3543
- 'A Base64-encoded email message in RFC 822 format. If you provide other fields along with raw, those fields will override the corresponding values in the raw message.'
3544
- )
3545
- .label('RFC822Raw')
3546
- .when('mailMerge', {
3547
- is: Joi.exist().not(false, null),
3548
- then: Joi.forbidden('y')
3549
- }),
3550
-
3551
- from: fromAddressSchema,
3552
-
3553
- replyTo: Joi.array()
3554
- .items(addressSchema.label('ReplyToAddress'))
3555
- .single()
3556
- .example([{ name: 'From Me', address: 'sender@example.com' }])
3557
- .description('List of Reply-To addresses')
3558
- .label('ReplyTo'),
3559
-
3560
- to: Joi.array()
3561
- .items(addressSchema.label('ToAddress'))
3562
- .single()
3563
- .example([{ address: 'recipient@example.com' }])
3564
- .description('List of recipient addresses')
3565
- .label('ToAddressList')
3566
- .when('mailMerge', {
3567
- is: Joi.exist().not(false, null),
3568
- then: Joi.forbidden('y')
3569
- }),
3570
-
3571
- cc: Joi.array()
3572
- .items(addressSchema.label('CcAddress'))
3573
- .single()
3574
- .description('List of CC addresses')
3575
- .label('CcAddressList')
3576
- .when('mailMerge', {
3577
- is: Joi.exist().not(false, null),
3578
- then: Joi.forbidden('y')
3579
- }),
3580
-
3581
- bcc: Joi.array()
3582
- .items(addressSchema.label('BccAddress'))
3583
- .single()
3584
- .description('List of BCC addresses')
3585
- .label('BccAddressList')
3586
- .when('mailMerge', {
3587
- is: Joi.exist().not(false, null),
3588
- then: Joi.forbidden('y')
3589
- }),
3590
-
3591
- subject: templateSchemas.subject,
3592
- text: templateSchemas.text,
3593
- html: templateSchemas.html,
3594
- previewText: templateSchemas.previewText,
3595
-
3596
- template: Joi.string().max(256).example('example').description('Stored template ID to load the email content from'),
3597
-
3598
- render: Joi.object({
3599
- format: Joi.string()
3600
- .valid('html', 'markdown')
3601
- .default('html')
3602
- .description('Markup language for HTML ("html" or "markdown")')
3603
- .label('RenderFormat'),
3604
- params: Joi.object().label('RenderValues').description('An object of variables for the template renderer')
3605
- })
3606
- .allow(false)
3607
- .description('Template rendering options')
3608
- .when('mailMerge', {
3609
- is: Joi.exist().not(false, null),
3610
- then: Joi.forbidden('y')
3611
- })
3612
- .label('TemplateRender'),
3613
-
3614
- mailMerge: Joi.array()
3615
- .items(
3616
- Joi.object({
3617
- to: addressSchema.label('ToAddress').required(),
3618
- messageId: Joi.string().max(996).example('<test123@example.com>').description('Message ID'),
3619
- params: Joi.object().label('RenderValues').description('An object of variables for the template renderer'),
3620
- sendAt: Joi.date()
3621
- .iso()
3622
- .example('2021-07-08T07:06:34.336Z')
3623
- .description('Send message at specified time. Overrides message level `sendAt` value.')
3624
- }).label('MailMergeListEntry')
3625
- )
3626
- .min(1)
3627
- .description(
3628
- 'Mail merge options. A separate email is generated for each recipient. Using mail merge disables `messageId`, `envelope`, `to`, `cc`, `bcc`, `render` keys for the message root.'
3629
- )
3630
- .label('MailMergeList'),
3631
-
3632
- attachments: Joi.array()
3633
- .items(
3634
- Joi.object({
3635
- filename: Joi.string().max(256).example('transparent.gif'),
3636
- content: Joi.string()
3637
- .base64()
3638
- .max(MAX_ATTACHMENT_SIZE)
3639
- .required()
3640
- .example('R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=')
3641
- .description('Base64 formatted attachment file')
3642
- .when('reference', {
3643
- is: Joi.exist().not(false, null),
3644
- then: Joi.forbidden(),
3645
- otherwise: Joi.required()
3646
- }),
3647
-
3648
- contentType: Joi.string().lowercase().max(256).example('image/gif'),
3649
- contentDisposition: Joi.string().lowercase().valid('inline', 'attachment').label('AttachmentContentDisposition'),
3650
- cid: Joi.string().max(256).example('unique-image-id@localhost').description('Content-ID value for embedded images'),
3651
- encoding: Joi.string().valid('base64').default('base64').label('AttachmentEncoding'),
3652
-
3653
- reference: Joi.string()
3654
- .base64({ paddingRequired: false, urlSafe: true })
3655
- .max(256)
3656
- .allow(false, null)
3657
- .example('AAAAAQAACnAcde')
3658
- .description(
3659
- 'References an existing attachment by its ID instead of providing new attachment content. If this field is set, the `content` field must not be included. If not set, the `content` field is required.'
3660
- )
3661
- .label('AttachmentReference')
3662
- }).label('UploadAttachment')
3663
- )
3664
- .description('List of attachments')
3665
- .label('UploadAttachmentList'),
3666
-
3667
- messageId: Joi.string().max(996).example('<test123@example.com>').description('Message ID'),
3668
- headers: Joi.object().label('CustomHeaders').description('Custom Headers').unknown().example({
3669
- 'X-My-Custom-Header': 'Custom header value'
3670
- }),
3671
-
3672
- trackingEnabled: Joi.boolean()
3673
- .example(false)
3674
- .description('Should EmailEngine track clicks and opens for this message')
3675
- .meta({ swaggerHidden: true }),
3676
-
3677
- trackOpens: Joi.boolean().example(false).description('Should EmailEngine track opens for this message'),
3678
- trackClicks: Joi.boolean().example(false).description('Should EmailEngine track clicks for this message'),
3679
-
3680
- copy: Joi.boolean()
3681
- .allow(null)
3682
- .example(null)
3683
- .description(
3684
- "If set then either copies the message to the Sent Mail folder or not. If not set then uses the account's default setting."
3685
- ),
3686
-
3687
- sentMailPath: Joi.string()
3688
- .empty('')
3689
- .max(1024)
3690
- .example('Sent Mail')
3691
- .description("Upload sent message to this folder. By default the account's Sent Mail folder is used."),
3692
-
3693
- locale: Joi.string().empty('').max(100).example('fr').description('Optional locale').label('MessageLocale'),
3694
- tz: Joi.string().empty('').max(100).example('Europe/Tallinn').description('Optional timezone'),
3695
-
3696
- sendAt: Joi.date().iso().example('2021-07-08T07:06:34.336Z').description('Send message at specified time'),
3697
- deliveryAttempts: Joi.number()
3698
- .integer()
3699
- .example(10)
3700
- .description('How many delivery attempts to make until message is considered as failed'),
3701
- gateway: Joi.string().max(256).example('example').description('Optional SMTP gateway ID for message routing').label('MessageGateway'),
3702
-
3703
- listId: Joi.string()
3704
- .hostname()
3705
- .example('test-list')
3706
- .description(
3707
- 'List ID for Mail Merge. Must use a subdomain name format. Lists are registered ad-hoc, so a new identifier defines a new list.'
3708
- )
3709
- .label('ListID')
3710
- .when('mailMerge', {
3711
- is: Joi.exist().not(false, null),
3712
- then: Joi.optional(),
3713
- otherwise: Joi.forbidden()
3714
- }),
3715
-
3716
- dsn: Joi.object({
3717
- id: Joi.string().trim().empty('').max(256).description('The envelope identifier that would be included in the response (ENVID)'),
3718
- return: Joi.string()
3719
- .trim()
3720
- .empty('')
3721
- .valid('headers', 'full')
3722
- .required()
3723
- .description('Specifies if only headers or the entire body of the message should be included in the response (RET)')
3724
- .label('DsnReturn'),
3725
- notify: Joi.array()
3726
- .single()
3727
- .items(Joi.string().valid('never', 'success', 'failure', 'delay').label('NotifyEntry'))
3728
- .description('Defines the conditions under which a DSN response should be sent')
3729
- .label('DsnNotify'),
3730
- recipient: Joi.string().trim().empty('').email().description('The email address the DSN should be sent (ORCPT)')
3731
- })
3732
- .description('Request DSN notifications')
3733
- .label('DSN'),
3734
-
3735
- baseUrl: Joi.string()
3736
- .trim()
3737
- .empty('')
3738
- .uri({
3739
- scheme: ['http', 'https'],
3740
- allowRelative: false
3741
- })
3742
- .example('https://customer123.myservice.com')
3743
- .description('Optional base URL for trackers. This URL must point to your EmailEngine instance.'),
3744
-
3745
- proxy: settingsSchema.proxyUrl.description('Optional proxy URL to use when connecting to the SMTP server'),
3746
- localAddress: ipSchema.description('Optional local IP address to bind to when connecting to the SMTP server'),
3747
-
3748
- dryRun: Joi.boolean()
3749
- .truthy('Y', 'true', '1')
3750
- .falsy('N', 'false', 0)
3751
- .default(false)
3752
- .description(
3753
- 'If true, then EmailEngine does not send the email and returns an RFC822 formatted email file. Tracking information is not added to the email.'
3754
- )
3755
- .label('Preview')
3756
- })
3757
- .oxor('raw', 'html')
3758
- .oxor('raw', 'text')
3759
- .oxor('raw', 'text')
3760
- .oxor('raw', 'attachments')
3761
- .label('SubmitMessage')
3762
- .example({
3763
- to: [
3764
- {
3765
- name: 'Nyan Cat',
3766
- address: 'nyan.cat@example.com'
3767
- }
3768
- ],
3769
- subject: 'What a wonderful message!',
3770
- text: 'Hello from myself!',
3771
- html: '<p>Hello from myself!</p>',
3772
- attachments: [
3773
- {
3774
- filename: 'transparent.gif',
3775
- content: 'R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=',
3776
- contentType: 'image/gif'
3777
- }
3778
- ]
3779
- })
3780
- },
3781
-
3782
- response: {
3783
- schema: Joi.object({
3784
- response: Joi.string().example('Queued for delivery'),
3785
- messageId: Joi.string()
3786
- .example('<a2184d08-a470-fec6-a493-fa211a3756e9@example.com>')
3787
- .description('Message-ID header value. Not present for bulk messages.'),
3788
- queueId: Joi.string().example('d41f0423195f271f').description('Queue identifier for scheduled email. Not present for bulk messages.'),
3789
- sendAt: Joi.date().example('2021-07-08T07:06:34.336Z').description('Scheduled send time'),
3790
-
3791
- reference: Joi.object({
3792
- message: Joi.string()
3793
- .base64({ paddingRequired: false, urlSafe: true })
3794
- .max(256)
3795
- .required()
3796
- .example('AAAAAQAACnA')
3797
- .description('Referenced message ID'),
3798
- documentStore: Joi.boolean()
3799
- .example(true)
3800
- .description('Was the message data loaded from the Document Store')
3801
- .label('ResponseDocumentStore')
3802
- .meta({ swaggerHidden: true }),
3803
- success: Joi.boolean().example(true).description('Was the referenced message processed successfully').label('ResponseReferenceSuccess'),
3804
- error: Joi.string().example('Referenced message was not found').description('An error message if referenced message processing failed')
3805
- })
3806
- .description('Reference info if referencing was requested')
3807
- .label('ResponseReference'),
3808
-
3809
- preview: Joi.string()
3810
- .base64()
3811
- .example('Q29udGVudC1UeXBlOiBtdWx0aX...')
3812
- .description('Base64 encoded RFC822 email if a preview was requested')
3813
- .label('ResponsePreview'),
3814
-
3815
- mailMerge: Joi.array()
3816
- .items(
3817
- Joi.object({
3818
- success: Joi.boolean()
3819
- .example(true)
3820
- .description('Was the referenced message processed successfully')
3821
- .label('ResponseReferenceSuccess'),
3822
- to: addressSchema.label('ToAddressSingle'),
3823
- messageId: Joi.string().max(996).example('<test123@example.com>').description('Message ID'),
3824
- queueId: Joi.string()
3825
- .example('d41f0423195f271f')
3826
- .description('Queue identifier for scheduled email. Not present for bulk messages.'),
3827
- reference: Joi.object({
3828
- message: Joi.string()
3829
- .base64({ paddingRequired: false, urlSafe: true })
3830
- .max(256)
3831
- .required()
3832
- .example('AAAAAQAACnA')
3833
- .description('Referenced message ID'),
3834
- documentStore: Joi.boolean()
3835
- .example(true)
3836
- .description('Was the message data loaded from the Document Store')
3837
- .label('ResponseDocumentStore')
3838
- .meta({ swaggerHidden: true }),
3839
- success: Joi.boolean()
3840
- .example(true)
3841
- .description('Was the referenced message processed successfully')
3842
- .label('ResponseReferenceSuccess'),
3843
- error: Joi.string()
3844
- .example('Referenced message was not found')
3845
- .description('An error message if referenced message processing failed')
3846
- })
3847
- .description('Reference info if referencing was requested')
3848
- .label('ResponseReference'),
3849
- sendAt: Joi.date()
3850
- .iso()
3851
- .example('2021-07-08T07:06:34.336Z')
3852
- .description('Send message at specified time. Overrides message level `sendAt` value.'),
3853
- skipped: Joi.object({
3854
- reason: Joi.string().example('unsubscribe').description('Why this message was skipped'),
3855
- listId: Joi.string().example('test-list')
3856
- })
3857
- .description('Info about skipped message. If this value is set, then the message was not sent')
3858
- .label('SkippedMessageInfo')
3859
- })
3860
- .label('BulkResponseEntry')
3861
- .example({
3862
- success: true,
3863
- to: {
3864
- name: 'Andris 2',
3865
- address: 'andris@ethereal.email'
3866
- },
3867
- messageId: '<19b9c433-d428-f6d8-1d00-d666ebcadfc4@ekiri.ee>',
3868
- queueId: '1812477338914c8372a',
3869
- reference: {
3870
- message: 'AAAAAQAACnA',
3871
- success: true
3872
- },
3873
- sendAt: '2021-07-08T07:06:34.336Z'
3874
- })
3875
- .unknown()
3876
- )
3877
- .label('BulkResponseList')
3878
- .description('Bulk message responses')
3879
- }).label('SubmitMessageResponse'),
3880
- failAction: 'log'
3881
- }
3882
- }
3883
- });
3884
-
3885
- server.route({
3886
- method: 'GET',
3887
- path: '/v1/settings',
3888
-
3889
- async handler(request) {
3890
- let values = {};
3891
- for (let key of Object.keys(request.query)) {
3892
- if (request.query[key]) {
3893
- if (key === 'eventTypes') {
3894
- values[key] = Object.keys(consts)
3895
- .map(key => {
3896
- if (/_NOTIFY?/.test(key)) {
3897
- return consts[key];
3898
- }
3899
- return false;
3900
- })
3901
- .map(key => key);
3902
- continue;
3903
- }
3904
-
3905
- let value = await settings.get(key);
3906
-
3907
- if (settings.encryptedKeys.includes(key)) {
3908
- // do not reveal secret values
3909
- // instead show boolean value true if value is set, or false if it's not
3910
- value = value ? true : false;
3911
- }
3912
-
3913
- values[key] = value;
3914
- }
3915
- }
3916
- return values;
3917
- },
3918
- options: {
3919
- description: 'List specific settings',
3920
- notes: 'List setting values for specific keys',
3921
- tags: ['api', 'Settings'],
3922
-
3923
- auth: {
3924
- strategy: 'api-token',
3925
- mode: 'required'
3926
- },
3927
- cors: CORS_CONFIG,
3928
-
3929
- validate: {
3930
- options: {
3931
- stripUnknown: false,
3932
- abortEarly: false,
3933
- convert: true
3934
- },
3935
- failAction,
3936
-
3937
- query: Joi.object(settingsQuerySchema).label('SettingsQuery')
3938
- },
3939
-
3940
- response: {
3941
- schema: Joi.object(settingsSchema).label('SettingsQueryResponse'),
3942
- failAction: 'log'
3943
- }
3944
- }
3945
- });
3946
-
3947
- server.route({
3948
- method: 'POST',
3949
- path: '/v1/settings',
3950
-
3951
- async handler(request) {
3952
- let updated = [];
3953
- for (let key of Object.keys(request.payload)) {
3954
- switch (key) {
3955
- case 'serviceUrl': {
3956
- let url = new URL(request.payload.serviceUrl);
3957
- request.payload.serviceUrl = url.origin;
3958
- break;
3959
- }
3960
-
3961
- case 'webhooksEnabled':
3962
- if (!request.payload.webhooksEnabled) {
3963
- // clear error message (if exists)
3964
- await settings.clear('webhookErrorFlag');
3965
- }
3966
- break;
3967
- }
3968
-
3969
- await settings.set(key, request.payload[key]);
3970
- updated.push(key);
3971
- }
3972
-
3973
- notify('settings', request.payload);
3974
- if ('httpProxyEnabled' in request.payload || 'httpProxyUrl' in request.payload) {
3975
- reloadHttpProxyAgent().catch(err => logger.error({ msg: 'Failed to reload HTTP proxy agent', err }));
3976
- }
3977
- return { updated };
3978
- },
3979
- options: {
3980
- description: 'Set setting values',
3981
- notes: 'Set setting values for specific keys',
3982
- tags: ['api', 'Settings'],
3983
-
3984
- plugins: {},
3985
-
3986
- auth: {
3987
- strategy: 'api-token',
3988
- mode: 'required'
3989
- },
3990
- cors: CORS_CONFIG,
3991
-
3992
- validate: {
3993
- options: {
3994
- stripUnknown: false,
3995
- abortEarly: false,
3996
- convert: true
3997
- },
3998
- failAction,
3999
-
4000
- payload: Joi.object(settingsSchema).label('Settings')
4001
- },
4002
-
4003
- response: {
4004
- schema: Joi.object({
4005
- updated: Joi.array().items(Joi.string().example('notifyHeaders')).description('List of updated setting keys').label('UpdatedSettings')
4006
- }).label('SettingsUpdatedResponse'),
4007
- failAction: 'log'
4008
- }
4009
- }
4010
- });
4011
-
4012
- server.route({
4013
- method: 'GET',
4014
- path: '/v1/settings/queue/{queue}',
4015
-
4016
- async handler(request) {
4017
- try {
4018
- let queue = request.params.queue;
4019
- let values = {
4020
- queue
4021
- };
4022
-
4023
- const [resActive, resDelayed, resPaused, resWaiting, resMeta] = await redis
4024
- .multi()
4025
- .llen(`${REDIS_PREFIX}bull:${queue}:active`)
4026
- .zcard(`${REDIS_PREFIX}bull:${queue}:delayed`)
4027
- .llen(`${REDIS_PREFIX}bull:${queue}:paused`)
4028
- .llen(`${REDIS_PREFIX}bull:${queue}:wait`)
4029
- .hget(`${REDIS_PREFIX}bull:${queue}:meta`, 'paused')
4030
- .exec();
4031
-
4032
- if (resActive[0] || resDelayed[0] || resPaused[0] || resWaiting[0]) {
4033
- // counting failed
4034
- let err = new Error('Failed to count queue lengtho');
4035
- err.statusCode = 500;
4036
- throw err;
4037
- }
4038
-
4039
- values.jobs = {
4040
- active: Number(resActive[1]) || 0,
4041
- delayed: Number(resDelayed[1]) || 0,
4042
- paused: Number(resPaused[1]) || 0,
4043
- waiting: Number(resWaiting[1]) || 0
4044
- };
4045
-
4046
- values.paused = !!Number(resMeta[1]) || false;
4047
-
4048
- return values;
4049
- } catch (err) {
4050
- request.logger.error({ msg: 'API request failed', err });
4051
- if (Boom.isBoom(err)) {
4052
- throw err;
4053
- }
4054
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
4055
- if (err.code) {
4056
- error.output.payload.code = err.code;
4057
- }
4058
- throw error;
4059
- }
4060
- },
4061
- options: {
4062
- description: 'Show queue information',
4063
- notes: 'Show queue status and current state',
4064
- tags: ['api', 'Settings'],
4065
-
4066
- auth: {
4067
- strategy: 'api-token',
4068
- mode: 'required'
4069
- },
4070
- cors: CORS_CONFIG,
4071
-
4072
- validate: {
4073
- options: {
4074
- stripUnknown: false,
4075
- abortEarly: false,
4076
- convert: true
4077
- },
4078
- failAction,
4079
-
4080
- params: Joi.object({
4081
- queue: Joi.string()
4082
- .empty('')
4083
- .trim()
4084
- .valid('notify', 'submit', 'documents')
4085
- .required()
4086
- .example('notify')
4087
- .description('Queue ID')
4088
- .label('QueueId')
4089
- })
4090
- },
4091
-
4092
- response: {
4093
- schema: Joi.object({
4094
- queue: Joi.string()
4095
- .empty('')
4096
- .trim()
4097
- .valid('notify', 'submit', 'documents')
4098
- .required()
4099
- .example('notify')
4100
- .description('Queue ID')
4101
- .label('QueueIdResponse'),
4102
- jobs: Joi.object({
4103
- active: Joi.number().integer().example(123).description('Jobs that are currently being processed'),
4104
- delayed: Joi.number().integer().example(123).description('Jobs that are processed in the future'),
4105
- paused: Joi.number().integer().example(123).description('Jobs that would be processed once queue processing is resumed'),
4106
- waiting: Joi.number()
4107
- .integer()
4108
- .example(123)
4109
- .description('Jobs that should be processed, but are waiting until there are any free handlers')
4110
- }).label('QueueJobs'),
4111
- paused: Joi.boolean().example(false).description('Is the queue paused or not')
4112
- }).label('SettingsQueueResponse'),
4113
- failAction: 'log'
4114
- }
4115
- }
4116
- });
4117
-
4118
- server.route({
4119
- method: 'PUT',
4120
- path: '/v1/settings/queue/{queue}',
4121
-
4122
- async handler(request) {
4123
- try {
4124
- let queue = request.params.queue;
4125
-
4126
- let queueObj = {
4127
- documents: documentsQueue,
4128
- notify: notifyQueue,
4129
- submit: submitQueue
4130
- }[queue];
4131
-
4132
- let values = {
4133
- queue
4134
- };
4135
-
4136
- for (let key of Object.keys(request.payload)) {
4137
- switch (key) {
4138
- case 'paused':
4139
- if (request.payload[key]) {
4140
- await queueObj.pause();
4141
- } else {
4142
- await queueObj.resume();
4143
- }
4144
- break;
4145
- }
4146
- }
4147
-
4148
- values.paused = await queueObj.isPaused();
4149
-
4150
- return values;
4151
- } catch (err) {
4152
- request.logger.error({ msg: 'API request failed', err });
4153
- if (Boom.isBoom(err)) {
4154
- throw err;
4155
- }
4156
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
4157
- if (err.code) {
4158
- error.output.payload.code = err.code;
4159
- }
4160
- throw error;
4161
- }
4162
- },
4163
- options: {
4164
- description: 'Set queue settings',
4165
- notes: 'Set queue settings',
4166
- tags: ['api', 'Settings'],
4167
-
4168
- plugins: {},
4169
-
4170
- auth: {
4171
- strategy: 'api-token',
4172
- mode: 'required'
4173
- },
4174
- cors: CORS_CONFIG,
4175
-
4176
- validate: {
4177
- options: {
4178
- stripUnknown: false,
4179
- abortEarly: false,
4180
- convert: true
4181
- },
4182
- failAction,
4183
-
4184
- params: Joi.object({
4185
- queue: Joi.string()
4186
- .empty('')
4187
- .trim()
4188
- .valid('notify', 'submit', 'documents')
4189
- .required()
4190
- .example('notify')
4191
- .description('Queue ID')
4192
- .label('QueueIdParam')
4193
- }),
4194
-
4195
- payload: Joi.object({
4196
- paused: Joi.boolean().empty('').example(false).description('Set queue state to paused')
4197
- }).label('SettingsPutQueuePayload')
4198
- },
4199
-
4200
- response: {
4201
- schema: Joi.object({
4202
- queue: Joi.string()
4203
- .empty('')
4204
- .trim()
4205
- .valid('notify', 'submit', 'documents')
4206
- .required()
4207
- .example('notify')
4208
- .description('Queue ID')
4209
- .label('QueueIdPutResponse'),
4210
- paused: Joi.boolean().example(false).description('Is the queue paused or not')
4211
- }).label('SettingsPutQueueResponse'),
4212
- failAction: 'log'
4213
- }
4214
- }
4215
- });
4216
-
4217
- server.route({
4218
- method: 'GET',
4219
- path: '/v1/logs/{account}',
4220
-
4221
- async handler(request) {
4222
- return getLogs(redis, request.params.account);
4223
- },
4224
- options: {
4225
- description: 'Return IMAP logs for an account',
4226
- notes: 'Output is a downloadable text file',
4227
- tags: ['api', 'Logs'],
4228
-
4229
- auth: {
4230
- strategy: 'api-token',
4231
- mode: 'required'
4232
- },
4233
- cors: CORS_CONFIG,
4234
-
4235
- plugins: {
4236
- 'hapi-swagger': {
4237
- produces: ['text/plain']
4238
- }
4239
- },
4240
-
4241
- validate: {
4242
- options: {
4243
- stripUnknown: false,
4244
- abortEarly: false,
4245
- convert: true
4246
- },
4247
- failAction,
4248
-
4249
- params: Joi.object({
4250
- account: accountIdSchema.required()
4251
- })
4252
- }
4253
- }
4254
- });
4255
-
4256
- server.route({
4257
- method: 'GET',
4258
- path: '/v1/stats',
4259
-
4260
- async handler(request) {
4261
- return await getStats(redis, call, request.query.seconds);
4262
- },
4263
-
4264
- options: {
4265
- description: 'Return server stats',
4266
- tags: ['api', 'Stats'],
4267
-
4268
- auth: {
4269
- strategy: 'api-token',
4270
- mode: 'required'
4271
- },
4272
- cors: CORS_CONFIG,
4273
-
4274
- validate: {
4275
- options: {
4276
- stripUnknown: false,
4277
- abortEarly: false,
4278
- convert: true
4279
- },
4280
- failAction,
4281
-
4282
- query: Joi.object({
4283
- seconds: Joi.number()
4284
- .integer()
4285
- .empty('')
4286
- .min(0)
4287
- .max(MAX_DAYS_STATS * 24 * 3600)
4288
- .default(3600)
4289
- .example(3600)
4290
- .description('Duration for counters')
4291
- .label('CounterSeconds')
4292
- }).label('ServerStats')
4293
- },
4294
-
4295
- response: {
4296
- schema: Joi.object({
4297
- version: Joi.string().example(packageData.version).description('EmailEngine version number'),
4298
- license: Joi.string().example(packageData.license).description('EmailEngine license'),
4299
- accounts: Joi.number().integer().example(26).description('Number of registered accounts'),
4300
- node: Joi.string().example('16.10.0').description('Node.js Version'),
4301
- redis: Joi.string().example('6.2.4').description('Redis Version'),
4302
- connections: Joi.object({
4303
- init: Joi.number().integer().example(2).description('Accounts not yet initialized'),
4304
- connected: Joi.number().integer().example(8).description('Successfully connected accounts'),
4305
- connecting: Joi.number().integer().example(7).description('Connection is being established'),
4306
- authenticationError: Joi.number().integer().example(3).description('Authentication failed'),
4307
- connectError: Joi.number().integer().example(5).description('Connection failed due to technical error'),
4308
- unset: Joi.number().integer().example(0).description('Accounts without valid IMAP settings'),
4309
- disconnected: Joi.number().integer().example(1).description('IMAP connection was closed')
4310
- })
4311
- .description('Counts of accounts in different connection states')
4312
- .label('ConnectionsStats'),
4313
- counters: Joi.object().label('CounterStats').unknown()
4314
- }).label('SettingsResponse'),
4315
- failAction: 'log'
4316
- }
4317
- }
4318
- });
4319
-
4320
- server.route({
4321
- method: 'POST',
4322
- path: '/v1/verifyAccount',
4323
-
4324
- async handler(request) {
4325
- try {
4326
- return await verifyAccountInfo(redis, request.payload, request.logger.child({ action: 'verify-account' }));
4327
- } catch (err) {
4328
- request.logger.error({ msg: 'API request failed', err });
4329
- if (Boom.isBoom(err)) {
4330
- throw err;
4331
- }
4332
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
4333
- if (err.code) {
4334
- error.output.payload.code = err.code;
4335
- }
4336
- throw error;
4337
- }
4338
- },
4339
- options: {
4340
- description: 'Verify IMAP and SMTP settings',
4341
- notes: 'Checks if can connect and authenticate using provided account info',
4342
- tags: ['api', 'Account'],
4343
-
4344
- plugins: {},
4345
-
4346
- auth: {
4347
- strategy: 'api-token',
4348
- mode: 'required'
4349
- },
4350
- cors: CORS_CONFIG,
4351
-
4352
- validate: {
4353
- options: {
4354
- stripUnknown: false,
4355
- abortEarly: false,
4356
- convert: true
4357
- },
4358
- failAction,
4359
-
4360
- payload: Joi.object({
4361
- mailboxes: Joi.boolean().example(false).description('Include mailbox listing in response').default(false).label('IncludeMailboxes'),
4362
- imap: Joi.object(imapSchema).allow(false).description('IMAP configuration').label('ImapConfiguration'),
4363
- smtp: Joi.object(smtpSchema).allow(false).description('SMTP configuration').label('SmtpConfiguration'),
4364
- proxy: settingsSchema.proxyUrl,
4365
- smtpEhloName: settingsSchema.smtpEhloName
4366
- }).label('VerifyAccount')
4367
- },
4368
- response: {
4369
- schema: Joi.object({
4370
- imap: Joi.object({
4371
- success: Joi.boolean().example(true).description('Was IMAP account verified').label('VerifyImapSuccess'),
4372
- error: Joi.string()
4373
- .example('Something went wrong')
4374
- .description('Error messages for IMAP verification. Only present if success=false')
4375
- .label('VerifyImapError'),
4376
- code: Joi.string()
4377
- .example('ERR_SSL_WRONG_VERSION_NUMBER')
4378
- .description('Error code. Only present if success=false')
4379
- .label('VerifyImapCode')
4380
- }).label('VerifyImapResult'),
4381
- smtp: Joi.object({
4382
- success: Joi.boolean().example(true).description('Was SMTP account verified').label('VerifySmtpSuccess'),
4383
- error: Joi.string()
4384
- .example('Something went wrong')
4385
- .description('Error messages for SMTP verification. Only present if success=false')
4386
- .label('VerifySmtpError'),
4387
- code: Joi.string()
4388
- .example('ERR_SSL_WRONG_VERSION_NUMBER')
4389
- .description('Error code. Only present if success=false')
4390
- .label('VerifySmtpCode')
4391
- }).label('VerifySmtpResult'),
4392
- mailboxes: shortMailboxesSchema
4393
- }).label('VerifyAccountResponse'),
4394
- failAction: 'log'
4395
- }
4396
- }
4397
- });
4398
-
4399
- server.route({
4400
- method: 'GET',
4401
- path: '/v1/license',
4402
-
4403
- async handler(request) {
4404
- try {
4405
- const licenseInfo = await call({ cmd: 'license', timeout: request.headers['x-ee-timeout'] });
4406
- if (!licenseInfo) {
4407
- let err = new Error('Failed to load license info');
4408
- err.statusCode = 403;
4409
- throw err;
4410
- }
4411
- return licenseInfo;
4412
- } catch (err) {
4413
- request.logger.error({ msg: 'API request failed', err });
4414
- if (Boom.isBoom(err)) {
4415
- throw err;
4416
- }
4417
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
4418
- if (err.code) {
4419
- error.output.payload.code = err.code;
4420
- }
4421
- throw error;
4422
- }
4423
- },
4424
- options: {
4425
- description: 'Request license info',
4426
- notes: 'Get active license information',
4427
- tags: ['api', 'License'],
4428
-
4429
- auth: {
4430
- strategy: 'api-token',
4431
- mode: 'required'
4432
- },
4433
- cors: CORS_CONFIG,
4434
-
4435
- response: {
4436
- schema: licenseSchema.label('LicenseResponse'),
4437
- failAction: 'log'
4438
- }
4439
- }
4440
- });
4441
-
4442
- server.route({
4443
- method: 'DELETE',
4444
- path: '/v1/license',
4445
-
4446
- async handler(request) {
4447
- try {
4448
- const licenseInfo = await call({ cmd: 'removeLicense', timeout: request.headers['x-ee-timeout'] });
4449
- if (!licenseInfo) {
4450
- let err = new Error('Failed to clear license info');
4451
- err.statusCode = 403;
4452
- throw err;
4453
- }
4454
- return licenseInfo;
4455
- } catch (err) {
4456
- request.logger.error({ msg: 'API request failed', err });
4457
- if (Boom.isBoom(err)) {
4458
- throw err;
4459
- }
4460
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
4461
- if (err.code) {
4462
- error.output.payload.code = err.code;
4463
- }
4464
- throw error;
4465
- }
4466
- },
4467
- options: {
4468
- description: 'Remove license',
4469
- notes: 'Remove registered active license',
4470
- tags: ['api', 'License'],
4471
-
4472
- plugins: {},
4473
-
4474
- auth: {
4475
- strategy: 'api-token',
4476
- mode: 'required'
4477
- },
4478
- cors: CORS_CONFIG,
4479
-
4480
- response: {
4481
- schema: Joi.object({
4482
- active: Joi.boolean().example(false),
4483
- details: Joi.boolean().example(false),
4484
- type: Joi.string().example('SSPL-1.0-or-later')
4485
- }).label('EmptyLicenseResponse'),
4486
- failAction: 'log'
4487
- }
4488
- }
4489
- });
4490
-
4491
- server.route({
4492
- method: 'POST',
4493
- path: '/v1/license',
4494
-
4495
- async handler(request) {
4496
- try {
4497
- const licenseInfo = await call({ cmd: 'updateLicense', license: request.payload.license, timeout: request.headers['x-ee-timeout'] });
4498
- if (!licenseInfo) {
4499
- let err = new Error('Failed to update license. Check license file contents.');
4500
- err.statusCode = 403;
4501
- throw err;
4502
- }
4503
- return licenseInfo;
4504
- } catch (err) {
4505
- request.logger.error({ msg: 'API request failed', err });
4506
- if (Boom.isBoom(err)) {
4507
- throw err;
4508
- }
4509
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
4510
- if (err.code) {
4511
- error.output.payload.code = err.code;
4512
- }
4513
- throw error;
4514
- }
4515
- },
4516
- options: {
4517
- description: 'Register a license',
4518
- notes: 'Set up a license for EmailEngine to unlock all features',
4519
- tags: ['api', 'License'],
4520
-
4521
- plugins: {},
4522
-
4523
- auth: {
4524
- strategy: 'api-token',
4525
- mode: 'required'
4526
- },
4527
- cors: CORS_CONFIG,
4528
-
4529
- validate: {
4530
- options: {
4531
- stripUnknown: false,
4532
- abortEarly: false,
4533
- convert: true
4534
- },
4535
- failAction,
4536
-
4537
- payload: Joi.object({
4538
- license: Joi.string()
4539
- .max(10 * 1024)
4540
- .required()
4541
- .example('-----BEGIN LICENSE-----\r\n...')
4542
- .description('License file')
4543
- }).label('RegisterLicense')
4544
- },
4545
-
4546
- response: {
4547
- schema: licenseSchema.label('LicenseResponse'),
4548
- failAction: 'log'
4549
- }
4550
- }
4551
- });
4552
-
4553
- server.route({
4554
- method: 'GET',
4555
- path: '/v1/autoconfig',
4556
-
4557
- async handler(request) {
4558
- try {
4559
- let serverSettings = await autodetectImapSettings(request.query.email, request.app.gt);
4560
- return serverSettings;
4561
- } catch (err) {
4562
- request.logger.error({ msg: 'API request failed', err });
4563
- if (Boom.isBoom(err)) {
4564
- throw err;
4565
- }
4566
- return { imap: false, smtp: false, _source: 'unknown' };
4567
- }
4568
- },
4569
-
4570
- options: {
4571
- description: 'Discover Email settings',
4572
- notes: 'Try to discover IMAP and SMTP settings for an email account',
4573
- tags: ['api', 'Settings'],
4574
-
4575
- plugins: {},
4576
-
4577
- auth: {
4578
- strategy: 'api-token',
4579
- mode: 'required'
4580
- },
4581
- cors: CORS_CONFIG,
4582
-
4583
- validate: {
4584
- options: {
4585
- stripUnknown: false,
4586
- abortEarly: false,
4587
- convert: true
4588
- },
4589
- failAction,
4590
-
4591
- query: Joi.object({
4592
- email: Joi.string()
4593
- .email()
4594
- .required()
4595
- .example('sender@example.com')
4596
- .description('Email address to discover email settings for')
4597
- .label('EmailAddress')
4598
- }).label('AutodiscoverQuery')
4599
- },
4600
-
4601
- response: {
4602
- schema: Joi.object({
4603
- imap: Joi.object({
4604
- auth: Joi.object({
4605
- user: Joi.string().max(256).example('myuser@gmail.com').description('Account username')
4606
- }).label('DetectedAuthenticationInfo'),
4607
-
4608
- host: Joi.string().hostname().required().example('imap.gmail.com').description('Hostname to connect to'),
4609
- port: Joi.number()
4610
- .integer()
4611
- .min(1)
4612
- .max(64 * 1024)
4613
- .required()
4614
- .example(993)
4615
- .description('Service port number'),
4616
- secure: Joi.boolean().default(false).example(true).description('Should connection use TLS. Usually true for port 993')
4617
- }).label('ResolvedServerSettings'),
4618
- smtp: Joi.object({
4619
- auth: Joi.object({
4620
- user: Joi.string().max(256).example('myuser@gmail.com').description('Account username')
4621
- }).label('DetectedAuthenticationInfo'),
4622
-
4623
- host: Joi.string().hostname().required().example('imap.gmail.com').description('Hostname to connect to'),
4624
- port: Joi.number()
4625
- .integer()
4626
- .min(1)
4627
- .max(64 * 1024)
4628
- .required()
4629
- .example(993)
4630
- .description('Service port number'),
4631
- secure: Joi.boolean().default(false).example(true).description('Should connection use TLS. Usually true for port 993')
4632
- }).label('DiscoveredServerSettings'),
4633
- _source: Joi.string().example('srv').description('Source for the detected info')
4634
- }).label('DiscoveredEmailSettings'),
4635
- failAction: 'log'
4636
- }
4637
- }
4638
- });
4639
-
4640
- server.route({
4641
- method: 'GET',
4642
- path: '/v1/outbox',
4643
-
4644
- async handler(request) {
4645
- try {
4646
- return await outbox.list(Object.assign({ logger }, request.query));
4647
- } catch (err) {
4648
- request.logger.error({ msg: 'API request failed', err });
4649
- if (Boom.isBoom(err)) {
4650
- throw err;
4651
- }
4652
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
4653
- if (err.code) {
4654
- error.output.payload.code = err.code;
4655
- }
4656
- throw error;
4657
- }
4658
- },
4659
-
4660
- options: {
4661
- description: 'List queued messages',
4662
- notes: 'Lists messages in the Outbox',
4663
- tags: ['api', 'Outbox'],
4664
-
4665
- plugins: {},
4666
-
4667
- auth: {
4668
- strategy: 'api-token',
4669
- mode: 'required'
4670
- },
4671
- cors: CORS_CONFIG,
4672
-
4673
- validate: {
4674
- options: {
4675
- stripUnknown: false,
4676
- abortEarly: false,
4677
- convert: true
4678
- },
4679
- failAction,
4680
-
4681
- query: Joi.object({
4682
- page: Joi.number()
4683
- .integer()
4684
- .min(0)
4685
- .max(1024 * 1024)
4686
- .default(0)
4687
- .example(0)
4688
- .description('Page number (zero indexed, so use 0 for first page)')
4689
- .label('PageNumber'),
4690
- pageSize: Joi.number().integer().min(1).max(1000).default(20).example(20).description('How many entries per page').label('PageSize')
4691
- }).label('OutbixListFilter')
4692
- },
4693
-
4694
- response: {
4695
- schema: Joi.object({
4696
- total: Joi.number().integer().example(120).description('How many matching entries').label('TotalNumber'),
4697
- page: Joi.number().integer().example(0).description('Current page (0-based index)').label('PageNumber'),
4698
- pages: Joi.number().integer().example(24).description('Total page count').label('PagesNumber'),
4699
-
4700
- messages: Joi.array().items(outboxEntrySchema).label('OutboxListEntries')
4701
- }).label('OutboxListResponse'),
4702
- failAction: 'log'
4703
- }
4704
- }
4705
- });
4706
-
4707
- server.route({
4708
- method: 'GET',
4709
- path: '/v1/outbox/{queueId}',
4710
-
4711
- async handler(request) {
4712
- try {
4713
- let outboxEntry = await outbox.get({ queueId: request.params.queueId, logger });
4714
- if (!outboxEntry) {
4715
- let message = 'Requested queue entry was not found';
4716
- let error = Boom.boomify(new Error(message), { statusCode: 404 });
4717
- throw error;
4718
- }
4719
- return outboxEntry;
4720
- } catch (err) {
4721
- request.logger.error({ msg: 'API request failed', err });
4722
- if (Boom.isBoom(err)) {
4723
- throw err;
4724
- }
4725
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
4726
- if (err.code) {
4727
- error.output.payload.code = err.code;
4728
- }
4729
- throw error;
4730
- }
4731
- },
4732
-
4733
- options: {
4734
- description: 'Get queued message',
4735
- notes: 'Gets a queued message in the Outbox',
4736
- tags: ['api', 'Outbox'],
4737
-
4738
- plugins: {},
4739
-
4740
- auth: {
4741
- strategy: 'api-token',
4742
- mode: 'required'
4743
- },
4744
- cors: CORS_CONFIG,
4745
-
4746
- validate: {
4747
- options: {
4748
- stripUnknown: false,
4749
- abortEarly: false,
4750
- convert: true
4751
- },
4752
- failAction,
4753
-
4754
- params: Joi.object({
4755
- queueId: Joi.string().max(100).example('d41f0423195f271f').description('Queue identifier for scheduled email').required()
4756
- }).label('OutboxEntryParams')
4757
- },
4758
-
4759
- response: {
4760
- schema: outboxEntrySchema,
4761
- failAction: 'log'
4762
- }
4763
- }
4764
- });
4765
-
4766
- server.route({
4767
- method: 'DELETE',
4768
- path: '/v1/outbox/{queueId}',
4769
-
4770
- async handler(request) {
4771
- try {
4772
- return {
4773
- deleted: await outbox.del({ queueId: request.params.queueId, logger })
4774
- };
4775
- } catch (err) {
4776
- request.logger.error({ msg: 'API request failed', err });
4777
- if (Boom.isBoom(err)) {
4778
- throw err;
4779
- }
4780
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
4781
- if (err.code) {
4782
- error.output.payload.code = err.code;
4783
- }
4784
- throw error;
4785
- }
4786
- },
4787
- options: {
4788
- description: 'Remove a message',
4789
- notes: 'Remove a message from the outbox',
4790
- tags: ['api', 'Outbox'],
4791
-
4792
- plugins: {},
4793
-
4794
- auth: {
4795
- strategy: 'api-token',
4796
- mode: 'required'
4797
- },
4798
- cors: CORS_CONFIG,
4799
-
4800
- validate: {
4801
- options: {
4802
- stripUnknown: false,
4803
- abortEarly: false,
4804
- convert: true
4805
- },
4806
- failAction,
4807
-
4808
- params: Joi.object({
4809
- queueId: Joi.string().max(100).example('d41f0423195f271f').description('Queue identifier for scheduled email').required()
4810
- }).label('OutboxEntryParams')
4811
- },
4812
-
4813
- response: {
4814
- schema: Joi.object({
4815
- deleted: Joi.boolean().truthy('Y', 'true', '1').falsy('N', 'false', 0).default(true).description('Was the message deleted')
4816
- }).label('DeleteOutboxEntryResponse'),
4817
- failAction: 'log'
4818
- }
4819
- }
4820
- });
4821
-
4822
- // setup template routes
4823
- await templateRoutes({ server, call, CORS_CONFIG });
4824
-
4825
- // setup "chat with email" routes
4826
- await chatRoutes({ server, call, CORS_CONFIG });
4827
-
4828
- // setup account CRUD routes
4829
- await accountRoutes({
4830
- server,
4831
- call,
4832
- documentsQueue,
4833
- oauth2Schema,
4834
- imapSchema,
4835
- smtpSchema,
4836
- CORS_CONFIG,
4837
- AccountTypeSchema
4838
- });
4839
-
4840
- // setup message routes
4841
- await messageRoutes({
4842
- server,
4843
- call,
4844
- CORS_CONFIG,
4845
- MAX_ATTACHMENT_SIZE,
4846
- MAX_BODY_SIZE,
4847
- MAX_PAYLOAD_TIMEOUT
4848
- });
4849
-
4850
- // setup export routes
4851
- await exportRoutes({
4852
- server,
4853
- CORS_CONFIG
4854
- });
4855
-
4856
- server.route({
4857
- method: 'GET',
4858
- path: '/v1/webhookRoutes',
4859
-
4860
- async handler(request) {
4861
- try {
4862
- return await Webhooks.list(request.query.page, request.query.pageSize);
4863
- } catch (err) {
4864
- request.logger.error({ msg: 'API request failed', err });
4865
- if (Boom.isBoom(err)) {
4866
- throw err;
4867
- }
4868
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
4869
- if (err.code) {
4870
- error.output.payload.code = err.code;
4871
- }
4872
- throw error;
4873
- }
4874
- },
4875
-
4876
- options: {
4877
- description: 'List webhook routes',
4878
- notes: 'List custom webhook routes',
4879
- tags: ['api', 'Webhooks'],
4880
-
4881
- plugins: {},
4882
-
4883
- auth: {
4884
- strategy: 'api-token',
4885
- mode: 'required'
4886
- },
4887
- cors: CORS_CONFIG,
4888
-
4889
- validate: {
4890
- options: {
4891
- stripUnknown: false,
4892
- abortEarly: false,
4893
- convert: true
4894
- },
4895
- failAction,
4896
-
4897
- query: Joi.object({
4898
- page: Joi.number()
4899
- .integer()
4900
- .min(0)
4901
- .max(1024 * 1024)
4902
- .default(0)
4903
- .example(0)
4904
- .description('Page number (zero indexed, so use 0 for first page)')
4905
- .label('PageNumber'),
4906
- pageSize: Joi.number().integer().min(1).max(1000).default(20).example(20).description('How many entries per page').label('PageSize')
4907
- }).label('WebhookRoutesListRequest')
4908
- },
4909
-
4910
- response: {
4911
- schema: Joi.object({
4912
- total: Joi.number().integer().example(120).description('How many matching entries').label('TotalNumber'),
4913
- page: Joi.number().integer().example(0).description('Current page (0-based index)').label('PageNumber'),
4914
- pages: Joi.number().integer().example(24).description('Total page count').label('PagesNumber'),
4915
-
4916
- webhooks: Joi.array()
4917
- .items(
4918
- Joi.object({
4919
- id: Joi.string().max(256).required().example('AAABgS-UcAYAAAABAA').description('Webhook ID'),
4920
- name: Joi.string().max(256).example('Send to Slack').description('Name of the route').label('WebhookRouteName').required(),
4921
- description: Joi.string()
4922
- .allow('')
4923
- .max(1024)
4924
- .example('Something about the route')
4925
- .description('Optional description of the webhook route')
4926
- .label('WebhookRouteDescription'),
4927
- created: Joi.date().iso().example('2021-02-17T13:43:18.860Z').description('The time this route was created'),
4928
- updated: Joi.date().iso().example('2021-02-17T13:43:18.860Z').description('The time this route was last updated'),
4929
- enabled: Joi.boolean().example(true).description('Is the route enabled').label('WebhookRouteEnabled'),
4930
- targetUrl: settingsSchema.webhooks,
4931
- tcount: Joi.number().integer().example(123).description('How many times this route has been applied')
4932
- }).label('WebhookRoutesListEntry')
4933
- )
4934
- .label('WebhookRoutesList')
4935
- }).label('WebhookRoutesListResponse'),
4936
- failAction: 'log'
4937
- }
4938
- }
4939
- });
4940
-
4941
- server.route({
4942
- method: 'GET',
4943
- path: '/v1/webhookRoutes/webhookRoute/{webhookRoute}',
4944
-
4945
- async handler(request) {
4946
- try {
4947
- return await Webhooks.get(request.params.webhookRoute);
4948
- } catch (err) {
4949
- request.logger.error({ msg: 'API request failed', err });
4950
- if (Boom.isBoom(err)) {
4951
- throw err;
4952
- }
4953
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
4954
- if (err.code) {
4955
- error.output.payload.code = err.code;
4956
- }
4957
- throw error;
4958
- }
4959
- },
4960
-
4961
- options: {
4962
- description: 'Get webhook route information',
4963
- notes: 'Retrieve webhook route content and information',
4964
- tags: ['api', 'Webhooks'],
4965
-
4966
- plugins: {},
4967
-
4968
- auth: {
4969
- strategy: 'api-token',
4970
- mode: 'required'
4971
- },
4972
- cors: CORS_CONFIG,
4973
-
4974
- validate: {
4975
- options: {
4976
- stripUnknown: false,
4977
- abortEarly: false,
4978
- convert: true
4979
- },
4980
- failAction,
4981
- params: Joi.object({
4982
- webhookRoute: Joi.string().max(256).required().example('example').description('Webhook ID')
4983
- }).label('GetWebhookRouteRequest')
4984
- },
4985
-
4986
- response: {
4987
- schema: Joi.object({
4988
- id: Joi.string().max(256).required().example('AAABgS-UcAYAAAABAA').description('Webhook ID'),
4989
- name: Joi.string().max(256).example('Send to Slack').description('Name of the route').label('WebhookRouteName').required(),
4990
- description: Joi.string()
4991
- .allow('')
4992
- .max(1024)
4993
- .example('Something about the route')
4994
- .description('Optional description of the webhook route')
4995
- .label('WebhookRouteDescription'),
4996
- created: Joi.date().iso().example('2021-02-17T13:43:18.860Z').description('The time this route was created'),
4997
- updated: Joi.date().iso().example('2021-02-17T13:43:18.860Z').description('The time this route was last updated'),
4998
- enabled: Joi.boolean().example(true).description('Is the route enabled').label('WebhookRouteEnabled'),
4999
- targetUrl: settingsSchema.webhooks,
5000
- tcount: Joi.number().integer().example(123).description('How many times this route has been applied'),
5001
- content: Joi.object({
5002
- fn: Joi.string().example('return true;').description('Filter function'),
5003
- map: Joi.string().example('payload.ts = Date.now(); return payload;').description('Mapping function')
5004
- }).label('WebhookRouteContent')
5005
- }).label('WebhookRouteResponse'),
5006
- failAction: 'log'
5007
- }
5008
- }
5009
- });
5010
-
5011
- server.route({
5012
- method: 'GET',
5013
- path: '/v1/oauth2',
5014
-
5015
- async handler(request) {
5016
- try {
5017
- let response = await oauth2Apps.list(request.query.page, request.query.pageSize);
5018
-
5019
- for (let app of response.apps) {
5020
- for (let secretKey of ['clientSecret', 'serviceKey', 'accessToken', 'externalAccount']) {
5021
- if (app[secretKey]) {
5022
- app[secretKey] = '******';
5023
- }
5024
- }
5025
-
5026
- if (app.extraScopes && !app.extraScopes.length) {
5027
- delete app.extraScopes;
5028
- }
5029
-
5030
- if (app.app) {
5031
- delete app.app;
5032
- }
5033
-
5034
- flattenOAuthAppMeta(app);
5035
- }
5036
-
5037
- return response;
5038
- } catch (err) {
5039
- request.logger.error({ msg: 'API request failed', err });
5040
- if (Boom.isBoom(err)) {
5041
- throw err;
5042
- }
5043
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
5044
- if (err.code) {
5045
- error.output.payload.code = err.code;
5046
- }
5047
- throw error;
5048
- }
5049
- },
5050
-
5051
- options: {
5052
- description: 'List OAuth2 applications',
5053
- notes: 'Lists registered OAuth2 applications',
5054
- tags: ['api', 'OAuth2 Applications'],
5055
-
5056
- plugins: {},
5057
-
5058
- auth: {
5059
- strategy: 'api-token',
5060
- mode: 'required'
5061
- },
5062
- cors: CORS_CONFIG,
5063
-
5064
- validate: {
5065
- options: {
5066
- stripUnknown: false,
5067
- abortEarly: false,
5068
- convert: true
5069
- },
5070
- failAction,
5071
-
5072
- query: Joi.object({
5073
- page: Joi.number()
5074
- .integer()
5075
- .min(0)
5076
- .max(1024 * 1024)
5077
- .default(0)
5078
- .example(0)
5079
- .description('Page number (zero indexed, so use 0 for first page)')
5080
- .label('PageNumber'),
5081
- pageSize: Joi.number().integer().min(1).max(1000).default(20).example(20).description('How many entries per page').label('PageSize')
5082
- }).label('GatewaysFilter')
5083
- },
5084
-
5085
- response: {
5086
- schema: Joi.object({
5087
- total: Joi.number().integer().example(120).description('How many matching entries').label('TotalNumber'),
5088
- page: Joi.number().integer().example(0).description('Current page (0-based index)').label('PageNumber'),
5089
- pages: Joi.number().integer().example(24).description('Total page count').label('PagesNumber'),
5090
-
5091
- apps: Joi.array()
5092
- .items(
5093
- Joi.object({
5094
- id: Joi.string().max(256).required().example('AAABhaBPHscAAAAH').description('OAuth2 application ID'),
5095
- name: Joi.string().max(256).example('My OAuth2 App').description('Display name for the app'),
5096
- description: Joi.string().empty('').trim().max(1024).example('App description').description('OAuth2 application description'),
5097
- title: Joi.string().empty('').trim().max(256).example('App title').description('Title for the application button'),
5098
- provider: OAuth2ProviderSchema,
5099
- enabled: Joi.boolean()
5100
- .truthy('Y', 'true', '1', 'on')
5101
- .falsy('N', 'false', 0, '')
5102
- .example(true)
5103
- .description('Is the application enabled')
5104
- .label('AppEnabled'),
5105
- legacy: Joi.boolean()
5106
- .truthy('Y', 'true', '1', 'on')
5107
- .falsy('N', 'false', 0, '')
5108
- .example(true)
5109
- .description('`true` for older OAuth2 apps set via the settings endpoint'),
5110
- created: Joi.date().iso().example('2021-02-17T13:43:18.860Z').description('The time this entry was added').required(),
5111
- updated: Joi.date().iso().example('2021-02-17T13:43:18.860Z').description('The time this entry was updated'),
5112
- includeInListing: Joi.boolean()
5113
- .truthy('Y', 'true', '1', 'on')
5114
- .falsy('N', 'false', 0, '')
5115
- .example(true)
5116
- .description('Is the application listed in the hosted authentication form'),
5117
-
5118
- clientId: Joi.string()
5119
- .example('4f05f488-d858-4f2c-bd12-1039062612fe')
5120
- .description('Client or Application ID for 3-legged OAuth2 applications')
5121
- .label('OAuth2AppListClientId'),
5122
- clientSecret: Joi.string()
5123
- .example('******')
5124
- .description('Client secret for 3-legged OAuth2 applications. Actual value is not revealed.'),
5125
- authority: Joi.string().example('common').description('Authorization tenant value for Outlook OAuth2 applications'),
5126
- redirectUrl: Joi.string()
5127
- .uri({
5128
- scheme: ['http', 'https'],
5129
- allowRelative: false
5130
- })
5131
- .example('https://myservice.com/oauth')
5132
- .description('Redirect URL for 3-legged OAuth2 applications')
5133
- .label('OAuth2AppListRedirectUrl'),
5134
-
5135
- serviceClient: Joi.string()
5136
- .example('9103965568215821627203')
5137
- .description('Service client ID for 2-legged OAuth2 applications')
5138
- .label('OAuth2AppListServiceClient'),
5139
-
5140
- googleProjectId: googleProjectIdSchema,
5141
- googleWorkspaceAccounts: googleWorkspaceAccountsSchema,
5142
- googleTopicName: googleTopicNameSchema,
5143
- googleSubscriptionName: googleSubscriptionNameSchema,
5144
-
5145
- serviceClientEmail: Joi.string()
5146
- .email()
5147
- .example('name@project-123.iam.gserviceaccount.com')
5148
- .description('Service Client Email for 2-legged OAuth2 applications'),
5149
-
5150
- serviceKey: Joi.string()
5151
- .example('******')
5152
- .description('PEM formatted service secret for 2-legged OAuth2 applications. Actual value is not revealed.'),
5153
-
5154
- lastError: lastErrorSchema.allow(null),
5155
- pubSubError: pubSubErrorSchema.allow(null)
5156
- }).label('OAuth2ResponseItem')
5157
- )
5158
- .label('OAuth2Entries')
5159
- }).label('OAuth2FilterResponse'),
5160
- failAction: 'log'
5161
- }
5162
- }
5163
- });
5164
-
5165
- server.route({
5166
- method: 'GET',
5167
- path: '/v1/oauth2/{app}',
5168
-
5169
- async handler(request) {
5170
- try {
5171
- let app = await oauth2Apps.get(request.params.app);
5172
-
5173
- // remove secrets
5174
- for (let secretKey of ['clientSecret', 'serviceKey', 'accessToken', 'externalAccount']) {
5175
- if (app[secretKey]) {
5176
- app[secretKey] = '******';
5177
- }
5178
- }
5179
-
5180
- if (app.extraScopes && !app.extraScopes.length) {
5181
- delete app.extraScopes;
5182
- }
5183
-
5184
- if (app.app) {
5185
- delete app.app;
5186
- }
5187
-
5188
- flattenOAuthAppMeta(app);
5189
-
5190
- return app;
5191
- } catch (err) {
5192
- request.logger.error({ msg: 'API request failed', err });
5193
- if (Boom.isBoom(err)) {
5194
- throw err;
5195
- }
5196
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
5197
- if (err.code) {
5198
- error.output.payload.code = err.code;
5199
- }
5200
- throw error;
5201
- }
5202
- },
5203
- options: {
5204
- description: 'Get application info',
5205
- notes: 'Returns stored information about an OAuth2 application. Secrets are not included.',
5206
- tags: ['api', 'OAuth2 Applications'],
5207
-
5208
- auth: {
5209
- strategy: 'api-token',
5210
- mode: 'required'
5211
- },
5212
- cors: CORS_CONFIG,
5213
-
5214
- validate: {
5215
- options: {
5216
- stripUnknown: false,
5217
- abortEarly: false,
5218
- convert: true
5219
- },
5220
- failAction,
5221
-
5222
- params: Joi.object({
5223
- app: Joi.string().max(256).required().example('AAABhaBPHscAAAAH').description('OAuth2 application ID')
5224
- })
5225
- },
5226
-
5227
- response: {
5228
- schema: Joi.object({
5229
- id: Joi.string().max(256).required().example('AAABhaBPHscAAAAH').description('OAuth2 application ID'),
5230
- name: Joi.string().max(256).example('My OAuth2 App').description('Display name for the app'),
5231
- description: Joi.string().empty('').trim().max(1024).example('App description').description('OAuth2 application description'),
5232
- title: Joi.string().empty('').trim().max(256).example('App title').description('Title for the application button'),
5233
- provider: OAuth2ProviderSchema,
5234
- enabled: Joi.boolean()
5235
- .truthy('Y', 'true', '1', 'on')
5236
- .falsy('N', 'false', 0, '')
5237
- .example(true)
5238
- .description('Is the application enabled')
5239
- .label('AppEnabled'),
5240
- legacy: Joi.boolean()
5241
- .truthy('Y', 'true', '1', 'on')
5242
- .falsy('N', 'false', 0, '')
5243
- .example(true)
5244
- .description('`true` for older OAuth2 apps set via the settings endpoint'),
5245
- created: Joi.date().iso().example('2021-02-17T13:43:18.860Z').description('The time this entry was added').required(),
5246
- updated: Joi.date().iso().example('2021-02-17T13:43:18.860Z').description('The time this entry was updated'),
5247
- includeInListing: Joi.boolean()
5248
- .truthy('Y', 'true', '1', 'on')
5249
- .falsy('N', 'false', 0, '')
5250
- .example(true)
5251
- .description('Is the application listed in the hosted authentication form'),
5252
-
5253
- clientId: Joi.string()
5254
- .example('4f05f488-d858-4f2c-bd12-1039062612fe')
5255
- .description('Client or Application ID for 3-legged OAuth2 applications')
5256
- .label('OAuth2AppGetClientId'),
5257
- clientSecret: Joi.string().example('******').description('Client secret for 3-legged OAuth2 applications. Actual value is not revealed.'),
5258
- authority: Joi.string().example('common').description('Authorization tenant value for Outlook OAuth2 applications'),
5259
- redirectUrl: Joi.string()
5260
- .uri({
5261
- scheme: ['http', 'https'],
5262
- allowRelative: false
5263
- })
5264
- .example('https://myservice.com/oauth')
5265
- .description('Redirect URL for 3-legged OAuth2 applications')
5266
- .label('OAuth2AppGetRedirectUrl'),
5267
-
5268
- googleProjectId: googleProjectIdSchema,
5269
- googleWorkspaceAccounts: googleWorkspaceAccountsSchema,
5270
- googleTopicName: googleTopicNameSchema,
5271
- googleSubscriptionName: googleSubscriptionNameSchema,
5272
-
5273
- serviceClientEmail: Joi.string()
5274
- .email()
5275
- .example('name@project-123.iam.gserviceaccount.com')
5276
- .description('Service Client Email for 2-legged OAuth2 applications'),
5277
-
5278
- serviceClient: Joi.string()
5279
- .example('9103965568215821627203')
5280
- .description('Service client ID for 2-legged OAuth2 applications')
5281
- .label('OAuth2AppGetServiceClient'),
5282
-
5283
- serviceKey: Joi.string()
5284
- .example('******')
5285
- .description('PEM formatted service secret for 2-legged OAuth2 applications. Actual value is not revealed.'),
5286
-
5287
- accounts: Joi.number()
5288
- .integer()
5289
- .example(12)
5290
- .description('The number of accounts registered with this application. Not available for legacy apps.'),
5291
-
5292
- lastError: lastErrorSchema.allow(null),
5293
- pubSubError: pubSubErrorSchema.allow(null)
5294
- }).label('ApplicationResponse'),
5295
- failAction: 'log'
5296
- }
5297
- }
5298
- });
5299
-
5300
- server.route({
5301
- method: 'POST',
5302
- path: '/v1/oauth2',
5303
-
5304
- async handler(request) {
5305
- try {
5306
- let result = await oauth2Apps.create(request.payload);
5307
-
5308
- if (result && result.pubsubUpdates && Object.keys(result.pubsubUpdates).length > 0) {
5309
- await call({ cmd: 'googlePubSub', app: result.id });
5310
- delete result.pubsubUpdates;
5311
- }
5312
-
5313
- return result;
5314
- } catch (err) {
5315
- request.logger.error({ msg: 'API request failed', err });
5316
- if (Boom.isBoom(err)) {
5317
- throw err;
5318
- }
5319
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
5320
- if (err.code) {
5321
- error.output.payload.code = err.code;
5322
- }
5323
- throw error;
5324
- }
5325
- },
5326
-
5327
- options: {
5328
- description: 'Register OAuth2 application',
5329
- notes: 'Registers a new OAuth2 application for a specific provider',
5330
- tags: ['api', 'OAuth2 Applications'],
5331
-
5332
- plugins: {},
5333
-
5334
- auth: {
5335
- strategy: 'api-token',
5336
- mode: 'required'
5337
- },
5338
- cors: CORS_CONFIG,
5339
-
5340
- validate: {
5341
- options: {
5342
- stripUnknown: false,
5343
- abortEarly: false,
5344
- convert: true
5345
- },
5346
- failAction,
5347
-
5348
- payload: Joi.object(oauthCreateSchema).tailor('api').label('CreateOAuth2App')
5349
- },
5350
-
5351
- response: {
5352
- schema: Joi.object({
5353
- id: Joi.string().max(256).required().example('AAABhaBPHscAAAAH').description('OAuth2 application ID'),
5354
- created: Joi.boolean().truthy('Y', 'true', '1').falsy('N', 'false', 0).default(true).description('Was the app created')
5355
- }).label('CreateAppResponse'),
5356
- failAction: 'log'
5357
- }
5358
- }
5359
- });
5360
-
5361
- server.route({
5362
- method: 'PUT',
5363
- path: '/v1/oauth2/{app}',
5364
-
5365
- async handler(request) {
5366
- try {
5367
- let result = await oauth2Apps.update(request.params.app, request.payload);
5368
-
5369
- if (result && result.pubsubUpdates && Object.keys(result.pubsubUpdates).length > 0) {
5370
- await call({ cmd: 'googlePubSub', app: result.id });
5371
- delete result.pubsubUpdates;
5372
- }
5373
-
5374
- return result;
5375
- } catch (err) {
5376
- request.logger.error({ msg: 'API request failed', err });
5377
- if (Boom.isBoom(err)) {
5378
- throw err;
5379
- }
5380
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
5381
- if (err.code) {
5382
- error.output.payload.code = err.code;
5383
- }
5384
- throw error;
5385
- }
5386
- },
5387
- options: {
5388
- description: 'Update OAuth2 application',
5389
- notes: 'Updates OAuth2 application information',
5390
- tags: ['api', 'OAuth2 Applications'],
5391
-
5392
- plugins: {},
5393
-
5394
- auth: {
5395
- strategy: 'api-token',
5396
- mode: 'required'
5397
- },
5398
- cors: CORS_CONFIG,
5399
-
5400
- validate: {
5401
- options: {
5402
- stripUnknown: false,
5403
- abortEarly: false,
5404
- convert: true
5405
- },
5406
- failAction,
5407
-
5408
- params: Joi.object({
5409
- app: Joi.string().max(256).required().example('AAABhaBPHscAAAAH').description('OAuth2 application ID')
5410
- }),
5411
-
5412
- payload: Joi.object({
5413
- name: Joi.string().trim().empty('').max(256).example('My Gmail App').description('Application name'),
5414
- description: Joi.string().trim().allow('').max(1024).example('My cool app').description('Application description'),
5415
- title: Joi.string().allow('').trim().max(256).example('App title').description('Title for the application button'),
5416
-
5417
- enabled: Joi.boolean().truthy('Y', 'true', '1', 'on').falsy('N', 'false', 0, '').example(true).description('Enable this app'),
5418
-
5419
- clientId: Joi.string()
5420
- .trim()
5421
- .allow('', null, false)
5422
- .max(256)
5423
- .example('52422112755-3uov8bjwlrullq122rdm6l8ui25ho7qf.apps.googleusercontent.com')
5424
- .description('Client or Application ID for 3-legged OAuth2 applications')
5425
- .label('UpdateOAuth2ClientId'),
5426
-
5427
- clientSecret: Joi.string()
5428
- .trim()
5429
- .empty('')
5430
- .max(256)
5431
- .example('boT7Q~dUljnfFdVuqpC11g8nGMjO8kpRAv-ZB')
5432
- .description('Client secret for 3-legged OAuth2 applications'),
5433
-
5434
- pubSubApp: Joi.string()
5435
- .empty('')
5436
- .base64({ paddingRequired: false, urlSafe: true })
5437
- .max(512)
5438
- .example('AAAAAQAACnA')
5439
- .description('Cloud Pub/Sub app for Gmail API webhooks')
5440
- .label('UpdatePubSubAppId'),
5441
-
5442
- extraScopes: Joi.array()
5443
- .items(Joi.string().trim().max(255).example('User.Read').label('UpdateExtraScopeEntry'))
5444
- .description('OAuth2 Extra Scopes')
5445
- .label('UpdateOAuth2ExtraScopes'),
5446
-
5447
- skipScopes: Joi.array()
5448
- .items(Joi.string().trim().max(255).example('SMTP.Send').label('UpdateSkipScopeEntry'))
5449
- .description('OAuth2 scopes to skip from the base set')
5450
- .label('UpdateOAuth2SkipScopes'),
5451
-
5452
- serviceClient: Joi.string()
5453
- .trim()
5454
- .allow('', null, false)
5455
- .max(256)
5456
- .example('7103296518315821565203')
5457
- .description('Service client ID for 2-legged OAuth2 applications')
5458
- .label('UpdateServiceClient'),
5459
-
5460
- googleProjectId: googleProjectIdSchema,
5461
- googleWorkspaceAccounts: googleWorkspaceAccountsSchema,
5462
- googleTopicName: googleTopicNameSchema,
5463
- googleSubscriptionName: googleSubscriptionNameSchema,
5464
-
5465
- serviceClientEmail: Joi.string()
5466
- .email()
5467
- .example('name@project-123.iam.gserviceaccount.com')
5468
- .description('Service Client Email for 2-legged OAuth2 applications'),
5469
-
5470
- serviceKey: Joi.string()
5471
- .trim()
5472
- .empty('')
5473
- .max(100 * 1024)
5474
- .example('-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgk...')
5475
- .description('PEM formatted service secret for 2-legged OAuth2 applications'),
5476
-
5477
- authority: Joi.string()
5478
- .trim()
5479
- .empty('')
5480
- .max(1024)
5481
- .example('common')
5482
- .description('Authorization tenant value for Outlook OAuth2 applications')
5483
- .label('SupportedAccountTypes'),
5484
-
5485
- cloud: Joi.string()
5486
- .trim()
5487
- .empty('')
5488
- .valid('global', 'gcc-high', 'dod', 'china')
5489
- .example('global')
5490
- .description('Azure cloud type for Outlook OAuth2 applications')
5491
- .label('AzureCloud'),
5492
-
5493
- tenant: Joi.string().trim().empty('').max(1024).example('f8cdef31-a31e-4b4a-93e4-5f571e91255a').label('Directorytenant'),
5494
-
5495
- redirectUrl: Joi.string()
5496
- .allow('', null, false)
5497
- .uri({ scheme: ['http', 'https'], allowRelative: false })
5498
- .example('https://myservice.com/oauth')
5499
- .description('Redirect URL for 3-legged OAuth2 applications')
5500
- .label('UpdateOAuth2RedirectUrl')
5501
- }).label('UpdateOAuthApp')
5502
- },
5503
-
5504
- response: {
5505
- schema: Joi.object({
5506
- id: Joi.string().max(256).required().example('example').description('OAuth2 app ID')
5507
- }).label('UpdateOAuthAppResponse'),
5508
- failAction: 'log'
5509
- }
5510
- }
5511
- });
5512
-
5513
- server.route({
5514
- method: 'DELETE',
5515
- path: '/v1/oauth2/{app}',
5516
-
5517
- async handler(request) {
5518
- try {
5519
- let result = await oauth2Apps.del(request.params.app);
5520
-
5521
- try {
5522
- await call({ cmd: 'googlePubSubRemove', app: request.params.app });
5523
- } catch (err) {
5524
- request.logger.error({ msg: 'Failed to notify workers about OAuth2 app deletion', err, app: request.params.app });
5525
- }
5526
-
5527
- return result;
5528
- } catch (err) {
5529
- request.logger.error({ msg: 'API request failed', err });
5530
- if (Boom.isBoom(err)) {
5531
- throw err;
5532
- }
5533
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
5534
- if (err.code) {
5535
- error.output.payload.code = err.code;
5536
- }
5537
- throw error;
5538
- }
5539
- },
5540
- options: {
5541
- description: 'Remove OAuth2 application',
5542
- notes: 'Delete OAuth2 application data',
5543
- tags: ['api', 'OAuth2 Applications'],
5544
-
5545
- plugins: {},
5546
-
5547
- auth: {
5548
- strategy: 'api-token',
5549
- mode: 'required'
5550
- },
5551
- cors: CORS_CONFIG,
5552
-
5553
- validate: {
5554
- options: {
5555
- stripUnknown: false,
5556
- abortEarly: false,
5557
- convert: true
5558
- },
5559
- failAction,
5560
-
5561
- params: Joi.object({
5562
- app: Joi.string().max(256).required().example('AAABhaBPHscAAAAH').description('OAuth2 application ID')
5563
- }).label('DeleteAppRequest')
5564
- },
5565
-
5566
- response: {
5567
- schema: Joi.object({
5568
- id: Joi.string().max(256).required().example('AAABhaBPHscAAAAH').description('OAuth2 application ID'),
5569
- deleted: Joi.boolean().truthy('Y', 'true', '1').falsy('N', 'false', 0).default(true).description('Was the OAuth2 application deleted'),
5570
- accounts: Joi.number()
5571
- .integer()
5572
- .example(12)
5573
- .description('The number of accounts registered with this application. Not available for legacy apps.')
5574
- }).label('DeleteAppRequestResponse'),
5575
- failAction: 'log'
5576
- }
5577
- }
5578
- });
5579
-
5580
- server.route({
5581
- method: 'POST',
5582
- path: '/v1/oauth2/{app}/verify',
5583
-
5584
- async handler(request) {
5585
- try {
5586
- return await verifyOAuth2App(request.params.app, {
5587
- account: request.payload.account,
5588
- testConnection: request.payload.testConnection
5589
- });
5590
- } catch (err) {
5591
- request.logger.error({ msg: 'API request failed', err });
5592
- if (Boom.isBoom(err)) {
5593
- throw err;
5594
- }
5595
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
5596
- if (err.code) {
5597
- error.output.payload.code = err.code;
5598
- }
5599
- throw error;
5600
- }
5601
- },
5602
- options: {
5603
- description: 'Verify OAuth2 application setup',
5604
- notes: 'Runs the provider authentication chain step by step and reports which steps pass or fail, with hints for fixing failures. For service-account apps an optional mailbox address enables the delegation and live mailbox checks.',
5605
- tags: ['api', 'OAuth2 Applications'],
5606
-
5607
- plugins: {},
5608
-
5609
- auth: {
5610
- strategy: 'api-token',
5611
- mode: 'required'
5612
- },
5613
- cors: CORS_CONFIG,
5614
-
5615
- validate: {
5616
- options: {
5617
- stripUnknown: false,
5618
- abortEarly: false,
5619
- convert: true
5620
- },
5621
- failAction,
5622
-
5623
- params: Joi.object({
5624
- app: Joi.string().max(256).required().example('AAABhaBPHscAAAAH').description('OAuth2 application ID')
5625
- }),
5626
-
5627
- payload: Joi.object({
5628
- account: Joi.string()
5629
- .trim()
5630
- .empty('')
5631
- .max(256)
5632
- .example('user@example.com')
5633
- .description('Mailbox address used to verify domain-wide delegation and live mailbox access'),
5634
- testConnection: Joi.boolean()
5635
- .truthy('Y', 'true', '1', 'on')
5636
- .falsy('N', 'false', 0, '')
5637
- .default(true)
5638
- .description('Perform the live IMAP/API connection step when an access token is obtained')
5639
- }).label('VerifyOAuth2AppRequest')
5640
- },
5641
-
5642
- response: {
5643
- schema: Joi.object({
5644
- app: Joi.string().max(256).required().example('AAABhaBPHscAAAAH').description('OAuth2 application ID'),
5645
- provider: Joi.string().example('gmailService').description('Provider type'),
5646
- authMethod: Joi.string().allow(null).example('externalAccount').description('Authentication method for service-account apps'),
5647
- account: Joi.string().allow(null).example('user@example.com').description('Mailbox used for the delegation/mailbox checks'),
5648
- ok: Joi.boolean().example(true).description('True when no verification step failed'),
5649
- steps: Joi.array()
5650
- .items(
5651
- Joi.object({
5652
- id: Joi.string().example('signJwt').description('Step identifier'),
5653
- label: Joi.string().example('Sign assertion (signJwt)').description('Human readable step name'),
5654
- status: Joi.string().valid('ok', 'fail', 'skip').example('ok').description('Step outcome'),
5655
- message: Joi.string().allow(null).example('Assertion signed via IAM signJwt').description('Outcome detail'),
5656
- hint: Joi.string()
5657
- .example('Grant roles/iam.serviceAccountTokenCreator to the workload principal')
5658
- .description('How to fix a failed step')
5659
- }).label('OAuth2VerifyStep')
5660
- )
5661
- .label('OAuth2VerifySteps')
5662
- }).label('VerifyOAuth2AppResponse'),
5663
- failAction: 'log'
5664
- }
5665
- }
5666
- });
5667
-
5668
- server.route({
5669
- method: 'GET',
5670
- path: '/v1/gateways',
5671
-
5672
- async handler(request) {
5673
- try {
5674
- let gatewayObject = new Gateway({ redis, gateway: request.params.gateway, call, secret: await getSecret() });
5675
-
5676
- return await gatewayObject.listGateways(request.query.page, request.query.pageSize);
5677
- } catch (err) {
5678
- request.logger.error({ msg: 'API request failed', err });
5679
- if (Boom.isBoom(err)) {
5680
- throw err;
5681
- }
5682
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
5683
- if (err.code) {
5684
- error.output.payload.code = err.code;
5685
- }
5686
- throw error;
5687
- }
5688
- },
5689
-
5690
- options: {
5691
- description: 'List gateways',
5692
- notes: 'Lists registered gateways',
5693
- tags: ['api', 'SMTP Gateway'],
5694
-
5695
- plugins: {},
5696
-
5697
- auth: {
5698
- strategy: 'api-token',
5699
- mode: 'required'
5700
- },
5701
- cors: CORS_CONFIG,
5702
-
5703
- validate: {
5704
- options: {
5705
- stripUnknown: false,
5706
- abortEarly: false,
5707
- convert: true
5708
- },
5709
- failAction,
5710
-
5711
- query: Joi.object({
5712
- page: Joi.number()
5713
- .integer()
5714
- .min(0)
5715
- .max(1024 * 1024)
5716
- .default(0)
5717
- .example(0)
5718
- .description('Page number (zero indexed, so use 0 for first page)')
5719
- .label('PageNumber'),
5720
- pageSize: Joi.number().integer().min(1).max(1000).default(20).example(20).description('How many entries per page').label('PageSize')
5721
- }).label('GatewaysFilter')
5722
- },
5723
-
5724
- response: {
5725
- schema: Joi.object({
5726
- total: Joi.number().integer().example(120).description('How many matching entries').label('TotalNumber'),
5727
- page: Joi.number().integer().example(0).description('Current page (0-based index)').label('PageNumber'),
5728
- pages: Joi.number().integer().example(24).description('Total page count').label('PagesNumber'),
5729
-
5730
- gateways: Joi.array()
5731
- .items(
5732
- Joi.object({
5733
- gateway: Joi.string().max(256).required().example('example').description('Gateway ID'),
5734
- name: Joi.string().max(256).example('My Email Gateway').description('Display name for the gateway'),
5735
- deliveries: Joi.number().integer().empty('').example(100).description('Count of email deliveries using this gateway'),
5736
- lastUse: Joi.date().iso().example('2021-02-17T13:43:18.860Z').description('Last delivery time'),
5737
- lastError: lastErrorSchema.allow(null)
5738
- }).label('GatewayResponseItem')
5739
- )
5740
- .label('GatewayEntries')
5741
- }).label('GatewaysFilterResponse'),
5742
- failAction: 'log'
5743
- }
5744
- }
5745
- });
5746
-
5747
- server.route({
5748
- method: 'GET',
5749
- path: '/v1/gateway/{gateway}',
5750
-
5751
- async handler(request) {
5752
- let gatewayObject = new Gateway({ redis, gateway: request.params.gateway, call, secret: await getSecret() });
5753
- try {
5754
- let gatewayData = await gatewayObject.loadGatewayData();
5755
-
5756
- // remove secrets
5757
- if (gatewayData.pass) {
5758
- gatewayData.pass = '******';
5759
- }
5760
-
5761
- let result = {};
5762
-
5763
- for (let key of ['gateway', 'name', 'host', 'port', 'user', 'pass', 'secure', 'deliveries', 'lastUse', 'lastError']) {
5764
- if (key in gatewayData) {
5765
- result[key] = gatewayData[key];
5766
- }
5767
- }
5768
-
5769
- return result;
5770
- } catch (err) {
5771
- request.logger.error({ msg: 'API request failed', err });
5772
- if (Boom.isBoom(err)) {
5773
- throw err;
5774
- }
5775
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
5776
- if (err.code) {
5777
- error.output.payload.code = err.code;
5778
- }
5779
- throw error;
5780
- }
5781
- },
5782
- options: {
5783
- description: 'Get gateway info',
5784
- notes: 'Returns stored information about the gateway. Passwords are not included.',
5785
- tags: ['api', 'SMTP Gateway'],
5786
-
5787
- auth: {
5788
- strategy: 'api-token',
5789
- mode: 'required'
5790
- },
5791
- cors: CORS_CONFIG,
5792
-
5793
- validate: {
5794
- options: {
5795
- stripUnknown: false,
5796
- abortEarly: false,
5797
- convert: true
5798
- },
5799
- failAction,
5800
-
5801
- params: Joi.object({
5802
- gateway: Joi.string().max(256).required().example('example').description('Gateway ID')
5803
- })
5804
- },
5805
-
5806
- response: {
5807
- schema: Joi.object({
5808
- gateway: Joi.string().max(256).required().example('example').description('Gateway ID'),
5809
-
5810
- name: Joi.string().max(256).required().example('My Email Gateway').description('Display name for the gateway'),
5811
- deliveries: Joi.number().integer().empty('').example(100).description('Count of email deliveries using this gateway'),
5812
- lastUse: Joi.date().iso().example('2021-02-17T13:43:18.860Z').description('Last delivery time'),
5813
-
5814
- user: Joi.string().empty('').trim().max(1024).description('SMTP authentication username').label('UserName'),
5815
- pass: Joi.string().empty('').max(1024).description('SMTP authentication password').label('Password'),
5816
-
5817
- host: Joi.string().hostname().example('smtp.gmail.com').description('Hostname to connect to').label('Hostname'),
5818
- port: Joi.number()
5819
- .integer()
5820
- .min(1)
5821
- .max(64 * 1024)
5822
- .example(465)
5823
- .description('Service port number')
5824
- .label('Port'),
5825
-
5826
- secure: Joi.boolean()
5827
- .truthy('Y', 'true', '1', 'on')
5828
- .falsy('N', 'false', 0, '')
5829
- .default(false)
5830
- .example(true)
5831
- .description('Should connection use TLS. Usually true for port 465')
5832
- .label('GatewayTlsOptions'),
5833
-
5834
- lastError: lastErrorSchema.allow(null)
5835
- }).label('GatewayResponse'),
5836
- failAction: 'log'
5837
- }
5838
- }
5839
- });
5840
-
5841
- server.route({
5842
- method: 'POST',
5843
- path: '/v1/gateway',
5844
-
5845
- async handler(request) {
5846
- let gatewayObject = new Gateway({ redis, call, secret: await getSecret() });
5847
-
5848
- try {
5849
- let result = await gatewayObject.create(request.payload);
5850
- return result;
5851
- } catch (err) {
5852
- request.logger.error({ msg: 'API request failed', err });
5853
- if (Boom.isBoom(err)) {
5854
- throw err;
5855
- }
5856
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
5857
- if (err.code) {
5858
- error.output.payload.code = err.code;
5859
- }
5860
- throw error;
5861
- }
5862
- },
5863
-
5864
- options: {
5865
- description: 'Register new gateway',
5866
- notes: 'Registers a new SMP gateway',
5867
- tags: ['api', 'SMTP Gateway'],
5868
-
5869
- plugins: {},
5870
-
5871
- auth: {
5872
- strategy: 'api-token',
5873
- mode: 'required'
5874
- },
5875
- cors: CORS_CONFIG,
5876
-
5877
- validate: {
5878
- options: {
5879
- stripUnknown: false,
5880
- abortEarly: false,
5881
- convert: true
5882
- },
5883
- failAction,
5884
-
5885
- payload: Joi.object({
5886
- gateway: Joi.string().empty('').trim().max(256).default(null).example('sendgun').description('Gateway ID').label('Gateway ID').required(),
5887
-
5888
- name: Joi.string().empty('').max(256).example('John Smith').description('Account Name').label('Gateway Name').required(),
5889
-
5890
- user: Joi.string().empty('').trim().default(null).max(1024).description('SMTP authentication username').label('UserName'),
5891
- pass: Joi.string().empty('').max(1024).default(null).description('SMTP authentication password').label('Password'),
5892
-
5893
- host: Joi.string().hostname().example('smtp.gmail.com').description('Hostname to connect to').label('Hostname').required(),
5894
- port: Joi.number()
5895
- .integer()
5896
- .min(1)
5897
- .max(64 * 1024)
5898
- .example(465)
5899
- .description('Service port number')
5900
- .label('Port')
5901
- .required(),
5902
-
5903
- secure: Joi.boolean()
5904
- .truthy('Y', 'true', '1', 'on')
5905
- .falsy('N', 'false', 0, '')
5906
- .default(false)
5907
- .example(true)
5908
- .description('Should connection use TLS. Usually true for port 465')
5909
- .label('GatewayCreateTlsOptions')
5910
- }).label('CreateGateway')
5911
- },
5912
-
5913
- response: {
5914
- schema: Joi.object({
5915
- gateway: Joi.string().max(256).required().example('example').description('Gateway ID'),
5916
- state: Joi.string()
5917
- .required()
5918
- .valid('existing', 'new')
5919
- .example('new')
5920
- .description('Is the gateway new or updated existing')
5921
- .label('CreateGatewayState')
5922
- }).label('CreateGatewayResponse'),
5923
- failAction: 'log'
5924
- }
5925
- }
5926
- });
5927
-
5928
- server.route({
5929
- method: 'PUT',
5930
- path: '/v1/gateway/edit/{gateway}',
5931
-
5932
- async handler(request) {
5933
- let gatewayObject = new Gateway({ redis, gateway: request.params.gateway, call, secret: await getSecret() });
5934
-
5935
- try {
5936
- return await gatewayObject.update(request.payload);
5937
- } catch (err) {
5938
- request.logger.error({ msg: 'API request failed', err });
5939
- if (Boom.isBoom(err)) {
5940
- throw err;
5941
- }
5942
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
5943
- if (err.code) {
5944
- error.output.payload.code = err.code;
5945
- }
5946
- throw error;
5947
- }
5948
- },
5949
- options: {
5950
- description: 'Update gateway info',
5951
- notes: 'Updates gateway information',
5952
- tags: ['api', 'SMTP Gateway'],
5953
-
5954
- plugins: {},
5955
-
5956
- auth: {
5957
- strategy: 'api-token',
5958
- mode: 'required'
5959
- },
5960
- cors: CORS_CONFIG,
5961
-
5962
- validate: {
5963
- options: {
5964
- stripUnknown: false,
5965
- abortEarly: false,
5966
- convert: true
5967
- },
5968
- failAction,
5969
-
5970
- params: Joi.object({
5971
- gateway: Joi.string().max(256).required().example('example').description('Gateway ID')
5972
- }),
5973
-
5974
- payload: Joi.object({
5975
- name: Joi.string().empty('').max(256).example('John Smith').description('Account Name').label('Gateway Name'),
5976
-
5977
- user: Joi.string().empty('').trim().max(1024).allow(null).description('SMTP authentication username').label('UserName'),
5978
- pass: Joi.string().empty('').max(1024).allow(null).description('SMTP authentication password').label('Password'),
5979
-
5980
- host: Joi.string().hostname().empty('').example('smtp.gmail.com').description('Hostname to connect to').label('Hostname'),
5981
- port: Joi.number()
5982
- .integer()
5983
- .min(1)
5984
- .empty('')
5985
- .max(64 * 1024)
5986
- .example(465)
5987
- .description('Service port number')
5988
- .label('Port'),
5989
-
5990
- secure: Joi.boolean()
5991
- .truthy('Y', 'true', '1', 'on')
5992
- .falsy('N', 'false', 0, '')
5993
- .example(true)
5994
- .description('Should connection use TLS. Usually true for port 465')
5995
- .label('GatewayUpdateTlsOptions')
5996
- }).label('UpdateGateway')
5997
- },
5998
-
5999
- response: {
6000
- schema: Joi.object({
6001
- gateway: Joi.string().max(256).required().example('example').description('Gateway ID')
6002
- }).label('UpdateGatewayResponse'),
6003
- failAction: 'log'
6004
- }
6005
- }
6006
- });
6007
-
6008
- server.route({
6009
- method: 'DELETE',
6010
- path: '/v1/gateway/{gateway}',
6011
-
6012
- async handler(request) {
6013
- let gatewayObject = new Gateway({
6014
- redis,
6015
- gateway: request.params.gateway,
6016
- secret: await getSecret()
6017
- });
6018
-
6019
- try {
6020
- return await gatewayObject.delete();
6021
- } catch (err) {
6022
- request.logger.error({ msg: 'API request failed', err });
6023
- if (Boom.isBoom(err)) {
6024
- throw err;
6025
- }
6026
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
6027
- if (err.code) {
6028
- error.output.payload.code = err.code;
6029
- }
6030
- throw error;
6031
- }
6032
- },
6033
- options: {
6034
- description: 'Remove SMTP gateway',
6035
- notes: 'Delete SMTP gateway data',
6036
- tags: ['api', 'SMTP Gateway'],
6037
-
6038
- plugins: {},
6039
-
6040
- auth: {
6041
- strategy: 'api-token',
6042
- mode: 'required'
6043
- },
6044
- cors: CORS_CONFIG,
6045
-
6046
- validate: {
6047
- options: {
6048
- stripUnknown: false,
6049
- abortEarly: false,
6050
- convert: true
6051
- },
6052
- failAction,
6053
-
6054
- params: Joi.object({
6055
- gateway: Joi.string().max(256).required().example('example').description('Gateway ID')
6056
- }).label('DeleteRequest')
6057
- },
6058
-
6059
- response: {
6060
- schema: Joi.object({
6061
- gateway: Joi.string().max(256).required().example('example').description('Gateway ID'),
6062
- deleted: Joi.boolean().truthy('Y', 'true', '1').falsy('N', 'false', 0).default(true).description('Was the gateway deleted')
6063
- }).label('DeleteGatewayResponse'),
6064
- failAction: 'log'
6065
- }
6066
- }
6067
- });
6068
-
6069
- server.route({
6070
- method: 'GET',
6071
- path: '/v1/account/{account}/oauth-token',
6072
-
6073
- async handler(request) {
6074
- let enableOAuthTokensApi = await settings.get('enableOAuthTokensApi');
6075
- if (!enableOAuthTokensApi) {
6076
- let error = Boom.boomify(new Error('Disabled API endpoint'), { statusCode: 403 });
6077
- error.output.payload.code = 'ApiEndpointDisabled';
6078
- throw error;
6079
- }
6080
-
6081
- let accountObject = new Account({
6082
- redis,
6083
- account: request.params.account,
6084
- call,
6085
- secret: await getSecret(),
6086
- timeout: request.headers['x-ee-timeout']
6087
- });
6088
-
6089
- try {
6090
- const tokenData = await accountObject.getActiveAccessTokenData();
6091
-
6092
- // Record metric if token was actually refreshed (not cached)
6093
- if (!tokenData.cached) {
6094
- const provider = tokenData.provider || 'unknown';
6095
- metrics(request.logger, 'oauth2TokenRefresh', 'inc', { status: 'success', provider, statusCode: '200' });
6096
- }
6097
-
6098
- return tokenData;
6099
- } catch (err) {
6100
- // Record failed token refresh
6101
- const statusCode = String(err.statusCode || 0);
6102
- metrics(request.logger, 'oauth2TokenRefresh', 'inc', { status: 'failure', provider: 'unknown', statusCode });
6103
-
6104
- request.logger.error({ msg: 'API request failed', err });
6105
- if (Boom.isBoom(err)) {
6106
- throw err;
6107
- }
6108
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
6109
- if (err.code) {
6110
- error.output.payload.code = err.code;
6111
- }
6112
- throw error;
6113
- }
6114
- },
6115
-
6116
- options: {
6117
- description: 'Get OAuth2 access token',
6118
- notes: 'Get the active OAuth2 access token for an account. NB! This endpoint is disabled by default and needs activation on the Service configuration page.',
6119
- tags: ['api', 'Account'],
6120
-
6121
- plugins: {},
6122
-
6123
- auth: {
6124
- strategy: 'api-token',
6125
- mode: 'required'
6126
- },
6127
- cors: CORS_CONFIG,
6128
-
6129
- validate: {
6130
- options: {
6131
- stripUnknown: false,
6132
- abortEarly: false,
6133
- convert: true
6134
- },
6135
- failAction,
6136
- params: Joi.object({
6137
- account: accountIdSchema.required()
6138
- })
6139
- },
6140
-
6141
- response: {
6142
- schema: Joi.object({
6143
- account: accountIdSchema.required(),
6144
- user: Joi.string().max(256).required().example('user@example.com').description('Username'),
6145
- accessToken: Joi.string().max(256).required().example('aGVsbG8gd29ybGQ=').description('Access Token').label('OAuthAccessToken'),
6146
- provider: OAuth2ProviderSchema
6147
- }).label('AccountTokenResponse'),
6148
- failAction: 'log'
6149
- }
6150
- }
6151
- });
6152
-
6153
- server.route({
6154
- method: 'GET',
6155
- path: '/v1/account/{account}/server-signatures',
6156
-
6157
- async handler(request) {
6158
- let accountObject = new Account({
6159
- redis,
6160
- account: request.params.account,
6161
- call,
6162
- secret: await getSecret(),
6163
- timeout: request.headers['x-ee-timeout']
6164
- });
6165
- try {
6166
- return await accountObject.listSignatures(request.query);
6167
- } catch (err) {
6168
- request.logger.error({ msg: 'API request failed', err });
6169
- if (Boom.isBoom(err)) {
6170
- throw err;
6171
- }
6172
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
6173
- if (err.code) {
6174
- error.output.payload.code = err.code;
6175
- }
6176
- throw error;
6177
- }
6178
- },
6179
-
6180
- options: {
6181
- description: 'List Account Signatures',
6182
- notes: 'Returns signatures associated with the account. Currently only Gmail is supported, and only "new message" signatures from the "sendAs" list are returned.',
6183
- tags: ['api', 'Account'],
6184
-
6185
- plugins: {},
6186
-
6187
- auth: {
6188
- strategy: 'api-token',
6189
- mode: 'required'
6190
- },
6191
- cors: CORS_CONFIG,
6192
-
6193
- validate: {
6194
- options: {
6195
- stripUnknown: false,
6196
- abortEarly: false,
6197
- convert: true
6198
- },
6199
- failAction,
6200
- params: Joi.object({
6201
- account: accountIdSchema.required()
6202
- })
6203
- },
6204
-
6205
- response: {
6206
- schema: Joi.object({
6207
- signatures: Joi.array()
6208
- .items(
6209
- Joi.object({
6210
- address: Joi.string().email().example('user@example.com').description('Email address associated with the signature').required(),
6211
- signature: Joi.string().example('<div>Best regards,</div>').description('Signature HTML code').required()
6212
- }).label('SignatureResponseItem')
6213
- )
6214
- .label('SignatureEntries')
6215
- }).label('AccountSignaturesResponse'),
6216
- failAction: 'log'
6217
- }
6218
- }
6219
- });
6220
-
6221
- server.route({
6222
- method: 'POST',
6223
- path: '/v1/delivery-test/account/{account}',
6224
- async handler(request) {
6225
- let accountObject = new Account({
6226
- redis,
6227
- account: request.params.account,
6228
- call,
6229
- secret: await getSecret(),
6230
- timeout: request.headers['x-ee-timeout']
6231
- });
6232
-
6233
- try {
6234
- // throws if account does not exist
6235
- let accountData = await accountObject.loadAccountData();
6236
-
6237
- request.logger.info({ msg: 'Requested SMTP delivery test', account: request.params.account });
6238
-
6239
- let headers = {
6240
- 'Content-Type': 'application/json',
6241
- 'User-Agent': `${packageData.name}/${packageData.version} (+${packageData.homepage})`
6242
- };
6243
-
6244
- let res = await fetchCmd(`${SMTP_TEST_HOST}/test-address`, {
6245
- method: 'post',
6246
- body: JSON.stringify({
6247
- version: packageData.version,
6248
- requestor: '@postalsys/emailengine-app'
6249
- }),
6250
- headers,
6251
- dispatcher: httpAgent.retry
6252
- });
6253
-
6254
- if (!res.ok) {
6255
- let err = new Error(`Invalid response: ${res.status} ${res.statusText}`);
6256
- err.statusCode = res.status;
6257
-
6258
- try {
6259
- err.details = await res.json();
6260
- } catch (err) {
6261
- // ignore
6262
- }
6263
-
6264
- throw err;
6265
- }
6266
-
6267
- let testAccount = await res.json();
6268
- if (!testAccount || !testAccount.user) {
6269
- let err = new Error(`Invalid test account`);
6270
- err.statusCode = 500;
6271
-
6272
- try {
6273
- err.details = testAccount;
6274
- } catch (err) {
6275
- // ignore
6276
- }
6277
-
6278
- throw err;
6279
- }
6280
-
6281
- if (request.payload.gateway) {
6282
- // try to load the gateway, throws if not set
6283
- let gatewayObject = new Gateway({ redis, gateway: request.payload.gateway, call, secret: await getSecret() });
6284
- await gatewayObject.loadGatewayData();
6285
- }
6286
-
6287
- try {
6288
- let now = new Date().toISOString();
6289
- let queueResponse = await accountObject.queueMessage(
6290
- {
6291
- account: accountData.account,
6292
- subject: `Delivery test ${now}`,
6293
- text: `Hello
6294
-
6295
- This is an automated email to test deliverability settings. If you see this email, you can safely delete it.
6296
-
6297
- ${now}`,
6298
- html: `<p>Hello</p>
6299
- <p>This is an automated email to test deliverability settings. If you see this email, you can safely delete it.</p>
6300
- <p>${now}</p>`,
6301
- from: {
6302
- name: accountData.name,
6303
- address: accountData.email
6304
- },
6305
- to: [{ name: 'Delivery Test Server', address: testAccount.address }],
6306
- copy: false,
6307
- gateway: request.payload.gateway,
6308
- feedbackKey: `${REDIS_PREFIX}test-send:${testAccount.user}`,
6309
- deliveryAttempts: 1
6310
- },
6311
- { source: 'test' }
6312
- );
6313
-
6314
- return {
6315
- success: !!queueResponse.queueId,
6316
- deliveryTest: testAccount.user
6317
- };
6318
- } catch (err) {
6319
- return {
6320
- error: err.message
6321
- };
6322
- }
6323
- } catch (err) {
6324
- request.logger.error({ msg: 'API request failed', err });
6325
- if (Boom.isBoom(err)) {
6326
- throw err;
6327
- }
6328
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
6329
- if (err.code) {
6330
- error.output.payload.code = err.code;
6331
- }
6332
- if (err.details) {
6333
- error.output.payload.details = err.details;
6334
- }
6335
- throw error;
6336
- }
6337
- },
6338
- options: {
6339
- description: 'Create delivery test',
6340
- notes: 'Initiate a delivery test',
6341
- tags: ['api', 'Delivery Test'],
6342
-
6343
- auth: {
6344
- strategy: 'api-token',
6345
- mode: 'required'
6346
- },
6347
- cors: CORS_CONFIG,
6348
-
6349
- validate: {
6350
- options: {
6351
- stripUnknown: false,
6352
- abortEarly: false,
6353
- convert: true
6354
- },
6355
- failAction,
6356
-
6357
- params: Joi.object({
6358
- account: accountIdSchema.required()
6359
- }),
6360
-
6361
- payload: Joi.object({
6362
- gateway: Joi.string().allow(false, null).empty('').max(256).example(false).description('Optional gateway ID').label('DeliveryTestGateway')
6363
- }).label('DeliveryStartRequest')
6364
- },
6365
-
6366
- response: {
6367
- schema: Joi.object({
6368
- success: Joi.boolean().example(true).description('Was the test started').label('ResponseDeliveryStartSuccess'),
6369
- deliveryTest: Joi.string()
6370
- .guid({
6371
- version: ['uuidv4', 'uuidv5']
6372
- })
6373
- .example('6420a6ad-7f82-4e4f-8112-82a9dad1f34d')
6374
- .description('Test ID')
6375
- }).label('DeliveryStartResponse'),
6376
- failAction: 'log'
6377
- }
6378
- }
6379
- });
6380
-
6381
- server.route({
6382
- method: 'GET',
6383
- path: '/v1/delivery-test/check/{deliveryTest}',
6384
- async handler(request) {
6385
- try {
6386
- request.logger.info({ msg: 'Requested SMTP delivery test check', deliveryTest: request.params.deliveryTest });
6387
-
6388
- let deliveryStatus = (await redis.hgetall(`${REDIS_PREFIX}test-send:${request.params.deliveryTest}`)) || {};
6389
- if (deliveryStatus.success === 'false') {
6390
- let err = new Error(`Failed to deliver email`);
6391
- err.statusCode = 500;
6392
- err.details = deliveryStatus;
6393
- throw err;
6394
- }
6395
-
6396
- let headers = {
6397
- 'Content-Type': 'application/json',
6398
- 'User-Agent': `${packageData.name}/${packageData.version} (+${packageData.homepage})`
6399
- };
6400
-
6401
- let res = await fetchCmd(`${SMTP_TEST_HOST}/test-address/${request.params.deliveryTest}`, {
6402
- method: 'get',
6403
- headers,
6404
- dispatcher: httpAgent.retry
6405
- });
6406
-
6407
- if (!res.ok) {
6408
- let err = new Error(`Invalid response: ${res.status} ${res.statusText}`);
6409
- err.statusCode = res.status;
6410
-
6411
- try {
6412
- err.details = await res.json();
6413
- } catch (err) {
6414
- // ignore
6415
- }
6416
-
6417
- throw err;
6418
- }
6419
-
6420
- let testResponse = await res.json();
6421
-
6422
- let success = testResponse && testResponse.status === 'success'; //Default
6423
-
6424
- if (testResponse && success) {
6425
- let mainSig =
6426
- testResponse.dkim &&
6427
- testResponse.dkim.results &&
6428
- testResponse.dkim.results.find(entry => entry && entry.status && entry.status.result === 'pass' && entry.status.aligned);
6429
-
6430
- if (!mainSig) {
6431
- mainSig =
6432
- testResponse.dkim &&
6433
- testResponse.dkim.results &&
6434
- testResponse.dkim.results.find(entry => entry && entry.status && entry.status.result === 'pass');
6435
- }
6436
-
6437
- if (!mainSig) {
6438
- mainSig = testResponse.dkim && testResponse.dkim.results && testResponse.dkim.results[0];
6439
- }
6440
-
6441
- testResponse.mainSig = mainSig || {
6442
- status: {
6443
- result: 'none'
6444
- }
6445
- };
6446
-
6447
- if (testResponse.spf && testResponse.spf.status && testResponse.spf.status.comment) {
6448
- testResponse.spf.status.comment = testResponse.spf.status.comment.replace(/^[^:\s]+:s*/, '');
6449
- }
6450
- }
6451
-
6452
- if (testResponse) {
6453
- if (testResponse.status === 'success') {
6454
- delete testResponse.status;
6455
- }
6456
- delete testResponse.user;
6457
- }
6458
-
6459
- return Object.assign({ success }, testResponse || {});
6460
- } catch (err) {
6461
- request.logger.error({ msg: 'API request failed', err });
6462
- if (Boom.isBoom(err)) {
6463
- throw err;
6464
- }
6465
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
6466
- if (err.code) {
6467
- error.output.payload.code = err.code;
6468
- }
6469
- if (err.details) {
6470
- error.output.payload.details = err.details;
6471
- }
6472
- throw error;
6473
- }
6474
- },
6475
- options: {
6476
- description: 'Check test status',
6477
- notes: 'Check delivery test status',
6478
- tags: ['api', 'Delivery Test'],
6479
-
6480
- auth: {
6481
- strategy: 'api-token',
6482
- mode: 'required'
6483
- },
6484
- cors: CORS_CONFIG,
6485
-
6486
- validate: {
6487
- options: {
6488
- stripUnknown: false,
6489
- abortEarly: false,
6490
- convert: true
6491
- },
6492
- failAction,
6493
-
6494
- params: Joi.object({
6495
- deliveryTest: Joi.string()
6496
- .guid({
6497
- version: ['uuidv4', 'uuidv5']
6498
- })
6499
- .example('6420a6ad-7f82-4e4f-8112-82a9dad1f34d')
6500
- .required()
6501
- .description('Test ID')
6502
- }).label('DeliveryCheckParams')
6503
- },
6504
-
6505
- response: {
6506
- schema: Joi.object({
6507
- success: Joi.boolean().example(true).description('Was the test completed').label('ResponseDeliveryCheckSuccess'),
6508
- dkim: Joi.object().unknown().description('DKIM results').label('DkimResults'),
6509
- spf: Joi.object().unknown().description('SPF results').label('SpfResults'),
6510
- dmarc: Joi.object().unknown().description('DMARC results').label('DmarcResults'),
6511
- bimi: Joi.object().unknown().description('BIMI results').label('BimiResults'),
6512
- arc: Joi.object().unknown().description('ARC results').label('ArcResults'),
6513
- mainSig: Joi.object()
6514
- .unknown()
6515
- .description('Primary DKIM signature. `status.aligned` should be set, otherwise DKIM check should not be considered as passed.')
6516
- .label('MainSignature')
6517
- }).label('DeliveryCheckResponse'),
6518
- failAction: 'log'
6519
- }
6520
- }
6521
- });
6522
-
6523
- server.route({
6524
- method: 'GET',
6525
- path: '/v1/blocklists',
6526
-
6527
- async handler(request) {
6528
- try {
6529
- return await lists.list(request.query.page, request.query.pageSize);
6530
- } catch (err) {
6531
- request.logger.error({ msg: 'API request failed', err });
6532
- if (Boom.isBoom(err)) {
6533
- throw err;
6534
- }
6535
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
6536
- if (err.code) {
6537
- error.output.payload.code = err.code;
6538
- }
6539
- throw error;
6540
- }
6541
- },
6542
-
6543
- options: {
6544
- description: 'List blocklists',
6545
- notes: 'List blocklists with blocked addresses',
6546
- tags: ['api', 'Blocklists'],
6547
-
6548
- plugins: {},
6549
-
6550
- auth: {
6551
- strategy: 'api-token',
6552
- mode: 'required'
6553
- },
6554
- cors: CORS_CONFIG,
6555
-
6556
- validate: {
6557
- options: {
6558
- stripUnknown: false,
6559
- abortEarly: false,
6560
- convert: true
6561
- },
6562
- failAction,
6563
-
6564
- query: Joi.object({
6565
- page: Joi.number()
6566
- .integer()
6567
- .min(0)
6568
- .max(1024 * 1024)
6569
- .default(0)
6570
- .example(0)
6571
- .description('Page number (zero indexed, so use 0 for first page)')
6572
- .label('PageNumber'),
6573
- pageSize: Joi.number().integer().min(1).max(1000).default(20).example(20).description('How many entries per page').label('PageSize')
6574
- }).label('PageListsRequest')
6575
- },
6576
-
6577
- response: {
6578
- schema: Joi.object({
6579
- total: Joi.number().integer().example(120).description('How many matching entries').label('TotalNumber'),
6580
- page: Joi.number().integer().example(0).description('Current page (0-based index)').label('PageNumber'),
6581
- pages: Joi.number().integer().example(24).description('Total page count').label('PagesNumber'),
6582
-
6583
- blocklists: Joi.array()
6584
- .items(
6585
- Joi.object({
6586
- listId: Joi.string().max(256).required().example('example').description('List ID'),
6587
- count: Joi.number().integer().example(12).description('Count of blocked addresses in this list')
6588
- }).label('BlocklistsResponseItem')
6589
- )
6590
- .label('BlocklistsEntries')
6591
- }).label('BlocklistsResponse'),
6592
- failAction: 'log'
6593
- }
6594
- }
6595
- });
6596
-
6597
- server.route({
6598
- method: 'GET',
6599
- path: '/v1/blocklist/{listId}',
6600
-
6601
- async handler(request) {
6602
- try {
6603
- return await lists.listContent(request.params.listId, request.query.page, request.query.pageSize);
6604
- } catch (err) {
6605
- request.logger.error({ msg: 'API request failed', err });
6606
- if (Boom.isBoom(err)) {
6607
- throw err;
6608
- }
6609
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
6610
- if (err.code) {
6611
- error.output.payload.code = err.code;
6612
- }
6613
- throw error;
6614
- }
6615
- },
6616
-
6617
- options: {
6618
- description: 'List blocklist entries',
6619
- notes: 'List blocked addresses for a list',
6620
- tags: ['api', 'Blocklists'],
6621
-
6622
- plugins: {},
6623
-
6624
- auth: {
6625
- strategy: 'api-token',
6626
- mode: 'required'
6627
- },
6628
- cors: CORS_CONFIG,
6629
-
6630
- validate: {
6631
- options: {
6632
- stripUnknown: false,
6633
- abortEarly: false,
6634
- convert: true
6635
- },
6636
- failAction,
6637
-
6638
- params: Joi.object({
6639
- listId: Joi.string()
6640
- .hostname()
6641
- .example('test-list')
6642
- .description('List ID. Must use a subdomain name format. Lists are registered ad-hoc, so a new identifier defines a new list.')
6643
- .label('ListID')
6644
- .required()
6645
- }).label('BlocklistListRequest'),
6646
-
6647
- query: Joi.object({
6648
- page: Joi.number()
6649
- .integer()
6650
- .min(0)
6651
- .max(1024 * 1024)
6652
- .default(0)
6653
- .example(0)
6654
- .description('Page number (zero indexed, so use 0 for first page)')
6655
- .label('PageNumber'),
6656
- pageSize: Joi.number().integer().min(1).max(1000).default(20).example(20).description('How many entries per page').label('PageSize')
6657
- }).label('PageListsRequest')
6658
- },
6659
-
6660
- response: {
6661
- schema: Joi.object({
6662
- listId: Joi.string().max(256).required().example('example').description('List ID'),
6663
- total: Joi.number().integer().example(120).description('How many matching entries').label('TotalNumber'),
6664
- page: Joi.number().integer().example(0).description('Current page (0-based index)').label('PageNumber'),
6665
- pages: Joi.number().integer().example(24).description('Total page count').label('PagesNumber'),
6666
- addresses: Joi.array()
6667
- .items(
6668
- Joi.object({
6669
- recipient: Joi.string().email().example('user@example.com').description('Listed email address').required(),
6670
- account: accountIdSchema.required().required(),
6671
- messageId: Joi.string().example('<test123@example.com>').description('Message ID'),
6672
- source: Joi.string().example('api').description('Which mechanism was used to add the entry'),
6673
- reason: Joi.string().example('api').description('Why this entry was added'),
6674
- remoteAddress: Joi.string()
6675
- .ip({
6676
- version: ['ipv4', 'ipv6'],
6677
- cidr: 'optional'
6678
- })
6679
- .description('Which IP address triggered the entry'),
6680
- userAgent: Joi.string().example('Mozilla/5.0 (Macintosh)').description('Which user agent triggered the entry'),
6681
- created: Joi.date().iso().example('2021-02-17T13:43:18.860Z').description('The time this entry was added or updated').required()
6682
- }).label('BlocklistListResponseItem')
6683
- )
6684
- .label('BlocklistListEntries')
6685
- }).label('BlocklistListResponse'),
6686
- failAction: 'log'
6687
- }
6688
- }
6689
- });
6690
-
6691
- server.route({
6692
- method: 'POST',
6693
- path: '/v1/blocklist/{listId}',
6694
- async handler(request) {
6695
- let accountObject = new Account({
6696
- redis,
6697
- account: request.payload.account,
6698
- call,
6699
- secret: await getSecret(),
6700
- timeout: request.headers['x-ee-timeout']
6701
- });
6702
-
6703
- try {
6704
- // throws if account does not exist
6705
- await accountObject.loadAccountData();
6706
-
6707
- let added = await redis.eeListAdd(
6708
- `${REDIS_PREFIX}lists:unsub:lists`,
6709
- `${REDIS_PREFIX}lists:unsub:entries:${request.params.listId}`,
6710
- request.params.listId,
6711
- request.payload.recipient.toLowerCase().trim(),
6712
- JSON.stringify({
6713
- recipient: request.payload.recipient,
6714
- account: request.payload.account,
6715
- source: 'api',
6716
- reason: request.payload.reason,
6717
- remoteAddress: request.app.ip,
6718
- userAgent: request.headers['user-agent'],
6719
- created: new Date().toISOString()
6720
- })
6721
- );
6722
-
6723
- return {
6724
- success: true,
6725
- added: !!added
6726
- };
6727
- } catch (err) {
6728
- request.logger.error({ msg: 'API request failed', err });
6729
- if (Boom.isBoom(err)) {
6730
- throw err;
6731
- }
6732
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
6733
- if (err.code) {
6734
- error.output.payload.code = err.code;
6735
- }
6736
- if (err.details) {
6737
- error.output.payload.details = err.details;
6738
- }
6739
- throw error;
6740
- }
6741
- },
6742
- options: {
6743
- description: 'Add to blocklist',
6744
- notes: 'Add an email address to a blocklist',
6745
- tags: ['api', 'Blocklists'],
6746
-
6747
- auth: {
6748
- strategy: 'api-token',
6749
- mode: 'required'
6750
- },
6751
- cors: CORS_CONFIG,
6752
-
6753
- validate: {
6754
- options: {
6755
- stripUnknown: false,
6756
- abortEarly: false,
6757
- convert: true
6758
- },
6759
- failAction,
6760
-
6761
- params: Joi.object({
6762
- listId: Joi.string()
6763
- .hostname()
6764
- .example('test-list')
6765
- .description('List ID. Must use a subdomain name format. Lists are registered ad-hoc, so a new identifier defines a new list.')
6766
- .label('ListID')
6767
- .required()
6768
- }).label('BlocklistListRequest'),
6769
-
6770
- payload: Joi.object({
6771
- account: accountIdSchema.required(),
6772
- recipient: Joi.string().empty('').email().example('user@example.com').description('Email address to add to the list').required(),
6773
- reason: Joi.string().empty('').default('block').description('Identifier for the blocking reason')
6774
- }).label('BlocklistListAddPayload')
6775
- },
6776
-
6777
- response: {
6778
- schema: Joi.object({
6779
- success: Joi.boolean().example(true).description('Was the request successful').label('BlocklistListAddSuccess'),
6780
- added: Joi.boolean().example(true).description('Was the address added to the list')
6781
- }).label('BlocklistListAddResponse'),
6782
- failAction: 'log'
6783
- }
6784
- }
6785
- });
6786
-
6787
- server.route({
6788
- method: 'DELETE',
6789
- path: '/v1/blocklist/{listId}',
6790
-
6791
- async handler(request) {
6792
- try {
6793
- let exists = await redis.hexists(`${REDIS_PREFIX}lists:unsub:lists`, request.params.listId);
6794
- if (!exists) {
6795
- let message = 'Requested blocklist was not found';
6796
- let error = Boom.boomify(new Error(message), { statusCode: 404 });
6797
- throw error;
6798
- }
6799
-
6800
- let deleted = await redis.eeListRemove(
6801
- `${REDIS_PREFIX}lists:unsub:lists`,
6802
- `${REDIS_PREFIX}lists:unsub:entries:${request.params.listId}`,
6803
- request.params.listId,
6804
- request.query.recipient.toLowerCase().trim()
6805
- );
6806
-
6807
- return {
6808
- deleted: !!deleted
6809
- };
6810
- } catch (err) {
6811
- request.logger.error({ msg: 'API request failed', err });
6812
- if (Boom.isBoom(err)) {
6813
- throw err;
6814
- }
6815
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
6816
- if (err.code) {
6817
- error.output.payload.code = err.code;
6818
- }
6819
- throw error;
6820
- }
6821
- },
6822
- options: {
6823
- description: 'Remove from blocklist',
6824
- notes: 'Delete a blocked email address from a list',
6825
- tags: ['api', 'Blocklists'],
6826
-
6827
- plugins: {},
6828
-
6829
- auth: {
6830
- strategy: 'api-token',
6831
- mode: 'required'
6832
- },
6833
- cors: CORS_CONFIG,
6834
-
6835
- validate: {
6836
- options: {
6837
- stripUnknown: false,
6838
- abortEarly: false,
6839
- convert: true
6840
- },
6841
- failAction,
6842
-
6843
- params: Joi.object({
6844
- listId: Joi.string()
6845
- .hostname()
6846
- .example('test-list')
6847
- .description('List ID. Must use a subdomain name format. Lists are registered ad-hoc, so a new identifier defines a new list.')
6848
- .label('ListID')
6849
- .required()
6850
- }).label('BlocklistListRequest'),
6851
-
6852
- query: Joi.object({
6853
- recipient: Joi.string().empty('').email().example('user@example.com').description('Email address to remove from the list').required()
6854
- }).label('RecipientQuery')
6855
- },
6856
-
6857
- response: {
6858
- schema: Joi.object({
6859
- deleted: Joi.boolean().truthy('Y', 'true', '1').falsy('N', 'false', 0).default(true).description('Was the address removed from the list')
6860
- }).label('DeleteBlocklistResponse'),
6861
- failAction: 'log'
6862
- }
6863
- }
6864
- });
6865
-
6866
- server.route({
6867
- method: 'GET',
6868
- path: '/v1/changes',
6869
-
6870
- async handler(request, h) {
6871
- request.app.stream = new ResponseStream();
6872
- finished(request.app.stream, err => request.app.stream.finalize(err));
6873
- setImmediate(() => {
6874
- try {
6875
- request.app.stream.write(`: EmailEngine v${packageData.version}\n\n`);
6876
- } catch (err) {
6877
- // ignore
6878
- }
6879
- });
6880
- return h
6881
- .response(request.app.stream)
6882
- .header('X-Accel-Buffering', 'no')
6883
- .header('Connection', 'keep-alive')
6884
- .header('Cache-Control', 'no-cache')
6885
- .type('text/event-stream');
6886
- },
6887
-
6888
- options: {
6889
- description: 'Stream state changes',
6890
- notes: 'Stream account state changes as an EventSource',
6891
- tags: ['api', 'Account'],
6892
-
6893
- plugins: {
6894
- 'hapi-swagger': {
6895
- produces: ['text/event-stream']
6896
- }
6897
- },
6898
-
6899
- auth: {
6900
- strategy: 'api-token',
6901
- mode: 'required'
6902
- },
6903
- cors: CORS_CONFIG
6904
- }
6905
- });
6906
-
6907
- // Web UI routes
6908
-
6909
- await server.register({
6910
- plugin: Crumb,
6911
-
6912
- options: {
6913
- cookieOptions: {
6914
- isSecure: secureCookie
6915
- },
6916
-
6917
- skip: (request /*, h*/) => {
6918
- let tags = (request.route && request.route.settings && request.route.settings.tags) || [];
6919
-
6920
- if (tags.includes('api') || tags.includes('metrics') || tags.includes('external')) {
6921
- return true;
6922
- }
6923
-
6924
- return false;
6925
- }
6926
- }
6927
- });
6928
-
6929
- server.views({
6930
- engines: {
6931
- hbs: handlebars
6932
- },
6933
- compileOptions: {
6934
- preventIndent: true
6935
- },
6936
-
6937
- relativeTo: pathlib.join(__dirname, '..'),
6938
- path: './views',
6939
- layout: 'app',
6940
- layoutPath: './views/layout',
6941
- partialsPath: './views/partials',
6942
-
6943
- isCached: false,
2710
+ isCached: false,
6944
2711
 
6945
2712
  async context(request) {
6946
2713
  const pendingMessages = await flash(redis, request);
@@ -7311,6 +3078,35 @@ ${now}`,
7311
3078
 
7312
3079
  await server.start();
7313
3080
 
3081
+ if (USE_REUSE_PORT) {
3082
+ // Hapi (autoListen:false) wired its request dispatcher to our listener but did not
3083
+ // bind it. Bind now with SO_REUSEPORT so the kernel distributes connections across
3084
+ // all API workers. listen() can throw synchronously (bad args) or emit 'error'
3085
+ // asynchronously (EADDRINUSE/EACCES/ENOTSUP); handle both, mirroring probeReusePort().
3086
+ await new Promise((resolve, reject) => {
3087
+ const onError = err => {
3088
+ let wrapped = new Error(
3089
+ `Failed to bind API worker ${WORKER_INDEX} to ${API_HOST}:${API_PORT} with SO_REUSEPORT` + (err && err.code ? ` (${err.code})` : '')
3090
+ );
3091
+ wrapped.code = err && err.code;
3092
+ wrapped.workerIndex = WORKER_INDEX;
3093
+ wrapped.host = API_HOST;
3094
+ wrapped.port = API_PORT;
3095
+ reject(wrapped);
3096
+ };
3097
+ reusePortListener.once('error', onError);
3098
+ try {
3099
+ reusePortListener.listen({ port: API_PORT, host: API_HOST, reusePort: true }, () => {
3100
+ reusePortListener.removeListener('error', onError);
3101
+ resolve();
3102
+ });
3103
+ } catch (err) {
3104
+ reusePortListener.removeListener('error', onError);
3105
+ onError(err);
3106
+ }
3107
+ });
3108
+ }
3109
+
7314
3110
  // trigger a request to cache swagger.json
7315
3111
  setImmediate(() => {
7316
3112
  server
@@ -7340,6 +3136,7 @@ ${now}`,
7340
3136
  }
7341
3137
 
7342
3138
  if (
3139
+ IS_PRIMARY_API_WORKER &&
7343
3140
  currentCert &&
7344
3141
  currentCert.validTo < new Date(Date.now() - RENEW_TLS_AFTER) &&
7345
3142
  (!currentCert.lastCheck || currentCert.lastCheck < new Date(Date.now() - BLOCK_TLS_RENEW))