cull_gmail/
message_list.rs

1//! # Message List Module
2//!
3//! This module provides the `MessageList` trait for interacting with Gmail message lists.
4//! The trait defines methods for retrieving, filtering, and managing collections of Gmail messages.
5//!
6//! ## Overview
7//!
8//! The `MessageList` trait provides:
9//!
10//! - Message list retrieval with pagination support
11//! - Label and query-based filtering
12//! - Message metadata fetching and logging
13//! - Configuration of result limits and query parameters
14//!
15//! ## Error Handling
16//!
17//! All asynchronous methods return `Result<T>` where errors may include:
18//! - Gmail API communication errors
19//! - Authentication failures
20//! - Network connectivity issues
21//! - Invalid query parameters
22//!
23//! ## Threading
24//!
25//! All async methods in this trait are `Send` compatible, allowing them to be used
26//! across thread boundaries in concurrent contexts.
27//!
28//! ## Example
29//!
30//! ```rust,no_run
31//! use cull_gmail::{GmailClient, MessageList, ClientConfig};
32//!
33//! async fn example() -> Result<(), Box<dyn std::error::Error>> {
34//!     // Create a client with proper configuration (credentials required)
35//!     let config = ClientConfig::builder()
36//!         .with_client_id("your-client-id")
37//!         .with_client_secret("your-client-secret")
38//!         .build();
39//!     let mut client = GmailClient::new_with_config(config).await?;
40//!     
41//!     // Configure search parameters
42//!     client.set_query("is:unread");
43//!     client.set_max_results(50);
44//!     
45//!     // Retrieve messages from Gmail
46//!     client.get_messages(1).await?;
47//!     
48//!     // Access retrieved message summaries
49//!     let messages = client.messages();
50//!     println!("Found {} messages", messages.len());
51//!     
52//!     Ok(())
53//! }
54//! ```
55
56#![warn(missing_docs)]
57#![allow(clippy::missing_errors_doc, clippy::missing_panics_doc)]
58
59use crate::{GmailClient, MessageSummary, Result};
60
61use google_gmail1::{
62    Gmail,
63    api::{ListMessagesResponse, Message as GmailMessage},
64    hyper_rustls::HttpsConnector,
65    hyper_util::client::legacy::connect::HttpConnector,
66};
67
68/// A trait for interacting with Gmail message lists, providing methods for
69/// retrieving, filtering, and managing collections of Gmail messages.
70///
71/// This trait abstracts the core operations needed to work with Gmail message lists,
72/// including pagination, filtering by labels and queries, and configuring result limits.
73///
74/// # Examples
75///
76/// ```rust,no_run
77/// use cull_gmail::{MessageList, GmailClient, ClientConfig};
78///
79/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
80/// let config = ClientConfig::builder().build();
81/// let mut client = GmailClient::new_with_config(config).await?;
82///
83/// // Set search parameters
84/// client.set_query("is:unread");
85/// client.set_max_results(100);
86///
87/// // Retrieve first page of messages
88/// client.get_messages(1).await?;
89/// # Ok(())
90/// # }
91/// ```
92pub trait MessageList {
93    /// Fetches detailed metadata for stored messages and logs their subjects and dates.
94    ///
95    /// This method retrieves the subject line and date for each message currently
96    /// stored in the message list and outputs them to the log.
97    ///
98    /// # Parameters
99    ///
100    /// - `pre` text to display before the date/subject message identifier
101    /// - `post` text to display after the date/subject message identifier
102    ///
103    /// # Returns
104    ///
105    /// Returns `Result<()>` on success, or an error if the Gmail API request fails.
106    ///
107    /// # Errors
108    ///
109    /// This method can fail if:
110    /// - The Gmail API is unreachable
111    /// - Authentication credentials are invalid or expired
112    /// - Network connectivity issues occur
113    /// - Individual message retrieval fails
114    fn log_messages(
115        &mut self,
116        pre: &str,
117        post: &str,
118    ) -> impl std::future::Future<Output = Result<()>> + Send;
119
120    /// Retrieves a list of messages from Gmail based on current filter settings.
121    ///
122    /// This method calls the Gmail API to get a page of messages matching the
123    /// configured query and label filters. Retrieved message IDs are stored
124    /// internally for further operations.
125    ///
126    /// # Arguments
127    ///
128    /// * `next_page_token` - Optional token for pagination. Use `None` for the first page,
129    ///   or the token from a previous response to get subsequent pages.
130    ///
131    /// # Returns
132    ///
133    /// Returns the raw `ListMessagesResponse` from the Gmail API, which contains
134    /// message metadata and pagination tokens.
135    ///
136    /// # Errors
137    ///
138    /// This method can fail if:
139    /// - The Gmail API request fails
140    /// - Authentication is invalid
141    /// - The query syntax is malformed
142    /// - Network issues prevent the API call
143    ///
144    /// # Examples
145    ///
146    /// ```rust,no_run
147    /// # use cull_gmail::{MessageList, GmailClient, ClientConfig};
148    /// # async fn example(mut client: impl MessageList) -> cull_gmail::Result<()> {
149    /// // Get the first page of results
150    /// let response = client.list_messages(None).await?;
151    ///
152    /// // Get the next page if available
153    /// if let Some(token) = response.next_page_token {
154    ///     let next_page = client.list_messages(Some(token)).await?;
155    /// }
156    /// # Ok(())
157    /// # }
158    /// ```
159    fn list_messages(
160        &mut self,
161        next_page_token: Option<String>,
162    ) -> impl std::future::Future<Output = Result<ListMessagesResponse>> + Send;
163
164    /// Retrieves multiple pages of messages based on the specified page limit.
165    ///
166    /// This method handles pagination automatically, fetching the specified number
167    /// of pages or all available pages if `pages` is 0.
168    ///
169    /// # Arguments
170    ///
171    /// * `pages` - Number of pages to retrieve:
172    ///   - `0`: Fetch all available pages
173    ///   - `1`: Fetch only the first page
174    ///   - `n > 1`: Fetch exactly `n` pages or until no more pages are available
175    ///
176    /// # Returns
177    ///
178    /// Returns `Result<()>` on success. All retrieved messages are stored internally
179    /// and can be accessed via `messages()`.
180    ///
181    /// # Errors
182    ///
183    /// This method can fail if any individual page request fails. See `list_messages`
184    /// for specific error conditions.
185    ///
186    /// # Examples
187    ///
188    /// ```rust,no_run
189    /// # use cull_gmail::{MessageList, GmailClient, ClientConfig};
190    /// # async fn example(mut client: impl MessageList) -> cull_gmail::Result<()> {
191    /// // Get all available pages
192    /// client.get_messages(0).await?;
193    ///
194    /// // Get exactly 3 pages
195    /// client.get_messages(3).await?;
196    /// # Ok(())
197    /// # }
198    /// ```
199    fn get_messages(&mut self, pages: u32) -> impl std::future::Future<Output = Result<()>> + Send;
200
201    /// Returns a reference to the Gmail API hub for direct API access.
202    ///
203    /// This method provides access to the underlying Gmail API client for
204    /// advanced operations not covered by this trait.
205    ///
206    /// # Returns
207    ///
208    /// A cloned `Gmail` hub instance configured with the appropriate connectors.
209    fn hub(&self) -> Gmail<HttpsConnector<HttpConnector>>;
210
211    /// Returns the list of label IDs currently configured for message filtering.
212    ///
213    /// # Returns
214    ///
215    /// A vector of Gmail label ID strings. These IDs are used to filter
216    /// messages during API calls.
217    ///
218    /// # Examples
219    ///
220    /// ```rust,no_run
221    /// # use cull_gmail::MessageList;
222    /// # fn example(client: impl MessageList) {
223    /// let labels = client.label_ids();
224    /// println!("Filtering by {} labels", labels.len());
225    /// # }
226    /// ```
227    fn label_ids(&self) -> Vec<String>;
228
229    /// Returns a list of message IDs for all currently stored messages.
230    ///
231    /// # Returns
232    ///
233    /// A vector of Gmail message ID strings. These IDs can be used for
234    /// further Gmail API operations on specific messages.
235    ///
236    /// # Examples
237    ///
238    /// ```rust,no_run
239    /// # use cull_gmail::MessageList;
240    /// # fn example(client: impl MessageList) {
241    /// let message_ids = client.message_ids();
242    /// println!("Found {} messages", message_ids.len());
243    /// # }
244    /// ```
245    fn message_ids(&self) -> Vec<String>;
246
247    /// Returns a reference to the collection of message summaries.
248    ///
249    /// This method provides access to all message summaries currently stored,
250    /// including any metadata that has been fetched.
251    ///
252    /// # Returns
253    ///
254    /// A reference to a vector of `MessageSummary` objects containing
255    /// message IDs and any retrieved metadata.
256    fn messages(&self) -> &Vec<MessageSummary>;
257
258    /// Sets the search query string for filtering messages.
259    ///
260    /// This method configures the Gmail search query that will be used in
261    /// subsequent API calls. The query uses Gmail's search syntax.
262    ///
263    /// # Arguments
264    ///
265    /// * `query` - A Gmail search query string (e.g., "is:unread", "from:example@gmail.com")
266    ///
267    /// # Examples
268    ///
269    /// ```rust,no_run
270    /// # use cull_gmail::MessageList;
271    /// # fn example(mut client: impl MessageList) {
272    /// client.set_query("is:unread older_than:30d");
273    /// client.set_query("from:noreply@example.com");
274    /// # }
275    /// ```
276    fn set_query(&mut self, query: &str);
277
278    /// Adds Gmail label IDs to the current filter list.
279    ///
280    /// This method appends the provided label IDs to the existing list of
281    /// labels used for filtering messages. Messages must match ALL specified labels.
282    ///
283    /// # Arguments
284    ///
285    /// * `label_ids` - A slice of Gmail label ID strings to add to the filter
286    ///
287    /// # Examples
288    ///
289    /// ```rust,no_run
290    /// # use cull_gmail::MessageList;
291    /// # fn example(mut client: impl MessageList) {
292    /// let label_ids = vec!["Label_1".to_string(), "Label_2".to_string()];
293    /// client.add_labels_ids(&label_ids);
294    /// # }
295    /// ```
296    fn add_labels_ids(&mut self, label_ids: &[String]);
297
298    /// Adds Gmail labels by name to the current filter list.
299    ///
300    /// This method resolves label names to their corresponding IDs and adds them
301    /// to the filter list. This is more convenient than using `add_labels_ids`
302    /// when you know the label names but not their IDs.
303    ///
304    /// # Arguments
305    ///
306    /// * `labels` - A slice of Gmail label name strings (e.g., "INBOX", "SPAM")
307    ///
308    /// # Returns
309    ///
310    /// Returns `Result<()>` on success, or an error if label name resolution fails.
311    ///
312    /// # Errors
313    ///
314    /// This method can fail if label name to ID resolution is not available
315    /// or if the underlying label ID mapping is not accessible.
316    ///
317    /// # Examples
318    ///
319    /// ```rust,no_run
320    /// # use cull_gmail::MessageList;
321    /// # async fn example(mut client: impl MessageList) -> cull_gmail::Result<()> {
322    /// let labels = vec!["INBOX".to_string(), "IMPORTANT".to_string()];
323    /// client.add_labels(&labels)?;
324    /// # Ok(())
325    /// # }
326    /// ```
327    fn add_labels(&mut self, labels: &[String]) -> Result<()>;
328
329    /// Returns the current maximum results limit per API request.
330    ///
331    /// # Returns
332    ///
333    /// The maximum number of messages to retrieve in a single API call.
334    /// Default is typically 200.
335    fn max_results(&self) -> u32;
336
337    /// Sets the maximum number of results to return per API request.
338    ///
339    /// This controls how many messages are retrieved in each page when calling
340    /// the Gmail API. Larger values reduce the number of API calls needed but
341    /// increase memory usage and response time.
342    ///
343    /// # Arguments
344    ///
345    /// * `value` - Maximum results per page (typically 1-500, Gmail API limits apply)
346    ///
347    /// # Examples
348    ///
349    /// ```rust,no_run
350    /// # use cull_gmail::MessageList;
351    /// # fn example(mut client: impl MessageList) {
352    /// client.set_max_results(100);  // Retrieve 100 messages per page
353    /// client.set_max_results(500);  // Retrieve 500 messages per page (maximum)
354    /// # }
355    /// ```
356    fn set_max_results(&mut self, value: u32);
357}
358
359/// Abstraction for Gmail API calls used by MessageList.
360pub(crate) trait GmailService {
361    /// Fetch a page of messages using current filters.
362    async fn list_messages_page(
363        &self,
364        label_ids: &[String],
365        query: &str,
366        max_results: u32,
367        page_token: Option<String>,
368    ) -> Result<ListMessagesResponse>;
369
370    /// Fetch minimal metadata for a message (subject, date, etc.).
371    async fn get_message_metadata(&self, message_id: &str) -> Result<GmailMessage>;
372}
373
374impl GmailClient {
375    /// Append any message IDs from a ListMessagesResponse into the provided messages vector.
376    fn append_list_to_messages(out: &mut Vec<MessageSummary>, list: &ListMessagesResponse) {
377        if let Some(msgs) = &list.messages {
378            let mut list_ids: Vec<MessageSummary> = msgs
379                .iter()
380                .flat_map(|item| item.id.as_deref().map(MessageSummary::new))
381                .collect();
382            out.append(&mut list_ids);
383        }
384    }
385}
386
387impl GmailService for GmailClient {
388    async fn list_messages_page(
389        &self,
390        label_ids: &[String],
391        query: &str,
392        max_results: u32,
393        page_token: Option<String>,
394    ) -> Result<ListMessagesResponse> {
395        let hub = self.hub();
396        let mut call = hub.users().messages_list("me").max_results(max_results);
397        if !label_ids.is_empty() {
398            for id in label_ids {
399                call = call.add_label_ids(id);
400            }
401        }
402        if !query.is_empty() {
403            call = call.q(query);
404        }
405        if let Some(token) = page_token {
406            call = call.page_token(&token);
407        }
408        let (_response, list) = call.doit().await.map_err(Box::new)?;
409        Ok(list)
410    }
411
412    async fn get_message_metadata(&self, message_id: &str) -> Result<GmailMessage> {
413        let hub = self.hub();
414        let (_res, m) = hub
415            .users()
416            .messages_get("me", message_id)
417            .add_scope("https://mail.google.com/")
418            .format("metadata")
419            .add_metadata_headers("subject")
420            .add_metadata_headers("date")
421            .doit()
422            .await
423            .map_err(Box::new)?;
424        Ok(m)
425    }
426}
427
428impl MessageList for GmailClient {
429    /// Set the maximum results
430    fn set_max_results(&mut self, value: u32) {
431        self.max_results = value;
432    }
433
434    /// Report the maximum results value
435    fn max_results(&self) -> u32 {
436        self.max_results
437    }
438
439    /// Add label to the labels collection
440    fn add_labels(&mut self, labels: &[String]) -> Result<()> {
441        log::debug!("labels from command line: {labels:?}");
442        let mut label_ids = Vec::new();
443        for label in labels {
444            if let Some(id) = self.get_label_id(label) {
445                label_ids.push(id)
446            }
447        }
448        self.add_labels_ids(label_ids.as_slice());
449        Ok(())
450    }
451
452    /// Add label to the labels collection
453    fn add_labels_ids(&mut self, label_ids: &[String]) {
454        if !label_ids.is_empty() {
455            self.label_ids.extend(label_ids.iter().cloned());
456        }
457    }
458
459    /// Set the query string
460    fn set_query(&mut self, query: &str) {
461        self.query = query.to_string()
462    }
463
464    /// Get the summary of the messages
465    fn messages(&self) -> &Vec<MessageSummary> {
466        &self.messages
467    }
468
469    /// Get a reference to the message_ids
470    fn message_ids(&self) -> Vec<String> {
471        self.messages.iter().map(|m| m.id().to_string()).collect()
472    }
473
474    /// Get a reference to the message_ids
475    fn label_ids(&self) -> Vec<String> {
476        self.label_ids.clone()
477    }
478
479    /// Get the hub
480    fn hub(&self) -> Gmail<HttpsConnector<HttpConnector>> {
481        self.hub().clone()
482    }
483
484    /// Run the Gmail api as configured
485    async fn get_messages(&mut self, pages: u32) -> Result<()> {
486        let list = self.list_messages(None).await?;
487        match pages {
488            1 => {}
489            0 => {
490                let mut list = list;
491                let mut page = 1;
492                loop {
493                    page += 1;
494                    log::debug!("Processing page #{page}");
495                    if list.next_page_token.is_none() {
496                        break;
497                    }
498                    list = self.list_messages(list.next_page_token).await?;
499                    // self.log_message_subjects(&list).await?;
500                }
501            }
502            _ => {
503                let mut list = list;
504                for page in 2..=pages {
505                    log::debug!("Processing page #{page}");
506                    if list.next_page_token.is_none() {
507                        break;
508                    }
509                    list = self.list_messages(list.next_page_token).await?;
510                    // self.log_message_subjects(&list).await?;
511                }
512            }
513        }
514
515        Ok(())
516    }
517
518    async fn list_messages(
519        &mut self,
520        next_page_token: Option<String>,
521    ) -> Result<ListMessagesResponse> {
522        if !self.label_ids.is_empty() {
523            log::debug!("Setting labels for list: {:#?}", self.label_ids);
524        }
525        if !self.query.is_empty() {
526            log::debug!("Setting query string `{}`", self.query);
527        }
528        if next_page_token.is_some() {
529            log::debug!("Setting token for next page.");
530        }
531
532        let list = self
533            .list_messages_page(
534                &self.label_ids,
535                &self.query,
536                self.max_results,
537                next_page_token,
538            )
539            .await?;
540        log::trace!(
541            "Estimated {} messages.",
542            list.result_size_estimate.unwrap_or(0)
543        );
544
545        if list.result_size_estimate.unwrap_or(0) == 0 {
546            log::warn!("Search returned no messages.");
547            return Ok(list);
548        }
549
550        Self::append_list_to_messages(&mut self.messages, &list);
551
552        Ok(list)
553    }
554
555    async fn log_messages(&mut self, pre: &str, post: &str) -> Result<()> {
556        for i in 0..self.messages.len() {
557            let id = self.messages[i].id().to_string();
558            log::trace!("{id}");
559            let m = self.get_message_metadata(&id).await?;
560            let message = &mut self.messages[i];
561            log::trace!("Got the message: {m:?}");
562            let Some(payload) = m.payload else { continue };
563            let Some(headers) = payload.headers else {
564                continue;
565            };
566
567            for header in headers {
568                if let Some(name) = header.name {
569                    match name.to_lowercase().as_str() {
570                        "subject" => message.set_subject(header.value),
571                        "date" => message.set_date(header.value),
572                        _ => {}
573                    }
574                } else {
575                    continue;
576                }
577            }
578            log::info!("{pre}{}{post}", message.list_date_and_subject());
579        }
580
581        Ok(())
582    }
583}
584
585#[cfg(test)]
586mod tests {
587    use super::*;
588
589    struct MockList {
590        label_ids: Vec<String>,
591        query: String,
592        max_results: u32,
593        messages: Vec<MessageSummary>,
594    }
595
596    impl MockList {
597        fn new() -> Self {
598            Self {
599                label_ids: vec![],
600                query: String::new(),
601                max_results: 200,
602                messages: vec![],
603            }
604        }
605
606        fn push_msg(&mut self, id: &str) {
607            self.messages.push(MessageSummary::new(id));
608        }
609    }
610
611    impl MessageList for MockList {
612        async fn log_messages(&mut self, _pre: &str, _post: &str) -> Result<()> {
613            Ok(())
614        }
615        async fn list_messages(
616            &mut self,
617            _next_page_token: Option<String>,
618        ) -> Result<ListMessagesResponse> {
619            Ok(ListMessagesResponse::default())
620        }
621        async fn get_messages(&mut self, _pages: u32) -> Result<()> {
622            Ok(())
623        }
624        fn hub(&self) -> Gmail<HttpsConnector<HttpConnector>> {
625            panic!("not used in tests")
626        }
627        fn label_ids(&self) -> Vec<String> {
628            self.label_ids.clone()
629        }
630        fn message_ids(&self) -> Vec<String> {
631            self.messages.iter().map(|m| m.id().to_string()).collect()
632        }
633        fn messages(&self) -> &Vec<MessageSummary> {
634            &self.messages
635        }
636        fn set_query(&mut self, query: &str) {
637            self.query = query.to_string();
638        }
639        fn add_labels_ids(&mut self, label_ids: &[String]) {
640            self.label_ids.extend_from_slice(label_ids);
641        }
642        fn add_labels(&mut self, _labels: &[String]) -> Result<()> {
643            Ok(())
644        }
645        fn max_results(&self) -> u32 {
646            self.max_results
647        }
648        fn set_max_results(&mut self, value: u32) {
649            self.max_results = value;
650        }
651    }
652
653    use std::collections::HashMap;
654    use std::sync::Mutex;
655
656    struct TestClient {
657        label_ids: Vec<String>,
658        query: String,
659        max_results: u32,
660        messages: Vec<MessageSummary>,
661        pages: Mutex<HashMap<Option<String>, ListMessagesResponse>>,
662    }
663
664    impl TestClient {
665        fn with_pages(map: HashMap<Option<String>, ListMessagesResponse>) -> Self {
666            Self {
667                label_ids: vec![],
668                query: String::new(),
669                max_results: 200,
670                messages: vec![],
671                pages: Mutex::new(map),
672            }
673        }
674    }
675
676    impl super::GmailService for TestClient {
677        async fn list_messages_page(
678            &self,
679            _label_ids: &[String],
680            _query: &str,
681            _max_results: u32,
682            page_token: Option<String>,
683        ) -> Result<ListMessagesResponse> {
684            let map = self.pages.lock().unwrap();
685            Ok(map
686                .get(&page_token)
687                .cloned()
688                .unwrap_or_else(ListMessagesResponse::default))
689        }
690
691        async fn get_message_metadata(&self, _message_id: &str) -> Result<GmailMessage> {
692            Ok(GmailMessage::default())
693        }
694    }
695
696    impl MessageList for TestClient {
697        fn set_max_results(&mut self, value: u32) {
698            self.max_results = value;
699        }
700        fn max_results(&self) -> u32 {
701            self.max_results
702        }
703        fn add_labels(&mut self, _labels: &[String]) -> Result<()> {
704            Ok(())
705        }
706        fn add_labels_ids(&mut self, label_ids: &[String]) {
707            self.label_ids.extend_from_slice(label_ids);
708        }
709        fn set_query(&mut self, query: &str) {
710            self.query = query.to_string();
711        }
712        fn messages(&self) -> &Vec<MessageSummary> {
713            &self.messages
714        }
715        fn message_ids(&self) -> Vec<String> {
716            self.messages.iter().map(|m| m.id().to_string()).collect()
717        }
718        fn label_ids(&self) -> Vec<String> {
719            self.label_ids.clone()
720        }
721        fn hub(&self) -> Gmail<HttpsConnector<HttpConnector>> {
722            unimplemented!("not used in tests")
723        }
724        async fn get_messages(&mut self, pages: u32) -> Result<()> {
725            let mut list = self.list_messages(None).await?;
726            match pages {
727                1 => {}
728                0 => loop {
729                    if list.next_page_token.is_none() {
730                        break;
731                    }
732                    list = self.list_messages(list.next_page_token).await?;
733                },
734                _ => {
735                    for _page in 2..=pages {
736                        if list.next_page_token.is_none() {
737                            break;
738                        }
739                        list = self.list_messages(list.next_page_token).await?;
740                    }
741                }
742            }
743            Ok(())
744        }
745        async fn list_messages(
746            &mut self,
747            next_page_token: Option<String>,
748        ) -> Result<ListMessagesResponse> {
749            let list = self
750                .list_messages_page(
751                    &self.label_ids,
752                    &self.query,
753                    self.max_results,
754                    next_page_token,
755                )
756                .await?;
757
758            if list.result_size_estimate.unwrap_or(0) == 0 {
759                return Ok(list);
760            }
761
762            if let Some(msgs) = &list.messages {
763                let mut list_ids: Vec<MessageSummary> = msgs
764                    .iter()
765                    .flat_map(|item| item.id.as_deref().map(MessageSummary::new))
766                    .collect();
767                self.messages.append(&mut list_ids);
768            }
769
770            Ok(list)
771        }
772        async fn log_messages(&mut self, _pre: &str, _post: &str) -> Result<()> {
773            Ok(())
774        }
775    }
776
777    #[test]
778    fn set_query_updates_state() {
779        let mut ml = MockList::new();
780        ml.set_query("from:noreply@example.com");
781        // not directly accessible; rely on behaviour by calling again
782        ml.set_query("is:unread");
783        assert_eq!(ml.query, "is:unread");
784    }
785
786    #[test]
787    fn add_label_ids_accumulates() {
788        let mut ml = MockList::new();
789        ml.add_labels_ids(&["Label_1".into()]);
790        ml.add_labels_ids(&["Label_2".into(), "Label_3".into()]);
791        assert_eq!(ml.label_ids, vec!["Label_1", "Label_2", "Label_3"]);
792    }
793
794    #[test]
795    fn max_results_get_set() {
796        let mut ml = MockList::new();
797        assert_eq!(ml.max_results(), 200);
798        ml.set_max_results(123);
799        assert_eq!(ml.max_results(), 123);
800    }
801
802    #[test]
803    fn message_ids_maps_from_messages() {
804        let mut ml = MockList::new();
805        ml.push_msg("abc");
806        ml.push_msg("def");
807        assert_eq!(ml.message_ids(), vec!["abc", "def"]);
808        assert_eq!(ml.messages().len(), 2);
809    }
810
811    #[test]
812    fn append_list_to_messages_extracts_ids() {
813        use google_gmail1::api::Message;
814        let mut out = Vec::<MessageSummary>::new();
815        let list = ListMessagesResponse {
816            messages: Some(vec![
817                Message {
818                    id: Some("m1".into()),
819                    ..Default::default()
820                },
821                Message {
822                    id: None,
823                    ..Default::default()
824                },
825                Message {
826                    id: Some("m2".into()),
827                    ..Default::default()
828                },
829            ]),
830            ..Default::default()
831        };
832
833        GmailClient::append_list_to_messages(&mut out, &list);
834        let ids: Vec<_> = out.iter().map(|m| m.id().to_string()).collect();
835        assert_eq!(ids, vec!["m1", "m2"]);
836    }
837
838    #[tokio::test]
839    async fn list_messages_across_pages_collects_ids() {
840        use google_gmail1::api::Message;
841        let page1 = ListMessagesResponse {
842            messages: Some(vec![
843                Message {
844                    id: Some("a".into()),
845                    ..Default::default()
846                },
847                Message {
848                    id: Some("b".into()),
849                    ..Default::default()
850                },
851            ]),
852            next_page_token: Some("t2".into()),
853            result_size_estimate: Some(2),
854        };
855        let page2 = ListMessagesResponse {
856            messages: Some(vec![Message {
857                id: Some("c".into()),
858                ..Default::default()
859            }]),
860            next_page_token: None,
861            result_size_estimate: Some(1),
862        };
863        let mut map = HashMap::new();
864        map.insert(None, page1);
865        map.insert(Some("t2".into()), page2);
866
867        let mut client = TestClient::with_pages(map);
868        client.set_max_results(2);
869        client.set_query("in:inbox");
870
871        client.get_messages(0).await.unwrap();
872        assert_eq!(client.message_ids(), vec!["a", "b", "c"]);
873    }
874
875    #[tokio::test]
876    async fn empty_first_page_returns_early() {
877        let page = ListMessagesResponse {
878            messages: None,
879            next_page_token: None,
880            result_size_estimate: Some(0),
881        };
882        let mut map = HashMap::new();
883        map.insert(None, page);
884        let mut client = TestClient::with_pages(map);
885        client.get_messages(0).await.unwrap();
886        assert!(client.message_ids().is_empty());
887    }
888
889    #[tokio::test]
890    async fn pages_param_gt1_but_no_next_token_stops() {
891        use google_gmail1::api::Message;
892        let first = ListMessagesResponse {
893            messages: Some(vec![Message {
894                id: Some("x".into()),
895                ..Default::default()
896            }]),
897            next_page_token: None,
898            result_size_estimate: Some(1),
899        };
900        let mut map = HashMap::new();
901        map.insert(None, first);
902        let mut client = TestClient::with_pages(map);
903        client.get_messages(5).await.unwrap();
904        assert_eq!(client.message_ids(), vec!["x"]);
905    }
906}