avila_cell/
smtp.rs

1//! SMTP (Simple Mail Transfer Protocol) - Envio de emails avançado
2
3use crate::{message::Email, auth::{AuthCapabilities, auth_plain, auth_login_username, auth_login_password, auth_xoauth2}};
4use avila_error::{Error, ErrorKind, Result};
5use avila_molecule::{NetworkAddress, tcp::TcpClient};
6use std::time::Duration;
7
8/// SMTP client with advanced features
9pub struct SmtpClient {
10    client: TcpClient,
11    capabilities: Option<AuthCapabilities>,
12    authenticated: bool,
13    timeout: Duration,
14}
15
16/// SMTP connection security
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum SmtpSecurity {
19    /// No encryption (port 25)
20    None,
21    /// STARTTLS (port 587)
22    StartTls,
23    /// TLS/SSL from start (port 465)
24    Tls,
25}
26
27impl SmtpClient {
28    /// Connects to SMTP server
29    pub async fn connect(server: NetworkAddress) -> Result<Self> {
30        Self::connect_with_security(server, SmtpSecurity::None).await
31    }
32
33    /// Connects with specific security mode
34    pub async fn connect_with_security(server: NetworkAddress, _security: SmtpSecurity) -> Result<Self> {
35        // For now, only support plain TCP (TLS support requires avila-molecule updates)
36        let client = TcpClient::connect(server).await?;
37
38        let mut smtp = Self {
39            client,
40            capabilities: None,
41            authenticated: false,
42            timeout: Duration::from_secs(30),
43        };
44
45        // Read banner
46        let response = smtp.read_response().await?;
47        if !response.starts_with("220") {
48            return Err(Error::new(
49                ErrorKind::Network,
50                format!("Invalid SMTP banner: {}", response),
51            ));
52        }
53
54        Ok(smtp)
55    }
56
57    /// Sends EHLO and discovers capabilities
58    pub async fn ehlo(&mut self, domain: &str) -> Result<()> {
59        let cmd = format!("EHLO {}\r\n", domain);
60        self.send(&cmd).await?;
61
62        let response = self.read_response().await?;
63        if !response.starts_with("250") {
64            return Err(Error::new(
65                ErrorKind::Network,
66                format!("EHLO failed: {}", response),
67            ));
68        }
69
70        self.capabilities = Some(AuthCapabilities::from_ehlo_response(&response));
71        Ok(())
72    }
73
74    /// Authenticates using PLAIN mechanism
75    pub async fn auth_plain(&mut self, username: &str, password: &str) -> Result<()> {
76        let auth_str = auth_plain(username, password);
77        let cmd = format!("AUTH PLAIN {}\r\n", auth_str);
78
79        self.send(&cmd).await?;
80        let response = self.read_response().await?;
81
82        if response.starts_with("235") {
83            self.authenticated = true;
84            Ok(())
85        } else {
86            Err(Error::new(
87                ErrorKind::InvalidInput,
88                format!("Authentication failed: {}", response),
89            ))
90        }
91    }
92
93    /// Authenticates using LOGIN mechanism
94    pub async fn auth_login(&mut self, username: &str, password: &str) -> Result<()> {
95        self.send("AUTH LOGIN\r\n").await?;
96        let response = self.read_response().await?;
97
98        if !response.starts_with("334") {
99            return Err(Error::new(
100                ErrorKind::InvalidInput,
101                format!("AUTH LOGIN failed: {}", response),
102            ));
103        }
104
105        // Send username
106        let username_b64 = auth_login_username(username);
107        self.send(&format!("{}\r\n", username_b64)).await?;
108        let response = self.read_response().await?;
109
110        if !response.starts_with("334") {
111            return Err(Error::new(
112                ErrorKind::InvalidInput,
113                format!("Username rejected: {}", response),
114            ));
115        }
116
117        // Send password
118        let password_b64 = auth_login_password(password);
119        self.send(&format!("{}\r\n", password_b64)).await?;
120        let response = self.read_response().await?;
121
122        if response.starts_with("235") {
123            self.authenticated = true;
124            Ok(())
125        } else {
126            Err(Error::new(
127                ErrorKind::InvalidInput,
128                format!("Password rejected: {}", response),
129            ))
130        }
131    }
132
133    /// Authenticates using XOAUTH2 (for Gmail/Outlook)
134    pub async fn auth_xoauth2(&mut self, username: &str, access_token: &str) -> Result<()> {
135        let auth_str = auth_xoauth2(username, access_token);
136        let cmd = format!("AUTH XOAUTH2 {}\r\n", auth_str);
137
138        self.send(&cmd).await?;
139        let response = self.read_response().await?;
140
141        if response.starts_with("235") {
142            self.authenticated = true;
143            Ok(())
144        } else {
145            Err(Error::new(
146                ErrorKind::InvalidInput,
147                format!("XOAUTH2 failed: {}", response),
148            ))
149        }
150    }
151
152    /// Sends email with multipart support
153    pub async fn send_email(&mut self, email: &Email) -> Result<()> {
154        // MAIL FROM
155        let cmd = format!("MAIL FROM:<{}>\r\n", email.from);
156        self.send(&cmd).await?;
157        self.expect_response("250").await?;
158
159        // RCPT TO (for all recipients)
160        for recipient in &email.to {
161            let cmd = format!("RCPT TO:<{}>\r\n", recipient);
162            self.send(&cmd).await?;
163            self.expect_response("250").await?;
164        }
165
166        // CC recipients
167        for recipient in &email.cc {
168            let cmd = format!("RCPT TO:<{}>\r\n", recipient);
169            self.send(&cmd).await?;
170            self.expect_response("250").await?;
171        }
172
173        // BCC recipients
174        for recipient in &email.bcc {
175            let cmd = format!("RCPT TO:<{}>\r\n", recipient);
176            self.send(&cmd).await?;
177            self.expect_response("250").await?;
178        }
179
180        // DATA
181        self.send("DATA\r\n").await?;
182        self.expect_response("354").await?;
183
184        // Message body (multipart)
185        let message = email.to_mime();
186        self.send(&message).await?;
187        self.send("\r\n.\r\n").await?;
188        self.expect_response("250").await?;
189
190        Ok(())
191    }
192
193    /// Verifies email address
194    pub async fn verify(&mut self, email: &str) -> Result<bool> {
195        let cmd = format!("VRFY {}\r\n", email);
196        self.send(&cmd).await?;
197        let response = self.read_response().await?;
198
199        Ok(response.starts_with("250") || response.starts_with("251"))
200    }
201
202    /// Resets session
203    pub async fn reset(&mut self) -> Result<()> {
204        self.send("RSET\r\n").await?;
205        self.expect_response("250").await?;
206        Ok(())
207    }
208
209    /// Closes connection
210    pub async fn quit(&mut self) -> Result<()> {
211        self.send("QUIT\r\n").await?;
212        let _ = self.read_response().await;
213        Ok(())
214    }
215
216    /// Gets capabilities
217    pub fn capabilities(&self) -> Option<&AuthCapabilities> {
218        self.capabilities.as_ref()
219    }
220
221    /// Checks if authenticated
222    pub fn is_authenticated(&self) -> bool {
223        self.authenticated
224    }
225
226    /// Sets timeout for operations
227    pub fn set_timeout(&mut self, timeout: Duration) {
228        self.timeout = timeout;
229    }
230
231    /// Gets current timeout
232    pub fn timeout(&self) -> Duration {
233        self.timeout
234    }
235
236    async fn send(&mut self, data: &str) -> Result<()> {
237        self.client.send(data.as_bytes()).await
238    }
239
240    async fn read_response(&mut self) -> Result<String> {
241        let mut buffer = vec![0u8; 4096];
242        let n = self.client.receive(&mut buffer).await?;
243
244        Ok(String::from_utf8_lossy(&buffer[..n]).to_string())
245    }
246
247    async fn expect_response(&mut self, expected_code: &str) -> Result<String> {
248        let response = self.read_response().await?;
249
250        if !response.starts_with(expected_code) {
251            return Err(Error::new(
252                ErrorKind::Network,
253                format!("Unexpected response: expected {}, received {}", expected_code, response),
254            ));
255        }
256
257        Ok(response)
258    }
259}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264
265    #[test]
266    fn test_smtp_security() {
267        assert_eq!(SmtpSecurity::None, SmtpSecurity::None);
268        assert_ne!(SmtpSecurity::Tls, SmtpSecurity::StartTls);
269    }
270}