From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9]) by lore.proxmox.com (Postfix) with ESMTPS id 2F3051FF138 for ; Wed, 04 Feb 2026 17:14:49 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 107611A721; Wed, 4 Feb 2026 17:14:37 +0100 (CET) From: Arthur Bied-Charreton To: pve-devel@lists.proxmox.com Subject: [PATCH proxmox-widget-toolkit 2/2] notifications: Add opt-in OAuth2 support for SMTP targets Date: Wed, 4 Feb 2026 17:13:47 +0100 Message-ID: <20260204161354.458814-9-a.bied-charreton@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260204161354.458814-1-a.bied-charreton@proxmox.com> References: <20260204161354.458814-1-a.bied-charreton@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL -0.102 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: UOOK5KUCCLTTKSL6OPX4KIYA4T7MGUOB X-Message-ID-Hash: UOOK5KUCCLTTKSL6OPX4KIYA4T7MGUOB 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 VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: Add Google & Microsoft OAuth2 authentication methods to SMTP endpoint config. The enableOAuth2 pmxSmtpEditPanel config flag allows consumers to opt into the new feature, so it can be gradually introduced into services. When disabled, no changes are visible from the UI, and only 'None' and 'Username/Password' are shown as authentication methods. The flag is passed from the schema config, as it is done for defaultMailAuthor. Signed-off-by: Arthur Bied-Charreton --- src/panel/SmtpEditPanel.js | 191 +++++++++++++++++++++++++++++++-- src/window/EndpointEditBase.js | 1 + 2 files changed, 181 insertions(+), 11 deletions(-) diff --git a/src/panel/SmtpEditPanel.js b/src/panel/SmtpEditPanel.js index 37e4d51..21ae883 100644 --- a/src/panel/SmtpEditPanel.js +++ b/src/panel/SmtpEditPanel.js @@ -6,10 +6,24 @@ 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', + authMethod: 'plain', + oAuth2ClientId: '', + oAuth2ClientSecret: '', + oAuth2TenantId: '', + oAuth2RefreshToken: '', authentication: true, originalAuthentication: true, }, @@ -39,6 +53,30 @@ Ext.define('Proxmox.panel.SmtpEditPanel', { return shouldShowUnchanged ? gettext('Unchanged') : ''; }, + isPlainAuthentication: function (get) { + return get('authMethod') === 'plain'; + }, + isOAuth2Authentication: function (get) { + if (!get('enableOAuth2')) { return false }; + const 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 }; + const clientId = get('oAuth2ClientId'); + const clientSecret = get('oAuth2ClientSecret'); + + if (get('authMethod') === 'microsoft-oauth2') { + const tenantId = get('oAuth2TenantId'); + return clientId && clientId.trim() !== '' && clientSecret && clientSecret.trim() !== '' && tenantId && tenantId.trim() !== ''; + } else { + return clientId && clientId.trim() !== '' && clientSecret && clientSecret.trim() !== ''; + } + } }, }, @@ -102,11 +140,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 +170,8 @@ Ext.define('Proxmox.panel.SmtpEditPanel', { deleteEmpty: '{!isCreate}', }, bind: { - disabled: '{!authentication}', + hidden: '{!isPlainAuthentication}', + disabled: '{!isPlainAuthentication}', }, }, { @@ -130,10 +183,93 @@ 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', + allowBlank: false, + bind: { + hidden: '{!isOAuth2Authentication}', + disabled: '{!isOAuth2Authentication}', + value: '{oAuth2ClientSecret}', + }, + cbind: { + deleteEmpty: '{!isCreate}', + }, + }, + { + xtype: 'proxmoxtextfield', + fieldLabel: gettext('Tenant ID'), + name: 'oauth2-tenant-id', + allowBlank: false, + bind: { + hidden: '{!isMicrosoftOAuth2Authentication}', + disabled: '{!isMicrosoftOAuth2Authentication}', + value: '{oAuth2TenantId}', + }, + cbind: { + deleteEmpty: '{!isCreate}', + }, + }, + { + xtype: 'button', + text: 'Authenticate', + fieldLabel: gettext('Authenticate'), + handler: async function () { + const panel = this.up('pmxSmtpEditPanel'); + const form = panel.up('form'); + const values = form.getValues(); + + const refreshToken = await panel.handleOAuth2Flow(values); + + panel.getViewModel().set('oAuth2RefreshToken', refreshToken); + }, + bind: { + hidden: '{!isOAuth2Authentication}', + disabled: '{!enableAuthenticate}', + }, + cbind: { + deleteEmpty: '{!isCreate}', + }, + }, + { + xtype: 'hiddenfield', + name: 'oauth2-refresh-token', + allowBlank: false, + bind: { + value: '{oAuth2RefreshToken}', + disabled: '{!isOAuth2Authentication}', + }, + getErrors: function () { + if (this.disabled) { + return []; + } + if (!this.getValue()) { + return ['']; + } + return []; + } + } ], columnB: [ { @@ -159,7 +295,6 @@ Ext.define('Proxmox.panel.SmtpEditPanel', { }, }, ], - advancedColumnB: [ { xtype: 'proxmoxtextfield', @@ -172,7 +307,15 @@ Ext.define('Proxmox.panel.SmtpEditPanel', { }, }, ], + handleOAuth2Flow: function (values) { + const 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 +323,24 @@ Ext.define('Proxmox.panel.SmtpEditPanel', { values.mailto = values.mailto.split(/[\s,;]+/); } - if (!values.authentication && !me.isCreate) { + if (values['auth-method'] === 'none' && !me.isCreate) { + delete values['auth-method']; Proxmox.Utils.assemble_field_data(values, { delete: 'username' }); Proxmox.Utils.assemble_field_data(values, { delete: 'password' }); + 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 (values['auth-method'] === 'plain' && !me.isCreate) { + 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 (values['auth-method'] === 'microsoft-oauth2' && !me.isCreate) { + Proxmox.Utils.assemble_field_data(values, { delete: 'username' }); + Proxmox.Utils.assemble_field_data(values, { delete: 'password' }); + } else if (values['auth-method'] === 'google-oauth2' && !me.isCreate) { + 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,12 +357,23 @@ 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) { + 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 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