use base64::{engine::general_purpose, Engine};
use chrono::{DateTime, Local, Utc};
use mail_parser::MessageParser;
use std::{
ffi::OsStr,
fs,
io::Write,
path::{Path, PathBuf},
};
use crate::flags::{self, Flags};
use crate::{util, Error, Message, Messages};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct M2dir {
pub(crate) path: PathBuf,
}
impl TryFrom<&Path> for M2dir {
type Error = Error;
fn try_from(path: &Path) -> Result<Self, Error> {
if !path.is_dir() {
return Err(Error::FolderNotFound);
}
let marker = PathBuf::from_iter([path.as_os_str(), OsStr::new(".m2dir")]);
if !marker.is_file() {
return Err(Error::FolderNotFound);
}
Ok(M2dir {
path: PathBuf::from(path),
})
}
}
impl M2dir {
pub fn open(path: impl AsRef<Path>) -> Result<Self, Error> {
M2dir::try_from(path.as_ref())
}
pub fn create(path: impl AsRef<Path>) -> Result<Self, Error> {
fs::create_dir_all(&path)?;
let marker = PathBuf::from_iter([path.as_ref().as_os_str(), OsStr::new(".m2dir")]);
let _ = fs::File::create(marker)?;
Ok(M2dir {
path: PathBuf::from(path.as_ref()),
})
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn count(&self) -> usize {
self.list().count()
}
pub fn list(&self) -> Messages {
Messages::new(self.path.clone())
}
pub fn deliver(&self, data: &[u8]) -> Result<Message, Error> {
self.store(data, &Flags::new())
}
pub fn find(&self, id: &str) -> Result<Option<Message>, Error> {
for msg in self.list() {
let msg = msg?;
if msg.id() == id {
return Ok(Some(msg));
}
}
Ok(None)
}
pub fn store(&self, data: &[u8], flags: &Flags) -> Result<Message, Error> {
let message = MessageParser::default()
.parse_headers(data)
.ok_or(Error::Parse())?;
let date = match message.date() {
None => Local::now(),
Some(dt) => DateTime::from_timestamp(dt.to_timestamp(), 0)
.unwrap_or_else(Utc::now)
.with_timezone(&Local),
};
let from = message
.from()
.and_then(|f| f.first())
.and_then(|a| a.address())
.unwrap_or("<parse_error>")
.replace("/", "_");
let mut id = [0u8; 12];
id[0..4].copy_from_slice(&(data.len() as u32).to_le_bytes());
let digest = &util::fnv64(&id[0..4], data).to_le_bytes();
id[4..].copy_from_slice(digest); let mut id_b64 = general_purpose::URL_SAFE.encode(id);
let final_name = format!("{}_{},{}", date.format("%Y-%m-%d_%H:%M"), from, id_b64);
let mut final_path = self.path.clone();
final_path.push(&final_name);
if final_path.exists() {
let mut i = 0u16;
while final_path.exists() {
i = i.wrapping_add(1);
if i == 0 {
return Err(Error::TooManyDuplicates);
}
final_path.set_file_name(format!(
"{}_{},{}.{}",
date.format("%Y-%m-%d_%H:%M"),
from,
id_b64,
i
));
}
id_b64 = format!("{}.{}", id_b64, i);
}
if !flags.is_empty() {
let flags_path = flags::flags_path_for(&self.path, &id_b64);
util::write_atomic(&flags_path, |f| {
for flag in flags.iter() {
writeln!(f, "{}", flag)?;
}
Ok(())
})
.map_err(|e| Error::WriteFlags(flags_path, e))?;
}
util::write_atomic(&final_path, |f| f.write_all(data)).inspect_err(|_| {
if !flags.is_empty() {
let flags_path = flags::flags_path_for(&self.path, &id_b64);
_ = fs::remove_file(flags_path);
}
})?;
Ok(Message {
id: id_b64,
path: final_path,
})
}
}