vomit-m2dir 0.3.1

Library for the m2dir email storage format
Documentation
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};

/// An m2dir as defined in the m2dir spec.
///
/// Any instance created by this implementation is guaranteed to be an existing
/// directory with a `.m2dir` marker file.
#[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 {
    /// Open an existing m2dir.
    ///
    /// Will fail if `path` does not exist or is not a valid m2dir. To create a
    /// new m2dir use [M2dir::create] instead.
    pub fn open(path: impl AsRef<Path>) -> Result<Self, Error> {
        M2dir::try_from(path.as_ref())
    }

    /// Creates a new m2dir.
    ///
    /// Unlike [M2dir::open], this method will try to create the m2dir if it
    /// does not exist.
    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)
    }

    /// Returns the path of the m2dir.
    ///
    /// The path is guaranteed to be [canonical].
    ///
    /// [canonical]: https://doc.rust-lang.org/std/path/struct.Path.html#method.canonicalize
    pub fn path(&self) -> &Path {
        &self.path
    }

    /// Returns the number of messages found in this m2dir.
    pub fn count(&self) -> usize {
        self.list().count()
    }

    /// Returns an iterator over the messages in this m2dir.
    ///
    /// The order of messages in the iterator is not specified, and is not
    /// guaranteed to be stable over multiple invocations of this method.
    pub fn list(&self) -> Messages {
        Messages::new(self.path.clone())
    }

    /// Deliver a new message to this m2dir.
    pub fn deliver(&self, data: &[u8]) -> Result<Message, Error> {
        self.store(data, &Flags::new())
    }

    /// Find the message with the given unique ID in this m2dir.
    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)
    }

    /// Stores the given message data as a new message in the m2dir, adding the
    /// given `flags` to it. Returns the inserted message on success.
    pub fn store(&self, data: &[u8], flags: &Flags) -> Result<Message, Error> {
        let message = MessageParser::default()
            .parse_headers(data)
            .ok_or(Error::Parse())?;

        // Get the messages date from the header or use "now" if that fails.
        // This is used for the human readable part of the filename.
        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,
        })
    }
}