diff --git a/src/gmail_client.rs b/src/gmail_client.rs index 563ff3a..db61de3 100644 --- a/src/gmail_client.rs +++ b/src/gmail_client.rs @@ -1,3 +1,101 @@ +//! # Gmail Client Module +//! +//! This module provides the core Gmail API client functionality for the cull-gmail application. +//! The `GmailClient` struct manages Gmail API connections, authentication, and message operations. +//! +//! ## Overview +//! +//! The Gmail client provides: +//! +//! - Authenticated Gmail API access using OAuth2 flows +//! - Label management and mapping functionality +//! - Message list operations with filtering support +//! - Configuration-based setup with credential management +//! - Integration with Gmail's REST API via the `google-gmail1` crate +//! +//! ## Authentication +//! +//! The client uses OAuth2 authentication with the "installed application" flow, +//! requiring client credentials (client ID and secret) to be configured. Tokens +//! are automatically managed and persisted to disk for reuse. +//! +//! ## Configuration +//! +//! The client is configured using [`ClientConfig`] which specifies: +//! - OAuth2 credentials (client ID, client secret) +//! - Token persistence location +//! - Configuration file paths +//! +//! ## Error Handling +//! +//! All operations return `Result` where [`Error`] encompasses: +//! - Gmail API errors (network, authentication, quota) +//! - Configuration and credential errors +//! - I/O errors from file operations +//! +//! ## Examples +//! +//! ### Basic Usage +//! +//! ```rust,no_run +//! use cull_gmail::{ClientConfig, GmailClient}; +//! +//! # async fn example() -> cull_gmail::Result<()> { +//! // Create configuration with OAuth2 credentials +//! let config = ClientConfig::builder() +//! .with_client_id("your-client-id.googleusercontent.com") +//! .with_client_secret("your-client-secret") +//! .build(); +//! +//! // Initialize Gmail client with authentication +//! let client = GmailClient::new_with_config(config).await?; +//! +//! // Display available labels +//! client.show_label(); +//! +//! // Get label ID for a specific label name +//! if let Some(inbox_id) = client.get_label_id("INBOX") { +//! println!("Inbox ID: {}", inbox_id); +//! } +//! # Ok(()) +//! # } +//! ``` +//! +//! ### Label Operations +//! +//! ```rust,no_run +//! use cull_gmail::{ClientConfig, GmailClient}; +//! +//! # async fn example() -> cull_gmail::Result<()> { +//! # let config = ClientConfig::builder().build(); +//! let client = GmailClient::new_with_config(config).await?; +//! +//! // Check if a label exists +//! match client.get_label_id("Important") { +//! Some(id) => println!("Important label ID: {}", id), +//! None => println!("Important label not found"), +//! } +//! +//! // List all available labels (logged to console) +//! client.show_label(); +//! # Ok(()) +//! # } +//! ``` +//! +//! ## Thread Safety +//! +//! The Gmail client contains async operations and internal state. While individual +//! operations are thread-safe, the client itself should not be shared across +//! threads without proper synchronization. +//! +//! ## Rate Limits +//! +//! The Gmail API has usage quotas and rate limits. The client does not implement +//! automatic retry logic, so applications should handle rate limit errors appropriately. +//! +//! [`ClientConfig`]: crate::ClientConfig +//! [`Error`]: crate::Error + use std::collections::BTreeMap; use google_gmail1::{ @@ -16,10 +114,43 @@ pub(crate) use message_summary::MessageSummary; use crate::{ClientConfig, Error, Result, rules::EolRule}; -/// Default for the maximum number of results to return on a page +/// Default maximum number of results to return per page from Gmail API calls. +/// +/// This constant defines the default page size for Gmail API list operations. +/// The value "200" represents a balance between API efficiency and memory usage. +/// +/// Gmail API supports up to 500 results per page, but 200 provides good performance +/// while keeping response sizes manageable. pub const DEFAULT_MAX_RESULTS: &str = "200"; -/// Struct to capture configuration for List API call. +/// Gmail API client providing authenticated access to Gmail operations. +/// +/// `GmailClient` manages the connection to Gmail's REST API, handles OAuth2 authentication, +/// maintains label mappings, and provides methods for message list operations. +/// +/// The client contains internal state for: +/// - Authentication credentials and tokens +/// - Label name-to-ID mappings +/// - Query filters and pagination settings +/// - Retrieved message summaries +/// - Rule processing configuration +/// +/// # Examples +/// +/// ```rust,no_run +/// use cull_gmail::{ClientConfig, GmailClient}; +/// +/// # async fn example() -> cull_gmail::Result<()> { +/// let config = ClientConfig::builder() +/// .with_client_id("client-id") +/// .with_client_secret("client-secret") +/// .build(); +/// +/// let mut client = GmailClient::new_with_config(config).await?; +/// client.show_label(); +/// # Ok(()) +/// # } +/// ``` #[derive(Clone)] pub struct GmailClient { hub: Gmail>, @@ -34,9 +165,14 @@ pub struct GmailClient { impl std::fmt::Debug for GmailClient { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Labels") + f.debug_struct("GmailClient") .field("label_map", &self.label_map) - .finish() + .field("max_results", &self.max_results) + .field("label_ids", &self.label_ids) + .field("query", &self.query) + .field("messages_count", &self.messages.len()) + .field("execute", &self.execute) + .finish_non_exhaustive() } } @@ -61,7 +197,56 @@ impl GmailClient { // GmailClient::new_from_secret(secret, &config_dir).await // } - /// Create a new List struct and add the Gmail api connection. + /// Creates a new Gmail client with the provided configuration. + /// + /// This method initializes a Gmail API client with OAuth2 authentication using the + /// "installed application" flow. It sets up the HTTPS connector, authenticates + /// using the provided credentials, and fetches the label mapping from Gmail. + /// + /// # Arguments + /// + /// * `config` - Client configuration containing OAuth2 credentials and settings + /// + /// # Returns + /// + /// Returns a configured `GmailClient` ready for API operations, or an error if: + /// - Authentication fails (invalid credentials, network issues) + /// - Gmail API is unreachable + /// - Label fetching fails + /// + /// # Errors + /// + /// This method can fail with: + /// - [`Error::GoogleGmail1`] - Gmail API errors during authentication or label fetch + /// - Network connectivity issues during OAuth2 flow + /// - [`Error::NoLabelsFound`] - If no labels exist in the mailbox (unusual) + /// + /// # Examples + /// + /// ```rust,no_run + /// use cull_gmail::{ClientConfig, GmailClient}; + /// + /// # async fn example() -> cull_gmail::Result<()> { + /// let config = ClientConfig::builder() + /// .with_client_id("123456789-abc.googleusercontent.com") + /// .with_client_secret("your-client-secret") + /// .build(); + /// + /// let client = GmailClient::new_with_config(config).await?; + /// println!("Gmail client initialized successfully"); + /// # Ok(()) + /// # } + /// ``` + /// + /// # Panics + /// + /// This method contains `.unwrap()` calls for: + /// - HTTPS connector building (should not fail with valid TLS setup) + /// - Default max results parsing (hardcoded valid string) + /// - OAuth2 authenticator building (should not fail with valid config) + /// + /// [`Error::GoogleGmail1`]: crate::Error::GoogleGmail1 + /// [`Error::NoLabelsFound`]: crate::Error::NoLabelsFound pub async fn new_with_config(config: ClientConfig) -> Result { let executor = TokioExecutor::new(); let connector = HttpsConnectorBuilder::new() @@ -99,7 +284,27 @@ impl GmailClient { }) } - /// Create a new List struct and add the Gmail api connection. + /// Fetches the label mapping from Gmail API. + /// + /// This method retrieves all labels from the user's Gmail account and creates + /// a mapping from label names to their corresponding label IDs. + /// + /// # Arguments + /// + /// * `hub` - The Gmail API hub instance for making API calls + /// + /// # Returns + /// + /// Returns a `BTreeMap` containing label name to ID mappings, or an error if + /// the API call fails or no labels are found. + /// + /// # Errors + /// + /// - [`Error::GoogleGmail1`] - Gmail API request failure + /// - [`Error::NoLabelsFound`] - No labels exist in the mailbox + /// + /// [`Error::GoogleGmail1`]: crate::Error::GoogleGmail1 + /// [`Error::NoLabelsFound`]: crate::Error::NoLabelsFound async fn get_label_map( hub: &Gmail>, ) -> Result> { @@ -126,20 +331,97 @@ impl GmailClient { Ok(label_map) } - /// Return the id for the name from the labels map. - /// Returns `None` if the name is not found in the map. + /// Retrieves the Gmail label ID for a given label name. + /// + /// This method looks up a label name in the internal label mapping and returns + /// the corresponding Gmail label ID if found. + /// + /// # Arguments + /// + /// * `name` - The label name to look up (case-sensitive) + /// + /// # Returns + /// + /// Returns `Some(String)` containing the label ID if the label exists, + /// or `None` if the label name is not found. + /// + /// # Examples + /// + /// ```rust,no_run + /// # use cull_gmail::{ClientConfig, GmailClient}; + /// # async fn example(client: &GmailClient) { + /// // Look up standard Gmail labels + /// if let Some(inbox_id) = client.get_label_id("INBOX") { + /// println!("Inbox ID: {}", inbox_id); + /// } + /// + /// // Look up custom labels + /// match client.get_label_id("Important") { + /// Some(id) => println!("Found label ID: {}", id), + /// None => println!("Label 'Important' not found"), + /// } + /// # } + /// ``` pub fn get_label_id(&self, name: &str) -> Option { self.label_map.get(name).cloned() } - /// Show the label names and related id. + /// Displays all available labels and their IDs to the log. + /// + /// This method iterates through the internal label mapping and outputs each + /// label name and its corresponding ID using the `log::info!` macro. + /// + /// # Examples + /// + /// ```rust,no_run + /// # use cull_gmail::{ClientConfig, GmailClient}; + /// # async fn example() -> cull_gmail::Result<()> { + /// # let config = ClientConfig::builder().build(); + /// let client = GmailClient::new_with_config(config).await?; + /// + /// // Display all labels (output goes to log) + /// client.show_label(); + /// # Ok(()) + /// # } + /// ``` + /// + /// Output example: + /// ```text + /// INFO: INBOX: Label_1 + /// INFO: SENT: Label_2 + /// INFO: Important: Label_3 + /// ``` pub fn show_label(&self) { for (name, id) in self.label_map.iter() { log::info!("{name}: {id}") } } - /// Get the hub from the client + /// Returns a clone of the Gmail API hub for direct API access. + /// + /// This method provides access to the underlying Gmail API client hub, + /// allowing for direct API operations not covered by the higher-level + /// methods in this struct. + /// + /// # Returns + /// + /// A cloned `Gmail` hub instance configured with the same authentication + /// and connectors as this client. + /// + /// # Examples + /// + /// ```rust,no_run + /// # use cull_gmail::{ClientConfig, GmailClient}; + /// # async fn example() -> cull_gmail::Result<()> { + /// # let config = ClientConfig::builder().build(); + /// let client = GmailClient::new_with_config(config).await?; + /// + /// // Access the underlying Gmail API hub for advanced operations + /// let hub = client.hub(); + /// // Use hub for direct Gmail API calls... + /// # Ok(()) + /// # } + /// ``` pub(crate) fn hub(&self) -> Gmail> { self.hub.clone() } diff --git a/src/gmail_client/message_summary.rs b/src/gmail_client/message_summary.rs index e1c1278..9934e14 100644 --- a/src/gmail_client/message_summary.rs +++ b/src/gmail_client/message_summary.rs @@ -1,5 +1,27 @@ +//! # Message Summary Module +//! +//! This module provides the `MessageSummary` struct for representing Gmail message metadata +//! in a simplified format suitable for display and processing. + use crate::utils::Elide; +/// A simplified representation of Gmail message metadata. +/// +/// `MessageSummary` stores essential message information including ID, subject, and date. +/// It provides methods for accessing this information with fallback text for missing data. +/// +/// # Examples +/// +/// ```rust +/// # use cull_gmail::gmail_client::message_summary::MessageSummary; +/// let mut summary = MessageSummary::new("message_123"); +/// summary.set_subject(Some("Hello World".to_string())); +/// summary.set_date(Some("2023-01-15 10:30:00".to_string())); +/// +/// println!("Subject: {}", summary.subject()); +/// println!("Date: {}", summary.date()); +/// println!("Summary: {}", summary.list_date_and_subject()); +/// ``` #[derive(Debug, Clone)] pub struct MessageSummary { id: String, @@ -8,6 +30,22 @@ pub struct MessageSummary { } impl MessageSummary { + /// Creates a new `MessageSummary` with the given message ID. + /// + /// The subject and date fields are initialized as `None` and can be set later + /// using the setter methods. + /// + /// # Arguments + /// + /// * `id` - The Gmail message ID + /// + /// # Examples + /// + /// ```rust + /// # use cull_gmail::gmail_client::message_summary::MessageSummary; + /// let summary = MessageSummary::new("1234567890abcdef"); + /// assert_eq!(summary.id(), "1234567890abcdef"); + /// ``` pub(crate) fn new(id: &str) -> Self { MessageSummary { id: id.to_string(), @@ -16,14 +54,53 @@ impl MessageSummary { } } + /// Returns the Gmail message ID. + /// + /// # Examples + /// + /// ```rust + /// # use cull_gmail::gmail_client::message_summary::MessageSummary; + /// let summary = MessageSummary::new("msg_123"); + /// assert_eq!(summary.id(), "msg_123"); + /// ``` pub(crate) fn id(&self) -> &str { &self.id } + /// Sets the subject line of the message. + /// + /// # Arguments + /// + /// * `subject` - Optional subject line text + /// + /// # Examples + /// + /// ```rust + /// # use cull_gmail::gmail_client::message_summary::MessageSummary; + /// let mut summary = MessageSummary::new("msg_123"); + /// summary.set_subject(Some("Important Email".to_string())); + /// assert_eq!(summary.subject(), "Important Email"); + /// ``` pub(crate) fn set_subject(&mut self, subject: Option) { self.subject = subject } + /// Returns the subject line or a fallback message if none is set. + /// + /// # Returns + /// + /// The subject line if available, otherwise "*** No Subject for Message ***". + /// + /// # Examples + /// + /// ```rust + /// # use cull_gmail::gmail_client::message_summary::MessageSummary; + /// let mut summary = MessageSummary::new("msg_123"); + /// assert_eq!(summary.subject(), "*** No Subject for Message ***"); + /// + /// summary.set_subject(Some("Hello".to_string())); + /// assert_eq!(summary.subject(), "Hello"); + /// ``` pub(crate) fn subject(&self) -> &str { if let Some(s) = &self.subject { s @@ -32,10 +109,40 @@ impl MessageSummary { } } + /// Sets the date of the message. + /// + /// # Arguments + /// + /// * `date` - Optional date string (typically in RFC format) + /// + /// # Examples + /// + /// ```rust + /// # use cull_gmail::gmail_client::message_summary::MessageSummary; + /// let mut summary = MessageSummary::new("msg_123"); + /// summary.set_date(Some("2023-12-25 09:00:00".to_string())); + /// assert_eq!(summary.date(), "2023-12-25 09:00:00"); + /// ``` pub(crate) fn set_date(&mut self, date: Option) { self.date = date } + /// Returns the message date or a fallback message if none is set. + /// + /// # Returns + /// + /// The date string if available, otherwise "*** No Date for Message ***". + /// + /// # Examples + /// + /// ```rust + /// # use cull_gmail::gmail_client::message_summary::MessageSummary; + /// let mut summary = MessageSummary::new("msg_123"); + /// assert_eq!(summary.date(), "*** No Date for Message ***"); + /// + /// summary.set_date(Some("2023-12-25".to_string())); + /// assert_eq!(summary.date(), "2023-12-25"); + /// ``` pub(crate) fn date(&self) -> &str { if let Some(d) = &self.date { d @@ -44,6 +151,27 @@ impl MessageSummary { } } + /// Creates a formatted string combining date and subject for list display. + /// + /// This method extracts a portion of the date (characters 5-16) and combines it + /// with an elided version of the subject line for compact display in message lists. + /// + /// # Returns + /// + /// A formatted string with date and subject, or an error message if either + /// field is missing. + /// + /// # Examples + /// + /// ```rust + /// # use cull_gmail::gmail_client::message_summary::MessageSummary; + /// let mut summary = MessageSummary::new("msg_123"); + /// summary.set_date(Some("2023-12-25 09:00:00 GMT".to_string())); + /// summary.set_subject(Some("This is a very long subject line that will be truncated".to_string())); + /// + /// let display = summary.list_date_and_subject(); + /// // Result would be something like: "2-25 09:00: This is a very long s..." + /// ``` pub(crate) fn list_date_and_subject(&self) -> String { let Some(date) = self.date.as_ref() else { return "***invalid date or subject***".to_string();