1use crate::auth::TokenProvider;
2use crate::error::{Error, Result};
3use crate::graph::SendMailRequest;
4use reqwest::Client;
5use std::sync::Arc;
6
7const DEFAULT_SCOPE: &str = "https://graph.microsoft.com/.default";
9
10const DEFAULT_GRAPH_BASE: &str = "https://graph.microsoft.com/v1.0";
12
13#[derive(Debug, Clone, Default)]
19pub struct GraphMailClientBuilder {
20 tenant_id: Option<String>,
21 client_id: Option<String>,
22 client_secret: Option<String>,
23 token_url: Option<String>,
24 graph_base: Option<String>,
25 scope: Option<String>,
26}
27
28impl GraphMailClientBuilder {
29 pub fn new() -> Self {
31 Self::default()
32 }
33
34 pub fn tenant_id(mut self, id: impl Into<String>) -> Self {
36 self.tenant_id = Some(id.into());
37 self
38 }
39
40 pub fn client_id(mut self, id: impl Into<String>) -> Self {
42 self.client_id = Some(id.into());
43 self
44 }
45
46 pub fn client_secret(mut self, secret: impl Into<String>) -> Self {
48 self.client_secret = Some(secret.into());
49 self
50 }
51
52 pub fn token_url(mut self, url: impl Into<String>) -> Self {
54 self.token_url = Some(url.into());
55 self
56 }
57
58 pub fn graph_base(mut self, base: impl Into<String>) -> Self {
60 self.graph_base = Some(base.into());
61 self
62 }
63
64 pub fn scope(mut self, scope: impl Into<String>) -> Self {
66 self.scope = Some(scope.into());
67 self
68 }
69
70 pub fn build(self) -> Result<GraphMailClient> {
72 let tenant_id = self
73 .tenant_id
74 .ok_or_else(|| Error::Config("tenant_id is required".into()))?;
75 let client_id = self
76 .client_id
77 .ok_or_else(|| Error::Config("client_id is required".into()))?;
78 let client_secret = self
79 .client_secret
80 .ok_or_else(|| Error::Config("client_secret is required".into()))?;
81
82 let token_url = self.token_url.unwrap_or_else(|| {
83 format!(
84 "https://login.microsoftonline.com/{}/oauth2/v2.0/token",
85 tenant_id
86 )
87 });
88 let graph_base = self
89 .graph_base
90 .unwrap_or_else(|| DEFAULT_GRAPH_BASE.to_string())
91 .trim_end_matches('/')
92 .to_string();
93 let scope = self.scope.unwrap_or_else(|| DEFAULT_SCOPE.to_string());
94
95 let http_client = Client::builder()
96 .build()
97 .map_err(|e| Error::Config(format!("HTTP client: {e}")))?;
98
99 let token_provider = TokenProvider::new(
100 tenant_id,
101 client_id,
102 client_secret,
103 token_url,
104 scope,
105 http_client.clone(),
106 );
107
108 Ok(GraphMailClient {
109 http_client,
110 token_provider: Arc::new(token_provider),
111 graph_base,
112 })
113 }
114}
115
116#[derive(Clone)]
121pub struct GraphMailClient {
122 http_client: Client,
123 token_provider: Arc<TokenProvider>,
124 graph_base: String,
125}
126
127impl GraphMailClient {
128 #[must_use]
130 pub fn builder() -> GraphMailClientBuilder {
131 GraphMailClientBuilder::new()
132 }
133
134 pub async fn send_mail(&self, from_user: &str, request: SendMailRequest) -> Result<()> {
139 let token = self.token_provider.get_token().await?;
140 let url = format!(
141 "{}/users/{}/sendMail",
142 self.graph_base,
143 urlencoding::encode(from_user)
144 );
145
146 let res = self
147 .http_client
148 .post(&url)
149 .bearer_auth(&token)
150 .json(&request)
151 .send()
152 .await?;
153
154 let status = res.status();
155 if status.as_u16() == 202 {
156 return Ok(());
157 }
158
159 let body = res.text().await.unwrap_or_default();
160 Err(Error::Graph(format!(
161 "sendMail failed: {} {}",
162 status, body
163 )))
164 }
165}