emailengine-app 2.67.2 → 2.68.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.
- package/.github/workflows/test.yml +3 -0
- package/CHANGELOG.md +34 -0
- package/data/google-crawlers.json +1 -1
- package/lib/account.js +24 -1
- package/lib/api-routes/account-routes.js +12 -2
- package/lib/email-client/base-client.js +26 -20
- package/lib/email-client/gmail-client.js +14 -12
- package/lib/oauth/external-account-config.js +132 -0
- package/lib/oauth/external-account-signer.js +256 -0
- package/lib/oauth/gmail.js +113 -14
- package/lib/oauth/verify-app.js +397 -0
- package/lib/oauth2-apps.js +51 -6
- package/lib/routes-ui.js +153 -1
- package/lib/schemas.js +80 -2
- package/lib/settings.js +1 -0
- package/lib/tools.js +10 -10
- package/package.json +22 -22
- package/sbom.json +1 -1
- package/static/js/ace/ace.js +1 -1
- package/static/js/ace/ext-searchbox.js +1 -1
- package/static/js/ace/mode-handlebars.js +1 -1
- package/static/js/ace/mode-html.js +1 -1
- package/static/js/ace/mode-javascript.js +1 -1
- package/static/js/ace/mode-markdown.js +1 -1
- package/static/js/ace/worker-html.js +1 -1
- package/static/js/ace/worker-javascript.js +1 -1
- package/static/js/ace/worker-json.js +1 -1
- package/static/licenses.html +226 -66
- package/translations/messages.pot +13 -13
- package/views/accounts/register/imap-server.hbs +6 -6
- package/views/config/oauth/app.hbs +224 -0
- package/views/config/oauth/edit.hbs +69 -0
- package/views/config/oauth/new.hbs +69 -0
- package/views/partials/oauth_form.hbs +99 -32
- package/workers/api.js +91 -2
package/lib/oauth2-apps.js
CHANGED
|
@@ -468,12 +468,16 @@ class OAuth2AppsHandler {
|
|
|
468
468
|
}
|
|
469
469
|
|
|
470
470
|
case 'gmailService': {
|
|
471
|
+
let storedAuthMethod = await settings.get(`${id}AuthMethod`);
|
|
472
|
+
let authMethod = storedAuthMethod === 'externalAccount' ? 'externalAccount' : 'serviceKey';
|
|
471
473
|
let appData = {
|
|
472
474
|
id: 'gmailService',
|
|
473
475
|
provider: 'gmailService',
|
|
474
476
|
legacy: true,
|
|
477
|
+
authMethod,
|
|
475
478
|
serviceClient: await settings.get(`${id}Client`),
|
|
476
479
|
serviceKey: await settings.get(`${id}Key`),
|
|
480
|
+
externalAccount: await settings.get(`${id}ExternalAccount`),
|
|
477
481
|
extraScopes,
|
|
478
482
|
skipScopes,
|
|
479
483
|
|
|
@@ -553,8 +557,17 @@ class OAuth2AppsHandler {
|
|
|
553
557
|
}
|
|
554
558
|
break;
|
|
555
559
|
|
|
556
|
-
case 'gmailService':
|
|
557
|
-
|
|
560
|
+
case 'gmailService': {
|
|
561
|
+
// The authentication method is fixed once a credential has been
|
|
562
|
+
// stored - never allow switching between serviceKey and externalAccount.
|
|
563
|
+
if (typeof updates.authMethod !== 'undefined') {
|
|
564
|
+
let hasStoredCredential = (await settings.get(`${id}Key`)) || (await settings.get(`${id}ExternalAccount`));
|
|
565
|
+
if (hasStoredCredential) {
|
|
566
|
+
let storedAuthMethod = await settings.get(`${id}AuthMethod`);
|
|
567
|
+
updates = Object.assign({}, updates, { authMethod: storedAuthMethod === 'externalAccount' ? 'externalAccount' : 'serviceKey' });
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
for (let key of ['serviceClient', 'serviceKey', 'externalAccount', 'authMethod', 'extraScopes', 'skipScopes']) {
|
|
558
571
|
if (typeof updates[key] !== 'undefined') {
|
|
559
572
|
let dataKey;
|
|
560
573
|
switch (key) {
|
|
@@ -564,6 +577,12 @@ class OAuth2AppsHandler {
|
|
|
564
577
|
case 'skipScopes':
|
|
565
578
|
dataKey = 'gmailServiceSkipScopes';
|
|
566
579
|
break;
|
|
580
|
+
case 'externalAccount':
|
|
581
|
+
dataKey = 'gmailServiceExternalAccount';
|
|
582
|
+
break;
|
|
583
|
+
case 'authMethod':
|
|
584
|
+
dataKey = 'gmailServiceAuthMethod';
|
|
585
|
+
break;
|
|
567
586
|
default:
|
|
568
587
|
dataKey = `gmail${key.replace(/^./, c => c.toUpperCase())}`;
|
|
569
588
|
}
|
|
@@ -571,6 +590,7 @@ class OAuth2AppsHandler {
|
|
|
571
590
|
}
|
|
572
591
|
}
|
|
573
592
|
break;
|
|
593
|
+
}
|
|
574
594
|
|
|
575
595
|
case 'outlook':
|
|
576
596
|
for (let key of ['clientId', 'clientSecret', 'redirectUrl', 'authority', 'extraScopes', 'skipScopes']) {
|
|
@@ -607,7 +627,20 @@ class OAuth2AppsHandler {
|
|
|
607
627
|
|
|
608
628
|
async delLegacyApp(id) {
|
|
609
629
|
let pipeline = redis.multi();
|
|
610
|
-
for (let key of [
|
|
630
|
+
for (let key of [
|
|
631
|
+
'Enabled',
|
|
632
|
+
'RedirectUrl',
|
|
633
|
+
'Client',
|
|
634
|
+
'ClientId',
|
|
635
|
+
'ClientSecret',
|
|
636
|
+
'Authority',
|
|
637
|
+
'ExtraScopes',
|
|
638
|
+
'SkipScopes',
|
|
639
|
+
'Key',
|
|
640
|
+
'AuthFlag',
|
|
641
|
+
'AuthMethod',
|
|
642
|
+
'ExternalAccount'
|
|
643
|
+
]) {
|
|
611
644
|
pipeline = pipeline.hdel(`${REDIS_PREFIX}settings`, `${id}${key}`);
|
|
612
645
|
}
|
|
613
646
|
await pipeline.exec();
|
|
@@ -669,7 +702,7 @@ class OAuth2AppsHandler {
|
|
|
669
702
|
const id = await this.generateId();
|
|
670
703
|
|
|
671
704
|
let encryptedValues = {};
|
|
672
|
-
for (let key of ['clientSecret', 'serviceKey', 'accessToken']) {
|
|
705
|
+
for (let key of ['clientSecret', 'serviceKey', 'accessToken', 'externalAccount']) {
|
|
673
706
|
if (data[key]) {
|
|
674
707
|
encryptedValues[key] = await this.encrypt(data[key]);
|
|
675
708
|
}
|
|
@@ -729,10 +762,17 @@ class OAuth2AppsHandler {
|
|
|
729
762
|
|
|
730
763
|
let existingData = msgpack.decode(existingDataBuf);
|
|
731
764
|
|
|
765
|
+
// The authentication method is fixed once an app has been configured with
|
|
766
|
+
// a credential - never allow switching between serviceKey and externalAccount
|
|
767
|
+
// (WIF). Pin the stored method, ignoring any incoming change.
|
|
768
|
+
if (existingData.provider === 'gmailService' && (existingData.serviceKey || existingData.externalAccount)) {
|
|
769
|
+
data = Object.assign({}, data || {}, { authMethod: existingData.authMethod === 'externalAccount' ? 'externalAccount' : 'serviceKey' });
|
|
770
|
+
}
|
|
771
|
+
|
|
732
772
|
let oldPubSubApp = existingData.pubSubApp || null;
|
|
733
773
|
|
|
734
774
|
let encryptedValues = {};
|
|
735
|
-
for (let key of ['clientSecret', 'serviceKey', 'accessToken']) {
|
|
775
|
+
for (let key of ['clientSecret', 'serviceKey', 'accessToken', 'externalAccount']) {
|
|
736
776
|
if (data[key]) {
|
|
737
777
|
encryptedValues[key] = await this.encrypt(data[key]);
|
|
738
778
|
}
|
|
@@ -1425,14 +1465,17 @@ class OAuth2AppsHandler {
|
|
|
1425
1465
|
let serviceClient = appData.serviceClient;
|
|
1426
1466
|
|
|
1427
1467
|
let serviceClientEmail = appData.serviceClientEmail;
|
|
1468
|
+
let authMethod = appData.authMethod === 'externalAccount' ? 'externalAccount' : 'serviceKey';
|
|
1428
1469
|
let serviceKey = appData.serviceKey ? await this.decrypt(appData.serviceKey) : null;
|
|
1470
|
+
let externalAccount = appData.externalAccount ? await this.decrypt(appData.externalAccount) : null;
|
|
1429
1471
|
|
|
1430
1472
|
let scopes = formatExtraScopes(appData.extraScopes, appData.baseScopes, GMAIL_SCOPES, appData.skipScopes);
|
|
1431
1473
|
|
|
1432
1474
|
let googleProjectId = appData.googleProjectId;
|
|
1433
1475
|
let workspaceAccounts = appData.googleWorkspaceAccounts;
|
|
1434
1476
|
|
|
1435
|
-
|
|
1477
|
+
let credentialsReady = serviceClient && (authMethod === 'externalAccount' ? !!externalAccount : !!serviceKey);
|
|
1478
|
+
if (!credentialsReady) {
|
|
1436
1479
|
let error = Boom.boomify(new Error('OAuth2 credentials not set up for Gmail'), { statusCode: 400 });
|
|
1437
1480
|
throw error;
|
|
1438
1481
|
}
|
|
@@ -1441,7 +1484,9 @@ class OAuth2AppsHandler {
|
|
|
1441
1484
|
Object.assign(
|
|
1442
1485
|
{
|
|
1443
1486
|
serviceClient,
|
|
1487
|
+
authMethod,
|
|
1444
1488
|
serviceKey,
|
|
1489
|
+
externalAccount,
|
|
1445
1490
|
googleProjectId,
|
|
1446
1491
|
serviceClientEmail,
|
|
1447
1492
|
scopes,
|
package/lib/routes-ui.js
CHANGED
|
@@ -38,6 +38,7 @@ const { Gateway } = require('./gateway');
|
|
|
38
38
|
const { redis, submitQueue, notifyQueue, documentsQueue } = require('./db');
|
|
39
39
|
const psl = require('psl');
|
|
40
40
|
const { oauth2Apps, OAUTH_PROVIDERS, oauth2ProviderData, SERVICE_ACCOUNT_PROVIDERS } = require('./oauth2-apps');
|
|
41
|
+
const { verifyOAuth2App } = require('./oauth/verify-app');
|
|
41
42
|
const { autodetectImapSettings } = require('./autodetect-imap-settings');
|
|
42
43
|
const getSecret = require('./get-secret');
|
|
43
44
|
const os = require('os');
|
|
@@ -719,6 +720,17 @@ function applyRoutes(server, call) {
|
|
|
719
720
|
throw error;
|
|
720
721
|
}
|
|
721
722
|
|
|
723
|
+
// Render-context booleans for the gmailService authMethod tab selector.
|
|
724
|
+
// When `locked` is set the selector is shown but cannot be switched - the
|
|
725
|
+
// authentication method is fixed once an app has been created.
|
|
726
|
+
function authMethodContext(authMethod, locked) {
|
|
727
|
+
return {
|
|
728
|
+
authMethodIsServiceKey: !authMethod || authMethod === 'serviceKey',
|
|
729
|
+
authMethodIsExternalAccount: authMethod === 'externalAccount',
|
|
730
|
+
authMethodLocked: !!locked
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
|
|
722
734
|
/**
|
|
723
735
|
* Fetch the list of Pub/Sub apps and mark the one matching selectedId as selected.
|
|
724
736
|
* Returns the apps array ready for template rendering.
|
|
@@ -2675,6 +2687,11 @@ return true;`
|
|
|
2675
2687
|
app.cloudData = AZURE_CLOUDS.find(entry => entry.id === app.cloud);
|
|
2676
2688
|
}
|
|
2677
2689
|
|
|
2690
|
+
// Service account apps scoped for email access (IMAP/SMTP or Gmail/Graph API) can register
|
|
2691
|
+
// accounts directly without an interactive consent flow. Pub/Sub-scoped service apps are for
|
|
2692
|
+
// webhook notifications only, so they must not offer the direct add-account shortcut.
|
|
2693
|
+
let canAddServiceAccount = SERVICE_ACCOUNT_PROVIDERS.has(app.provider) && app.enabled && app.baseScopes !== 'pubsub';
|
|
2694
|
+
|
|
2678
2695
|
return h.view(
|
|
2679
2696
|
'config/oauth/app',
|
|
2680
2697
|
{
|
|
@@ -2690,6 +2707,11 @@ return true;`
|
|
|
2690
2707
|
baseScopesImap: app.baseScopes === 'imap' || !app.baseScopes,
|
|
2691
2708
|
baseScopesPubsub: app.baseScopes === 'pubsub',
|
|
2692
2709
|
|
|
2710
|
+
appShowAuthMethod: app.provider === 'gmailService',
|
|
2711
|
+
authMethodIsExternalAccount: app.authMethod === 'externalAccount',
|
|
2712
|
+
|
|
2713
|
+
canAddServiceAccount,
|
|
2714
|
+
|
|
2693
2715
|
disabledScopes,
|
|
2694
2716
|
isSendOnlyGmail,
|
|
2695
2717
|
|
|
@@ -2764,6 +2786,119 @@ return true;`
|
|
|
2764
2786
|
}
|
|
2765
2787
|
});
|
|
2766
2788
|
|
|
2789
|
+
server.route({
|
|
2790
|
+
method: 'POST',
|
|
2791
|
+
path: '/admin/config/oauth/app/{app}/add-account',
|
|
2792
|
+
async handler(request, h) {
|
|
2793
|
+
const appId = request.params.app;
|
|
2794
|
+
try {
|
|
2795
|
+
const app = await oauth2Apps.get(appId);
|
|
2796
|
+
if (!app) {
|
|
2797
|
+
let error = Boom.boomify(new Error('Application was not found.'), { statusCode: 404 });
|
|
2798
|
+
throw error;
|
|
2799
|
+
}
|
|
2800
|
+
|
|
2801
|
+
// Direct account registration is only valid for email-scoped service account apps. Interactive
|
|
2802
|
+
// providers must use the hosted consent flow, and Pub/Sub-scoped apps grant no mailbox access.
|
|
2803
|
+
if (!SERVICE_ACCOUNT_PROVIDERS.has(app.provider) || !app.enabled || app.baseScopes === 'pubsub') {
|
|
2804
|
+
let error = Boom.boomify(new Error('This application can not register accounts directly.'), { statusCode: 400 });
|
|
2805
|
+
throw error;
|
|
2806
|
+
}
|
|
2807
|
+
|
|
2808
|
+
let accountData = {
|
|
2809
|
+
account: request.payload.account || null,
|
|
2810
|
+
email: request.payload.email,
|
|
2811
|
+
oauth2: {
|
|
2812
|
+
provider: app.id,
|
|
2813
|
+
auth: {
|
|
2814
|
+
user: request.payload.email
|
|
2815
|
+
}
|
|
2816
|
+
}
|
|
2817
|
+
};
|
|
2818
|
+
|
|
2819
|
+
if (request.payload.name) {
|
|
2820
|
+
accountData.name = request.payload.name;
|
|
2821
|
+
}
|
|
2822
|
+
|
|
2823
|
+
const accountObject = new Account({ redis, call, secret: await getSecret() });
|
|
2824
|
+
const result = await accountObject.create(accountData);
|
|
2825
|
+
|
|
2826
|
+
await request.flash({ type: 'info', message: `Account ${result.state === 'existing' ? 'updated' : 'added'}` });
|
|
2827
|
+
|
|
2828
|
+
return h.redirect(`/admin/accounts/${result.account}`);
|
|
2829
|
+
} catch (err) {
|
|
2830
|
+
request.logger.error({ msg: 'Failed to register service account', err, app: appId, remoteAddress: request.app.ip });
|
|
2831
|
+
await request.flash({ type: 'danger', message: `Failed to add account${err.message ? `: ${err.message}` : ''}` });
|
|
2832
|
+
return h.redirect(`/admin/config/oauth/app/${appId}`);
|
|
2833
|
+
}
|
|
2834
|
+
},
|
|
2835
|
+
options: {
|
|
2836
|
+
validate: {
|
|
2837
|
+
options: {
|
|
2838
|
+
stripUnknown: true,
|
|
2839
|
+
abortEarly: false,
|
|
2840
|
+
convert: true
|
|
2841
|
+
},
|
|
2842
|
+
|
|
2843
|
+
async failAction(request, h, err) {
|
|
2844
|
+
request.logger.error({ msg: 'Failed to register service account', err, app: request.params.app });
|
|
2845
|
+
await request.flash({ type: 'danger', message: `Failed to add account. Provide a valid email address.` });
|
|
2846
|
+
return h.redirect(`/admin/config/oauth/app/${request.params.app}`).takeover();
|
|
2847
|
+
},
|
|
2848
|
+
|
|
2849
|
+
params: Joi.object({
|
|
2850
|
+
app: Joi.string().empty('').max(255).example('gmail').label('Provider').required()
|
|
2851
|
+
}),
|
|
2852
|
+
|
|
2853
|
+
payload: Joi.object({
|
|
2854
|
+
account: accountIdSchema.default(null),
|
|
2855
|
+
name: Joi.string().empty('').max(256).example('John Smith').description('Account Name'),
|
|
2856
|
+
email: Joi.string().email().required().example('user@example.com').label('Email').description('Mailbox email address')
|
|
2857
|
+
})
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2860
|
+
});
|
|
2861
|
+
|
|
2862
|
+
server.route({
|
|
2863
|
+
method: 'POST',
|
|
2864
|
+
path: '/admin/config/oauth/verify/{app}',
|
|
2865
|
+
async handler(request, h) {
|
|
2866
|
+
try {
|
|
2867
|
+
return await verifyOAuth2App(request.params.app, {
|
|
2868
|
+
account: request.payload.account,
|
|
2869
|
+
testConnection: request.payload.testConnection
|
|
2870
|
+
});
|
|
2871
|
+
} catch (err) {
|
|
2872
|
+
request.logger.error({ msg: 'Failed to verify OAuth2 application', err, app: request.params.app });
|
|
2873
|
+
return h.response({ error: err.message, code: err.code || null }).code(err.statusCode || 500);
|
|
2874
|
+
}
|
|
2875
|
+
},
|
|
2876
|
+
options: {
|
|
2877
|
+
validate: {
|
|
2878
|
+
options: {
|
|
2879
|
+
stripUnknown: true,
|
|
2880
|
+
abortEarly: false,
|
|
2881
|
+
convert: true
|
|
2882
|
+
},
|
|
2883
|
+
|
|
2884
|
+
async failAction(request, h, err) {
|
|
2885
|
+
request.logger.error({ msg: 'Invalid verify request', err, app: request.params.app });
|
|
2886
|
+
return h.response({ error: 'Invalid request' }).code(400).takeover();
|
|
2887
|
+
},
|
|
2888
|
+
|
|
2889
|
+
params: Joi.object({
|
|
2890
|
+
app: Joi.string().empty('').max(255).required().label('Provider')
|
|
2891
|
+
}),
|
|
2892
|
+
|
|
2893
|
+
payload: Joi.object({
|
|
2894
|
+
crumb: Joi.string().optional(),
|
|
2895
|
+
account: Joi.string().trim().empty('').max(256).optional(),
|
|
2896
|
+
testConnection: Joi.boolean().truthy('Y', 'true', '1', 'on').falsy('N', 'false', 0, '').default(true)
|
|
2897
|
+
})
|
|
2898
|
+
}
|
|
2899
|
+
}
|
|
2900
|
+
});
|
|
2901
|
+
|
|
2767
2902
|
server.route({
|
|
2768
2903
|
method: 'GET',
|
|
2769
2904
|
path: '/admin/config/oauth/new',
|
|
@@ -2794,6 +2929,8 @@ return true;`
|
|
|
2794
2929
|
baseScopesApi: false,
|
|
2795
2930
|
baseScopesPubsub: false,
|
|
2796
2931
|
|
|
2932
|
+
...authMethodContext('serviceKey'),
|
|
2933
|
+
|
|
2797
2934
|
pubSubApps: await getPubSubAppsForSelect(null),
|
|
2798
2935
|
|
|
2799
2936
|
azureClouds: structuredClone(AZURE_CLOUDS).map(entry => {
|
|
@@ -2805,7 +2942,8 @@ return true;`
|
|
|
2805
2942
|
|
|
2806
2943
|
values: {
|
|
2807
2944
|
provider,
|
|
2808
|
-
redirectUrl: defaultRedirectUrl
|
|
2945
|
+
redirectUrl: defaultRedirectUrl,
|
|
2946
|
+
authMethod: 'serviceKey'
|
|
2809
2947
|
},
|
|
2810
2948
|
|
|
2811
2949
|
authorityCommon: true
|
|
@@ -2907,6 +3045,8 @@ return true;`
|
|
|
2907
3045
|
baseScopesImap: baseScopes === 'imap' || !baseScopes,
|
|
2908
3046
|
baseScopesPubsub: baseScopes === 'pubsub',
|
|
2909
3047
|
|
|
3048
|
+
...authMethodContext(request.payload.authMethod),
|
|
3049
|
+
|
|
2910
3050
|
azureClouds: structuredClone(AZURE_CLOUDS).map(entry => {
|
|
2911
3051
|
entry.selected = request.payload.cloud === entry.id;
|
|
2912
3052
|
return entry;
|
|
@@ -2978,6 +3118,8 @@ return true;`
|
|
|
2978
3118
|
baseScopesImap: baseScopes === 'imap' || !baseScopes,
|
|
2979
3119
|
baseScopesPubsub: baseScopes === 'pubsub',
|
|
2980
3120
|
|
|
3121
|
+
...authMethodContext(request.payload.authMethod),
|
|
3122
|
+
|
|
2981
3123
|
azureClouds: structuredClone(AZURE_CLOUDS).map(entry => {
|
|
2982
3124
|
entry.selected = request.payload.cloud === entry.id;
|
|
2983
3125
|
return entry;
|
|
@@ -3022,6 +3164,7 @@ return true;`
|
|
|
3022
3164
|
let values = Object.assign({}, appData, {
|
|
3023
3165
|
clientSecret: '',
|
|
3024
3166
|
serviceKey: '',
|
|
3167
|
+
externalAccount: '',
|
|
3025
3168
|
extraScopes: [].concat(appData.extraScopes || []).join('\n'),
|
|
3026
3169
|
skipScopes: [].concat(appData.skipScopes || []).join('\n'),
|
|
3027
3170
|
|
|
@@ -3043,6 +3186,9 @@ return true;`
|
|
|
3043
3186
|
|
|
3044
3187
|
hasClientSecret: !!appData.clientSecret,
|
|
3045
3188
|
hasServiceKey: !!appData.serviceKey,
|
|
3189
|
+
hasExternalAccount: !!appData.externalAccount,
|
|
3190
|
+
|
|
3191
|
+
...authMethodContext(appData.authMethod, !!appData.serviceKey || !!appData.externalAccount),
|
|
3046
3192
|
|
|
3047
3193
|
pubSubApps: await getPubSubAppsForSelect(values.pubSubApp),
|
|
3048
3194
|
|
|
@@ -3151,6 +3297,9 @@ return true;`
|
|
|
3151
3297
|
|
|
3152
3298
|
hasClientSecret: !!appData.clientSecret,
|
|
3153
3299
|
hasServiceKey: !!appData.serviceKey,
|
|
3300
|
+
hasExternalAccount: !!appData.externalAccount,
|
|
3301
|
+
|
|
3302
|
+
...authMethodContext(appData.authMethod, !!appData.serviceKey || !!appData.externalAccount),
|
|
3154
3303
|
|
|
3155
3304
|
pubSubApps: await getPubSubAppsForSelect(request.payload.pubSubApp),
|
|
3156
3305
|
|
|
@@ -3232,6 +3381,9 @@ return true;`
|
|
|
3232
3381
|
|
|
3233
3382
|
hasClientSecret: !!appData.clientSecret,
|
|
3234
3383
|
hasServiceKey: !!appData.serviceKey,
|
|
3384
|
+
hasExternalAccount: !!appData.externalAccount,
|
|
3385
|
+
|
|
3386
|
+
...authMethodContext(request.payload.authMethod),
|
|
3235
3387
|
|
|
3236
3388
|
pubSubApps: await getPubSubAppsForSelect(request.payload.pubSubApp),
|
|
3237
3389
|
|
package/lib/schemas.js
CHANGED
|
@@ -5,6 +5,28 @@ const config = require('@zone-eu/wild-config');
|
|
|
5
5
|
const { getByteSize } = require('./tools');
|
|
6
6
|
const { locales } = require('./translations');
|
|
7
7
|
const { LEGACY_KEYS, OAUTH_PROVIDERS } = require('./oauth2-apps');
|
|
8
|
+
const { validateConfig: validateExternalAccountConfig } = require('./oauth/external-account-config');
|
|
9
|
+
|
|
10
|
+
// Shared Joi custom validator for the gmailService external_account credential
|
|
11
|
+
// JSON. Rejects malformed configurations at save time (with an inline field
|
|
12
|
+
// error) instead of letting them fail opaquely on the first token refresh.
|
|
13
|
+
const externalAccountCustomValidator = (value, helpers) => {
|
|
14
|
+
if (!value) {
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
let parsed;
|
|
18
|
+
try {
|
|
19
|
+
parsed = JSON.parse(value);
|
|
20
|
+
} catch (err) {
|
|
21
|
+
return helpers.message('externalAccount is not valid JSON: ' + err.message);
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
validateExternalAccountConfig(parsed);
|
|
25
|
+
} catch (err) {
|
|
26
|
+
return helpers.message(err.message);
|
|
27
|
+
}
|
|
28
|
+
return value;
|
|
29
|
+
};
|
|
8
30
|
|
|
9
31
|
const RESYNC_DELAY = 15 * 60;
|
|
10
32
|
|
|
@@ -1512,12 +1534,43 @@ const oauthCreateSchema = {
|
|
|
1512
1534
|
.max(100 * 1024)
|
|
1513
1535
|
.when('provider', {
|
|
1514
1536
|
is: 'gmailService',
|
|
1515
|
-
then: Joi.
|
|
1537
|
+
then: Joi.when('authMethod', {
|
|
1538
|
+
is: 'externalAccount',
|
|
1539
|
+
then: Joi.optional().allow(''),
|
|
1540
|
+
otherwise: Joi.required()
|
|
1541
|
+
}),
|
|
1516
1542
|
otherwise: Joi.optional().valid(false, null)
|
|
1517
1543
|
})
|
|
1518
1544
|
.example('-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgk...')
|
|
1519
1545
|
.description('Service account private key in PEM format'),
|
|
1520
1546
|
|
|
1547
|
+
authMethod: Joi.string()
|
|
1548
|
+
.valid('serviceKey', 'externalAccount')
|
|
1549
|
+
.when('provider', {
|
|
1550
|
+
is: 'gmailService',
|
|
1551
|
+
then: Joi.optional().default('serviceKey'),
|
|
1552
|
+
otherwise: Joi.optional().valid(false, null)
|
|
1553
|
+
})
|
|
1554
|
+
.example('externalAccount')
|
|
1555
|
+
.description('Authentication method for Gmail service account ("serviceKey" or "externalAccount")'),
|
|
1556
|
+
|
|
1557
|
+
externalAccount: Joi.string()
|
|
1558
|
+
.trim()
|
|
1559
|
+
.empty('')
|
|
1560
|
+
.max(64 * 1024)
|
|
1561
|
+
.when('provider', {
|
|
1562
|
+
is: 'gmailService',
|
|
1563
|
+
then: Joi.when('authMethod', {
|
|
1564
|
+
is: 'externalAccount',
|
|
1565
|
+
then: Joi.required(),
|
|
1566
|
+
otherwise: Joi.optional().allow('')
|
|
1567
|
+
}),
|
|
1568
|
+
otherwise: Joi.optional().valid(false, null)
|
|
1569
|
+
})
|
|
1570
|
+
.custom(externalAccountCustomValidator, 'external account JSON shape')
|
|
1571
|
+
.example('{"type":"external_account","audience":"//iam.googleapis.com/projects/.../providers/...",...}')
|
|
1572
|
+
.description('Google external_account credential JSON for Workload Identity Federation'),
|
|
1573
|
+
|
|
1521
1574
|
authority: Joi.string()
|
|
1522
1575
|
.trim()
|
|
1523
1576
|
.empty('')
|
|
@@ -1689,6 +1742,27 @@ const oauthUpdateSchema = {
|
|
|
1689
1742
|
})
|
|
1690
1743
|
.description('OAuth2 Secret Service Key'),
|
|
1691
1744
|
|
|
1745
|
+
authMethod: Joi.string()
|
|
1746
|
+
.valid('serviceKey', 'externalAccount')
|
|
1747
|
+
.when('provider', {
|
|
1748
|
+
is: 'gmailService',
|
|
1749
|
+
then: Joi.optional().default('serviceKey'),
|
|
1750
|
+
otherwise: Joi.forbidden()
|
|
1751
|
+
})
|
|
1752
|
+
.description('Authentication method for Gmail service account ("serviceKey" or "externalAccount")'),
|
|
1753
|
+
|
|
1754
|
+
externalAccount: Joi.string()
|
|
1755
|
+
.trim()
|
|
1756
|
+
.empty('', false, null)
|
|
1757
|
+
.max(64 * 1024)
|
|
1758
|
+
.when('provider', {
|
|
1759
|
+
is: 'gmailService',
|
|
1760
|
+
then: Joi.optional(),
|
|
1761
|
+
otherwise: Joi.forbidden()
|
|
1762
|
+
})
|
|
1763
|
+
.custom(externalAccountCustomValidator, 'external account JSON shape')
|
|
1764
|
+
.description('Google external_account credential JSON for Workload Identity Federation'),
|
|
1765
|
+
|
|
1692
1766
|
authority: Joi.string()
|
|
1693
1767
|
.trim()
|
|
1694
1768
|
.empty('')
|
|
@@ -1851,7 +1925,6 @@ const messageReferenceSchema = Joi.object({
|
|
|
1851
1925
|
message: Joi.string()
|
|
1852
1926
|
.base64({ paddingRequired: false, urlSafe: true })
|
|
1853
1927
|
.max(256)
|
|
1854
|
-
.required()
|
|
1855
1928
|
.example('AAAAAQAACnA')
|
|
1856
1929
|
.description('EmailEngine message ID to reply to or forward'),
|
|
1857
1930
|
|
|
@@ -1894,8 +1967,13 @@ const messageReferenceSchema = Joi.object({
|
|
|
1894
1967
|
.example('<test123@example.com>')
|
|
1895
1968
|
.description('Verify the Message-ID of the referenced email matches this value before proceeding'),
|
|
1896
1969
|
|
|
1970
|
+
threadId: threadIdSchema.description(
|
|
1971
|
+
'Gmail thread ID to attach the outgoing message to. Used only by Gmail-API accounts; ignored for IMAP and Microsoft Graph backends, which derive threading from RFC In-Reply-To/References headers. When both "message" and "threadId" are provided, the caller-supplied "threadId" takes precedence over the value derived from the referenced message.'
|
|
1972
|
+
),
|
|
1973
|
+
|
|
1897
1974
|
documentStore: documentStoreSchema.default(false).meta({ swaggerHidden: true })
|
|
1898
1975
|
})
|
|
1976
|
+
.or('message', 'threadId')
|
|
1899
1977
|
.description('Configuration for replying to or forwarding an existing message')
|
|
1900
1978
|
.label('MessageReference');
|
|
1901
1979
|
|
package/lib/settings.js
CHANGED
package/lib/tools.js
CHANGED
|
@@ -1999,6 +1999,10 @@ s7YWddnO3q0D3Kqc
|
|
|
1999
1999
|
BZuj6C0wk6AflB86
|
|
2000
2000
|
Oa4rS6X5XHZenDBh
|
|
2001
2001
|
K48CLaXb35Wyf8ud
|
|
2002
|
+
vlPaOaFr8rI0WLUc
|
|
2003
|
+
PeOFrimigEH1HdYk
|
|
2004
|
+
mQN7zFRsKgMoCQ5S
|
|
2005
|
+
pwJ9dhcxKxo7LWtz
|
|
2002
2006
|
6JVHv+g7kUXqgLct
|
|
2003
2007
|
fbc/2bFTnDtwN5Kc
|
|
2004
2008
|
vVTQRmss6oa+o3Bj
|
|
@@ -2040,6 +2044,7 @@ Wp6z04Yho9uWIObC
|
|
|
2040
2044
|
aPkKbjEuTbUCplG7
|
|
2041
2045
|
8ORR3ntJhKaosv0/
|
|
2042
2046
|
IbZu0b/MWBxfqGte
|
|
2047
|
+
MtZY/1P31tzM1aEv
|
|
2043
2048
|
TYJ9FfWyzUlZbQaB
|
|
2044
2049
|
bg5xk8Y5UaJ5MiNo
|
|
2045
2050
|
V+7QViQcrIxNEjZO
|
|
@@ -2063,13 +2068,7 @@ GInvRxkFTnPN97g2
|
|
|
2063
2068
|
zGGN/NlynQ4GvZ3e
|
|
2064
2069
|
MQ87qSEBNQJip6GS
|
|
2065
2070
|
Vqm95KM44kL4+7qd
|
|
2066
|
-
DBAhSvOXy5FuI/il
|
|
2067
|
-
C6il2vOLXSqXKzNL
|
|
2068
|
-
RHH2IL/f3lipzTFk
|
|
2069
|
-
5q3TdjzMplq0+yI7
|
|
2070
|
-
nL6uRkZxgug/JFyl
|
|
2071
2071
|
uSY0M+yfff4Op/HP
|
|
2072
|
-
tpADMZ5Ir6bmwexH
|
|
2073
2072
|
h7Squx/sKZNkDat0
|
|
2074
2073
|
elb9jWFuO3UHnphx
|
|
2075
2074
|
//m57pmYUuiv84pZ
|
|
@@ -2103,10 +2102,6 @@ r/CVn8qB2xdyVXCg
|
|
|
2103
2102
|
iSeRSmpWBZGpkTZi
|
|
2104
2103
|
ZZkXc6t+ZDyuhEoO
|
|
2105
2104
|
vPHQ3BYeEr+9DSlW
|
|
2106
|
-
5ChBAv/YX3PWk/Wz
|
|
2107
|
-
YjuOzxqQEGdKLC0Q
|
|
2108
|
-
nmu7gC+IxPl4AgjV
|
|
2109
|
-
43Du3vhZaEGambkT
|
|
2110
2105
|
vIwyKmyMuykMUPeA
|
|
2111
2106
|
RTIy3POEF2gZADpU
|
|
2112
2107
|
pqvSQnQ/jx8sYGi4
|
|
@@ -2129,7 +2124,9 @@ RSvpJCGXq7lbwx3S
|
|
|
2129
2124
|
h/TUqvLGXjB8N8xu
|
|
2130
2125
|
u61uYKx5DLO9eNR7
|
|
2131
2126
|
vWuuhT9ely8AUX2F
|
|
2127
|
+
mF3GOI+Ev7mJODtG
|
|
2132
2128
|
nQNwPlZ+tyx24XVo
|
|
2129
|
+
olObiSmdzVaMp7lH
|
|
2133
2130
|
cyp1GKV4Cl+eC8G/
|
|
2134
2131
|
Q1y/TzbtUQCqnotR
|
|
2135
2132
|
59Q2qOkuyTLzQDhd
|
|
@@ -2153,6 +2150,9 @@ EsbWnuADOq9qe/EZ
|
|
|
2153
2150
|
YfUSxQtq1+kpwW/W
|
|
2154
2151
|
QodzxvoJ6NPGhCgW
|
|
2155
2152
|
5/VK+O950efBio0t
|
|
2153
|
+
RA/lrkwxcTX80nxX
|
|
2154
|
+
b85BMAFRHn10uX8y
|
|
2155
|
+
vV2RgfFH2YFTYsly
|
|
2156
2156
|
`
|
|
2157
2157
|
.split(/\r?\n/)
|
|
2158
2158
|
.map(l => l.trim())
|