#[warn(missing_docs)] #[doc = include_str!("../README.md")] mod cli; mod command; mod feed; mod message; mod message_reader; use chrono::Utc; use clap::Parser; use mail_parser::Message as ParsedMessage; use message_reader::{DataDirectoryMessageReader, EmailReader, ImapReader}; use sha2::{Digest, Sha256}; use std::{ error::Error, fs::OpenOptions, io::Write, path::{Path, PathBuf}, }; pub(crate) use message::Message; const INDEX_HTML: &str = include_str!("../resources/index.html"); const FEED_STYLESHEET: &str = include_str!("../resources/feed.xsl"); fn main() -> Result<(), Box> { let cli = cli::Cli::parse(); let data_directory = "data"; let result = match &cli.command { cli::Command::FetchFromImap { server, port, username, password, } => fetch_from_imap( data_directory, server.to_owned(), *port, username.to_owned(), password.to_owned(), ), cli::Command::BuildFeed { filename, hostname, include_html, } => build_feed(filename, hostname, *include_html), cli::Command::Update => command::update::self_update(), _ => unimplemented!("This method is not yet implemented."), }; result } fn create_directory>(dir: P) -> Result<(), std::io::Error> { if !dir.as_ref().exists() { return std::fs::create_dir(&dir); } Ok(()) } fn build_feed( filename: &PathBuf, hostname: &String, include_html: bool, ) -> Result<(), Box> { let dir = filename.parent().ok_or(format!( "Could not get parent directory of {}", filename.display() ))?; println!( "Building the feed to {} in {}/", filename.display(), dir.display() ); create_directory(dir)?; let feed_file = filename .file_name() .expect("Feed path should have a file name") .to_str() .expect("Feed path should be printable."); let mut feed = feed::build_atom_feed(&hostname, feed_file); let mut reader = DataDirectoryMessageReader::new(Path::new("data").to_path_buf()); for msg in reader.read_rfc822_messages() { let parsed = msg.get_parsed().expect("A parsed messsage."); let date = parsed.date().ok_or(format!( "Could not get the date of message {}", msg.get_uid() ))?; let subject = parsed.subject().unwrap_or("No subject"); println!( "Processing message {} from {} with subject {}", msg.get_uid(), date, subject ); let html_body = parsed.body_html(0).expect("Could not read html body"); let processed_html = process_html(&html_body).expect("Could not process the HTML"); if include_html { let path: PathBuf = [dir, Path::new(&get_path(&parsed, &msg))].iter().collect(); write_file(&path, processed_html.as_bytes())?; } feed::add_entry_to_feed(&mut feed, &msg, &processed_html, &hostname, include_html); } if !feed.entries.is_empty() { feed.set_updated(Utc::now()); println!("Writing feed to {}", filename.display()); feed::write_feed(feed, open_file(filename).unwrap())?; write_file(dir.join("feed.xsl"), FEED_STYLESHEET)?; write_file( dir.join("index.html"), INDEX_HTML.replace("{FEED}", feed_file), )?; } println!("Finished building the feed."); Ok(()) } fn fetch_from_imap( data_directory: &str, server: String, port: u16, username: String, password: String, ) -> Result<(), Box> { create_directory(data_directory)?; print!("Getting mail from {} for mailbox {}", server, username); let mut reader = ImapReader::new(server, port, username, password); for msg in reader.read_rfc822_messages() { let parsed = msg.get_parsed().ok_or(format!( "Could not parse the message with id {}", msg.get_uid() ))?; let date = parsed.date().ok_or(format!( "Could not get the date of message {}", msg.get_uid() ))?; let subject = parsed.subject().unwrap_or("No subject"); println!( "Processing message {} from {} with subject {}", msg.get_uid(), date, subject ); let path = get_path(&parsed, &msg); let html_path: PathBuf = [ Path::new(data_directory), Path::new(&format!("{}.eml", path)), ] .iter() .collect(); println!("Storing to {}", &html_path.display()); write_file(&html_path, msg.get_data())?; } Ok(()) } fn get_path(parsed: &ParsedMessage, msg: &Message) -> String { let date = parsed.date().expect("Could not extract date"); let date_str = format!( "{:04}{:02}{:02}{:02}{:02}{:02}", &date.year, &date.month, &date.day, &date.hour, &date.minute, &date.second ); let hash = base16ct::lower::encode_string(&Sha256::digest( parsed.body_html(0).expect("Expected a body").as_bytes(), )); let uid: i32 = msg .get_uid() .parse() .unwrap_or_else(|_| panic!("Could not convert message id {} to an i32.", msg.get_uid())); format!("{:05}_{}_{}.html", uid, date_str, &hash) } fn process_html(input: &str) -> Result { Ok(input.replace("src", "data-source")) } fn open_file>(path: P) -> std::io::Result { OpenOptions::new() .write(true) .truncate(true) .create(true) .open(path.into()) } fn write_file, D: AsRef<[u8]>>(path: P, data: D) -> Result<(), std::io::Error> { let path: PathBuf = path.into(); open_file(path.clone()) .unwrap_or_else(|_| panic!("Could not open file '{}' for writing", &path.display())) .write_all(data.as_ref()) }