m2 0.0.0

Set of Unix tools to work with m2dirs
Documentation
#![feature(iter_intersperse)]
use std::{
    borrow::Cow,
    error::Error,
    io::{IsTerminal, stdin},
    str::FromStr,
};

use chrono::{DateTime as ChronoDateTime, Local, Locale};
use clap::Parser;
use m2::table::{Cell, Table};
use mail_parser::{Address, DateTime, MessageParser};

#[derive(Parser)]
struct Args {
    messages: Vec<String>,
    #[arg(short, long, default_value_t = 3)]
    padding: usize,
}

pub fn run() -> Result<(), Box<dyn Error>> {
    let args = Args::try_parse()?;

    let mut messages = args.messages;
    if !stdin().is_terminal() {
        messages.extend(stdin().lines().filter_map(Result::ok));
    }

    let mut table = Table::default().padding(args.padding);
    for message in messages {
        let bytes = std::fs::read(&message)?;
        let message = MessageParser::new()
            .parse_headers(&bytes)
            .ok_or_else(|| format!("no headers in message given by selector: {message}"))?;

        let mut row = Vec::new();

        row.push(Cell::new(match message.date() {
            Some(date) => format_date(date),
            None => "-".to_string(),
        }));

        row.push(Cell::new(match message.from() {
            Some(from) => format_address(from),
            None => "<no addressant>".to_string(),
        }));

        match message.subject() {
            Some(subj) => row.push(Cell::new(subj.replace('\n', " ").trim())),
            None => row.push(Cell::new("<no subject>")),
        }

        table.row(row);
    }

    eprint!("{table}");

    Ok(())
}

fn format_date(date: &DateTime) -> String {
    ChronoDateTime::from_timestamp(date.to_timestamp(), 0)
        .expect("out of range timestamp")
        .with_timezone(&Local)
        // .format("%d %b, %Y at %H:%M")
        .format_localized(
            "%c",
            std::env::var("LANG")
                .ok()
                .and_then(|lang| {
                    Locale::from_str(
                        lang.split(".")
                            .next()
                            .expect("split will always have one item"),
                    )
                    .ok()
                })
                .unwrap_or_default(),
        )
        .to_string()
}

fn format_address(addr: &Address) -> String {
    match addr {
        Address::List(addrs) => addrs
            .iter()
            .filter_map(|ad| ad.address.clone())
            .intersperse(Cow::Borrowed(", "))
            .collect(),
        Address::Group(gs) => gs
            .iter()
            .flat_map(|g| g.addresses.clone())
            .filter_map(|ad| ad.address)
            .intersperse(Cow::Borrowed(", "))
            .collect(),
    }
}

pub fn main() {
    if let Err(e) = run() {
        eprintln!("m2ls: {e}");
    }
}