1use crate::SyncError;
13use crate::sasl::{ImapXoauth2, OAuth};
14use async_imap::{Client, Session};
15use rustls::pki_types::ServerName;
16use sioul_core::config::{Account, Security};
17use std::sync::{Arc, OnceLock};
18use std::time::Duration;
19use tokio::io::{AsyncReadExt, AsyncWriteExt};
20use tokio::net::TcpStream;
21use tokio::time::timeout;
22use tokio_rustls::TlsConnector;
23use tokio_rustls::client::TlsStream;
24
25pub type Imap = Session<TlsStream<TcpStream>>;
26
27pub(crate) const CONNECT: Duration = Duration::from_secs(20);
28pub const COMMAND: Duration = Duration::from_secs(60);
29
30#[derive(Debug, Clone)]
32pub struct Server {
33 pub host: String,
34 pub port: u16,
35 pub security: Security,
36 pub login: String,
37 pub oauth: Option<OAuth>,
39}
40
41impl Server {
42 pub fn of(account: &Account) -> Result<Server, SyncError> {
43 Ok(Server {
44 host: account.host.clone().ok_or(SyncError::NoServer)?,
45 port: account.port_or_default(),
46 security: account.security,
47 login: account.login().ok_or(SyncError::NoServer)?.to_string(),
48 oauth: OAuth::of(account),
49 })
50 }
51}
52
53pub async fn open(server: &Server, password: &str) -> Result<Imap, SyncError> {
56 if let Some(oauth) = server.oauth {
57 oauth.may_reach(&server.host, false)?;
59 return sign_in_with_tokens(&server.login, || connect(server), |fresh| oauth.token_async(&server.login, fresh), oauth).await;
60 }
61 let client = connect(server).await?;
62 within(COMMAND, client.login(&server.login, password)).await?.map_err(|(e, _)| login_error(e, &server.host))
63}
64
65pub(crate) async fn sign_in_with_tokens<T, C, CF, K, KF>(login: &str, connect: C, token: K, oauth: OAuth) -> Result<Session<T>, SyncError>
71where
72 T: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + std::fmt::Debug + Send,
73 C: Fn() -> CF,
74 CF: Future<Output = Result<Client<T>, SyncError>>,
75 K: Fn(bool) -> KF,
76 KF: Future<Output = Result<String, SyncError>>,
77{
78 let mut refusal = String::new();
79 for fresh in [false, true] {
80 let token = token(fresh).await?;
81 let client = connect().await?;
82 let mut sasl = ImapXoauth2::new(login, &token);
83 match within(COMMAND, client.authenticate("XOAUTH2", &mut sasl)).await? {
84 Ok(session) => return Ok(session),
85 Err((async_imap::error::Error::No(m) | async_imap::error::Error::Bad(m), _)) => {
86 refusal = sasl.refusal.clone().unwrap_or_else(|| server_words(&m));
87 if let Some(said) = oauth.not_connected(&refusal) {
88 return Err(said);
89 }
90 }
91 Err((other, _)) => return Err(SyncError::Network(other.to_string())),
92 }
93 }
94 Err(oauth.refused(&refusal))
95}
96
97fn server_words(said: &str) -> String {
100 said.split_once("info: Some(\"")
101 .and_then(|(_, rest)| rest.rsplit_once("\")"))
102 .map_or_else(|| said.trim().to_string(), |(words, _)| words.replace("\\\"", "\"").trim().to_string())
103}
104
105async fn connect(server: &Server) -> Result<Client<TlsStream<TcpStream>>, SyncError> {
107 let tcp = within(CONNECT, TcpStream::connect((server.host.as_str(), server.port))).await?.map_err(network)?;
108 let tcp = match server.security {
109 Security::Tls => tcp,
110 Security::Starttls => starttls(tcp).await?,
111 };
112 let name = ServerName::try_from(server.host.clone()).map_err(|e| SyncError::Network(e.to_string()))?;
113 let tls = within(CONNECT, TlsConnector::from(tls_config()).connect(name, tcp)).await?.map_err(tls_error)?;
114 let mut client = Client::new(tls);
115 if server.security == Security::Tls {
116 let greeting = within(CONNECT, client.read_response()).await?.map_err(network)?;
118 greeting.ok_or_else(|| SyncError::Network("closed before greeting".into()))?;
119 }
120 Ok(client)
121}
122
123async fn starttls(mut tcp: TcpStream) -> Result<TcpStream, SyncError> {
127 let greeting = within(CONNECT, read_line(&mut tcp)).await??;
128 if !greeting.starts_with("* OK") {
129 return Err(SyncError::Server(greeting));
130 }
131 tcp.write_all(b"s1 STARTTLS\r\n").await.map_err(network)?;
132 loop {
133 let line = within(CONNECT, read_line(&mut tcp)).await??;
134 if line.starts_with("s1 OK") {
135 return Ok(tcp);
136 }
137 if line.starts_with("s1 ") {
138 return Err(SyncError::Server(line));
139 }
140 }
141}
142
143async fn read_line(tcp: &mut TcpStream) -> Result<String, SyncError> {
144 let mut line = Vec::new();
145 loop {
146 let byte = tcp.read_u8().await.map_err(network)?;
147 if byte == b'\n' {
148 return Ok(String::from_utf8_lossy(&line).trim_end().to_string());
149 }
150 line.push(byte);
151 if line.len() > 8192 {
152 return Err(SyncError::Server("greeting too long".into()));
153 }
154 }
155}
156
157pub(crate) fn tls_config() -> Arc<rustls::ClientConfig> {
158 #[cfg(feature = "insecure-test-tls")]
159 if std::env::var_os("SIOUL_TEST_INSECURE_TLS").is_some() {
160 return insecure::config();
161 }
162 static CONFIG: OnceLock<Arc<rustls::ClientConfig>> = OnceLock::new();
163 CONFIG
164 .get_or_init(|| {
165 let mut roots = rustls::RootCertStore::empty();
166 let (added, _) = roots.add_parsable_certificates(rustls_native_certs::load_native_certs().certs);
167 if added == 0 {
168 roots.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
170 }
171 let config = rustls::ClientConfig::builder_with_provider(Arc::new(rustls::crypto::ring::default_provider()))
172 .with_safe_default_protocol_versions()
173 .expect("ring supports TLS 1.2 and 1.3")
174 .with_root_certificates(roots)
175 .with_no_client_auth();
176 Arc::new(config)
177 })
178 .clone()
179}
180
181pub async fn within<F: Future>(limit: Duration, future: F) -> Result<F::Output, SyncError> {
183 timeout(limit, future).await.map_err(|_| SyncError::Network(format!("no answer within {} s", limit.as_secs())))
184}
185
186fn network(e: std::io::Error) -> SyncError {
187 SyncError::Network(e.to_string())
188}
189
190fn tls_error(e: std::io::Error) -> SyncError {
192 match e.get_ref().and_then(|inner| inner.downcast_ref::<rustls::Error>()) {
193 Some(tls) => SyncError::Tls(tls.to_string()),
194 None => network(e),
195 }
196}
197
198fn login_error(e: async_imap::error::Error, host: &str) -> SyncError {
203 match e {
204 async_imap::error::Error::No(m) | async_imap::error::Error::Bad(m) => {
205 let detail = m.trim().to_string();
206 let lower = detail.to_ascii_lowercase();
207 if crate::microsoft::is_imap_host(host) {
208 SyncError::Login(crate::microsoft::PASSWORD.into())
209 } else if lower.contains("application-specific password") || lower.contains("app password") || host.eq_ignore_ascii_case(crate::discover::GOOGLE_IMAP) {
210 SyncError::AppPassword(detail)
211 } else {
212 SyncError::Login(detail)
213 }
214 }
215 other => SyncError::Network(other.to_string()),
216 }
217}
218
219pub fn server(e: async_imap::error::Error) -> SyncError {
221 match e {
222 async_imap::error::Error::No(m) | async_imap::error::Error::Bad(m) => SyncError::Server(m.trim().to_string()),
223 other => SyncError::Network(other.to_string()),
224 }
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230
231 #[test]
232 fn tells_app_passwords_apart() {
233 let gmail = async_imap::error::Error::No("[ALERT] Application-specific password required: https://support.google.com/accounts/answer/185833 (Failure)".into());
234 assert!(matches!(login_error(gmail, "mail.example.org"), SyncError::AppPassword(_)));
235 let wrong = async_imap::error::Error::No("[AUTHENTICATIONFAILED] Authentication failed.".into());
236 assert!(matches!(login_error(wrong, "mail.example.org"), SyncError::Login(_)));
237 let google = async_imap::error::Error::No("[AUTHENTICATIONFAILED] Invalid credentials (Failure)".into());
239 assert!(matches!(login_error(google, "imap.gmail.com"), SyncError::AppPassword(_)));
240 assert_eq!(server_words("code: None, info: Some(\"User is authenticated but not connected.\")"), "User is authenticated but not connected.");
241 assert_eq!(server_words(" AUTHENTICATE failed. "), "AUTHENTICATE failed.");
242 let microsoft = async_imap::error::Error::No("LOGIN failed.".into());
244 assert_eq!(login_error(microsoft, "outlook.office365.com"), SyncError::Login(crate::microsoft::PASSWORD.into()));
245 }
246
247 #[test]
250 fn a_microsoft_token_reaches_no_other_server() {
251 let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap();
252 let elsewhere = Server { host: "imap.example.org".into(), port: 993, security: Security::Tls, login: "you@example.org".into(), oauth: Some(OAuth::Microsoft { personal: true }) };
253 assert_eq!(runtime.block_on(open(&elsewhere, "")).err(), Some(SyncError::Said(crate::microsoft::OTHER_HOST, "imap.example.org".into())));
254 }
255
256 #[test]
259 #[ignore]
260 fn reaches_real_servers_up_to_the_greeting() {
261 let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap();
262 for (host, port, security) in [("imap.gmail.com", 993, Security::Tls), ("mail.ecloud.global", 993, Security::Tls), ("outlook.office365.com", 143, Security::Starttls)] {
263 let server = Server { host: host.into(), port, security, login: String::new(), oauth: None };
264 let client = runtime.block_on(connect(&server));
265 assert!(client.is_ok(), "{host}:{port}: {:?}", client.err());
266 }
267 let wrong_name = Server { host: "wrong.host.badssl.com".into(), port: 443, security: Security::Tls, login: String::new(), oauth: None };
268 assert!(matches!(runtime.block_on(connect(&wrong_name)), Err(SyncError::Tls(_))));
269 }
270}
271
272#[cfg(feature = "insecure-test-tls")]
275mod insecure {
276 use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
277 use rustls::pki_types::{CertificateDer, ServerName, UnixTime};
278 use rustls::{DigitallySignedStruct, SignatureScheme};
279 use std::sync::Arc;
280
281 #[derive(Debug)]
282 struct AnyCertificate;
283
284 impl ServerCertVerifier for AnyCertificate {
285 fn verify_server_cert(&self, _: &CertificateDer, _: &[CertificateDer], _: &ServerName, _: &[u8], _: UnixTime) -> Result<ServerCertVerified, rustls::Error> {
286 Ok(ServerCertVerified::assertion())
287 }
288 fn verify_tls12_signature(&self, _: &[u8], _: &CertificateDer, _: &DigitallySignedStruct) -> Result<HandshakeSignatureValid, rustls::Error> {
289 Ok(HandshakeSignatureValid::assertion())
290 }
291 fn verify_tls13_signature(&self, _: &[u8], _: &CertificateDer, _: &DigitallySignedStruct) -> Result<HandshakeSignatureValid, rustls::Error> {
292 Ok(HandshakeSignatureValid::assertion())
293 }
294 fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
295 rustls::crypto::ring::default_provider().signature_verification_algorithms.supported_schemes()
296 }
297 }
298
299 pub(super) fn config() -> Arc<rustls::ClientConfig> {
300 let config = rustls::ClientConfig::builder_with_provider(Arc::new(rustls::crypto::ring::default_provider()))
301 .with_safe_default_protocol_versions()
302 .expect("ring supports TLS 1.2 and 1.3")
303 .dangerous()
304 .with_custom_certificate_verifier(Arc::new(AnyCertificate))
305 .with_no_client_auth();
306 Arc::new(config)
307 }
308}