#![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_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}");
}
}