From 1412ecfbb863a52f8d483fc0bb744e545f5972ce Mon Sep 17 00:00:00 2001 From: Shautvast Date: Tue, 17 Feb 2026 20:36:24 +0100 Subject: [PATCH] refactor --- config.toml | 6 --- src/connect.rs | 51 ++++++++++++++++++ src/inbox.rs | 83 +++++++++++++++++++++++++++++ src/lib.rs | 139 +++---------------------------------------------- 4 files changed, 140 insertions(+), 139 deletions(-) delete mode 100644 config.toml create mode 100644 src/connect.rs create mode 100644 src/inbox.rs diff --git a/config.toml b/config.toml deleted file mode 100644 index 2927393..0000000 --- a/config.toml +++ /dev/null @@ -1,6 +0,0 @@ -[imap] -host = "imap.gmail.com" -port = 993 -username = -password = -use_tls = true diff --git a/src/connect.rs b/src/connect.rs new file mode 100644 index 0000000..9d8b630 --- /dev/null +++ b/src/connect.rs @@ -0,0 +1,51 @@ +use std::net::TcpStream; +use native_tls::TlsStream; +use crate::config::Config; + +pub(crate) enum ImapSession { + Plain(imap::Session), + Tls(imap::Session>), +} + +impl ImapSession { + pub(crate) fn noop(&mut self) -> imap::error::Result<()> { + match self { + Self::Plain(s) => s.noop(), + Self::Tls(s) => s.noop(), + } + } + + pub(crate) fn logout(&mut self) -> imap::error::Result<()> { + match self { + Self::Plain(s) => s.logout(), + Self::Tls(s) => s.logout(), + } + } +} + +pub(crate) fn connect(config: &Config) -> Result { + let imap_cfg = &config.imap; + 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)) + } +} diff --git a/src/inbox.rs b/src/inbox.rs new file mode 100644 index 0000000..a2f7ea5 --- /dev/null +++ b/src/inbox.rs @@ -0,0 +1,83 @@ +use crate::config::Config; +use crate::connect::ImapSession; +use crate::{connect, Email}; + +const MAX_FETCH: u32 = 50; + +/// Refresh inbox using NOOP + fetch. Reconnects on error. +pub(crate) fn refresh( + session: &mut Option, + config: &Config, +) -> Result, String> { + // If we have a session, try NOOP to keep alive / detect changes + if let Some(s) = session.as_mut() { + if s.noop().is_ok() { + return fetch_inbox(s); + } + } + + // Session is dead or missing — reconnect + *session = None; + let mut new_session = connect::connect(config)?; + let result = fetch_inbox(&mut new_session); + *session = Some(new_session); + result +} + + +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)) + } + } +} + +fn parse_emails(fetches: &[imap::types::Fetch]) -> Vec { + let mut emails = Vec::new(); + for message in fetches { + if let Some(body) = message.header() { + let header = String::from_utf8_lossy(body); + let mut subject = String::new(); + let mut from = String::new(); + let mut date = String::new(); + + for line in header.lines() { + if let Some(val) = line.strip_prefix("Subject: ") { + subject = val.to_string(); + } else if let Some(val) = line.strip_prefix("From: ") { + from = val.to_string(); + } else if let Some(val) = line.strip_prefix("Date: ") { + date = val.to_string(); + } + } + + emails.push(Email { subject, from, date }); + } + } + emails +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 5c50cca..c4d4840 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,9 +1,7 @@ use std::io::{Error, Stdout}; -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}; @@ -12,6 +10,8 @@ use ratatui::widgets::{Block, Borders, List, ListItem, ListState, Paragraph}; use crate::config::Config; pub mod config; +mod connect; +mod inbox; const POLL_INTERVAL: Duration = Duration::from_secs(30); @@ -21,136 +21,9 @@ struct Email { date: String, } -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; - 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 parse_emails(fetches: &[imap::types::Fetch]) -> Vec { - let mut emails = Vec::new(); - for message in fetches { - if let Some(body) = message.header() { - let header = String::from_utf8_lossy(body); - let mut subject = String::new(); - let mut from = String::new(); - let mut date = String::new(); - - for line in header.lines() { - if let Some(val) = line.strip_prefix("Subject: ") { - subject = val.to_string(); - } else if let Some(val) = line.strip_prefix("From: ") { - from = val.to_string(); - } else if let Some(val) = line.strip_prefix("Date: ") { - date = val.to_string(); - } - } - - emails.push(Email { subject, from, date }); - } - } - 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. -fn refresh_inbox( - session: &mut Option, - config: &Config, -) -> Result, String> { - // If we have a session, try NOOP to keep alive / detect changes - if let Some(s) = session.as_mut() { - if s.noop().is_ok() { - return fetch_inbox(s); - } - } - - // Session is dead or missing — reconnect - *session = None; - let mut new_session = connect(config)?; - let result = fetch_inbox(&mut new_session); - *session = Some(new_session); - result -} - pub fn main(config: &Config, terminal: &mut Terminal>) -> Result<(), Error> { - let mut session = connect(config).ok(); - let mut inbox = refresh_inbox(&mut session, config); + let mut session = connect::connect(config).ok(); + let mut inbox = inbox::refresh(&mut session, config); let mut last_fetch = Instant::now(); let mut list_state = ListState::default(); if let Ok(emails) = &inbox { @@ -162,7 +35,7 @@ pub fn main(config: &Config, terminal: &mut Terminal>) // --- Main loop --- loop { if last_fetch.elapsed() >= POLL_INTERVAL { - inbox = refresh_inbox(&mut session, config); + inbox = inbox::refresh(&mut session, config); last_fetch = Instant::now(); } @@ -223,7 +96,7 @@ pub fn main(config: &Config, terminal: &mut Terminal>) match key.code { KeyCode::Char('q') | KeyCode::Esc => break, KeyCode::Char('r') => { - inbox = refresh_inbox(&mut session, config); + inbox = inbox::refresh(&mut session, config); last_fetch = Instant::now(); if let Ok(emails) = &inbox { if !emails.is_empty() {