Skip to main content

postal_rs/
lib.rs

1//! The crate provides an interface to [Postal]'s http [API].
2//!
3//! # Examples
4//!
5//! ```no_run
6//! use postal_rs::{Client, DetailsInterest, Message, SendResult};
7//! use std::env;
8//! 
9//! #[tokio::main]
10//! async fn main() {
11//!    let address = env::var("POSTAL_ADDRESS").unwrap_or_default();
12//!    let token = env::var("POSTAL_TOKEN").unwrap_or_default();
13//!
14//!    let message = Message::default()
15//!        .to(&["example@gmail.com".to_owned()])
16//!        .from("test@yourserver.io")
17//!        .subject("Hello World")
18//!        .text("A test message");
19//!    let client = Client::new(address, token).unwrap();
20//!    let _ = client
21//!        .send(message)
22//!        .await
23//!        .unwrap();
24//!}
25//!
26//! ```
27//!
28//! [Postal]: https://postal.atech.media/
29//! [API]: https://github.com/postalhq/postal/wiki/Using-the-API
30
31mod error;
32
33pub use error::PostalError;
34
35use reqwest::StatusCode;
36use serde::{Deserialize, Serialize};
37use serde_json::Value as Json;
38use std::collections::HashMap;
39use url::Url;
40
41/// Client holds a session information
42#[derive(Debug, Clone, Eq, PartialEq)]
43pub struct Client {
44    address: Url,
45    token: String,
46}
47
48impl Client {
49    /// Constructs a new instance of client
50    pub fn new<U, S>(url: U, token: S) -> Result<Self, PostalError>
51    where
52        U: AsRef<str>,
53        S: Into<String>,
54    {
55        let url = Url::parse(url.as_ref())?;
56        let token = token.into();
57
58        Ok(Self {
59            address: url,
60            token,
61        })
62    }
63
64    /// Sends a message to Postal
65    pub async fn send<M: Into<Message>>(&self, message: M) -> Result<Vec<SendResult>, PostalError> {
66        let address = self.address.join("/api/v1/send/message")?;
67        let message = message.into();
68
69        let client = reqwest::Client::new();
70        let res = client
71            .post(address)
72            .json(&message)
73            .header("X-Server-API-Key", &self.token)
74            .send()
75            .await?;
76
77        handle_send(res).await
78    }
79
80    /// Sends a standart SMTP message to Postal
81    pub async fn send_raw<M: Into<RawMessage>>(
82        &self,
83        message: M,
84    ) -> Result<Vec<SendResult>, PostalError> {
85        let address = self.address.join("/api/v1/send/raw")?;
86        let message = message.into();
87
88        let client = reqwest::Client::new();
89        let res = client
90            .post(address)
91            .json(&message)
92            .header("X-Server-API-Key", &self.token)
93            .send()
94            .await?;
95
96        handle_send(res).await
97    }
98
99    /// Asks a Postal server to provide an information details
100    /// about a message
101    ///
102    /// By default it provides a limited information.
103    /// To increase this volume you can specify expansions via [DetailsInterest]
104    ///
105    /// [DetailsInterest]: ./struct.DetailsInterest.html
106    pub async fn get_message_details<I: Into<DetailsInterest>>(
107        &self,
108        interest: I,
109    ) -> Result<HashMap<String, Json>, PostalError> {
110        let interest = interest.into();
111        let address = self.address.join("/api/v1/messages/message")?;
112
113        let client = reqwest::Client::new();
114        let body: Json = interest.into();
115        let res = client
116            .post(address)
117            .json(&body)
118            .header("X-Server-API-Key", &self.token)
119            .send()
120            .await?;
121
122        check_status(res.status())?;
123
124        let data: api_structures::Responce<HashMap<String, Json>> = res.json().await?;
125        let data = check_responce(data)?;
126
127        Ok(data)
128    }
129
130    /// Obtains a delivery information according to a message.
131    pub async fn get_message_deliveries(
132        &self,
133        id: MessageHash,
134    ) -> Result<Vec<HashMap<String, Json>>, PostalError> {
135        let address = self.address.join("/api/v1/messages/deliveries")?;
136
137        let client = reqwest::Client::new();
138        let body: Json = serde_json::json!({ "id": id });
139        let res = client
140            .post(address)
141            .json(&body)
142            .header("X-Server-API-Key", &self.token)
143            .send()
144            .await?;
145
146        check_status(res.status())?;
147
148        let data: api_structures::Responce<Vec<HashMap<String, Json>>> = res.json().await?;
149        let data = check_responce(data)?;
150
151        Ok(data)
152    }
153}
154
155async fn handle_send(resp: reqwest::Response) -> Result<Vec<SendResult>, PostalError> {
156    check_status(resp.status())?;
157
158    let data: api_structures::Responce<api_structures::MessageSucessData> = resp.json().await?;
159    let data = check_responce(data)?;
160
161    let messages = data
162        .messages
163        .into_iter()
164        .map(|(to, m)| SendResult { to, id: m.id })
165        .collect();
166
167    Ok(messages)
168}
169
170fn check_status(sc: StatusCode) -> Result<(), PostalError> {
171    match sc {
172        StatusCode::OK => Ok(()),
173        StatusCode::INTERNAL_SERVER_ERROR => Err(PostalError::InternalServerError),
174        StatusCode::MOVED_PERMANENTLY | StatusCode::PERMANENT_REDIRECT => {
175            Err(PostalError::ExpectedAlternativeUrl)
176        }
177        StatusCode::SERVICE_UNAVAILABLE => Err(PostalError::ServiceUnavailableError),
178        // according to postal docs it's imposible to get a different status code
179        // https://krystal.github.io/postal-api/index.html
180        _ => unreachable!(),
181    }
182}
183
184fn check_responce<T>(data: api_structures::Responce<T>) -> Result<T, PostalError> {
185    match data {
186        api_structures::Responce::Success { data, .. } => Ok(data),
187        api_structures::Responce::Error { data, .. } => Err(PostalError::Error {
188            code: data.code,
189            message: data.message,
190        }),
191        // the format of this error is unclear
192        api_structures::Responce::ParameterError {} => unimplemented!(),
193    }
194}
195
196/// MessageHash represents a hash which can be used to
197/// get a different information bout a message.
198pub type MessageHash = u64;
199
200/// Message represents a email which can be sent
201#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)]
202pub struct Message {
203    ///The e-mail addresses of the recipients (max 50)
204    pub to: Option<Vec<String>>,
205    /// The e-mail addresses of any CC contacts (max 50)
206    pub cc: Option<Vec<String>>,
207    /// The e-mail addresses of any BCC contacts (max 50)
208    pub bcc: Option<Vec<String>>,
209    /// The e-mail address for the From header
210    pub from: Option<String>,
211    /// The e-mail address for the Sender header
212    pub sender: Option<String>,
213    /// The subject of the e-mail
214    pub subject: Option<String>,
215    /// The tag of the e-mail
216    pub tag: Option<String>,
217    /// Set the reply-to address for the mail
218    pub reply_to: Option<String>,
219    /// The plain text body of the e-mail
220    pub plain_body: Option<String>,
221    /// The HTML body of the e-mail
222    pub html_body: Option<String>,
223    /// An array of attachments for this e-mail
224    pub attachments: Option<Vec<Vec<u8>>>,
225    /// A hash of additional headers
226    pub headers: Option<MessageHash>,
227    /// Is this message a bounce?
228    pub bounce: Option<bool>,
229}
230
231impl Message {
232    pub fn from<S: Into<String>>(mut self, s: S) -> Self {
233        self.from = Some(s.into());
234        self
235    }
236
237    pub fn to(mut self, to: &[String]) -> Self {
238        self.to = Some(to.to_vec());
239        self
240    }
241
242    pub fn subject<S: Into<String>>(mut self, s: S) -> Self {
243        self.subject = Some(s.into());
244        self
245    }
246
247    pub fn text<S: Into<String>>(mut self, s: S) -> Self {
248        self.plain_body = Some(s.into());
249        self
250    }
251
252    pub fn html<S: Into<String>>(mut self, s: S) -> Self {
253        self.html_body = Some(s.into());
254        self
255    }
256}
257
258/// RawMessage allows you to send us a raw RFC2822 formatted message along with
259/// the recipients that it should be sent to.
260#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)]
261pub struct RawMessage {
262    /// The address that should be logged as sending the message
263    pub mail_from: String,
264    /// The addresses this message should be sent to
265    pub rcpt_to: Vec<String>,
266    /// A base64 encoded RFC2822 message to send
267    pub data: String,
268    /// Is this message a bounce?
269    pub bounce: Option<bool>,
270}
271
272impl RawMessage {
273    pub fn new<S1: Into<String>, S2: Into<String>>(to: &[String], from: S1, data: S2) -> Self {
274        Self {
275            rcpt_to: to.to_owned(),
276            mail_from: from.into(),
277            data: data.into(),
278            bounce: None,
279        }
280    }
281}
282
283/// DetailsInterest contains an options which can be used to
284/// turn on expansions while obtaining details of a message.
285#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)]
286pub struct DetailsInterest {
287    id: MessageHash,
288    status: Option<()>,
289    details: Option<()>,
290    inspection: Option<()>,
291    plain_body: Option<()>,
292    html_body: Option<()>,
293    attachments: Option<()>,
294    headers: Option<()>,
295    raw_message: Option<()>,
296}
297
298impl DetailsInterest {
299    pub fn new(id: MessageHash) -> Self {
300        id.into()
301    }
302
303    pub fn with_status(mut self) -> Self {
304        self.status = Some(());
305        self
306    }
307
308    pub fn with_details(mut self) -> Self {
309        self.details = Some(());
310        self
311    }
312
313    pub fn with_inspection(mut self) -> Self {
314        self.inspection = Some(());
315        self
316    }
317
318    pub fn with_plain_body(mut self) -> Self {
319        self.plain_body = Some(());
320        self
321    }
322
323    pub fn with_html_body(mut self) -> Self {
324        self.html_body = Some(());
325        self
326    }
327
328    pub fn with_headers(mut self) -> Self {
329        self.headers = Some(());
330        self
331    }
332
333    pub fn with_raw_message(mut self) -> Self {
334        self.raw_message = Some(());
335        self
336    }
337
338    fn build_expansions_list(&self) -> Option<Vec<Json>> {
339        let mut expansions: Option<Vec<Json>> = None;
340        if self.status.is_some() {
341            expansions = Some(expansions.unwrap_or_default());
342            expansions
343                .as_mut()
344                .unwrap()
345                .push(Json::String("status".to_owned()));
346        }
347        if self.details.is_some() {
348            expansions = Some(expansions.unwrap_or_default());
349            expansions
350                .as_mut()
351                .unwrap()
352                .push(Json::String("details".to_owned()));
353        }
354        if self.inspection.is_some() {
355            expansions = Some(expansions.unwrap_or_default());
356            expansions
357                .as_mut()
358                .unwrap()
359                .push(Json::String("inspection".to_owned()));
360        }
361        if self.plain_body.is_some() {
362            expansions = Some(expansions.unwrap_or_default());
363            expansions
364                .as_mut()
365                .unwrap()
366                .push(Json::String("plain_body".to_owned()));
367        }
368        if self.html_body.is_some() {
369            expansions = Some(expansions.unwrap_or_default());
370            expansions
371                .as_mut()
372                .unwrap()
373                .push(Json::String("html_body".to_owned()));
374        }
375        if self.headers.is_some() {
376            expansions = Some(expansions.unwrap_or_default());
377            expansions
378                .as_mut()
379                .unwrap()
380                .push(Json::String("headers".to_owned()));
381        }
382        if self.raw_message.is_some() {
383            expansions = Some(expansions.unwrap_or_default());
384            expansions
385                .as_mut()
386                .unwrap()
387                .push(Json::String("raw_message".to_owned()));
388        }
389
390        expansions
391    }
392}
393
394impl Into<DetailsInterest> for MessageHash {
395    fn into(self) -> DetailsInterest {
396        DetailsInterest {
397            id: self,
398            status: None,
399            details: None,
400            inspection: None,
401            plain_body: None,
402            html_body: None,
403            attachments: None,
404            headers: None,
405            raw_message: None,
406        }
407    }
408}
409
410impl Into<Json> for DetailsInterest {
411    fn into(self) -> Json {
412        let mut map: HashMap<String, Json> = HashMap::new();
413        map.insert("id".to_owned(), self.id.into());
414
415        let expansions = self.build_expansions_list();
416        if let Some(expansions) = expansions {
417            map.insert("_expansions".to_owned(), Json::Array(expansions));
418        }
419
420        serde_json::json!(map)
421    }
422}
423
424/// SendResult represent a result of sending request
425#[derive(Debug, Eq, PartialEq, Clone)]
426pub struct SendResult {
427    /// An email To which an email was sent
428    pub to: String,
429    /// A message id which can be used to retrieve message details
430    /// and message deliveries
431    pub id: MessageHash,
432}
433
434mod api_structures {
435    use super::*;
436
437    #[derive(Debug, Clone, Serialize, Deserialize)]
438    #[serde(tag = "status", rename_all = "camelCase")]
439    pub enum Responce<D> {
440        Success {
441            time: f64,
442            flags: HashMap<String, u64>,
443            data: D,
444        },
445        ParameterError {},
446        Error {
447            time: f64,
448            flags: HashMap<String, u64>,
449            data: ResponceError,
450        },
451    }
452
453    #[derive(Debug, Clone, Serialize, Deserialize)]
454    pub struct MessageSucessData {
455        pub message_id: String,
456        pub messages: HashMap<String, MessageDataTo>,
457    }
458
459    #[derive(Debug, Clone, Serialize, Deserialize)]
460    pub struct MessageDataTo {
461        pub id: u64,
462        pub token: String,
463    }
464
465    #[derive(Debug, Clone, Serialize, Deserialize)]
466    pub struct ResponceError {
467        pub code: String,
468        pub message: String,
469    }
470}