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