use serde::{Deserialize, Serialize};
use std::path::Path;
use crate::error::MailError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum AttachmentType {
#[default]
Attachment,
Inline,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Attachment {
pub filename: String,
pub content_type: String,
pub data: Vec<u8>,
#[serde(default)]
pub path: Option<String>,
pub disposition: AttachmentType,
pub content_id: Option<String>,
#[serde(default)]
pub headers: Vec<(String, String)>,
}
impl Attachment {
pub fn from_bytes(filename: impl Into<String>, data: Vec<u8>) -> Self {
let filename = filename.into();
let content_type = mime_guess::from_path(&filename)
.first_or_octet_stream()
.to_string();
Self {
filename,
content_type,
data,
path: None,
disposition: AttachmentType::Attachment,
content_id: None,
headers: Vec::new(),
}
}
pub fn from_path(path: impl AsRef<Path>) -> Result<Self, MailError> {
let path = path.as_ref();
let filename = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("attachment")
.to_string();
let data = std::fs::read(path).map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
MailError::AttachmentFileNotFound(path.display().to_string())
} else {
MailError::AttachmentReadError(format!("{}: {}", path.display(), e))
}
})?;
let content_type = mime_guess::from_path(path)
.first_or_octet_stream()
.to_string();
Ok(Self {
filename,
content_type,
data,
path: None, disposition: AttachmentType::Attachment,
content_id: None,
headers: Vec::new(),
})
}
pub fn from_path_lazy(path: impl AsRef<Path>) -> Result<Self, MailError> {
let path_ref = path.as_ref();
if !path_ref.exists() {
return Err(MailError::AttachmentFileNotFound(
path_ref.display().to_string(),
));
}
let filename = path_ref
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("attachment")
.to_string();
let content_type = mime_guess::from_path(path_ref)
.first_or_octet_stream()
.to_string();
let path_string = path_ref.to_string_lossy().to_string();
Ok(Self {
filename,
content_type,
data: Vec::new(), path: Some(path_string),
disposition: AttachmentType::Attachment,
content_id: None,
headers: Vec::new(),
})
}
pub fn content_type(mut self, content_type: impl Into<String>) -> Self {
self.content_type = content_type.into();
self
}
pub fn inline(mut self) -> Self {
self.disposition = AttachmentType::Inline;
if self.content_id.is_none() {
self.content_id = Some(self.filename.clone());
}
self
}
pub fn content_id(mut self, cid: impl Into<String>) -> Self {
self.content_id = Some(cid.into());
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 get_data(&self) -> Result<Vec<u8>, MailError> {
if let Some(ref path) = self.path {
std::fs::read(path).map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
MailError::AttachmentFileNotFound(path.clone())
} else {
MailError::AttachmentReadError(format!("{}: {}", path, e))
}
})
} else if self.data.is_empty() && self.path.is_none() {
Err(MailError::AttachmentMissingContent(self.filename.clone()))
} else {
Ok(self.data.clone())
}
}
pub fn base64_data(&self) -> String {
use base64::Engine;
let data = self.get_data().unwrap_or_default();
base64::engine::general_purpose::STANDARD.encode(&data)
}
pub fn size(&self) -> usize {
self.data.len()
}
pub fn get_size(&self) -> Result<usize, MailError> {
if let Some(ref path) = self.path {
let metadata =
std::fs::metadata(path).map_err(|e| MailError::AttachmentError(e.to_string()))?;
Ok(metadata.len() as usize)
} else {
Ok(self.data.len())
}
}
pub fn is_lazy(&self) -> bool {
self.path.is_some()
}
pub fn is_inline(&self) -> bool {
self.disposition == AttachmentType::Inline
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_from_bytes() {
let attachment = Attachment::from_bytes("test.txt", b"Hello".to_vec());
assert_eq!(attachment.filename, "test.txt");
assert_eq!(attachment.content_type, "text/plain");
assert_eq!(attachment.data, b"Hello");
assert_eq!(attachment.disposition, AttachmentType::Attachment);
}
#[test]
fn test_inline() {
let attachment = Attachment::from_bytes("logo.png", vec![1, 2, 3]).inline();
assert_eq!(attachment.disposition, AttachmentType::Inline);
assert_eq!(attachment.content_id, Some("logo.png".to_string()));
}
#[test]
fn test_content_id() {
let attachment = Attachment::from_bytes("image.png", vec![])
.inline()
.content_id("my-logo");
assert_eq!(attachment.content_id, Some("my-logo".to_string()));
}
#[test]
fn test_mime_guess() {
let pdf = Attachment::from_bytes("doc.pdf", vec![]);
assert_eq!(pdf.content_type, "application/pdf");
let png = Attachment::from_bytes("image.png", vec![]);
assert_eq!(png.content_type, "image/png");
let unknown = Attachment::from_bytes("file.unknown_ext_12345", vec![]);
assert_eq!(unknown.content_type, "application/octet-stream");
}
#[test]
fn test_base64() {
let attachment = Attachment::from_bytes("test.txt", b"Hello".to_vec());
assert_eq!(attachment.base64_data(), "SGVsbG8=");
}
}