himalaya 1.0.0-beta

CLI to manage emails
pub mod arg;
pub mod args;

use anyhow::{anyhow, Context, Result};
use email::account::config::AccountConfig;
use log::{debug, trace};
use std::path::{Path, PathBuf};

const ID_MAPPER_DB_FILE_NAME: &str = ".id-mapper.sqlite";

#[derive(Debug)]
pub enum IdMapper {
    Dummy,
    Mapper(String, rusqlite::Connection),
}

impl IdMapper {
    pub fn find_closest_db_path(dir: impl AsRef<Path>) -> PathBuf {
        let mut db_path = dir.as_ref().join(ID_MAPPER_DB_FILE_NAME);
        let mut db_parent_dir = dir.as_ref().parent();

        while !db_path.is_file() {
            match db_parent_dir {
                Some(dir) => {
                    db_path = dir.join(ID_MAPPER_DB_FILE_NAME);
                    db_parent_dir = dir.parent();
                }
                None => {
                    db_path = dir.as_ref().join(ID_MAPPER_DB_FILE_NAME);
                    break;
                }
            }
        }

        db_path
    }

    pub fn new(account_config: &AccountConfig, folder: &str, db_path: PathBuf) -> Result<Self> {
        let folder = account_config.get_folder_alias(folder);
        let digest = md5::compute(account_config.name.clone() + &folder);
        let table = format!("id_mapper_{digest:x}");
        debug!("creating id mapper table {table} at {db_path:?}…");

        let db_path = Self::find_closest_db_path(db_path);
        let conn = rusqlite::Connection::open(&db_path)
            .with_context(|| format!("cannot open id mapper database at {db_path:?}"))?;

        let query = format!(
            "CREATE TABLE IF NOT EXISTS {table} (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                internal_id TEXT UNIQUE
            )",
        );
        trace!("create table query: {query:#?}");

        conn.execute(&query, [])
            .context("cannot create id mapper table")?;

        Ok(Self::Mapper(table, conn))
    }

    pub fn create_alias<I>(&self, id: I) -> Result<String>
    where
        I: AsRef<str>,
    {
        let id = id.as_ref();
        match self {
            Self::Dummy => Ok(id.to_owned()),
            Self::Mapper(table, conn) => {
                debug!("creating alias for id {id}…");

                let query = format!("INSERT OR IGNORE INTO {} (internal_id) VALUES (?)", table);
                trace!("insert query: {query:#?}");

                conn.execute(&query, [id])
                    .with_context(|| format!("cannot create id alias for id {id}"))?;

                let alias = conn.last_insert_rowid().to_string();
                debug!("created alias {alias} for id {id}");

                Ok(alias)
            }
        }
    }

    pub fn get_or_create_alias<I>(&self, id: I) -> Result<String>
    where
        I: AsRef<str>,
    {
        let id = id.as_ref();
        match self {
            Self::Dummy => Ok(id.to_owned()),
            Self::Mapper(table, conn) => {
                debug!("getting alias for id {id}…");

                let query = format!("SELECT id FROM {} WHERE internal_id = ?", table);
                trace!("select query: {query:#?}");

                let mut stmt = conn
                    .prepare(&query)
                    .with_context(|| format!("cannot get alias for id {id}"))?;
                let aliases: Vec<i64> = stmt
                    .query_map([id], |row| row.get(0))
                    .with_context(|| format!("cannot get alias for id {id}"))?
                    .collect::<rusqlite::Result<_>>()
                    .with_context(|| format!("cannot get alias for id {id}"))?;
                let alias = match aliases.first() {
                    Some(alias) => {
                        debug!("found alias {alias} for id {id}");
                        alias.to_string()
                    }
                    None => {
                        debug!("alias not found, creating it…");
                        self.create_alias(id)?
                    }
                };

                Ok(alias)
            }
        }
    }

    pub fn get_id<A>(&self, alias: A) -> Result<String>
    where
        A: ToString,
    {
        let alias = alias.to_string();
        let alias = alias
            .parse::<i64>()
            .context(format!("cannot parse id mapper alias {alias}"))?;

        match self {
            Self::Dummy => Ok(alias.to_string()),
            Self::Mapper(table, conn) => {
                debug!("getting id from alias {alias}…");

                let query = format!("SELECT internal_id FROM {} WHERE id = ?", table);
                trace!("select query: {query:#?}");

                let mut stmt = conn
                    .prepare(&query)
                    .with_context(|| format!("cannot get id from alias {alias}"))?;
                let ids: Vec<String> = stmt
                    .query_map([alias], |row| row.get(0))
                    .with_context(|| format!("cannot get id from alias {alias}"))?
                    .collect::<rusqlite::Result<_>>()
                    .with_context(|| format!("cannot get id from alias {alias}"))?;
                let id = ids
                    .first()
                    .ok_or_else(|| anyhow!("cannot get id from alias {alias}"))?
                    .to_owned();
                debug!("found id {id} from alias {alias}");

                Ok(id)
            }
        }
    }

    pub fn get_ids<A, I>(&self, aliases: I) -> Result<Vec<String>>
    where
        A: ToString,
        I: IntoIterator<Item = A>,
    {
        aliases
            .into_iter()
            .map(|alias| self.get_id(alias))
            .collect()
    }
}