crabmail

Static HTML email archive viewer in Rust
git clone git://git.alexwennerberg.com/crabmail
Log | Files | Refs | README | LICENSE

main.rs (7727B) - raw


      1 // this code is not good
      2 // i am not very good at rust
      3 // that is ok though
      4 #[forbid(unsafe_code)]
      5 use anyhow::Result;
      6 use mail_parser::Message;
      7 use maildir::Maildir;
      8 use std::collections::HashSet;
      9 use std::fs;
     10 use std::path::PathBuf;
     11 
     12 use models::*;
     13 
     14 use config::{Config, INSTANCE};
     15 mod arg;
     16 mod config;
     17 mod maildir;
     18 mod models;
     19 mod templates;
     20 mod threading;
     21 mod time;
     22 mod util;
     23 
     24 use std::ffi::{OsStr, OsString};
     25 
     26 const ATOM_ENTRY_LIMIT: usize = 100;
     27 
     28 // stole it from the internet
     29 pub fn append_ext(ext: impl AsRef<OsStr>, path: &PathBuf) -> PathBuf {
     30     let mut os_string: OsString = path.into();
     31     os_string.push(".");
     32     os_string.push(ext.as_ref());
     33     os_string.into()
     34 }
     35 
     36 impl Lists {
     37     fn write_lists(&mut self) {
     38         std::fs::create_dir_all(&self.out_dir).ok();
     39         let css = include_bytes!("style.css");
     40         write_if_unchanged(&self.out_dir.join("style.css"), css);
     41         let base_path = self.out_dir.join("index");
     42         if Config::global().include_html {
     43             write_if_unchanged(&base_path.with_extension("html"), self.to_html().as_bytes());
     44         }
     45         if Config::global().include_gemini {
     46             write_if_unchanged(&base_path.with_extension("gmi"), self.to_gmi().as_bytes());
     47         }
     48         for list in &mut self.lists {
     49             list.persist();
     50         }
     51     }
     52 }
     53 use std::fs::{read, write};
     54 
     55 // TODO: use checksum / cache. bool whether it writes
     56 fn write_if_unchanged(path: &PathBuf, data: &[u8]) -> bool {
     57     if let Ok(d) = read(path) {
     58         if &d == data {
     59             return false;
     60         }
     61     }
     62     write(path, data).unwrap();
     63     return true;
     64 }
     65 
     66 impl List {
     67     fn persist(&mut self) {
     68         self.write_threads();
     69     }
     70     fn write_index(&self) {
     71         // TODO fix lazy copy paste
     72         if Config::global().include_gemini {
     73             for (n, gmi) in self.to_gmi().iter().enumerate() {
     74                 let index;
     75                 if n == 0 {
     76                     index = self.out_dir.join("index");
     77                 } else {
     78                     index = self.out_dir.join(format!("{}-{}", "index", n + 1));
     79                 }
     80                 write_if_unchanged(&index.with_extension("gmi"), gmi.as_bytes());
     81             }
     82         }
     83         if Config::global().include_html {
     84             for (n, html) in self.to_html().iter().enumerate() {
     85                 let index;
     86                 if n == 0 {
     87                     index = self.out_dir.join("index");
     88                 } else {
     89                     index = self.out_dir.join(format!("{}-{}", "index", n + 1));
     90                 }
     91                 write_if_unchanged(&index.with_extension("html"), html.as_bytes());
     92             }
     93         }
     94         write_if_unchanged(&self.out_dir.join("atom.xml"), self.to_xml().as_bytes());
     95     }
     96 
     97     // Used with atom
     98     fn get_recent_messages(&self) -> Vec<StrMessage> {
     99         let mut out = Vec::new();
    100         let mut msgs: Vec<&threading::Msg> = self.thread_idx.threads.iter().flatten().collect();
    101         msgs.sort_by_key(|x| x.time);
    102         msgs.reverse();
    103         for m in msgs.iter().take(ATOM_ENTRY_LIMIT) {
    104             let data = std::fs::read(&m.path).unwrap();
    105             let msg = StrMessage::new(&Message::parse(&data).unwrap());
    106             out.push(msg);
    107         }
    108         out
    109     }
    110 
    111     fn write_threads(&mut self) {
    112         // wonky
    113         let mut files_written: HashSet<PathBuf> = HashSet::new();
    114         let thread_dir = self.out_dir.join("threads");
    115         let message_dir = self.out_dir.join("messages");
    116         std::fs::create_dir_all(&thread_dir).ok();
    117         std::fs::create_dir_all(&message_dir).ok();
    118         for thread_ids in &self.thread_idx.threads {
    119             // Load thread
    120             let mut thread = Thread::new(thread_ids, &self.config.name, &self.config.email);
    121             let basepath = thread_dir.join(&thread.messages[0].pathescape_msg_id());
    122             // this is a bit awkward
    123             let summary = ThreadSummary {
    124                 message: thread.messages[0].clone(),
    125                 reply_count: (thread.messages.len() - 1) as u64,
    126                 last_reply: thread_ids[thread_ids.len() - 1].time,
    127             };
    128             for msg in &mut thread.messages {
    129                 msg.set_url(&self, &summary); // awkward) // hacky
    130             }
    131             self.thread_topics.push(summary);
    132             if Config::global().include_html {
    133                 let html = append_ext("html", &basepath);
    134                 write_if_unchanged(&html, thread.to_html().as_bytes());
    135                 files_written.insert(html);
    136             }
    137             let xml = append_ext("xml", &basepath);
    138             write_if_unchanged(&xml, thread.to_xml().as_bytes());
    139             files_written.insert(xml);
    140             if Config::global().include_gemini {
    141                 let gmi = append_ext("gmi", &basepath);
    142                 write_if_unchanged(&gmi, thread.to_gmi().as_bytes());
    143                 files_written.insert(gmi);
    144             }
    145 
    146             for msg in thread.messages {
    147                 let eml = append_ext("mbox", &message_dir.join(&msg.pathescape_msg_id()));
    148                 write_if_unchanged(&eml, &msg.export_mbox());
    149                 files_written.insert(eml);
    150             }
    151         }
    152         self.thread_topics.sort_by_key(|t| t.last_reply);
    153         self.thread_topics.reverse();
    154         self.recent_messages = self.get_recent_messages();
    155         // for msg in &mut self.recent_messages {
    156         // TBD
    157         // msg.set_url(&self, &summary); // awkward) // hacky
    158         // }
    159         // Remove deleted stuff
    160         for dir in vec![message_dir, thread_dir] {
    161             for entry in fs::read_dir(&dir).unwrap() {
    162                 match entry {
    163                     Ok(e) => {
    164                         if !files_written.contains(&e.path()) {
    165                             fs::remove_file(&e.path()).ok();
    166                         }
    167                     }
    168                     Err(_) => continue,
    169                 }
    170             }
    171         }
    172         //
    173         self.write_index();
    174     }
    175 }
    176 
    177 fn main() -> Result<()> {
    178     let args = arg::Args::from_env();
    179     let maildir = &args.positional[0];
    180     let mut config = Config::from_file(&args.config)?;
    181     // Default to both true if both absent
    182     if !args.include_gemini && !args.include_html {
    183         config.include_gemini = true;
    184         config.include_html = true;
    185     } else {
    186         config.include_gemini = args.include_gemini;
    187         config.include_html = args.include_html;
    188     }
    189     config.out_dir = args.out_dir;
    190     INSTANCE.set(config).unwrap();
    191 
    192     // TODO allow one level lower -- one list etc
    193     let mut lists = Lists {
    194         lists: vec![],
    195         out_dir: Config::global().out_dir.clone(),
    196     };
    197     for maildir in std::fs::read_dir(maildir)?.filter_map(|m| m.ok()) {
    198         let dir_name = maildir.file_name().into_string().unwrap(); // TODO no unwrap
    199         if dir_name.as_bytes()[0] == b'.' || ["cur", "new", "tmp"].contains(&dir_name.as_str()) {
    200             continue;
    201         }
    202 
    203         let mut list = threading::ThreadIdx::new();
    204         let dirreader = Maildir::from(maildir.path());
    205         for f in dirreader
    206             .list_cur()
    207             .chain(dirreader.list_new())
    208             .filter_map(|e| e.ok())
    209         {
    210             let data = std::fs::read(f.path())?;
    211             // TODO move these 2 lines to dirreader
    212             let msg = match mail_parser::Message::parse(&data) {
    213                 Some(e) => e,
    214                 None => {
    215                     println!("Could not parse message {:?}", f.path());
    216                     continue;
    217                 }
    218             };
    219             list.add_email(&msg, f.path().to_path_buf());
    220         }
    221         list.finalize();
    222         lists.add(list, &dir_name);
    223     }
    224 
    225     lists.write_lists();
    226     Ok(())
    227 }