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}