vomit-m2dir 0.1.4

Library for the m2dir email storage format
Documentation
use std::fs::{self, read_dir, File, ReadDir};
use std::ops::Deref;
use std::path::{Path, PathBuf};

use crate::flags::{self, Flags};
use crate::{util, Error, M2dir};

/// An email message stored in an m2dir.
///
/// Note that this implementation does not actually verify that the backing file
/// contains a valid email message, as it has no need to parse the contents.
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Message {
    pub(crate) id: String,
    pub(crate) path: PathBuf,
}

impl TryFrom<&Path> for Message {
    type Error = Error;

    fn try_from(path: &Path) -> Result<Self, Error> {
        if !path.is_file() {
            return Err(Error::MessageNotFound);
        }
        let fname = path.file_name().unwrap().to_string_lossy();
        let (_, id) = fname
            .rsplit_once(',')
            .ok_or(Error::InvalidFileName(path.to_path_buf()))?;
        Ok(Message {
            id: id.to_string(),
            path: PathBuf::from(path),
        })
    }
}

impl Message {
    pub fn path(&self) -> &Path {
        &self.path
    }

    pub fn id(&self) -> &str {
        &self.id
    }

    pub fn flags_path(&self) -> PathBuf {
        flags::flags_path_for(self.path.parent().unwrap(), self.id())
    }

    pub fn flags(&self) -> Result<Flags, Error> {
        let flags_path = self.flags_path();

        if !flags_path.exists() {
            return Ok(Flags::default());
        }

        let file = File::open(&flags_path)?;
        Flags::parse_file(file).map_err(|e| Error::Flags(flags_path, e))
    }

    pub fn set_flags(&self, flags: &Flags) -> Result<(), Error> {
        let flags_path = self.flags_path();
        fs::create_dir_all(flags_path.parent().unwrap())
            .map_err(|e| Error::WriteFlags(flags_path.clone(), e))?;
        let mut file =
            File::create(&flags_path).map_err(|e| Error::WriteFlags(flags_path.clone(), e))?;
        flags
            .write_file(&mut file)
            .map_err(|e| Error::WriteFlags(flags_path, e))?;
        Ok(())
    }

    pub fn copy_to(&self, target: &M2dir) -> Result<Message, Error> {
        let src_flags = self.flags_path();
        let dst_flags = flags::flags_path_for(&target.path, self.id());
        let dst = PathBuf::from_iter([target.path.as_os_str(), self.path.file_name().unwrap()]);

        fs::create_dir_all(dst_flags.parent().unwrap())?;
        if src_flags.exists() {
            util::copy_atomic(src_flags, &dst_flags)?;
        }
        util::copy_atomic(&self.path, &dst).inspect_err(|_| {
            _ = fs::remove_file(&dst_flags);
        })?;
        Ok(Message {
            id: self.id.clone(),
            path: dst,
        })
    }

    pub fn move_to(&mut self, target: &M2dir) -> Result<(), Error> {
        let src_flags = self.flags_path();
        let dst_flags = flags::flags_path_for(&target.path, self.id());
        let dst = PathBuf::from_iter([target.path.as_os_str(), self.path.file_name().unwrap()]);

        fs::create_dir_all(dst_flags.parent().unwrap())?;
        if src_flags.exists() {
            fs::rename(&src_flags, &dst_flags)?;
        }
        fs::rename(&self.path, &dst).inspect_err(|_| {
            _ = fs::rename(&dst_flags, &src_flags);
        })?;
        self.path = dst;
        Ok(())
    }

    pub fn delete(self) -> Result<(), Error> {
        let flags = self.flags_path();
        fs::remove_file(self.path())?;
        if flags.exists() {
            fs::remove_file(self.flags_path())?;
        }
        Ok(())
    }
}

/// An iterator over the email messages in a particular m2dir.
///
/// Usually constructed via [`M2dir::list`].
///
/// The order of messages in the iterator is not specified, and is not
/// guaranteed to be stable over multiple invocations of this method.
///
/// Note that each file with a valid name according to the m2dir spec is
/// considered a message. The contents of files are not parsed or validated.
pub struct Messages {
    path: PathBuf,
    readdir: Option<ReadDir>,
}

impl Messages {
    pub(crate) fn new(path: PathBuf) -> Messages {
        Messages {
            path,
            readdir: None,
        }
    }

    pub fn path(&self) -> &Path {
        &self.path
    }
}

impl Iterator for Messages {
    type Item = Result<Message, Error>;

    fn next(&mut self) -> Option<Result<Message, Error>> {
        if self.readdir.is_none() {
            self.readdir = match read_dir(&self.path) {
                Err(_) => return None,
                Ok(v) => Some(v),
            };
        }

        loop {
            // skip over directories and files starting with a '.'
            let dir_entry = self.readdir.iter_mut().next().unwrap().next();
            let result = dir_entry.map(|e| {
                let entry = e?;
                let ftype = entry.file_type()?;
                if ftype.is_dir() {
                    return Ok(None);
                }
                let filename = String::from(entry.file_name().to_string_lossy().deref());
                if filename.starts_with('.') {
                    return Ok(None);
                }
                Ok(Some(Message::try_from(entry.path().as_path())?))
            });
            return match result {
                None => None,
                Some(Err(e)) => Some(Err(e)),
                Some(Ok(None)) => continue,
                Some(Ok(Some(v))) => Some(Ok(v)),
            };
        }
    }
}