Skip to main content

smbcloud_email_sdk/
message.rs

1use serde::{Deserialize, Serialize};
2use serde_repr::{Deserialize_repr, Serialize_repr};
3
4/// Delivery status of a transactional email, mirroring the server-side
5/// integer-backed enum. Deserialized with `serde_repr` from the plain integer
6/// the API returns.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize_repr, Deserialize_repr)]
8#[repr(u8)]
9pub enum EmailStatus {
10    Queued = 0,
11    Sent = 1,
12    Delivered = 2,
13    Bounced = 3,
14    Complained = 4,
15    Failed = 5,
16}
17
18/// One attachment on an outbound email. `content_base64` is the file content
19/// encoded as standard base64; the server reassembles it into the MIME message.
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct Attachment {
22    pub filename: String,
23    pub content_base64: String,
24}
25
26/// An outbound transactional email.
27///
28/// Build one with [`SendEmail::new`] and the builder helpers, then hand it to
29/// [`crate::EmailClient::send`]. At least one of `html` or `text` is required;
30/// `from` must be on the API key's verified domain.
31#[derive(Debug, Clone, Default, Serialize, Deserialize)]
32#[serde(default)]
33pub struct SendEmail {
34    pub from: String,
35    pub to: Vec<String>,
36    #[serde(skip_serializing_if = "Vec::is_empty")]
37    pub cc: Vec<String>,
38    #[serde(skip_serializing_if = "Vec::is_empty")]
39    pub bcc: Vec<String>,
40    #[serde(skip_serializing_if = "Vec::is_empty")]
41    pub reply_to: Vec<String>,
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub subject: Option<String>,
44    #[serde(skip_serializing_if = "Option::is_none")]
45    pub html: Option<String>,
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub text: Option<String>,
48    #[serde(skip_serializing_if = "Vec::is_empty")]
49    pub attachments: Vec<Attachment>,
50    #[serde(skip_serializing_if = "std::collections::HashMap::is_empty")]
51    pub headers: std::collections::HashMap<String, String>,
52    #[serde(skip_serializing_if = "std::collections::HashMap::is_empty")]
53    pub tags: std::collections::HashMap<String, String>,
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub idempotency_key: Option<String>,
56}
57
58impl SendEmail {
59    /// Start a message from a sender address and one or more recipients.
60    pub fn new(from: impl Into<String>, to: impl IntoIterator<Item = impl Into<String>>) -> Self {
61        SendEmail {
62            from: from.into(),
63            to: to.into_iter().map(Into::into).collect(),
64            ..Default::default()
65        }
66    }
67
68    pub fn subject(mut self, subject: impl Into<String>) -> Self {
69        self.subject = Some(subject.into());
70        self
71    }
72
73    pub fn html(mut self, html: impl Into<String>) -> Self {
74        self.html = Some(html.into());
75        self
76    }
77
78    pub fn text(mut self, text: impl Into<String>) -> Self {
79        self.text = Some(text.into());
80        self
81    }
82
83    pub fn cc(mut self, cc: impl IntoIterator<Item = impl Into<String>>) -> Self {
84        self.cc = cc.into_iter().map(Into::into).collect();
85        self
86    }
87
88    pub fn bcc(mut self, bcc: impl IntoIterator<Item = impl Into<String>>) -> Self {
89        self.bcc = bcc.into_iter().map(Into::into).collect();
90        self
91    }
92
93    pub fn reply_to(mut self, reply_to: impl IntoIterator<Item = impl Into<String>>) -> Self {
94        self.reply_to = reply_to.into_iter().map(Into::into).collect();
95        self
96    }
97
98    pub fn attachment(mut self, attachment: Attachment) -> Self {
99        self.attachments.push(attachment);
100        self
101    }
102
103    pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
104        self.headers.insert(key.into(), value.into());
105        self
106    }
107
108    pub fn tag(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
109        self.tags.insert(key.into(), value.into());
110        self
111    }
112
113    /// Make a retried send safe: the same key returns the original message
114    /// instead of sending again.
115    pub fn idempotency_key(mut self, key: impl Into<String>) -> Self {
116        self.idempotency_key = Some(key.into());
117        self
118    }
119}
120
121/// One delivery event in a message's timeline (delivered, bounced, opened, …).
122/// Present on [`EmailMessage`] only when fetched via
123/// [`crate::EmailClient::get_message`].
124#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct EmailEvent {
126    pub id: String,
127    pub event_type: u8,
128    #[serde(default)]
129    pub payload: serde_json::Value,
130    pub occurred_at: String,
131}
132
133/// A transactional email as returned by the API: the send result and, when
134/// fetched by id, its delivery status and event timeline.
135#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct EmailMessage {
137    pub id: String,
138    pub status: EmailStatus,
139    pub provider_message_id: Option<String>,
140    pub idempotency_key: Option<String>,
141    pub from_email: String,
142    #[serde(default)]
143    pub to_emails: Vec<String>,
144    pub subject: Option<String>,
145    pub sent_at: Option<String>,
146    pub created_at: Option<String>,
147    /// Populated by `get_message`; empty for the bare send response.
148    #[serde(default)]
149    pub events: Vec<EmailEvent>,
150}