use std::fmt;
use anyhow::{Result, anyhow};
use clap::Parser;
use io_gmail::v1::rest::messages::{
GmailMessageFormat, GmailMessageHeader, GmailMessagePayload, decode_raw,
};
use pimalaya_cli::printer::Printer;
use schemars::JsonSchema;
use serde::Serialize;
use crate::{
gmail::{client::GmailClient, format::FormatArg},
shared::output::write_bytes_or_save,
};
#[derive(Debug, Parser)]
pub struct GmailMessageGetCommand {
#[arg(value_name = "ID")]
pub id: String,
#[arg(long, value_enum, default_value_t)]
pub format: FormatArg,
#[arg(long = "header", value_name = "NAME")]
pub headers: Vec<String>,
}
impl GmailMessageGetCommand {
pub fn execute(self, printer: &mut impl Printer, client: &mut GmailClient) -> Result<()> {
let format = GmailMessageFormat::from(self.format);
let hs: Vec<&str> = self.headers.iter().map(String::as_str).collect();
let msg = client.message_get(&self.id, format, &hs)?.response;
if format == GmailMessageFormat::Raw
&& let Some(raw) = &msg.raw
{
let bytes =
decode_raw(raw).map_err(|err| anyhow!("Decode Gmail message error: {err}"))?;
return write_bytes_or_save(printer, None, &bytes);
}
printer.out(GmailMessageGetOutput {
id: msg.id,
thread_id: msg.thread_id,
label_ids: msg.label_ids,
snippet: msg.snippet,
size_estimate: msg.size_estimate,
internal_date: msg.internal_date,
headers: message_headers(msg.payload, &hs),
})
}
}
#[derive(Serialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub(crate) struct GmailMessageGetOutput {
id: String,
#[serde(skip_serializing_if = "Option::is_none")]
thread_id: Option<String>,
label_ids: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
snippet: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
size_estimate: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
internal_date: Option<String>,
headers: Vec<GmailMessageHeaderOutput>,
}
impl fmt::Display for GmailMessageGetOutput {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "Id: {}", self.id)?;
if let Some(thread_id) = &self.thread_id {
writeln!(f, "Thread: {thread_id}")?;
}
if !self.label_ids.is_empty() {
writeln!(f, "Labels: {}", self.label_ids.join(", "))?;
}
if let Some(snippet) = &self.snippet {
writeln!(f, "Snippet: {snippet}")?;
}
if let Some(size) = self.size_estimate {
writeln!(f, "Size: {size}")?;
}
if let Some(internal_date) = &self.internal_date {
writeln!(f, "Internal date: {internal_date}")?;
}
for header in &self.headers {
writeln!(f, "{}: {}", header.name, header.value)?;
}
Ok(())
}
}
#[derive(Serialize, JsonSchema)]
pub(crate) struct GmailMessageHeaderOutput {
pub name: String,
pub value: String,
}
impl From<GmailMessageHeader> for GmailMessageHeaderOutput {
fn from(header: GmailMessageHeader) -> Self {
Self {
name: header.name,
value: header.value,
}
}
}
pub(crate) fn message_headers(
payload: Option<GmailMessagePayload>,
names: &[&str],
) -> Vec<GmailMessageHeaderOutput> {
payload
.map(|payload| payload.headers)
.unwrap_or_default()
.into_iter()
.filter(|header| {
names.is_empty()
|| names
.iter()
.any(|name| name.eq_ignore_ascii_case(&header.name))
})
.map(GmailMessageHeaderOutput::from)
.collect()
}
#[cfg(test)]
mod tests {
use io_gmail::v1::rest::messages::{GmailMessageHeader, GmailMessagePayload};
use super::message_headers;
fn payload(headers: &[(&str, &str)]) -> Option<GmailMessagePayload> {
Some(GmailMessagePayload {
headers: headers
.iter()
.map(|(name, value)| GmailMessageHeader {
name: name.to_string(),
value: value.to_string(),
})
.collect(),
..Default::default()
})
}
#[test]
fn no_requested_name_keeps_every_header() {
let headers = message_headers(payload(&[("Subject", "hi"), ("From", "a@b")]), &[]);
let names: Vec<_> = headers.iter().map(|h| h.name.as_str()).collect();
assert_eq!(names, ["Subject", "From"]);
}
#[test]
fn requested_names_match_case_insensitively() {
let headers = message_headers(
payload(&[("Subject", "hi"), ("From", "a@b"), ("To", "c@d")]),
&["subject", "TO"],
);
let names: Vec<_> = headers.iter().map(|h| h.name.as_str()).collect();
assert_eq!(names, ["Subject", "To"]);
}
#[test]
fn repeated_headers_keep_their_order_and_duplicates() {
let headers = message_headers(
payload(&[("Received", "one"), ("Subject", "hi"), ("Received", "two")]),
&["Received"],
);
let values: Vec<_> = headers.iter().map(|h| h.value.as_str()).collect();
assert_eq!(values, ["one", "two"]);
}
#[test]
fn a_payload_less_message_has_no_header() {
assert!(message_headers(None, &["Subject"]).is_empty());
}
}