vomit-config 0.4.0

Shared configuration library for all Vomit project tools
Documentation
use std::{fs, path::Path, process::Command};

use shellexpand::tilde;

use crate::{ConfigError, DEFAULT_MAILDIR};

pub(crate) struct TomlAccount<'a> {
    name: String,
    settings: &'a toml::value::Table,
    // Keep a copy of this, as it is potentially computed (tilde expansion)
    local: String,
}

pub(crate) struct TomlConfig {
    cfg: toml::value::Table,
}

impl TomlConfig {
    pub(crate) fn for_account(&self, name: Option<&str>) -> Option<TomlAccount<'_>> {
        let name = match name {
            Some(n) => n,
            // Leaving this a .expect() as load() already checks that there is an account
            None => self.cfg.keys().next().expect("no account found"),
        };

        if let Some(a) = self.cfg.get(name) {
            if let Some(t) = a.as_table() {
                // Compute the local maildir
                let mut path = DEFAULT_MAILDIR;
                if let Some(l) = t.get("local") {
                    if let Some(p) = l.as_str() {
                        path = p
                    }
                };

                return Some(TomlAccount {
                    name: String::from(name),
                    settings: t,
                    local: tilde(path).into_owned(),
                });
            }
        }
        None
    }
}

impl TomlAccount<'_> {
    pub(crate) fn name(&self) -> &String {
        &self.name
    }

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

    pub(crate) fn remote(&self) -> Option<&str> {
        self.settings.get("remote").and_then(|v| v.as_str())
    }

    pub(crate) fn user(&self) -> Option<&str> {
        self.settings.get("user").and_then(|v| v.as_str())
    }

    pub(crate) fn send_from(&self) -> Option<&str> {
        let v = self
            .settings
            .get("send")
            .and_then(|v| v.as_table())
            .and_then(|v| v.get("from"))
            .and_then(|v| v.as_str());
        v.or_else(|| self.user())
    }

    pub(crate) fn send_user(&self) -> Option<&str> {
        let v = self
            .settings
            .get("send")
            .and_then(|v| v.as_table())
            .and_then(|v| v.get("user"))
            .and_then(|v| v.as_str());
        v.or_else(|| self.user())
    }

    pub(crate) fn send_remote(&self) -> Option<&str> {
        let v = self
            .settings
            .get("send")
            .and_then(|v| v.as_table())
            .and_then(|v| v.get("remote"))
            .and_then(|v| v.as_str());
        v.or_else(|| self.remote())
    }

    fn pass_cmd(cmd: &str) -> Result<Option<String>, ConfigError> {
        let out = Command::new("sh").arg("-c").arg(cmd).output();
        match out {
            Ok(mut output) => {
                let newline: u8 = 10;
                if Some(&newline) == output.stdout.last() {
                    _ = output.stdout.pop(); // remove trailing newline
                }
                Ok(Some(String::from_utf8(output.stdout)?))
            }
            Err(e) => Err(ConfigError::PassExecError(e.to_string())),
        }
    }

    pub(crate) fn password(&self) -> Result<Option<String>, ConfigError> {
        if let Some(p) = self.settings.get("password").and_then(|v| v.as_str()) {
            return Ok(Some(String::from(p)));
        } else if let Some(cmd) = self.settings.get("pass-cmd").and_then(|v| v.as_str()) {
            return TomlAccount::pass_cmd(cmd);
        }
        Ok(None)
    }

    pub(crate) fn send_password(&self) -> Result<Option<String>, ConfigError> {
        let pass = self
            .settings
            .get("send")
            .and_then(|v| v.as_table())
            .and_then(|v| v.get("password"))
            .and_then(|v| v.as_str());
        if let Some(p) = pass {
            return Ok(Some(String::from(p)));
        }

        let pcmd = self
            .settings
            .get("send")
            .and_then(|v| v.as_table())
            .and_then(|v| v.get("pass-cmd"))
            .and_then(|v| v.as_str());
        if let Some(cmd) = pcmd {
            return TomlAccount::pass_cmd(cmd);
        }
        self.password()
    }
}

