1use 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
8pub struct SmtpClient {
10 client: TcpClient,
11 capabilities: Option<AuthCapabilities>,
12 authenticated: bool,
13 timeout: Duration,
14}
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum SmtpSecurity {
19 None,
21 StartTls,
23 Tls,
25}
26
27impl SmtpClient {
28 pub async fn connect(server: NetworkAddress) -> Result<Self> {
30 Self::connect_with_security(server, SmtpSecurity::None).await
31 }
32
33 pub async fn connect_with_security(server: NetworkAddress, _security: SmtpSecurity) -> Result<Self> {
35 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 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 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 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 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 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 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 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 pub async fn send_email(&mut self, email: &Email) -> Result<()> {
154 let cmd = format!("MAIL FROM:<{}>\r\n", email.from);
156 self.send(&cmd).await?;
157 self.expect_response("250").await?;
158
159 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 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 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 self.send("DATA\r\n").await?;
182 self.expect_response("354").await?;
183
184 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 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 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 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 pub fn capabilities(&self) -> Option<&AuthCapabilities> {
218 self.capabilities.as_ref()
219 }
220
221 pub fn is_authenticated(&self) -> bool {
223 self.authenticated
224 }
225
226 pub fn set_timeout(&mut self, timeout: Duration) {
228 self.timeout = timeout;
229 }
230
231 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}