#[warn(missing_docs)] #[doc = include_str!("../README.md")] mod cli; 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::{File, OpenOptions}, io::Write, path::{Path, PathBuf}, }; pub(crate) use message::Message; const INDEX_HTML: & 'static str = include_str!("../resources/index.html"); 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.to_owned(), *include_html), _ => 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 mut feed = feed::build_atom_feed(&hostname); 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.get_date().ok_or(format!( "Could not get the date of message {}", msg.get_uid() ))?; let subject = match parsed.get_subject() { Some(subject) => subject, None => "No subject", }; println!( "Processing message {} from {} with subject {}", msg.get_uid(), date.to_string(), subject ); let html_body = parsed.get_html_body(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())?; } // Ugly hack, but I don't know how to do this better... let file_name = format!("{:?}", filename.file_name().unwrap()).replace('"', ""); write_file(dir.join("index.html"), INDEX_HTML.replace("{FEED}", file_name.as_str()))?; feed::add_entry_to_feed(&mut feed, &msg, &processed_html, &hostname, include_html); } if feed.entries.len() > 0 { feed.set_updated(Utc::now()); println!("Writing feed to {}", filename.display()); let _ = feed.write_to(File::create(filename).unwrap()); } 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( String::from(server), port, String::from(username), String::from(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.get_date().ok_or(format!( "Could not get the date of message {}", msg.get_uid() ))?; let subject = match parsed.get_subject() { Some(subject) => subject, None => "No subject", }; println!( "Processing message {} from {} with subject {}", msg.get_uid(), date.to_string(), 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.get_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.get_html_body(0).expect("Expected a body").as_bytes(), )); let uid: i32 = msg.get_uid() .parse() .expect(&format!("Could not convert message id {} to an i32.", msg.get_uid())); format!("{:05}_{}_{}.html", uid, date_str, &hash).to_owned() } fn process_html(input: &str) -> Result { Ok(input.replace("src", "data-source")) } fn write_file, D: AsRef<[u8]>>(html_path: P, data: D) -> Result<(), std::io::Error> { let path : PathBuf = html_path.into(); OpenOptions::new() .write(true) .create(true) .open(&path) .expect(format!("Could not open file '{}' for writing", &path.display()).as_str()) .write_all(data.as_ref()) }