async_smtp/
authentication.rs

1//! Provides limited SASL authentication mechanisms
2
3use crate::error::Error;
4use std::fmt::{self, Display, Formatter};
5
6/// Accepted authentication mechanisms on an encrypted connection
7/// Trying LOGIN last as it is deprecated.
8pub const DEFAULT_ENCRYPTED_MECHANISMS: &[Mechanism] = &[Mechanism::Plain, Mechanism::Login];
9
10/// Accepted authentication mechanisms on an unencrypted connection
11pub const DEFAULT_UNENCRYPTED_MECHANISMS: &[Mechanism] = &[];
12
13/// Convertible to user credentials
14pub trait IntoCredentials {
15    /// Converts to a `Credentials` struct
16    fn into_credentials(self) -> Credentials;
17}
18
19impl IntoCredentials for Credentials {
20    fn into_credentials(self) -> Credentials {
21        self
22    }
23}
24
25impl<S: Into<String>, T: Into<String>> IntoCredentials for (S, T) {
26    fn into_credentials(self) -> Credentials {
27        let (username, password) = self;
28        Credentials::new(username.into(), password.into())
29    }
30}
31
32/// Contains user credentials
33#[derive(PartialEq, Eq, Clone, Hash, Debug)]
34pub struct Credentials {
35    authentication_identity: String,
36    secret: String,
37}
38
39impl Credentials {
40    /// Create a `Credentials` struct from username and password
41    pub fn new(username: String, password: String) -> Credentials {
42        Credentials {
43            authentication_identity: username,
44            secret: password,
45        }
46    }
47}
48
49/// Represents authentication mechanisms
50#[derive(PartialEq, Eq, Copy, Clone, Hash, Debug)]
51pub enum Mechanism {
52    /// PLAIN authentication mechanism
53    /// RFC 4616: <https://tools.ietf.org/html/rfc4616>
54    Plain,
55    /// LOGIN authentication mechanism
56    /// Obsolete but needed for some providers (like office365)
57    /// <https://www.ietf.org/archive/id/draft-murchison-sasl-login-00.txt>
58    Login,
59    /// Non-standard XOAUTH2 mechanism
60    /// <https://developers.google.com/gmail/imap/xoauth2-protocol>
61    Xoauth2,
62}
63
64impl Display for Mechanism {
65    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
66        write!(
67            f,
68            "{}",
69            match *self {
70                Mechanism::Plain => "PLAIN",
71                Mechanism::Login => "LOGIN",
72                Mechanism::Xoauth2 => "XOAUTH2",
73            }
74        )
75    }
76}
77
78impl Mechanism {
79    /// Does the mechanism supports initial response
80    pub fn supports_initial_response(self) -> bool {
81        match self {
82            Mechanism::Plain | Mechanism::Xoauth2 => true,
83            Mechanism::Login => false,
84        }
85    }
86
87    /// Returns the string to send to the server, using the provided username, password and
88    /// challenge in some cases
89    pub fn response(
90        self,
91        credentials: &Credentials,
92        challenge: Option<&str>,
93    ) -> Result<String, Error> {
94        match self {
95            Mechanism::Plain => match challenge {
96                Some(_) => Err(Error::Client("This mechanism does not expect a challenge")),
97                None => Ok(format!(
98                    "\u{0}{}\u{0}{}",
99                    credentials.authentication_identity, credentials.secret
100                )),
101            },
102            Mechanism::Login => {
103                let decoded_challenge =
104                    challenge.ok_or(Error::Client("This mechanism does expect a challenge"))?;
105
106                if ["User Name", "Username:", "Username"].contains(&decoded_challenge) {
107                    return Ok(credentials.authentication_identity.to_string());
108                }
109
110                if ["Password", "Password:"].contains(&decoded_challenge) {
111                    return Ok(credentials.secret.to_string());
112                }
113
114                Err(Error::Client("Unrecognized challenge"))
115            }
116            Mechanism::Xoauth2 => match challenge {
117                Some(_) => Err(Error::Client("This mechanism does not expect a challenge")),
118                None => Ok(format!(
119                    "user={}\x01auth=Bearer {}\x01\x01",
120                    credentials.authentication_identity, credentials.secret
121                )),
122            },
123        }
124    }
125}
126
127#[cfg(test)]
128mod test {
129    use super::{Credentials, Mechanism};
130
131    #[test]
132    fn test_plain() {
133        let mechanism = Mechanism::Plain;
134
135        let credentials = Credentials::new("username".to_string(), "password".to_string());
136
137        assert_eq!(
138            mechanism.response(&credentials, None).unwrap(),
139            "\u{0}username\u{0}password"
140        );
141        assert!(mechanism.response(&credentials, Some("test")).is_err());
142    }
143
144    #[test]
145    fn test_login() {
146        let mechanism = Mechanism::Login;
147
148        let credentials = Credentials::new("alice".to_string(), "wonderland".to_string());
149
150        assert_eq!(
151            mechanism.response(&credentials, Some("Username")).unwrap(),
152            "alice"
153        );
154        assert_eq!(
155            mechanism.response(&credentials, Some("Password")).unwrap(),
156            "wonderland"
157        );
158        assert!(mechanism.response(&credentials, None).is_err());
159    }
160
161    #[test]
162    fn test_xoauth2() {
163        let mechanism = Mechanism::Xoauth2;
164
165        let credentials = Credentials::new(
166            "username".to_string(),
167            "vF9dft4qmTc2Nvb3RlckBhdHRhdmlzdGEuY29tCg==".to_string(),
168        );
169
170        assert_eq!(
171            mechanism.response(&credentials, None).unwrap(),
172            "user=username\x01auth=Bearer vF9dft4qmTc2Nvb3RlckBhdHRhdmlzdGEuY29tCg==\x01\x01"
173        );
174        assert!(mechanism.response(&credentials, Some("test")).is_err());
175    }
176}