simplemailclient 0.2.0

A simple terminal mail client (SMTP send, IMAP fetch) with a TUI.
use std::fs;
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use anyhow::{Context, Result};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SmtpConfig {
    pub host: String,
    pub port: u16,
    pub username: String,
    pub password: String,
    pub tls: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImapConfig {
    pub host: String,
    pub port: u16,
    pub username: String,
    pub password: String,
    pub tls: bool,
    pub inbox_folder: String,
    /// Server folder to move deleted messages into.
    /// Gmail users: set to "[Gmail]/Trash".  Others: "Trash" or "Deleted".
    #[serde(default = "default_trash_folder")]
    pub trash_folder: String,
}

fn default_trash_folder() -> String {
    "Trash".to_string()
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Keybinds {
    pub quit: String,
    pub compose: String,
    pub reply: String,
    pub delete: String,
    pub star: String,
    pub sync: String,
    pub next_folder: String,
    pub prev_folder: String,
}

impl Default for Keybinds {
    fn default() -> Self {
        Self {
            quit: "q".into(),
            compose: "c".into(),
            reply: "r".into(),
            delete: "d".into(),
            star: "s".into(),
            sync: "S".into(),
            next_folder: "Tab".into(),
            prev_folder: "BackTab".into(),
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
    pub identity: String,
    pub display_name: Option<String>,
    /// Domain appended to bare usernames — defaults to "gmail.com"
    #[serde(default = "default_domain")]
    pub domain: String,
    pub smtp: SmtpConfig,
    pub imap: ImapConfig,
    #[serde(default)]
    pub keybinds: Keybinds,
}

fn default_domain() -> String {
    "gmail.com".to_string()
}

impl Config {
    /// Resolve a bare username or full address to a full address.
    /// "hanzo"          → "hanzo@gmail.com"
    /// "hanzo@other.io" → "hanzo@other.io"  (left alone)
    pub fn resolve_address(&self, input: &str) -> String {
        if input.contains('@') {
            input.to_string()
        } else {
            format!("{}@{}", input, self.domain)
        }
    }

    pub fn config_path() -> PathBuf {
        dirs::config_dir()
            .unwrap_or_else(|| PathBuf::from("."))
            .join("mail-rs")
            .join("config.toml")
    }

    pub fn load() -> Result<Self> {
        let path = Self::config_path();

        if !path.exists() {
            // Write a starter config and bail with instructions
            let example = Self::example_toml();
            if let Some(parent) = path.parent() {
                fs::create_dir_all(parent)?;
            }
            fs::write(&path, &example)?;
            eprintln!("No config found. A starter config has been written to:");
            eprintln!("  {}", path.display());
            eprintln!();
            eprintln!("Edit it with your SMTP/IMAP credentials, then re-run mailrs.");
            std::process::exit(0);
        }

        let raw = fs::read_to_string(&path)
            .with_context(|| format!("Cannot read {}", path.display()))?;
        toml::from_str(&raw)
            .with_context(|| format!("Invalid TOML in {}", path.display()))
    }

    fn example_toml() -> String {
        r#"# mail-rs configuration
# ~/.config/mail-rs/config.toml

identity     = "you@gmail.com"
display_name = "Your Name"    # optional
domain       = "gmail.com"    # bare usernames expand to this: mailrs send hanzo → hanzo@gmail.com

[smtp]
host     = "smtp.gmail.com"
port     = 587
username = "you@gmail.com"
password = "your-app-password"   # use a Gmail App Password, not your real password
tls      = true

[imap]
host          = "imap.gmail.com"
port          = 993
username      = "you@gmail.com"
password      = "your-app-password"
tls           = true
inbox_folder  = "INBOX"
trash_folder  = "[Gmail]/Trash"  # Gmail; other providers may use "Trash" or "Deleted"

[keybinds]
quit         = "q"
compose      = "c"
reply        = "r"
delete       = "d"
star         = "s"
sync         = "S"
next_folder  = "Tab"
prev_folder  = "BackTab"
"#
        .to_string()
    }
}