mod session; mod utf7; use log::{debug, error, info}; use std::sync::Arc; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; use tokio::net::TcpListener; use tokio::sync::watch; use tokio_rustls::TlsAcceptor; use crate::sync::MailStore; use crate::tuta::MailBackend; use session::ImapSession; pub async fn serve( port: u16, store: Arc, backend: Arc, tls: TlsAcceptor, password_hash: Option, ) -> Result<(), Box> { let listener = TcpListener::bind(format!("127.0.0.1:{}", port)).await?; info!("IMAP server listening on 127.0.0.1:{} (TLS)", port); loop { let (stream, addr) = listener.accept().await?; debug!("IMAP connection from {}", addr); let store = store.clone(); let backend = backend.clone(); let tls = tls.clone(); let pw_hash = password_hash.clone(); tokio::spawn(async move { match tls.accept(stream).await { Ok(tls_stream) => { if let Err(e) = handle_connection(tls_stream, store, backend, pw_hash).await { error!("IMAP connection error: {}", e); } } Err(e) => { error!("IMAP TLS handshake failed: {}", e); } } }); } } async fn handle_connection( stream: tokio_rustls::server::TlsStream, store: Arc, backend: Arc, password_hash: Option, ) -> Result<(), Box> { let (reader, mut writer) = tokio::io::split(stream); let mut reader = BufReader::new(reader); let mut store_watch: watch::Receiver = store.subscribe(); let mut session = ImapSession::new(store, backend, password_hash); writer .write_all(b"* OK TutaBridge IMAP4rev1 ready\r\n") .await?; writer.flush().await?; let mut line = String::new(); loop { if session.is_idle() { line.clear(); tokio::select! { result = reader.read_line(&mut line) => { let n = result?; if n == 0 { break; } let trimmed = line.trim_end(); debug!("IMAP C (idle): {}", trimmed); if trimmed.eq_ignore_ascii_case("DONE") { let responses = session.end_idle(); for resp in &responses { debug!("IMAP S: {}", resp.trim_end()); writer.write_all(resp.as_bytes()).await?; } writer.flush().await?; } } _ = store_watch.changed() => { let updates = session.check_new_mail().await; for resp in &updates { debug!("IMAP S (store update): {}", resp.trim_end()); writer.write_all(resp.as_bytes()).await?; } writer.flush().await?; } } continue; } line.clear(); let n = reader.read_line(&mut line).await?; if n == 0 { break; } let trimmed = line.trim_end(); debug!("IMAP C: {}", trimmed); let responses = if session.is_awaiting_auth() { session.handle_auth_response(trimmed) } else { session.handle_command(trimmed).await }; for resp in &responses { debug!("IMAP S: {}", resp.trim_end()); writer.write_all(resp.as_bytes()).await?; } writer.flush().await?; if session.is_logout() { break; } } Ok(()) }