pub(crate) fn load_toml<P: AsRef<Path>>(path: P) -> Result<TomlConfig, ConfigError> {
    let contents = fs::read_to_string(path)?;

    let table = contents.parse::<toml::Table>()?;
    if table.is_empty() {
        Err(ConfigError::Error("no accounts found"))
    } else {
        Ok(TomlConfig { cfg: table })
    }
}

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

    #[test]
    fn test_simple() {
        let table: toml::value::Table = toml::from_str(
            r#"
            [example]
            local = '/home/test/.maildir'
            remote = 'mx.example.com'
            user = 'johndoe'
            password = 'hunter1'
        "#,
        )
        .unwrap();

        let config = TomlConfig { cfg: table };

        let acc = config.for_account(None).expect("no account found");

        assert_eq!(acc.user(), Some("johndoe"));
        assert_eq!(acc.remote(), Some("mx.example.com"));
        assert_eq!(
            acc.password().expect("failed to get password"),
            Some(String::from("hunter1"))
        );
        assert_eq!(acc.send_user(), Some("johndoe"));
        assert_eq!(acc.send_remote(), Some("mx.example.com"));
        assert_eq!(
            acc.send_password().expect("failed to get password"),
            Some(String::from("hunter1"))
        );
    }

    #[test]
    fn test_minimal() {
        let table: toml::value::Table = toml::from_str(
            r#"
            [example]
        "#,
        )
        .unwrap();

        let config = TomlConfig { cfg: table };

        let _ = config.for_account(None).expect("no account found");
    }

    #[test]
    fn test_send() {
        let table: toml::value::Table = toml::from_str(
            r#"
            [example]
            local = '/home/test/.maildir'
            remote = 'imap.example.com'
            user = 'johndoe'
            password = 'hunter1'
            send.remote = 'smtp.example.com'
            send.user = 'johndoe@example.com'
            send.password = 's3cr3t'
        "#,
        )
        .unwrap();

        let config = TomlConfig { cfg: table };

        let acc = config.for_account(None).expect("no account found");

        assert_eq!(acc.user(), Some("johndoe"));
        assert_eq!(acc.remote(), Some("imap.example.com"));
        assert_eq!(
            acc.password().expect("failed to get password"),
            Some(String::from("hunter1"))
        );
        assert_eq!(acc.send_user(), Some("johndoe@example.com"));
        assert_eq!(acc.send_remote(), Some("smtp.example.com"));
        assert_eq!(
            acc.send_password().expect("failed to get password"),
            Some(String::from("s3cr3t"))
        );
    }

    #[test]
    fn test_simple_cmd() {
        let table: toml::value::Table = toml::from_str(
            r#"
            [example]
            local = '/home/test/.maildir'
            remote = 'mx.example.com'
            user = 'johndoe'
            pass-cmd = 'echo hunter1'
        "#,
        )
        .unwrap();

        let config = TomlConfig { cfg: table };

        let acc = config.for_account(None).expect("no account found");

        assert_eq!(acc.user(), Some("johndoe"));
        assert_eq!(acc.remote(), Some("mx.example.com"));
        assert_eq!(
            acc.password().expect("failed to get password"),
            Some(String::from("hunter1"))
        );
        assert_eq!(acc.send_user(), Some("johndoe"));
        assert_eq!(acc.send_remote(), Some("mx.example.com"));
        assert_eq!(
            acc.send_password().expect("failed to get password"),
            Some(String::from("hunter1"))
        );
    }

    #[test]
    fn test_send_cmd() {
        let table: toml::value::Table = toml::from_str(
            r#"
            [example]
            local = '/home/test/.maildir'
            remote = 'imap.example.com'
            user = 'johndoe'
            pass-cmd = 'echo hunter1'
            send.remote = 'smtp.example.com'
            send.user = 'johndoe@example.com'
            send.pass-cmd = 'echo s3cr3t'
        "#,
        )
        .unwrap();

        let config = TomlConfig { cfg: table };

        let acc = config.for_account(None).expect("no account found");

        assert_eq!(acc.user(), Some("johndoe"));
        assert_eq!(acc.remote(), Some("imap.example.com"));
        assert_eq!(
            acc.password().expect("failed to get password"),
            Some(String::from("hunter1"))
        );
        assert_eq!(acc.send_user(), Some("johndoe@example.com"));
        assert_eq!(acc.send_remote(), Some("smtp.example.com"));
        assert_eq!(
            acc.send_password().expect("failed to get password"),
            Some(String::from("s3cr3t"))
        );
    }
}