pub mod reademail;
use gmail1::hyper_rustls::HttpsConnectorBuilder;
use gmail1::hyper_util::{client::legacy::Client, rt::TokioExecutor};
use gmail1::{
api::{ListMessagesResponse, MessagePart},
Gmail,
};
use google_gmail1 as gmail1;
use serde::{Deserialize, Serialize};
use tracing::{error, info, warn};
use yup_oauth2::{InstalledFlowAuthenticator, InstalledFlowReturnMethod};
#[derive(Serialize, Deserialize, Debug)]
pub struct EmailSummary {
pub id: String,
pub from: String,
pub subject: String,
pub snippet: String,
pub body_raw: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct EmailResponse {
pub emails: Vec<EmailSummary>,
pub count: usize,
}
fn bytes_to_string(data: &[u8]) -> Option<String> {
String::from_utf8(data.to_vec()).ok()
}
fn extract_body(msg: &gmail1::api::Message) -> String {
if let Some(payload) = &msg.payload {
if let Some(body) = &payload.body {
if let Some(data) = &body.data {
if let Some(txt) = bytes_to_string(data) {
return txt;
}
}
}
if let Some(parts) = &payload.parts {
if let Some(txt) = find_plain_text(parts) {
return txt;
}
}
}
String::new()
}
fn find_plain_text(parts: &[MessagePart]) -> Option<String> {
for part in parts {
if part.mime_type.as_deref() == Some("text/plain") {
if let Some(body) = &part.body {
if let Some(data) = &body.data {
if let Some(txt) = bytes_to_string(data) {
return Some(txt);
}
}
}
}
if let Some(sub_parts) = &part.parts {
if let Some(txt) = find_plain_text(sub_parts) {
return Some(txt);
}
}
}
None
}
pub async fn run(max_results: u32) -> Result<String, Box<dyn std::error::Error>> {
let max_results = max_results.clamp(1, 500);
info!("Gmail API: Starting to fetch {} emails", max_results);
info!("Gmail API: Loading credentials from client_secret.json");
let secret = yup_oauth2::read_application_secret("client_secret.json")
.await
.map_err(|e| {
error!("Gmail API: Failed to read client_secret.json: {}", e);
e
})?;
info!("Gmail API: Setting up OAuth2 authenticator");
let auth = InstalledFlowAuthenticator::builder(secret, InstalledFlowReturnMethod::HTTPRedirect)
.persist_tokens_to_disk("token_cache.json")
.build()
.await
.map_err(|e| {
error!("Gmail API: Failed to build authenticator: {}", e);
e
})?;
info!("Gmail API: Creating HTTPS client");
let https = HttpsConnectorBuilder::new()
.with_native_roots()?
.https_or_http()
.enable_http1()
.build();
let client = Client::builder(TokioExecutor::new()).build(https);
let hub = Gmail::new(client, auth);
info!("Gmail API: Requesting message list from inbox");
let result = hub
.users()
.messages_list("me")
.q("in:inbox")
.max_results(max_results)
.doit()
.await
.map_err(|e| {
error!("Gmail API: Failed to list messages: {}", e);
e
})?;
let mut summaries = Vec::new();
if let (
_,
ListMessagesResponse {
messages: Some(messages),
..
},
) = result
{
let message_count = messages.len();
info!(
"Gmail API: Found {} messages, fetching details",
message_count
);
for (i, message) in messages.into_iter().enumerate() {
if let Some(id) = message.id {
info!(
"Gmail API: Fetching message {}/{}: {}",
i + 1,
message_count,
id
);
match hub
.users()
.messages_get("me", &id)
.format("full")
.add_scope("https://www.googleapis.com/auth/gmail.readonly")
.doit()
.await
{
Ok((_, msg)) => {
if let Some(payload) = &msg.payload {
if let Some(headers) = &payload.headers {
let subject = headers
.iter()
.find(|h| h.name.as_deref() == Some("Subject"))
.and_then(|h| h.value.clone())
.unwrap_or_else(|| "No Subject".to_string());
let from = headers
.iter()
.find(|h| h.name.as_deref() == Some("From"))
.and_then(|h| h.value.clone())
.unwrap_or_else(|| "Unknown Sender".to_string());
let snippet = msg.snippet.clone().unwrap_or_default();
let body_raw = extract_body(&msg);
summaries.push(EmailSummary {
id: id.clone(),
from,
subject: subject.clone(),
snippet,
body_raw,
});
info!("Gmail API: Successfully processed email: {}", subject);
} else {
warn!("Gmail API: Message {} has no headers", id);
}
} else {
warn!("Gmail API: Message {} has no payload", id);
}
}
Err(e) => {
error!("Gmail API: Failed to fetch message {}: {}", id, e);
if e.to_string().contains("403")
|| e.to_string().contains("PERMISSION_DENIED")
{
error!("Gmail API: This appears to be an authentication issue");
warn!("Gmail API: Consider deleting token_cache.json and restarting");
}
}
}
} else {
warn!("Gmail API: Message has no ID");
}
}
} else {
warn!("Gmail API: No messages found in response");
}
let response = EmailResponse {
count: summaries.len(),
emails: summaries,
};
info!(
"Gmail API: Completed successfully, returning {} emails",
response.count
);
Ok(serde_json::to_string_pretty(&response)?)
}