From: Arthur Bied-Charreton <a.bied-charreton@proxmox.com>
To: pve-devel@lists.proxmox.com, pbs-devel@lists.proxmox.com
Subject: [PATCH proxmox v5 05/27] notify: smtp: update API with OAuth2 parameters
Date: Tue, 5 May 2026 10:32:26 +0200 [thread overview]
Message-ID: <20260505083248.36450-6-a.bied-charreton@proxmox.com> (raw)
In-Reply-To: <20260505083248.36450-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 in per-endpoint state files.
Signed-off-by: Arthur Bied-Charreton <a.bied-charreton@proxmox.com>
---
proxmox-notify/src/api/smtp.rs | 101 ++++++++++++++++++++-------
proxmox-notify/src/endpoints/smtp.rs | 45 +++++++++++-
2 files changed, 118 insertions(+), 28 deletions(-)
diff --git a/proxmox-notify/src/api/smtp.rs b/proxmox-notify/src/api/smtp.rs
index 470701bf..d1482047 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,16 @@ 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`
+///
+/// The OAuth2 refresh token lives in a per-endpoint state file rather than
+/// the notifications config, so it is passed through as a separate parameter.
+/// Callers pass `Some(_)` only when seeding the state at create time or after
+/// a re-authorization.
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 +115,17 @@ 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`
+///
+/// The OAuth2 refresh token lives in a per-endpoint state file rather than
+/// the notifications config, so it is passed through as a separate parameter.
+/// Callers pass `Some(_)` only when seeding the state at create time or after
+/// a re-authorization.
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 +141,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 +177,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 +202,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 +236,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 +246,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 +259,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 +268,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 +301,7 @@ pub mod tests {
Default::default(),
None,
None,
+ None,
)
.is_err());
@@ -273,6 +319,7 @@ pub mod tests {
Default::default(),
Default::default(),
None,
+ None,
Some(&[0; 32]),
)
.is_err());
@@ -304,6 +351,7 @@ pub mod tests {
},
Default::default(),
None,
+ None,
Some(&digest),
)?;
@@ -327,6 +375,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..19b97113 100644
--- a/proxmox-notify/src/endpoints/smtp.rs
+++ b/proxmox-notify/src/endpoints/smtp.rs
@@ -83,11 +83,20 @@ 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.
+ /// If set to `None`, no authentication will be performed.
+ #[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 +143,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 +186,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.
--
2.47.3
next prev parent reply other threads:[~2026-05-05 8:35 UTC|newest]
Thread overview: 28+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-05-05 8:32 [PATCH docs/manager/proxmox{,-perl-rs,-widget-toolkit,-backup} v5 00/27] fix #7238: Add XOAUTH2 authentication support for SMTP notification targets Arthur Bied-Charreton
2026-05-05 8:32 ` [PATCH proxmox v5 01/27] add oauth2 and ureq to workspace dependencies Arthur Bied-Charreton
2026-05-05 8:32 ` [PATCH proxmox v5 02/27] notify: smtp: introduce xoauth2 module Arthur Bied-Charreton
2026-05-05 8:32 ` [PATCH proxmox v5 03/27] notify: smtp: introduce state management Arthur Bied-Charreton
2026-05-05 8:32 ` [PATCH proxmox v5 04/27] notify: smtp: factor out transport building logic Arthur Bied-Charreton
2026-05-05 8:32 ` Arthur Bied-Charreton [this message]
2026-05-05 8:32 ` [PATCH proxmox v5 06/27] notify: smtp: add API to exchange authorization code for refresh token Arthur Bied-Charreton
2026-05-05 8:32 ` [PATCH proxmox v5 07/27] notify: smtp: infer auth method for backwards compatibility Arthur Bied-Charreton
2026-05-05 8:32 ` [PATCH proxmox v5 08/27] notify: smtp: add state handling logic Arthur Bied-Charreton
2026-05-05 8:32 ` [PATCH proxmox v5 09/27] notify: smtp: add XOAUTH2 authentication support Arthur Bied-Charreton
2026-05-05 8:32 ` [PATCH proxmox-perl-rs v5 10/27] pve-rs: notify: smtp: add OAuth2 parameters to bindings Arthur Bied-Charreton
2026-05-05 8:32 ` [PATCH proxmox-perl-rs v5 11/27] pve-rs: notify: add binding for triggering state refresh Arthur Bied-Charreton
2026-05-05 8:32 ` [PATCH proxmox-perl-rs v5 12/27] pve-rs: notify: add binding for initial OAuth2 refresh token exchange Arthur Bied-Charreton
2026-05-05 8:32 ` [PATCH proxmox-widget-toolkit v5 13/27] utils: add OAuth2 flow handlers Arthur Bied-Charreton
2026-05-05 8:32 ` [PATCH proxmox-widget-toolkit v5 14/27] utils: oauth2: add callback handler Arthur Bied-Charreton
2026-05-05 8:32 ` [PATCH proxmox-widget-toolkit v5 15/27] notifications: add opt-in OAuth2 support for SMTP targets Arthur Bied-Charreton
2026-05-05 8:32 ` [PATCH pve-manager v5 16/27] notifications: smtp: api: add XOAUTH2 parameters Arthur Bied-Charreton
2026-05-05 8:32 ` [PATCH pve-manager v5 17/27] notifications: add endpoint for initial OAuth2 refresh token exchange Arthur Bied-Charreton
2026-05-05 8:32 ` [PATCH pve-manager v5 18/27] pveupdate: refresh notification targets' OAuth2 state Arthur Bied-Charreton
2026-05-05 8:32 ` [PATCH pve-manager v5 19/27] login: handle OAuth2 callback Arthur Bied-Charreton
2026-05-05 8:32 ` [PATCH pve-manager v5 20/27] fix #7238: notifications: smtp: add XOAUTH2 support Arthur Bied-Charreton
2026-05-05 8:32 ` [PATCH proxmox-backup v5 21/27] notifications: add XOAUTH2 parameters to endpoints Arthur Bied-Charreton
2026-05-05 8:32 ` [PATCH proxmox-backup v5 22/27] notifications: add endpoint for initial OAuth2 refresh token exchange Arthur Bied-Charreton
2026-05-05 8:32 ` [PATCH proxmox-backup v5 23/27] login: handle OAuth2 callback Arthur Bied-Charreton
2026-05-05 8:32 ` [PATCH proxmox-backup v5 24/27] fix #7238: notifications: smtp: add XOAUTH2 support Arthur Bied-Charreton
2026-05-05 8:32 ` [PATCH proxmox-backup v5 25/27] daily-update: refresh OAuth2 state for SMTP notification endpoints Arthur Bied-Charreton
2026-05-05 8:32 ` [PATCH proxmox-backup v5 26/27] notifications: add OAuth2 section to SMTP targets docs Arthur Bied-Charreton
2026-05-05 8:32 ` [PATCH pve-docs v5 27/27] " 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=20260505083248.36450-6-a.bied-charreton@proxmox.com \
--to=a.bied-charreton@proxmox.com \
--cc=pbs-devel@lists.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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox