newsletter-to-web/src/main.rs

223 lines
5.9 KiB
Rust

#[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<dyn Error>> {
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<P: AsRef<Path>>(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<dyn Error>> {
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<dyn Error>> {
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<String, ()> {
Ok(input.replace("src", "data-source"))
}
fn open_file<P: Into<PathBuf>>(path: P) -> std::io::Result<std::fs::File> {
OpenOptions::new()
.write(true)
.truncate(true)
.create(true)
.open(path.into())
}
fn write_file<P: Into<PathBuf>, 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())
}