Files
tutabridge/crates/bridge/src/imap/mod.rs
T
Anthony a8a02ee0bf Format the workspace with rustfmt
Make the tree rustfmt-clean so CI can enforce `cargo fmt --check`.
2026-05-29 14:59:39 +02:00

124 lines
3.8 KiB
Rust

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<MailStore>,
backend: Arc<dyn MailBackend>,
tls: TlsAcceptor,
password_hash: Option<String>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
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<tokio::net::TcpStream>,
store: Arc<MailStore>,
backend: Arc<dyn MailBackend>,
password_hash: Option<String>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let (reader, mut writer) = tokio::io::split(stream);
let mut reader = BufReader::new(reader);
let mut store_watch: watch::Receiver<u64> = 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(())
}