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:
parent
78f5c4655c
commit
9bbfad554e
3 changed files with 100 additions and 38 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -524,7 +524,6 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"crossterm",
|
||||
"imap",
|
||||
"native-tls",
|
||||
"ratatui",
|
||||
"serde",
|
||||
"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"
|
||||
136
src/main.rs
136
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<String, String> {
|
||||
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<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 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<ListItem> = 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();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue