Skip to main content

mcp_gmailcal/
gmail_api.rs

1use crate::auth::TokenManager;
2use crate::config::Config;
3use crate::config::GMAIL_API_BASE_URL;
4use crate::errors::{GmailApiError, GmailResult};
5use log::{debug, error, info};
6use reqwest::Client;
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9use std::time::Duration;
10
11// Email message model
12#[derive(Serialize, Deserialize, Debug, Clone)]
13pub struct EmailMessage {
14    pub id: String,
15    pub thread_id: String,
16    pub subject: Option<String>,
17    pub from: Option<String>,
18    pub to: Option<String>,
19    pub date: Option<String>,
20    pub snippet: Option<String>,
21    pub body_text: Option<String>,
22    pub body_html: Option<String>,
23}
24
25// Draft email model for creating new emails
26#[derive(Serialize, Deserialize, Debug, Clone)]
27pub struct DraftEmail {
28    pub to: String,
29    pub subject: String,
30    pub body: String,
31    pub cc: Option<String>,
32    pub bcc: Option<String>,
33    pub thread_id: Option<String>,
34    pub in_reply_to: Option<String>,
35    pub references: Option<String>,
36}
37
38// Alias for backward compatibility within this module
39type Result<T> = GmailResult<T>;
40
41pub struct GmailService {
42    client: Client,
43    token_manager: TokenManager,
44}
45
46impl GmailService {
47    pub fn new(config: &Config) -> Result<Self> {
48        debug!("Creating new GmailService with config");
49
50        // Create HTTP client with reasonable timeouts
51        debug!("Creating HTTP client with timeouts");
52        let client = Client::builder()
53            .timeout(Duration::from_secs(60)) // Longer timeout for Gmail API
54            .connect_timeout(Duration::from_secs(30))
55            .pool_idle_timeout(Duration::from_secs(90))
56            .pool_max_idle_per_host(5)
57            .user_agent("mcp-gmailcal/0.1.0")
58            .build()
59            .map_err(|e| {
60                error!("Failed to create HTTP client: {}", e);
61                GmailApiError::NetworkError(format!("Failed to create HTTP client: {}", e))
62            })?;
63
64        debug!("HTTP client created successfully");
65
66        let token_manager = TokenManager::new(config);
67
68        Ok(Self {
69            client,
70            token_manager,
71        })
72    }
73
74    // Helper function to make authenticated requests to Gmail API
75    async fn request<T: for<'de> Deserialize<'de>>(
76        &mut self,
77        method: reqwest::Method,
78        endpoint: &str,
79        query: Option<&[(&str, &str)]>,
80    ) -> Result<T> {
81        // Get valid access token
82        let token = self.token_manager.get_token(&self.client).await?;
83
84        let url = format!("{}{}", GMAIL_API_BASE_URL, endpoint);
85        debug!("Making request to: {}", url);
86
87        // Build request with authorization header
88        debug!("Making authenticated request to {}", url);
89        let mut req_builder = self
90            .client
91            .request(method, &url)
92            .header("Authorization", format!("Bearer {}", token))
93            .header("Accept", "application/json")
94            .header("User-Agent", "mcp-gmailcal/0.1.0");
95
96        // Add query parameters if provided
97        if let Some(q) = query {
98            req_builder = req_builder.query(q);
99        }
100
101        // Send request
102        debug!("Sending request to Gmail API");
103        let response = req_builder.send().await.map_err(|e| {
104            error!("Network error sending request: {}", e);
105            GmailApiError::NetworkError(e.to_string())
106        })?;
107
108        debug!("Response received with status: {}", response.status());
109
110        // Handle response status
111        let status = response.status();
112        if !status.is_success() {
113            let status_code = status.as_u16();
114            let error_text = response
115                .text()
116                .await
117                .unwrap_or_else(|_| "<no response body>".to_string());
118
119            // Map common error codes to appropriate error types
120            return match status_code {
121                401 | 403 => Err(GmailApiError::AuthError(format!(
122                    "Authentication failed. Status: {}, Error: {}",
123                    status, error_text
124                ))),
125                404 => Err(GmailApiError::MessageRetrievalError(format!(
126                    "Resource not found. Status: {}, Error: {}",
127                    status, error_text
128                ))),
129                429 => Err(GmailApiError::RateLimitError(format!(
130                    "Rate limit exceeded. Status: {}, Error: {}",
131                    status, error_text
132                ))),
133                _ => Err(GmailApiError::ApiError(format!(
134                    "API request failed. Status: {}, Error: {}",
135                    status, error_text
136                ))),
137            };
138        }
139
140        // Parse JSON response
141        response.json::<T>().await.map_err(|e| {
142            GmailApiError::MessageFormatError(format!("Failed to parse response: {}", e))
143        })
144    }
145
146    // Helper function to make a request and return the raw JSON response
147    async fn request_raw(
148        &mut self,
149        method: reqwest::Method,
150        endpoint: &str,
151        query: Option<&[(&str, &str)]>,
152    ) -> Result<String> {
153        // Get valid access token
154        let token = self.token_manager.get_token(&self.client).await?;
155
156        let url = format!("{}{}", GMAIL_API_BASE_URL, endpoint);
157        debug!("Making raw request to: {}", url);
158
159        // Build request with authorization header
160        debug!("Making raw authenticated request to {}", url);
161        let mut req_builder = self
162            .client
163            .request(method, &url)
164            .header("Authorization", format!("Bearer {}", token))
165            .header("Accept", "application/json")
166            .header("User-Agent", "mcp-gmailcal/0.1.0");
167
168        // Add query parameters if provided
169        if let Some(q) = query {
170            req_builder = req_builder.query(q);
171        }
172
173        // Send request
174        debug!("Sending raw request to Gmail API");
175        let response = req_builder.send().await.map_err(|e| {
176            error!("Network error sending raw request: {}", e);
177            GmailApiError::NetworkError(e.to_string())
178        })?;
179
180        debug!("Raw response received with status: {}", response.status());
181
182        // Handle response status
183        let status = response.status();
184        if !status.is_success() {
185            let status_code = status.as_u16();
186            let error_text = response
187                .text()
188                .await
189                .unwrap_or_else(|_| "<no response body>".to_string());
190
191            // Map common error codes to appropriate error types
192            return match status_code {
193                401 | 403 => Err(GmailApiError::AuthError(format!(
194                    "Authentication failed. Status: {}, Error: {}",
195                    status, error_text
196                ))),
197                404 => Err(GmailApiError::MessageRetrievalError(format!(
198                    "Resource not found. Status: {}, Error: {}",
199                    status, error_text
200                ))),
201                429 => Err(GmailApiError::RateLimitError(format!(
202                    "Rate limit exceeded. Status: {}, Error: {}",
203                    status, error_text
204                ))),
205                _ => Err(GmailApiError::ApiError(format!(
206                    "API request failed. Status: {}, Error: {}",
207                    status, error_text
208                ))),
209            };
210        }
211
212        // Get raw JSON as string
213        debug!("Reading response body");
214        let json_text = response.text().await.map_err(|e| {
215            error!("Failed to get response body: {}", e);
216            GmailApiError::NetworkError(format!("Failed to get response body: {}", e))
217        })?;
218
219        // Log a preview of the response
220        let preview = if json_text.len() > 200 {
221            format!(
222                "{}... (truncated, total size: {} bytes)",
223                &json_text[..200],
224                json_text.len()
225            )
226        } else {
227            json_text.clone()
228        };
229        debug!("Raw response body: {}", preview);
230
231        // Format JSON for pretty printing
232        match serde_json::from_str::<Value>(&json_text) {
233            Ok(value) => {
234                debug!("Successfully parsed response as JSON");
235                serde_json::to_string_pretty(&value).map_err(|e| {
236                    error!("Failed to format JSON: {}", e);
237                    GmailApiError::MessageFormatError(format!("Failed to format JSON: {}", e))
238                })
239            }
240            Err(e) => {
241                error!("Failed to parse response as JSON: {}", e);
242                debug!("Returning raw response text");
243                Ok(json_text) // Return as-is if not valid JSON
244            }
245        }
246    }
247
248    /// Get a message by ID and return as raw JSON
249    pub async fn get_message_raw(&mut self, message_id: &str) -> Result<String> {
250        debug!("Getting raw message with ID: {}", message_id);
251
252        // Log request details
253        let request_details = format!(
254            "Request details: User ID: 'me', Message ID: '{}', Format: 'full'",
255            message_id
256        );
257        info!("{}", request_details);
258
259        // Build query params for full message format
260        let query = [("format", "full")];
261
262        // Execute request
263        let endpoint = format!("/users/me/messages/{}", message_id);
264        self.request_raw(reqwest::Method::GET, &endpoint, Some(&query))
265            .await
266    }
267
268    /// List messages and return raw JSON response
269    pub async fn list_messages_raw(
270        &mut self,
271        max_results: u32,
272        query: Option<&str>,
273    ) -> Result<String> {
274        debug!(
275            "Listing raw messages with max_results={}, query={:?}",
276            max_results, query
277        );
278
279        // Create string representation of max_results
280        let max_results_str = max_results.to_string();
281
282        // Execute request
283        let endpoint = "/users/me/messages";
284
285        // Handle query parameter differently to avoid lifetime issues
286        if let Some(q) = query {
287            // Use separate array for each case
288            let params = [("maxResults", max_results_str.as_str()), ("q", q)];
289            self.request_raw(reqwest::Method::GET, endpoint, Some(&params))
290                .await
291        } else {
292            let params = [("maxResults", max_results_str.as_str())];
293            self.request_raw(reqwest::Method::GET, endpoint, Some(&params))
294                .await
295        }
296    }
297
298    /// Get message details with all metadata and content
299    pub async fn get_message_details(&mut self, message_id: &str) -> Result<EmailMessage> {
300        use base64;
301
302        // First get the full message
303        let message_json = self.get_message_raw(message_id).await?;
304
305        // Parse the JSON
306        let parsed: serde_json::Value = serde_json::from_str(&message_json).map_err(|e| {
307            GmailApiError::MessageFormatError(format!("Failed to parse message JSON: {}", e))
308        })?;
309
310        // Extract the basic message data
311        let id = parsed["id"]
312            .as_str()
313            .ok_or_else(|| {
314                GmailApiError::MessageFormatError("Message missing 'id' field".to_string())
315            })?
316            .to_string();
317
318        let thread_id = parsed["threadId"]
319            .as_str()
320            .ok_or_else(|| {
321                GmailApiError::MessageFormatError("Message missing 'threadId' field".to_string())
322            })?
323            .to_string();
324
325        // Extract metadata
326        let mut subject = None;
327        let mut from = None;
328        let mut to = None;
329        let mut date = None;
330        let mut snippet = None;
331        let mut body_text = None;
332        let mut body_html = None;
333
334        // Extract snippet if available
335        if let Some(s) = parsed.get("snippet").and_then(|s| s.as_str()) {
336            snippet = Some(s.to_string());
337        }
338
339        // Process payload to extract headers and body parts
340        if let Some(payload) = parsed.get("payload") {
341            // Extract headers
342            if let Some(headers) = payload.get("headers").and_then(|h| h.as_array()) {
343                for header in headers {
344                    if let (Some(name), Some(value)) = (
345                        header.get("name").and_then(|n| n.as_str()),
346                        header.get("value").and_then(|v| v.as_str()),
347                    ) {
348                        match name {
349                            "Subject" => subject = Some(value.to_string()),
350                            "From" => from = Some(value.to_string()),
351                            "To" => to = Some(value.to_string()),
352                            "Date" => date = Some(value.to_string()),
353                            _ => {}
354                        }
355                    }
356                }
357            }
358
359            // Extract message body parts
360            if let Some(parts) = payload.get("parts").and_then(|p| p.as_array()) {
361                // Process each part
362                for part in parts {
363                    if let Some(mime_type) = part.get("mimeType").and_then(|m| m.as_str()) {
364                        // Handle text parts
365                        if mime_type == "text/plain" || mime_type == "text/html" {
366                            if let Some(body) = part.get("body") {
367                                if let Some(data) = body.get("data").and_then(|d| d.as_str()) {
368                                    // Decode base64
369                                    if let Ok(decoded) =
370                                        base64::decode(data.replace('-', "+").replace('_', "/"))
371                                    {
372                                        if let Ok(text) = String::from_utf8(decoded) {
373                                            match mime_type {
374                                                "text/plain" => body_text = Some(text),
375                                                "text/html" => body_html = Some(text),
376                                                _ => {}
377                                            }
378                                        }
379                                    }
380                                }
381                            }
382                        }
383                    }
384                }
385            }
386
387            // Check for body directly in payload (for simple messages)
388            if body_text.is_none() && body_html.is_none() {
389                if let Some(body) = payload.get("body") {
390                    if let Some(data) = body.get("data").and_then(|d| d.as_str()) {
391                        // Decode base64
392                        if let Ok(decoded) =
393                            base64::decode(data.replace('-', "+").replace('_', "/"))
394                        {
395                            if let Ok(text) = String::from_utf8(decoded) {
396                                if let Some(mime_type) =
397                                    payload.get("mimeType").and_then(|m| m.as_str())
398                                {
399                                    match mime_type {
400                                        "text/plain" => body_text = Some(text),
401                                        "text/html" => body_html = Some(text),
402                                        // Default to text if we can't determine
403                                        _ => body_text = Some(text),
404                                    }
405                                } else {
406                                    body_text = Some(text);
407                                }
408                            }
409                        }
410                    }
411                }
412            }
413        }
414
415        // Create the EmailMessage
416        Ok(EmailMessage {
417            id,
418            thread_id,
419            subject,
420            from,
421            to,
422            date,
423            snippet,
424            body_text,
425            body_html,
426        })
427    }
428
429    /// List messages and parse metadata into structured EmailMessage objects
430    pub async fn list_messages(
431        &mut self,
432        max_results: u32,
433        query: Option<&str>,
434    ) -> Result<Vec<EmailMessage>> {
435        // First get the list of message IDs
436        let raw_json = self.list_messages_raw(max_results, query).await?;
437
438        // Parse the raw JSON
439        let parsed: serde_json::Value = serde_json::from_str(&raw_json).map_err(|e| {
440            GmailApiError::MessageFormatError(format!("Failed to parse message list: {}", e))
441        })?;
442
443        // Extract messages array
444        let messages = parsed["messages"].as_array().ok_or_else(|| {
445            GmailApiError::MessageFormatError("Missing 'messages' array in response".to_string())
446        })?;
447
448        // Create EmailMessage structs by fetching details for each message ID
449        let mut result = Vec::new();
450
451        for message in messages {
452            let id = message["id"].as_str().ok_or_else(|| {
453                GmailApiError::MessageFormatError("Message missing 'id' field".to_string())
454            })?;
455
456            // Get full message details
457            match self.get_message_details(id).await {
458                Ok(email) => {
459                    result.push(email);
460                }
461                Err(e) => {
462                    // Log error but continue with other messages
463                    error!("Failed to get details for message {}: {}", id, e);
464                }
465            }
466
467            // Limit to 3 messages to avoid timeout during development
468            if result.len() >= 3 {
469                debug!("Reached limit of 3 messages, stopping fetch to avoid timeout");
470                break;
471            }
472        }
473
474        Ok(result)
475    }
476
477    /// List labels and return raw JSON response
478    pub async fn list_labels(&mut self) -> Result<String> {
479        debug!("Listing labels");
480
481        let endpoint = "/users/me/labels";
482        self.request_raw(reqwest::Method::GET, endpoint, None).await
483    }
484
485    /// Check connection by getting profile and return raw JSON response
486    pub async fn check_connection_raw(&mut self) -> Result<String> {
487        debug!("Checking connection raw");
488
489        let endpoint = "/users/me/profile";
490        self.request_raw(reqwest::Method::GET, endpoint, None).await
491    }
492
493    /// Check connection by getting profile and return email and message count
494    pub async fn check_connection(&mut self) -> Result<(String, u64)> {
495        debug!("Checking connection");
496
497        let endpoint = "/users/me/profile";
498
499        #[derive(Deserialize)]
500        struct Profile {
501            #[serde(rename = "emailAddress")]
502            email_address: String,
503            #[serde(rename = "messagesTotal")]
504            messages_total: Option<u64>,
505        }
506
507        let profile: Profile = self.request(reqwest::Method::GET, endpoint, None).await?;
508
509        let email = profile.email_address;
510        let messages_total = profile.messages_total.unwrap_or(0);
511
512        Ok((email, messages_total))
513    }
514
515    /// Create a draft email in Gmail
516    pub async fn create_draft(&mut self, draft: &DraftEmail) -> Result<String> {
517        debug!("Creating draft email to: {}", draft.to);
518
519        // Construct the RFC 5322 formatted message
520        let mut message = format!(
521            "From: me\r\n\
522             To: {}\r\n\
523             Subject: {}\r\n",
524            draft.to, draft.subject
525        );
526
527        // Add optional CC and BCC fields
528        if let Some(cc) = &draft.cc {
529            message.push_str(&format!("Cc: {}\r\n", cc));
530        }
531
532        if let Some(bcc) = &draft.bcc {
533            message.push_str(&format!("Bcc: {}\r\n", bcc));
534        }
535
536        // Add threading headers for replies
537        if let Some(in_reply_to) = &draft.in_reply_to {
538            message.push_str(&format!("In-Reply-To: {}\r\n", in_reply_to));
539        }
540
541        if let Some(references) = &draft.references {
542            message.push_str(&format!("References: {}\r\n", references));
543        }
544
545        // Add body
546        message.push_str("\r\n");
547        message.push_str(&draft.body);
548
549        // Base64 encode the message
550        // Encode the message as base64url format for Gmail API
551        // Note: For large messages with attachments or nested MIME structures,
552        // we would need enhanced handling to process them in chunks or parts
553        // This handles the basic email case - more complex handling would be needed for
554        // large attachments, nested content, or multipart messages exceeding size limits
555        let encoded_message = base64::encode(message.as_bytes())
556            .replace('+', "-")
557            .replace('/', "_");
558
559        // Log the message size for debugging large messages
560        debug!("Encoded message size: {} bytes", encoded_message.len());
561
562        // Create the JSON payload
563        let mut message_payload = serde_json::json!({
564            "raw": encoded_message
565        });
566
567        // Add thread_id if specified
568        if let Some(thread_id) = &draft.thread_id {
569            message_payload = serde_json::json!({
570                "raw": encoded_message,
571                "threadId": thread_id
572            });
573        }
574
575        let payload = serde_json::json!({
576            "message": message_payload
577        });
578
579        // Make the request to create a draft
580        let endpoint = "/users/me/drafts";
581
582        // Get valid access token
583        let token = self.token_manager.get_token(&self.client).await?;
584
585        let url = format!("{}{}", GMAIL_API_BASE_URL, endpoint);
586        debug!("Creating draft at: {}", url);
587
588        // Send the request
589        let response = self
590            .client
591            .post(&url)
592            .header("Authorization", format!("Bearer {}", token))
593            .header("Content-Type", "application/json")
594            .json(&payload)
595            .send()
596            .await
597            .map_err(|e| {
598                error!("Network error creating draft: {}", e);
599                GmailApiError::NetworkError(e.to_string())
600            })?;
601
602        // Handle response
603        let status = response.status();
604        debug!("Draft creation response status: {}", status);
605
606        if !status.is_success() {
607            let error_text = response
608                .text()
609                .await
610                .unwrap_or_else(|_| "<no response body>".to_string());
611
612            error!("Failed to create draft: {}", error_text);
613            return Err(GmailApiError::ApiError(format!(
614                "Failed to create draft. Status: {}, Error: {}",
615                status, error_text
616            )));
617        }
618
619        // Parse the response to get the draft ID
620        let response_text = response.text().await.map_err(|e| {
621            error!("Failed to get response body: {}", e);
622            GmailApiError::NetworkError(format!("Failed to get response body: {}", e))
623        })?;
624
625        // Parse the JSON response
626        let response_json: serde_json::Value =
627            serde_json::from_str(&response_text).map_err(|e| {
628                error!("Failed to parse draft response: {}", e);
629                GmailApiError::MessageFormatError(format!("Failed to parse draft response: {}", e))
630            })?;
631
632        // Extract the draft ID
633        let draft_id = response_json["id"]
634            .as_str()
635            .ok_or_else(|| {
636                GmailApiError::MessageFormatError("Draft response missing 'id' field".to_string())
637            })?
638            .to_string();
639
640        debug!("Draft created successfully with ID: {}", draft_id);
641
642        Ok(draft_id)
643    }
644}