pub mod transport;
use std::error::Error as StdError;
use std::sync::Arc;
use cot::config::{EmailConfig, EmailTransportTypeConfig};
use cot::email::transport::smtp::Smtp;
use cot_core::error::impl_into_cot_error;
use derive_builder::Builder;
use derive_more::with_trait::Debug;
use thiserror::Error;
use transport::{BoxedTransport, Transport};
use crate::email::transport::TransportError;
use crate::email::transport::console::Console;
const ERROR_PREFIX: &str = "email message build error:";
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum EmailError {
#[error(transparent)]
Transport(TransportError),
}
impl_into_cot_error!(EmailError);
pub type EmailResult<T> = Result<T, EmailError>;
#[derive(Debug, Clone)]
pub struct AttachmentData {
pub filename: String,
pub content_type: String,
pub data: Vec<u8>,
}
#[derive(Debug, Clone, Builder)]
#[builder(build_fn(skip))]
pub struct EmailMessage {
#[builder(setter(into))]
subject: String,
#[builder(setter(into))]
body: String,
from: crate::common_types::Email,
to: Vec<crate::common_types::Email>,
cc: Vec<crate::common_types::Email>,
bcc: Vec<crate::common_types::Email>,
reply_to: Vec<crate::common_types::Email>,
attachments: Vec<AttachmentData>,
}
impl EmailMessage {
#[must_use]
pub fn builder() -> EmailMessageBuilder {
EmailMessageBuilder::default()
}
}
impl EmailMessageBuilder {
pub fn build(&self) -> Result<EmailMessage, EmailMessageError> {
let from = self
.from
.clone()
.ok_or_else(|| EmailMessageError::MissingField("from".to_string()))?;
let subject = self.subject.clone().unwrap_or_default();
let body = self.body.clone().unwrap_or_default();
let to = self.to.clone().unwrap_or_default();
let cc = self.cc.clone().unwrap_or_default();
let bcc = self.bcc.clone().unwrap_or_default();
let reply_to = self.reply_to.clone().unwrap_or_default();
let attachments = self.attachments.clone().unwrap_or_default();
Ok(EmailMessage {
subject,
body,
from,
to,
cc,
bcc,
reply_to,
attachments,
})
}
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum EmailMessageError {
#[error("{ERROR_PREFIX} invalid email address: {0}")]
InvalidEmailAddress(Box<dyn StdError + Send + Sync + 'static>),
#[error("{ERROR_PREFIX} failed to build email message: {0}")]
BuildError(Box<dyn StdError + Send + Sync + 'static>),
#[error("{ERROR_PREFIX} The `{0}` field is required but was not set")]
MissingField(String),
}
impl_into_cot_error!(EmailMessageError);
#[derive(Debug)]
struct EmailImpl {
#[debug("..")]
transport: Box<dyn BoxedTransport>,
}
#[derive(Debug, Clone)]
pub struct Email {
inner: Arc<EmailImpl>,
}
impl Email {
pub fn new(transport: impl Transport) -> Self {
let transport: Box<dyn BoxedTransport> = Box::new(transport);
Self {
inner: Arc::new(EmailImpl { transport }),
}
}
pub async fn send(&self, message: EmailMessage) -> EmailResult<()> {
self.inner
.transport
.send(&[message])
.await
.map_err(EmailError::Transport)
}
pub async fn send_multiple(&self, messages: &[EmailMessage]) -> EmailResult<()> {
self.inner
.transport
.send(messages)
.await
.map_err(EmailError::Transport)
}
pub fn from_config(config: &EmailConfig) -> EmailResult<Self> {
let transport = &config.transport;
let this = {
match &transport.transport_type {
EmailTransportTypeConfig::Console => {
let console = Console::new();
Self::new(console)
}
EmailTransportTypeConfig::Smtp { url, mechanism } => {
let smtp = Smtp::new(url, *mechanism).map_err(EmailError::Transport)?;
Self::new(smtp)
}
}
};
Ok(this)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::{EmailTransportConfig, EmailUrl};
use crate::email::transport::smtp::Mechanism;
#[cot::test]
async fn builder_errors_when_from_missing() {
let res = EmailMessage::builder()
.subject("Hello".to_string())
.body("World".to_string())
.build();
assert!(res.is_err());
let err = res.err().unwrap();
assert_eq!(
err.to_string(),
"email message build error: The `from` field is required but was not set"
);
}
#[cot::test]
async fn builder_defaults_when_only_from_set() {
let msg = EmailMessage::builder()
.from(crate::common_types::Email::new("sender@example.com").unwrap())
.build()
.expect("should build with defaults");
assert_eq!(msg.subject, "");
assert_eq!(msg.body, "");
assert!(msg.to.is_empty());
assert!(msg.cc.is_empty());
assert!(msg.bcc.is_empty());
assert!(msg.reply_to.is_empty());
assert!(msg.attachments.is_empty());
}
#[cot::test]
async fn from_config_console_builds() {
use crate::config::{EmailConfig, EmailTransportTypeConfig};
let cfg = EmailConfig {
transport: EmailTransportConfig {
transport_type: EmailTransportTypeConfig::Console,
},
};
let email = Email::from_config(&cfg);
assert!(email.is_ok());
}
#[cot::test]
async fn from_config_smtp_builds() {
let cfg = EmailConfig {
transport: EmailTransportConfig {
transport_type: EmailTransportTypeConfig::Smtp {
url: EmailUrl::from("smtp://localhost:1025"),
mechanism: Mechanism::Plain,
},
},
};
let email = Email::from_config(&cfg);
assert!(email.is_ok());
}
#[cot::test]
async fn email_send_console() {
let console = Console::new();
let email = Email::new(console);
let msg = EmailMessage::builder()
.from(crate::common_types::Email::new("user@example.com").unwrap())
.to(vec![
crate::common_types::Email::new("recipient@example.com").unwrap(),
])
.subject("Test Email".to_string())
.body("This is a test email body.".to_string())
.build()
.unwrap();
assert!(email.send(msg).await.is_ok());
}
#[cot::test]
async fn email_send_multiple_console() {
let console = Console::new();
let email = Email::new(console);
let msg1 = EmailMessage::builder()
.from(crate::common_types::Email::new("user1@example.com").unwrap())
.to(vec![
crate::common_types::Email::new("recipient@example.com").unwrap(),
])
.subject("Test Email")
.body("This is a test email body.")
.build()
.unwrap();
let msg2 = EmailMessage::builder()
.from(crate::common_types::Email::new("user2@example.com").unwrap())
.to(vec![
crate::common_types::Email::new("user2@example.com").unwrap(),
])
.subject("Another Test Email")
.body("This is another test email body.")
.build()
.unwrap();
assert!(email.send_multiple(&[msg1, msg2]).await.is_ok());
}
}