From: "Lukas Wagner" <l.wagner@proxmox.com>
To: "Arthur Bied-Charreton" <a.bied-charreton@proxmox.com>,
<pve-devel@lists.proxmox.com>
Subject: Re: [PATCH proxmox 5/7] notify (smtp): Add state handling logic
Date: Mon, 23 Mar 2026 13:26:08 +0100 [thread overview]
Message-ID: <DHA5SBZBKJFM.G88XD2LRX2B1@proxmox.com> (raw)
In-Reply-To: <20260213160415.609868-6-a.bied-charreton@proxmox.com>
I think this misses removing the endpoint state file in
`delete_endpoint`. Some more notes inline.
On Fri Feb 13, 2026 at 5:04 PM CET, Arthur Bied-Charreton wrote:
> Create new state file in add_endpoint, and create/update existing one in
> update_endpoint.
>
> Add trigger_state_refresh to the Endpoint trait, with no-op default
> implementation. Override trigger_state_refresh in SmtpEndpoint's
> Endpoint impl to trigger an OAuth2 token exchange, in order to rotate
> an existing token, or extend its lifetime.
>
> Signed-off-by: Arthur Bied-Charreton <a.bied-charreton@proxmox.com>
> ---
> proxmox-notify/src/api/common.rs | 16 ++++++
> proxmox-notify/src/api/smtp.rs | 29 ++++++++++-
> proxmox-notify/src/endpoints/smtp.rs | 75 +++++++++++++++++++++++++++-
> proxmox-notify/src/lib.rs | 19 +++++++
> 4 files changed, 137 insertions(+), 2 deletions(-)
>
> diff --git a/proxmox-notify/src/api/common.rs b/proxmox-notify/src/api/common.rs
> index fa2356e2..1e6b7d46 100644
> --- a/proxmox-notify/src/api/common.rs
> +++ b/proxmox-notify/src/api/common.rs
> @@ -3,6 +3,22 @@ use proxmox_http_error::HttpError;
> use super::http_err;
> use crate::{Bus, Config, Notification};
>
> +/// Refresh all notification targets' internal state.
> +///
> +/// The caller is responsible for any needed permission checks.
> +pub fn trigger_state_refresh(config: &Config) -> Result<(), HttpError> {
> + let bus = Bus::from_config(config).map_err(|err| {
> + http_err!(
> + INTERNAL_SERVER_ERROR,
> + "Could not instantiate notification bus: {err}"
> + )
> + })?;
> +
> + bus.trigger_state_refresh();
> +
> + Ok(())
> +}
> +
> /// Send a notification to a given target.
> ///
> /// The caller is responsible for any needed permission checks.
> diff --git a/proxmox-notify/src/api/smtp.rs b/proxmox-notify/src/api/smtp.rs
> index 4231cdae..8992e789 100644
> --- a/proxmox-notify/src/api/smtp.rs
> +++ b/proxmox-notify/src/api/smtp.rs
> @@ -2,7 +2,7 @@ use proxmox_http_error::HttpError;
>
> use crate::api::{http_bail, http_err};
> use crate::endpoints::smtp::{
> - DeleteableSmtpProperty, SmtpConfig, SmtpConfigUpdater, SmtpPrivateConfig,
> + self, DeleteableSmtpProperty, SmtpConfig, SmtpConfigUpdater, SmtpPrivateConfig,
> SmtpPrivateConfigUpdater, SMTP_TYPENAME,
> };
> use crate::Config;
> @@ -69,6 +69,16 @@ pub fn add_endpoint(
> &endpoint_config.name,
> )?;
>
> + smtp::State::new(oauth2_refresh_token)
> + .store(&endpoint_config.name)
> + .map_err(|e| {
> + http_err!(
> + INTERNAL_SERVER_ERROR,
> + "could not create state file for endpoint '{}': {e}",
> + endpoint_config.name
> + )
> + })?;
> +
> config
> .config
> .set_data(&endpoint_config.name, SMTP_TYPENAME, &endpoint_config)
> @@ -206,6 +216,23 @@ pub fn update_endpoint(
> }
> })?;
>
> + smtp::State::load(name)
> + .map_err(|e| {
> + http_err!(
> + INTERNAL_SERVER_ERROR,
> + "could not load state for endpoint '{name}': {e}"
> + )
> + })?
> + .set_oauth2_refresh_token(oauth2_refresh_token)
> + .set_last_refreshed(proxmox_time::epoch_i64())
> + .store(name)
> + .map_err(|e| {
> + http_err!(
> + INTERNAL_SERVER_ERROR,
> + "could not persist state for endpoint '{name}': {e}"
> + )
> + })?;
> +
> config
> .config
> .set_data(name, SMTP_TYPENAME, &endpoint)
> diff --git a/proxmox-notify/src/endpoints/smtp.rs b/proxmox-notify/src/endpoints/smtp.rs
> index 361c4da9..244799fd 100644
> --- a/proxmox-notify/src/endpoints/smtp.rs
> +++ b/proxmox-notify/src/endpoints/smtp.rs
> @@ -1,12 +1,15 @@
> use std::borrow::Cow;
> -use std::time::Duration;
> +use std::time::{Duration, SystemTime, UNIX_EPOCH};
>
> use lettre::message::header::{HeaderName, HeaderValue};
> use lettre::message::{Mailbox, MultiPart, SinglePart};
> +use lettre::transport::smtp::authentication::{Credentials, Mechanism};
> use lettre::transport::smtp::client::{Tls, TlsParameters};
> use lettre::{message::header::ContentType, Message, SmtpTransport, Transport};
> use serde::{Deserialize, Serialize};
>
> +use oauth2::{ClientId, ClientSecret, RefreshToken};
> +
> use proxmox_schema::api_types::COMMENT_SCHEMA;
> use proxmox_schema::{api, Updater};
>
> @@ -22,6 +25,7 @@ const SMTP_PORT: u16 = 25;
> const SMTP_SUBMISSION_STARTTLS_PORT: u16 = 587;
> const SMTP_SUBMISSION_TLS_PORT: u16 = 465;
> const SMTP_TIMEOUT: u16 = 5;
> +const SMTP_STATE_REFRESH_CUTOFF_SECONDS: u64 = 60 * 60 * 12;
Duration::from_secs is actually a `const fn`, so you could
const SMTP_STATE_REFRESH_CUTOFF: Duration = Duration::from_secs(12 * 60 * 60);
>
> mod state;
> mod xoauth2;
> @@ -205,6 +209,43 @@ pub struct SmtpEndpoint {
> }
>
> impl SmtpEndpoint {
> + fn get_access_token(
> + &self,
> + refresh_token: &str,
> + auth_method: &SmtpAuthMethod,
> + ) -> Result<xoauth2::TokenExchangeResult, Error> {
> + let client_id = ClientId::new(
> + self.config
> + .oauth2_client_id
> + .as_ref()
> + .ok_or_else(|| Error::Generic("oauth2-client-id not set".into()))?
> + .to_string(),
> + );
> + let client_secret = ClientSecret::new(
> + self.private_config
> + .oauth2_client_secret
> + .as_ref()
> + .ok_or_else(|| Error::Generic("oauth2-client-secret not set".into()))?
> + .to_string(),
> + );
> + let refresh_token = RefreshToken::new(refresh_token.into());
> +
> + match auth_method {
> + SmtpAuthMethod::GoogleOAuth2 => {
> + xoauth2::get_google_token(client_id, client_secret, refresh_token)
> + }
> + SmtpAuthMethod::MicrosoftOAuth2 => xoauth2::get_microsoft_token(
> + client_id,
> + client_secret,
> + self.config.oauth2_tenant_id.as_ref().ok_or(Error::Generic(
> + "tenant ID not set, required for Microsoft OAuth2".into(),
> + ))?,
> + refresh_token,
> + ),
> + _ => Err(Error::Generic("OAuth2 not configured".into())),
> + }
> + }
> +
> fn build_transport(&self, tls: Tls, port: u16) -> Result<SmtpTransport, Error> {
> let mut transport_builder = SmtpTransport::builder_dangerous(&self.config.server)
> .tls(tls)
> @@ -336,6 +377,38 @@ impl Endpoint for SmtpEndpoint {
> fn disabled(&self) -> bool {
> self.config.disable.unwrap_or_default()
> }
> +
> + fn trigger_state_refresh(&self) -> Result<(), Error> {
> + let state = State::load(self.name())?;
> +
> + let Some(refresh_token) = &state.oauth2_refresh_token else {
> + return Ok(());
> + };
> +
> + // The refresh job is configured in pveupdate, which runs once for each node.
> + // Don't refresh if we already did it recently.
> + if SystemTime::now()
> + .duration_since(UNIX_EPOCH + Duration::from_secs(state.last_refreshed as u64))
> + .map_err(|e| Error::Generic(e.to_string()))?
> + < Duration::from_secs(SMTP_STATE_REFRESH_CUTOFF_SECONDS)
This reminds me, we deal with "raw" timestamps so often (especially in
PDM), we really need to have some better types/helpers for things like
these... ofc out of scope for this series...
> + {
> + return Ok(());
> + }
> +
> + let Some(auth_method) = self.config.auth_method.as_ref() else {
> + return Ok(());
> + };
> +
> + match self
> + .get_access_token(refresh_token, auth_method)?
> + .refresh_token
> + {
> + Some(tok) => state.set_oauth2_refresh_token(Some(tok.into_secret())), // New token was returned, rotate
> + None => state,
> + }
> + .set_last_refreshed(proxmox_time::epoch_i64())
> + .store(self.name())
> + }
> }
>
> /// Construct a lettre `Message` from a raw email message.
> diff --git a/proxmox-notify/src/lib.rs b/proxmox-notify/src/lib.rs
> index 879f8326..c1a5e535 100644
> --- a/proxmox-notify/src/lib.rs
> +++ b/proxmox-notify/src/lib.rs
> @@ -157,6 +157,11 @@ pub trait Endpoint {
>
> /// Check if the endpoint is disabled
> fn disabled(&self) -> bool;
> +
> + /// Refresh endpoint's state
> + fn trigger_state_refresh(&self) -> Result<(), Error> {
> + Ok(())
> + }
> }
>
> #[derive(Debug, Clone, Serialize, Deserialize)]
> @@ -593,6 +598,20 @@ impl Bus {
>
> Ok(())
> }
> +
> + /// Refresh all endpoints' internal state.
> + ///
> + /// This function works on a best effort basis, if an endpoint's state cannot
> + /// be updated for whatever reason, the error is logged and the next one(s)
> + /// are attempted.
> + pub fn trigger_state_refresh(&self) {
> + for (name, endpoint) in &self.endpoints {
> + match endpoint.trigger_state_refresh() {
> + Ok(()) => info!("triggered state refresh for endpoint '{name}'"),
I'd probably rather use `debug!` here and then `info!` only if one endpoint
actually refreshed its token
> + Err(e) => error!("could not trigger state refresh for endpoint '{name}': {e}"),
> + };
> + }
> + }
> }
>
> #[cfg(test)]
next prev parent reply other threads:[~2026-03-23 12:25 UTC|newest]
Thread overview: 35+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-02-13 16:03 [PATCH cluster/docs/manager/proxmox{,-perl-rs,-widget-toolkit} 00/17] fix #7238: Add XOAUTH2 authentication support for SMTP notification targets Arthur Bied-Charreton
2026-02-13 16:03 ` [PATCH proxmox 1/7] notify (smtp): Introduce xoauth2 module Arthur Bied-Charreton
2026-02-13 16:04 ` [PATCH proxmox 2/7] notify (smtp): Introduce state module Arthur Bied-Charreton
2026-03-23 12:26 ` Lukas Wagner
2026-03-23 16:32 ` Arthur Bied-Charreton
2026-03-24 8:50 ` Arthur Bied-Charreton
2026-02-13 16:04 ` [PATCH proxmox 3/7] notify (smtp): Factor out transport building logic into own function Arthur Bied-Charreton
2026-02-13 16:04 ` [PATCH proxmox 4/7] notify (smtp): Update API with OAuth2 parameters Arthur Bied-Charreton
2026-02-13 16:04 ` [PATCH proxmox 5/7] notify (smtp): Add state handling logic Arthur Bied-Charreton
2026-03-23 12:26 ` Lukas Wagner [this message]
2026-02-13 16:04 ` [PATCH proxmox 6/7] notify (smtp): Add XOAUTH2 authentication support Arthur Bied-Charreton
2026-03-23 12:26 ` Lukas Wagner
2026-02-13 16:04 ` [PATCH proxmox 7/7] notify (smtp): Add logging and state-related error types Arthur Bied-Charreton
2026-03-23 12:26 ` Lukas Wagner
2026-02-13 16:04 ` [PATCH proxmox-perl-rs 1/1] notify (smtp): add oauth2 parameters to bindings Arthur Bied-Charreton
2026-03-23 12:26 ` Lukas Wagner
2026-03-23 16:44 ` Arthur Bied-Charreton
2026-02-13 16:04 ` [PATCH proxmox-widget-toolkit 1/2] utils: Add OAuth2 flow handlers Arthur Bied-Charreton
2026-03-23 12:26 ` Lukas Wagner
2026-02-13 16:04 ` [PATCH proxmox-widget-toolkit 2/2] notifications: Add opt-in OAuth2 support for SMTP targets Arthur Bied-Charreton
2026-03-23 12:26 ` Lukas Wagner
2026-03-23 16:49 ` Arthur Bied-Charreton
2026-02-13 16:04 ` [PATCH pve-manager 1/5] notifications: Add OAuth2 parameters to schema and add/update endpoints Arthur Bied-Charreton
2026-03-23 12:26 ` Lukas Wagner
2026-02-13 16:04 ` [PATCH pve-manager 2/5] notifications: Add trigger-state-refresh endpoint Arthur Bied-Charreton
2026-03-23 12:26 ` Lukas Wagner
2026-02-13 16:04 ` [PATCH pve-manager 3/5] notifications: Trigger notification target refresh in pveupdate Arthur Bied-Charreton
2026-02-13 16:04 ` [PATCH pve-manager 4/5] notifications: Handle OAuth2 callback in login handler Arthur Bied-Charreton
2026-02-13 16:04 ` [PATCH pve-manager 5/5] notifications: Opt into OAuth2 authentication Arthur Bied-Charreton
2026-02-13 16:04 ` [PATCH pve-cluster 1/1] notifications: Add refresh_targets subroutine to PVE::Notify Arthur Bied-Charreton
2026-03-23 12:26 ` Lukas Wagner
2026-03-23 16:54 ` Arthur Bied-Charreton
2026-02-13 16:04 ` [PATCH pve-docs 1/1] notifications: Add section about OAuth2 to SMTP targets docs Arthur Bied-Charreton
2026-03-23 12:25 ` [PATCH cluster/docs/manager/proxmox{,-perl-rs,-widget-toolkit} 00/17] fix #7238: Add XOAUTH2 authentication support for SMTP notification targets Lukas Wagner
2026-03-25 13:16 ` superseded: " 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=DHA5SBZBKJFM.G88XD2LRX2B1@proxmox.com \
--to=l.wagner@proxmox.com \
--cc=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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox