agent-first-mail 0.2.1

Let your AI agent work your inbox — email pulled into plain files it reads, sorts, and drafts on your machine, with nothing sent until you confirm.
Documentation
#![cfg_attr(
    not(test),
    deny(
        clippy::unwrap_used,
        clippy::expect_used,
        clippy::panic,
        clippy::print_stdout,
        clippy::print_stderr,
    )
)]

use agent_first_data::{
    build_cli_error, cli_output, cli_parse_output, OutputFormat, VersionConfig,
};
use agent_first_mail::cli;
use agent_first_mail::runner::run_command;
use serde_json::{json, Value};
use std::io::Write;
use std::time::Instant;

fn main() {
    let started = Instant::now();
    handle_help_and_version(&started);
    let argv = std::env::args().collect::<Vec<_>>();
    let parsed = match cli::parse_args() {
        Ok(mode) => mode,
        Err(err) => {
            let hint = cli::error_hint(&argv);
            let value = cli_error_with_trace(&err, Some(hint.as_str()), elapsed_ms(&started));
            let output = requested_output_format().unwrap_or(OutputFormat::Json);
            let _ = writeln!(std::io::stdout(), "{}", cli_output(&value, output));
            std::process::exit(2);
        }
    };
    let cli::ParsedArgs {
        command,
        output,
        log,
    } = parsed;
    std::process::exit(run_command(command, output, &log, &argv));
}

fn handle_help_and_version(started: &Instant) {
    let raw: Vec<String> = std::env::args().collect();
    match agent_first_data::cli_handle_version_or_continue(
        &raw,
        "afmail",
        env!("CARGO_PKG_VERSION"),
        &VersionConfig::conventional_default(),
    ) {
        Ok(Some(version)) => {
            let _ = write!(std::io::stdout(), "{version}");
            std::process::exit(0);
        }
        Ok(None) => {}
        Err(err) => {
            let mut err = err;
            attach_trace(&mut err, elapsed_ms(started));
            let _ = writeln!(
                std::io::stdout(),
                "{}",
                cli_output(&err, OutputFormat::Json)
            );
            std::process::exit(2);
        }
    }
    match agent_first_data::cli_handle_help_or_continue(
        &raw,
        &cli::command(),
        &agent_first_data::HelpConfig::human_cli_default(),
    ) {
        Ok(Some(help)) => {
            let _ = write!(std::io::stdout(), "{help}");
            std::process::exit(0);
        }
        Ok(None) => {}
        Err(err) => {
            let mut err = err;
            attach_trace(&mut err, elapsed_ms(started));
            let output = requested_output_format().unwrap_or(OutputFormat::Json);
            let _ = writeln!(std::io::stdout(), "{}", cli_output(&err, output));
            std::process::exit(2);
        }
    }
}

fn requested_output_format() -> Option<OutputFormat> {
    requested_output_format_result().ok().flatten()
}

fn requested_output_format_result() -> Result<Option<OutputFormat>, String> {
    let raw = std::env::args().collect::<Vec<_>>();
    let mut output = None;
    let mut iter = raw.iter().skip(1);
    while let Some(arg) = iter.next() {
        if arg == "--" {
            break;
        }
        if let Some(value) = arg.strip_prefix("--output=") {
            output = Some(cli_parse_output(value)?);
            continue;
        }
        if arg == "--output" {
            if let Some(value) = iter.next() {
                output = Some(cli_parse_output(value)?);
            } else {
                return Err("--output requires a value: expected json, yaml, or plain".to_string());
            }
        }
    }
    Ok(output)
}

fn cli_error_with_trace(message: &str, hint: Option<&str>, duration_ms: u64) -> Value {
    let mut value = build_cli_error(message, hint);
    attach_trace(&mut value, duration_ms);
    value
}

fn attach_trace(value: &mut Value, duration_ms: u64) {
    let Value::Object(map) = value else {
        return;
    };
    let trace = map.entry("trace").or_insert_with(|| json!({}));
    if !trace.is_object() {
        *trace = json!({});
    }
    if let Value::Object(trace_obj) = trace {
        trace_obj.insert("duration_ms".to_string(), json!(duration_ms));
    }
}

fn elapsed_ms(started: &Instant) -> u64 {
    started.elapsed().as_millis() as u64
}