lettre/transport/smtp/
authentication.rs

1//! Provides limited SASL authentication mechanisms
2
3use std::fmt::{self, Debug, Display, Formatter};
4
5use crate::transport::smtp::error::{self, Error};
6
7/// Accepted authentication mechanisms
8///
9/// Trying LOGIN last as it is deprecated.
10pub const DEFAULT_MECHANISMS: &[Mechanism] = &[Mechanism::Plain, Mechanism::Login];
11
12/// Contains user credentials
13#[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    /// Create a `Credentials` struct from username and password
22    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/// Represents authentication mechanisms
47#[derive(PartialEq, Eq, Copy, Clone, Hash, Debug)]
48#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
49pub enum Mechanism {
50    /// PLAIN authentication mechanism, defined in
51    /// [RFC 4616](https://tools.ietf.org/html/rfc4616)
52    Plain,
53    /// LOGIN authentication mechanism
54    /// Obsolete but needed for some providers (like Office 365)
55    ///
56    /// Defined in [draft-murchison-sasl-login-00](https://www.ietf.org/archive/id/draft-murchison-sasl-login-00.txt).
57    Login,
58    /// Non-standard XOAUTH2 mechanism, defined in
59    /// [xoauth2-protocol](https://developers.google.com/gmail/imap/xoauth2-protocol)
60    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    /// Does the mechanism support initial response?
75    pub fn supports_initial_response(self) -> bool {
76        match self {
77            Mechanism::Plain | Mechanism::Xoauth2 => true,
78            Mechanism::Login => false,
79        }
80    }
81
82    /// Returns the string to send to the server, using the provided username, password and
83    /// challenge in some cases
84    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}