On Tue, Feb 10, 2026 at 04:52:51PM +0100, Lukas Wagner wrote:
> Hey Arthur!
> 
> Great work, looking really good so far. Left some comments inline.
> 
> On Wed Feb 4, 2026 at 5:13 PM CET, Arthur Bied-Charreton wrote:
> > Add Google & Microsoft OAuth2 support for SMTP notification targets. The
> > SmtpEndpoint implements refresh_state() to allow proactively refreshing
> > tokens through pveupdate.
> >
> > The SMTP API functions are updated to handle OAuth2 state. The refresh
> > token initially comes from the frontend, and it is more state than it is
> > configuration, therefore it is passed as a single parameter in
> > {add,update}_endpoint and persisted separately.
> >
> > Signed-off-by: Arthur Bied-Charreton <[email protected]>
> > [...]
> > +    update_state(
> > +        &endpoint_config.name,
> > +        Some(SmtpState {
> > +            oauth2_refresh_token,
> > +            last_refreshed: SystemTime::now()
> > +                .duration_since(UNIX_EPOCH)
> 
> We already have a nice helper for this - proxmox_time::epoch_i64
> 
I was looking for something like that, thanks :)

> > +                .unwrap() +                .as_secs(),
> > +        }),
> > +    )?;
> > +
> >      config
> >          .config
> >          .set_data(&endpoint_config.name, SMTP_TYPENAME, &endpoint_config)
> > @@ -76,6 +119,28 @@ pub fn add_endpoint(
> >          })
> >  }
> >  
> > +/// Apply `updater` to the private config identified by `name`, and set
> > +/// the private config entry afterwards.
> > +pub fn update_private_config(
> 
> 
> This function does not need to be `pub`, I think?
> 

> 
> > +    config: &mut Config,
> > +    name: &str,
> > +    updater: impl FnOnce(&mut SmtpPrivateConfig),
> 
> nice idea btw, using a closure like this
> 
> > [...]
> > +    update_state(
> > +        name,
> > +        Some(SmtpState {
> > +            oauth2_refresh_token,
> > +            last_refreshed: SystemTime::now()
> > +                .duration_since(UNIX_EPOCH)
> > +                .unwrap()
> > +                .as_secs(),
> 
> same here regarding proxmox_time::epoch_i64
> 
> > [...]
> > +/// Authentication mode to use for SMTP.
> > +#[api]
> > +#[derive(Serialize, Deserialize, Clone, Debug, Default)]
> 
> You could also derive `Copy` here, then you don't need to clone it in
> some places in the code.
> 

[...]
> Same here regarding proxmox_time::epoch_i64
> 
> > +
> > +                transport_builder
> > +                    .credentials(Credentials::new(
> > +                        self.config.from_address.clone(),
> > +                        token_exchange_result.access_token.into_secret(),
> > +                    ))
> > +                    .authentication(vec![Mechanism::Xoauth2])
> >              }
> > -        }
> > +        };
> >  
> >          let transport = transport_builder.build();
> 
> I think it could be nice to move everything above this line to a
> separate method `build_smtp_tansport` - this method is getting rather
> long and this part is a very distinct step of the things performed here.
> 
Thanks for all the feedback, I will address all of it in v2

> > +        {
> > +            return Ok(false);
> > +        }
> > +
> > +        let Some(auth_method) = self.config.auth_method.as_ref() else {
> > +            return Ok(false);
> > +        };
> > +
> > +        let new_state = match self
> > +            .get_access_token(&refresh_token, auth_method)?
> > +            .refresh_token
> > +        {
> > +            // Microsoft OAuth2, new token was returned
> > +            Some(new_refresh_token) => SmtpState {
> > +                oauth2_refresh_token: 
> > Some(new_refresh_token.into_secret()),
> > +                last_refreshed: SystemTime::now()
> > +                    .duration_since(UNIX_EPOCH)
> > +                    .unwrap()
> > +                    .as_secs(),
> > +            },
> > +            // Google OAuth2, refresh token's lifetime was extended
> > +            None => SmtpState {
> > +                oauth2_refresh_token: Some(refresh_token),
> > +                last_refreshed: SystemTime::now()
> > +                    .duration_since(UNIX_EPOCH)
> > +                    .unwrap()
> > +                    .as_secs(),
> > +            },
> > +        };
> > +
> > +        state.set(self.name(), &new_state)?;
> > +        Ok(true)
> 
> It seems like you return Ok(true) in case that the state changed (token
> was refreshed) and Ok(false) if nothing changed, is this correct?
> 
> The boolean parameter should be documented in the trait doc-comments.
> Also at the moment you don't seem to use the boolean part anywhere? I
> guess we could use it to determine whether we need to replace the
> existing state file at all (we don't if all endpoints returned `false`).
> 
The plan was originally to be able to only persist the state if
something changed yes, however with the new approach we discussed
off-list, each endpoint will only be passed its own state (as opposed to 
currently, getting the state for all endpoints and being responsible for 
looking up its own).

I was thinking about changing send() and refresh_state() to return an
Option<S> instead, that way we avoid the output parameter and the bus
can determine whether it needs to update the global state itself.



Reply via email to