1use crate::encoding::base64_encode;
4use avila_error::Result;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum AuthMechanism {
9 Plain,
11 Login,
13 CramMd5,
15 XOAuth2,
17}
18
19impl AuthMechanism {
20 pub fn as_str(&self) -> &'static str {
22 match self {
23 Self::Plain => "PLAIN",
24 Self::Login => "LOGIN",
25 Self::CramMd5 => "CRAM-MD5",
26 Self::XOAuth2 => "XOAUTH2",
27 }
28 }
29}
30
31pub fn auth_plain(username: &str, password: &str) -> String {
33 let auth_str = format!("\0{}\0{}", username, password);
34 base64_encode(auth_str.as_bytes())
35}
36
37pub fn auth_login_username(username: &str) -> String {
39 base64_encode(username.as_bytes())
40}
41
42pub fn auth_login_password(password: &str) -> String {
44 base64_encode(password.as_bytes())
45}
46
47pub fn auth_cram_md5(username: &str, _password: &str, challenge: &str) -> Result<String> {
49 let _challenge_bytes = crate::encoding::base64_decode(challenge)?;
51
52 let response = format!("{} {}", username, "placeholder_digest");
55 Ok(base64_encode(response.as_bytes()))
56}
57
58pub fn auth_xoauth2(username: &str, access_token: &str) -> String {
60 let auth_str = format!(
61 "user={}\x01auth=Bearer {}\x01\x01",
62 username, access_token
63 );
64 base64_encode(auth_str.as_bytes())
65}
66
67#[derive(Debug, Default)]
69pub struct AuthCapabilities {
70 pub plain: bool,
72 pub login: bool,
74 pub cram_md5: bool,
76 pub xoauth2: bool,
78 pub starttls: bool,
80 pub eight_bit_mime: bool,
82 pub pipelining: bool,
84 pub size: Option<usize>,
86}
87
88impl AuthCapabilities {
89 pub fn from_ehlo_response(response: &str) -> Self {
91 let mut caps = Self::default();
92
93 for line in response.lines() {
94 let line = line.trim();
95
96 if line.contains("AUTH") {
97 if line.contains("PLAIN") {
98 caps.plain = true;
99 }
100 if line.contains("LOGIN") {
101 caps.login = true;
102 }
103 if line.contains("CRAM-MD5") {
104 caps.cram_md5 = true;
105 }
106 if line.contains("XOAUTH2") {
107 caps.xoauth2 = true;
108 }
109 }
110
111 if line.contains("STARTTLS") {
112 caps.starttls = true;
113 }
114
115 if line.contains("8BITMIME") {
116 caps.eight_bit_mime = true;
117 }
118
119 if line.contains("PIPELINING") {
120 caps.pipelining = true;
121 }
122
123 if line.starts_with("250-SIZE") || line.starts_with("250 SIZE") {
124 if let Some(size_str) = line.split_whitespace().nth(1) {
125 caps.size = size_str.parse().ok();
126 }
127 }
128 }
129
130 caps
131 }
132
133 pub fn best_auth_mechanism(&self) -> Option<AuthMechanism> {
135 if self.cram_md5 {
136 Some(AuthMechanism::CramMd5)
137 } else if self.plain {
138 Some(AuthMechanism::Plain)
139 } else if self.login {
140 Some(AuthMechanism::Login)
141 } else if self.xoauth2 {
142 Some(AuthMechanism::XOAuth2)
143 } else {
144 None
145 }
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152
153 #[test]
154 fn test_auth_plain() {
155 let auth = auth_plain("user", "pass");
156 assert!(!auth.is_empty());
157 }
158
159 #[test]
160 fn test_auth_login() {
161 let username = auth_login_username("user");
162 let password = auth_login_password("pass");
163 assert!(!username.is_empty());
164 assert!(!password.is_empty());
165 }
166
167 #[test]
168 fn test_auth_xoauth2() {
169 let auth = auth_xoauth2("user@gmail.com", "ya29.token123");
170 assert!(!auth.is_empty());
172 assert!(auth.len() > 20); }
174
175 #[test]
176 fn test_auth_capabilities() {
177 let response = "250-STARTTLS\r\n250-AUTH PLAIN LOGIN\r\n250 8BITMIME\r\n";
178 let caps = AuthCapabilities::from_ehlo_response(response);
179
180 assert!(caps.starttls);
181 assert!(caps.plain);
182 assert!(caps.login);
183 assert!(caps.eight_bit_mime);
184 assert_eq!(caps.best_auth_mechanism(), Some(AuthMechanism::Plain));
185 }
186}