1use map_core::client::MapClient;
9use map_core::folders::Folder;
10use map_core::messages::{ListMessagesFilter, MessageEntry};
11use tokio::io::{AsyncRead, AsyncWrite};
12
13use crate::util::datetime_to_ms;
14
15pub struct FetchedMessage {
21 pub handle: String,
23 pub timestamp_ms: i64,
26 pub folder: String,
28 pub sent: bool,
30 pub address: String,
32 pub read: bool,
34 pub text: String,
36}
37
38impl FetchedMessage {
39 fn from_entry(entry: &MessageEntry, folder: &str, text: String, now: i64) -> Self {
40 let address = if entry.sent {
41 entry.recipient_addressing.clone()
42 } else {
43 entry.sender_addressing.clone()
44 };
45 Self {
46 handle: entry.handle.clone(),
47 timestamp_ms: datetime_to_ms(&entry.datetime).unwrap_or(now),
48 folder: folder.to_owned(),
49 sent: entry.sent,
50 address,
51 read: entry.read,
52 text,
53 }
54 }
55}
56
57pub async fn list_folder<T: AsyncRead + AsyncWrite + Unpin>(
69 client: &mut MapClient<T>,
70 folder: Folder,
71 template: &ListMessagesFilter,
72) -> anyhow::Result<Vec<MessageEntry>> {
73 const PAGE: u16 = 1024;
74
75 client.set_folder(folder).await?;
76 let mut out = Vec::new();
77 let mut offset: u16 = 0;
78 loop {
79 let filter = ListMessagesFilter { max_count: PAGE, offset, ..template.clone() };
80 let entries = client.list_messages(&filter).await?;
81 let count = entries.len();
82 out.extend(entries);
83 if count < usize::from(PAGE) {
84 break;
85 }
86 match offset.checked_add(PAGE) {
87 Some(next) => offset = next,
88 None => break,
89 }
90 }
91 Ok(out)
92}
93
94pub async fn fetch_folder<T: AsyncRead + AsyncWrite + Unpin>(
106 client: &mut MapClient<T>,
107 folder: Folder,
108 since_ms: Option<i64>,
109 now: i64,
110 template: &ListMessagesFilter,
111) -> anyhow::Result<Vec<FetchedMessage>> {
112 let folder_str = folder.as_str();
113 let mut out = Vec::new();
114 for entry in list_folder(client, folder, template).await? {
115 if since_ms.is_some_and(|since| datetime_to_ms(&entry.datetime).unwrap_or(i64::MAX) < since)
116 {
117 continue;
118 }
119 let bmsg = client.get_message(&entry.handle).await?;
120 let text = bmsg.envelope().body.text.clone();
121 out.push(FetchedMessage::from_entry(&entry, folder_str, text, now));
122 }
123 Ok(out)
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129
130 fn entry(sent: bool) -> MessageEntry {
131 MessageEntry {
132 handle: "0400".to_owned(),
133 subject: "hi".to_owned(),
134 datetime: "20260101T120000".to_owned(),
135 sender_name: "Alice".to_owned(),
136 sender_addressing: "+15550001".to_owned(),
137 recipient_name: "Bob".to_owned(),
138 recipient_addressing: "+15550002".to_owned(),
139 msg_type: "SMS_GSM".to_owned(),
140 size: 2,
141 read: true,
142 sent,
143 }
144 }
145
146 #[test]
147 fn received_resolves_address_to_sender() {
148 let m = FetchedMessage::from_entry(&entry(false), "inbox", "body".to_owned(), 0);
149 assert!(!m.sent);
150 assert_eq!(m.address, "+15550001");
151 assert_eq!(m.folder, "inbox");
152 assert!(m.read);
153 }
154
155 #[test]
156 fn sent_resolves_address_to_recipient() {
157 let m = FetchedMessage::from_entry(&entry(true), "sent", "body".to_owned(), 0);
158 assert!(m.sent);
159 assert_eq!(m.address, "+15550002");
160 }
161
162 #[test]
163 fn malformed_datetime_falls_back_to_now() {
164 let mut e = entry(false);
165 e.datetime = "not-a-date".to_owned();
166 let m = FetchedMessage::from_entry(&e, "inbox", "body".to_owned(), 42);
167 assert_eq!(m.timestamp_ms, 42);
168 }
169}