server: WIP attachment serving

This commit is contained in:
Bill Thiede 2023-12-03 09:11:00 -08:00
parent 488c3b86f8
commit f5f9eb175d
2 changed files with 123 additions and 3 deletions

View File

@ -89,6 +89,22 @@ impl<'r, 'o: 'r> Responder<'r, 'o> for PartResponder {
.ok()
}
}
#[get("/attachment/<id>/<idx>")]
async fn attachment(
nm: &State<Notmuch>,
id: &str,
idx: usize,
) -> Result<PartResponder, Debug<NotmuchError>> {
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/<id>/part/<part>")]
async fn original_part(

View File

@ -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<Attachment>,
}
// Content-Type: image/jpeg; name="PXL_20231125_204826860.jpg"
// Content-Disposition: attachment; filename="PXL_20231125_204826860.jpg"
// Content-Transfer-Encoding: base64
// Content-ID: <f_lponoluo1>
// X-Attachment-Id: f_lponoluo1
#[derive(Debug, SimpleObject)]
pub struct Attachment {
filename: String,
content_type: Option<String>,
content_id: Option<String>,
}
#[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<Self, Self::Err> {
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<Header> {
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<Body, Error> {
Err("extract_related".into())
}
fn extract_attachments(m: &ParsedMail) -> Result<Vec<Attachment>, 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<String> {
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<String> {
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 {