asfml 0.1.1

CLI for reading Apache Pony Mail archives
mod output;

use std::io::{self, IsTerminal, Read};

use asfml_core::{
    Error, ListAddress, PonyMailClient, Result, Session, clear_session, load_session,
    parse_ponymail_cookie, store_session, validate_session,
};
use clap::{Args, Parser, Subcommand};

use crate::output::{
    ReadFormat, TableFormat, print_email, print_error, print_summaries, print_thread,
};

#[derive(Debug, Parser)]
#[command(
    version,
    about = "Read Apache Pony Mail archives from lists.apache.org"
)]
struct Cli {
    #[command(subcommand)]
    command: Command,
}

#[derive(Debug, Subcommand)]
enum Command {
    #[command(about = "Manage the stored lists.apache.org session")]
    Auth(AuthCommand),
    #[command(about = "List recent emails from a mailing list")]
    List(ListCommand),
    #[command(about = "Search emails in a mailing list")]
    Search(SearchCommand),
    #[command(about = "Read an email, its parent/root, or its thread")]
    Read(ReadCommand),
}

#[derive(Debug, Args)]
struct AuthCommand {
    #[command(subcommand)]
    command: AuthSubcommand,
}

#[derive(Debug, Subcommand)]
enum AuthSubcommand {
    #[command(about = "Store a manually copied ponymail cookie")]
    Set,
    #[command(about = "Validate the stored session")]
    Status { list: Option<String> },
    #[command(about = "Delete the stored session")]
    Clear,
}

#[derive(Debug, Args)]
struct ListCommand {
    list: String,

    #[arg(long, default_value = "30d")]
    since: String,

    #[arg(long, default_value_t = 50)]
    limit: usize,

    #[arg(long, value_enum, default_value_t = TableFormat::Table)]
    format: TableFormat,
}

#[derive(Debug, Args)]
struct SearchCommand {
    list: String,

    query: String,

    #[arg(long, default_value = "1y")]
    since: String,

    #[arg(long, default_value_t = 50)]
    limit: usize,

    #[arg(long, value_enum, default_value_t = TableFormat::Table)]
    format: TableFormat,
}

#[derive(Debug, Args)]
struct ReadCommand {
    mid: String,

    #[arg(long, conflicts_with_all = ["root", "thread"])]
    parent: bool,

    #[arg(long, conflicts_with_all = ["parent", "thread"])]
    root: bool,

    #[arg(long, conflicts_with_all = ["parent", "root"])]
    thread: bool,

    #[arg(long, value_enum, default_value_t = ReadFormat::Text)]
    format: ReadFormat,
}

fn main() {
    if let Err(error) = run() {
        print_error(&error);
        std::process::exit(1);
    }
}

fn run() -> Result<()> {
    let cli = Cli::parse();
    match cli.command {
        Command::Auth(command) => handle_auth(command),
        Command::List(command) => handle_list(command),
        Command::Search(command) => handle_search(command),
        Command::Read(command) => handle_read(command),
    }
}

fn handle_auth(command: AuthCommand) -> Result<()> {
    match command.command {
        AuthSubcommand::Set => {
            let ponymail = read_cookie_from_stdin()?;
            let session = Session { ponymail };
            let client = PonyMailClient::new(Some(session.clone()))?;
            let user = validate_session(&client, None)?;
            store_session(&session)?;
            println!("Stored session for lists.apache.org.");
            println!("Logged in as {user}.");
            Ok(())
        }
        AuthSubcommand::Status { list } => {
            let session = load_session()?;
            let client = PonyMailClient::new(Some(session))?;
            let list = parse_optional_list(list)?;
            let user = validate_session(&client, list.as_ref())?;
            println!("Logged in as {user}.");
            if let Some(list) = list {
                println!("Access: {list} yes");
            }
            Ok(())
        }
        AuthSubcommand::Clear => {
            clear_session()?;
            println!("Cleared session for lists.apache.org.");
            Ok(())
        }
    }
}

fn handle_list(command: ListCommand) -> Result<()> {
    let list = ListAddress::parse(&command.list)?;
    let client = client_for_list(&list)?;
    let emails = client.list(&list, &command.since, command.limit)?;
    print_summaries(&emails, command.format)
}

fn handle_search(command: SearchCommand) -> Result<()> {
    let list = ListAddress::parse(&command.list)?;
    let client = client_for_list(&list)?;
    let emails = client.search(&list, &command.query, &command.since, command.limit)?;
    print_summaries(&emails, command.format)
}

fn handle_read(command: ReadCommand) -> Result<()> {
    let client = client_with_optional_session()?;
    if command.parent {
        let thread = client.thread(&command.mid)?;
        let parent = thread.direct_parent(&command.mid)?;
        print_email(parent, command.format)
    } else if command.root {
        let thread = client.thread(&command.mid)?;
        let root = thread.root_parent(&command.mid)?;
        print_email(root, command.format)
    } else if command.thread {
        let thread = client.thread(&command.mid)?;
        print_thread(&thread, command.format)
    } else {
        let email = client.email(&command.mid)?;
        print_email(&email, command.format)
    }
}

fn client_for_list(list: &ListAddress) -> Result<PonyMailClient> {
    let session = match load_session() {
        Ok(session) => Some(session),
        Err(Error::NoSession) => None,
        Err(error) => return Err(error),
    };
    let client = PonyMailClient::new(session)?;
    let prefs = client.preferences()?;
    if prefs.has_list_access(list) {
        return Ok(client);
    }
    if list.list == "private" {
        return Err(Error::NoSession);
    }
    Err(Error::NoListAccess(list.to_string()))
}

fn client_with_optional_session() -> Result<PonyMailClient> {
    let session = match load_session() {
        Ok(session) => Some(session),
        Err(Error::NoSession) => None,
        Err(error) => return Err(error),
    };
    PonyMailClient::new(session)
}

fn parse_optional_list(list: Option<String>) -> Result<Option<ListAddress>> {
    list.as_deref().map(ListAddress::parse).transpose()
}

fn read_cookie_from_stdin() -> Result<String> {
    let input = if io::stdin().is_terminal() {
        rpassword::prompt_password(
            "Paste Cookie header or ponymail cookie value from lists.apache.org: ",
        )?
    } else {
        let mut input = String::new();
        io::stdin().read_to_string(&mut input)?;
        input
    };

    parse_ponymail_cookie(&input)
}