diff --git a/server/src/bin/server.rs b/server/src/bin/server.rs index d3c01d1..e1bec9b 100644 --- a/server/src/bin/server.rs +++ b/server/src/bin/server.rs @@ -89,6 +89,22 @@ impl<'r, 'o: 'r> Responder<'r, 'o> for PartResponder { .ok() } } +#[get("/attachment//")] +async fn attachment( + nm: &State, + id: &str, + idx: usize, +) -> Result> { + let mid = if id.starts_with("id:") { + id.to_string() + } else { + format!("id:{}", id) + }; + let bytes = Vec::new(); + let filename = None; + // TODO(wathiede): use walk_attachments from graphql to fill this out + Ok(PartResponder { bytes, filename }) +} #[get("/original//part/")] async fn original_part( diff --git a/server/src/graphql.rs b/server/src/graphql.rs index 19e1ef8..cc51cb9 100644 --- a/server/src/graphql.rs +++ b/server/src/graphql.rs @@ -2,15 +2,16 @@ use std::{ collections::HashMap, fs::File, hash::{DefaultHasher, Hash, Hasher}, + str::FromStr, }; use async_graphql::{ connection::{self, Connection, Edge}, - Context, EmptyMutation, EmptySubscription, Error, FieldResult, Object, Schema, SimpleObject, - Union, + Context, EmptyMutation, EmptySubscription, Enum, Error, FieldResult, Object, Schema, + SimpleObject, Union, }; use log::{error, info, warn}; -use mailparse::{parse_mail, MailHeaderMap, ParsedMail}; +use mailparse::{parse_mail, MailHeader, MailHeaderMap, ParsedMail}; use memmap::MmapOptions; use notmuch::Notmuch; use rocket::time::Instant; @@ -65,6 +66,38 @@ pub struct Message { pub body: Body, // On disk location of message pub path: String, + pub attachments: Vec, +} + +// Content-Type: image/jpeg; name="PXL_20231125_204826860.jpg" +// Content-Disposition: attachment; filename="PXL_20231125_204826860.jpg" +// Content-Transfer-Encoding: base64 +// Content-ID: +// X-Attachment-Id: f_lponoluo1 +#[derive(Debug, SimpleObject)] +pub struct Attachment { + filename: String, + content_type: Option, + content_id: Option, +} + +#[derive(Debug, Enum, Copy, Clone, Eq, PartialEq)] +enum DispositionType { + Inline, + Attachment, +} + +impl FromStr for DispositionType { + type Err = String; + + // Required method + fn from_str(s: &str) -> Result { + Ok(match s { + "inline" => DispositionType::Inline, + "attachment" => DispositionType::Attachment, + c => return Err(format!("unknown disposition type: {c}")), + }) + } } #[derive(Debug, SimpleObject)] @@ -115,6 +148,9 @@ impl Html { async fn content_tree(&self) -> &str { &self.content_tree } + async fn headers(&self) -> Vec
{ + Vec::new() + } } #[derive(Debug, Union)] @@ -330,6 +366,8 @@ impl QueryRoot { value: h.get_value(), }) .collect(); + // TODO(wathiede): parse message and fill out attachments + let attachments = extract_attachments(&m)?; messages.push(Message { id, from, @@ -340,6 +378,7 @@ impl QueryRoot { headers, body, path, + attachments, }); } messages.reverse(); @@ -442,6 +481,71 @@ fn extract_related(m: &ParsedMail) -> Result { Err("extract_related".into()) } +fn extract_attachments(m: &ParsedMail) -> Result, Error> { + let mut attachements = Vec::new(); + for sp in &m.subparts { + for h in &sp.headers { + if h.get_key() == "Content-Disposition" { + let v = h.get_value(); + if let Some(idx) = v.find(";") { + let dt = &v[..idx]; + match DispositionType::from_str(dt) { + Ok(DispositionType::Attachment) => { + attachements.push(Attachment { + filename: get_attachment_filename(&v).to_string(), + content_type: get_content_type(&sp.headers), + content_id: get_content_id(&sp.headers), + }); + } + Ok(DispositionType::Inline) => continue, + Err(e) => { + warn!("failed to parse Content-Disposition type '{}'", e); + continue; + } + }; + } else { + warn!("header has Content-Disposition missing ';'"); + continue; + } + } + } + } + Ok(attachements) +} + +fn get_attachment_filename(header_value: &str) -> &str { + // Strip last " + let v = &header_value[..header_value.len() - 1]; + if let Some(idx) = v.rfind('"') { + &v[idx + 1..] + } else { + "" + } +} + +fn get_content_type<'a>(headers: &[MailHeader<'a>]) -> Option { + for h in headers { + if h.get_key() == "Content-Type" { + let v = h.get_value(); + if let Some(idx) = v.find(';') { + return Some(v[..idx].to_string()); + } else { + return Some(v); + } + } + } + None +} + +fn get_content_id<'a>(headers: &[MailHeader<'a>]) -> Option { + for h in headers { + if h.get_key() == "Content-ID" { + return Some(h.get_value()); + } + } + None +} + fn render_content_type_tree(m: &ParsedMail) -> String { const WIDTH: usize = 4; fn render_rec(m: &ParsedMail, depth: usize) -> String {