server: WIP attachment serving
This commit is contained in:
parent
488c3b86f8
commit
f5f9eb175d
@ -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(
|
||||
|
||||
@ -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 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user