1pub mod models;
9
10use std::collections::HashMap;
11
12use map_core::client::MapClient;
13use map_core::folders::Folder;
14use map_core::messages::{ListMessagesFilter, MessageEntry, ReadStatus};
15use map_core::{BMessage, MessageStatus};
16use store::PhoneField;
17use tokio::io::{AsyncRead, AsyncWrite};
18
19use crate::fetch::{fetch_folder, list_folder};
20use crate::util::{datetime_to_ms, now_ms};
21use models::{Direction, LiveBody, LiveFolder, LiveMessage, LiveThread};
22
23#[derive(Debug, Default)]
29pub struct ListFilter {
30 pub unread: bool,
32 pub from: Option<String>,
34 pub since_ms: Option<i64>,
36 pub limit: Option<u16>,
38 pub offset: u16,
40}
41
42pub async fn list<T: AsyncRead + AsyncWrite + Unpin>(
52 client: &mut MapClient<T>,
53 folder: Folder,
54 filter: &ListFilter,
55) -> anyhow::Result<Vec<LiveMessage>> {
56 let template =
57 ListMessagesFilter { read_status: read_status_for(filter), ..Default::default() };
58 let mut msgs: Vec<LiveMessage> = fetch_folder(client, folder, None, now_ms(), &template)
59 .await?
60 .into_iter()
61 .map(|m| LiveMessage {
62 handle: m.handle,
63 timestamp_ms: m.timestamp_ms,
64 address: canonical(&m.address),
65 folder: m.folder,
66 read: m.read,
67 text: m.text,
68 })
69 .filter(|m| keep(m, filter))
70 .collect();
71 msgs.sort_by(|a, b| b.timestamp_ms.cmp(&a.timestamp_ms));
72 Ok(window(msgs, filter))
73}
74
75fn read_status_for(f: &ListFilter) -> Option<ReadStatus> {
76 f.unread.then_some(ReadStatus::Unread)
77}
78
79fn canonical(raw: &str) -> String {
81 PhoneField::new(raw, None).display().to_owned()
82}
83
84fn keep(m: &LiveMessage, f: &ListFilter) -> bool {
85 if f.from.as_deref().is_some_and(|addr| canonical(addr) != m.address) {
88 return false;
89 }
90 f.since_ms.is_none_or(|since| m.timestamp_ms >= since)
91}
92
93fn window(msgs: Vec<LiveMessage>, f: &ListFilter) -> Vec<LiveMessage> {
94 let rest = msgs.into_iter().skip(usize::from(f.offset));
95 match f.limit {
96 Some(n) => rest.take(usize::from(n)).collect(),
97 None => rest.collect(),
98 }
99}
100
101pub async fn threads<T: AsyncRead + AsyncWrite + Unpin>(
111 client: &mut MapClient<T>,
112) -> anyhow::Result<Vec<LiveThread>> {
113 let mut acc: HashMap<String, LiveThread> = HashMap::new();
114 for folder in [Folder::Inbox, Folder::Sent] {
115 for entry in list_folder(client, folder, &ListMessagesFilter::default()).await? {
116 accumulate(&mut acc, &entry);
117 }
118 }
119 let mut threads: Vec<LiveThread> = acc.into_values().collect();
120 threads.sort_by(|a, b| b.latest_ms.cmp(&a.latest_ms));
121 Ok(threads)
122}
123
124pub async fn folders<T: AsyncRead + AsyncWrite + Unpin>(
134 client: &mut MapClient<T>,
135) -> anyhow::Result<Vec<LiveFolder>> {
136 let listing = client.list_message_folders().await?;
137 Ok(listing.folders().iter().map(|f| LiveFolder { name: f.name().to_owned() }).collect())
138}
139
140fn accumulate(acc: &mut HashMap<String, LiveThread>, entry: &MessageEntry) {
141 let address = peer_address(entry);
142 if address.is_empty() {
143 return;
144 }
145 let address = canonical(&address);
147 let ts = datetime_to_ms(&entry.datetime).unwrap_or(0);
148 let t = acc.entry(address.clone()).or_insert_with(|| LiveThread {
149 address,
150 latest_ms: ts,
151 total: 0,
152 unread: 0,
153 });
154 t.total = t.total.saturating_add(1);
155 t.unread = t.unread.saturating_add(u32::from(!entry.sent && !entry.read));
156 if ts > t.latest_ms {
157 t.latest_ms = ts;
158 }
159}
160
161fn peer_address(entry: &MessageEntry) -> String {
162 if entry.sent {
163 entry.recipient_addressing.clone()
164 } else {
165 entry.sender_addressing.clone()
166 }
167}
168
169pub async fn get<T: AsyncRead + AsyncWrite + Unpin>(
179 client: &mut MapClient<T>,
180 handle: String,
181) -> anyhow::Result<LiveBody> {
182 let bmsg = client.get_message(&handle).await?;
183 Ok(to_live_body(handle, &bmsg))
184}
185
186fn to_live_body(handle: String, bmsg: &BMessage) -> LiveBody {
187 let direction = direction_of(bmsg.folder());
188 let address = match direction {
189 Direction::Sent => bmsg.envelope().recipients.first().map(|v| v.tel.clone()),
190 Direction::Received => bmsg.originator().map(|v| v.tel.clone()),
191 }
192 .unwrap_or_default();
193 LiveBody {
194 handle,
195 direction,
196 address: canonical(&address),
197 folder: bmsg.folder().to_owned(),
198 read: matches!(bmsg.status(), MessageStatus::Read),
199 text: bmsg.envelope().body.text.clone(),
200 }
201}
202
203fn direction_of(folder: &str) -> Direction {
204 let f = folder.to_ascii_lowercase();
205 if f.contains("sent") || f.contains("outbox") {
206 Direction::Sent
207 } else {
208 Direction::Received
209 }
210}
211
212#[cfg(test)]
213mod tests;