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}