diff --git a/Cargo.lock b/Cargo.lock index f1e67b6..b4a2c2f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -524,6 +524,7 @@ version = "0.1.0" dependencies = [ "crossterm", "imap", + "native-tls", "ratatui", "serde", "toml", diff --git a/Cargo.toml b/Cargo.toml index f9f7d21..14f94de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,5 +7,6 @@ edition = "2024" ratatui = "0.30" crossterm = "0.29" imap = "2.4" +native-tls = "0.2" serde = { version = "1.0", features = ["derive"] } toml = "1.0" \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 248e784..5c50cca 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,7 @@ use std::net::TcpStream; use std::time::{Duration, Instant}; use crossterm::event; use crossterm::event::{Event, KeyCode}; +use native_tls::TlsStream; use ratatui::backend::CrosstermBackend; use ratatui::layout::{Constraint, Direction, Layout}; use ratatui::prelude::{Color, Modifier, Style}; @@ -20,28 +21,57 @@ struct Email { date: String, } -type ImapSession = imap::Session; +enum ImapSession { + Plain(imap::Session), + Tls(imap::Session>), +} + +impl ImapSession { + fn noop(&mut self) -> imap::error::Result<()> { + match self { + Self::Plain(s) => s.noop(), + Self::Tls(s) => s.noop(), + } + } + + fn logout(&mut self) -> imap::error::Result<()> { + match self { + Self::Plain(s) => s.logout(), + Self::Tls(s) => s.logout(), + } + } +} fn connect(config: &Config) -> Result { let imap_cfg = &config.imap; - let stream = - TcpStream::connect((&*imap_cfg.host, imap_cfg.port)).map_err(|e| e.to_string())?; - let client = imap::Client::new(stream); - let session = client - .login(&imap_cfg.username, &imap_cfg.password) - .map_err(|(e, _)| e.to_string())?; - Ok(session) + if imap_cfg.use_tls { + let tls = native_tls::TlsConnector::builder() + .build() + .map_err(|e| e.to_string())?; + let client = imap::connect( + (&*imap_cfg.host, imap_cfg.port), + &imap_cfg.host, + &tls, + ) + .map_err(|e| e.to_string())?; + let session = client + .login(&imap_cfg.username, &imap_cfg.password) + .map_err(|(e, _)| e.to_string())?; + Ok(ImapSession::Tls(session)) + } else { + let stream = + TcpStream::connect((&*imap_cfg.host, imap_cfg.port)).map_err(|e| e.to_string())?; + let client = imap::Client::new(stream); + let session = client + .login(&imap_cfg.username, &imap_cfg.password) + .map_err(|(e, _)| e.to_string())?; + Ok(ImapSession::Plain(session)) + } } -fn fetch_inbox(session: &mut ImapSession) -> Result, String> { - session.select("INBOX").map_err(|e| e.to_string())?; - - let messages = session - .fetch("1:*", "BODY.PEEK[HEADER.FIELDS (SUBJECT FROM DATE)]") - .map_err(|e| e.to_string())?; - +fn parse_emails(fetches: &[imap::types::Fetch]) -> Vec { let mut emails = Vec::new(); - for message in messages.iter() { + for message in fetches { if let Some(body) = message.header() { let header = String::from_utf8_lossy(body); let mut subject = String::new(); @@ -61,8 +91,41 @@ fn fetch_inbox(session: &mut ImapSession) -> Result, String> { emails.push(Email { subject, from, date }); } } + emails +} - Ok(emails) +const MAX_FETCH: u32 = 50; + +fn fetch_range(exists: u32) -> String { + let start = exists.saturating_sub(MAX_FETCH - 1).max(1); + format!("{}:{}", start, exists) +} + +fn fetch_inbox(session: &mut ImapSession) -> Result, String> { + match session { + ImapSession::Plain(s) => { + let mailbox = s.select("INBOX").map_err(|e| e.to_string())?; + if mailbox.exists == 0 { + return Ok(Vec::new()); + } + let range = fetch_range(mailbox.exists); + let messages = s + .fetch(range, "BODY.PEEK[HEADER.FIELDS (SUBJECT FROM DATE)]") + .map_err(|e| e.to_string())?; + Ok(parse_emails(&messages)) + } + ImapSession::Tls(s) => { + let mailbox = s.select("INBOX").map_err(|e| e.to_string())?; + if mailbox.exists == 0 { + return Ok(Vec::new()); + } + let range = fetch_range(mailbox.exists); + let messages = s + .fetch(range, "BODY.PEEK[HEADER.FIELDS (SUBJECT FROM DATE)]") + .map_err(|e| e.to_string())?; + Ok(parse_emails(&messages)) + } + } } /// Refresh inbox using NOOP + fetch. Reconnects on error.