cull_gmail/gmail_client.rs
1//! # Gmail Client Module
2//!
3//! This module provides the core Gmail API client functionality for the cull-gmail application.
4//! The `GmailClient` struct manages Gmail API connections, authentication, and message operations.
5//!
6//! ## Overview
7//!
8//! The Gmail client provides:
9//!
10//! - Authenticated Gmail API access using OAuth2 flows
11//! - Label management and mapping functionality
12//! - Message list operations with filtering support
13//! - Configuration-based setup with credential management
14//! - Integration with Gmail's REST API via the `google-gmail1` crate
15//!
16//! ## Authentication
17//!
18//! The client uses OAuth2 authentication with the "installed application" flow,
19//! requiring client credentials (client ID and secret) to be configured. Tokens
20//! are automatically managed and persisted to disk for reuse.
21//!
22//! ## Configuration
23//!
24//! The client is configured using [`ClientConfig`] which specifies:
25//! - OAuth2 credentials (client ID, client secret)
26//! - Token persistence location
27//! - Configuration file paths
28//!
29//! ## Error Handling
30//!
31//! All operations return `Result<T, Error>` where [`Error`] encompasses:
32//! - Gmail API errors (network, authentication, quota)
33//! - Configuration and credential errors
34//! - I/O errors from file operations
35//!
36//! ## Examples
37//!
38//! ### Basic Usage
39//!
40//! ```rust,no_run
41//! use cull_gmail::{ClientConfig, GmailClient};
42//!
43//! # async fn example() -> cull_gmail::Result<()> {
44//! // Create configuration with OAuth2 credentials
45//! let config = ClientConfig::builder()
46//! .with_client_id("your-client-id.googleusercontent.com")
47//! .with_client_secret("your-client-secret")
48//! .build();
49//!
50//! // Initialize Gmail client with authentication
51//! let client = GmailClient::new_with_config(config).await?;
52//!
53//! // Display available labels
54//! client.show_label();
55//!
56//! // Get label ID for a specific label name
57//! if let Some(inbox_id) = client.get_label_id("INBOX") {
58//! println!("Inbox ID: {}", inbox_id);
59//! }
60//! # Ok(())
61//! # }
62//! ```
63//!
64//! ### Label Operations
65//!
66//! ```rust,no_run
67//! use cull_gmail::{ClientConfig, GmailClient};
68//!
69//! # async fn example() -> cull_gmail::Result<()> {
70//! # let config = ClientConfig::builder().build();
71//! let client = GmailClient::new_with_config(config).await?;
72//!
73//! // Check if a label exists
74//! match client.get_label_id("Important") {
75//! Some(id) => println!("Important label ID: {}", id),
76//! None => println!("Important label not found"),
77//! }
78//!
79//! // List all available labels (logged to console)
80//! client.show_label();
81//! # Ok(())
82//! # }
83//! ```
84//!
85//! ## Thread Safety
86//!
87//! The Gmail client contains async operations and internal state. While individual
88//! operations are thread-safe, the client itself should not be shared across
89//! threads without proper synchronization.
90//!
91//! ## Rate Limits
92//!
93//! The Gmail API has usage quotas and rate limits. The client does not implement
94//! automatic retry logic, so applications should handle rate limit errors appropriately.
95//!
96//! [`ClientConfig`]: crate::ClientConfig
97//! [`Error`]: crate::Error
98
99use std::collections::BTreeMap;
100
101use google_gmail1::{
102 Gmail,
103 hyper_rustls::{HttpsConnector, HttpsConnectorBuilder},
104 hyper_util::{
105 client::legacy::{Client, connect::HttpConnector},
106 rt::TokioExecutor,
107 },
108 yup_oauth2::{InstalledFlowAuthenticator, InstalledFlowReturnMethod},
109};
110
111mod message_summary;
112
113pub(crate) use message_summary::MessageSummary;
114
115use crate::{ClientConfig, Error, Result, rules::EolRule};
116
117/// Default maximum number of results to return per page from Gmail API calls.
118///
119/// This constant defines the default page size for Gmail API list operations.
120/// The value "200" represents a balance between API efficiency and memory usage.
121///
122/// Gmail API supports up to 500 results per page, but 200 provides good performance
123/// while keeping response sizes manageable.
124pub const DEFAULT_MAX_RESULTS: &str = "200";
125
126/// Gmail API client providing authenticated access to Gmail operations.
127///
128/// `GmailClient` manages the connection to Gmail's REST API, handles OAuth2 authentication,
129/// maintains label mappings, and provides methods for message list operations.
130///
131/// The client contains internal state for:
132/// - Authentication credentials and tokens
133/// - Label name-to-ID mappings
134/// - Query filters and pagination settings
135/// - Retrieved message summaries
136/// - Rule processing configuration
137///
138/// # Examples
139///
140/// ```rust,no_run
141/// use cull_gmail::{ClientConfig, GmailClient};
142///
143/// # async fn example() -> cull_gmail::Result<()> {
144/// let config = ClientConfig::builder()
145/// .with_client_id("client-id")
146/// .with_client_secret("client-secret")
147/// .build();
148///
149/// let mut client = GmailClient::new_with_config(config).await?;
150/// client.show_label();
151/// # Ok(())
152/// # }
153/// ```
154#[derive(Clone)]
155pub struct GmailClient {
156 hub: Gmail<HttpsConnector<HttpConnector>>,
157 label_map: BTreeMap<String, String>,
158 pub(crate) max_results: u32,
159 pub(crate) label_ids: Vec<String>,
160 pub(crate) query: String,
161 pub(crate) messages: Vec<MessageSummary>,
162 pub(crate) rule: Option<EolRule>,
163 pub(crate) execute: bool,
164}
165
166impl std::fmt::Debug for GmailClient {
167 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
168 f.debug_struct("GmailClient")
169 .field("label_map", &self.label_map)
170 .field("max_results", &self.max_results)
171 .field("label_ids", &self.label_ids)
172 .field("query", &self.query)
173 .field("messages_count", &self.messages.len())
174 .field("execute", &self.execute)
175 .finish_non_exhaustive()
176 }
177}
178
179impl GmailClient {
180 // /// Create a new Gmail Api connection and fetch label map using credential file.
181 // pub async fn new_from_credential_file(credential_file: &str) -> Result<Self> {
182 // let (config_dir, secret) = {
183 // let config_dir = crate::utils::assure_config_dir_exists("~/.cull-gmail")?;
184
185 // let home_dir = env::home_dir().unwrap();
186
187 // let path = home_dir.join(".cull-gmail").join(credential_file);
188 // let json_str = fs::read_to_string(path).expect("could not read path");
189
190 // let console: ConsoleApplicationSecret =
191 // serde_json::from_str(&json_str).expect("could not convert to struct");
192
193 // let secret: ApplicationSecret = console.installed.unwrap();
194 // (config_dir, secret)
195 // };
196
197 // GmailClient::new_from_secret(secret, &config_dir).await
198 // }
199
200 /// Creates a new Gmail client with the provided configuration.
201 ///
202 /// This method initializes a Gmail API client with OAuth2 authentication using the
203 /// "installed application" flow. It sets up the HTTPS connector, authenticates
204 /// using the provided credentials, and fetches the label mapping from Gmail.
205 ///
206 /// # Arguments
207 ///
208 /// * `config` - Client configuration containing OAuth2 credentials and settings
209 ///
210 /// # Returns
211 ///
212 /// Returns a configured `GmailClient` ready for API operations, or an error if:
213 /// - Authentication fails (invalid credentials, network issues)
214 /// - Gmail API is unreachable
215 /// - Label fetching fails
216 ///
217 /// # Errors
218 ///
219 /// This method can fail with:
220 /// - [`Error::GoogleGmail1`] - Gmail API errors during authentication or label fetch
221 /// - Network connectivity issues during OAuth2 flow
222 /// - [`Error::NoLabelsFound`] - If no labels exist in the mailbox (unusual)
223 ///
224 /// # Examples
225 ///
226 /// ```rust,no_run
227 /// use cull_gmail::{ClientConfig, GmailClient};
228 ///
229 /// # async fn example() -> cull_gmail::Result<()> {
230 /// let config = ClientConfig::builder()
231 /// .with_client_id("123456789-abc.googleusercontent.com")
232 /// .with_client_secret("your-client-secret")
233 /// .build();
234 ///
235 /// let client = GmailClient::new_with_config(config).await?;
236 /// println!("Gmail client initialized successfully");
237 /// # Ok(())
238 /// # }
239 /// ```
240 ///
241 /// # Panics
242 ///
243 /// This method contains `.unwrap()` calls for:
244 /// - HTTPS connector building (should not fail with valid TLS setup)
245 /// - Default max results parsing (hardcoded valid string)
246 /// - OAuth2 authenticator building (should not fail with valid config)
247 ///
248 /// [`Error::GoogleGmail1`]: crate::Error::GoogleGmail1
249 /// [`Error::NoLabelsFound`]: crate::Error::NoLabelsFound
250 pub async fn new_with_config(config: ClientConfig) -> Result<Self> {
251 let executor = TokioExecutor::new();
252 let connector = HttpsConnectorBuilder::new()
253 .with_native_roots()
254 .unwrap()
255 .https_or_http()
256 .enable_http1()
257 .build();
258
259 let client = Client::builder(executor.clone()).build(connector.clone());
260 log::trace!("file to persist tokens to `{}`", config.persist_path());
261
262 let auth = InstalledFlowAuthenticator::with_client(
263 config.secret().clone(),
264 InstalledFlowReturnMethod::HTTPRedirect,
265 Client::builder(executor).build(connector),
266 )
267 .persist_tokens_to_disk(config.persist_path())
268 .build()
269 .await
270 .unwrap();
271
272 let hub = Gmail::new(client, auth);
273 let label_map = GmailClient::get_label_map(&hub).await?;
274
275 Ok(GmailClient {
276 hub,
277 label_map,
278 max_results: DEFAULT_MAX_RESULTS.parse::<u32>().unwrap(),
279 label_ids: Vec::new(),
280 query: String::new(),
281 messages: Vec::new(),
282 rule: None,
283 execute: false,
284 })
285 }
286
287 /// Fetches the label mapping from Gmail API.
288 ///
289 /// This method retrieves all labels from the user's Gmail account and creates
290 /// a mapping from label names to their corresponding label IDs.
291 ///
292 /// # Arguments
293 ///
294 /// * `hub` - The Gmail API hub instance for making API calls
295 ///
296 /// # Returns
297 ///
298 /// Returns a `BTreeMap` containing label name to ID mappings, or an error if
299 /// the API call fails or no labels are found.
300 ///
301 /// # Errors
302 ///
303 /// - [`Error::GoogleGmail1`] - Gmail API request failure
304 /// - [`Error::NoLabelsFound`] - No labels exist in the mailbox
305 ///
306 /// [`Error::GoogleGmail1`]: crate::Error::GoogleGmail1
307 /// [`Error::NoLabelsFound`]: crate::Error::NoLabelsFound
308 async fn get_label_map(
309 hub: &Gmail<HttpsConnector<HttpConnector>>,
310 ) -> Result<BTreeMap<String, String>> {
311 let call = hub.users().labels_list("me");
312 let (_response, list) = call
313 .add_scope("https://mail.google.com/")
314 .doit()
315 .await
316 .map_err(Box::new)?;
317
318 let Some(label_list) = list.labels else {
319 return Err(Error::NoLabelsFound);
320 };
321
322 let mut label_map = BTreeMap::new();
323 for label in &label_list {
324 if label.id.is_some() && label.name.is_some() {
325 let name = label.name.clone().unwrap();
326 let id = label.id.clone().unwrap();
327 label_map.insert(name, id);
328 }
329 }
330
331 Ok(label_map)
332 }
333
334 /// Retrieves the Gmail label ID for a given label name.
335 ///
336 /// This method looks up a label name in the internal label mapping and returns
337 /// the corresponding Gmail label ID if found.
338 ///
339 /// # Arguments
340 ///
341 /// * `name` - The label name to look up (case-sensitive)
342 ///
343 /// # Returns
344 ///
345 /// Returns `Some(String)` containing the label ID if the label exists,
346 /// or `None` if the label name is not found.
347 ///
348 /// # Examples
349 ///
350 /// ```rust,no_run
351 /// # use cull_gmail::{ClientConfig, GmailClient};
352 /// # async fn example(client: &GmailClient) {
353 /// // Look up standard Gmail labels
354 /// if let Some(inbox_id) = client.get_label_id("INBOX") {
355 /// println!("Inbox ID: {}", inbox_id);
356 /// }
357 ///
358 /// // Look up custom labels
359 /// match client.get_label_id("Important") {
360 /// Some(id) => println!("Found label ID: {}", id),
361 /// None => println!("Label 'Important' not found"),
362 /// }
363 /// # }
364 /// ```
365 pub fn get_label_id(&self, name: &str) -> Option<String> {
366 self.label_map.get(name).cloned()
367 }
368
369 /// Displays all available labels and their IDs to the log.
370 ///
371 /// This method iterates through the internal label mapping and outputs each
372 /// label name and its corresponding ID using the `log::info!` macro.
373 ///
374 /// # Examples
375 ///
376 /// ```rust,no_run
377 /// # use cull_gmail::{ClientConfig, GmailClient};
378 /// # async fn example() -> cull_gmail::Result<()> {
379 /// # let config = ClientConfig::builder().build();
380 /// let client = GmailClient::new_with_config(config).await?;
381 ///
382 /// // Display all labels (output goes to log)
383 /// client.show_label();
384 /// # Ok(())
385 /// # }
386 /// ```
387 ///
388 /// Output example:
389 /// ```text
390 /// INFO: INBOX: Label_1
391 /// INFO: SENT: Label_2
392 /// INFO: Important: Label_3
393 /// ```
394 pub fn show_label(&self) {
395 for (name, id) in self.label_map.iter() {
396 log::info!("{name}: {id}")
397 }
398 }
399
400 /// Returns a clone of the Gmail API hub for direct API access.
401 ///
402 /// This method provides access to the underlying Gmail API client hub,
403 /// allowing for direct API operations not covered by the higher-level
404 /// methods in this struct.
405 ///
406 /// # Returns
407 ///
408 /// A cloned `Gmail` hub instance configured with the same authentication
409 /// and connectors as this client.
410 ///
411 /// # Examples
412 ///
413 /// ```rust,no_run
414 /// # fn example() { }
415 /// ```
416 pub(crate) fn hub(&self) -> Gmail<HttpsConnector<HttpConnector>> {
417 self.hub.clone()
418 }
419}