1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
use std::convert::TryInto;

use crate::{Client, POSTMARK_API_URL};
use async_trait::async_trait;
use bytes::Bytes;
use http::{Request, Response};
use thiserror::Error;
use typed_builder::TypedBuilder;

/// A representation of the asynchronous Postmark API for a single user.
/// Separate users should use separate instances of this.
///
/// A reqwest based [`Client`]
///
/// ```
/// # use postmark::reqwest::PostmarkClient;
/// let client = PostmarkClient::default();
/// ```
///
/// Using the builder:
/// ```
/// # use postmark::reqwest::PostmarkClient;
/// let client = PostmarkClient::builder()
///   .base_url("https://api.postmarkapp.com/")
///   .token("<sometoken>")
///   .build();
/// ```
#[derive(TypedBuilder)]
pub struct PostmarkClient {
    #[builder(default, setter(into, strip_option))]
    pub token: Option<String>,
    #[builder(default=POSTMARK_API_URL.into(), setter(into))]
    pub base_url: String,
}

impl std::fmt::Debug for PostmarkClient {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let Self {
            token: ref _token,
            base_url: ref _base_url,
        } = *self;

        let mut builder = f.debug_struct("PostmarkClient");
        builder.field("token", &_token.as_ref().map(|_| "***"));
        builder.field("base_url", &(*_base_url));
        builder.finish()
    }
}

impl Default for PostmarkClient {
    fn default() -> Self {
        Self {
            base_url: POSTMARK_API_URL.into(),
            token: None,
        }
    }
}

#[derive(Error, Debug)]
pub enum PostmarkClientError {
    #[error("error setting auth header: {}", source)]
    AuthError {
        #[from]
        source: http::header::InvalidHeaderValue,
    },
    #[error("communication with postmark: {}", source)]
    Communication {
        #[from]
        source: reqwest::Error,
    },
    #[error("`http` error: {}", source)]
    Http {
        #[from]
        source: http::Error,
    },
}

#[async_trait]
impl Client for PostmarkClient {
    type Error = PostmarkClientError;

    async fn execute(&self, req: Request<Bytes>) -> Result<Response<Bytes>, Self::Error> {
        let client = reqwest::Client::builder().build()?;
        let mut req = req;

        if let Some(tok) = &self.token {
            req.headers_mut()
                .append("X-Postmark-Server-Token", tok.try_into()?);
        }

        let reqwest_req = req.try_into()?;
        let reqwest_rsp = client.execute(reqwest_req).await?;

        let mut rsp = Response::builder()
            .status(reqwest_rsp.status())
            .version(reqwest_rsp.version());

        let headers = rsp.headers_mut().unwrap();
        for (k, v) in reqwest_rsp.headers() {
            headers.insert(k, v.clone());
        }

        Ok(rsp.body(reqwest_rsp.bytes().await?)?)
    }
}