[PATCH proxmox-widget-toolkit v3 13/23] notifications: Add opt-in OAuth2 support for SMTP targets

Arthur Bied-Charreton
2026-04-15T07:03:55Z

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 <a.bied-charreton@proxmox.com>
---
src/panel/SmtpEditPanel.js | 263 +++++++++++++++++++++++++++++++--
src/window/EndpointEditBase.js | 1 +
2 files changed, 251 insertions(+), 13 deletions(-)

diff --git a/src/panel/SmtpEditPanel.js b/src/panel/SmtpEditPanel.js
index 37e4d51..e6c717b 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,62 @@ Ext.define('Proxmox.panel.SmtpEditPanel', {

return shouldShowUnchanged ? gettext('Unchanged') : '';
},
+ 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() !== ''
+ );
+ }
+ },
+ oAuth2ClientSecretEmptyText: function (get) {
+ let isCreate = this.getView().isCreate;
+ let isOAuth2 = get('isOAuth2Authentication');
+ return !isCreate && isOAuth2 ? gettext('Unchanged') : '';
+ },
+ isAuthorized: function (get) {
+ if (!get('enableOAuth2')) {
+ return false;
+ }
+ return !!get('oAuth2RefreshToken');
+ },
+ authorizeButtonDisabled: function (get) {
+ return !get('enableAuthenticate') || get('isAuthorized');
+ },
},
},

@@ -102,11 +172,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 +202,8 @@ Ext.define('Proxmox.panel.SmtpEditPanel', {
deleteEmpty: '{!isCreate}',
},
bind: {
- disabled: '{!authentication}',
+ hidden: '{!isPlainAuthentication}',
+ disabled: '{!isPlainAuthentication}',
},
},
{
@@ -130,10 +215,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 +347,6 @@ Ext.define('Proxmox.panel.SmtpEditPanel', {
},
},
],
-
advancedColumnB: [
{
xtype: 'proxmoxtextfield',
@@ -172,7 +359,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 +382,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,12 +425,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 && 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
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