use std::fs;
use std::io::{self, Read};
use std::path::PathBuf;
use clap::{Parser, ValueEnum};
use dmarc_report_parser::Report;
mod render;
#[derive(Parser)]
#[command(name = "dmarc-report", version, about)]
struct Cli {
file: PathBuf,
#[arg(short, long, value_enum, default_value_t = Format::Terminal)]
format: Format,
#[arg(short, long)]
output: Option<PathBuf>,
}
#[derive(Clone, Copy, ValueEnum)]
enum Format {
Terminal,
Html,
Markdown,
}
fn main() {
let cli = Cli::parse();
let report = match load_report(&cli.file) {
Ok(r) => r,
Err(e) => {
eprintln!("Error: {e}");
std::process::exit(1);
}
};
let rendered = match cli.format {
Format::Terminal => render::terminal(&report),
Format::Html => render::html(&report),
Format::Markdown => render::markdown(&report),
};
match cli.output {
Some(path) => {
if let Err(e) = fs::write(&path, &rendered) {
eprintln!("Error writing to {}: {e}", path.display());
std::process::exit(1);
}
eprintln!("Report written to {}", path.display());
}
None => print!("{rendered}"),
}
}
fn load_report(path: &PathBuf) -> Result<Report, String> {
let filename = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("")
.to_lowercase();
let xml_bytes = if filename.ends_with(".zip") {
extract_xml_from_zip(path).map_err(|e| format!("Failed to read zip: {e}"))?
} else if filename.ends_with(".gz") {
decompress_gzip(path).map_err(|e| format!("Failed to decompress gzip: {e}"))?
} else {
fs::read(path).map_err(|e| format!("Failed to read file: {e}"))?
};
let xml =
std::str::from_utf8(&xml_bytes).map_err(|e| format!("File is not valid UTF-8: {e}"))?;
dmarc_report_parser::parse(xml).map_err(|e| format!("Failed to parse DMARC report: {e}"))
}
fn extract_xml_from_zip(path: &PathBuf) -> Result<Vec<u8>, io::Error> {
let file = fs::File::open(path)?;
let mut archive =
zip::ZipArchive::new(file).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
for i in 0..archive.len() {
let mut entry = archive
.by_index(i)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
let name = entry.name().to_lowercase();
if name.ends_with(".xml") {
let mut buf = Vec::new();
entry.read_to_end(&mut buf)?;
return Ok(buf);
}
}
Err(io::Error::new(
io::ErrorKind::NotFound,
"No .xml file found in the zip archive",
))
}
fn decompress_gzip(path: &PathBuf) -> Result<Vec<u8>, io::Error> {
let file = fs::File::open(path)?;
let mut decoder = flate2::read::GzDecoder::new(file);
let mut buf = Vec::new();
decoder.read_to_end(&mut buf)?;
Ok(buf)
}