emailengine-app 2.68.1 → 2.70.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/.github/workflows/deploy.yml +8 -3
  2. package/.github/workflows/release.yaml +6 -0
  3. package/CHANGELOG.md +59 -0
  4. package/Gruntfile.js +3 -1
  5. package/config/default.toml +2 -0
  6. package/data/google-crawlers.json +7 -1
  7. package/getswagger.sh +40 -4
  8. package/gettext-extract.js +163 -0
  9. package/lib/account.js +135 -72
  10. package/lib/api-routes/account-routes.js +684 -106
  11. package/lib/api-routes/blocklist-routes.js +344 -0
  12. package/lib/api-routes/chat-routes.js +32 -14
  13. package/lib/api-routes/delivery-test-routes.js +346 -0
  14. package/lib/api-routes/export-routes.js +28 -14
  15. package/lib/api-routes/gateway-routes.js +427 -0
  16. package/lib/api-routes/license-routes.js +156 -0
  17. package/lib/api-routes/mailbox-routes.js +344 -0
  18. package/lib/api-routes/message-routes.js +221 -187
  19. package/lib/api-routes/oauth2-app-routes.js +697 -0
  20. package/lib/api-routes/outbox-routes.js +185 -0
  21. package/lib/api-routes/pubsub-routes.js +102 -0
  22. package/lib/api-routes/route-helpers.js +58 -0
  23. package/lib/api-routes/settings-routes.js +357 -0
  24. package/lib/api-routes/stats-routes.js +111 -0
  25. package/lib/api-routes/submit-routes.js +461 -0
  26. package/lib/api-routes/template-routes.js +60 -75
  27. package/lib/api-routes/token-routes.js +297 -0
  28. package/lib/api-routes/webhook-route-routes.js +181 -0
  29. package/lib/autodetect-imap-settings.js +0 -2
  30. package/lib/consts.js +5 -0
  31. package/lib/email-client/base-client.js +28 -6
  32. package/lib/email-client/gmail-client.js +133 -112
  33. package/lib/email-client/imap/mailbox.js +34 -11
  34. package/lib/email-client/imap/subconnection.js +20 -13
  35. package/lib/email-client/imap/sync-operations.js +131 -3
  36. package/lib/email-client/imap-client.js +152 -75
  37. package/lib/email-client/notification-handler.js +1 -4
  38. package/lib/email-client/outlook-client.js +134 -75
  39. package/lib/export.js +97 -20
  40. package/lib/feature-flags.js +2 -2
  41. package/lib/gateway.js +4 -9
  42. package/lib/get-raw-email.js +5 -5
  43. package/lib/imapproxy/imap-core/lib/commands/starttls.js +18 -0
  44. package/lib/imapproxy/imap-core/lib/imap-command.js +6 -1
  45. package/lib/imapproxy/imap-core/lib/imap-connection.js +106 -24
  46. package/lib/imapproxy/imap-core/lib/imap-server.js +24 -0
  47. package/lib/imapproxy/imap-core/lib/imap-stream.js +26 -0
  48. package/lib/logger.js +24 -21
  49. package/lib/message-port-stream.js +113 -16
  50. package/lib/metrics-collector.js +0 -2
  51. package/lib/oauth2-apps.js +13 -4
  52. package/lib/outbox.js +24 -40
  53. package/lib/redis-operations.js +1 -1
  54. package/lib/reject-worker-calls.js +42 -0
  55. package/lib/routes-ui.js +37 -8778
  56. package/lib/schemas.js +429 -84
  57. package/lib/sentry.js +139 -0
  58. package/lib/settings.js +9 -3
  59. package/lib/stream-encrypt.js +1 -1
  60. package/lib/templates.js +1 -1
  61. package/lib/tokens.js +5 -3
  62. package/lib/tools.js +70 -4
  63. package/lib/ui-routes/account-routes.js +45 -212
  64. package/lib/ui-routes/admin-config-routes.js +928 -489
  65. package/lib/ui-routes/admin-entities-routes.js +1 -0
  66. package/lib/ui-routes/auth-routes.js +1339 -0
  67. package/lib/ui-routes/dashboard-routes.js +188 -0
  68. package/lib/ui-routes/document-store-routes.js +800 -0
  69. package/lib/ui-routes/export-routes.js +217 -0
  70. package/lib/ui-routes/internals-routes.js +354 -0
  71. package/lib/ui-routes/network-config-routes.js +759 -0
  72. package/lib/ui-routes/{oauth-routes.js → oauth-config-routes.js} +369 -91
  73. package/lib/ui-routes/route-helpers.js +314 -0
  74. package/lib/ui-routes/smtp-test-routes.js +236 -0
  75. package/lib/ui-routes/unsubscribe-routes.js +232 -0
  76. package/lib/webhook-request.js +36 -0
  77. package/lib/webhooks.js +8 -4
  78. package/package.json +13 -12
  79. package/sbom.json +1 -1
  80. package/server.js +222 -39
  81. package/static/licenses.html +160 -300
  82. package/translations/messages.pot +112 -132
  83. package/update-info.sh +19 -1
  84. package/views/config/logging.hbs +48 -0
  85. package/views/dashboard.hbs +7 -26
  86. package/views/internals/index.hbs +15 -0
  87. package/views/tokens/index.hbs +9 -0
  88. package/workers/api.js +200 -4424
  89. package/workers/documents.js +2 -22
  90. package/workers/export.js +103 -104
  91. package/workers/imap-proxy.js +3 -23
  92. package/workers/imap.js +32 -36
  93. package/workers/smtp.js +2 -22
  94. package/workers/submit.js +26 -35
  95. package/workers/webhooks.js +9 -43
