maildirpp 0.4.0

Maildir++ library for Rust
Documentation
use std::{
    collections::HashSet,
    fs::{self, read, read_dir, ReadDir},
    io,
    path::{Path, PathBuf},
};

use crate::{validate::validate_id, Error, Flag, CUR, NEW, SEP, TMP};

/// A struct representing a single email message inside the maildir.
///
/// No parsing is done. This struct only holds the path to the message file,
/// and handles file system operations. The struct can only be created by
/// methods in [`Maildir`](crate::Maildir).
#[derive(Debug)]
pub struct MailEntry {
    id: String,
    flags: HashSet<Flag>,
    path: PathBuf,
}

impl MailEntry {
    /// Create a new `MailEntry` from a path.
    ///
    /// # Errors
    ///
    /// Will error if the path does not contain a file name part.
    pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
        let path = path.as_ref();
        let filename = path
            .file_name()
            .ok_or(Error::InvalidFilenameError(
                path.to_string_lossy().to_string(),
            ))?
            .to_string_lossy()
            .to_string();

        let (id, flags) = match filename.rsplit_once(SEP) {
            Some((id, flags)) => (
                id,
                flags
                    .chars()
                    .map(TryFrom::try_from)
                    .filter_map(Result::ok)
                    .collect(),
            ),
            None => (filename.as_ref(), HashSet::new()),
        };

        Ok(MailEntry {
            id: id.to_string(),
            flags,
            path: path.to_path_buf(),
        })
    }

    pub(crate) fn create<P: AsRef<Path>, S: ToString>(
        id: S,
        path: P,
        data: &[u8],
    ) -> Result<Self, Error> {
        let path = path.as_ref();
        fs::write(path, data)?;
        Ok(MailEntry {
            id: id.to_string(),
            flags: HashSet::new(),
            path: path.to_path_buf(),
        })
    }

    fn update(&mut self) -> Result<(), Error> {
        let mut new_file_name = self.id.clone();
        let flags = self.flags_to_string();
        if !flags.is_empty() {
            new_file_name.push_str(SEP);
            new_file_name.push_str(&flags);
        }

        let prev_path = self.path.clone();
        let new_path = self.path.with_file_name(new_file_name);

        match fs::rename(prev_path, &new_path) {
            Ok(_) => {
                self.path = new_path;
                Ok(())
            }
            Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {
                Err(Error::AlreadyExistsError(new_path))
            }
            Err(e) => Err(e.into()),
        }
    }

    /// Get the unique identifier of the email message.
    pub fn id(&self) -> &str {
        &self.id
    }

    /// Set the unique identifier of the email message.
    ///
    /// This also updates the path to the email message and renames the file on
    /// the file system.
    ///
    /// # Errors
    ///
    /// This method will return an error if the new ID is invalid or if there
    /// was an error renaming the file on the file system.
    pub fn set_id<S: ToString>(&mut self, id: S) -> Result<(), Error> {
        self.id = validate_id(id.to_string())?;
        self.update()
    }

    /// Get the path to the email message.
    pub fn path(&self) -> &Path {
        &self.path
    }

    /// Takes ownership of the [`MailEntry`] and returns the base [`PathBuf`]
    pub fn to_path_buf(self) -> PathBuf {
        self.path
    }

    /// Moves the mail entry
    fn move_to(&mut self, folder: &str) -> Result<(), Error> {
        let parent = self
            .path
            .parent()
            .ok_or_else(|| Error::NoParentError(self.path.clone()))?;

        if parent.file_name() == Some(folder.as_ref()) {
            return Ok(());
        }

        let new_path = parent
            .parent()
            .ok_or_else(|| Error::NoParentError(parent.to_path_buf()))?
            .join(folder)
            // We can unwrap here because we know that the parent is a directory
            .join(self.path.file_name().unwrap());

        fs::rename(&self.path, &new_path)?;
        self.path = new_path;

        Ok(())
    }

    /// Moves the email message to the `new` directory.
    pub fn move_to_new(&mut self) -> Result<(), Error> {
        self.move_to(NEW)
    }

    /// Moves the email message to the `cur` directory.
    pub fn move_to_cur(&mut self) -> Result<(), Error> {
        self.move_to(CUR)
    }

    /// Moves the email message to the `tmp` directory.
    pub fn move_to_tmp(&mut self) -> Result<(), Error> {
        self.move_to(TMP)
    }

    /// Get the flags of the email message.
    pub fn flags(&self) -> impl Iterator<Item = &Flag> {
        self.flags.iter()
    }

    /// Get the flags of the email message as a string.
    pub fn flags_to_string(&self) -> String {
        let mut flags: Vec<&str> = self.flags().map(AsRef::as_ref).collect();
        flags.sort();
        flags.join("")
    }

    /// Set a flag on the email message.
    ///
    /// This also updates the path to the email message and renames the file on
    /// the file system.
    ///
    /// # Errors
    ///
    /// This method will return an error if there was an error renaming the
    /// file.
    pub fn set_flag(&mut self, flag: Flag) -> Result<(), Error> {
        if self.flags.insert(flag) {
            self.update()?;
        }
        Ok(())
    }

    /// Unset a flag on the email message.
    ///
    /// This also updates the path to the email message and renames the file on
    /// the file system.
    ///
    /// # Errors
    ///
    /// This method will return an error if there was an error renaming the
    /// file.
    pub fn unset_flag(&mut self, flag: Flag) -> Result<(), Error> {
        if self.flags.remove(&flag) {
            self.update()?;
        }
        Ok(())
    }

    /// Returns `true` if the email message has the supplied flag
    pub fn has_flag(&self, flag: Flag) -> bool {
        self.flags.contains(&flag)
    }

    /// Get the raw bytes of the email message.
    ///
    /// # Errors
    ///
    /// This method will return an error if the email message could not be read
    /// from the file system. This could be because the path does not exists, or
    /// if there was another read error (e.g. permission denied.)
    pub fn to_bytes(&self) -> io::Result<Vec<u8>> {
        read(&self.path)
    }
}

/// An iterator over email messages in a maildir (either from `cur`, `new` or
/// `tmp`).
///
/// This iterator produces a `Result<MailEntry>`, which can be an `Err` if an
/// error was encountered while trying to read file system properties on a
/// particular entry, or if an invalid file was found in the maildir. Files
/// starting with a dot (.) character in the maildir folder are ignored.
pub struct MailEntries {
    readdir: Option<ReadDir>,
    move_to_cur: bool,
}

impl MailEntries {
    pub(crate) fn new<P: AsRef<Path>>(path: P, move_to_cur: bool) -> MailEntries {
        MailEntries {
            readdir: read_dir(path).ok(),
            move_to_cur,
        }
    }
}

impl Iterator for MailEntries {
    type Item = Result<MailEntry, Error>;

    fn next(&mut self) -> Option<Self::Item> {
        if let Some(ref mut readdir) = self.readdir {
            for entry in readdir {
                let path = match entry {
                    Err(e) => return Some(Err(e.into())),
                    Ok(e) => e.path(),
                };

                if path.is_dir()
                    || path
                        .file_name()
                        .map_or(true, |n| n.to_string_lossy().starts_with('.'))
                {
                    continue;
                }

                let mut entry = MailEntry::from_path(path);

                if self.move_to_cur {
                    if let Ok(ref mut entry) = entry {
                        if let Err(e) = entry.move_to_cur() {
                            return Some(Err(e));
                        }
                    }
                }

                return Some(entry);
            }
        }

        None
    }
}