use tracing::info;
use url::Url;
use crate::core::auth::{DeviceAuthChallenge, GoogleAuth, TokenStorage};
use crate::core::error::{GrrError, Result};
use crate::core::http::{HttpCore, QueryParams, TransportInfo, join_url, parse_url};
use crate::core::runtime::detect_runtime_features;
use crate::core::{Page, paginate};
use crate::gmail::models::*;
const DEFAULT_BASE_URL: &str = "https://gmail.googleapis.com/gmail/v1/";
const DEFAULT_UPLOAD_BASE_URL: &str = "https://gmail.googleapis.com/upload/gmail/v1/";
pub struct GmailClientBuilder {
auth: Option<GoogleAuth>,
base_url: Option<Url>,
upload_base_url: Option<Url>,
}
impl GmailClientBuilder {
pub fn new() -> Self {
Self {
auth: None,
base_url: None,
upload_base_url: None,
}
}
pub fn auth(mut self, auth: GoogleAuth) -> Self {
self.auth = Some(auth);
self
}
pub fn base_url(mut self, url: Url) -> Self {
self.base_url = Some(url);
self
}
pub fn upload_base_url(mut self, url: Url) -> Self {
self.upload_base_url = Some(url);
self
}
pub async fn build(self) -> Result<GmailClient> {
let auth = self
.auth
.ok_or_else(|| GrrError::Config("Auth is required".into()))?;
let has_base_override = self.base_url.is_some();
let base_url = match self.base_url {
Some(url) => url,
None => parse_url(DEFAULT_BASE_URL, "base")?,
};
let upload_base_url = match self.upload_base_url {
Some(url) => url,
None => parse_url(DEFAULT_UPLOAD_BASE_URL, "upload base")?,
};
let core = if has_base_override {
HttpCore::unprobed(auth, crate::core::http::build_http_client()?)
} else {
HttpCore::connect(auth, &base_url, "users/me/profile").await?
};
GmailClient::new(core, base_url, upload_base_url).await
}
}
impl Default for GmailClientBuilder {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone)]
pub struct GmailClient {
core: HttpCore,
base_url: Url,
upload_base_url: Url,
}
impl GmailClient {
pub async fn new(core: HttpCore, base_url: Url, upload_base_url: Url) -> Result<Self> {
let features = detect_runtime_features().await;
info!(
"GmailClient initialized: http3=always, io_uring={}",
features.io_uring
);
Ok(Self {
core,
base_url,
upload_base_url,
})
}
pub fn core(&self) -> &HttpCore {
&self.core
}
pub fn transport_info(&self) -> &TransportInfo {
self.core.transport_info()
}
pub fn token_backend(&self) -> &'static str {
self.core.auth().token_backend()
}
pub async fn login(&self) -> Result<TokenStorage> {
self.core.auth().login().await
}
pub async fn request_device_code(&self) -> Result<DeviceAuthChallenge> {
self.core.auth().request_device_code().await
}
pub async fn poll_device_code(
&self,
challenge: &mut DeviceAuthChallenge,
) -> Result<Option<TokenStorage>> {
self.core.auth().poll_device_code(challenge).await
}
fn api_url(&self, path: &str) -> Result<Url> {
join_url(&self.base_url, path, "API")
}
pub async fn search(&self, query: &str, max_results: usize) -> Result<Vec<MessageRef>> {
let batch_size = max_results.min(500);
paginate(Some(max_results), move |page_token| async move {
let params = QueryParams::new()
.add("q", query)
.add("maxResults", batch_size.to_string())
.add_page_token(page_token.as_deref());
let page: SearchResponse = self
.core
.execute_json(params.apply(self.core.get(self.api_url("users/me/messages")?)))
.await?;
Ok(Page::new(page.messages, page.next_page_token))
})
.await
}
}
mod drafts;
mod history;
mod labels;
mod messages;
mod settings;
mod streaming;
mod threads;
mod watch;
pub use streaming::*;
fn build_email(
to: &str,
subject: &str,
body: &str,
cc: Option<&str>,
bcc: Option<&str>,
) -> Result<String> {
let mut email = String::new();
email.push_str(&format!("To: {}\r\n", to));
if let Some(cc) = cc {
email.push_str(&format!("Cc: {}\r\n", cc));
}
if let Some(bcc) = bcc {
email.push_str(&format!("Bcc: {}\r\n", bcc));
}
email.push_str(&format!("Subject: {}\r\n", subject));
email.push_str("Content-Type: text/plain; charset=utf-8\r\n");
email.push_str("\r\n");
email.push_str(body);
Ok(email)
}
pub use crate::gmail::models::extract_body;