Skip to main content

asfml_core/
client.rs

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", &params)?;
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}