1use crate::{config::SendGridConfig, Email, EmailError, EmailProvider, EmailResult};
2use async_trait::async_trait;
3use reqwest::{Client, header::{HeaderMap, HeaderValue, AUTHORIZATION, CONTENT_TYPE}};
4use serde::{Deserialize, Serialize};
5use std::time::Duration;
6use tracing::{debug, error};
7
8#[derive(Clone)]
10pub struct SendGridProvider {
11 config: SendGridConfig,
12 client: Client,
13}
14
15#[derive(Debug, Serialize)]
16struct SendGridEmail {
17 personalizations: Vec<Personalization>,
18 from: EmailAddress,
19 reply_to: Option<EmailAddress>,
20 subject: String,
21 content: Vec<Content>,
22 attachments: Option<Vec<SendGridAttachment>>,
23 headers: Option<std::collections::HashMap<String, String>>,
24 custom_args: Option<std::collections::HashMap<String, String>>,
25}
26
27#[derive(Debug, Serialize)]
28struct Personalization {
29 to: Vec<EmailAddress>,
30 cc: Option<Vec<EmailAddress>>,
31 bcc: Option<Vec<EmailAddress>>,
32 custom_args: Option<std::collections::HashMap<String, String>>,
33}
34
35#[derive(Debug, Serialize)]
36struct EmailAddress {
37 email: String,
38 name: Option<String>,
39}
40
41#[derive(Debug, Serialize)]
42struct Content {
43 #[serde(rename = "type")]
44 content_type: String,
45 value: String,
46}
47
48#[derive(Debug, Serialize)]
49struct SendGridAttachment {
50 content: String, #[serde(rename = "type")]
52 content_type: String,
53 filename: String,
54 disposition: String,
55 content_id: Option<String>,
56}
57
58#[derive(Debug, Deserialize)]
59struct SendGridResponse {
60 message_id: Option<String>,
61 errors: Option<Vec<SendGridError>>,
62}
63
64#[derive(Debug, Deserialize)]
65struct SendGridError {
66 message: String,
67 field: Option<String>,
68 help: Option<String>,
69}
70
71impl SendGridProvider {
72 pub fn new(config: SendGridConfig) -> Result<Self, EmailError> {
74 let timeout = config.timeout.unwrap_or(30);
75 let client = Client::builder()
76 .timeout(Duration::from_secs(timeout))
77 .build()
78 .map_err(|e| EmailError::configuration(format!("Failed to create HTTP client: {}", e)))?;
79
80 Ok(Self { config, client })
81 }
82
83 fn convert_email(&self, email: &Email) -> Result<SendGridEmail, EmailError> {
85 let from = self.parse_email_address(&email.from)?;
87
88 let reply_to = if let Some(reply_to) = &email.reply_to {
90 Some(self.parse_email_address(reply_to)?)
91 } else {
92 None
93 };
94
95 let to: Result<Vec<EmailAddress>, EmailError> = email
97 .to
98 .iter()
99 .map(|addr| self.parse_email_address(addr))
100 .collect();
101 let to = to?;
102
103 let cc: Option<Vec<EmailAddress>> = if let Some(cc_list) = &email.cc {
104 let cc_result: Result<Vec<EmailAddress>, EmailError> = cc_list
105 .iter()
106 .map(|addr| self.parse_email_address(addr))
107 .collect();
108 Some(cc_result?)
109 } else {
110 None
111 };
112
113 let bcc: Option<Vec<EmailAddress>> = if let Some(bcc_list) = &email.bcc {
114 let bcc_result: Result<Vec<EmailAddress>, EmailError> = bcc_list
115 .iter()
116 .map(|addr| self.parse_email_address(addr))
117 .collect();
118 Some(bcc_result?)
119 } else {
120 None
121 };
122
123 let mut content = Vec::new();
125 if let Some(text) = &email.text_body {
126 content.push(Content {
127 content_type: "text/plain".to_string(),
128 value: text.clone(),
129 });
130 }
131 if let Some(html) = &email.html_body {
132 content.push(Content {
133 content_type: "text/html".to_string(),
134 value: html.clone(),
135 });
136 }
137
138 if content.is_empty() {
139 return Err(EmailError::validation("body", "Email must have either HTML or text body"));
140 }
141
142 let attachments = if email.attachments.is_empty() {
144 None
145 } else {
146 let sendgrid_attachments: Vec<SendGridAttachment> = email
147 .attachments
148 .iter()
149 .map(|att| SendGridAttachment {
150 content: base64::encode(&att.content),
151 content_type: att.content_type.clone(),
152 filename: att.filename.clone(),
153 disposition: match att.disposition {
154 crate::AttachmentDisposition::Inline => "inline".to_string(),
155 crate::AttachmentDisposition::Attachment => "attachment".to_string(),
156 },
157 content_id: att.content_id.clone(),
158 })
159 .collect();
160 Some(sendgrid_attachments)
161 };
162
163 let mut custom_args = std::collections::HashMap::new();
165 custom_args.insert("email_id".to_string(), email.id.to_string());
166 custom_args.extend(email.tracking.custom_params.clone());
167
168 let personalization = Personalization {
169 to,
170 cc,
171 bcc,
172 custom_args: Some(custom_args),
173 };
174
175 Ok(SendGridEmail {
176 personalizations: vec![personalization],
177 from,
178 reply_to,
179 subject: email.subject.clone(),
180 content,
181 attachments,
182 headers: if email.headers.is_empty() {
183 None
184 } else {
185 Some(email.headers.clone())
186 },
187 custom_args: Some(email.tracking.custom_params.clone()),
188 })
189 }
190
191 fn parse_email_address(&self, addr: &str) -> Result<EmailAddress, EmailError> {
193 if addr.contains('<') && addr.contains('>') {
195 let parts: Vec<&str> = addr.split('<').collect();
197 if parts.len() != 2 {
198 return Err(EmailError::validation("email", format!("Invalid email format: {}", addr)));
199 }
200 let name = parts[0].trim().trim_matches('"');
201 let email = parts[1].trim().trim_end_matches('>');
202
203 Ok(EmailAddress {
204 email: email.to_string(),
205 name: if name.is_empty() { None } else { Some(name.to_string()) },
206 })
207 } else {
208 Ok(EmailAddress {
210 email: addr.to_string(),
211 name: None,
212 })
213 }
214 }
215
216 fn get_endpoint(&self) -> String {
218 self.config
219 .endpoint
220 .clone()
221 .unwrap_or_else(|| "https://api.sendgrid.com/v3/mail/send".to_string())
222 }
223
224 fn build_headers(&self) -> Result<HeaderMap, EmailError> {
226 let mut headers = HeaderMap::new();
227
228 headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
229
230 let auth_header = format!("Bearer {}", self.config.api_key);
231 headers.insert(
232 AUTHORIZATION,
233 HeaderValue::from_str(&auth_header)
234 .map_err(|e| EmailError::configuration(format!("Invalid API key format: {}", e)))?,
235 );
236
237 Ok(headers)
238 }
239}
240
241#[async_trait]
242impl EmailProvider for SendGridProvider {
243 async fn send(&self, email: &Email) -> Result<EmailResult, EmailError> {
244 debug!(
245 "Sending email via SendGrid: {} -> {:?}",
246 email.from, email.to
247 );
248
249 let sendgrid_email = self.convert_email(email)?;
250 let headers = self.build_headers()?;
251 let endpoint = self.get_endpoint();
252
253 let response = self
254 .client
255 .post(&endpoint)
256 .headers(headers)
257 .json(&sendgrid_email)
258 .send()
259 .await?;
260
261 let status = response.status();
262 let response_text = response.text().await?;
263
264 if status.is_success() {
265 let message_id = format!("sendgrid-{}", email.id);
268
269 Ok(EmailResult {
270 email_id: email.id,
271 message_id,
272 sent_at: chrono::Utc::now(),
273 provider: "sendgrid".to_string(),
274 })
275 } else {
276 let error_msg = if let Ok(sendgrid_response) = serde_json::from_str::<SendGridResponse>(&response_text) {
278 if let Some(errors) = sendgrid_response.errors {
279 errors
280 .into_iter()
281 .map(|e| e.message)
282 .collect::<Vec<_>>()
283 .join("; ")
284 } else {
285 format!("HTTP {}: {}", status, response_text)
286 }
287 } else {
288 format!("HTTP {}: {}", status, response_text)
289 };
290
291 error!("SendGrid send failed: {}", error_msg);
292 Err(EmailError::provider("SendGrid", error_msg))
293 }
294 }
295
296 async fn validate_config(&self) -> Result<(), EmailError> {
297 debug!("Validating SendGrid configuration");
298
299 let headers = self.build_headers()?;
300
301 let test_endpoint = "https://api.sendgrid.com/v3/user/account";
303
304 let response = self
305 .client
306 .get(test_endpoint)
307 .headers(headers)
308 .send()
309 .await?;
310
311 if response.status().is_success() {
312 debug!("SendGrid API key validation successful");
313 Ok(())
314 } else {
315 let status = response.status();
316 let error_text = response.text().await.unwrap_or_default();
317 error!("SendGrid API key validation failed: {} - {}", status, error_text);
318 Err(EmailError::configuration(format!(
319 "SendGrid API key validation failed: {} - {}",
320 status, error_text
321 )))
322 }
323 }
324
325 fn provider_name(&self) -> &'static str {
326 "sendgrid"
327 }
328}
329
330mod base64 {
332 use base64::engine::general_purpose::STANDARD;
333 use base64::Engine;
334
335 pub fn encode(data: &[u8]) -> String {
336 STANDARD.encode(data)
337 }
338}
339
340#[cfg(test)]
341mod tests {
342 use super::*;
343
344 #[test]
345 fn test_sendgrid_provider_creation() {
346 let config = SendGridConfig::new("test-api-key");
347 let provider = SendGridProvider::new(config);
348 assert!(provider.is_ok());
349 }
350
351 #[test]
352 fn test_email_address_parsing() {
353 let config = SendGridConfig::new("test-api-key");
354 let provider = SendGridProvider::new(config).unwrap();
355
356 let result = provider.parse_email_address("user@example.com");
358 assert!(result.is_ok());
359 let addr = result.unwrap();
360 assert_eq!(addr.email, "user@example.com");
361 assert_eq!(addr.name, None);
362
363 let result = provider.parse_email_address("\"John Doe\" <john@example.com>");
365 assert!(result.is_ok());
366 let addr = result.unwrap();
367 assert_eq!(addr.email, "john@example.com");
368 assert_eq!(addr.name, Some("John Doe".to_string()));
369 }
370
371 #[test]
372 fn test_email_conversion() {
373 let config = SendGridConfig::new("test-api-key");
374 let provider = SendGridProvider::new(config).unwrap();
375
376 let email = Email::new()
377 .from("sender@example.com")
378 .to("recipient@example.com")
379 .subject("Test Email")
380 .text_body("Hello, World!");
381
382 let result = provider.convert_email(&email);
383 assert!(result.is_ok());
384
385 let sendgrid_email = result.unwrap();
386 assert_eq!(sendgrid_email.from.email, "sender@example.com");
387 assert_eq!(sendgrid_email.personalizations[0].to[0].email, "recipient@example.com");
388 assert_eq!(sendgrid_email.subject, "Test Email");
389 }
390}