use std::sync::Arc;
use std::time::Duration;
use async_trait::async_trait;
use connectrpc::client::{CallOptions, ClientConfig, HttpClient};
use polyc_proto::proto::polychrome::email_relay::v1::{
EmailRelayServiceClient, SendVerificationEmailRequest,
};
const CONNECT_TIMEOUT: Duration = Duration::from_secs(5);
const SEND_TIMEOUT: Duration = Duration::from_secs(10);
pub const RELAY_MOUNT_PREFIX: &str = "/api/wallet/internal-api";
#[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(transparent)]
Connect(#[from] connectrpc::ConnectError),
}
#[derive(Debug, thiserror::Error)]
pub enum RelayConfigError {
#[error("invalid mail-relay url {url:?}: {source}")]
InvalidUrl {
url: String,
#[source]
source: http::uri::InvalidUri,
},
#[error("mail-relay bearer token is not a valid http header value")]
InvalidToken,
#[error("tls setup failed for mail relay: {0}")]
Tls(String),
}
#[async_trait]
pub trait OutboundMail: Send + Sync {
async fn send(&self, reply: &OutboundReply) -> Result<(), SendError>;
}
fn http_client_for(uri: &http::Uri) -> Result<HttpClient, RelayConfigError> {
if uri.scheme_str() == Some("https") {
use rustls_platform_verifier::ConfigVerifierExt;
let tls = rustls::ClientConfig::with_platform_verifier()
.map_err(|e| RelayConfigError::Tls(e.to_string()))?;
Ok(HttpClient::builder()
.connect_timeout(CONNECT_TIMEOUT)
.with_tls(Arc::new(tls)))
} else {
Ok(HttpClient::builder()
.connect_timeout(CONNECT_TIMEOUT)
.plaintext())
}
}
#[derive(Clone)]
pub struct EmailRelayClient {
client: Arc<EmailRelayServiceClient<HttpClient>>,
}
impl std::fmt::Debug for EmailRelayClient {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EmailRelayClient").finish_non_exhaustive()
}
}
impl EmailRelayClient {
pub fn new(base_url: &str, token: &str) -> Result<Self, RelayConfigError> {
let uri: http::Uri = format!(
"{base}{RELAY_MOUNT_PREFIX}",
base = base_url.trim_end_matches('/')
)
.parse()
.map_err(|source| RelayConfigError::InvalidUrl {
url: base_url.to_owned(),
source,
})?;
let http = http_client_for(&uri)?;
let header_value = http::HeaderValue::try_from(format!("Bearer {token}"))
.map_err(|_| RelayConfigError::InvalidToken)?;
let config = ClientConfig::new(uri)
.with_default_timeout(SEND_TIMEOUT)
.with_default_header(http::header::AUTHORIZATION, header_value);
Ok(Self {
client: Arc::new(EmailRelayServiceClient::new(http, config)),
})
}
}
#[async_trait]
impl OutboundMail for EmailRelayClient {
async fn send(&self, reply: &OutboundReply) -> Result<(), SendError> {
let request = SendVerificationEmailRequest {
to: reply.to.clone(),
subject: reply.subject.clone(),
text: reply.text.clone(),
..Default::default()
};
self.client
.send_verification_email_with_options(request, CallOptions::default())
.await?;
Ok(())
}
}