Skip to main content

rustack_ses_http/v2/
mod.rs

1//! SES v2 HTTP handler (restJson1 protocol).
2//!
3//! SES v2 uses path-based routing under `/v2/email/` with JSON request/response bodies.
4
5use std::{convert::Infallible, future::Future, pin::Pin, sync::Arc};
6
7use http_body_util::BodyExt;
8use hyper::body::Incoming;
9use rustack_ses_model::error::SesError;
10
11use crate::{
12    body::SesResponseBody,
13    dispatch::SesHandler,
14    request::parse_query_params,
15    response::{JSON_CONTENT_TYPE, error_to_json_response, json_response},
16};
17
18/// Hyper `Service` implementation for SES v2 (restJson1).
19#[derive(Debug)]
20pub struct SesV2HttpService<H: SesHandler> {
21    handler: Arc<H>,
22}
23
24impl<H: SesHandler> SesV2HttpService<H> {
25    /// Create a new `SesV2HttpService`.
26    pub fn new(handler: Arc<H>) -> Self {
27        Self { handler }
28    }
29}
30
31impl<H: SesHandler> Clone for SesV2HttpService<H> {
32    fn clone(&self) -> Self {
33        Self {
34            handler: Arc::clone(&self.handler),
35        }
36    }
37}
38
39impl<H: SesHandler> hyper::service::Service<http::Request<Incoming>> for SesV2HttpService<H> {
40    type Response = http::Response<SesResponseBody>;
41    type Error = Infallible;
42    type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
43
44    fn call(&self, req: http::Request<Incoming>) -> Self::Future {
45        let handler = Arc::clone(&self.handler);
46
47        Box::pin(async move {
48            let method = req.method().clone();
49            let uri = req.uri().clone();
50            let path = uri.path().to_owned();
51            let query = uri.query().map(str::to_owned);
52
53            let body = match req
54                .into_body()
55                .collect()
56                .await
57                .map(http_body_util::Collected::to_bytes)
58            {
59                Ok(body) => body,
60                Err(e) => {
61                    let err = SesError::internal_error(format!("Failed to read body: {e}"));
62                    return Ok(error_to_json_response(&err));
63                }
64            };
65
66            // Handle retrospection endpoints.
67            if path.starts_with("/_aws/ses") {
68                let query_params = parse_query_params(query.as_deref());
69                return Ok(handle_retrospection(
70                    handler.as_ref(),
71                    &method,
72                    &query_params,
73                ));
74            }
75
76            // All other paths are SES v2 operations.
77            let response = match handler.handle_v2_operation(method, path, body).await {
78                Ok(resp) => resp,
79                Err(err) => error_to_json_response(&err),
80            };
81
82            Ok(add_v2_headers(response))
83        })
84    }
85}
86
87/// Handle retrospection endpoints (`/_aws/ses`).
88fn handle_retrospection<H: SesHandler>(
89    handler: &H,
90    method: &http::Method,
91    query_params: &std::collections::HashMap<String, String>,
92) -> http::Response<SesResponseBody> {
93    match *method {
94        http::Method::GET => {
95            let filter_id = query_params.get("id").map(String::as_str);
96            let filter_source = query_params.get("email").map(String::as_str);
97            let json = handler.query_emails(filter_id, filter_source);
98            json_response(json, http::StatusCode::OK)
99        }
100        http::Method::DELETE => {
101            let filter_id = query_params.get("id").map(String::as_str);
102            handler.clear_emails(filter_id);
103            json_response("{}".to_owned(), http::StatusCode::OK)
104        }
105        _ => {
106            let err = SesError::invalid_parameter_value(format!(
107                "Method {method} not supported on /_aws/ses"
108            ));
109            error_to_json_response(&err)
110        }
111    }
112}
113
114/// Add common headers to SES v2 responses.
115fn add_v2_headers(
116    mut response: http::Response<SesResponseBody>,
117) -> http::Response<SesResponseBody> {
118    let headers = response.headers_mut();
119
120    headers
121        .entry("content-type")
122        .or_insert(http::HeaderValue::from_static(JSON_CONTENT_TYPE));
123
124    headers.insert("server", http::HeaderValue::from_static("Rustack"));
125
126    headers.insert(
127        "access-control-allow-origin",
128        http::HeaderValue::from_static("*"),
129    );
130
131    response
132}