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
use crate::cli::Command;
use crate::error::Result;
use agent_first_data::{cli_output, OutputFormat};
use serde_json::{json, Map, Value};
use std::io::Write;
use std::time::{SystemTime, UNIX_EPOCH};

pub fn emit_result(result: Result<Value>, output: OutputFormat, duration_ms: u64) -> i32 {
    match result {
        Ok(mut value) => {
            attach_trace(&mut value, duration_ms);
            emit_value(&value, output);
            0
        }
        Err(err) => {
            let mut value = err.to_value();
            attach_trace(&mut value, duration_ms);
            emit_value(&value, output);
            1
        }
    }
}

pub fn emit_value(value: &Value, output: OutputFormat) {
    let rendered = cli_output(value, output);
    let _ = writeln!(std::io::stdout(), "{rendered}");
}

fn attach_trace(value: &mut Value, duration_ms: u64) {
    let Value::Object(map) = value else {
        return;
    };
    map.remove("duration_ms");
    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));
    }
}

pub(super) fn log_event(event: &str, level: &str, fields: Value, duration_ms: u64) -> Value {
    let mut value = match fields {
        Value::Object(map) => Value::Object(map),
        _ => json!({}),
    };
    if let Value::Object(map) = &mut value {
        let default_message = if map.contains_key("message") {
            None
        } else {
            Some(log_message(event, map))
        };
        map.insert("code".to_string(), json!("log"));
        map.insert("level".to_string(), json!(level));
        map.insert("event".to_string(), json!(event));
        map.insert(
            "timestamp_epoch_ms".to_string(),
            json!(timestamp_epoch_ms()),
        );
        if let Some(message) = default_message {
            map.insert("message".to_string(), json!(message));
        }
        map.insert("trace".to_string(), json!({"duration_ms": duration_ms}));
    }
    value
}

fn timestamp_epoch_ms() -> u64 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|duration| duration.as_millis() as u64)
        .unwrap_or(0)
}

fn log_message(event: &str, fields: &Map<String, Value>) -> String {
    match event {
        "startup" => "afmail startup".to_string(),
        "request" => "afmail request started".to_string(),
        "progress" => match fields.get("phase").and_then(Value::as_str) {
            Some("finish") => "afmail command finished".to_string(),
            Some(phase) => format!("afmail command progress: {phase}"),
            None => "afmail command progress".to_string(),
        },
        "retry" => "afmail retryable error".to_string(),
        _ => "afmail log event".to_string(),
    }
}

pub(super) fn log_enabled(filters: &[String], event: &str) -> bool {
    filters.iter().any(|filter| filter == event)
}

pub(super) fn redact_argv(argv: &[String]) -> Vec<String> {
    let mut redact_next = false;
    let mut out = Vec::with_capacity(argv.len());
    for arg in argv {
        if redact_next {
            out.push("***".to_string());
            redact_next = false;
            continue;
        }
        if is_secret_assignment(arg) || arg.starts_with("literal:") || arg.contains("=literal:") {
            out.push("***".to_string());
            continue;
        }
        if is_secret_arg_name(arg) {
            out.push(arg.clone());
            redact_next = true;
            continue;
        }
        out.push(arg.clone());
    }
    out
}

fn is_secret_assignment(arg: &str) -> bool {
    arg.contains(".password_secret=")
        || (arg.contains("_secret=") && !arg.contains("_secret_env="))
        || (arg.contains("-secret=") && !arg.contains("-secret-env="))
}

fn is_secret_arg_name(arg: &str) -> bool {
    arg.ends_with(".password_secret")
        || arg.ends_with("_secret")
        || arg.ends_with("-secret")
        || arg == "--password-secret"
}

pub(super) fn output_format_name(output: OutputFormat) -> &'static str {
    match output {
        OutputFormat::Json => "json",
        OutputFormat::Yaml => "yaml",
        OutputFormat::Plain => "plain",
    }
}

pub(super) fn command_name(command: &Command) -> &'static str {
    match command {
        Command::Init { .. } => "init",
        Command::Pull { .. } => "pull",
        Command::Config { .. } => "config",
        Command::Remote { .. } => "remote",
        Command::Push { .. } => "push",
        Command::Status => "status",
        Command::Doctor { .. } => "doctor",
        Command::Purge { .. } => "purge",
        Command::Skill { .. } => "skill",
        Command::Triage { .. } => "triage",
        Command::Message { .. } => "message",
        Command::Case { .. } => "case",
        Command::Archive { .. } => "archive",
        Command::Render { .. } => "render",
        Command::Log { .. } => "log",
        #[cfg(feature = "ui")]
        Command::Ui(_) => "ui",
    }
}