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#[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#[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
38type 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 debug!("Creating HTTP client with timeouts");
52 let client = Client::builder()
53 .timeout(Duration::from_secs(60)) .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 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 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 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 if let Some(q) = query {
98 req_builder = req_builder.query(q);
99 }
100
101 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 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 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 response.json::<T>().await.map_err(|e| {
142 GmailApiError::MessageFormatError(format!("Failed to parse response: {}", e))
143 })
144 }
145
146 async fn request_raw(
148 &mut self,
149 method: reqwest::Method,
150 endpoint: &str,
151 query: Option<&[(&str, &str)]>,
152 ) -> Result<String> {
153 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 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 if let Some(q) = query {
170 req_builder = req_builder.query(q);
171 }
172
173 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 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 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 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 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 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) }
245 }
246 }
247
248 pub async fn get_message_raw(&mut self, message_id: &str) -> Result<String> {
250 debug!("Getting raw message with ID: {}", message_id);
251
252 let request_details = format!(
254 "Request details: User ID: 'me', Message ID: '{}', Format: 'full'",
255 message_id
256 );
257 info!("{}", request_details);
258
259 let query = [("format", "full")];
261
262 let endpoint = format!("/users/me/messages/{}", message_id);
264 self.request_raw(reqwest::Method::GET, &endpoint, Some(&query))
265 .await
266 }
267
268 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 let max_results_str = max_results.to_string();
281
282 let endpoint = "/users/me/messages";
284
285 if let Some(q) = query {
287 let params = [("maxResults", max_results_str.as_str()), ("q", q)];
289 self.request_raw(reqwest::Method::GET, endpoint, Some(¶ms))
290 .await
291 } else {
292 let params = [("maxResults", max_results_str.as_str())];
293 self.request_raw(reqwest::Method::GET, endpoint, Some(¶ms))
294 .await
295 }
296 }
297
298 pub async fn get_message_details(&mut self, message_id: &str) -> Result<EmailMessage> {
300 use base64;
301
302 let message_json = self.get_message_raw(message_id).await?;
304
305 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 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 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 if let Some(s) = parsed.get("snippet").and_then(|s| s.as_str()) {
336 snippet = Some(s.to_string());
337 }
338
339 if let Some(payload) = parsed.get("payload") {
341 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 if let Some(parts) = payload.get("parts").and_then(|p| p.as_array()) {
361 for part in parts {
363 if let Some(mime_type) = part.get("mimeType").and_then(|m| m.as_str()) {
364 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 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 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 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 _ => body_text = Some(text),
404 }
405 } else {
406 body_text = Some(text);
407 }
408 }
409 }
410 }
411 }
412 }
413 }
414
415 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 pub async fn list_messages(
431 &mut self,
432 max_results: u32,
433 query: Option<&str>,
434 ) -> Result<Vec<EmailMessage>> {
435 let raw_json = self.list_messages_raw(max_results, query).await?;
437
438 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 let messages = parsed["messages"].as_array().ok_or_else(|| {
445 GmailApiError::MessageFormatError("Missing 'messages' array in response".to_string())
446 })?;
447
448 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 match self.get_message_details(id).await {
458 Ok(email) => {
459 result.push(email);
460 }
461 Err(e) => {
462 error!("Failed to get details for message {}: {}", id, e);
464 }
465 }
466
467 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 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 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 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 pub async fn create_draft(&mut self, draft: &DraftEmail) -> Result<String> {
517 debug!("Creating draft email to: {}", draft.to);
518
519 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 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 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 message.push_str("\r\n");
547 message.push_str(&draft.body);
548
549 let encoded_message = base64::encode(message.as_bytes())
556 .replace('+', "-")
557 .replace('/', "_");
558
559 debug!("Encoded message size: {} bytes", encoded_message.len());
561
562 let mut message_payload = serde_json::json!({
564 "raw": encoded_message
565 });
566
567 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 let endpoint = "/users/me/drafts";
581
582 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 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 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 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 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 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}