use std::sync::OnceLock;
use serde::Deserialize;
static SERVICE_NAME: OnceLock<String> = OnceLock::new();
pub fn init_service_name(name: &str) {
SERVICE_NAME.set(name.to_string()).ok();
}
pub fn service_name() -> &'static str {
SERVICE_NAME
.get()
.map(|s| s.as_str())
.unwrap_or("agentmail")
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Secret {
Raw(String),
Keyring(String),
#[serde(alias = "cmd")]
Command(String),
}
impl Secret {
pub fn new_raw(value: impl Into<String>) -> Self {
Self::Raw(value.into())
}
pub fn new_keyring(key: impl Into<String>) -> Self {
Self::Keyring(key.into())
}
pub async fn get(&self) -> Result<String, String> {
match self {
Secret::Raw(v) => Ok(v.clone()),
Secret::Keyring(key) => {
let service = service_name().to_string();
let key = key.clone();
tokio::task::spawn_blocking(move || {
let entry = keyring_core::Entry::new(&service, &key)
.map_err(|e| format!("keyring entry error: {e}"))?;
entry
.get_password()
.map_err(|e| format!("keyring get_password error: {e}"))
})
.await
.map_err(|e| format!("spawn_blocking error: {e}"))?
}
Secret::Command(cmd) => {
let output = tokio::process::Command::new("sh")
.args(["-c", cmd])
.output()
.await
.map_err(|e| format!("command error: {e}"))?;
if !output.status.success() {
return Err(format!(
"command failed ({}): {}",
output.status,
String::from_utf8_lossy(&output.stderr).trim()
));
}
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
}
}
}
pub async fn set(&mut self, value: &str) -> Result<(), String> {
match self {
Secret::Raw(v) => {
*v = value.to_string();
Ok(())
}
Secret::Keyring(key) => {
let service = service_name().to_string();
let key = key.clone();
let value = value.to_string();
tokio::task::spawn_blocking(move || {
let entry = keyring_core::Entry::new(&service, &key)
.map_err(|e| format!("keyring entry error: {e}"))?;
entry
.set_password(&value)
.map_err(|e| format!("keyring set_password error: {e}"))
})
.await
.map_err(|e| format!("spawn_blocking error: {e}"))?
}
Secret::Command(_) => Err("Cannot set a command-based secret".to_string()),
}
}
pub async fn delete(&mut self) -> Result<(), String> {
match self {
Secret::Raw(v) => {
v.clear();
Ok(())
}
Secret::Keyring(key) => {
let service = service_name().to_string();
let key = key.clone();
tokio::task::spawn_blocking(move || {
if let Ok(entry) = keyring_core::Entry::new(&service, &key) {
let _ = entry.delete_credential();
}
Ok(())
})
.await
.map_err(|e| format!("spawn_blocking error: {e}"))?
}
Secret::Command(_) => Ok(()),
}
}
}