use async_trait::async_trait;
use serde::Serialize;
use std::time::Duration;
const CONNECT_TIMEOUT: Duration = Duration::from_secs(5);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OutboundReply {
pub to: String,
pub subject: String,
pub text: String,
}
#[derive(Debug, thiserror::Error)]
pub enum SendError {
#[error("http: {0}")]
Http(#[from] reqwest::Error),
#[error("mail relay status {code}: {body}")]
Status {
code: u16,
body: String,
},
}
#[async_trait]
pub trait OutboundMail: Send + Sync {
async fn send(&self, reply: &OutboundReply) -> Result<(), SendError>;
}
#[derive(Debug, Serialize)]
struct SendRequest<'a> {
to: &'a str,
subject: &'a str,
text: &'a str,
}
#[derive(Clone)]
pub struct RelayMailApi {
http: reqwest::Client,
base_url: String,
token: String,
}
impl RelayMailApi {
#[must_use]
pub fn with_base_url(base_url: impl Into<String>, token: impl Into<String>) -> Self {
let http = reqwest::Client::builder()
.timeout(Duration::from_secs(10))
.connect_timeout(CONNECT_TIMEOUT)
.build()
.expect("build reqwest client");
Self {
http,
base_url: base_url.into(),
token: token.into(),
}
}
}
#[async_trait]
impl OutboundMail for RelayMailApi {
async fn send(&self, reply: &OutboundReply) -> Result<(), SendError> {
let url = format!(
"{base}/internal/send-verification-email",
base = self.base_url.trim_end_matches('/')
);
let payload = SendRequest {
to: &reply.to,
subject: &reply.subject,
text: &reply.text,
};
let resp = self
.http
.post(&url)
.bearer_auth(&self.token)
.json(&payload)
.send()
.await?;
let status = resp.status();
if status.is_success() {
Ok(())
} else {
let body = resp.text().await.unwrap_or_default();
Err(SendError::Status {
code: status.as_u16(),
body,
})
}
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::pedantic, clippy::nursery, missing_docs)]
use super::*;
#[test]
fn send_error_display_includes_status_and_body() {
let err = SendError::Status {
code: 422,
body: "bad subject".to_owned(),
};
let rendered = err.to_string();
assert!(rendered.contains("422"));
assert!(rendered.contains("bad subject"));
}
}