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}