From c17837fa2ab40b0aa9bc223b28ab27a02ede52af Mon Sep 17 00:00:00 2001 From: Henning Date: Sun, 21 Dec 2025 18:00:48 +0100 Subject: [PATCH] Add SMTP OAuth2 configuration options and improve error handling --- .env.template | 11 +++++++++++ src/api/admin.rs | 16 ++++++++-------- src/config.rs | 2 +- src/mail.rs | 6 +++--- src/static/scripts/admin_settings.js | 2 +- 5 files changed, 24 insertions(+), 13 deletions(-) diff --git a/.env.template b/.env.template index 67f531fc..b1fc5fd8 100644 --- a/.env.template +++ b/.env.template @@ -624,6 +624,17 @@ ## Multiple options need to be separated by a comma ','. # SMTP_AUTH_MECHANISM= +## SMTP OAuth2 settings +## These are required if SMTP_AUTH_MECHANISM includes "Xoauth2". +## After configuring these, you'll need to use the admin panel to complete the authorization flow. +# SMTP_OAUTH2_CLIENT_ID= +# SMTP_OAUTH2_CLIENT_SECRET= +# SMTP_OAUTH2_AUTH_URL= +# SMTP_OAUTH2_TOKEN_URL= +# SMTP_OAUTH2_SCOPES= +## The refresh token is typically obtained automatically during the authorization flow in the admin panel. +# SMTP_OAUTH2_REFRESH_TOKEN= + ## Server name sent during the SMTP HELO ## By default this value should be the machine's hostname, ## but might need to be changed in case it trips some anti-spam filters diff --git a/src/api/admin.rs b/src/api/admin.rs index 5b1dc7c7..4c905eed 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -352,7 +352,7 @@ async fn test_smtp(data: Json, _token: AdminToken) -> EmptyResult { } } -#[post("/test/oauth2")] +#[post("/oauth2/test")] async fn refresh_oauth2_token_endpoint(_token: AdminToken) -> EmptyResult { if CONFIG.smtp_oauth2_client_id().is_none() { err!("OAuth2 is not configured") @@ -384,7 +384,7 @@ fn oauth2_authorize(_token: AdminToken) -> Result { let redirect_uri = format!("{}/admin/oauth2/callback", CONFIG.domain()); // Build authorization URL using url crate to ensure proper encoding - let mut url = Url::parse(&auth_url).map_err(|e| Error::new("Invalid OAuth2 Authorization URL", e.to_string()))?; + let mut url = Url::parse(&auth_url).map_err(|e| err!(format!("Invalid OAuth2 Authorization URL: {e}")))?; { let mut qp = url.query_pairs_mut(); qp.append_pair("client_id", &client_id); @@ -414,7 +414,7 @@ async fn oauth2_callback(params: OAuth2CallbackParams) -> Result, E // Check for errors from OAuth2 provider if let Some(error) = params.error { let description = params.error_description.unwrap_or_else(|| "Unknown error".to_string()); - return Err(Error::new("OAuth2 Authorization Failed", format!("{}: {}", error, description))); + err!("OAuth2 Authorization Failed", format!("{error}: {description}")); } // Validate required parameters @@ -429,7 +429,7 @@ async fn oauth2_callback(params: OAuth2CallbackParams) -> Result, E }; if !valid_state { - return Err(Error::new("OAuth2 State Validation Failed", "Invalid or expired state token")); + err!("OAuth2 State Validation Failed", "Invalid or expired state token"); } // Remove used state @@ -453,16 +453,16 @@ async fn oauth2_callback(params: OAuth2CallbackParams) -> Result, E .form(&form_params) .send() .await - .map_err(|e| Error::new("OAuth2 Token Exchange Error", e.to_string()))?; + .map_err(|e| err!(format!("OAuth2 Token Exchange Error: {e}")))?; if !response.status().is_success() { let status = response.status(); let body = response.text().await.unwrap_or_else(|_| String::from("Unable to read response body")); - return Err(Error::new("OAuth2 Token Exchange Failed", format!("HTTP {}: {}", status, body))); + err!("OAuth2 Token Exchange Failed", format!("HTTP {status}: {body}")); } let token_response: Value = - response.json().await.map_err(|e| Error::new("OAuth2 Token Parse Error", e.to_string()))?; + response.json().await.map_err(|e| err!(format!("OAuth2 Token Parse Error: {e}")))?; // Extract refresh_token from response let refresh_token = @@ -472,7 +472,7 @@ async fn oauth2_callback(params: OAuth2CallbackParams) -> Result, E let config_builder: ConfigBuilder = serde_json::from_value(json!({ "smtp_oauth2_refresh_token": refresh_token })) - .map_err(|e| Error::new("ConfigBuilder serialization error", e.to_string()))?; + .map_err(|e| err!(format!("ConfigBuilder serialization error: {e}")))?; CONFIG.update_config_partial(config_builder).await?; // Return success page via template diff --git a/src/config.rs b/src/config.rs index b98f0249..bdf304c2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -898,7 +898,7 @@ make_config! { /// SMTP OAuth2 Refresh Token |> OAuth2 Refresh Token for obtaining new access tokens smtp_oauth2_refresh_token: Pass, true, option; /// SMTP OAuth2 Scopes |> Comma-separated list of OAuth2 scopes - smtp_oauth2_scopes: String, true, def, "https://mail.google.com/".to_string(); + smtp_oauth2_scopes: String, true, def, "".to_string(); /// SMTP connection timeout |> Number of seconds when to stop trying to connect to the SMTP server smtp_timeout: u64, true, def, 15; /// Server name sent during HELO |> By default this value should be the machine's hostname, but might need to be changed in case it trips some anti-spam filters diff --git a/src/mail.rs b/src/mail.rs index deadc45a..01092684 100644 --- a/src/mail.rs +++ b/src/mail.rs @@ -60,16 +60,16 @@ pub async fn refresh_oauth2_token() -> Result { .form(&form_params) .send() .await - .map_err(|e| Error::new("OAuth2 Token Refresh Error", e.to_string()))?; + .map_err(|e| err!(format!("OAuth2 Token Refresh Error: {e}")))?; if !response.status().is_success() { let status = response.status(); let body = response.text().await.unwrap_or_else(|_| String::from("Unable to read response body")); - return Err(Error::new("OAuth2 Token Refresh Failed", format!("HTTP {status}: {body}"))); + err!("OAuth2 Token Refresh Failed", format!("HTTP {status}: {body}")); } let token_response: TokenRefreshResponse = - response.json().await.map_err(|e| Error::new("OAuth2 Token Parse Error", e.to_string()))?; + response.json().await.map_err(|e| err!(format!("OAuth2 Token Parse Error: {e}")))?; let expires_at = token_response .expires_in diff --git a/src/static/scripts/admin_settings.js b/src/static/scripts/admin_settings.js index 8ed1f13c..49ad44d5 100644 --- a/src/static/scripts/admin_settings.js +++ b/src/static/scripts/admin_settings.js @@ -34,7 +34,7 @@ function oauth2RefreshToken(event) { return false; } - _post(`${BASE_URL}/admin/test/oauth2`, + _post(`${BASE_URL}/admin/oauth2/test`, "OAuth2 token refreshed successfully", "Error refreshing OAuth2 token", null, false