use std::path::PathBuf;
#[derive(Debug, Clone)]
pub struct Alternative {
content_type: String,
content: Vec<u8>,
}
impl Alternative {
pub fn new(content_type: impl Into<String>, content: Vec<u8>) -> Self {
Self {
content_type: content_type.into(),
content,
}
}
pub fn html(content: impl Into<String>) -> Self {
let content_str = content.into();
Self::new("text/html", content_str.into_bytes())
}
pub fn plain(content: impl Into<String>) -> Self {
let content_str = content.into();
Self::new("text/plain", content_str.into_bytes())
}
pub fn content_type(&self) -> &str {
&self.content_type
}
pub fn content(&self) -> &[u8] {
&self.content
}
pub fn content_as_string(&self) -> Option<&str> {
std::str::from_utf8(&self.content).ok()
}
}
#[derive(Debug, Clone)]
pub struct Attachment {
filename: String,
content: Vec<u8>,
mime_type: String,
content_id: Option<String>,
inline: bool,
}
impl Attachment {
pub fn new(filename: impl Into<String>, content: Vec<u8>) -> Self {
let filename_str = filename.into();
let mime_type = Self::detect_mime_type(&filename_str);
Self {
filename: filename_str,
content,
mime_type,
content_id: None,
inline: false,
}
}
pub fn from_path(path: PathBuf, filename: impl Into<String>) -> std::io::Result<Self> {
let content = std::fs::read(&path)?;
let filename_str = filename.into();
let mime_type = Self::detect_mime_type(&filename_str);
Ok(Self {
filename: filename_str,
content,
mime_type,
content_id: None,
inline: false,
})
}
pub fn inline(
filename: impl Into<String>,
content: Vec<u8>,
content_id: impl Into<String>,
) -> Self {
let filename_str = filename.into();
let mime_type = Self::detect_mime_type(&filename_str);
Self {
filename: filename_str,
content,
mime_type,
content_id: Some(content_id.into()),
inline: true,
}
}
pub fn with_mime_type(&mut self, mime_type: impl Into<String>) -> &mut Self {
self.mime_type = mime_type.into();
self
}
pub fn as_inline(&mut self, content_id: impl Into<String>) -> &mut Self {
self.content_id = Some(content_id.into());
self.inline = true;
self
}
pub fn filename(&self) -> &str {
&self.filename
}
pub fn content(&self) -> &[u8] {
&self.content
}
pub fn mime_type(&self) -> &str {
&self.mime_type
}
pub fn content_id(&self) -> Option<&str> {
self.content_id.as_deref()
}
pub fn is_inline(&self) -> bool {
self.inline
}
fn detect_mime_type(filename: &str) -> String {
mime_guess::from_path(filename)
.first()
.map(|mime| mime.to_string())
.unwrap_or_else(|| "application/octet-stream".to_string())
}
}
#[derive(Debug, Clone)]
pub struct EmailMessage {
subject: String,
body: String,
from_email: String,
to: Vec<String>,
cc: Vec<String>,
bcc: Vec<String>,
reply_to: Vec<String>,
html_body: Option<String>,
alternatives: Vec<Alternative>,
attachments: Vec<Attachment>,
headers: Vec<(String, String)>,
}
impl EmailMessage {
pub fn builder() -> EmailMessageBuilder {
EmailMessageBuilder::default()
}
pub fn subject(&self) -> &str {
&self.subject
}
pub fn body(&self) -> &str {
&self.body
}
pub fn from_email(&self) -> &str {
&self.from_email
}
pub fn to(&self) -> &[String] {
&self.to
}
pub fn cc(&self) -> &[String] {
&self.cc
}
pub fn bcc(&self) -> &[String] {
&self.bcc
}
pub fn reply_to(&self) -> &[String] {
&self.reply_to
}
pub fn html_body(&self) -> Option<&str> {
self.html_body.as_deref()
}
pub fn alternatives(&self) -> &[Alternative] {
&self.alternatives
}
pub fn attachments(&self) -> &[Attachment] {
&self.attachments
}
pub fn headers(&self) -> &[(String, String)] {
&self.headers
}
pub async fn send(
&self,
backend: &dyn crate::backends::EmailBackend,
) -> crate::EmailResult<()> {
backend.send_messages(std::slice::from_ref(self)).await?;
Ok(())
}
pub async fn send_with_backend(
&self,
backend: &dyn crate::backends::EmailBackend,
) -> crate::EmailResult<()> {
backend.send_messages(std::slice::from_ref(self)).await?;
Ok(())
}
}
#[derive(Default)]
pub struct EmailMessageBuilder {
subject: String,
body: String,
from_email: String,
to: Vec<String>,
cc: Vec<String>,
bcc: Vec<String>,
reply_to: Vec<String>,
html_body: Option<String>,
alternatives: Vec<Alternative>,
attachments: Vec<Attachment>,
headers: Vec<(String, String)>,
}
impl EmailMessageBuilder {
pub fn subject(mut self, subject: impl Into<String>) -> Self {
self.subject = subject.into();
self
}
pub fn body(mut self, body: impl Into<String>) -> Self {
self.body = body.into();
self
}
pub fn from(mut self, from: impl Into<String>) -> Self {
self.from_email = from.into();
self
}
pub fn from_email(mut self, from: impl Into<String>) -> Self {
self.from_email = from.into();
self
}
pub fn to(mut self, to: Vec<String>) -> Self {
self.to = to;
self
}
pub fn cc(mut self, cc: Vec<String>) -> Self {
self.cc = cc;
self
}
pub fn bcc(mut self, bcc: Vec<String>) -> Self {
self.bcc = bcc;
self
}
pub fn reply_to(mut self, reply_to: Vec<String>) -> Self {
self.reply_to = reply_to;
self
}
pub fn html(mut self, html: impl Into<String>) -> Self {
self.html_body = Some(html.into());
self
}
pub fn attachment(mut self, attachment: Attachment) -> Self {
self.attachments.push(attachment);
self
}
pub fn alternative(mut self, alternative: Alternative) -> Self {
self.alternatives.push(alternative);
self
}
pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.headers.push((name.into(), value.into()));
self
}
pub fn build(self) -> crate::EmailResult<EmailMessage> {
use crate::validation::{
check_header_injection, validate_email, validate_email_list, validate_header_name,
};
if !self.from_email.is_empty() {
validate_email(&self.from_email)?;
}
validate_email_list(&self.to)?;
validate_email_list(&self.cc)?;
validate_email_list(&self.bcc)?;
validate_email_list(&self.reply_to)?;
check_header_injection(&self.subject)?;
for (name, value) in &self.headers {
validate_header_name(name)?;
check_header_injection(value)?;
}
Ok(EmailMessage {
subject: self.subject,
body: self.body,
from_email: self.from_email,
to: self.to,
cc: self.cc,
bcc: self.bcc,
reply_to: self.reply_to,
html_body: self.html_body,
alternatives: self.alternatives,
attachments: self.attachments,
headers: self.headers,
})
}
}