Skip to main content

postmark/
client.rs

1use reqwest::{
2    Method,
3    header::{self, HeaderMap, HeaderValue},
4};
5use serde::{Deserialize, Serialize, de::DeserializeOwned};
6
7pub struct Client {
8    pub http_client: reqwest::Client,
9    pub api_base_url: &'static str,
10    pub account_api_token: Option<String>,
11}
12
13#[derive(Clone, Debug, Deserialize, Serialize)]
14#[serde(rename_all = "PascalCase")]
15pub struct ApiError {
16    pub error_code: i64,
17    pub message: String,
18}
19
20pub struct SendRequestInput<B: Serialize> {
21    pub method: Method,
22    pub url: String,
23    pub body: B,
24    pub server_token: Option<String>,
25}
26
27impl Client {
28    /// Builds a client for the Postmark API.
29    ///
30    /// Installs the stdx rustls `CryptoProvider` as the process default (if
31    /// none is installed yet) before creating the underlying `reqwest` client,
32    /// because `reqwest` is compiled with the `rustls-no-provider` feature and
33    /// would otherwise panic for lack of a cryptography provider.
34    pub fn new(account_api_token: Option<String>) -> Client {
35        crypto_rustls::install_default_rustls_provider();
36        let http_client = reqwest::Client::new();
37
38        return Client {
39            http_client,
40            api_base_url: "https://api.postmarkapp.com",
41            account_api_token,
42        };
43    }
44
45    pub(crate) async fn send_request<B: Serialize, R: DeserializeOwned>(
46        &self,
47        input: SendRequestInput<B>,
48    ) -> Result<R, ApiError> {
49        // it's okay to unwrap here because we are sure that the headers won't contain any invalid
50        // characters
51        let mut headers: HeaderMap<HeaderValue> = HeaderMap::new();
52        headers.insert(header::ACCEPT, HeaderValue::from_str("application/json").unwrap());
53        headers.insert(header::CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap());
54
55        if let Some(server_api_token) = input.server_token {
56            headers.insert("X-Postmark-Server-Token", HeaderValue::from_str(&server_api_token).unwrap());
57        } else if let Some(account_api_token) = &self.account_api_token {
58            headers.insert(
59                "X-Postmark-Account-Token",
60                HeaderValue::from_str(account_api_token.as_str()).unwrap(),
61            );
62        }
63
64        let url = format!("{}{}", &self.api_base_url, input.url);
65        let res = self
66            .http_client
67            .request(input.method, url)
68            .headers(headers)
69            .json(&input.body)
70            .send()
71            .await
72            .map_err(|err| ApiError {
73                error_code: 0,
74                message: format!("postmark: error sending request: {err}"),
75            })?;
76
77        if res.status().as_u16() > 399 {
78            let err: ApiError = res.json().await.map_err(|err| ApiError {
79                error_code: 0,
80                message: format!("postmark: error parsing error response: {err}"),
81            })?;
82            return Err(err);
83        }
84
85        let res: R = res.json().await.map_err(|err| ApiError {
86            error_code: 0,
87            message: format!("postmark: error parsing response: {err}"),
88        })?;
89
90        return Ok(res);
91    }
92}