1use std::fmt::{self, Debug, Display, Formatter};
4
5use crate::transport::smtp::error::{self, Error};
6
7pub const DEFAULT_MECHANISMS: &[Mechanism] = &[Mechanism::Plain, Mechanism::Login];
11
12#[derive(PartialEq, Eq, Clone, Hash)]
14#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
15pub struct Credentials {
16 authentication_identity: String,
17 secret: String,
18}
19
20impl Credentials {
21 pub fn new(username: String, password: String) -> Credentials {
23 Credentials {
24 authentication_identity: username,
25 secret: password,
26 }
27 }
28}
29
30impl<S, T> From<(S, T)> for Credentials
31where
32 S: Into<String>,
33 T: Into<String>,
34{
35 fn from((username, password): (S, T)) -> Self {
36 Credentials::new(username.into(), password.into())
37 }
38}
39
40impl Debug for Credentials {
41 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
42 f.debug_struct("Credentials").finish()
43 }
44}
45
46#[derive(PartialEq, Eq, Copy, Clone, Hash, Debug)]
48#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
49pub enum Mechanism {
50 Plain,
53 Login,
58 Xoauth2,
61}
62
63impl Display for Mechanism {
64 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
65 f.write_str(match *self {
66 Mechanism::Plain => "PLAIN",
67 Mechanism::Login => "LOGIN",
68 Mechanism::Xoauth2 => "XOAUTH2",
69 })
70 }
71}
72
73impl Mechanism {
74 pub fn supports_initial_response(self) -> bool {
76 match self {
77 Mechanism::Plain | Mechanism::Xoauth2 => true,
78 Mechanism::Login => false,
79 }
80 }
81
82 pub fn response(
85 self,
86 credentials: &Credentials,
87 challenge: Option<&str>,
88 ) -> Result<String, Error> {
89 match self {
90 Mechanism::Plain => match challenge {
91 Some(_) => Err(error::client("This mechanism does not expect a challenge")),
92 None => Ok(format!(
93 "\u{0}{}\u{0}{}",
94 credentials.authentication_identity, credentials.secret
95 )),
96 },
97 Mechanism::Login => {
98 let decoded_challenge = challenge
99 .ok_or_else(|| error::client("This mechanism does expect a challenge"))?;
100
101 if contains_ignore_ascii_case(
102 decoded_challenge,
103 ["User Name", "Username:", "Username", "User Name\0"],
104 ) {
105 return Ok(credentials.authentication_identity.clone());
106 }
107
108 if contains_ignore_ascii_case(
109 decoded_challenge,
110 ["Password", "Password:", "Password\0"],
111 ) {
112 return Ok(credentials.secret.clone());
113 }
114
115 Err(error::client("Unrecognized challenge"))
116 }
117 Mechanism::Xoauth2 => match challenge {
118 Some(_) => Err(error::client("This mechanism does not expect a challenge")),
119 None => Ok(format!(
120 "user={}\x01auth=Bearer {}\x01\x01",
121 credentials.authentication_identity, credentials.secret
122 )),
123 },
124 }
125 }
126}
127
128fn contains_ignore_ascii_case<'a>(
129 haystack: &str,
130 needles: impl IntoIterator<Item = &'a str>,
131) -> bool {
132 needles
133 .into_iter()
134 .any(|item| item.eq_ignore_ascii_case(haystack))
135}
136
137#[cfg(test)]
138mod test {
139 use super::{Credentials, Mechanism};
140
141 #[test]
142 fn test_plain() {
143 let mechanism = Mechanism::Plain;
144
145 let credentials = Credentials::new("username".to_owned(), "password".to_owned());
146
147 assert_eq!(
148 mechanism.response(&credentials, None).unwrap(),
149 "\u{0}username\u{0}password"
150 );
151 assert!(mechanism.response(&credentials, Some("test")).is_err());
152 }
153
154 #[test]
155 fn test_login() {
156 let mechanism = Mechanism::Login;
157
158 let credentials = Credentials::new("alice".to_owned(), "wonderland".to_owned());
159
160 assert_eq!(
161 mechanism.response(&credentials, Some("Username")).unwrap(),
162 "alice"
163 );
164 assert_eq!(
165 mechanism.response(&credentials, Some("Password")).unwrap(),
166 "wonderland"
167 );
168 assert!(mechanism.response(&credentials, None).is_err());
169 }
170
171 #[test]
172 fn test_login_case_insensitive() {
173 let mechanism = Mechanism::Login;
174
175 let credentials = Credentials::new("alice".to_owned(), "wonderland".to_owned());
176
177 assert_eq!(
178 mechanism.response(&credentials, Some("username")).unwrap(),
179 "alice"
180 );
181 assert_eq!(
182 mechanism.response(&credentials, Some("password")).unwrap(),
183 "wonderland"
184 );
185 assert!(mechanism.response(&credentials, None).is_err());
186 }
187
188 #[test]
189 fn test_xoauth2() {
190 let mechanism = Mechanism::Xoauth2;
191
192 let credentials = Credentials::new(
193 "username".to_owned(),
194 "vF9dft4qmTc2Nvb3RlckBhdHRhdmlzdGEuY29tCg==".to_owned(),
195 );
196
197 assert_eq!(
198 mechanism.response(&credentials, None).unwrap(),
199 "user=username\x01auth=Bearer vF9dft4qmTc2Nvb3RlckBhdHRhdmlzdGEuY29tCg==\x01\x01"
200 );
201 assert!(mechanism.response(&credentials, Some("test")).is_err());
202 }
203
204 #[test]
205 fn test_from_user_pass_for_credentials() {
206 assert_eq!(
207 Credentials::new("alice".to_owned(), "wonderland".to_owned()),
208 Credentials::from(("alice", "wonderland"))
209 );
210 }
211}