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 return sign_in_with_tokens(&server.login, || connect(server), |fresh| oauth.token_async(&server.login, fresh), oauth).await;
58 }
59 let client = connect(server).await?;
60 within(COMMAND, client.login(&server.login, password)).await?.map_err(|(e, _)| login_error(e, &server.host))
61}
62
63pub(crate) async fn sign_in_with_tokens<T, C, CF, K, KF>(login: &str, connect: C, token: K, oauth: OAuth) -> Result<Session<T>, SyncError>
68where
69 T: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + std::fmt::Debug + Send,
70 C: Fn() -> CF,
71 CF: Future<Output = Result<Client<T>, SyncError>>,
72 K: Fn(bool) -> KF,
73 KF: Future<Output = Result<String, SyncError>>,
74{
75 let mut refusal = String::new();
76 for fresh in [false, true] {
77 let token = token(fresh).await?;
78 let client = connect().await?;
79 let mut sasl = ImapXoauth2::new(login, &token);
80 match within(COMMAND, client.authenticate("XOAUTH2", &mut sasl)).await? {
81 Ok(session) => return Ok(session),
82 Err((async_imap::error::Error::No(m) | async_imap::error::Error::Bad(m), _)) => refusal = sasl.refusal.clone().unwrap_or_else(|| m.trim().to_string()),
83 Err((other, _)) => return Err(SyncError::Network(other.to_string())),
84 }
85 }
86 Err(oauth.refused(&refusal))
87}
88
89async fn connect(server: &Server) -> Result<Client<TlsStream<TcpStream>>, SyncError> {
91 let tcp = within(CONNECT, TcpStream::connect((server.host.as_str(), server.port))).await?.map_err(network)?;
92 let tcp = match server.security {
93 Security::Tls => tcp,
94 Security::Starttls => starttls(tcp).await?,
95 };
96 let name = ServerName::try_from(server.host.clone()).map_err(|e| SyncError::Network(e.to_string()))?;
97 let tls = within(CONNECT, TlsConnector::from(tls_config()).connect(name, tcp)).await?.map_err(tls_error)?;
98 let mut client = Client::new(tls);
99 if server.security == Security::Tls {
100 let greeting = within(CONNECT, client.read_response()).await?.map_err(network)?;
102 greeting.ok_or_else(|| SyncError::Network("closed before greeting".into()))?;
103 }
104 Ok(client)
105}
106
107async fn starttls(mut tcp: TcpStream) -> Result<TcpStream, SyncError> {
111 let greeting = within(CONNECT, read_line(&mut tcp)).await??;
112 if !greeting.starts_with("* OK") {
113 return Err(SyncError::Server(greeting));
114 }
115 tcp.write_all(b"s1 STARTTLS\r\n").await.map_err(network)?;
116 loop {
117 let line = within(CONNECT, read_line(&mut tcp)).await??;
118 if line.starts_with("s1 OK") {
119 return Ok(tcp);
120 }
121 if line.starts_with("s1 ") {
122 return Err(SyncError::Server(line));
123 }
124 }
125}
126
127async fn read_line(tcp: &mut TcpStream) -> Result<String, SyncError> {
128 let mut line = Vec::new();
129 loop {
130 let byte = tcp.read_u8().await.map_err(network)?;
131 if byte == b'\n' {
132 return Ok(String::from_utf8_lossy(&line).trim_end().to_string());
133 }
134 line.push(byte);
135 if line.len() > 8192 {
136 return Err(SyncError::Server("greeting too long".into()));
137 }
138 }
139}
140
141pub(crate) fn tls_config() -> Arc<rustls::ClientConfig> {
142 #[cfg(feature = "insecure-test-tls")]
143 if std::env::var_os("SIOUL_TEST_INSECURE_TLS").is_some() {
144 return insecure::config();
145 }
146 static CONFIG: OnceLock<Arc<rustls::ClientConfig>> = OnceLock::new();
147 CONFIG
148 .get_or_init(|| {
149 let mut roots = rustls::RootCertStore::empty();
150 let (added, _) = roots.add_parsable_certificates(rustls_native_certs::load_native_certs().certs);
151 if added == 0 {
152 roots.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
154 }
155 let config = rustls::ClientConfig::builder_with_provider(Arc::new(rustls::crypto::ring::default_provider()))
156 .with_safe_default_protocol_versions()
157 .expect("ring supports TLS 1.2 and 1.3")
158 .with_root_certificates(roots)
159 .with_no_client_auth();
160 Arc::new(config)
161 })
162 .clone()
163}
164
165pub async fn within<F: Future>(limit: Duration, future: F) -> Result<F::Output, SyncError> {
167 timeout(limit, future).await.map_err(|_| SyncError::Network(format!("no answer within {} s", limit.as_secs())))
168}
169
170fn network(e: std::io::Error) -> SyncError {
171 SyncError::Network(e.to_string())
172}
173
174fn tls_error(e: std::io::Error) -> SyncError {
176 match e.get_ref().and_then(|inner| inner.downcast_ref::<rustls::Error>()) {
177 Some(tls) => SyncError::Tls(tls.to_string()),
178 None => network(e),
179 }
180}
181
182fn login_error(e: async_imap::error::Error, host: &str) -> SyncError {
186 match e {
187 async_imap::error::Error::No(m) | async_imap::error::Error::Bad(m) => {
188 let detail = m.trim().to_string();
189 let lower = detail.to_ascii_lowercase();
190 if lower.contains("application-specific password") || lower.contains("app password") || host.eq_ignore_ascii_case(crate::discover::GOOGLE_IMAP) {
191 SyncError::AppPassword(detail)
192 } else {
193 SyncError::Login(detail)
194 }
195 }
196 other => SyncError::Network(other.to_string()),
197 }
198}
199
200pub fn server(e: async_imap::error::Error) -> SyncError {
202 match e {
203 async_imap::error::Error::No(m) | async_imap::error::Error::Bad(m) => SyncError::Server(m.trim().to_string()),
204 other => SyncError::Network(other.to_string()),
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211
212 #[test]
213 fn tells_app_passwords_apart() {
214 let gmail = async_imap::error::Error::No("[ALERT] Application-specific password required: https://support.google.com/accounts/answer/185833 (Failure)".into());
215 assert!(matches!(login_error(gmail, "mail.example.org"), SyncError::AppPassword(_)));
216 let wrong = async_imap::error::Error::No("[AUTHENTICATIONFAILED] Authentication failed.".into());
217 assert!(matches!(login_error(wrong, "mail.example.org"), SyncError::Login(_)));
218 let google = async_imap::error::Error::No("[AUTHENTICATIONFAILED] Invalid credentials (Failure)".into());
220 assert!(matches!(login_error(google, "imap.gmail.com"), SyncError::AppPassword(_)));
221 }
222
223 #[test]
226 #[ignore]
227 fn reaches_real_servers_up_to_the_greeting() {
228 let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap();
229 for (host, port, security) in [("imap.gmail.com", 993, Security::Tls), ("mail.ecloud.global", 993, Security::Tls), ("outlook.office365.com", 143, Security::Starttls)] {
230 let server = Server { host: host.into(), port, security, login: String::new(), oauth: None };
231 let client = runtime.block_on(connect(&server));
232 assert!(client.is_ok(), "{host}:{port}: {:?}", client.err());
233 }
234 let wrong_name = Server { host: "wrong.host.badssl.com".into(), port: 443, security: Security::Tls, login: String::new(), oauth: None };
235 assert!(matches!(runtime.block_on(connect(&wrong_name)), Err(SyncError::Tls(_))));
236 }
237}
238
239#[cfg(feature = "insecure-test-tls")]
242mod insecure {
243 use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
244 use rustls::pki_types::{CertificateDer, ServerName, UnixTime};
245 use rustls::{DigitallySignedStruct, SignatureScheme};
246 use std::sync::Arc;
247
248 #[derive(Debug)]
249 struct AnyCertificate;
250
251 impl ServerCertVerifier for AnyCertificate {
252 fn verify_server_cert(&self, _: &CertificateDer, _: &[CertificateDer], _: &ServerName, _: &[u8], _: UnixTime) -> Result<ServerCertVerified, rustls::Error> {
253 Ok(ServerCertVerified::assertion())
254 }
255 fn verify_tls12_signature(&self, _: &[u8], _: &CertificateDer, _: &DigitallySignedStruct) -> Result<HandshakeSignatureValid, rustls::Error> {
256 Ok(HandshakeSignatureValid::assertion())
257 }
258 fn verify_tls13_signature(&self, _: &[u8], _: &CertificateDer, _: &DigitallySignedStruct) -> Result<HandshakeSignatureValid, rustls::Error> {
259 Ok(HandshakeSignatureValid::assertion())
260 }
261 fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
262 rustls::crypto::ring::default_provider().signature_verification_algorithms.supported_schemes()
263 }
264 }
265
266 pub(super) fn config() -> Arc<rustls::ClientConfig> {
267 let config = rustls::ClientConfig::builder_with_provider(Arc::new(rustls::crypto::ring::default_provider()))
268 .with_safe_default_protocol_versions()
269 .expect("ring supports TLS 1.2 and 1.3")
270 .dangerous()
271 .with_custom_certificate_verifier(Arc::new(AnyCertificate))
272 .with_no_client_auth();
273 Arc::new(config)
274 }
275}