From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) by lore.proxmox.com (Postfix) with ESMTPS id 748B91FF142 for ; Tue, 21 Apr 2026 14:03:35 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id AB5CF1F82C; Tue, 21 Apr 2026 14:00:54 +0200 (CEST) From: Arthur Bied-Charreton To: pbs-devel@lists.proxmox.com, pve-devel@lists.proxmox.com Subject: [PATCH proxmox-widget-toolkit v4 13/24] notifications: Add opt-in OAuth2 support for SMTP targets Date: Tue, 21 Apr 2026 13:59:46 +0200 Message-ID: <20260421115957.402589-14-a.bied-charreton@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260421115957.402589-1-a.bied-charreton@proxmox.com> References: <20260421115957.402589-1-a.bied-charreton@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL -0.115 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment KAM_LAZY_DOMAIN_SECURITY 1 Sending domain does not have any anti-forgery methods RDNS_NONE 0.793 Delivered to internal network by a host with no rDNS SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_NONE 0.001 SPF: sender does not publish an SPF Record Message-ID-Hash: SW37FALP3UYSOJPVDUEMBIJSGBLOJMG4 X-Message-ID-Hash: SW37FALP3UYSOJPVDUEMBIJSGBLOJMG4 X-MailFrom: abied-charreton@jett.proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox Backup Server development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: Add Google & Microsoft OAuth2 authorization support to SMTP endpoint config. The enableOAuth2 config flag in pmxSmtpEditPanel allows consumers to opt into this new feature, so it can be gradually introduced into products. When disabled, no changes are visible from the UI. Signed-off-by: Arthur Bied-Charreton --- src/panel/SmtpEditPanel.js | 280 ++++++++++++++++++++++++++++++--- src/window/EndpointEditBase.js | 1 + 2 files changed, 258 insertions(+), 23 deletions(-) diff --git a/src/panel/SmtpEditPanel.js b/src/panel/SmtpEditPanel.js index 37e4d51..7ee247a 100644 --- a/src/panel/SmtpEditPanel.js +++ b/src/panel/SmtpEditPanel.js @@ -6,12 +6,25 @@ Ext.define('Proxmox.panel.SmtpEditPanel', { type: 'smtp', + enableOAuth2: false, + + initConfig: function (config) { + this.callParent(arguments); + this.getViewModel().set('enableOAuth2', config.enableOAuth2 || false); + return config; + }, + viewModel: { xtype: 'viewmodel', data: { + enableOAuth2: false, mode: 'tls', - authentication: true, - originalAuthentication: true, + authMethod: 'plain', + oAuth2ClientId: '', + oAuth2ClientSecret: '', + oAuth2TenantId: '', + oAuth2RefreshToken: '', + originalAuthMethod: undefined, }, formulas: { portEmptyText: function (get) { @@ -30,14 +43,69 @@ Ext.define('Proxmox.panel.SmtpEditPanel', { } return `${Proxmox.Utils.defaultText} (${port})`; }, + isPlainAuthentication: function (get) { + return get('authMethod') === 'plain'; + }, + isOAuth2Authentication: function (get) { + if (!get('enableOAuth2')) { + return false; + } + let authMethod = get('authMethod'); + return authMethod === 'google-oauth2' || authMethod === 'microsoft-oauth2'; + }, + isMicrosoftOAuth2Authentication: function (get) { + if (!get('enableOAuth2')) { + return false; + } + return get('authMethod') === 'microsoft-oauth2'; + }, + enableAuthenticate: function (get) { + if (!get('enableOAuth2')) { + return false; + } + let clientId = get('oAuth2ClientId'); + let clientSecret = get('oAuth2ClientSecret'); + + if (get('authMethod') === 'microsoft-oauth2') { + let tenantId = get('oAuth2TenantId'); + return ( + clientId && + clientId.trim() !== '' && + clientSecret && + clientSecret.trim() !== '' && + tenantId && + tenantId.trim() !== '' + ); + } else { + return ( + clientId && + clientId.trim() !== '' && + clientSecret && + clientSecret.trim() !== '' + ); + } + }, passwordEmptyText: function (get) { let isCreate = this.getView().isCreate; - - let auth = get('authentication'); - let origAuth = get('originalAuthentication'); - let shouldShowUnchanged = !isCreate && auth && origAuth; - - return shouldShowUnchanged ? gettext('Unchanged') : ''; + let isPlain = get('isPlainAuthentication'); + let wasPlain = get('originalAuthMethod') === 'plain'; + return !isCreate && isPlain && wasPlain ? gettext('Unchanged') : ''; + }, + oAuth2ClientSecretEmptyText: function (get) { + let isCreate = this.getView().isCreate; + let isOAuth2 = get('isOAuth2Authentication'); + let origMethod = get('originalAuthMethod'); + let wasOAuth2 = origMethod === 'google-oauth2' || origMethod === 'microsoft-oauth2'; + return !isCreate && isOAuth2 && wasOAuth2 ? gettext('Unchanged') : ''; + }, + isAuthorized: function (get) { + if (!get('enableOAuth2')) { + return false; + } + return !!get('oAuth2RefreshToken'); + }, + authorizeButtonDisabled: function (get) { + return !get('enableAuthenticate') || get('isAuthorized'); }, }, }, @@ -102,11 +170,25 @@ Ext.define('Proxmox.panel.SmtpEditPanel', { ], column2: [ { - xtype: 'proxmoxcheckbox', - fieldLabel: gettext('Authenticate'), - name: 'authentication', - bind: { - value: '{authentication}', + xtype: 'proxmoxKVComboBox', + fieldLabel: gettext('Authentication'), + name: 'auth-method', + comboItems: [ + ['none', gettext('None')], + ['plain', gettext('Username/Password')], + ['google-oauth2', gettext('OAuth2 (Google)')], + ['microsoft-oauth2', gettext('OAuth2 (Microsoft)')], + ], + bind: '{authMethod}', + cbind: { + deleteEmpty: '{!isCreate}', + }, + listeners: { + render: function () { + if (!this.up('pmxSmtpEditPanel').enableOAuth2) { + this.getStore().filter('key', /^(none|plain)$/); + } + }, }, }, { @@ -118,7 +200,8 @@ Ext.define('Proxmox.panel.SmtpEditPanel', { deleteEmpty: '{!isCreate}', }, bind: { - disabled: '{!authentication}', + hidden: '{!isPlainAuthentication}', + disabled: '{!isPlainAuthentication}', }, }, { @@ -130,10 +213,113 @@ Ext.define('Proxmox.panel.SmtpEditPanel', { allowBlank: '{!isCreate}', }, bind: { - disabled: '{!authentication}', + hidden: '{!isPlainAuthentication}', + disabled: '{!isPlainAuthentication}', emptyText: '{passwordEmptyText}', }, }, + { + xtype: 'proxmoxtextfield', + fieldLabel: gettext('Client ID'), + name: 'oauth2-client-id', + allowBlank: false, + bind: { + hidden: '{!isOAuth2Authentication}', + disabled: '{!isOAuth2Authentication}', + value: '{oAuth2ClientId}', + }, + cbind: { + deleteEmpty: '{!isCreate}', + }, + }, + { + xtype: 'proxmoxtextfield', + inputType: 'password', + fieldLabel: gettext('Client Secret'), + name: 'oauth2-client-secret', + bind: { + hidden: '{!isOAuth2Authentication}', + disabled: '{!isOAuth2Authentication}', + value: '{oAuth2ClientSecret}', + emptyText: '{oAuth2ClientSecretEmptyText}', + }, + cbind: { + allowBlank: '{!isCreate}', + }, + }, + { + xtype: 'proxmoxtextfield', + fieldLabel: gettext('Tenant ID'), + name: 'oauth2-tenant-id', + allowBlank: false, + bind: { + hidden: '{!isMicrosoftOAuth2Authentication}', + disabled: '{!isMicrosoftOAuth2Authentication}', + value: '{oAuth2TenantId}', + }, + cbind: { + deleteEmpty: '{!isCreate}', + }, + }, + { + xtype: 'fieldcontainer', + fieldLabel: gettext('Authorize'), + layout: 'hbox', + bind: { + hidden: '{!isOAuth2Authentication}', + }, + items: [ + { + xtype: 'button', + text: gettext('Authorize'), + handler: async function () { + let panel = this.up('pmxSmtpEditPanel'); + let form = panel.up('form'); + let values = form.getValues(); + + try { + let refreshToken = await panel.handleOAuth2Flow(values); + panel.getViewModel().set('oAuth2RefreshToken', refreshToken); + } catch (e) { + Ext.Msg.alert('Error', e); + } + }, + bind: { + disabled: '{authorizeButtonDisabled}', + }, + }, + { + xtype: 'displayfield', + renderer: Ext.identityFn, + value: ` ${gettext('Authorized')}`, + margin: '0 0 0 8', + bind: { + hidden: '{!isAuthorized}', + }, + }, + ], + }, + { + xtype: 'hiddenfield', + name: 'oauth2-refresh-token', + allowBlank: false, + bind: { + value: '{oAuth2RefreshToken}', + disabled: '{!isOAuth2Authentication}', + }, + getErrors: function () { + if (this.disabled) { + return []; + } + if (!this.up('pmxSmtpEditPanel').isCreate) { + return []; + } + if (!this.getValue()) { + return ['']; + } + return []; + }, + }, ], columnB: [ { @@ -159,7 +345,6 @@ Ext.define('Proxmox.panel.SmtpEditPanel', { }, }, ], - advancedColumnB: [ { xtype: 'proxmoxtextfield', @@ -172,7 +357,22 @@ Ext.define('Proxmox.panel.SmtpEditPanel', { }, }, ], + handleOAuth2Flow: function (values) { + let authMethod = values['auth-method']; + if (authMethod === 'microsoft-oauth2') { + return Proxmox.OAuth2.handleMicrosoftFlow( + values['oauth2-client-id'], + values['oauth2-client-secret'], + values['oauth2-tenant-id'], + ); + } else if (authMethod === 'google-oauth2') { + return Proxmox.OAuth2.handleGoogleFlow( + values['oauth2-client-id'], + values['oauth2-client-secret'], + ); + } + }, onGetValues: function (values) { let me = this; @@ -180,9 +380,33 @@ Ext.define('Proxmox.panel.SmtpEditPanel', { values.mailto = values.mailto.split(/[\s,;]+/); } - if (!values.authentication && !me.isCreate) { - Proxmox.Utils.assemble_field_data(values, { delete: 'username' }); - Proxmox.Utils.assemble_field_data(values, { delete: 'password' }); + let authMethod = values['auth-method']; + if (!this.enableOAuth2 || authMethod === 'none') { + delete values['auth-method']; + } + + if (!me.isCreate) { + if (authMethod === 'none') { + if (this.enableOAuth2) { + Proxmox.Utils.assemble_field_data(values, { delete: 'auth-method' }); + Proxmox.Utils.assemble_field_data(values, { delete: 'oauth2-client-id' }); + Proxmox.Utils.assemble_field_data(values, { delete: 'oauth2-client-secret' }); + Proxmox.Utils.assemble_field_data(values, { delete: 'oauth2-tenant-id' }); + } + Proxmox.Utils.assemble_field_data(values, { delete: 'username' }); + Proxmox.Utils.assemble_field_data(values, { delete: 'password' }); + } else if (authMethod === 'plain' && this.enableOAuth2) { + Proxmox.Utils.assemble_field_data(values, { delete: 'oauth2-client-id' }); + Proxmox.Utils.assemble_field_data(values, { delete: 'oauth2-client-secret' }); + Proxmox.Utils.assemble_field_data(values, { delete: 'oauth2-tenant-id' }); + } else if (authMethod === 'microsoft-oauth2') { + Proxmox.Utils.assemble_field_data(values, { delete: 'username' }); + Proxmox.Utils.assemble_field_data(values, { delete: 'password' }); + } else if (authMethod === 'google-oauth2') { + Proxmox.Utils.assemble_field_data(values, { delete: 'username' }); + Proxmox.Utils.assemble_field_data(values, { delete: 'password' }); + Proxmox.Utils.assemble_field_data(values, { delete: 'oauth2-tenant-id' }); + } } if (values.enable) { @@ -199,19 +423,29 @@ Ext.define('Proxmox.panel.SmtpEditPanel', { return values; }, - onSetValues: function (values) { let me = this; - values.authentication = !!values.username; values.enable = !values.disable; + + if (values['auth-method'] === undefined && this.enableOAuth2) { + if (values['oauth2-tenant-id']) { + values['auth-method'] = 'microsoft-oauth2'; + } else if (values['oauth2-client-id']) { + values['auth-method'] = 'google-oauth2'; + } else if (values.username) { + values['auth-method'] = 'plain'; + } else { + values['auth-method'] = 'none'; + } + } + delete values.disable; // Fix race condition in chromium-based browsers. Without this, the // 'Authenticate' remains ticked (the default value) if loading an // SMTP target without authentication. - me.getViewModel().set('authentication', values.authentication); - me.getViewModel().set('originalAuthentication', values.authentication); + me.getViewModel().set('originalAuthMethod', values['auth-method']); return values; }, diff --git a/src/window/EndpointEditBase.js b/src/window/EndpointEditBase.js index 8c1bfc1..8df2016 100644 --- a/src/window/EndpointEditBase.js +++ b/src/window/EndpointEditBase.js @@ -47,6 +47,7 @@ Ext.define('Proxmox.window.EndpointEditBase', { baseUrl: me.baseUrl, type: me.type, defaultMailAuthor: endpointConfig.defaultMailAuthor, + enableOAuth2: endpointConfig.enableOAuth2, }, ], }); -- 2.47.3