ecr-core 0.2.0

Wire types shared by the ecr mail server and its clients
Documentation
use serde::{Deserialize, Serialize};
use std::fmt;
use std::path::PathBuf;

#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct AccountId(pub String);

impl AccountId {
    pub fn as_str(&self) -> &str {
        &self.0
    }
}

impl fmt::Display for AccountId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(&self.0)
    }
}

impl From<&str> for AccountId {
    fn from(s: &str) -> Self {
        AccountId(s.to_string())
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Account {
    pub id: AccountId,
    pub display_name: String,
    pub maildir_path: PathBuf,
    pub address: Option<String>,
    pub mbsync_channel: Option<String>,
    pub msmtp_account: Option<String>,
    pub folders: Vec<Folder>,
}

impl Account {
    pub fn tag_query(&self) -> String {
        format!("tag:{}", self.id)
    }

    pub fn path_query(&self) -> String {
        format!("path:\"{}/**\"", self.id)
    }

    pub fn folder(&self, name: &str) -> Option<&Folder> {
        self.folders.iter().find(|f| f.name == name)
    }

    pub fn can_sync(&self) -> bool {
        self.mbsync_channel.is_some()
    }

    pub fn can_send(&self) -> bool {
        self.msmtp_account.is_some()
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FolderKind {
    Inbox,
    Sent,
    Drafts,
    Trash,
    Junk,
    Archive,
    Other,
}

impl FolderKind {
    pub fn classify(name: &str) -> Self {
        let leaf = name.rsplit('/').next().unwrap_or(name);
        match leaf.to_ascii_lowercase().as_str() {
            "inbox" => FolderKind::Inbox,
            "sent" | "sent mail" | "sent items" => FolderKind::Sent,
            "drafts" | "draft" => FolderKind::Drafts,
            "trash" | "deleted items" | "bin" => FolderKind::Trash,
            "junk" | "spam" => FolderKind::Junk,
            "archive" | "all mail" => FolderKind::Archive,
            _ => FolderKind::Other,
        }
    }

    pub fn sort_rank(&self) -> u8 {
        match self {
            FolderKind::Inbox => 0,
            FolderKind::Drafts => 1,
            FolderKind::Sent => 2,
            FolderKind::Archive => 3,
            FolderKind::Junk => 4,
            FolderKind::Trash => 5,
            FolderKind::Other => 6,
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Folder {
    pub name: String,
    pub relative_path: String,
    pub kind: FolderKind,
}

impl Folder {
    pub fn new(account: &AccountId, relative_path: impl Into<String>) -> Self {
        let relative_path = relative_path.into();
        let name = relative_path.clone();
        Self {
            kind: FolderKind::classify(&name),
            relative_path: format!("{account}/{relative_path}"),
            name,
        }
    }

    pub fn query(&self) -> String {
        format!("path:\"{}/**\"", self.relative_path)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn folder_query_is_recursive_and_quoted() {
        let folder = Folder::new(&AccountId::from("main"), "Inbox");
        assert_eq!(folder.query(), "path:\"main/Inbox/**\"");
    }

    #[test]
    fn folder_names_with_spaces_stay_quoted() {
        let folder = Folder::new(&AccountId::from("main"), "General Payments");
        assert_eq!(folder.query(), "path:\"main/General Payments/**\"");
    }

    #[test]
    fn gmail_style_nested_folders_classify_on_the_leaf() {
        assert_eq!(FolderKind::classify("[Gmail]/Sent Mail"), FolderKind::Sent);
        assert_eq!(
            FolderKind::classify("[Gmail]/All Mail"),
            FolderKind::Archive
        );
        assert_eq!(FolderKind::classify("Swiggy"), FolderKind::Other);
    }

    #[test]
    fn classification_is_case_insensitive() {
        assert_eq!(FolderKind::classify("INBOX"), FolderKind::Inbox);
        assert_eq!(FolderKind::classify("Junk"), FolderKind::Junk);
    }
}