elif_email/providers/
sendgrid.rs

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/// SendGrid email provider using reqwest HTTP client
9#[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, // Base64 encoded
51    #[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    /// Create new SendGrid provider
73    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    /// Convert elif Email to SendGrid format
84    fn convert_email(&self, email: &Email) -> Result<SendGridEmail, EmailError> {
85        // Parse from address
86        let from = self.parse_email_address(&email.from)?;
87
88        // Parse reply-to
89        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        // Parse recipients
96        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        // Build content
124        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        // Build attachments
143        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        // Build custom args for tracking
164        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    /// Parse email address (simple implementation)
192    fn parse_email_address(&self, addr: &str) -> Result<EmailAddress, EmailError> {
193        // Simple parsing - in real implementation you might want more sophisticated parsing
194        if addr.contains('<') && addr.contains('>') {
195            // Format: "Name <email@domain.com>"
196            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            // Simple email address
209            Ok(EmailAddress {
210                email: addr.to_string(),
211                name: None,
212            })
213        }
214    }
215
216    /// Get SendGrid API endpoint
217    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    /// Build request headers
225    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            // SendGrid returns message ID in the response headers for some endpoints
266            // For v3/mail/send, we generate one based on the email ID
267            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            // Try to parse error response
277            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        // Test with a simple API call to validate the API key
302        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
330// Add base64 encoding for attachments
331mod 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        // Simple email
357        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        // Email with name
364        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}