git.lucas.co / cce-mail
mail client (IMAP/SMTP)
git clone https://git.lucas.co/cce-mail.git

commit9d1b42347662410c4726f9a75df80dbf95213669
parenta6b91905b7
authorLucas Galante <[email protected]>
date2026-06-04 13:21
Support loading Google OAuth client credentials dynamically from ~/.config/ccec/google_client.json

 src/main.rs | 53 +++++++++++++++++++++++++++++++++++++++++++++--------
 1 file changed, 45 insertions(+), 8 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index 52da586..517e10e 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -262,12 +262,47 @@ fn clean_body(body: &str) -> String {
 const GOOGLE_CLIENT_ID: &str = "946029775684-m4u4mme60a6a0qj3p5m5jvea8d2987o9.apps.googleusercontent.com";
 const GOOGLE_CLIENT_SECRET: &str = "GOCSPX-dummysecret";
 
+#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
+struct GoogleClientConfig {
+    client_id: String,
+    client_secret: String,
+}
+
+fn load_google_client_config() -> GoogleClientConfig {
+    let p = std::path::PathBuf::from("/home/lsgalante/.config/ccec/google_client.json");
+    if p.exists() {
+        if let Ok(content) = std::fs::read_to_string(&p) {
+            if let Ok(config) = serde_json::from_str::<GoogleClientConfig>(&content) {
+                return config;
+            }
+        }
+    }
+    let default_config = GoogleClientConfig {
+        client_id: GOOGLE_CLIENT_ID.to_string(),
+        client_secret: GOOGLE_CLIENT_SECRET.to_string(),
+    };
+    if let Ok(content) = serde_json::to_string_pretty(&default_config) {
+        let _ = std::fs::write(&p, content);
+        #[cfg(unix)]
+        {
+            use std::os::unix::fs::PermissionsExt;
+            if let Ok(metadata) = std::fs::metadata(&p) {
+                let mut perms = metadata.permissions();
+                perms.set_mode(0o600);
+                let _ = std::fs::set_permissions(&p, perms);
+            }
+        }
+    }
+    default_config
+}
+
 async fn exchange_code_for_tokens(code: String, sender: calloop::channel::Sender<AppMessage>) {
+    let client_config = load_google_client_config();
     let client = reqwest::Client::new();
     let params = [
         ("code", code.as_str()),
-        ("client_id", GOOGLE_CLIENT_ID),
-        ("client_secret", GOOGLE_CLIENT_SECRET),
+        ("client_id", client_config.client_id.as_str()),
+        ("client_secret", client_config.client_secret.as_str()),
         ("redirect_uri", "http://127.0.0.1:8080"),
         ("grant_type", "authorization_code"),
     ];
@@ -289,7 +324,7 @@ async fn exchange_code_for_tokens(code: String, sender: calloop::channel::Sender
                         .unwrap_or_default()
                         .as_secs();
                     let expiry = now + expires_in;
-
+ 
                     // Request user profile info to get the email address
                     if let Ok(email_resp) = client.get("https://www.googleapis.com/oauth2/v2/userinfo")
                         .bearer_auth(&access_token)
@@ -308,8 +343,8 @@ async fn exchange_code_for_tokens(code: String, sender: calloop::channel::Sender
                                     access_token: Some(access_token),
                                     refresh_token: Some(refresh_token),
                                     token_expiry: Some(expiry),
-                                    client_id: Some(GOOGLE_CLIENT_ID.to_string()),
-                                    client_secret: Some(GOOGLE_CLIENT_SECRET.to_string()),
+                                    client_id: Some(client_config.client_id),
+                                    client_secret: Some(client_config.client_secret),
                                 };
                                 let _ = sender.send(AppMessage::AddAccountSaveOAuth(new_acc));
                                 return;
@@ -351,8 +386,9 @@ async fn refresh_access_token(account: &mut AccountInfo) -> Result<String, Strin
         None => return Err("No refresh token".to_string()),
     };
 
-    let client_id = account.client_id.as_deref().unwrap_or(GOOGLE_CLIENT_ID);
-    let client_secret = account.client_secret.as_deref().unwrap_or(GOOGLE_CLIENT_SECRET);
+    let client_config = load_google_client_config();
+    let client_id = account.client_id.as_deref().unwrap_or(&client_config.client_id);
+    let client_secret = account.client_secret.as_deref().unwrap_or(&client_config.client_secret);
 
     let client = reqwest::Client::new();
     let params = [
@@ -1483,6 +1519,7 @@ impl Application for ClearEmailApp {
             }
             AppMessage::AddAccountOAuth => {
                 let sender = self.sender.clone();
+                let client_config = load_google_client_config();
                 tokio::spawn(async move {
                     let listener = match tokio::net::TcpListener::bind("127.0.0.1:8080").await {
                         Ok(l) => l,
@@ -1496,7 +1533,7 @@ impl Application for ClearEmailApp {
                     
                     let auth_url = format!(
                         "https://accounts.google.com/o/oauth2/v2/auth?client_id={}&redirect_uri=http%3A%2F%2F127.0.0.1%3A8080&response_type=code&scope=https%3A%2F%2Fmail.google.com%2F&access_type=offline&prompt=consent",
-                        GOOGLE_CLIENT_ID
+                        client_config.client_id
                     );
                     let _ = std::process::Command::new("xdg-open").arg(&auth_url).spawn();