1use reqwest::blocking::Client;
2use url::Url;
3
4use crate::error::{Error, Result};
5use crate::models::{Email, ListAddress, Preferences, Session, StatsResponse, ThreadResponse};
6
7const DEFAULT_BASE: &str = "https://lists.apache.org/";
8
9pub struct PonyMailClient {
10 http: Client,
11 base: Url,
12 session: Option<Session>,
13}
14
15impl PonyMailClient {
16 pub fn new(session: Option<Session>) -> Result<Self> {
17 Ok(Self {
18 http: Client::builder().user_agent("asfml/0.1.0").build()?,
19 base: Url::parse(DEFAULT_BASE)?,
20 session,
21 })
22 }
23
24 pub fn preferences(&self) -> Result<Preferences> {
25 self.get_json("api/preferences.lua", &[])
26 }
27
28 pub fn list(
29 &self,
30 list: &ListAddress,
31 since: &str,
32 limit: usize,
33 ) -> Result<Vec<crate::models::EmailSummary>> {
34 let mut emails = self.stats(list, since, None)?;
35 emails.truncate(limit);
36 Ok(emails)
37 }
38
39 pub fn search(
40 &self,
41 list: &ListAddress,
42 query: &str,
43 since: &str,
44 limit: usize,
45 ) -> Result<Vec<crate::models::EmailSummary>> {
46 let mut emails = self.stats(list, since, Some(query))?;
47 emails.truncate(limit);
48 Ok(emails)
49 }
50
51 pub fn email(&self, mid: &str) -> Result<Email> {
52 let email: Email = self.get_json("api/email.lua", &[("id", mid)])?;
53 if email.id.is_empty() {
54 return Err(Error::EmailNotFound(mid.to_string()));
55 }
56 Ok(email)
57 }
58
59 pub fn thread(&self, mid: &str) -> Result<ThreadResponse> {
60 self.get_json("api/thread.lua", &[("id", mid)])
61 }
62
63 fn stats(
64 &self,
65 list: &ListAddress,
66 since: &str,
67 query: Option<&str>,
68 ) -> Result<Vec<crate::models::EmailSummary>> {
69 let d = format!("lte={since}");
70 let mut params = vec![
71 ("list", list.list.as_str()),
72 ("domain", list.domain.as_str()),
73 ("d", d.as_str()),
74 ("emailsOnly", "true"),
75 ];
76 if let Some(query) = query {
77 params.push(("q", query));
78 }
79
80 let response: StatsResponse = self.get_json("api/stats.lua", ¶ms)?;
81 Ok(response.emails)
82 }
83
84 fn get_json<T>(&self, path: &str, params: &[(&str, &str)]) -> Result<T>
85 where
86 T: serde::de::DeserializeOwned,
87 {
88 let mut url = self.base.join(path)?;
89 {
90 let mut pairs = url.query_pairs_mut();
91 for (key, value) in params {
92 pairs.append_pair(key, value);
93 }
94 }
95
96 let mut request = self.http.get(url);
97 if let Some(session) = &self.session {
98 request = request.header(
99 reqwest::header::COOKIE,
100 format!("ponymail={}", session.ponymail),
101 );
102 }
103
104 let response = request.send()?.error_for_status()?;
105 let text = response.text()?;
106 serde_json::from_str(&text).map_err(|err| Error::ApiShapeChanged {
107 endpoint: endpoint_name(path),
108 reason: err.to_string(),
109 })
110 }
111}
112
113fn endpoint_name(path: &str) -> &'static str {
114 match path {
115 "api/preferences.lua" => "preferences.lua",
116 "api/stats.lua" => "stats.lua",
117 "api/email.lua" => "email.lua",
118 "api/thread.lua" => "thread.lua",
119 _ => "unknown endpoint",
120 }
121}