mod entry;
mod error;
mod flag;
mod validate;
#[cfg(unix)]
use std::os::unix::fs::MetadataExt;
#[cfg(windows)]
use std::os::windows::fs::MetadataExt;
use std::{
fs::{self, File, OpenOptions, ReadDir},
io::{self, ErrorKind, Write},
path::{Path, PathBuf},
process, str,
sync::atomic::{AtomicUsize, Ordering},
time::{SystemTime, UNIX_EPOCH},
};
pub use entry::{MailEntries, MailEntry};
pub use error::Error;
pub use flag::Flag;
use gethostname::gethostname;
const CUR: &str = "cur";
const NEW: &str = "new";
const TMP: &str = "tmp";
#[cfg(unix)]
const SEP: &str = ":2,";
#[cfg(windows)]
const SEP: &str = ";2,";
static COUNTER: AtomicUsize = AtomicUsize::new(0);
#[derive(Debug)]
pub struct Maildir {
root: PathBuf,
cur: PathBuf,
new: PathBuf,
tmp: PathBuf,
}
impl Maildir {
pub fn new<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
let maildir = Maildir::from(path);
maildir.ensure_dirs()?;
maildir.clean_tmp()?;
Ok(maildir)
}
pub fn path(&self) -> &Path {
&self.root
}
fn ensure_dirs(&self) -> Result<(), Error> {
for dir in &[&self.cur, &self.new, &self.tmp] {
fs::create_dir_all(dir)?;
}
Ok(())
}
pub fn clean_tmp(&self) -> Result<(), Error> {
for entry in fs::read_dir(&self.tmp)? {
let path = entry?.path();
let metadata = path.metadata()?;
if metadata.is_file() && metadata.modified()?.elapsed()?.as_secs() > 36 * 60 * 60 {
fs::remove_file(path)?;
}
}
Ok(())
}
pub fn create_folder(&self, folder: &str) -> Result<Maildir, Error> {
validate::validate_folder(folder)?;
let path = if self.root.join("maildirfolder").exists() {
self.root.parent().unwrap().join(format!(
"{}.{folder}",
self.root.file_name().unwrap().to_string_lossy()
))
} else {
self.root.join(format!(".{folder}"))
};
fs::create_dir_all(&path)?;
fs::write(path.join("maildirfolder"), "")?;
Maildir::new(path)
}
pub fn folders(&self) -> Maildirs {
Maildirs::new(&self.root)
}
pub fn count_new(&self) -> usize {
MailEntries::new(&self.new, false).count()
}
pub fn count_cur(&self) -> usize {
MailEntries::new(&self.cur, false)
.inspect(|e| println!("{:?}", e))
.count()
}
pub fn count_tmp(&self) -> usize {
MailEntries::new(&self.tmp, false).count()
}
pub fn list_new(&self) -> MailEntries {
MailEntries::new(&self.new, true)
}
pub fn list_cur(&self) -> MailEntries {
MailEntries::new(&self.cur, false)
}
pub fn list_tmp(&self) -> MailEntries {
MailEntries::new(&self.tmp, false)
}
pub fn peek_new(&self) -> MailEntries {
MailEntries::new(&self.new, true)
}
pub fn copy_to(&self, id: &str, target: &Maildir) -> Result<(), Error> {
let entry = self
.find(id)
.ok_or_else(|| Error::FindEmailError(id.to_owned()))?;
let filename = entry
.path()
.file_name()
.ok_or_else(|| Error::InvalidFilenameError(id.to_owned()))?;
let src_path = entry.path();
let dst_path = target.path().join("cur").join(filename);
if src_path == dst_path {
return Err(Error::CopyEmailSamePathError(dst_path));
}
fs::copy(src_path, dst_path)?;
Ok(())
}
pub fn move_to(&self, id: &str, target: &Maildir) -> Result<(), Error> {
let entry = self
.find(id)
.ok_or_else(|| Error::FindEmailError(id.to_owned()))?;
let filename = entry
.path()
.file_name()
.ok_or_else(|| Error::InvalidFilenameError(id.to_owned()))?;
fs::rename(entry.path(), target.path().join("cur").join(filename))?;
Ok(())
}
pub fn find(&self, id: &str) -> Option<MailEntry> {
self.list_new()
.chain(self.list_cur())
.filter_map(Result::ok)
.find(|entry| entry.id() == id)
}
pub fn delete(&self, id: &str) -> Result<(), Error> {
match self.find(id) {
Some(m) => Ok(fs::remove_file(m.path())?),
None => Err(Error::FindEmailError(id.to_owned())),
}
}
pub fn store_new(&self, data: &[u8]) -> Result<MailEntry, Error> {
self.store(data, true, None)
}
pub fn store_cur(&self, data: &[u8]) -> Result<MailEntry, Error> {
self.store(data, false, None)
}
fn store(&self, data: &[u8], new: bool, id: Option<String>) -> Result<MailEntry, Error> {
self.ensure_dirs()?;
let mut tmp_file;
let mut tmp_path = self.tmp.clone();
loop {
tmp_path.push(generate_tmp_id());
match OpenOptions::new()
.write(true)
.create_new(true)
.open(&tmp_path)
{
Ok(f) => {
tmp_file = f;
break;
}
Err(err) => {
if err.kind() != ErrorKind::AlreadyExists {
return Err(err.into());
}
tmp_path.pop();
}
}
}
struct RemoveOnDrop {
path_to_remove: PathBuf,
}
impl Drop for RemoveOnDrop {
fn drop(&mut self) {
fs::remove_file(&self.path_to_remove).ok();
}
}
let _remove_guard = RemoveOnDrop {
path_to_remove: tmp_path.clone(),
};
tmp_file.write_all(data)?;
tmp_file.sync_all()?;
let id = id.map_or_else(|| generate_id(tmp_file), Ok)?;
let mut new_path = self.root.clone();
if new {
new_path.push(NEW);
new_path.push(&id);
} else {
new_path.push(CUR);
new_path.push(format!("{id}{SEP}2"));
}
fs::rename(&tmp_path, &new_path)?;
MailEntry::create(id, new_path, data)
}
}
impl<P: AsRef<Path>> From<P> for Maildir {
fn from(p: P) -> Maildir {
Maildir {
root: p.as_ref().to_path_buf(),
cur: p.as_ref().join(CUR),
new: p.as_ref().join(NEW),
tmp: p.as_ref().join(TMP),
}
}
}
pub struct Maildirs {
readdir: Option<ReadDir>,
}
impl Maildirs {
pub(crate) fn new<P: AsRef<Path>>(path: P) -> Maildirs {
Maildirs {
readdir: fs::read_dir(path).ok(),
}
}
}
impl Iterator for Maildirs {
type Item = io::Result<Maildir>;
fn next(&mut self) -> Option<io::Result<Maildir>> {
if let Some(ref mut readdir) = self.readdir {
for entry in readdir {
let path = match entry {
Err(e) => return Some(Err(e)),
Ok(e) => e.path(),
};
let filename = path.file_name()?.to_string_lossy();
if !filename.starts_with('.') || filename.starts_with("..") || !path.is_dir() {
continue;
}
return Some(Ok(Maildir::from(path)));
}
}
None
}
}
fn generate_tmp_id() -> String {
let ts = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
let secs = ts.as_secs();
let nanos = ts.subsec_nanos();
let counter = COUNTER.fetch_add(1, Ordering::SeqCst);
format!(
"{secs}.#{counter:x}M{nanos}P{pid}",
secs = secs,
counter = counter,
nanos = nanos,
pid = process::id()
)
}
fn generate_id(file: File) -> Result<String, Error> {
let meta = file.metadata()?;
#[cfg(unix)]
let dev = meta.dev();
#[cfg(windows)]
let dev: u64 = 0;
#[cfg(unix)]
let ino = meta.ino();
#[cfg(windows)]
let ino: u64 = 0;
let hostname = gethostname()
.into_string()
.expect("hostname is not valid UTF-8. how the fuck did you achieve that?");
Ok(format!("{}V{dev}I{ino}.{hostname}", generate_tmp_id()))
}