gmail_mcp_server/
lib.rs

1//! Gmail API Client Library
2//! Simple library for fetching Gmail emails with OAuth2 authentication.
3
4pub mod reademail;
5
6use gmail1::hyper_rustls::HttpsConnectorBuilder;
7use gmail1::hyper_util::{client::legacy::Client, rt::TokioExecutor};
8use gmail1::{
9    api::{ListMessagesResponse, MessagePart},
10    Gmail,
11};
12use google_gmail1 as gmail1;
13use serde::{Deserialize, Serialize};
14use tracing::{error, info, warn};
15use yup_oauth2::{InstalledFlowAuthenticator, InstalledFlowReturnMethod};
16
17/// Lightweight representation of an email message that our API returns.
18#[derive(Serialize, Deserialize, Debug)]
19pub struct EmailSummary {
20    /// The unique Gmail message ID.
21    pub id: String,
22    /// The value of the `From` header.
23    pub from: String,
24    /// The value of the `Subject` header.
25    pub subject: String,
26    /// A short snippet of the message body.
27    pub snippet: String,
28    /// Raw body (HTML or plain text).
29    pub body_raw: String,
30}
31
32/// Response structure that wraps the email summaries
33#[derive(Serialize, Deserialize, Debug)]
34pub struct EmailResponse {
35    /// The list of email summaries
36    pub emails: Vec<EmailSummary>,
37    /// The total number of emails fetched
38    pub count: usize,
39}
40
41/// Extract the plain-text body from a `Message`. Falls back to empty string.
42fn bytes_to_string(data: &[u8]) -> Option<String> {
43    String::from_utf8(data.to_vec()).ok()
44}
45
46fn extract_body(msg: &gmail1::api::Message) -> String {
47    if let Some(payload) = &msg.payload {
48        // Try top-level body first
49        if let Some(body) = &payload.body {
50            if let Some(data) = &body.data {
51                if let Some(txt) = bytes_to_string(data) {
52                    return txt;
53                }
54            }
55        }
56
57        // Search parts for text/plain
58        if let Some(parts) = &payload.parts {
59            if let Some(txt) = find_plain_text(parts) {
60                return txt;
61            }
62        }
63    }
64    String::new()
65}
66
67/// Recursively traverse message parts to find the first `text/plain` body.
68fn find_plain_text(parts: &[MessagePart]) -> Option<String> {
69    for part in parts {
70        if part.mime_type.as_deref() == Some("text/plain") {
71            if let Some(body) = &part.body {
72                if let Some(data) = &body.data {
73                    if let Some(txt) = bytes_to_string(data) {
74                        return Some(txt);
75                    }
76                }
77            }
78        }
79
80        // Recurse into sub-parts
81        if let Some(sub_parts) = &part.parts {
82            if let Some(txt) = find_plain_text(sub_parts) {
83                return Some(txt);
84            }
85        }
86    }
87    None
88}
89
90/// Fetch Gmail emails using OAuth2 authentication
91pub async fn run(max_results: u32) -> Result<String, Box<dyn std::error::Error>> {
92    let max_results = max_results.clamp(1, 500);
93    info!("Gmail API: Starting to fetch {} emails", max_results);
94
95    // Load credentials
96    info!("Gmail API: Loading credentials from client_secret.json");
97    let secret = yup_oauth2::read_application_secret("client_secret.json")
98        .await
99        .map_err(|e| {
100            error!("Gmail API: Failed to read client_secret.json: {}", e);
101            e
102        })?;
103
104    // Set up authenticator
105    info!("Gmail API: Setting up OAuth2 authenticator");
106    let auth = InstalledFlowAuthenticator::builder(secret, InstalledFlowReturnMethod::HTTPRedirect)
107        .persist_tokens_to_disk("token_cache.json")
108        .build()
109        .await
110        .map_err(|e| {
111            error!("Gmail API: Failed to build authenticator: {}", e);
112            e
113        })?;
114
115    // Create HTTPS client
116    info!("Gmail API: Creating HTTPS client");
117    let https = HttpsConnectorBuilder::new()
118        .with_native_roots()?
119        .https_or_http()
120        .enable_http1()
121        .build();
122
123    let client = Client::builder(TokioExecutor::new()).build(https);
124    let hub = Gmail::new(client, auth);
125
126    // Fetch messages
127    info!("Gmail API: Requesting message list from inbox");
128    let result = hub
129        .users()
130        .messages_list("me")
131        .q("in:inbox")
132        .max_results(max_results)
133        .doit()
134        .await
135        .map_err(|e| {
136            error!("Gmail API: Failed to list messages: {}", e);
137            e
138        })?;
139
140    let mut summaries = Vec::new();
141
142    if let (
143        _,
144        ListMessagesResponse {
145            messages: Some(messages),
146            ..
147        },
148    ) = result
149    {
150        let message_count = messages.len();
151        info!(
152            "Gmail API: Found {} messages, fetching details",
153            message_count
154        );
155
156        for (i, message) in messages.into_iter().enumerate() {
157            if let Some(id) = message.id {
158                info!(
159                    "Gmail API: Fetching message {}/{}: {}",
160                    i + 1,
161                    message_count,
162                    id
163                );
164
165                match hub
166                    .users()
167                    .messages_get("me", &id)
168                    .format("full")
169                    .add_scope("https://www.googleapis.com/auth/gmail.readonly")
170                    .doit()
171                    .await
172                {
173                    Ok((_, msg)) => {
174                        if let Some(payload) = &msg.payload {
175                            if let Some(headers) = &payload.headers {
176                                let subject = headers
177                                    .iter()
178                                    .find(|h| h.name.as_deref() == Some("Subject"))
179                                    .and_then(|h| h.value.clone())
180                                    .unwrap_or_else(|| "No Subject".to_string());
181
182                                let from = headers
183                                    .iter()
184                                    .find(|h| h.name.as_deref() == Some("From"))
185                                    .and_then(|h| h.value.clone())
186                                    .unwrap_or_else(|| "Unknown Sender".to_string());
187
188                                let snippet = msg.snippet.clone().unwrap_or_default();
189                                let body_raw = extract_body(&msg);
190
191                                summaries.push(EmailSummary {
192                                    id: id.clone(),
193                                    from,
194                                    subject: subject.clone(),
195                                    snippet,
196                                    body_raw,
197                                });
198
199                                info!("Gmail API: Successfully processed email: {}", subject);
200                            } else {
201                                warn!("Gmail API: Message {} has no headers", id);
202                            }
203                        } else {
204                            warn!("Gmail API: Message {} has no payload", id);
205                        }
206                    }
207                    Err(e) => {
208                        error!("Gmail API: Failed to fetch message {}: {}", id, e);
209                        // Check if it's an authentication error
210                        if e.to_string().contains("403")
211                            || e.to_string().contains("PERMISSION_DENIED")
212                        {
213                            error!("Gmail API: This appears to be an authentication issue");
214                            warn!("Gmail API: Consider deleting token_cache.json and restarting");
215                        }
216                    }
217                }
218            } else {
219                warn!("Gmail API: Message has no ID");
220            }
221        }
222    } else {
223        warn!("Gmail API: No messages found in response");
224    }
225
226    let response = EmailResponse {
227        count: summaries.len(),
228        emails: summaries,
229    };
230
231    info!(
232        "Gmail API: Completed successfully, returning {} emails",
233        response.count
234    );
235    Ok(serde_json::to_string_pretty(&response)?)
236}