1pub 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#[derive(Serialize, Deserialize, Debug)]
19pub struct EmailSummary {
20 pub id: String,
22 pub from: String,
24 pub subject: String,
26 pub snippet: String,
28 pub body_raw: String,
30}
31
32#[derive(Serialize, Deserialize, Debug)]
34pub struct EmailResponse {
35 pub emails: Vec<EmailSummary>,
37 pub count: usize,
39}
40
41fn 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 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 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
67fn 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 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
90pub 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 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 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 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 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 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}