use chrono::{DateTime, Local, Utc};
use mail_parser::MessageParser;
use std::{
env,
ffi::OsStr,
fs,
io::Write,
path::{Path, PathBuf},
};
use crate::{
flags::{self, Flags},
util::sanitize_filename,
ID,
};
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> {
let path = path.canonicalize()?;
let marker = PathBuf::from_iter([path.as_os_str(), OsStr::new(".m2dir")]);
if !marker.is_file() {
return Err(Error::FolderNotFound);
}
Ok(M2dir { 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)?;
M2dir::open(path)
}
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: &ID) -> 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>");
let from = sanitize_filename(from);
let id = ID::new(data);
let fmt = env::var("VOMIT_M2DIR_DATE_FORMAT").unwrap_or("%Y-%m-%d_%H:%M".to_string());
let final_name = format!("{}_{},{}", date.format(&fmt), from, id);
let mut final_path = self.path.clone();
final_path.push(&final_name);
if !flags.is_empty() {
let flags_path = flags::flags_path_for(&self.path, id.as_ref());
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.as_ref());
_ = fs::remove_file(flags_path);
}
})?;
Ok(Message {
id,
path: final_path,
})
}
}