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 }