models.rs (9512B) - raw
1 use crate::config::{Config, Subsection}; 2 use crate::threading::{Msg, ThreadIdx}; 3 use mail_builder::headers::text::Text; 4 use mail_builder::MessageBuilder; 5 use mail_parser::{Addr, HeaderValue, Message, RfcHeader}; 6 use std::borrow::Cow; 7 use std::path::PathBuf; 8 9 // messages are path-cleaned in this context (/ replaced) 10 // list_path = "/{list_name}/index.html" 11 // xml = "/{list_name}/atom.xml" 12 // thread_path = "/{list_name}/{thread_id}.html 13 // thread_xml = "/{list_name}/{thread_id}.xml 14 // raw_email = "/{list_name}/messages/{message_id}.mbox 15 16 // TODO a better way to handle these is to use lifetimes rather than ownership 17 // I should implement an iterator that writes each message without holding them in memory probably 18 pub struct Lists { 19 pub lists: Vec<List>, 20 pub out_dir: PathBuf, 21 } 22 23 impl Lists { 24 pub fn add(&mut self, thread_idx: ThreadIdx, name: &str) { 25 // TODO safe name? 26 let config = match Config::global().get_subsection(name) { 27 Some(sub) => sub, 28 None => Config::global().default_subsection(name), 29 }; 30 let url = format!("{}/{}", Config::global().base_url, config.name); 31 self.lists.push(List { 32 thread_idx, 33 config, 34 url, 35 thread_topics: vec![], 36 recent_messages: vec![], 37 out_dir: self.out_dir.join(name), 38 }) 39 } 40 } 41 pub struct List { 42 pub thread_idx: ThreadIdx, 43 pub thread_topics: Vec<ThreadSummary>, // TODO 44 pub recent_messages: Vec<StrMessage>, 45 pub config: Subsection, // path 46 pub out_dir: PathBuf, 47 pub url: String, 48 } 49 50 // doesnt include full msg data 51 pub struct ThreadSummary { 52 pub message: StrMessage, 53 pub reply_count: u64, 54 pub last_reply: i64, // unix 55 } 56 57 pub struct Thread { 58 pub messages: Vec<StrMessage>, 59 pub url: String, 60 } 61 62 impl Thread { 63 pub fn new(thread_idx: &Vec<Msg>, list_name: &str, list_email: &str) -> Self { 64 let mut messages = vec![]; 65 for m in thread_idx { 66 let data = std::fs::read(&m.path).unwrap(); 67 let mut msg = StrMessage::new(&Message::parse(&data).unwrap()); 68 msg.mailto = msg.mailto(list_name, list_email); 69 messages.push(msg); 70 } 71 let url = format!( 72 "{}/{}/{}", 73 Config::global().base_url, 74 list_name, 75 messages[0].pathescape_msg_id().to_str().unwrap(), 76 ); 77 Thread { url, messages } 78 } 79 } 80 81 // simplified, stringified-email for templating 82 // making everything owned because I'm l a z y 83 #[derive(Debug, Clone)] 84 pub struct StrMessage { 85 pub id: String, 86 pub subject: String, 87 pub thread_subject: String, 88 pub received: i64, 89 pub preview: String, 90 pub from: MailAddress, 91 pub date: String, // TODO better dates 92 pub body: String, 93 pub flowed: bool, 94 pub mailto: String, // mailto link 95 pub in_reply_to: Option<String>, 96 pub to: Vec<MailAddress>, 97 pub cc: Vec<MailAddress>, 98 pub url: String, 99 } 100 101 // i suck at Cow and strings 102 #[derive(Debug, Clone)] 103 pub struct MailAddress { 104 pub name: Option<String>, 105 pub address: String, 106 } 107 impl MailAddress { 108 fn from_addr(addr: &Addr) -> Self { 109 // todo wtf 110 let address = addr.address.to_owned(); 111 MailAddress { 112 name: addr.name.to_owned().and_then(|a| Some(a.to_string())), 113 address: address.unwrap().to_string(), 114 } 115 } 116 117 pub fn to_string(&self) -> String { 118 let mut out = String::new(); 119 if let Some(n) = &self.name { 120 out.push('"'); 121 out.push_str(&n); 122 out.push('"'); 123 out.push(' '); 124 } 125 out.push('<'); 126 out.push_str(&self.address); 127 out.push('>'); 128 out 129 } 130 } 131 132 // TODO rename 133 impl StrMessage { 134 pub fn pathescape_msg_id(&self) -> PathBuf { 135 // use at your own risk on windows. idk how safe filepaths look there. 136 PathBuf::from(self.id.replace("/", ";")) 137 } 138 // wonky 139 // for some reason mbox is used over eml for things like git, mutt, etc 140 pub fn export_mbox(&self) -> Vec<u8> { 141 let mut message = MessageBuilder::new(); 142 if self.flowed { 143 message.format_flowed(); 144 } 145 let from = self.from.name.clone().unwrap_or(String::new()); 146 message.message_id(self.id.as_str()); 147 message.from((from.as_str(), self.from.address.as_str())); 148 // TODO no alloc. No copy pasta 149 message.to(self 150 .to 151 .iter() 152 .map(|x| (x.name.clone().unwrap_or(String::new()), x.address.clone())) 153 .collect::<Vec<(String, String)>>()); 154 message.cc(self 155 .cc 156 .iter() 157 .map(|x| (x.name.clone().unwrap_or(String::new()), x.address.clone())) 158 .collect::<Vec<(String, String)>>()); 159 message.header("Date", Text::from(self.date.as_str())); 160 if let Some(irt) = &self.in_reply_to { 161 message.in_reply_to(irt.as_str()); 162 } 163 // list-archive 164 message.subject(&self.subject); 165 // Figure out body export and content-transfer... 166 message.text_body(&self.body); 167 let mut output = Vec::new(); 168 // Dummy data for mbox 169 output.extend_from_slice(&b"From mboxrd@z Thu Jan 1 00:00:00 1970\n"[..]); 170 message.write_to(&mut output).unwrap(); 171 // for mbox 172 output.push(b'\n'); 173 output 174 } 175 176 pub fn mailto(&self, list_name: &str, list_email: &str) -> String { 177 let mut url = format!("mailto:{}?", list_email); 178 179 let from = self.from.address.clone(); 180 // make sure k is already urlencoded 181 let mut pushencode = |k: &str, v| { 182 url.push_str(&format!("{}={}&", k, urlencoding::encode(v))); 183 }; 184 let fixed_id = format!("<{}>", &self.id); 185 pushencode("cc", &from); 186 pushencode("in-reply-to", &fixed_id); 187 let list_url = format!("{}/{}", &Config::global().base_url, list_name); 188 pushencode("list-archive", &list_url); 189 pushencode("subject", &format!("Re: {}", self.thread_subject)); 190 // quoted body 191 url.push_str("body="); 192 for line in self.body.lines() { 193 url.push_str("%3E%20"); 194 url.push_str(&urlencoding::encode(&line)); 195 url.push_str("%0A"); 196 } 197 url.into() 198 } 199 200 // only place that depends on list and thread. hmm 201 pub fn set_url(&mut self, list: &List, thread: &ThreadSummary) { 202 self.url = format!( 203 "{}/{}/{}#{}", 204 Config::global().base_url, 205 list.config.name, 206 thread.message.pathescape_msg_id().to_str().unwrap(), 207 self.id 208 ); 209 } 210 211 pub fn new(msg: &Message) -> StrMessage { 212 let id = msg.get_message_id().unwrap_or(""); 213 // TODO duplicate in threading.rs 214 let received = msg 215 .get_received() 216 .as_datetime_ref() 217 .or_else(|| msg.get_date()) 218 .unwrap() 219 .to_timestamp() 220 .unwrap_or(-1); 221 let subject = msg.get_subject().unwrap_or("(No Subject)"); 222 let thread_subject = msg.get_thread_name().unwrap_or("(No Subject)"); 223 let invalid_email = Addr::new(None, "invalid-email"); 224 let preview = match msg.get_body_preview(80) { 225 Some(b) => b.to_string(), 226 None => String::new(), 227 }; 228 let from = match msg.get_from() { 229 HeaderValue::Address(fr) => fr, 230 _ => &invalid_email, 231 }; 232 let from = MailAddress::from_addr(from); 233 let date = msg 234 .get_rfc_header(RfcHeader::Date) 235 .and_then(|x| Some(x.get(0).unwrap_or(&Cow::from("")).to_string())) 236 .unwrap_or(String::new()) 237 .trim() 238 .to_owned(); // TODO awkawrd 239 let to = match msg.get_to() { 240 HeaderValue::Address(fr) => vec![MailAddress::from_addr(fr)], 241 HeaderValue::AddressList(fr) => fr.iter().map(|a| MailAddress::from_addr(a)).collect(), 242 _ => vec![], 243 }; 244 // todo no copypaste 245 let cc = match msg.get_cc() { 246 HeaderValue::Address(fr) => vec![MailAddress::from_addr(fr)], 247 HeaderValue::AddressList(fr) => fr.iter().map(|a| MailAddress::from_addr(a)).collect(), 248 _ => vec![], 249 }; 250 let in_reply_to = msg 251 .get_in_reply_to() 252 .as_text_ref() 253 .and_then(|a| Some(a.to_string())); 254 255 // TODO linkify body 256 // TODO unformat-flowed 257 let body = msg 258 .get_text_body(0) 259 .unwrap_or(Cow::Borrowed("[No message body]")); 260 261 // life is a nightmare 262 let flowed = msg 263 .get_text_part(0) 264 .and_then(|x| x.headers_rfc.get(&RfcHeader::ContentType)) 265 .and_then(|x| x.as_content_type_ref()) 266 .and_then(|x| x.attributes.as_ref()) 267 .and_then(|x| x.get("format")) 268 .and_then(|x| Some(x == "flowed")) 269 .unwrap_or(false); 270 StrMessage { 271 id: id.to_owned(), 272 subject: subject.to_owned(), 273 from: from, 274 received, 275 preview, 276 to, 277 cc, 278 url: String::new(), 279 thread_subject: thread_subject.to_owned(), 280 date: date.to_owned(), 281 body: body.to_string(), 282 flowed, 283 mailto: String::new(), 284 in_reply_to: in_reply_to, 285 } 286 } 287 }