diff --git a/lib/oauth2-apps.js b/lib/oauth2-apps.js index f2fc8eca..6366f4a2 100644 --- a/lib/oauth2-apps.js +++ b/lib/oauth2-apps.js @@ -101,9 +101,11 @@ function oauth2ProviderData(provider) { return providerData; } -function formatExtraScopes(extraScopes, baseScopes, defaultScopesList) { +function formatExtraScopes(extraScopes, baseScopes, defaultScopesList, skipScopes) { let defaultScopes; + skipScopes = [].concat(skipScopes || []); + if (Array.isArray(defaultScopesList)) { defaultScopes = defaultScopesList; } else { @@ -111,7 +113,7 @@ function formatExtraScopes(extraScopes, baseScopes, defaultScopesList) { } let extras = []; - if (!extraScopes) { + if (!extraScopes && !skipScopes.length) { return defaultScopes; } @@ -122,11 +124,27 @@ function formatExtraScopes(extraScopes, baseScopes, defaultScopesList) { } extras.push(extraScope); } + + let result = []; + if (extras.length) { - return extras.concat(defaultScopes); + result = extras.concat(defaultScopes); + } + + result = defaultScopes; + + if (skipScopes.length) { + result = result.filter(scope => { + for (let skipScope of skipScopes) { + if (scope === skipScope || scope === `https://outlook.office.com/${skipScope}`) { + return false; + } + } + return true; + }); } - return defaultScopes; + return result; } class OAuth2AppsHandler { @@ -242,6 +260,12 @@ class OAuth2AppsHandler { } extraScopes = extraScopes.filter(entry => entry); + let skipScopes = await settings.get(`${id}SkipScopes`); + if (!Array.isArray(skipScopes)) { + skipScopes = (skipScopes || '').toString().split(/[\s,]+/); + } + skipScopes = skipScopes.filter(entry => entry); + switch (id) { case 'gmail': { let appData = { @@ -253,6 +277,7 @@ class OAuth2AppsHandler { clientSecret: await settings.get(`${id}ClientSecret`), redirectUrl: await settings.get(`${id}RedirectUrl`), extraScopes, + skipScopes, name: 'Gmail', description: 'Legacy OAuth2 app', @@ -272,6 +297,7 @@ class OAuth2AppsHandler { serviceClient: await settings.get(`${id}Client`), serviceKey: await settings.get(`${id}Key`), extraScopes, + skipScopes, name: 'Gmail service', description: 'Legacy OAuth2 app', @@ -296,6 +322,7 @@ class OAuth2AppsHandler { clientSecret: await settings.get(`${id}ClientSecret`), redirectUrl: await settings.get(`${id}RedirectUrl`), extraScopes, + skipScopes, name: 'Outlook', description: 'Legacy OAuth2 app', @@ -317,6 +344,7 @@ class OAuth2AppsHandler { clientSecret: await settings.get(`${id}ClientSecret`), redirectUrl: await settings.get(`${id}RedirectUrl`), extraScopes, + skipScopes, name: 'Mail.ru', description: 'Legacy OAuth2 app', @@ -340,7 +368,7 @@ class OAuth2AppsHandler { switch (id) { case 'gmail': - for (let key of ['clientId', 'clientSecret', 'redirectUrl', 'extraScopes']) { + for (let key of ['clientId', 'clientSecret', 'redirectUrl', 'extraScopes', 'skipScopes']) { if (typeof updates[key] !== 'undefined') { data[`${id}${key.replace(/^./, c => c.toUpperCase())}`] = updates[key]; } @@ -348,13 +376,16 @@ class OAuth2AppsHandler { break; case 'gmailService': - for (let key of ['serviceClient', 'serviceKey', 'extraScopes']) { + for (let key of ['serviceClient', 'serviceKey', 'extraScopes', 'skipScopes']) { if (typeof updates[key] !== 'undefined') { let dataKey; switch (key) { case 'extraScopes': dataKey = 'gmailServiceExtraScopes'; break; + case 'skipScopes': + dataKey = 'gmailServiceSkipScopes'; + break; default: dataKey = `gmail${key.replace(/^./, c => c.toUpperCase())}`; } @@ -364,7 +395,7 @@ class OAuth2AppsHandler { break; case 'outlook': - for (let key of ['clientId', 'clientSecret', 'redirectUrl', 'authority', 'extraScopes']) { + for (let key of ['clientId', 'clientSecret', 'redirectUrl', 'authority', 'extraScopes', 'skipScopes']) { if (typeof updates[key] !== 'undefined') { data[`${id}${key.replace(/^./, c => c.toUpperCase())}`] = updates[key]; } @@ -373,7 +404,7 @@ class OAuth2AppsHandler { break; case 'mailRu': - for (let key of ['clientId', 'clientSecret', 'redirectUrl', 'extraScopes']) { + for (let key of ['clientId', 'clientSecret', 'redirectUrl', 'extraScopes', 'skipScopes']) { if (typeof updates[key] !== 'undefined') { data[`${id}${key.replace(/^./, c => c.toUpperCase())}`] = updates[key]; } @@ -398,7 +429,7 @@ class OAuth2AppsHandler { async delLegacyApp(id) { let pipeline = redis.multi(); - for (let key of ['Enabled', 'RedirectUrl', 'Client', 'ClientId', 'ClientSecret', 'Authority', 'ExtraScopes', 'Key', 'AuthFlag']) { + for (let key of ['Enabled', 'RedirectUrl', 'Client', 'ClientId', 'ClientSecret', 'Authority', 'ExtraScopes', 'SkipScopes', 'Key', 'AuthFlag']) { pipeline = pipeline.hdel(`${REDIS_PREFIX}settings`, `${id}${key}`); } await pipeline.exec(); @@ -604,7 +635,7 @@ class OAuth2AppsHandler { let clientId = appData.clientId; let clientSecret = appData.clientSecret ? await decrypt(appData.clientSecret) : null; let redirectUrl = appData.redirectUrl; - let scopes = formatExtraScopes(appData.extraScopes, appData.baseScopes, GMAIL_SCOPES); + let scopes = formatExtraScopes(appData.extraScopes, appData.baseScopes, GMAIL_SCOPES, appData.skipScopes); if (!clientId || !clientSecret || !redirectUrl) { let error = Boom.boomify(new Error('OAuth2 credentials not set up for Gmail'), { statusCode: 400 }); @@ -638,7 +669,7 @@ class OAuth2AppsHandler { case 'gmailService': { let serviceClient = appData.serviceClient; let serviceKey = appData.serviceKey ? await decrypt(appData.serviceKey) : null; - let scopes = formatExtraScopes(appData.extraScopes, appData.baseScopes, GMAIL_SCOPES); + let scopes = formatExtraScopes(appData.extraScopes, appData.baseScopes, GMAIL_SCOPES, appData.skipScopes); if (!serviceClient || !serviceKey) { let error = Boom.boomify(new Error('OAuth2 credentials not set up for Gmail'), { statusCode: 400 }); @@ -673,7 +704,7 @@ class OAuth2AppsHandler { let clientId = appData.clientId; let clientSecret = appData.clientSecret ? await decrypt(appData.clientSecret) : null; let redirectUrl = appData.redirectUrl; - let scopes = formatExtraScopes(appData.extraScopes, appData.baseScopes, OUTLOOK_SCOPES); + let scopes = formatExtraScopes(appData.extraScopes, appData.baseScopes, OUTLOOK_SCOPES, appData.skipScopes); if (!clientId || !clientSecret || !authority || !redirectUrl) { let error = Boom.boomify(new Error('OAuth2 credentials not set up for Outlook'), { statusCode: 400 }); @@ -709,7 +740,7 @@ class OAuth2AppsHandler { let clientId = appData.clientId; let clientSecret = appData.clientSecret ? await decrypt(appData.clientSecret) : null; let redirectUrl = appData.redirectUrl; - let scopes = formatExtraScopes(appData.extraScopes, appData.baseScopes, MAIL_RU_SCOPES); + let scopes = formatExtraScopes(appData.extraScopes, appData.baseScopes, MAIL_RU_SCOPES, appData.skipScopes); if (!clientId || !clientSecret || !redirectUrl) { let error = Boom.boomify(new Error('OAuth2 credentials not set up for Mail.ru'), { statusCode: 400 }); diff --git a/lib/routes-ui.js b/lib/routes-ui.js index e114a1ba..d4a5a590 100644 --- a/lib/routes-ui.js +++ b/lib/routes-ui.js @@ -337,6 +337,12 @@ const oauthUpdateSchema = { .max(10 * 1024) .description('OAuth2 Extra Scopes'), + skipScopes: Joi.string() + .allow('') + .trim() + .max(10 * 1024) + .description('OAuth2 scopes to skip from the base set'), + serviceClient: Joi.string() .trim() .allow('') @@ -2151,6 +2157,11 @@ return true;` let providerData = oauth2ProviderData(app.provider); + let disabledScopes = {}; + if (app.skipScopes?.includes('SMTP.Send') || app.skipScopes?.includes('https://outlook.office.com/SMTP.Send')) { + disabledScopes.SMTP_Send = true; + } + return h.view( 'config/oauth/app', { @@ -2161,6 +2172,8 @@ return true;` app, + disabledScopes, + providerData }, { @@ -2298,6 +2311,11 @@ return true;` .map(scope => scope.trim()) .filter(scope => scope); + appData.skipScopes = appData.skipScopes + .split(/\s+/) + .map(scope => scope.trim()) + .filter(scope => scope); + let oauth2App = await oauth2Apps.create(appData); if (!oauth2App || !oauth2App.id) { throw new Error('Unexpected result'); @@ -2424,7 +2442,8 @@ return true;` let values = Object.assign({}, appData, { clientSecret: '', serviceKey: '', - extraScopes: [].concat(appData.extraScopes || []).join('\n') + extraScopes: [].concat(appData.extraScopes || []).join('\n'), + skipScopes: [].concat(appData.skipScopes || []).join('\n') }); return h.view( @@ -2489,6 +2508,11 @@ return true;` .map(scope => scope.trim()) .filter(scope => scope); + updates.skipScopes = updates.skipScopes + .split(/\s+/) + .map(scope => scope.trim()) + .filter(scope => scope); + let oauth2App = await oauth2Apps.update(appData.id, updates); if (!oauth2App || !oauth2App.id) { throw new Error('Unexpected result'); diff --git a/lib/schemas.js b/lib/schemas.js index 994d7900..8c40cb87 100644 --- a/lib/schemas.js +++ b/lib/schemas.js @@ -1135,6 +1135,18 @@ const oauthCreateSchema = { }) .description('OAuth2 Extra Scopes'), + skipScopes: Joi.any() + .alter({ + web: () => + Joi.string() + .allow('') + .trim() + .example('SMTP.Send') + .max(10 * 1024), + api: () => Joi.array().items(Joi.string().trim().max(255).example('SMTP.Send')) + }) + .description('OAuth2 scopes to skip from the base set'), + serviceClient: Joi.string() .trim() .allow('') diff --git a/views/config/oauth/app.hbs b/views/config/oauth/app.hbs index 32b15086..f7cf8eeb 100644 --- a/views/config/oauth/app.hbs +++ b/views/config/oauth/app.hbs @@ -39,7 +39,11 @@
"IMAP.AccessAsUser.All"
+
+ {{#unless disabledScopes.SMTP_Send}}
"SMTP.Send"
+ {{/unless}}
+
"offline_access"
Here you can list OAuth2 scopes that EmailEngine should not use. This is mainly needed
+ when you want to disable some base scopes like SMTP.Send
in Outlook.