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 <noreply@anthropic.com>
This commit is contained in:
Shautvast 2026-02-17 17:31:37 +01:00
parent 78f5c4655c
commit 9bbfad554e
3 changed files with 100 additions and 38 deletions

1
Cargo.lock generated
View file

@ -524,7 +524,6 @@ version = "0.1.0"
dependencies = [ dependencies = [
"crossterm", "crossterm",
"imap", "imap",
"native-tls",
"ratatui", "ratatui",
"serde", "serde",
"toml", "toml",

View file

@ -7,6 +7,5 @@ edition = "2024"
ratatui = "0.30" ratatui = "0.30"
crossterm = "0.29" crossterm = "0.29"
imap = "2.4" imap = "2.4"
native-tls = "0.2"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
toml = "1.0" toml = "1.0"

View file

@ -1,4 +1,4 @@
use std::{io, net::TcpStream, time::Duration}; use std::{io, net::TcpStream, time::{Duration, Instant}};
use crossterm::{ use crossterm::{
event::{self, Event, KeyCode}, event::{self, Event, KeyCode},
@ -7,30 +7,71 @@ use crossterm::{
}; };
use ratatui::{ use ratatui::{
backend::CrosstermBackend, backend::CrosstermBackend,
layout::{Alignment, Constraint, Direction, Layout}, layout::{Constraint, Direction, Layout},
style::{Color, Style}, style::{Color, Modifier, Style},
widgets::{Block, Borders, Paragraph}, widgets::{Block, Borders, List, ListItem, Paragraph},
Terminal, Terminal,
}; };
use hello_ratatui::config::Config; use hello_ratatui::config::Config;
fn imap_login(config: &Config) -> Result<String, String> { struct Email {
let imap = &config.imap; subject: String,
let stream = TcpStream::connect((&*imap.host, imap.port)).map_err(|e| e.to_string())?; from: String,
date: String,
}
fn fetch_inbox(config: &Config) -> Result<Vec<Email>, 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 client = imap::Client::new(stream);
let mut session = client let mut session = client
.login(&imap.username, &imap.password) .login(&imap_cfg.username, &imap_cfg.password)
.map_err(|(e, _)| e.to_string())?; .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(); let _ = session.logout();
Ok(format!("Logged in as {}", imap.username)) Ok(emails)
} }
const POLL_INTERVAL: Duration = Duration::from_secs(30);
fn main() -> io::Result<()> { fn main() -> io::Result<()> {
let config = Config::load().unwrap(); let config = Config::load().unwrap();
let imap_status = match imap_login(&config) { let mut inbox = fetch_inbox(&config);
Ok(msg) => msg, let mut last_fetch = Instant::now();
Err(e) => format!("IMAP error: {}", e),
};
// --- Setup terminal --- // --- Setup terminal ---
enable_raw_mode()?; enable_raw_mode()?;
@ -42,43 +83,66 @@ fn main() -> io::Result<()> {
// --- Main loop --- // --- Main loop ---
loop { loop {
if last_fetch.elapsed() >= POLL_INTERVAL {
inbox = fetch_inbox(&config);
last_fetch = Instant::now();
}
terminal.draw(|frame| { terminal.draw(|frame| {
// Split the screen into a centered area
let area = frame.area(); let area = frame.area();
let vertical = Layout::default() let layout = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.constraints([ .constraints([Constraint::Min(3), Constraint::Length(1)])
Constraint::Percentage(40),
Constraint::Percentage(20),
Constraint::Percentage(40),
])
.split(area); .split(area);
let horizontal = Layout::default() match &inbox {
.direction(Direction::Horizontal) Ok(emails) if !emails.is_empty() => {
.constraints([ let items: Vec<ListItem> = emails
Constraint::Percentage(25), .iter()
Constraint::Percentage(50), .map(|e| {
Constraint::Percentage(25), ListItem::new(format!("{} | {} | {}", e.date, e.from, e.subject))
]) })
.split(vertical[1]); .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!" frame.render_widget(list, layout[0]);
let paragraph = Paragraph::new(imap_status.as_str()) }
.block(Block::default().title("Ratatui").borders(Borders::ALL)) Ok(_) => {
.style(Style::default().fg(Color::Green)) let p = Paragraph::new("No messages in inbox.")
.alignment(Alignment::Center); .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 event::poll(Duration::from_millis(200))? {
if let Event::Key(key) = event::read()? { if let Event::Key(key) = event::read()? {
match key.code { match key.code {
KeyCode::Char('q') | KeyCode::Esc => break, KeyCode::Char('q') | KeyCode::Esc => break,
KeyCode::Char('r') => {
inbox = fetch_inbox(&config);
last_fetch = Instant::now();
}
_ => {} _ => {}
} }
} }