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()
|
.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>")]
|
#[get("/original/<id>/part/<part>")]
|
||||||
async fn original_part(
|
async fn original_part(
|
||||||
|
|||||||
@ -2,15 +2,16 @@ use std::{
|
|||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
fs::File,
|
fs::File,
|
||||||
hash::{DefaultHasher, Hash, Hasher},
|
hash::{DefaultHasher, Hash, Hasher},
|
||||||
|
str::FromStr,
|
||||||
};
|
};
|
||||||
|
|
||||||
use async_graphql::{
|
use async_graphql::{
|
||||||
connection::{self, Connection, Edge},
|
connection::{self, Connection, Edge},
|
||||||
Context, EmptyMutation, EmptySubscription, Error, FieldResult, Object, Schema, SimpleObject,
|
Context, EmptyMutation, EmptySubscription, Enum, Error, FieldResult, Object, Schema,
|
||||||
Union,
|
SimpleObject, Union,
|
||||||
};
|
};
|
||||||
use log::{error, info, warn};
|
use log::{error, info, warn};
|
||||||
use mailparse::{parse_mail, MailHeaderMap, ParsedMail};
|
use mailparse::{parse_mail, MailHeader, MailHeaderMap, ParsedMail};
|
||||||
use memmap::MmapOptions;
|
use memmap::MmapOptions;
|
||||||
use notmuch::Notmuch;
|
use notmuch::Notmuch;
|
||||||
use rocket::time::Instant;
|
use rocket::time::Instant;
|
||||||
@ -65,6 +66,38 @@ pub struct Message {
|
|||||||
pub body: Body,
|
pub body: Body,
|
||||||
// On disk location of message
|
// On disk location of message
|
||||||
pub path: String,
|
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)]
|
#[derive(Debug, SimpleObject)]
|
||||||
@ -115,6 +148,9 @@ impl Html {
|
|||||||
async fn content_tree(&self) -> &str {
|
async fn content_tree(&self) -> &str {
|
||||||
&self.content_tree
|
&self.content_tree
|
||||||
}
|
}
|
||||||
|
async fn headers(&self) -> Vec<Header> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Union)]
|
#[derive(Debug, Union)]
|
||||||
@ -330,6 +366,8 @@ impl QueryRoot {
|
|||||||
value: h.get_value(),
|
value: h.get_value(),
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
// TODO(wathiede): parse message and fill out attachments
|
||||||
|
let attachments = extract_attachments(&m)?;
|
||||||
messages.push(Message {
|
messages.push(Message {
|
||||||
id,
|
id,
|
||||||
from,
|
from,
|
||||||
@ -340,6 +378,7 @@ impl QueryRoot {
|
|||||||
headers,
|
headers,
|
||||||
body,
|
body,
|
||||||
path,
|
path,
|
||||||
|
attachments,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
messages.reverse();
|
messages.reverse();
|
||||||
@ -442,6 +481,71 @@ fn extract_related(m: &ParsedMail) -> Result<Body, Error> {
|
|||||||
Err("extract_related".into())
|
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 {
|
fn render_content_type_tree(m: &ParsedMail) -> String {
|
||||||
const WIDTH: usize = 4;
|
const WIDTH: usize = 4;
|
||||||
fn render_rec(m: &ParsedMail, depth: usize) -> String {
|
fn render_rec(m: &ParsedMail, depth: usize) -> String {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user