From: Arthur Bied-Charreton <a.bied-charreton@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH proxmox v3 05/23] notify: smtp: Update API with OAuth2 parameters
Date: Wed, 15 Apr 2026 09:02:02 +0200 [thread overview]
Message-ID: <20260415070220.100306-6-a.bied-charreton@proxmox.com> (raw)
In-Reply-To: <20260415070220.100306-1-a.bied-charreton@proxmox.com>
Add the OAuth2 client & tenant IDs to the public config, and the client
secret to the private config.
The refresh token is more dynamic and will be updated on the fly by
proxmox-notify. In order to avoid the config file changing without user
interaction, it is passed as its own parameter and will be managed
separately.
Signed-off-by: Arthur Bied-Charreton <a.bied-charreton@proxmox.com>
Reviewed-by: Lukas Wagner <l.wagner@proxmox.com>
---
proxmox-notify/src/api/smtp.rs | 99 ++++++++++++++++++++--------
proxmox-notify/src/endpoints/smtp.rs | 42 ++++++++++++
proxmox-notify/src/lib.rs | 2 +-
3 files changed, 116 insertions(+), 27 deletions(-)
diff --git a/proxmox-notify/src/api/smtp.rs b/proxmox-notify/src/api/smtp.rs
index 470701bf..b265c26a 100644
--- a/proxmox-notify/src/api/smtp.rs
+++ b/proxmox-notify/src/api/smtp.rs
@@ -1,9 +1,10 @@
use proxmox_http_error::HttpError;
use crate::api::{http_bail, http_err};
+use crate::context::context;
use crate::endpoints::smtp::{
- DeleteableSmtpProperty, SmtpConfig, SmtpConfigUpdater, SmtpPrivateConfig,
- SmtpPrivateConfigUpdater, SMTP_TYPENAME,
+ DeleteableSmtpProperty, SmtpAuthMethod, SmtpConfig, SmtpConfigUpdater, SmtpPrivateConfig,
+ SmtpPrivateConfigUpdater, State, SMTP_TYPENAME,
};
use crate::Config;
@@ -30,6 +31,31 @@ pub fn get_endpoint(config: &Config, name: &str) -> Result<SmtpConfig, HttpError
.map_err(|_| http_err!(NOT_FOUND, "endpoint '{name}' not found"))
}
+/// Apply `updater` to the private config identified by `name`, and set
+/// the private config entry afterwards.
+fn update_private_config(
+ config: &mut Config,
+ name: &str,
+ updater: impl FnOnce(&mut SmtpPrivateConfig),
+) -> Result<(), HttpError> {
+ let mut private_config: SmtpPrivateConfig = get_private_config(config, name)?;
+ updater(&mut private_config);
+
+ super::set_private_config_entry(config, private_config, SMTP_TYPENAME, name)
+}
+
+fn get_private_config(config: &Config, name: &str) -> Result<SmtpPrivateConfig, HttpError> {
+ config
+ .private_config
+ .lookup(SMTP_TYPENAME, name)
+ .map_err(|e| {
+ http_err!(
+ NOT_FOUND,
+ "no private config found for SMTP endpoint : '{e}'"
+ )
+ })
+}
+
/// Add a new smtp endpoint.
///
/// The caller is responsible for any needed permission checks.
@@ -38,10 +64,15 @@ pub fn get_endpoint(config: &Config, name: &str) -> Result<SmtpConfig, HttpError
/// - an entity with the same name already exists (`400 Bad request`)
/// - the configuration could not be saved (`500 Internal server error`)
/// - mailto *and* mailto_user are both set to `None`
+///
+/// `oauth2_refresh_token` is initially passed through the API when an OAuth2
+/// endpoint is created/updated, however its state is not managed through a
+/// config, which is why it is passed separately.
pub fn add_endpoint(
config: &mut Config,
endpoint_config: SmtpConfig,
private_endpoint_config: SmtpPrivateConfig,
+ oauth2_refresh_token: Option<String>,
) -> Result<(), HttpError> {
if endpoint_config.name != private_endpoint_config.name {
// Programming error by the user of the crate, thus we panic
@@ -83,11 +114,16 @@ pub fn add_endpoint(
/// Returns a `HttpError` if:
/// - the configuration could not be saved (`500 Internal server error`)
/// - mailto *and* mailto_user are both set to `None`
+///
+/// `oauth2_refresh_token` is initially passed through the API when an OAuth2
+/// endpoint is created/updated, however its state is not managed through a
+/// config, which is why it is passed separately.
pub fn update_endpoint(
config: &mut Config,
name: &str,
updater: SmtpConfigUpdater,
private_endpoint_config_updater: SmtpPrivateConfigUpdater,
+ oauth2_refresh_token: Option<String>,
delete: Option<&[DeleteableSmtpProperty]>,
digest: Option<&[u8]>,
) -> Result<(), HttpError> {
@@ -103,20 +139,20 @@ pub fn update_endpoint(
DeleteableSmtpProperty::Disable => endpoint.disable = None,
DeleteableSmtpProperty::Mailto => endpoint.mailto.clear(),
DeleteableSmtpProperty::MailtoUser => endpoint.mailto_user.clear(),
- DeleteableSmtpProperty::Password => super::set_private_config_entry(
- config,
- SmtpPrivateConfig {
- name: name.to_string(),
- password: None,
- },
- SMTP_TYPENAME,
- name,
- )?,
+ DeleteableSmtpProperty::Password => {
+ update_private_config(config, name, |c| c.password = None)?
+ }
+ DeleteableSmtpProperty::AuthMethod => endpoint.auth_method = None,
+ DeleteableSmtpProperty::OAuth2ClientId => endpoint.oauth2_client_id = None,
+ DeleteableSmtpProperty::OAuth2ClientSecret => {
+ update_private_config(config, name, |c| c.oauth2_client_secret = None)?
+ }
+ DeleteableSmtpProperty::OAuth2TenantId => endpoint.oauth2_tenant_id = None,
DeleteableSmtpProperty::Port => endpoint.port = None,
DeleteableSmtpProperty::Username => endpoint.username = None,
}
}
- }
+ };
if let Some(mailto) = updater.mailto {
endpoint.mailto = mailto;
@@ -139,29 +175,24 @@ pub fn update_endpoint(
if let Some(mode) = updater.mode {
endpoint.mode = Some(mode);
}
- if let Some(password) = private_endpoint_config_updater.password {
- super::set_private_config_entry(
- config,
- SmtpPrivateConfig {
- name: name.into(),
- password: Some(password),
- },
- SMTP_TYPENAME,
- name,
- )?;
+ if let Some(auth_method) = updater.auth_method {
+ endpoint.auth_method = Some(auth_method);
}
-
if let Some(author) = updater.author {
endpoint.author = Some(author);
}
-
if let Some(comment) = updater.comment {
endpoint.comment = Some(comment);
}
-
if let Some(disable) = updater.disable {
endpoint.disable = Some(disable);
}
+ if let Some(oauth2_client_id) = updater.oauth2_client_id {
+ endpoint.oauth2_client_id = Some(oauth2_client_id);
+ }
+ if let Some(oauth2_tenant_id) = updater.oauth2_tenant_id {
+ endpoint.oauth2_tenant_id = Some(oauth2_tenant_id);
+ }
if endpoint.mailto.is_empty() && endpoint.mailto_user.is_empty() {
http_bail!(
@@ -169,6 +200,14 @@ pub fn update_endpoint(
"must at least provide one recipient, either in mailto or in mailto-user"
);
}
+ update_private_config(config, name, |c| {
+ if let Some(password) = private_endpoint_config_updater.password {
+ c.password = Some(password);
+ }
+ if let Some(oauth2_client_secret) = private_endpoint_config_updater.oauth2_client_secret {
+ c.oauth2_client_secret = Some(oauth2_client_secret);
+ }
+ })?;
config
.config
@@ -195,6 +234,7 @@ pub fn delete_endpoint(config: &mut Config, name: &str) -> Result<(), HttpError>
super::ensure_safe_to_delete(config, name)?;
super::remove_private_config_entry(config, name)?;
+
config.config.sections.remove(name);
Ok(())
@@ -204,7 +244,7 @@ pub fn delete_endpoint(config: &mut Config, name: &str) -> Result<(), HttpError>
pub mod tests {
use super::*;
use crate::api::test_helpers::*;
- use crate::endpoints::smtp::SmtpMode;
+ use crate::endpoints::smtp::{SmtpAuthMethod, SmtpMode};
pub fn add_smtp_endpoint_for_test(config: &mut Config, name: &str) -> Result<(), HttpError> {
add_endpoint(
@@ -217,6 +257,7 @@ pub mod tests {
author: Some("root".into()),
comment: Some("Comment".into()),
mode: Some(SmtpMode::StartTls),
+ auth_method: Some(SmtpAuthMethod::Plain),
server: "localhost".into(),
port: Some(555),
username: Some("username".into()),
@@ -225,7 +266,9 @@ pub mod tests {
SmtpPrivateConfig {
name: name.into(),
password: Some("password".into()),
+ oauth2_client_secret: None,
},
+ None,
)?;
assert!(get_endpoint(config, name).is_ok());
@@ -256,6 +299,7 @@ pub mod tests {
Default::default(),
None,
None,
+ None,
)
.is_err());
@@ -273,6 +317,7 @@ pub mod tests {
Default::default(),
Default::default(),
None,
+ None,
Some(&[0; 32]),
)
.is_err());
@@ -304,6 +349,7 @@ pub mod tests {
},
Default::default(),
None,
+ None,
Some(&digest),
)?;
@@ -327,6 +373,7 @@ pub mod tests {
"smtp-endpoint",
Default::default(),
Default::default(),
+ None,
Some(&[
DeleteableSmtpProperty::Author,
DeleteableSmtpProperty::MailtoUser,
diff --git a/proxmox-notify/src/endpoints/smtp.rs b/proxmox-notify/src/endpoints/smtp.rs
index 48fdbd8f..b92f96f0 100644
--- a/proxmox-notify/src/endpoints/smtp.rs
+++ b/proxmox-notify/src/endpoints/smtp.rs
@@ -83,11 +83,21 @@ pub struct SmtpConfig {
pub port: Option<u16>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mode: Option<SmtpMode>,
+ /// Method to be used for authentication.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub auth_method: Option<SmtpAuthMethod>,
/// Username to use during authentication.
/// If no username is set, no authentication will be performed.
/// The PLAIN and LOGIN authentication methods are supported
#[serde(skip_serializing_if = "Option::is_none")]
pub username: Option<String>,
+ /// Client ID for XOAUTH2 authentication method.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub oauth2_client_id: Option<String>,
+ /// Tenant ID for XOAUTH2 authentication method. Only required for
+ /// Microsoft Exchange Online OAuth2.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub oauth2_tenant_id: Option<String>,
/// Mail address to send a mail to.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[updater(serde(skip_serializing_if = "Option::is_none"))]
@@ -134,12 +144,39 @@ pub enum DeleteableSmtpProperty {
MailtoUser,
/// Delete `password`
Password,
+ /// Delete `auth_method`
+ AuthMethod,
+ /// Delete `oauth2_client_id`
+ #[serde(rename = "oauth2-client-id")]
+ OAuth2ClientId,
+ /// Delete `oauth2_client_secret`
+ #[serde(rename = "oauth2-client-secret")]
+ OAuth2ClientSecret,
+ /// Delete `oauth2_tenant_id`
+ #[serde(rename = "oauth2-tenant-id")]
+ OAuth2TenantId,
/// Delete `port`
Port,
/// Delete `username`
Username,
}
+/// Authentication mode to use for SMTP.
+#[api]
+#[derive(Serialize, Deserialize, Clone, Debug, Default, Copy)]
+#[serde(rename_all = "kebab-case")]
+pub enum SmtpAuthMethod {
+ /// Username + password
+ #[default]
+ Plain,
+ /// Google OAuth2
+ #[serde(rename = "google-oauth2")]
+ GoogleOAuth2,
+ /// Microsoft OAuth2
+ #[serde(rename = "microsoft-oauth2")]
+ MicrosoftOAuth2,
+}
+
#[api]
#[derive(Serialize, Deserialize, Clone, Updater, Debug)]
#[serde(rename_all = "kebab-case")]
@@ -150,9 +187,14 @@ pub struct SmtpPrivateConfig {
/// Name of the endpoint
#[updater(skip)]
pub name: String,
+
/// The password to use during authentication.
#[serde(skip_serializing_if = "Option::is_none")]
pub password: Option<String>,
+
+ /// OAuth2 client secret
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub oauth2_client_secret: Option<String>,
}
/// A sendmail notification endpoint.
diff --git a/proxmox-notify/src/lib.rs b/proxmox-notify/src/lib.rs
index 619dd7db..d443b738 100644
--- a/proxmox-notify/src/lib.rs
+++ b/proxmox-notify/src/lib.rs
@@ -9,7 +9,7 @@ use context::context;
use serde::{Deserialize, Serialize};
use serde_json::json;
use serde_json::Value;
-use tracing::{error, info};
+use tracing::{debug, error, info};
use proxmox_schema::api;
use proxmox_section_config::SectionConfigData;
--
2.47.3
next prev parent reply other threads:[~2026-04-15 7:03 UTC|newest]
Thread overview: 24+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-04-15 7:01 [PATCH docs/manager/proxmox{,-perl-rs,-widget-toolkit,-backup} v3 00/23] fix #7238: Add XOAUTH2 authentication support for SMTP notification targets Arthur Bied-Charreton
2026-04-15 7:01 ` [PATCH proxmox v3 01/23] Add oauth2 and ureq to workspace dependencies Arthur Bied-Charreton
2026-04-15 7:01 ` [PATCH proxmox v3 02/23] notify: smtp: Introduce xoauth2 module Arthur Bied-Charreton
2026-04-15 7:02 ` [PATCH proxmox v3 03/23] notify: smtp: Introduce state management Arthur Bied-Charreton
2026-04-15 7:02 ` [PATCH proxmox v3 04/23] notify: smtp: Factor out transport building logic Arthur Bied-Charreton
2026-04-15 7:02 ` Arthur Bied-Charreton [this message]
2026-04-15 7:02 ` [PATCH proxmox v3 06/23] notify: smtp: Infer auth method for backwards compatibility Arthur Bied-Charreton
2026-04-15 7:02 ` [PATCH proxmox v3 07/23] notify: smtp: Add state handling logic Arthur Bied-Charreton
2026-04-15 7:02 ` [PATCH proxmox v3 08/23] notify: smtp: Add XOAUTH2 authentication support Arthur Bied-Charreton
2026-04-15 7:02 ` [PATCH proxmox-perl-rs v3 09/23] pve-rs: notify: smtp: add OAuth2 parameters to bindings Arthur Bied-Charreton
2026-04-15 7:02 ` [PATCH proxmox-perl-rs v3 10/23] pve-rs: notify: Add binding for triggering state refresh Arthur Bied-Charreton
2026-04-15 7:02 ` [PATCH proxmox-widget-toolkit v3 11/23] utils: Add OAuth2 flow handlers Arthur Bied-Charreton
2026-04-15 7:02 ` [PATCH proxmox-widget-toolkit v3 12/23] utils: oauth2: Add callback handler Arthur Bied-Charreton
2026-04-15 7:02 ` [PATCH proxmox-widget-toolkit v3 13/23] notifications: Add opt-in OAuth2 support for SMTP targets Arthur Bied-Charreton
2026-04-15 7:02 ` [PATCH pve-manager v3 14/23] notifications: smtp: api: Add XOAUTH2 parameters Arthur Bied-Charreton
2026-04-15 7:02 ` [PATCH pve-manager v3 15/23] notifications: Add trigger-state-refresh endpoint Arthur Bied-Charreton
2026-04-15 7:02 ` [PATCH pve-manager v3 16/23] notifications: Trigger notification target refresh in pveupdate Arthur Bied-Charreton
2026-04-15 7:02 ` [PATCH pve-manager v3 17/23] login: Handle OAuth2 callback Arthur Bied-Charreton
2026-04-15 7:02 ` [PATCH pve-manager v3 18/23] fix #7238: notifications: smtp: Add XOAUTH2 support Arthur Bied-Charreton
2026-04-15 7:02 ` [PATCH proxmox-backup v3 19/23] notifications: Add XOAUTH2 parameters to endpoints Arthur Bied-Charreton
2026-04-15 7:02 ` [PATCH proxmox-backup v3 20/23] login: Handle OAuth2 callback Arthur Bied-Charreton
2026-04-15 7:02 ` [PATCH proxmox-backup v3 21/23] fix #7238: notifications: smtp: Add XOAUTH2 support Arthur Bied-Charreton
2026-04-15 7:02 ` [PATCH proxmox-backup v3 22/23] daily-update: Refresh OAuth2 state for SMTP notification endpoints Arthur Bied-Charreton
2026-04-15 7:02 ` [PATCH pve-docs v3 23/23] notifications: Add OAuth2 section to SMTP targets docs Arthur Bied-Charreton
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260415070220.100306-6-a.bied-charreton@proxmox.com \
--to=a.bied-charreton@proxmox.com \
--cc=pve-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.