use std::io;
use std::sync::Arc;
use std::thread;
use tiny_http::{Header, Method, Request, Response, Server, StatusCode};
use crate::storage::MemoryStorage;
use super::core::{self, EmailListResponse, PreviewConfig};
pub fn serve(addr: &str, storage: Arc<MemoryStorage>) -> io::Result<()> {
PreviewServer::new(addr, storage)?.run()
}
pub struct PreviewServer {
server: Server,
storage: Arc<MemoryStorage>,
config: PreviewConfig,
}
impl PreviewServer {
pub fn new(addr: &str, storage: Arc<MemoryStorage>) -> io::Result<Self> {
Self::with_config(addr, storage, PreviewConfig::default())
}
pub fn with_config(
addr: &str,
storage: Arc<MemoryStorage>,
config: PreviewConfig,
) -> io::Result<Self> {
let server = Server::http(addr).map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
Ok(Self {
server,
storage,
config,
})
}
pub fn run(self) -> io::Result<()> {
run_server(&self.server, &self.storage, &self.config)
}
pub fn spawn(self) {
thread::spawn(move || {
let _ = run_server(&self.server, &self.storage, &self.config);
});
}
}
fn run_server(
server: &Server,
storage: &Arc<MemoryStorage>,
config: &PreviewConfig,
) -> io::Result<()> {
loop {
let request = match server.recv() {
Ok(req) => req,
Err(e) => return Err(io::Error::new(io::ErrorKind::Other, e)),
};
handle_request(request, storage, config);
}
}
fn handle_request(request: Request, storage: &Arc<MemoryStorage>, config: &PreviewConfig) {
let method = request.method().clone();
let path = request.url().to_string();
let (path, query) = parse_path_and_query(&path);
let response = match (&method, path) {
(Method::Get, "/") => handle_index(storage, config, &query),
(Method::Get, "/json") => handle_list_json(storage),
(Method::Post, "/clear") => handle_clear(storage),
(Method::Get, p) => handle_dynamic_route(p, storage),
_ => not_found(),
};
let _ = request.respond(response);
}
fn handle_dynamic_route(path: &str, storage: &Arc<MemoryStorage>) -> Response<io::Cursor<Vec<u8>>> {
let path = path.strip_prefix('/').unwrap_or(path);
if let Some(id) = path.strip_suffix("/html") {
if is_uuid(id) {
return handle_email_html(id, storage);
}
}
if let Some((id, rest)) = path.split_once("/attachments/") {
if is_uuid(id) {
if let Ok(idx) = rest.parse::<usize>() {
return handle_attachment(id, idx, storage);
}
}
}
if is_uuid(path) {
return handle_view_email(path, storage);
}
not_found()
}
fn handle_index(
storage: &Arc<MemoryStorage>,
config: &PreviewConfig,
query: &QueryParams,
) -> Response<io::Cursor<Vec<u8>>> {
let emails = core::list_emails(storage);
let script_nonce = query
.get("script_nonce")
.or(config.script_nonce.as_deref())
.map(String::from);
let style_nonce = query
.get("style_nonce")
.or(config.style_nonce.as_deref())
.map(String::from);
let html = core::render_index(&emails, script_nonce, style_nonce);
html_response(html)
}
fn handle_list_json(storage: &Arc<MemoryStorage>) -> Response<io::Cursor<Vec<u8>>> {
let emails = core::list_emails(storage);
let response = EmailListResponse { data: emails };
json_response(&response)
}
fn handle_view_email(id: &str, storage: &Arc<MemoryStorage>) -> Response<io::Cursor<Vec<u8>>> {
match core::get_email(storage, id) {
Some(email) => json_response(&email),
None => not_found(),
}
}
fn handle_email_html(id: &str, storage: &Arc<MemoryStorage>) -> Response<io::Cursor<Vec<u8>>> {
match core::get_email_html(storage, id) {
Some(html) => html_response(html),
None => not_found(),
}
}
fn handle_attachment(
id: &str,
idx: usize,
storage: &Arc<MemoryStorage>,
) -> Response<io::Cursor<Vec<u8>>> {
match core::get_attachment(storage, id, idx) {
Some(att) => {
let cursor = io::Cursor::new(att.data);
let content_type =
Header::from_bytes("Content-Type", att.content_type.as_bytes()).unwrap();
let disposition = Header::from_bytes(
"Content-Disposition",
format!("attachment; filename=\"{}\"", att.filename).as_bytes(),
)
.unwrap();
Response::from_data(cursor.into_inner())
.with_header(content_type)
.with_header(disposition)
}
None => not_found(),
}
}
fn handle_clear(storage: &Arc<MemoryStorage>) -> Response<io::Cursor<Vec<u8>>> {
core::clear_emails(storage);
Response::from_data(Vec::new()).with_status_code(StatusCode(204))
}
fn html_response(body: String) -> Response<io::Cursor<Vec<u8>>> {
let header = Header::from_bytes("Content-Type", "text/html; charset=utf-8").unwrap();
Response::from_data(body.into_bytes()).with_header(header)
}
fn json_response<T: serde::Serialize>(data: &T) -> Response<io::Cursor<Vec<u8>>> {
let body = serde_json::to_vec(data).unwrap_or_default();
let header = Header::from_bytes("Content-Type", "application/json").unwrap();
Response::from_data(body).with_header(header)
}
fn not_found() -> Response<io::Cursor<Vec<u8>>> {
Response::from_data(Vec::new()).with_status_code(StatusCode(404))
}
fn is_uuid(s: &str) -> bool {
if s.len() != 36 {
return false;
}
let bytes = s.as_bytes();
if bytes[8] != b'-' || bytes[13] != b'-' || bytes[18] != b'-' || bytes[23] != b'-' {
return false;
}
s.chars().enumerate().all(|(i, c)| {
if i == 8 || i == 13 || i == 18 || i == 23 {
true } else {
c.is_ascii_hexdigit()
}
})
}
struct QueryParams {
params: Vec<(String, String)>,
}
impl QueryParams {
fn get(&self, key: &str) -> Option<&str> {
self.params
.iter()
.find(|(k, _)| k == key)
.map(|(_, v)| v.as_str())
}
}
fn parse_path_and_query(url: &str) -> (&str, QueryParams) {
let (path, query_str) = url.split_once('?').unwrap_or((url, ""));
let params = query_str
.split('&')
.filter(|s| !s.is_empty())
.filter_map(|pair| {
let (k, v) = pair.split_once('=')?;
Some((k.to_string(), v.to_string()))
})
.collect();
(path, QueryParams { params })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_uuid() {
assert!(is_uuid("550e8400-e29b-41d4-a716-446655440000"));
assert!(is_uuid("00000000-0000-0000-0000-000000000000"));
assert!(is_uuid("ffffffff-ffff-ffff-ffff-ffffffffffff"));
assert!(!is_uuid("not-a-uuid"));
assert!(!is_uuid("550e8400-e29b-41d4-a716-44665544000")); assert!(!is_uuid("550e8400-e29b-41d4-a716-4466554400000")); assert!(!is_uuid("550e8400xe29b-41d4-a716-446655440000")); assert!(!is_uuid("550g8400-e29b-41d4-a716-446655440000")); }
#[test]
fn test_parse_path_and_query() {
let (path, query) = parse_path_and_query("/");
assert_eq!(path, "/");
assert!(query.get("foo").is_none());
let (path, query) = parse_path_and_query("/?script_nonce=abc123");
assert_eq!(path, "/");
assert_eq!(query.get("script_nonce"), Some("abc123"));
let (path, query) = parse_path_and_query("/?a=1&b=2");
assert_eq!(path, "/");
assert_eq!(query.get("a"), Some("1"));
assert_eq!(query.get("b"), Some("2"));
}
}