├── src ├── api │ ├── bounce │ │ ├── bounces.rs │ │ └── delivery_stats.rs │ ├── bounce.rs │ ├── email.rs │ ├── webhooks.rs │ ├── message_streams.rs │ ├── server.rs │ ├── templates.rs │ ├── email │ │ ├── send_email_batch.rs │ │ ├── send_email_batch_with_templates.rs │ │ ├── send_email_with_template.rs │ │ └── send_email.rs │ ├── server │ │ ├── get_server.rs │ │ └── create_server.rs │ ├── templates │ │ ├── copy_templates.rs │ │ ├── delete_template.rs │ │ ├── get_template.rs │ │ ├── edit_template.rs │ │ └── create_template.rs │ ├── message_streams │ │ ├── get_suppressions.rs │ │ └── delete_suppression.rs │ └── webhooks │ │ └── create_webhook.rs ├── api.rs ├── lib.rs ├── client.rs └── reqwest.rs ├── .github ├── ISSUE_TEMPLATE │ ├── blank-issue.md │ └── bug_report.md └── workflows │ ├── audit.yml │ ├── release-plz.yml │ └── ci.yml ├── .gitignore ├── .editorconfig ├── .vscode └── launch.json ├── LICENSE-MIT ├── tests └── send_email.rs ├── Cargo.toml ├── README.md ├── CHANGELOG.md └── LICENSE-APACHE /src/api/bounce/bounces.rs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/api/bounce.rs: -------------------------------------------------------------------------------- 1 | //! You'll find in bounce sending related endpoints. 2 | mod delivery_stats; 3 | 4 | pub use delivery_stats::*; 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/blank-issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Blank Issue 3 | about: An empty template 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | # https://doc.rust-lang.org/cargo/faq.html#why-do-binaries-have-cargolock-in-version-control-but-not-libraries 3 | Cargo.lock 4 | 5 | .DS_Store 6 | .env 7 | .idea/ -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | 9 | [*.yml] 10 | indent_style = space 11 | indent_size = 2 12 | 13 | -------------------------------------------------------------------------------- /src/api/email.rs: -------------------------------------------------------------------------------- 1 | //! You'll find in email sending related endpoints. 2 | mod send_email; 3 | mod send_email_batch; 4 | mod send_email_batch_with_templates; 5 | mod send_email_with_template; 6 | 7 | pub use send_email::*; 8 | pub use send_email_batch::*; 9 | pub use send_email_batch_with_templates::*; 10 | pub use send_email_with_template::*; 11 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "lldb", 9 | "request": "launch", 10 | "name": "Debug", 11 | "program": "${workspaceFolder}/", 12 | "args": [], 13 | "cwd": "${workspaceFolder}" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/audit.yml: -------------------------------------------------------------------------------- 1 | name: audit 2 | on: 3 | push: 4 | paths: 5 | - '**/Cargo.toml' 6 | - '**/Cargo.lock' 7 | schedule: 8 | - cron: '0 0 * * *' 9 | 10 | concurrency: 11 | group: audit.yml-${{ github.head_ref || github.ref }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | security_audit: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: actions-rs/audit-check@v1 20 | with: 21 | token: ${{ secrets.GITHUB_TOKEN }} 22 | -------------------------------------------------------------------------------- /src/api/webhooks.rs: -------------------------------------------------------------------------------- 1 | //! You'll find in email sending related endpoints. 2 | 3 | pub use create_webhook::*; 4 | use serde::{Deserialize, Serialize}; 5 | use std::fmt; 6 | 7 | mod create_webhook; 8 | 9 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 10 | pub enum ServerIdOrName { 11 | ServerId(isize), 12 | ServerName(String), 13 | } 14 | 15 | impl fmt::Display for ServerIdOrName { 16 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 17 | match self { 18 | ServerIdOrName::ServerId(id) => write!(f, "{}", id), 19 | ServerIdOrName::ServerName(name) => write!(f, "{}", name), 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/api/message_streams.rs: -------------------------------------------------------------------------------- 1 | //! You'll find in templates sending related endpoints. 2 | 3 | use serde::{Deserialize, Serialize}; 4 | use std::fmt; 5 | 6 | mod delete_suppression; 7 | mod get_suppressions; 8 | 9 | pub use delete_suppression::*; 10 | pub use get_suppressions::*; 11 | 12 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] 13 | pub enum SuppressionStatusType { 14 | #[default] 15 | Deleted, 16 | Failed, 17 | } 18 | 19 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 20 | pub enum StreamIdOrName { 21 | StreamId(String), 22 | } 23 | 24 | impl fmt::Display for StreamIdOrName { 25 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 26 | match self { 27 | StreamIdOrName::StreamId(id) => write!(f, "{}", id), 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/release-plz.yml: -------------------------------------------------------------------------------- 1 | name: Release-plz 2 | # release-plz creates a pull request with the new versions, where it prepares the next release. 3 | # release-plz releases the unpublished packages. 4 | # see: https://marcoieni.github.io/release-plz/github/index.html for more example and configurations 5 | permissions: 6 | pull-requests: write 7 | contents: write 8 | 9 | on: 10 | push: 11 | branches: 12 | - main 13 | 14 | jobs: 15 | release-plz: 16 | name: Release-plz 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout repository 20 | uses: actions/checkout@v4 21 | with: 22 | fetch-depth: 0 23 | - name: Install Rust toolchain 24 | uses: dtolnay/rust-toolchain@stable 25 | - name: Run release-plz 26 | uses: MarcoIeni/release-plz-action@v0.5 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 14 | 15 | I tried this code: 16 | 17 | ```rust 18 | 19 | ``` 20 | 21 | I expected to see this happen: *explanation* 22 | 23 | Instead, this happened: *explanation* 24 | 25 | ### Meta 26 | 30 | 31 | `rustc --version --verbose`: 32 | ``` 33 | 34 | ``` 35 | 36 | 40 |
Backtrace 41 |

