use std::collections::HashMap;
use std::sync::Arc;
use reqwest::Method;
use serde::{Deserialize, Serialize};
use crate::config::Config;
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum EmailState {
Scheduled,
Delivered,
Bounced,
Failed,
#[serde(untagged)]
Unknown(String),
}
impl std::fmt::Display for EmailState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Scheduled => write!(f, "scheduled"),
Self::Delivered => write!(f, "delivered"),
Self::Bounced => write!(f, "bounced"),
Self::Failed => write!(f, "failed"),
Self::Unknown(s) => write!(f, "{s}"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum ScheduledEmailState {
Submitted,
Generating,
Scheduled,
Delivered,
Bounced,
Failed,
Unknown,
#[serde(untagged)]
Other(String),
}
impl std::fmt::Display for ScheduledEmailState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Submitted => write!(f, "submitted"),
Self::Generating => write!(f, "generating"),
Self::Scheduled => write!(f, "scheduled"),
Self::Delivered => write!(f, "delivered"),
Self::Bounced => write!(f, "bounced"),
Self::Failed => write!(f, "failed"),
Self::Unknown => write!(f, "unknown"),
Self::Other(s) => write!(f, "{s}"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum EventType {
Injection,
Delivery,
Bounce,
Delay,
OutOfBand,
SpamComplaint,
PolicyRejection,
Click,
Open,
InitialOpen,
AmpClick,
AmpOpen,
AmpInitialOpen,
GenerationFailure,
GenerationRejection,
ListUnsubscribe,
LinkUnsubscribe,
#[serde(untagged)]
Unknown(String),
}
impl std::fmt::Display for EventType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Injection => write!(f, "injection"),
Self::Delivery => write!(f, "delivery"),
Self::Bounce => write!(f, "bounce"),
Self::Delay => write!(f, "delay"),
Self::OutOfBand => write!(f, "out_of_band"),
Self::SpamComplaint => write!(f, "spam_complaint"),
Self::PolicyRejection => write!(f, "policy_rejection"),
Self::Click => write!(f, "click"),
Self::Open => write!(f, "open"),
Self::InitialOpen => write!(f, "initial_open"),
Self::AmpClick => write!(f, "amp_click"),
Self::AmpOpen => write!(f, "amp_open"),
Self::AmpInitialOpen => write!(f, "amp_initial_open"),
Self::GenerationFailure => write!(f, "generation_failure"),
Self::GenerationRejection => write!(f, "generation_rejection"),
Self::ListUnsubscribe => write!(f, "list_unsubscribe"),
Self::LinkUnsubscribe => write!(f, "link_unsubscribe"),
Self::Unknown(s) => write!(f, "{s}"),
}
}
}
#[derive(Clone, Debug)]
pub struct EmailsSvc(pub(crate) Arc<Config>);
impl EmailsSvc {
#[maybe_async::maybe_async]
pub async fn send(&self, email: CreateEmailOptions) -> crate::Result<SendEmailResponse> {
let request = self.0.build(Method::POST, "/emails").json(&email);
let response = self.0.send(request).await?;
let wrapper = response.json::<SendEmailResponseWrapper>().await?;
Ok(wrapper.data)
}
#[maybe_async::maybe_async]
pub async fn send_with_quota(
&self,
email: CreateEmailOptions,
) -> crate::Result<SendEmailWithQuotaResponse> {
let request = self.0.build(Method::POST, "/emails").json(&email);
let response = self.0.send(request).await?;
let quota = QuotaInfo::from_headers(response.headers());
let wrapper = response.json::<SendEmailResponseWrapper>().await?;
Ok(SendEmailWithQuotaResponse {
response: wrapper.data,
quota,
})
}
#[maybe_async::maybe_async]
pub async fn list(&self, options: ListEmailsOptions) -> crate::Result<ListEmailsResponse> {
let mut request = self.0.build(Method::GET, "/emails");
if let Some(per_page) = options.per_page {
request = request.query(&[("per_page", per_page.to_string())]);
}
if let Some(ref cursor) = options.cursor {
request = request.query(&[("cursor", cursor.as_str())]);
}
if let Some(ref recipients) = options.recipients {
request = request.query(&[("recipients", recipients.as_str())]);
}
if let Some(ref from) = options.from {
request = request.query(&[("from", from.as_str())]);
}
if let Some(ref to) = options.to {
request = request.query(&[("to", to.as_str())]);
}
let response = self.0.send(request).await?;
let wrapper = response.json::<ListEmailsResponseWrapper>().await?;
Ok(wrapper.data)
}
#[maybe_async::maybe_async]
pub async fn get(
&self,
request_id: &str,
from: Option<&str>,
to: Option<&str>,
) -> crate::Result<GetEmailResponse> {
let path = format!("/emails/{request_id}");
let mut request = self.0.build(Method::GET, &path);
if let Some(from) = from {
request = request.query(&[("from", from)]);
}
if let Some(to) = to {
request = request.query(&[("to", to)]);
}
let response = self.0.send(request).await?;
let wrapper = response.json::<GetEmailResponseWrapper>().await?;
Ok(wrapper.data)
}
#[maybe_async::maybe_async]
pub async fn list_events(
&self,
options: ListEmailEventsOptions,
) -> crate::Result<ListEmailEventsResponse> {
let mut request = self.0.build(Method::GET, "/emails/events");
if let Some(ref events) = options.events {
let joined = events.join(",");
request = request.query(&[("events", &joined)]);
}
if let Some(ref recipients) = options.recipients {
let joined = recipients.join(",");
request = request.query(&[("recipients", &joined)]);
}
if let Some(ref from) = options.from {
request = request.query(&[("from", from.as_str())]);
}
if let Some(ref to) = options.to {
request = request.query(&[("to", to.as_str())]);
}
if let Some(per_page) = options.per_page {
request = request.query(&[("per_page", per_page.to_string())]);
}
if let Some(ref cursor) = options.cursor {
request = request.query(&[("cursor", cursor.as_str())]);
}
if let Some(ref transmissions) = options.transmissions {
request = request.query(&[("transmissions", transmissions.as_str())]);
}
if let Some(ref bounce_classes) = options.bounce_classes {
request = request.query(&[("bounce_classes", bounce_classes.as_str())]);
}
let response = self.0.send(request).await?;
let wrapper = response.json::<ListEmailEventsResponseWrapper>().await?;
Ok(wrapper.data)
}
#[maybe_async::maybe_async]
pub async fn schedule(
&self,
options: ScheduleEmailOptions,
) -> crate::Result<SendEmailResponse> {
let request = self
.0
.build(Method::POST, "/emails/scheduled")
.json(&options);
let response = self.0.send(request).await?;
let wrapper = response.json::<SendEmailResponseWrapper>().await?;
Ok(wrapper.data)
}
#[maybe_async::maybe_async]
pub async fn schedule_with_quota(
&self,
options: ScheduleEmailOptions,
) -> crate::Result<SendEmailWithQuotaResponse> {
let request = self
.0
.build(Method::POST, "/emails/scheduled")
.json(&options);
let response = self.0.send(request).await?;
let quota = QuotaInfo::from_headers(response.headers());
let wrapper = response.json::<SendEmailResponseWrapper>().await?;
Ok(SendEmailWithQuotaResponse {
response: wrapper.data,
quota,
})
}
#[maybe_async::maybe_async]
pub async fn get_scheduled(
&self,
transmission_id: &str,
) -> crate::Result<ScheduledTransmission> {
let path = format!("/emails/scheduled/{transmission_id}");
let request = self.0.build(Method::GET, &path);
let response = self.0.send(request).await?;
let wrapper = response
.json::<ScheduledTransmissionResponseWrapper>()
.await?;
Ok(wrapper.data)
}
#[maybe_async::maybe_async]
pub async fn cancel_scheduled(&self, transmission_id: &str) -> crate::Result<()> {
let path = format!("/emails/scheduled/{transmission_id}");
let request = self.0.build(Method::DELETE, &path);
self.0.send(request).await?;
Ok(())
}
}
#[must_use]
#[derive(Debug, Clone, Serialize)]
pub struct CreateEmailOptions {
from: String,
#[serde(skip_serializing_if = "Option::is_none")]
from_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
subject: Option<String>,
to: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
cc: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
bcc: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
reply_to: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
reply_to_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
html: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
text: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
amp_html: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
project_id: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
template_slug: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
template_version: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
tag: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
metadata: Option<HashMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
headers: Option<HashMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
substitution_data: Option<HashMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
options: Option<EmailOptions>,
#[serde(skip_serializing_if = "Option::is_none")]
attachments: Option<Vec<Attachment>>,
}
impl CreateEmailOptions {
pub fn new<T, A>(from: impl Into<String>, to: T, subject: impl Into<String>) -> Self
where
T: IntoIterator<Item = A>,
A: Into<String>,
{
Self {
from: from.into(),
from_name: None,
subject: Some(subject.into()),
to: to.into_iter().map(Into::into).collect(),
cc: None,
bcc: None,
reply_to: None,
reply_to_name: None,
html: None,
text: None,
amp_html: None,
project_id: None,
template_slug: None,
template_version: None,
tag: None,
metadata: None,
headers: None,
substitution_data: None,
options: None,
attachments: None,
}
}
pub fn new_with_template<T, A>(
from: impl Into<String>,
to: T,
template_slug: impl Into<String>,
) -> Self
where
T: IntoIterator<Item = A>,
A: Into<String>,
{
Self {
from: from.into(),
from_name: None,
subject: None,
to: to.into_iter().map(Into::into).collect(),
cc: None,
bcc: None,
reply_to: None,
reply_to_name: None,
html: None,
text: None,
amp_html: None,
project_id: None,
template_slug: Some(template_slug.into()),
template_version: None,
tag: None,
metadata: None,
headers: None,
substitution_data: None,
options: None,
attachments: None,
}
}
#[inline]
pub fn with_from_name(mut self, name: impl Into<String>) -> Self {
self.from_name = Some(name.into());
self
}
#[inline]
pub fn with_subject(mut self, subject: impl Into<String>) -> Self {
self.subject = Some(subject.into());
self
}
#[inline]
pub fn with_cc(mut self, address: impl Into<String>) -> Self {
self.cc.get_or_insert_with(Vec::new).push(address.into());
self
}
#[inline]
pub fn with_bcc(mut self, address: impl Into<String>) -> Self {
self.bcc.get_or_insert_with(Vec::new).push(address.into());
self
}
#[inline]
pub fn with_reply_to(mut self, address: impl Into<String>) -> Self {
self.reply_to = Some(address.into());
self
}
#[inline]
pub fn with_reply_to_name(mut self, name: impl Into<String>) -> Self {
self.reply_to_name = Some(name.into());
self
}
#[inline]
pub fn with_html(mut self, html: impl Into<String>) -> Self {
self.html = Some(html.into());
self
}
#[inline]
pub fn with_text(mut self, text: impl Into<String>) -> Self {
self.text = Some(text.into());
self
}
#[inline]
pub fn with_amp_html(mut self, amp_html: impl Into<String>) -> Self {
self.amp_html = Some(amp_html.into());
self
}
#[inline]
pub fn with_template(mut self, slug: impl Into<String>) -> Self {
self.template_slug = Some(slug.into());
self
}
#[inline]
pub fn with_template_version(mut self, version: u32) -> Self {
self.template_version = Some(version);
self
}
#[inline]
pub fn with_project_id(mut self, project_id: u64) -> Self {
self.project_id = Some(project_id);
self
}
#[inline]
pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
self.tag = Some(tag.into());
self
}
#[inline]
pub fn with_substitution(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.substitution_data
.get_or_insert_with(HashMap::new)
.insert(key.into(), value.into());
self
}
#[inline]
pub fn with_substitution_data(mut self, data: HashMap<String, String>) -> Self {
self.substitution_data = Some(data);
self
}
#[inline]
pub fn with_metadata_entry(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata
.get_or_insert_with(HashMap::new)
.insert(key.into(), value.into());
self
}
#[inline]
pub fn with_metadata(mut self, metadata: HashMap<String, String>) -> Self {
self.metadata = Some(metadata);
self
}
#[inline]
pub fn with_header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.headers
.get_or_insert_with(HashMap::new)
.insert(key.into(), value.into());
self
}
#[inline]
pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
self.headers = Some(headers);
self
}
#[inline]
pub fn with_attachment(mut self, attachment: Attachment) -> Self {
self.attachments
.get_or_insert_with(Vec::new)
.push(attachment);
self
}
#[inline]
pub fn with_click_tracking(mut self, enabled: bool) -> Self {
self.options
.get_or_insert_with(EmailOptions::default)
.click_tracking = Some(enabled);
self
}
#[inline]
pub fn with_open_tracking(mut self, enabled: bool) -> Self {
self.options
.get_or_insert_with(EmailOptions::default)
.open_tracking = Some(enabled);
self
}
#[inline]
pub fn with_transactional(mut self, transactional: bool) -> Self {
self.options
.get_or_insert_with(EmailOptions::default)
.transactional = Some(transactional);
self
}
#[inline]
pub fn with_inline_css(mut self, enabled: bool) -> Self {
self.options
.get_or_insert_with(EmailOptions::default)
.inline_css = Some(enabled);
self
}
#[inline]
pub fn with_perform_substitutions(mut self, enabled: bool) -> Self {
self.options
.get_or_insert_with(EmailOptions::default)
.perform_substitutions = Some(enabled);
self
}
}
#[must_use]
#[derive(Debug, Default, Clone, Serialize)]
pub struct EmailOptions {
#[serde(skip_serializing_if = "Option::is_none")]
pub click_tracking: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub open_tracking: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub transactional: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub inline_css: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub perform_substitutions: Option<bool>,
}
#[must_use]
#[derive(Debug, Clone, Serialize)]
pub struct Attachment {
pub name: String,
#[serde(rename = "type")]
pub content_type: String,
pub data: String,
}
impl Attachment {
pub fn new(
name: impl Into<String>,
content_type: impl Into<String>,
data: impl Into<String>,
) -> Self {
Self {
name: name.into(),
content_type: content_type.into(),
data: data.into(),
}
}
}
#[must_use]
#[derive(Debug, Default, Clone)]
pub struct ListEmailsOptions {
per_page: Option<u32>,
cursor: Option<String>,
recipients: Option<String>,
from: Option<String>,
to: Option<String>,
}
impl ListEmailsOptions {
pub fn new() -> Self {
Self::default()
}
#[inline]
pub fn per_page(mut self, per_page: u32) -> Self {
self.per_page = Some(per_page);
self
}
#[inline]
pub fn cursor(mut self, cursor: impl Into<String>) -> Self {
self.cursor = Some(cursor.into());
self
}
#[inline]
pub fn recipients(mut self, recipients: impl Into<String>) -> Self {
self.recipients = Some(recipients.into());
self
}
#[inline]
pub fn from_date(mut self, from: impl Into<String>) -> Self {
self.from = Some(from.into());
self
}
#[inline]
pub fn to_date(mut self, to: impl Into<String>) -> Self {
self.to = Some(to.into());
self
}
}
#[must_use]
#[derive(Debug, Default, Clone)]
pub struct ListEmailEventsOptions {
events: Option<Vec<String>>,
recipients: Option<Vec<String>>,
from: Option<String>,
to: Option<String>,
per_page: Option<u32>,
cursor: Option<String>,
transmissions: Option<String>,
bounce_classes: Option<String>,
}
impl ListEmailEventsOptions {
pub fn new() -> Self {
Self::default()
}
#[inline]
pub fn events(mut self, events: Vec<String>) -> Self {
self.events = Some(events);
self
}
#[inline]
pub fn recipients(mut self, recipients: Vec<String>) -> Self {
self.recipients = Some(recipients);
self
}
#[inline]
pub fn from_date(mut self, from: impl Into<String>) -> Self {
self.from = Some(from.into());
self
}
#[inline]
pub fn to_date(mut self, to: impl Into<String>) -> Self {
self.to = Some(to.into());
self
}
#[inline]
pub fn per_page(mut self, per_page: u32) -> Self {
self.per_page = Some(per_page);
self
}
#[inline]
pub fn cursor(mut self, cursor: impl Into<String>) -> Self {
self.cursor = Some(cursor.into());
self
}
#[inline]
pub fn transmissions(mut self, transmissions: impl Into<String>) -> Self {
self.transmissions = Some(transmissions.into());
self
}
#[inline]
pub fn bounce_classes(mut self, bounce_classes: impl Into<String>) -> Self {
self.bounce_classes = Some(bounce_classes.into());
self
}
}
#[must_use]
#[derive(Debug, Clone, Serialize)]
pub struct ScheduleEmailOptions {
#[serde(flatten)]
pub email: CreateEmailOptions,
pub scheduled_at: String,
}
impl ScheduleEmailOptions {
pub fn new(email: CreateEmailOptions, scheduled_at: impl Into<String>) -> Self {
Self {
email,
scheduled_at: scheduled_at.into(),
}
}
}
#[derive(Debug, Deserialize)]
struct SendEmailResponseWrapper {
#[allow(dead_code)]
message: String,
data: SendEmailResponse,
}
#[derive(Debug, Clone, Deserialize)]
pub struct SendEmailResponse {
pub request_id: String,
pub accepted: u32,
pub rejected: u32,
}
#[derive(Debug, Clone)]
pub struct SendEmailWithQuotaResponse {
pub response: SendEmailResponse,
pub quota: Option<QuotaInfo>,
}
#[derive(Debug, Clone)]
pub struct QuotaInfo {
pub monthly_limit: Option<u64>,
pub monthly_remaining: Option<u64>,
pub monthly_reset: Option<u64>,
pub daily_limit: Option<u64>,
pub daily_remaining: Option<u64>,
pub daily_reset: Option<u64>,
}
impl QuotaInfo {
fn from_headers(headers: &reqwest::header::HeaderMap) -> Option<Self> {
let get = |name: &str| -> Option<u64> { headers.get(name)?.to_str().ok()?.parse().ok() };
let info = Self {
monthly_limit: get("X-Monthly-Limit"),
monthly_remaining: get("X-Monthly-Remaining"),
monthly_reset: get("X-Monthly-Reset"),
daily_limit: get("X-Daily-Limit"),
daily_remaining: get("X-Daily-Remaining"),
daily_reset: get("X-Daily-Reset"),
};
if info.monthly_limit.is_some()
|| info.monthly_remaining.is_some()
|| info.daily_limit.is_some()
|| info.daily_remaining.is_some()
{
Some(info)
} else {
None
}
}
}
#[derive(Debug, Deserialize)]
struct ListEmailsResponseWrapper {
#[allow(dead_code)]
message: String,
data: ListEmailsResponse,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ListEmailsResponse {
pub events: SentEmailEventsData,
}
#[derive(Debug, Clone, Deserialize)]
pub struct SentEmailEventsData {
pub data: Vec<SentEmailListItem>,
pub total_count: u64,
pub from: Option<String>,
pub to: Option<String>,
pub pagination: Pagination,
}
#[derive(Debug, Clone, Deserialize)]
pub struct SentEmailListItem {
pub event_id: String,
#[serde(rename = "type")]
pub event_type: EventType,
pub timestamp: String,
#[serde(default)]
pub request_id: Option<String>,
#[serde(default)]
pub message_id: Option<String>,
#[serde(default)]
pub subject: Option<String>,
#[serde(default)]
pub friendly_from: Option<String>,
#[serde(default)]
pub sending_domain: Option<String>,
#[serde(default)]
pub rcpt_to: Option<String>,
#[serde(default)]
pub raw_rcpt_to: Option<String>,
#[serde(default)]
pub recipient_domain: Option<String>,
#[serde(default)]
pub mailbox_provider: Option<String>,
#[serde(default)]
pub mailbox_provider_region: Option<String>,
#[serde(default)]
pub sending_ip: Option<String>,
#[serde(default)]
pub click_tracking: Option<bool>,
#[serde(default)]
pub open_tracking: Option<bool>,
#[serde(default)]
pub transactional: Option<bool>,
#[serde(default)]
pub msg_size: Option<u64>,
#[serde(default)]
pub injection_time: Option<String>,
#[serde(default)]
pub rcpt_meta: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Pagination {
pub next_cursor: Option<String>,
pub per_page: u32,
}
#[derive(Debug, Deserialize)]
struct GetEmailResponseWrapper {
#[allow(dead_code)]
message: String,
data: GetEmailResponse,
}
#[derive(Debug, Clone, Deserialize)]
pub struct GetEmailResponse {
pub transmission_id: String,
pub state: EmailState,
pub from: String,
#[serde(default)]
pub from_name: Option<String>,
pub subject: String,
pub recipients: Vec<String>,
pub num_recipients: u32,
#[serde(default)]
pub scheduled_at: Option<String>,
pub events: Vec<EmailEvent>,
}
#[derive(Debug, Deserialize)]
struct ListEmailEventsResponseWrapper {
#[allow(dead_code)]
message: String,
data: ListEmailEventsResponse,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ListEmailEventsResponse {
pub events: EmailEventsData,
}
#[derive(Debug, Clone, Deserialize)]
pub struct EmailEventsData {
pub data: Vec<EmailEvent>,
pub total_count: u64,
pub from: Option<String>,
pub to: Option<String>,
pub pagination: Pagination,
}
#[derive(Debug, Clone, Deserialize)]
pub struct EmailEvent {
pub event_id: String,
#[serde(rename = "type")]
pub event_type: EventType,
pub timestamp: String,
#[serde(default)]
pub request_id: Option<String>,
#[serde(default)]
pub rcpt_to: Option<String>,
#[serde(default)]
pub raw_rcpt_to: Option<String>,
#[serde(default)]
pub recipient_domain: Option<String>,
#[serde(default)]
pub mailbox_provider: Option<String>,
#[serde(default)]
pub mailbox_provider_region: Option<String>,
#[serde(default)]
pub message_id: Option<String>,
#[serde(default)]
pub subject: Option<String>,
#[serde(default)]
pub friendly_from: Option<String>,
#[serde(default)]
pub sending_domain: Option<String>,
#[serde(default)]
pub sending_ip: Option<String>,
#[serde(default)]
pub click_tracking: Option<bool>,
#[serde(default)]
pub open_tracking: Option<bool>,
#[serde(default)]
pub transactional: Option<bool>,
#[serde(default)]
pub msg_size: Option<u64>,
#[serde(default)]
pub injection_time: Option<String>,
#[serde(default)]
pub rcpt_meta: Option<serde_json::Value>,
#[serde(default)]
pub campaign_id: Option<String>,
#[serde(default)]
pub template_id: Option<String>,
#[serde(default)]
pub template_version: Option<String>,
#[serde(default)]
pub ip_pool: Option<String>,
#[serde(default)]
pub msg_from: Option<String>,
#[serde(default)]
pub rcpt_type: Option<String>,
#[serde(default)]
pub rcpt_tags: Option<Vec<String>>,
#[serde(default)]
pub amp_enabled: Option<bool>,
#[serde(default)]
pub delv_method: Option<String>,
#[serde(default)]
pub recv_method: Option<String>,
#[serde(default)]
pub routing_domain: Option<String>,
#[serde(default)]
pub scheduled_time: Option<String>,
#[serde(default)]
pub ab_test_id: Option<String>,
#[serde(default)]
pub ab_test_version: Option<String>,
#[serde(default)]
pub bounce_class: Option<i64>,
#[serde(default)]
pub error_code: Option<String>,
#[serde(default)]
pub reason: Option<String>,
#[serde(default)]
pub raw_reason: Option<String>,
#[serde(default)]
pub num_retries: Option<u32>,
#[serde(default)]
pub device_token: Option<String>,
#[serde(default)]
pub queue_time: Option<u64>,
#[serde(default)]
pub outbound_tls: Option<String>,
#[serde(default)]
pub target_link_url: Option<String>,
#[serde(default)]
pub target_link_name: Option<String>,
#[serde(default)]
pub user_agent: Option<String>,
#[serde(default)]
pub user_agent_parsed: Option<UserAgentParsed>,
#[serde(default)]
pub geo_ip: Option<GeoIp>,
#[serde(default)]
pub ip_address: Option<String>,
#[serde(default)]
pub initial_pixel: Option<bool>,
#[serde(default)]
pub fbtype: Option<String>,
#[serde(default)]
pub report_by: Option<String>,
#[serde(default)]
pub report_to: Option<String>,
#[serde(default)]
pub remote_addr: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct UserAgentParsed {
#[serde(default)]
pub agent_family: Option<String>,
#[serde(default)]
pub device_brand: Option<String>,
#[serde(default)]
pub device_family: Option<String>,
#[serde(default)]
pub os_family: Option<String>,
#[serde(default)]
pub os_version: Option<String>,
#[serde(default)]
pub is_mobile: Option<bool>,
#[serde(default)]
pub is_proxy: Option<bool>,
#[serde(default)]
pub is_prefetched: Option<bool>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct GeoIp {
#[serde(default)]
pub country: Option<String>,
#[serde(default)]
pub region: Option<String>,
#[serde(default)]
pub city: Option<String>,
#[serde(default)]
pub latitude: Option<f64>,
#[serde(default)]
pub longitude: Option<f64>,
#[serde(default)]
pub zip: Option<String>,
#[serde(default)]
pub postal_code: Option<String>,
}
#[derive(Debug, Deserialize)]
struct ScheduledTransmissionResponseWrapper {
#[allow(dead_code)]
message: String,
data: ScheduledTransmission,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ScheduledTransmission {
pub transmission_id: String,
pub state: ScheduledEmailState,
#[serde(default)]
pub scheduled_at: Option<String>,
pub from: String,
#[serde(default)]
pub from_name: Option<String>,
pub subject: String,
pub recipients: Vec<String>,
pub num_recipients: u32,
pub events: Vec<EmailEvent>,
}