server: add functioning download attachment handler

This commit is contained in:
Bill Thiede 2024-03-26 08:25:52 -07:00
parent ff1c3f5791
commit 9a5dc20f83
2 changed files with 28 additions and 47 deletions

View File

@ -91,23 +91,24 @@ impl<'r, 'o: 'r> Responder<'r, 'o> for InlineAttachmentResponder {
} }
} }
struct DownloadPartResponder { struct DownloadAttachmentResponder(Attachment);
bytes: Vec<u8>,
filename: Option<String>,
}
impl<'r, 'o: 'r> Responder<'r, 'o> for DownloadPartResponder { impl<'r, 'o: 'r> Responder<'r, 'o> for DownloadAttachmentResponder {
fn respond_to(self, _: &'r Request<'_>) -> rocket::response::Result<'o> { fn respond_to(self, _: &'r Request<'_>) -> rocket::response::Result<'o> {
let mut resp = Response::build(); let mut resp = Response::build();
if let Some(filename) = self.filename { if let Some(filename) = self.0.filename {
info!("filename {:?}", filename); info!("filename {:?}", filename);
resp.header(Header::new( resp.header(Header::new(
"Content-Disposition", "Content-Disposition",
format!(r#"attachment; filename="{}""#, filename), format!(r#"attachment; filename="{}""#, filename),
)) ));
.header(ContentType::Binary);
} }
resp.sized_body(self.bytes.len(), Cursor::new(self.bytes)) if let Some(content_type) = self.0.content_type {
if let Some(ct) = ContentType::parse_flexible(&content_type) {
resp.header(ct);
}
}
resp.sized_body(self.0.bytes.len(), Cursor::new(self.0.bytes))
.ok() .ok()
} }
} }
@ -129,7 +130,6 @@ async fn view_attachment(
.map(|s| s.parse().expect("not a usize")) .map(|s| s.parse().expect("not a usize"))
.collect(); .collect();
let attachment = attachment_bytes(nm, &mid, &idx)?; let attachment = attachment_bytes(nm, &mid, &idx)?;
// TODO: plumb Content-Type, or just create wrappers for serving the Attachment type
Ok(InlineAttachmentResponder(attachment)) Ok(InlineAttachmentResponder(attachment))
} }
@ -138,7 +138,7 @@ async fn download_attachment(
nm: &State<Notmuch>, nm: &State<Notmuch>,
id: &str, id: &str,
idx: &str, idx: &str,
) -> Result<DownloadPartResponder, Debug<ServerError>> { ) -> Result<DownloadAttachmentResponder, Debug<ServerError>> {
let mid = if id.starts_with("id:") { let mid = if id.starts_with("id:") {
id.to_string() id.to_string()
} else { } else {
@ -150,30 +150,7 @@ async fn download_attachment(
.map(|s| s.parse().expect("not a usize")) .map(|s| s.parse().expect("not a usize"))
.collect(); .collect();
let attachment = attachment_bytes(nm, &mid, &idx)?; let attachment = attachment_bytes(nm, &mid, &idx)?;
// TODO(wathiede): use walk_attachments from graphql to fill this out Ok(DownloadAttachmentResponder(attachment))
Ok(DownloadPartResponder {
bytes: attachment.bytes,
filename: attachment.filename,
})
}
#[get("/original/<id>/part/<part>")]
async fn original_part(
nm: &State<Notmuch>,
id: &str,
part: usize,
) -> Result<DownloadPartResponder, Debug<NotmuchError>> {
let mid = if id.starts_with("id:") {
id.to_string()
} else {
format!("id:{}", id)
};
let meta = nm.show_part(&mid, part)?;
let res = nm.show_original_part(&mid, part)?;
Ok(DownloadPartResponder {
bytes: res,
filename: meta.filename,
})
} }
#[get("/original/<id>")] #[get("/original/<id>")]
@ -240,7 +217,6 @@ async fn main() -> Result<(), Box<dyn Error>> {
.mount( .mount(
"/", "/",
routes![ routes![
original_part,
original, original,
refresh, refresh,
search_all, search_all,

View File

@ -86,8 +86,10 @@ pub struct Message {
// X-Attachment-Id: f_lponoluo1 // X-Attachment-Id: f_lponoluo1
#[derive(Default, Debug, SimpleObject)] #[derive(Default, Debug, SimpleObject)]
pub struct Attachment { pub struct Attachment {
pub id: String,
pub idx: String,
pub filename: Option<String>, pub filename: Option<String>,
pub size: Option<usize>, pub size: usize,
pub content_type: Option<String>, pub content_type: Option<String>,
pub content_id: Option<String>, pub content_id: Option<String>,
pub disposition: DispositionType, pub disposition: DispositionType,
@ -375,7 +377,7 @@ impl QueryRoot {
}) })
.collect(); .collect();
// TODO(wathiede): parse message and fill out attachments // TODO(wathiede): parse message and fill out attachments
let attachments = extract_attachments(&m)?; let attachments = extract_attachments(&m, &id)?;
messages.push(Message { messages.push(Message {
id, id,
from, from,
@ -656,17 +658,17 @@ fn walk_attachments<T, F: Fn(&ParsedMail, &[usize]) -> Option<T>>(
// TODO(wathiede): make this walk_attachments that takes a closure. // TODO(wathiede): make this walk_attachments that takes a closure.
// Then implement one closure for building `Attachment` and imlement another that can be used to // Then implement one closure for building `Attachment` and imlement another that can be used to
// get the bytes for serving attachments of HTTP // get the bytes for serving attachments of HTTP
fn extract_attachments(m: &ParsedMail) -> Result<Vec<Attachment>, Error> { fn extract_attachments(m: &ParsedMail, id: &str) -> Result<Vec<Attachment>, Error> {
let mut attachments = Vec::new(); let mut attachments = Vec::new();
for sp in &m.subparts { for (idx, sp) in m.subparts.iter().enumerate() {
if let Some(attachment) = extract_attachment(sp) { if let Some(attachment) = extract_attachment(sp, id, &[idx]) {
attachments.push(attachment); attachments.push(attachment);
} }
} }
Ok(attachments) Ok(attachments)
} }
fn extract_attachment(m: &ParsedMail) -> Option<Attachment> { fn extract_attachment(m: &ParsedMail, id: &str, idx: &[usize]) -> Option<Attachment> {
let pcd = m.get_content_disposition(); let pcd = m.get_content_disposition();
// TODO: do we need to handle empty filename attachments, or should we change the definition of // TODO: do we need to handle empty filename attachments, or should we change the definition of
// Attachment::filename? // Attachment::filename?
@ -684,12 +686,15 @@ fn extract_attachment(m: &ParsedMail) -> Option<Attachment> {
} }
}; };
return Some(Attachment { return Some(Attachment {
id: id.to_string(),
idx: idx
.iter()
.map(|i| i.to_string())
.collect::<Vec<_>>()
.join("."),
disposition: pcd.disposition.into(), disposition: pcd.disposition.into(),
filename: Some(filename), filename: Some(filename),
size: pcd size: bytes.len(),
.params
.get("size")
.map(|s| s.parse().unwrap_or_default()),
// TODO: what is the default for ctype? // TODO: what is the default for ctype?
// TODO: do we want to use m.ctype.params for anything? // TODO: do we want to use m.ctype.params for anything?
content_type: Some(m.ctype.mimetype.clone()), content_type: Some(m.ctype.mimetype.clone()),
@ -841,7 +846,7 @@ pub fn attachment_bytes(nm: &Notmuch, id: &str, idx: &[usize]) -> Result<Attachm
if let Some(attachment) = walk_attachments(&m, |sp, cur_idx| { if let Some(attachment) = walk_attachments(&m, |sp, cur_idx| {
info!("checking {cur_idx:?}=={idx:?}"); info!("checking {cur_idx:?}=={idx:?}");
if cur_idx == idx { if cur_idx == idx {
let attachment = extract_attachment(&sp).unwrap_or(Attachment { let attachment = extract_attachment(&sp, id, idx).unwrap_or(Attachment {
..Attachment::default() ..Attachment::default()
}); });
return Some(attachment); return Some(attachment);