use std::{
fmt,
io::{Write, stdout},
};
use anyhow::{Result, bail};
use clap::Parser;
use mail_parser::{Addr, Address, HeaderValue, Message, MessageParser};
use pimalaya_cli::printer::Printer;
use schemars::JsonSchema;
use serde::Serialize;
use crate::account::context::Account;
use crate::shared::{client::EmailClient, mailbox::arg::MailboxArg};
#[derive(Debug, Parser)]
pub struct MessageReadCommand {
#[arg(value_name = "ID")]
pub id: String,
#[command(flatten)]
pub mailbox: MailboxArg,
#[arg(long)]
pub raw: bool,
}
impl MessageReadCommand {
pub fn execute(
self,
printer: &mut impl Printer,
account: &mut Account,
client: &mut EmailClient,
) -> Result<()> {
if self.raw && printer.is_json() {
bail!("`--raw` and `--json` cannot be combined");
}
let mailbox = self.mailbox.resolve(account)?;
let raw = client.get_message(&mailbox, &self.id)?;
if self.raw {
let mut out = stdout().lock();
out.write_all(&raw)?;
return Ok(());
}
let Some(parsed) = MessageParser::new().parse(&raw) else {
bail!("Failed to parse RFC 5322 message");
};
printer.out(MessageView(parsed.into_owned()))
}
}
#[derive(Serialize, JsonSchema)]
#[serde(transparent)]
pub struct MessageView(#[schemars(with = "serde_json::Value")] Message<'static>);
impl fmt::Display for MessageView {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for header in self.0.headers() {
let value = render_header_value(&header.value);
writeln!(f, "{}: {value}", header.name.as_str())?;
}
writeln!(f)?;
for (i, part) in self.0.text_bodies().enumerate() {
if i > 0 {
writeln!(f)?;
writeln!(f)?;
}
if let Some(contents) = part.text_contents() {
write!(f, "{}", contents.trim_end())?;
}
}
Ok(())
}
}
fn render_header_value(value: &HeaderValue) -> String {
match value {
HeaderValue::Text(text) => text.to_string(),
HeaderValue::TextList(list) => list
.iter()
.map(|text| text.as_ref())
.collect::<Vec<_>>()
.join(", "),
HeaderValue::Address(address) => {
let addrs: Vec<String> = match address {
Address::List(list) => list.iter().map(format_addr).collect(),
Address::Group(groups) => groups
.iter()
.flat_map(|group| group.addresses.iter())
.map(format_addr)
.collect(),
};
addrs.join(", ")
}
HeaderValue::DateTime(date) => date.to_rfc822(),
HeaderValue::ContentType(ctype) => match ctype.subtype() {
Some(subtype) => format!("{}/{subtype}", ctype.ctype()),
None => ctype.ctype().to_owned(),
},
HeaderValue::Received(_) | HeaderValue::Empty => String::new(),
}
}
fn format_addr(addr: &Addr) -> String {
let email = addr.address.as_deref().unwrap_or_default();
match addr.name.as_deref() {
Some(name) if !name.is_empty() => format!("{name} <{email}>"),
_ => email.to_owned(),
}
}