#[macro_use] extern crate rocket; use std::{error::Error, io::Cursor, str::FromStr}; use async_graphql::{http::GraphiQLSource, EmptyMutation, EmptySubscription, Schema}; use async_graphql_rocket::{GraphQLQuery, GraphQLRequest, GraphQLResponse}; use glog::Flags; use notmuch::{Notmuch, NotmuchError, ThreadSet}; use rocket::{ http::{ContentType, Header}, request::Request, response::{content, Debug, Responder}, serde::json::Json, Response, State, }; use rocket_cors::{AllowedHeaders, AllowedOrigins}; use server::{ error::ServerError, graphql::{GraphqlSchema, QueryRoot}, nm::threadset_to_messages, }; use shared::Message; #[get("/refresh")] async fn refresh(nm: &State) -> Result, Debug> { Ok(Json(String::from_utf8_lossy(&nm.new()?).to_string())) } #[get("/search")] async fn search_all( nm: &State, ) -> Result, Debug> { search(nm, "*", None, None).await } #[get("/search/?&")] async fn search( nm: &State, query: &str, page: Option, results_per_page: Option, ) -> Result, Debug> { let page = page.unwrap_or(0); let results_per_page = results_per_page.unwrap_or(20); let query = urlencoding::decode(query).map_err(NotmuchError::from)?; info!(" search '{query}'"); let res = shared::SearchResult { summary: nm.search(&query, page * results_per_page, results_per_page)?, query: query.to_string(), page, results_per_page, total: nm.count(&query)?, }; Ok(Json(res)) } #[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 = threadset_to_messages(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 PartResponder { bytes: Vec, filename: Option, } impl<'r, 'o: 'r> Responder<'r, 'o> for PartResponder { fn respond_to(self, _: &'r Request<'_>) -> rocket::response::Result<'o> { let mut resp = Response::build(); if let Some(filename) = self.filename { info!("filename {:?}", filename); resp.header(Header::new( "Content-Disposition", format!(r#"attachment; filename="{}""#, filename), )) .header(ContentType::Binary); } resp.sized_body(self.bytes.len(), Cursor::new(self.bytes)) .ok() } } #[get("/original//part/")] async fn original_part( nm: &State, id: &str, part: usize, ) -> Result> { 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(PartResponder { bytes: res, filename: meta.filename, }) } #[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 schema = Schema::build(QueryRoot, EmptyMutation, EmptySubscription) .data(Notmuch::default()) .finish(); let _ = rocket::build() .mount( "/", routes![ original_part, original, refresh, search_all, search, show_pretty, show, graphql_query, graphql_request, graphiql ], ) .attach(cors) .manage(schema) .manage(Notmuch::default()) //.manage(Notmuch::with_config("../notmuch/testdata/notmuch.config")) .launch() .await?; Ok(()) }