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>(
67 client: &mut MapClient<T>,
68 folder: Folder,
69) -> anyhow::Result<Vec<MessageEntry>> {
70 const PAGE: u16 = 1024;
71
72 client.set_folder(folder).await?;
73 let mut out = Vec::new();
74 let mut offset: u16 = 0;
75 loop {
76 let filter = ListMessagesFilter { max_count: PAGE, offset, ..Default::default() };
77 let entries = client.list_messages(&filter).await?;
78 let count = entries.len();
79 out.extend(entries);
80 if count < usize::from(PAGE) {
81 break;
82 }
83 match offset.checked_add(PAGE) {
84 Some(next) => offset = next,
85 None => break,
86 }
87 }
88 Ok(out)
89}
90
91pub async fn fetch_folder<T: AsyncRead + AsyncWrite + Unpin>(
102 client: &mut MapClient<T>,
103 folder: Folder,
104 since_ms: Option<i64>,
105 now: i64,
106) -> anyhow::Result<Vec<FetchedMessage>> {
107 let folder_str = folder.as_str();
108 let mut out = Vec::new();
109 for entry in list_folder(client, folder).await? {
110 if since_ms.is_some_and(|since| datetime_to_ms(&entry.datetime).unwrap_or(i64::MAX) < since)
111 {
112 continue;
113 }
114 let bmsg = client.get_message(&entry.handle).await?;
115 let text = bmsg.envelope().body.text.clone();
116 out.push(FetchedMessage::from_entry(&entry, folder_str, text, now));
117 }
118 Ok(out)
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124
125 fn entry(sent: bool) -> MessageEntry {
126 MessageEntry {
127 handle: "0400".to_owned(),
128 subject: "hi".to_owned(),
129 datetime: "20260101T120000".to_owned(),
130 sender_name: "Alice".to_owned(),
131 sender_addressing: "+15550001".to_owned(),
132 recipient_name: "Bob".to_owned(),
133 recipient_addressing: "+15550002".to_owned(),
134 msg_type: "SMS_GSM".to_owned(),
135 size: 2,
136 read: true,
137 sent,
138 }
139 }
140
141 #[test]
142 fn received_resolves_address_to_sender() {
143 let m = FetchedMessage::from_entry(&entry(false), "inbox", "body".to_owned(), 0);
144 assert!(!m.sent);
145 assert_eq!(m.address, "+15550001");
146 assert_eq!(m.folder, "inbox");
147 assert!(m.read);
148 }
149
150 #[test]
151 fn sent_resolves_address_to_recipient() {
152 let m = FetchedMessage::from_entry(&entry(true), "sent", "body".to_owned(), 0);
153 assert!(m.sent);
154 assert_eq!(m.address, "+15550002");
155 }
156
157 #[test]
158 fn malformed_datetime_falls_back_to_now() {
159 let mut e = entry(false);
160 e.datetime = "not-a-date".to_owned();
161 let m = FetchedMessage::from_entry(&e, "inbox", "body".to_owned(), 42);
162 assert_eq!(m.timestamp_ms, 42);
163 }
164}