Skip to main content

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::{CustomHyperClientBuilder, 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_client = Client::builder(executor).build(connector);
263        let auth = InstalledFlowAuthenticator::with_client(
264            config.secret().clone(),
265            InstalledFlowReturnMethod::HTTPRedirect,
266            CustomHyperClientBuilder::from(auth_client),
267        )
268        .persist_tokens_to_disk(config.persist_path())
269        .build()
270        .await
271        .unwrap();
272
273        let hub = Gmail::new(client, auth);
274        let label_map = GmailClient::get_label_map(&hub).await?;
275
276        Ok(GmailClient {
277            hub,
278            label_map,
279            max_results: DEFAULT_MAX_RESULTS.parse::<u32>().unwrap(),
280            label_ids: Vec::new(),
281            query: String::new(),
282            messages: Vec::new(),
283            rule: None,
284            execute: false,
285        })
286    }
287
288    /// Fetches the label mapping from Gmail API.
289    ///
290    /// This method retrieves all labels from the user's Gmail account and creates
291    /// a mapping from label names to their corresponding label IDs.
292    ///
293    /// # Arguments
294    ///
295    /// * `hub` - The Gmail API hub instance for making API calls
296    ///
297    /// # Returns
298    ///
299    /// Returns a `BTreeMap` containing label name to ID mappings, or an error if
300    /// the API call fails or no labels are found.
301    ///
302    /// # Errors
303    ///
304    /// - [`Error::GoogleGmail1`] - Gmail API request failure
305    /// - [`Error::NoLabelsFound`] - No labels exist in the mailbox
306    ///
307    /// [`Error::GoogleGmail1`]: crate::Error::GoogleGmail1
308    /// [`Error::NoLabelsFound`]: crate::Error::NoLabelsFound
309    async fn get_label_map(
310        hub: &Gmail<HttpsConnector<HttpConnector>>,
311    ) -> Result<BTreeMap<String, String>> {
312        let call = hub.users().labels_list("me");
313        let (_response, list) = call
314            .add_scope("https://mail.google.com/")
315            .doit()
316            .await
317            .map_err(Box::new)?;
318
319        let Some(label_list) = list.labels else {
320            return Err(Error::NoLabelsFound);
321        };
322
323        let mut label_map = BTreeMap::new();
324        for label in &label_list {
325            if label.id.is_some() && label.name.is_some() {
326                let name = label.name.clone().unwrap();
327                let id = label.id.clone().unwrap();
328                label_map.insert(name, id);
329            }
330        }
331
332        Ok(label_map)
333    }
334
335    /// Retrieves the Gmail label ID for a given label name.
336    ///
337    /// This method looks up a label name in the internal label mapping and returns
338    /// the corresponding Gmail label ID if found.
339    ///
340    /// # Arguments
341    ///
342    /// * `name` - The label name to look up (case-sensitive)
343    ///
344    /// # Returns
345    ///
346    /// Returns `Some(String)` containing the label ID if the label exists,
347    /// or `None` if the label name is not found.
348    ///
349    /// # Examples
350    ///
351    /// ```rust,no_run
352    /// # use cull_gmail::{ClientConfig, GmailClient};
353    /// # async fn example(client: &GmailClient) {
354    /// // Look up standard Gmail labels
355    /// if let Some(inbox_id) = client.get_label_id("INBOX") {
356    ///     println!("Inbox ID: {}", inbox_id);
357    /// }
358    ///
359    /// // Look up custom labels
360    /// match client.get_label_id("Important") {
361    ///     Some(id) => println!("Found label ID: {}", id),
362    ///     None => println!("Label 'Important' not found"),
363    /// }
364    /// # }
365    /// ```
366    pub fn get_label_id(&self, name: &str) -> Option<String> {
367        self.label_map.get(name).cloned()
368    }
369
370    /// Displays all available labels and their IDs to the log.
371    ///
372    /// This method iterates through the internal label mapping and outputs each
373    /// label name and its corresponding ID using the `log::info!` macro.
374    ///
375    /// # Examples
376    ///
377    /// ```rust,no_run
378    /// # use cull_gmail::{ClientConfig, GmailClient};
379    /// # async fn example() -> cull_gmail::Result<()> {
380    /// # let config = ClientConfig::builder().build();
381    /// let client = GmailClient::new_with_config(config).await?;
382    ///
383    /// // Display all labels (output goes to log)
384    /// client.show_label();
385    /// # Ok(())
386    /// # }
387    /// ```
388    ///
389    /// Output example:
390    /// ```text
391    /// INFO: INBOX: Label_1
392    /// INFO: SENT: Label_2
393    /// INFO: Important: Label_3
394    /// ```
395    pub fn show_label(&self) {
396        for (name, id) in self.label_map.iter() {
397            log::info!("{name}: {id}")
398        }
399    }
400
401    /// Returns a clone of the Gmail API hub for direct API access.
402    ///
403    /// This method provides access to the underlying Gmail API client hub,
404    /// allowing for direct API operations not covered by the higher-level
405    /// methods in this struct.
406    ///
407    /// # Returns
408    ///
409    /// A cloned `Gmail` hub instance configured with the same authentication
410    /// and connectors as this client.
411    ///
412    /// # Examples
413    ///
414    /// ```rust,no_run
415    /// # fn example() { }
416    /// ```
417    pub(crate) fn hub(&self) -> Gmail<HttpsConnector<HttpConnector>> {
418        self.hub.clone()
419    }
420}