From 9bbfad554e7aff82d1f28960077158c31a588039 Mon Sep 17 00:00:00 2001 From: Shautvast Date: Tue, 17 Feb 2026 17:31:37 +0100 Subject: [PATCH] Fetch and display inbox emails with periodic polling Replace simple IMAP login with full inbox fetch that displays Subject, From, and Date for each message. Auto-refreshes every 30 seconds and supports manual refresh with 'r' key. Co-Authored-By: Claude Opus 4.6 --- Cargo.lock | 1 - Cargo.toml | 1 - src/main.rs | 136 ++++++++++++++++++++++++++++++++++++++-------------- 3 files changed, 100 insertions(+), 38 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b4a2c2f..f1e67b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -524,7 +524,6 @@ version = "0.1.0" dependencies = [ "crossterm", "imap", - "native-tls", "ratatui", "serde", "toml", diff --git a/Cargo.toml b/Cargo.toml index 14f94de..f9f7d21 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,5 @@ 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/main.rs b/src/main.rs index 0b7d9a9..2921f36 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use std::{io, net::TcpStream, time::Duration}; +use std::{io, net::TcpStream, time::{Duration, Instant}}; use crossterm::{ event::{self, Event, KeyCode}, @@ -7,30 +7,71 @@ use crossterm::{ }; use ratatui::{ backend::CrosstermBackend, - layout::{Alignment, Constraint, Direction, Layout}, - style::{Color, Style}, - widgets::{Block, Borders, Paragraph}, + layout::{Constraint, Direction, Layout}, + style::{Color, Modifier, Style}, + widgets::{Block, Borders, List, ListItem, Paragraph}, Terminal, }; use hello_ratatui::config::Config; -fn imap_login(config: &Config) -> Result { - let imap = &config.imap; - let stream = TcpStream::connect((&*imap.host, imap.port)).map_err(|e| e.to_string())?; +struct Email { + subject: String, + from: String, + date: String, +} + +fn fetch_inbox(config: &Config) -> Result, String> { + 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 mut session = client - .login(&imap.username, &imap.password) + .login(&imap_cfg.username, &imap_cfg.password) .map_err(|(e, _)| e.to_string())?; + + session.select("INBOX").map_err(|e| e.to_string())?; + + let mut emails = Vec::new(); + + let messages = session + .fetch("1:*", "BODY.PEEK[HEADER.FIELDS (SUBJECT FROM DATE)]") + .map_err(|e| e.to_string())?; + + for message in messages.iter() { + 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, + }); + } + } + let _ = session.logout(); - Ok(format!("Logged in as {}", imap.username)) + Ok(emails) } +const POLL_INTERVAL: Duration = Duration::from_secs(30); + fn main() -> io::Result<()> { let config = Config::load().unwrap(); - let imap_status = match imap_login(&config) { - Ok(msg) => msg, - Err(e) => format!("IMAP error: {}", e), - }; + let mut inbox = fetch_inbox(&config); + let mut last_fetch = Instant::now(); // --- Setup terminal --- enable_raw_mode()?; @@ -42,43 +83,66 @@ fn main() -> io::Result<()> { // --- Main loop --- loop { + if last_fetch.elapsed() >= POLL_INTERVAL { + inbox = fetch_inbox(&config); + last_fetch = Instant::now(); + } + terminal.draw(|frame| { - // Split the screen into a centered area let area = frame.area(); - let vertical = Layout::default() + let layout = Layout::default() .direction(Direction::Vertical) - .constraints([ - Constraint::Percentage(40), - Constraint::Percentage(20), - Constraint::Percentage(40), - ]) + .constraints([Constraint::Min(3), Constraint::Length(1)]) .split(area); - let horizontal = Layout::default() - .direction(Direction::Horizontal) - .constraints([ - Constraint::Percentage(25), - Constraint::Percentage(50), - Constraint::Percentage(25), - ]) - .split(vertical[1]); + match &inbox { + Ok(emails) if !emails.is_empty() => { + let items: Vec = emails + .iter() + .map(|e| { + ListItem::new(format!("{} | {} | {}", e.date, e.from, e.subject)) + }) + .collect(); - let center = horizontal[1]; + let list = List::new(items) + .block( + Block::default() + .title(format!("Inbox ({} messages)", emails.len())) + .borders(Borders::ALL), + ) + .style(Style::default().fg(Color::White)) + .highlight_style(Style::default().add_modifier(Modifier::BOLD)); - // Render a bordered box with "Hello, World!" - let paragraph = Paragraph::new(imap_status.as_str()) - .block(Block::default().title("Ratatui").borders(Borders::ALL)) - .style(Style::default().fg(Color::Green)) - .alignment(Alignment::Center); + frame.render_widget(list, layout[0]); + } + Ok(_) => { + let p = Paragraph::new("No messages in inbox.") + .block(Block::default().title("Inbox").borders(Borders::ALL)) + .style(Style::default().fg(Color::Yellow)); + frame.render_widget(p, layout[0]); + } + Err(e) => { + let p = Paragraph::new(format!("IMAP error: {}", e)) + .block(Block::default().title("Inbox").borders(Borders::ALL)) + .style(Style::default().fg(Color::Red)); + frame.render_widget(p, layout[0]); + } + } - frame.render_widget(paragraph, center); + let status = Paragraph::new(" 'q' quit | 'r' refresh") + .style(Style::default().fg(Color::DarkGray)); + frame.render_widget(status, layout[1]); })?; - // --- Input handling: quit on 'q' or Escape --- + // --- Input handling --- if event::poll(Duration::from_millis(200))? { if let Event::Key(key) = event::read()? { match key.code { KeyCode::Char('q') | KeyCode::Esc => break, + KeyCode::Char('r') => { + inbox = fetch_inbox(&config); + last_fetch = Instant::now(); + } _ => {} } }