Skip to main content

max / goingson

5.4 KB · 160 lines History Blame Raw
1 //! JMAP HTTP client for making method calls.
2
3 use super::session::{discover_session, JmapSession};
4 use super::types::{JmapRequest, JmapResponse, MethodResponse};
5 use std::time::Duration;
6
7 /// JMAP client for making API calls.
8 pub struct JmapClient {
9 /// HTTP client
10 client: reqwest::Client,
11 /// Access token
12 access_token: String,
13 /// Cached session
14 session: Option<JmapSession>,
15 /// Session discovery URL
16 session_url: String,
17 }
18
19 impl JmapClient {
20 /// Creates a new JMAP client.
21 ///
22 /// # Arguments
23 /// * `session_url` - The session discovery URL (e.g., https://api.fastmail.com/jmap/session)
24 /// * `access_token` - OAuth2 access token
25 pub fn new(session_url: impl Into<String>, access_token: impl Into<String>) -> Result<Self, String> {
26 let client = reqwest::Client::builder()
27 .timeout(Duration::from_secs(30))
28 .connect_timeout(Duration::from_secs(10))
29 .build()
30 .map_err(|e| format!("Failed to build HTTP client: {}", e))?;
31 Ok(Self {
32 client,
33 access_token: access_token.into(),
34 session: None,
35 session_url: session_url.into(),
36 })
37 }
38
39 /// Creates a client with a pre-fetched session.
40 pub fn with_session(
41 session: JmapSession,
42 access_token: impl Into<String>,
43 session_url: impl Into<String>,
44 ) -> Result<Self, String> {
45 let client = reqwest::Client::builder()
46 .timeout(Duration::from_secs(30))
47 .connect_timeout(Duration::from_secs(10))
48 .build()
49 .map_err(|e| format!("Failed to build HTTP client: {}", e))?;
50 Ok(Self {
51 client,
52 access_token: access_token.into(),
53 session: Some(session),
54 session_url: session_url.into(),
55 })
56 }
57
58 /// Gets or discovers the JMAP session.
59 pub async fn session(&mut self) -> Result<&JmapSession, String> {
60 if self.session.is_none() {
61 let session = discover_session(&self.session_url, &self.access_token).await?;
62 self.session = Some(session);
63 }
64 self.session
65 .as_ref()
66 .ok_or_else(|| "Failed to populate JMAP session".to_string())
67 }
68
69 /// Forces a session refresh.
70 pub async fn refresh_session(&mut self) -> Result<&JmapSession, String> {
71 let session = discover_session(&self.session_url, &self.access_token).await?;
72 self.session = Some(session);
73 self.session
74 .as_ref()
75 .ok_or_else(|| "Failed to populate JMAP session".to_string())
76 }
77
78 /// Gets the primary email account ID.
79 pub async fn account_id(&mut self) -> Result<String, String> {
80 let session = self.session().await?;
81 session
82 .primary_email_account()
83 .map(|s| s.to_string())
84 .ok_or_else(|| "No primary email account found".to_string())
85 }
86
87 /// Gets the API URL for method calls.
88 pub async fn api_url(&mut self) -> Result<String, String> {
89 let session = self.session().await?;
90 Ok(session.api_url().to_string())
91 }
92
93 /// Gets the username (email address) from the session.
94 pub async fn username(&mut self) -> Result<String, String> {
95 let session = self.session().await?;
96 Ok(session.username.clone())
97 }
98
99 /// Executes a JMAP request and returns the response.
100 pub async fn execute(&mut self, request: JmapRequest) -> Result<JmapResponse, String> {
101 let api_url = self.api_url().await?;
102
103 let response = self
104 .client
105 .post(&api_url)
106 .bearer_auth(&self.access_token)
107 .json(&request)
108 .send()
109 .await
110 .map_err(|e| format!("JMAP request failed: {}", e))?;
111
112 if !response.status().is_success() {
113 let status = response.status();
114 let body = response.text().await.unwrap_or_default();
115 return Err(format!("JMAP request failed ({}): {}", status, body));
116 }
117
118 let jmap_response: JmapResponse = response
119 .json()
120 .await
121 .map_err(|e| format!("Failed to parse JMAP response: {}", e))?;
122
123 // Check for method-level errors
124 for method_response in &jmap_response.method_responses {
125 if method_response.method == "error" {
126 let error_type = method_response.data["type"].as_str().unwrap_or("unknown");
127 let description = method_response.data["description"]
128 .as_str()
129 .unwrap_or("Unknown error");
130 return Err(format!("JMAP error ({}): {}", error_type, description));
131 }
132 }
133
134 Ok(jmap_response)
135 }
136
137 /// Executes a single method call and returns its response.
138 pub async fn call(
139 &mut self,
140 method: &str,
141 args: serde_json::Value,
142 ) -> Result<MethodResponse, String> {
143 let mut request = JmapRequest::new();
144 request.add_call(method, args, "c0");
145
146 let response = self.execute(request).await?;
147
148 response
149 .method_responses
150 .into_iter()
151 .next()
152 .ok_or_else(|| "No response from JMAP method".to_string())
153 }
154
155 /// Updates the access token (after a token refresh).
156 pub fn update_token(&mut self, access_token: impl Into<String>) {
157 self.access_token = access_token.into();
158 }
159 }
160