42 | 43 | ``` 44 | 45 | ``` 46 | 47 |

48 |
49 | -------------------------------------------------------------------------------- /src/api/server.rs: -------------------------------------------------------------------------------- 1 | //! You'll find in email sending related endpoints. 2 | 3 | pub use create_server::*; 4 | pub use get_server::*; 5 | use serde::{Deserialize, Serialize}; 6 | use std::fmt; 7 | 8 | mod create_server; 9 | mod get_server; 10 | 11 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 12 | pub enum ServerIdOrName { 13 | ServerId(isize), 14 | ServerName(String), 15 | } 16 | 17 | #[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)] 18 | pub enum DeliveryType { 19 | #[default] 20 | Live, 21 | Sandbox, 22 | } 23 | 24 | #[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)] 25 | pub enum ServerColor { 26 | #[default] 27 | Purple, 28 | Blue, 29 | Turquoise, 30 | Green, 31 | Red, 32 | Yellow, 33 | Grey, 34 | Orange, 35 | } 36 | 37 | impl fmt::Display for ServerIdOrName { 38 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 39 | match self { 40 | ServerIdOrName::ServerId(id) => write!(f, "{}", id), 41 | ServerIdOrName::ServerName(name) => write!(f, "{}", name), 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022 Pierre-Alexandre St-Jean 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /tests/send_email.rs: -------------------------------------------------------------------------------- 1 | // This is not a core part of the package, 2 | // but is instead a helper main that allows 3 | // manual testing against a Postmark account 4 | use std::env; 5 | 6 | use postmark::api::email::SendEmailRequest; 7 | use postmark::api::Body; 8 | use postmark::reqwest::PostmarkClient; 9 | use postmark::Query; 10 | 11 | #[tokio::test(flavor = "multi_thread")] 12 | #[ignore] 13 | async fn send_email() { 14 | println!("Started test"); 15 | 16 | let api_token = env::var("POSTMARK_API_TOKEN").expect("POSTMARK_API_TOKEN is not set"); 17 | 18 | println!("Loaded env variable"); 19 | 20 | let client = PostmarkClient::builder() 21 | .base_url("https://api.postmarkapp.com/") 22 | .server_token(api_token) 23 | .build(); 24 | 25 | println!("Created client"); 26 | 27 | let req = SendEmailRequest::builder() 28 | .from("dan@ourfructus.com") 29 | .to("customers@ourfructus.com") 30 | .body(Body::html("This is a basic e-mail test!".into())) 31 | .subject("Test") 32 | .build(); 33 | 34 | println!("Creaed request"); 35 | 36 | let resp = req.execute(&client).await; 37 | resp.unwrap(); 38 | } 39 | -------------------------------------------------------------------------------- /src/api/templates.rs: -------------------------------------------------------------------------------- 1 | //! You'll find in templates sending related endpoints. 2 | use serde::{Deserialize, Serialize}; 3 | use std::fmt; 4 | 5 | mod copy_templates; 6 | mod create_template; 7 | mod delete_template; 8 | mod edit_template; 9 | mod get_template; 10 | 11 | pub use copy_templates::*; 12 | pub use create_template::*; 13 | pub use delete_template::*; 14 | pub use edit_template::*; 15 | pub use get_template::*; 16 | 17 | #[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)] 18 | pub enum TemplateType { 19 | #[default] 20 | Standard, 21 | Layout, 22 | } 23 | 24 | #[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)] 25 | pub enum TemplateAction { 26 | #[default] 27 | Create, 28 | Edit, 29 | } 30 | 31 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 32 | pub enum TemplateIdOrAlias { 33 | TemplateId(isize), 34 | Alias(String), 35 | } 36 | 37 | impl fmt::Display for TemplateIdOrAlias { 38 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 39 | match self { 40 | TemplateIdOrAlias::TemplateId(id) => write!(f, "{}", id), 41 | TemplateIdOrAlias::Alias(alias) => write!(f, "{}", alias), 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "postmark" 3 | description = "Postmark rust client" 4 | license = "MIT OR Apache-2.0" 5 | authors = ["Pierre-Alexandre St-Jean "] 6 | repository = "https://github.com/pastjean/postmark-rs" 7 | homepage = "https://github.com/pastjean/postmark-rs" 8 | documentation = "https://docs.rs/postmark" 9 | keywords = ["postmark", "email", "e-mail", "http"] 10 | readme = "README.md" 11 | categories = ["api-bindings", "email", "web-programming::http-client"] 12 | version = "0.11.4" 13 | edition = "2018" 14 | 15 | [dependencies] 16 | async-trait = { version = "0.1" } 17 | bytes = { version = "1.6" } 18 | http = { version = "1.1" } 19 | reqwest = { version = "0.12", optional = true, default-features = false } 20 | serde = { version = "1.0", features = ["derive"] } 21 | serde_json = { version = "1.0" } 22 | thiserror = { version = "2.0" } 23 | typed-builder = { version = "0.21" } 24 | url = { version = "2.5" } 25 | indexmap = { version = "2.2", features = ["serde"], optional = true } 26 | time = { version = "0.3.17", features = ["serde-human-readable", "macros"] } 27 | 28 | [features] 29 | default = [] 30 | reqwest = ["dep:reqwest"] 31 | reqwest-native-tls = ["reqwest", "reqwest/native-tls"] 32 | reqwest-rustls-tls = ["reqwest", "reqwest/rustls-tls"] 33 | indexmap = ["dep:indexmap"] 34 | 35 | [dev-dependencies] 36 | httptest = { version = "0.16" } 37 | tokio = { version = "1.38", default-features = false, features = [ 38 | "rt", 39 | "macros", 40 | ] } 41 | 42 | # Getting all features for testing 43 | postmark = { path = ".", features = ["reqwest", "reqwest-rustls-tls"] } 44 | -------------------------------------------------------------------------------- /src/api.rs: -------------------------------------------------------------------------------- 1 | //! You'll find in `api` all the different predefined endpoints organized 2 | //! by Postmark api sections. 3 | //! 4 | //! In addition, some structures that are common to multiple endpoint API 5 | //! sections are included in here, specifically the definition of text 6 | //! and HTML based bodies. 7 | 8 | use serde::{Deserialize, Serialize}; 9 | 10 | pub mod bounce; 11 | pub mod email; 12 | pub mod message_streams; 13 | pub mod server; 14 | pub mod templates; 15 | pub mod webhooks; 16 | 17 | /// The body of a email message 18 | #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] 19 | #[serde(untagged)] 20 | pub enum Body { 21 | Text { 22 | #[serde(rename = "TextBody")] 23 | text: String, 24 | }, 25 | Html { 26 | #[serde(rename = "HtmlBody")] 27 | html: String, 28 | }, 29 | HtmlAndText { 30 | #[serde(rename = "HtmlBody")] 31 | html: String, 32 | #[serde(rename = "TextBody")] 33 | text: String, 34 | }, 35 | } 36 | 37 | impl Default for Body { 38 | fn default() -> Self { 39 | Body::Text { text: "".into() } 40 | } 41 | } 42 | 43 | impl Body { 44 | /// Constructor to create a text-only [`Body`] enum 45 | pub fn text(text: String) -> Self { 46 | Body::Text { text } 47 | } 48 | /// Constructor to create a html-only [`Body`] enum 49 | pub fn html(html: String) -> Self { 50 | Body::Html { html } 51 | } 52 | /// Constructor to create a text and html [`Body`] enum 53 | pub fn html_and_text(html: String, text: String) -> Self { 54 | Body::HtmlAndText { html, text } 55 | } 56 | } 57 | 58 | #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] 59 | pub struct HtmlAndText { 60 | #[serde(flatten, rename = "HtmlBody")] 61 | pub html: String, 62 | #[serde(flatten, rename = "TextBody")] 63 | pub text: String, 64 | } 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Postmark 2 | 3 | [![ci](https://github.com/pastjean/postmark-rs/actions/workflows/ci.yml/badge.svg)](https://github.com/pastjean/postmark-rs/actions/workflows/ci.yml) 4 | [![crates.io](https://img.shields.io/crates/v/postmark.svg)](https://crates.io/crates/postmark) 5 | [![Documentation](https://docs.rs/postmark/badge.svg)](https://docs.rs/postmark) 6 | [![License](https://img.shields.io/crates/l/postmark.svg)](#license) 7 | 8 | A rust library to query Postmark API. 9 | 10 | # Usage 11 | 12 | Add the crate dependency to your Cargo.toml: 13 | 14 | ```toml 15 | [dependencies] 16 | postmark = "x.y.z" 17 | ``` 18 | 19 | And use it, see documentation at: https://docs.rs/postmark. 20 | 21 | ```rust 22 | use postmark::api::email::SendEmailRequest; 23 | use postmark::api::Body; 24 | use postmark::reqwest::PostmarkClient; 25 | use postmark::Query; 26 | 27 | async fn send_email(){ 28 | let client = PostmarkClient::builder() 29 | .server_token("") 30 | .build(); 31 | 32 | let req = SendEmailRequest::builder() 33 | .from("me@example.com") 34 | .to("you@example.com") 35 | .body(Body::text("it's me, Mario!".to_string())) 36 | .build(); 37 | let resp = req.execute(&client).await; 38 | } 39 | ``` 40 | 41 | # Releasing a new version 42 | 43 | Prerequisite: 44 | 45 | ```sh 46 | cargo install cargo-release 47 | ``` 48 | 49 | On Release: 50 | 51 | ```sh 52 | cargo release --dry-run 53 | # check it does the good thing 54 | cargo release 55 | ``` 56 | 57 | # Thanks 58 | 59 | This crate's API design is heavily inspired by the article ["Designing Rust bindings for REST APIs](https://plume.benboeckel.net/~/JustAnotherBlog/designing-rust-bindings-for-rest-ap-is) by Ben Boeckel. 60 | 61 | # License 62 | 63 | postmark-rs is dual-licensed under either: 64 | 65 | - MIT License ([LICENSE-MIT](LICENSE-MIT)) 66 | - Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE)) 67 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # Based on https://github.com/actions-rs/meta/blob/master/recipes/quickstart.md 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | name: ci 10 | 11 | concurrency: 12 | group: ci.yml-${{ github.head_ref || github.ref }} 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | check: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout sources 20 | uses: actions/checkout@v4 21 | 22 | - name: Install stable toolchain 23 | uses: actions-rs/toolchain@v1 24 | with: 25 | profile: minimal 26 | toolchain: stable 27 | override: true 28 | 29 | - name: Run cargo check 30 | uses: actions-rs/cargo@v1 31 | with: 32 | command: check 33 | 34 | test: 35 | runs-on: ubuntu-latest 36 | steps: 37 | - name: Checkout sources 38 | uses: actions/checkout@v4 39 | 40 | - name: Install stable toolchain 41 | uses: actions-rs/toolchain@v1 42 | with: 43 | profile: minimal 44 | toolchain: stable 45 | override: true 46 | 47 | - name: Run cargo test 48 | uses: actions-rs/cargo@v1 49 | with: 50 | command: test 51 | 52 | lints: 53 | runs-on: ubuntu-latest 54 | steps: 55 | - name: Checkout sources 56 | uses: actions/checkout@v2 57 | 58 | - name: Install stable toolchain 59 | uses: actions-rs/toolchain@v1 60 | with: 61 | profile: minimal 62 | toolchain: stable 63 | override: true 64 | components: rustfmt, clippy 65 | 66 | - name: Run cargo fmt 67 | uses: actions-rs/cargo@v1 68 | with: 69 | command: fmt 70 | args: --all -- --check 71 | 72 | - name: Run cargo clippy 73 | uses: actions-rs/clippy-check@v1 74 | with: 75 | token: ${{ secrets.GITHUB_TOKEN }} 76 | args: --all-features 77 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Postmark is a HTTP client agnostic rust client to postmark. We 2 | //! Provide a `reqwest` implementation of a client that can be used pretty 3 | //! simply by initializing it and passing it into the execute function of a 4 | //! [`Query`], all [`Endpoint`] implement the Query trait. 5 | //! 6 | //! Some Endpoints are already provided to you. But if you need some that are 7 | //! not implemented you are not constrained to modified this crated, you can 8 | //! implement your own by implementing the [`Endpoint`] trait and it will 9 | //! work transparently with this library. 10 | //! 11 | //! To use the [`reqwest`] based client, you need to enable the feature `"reqwest"` 12 | //! You can also implement you own client by implementing the [`Client`] trait 13 | //! 14 | //! This crate is heavily inspired by the article ["Designing Rust bindings for REST APIs](https://plume.benboeckel.net/~/JustAnotherBlog/designing-rust-bindings-for-rest-ap-is) 15 | //! by Ben Boeckel and used in the [gitlab](https://crates.io/crates/gitlab) crate. 16 | //! It allows to have modular clients (someone wants to use something else than 17 | //! reqwest), and [`Endpoint`]s not supported by the library without needing to fork it. 18 | //! 19 | //! # Example: 20 | //! ``` 21 | //! use postmark::reqwest::PostmarkClient; 22 | //! use postmark::*; 23 | //! 24 | //! # async fn send_email(){ 25 | //! let client = PostmarkClient::builder() 26 | //! .base_url("https://api.postmarkapp.com/") 27 | //! .server_token("") 28 | //! .build(); 29 | //! 30 | //! let req = api::email::SendEmailRequest::builder() 31 | //! .from("me@example.com") 32 | //! .to("you@example.com") 33 | //! .body(api::Body::text("Hi, this is me!".to_string())) 34 | //! .build(); 35 | //! let resp = req.execute(&client).await; 36 | //! resp.unwrap(); 37 | //! # } 38 | //! ``` 39 | 40 | /// POSTMARK_API_URL is the default url to poke Postmark's API 41 | pub const POSTMARK_API_URL: &str = "https://api.postmarkapp.com/"; 42 | 43 | pub mod api; 44 | mod client; 45 | 46 | pub use client::*; 47 | 48 | #[cfg(feature = "reqwest")] 49 | pub mod reqwest; 50 | -------------------------------------------------------------------------------- /src/api/email/send_email_batch.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use super::send_email::{SendEmailRequest, SendEmailResponse}; 4 | use crate::Endpoint; 5 | 6 | /// Send multiple emails at once 7 | pub type SendEmailBatchRequest = Vec; 8 | /// Response for [`SendEmailBatchRequest`] 9 | pub type SendEmailBatchResponse = Vec; 10 | 11 | impl Endpoint for SendEmailBatchRequest { 12 | type Request = SendEmailBatchRequest; 13 | type Response = SendEmailBatchResponse; 14 | 15 | fn endpoint(&self) -> Cow<'static, str> { 16 | "/email/batch".into() 17 | } 18 | 19 | fn body(&self) -> &Self::Request { 20 | self 21 | } 22 | } 23 | 24 | #[cfg(test)] 25 | mod tests { 26 | use httptest::matchers::request; 27 | use httptest::{responders::*, Expectation, Server}; 28 | use serde_json::json; 29 | 30 | use crate::api::email::*; 31 | use crate::api::Body; 32 | use crate::reqwest::PostmarkClient; 33 | use crate::Query; 34 | 35 | #[tokio::test] 36 | pub async fn send_email_test() { 37 | let server = Server::run(); 38 | 39 | server.expect( 40 | Expectation::matching(request::method_path("POST", "/email/batch")).respond_with( 41 | json_encoded(json!([{ 42 | "To": "receiver@example.com", 43 | "SubmittedAt": "2014-02-17T07:25:01.4178645-05:00", 44 | "MessageID": "0a129aee-e1cd-480d-b08d-4f48548ff48d", 45 | "ErrorCode": 0, 46 | "Message": "OK" 47 | },{ 48 | "ErrorCode": 406, 49 | "Message": "You tried to send to a recipient that has been marked as inactive. Found inactive addresses: example@example.com. Inactive recipients are ones that have generated a hard bounce, a spam complaint, or a manual suppression." 50 | }])), 51 | ), 52 | ); 53 | 54 | let client = PostmarkClient::builder() 55 | .base_url(server.url("/").to_string()) 56 | .build(); 57 | 58 | let req_builder = SendEmailRequest::builder() 59 | .from("pa@example.com") 60 | .body(Body::text("hello matt".into())) 61 | .subject("hello"); 62 | 63 | let req: SendEmailBatchRequest = vec![ 64 | req_builder.clone().to("mathieu@example.com").build(), 65 | req_builder.to("pa@example.com").build(), 66 | ]; 67 | 68 | req.execute(&client) 69 | .await 70 | .expect("Should get a response and be able to json decode it"); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/api/email/send_email_batch_with_templates.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use super::send_email_batch::SendEmailBatchResponse; 6 | use super::send_email_with_template::SendEmailWithTemplateRequest; 7 | use crate::Endpoint; 8 | 9 | /// Send multiple emails at once 10 | #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] 11 | #[serde(rename_all = "PascalCase")] 12 | pub struct SendEmailBatchWithTemplatesRequest { 13 | pub messages: Vec, 14 | } 15 | 16 | impl Endpoint for SendEmailBatchWithTemplatesRequest { 17 | type Request = SendEmailBatchWithTemplatesRequest; 18 | type Response = SendEmailBatchResponse; 19 | 20 | fn endpoint(&self) -> Cow<'static, str> { 21 | "/email/batchWithTemplates".into() 22 | } 23 | 24 | fn body(&self) -> &Self::Request { 25 | self 26 | } 27 | } 28 | 29 | #[cfg(test)] 30 | mod tests { 31 | use httptest::matchers::request; 32 | use httptest::{responders::*, Expectation, Server}; 33 | use serde_json::json; 34 | 35 | use crate::api::email::*; 36 | use crate::reqwest::PostmarkClient; 37 | use crate::Query; 38 | 39 | #[tokio::test] 40 | pub async fn send_email_test() { 41 | let server = Server::run(); 42 | 43 | server.expect( 44 | Expectation::matching(request::method_path("POST", "/email/batchWithTemplates")).respond_with( 45 | json_encoded(json!([{ 46 | "To": "receiver@example.com", 47 | "SubmittedAt": "2014-02-17T07:25:01.4178645-05:00", 48 | "MessageID": "0a129aee-e1cd-480d-b08d-4f48548ff48d", 49 | "ErrorCode": 0, 50 | "Message": "OK" 51 | },{ 52 | "ErrorCode": 406, 53 | "Message": "You tried to send to a recipient that has been marked as inactive. Found inactive addresses: example@example.com. Inactive recipients are ones that have generated a hard bounce, a spam complaint, or a manual suppression." 54 | }])), 55 | ), 56 | ); 57 | 58 | let client = PostmarkClient::builder() 59 | .base_url(server.url("/").to_string()) 60 | .build(); 61 | 62 | let req_builder = SendEmailWithTemplateRequest::builder() 63 | .from("pa@example.com") 64 | .template_alias("my_template".to_string()); 65 | 66 | let req = SendEmailBatchWithTemplatesRequest { 67 | messages: vec![ 68 | req_builder.clone().to("mathieu@example.com").build(), 69 | req_builder.to("pa@example.com").build(), 70 | ], 71 | }; 72 | 73 | req.execute(&client) 74 | .await 75 | .expect("Should get a response and be able to json decode it"); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [0.11.4](https://github.com/pastjean/postmark-rs/compare/v0.11.3...v0.11.4) - 2025-08-07 11 | 12 | ### Other 13 | 14 | - Upgrade thiserror and typed-builder ([#43](https://github.com/pastjean/postmark-rs/pull/43)) 15 | - Update README.md 16 | # Changelog 17 | All notable changes to this project will be documented in this file. 18 | 19 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 20 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 21 | 22 | ## [Unreleased] 23 | 24 | ## [0.11.3](https://github.com/pastjean/postmark-rs/compare/v0.11.2...v0.11.3) - 2025-04-28 25 | 26 | ### Fixed 27 | 28 | - fix missing closing parenthesis in readme 29 | - fix the license readme button link 30 | 31 | ### Other 32 | 33 | - Make sure the license mit 'or' apache 2.0 is clear 34 | 35 | ## [0.11.2](https://github.com/pastjean/postmark-rs/compare/v0.11.1...v0.11.2) - 2025-04-08 36 | 37 | ### Other 38 | 39 | - Add error_for_status fn ([#37](https://github.com/pastjean/postmark-rs/pull/37)) 40 | 41 | ## [0.11.1](https://github.com/pastjean/postmark-rs/compare/v0.11.0...v0.11.1) - 2025-01-27 42 | 43 | ### Other 44 | 45 | - Fix Readme example ([#35](https://github.com/pastjean/postmark-rs/pull/35)) 46 | 47 | ## [0.11.0](https://github.com/pastjean/postmark-rs/compare/v0.10.2...v0.11.0) - 2024-09-03 48 | 49 | ### Other 50 | - Add functionality for servers, templates, webhooks ([#32](https://github.com/pastjean/postmark-rs/pull/32)) 51 | - Update actions checkout ([#29](https://github.com/pastjean/postmark-rs/pull/29)) 52 | 53 | ## [0.10.2](https://github.com/pastjean/postmark-rs/compare/v0.10.1...v0.10.2) - 2024-07-29 54 | 55 | ### Other 56 | - Implement send batch email with templates ([#27](https://github.com/pastjean/postmark-rs/pull/27)) 57 | 58 | ## [0.10.1](https://github.com/pastjean/postmark-rs/compare/v0.10.0...v0.10.1) - 2024-06-21 59 | 60 | ### Other 61 | - Update dependencies to latest version from 2024-06-21 62 | 63 | ## [0.10.0](https://github.com/pastjean/postmark-rs/compare/v0.9.2...v0.10.0) - 2023-11-21 64 | 65 | ### Other 66 | - Update dependencies ([#23](https://github.com/pastjean/postmark-rs/pull/23)) 67 | 68 | ## [0.9.2](https://github.com/pastjean/postmark-rs/compare/v0.9.1...v0.9.2) - 2023-09-06 69 | 70 | ### Other 71 | - Add TLS to test dependencies 72 | 73 | ## [0.9.1](https://github.com/pastjean/postmark-rs/compare/v0.9.0...v0.9.1) - 2023-09-05 74 | 75 | ### Other 76 | - Return send email with template to exported status ([#21](https://github.com/pastjean/postmark-rs/pull/21)) 77 | 78 | ## [0.9.0](https://github.com/pastjean/postmark-rs/compare/v0.8.1...v0.9.0) - 2023-08-31 79 | 80 | ### Other 81 | - Add a manual test (that is skipped) ([#17](https://github.com/pastjean/postmark-rs/pull/17)) 82 | - Implement edit and create template endpoints ([#13](https://github.com/pastjean/postmark-rs/pull/13)) 83 | - Update README.md ([#14](https://github.com/pastjean/postmark-rs/pull/14)) 84 | 85 | ## [0.8.1](https://github.com/pastjean/postmark-rs/compare/v0.8.0...v0.8.1) - 2023-06-14 86 | 87 | ### Other 88 | - cargo features and clippy happiness 89 | - new cargo.toml features && info on release-plz 90 | - Add release-plz as a auto releaser 91 | -------------------------------------------------------------------------------- /src/client.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use async_trait::async_trait; 4 | use bytes::Bytes; 5 | use http::{Request, Response}; 6 | use std::error::Error; 7 | use thiserror::Error; 8 | 9 | /// A trait for providing the necessary information for a single REST API endpoint. 10 | pub trait Endpoint { 11 | type Request: serde::Serialize + Send + Sync; 12 | type Response: serde::de::DeserializeOwned + Send + Sync; 13 | 14 | /// The path to the endpoint. 15 | fn endpoint(&self) -> Cow<'static, str>; 16 | /// The body for the endpoint. 17 | fn body(&self) -> &Self::Request; 18 | /// The http method for the endpoint 19 | fn method(&self) -> http::Method { 20 | http::Method::POST 21 | } 22 | } 23 | 24 | /// A trait which represents an asynchronous query which may be made to a Postmark client. 25 | #[async_trait] 26 | pub trait Query { 27 | /// The Result of executing a query 28 | type Result; 29 | /// Perform the query against the client. 30 | async fn execute(self, client: &C) -> Self::Result; 31 | } 32 | 33 | /// An error thrown by the [`Query`] trait 34 | #[derive(Debug, Error)] 35 | pub enum QueryError 36 | where 37 | E: Error + Send + Sync + 'static, 38 | { 39 | /// The client encountered an error. 40 | #[error("client error: {}", source)] 41 | Client { 42 | /// The client error. 43 | source: E, 44 | }, 45 | /// JSON deserialization from GitLab failed. 46 | #[error("could not parse JSON response: {}", source)] 47 | Json { 48 | /// The source of the error. 49 | #[from] 50 | source: serde_json::Error, 51 | }, 52 | /// Body data could not be created. 53 | #[error("failed to create form data: {}", source)] 54 | Body { 55 | /// The source of the error. 56 | #[from] 57 | source: http::Error, 58 | }, 59 | } 60 | 61 | impl QueryError 62 | where 63 | E: Error + Send + Sync + 'static, 64 | { 65 | /// Create an API error in a client error. 66 | pub fn client(source: E) -> Self { 67 | QueryError::Client { source } 68 | } 69 | } 70 | 71 | /// Extension method to all Endpoints to execute themselves againts a query 72 | #[async_trait] 73 | impl Query for T 74 | where 75 | T: Endpoint + Send + Sync, 76 | C: Client + Send + Sync, 77 | { 78 | /// An endpoint return it's Response or the Client's Error 79 | type Result = Result>; 80 | 81 | async fn execute(self, client: &C) -> Self::Result { 82 | let body = serde_json::to_vec(self.body())?; 83 | 84 | let http_req = http::Request::builder() 85 | .method(self.method()) 86 | .uri(String::from(self.endpoint())) 87 | .header("Accept", "application/json") 88 | .header("Content-Type", "application/json") 89 | .body(body.into())?; 90 | 91 | let response = client.execute(http_req).await.map_err(QueryError::client)?; 92 | 93 | Ok(serde_json::from_slice(response.body())?) 94 | } 95 | } 96 | 97 | /// A trait representing a client which can communicate with a Postmark instance. 98 | #[async_trait] 99 | pub trait Client { 100 | /// The errors which may occur for this client. 101 | type Error: Error + Send + Sync + 'static; 102 | /// Execute the request which was formed by [`Endpoint`] 103 | async fn execute(&self, req: Request) -> Result, Self::Error>; 104 | } 105 | -------------------------------------------------------------------------------- /src/api/server/get_server.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | use typed_builder::TypedBuilder; 5 | 6 | use crate::api::server::ServerIdOrName; 7 | use crate::Endpoint; 8 | 9 | #[derive(Debug, Clone, PartialEq, Serialize)] 10 | #[serde(rename_all = "PascalCase")] 11 | #[derive(TypedBuilder)] 12 | pub struct GetServerRequest { 13 | #[serde(skip)] 14 | pub server_id: ServerIdOrName, 15 | } 16 | 17 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 18 | #[serde(rename_all = "PascalCase")] 19 | pub struct GetServerResponse { 20 | #[serde(rename = "ID")] 21 | pub id: isize, 22 | pub name: String, 23 | pub api_tokens: Vec, 24 | } 25 | 26 | impl Endpoint for GetServerRequest { 27 | type Request = GetServerRequest; 28 | type Response = GetServerResponse; 29 | 30 | fn endpoint(&self) -> Cow<'static, str> { 31 | format!("/servers/{}", self.server_id).into() 32 | } 33 | 34 | fn body(&self) -> &Self::Request { 35 | self 36 | } 37 | 38 | fn method(&self) -> http::Method { 39 | http::Method::GET 40 | } 41 | } 42 | 43 | #[cfg(test)] 44 | mod tests { 45 | use httptest::matchers::request; 46 | use httptest::{responders::*, Expectation, Server}; 47 | use serde_json::json; 48 | 49 | use crate::reqwest::PostmarkClient; 50 | use crate::Query; 51 | 52 | use super::*; 53 | 54 | const SERVER_ID: isize = 123456; 55 | #[tokio::test] 56 | pub async fn get_server() { 57 | let server = Server::run(); 58 | 59 | server.expect( 60 | Expectation::matching(request::method_path("GET", format!("/servers/{SERVER_ID}"))) 61 | .respond_with(json_encoded(json!({ 62 | "ID": 1, 63 | "Name": "Staging Testing", 64 | "ApiTokens": [ 65 | "server token" 66 | ], 67 | "Color": "red", 68 | "SmtpApiActivated": true, 69 | "RawEmailEnabled": false, 70 | "DeliveryType": "Live", 71 | "ServerLink": "https://postmarkapp.com/servers/1/streams", 72 | "InboundAddress": "yourhash@inbound.postmarkapp.com", 73 | "InboundHookUrl": "http://hooks.example.com/inbound", 74 | "BounceHookUrl": "http://hooks.example.com/bounce", 75 | "OpenHookUrl": "http://hooks.example.com/open", 76 | "DeliveryHookUrl": "http://hooks.example.com/delivery", 77 | "PostFirstOpenOnly": false, 78 | "InboundDomain": "", 79 | "InboundHash": "yourhash", 80 | "InboundSpamThreshold": 0, 81 | "TrackOpens": false, 82 | "TrackLinks": "None", 83 | "IncludeBounceContentInHook": true, 84 | "ClickHookUrl": "http://hooks.example.com/click", 85 | "EnableSmtpApiErrorHooks": false 86 | }))), 87 | ); 88 | 89 | let client = PostmarkClient::builder() 90 | .base_url(server.url("/").to_string()) 91 | .build(); 92 | 93 | let req = GetServerRequest::builder() 94 | .server_id(ServerIdOrName::ServerId(SERVER_ID)) 95 | .build(); 96 | 97 | print!("{}\n", req.endpoint()); 98 | 99 | req.execute(&client) 100 | .await 101 | .expect("Should get a response and be able to json decode it"); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/api/templates/copy_templates.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | use typed_builder::TypedBuilder; 5 | 6 | use crate::api::templates::{TemplateAction, TemplateType}; 7 | use crate::Endpoint; 8 | 9 | #[derive(Debug, Clone, PartialEq, Serialize)] 10 | #[serde(rename_all = "PascalCase")] 11 | #[derive(TypedBuilder)] 12 | pub struct CopyTemplatesRequest { 13 | #[serde(rename = "SourceServerID")] 14 | pub source_server_id: isize, 15 | #[serde(rename = "DestinationServerID")] 16 | pub destination_server_id: isize, 17 | #[builder(default = true)] 18 | pub perform_changes: bool, 19 | } 20 | 21 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 22 | #[serde(rename_all = "PascalCase")] 23 | pub struct CopyTemplatesResponse { 24 | pub total_count: isize, 25 | pub templates: Vec