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