use crate::auth::TokenProvider;
use crate::error::{Error, Result};
use crate::graph::SendMailRequest;
use reqwest::Client;
use std::sync::Arc;
const DEFAULT_SCOPE: &str = "https://graph.microsoft.com/.default";
const DEFAULT_GRAPH_BASE: &str = "https://graph.microsoft.com/v1.0";
#[derive(Debug, Clone, Default)]
pub struct GraphMailClientBuilder {
tenant_id: Option<String>,
client_id: Option<String>,
client_secret: Option<String>,
token_url: Option<String>,
graph_base: Option<String>,
scope: Option<String>,
}
impl GraphMailClientBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn tenant_id(mut self, id: impl Into<String>) -> Self {
self.tenant_id = Some(id.into());
self
}
pub fn client_id(mut self, id: impl Into<String>) -> Self {
self.client_id = Some(id.into());
self
}
pub fn client_secret(mut self, secret: impl Into<String>) -> Self {
self.client_secret = Some(secret.into());
self
}
pub fn token_url(mut self, url: impl Into<String>) -> Self {
self.token_url = Some(url.into());
self
}
pub fn graph_base(mut self, base: impl Into<String>) -> Self {
self.graph_base = Some(base.into());
self
}
pub fn scope(mut self, scope: impl Into<String>) -> Self {
self.scope = Some(scope.into());
self
}
pub fn build(self) -> Result<GraphMailClient> {
let tenant_id = self
.tenant_id
.ok_or_else(|| Error::Config("tenant_id is required".into()))?;
let client_id = self
.client_id
.ok_or_else(|| Error::Config("client_id is required".into()))?;
let client_secret = self
.client_secret
.ok_or_else(|| Error::Config("client_secret is required".into()))?;
let token_url = self.token_url.unwrap_or_else(|| {
format!(
"https://login.microsoftonline.com/{}/oauth2/v2.0/token",
tenant_id
)
});
let graph_base = self
.graph_base
.unwrap_or_else(|| DEFAULT_GRAPH_BASE.to_string())
.trim_end_matches('/')
.to_string();
let scope = self.scope.unwrap_or_else(|| DEFAULT_SCOPE.to_string());
let http_client = Client::builder()
.build()
.map_err(|e| Error::Config(format!("HTTP client: {e}")))?;
let token_provider = TokenProvider::new(
tenant_id,
client_id,
client_secret,
token_url,
scope,
http_client.clone(),
);
Ok(GraphMailClient {
http_client,
token_provider: Arc::new(token_provider),
graph_base,
})
}
}
#[derive(Clone)]
pub struct GraphMailClient {
http_client: Client,
token_provider: Arc<TokenProvider>,
graph_base: String,
}
impl GraphMailClient {
#[must_use]
pub fn builder() -> GraphMailClientBuilder {
GraphMailClientBuilder::new()
}
pub async fn send_mail(&self, from_user: &str, request: SendMailRequest) -> Result<()> {
let token = self.token_provider.get_token().await?;
let url = format!(
"{}/users/{}/sendMail",
self.graph_base,
urlencoding::encode(from_user)
);
let res = self
.http_client
.post(&url)
.bearer_auth(&token)
.json(&request)
.send()
.await?;
let status = res.status();
if status.as_u16() == 202 {
return Ok(());
}
let body = res.text().await.unwrap_or_default();
Err(Error::Graph(format!(
"sendMail failed: {} {}",
status, body
)))
}
}