1pub mod models;
9
10use std::collections::HashMap;
11
12use map_core::client::MapClient;
13use map_core::folders::Folder;
14use map_core::messages::MessageEntry;
15use map_core::{BMessage, MessageStatus};
16use tokio::io::{AsyncRead, AsyncWrite};
17
18use crate::fetch::{fetch_folder, list_folder};
19use crate::util::{datetime_to_ms, now_ms};
20use models::{Direction, LiveBody, LiveMessage, LiveThread};
21
22#[derive(Debug, Default)]
27pub struct ListFilter {
28 pub unread: bool,
30 pub from: Option<String>,
32 pub since_ms: Option<i64>,
34 pub limit: Option<u16>,
36 pub offset: u16,
38}
39
40pub async fn list<T: AsyncRead + AsyncWrite + Unpin>(
49 client: &mut MapClient<T>,
50 folder: Folder,
51 filter: &ListFilter,
52) -> anyhow::Result<Vec<LiveMessage>> {
53 let mut msgs: Vec<LiveMessage> = fetch_folder(client, folder, None, now_ms())
54 .await?
55 .into_iter()
56 .map(|m| LiveMessage {
57 handle: m.handle,
58 timestamp_ms: m.timestamp_ms,
59 address: m.address,
60 folder: m.folder,
61 read: m.read,
62 text: m.text,
63 })
64 .filter(|m| keep(m, filter))
65 .collect();
66 msgs.sort_by(|a, b| b.timestamp_ms.cmp(&a.timestamp_ms));
67 Ok(window(msgs, filter))
68}
69
70fn keep(m: &LiveMessage, f: &ListFilter) -> bool {
71 if f.unread && m.read {
72 return false;
73 }
74 if f.from.as_deref().is_some_and(|addr| addr != m.address) {
75 return false;
76 }
77 f.since_ms.is_none_or(|since| m.timestamp_ms >= since)
78}
79
80fn window(msgs: Vec<LiveMessage>, f: &ListFilter) -> Vec<LiveMessage> {
81 let rest = msgs.into_iter().skip(usize::from(f.offset));
82 match f.limit {
83 Some(n) => rest.take(usize::from(n)).collect(),
84 None => rest.collect(),
85 }
86}
87
88pub async fn threads<T: AsyncRead + AsyncWrite + Unpin>(
98 client: &mut MapClient<T>,
99) -> anyhow::Result<Vec<LiveThread>> {
100 let mut acc: HashMap<String, LiveThread> = HashMap::new();
101 for folder in [Folder::Inbox, Folder::Sent] {
102 for entry in list_folder(client, folder).await? {
103 accumulate(&mut acc, &entry);
104 }
105 }
106 let mut threads: Vec<LiveThread> = acc.into_values().collect();
107 threads.sort_by(|a, b| b.latest_ms.cmp(&a.latest_ms));
108 Ok(threads)
109}
110
111fn accumulate(acc: &mut HashMap<String, LiveThread>, entry: &MessageEntry) {
112 let address = peer_address(entry);
113 if address.is_empty() {
114 return;
115 }
116 let ts = datetime_to_ms(&entry.datetime).unwrap_or(0);
117 let t = acc.entry(address.clone()).or_insert_with(|| LiveThread {
118 address,
119 latest_ms: ts,
120 total: 0,
121 unread: 0,
122 });
123 t.total = t.total.saturating_add(1);
124 t.unread = t.unread.saturating_add(u32::from(!entry.sent && !entry.read));
125 if ts > t.latest_ms {
126 t.latest_ms = ts;
127 }
128}
129
130fn peer_address(entry: &MessageEntry) -> String {
131 if entry.sent {
132 entry.recipient_addressing.clone()
133 } else {
134 entry.sender_addressing.clone()
135 }
136}
137
138pub async fn get<T: AsyncRead + AsyncWrite + Unpin>(
148 client: &mut MapClient<T>,
149 handle: String,
150) -> anyhow::Result<LiveBody> {
151 let bmsg = client.get_message(&handle).await?;
152 Ok(to_live_body(handle, &bmsg))
153}
154
155fn to_live_body(handle: String, bmsg: &BMessage) -> LiveBody {
156 let direction = direction_of(bmsg.folder());
157 let address = match direction {
158 Direction::Sent => bmsg.envelope().recipients.first().map(|v| v.tel.clone()),
159 Direction::Received => bmsg.originator().map(|v| v.tel.clone()),
160 }
161 .unwrap_or_default();
162 LiveBody {
163 handle,
164 direction,
165 address,
166 folder: bmsg.folder().to_owned(),
167 read: matches!(bmsg.status(), MessageStatus::Read),
168 text: bmsg.envelope().body.text.clone(),
169 }
170}
171
172fn direction_of(folder: &str) -> Direction {
173 let f = folder.to_ascii_lowercase();
174 if f.contains("sent") || f.contains("outbox") {
175 Direction::Sent
176 } else {
177 Direction::Received
178 }
179}
180
181#[cfg(test)]
182mod tests;