crabmail

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

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 }