#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
use std::time::Instant;
#[cfg(all(target_family = "wasm", target_os = "unknown"))]
use web_time::Instant;
use crate::address::{Address, ToAddress};
use crate::email::{Email, PreparedEmail};
use crate::error::MailError;
use crate::mailer::{DeliveryResult, Mailer};
use tracing::Instrument;
#[derive(Clone)]
pub struct EmailClient<M> {
mailer: M,
default_from: Option<Address>,
}
impl<M> EmailClient<M> {
pub fn new(mailer: M) -> Self {
Self {
mailer,
default_from: None,
}
}
#[must_use = "with_default_from returns a modified client; chain or assign the returned value"]
pub fn with_default_from(mut self, addr: impl ToAddress) -> Self {
self.default_from = Some(addr.to_address());
self
}
pub(crate) fn with_optional_default_from(mut self, addr: Option<Address>) -> Self {
self.default_from = addr;
self
}
pub fn mailer(&self) -> &M {
&self.mailer
}
fn prepare_email(&self, email: Email) -> Result<PreparedEmail, MailError>
where
M: Mailer,
{
self.mailer.prepare_email(email, self.default_from.clone())
}
}
impl<M: Mailer> EmailClient<M> {
pub async fn deliver(&self, email: Email) -> Result<DeliveryResult, MailError> {
let email = self.prepare_email(email)?;
deliver_prepared(&self.mailer, &email).await
}
pub async fn deliver_many<I>(&self, emails: I) -> Result<Vec<DeliveryResult>, MailError>
where
I: IntoIterator<Item = Email>,
{
let emails = emails
.into_iter()
.map(|email| self.prepare_email(email))
.collect::<Result<Vec<_>, _>>()?;
self.mailer.validate_batch(&emails)?;
let provider = self.mailer.provider_name();
let count = emails.len();
let span = tracing::info_span!("missive.deliver_many", provider = provider, count = count,);
let start = Instant::now();
let result = self
.mailer
.deliver_many_prepared(&emails)
.instrument(span.clone())
.await;
let duration = start.elapsed();
let status = if result.is_ok() { "success" } else { "error" };
#[cfg(feature = "metrics")]
{
metrics::counter!("missive_emails_total", "provider" => provider, "status" => status)
.increment(count as u64);
metrics::counter!("missive_batch_total", "provider" => provider, "status" => status)
.increment(1);
metrics::histogram!("missive_delivery_duration_seconds", "provider" => provider, "batch" => "true")
.record(duration.as_secs_f64());
metrics::histogram!("missive_batch_size", "provider" => provider).record(count as f64);
}
match &result {
Ok(_) => tracing::info!(
parent: &span,
provider = provider,
status = status,
count = count,
duration_ms = duration.as_millis() as u64,
"Emails delivered",
),
Err(e) => tracing::error!(
parent: &span,
provider = provider,
status = status,
count = count,
duration_ms = duration.as_millis() as u64,
error_kind = e.kind(),
"Email batch delivery failed",
),
}
result
}
}
async fn deliver_prepared<M: Mailer>(
mailer: &M,
email: &PreparedEmail,
) -> Result<DeliveryResult, MailError> {
let provider = mailer.provider_name();
let recipient_count = email.all_recipients().len();
let attachment_count = email.attachments.len();
let span = tracing::info_span!(
"missive.deliver",
provider = provider,
recipient_count = recipient_count,
attachment_count = attachment_count,
status = tracing::field::Empty,
duration_ms = tracing::field::Empty,
);
tracing::debug!(parent: &span, "Delivering email");
let start = Instant::now();
let result = mailer
.deliver_prepared(email)
.instrument(span.clone())
.await;
let duration = start.elapsed();
let duration_ms = duration.as_millis() as u64;
let status = if result.is_ok() { "success" } else { "error" };
span.record("status", status);
span.record("duration_ms", duration_ms);
#[cfg(feature = "metrics")]
{
metrics::counter!("missive_emails_total", "provider" => provider, "status" => status)
.increment(1);
metrics::histogram!("missive_delivery_duration_seconds", "provider" => provider)
.record(duration.as_secs_f64());
}
match &result {
Ok(r) => {
tracing::info!(
parent: &span,
provider = provider,
status = status,
recipient_count = recipient_count,
attachment_count = attachment_count,
duration_ms = duration_ms,
"Email delivered",
);
tracing::debug!(parent: &span, message_id = %r.message_id, "Provider message id");
}
Err(e) => {
tracing::error!(
parent: &span,
provider = provider,
status = status,
recipient_count = recipient_count,
attachment_count = attachment_count,
duration_ms = duration_ms,
error_kind = e.kind(),
"Email delivery failed",
);
tracing::debug!(parent: &span, error = %e, "Email delivery error details");
}
}
result
}