scufflecloud_email/
aws_ses.rs

1use std::sync::Arc;
2
3use base64::Engine;
4use email_traits::AwsInterface;
5use reqwest::Method;
6
7#[derive(Debug, thiserror::Error)]
8pub(crate) enum SendEmailError {
9    #[error("serialization error: {0}")]
10    Serialization(#[from] serde_json::Error),
11    #[error("request builder error: {0}")]
12    Request(#[from] http::Error),
13    #[error("HTTP error: {0}")]
14    Http(#[from] reqwest::Error),
15    #[error("failed to sign request: {0}")]
16    Signer(#[from] reqsign::Error),
17}
18
19pub(crate) async fn send_email<G: email_traits::HttpClientInterface + AwsInterface>(
20    global: &Arc<G>,
21    raw_email: Vec<u8>,
22) -> Result<(), SendEmailError> {
23    // https://docs.aws.amazon.com/general/latest/gr/ses.html
24
25    let data = base64::prelude::BASE64_STANDARD.encode(raw_email);
26    let payload = SendEmailPayload {
27        content: EmailContent::Raw(RawMessage { data }),
28    };
29
30    let req_body = serde_json::to_vec(&payload)?;
31    let mut req_parts = http::Request::builder()
32        .method(Method::POST)
33        .uri(format!(
34            "https://email.{}.amazonaws.com/v2/email/outbound-emails",
35            global.aws_region()
36        ))
37        .header("content-type", "application/x-amz-json-1.1")
38        .header("x-amz-content-sha256", &reqsign::hash::hex_sha256(&req_body))
39        .body(())? // body is set later
40        .into_parts()
41        .0;
42
43    global.aws_ses_req_signer().sign(&mut req_parts, None).await?;
44
45    let req = http::Request::from_parts(req_parts, req_body).try_into()?;
46    global.external_http_client().execute(req).await?.error_for_status()?;
47
48    Ok(())
49}
50
51/// https://docs.aws.amazon.com/ses/latest/APIReference-V2/API_SendEmail.html
52#[derive(serde_derive::Serialize)]
53#[serde(rename_all = "PascalCase")]
54struct SendEmailPayload {
55    content: EmailContent,
56}
57
58/// https://docs.aws.amazon.com/ses/latest/APIReference-V2/API_EmailContent.html
59#[derive(serde_derive::Serialize)]
60#[serde(rename_all = "PascalCase")]
61enum EmailContent {
62    Raw(RawMessage),
63}
64
65/// https://docs.aws.amazon.com/ses/latest/APIReference-V2/API_RawMessage.html
66#[derive(serde_derive::Serialize)]
67#[serde(rename_all = "PascalCase")]
68struct RawMessage {
69    /// Base64-encoded email
70    data: String,
71}