#[macro_use] extern crate rocket; use std::{error::Error, io::Cursor, str::FromStr}; use async_graphql::{http::GraphiQLSource, EmptySubscription, Schema}; use async_graphql_rocket::{GraphQLQuery, GraphQLRequest, GraphQLResponse}; use glog::Flags; use notmuch::{Notmuch, NotmuchError, ThreadSet}; use rocket::{ fairing::AdHoc, http::{ContentType, Header}, request::Request, response::{content, Debug, Responder}, serde::json::Json, Response, State, }; use rocket_cors::{AllowedHeaders, AllowedOrigins}; use serde::Deserialize; use server::{ error::ServerError, graphql::{Attachment, GraphqlSchema, Mutation, QueryRoot}, nm::{attachment_bytes, cid_attachment_bytes}, }; use sqlx::postgres::PgPool; #[derive(Deserialize)] struct Config { newsreader_database_url: String, } #[get("/refresh")] async fn refresh(nm: &State) -> Result, Debug> { Ok(Json(String::from_utf8_lossy(&nm.new()?).to_string())) } #[get("/show//pretty")] async fn show_pretty( nm: &State, query: &str, ) -> Result, Debug> { let query = urlencoding::decode(query).map_err(|e| ServerError::from(NotmuchError::from(e)))?; let res = nm.show(&query).map_err(ServerError::from)?; Ok(Json(res)) } #[get("/show/")] async fn show(nm: &State, query: &str) -> Result, Debug> { let query = urlencoding::decode(query).map_err(NotmuchError::from)?; let res = nm.show(&query)?; Ok(Json(res)) } struct InlineAttachmentResponder(Attachment); impl<'r, 'o: 'r> Responder<'r, 'o> for InlineAttachmentResponder { fn respond_to(self, _: &'r Request<'_>) -> rocket::response::Result<'o> { let mut resp = Response::build(); if let Some(filename) = self.0.filename { resp.header(Header::new( "Content-Disposition", format!(r#"inline; filename="{}""#, filename), )); } 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() } } struct DownloadAttachmentResponder(Attachment); impl<'r, 'o: 'r> Responder<'r, 'o> for DownloadAttachmentResponder { fn respond_to(self, _: &'r Request<'_>) -> rocket::response::Result<'o> { let mut resp = Response::build(); if let Some(filename) = self.0.filename { resp.header(Header::new( "Content-Disposition", format!(r#"attachment; filename="{}""#, filename), )); } 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() } } #[get("/cid//")] async fn view_cid( nm: &State, id: &str, cid: &str, ) -> Result> { let mid = if id.starts_with("id:") { id.to_string() } else { format!("id:{}", id) }; info!("view cid attachment {mid} {cid}"); let attachment = cid_attachment_bytes(nm, &mid, &cid)?; Ok(InlineAttachmentResponder(attachment)) } #[get("/view/attachment///<_>")] async fn view_attachment( nm: &State, id: &str, idx: &str, ) -> Result> { let mid = if id.starts_with("id:") { id.to_string() } else { format!("id:{}", id) }; info!("view attachment {mid} {idx}"); let idx: Vec<_> = idx .split('.') .map(|s| s.parse().expect("not a usize")) .collect(); let attachment = attachment_bytes(nm, &mid, &idx)?; Ok(InlineAttachmentResponder(attachment)) } #[get("/download/attachment///<_>")] async fn download_attachment( nm: &State, id: &str, idx: &str, ) -> Result> { let mid = if id.starts_with("id:") { id.to_string() } else { format!("id:{}", id) }; info!("download attachment {mid} {idx}"); let idx: Vec<_> = idx .split('.') .map(|s| s.parse().expect("not a usize")) .collect(); let attachment = attachment_bytes(nm, &mid, &idx)?; Ok(DownloadAttachmentResponder(attachment)) } #[get("/original/")] async fn original( nm: &State, id: &str, ) -> Result<(ContentType, Vec), Debug> { let mid = if id.starts_with("id:") { id.to_string() } else { format!("id:{}", id) }; let res = nm.show_original(&mid)?; Ok((ContentType::Plain, res)) } #[rocket::get("/")] fn graphiql() -> content::RawHtml { content::RawHtml(GraphiQLSource::build().endpoint("/graphql").finish()) } #[rocket::get("/graphql?")] async fn graphql_query(schema: &State, query: GraphQLQuery) -> GraphQLResponse { query.execute(schema.inner()).await } #[rocket::post("/graphql", data = "", format = "application/json")] async fn graphql_request( schema: &State, request: GraphQLRequest, ) -> GraphQLResponse { request.execute(schema.inner()).await } #[rocket::main] async fn main() -> Result<(), Box> { glog::new() .init(Flags { colorlogtostderr: true, //alsologtostderr: true, // use logtostderr to only write to stderr and not to files logtostderr: true, ..Default::default() }) .unwrap(); let allowed_origins = AllowedOrigins::all(); let cors = rocket_cors::CorsOptions { allowed_origins, allowed_methods: vec!["Get"] .into_iter() .map(|s| FromStr::from_str(s).unwrap()) .collect(), allowed_headers: AllowedHeaders::some(&["Authorization", "Accept"]), allow_credentials: true, ..Default::default() } .to_cors()?; let rkt = rocket::build() .mount( shared::urls::MOUNT_POINT, routes![ original, refresh, show_pretty, show, graphql_query, graphql_request, graphiql, view_cid, view_attachment, download_attachment, ], ) .attach(cors) .attach(AdHoc::config::()); let config: Config = rkt.figment().extract()?; let pool = PgPool::connect(&config.newsreader_database_url).await?; let schema = Schema::build(QueryRoot, Mutation, EmptySubscription) .data(Notmuch::default()) .data(pool.clone()) .extension(async_graphql::extensions::Logger) .finish(); let rkt = rkt.manage(schema).manage(pool).manage(Notmuch::default()); //.manage(Notmuch::with_config("../notmuch/testdata/notmuch.config")) rkt.launch().await?; Ok(()) }