//! JMAP HTTP client for making method calls. use super::session::{discover_session, JmapSession}; use super::types::{JmapRequest, JmapResponse, MethodResponse}; use std::time::Duration; /// JMAP client for making API calls. pub struct JmapClient { /// HTTP client client: reqwest::Client, /// Access token access_token: String, /// Cached session session: Option, /// Session discovery URL session_url: String, } impl JmapClient { /// Creates a new JMAP client. /// /// # Arguments /// * `session_url` - The session discovery URL (e.g., https://api.fastmail.com/jmap/session) /// * `access_token` - OAuth2 access token pub fn new(session_url: impl Into, access_token: impl Into) -> Result { let client = reqwest::Client::builder() .timeout(Duration::from_secs(30)) .connect_timeout(Duration::from_secs(10)) .build() .map_err(|e| format!("Failed to build HTTP client: {}", e))?; Ok(Self { client, access_token: access_token.into(), session: None, session_url: session_url.into(), }) } /// Creates a client with a pre-fetched session. pub fn with_session( session: JmapSession, access_token: impl Into, session_url: impl Into, ) -> Result { let client = reqwest::Client::builder() .timeout(Duration::from_secs(30)) .connect_timeout(Duration::from_secs(10)) .build() .map_err(|e| format!("Failed to build HTTP client: {}", e))?; Ok(Self { client, access_token: access_token.into(), session: Some(session), session_url: session_url.into(), }) } /// Gets or discovers the JMAP session. pub async fn session(&mut self) -> Result<&JmapSession, String> { if self.session.is_none() { let session = discover_session(&self.session_url, &self.access_token).await?; self.session = Some(session); } self.session .as_ref() .ok_or_else(|| "Failed to populate JMAP session".to_string()) } /// Forces a session refresh. pub async fn refresh_session(&mut self) -> Result<&JmapSession, String> { let session = discover_session(&self.session_url, &self.access_token).await?; self.session = Some(session); self.session .as_ref() .ok_or_else(|| "Failed to populate JMAP session".to_string()) } /// Gets the primary email account ID. pub async fn account_id(&mut self) -> Result { let session = self.session().await?; session .primary_email_account() .map(|s| s.to_string()) .ok_or_else(|| "No primary email account found".to_string()) } /// Gets the API URL for method calls. pub async fn api_url(&mut self) -> Result { let session = self.session().await?; Ok(session.api_url().to_string()) } /// Gets the username (email address) from the session. pub async fn username(&mut self) -> Result { let session = self.session().await?; Ok(session.username.clone()) } /// Executes a JMAP request and returns the response. pub async fn execute(&mut self, request: JmapRequest) -> Result { let api_url = self.api_url().await?; let response = self .client .post(&api_url) .bearer_auth(&self.access_token) .json(&request) .send() .await .map_err(|e| format!("JMAP request failed: {}", e))?; if !response.status().is_success() { let status = response.status(); let body = response.text().await.unwrap_or_default(); return Err(format!("JMAP request failed ({}): {}", status, body)); } let jmap_response: JmapResponse = response .json() .await .map_err(|e| format!("Failed to parse JMAP response: {}", e))?; // Check for method-level errors for method_response in &jmap_response.method_responses { if method_response.method == "error" { let error_type = method_response.data["type"].as_str().unwrap_or("unknown"); let description = method_response.data["description"] .as_str() .unwrap_or("Unknown error"); return Err(format!("JMAP error ({}): {}", error_type, description)); } } Ok(jmap_response) } /// Executes a single method call and returns its response. pub async fn call( &mut self, method: &str, args: serde_json::Value, ) -> Result { let mut request = JmapRequest::new(); request.add_call(method, args, "c0"); let response = self.execute(request).await?; response .method_responses .into_iter() .next() .ok_or_else(|| "No response from JMAP method".to_string()) } /// Updates the access token (after a token refresh). pub fn update_token(&mut self, access_token: impl Into) { self.access_token = access_token.into(); } }