vmp 1.0.0-alpha.2

CLI tool to quickly pick mails from an m2dir
use clap::{Args, Parser};
use colored::*;
use std::process::exit;
use vomit::Options;

const VERSION: &str = env!("CARGO_PKG_VERSION");
const AUTHORS: &str = env!("CARGO_PKG_AUTHORS");

/// Efficiently work with emails or attachments from the command line.
///
/// Quickly and interactively pick mails (and more) from an m2dir to use their
/// filesystem path with other tools.
///
/// Part of Vomit, the Very Opinionated Mail Interaction Toolkit.
#[derive(Parser)]
#[clap(name = "vmt", version = VERSION, author = AUTHORS, max_term_width = 80)]
struct Opts {
    #[command(flatten)]
    target: Target,

    /// Allow selection of multiple items
    #[arg(short, long)]
    multiple: bool,

    /// Only list target-level items
    #[arg(short, long)]
    list: bool,

    /// Show empty mailboxes when picking a mailbox
    #[arg(short, long)]
    empty: bool,

    /// Show unread messages count for mailboxes
    #[arg(short, long)]
    unread: bool,

    /// Show attachment marker (can be slow)
    ///
    /// Shows a marker on all mails with attachments. Doing so requires parsing
    /// mail bodies in addition to the headers, so it can make loading large
    /// amounts of mails a bit slower.
    #[arg(long = "am")]
    marker: bool,

    /// Sets a custom config file location
    ///
    /// A configuration is only required in certain cases, see vmp(1).
    #[clap(short, long, value_parser)]
    config: Option<String>,

    /// The account to operate on (defaults to first account in config file)
    #[clap(short, long, value_parser)]
    account: Option<String>,

    /// Starting point (default: m2store root from configuration)
    ///
    /// Can be an m2store root, an m2dir, or a message file. Must be either an
    /// actual filesystem path, or a path relative to a configured account (e.g.
    /// simply `INBOX`). In the latter case, a working vomit configuration is
    /// required, see vmp(1).
    #[clap(value_parser)]
    pub path: Option<String>,
}

#[derive(Args)]
#[group(multiple = false)]
struct Target {
    /// Pick a mailbox (default: pick a mail)
    #[arg(short = 'B', long)]
    mailbox: bool,

    /// Pick an attachment (default: pick a mail)
    #[arg(short = 'A', long)]
    attachment: bool,

    /// Pick a MIME part (default: pick a mail)
    #[arg(short = 'P', long)]
    part: bool,
}

const E_NON_UTF8: &str = "path contains non-UTF8 character";
const E_KEEP_TMP: &str = "error persisting tmp file";

fn pick_part(path: Option<&str>, o: &Options) -> Result<(), vomit::Error> {
    for part in vomit::pick_part(path, o)? {
        let (_, path) = part.as_tmp_file()?.keep().expect(E_KEEP_TMP);
        println!("{}", path.to_str().expect(E_NON_UTF8));
    }
    Ok(())
}

fn pick_attachment(path: Option<&str>, o: &Options) -> Result<(), vomit::Error> {
    for attachment in vomit::pick_attachment(path, o)? {
        let (_, path) = attachment.as_tmp_file()?.keep().expect(E_KEEP_TMP);
        println!("{}", path.to_str().expect(E_NON_UTF8));
    }
    Ok(())
}

fn pick_mail(path: Option<&str>, o: &Options) -> Result<(), vomit::Error> {
    for mail in vomit::pick_mail(path, o)? {
        println!("{}", mail.path.to_str().expect(E_NON_UTF8));
    }
    Ok(())
}

fn pick_mailbox(path: Option<&str>, o: &Options) -> Result<(), vomit::Error> {
    for m in vomit::pick_mailbox(path, o)? {
        println!("{}", m.path.to_str().expect(E_NON_UTF8));
    }
    Ok(())
}

fn ls_part(path: Option<&str>, o: &Options) -> Result<(), vomit::Error> {
    for p in vomit::ls_part(path, o)? {
        println!(
            "{:<7.7} {:>10.10} {:<30.30} {}",
            p.index(),
            p.human_size().green(),
            p.mimetype().bright_blue(),
            p.name.as_deref().unwrap_or_default().bright_white(),
        );
    }
    Ok(())
}

fn ls_attachment(path: Option<&str>, o: &Options) -> Result<(), vomit::Error> {
    for a in vomit::ls_attachment(path, o)? {
        println!(
            "{:>10.10} {} {}",
            a.part.human_size().green(),
            a.name,
            format!("({})", a.mimetype()).bright_blue(),
        );
    }
    Ok(())
}

fn ls_mail(path: Option<&str>, o: &Options) -> Result<(), vomit::Error> {
    for m in vomit::ls_mail(path, o)? {
        let date = match m.date {
            None => format!("{:<16.16}", "<no date>").white().dimmed(),
            Some(d) => format!("{:<16.16}", d.format("%Y-%m-%d %H:%M")).bright_blue(),
        };
        println!(
            "{} {:<30.30} {}",
            date,
            m.from.green(),
            if m.unread {
                m.subject.bold()
            } else {
                m.subject.normal()
            }
        );
    }
    Ok(())
}

fn ls_mailbox(path: Option<&str>, o: &Options) -> Result<(), vomit::Error> {
    for m in vomit::ls_mailbox(path, o)? {
        let total = format!("{:>5} ", m.total).bright_blue();
        let unread = m
            .unread
            .map(|u| format!("{:>5} ", u))
            .unwrap_or_default()
            .green();
        println!(
            "{}{}{}",
            total,
            unread,
            m.name
                .unwrap_or_else(|| m.path.to_string_lossy().into_owned()),
        );
    }
    Ok(())
}

fn main() {
    let opts: Opts = Opts::parse();

    let o = vomit::Options {
        account: opts.account,
        multiple: opts.multiple,
        date: true,
        attachment_marker: opts.marker || opts.target.attachment,
        attachments_only: opts.target.attachment,
        empty: opts.empty,
        mailbox_unread: opts.unread,
        config: opts.config,
    };

    let path = opts.path.as_deref();

    let r = if opts.list {
        if opts.target.part {
            ls_part(path, &o)
        } else if opts.target.attachment {
            ls_attachment(path, &o)
        } else if opts.target.mailbox {
            ls_mailbox(path, &o)
        } else {
            ls_mail(path, &o)
        }
    } else if opts.target.part {
        pick_part(path, &o)
    } else if opts.target.attachment {
        pick_attachment(path, &o)
    } else if opts.target.mailbox {
        pick_mailbox(path, &o)
    } else {
        pick_mail(path, &o)
    };
    if let Err(e) = r {
        eprintln!("error: {}", e);
        exit(1);
    }
}