@@ -0,0 +1,697 @@
1
+ 'use strict';
2
+
3
+ const Joi = require('joi');
4
+ const { failAction } = require('../tools');
5
+ const { oauth2Apps } = require('../oauth2-apps');
6
+ const { verifyOAuth2App } = require('../oauth/verify-app');
7
+ const {
8
+ oauthCreateSchema,
9
+ lastErrorSchema,
10
+ pubSubErrorSchema,
11
+ googleProjectIdSchema,
12
+ googleWorkspaceAccountsSchema,
13
+ googleTopicNameSchema,
14
+ googleSubscriptionNameSchema,
15
+ errorResponses
16
+ } = require('../schemas');
17
+ const { handleError, throwNotFound, flattenOAuthAppMeta } = require('./route-helpers');
18
+
19
+ // Stored fields that are returned on OAuth2 application objects in addition to the basic fields
20
+ const oauth2AppExtraFields = {
21
+ extraScopes: Joi.array()
22
+ .items(Joi.string().example('https://graph.microsoft.com/.default').label('ExtraScopeEntry'))
23
+ .description('Additional OAuth2 scopes requested for this app')
24
+ .label('AppExtraScopes'),
25
+ skipScopes: Joi.array()
26
+ .items(Joi.string().example('SMTP.Send').label('SkipScopeEntry'))
27
+ .description('OAuth2 scopes excluded from the defaults for this app')
28
+ .label('AppSkipScopes'),
29
+ baseScopes: oauthCreateSchema.baseScopes.description('OAuth2 base scopes for this app').label('AppBaseScopes'),
30
+ pubSubApp: oauthCreateSchema.pubSubApp.description('Cloud Pub/Sub app ID used for Gmail change notifications').label('AppPubSubApp'),
31
+ authMethod: Joi.string().example('serviceKey').description('Authentication method for Gmail service accounts'),
32
+ cloud: Joi.string().example('global').description('Azure cloud type for Outlook OAuth2 applications'),
33
+ tenant: Joi.string()
34
+ .example('f8cdef31-a31e-4b4a-93e4-5f571e91255a')
35
+ .description('Deprecated and unused directory tenant value. Use the authority field instead'),
36
+ externalAccount: Joi.string().example('******').description('External account identifier for 2-legged OAuth2 applications. Actual value is not revealed.'),
37
+ accessToken: Joi.string().example('******').description('Access token for app-based authentication. Actual value is not revealed.'),
38
+ pubSubTopic: Joi.string().example('projects/project-name/topics/ee-pub-12345').description('Cloud Pub/Sub topic name for Gmail change notifications'),
39
+ pubSubSubscription: Joi.string()
40
+ .example('projects/project-name/subscriptions/ee-sub-12345')
41
+ .description('Cloud Pub/Sub subscription name for Gmail change notifications'),
42
+ pubSubIamPolicy: Joi.boolean().example(true).description('Whether the IAM policy for the Cloud Pub/Sub topic has been set up')
43
+ };
44
+
45
+ async function init(args) {
46
+ const { server, call, CORS_CONFIG, OAuth2ProviderSchema } = args;
47
+
48
+ // Notify the worker when an app create/update changed Pub/Sub resources, and strip the
49
+ // internal marker from the API response
50
+ let applyPubSubUpdates = async result => {
51
+ if (result && result.pubsubUpdates) {
52
+ if (Object.keys(result.pubsubUpdates).length > 0) {
53
+ await call({ cmd: 'googlePubSub', app: result.id });
54
+ }
55
+ delete result.pubsubUpdates;
56
+ }
57
+ return result;
58
+ };
59
+
60
+ server.route({
61
+ method: 'GET',
62
+ path: '/v1/oauth2',
63
+
64
+ async handler(request) {
65
+ try {
66
+ let response = await oauth2Apps.list(request.query.page, request.query.pageSize);
67
+
68
+ for (let app of response.apps) {
69
+ for (let secretKey of ['clientSecret', 'serviceKey', 'accessToken', 'externalAccount']) {
70
+ if (app[secretKey]) {
71
+ app[secretKey] = '******';
72
+ }
73
+ }
74
+
75
+ if (app.extraScopes && !app.extraScopes.length) {
76
+ delete app.extraScopes;
77
+ }
78
+
79
+ if (app.app) {
80
+ delete app.app;
81
+ }
82
+
83
+ flattenOAuthAppMeta(app);
84
+ }
85
+
86
+ return response;
87
+ } catch (err) {
88
+ handleError(request, err);
89
+ }
90
+ },
91
+
92
+ options: {
93
+ description: 'List OAuth2 applications',
94
+ notes: 'Lists registered OAuth2 applications',
95
+ tags: ['api', 'OAuth2 Applications'],
96
+
97
+ plugins: {
98
+ 'hapi-swagger': {
99
+ responses: errorResponses(400, 401, 403, 429, 500)
100
+ }
101
+ },
102
+
103
+ auth: {
104
+ strategy: 'api-token',
105
+ mode: 'required'
106
+ },
107
+ cors: CORS_CONFIG,
108
+
109
+ validate: {
110
+ options: {
111
+ stripUnknown: false,
112
+ abortEarly: false,
113
+ convert: true
114
+ },
115
+ failAction,
116
+
117
+ query: Joi.object({
118
+ page: Joi.number()
119
+ .integer()
120
+ .min(0)
121
+ .max(1024 * 1024)
122
+ .default(0)
123
+ .example(0)
124
+ .description('Page number (zero indexed, so use 0 for first page)')
125
+ .label('PageNumber'),
126
+ pageSize: Joi.number().integer().min(1).max(1000).default(20).example(20).description('How many entries per page').label('PageSize')
127
+ }).label('GatewaysFilter')
128
+ },
129
+
130
+ response: {
131
+ schema: Joi.object({
132
+ total: Joi.number().integer().example(120).description('How many matching entries').label('TotalNumber'),
133
+ page: Joi.number().integer().example(0).description('Current page (0-based index)').label('PageNumber'),
134
+ pages: Joi.number().integer().example(24).description('Total page count').label('PagesNumber'),
135
+
136
+ apps: Joi.array()
137
+ .items(
138
+ Joi.object({
139
+ id: Joi.string().max(256).required().example('AAABhaBPHscAAAAH').description('OAuth2 application ID'),
140
+ name: Joi.string().max(256).example('My OAuth2 App').description('Display name for the app'),
141
+ description: Joi.string().empty('').trim().max(1024).example('App description').description('OAuth2 application description'),
142
+ title: Joi.string().empty('').trim().max(256).example('App title').description('Title for the application button'),
143
+ provider: OAuth2ProviderSchema,
144
+ enabled: Joi.boolean()
145
+ .truthy('Y', 'true', '1', 'on')
146
+ .falsy('N', 'false', 0, '')
147
+ .example(true)
148
+ .description('Is the application enabled')
149
+ .label('AppEnabled'),
150
+ legacy: Joi.boolean()
151
+ .truthy('Y', 'true', '1', 'on')
152
+ .falsy('N', 'false', 0, '')
153
+ .example(true)
154
+ .description('`true` for older OAuth2 apps set via the settings endpoint'),
155
+ created: Joi.date()
156
+ .iso()
157
+ .example('2021-02-17T13:43:18.860Z')
158
+ .description('The time this entry was added. Not present for legacy apps'),
159
+ updated: Joi.date().iso().example('2021-02-17T13:43:18.860Z').description('The time this entry was updated'),
160
+ includeInListing: Joi.boolean()
161
+ .truthy('Y', 'true', '1', 'on')
162
+ .falsy('N', 'false', 0, '')
163
+ .example(true)
164
+ .description('Is the application listed in the hosted authentication form'),
165
+
166
+ clientId: Joi.string()
167
+ .example('4f05f488-d858-4f2c-bd12-1039062612fe')
168
+ .description('Client or Application ID for 3-legged OAuth2 applications')
169
+ .label('OAuth2AppListClientId'),
170
+ clientSecret: Joi.string()
171
+ .example('******')
172
+ .description('Client secret for 3-legged OAuth2 applications. Actual value is not revealed.'),
173
+ authority: Joi.string().example('common').description('Authorization tenant value for Outlook OAuth2 applications'),
174
+ redirectUrl: Joi.string()
175
+ .uri({
176
+ scheme: ['http', 'https'],
177
+ allowRelative: false
178
+ })
179
+ .example('https://myservice.com/oauth')
180
+ .description('Redirect URL for 3-legged OAuth2 applications')
181
+ .label('OAuth2AppListRedirectUrl'),
182
+
183
+ serviceClient: Joi.string()
184
+ .example('9103965568215821627203')
185
+ .description('Service client ID for 2-legged OAuth2 applications')
186
+ .label('OAuth2AppListServiceClient'),
187
+
188
+ googleProjectId: googleProjectIdSchema,
189
+ googleWorkspaceAccounts: googleWorkspaceAccountsSchema,
190
+ googleTopicName: googleTopicNameSchema,
191
+ googleSubscriptionName: googleSubscriptionNameSchema,
192
+
193
+ serviceClientEmail: Joi.string()
194
+ .email()
195
+ .example('name@project-123.iam.gserviceaccount.com')
196
+ .description('Service Client Email for 2-legged OAuth2 applications'),
197
+
198
+ serviceKey: Joi.string()
199
+ .example('******')
200
+ .description('PEM formatted service secret for 2-legged OAuth2 applications. Actual value is not revealed.'),
201
+
202
+ ...oauth2AppExtraFields,
203
+
204
+ lastError: lastErrorSchema.allow(null),
205
+ pubSubError: pubSubErrorSchema.allow(null)
206
+ }).label('OAuth2ResponseItem')
207
+ )
208
+ .label('OAuth2Entries')
209
+ }).label('OAuth2FilterResponse'),
210
+ failAction: 'log'
211
+ }
212
+ }
213
+ });
214
+
215
+ server.route({
216
+ method: 'GET',
217
+ path: '/v1/oauth2/{app}',
218
+
219
+ async handler(request) {
220
+ try {
221
+ let app = await oauth2Apps.get(request.params.app);
222
+ if (!app) {
223
+ throwNotFound();
224
+ }
225
+
226
+ // remove secrets
227
+ for (let secretKey of ['clientSecret', 'serviceKey', 'accessToken', 'externalAccount']) {
228
+ if (app[secretKey]) {
229
+ app[secretKey] = '******';
230
+ }
231
+ }
232
+
233
+ if (app.extraScopes && !app.extraScopes.length) {
234
+ delete app.extraScopes;
235
+ }
236
+
237
+ if (app.app) {
238
+ delete app.app;
239
+ }
240
+
241
+ flattenOAuthAppMeta(app);
242
+
243
+ return app;
244
+ } catch (err) {
245
+ handleError(request, err);
246
+ }
247
+ },
248
+ options: {
249
+ description: 'Get application info',
250
+ notes: 'Returns stored information about an OAuth2 application. Secrets are not included.',
251
+ tags: ['api', 'OAuth2 Applications'],
252
+
253
+ plugins: {
254
+ 'hapi-swagger': {
255
+ responses: errorResponses(400, 401, 403, 404, 429, 500)
256
+ }
257
+ },
258
+
259
+ auth: {
260
+ strategy: 'api-token',
261
+ mode: 'required'
262
+ },
263
+ cors: CORS_CONFIG,
264
+
265
+ validate: {
266
+ options: {
267
+ stripUnknown: false,
268
+ abortEarly: false,
269
+ convert: true
270
+ },
271
+ failAction,
272
+
273
+ params: Joi.object({
274
+ app: Joi.string().max(256).required().example('AAABhaBPHscAAAAH').description('OAuth2 application ID')
275
+ })
276
+ },
277
+
278
+ response: {
279
+ schema: Joi.object({
280
+ id: Joi.string().max(256).required().example('AAABhaBPHscAAAAH').description('OAuth2 application ID'),
281
+ name: Joi.string().max(256).example('My OAuth2 App').description('Display name for the app'),
282
+ description: Joi.string().empty('').trim().max(1024).example('App description').description('OAuth2 application description'),
283
+ title: Joi.string().empty('').trim().max(256).example('App title').description('Title for the application button'),
284
+ provider: OAuth2ProviderSchema,
285
+ enabled: Joi.boolean()
286
+ .truthy('Y', 'true', '1', 'on')
287
+ .falsy('N', 'false', 0, '')
288
+ .example(true)
289
+ .description('Is the application enabled')
290
+ .label('AppEnabled'),
291
+ legacy: Joi.boolean()
292
+ .truthy('Y', 'true', '1', 'on')
293
+ .falsy('N', 'false', 0, '')
294
+ .example(true)
295
+ .description('`true` for older OAuth2 apps set via the settings endpoint'),
296
+ created: Joi.date().iso().example('2021-02-17T13:43:18.860Z').description('The time this entry was added. Not present for legacy apps'),
297
+ updated: Joi.date().iso().example('2021-02-17T13:43:18.860Z').description('The time this entry was updated'),
298
+ includeInListing: Joi.boolean()
299
+ .truthy('Y', 'true', '1', 'on')
300
+ .falsy('N', 'false', 0, '')
301
+ .example(true)
302
+ .description('Is the application listed in the hosted authentication form'),
303
+
304
+ clientId: Joi.string()
305
+ .example('4f05f488-d858-4f2c-bd12-1039062612fe')
306
+ .description('Client or Application ID for 3-legged OAuth2 applications')
307
+ .label('OAuth2AppGetClientId'),
308
+ clientSecret: Joi.string().example('******').description('Client secret for 3-legged OAuth2 applications. Actual value is not revealed.'),
309
+ authority: Joi.string().example('common').description('Authorization tenant value for Outlook OAuth2 applications'),
310
+ redirectUrl: Joi.string()
311
+ .uri({
312
+ scheme: ['http', 'https'],
313
+ allowRelative: false
314
+ })
315
+ .example('https://myservice.com/oauth')
316
+ .description('Redirect URL for 3-legged OAuth2 applications')
317
+ .label('OAuth2AppGetRedirectUrl'),
318
+
319
+ googleProjectId: googleProjectIdSchema,
320
+ googleWorkspaceAccounts: googleWorkspaceAccountsSchema,
321
+ googleTopicName: googleTopicNameSchema,
322
+ googleSubscriptionName: googleSubscriptionNameSchema,
323
+
324
+ serviceClientEmail: Joi.string()
325
+ .email()
326
+ .example('name@project-123.iam.gserviceaccount.com')
327
+ .description('Service Client Email for 2-legged OAuth2 applications'),
328
+
329
+ serviceClient: Joi.string()
330
+ .example('9103965568215821627203')
331
+ .description('Service client ID for 2-legged OAuth2 applications')
332
+ .label('OAuth2AppGetServiceClient'),
333
+
334
+ serviceKey: Joi.string()
335
+ .example('******')
336
+ .description('PEM formatted service secret for 2-legged OAuth2 applications. Actual value is not revealed.'),
337
+
338
+ accounts: Joi.number()
339
+ .integer()
340
+ .example(12)
341
+ .description('The number of accounts registered with this application. Not available for legacy apps.'),
342
+
343
+ ...oauth2AppExtraFields,
344
+
345
+ lastError: lastErrorSchema.allow(null),
346
+ pubSubError: pubSubErrorSchema.allow(null)
347
+ }).label('ApplicationResponse'),
348
+ failAction: 'log'
349
+ }
350
+ }
351
+ });
352
+
353
+ server.route({
354
+ method: 'POST',
355
+ path: '/v1/oauth2',
356
+
357
+ async handler(request) {
358
+ try {
359
+ let result = await oauth2Apps.create(request.payload);
360
+ return await applyPubSubUpdates(result);
361
+ } catch (err) {
362
+ handleError(request, err);
363
+ }
364
+ },
365
+
366
+ options: {
367
+ description: 'Register OAuth2 application',
368
+ notes: 'Registers a new OAuth2 application for a specific provider',
369
+ tags: ['api', 'OAuth2 Applications'],
370
+
371
+ plugins: {
372
+ 'hapi-swagger': {
373
+ responses: errorResponses(400, 401, 403, 429, 500)
374
+ }
375
+ },
376
+
377
+ auth: {
378
+ strategy: 'api-token',
379
+ mode: 'required'
380
+ },
381
+ cors: CORS_CONFIG,
382
+
383
+ validate: {
384
+ options: {
385
+ stripUnknown: false,
386
+ abortEarly: false,
387
+ convert: true
388
+ },
389
+ failAction,
390
+
391
+ payload: Joi.object(oauthCreateSchema).tailor('api').label('CreateOAuth2App')
392
+ },
393
+
394
+ response: {
395
+ schema: Joi.object({
396
+ id: Joi.string().max(256).required().example('AAABhaBPHscAAAAH').description('OAuth2 application ID'),
397
+ created: Joi.boolean().truthy('Y', 'true', '1').falsy('N', 'false', 0).default(true).description('Was the app created')
398
+ }).label('CreateAppResponse'),
399
+ failAction: 'log'
400
+ }
401
+ }
402
+ });
403
+
404
+ server.route({
405
+ method: 'PUT',
406
+ path: '/v1/oauth2/{app}',
407
+
408
+ async handler(request) {
409
+ try {
410
+ let result = await oauth2Apps.update(request.params.app, request.payload);
411
+ return await applyPubSubUpdates(result);
412
+ } catch (err) {
413
+ handleError(request, err);
414
+ }
415
+ },
416
+ options: {
417
+ description: 'Update OAuth2 application',
418
+ notes: 'Updates OAuth2 application information',
419
+ tags: ['api', 'OAuth2 Applications'],
420
+
421
+ plugins: {
422
+ 'hapi-swagger': {
423
+ responses: errorResponses(400, 401, 403, 404, 429, 500)
424
+ }
425
+ },
426
+
427
+ auth: {
428
+ strategy: 'api-token',
429
+ mode: 'required'
430
+ },
431
+ cors: CORS_CONFIG,
432
+
433
+ validate: {
434
+ options: {
435
+ stripUnknown: false,
436
+ abortEarly: false,
437
+ convert: true
438
+ },
439
+ failAction,
440
+
441
+ params: Joi.object({
442
+ app: Joi.string().max(256).required().example('AAABhaBPHscAAAAH').description('OAuth2 application ID')
443
+ }),
444
+
445
+ payload: Joi.object({
446
+ name: Joi.string().trim().empty('').max(256).example('My Gmail App').description('Application name'),
447
+ description: Joi.string().trim().allow('').max(1024).example('My cool app').description('Application description'),
448
+ title: Joi.string().allow('').trim().max(256).example('App title').description('Title for the application button'),
449
+
450
+ enabled: Joi.boolean().truthy('Y', 'true', '1', 'on').falsy('N', 'false', 0, '').example(true).description('Enable this app'),
451
+
452
+ clientId: Joi.string()
453
+ .trim()
454
+ .allow('', null, false)
455
+ .max(256)
456
+ .example('52422112755-3uov8bjwlrullq122rdm6l8ui25ho7qf.apps.googleusercontent.com')
457
+ .description('Client or Application ID for 3-legged OAuth2 applications')
458
+ .label('UpdateOAuth2ClientId'),
459
+
460
+ clientSecret: Joi.string()
461
+ .trim()
462
+ .empty('')
463
+ .max(256)
464
+ .example('boT7Q~dUljnfFdVuqpC11g8nGMjO8kpRAv-ZB')
465
+ .description('Client secret for 3-legged OAuth2 applications'),
466
+
467
+ pubSubApp: Joi.string()
468
+ .empty('')
469
+ .base64({ paddingRequired: false, urlSafe: true })
470
+ .max(512)
471
+ .example('AAAAAQAACnA')
472
+ .description('Cloud Pub/Sub app for Gmail API webhooks')
473
+ .label('UpdatePubSubAppId'),
474
+
475
+ extraScopes: Joi.array()
476
+ .items(Joi.string().trim().max(255).example('User.Read').label('UpdateExtraScopeEntry'))
477
+ .description('OAuth2 Extra Scopes')
478
+ .label('UpdateOAuth2ExtraScopes'),
479
+
480
+ skipScopes: Joi.array()
481
+ .items(Joi.string().trim().max(255).example('SMTP.Send').label('UpdateSkipScopeEntry'))
482
+ .description('OAuth2 scopes to skip from the base set')
483
+ .label('UpdateOAuth2SkipScopes'),
484
+
485
+ serviceClient: Joi.string()
486
+ .trim()
487
+ .allow('', null, false)
488
+ .max(256)
489
+ .example('7103296518315821565203')
490
+ .description('Service client ID for 2-legged OAuth2 applications')
491
+ .label('UpdateServiceClient'),
492
+
493
+ googleProjectId: googleProjectIdSchema,
494
+ googleWorkspaceAccounts: googleWorkspaceAccountsSchema,
495
+ googleTopicName: googleTopicNameSchema,
496
+ googleSubscriptionName: googleSubscriptionNameSchema,
497
+
498
+ serviceClientEmail: Joi.string()
499
+ .email()
500
+ .example('name@project-123.iam.gserviceaccount.com')
501
+ .description('Service Client Email for 2-legged OAuth2 applications'),
502
+
503
+ serviceKey: Joi.string()
504
+ .trim()
505
+ .empty('')
506
+ .max(100 * 1024)
507
+ .example('-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgk...')
508
+ .description('PEM formatted service secret for 2-legged OAuth2 applications'),
509
+
510
+ authority: Joi.string()
511
+ .trim()
512
+ .empty('')
513
+ .max(1024)
514
+ .example('common')
515
+ .description('Authorization tenant value for Outlook OAuth2 applications')
516
+ .label('SupportedAccountTypes'),
517
+
518
+ cloud: Joi.string()
519
+ .trim()
520
+ .empty('')
521
+ .valid('global', 'gcc-high', 'dod', 'china')
522
+ .example('global')
523
+ .description('Azure cloud type for Outlook OAuth2 applications')
524
+ .label('AzureCloud'),
525
+
526
+ tenant: Joi.string().trim().empty('').max(1024).example('f8cdef31-a31e-4b4a-93e4-5f571e91255a').label('Directorytenant'),
527
+
528
+ redirectUrl: Joi.string()
529
+ .allow('', null, false)
530
+ .uri({ scheme: ['http', 'https'], allowRelative: false })
531
+ .example('https://myservice.com/oauth')
532
+ .description('Redirect URL for 3-legged OAuth2 applications')
533
+ .label('UpdateOAuth2RedirectUrl')
534
+ }).label('UpdateOAuthApp')
535
+ },
536
+
537
+ response: {
538
+ schema: Joi.object({
539
+ id: Joi.string().max(256).required().example('example').description('OAuth2 app ID'),
540
+ updated: Joi.boolean().example(true).description('Was the application updated'),
541
+ legacy: Joi.boolean().example(false).description('`true` for older OAuth2 apps set via the settings endpoint')
542
+ }).label('UpdateOAuthAppResponse'),
543
+ failAction: 'log'
544
+ }
545
+ }
546
+ });
547
+
548
+ server.route({
549
+ method: 'DELETE',
550
+ path: '/v1/oauth2/{app}',
551
+
552
+ async handler(request) {
553
+ try {
554
+ let result = await oauth2Apps.del(request.params.app);
555
+
556
+ try {
557
+ await call({ cmd: 'googlePubSubRemove', app: request.params.app });
558
+ } catch (err) {
559
+ request.logger.error({ msg: 'Failed to notify workers about OAuth2 app deletion', err, app: request.params.app });
560
+ }
561
+
562
+ return result;
563
+ } catch (err) {
564
+ handleError(request, err);
565
+ }
566
+ },
567
+ options: {
568
+ description: 'Remove OAuth2 application',
569
+ notes: 'Delete OAuth2 application data',
570
+ tags: ['api', 'OAuth2 Applications'],
571
+
572
+ plugins: {
573
+ 'hapi-swagger': {
574
+ responses: errorResponses(400, 401, 403, 404, 429, 500)
575
+ }
576
+ },
577
+
578
+ auth: {
579
+ strategy: 'api-token',
580
+ mode: 'required'
581
+ },
582
+ cors: CORS_CONFIG,
583
+
584
+ validate: {
585
+ options: {
586
+ stripUnknown: false,
587
+ abortEarly: false,
588
+ convert: true
589
+ },
590
+ failAction,
591
+
592
+ params: Joi.object({
593
+ app: Joi.string().max(256).required().example('AAABhaBPHscAAAAH').description('OAuth2 application ID')
594
+ }).label('DeleteAppRequest')
595
+ },
596
+
597
+ response: {
598
+ schema: Joi.object({
599
+ id: Joi.string().max(256).required().example('AAABhaBPHscAAAAH').description('OAuth2 application ID'),
600
+ deleted: Joi.boolean().truthy('Y', 'true', '1').falsy('N', 'false', 0).default(true).description('Was the OAuth2 application deleted'),
601
+ legacy: Joi.boolean().example(false).description('`true` for older OAuth2 apps set via the settings endpoint'),
602
+ accounts: Joi.number()
603
+ .integer()
604
+ .example(12)
605
+ .description('The number of accounts registered with this application. Not available for legacy apps.')
606
+ }).label('DeleteAppRequestResponse'),
607
+ failAction: 'log'
608
+ }
609
+ }
610
+ });
611
+
612
+ server.route({
613
+ method: 'POST',
614
+ path: '/v1/oauth2/{app}/verify',
615
+
616
+ async handler(request) {
617
+ try {
618
+ return await verifyOAuth2App(request.params.app, {
619
+ account: request.payload.account,
620
+ testConnection: request.payload.testConnection
621
+ });
622
+ } catch (err) {
623
+ handleError(request, err);
624
+ }
625
+ },
626
+ options: {
627
+ description: 'Verify OAuth2 application setup',
628
+ 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.',
629
+ tags: ['api', 'OAuth2 Applications'],
630
+
631
+ plugins: {
632
+ 'hapi-swagger': {
633
+ responses: errorResponses(400, 401, 403, 404, 429, 500)
634
+ }
635
+ },
636
+
637
+ auth: {
638
+ strategy: 'api-token',
639
+ mode: 'required'
640
+ },
641
+ cors: CORS_CONFIG,
642
+
643
+ validate: {
644
+ options: {
645
+ stripUnknown: false,
646
+ abortEarly: false,
647
+ convert: true
648
+ },
649
+ failAction,
650
+
651
+ params: Joi.object({
652
+ app: Joi.string().max(256).required().example('AAABhaBPHscAAAAH').description('OAuth2 application ID')
653
+ }),
654
+
655
+ payload: Joi.object({
656
+ account: Joi.string()
657
+ .trim()
658
+ .empty('')
659
+ .max(256)
660
+ .example('user@example.com')
661
+ .description('Mailbox address used to verify domain-wide delegation and live mailbox access'),
662
+ testConnection: Joi.boolean()
663
+ .truthy('Y', 'true', '1', 'on')
664
+ .falsy('N', 'false', 0, '')
665
+ .default(true)
666
+ .description('Perform the live IMAP/API connection step when an access token is obtained')
667
+ }).label('VerifyOAuth2AppRequest')
668
+ },
669
+
670
+ response: {
671
+ schema: Joi.object({
672
+ app: Joi.string().max(256).required().example('AAABhaBPHscAAAAH').description('OAuth2 application ID'),
673
+ provider: Joi.string().example('gmailService').description('Provider type'),
674
+ authMethod: Joi.string().allow(null).example('externalAccount').description('Authentication method for service-account apps'),
675
+ account: Joi.string().allow(null).example('user@example.com').description('Mailbox used for the delegation/mailbox checks'),
676
+ ok: Joi.boolean().example(true).description('True when no verification step failed'),
677
+ steps: Joi.array()
678
+ .items(
679
+ Joi.object({
680
+ id: Joi.string().example('signJwt').description('Step identifier'),
681
+ label: Joi.string().example('Sign assertion (signJwt)').description('Human readable step name'),
682
+ status: Joi.string().valid('ok', 'fail', 'skip').example('ok').description('Step outcome'),
683
+ message: Joi.string().allow(null).example('Assertion signed via IAM signJwt').description('Outcome detail'),
684
+ hint: Joi.string()
685
+ .example('Grant roles/iam.serviceAccountTokenCreator to the workload principal')
686
+ .description('How to fix a failed step')
687
+ }).label('OAuth2VerifyStep')
688
+ )
689
+ .label('OAuth2VerifySteps')
690
+ }).label('VerifyOAuth2AppResponse'),
691
+ failAction: 'log'
692
+ }
693
+ }
694
+ });
695
+ }
696
+
697
+ module.exports = init;