show mail contents
This commit is contained in:
parent
df9f9f0b2f
commit
b781b0fa08
2 changed files with 107 additions and 9 deletions
50
src/inbox.rs
50
src/inbox.rs
|
|
@ -44,11 +44,7 @@ pub(crate) fn fetch_older_batch(
|
||||||
return Ok((Vec::new(), 1));
|
return Ok((Vec::new(), 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure session is alive
|
ensure_session(session, config)?;
|
||||||
if session.as_mut().map_or(true, |s| s.noop().is_err()) {
|
|
||||||
*session = None;
|
|
||||||
*session = Some(connect::connect(config)?);
|
|
||||||
}
|
|
||||||
|
|
||||||
let s = session.as_mut().unwrap();
|
let s = session.as_mut().unwrap();
|
||||||
let end = oldest_seq - 1;
|
let end = oldest_seq - 1;
|
||||||
|
|
@ -131,8 +127,50 @@ fn parse_emails(fetches: &[imap::types::Fetch]) -> Vec<Email> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
emails.push(Email { subject, from, date });
|
emails.push(Email { seq: message.message, subject, from, date });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
emails
|
emails
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn ensure_session<'a>(
|
||||||
|
session: &'a mut Option<ImapSession>,
|
||||||
|
config: &Config,
|
||||||
|
) -> Result<&'a mut ImapSession, String> {
|
||||||
|
if session.as_mut().map_or(true, |s| s.noop().is_err()) {
|
||||||
|
*session = None;
|
||||||
|
*session = Some(connect::connect(config)?);
|
||||||
|
}
|
||||||
|
Ok(session.as_mut().unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetch the text body of a single email by sequence number.
|
||||||
|
pub(crate) fn fetch_body(
|
||||||
|
session: &mut Option<ImapSession>,
|
||||||
|
seq: u32,
|
||||||
|
config: &Config,
|
||||||
|
) -> Result<String, String> {
|
||||||
|
let s = ensure_session(session, config)?;
|
||||||
|
let range = seq.to_string();
|
||||||
|
let body = match s {
|
||||||
|
ImapSession::Plain(s) => {
|
||||||
|
let messages = s
|
||||||
|
.fetch(&range, "BODY.PEEK[TEXT]")
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
extract_body(&messages)
|
||||||
|
}
|
||||||
|
ImapSession::Tls(s) => {
|
||||||
|
let messages = s
|
||||||
|
.fetch(&range, "BODY.PEEK[TEXT]")
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
extract_body(&messages)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
body.ok_or_else(|| "No body found".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_body(fetches: &[imap::types::Fetch]) -> Option<String> {
|
||||||
|
fetches.first().and_then(|f| {
|
||||||
|
f.text().map(|b| String::from_utf8_lossy(b).to_string())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
66
src/lib.rs
66
src/lib.rs
|
|
@ -18,6 +18,7 @@ mod inbox;
|
||||||
const POLL_INTERVAL: Duration = Duration::from_secs(30);
|
const POLL_INTERVAL: Duration = Duration::from_secs(30);
|
||||||
|
|
||||||
pub(crate) struct Email {
|
pub(crate) struct Email {
|
||||||
|
pub seq: u32,
|
||||||
pub subject: String,
|
pub subject: String,
|
||||||
pub from: String,
|
pub from: String,
|
||||||
pub date: String,
|
pub date: String,
|
||||||
|
|
@ -26,12 +27,14 @@ pub(crate) struct Email {
|
||||||
enum WorkerCmd {
|
enum WorkerCmd {
|
||||||
Refresh,
|
Refresh,
|
||||||
FetchMore { oldest_seq: u32 },
|
FetchMore { oldest_seq: u32 },
|
||||||
|
FetchBody { seq: u32 },
|
||||||
Quit,
|
Quit,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum WorkerResult {
|
enum WorkerResult {
|
||||||
Refreshed(Result<inbox::Inbox, String>),
|
Refreshed(Result<inbox::Inbox, String>),
|
||||||
FetchedMore(Result<(Vec<Email>, u32), String>),
|
FetchedMore(Result<(Vec<Email>, u32), String>),
|
||||||
|
Body { seq: u32, result: Result<String, String> },
|
||||||
}
|
}
|
||||||
|
|
||||||
fn worker_loop(
|
fn worker_loop(
|
||||||
|
|
@ -51,6 +54,10 @@ fn worker_loop(
|
||||||
let result = inbox::fetch_older_batch(&mut session, oldest_seq, &config);
|
let result = inbox::fetch_older_batch(&mut session, oldest_seq, &config);
|
||||||
let _ = result_tx.send(WorkerResult::FetchedMore(result));
|
let _ = result_tx.send(WorkerResult::FetchedMore(result));
|
||||||
}
|
}
|
||||||
|
WorkerCmd::FetchBody { seq } => {
|
||||||
|
let result = inbox::fetch_body(&mut session, seq, &config);
|
||||||
|
let _ = result_tx.send(WorkerResult::Body { seq, result });
|
||||||
|
}
|
||||||
WorkerCmd::Quit => break,
|
WorkerCmd::Quit => break,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -79,6 +86,9 @@ pub fn main(config: &Config, terminal: &mut Terminal<CrosstermBackend<Stdout>>)
|
||||||
let mut loading = true;
|
let mut loading = true;
|
||||||
let mut list_state = ListState::default();
|
let mut list_state = ListState::default();
|
||||||
let mut last_fetch = Instant::now();
|
let mut last_fetch = Instant::now();
|
||||||
|
let mut preview_body: String = String::new();
|
||||||
|
let mut preview_seq: Option<u32> = None;
|
||||||
|
let mut body_loading = false;
|
||||||
|
|
||||||
// --- Main loop ---
|
// --- Main loop ---
|
||||||
loop {
|
loop {
|
||||||
|
|
@ -93,8 +103,15 @@ pub fn main(config: &Config, terminal: &mut Terminal<CrosstermBackend<Stdout>>)
|
||||||
loading = false;
|
loading = false;
|
||||||
if !emails.is_empty() {
|
if !emails.is_empty() {
|
||||||
list_state.select(Some(0));
|
list_state.select(Some(0));
|
||||||
|
let seq = emails[0].seq;
|
||||||
|
preview_seq = Some(seq);
|
||||||
|
preview_body.clear();
|
||||||
|
body_loading = true;
|
||||||
|
let _ = cmd_tx.send(WorkerCmd::FetchBody { seq });
|
||||||
} else {
|
} else {
|
||||||
list_state.select(None);
|
list_state.select(None);
|
||||||
|
preview_seq = None;
|
||||||
|
preview_body.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
WorkerResult::Refreshed(Err(e)) => {
|
WorkerResult::Refreshed(Err(e)) => {
|
||||||
|
|
@ -111,6 +128,15 @@ pub fn main(config: &Config, terminal: &mut Terminal<CrosstermBackend<Stdout>>)
|
||||||
error = Some(e);
|
error = Some(e);
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
|
WorkerResult::Body { seq, result } => {
|
||||||
|
body_loading = false;
|
||||||
|
if preview_seq == Some(seq) {
|
||||||
|
match result {
|
||||||
|
Ok(body) => preview_body = body,
|
||||||
|
Err(e) => preview_body = format!("Error loading body: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -125,9 +151,14 @@ pub fn main(config: &Config, terminal: &mut Terminal<CrosstermBackend<Stdout>>)
|
||||||
let area = frame.area();
|
let area = frame.area();
|
||||||
let layout = Layout::default()
|
let layout = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.constraints([Constraint::Min(3), Constraint::Length(1)])
|
.constraints([
|
||||||
|
Constraint::Percentage(50),
|
||||||
|
Constraint::Percentage(50),
|
||||||
|
Constraint::Length(1),
|
||||||
|
])
|
||||||
.split(area);
|
.split(area);
|
||||||
|
|
||||||
|
// --- Top: Inbox list ---
|
||||||
if let Some(e) = &error {
|
if let Some(e) = &error {
|
||||||
let p = Paragraph::new(format!("IMAP error: {}", e))
|
let p = Paragraph::new(format!("IMAP error: {}", e))
|
||||||
.block(Block::default().title("Inbox").borders(Borders::ALL))
|
.block(Block::default().title("Inbox").borders(Borders::ALL))
|
||||||
|
|
@ -174,9 +205,21 @@ pub fn main(config: &Config, terminal: &mut Terminal<CrosstermBackend<Stdout>>)
|
||||||
frame.render_stateful_widget(list, layout[0], &mut list_state);
|
frame.render_stateful_widget(list, layout[0], &mut list_state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Bottom: Email preview ---
|
||||||
|
let preview_text = if body_loading {
|
||||||
|
"Loading...".to_string()
|
||||||
|
} else {
|
||||||
|
preview_body.clone()
|
||||||
|
};
|
||||||
|
let preview = Paragraph::new(preview_text)
|
||||||
|
.block(Block::default().title("Preview").borders(Borders::ALL))
|
||||||
|
.style(Style::default().fg(Color::White))
|
||||||
|
.wrap(ratatui::widgets::Wrap { trim: false });
|
||||||
|
frame.render_widget(preview, layout[1]);
|
||||||
|
|
||||||
let status = Paragraph::new(" 'q' quit | 'r' refresh | ↑/↓ navigate")
|
let status = Paragraph::new(" 'q' quit | 'r' refresh | ↑/↓ navigate")
|
||||||
.style(Style::default().fg(Color::DarkGray));
|
.style(Style::default().fg(Color::DarkGray));
|
||||||
frame.render_widget(status, layout[1]);
|
frame.render_widget(status, layout[2]);
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// --- Input handling ---
|
// --- Input handling ---
|
||||||
|
|
@ -201,12 +244,29 @@ pub fn main(config: &Config, terminal: &mut Terminal<CrosstermBackend<Stdout>>)
|
||||||
loading = true;
|
loading = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
list_state.select(Some(i.min(len.saturating_sub(1))));
|
let new_idx = i.min(len.saturating_sub(1));
|
||||||
|
list_state.select(Some(new_idx));
|
||||||
|
let seq = emails[new_idx].seq;
|
||||||
|
if preview_seq != Some(seq) {
|
||||||
|
preview_seq = Some(seq);
|
||||||
|
preview_body.clear();
|
||||||
|
body_loading = true;
|
||||||
|
let _ = cmd_tx.send(WorkerCmd::FetchBody { seq });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
KeyCode::Up | KeyCode::Char('k') => {
|
KeyCode::Up | KeyCode::Char('k') => {
|
||||||
let i = list_state.selected().map_or(0, |i| i.saturating_sub(1));
|
let i = list_state.selected().map_or(0, |i| i.saturating_sub(1));
|
||||||
list_state.select(Some(i));
|
list_state.select(Some(i));
|
||||||
|
if !emails.is_empty() {
|
||||||
|
let seq = emails[i].seq;
|
||||||
|
if preview_seq != Some(seq) {
|
||||||
|
preview_seq = Some(seq);
|
||||||
|
preview_body.clear();
|
||||||
|
body_loading = true;
|
||||||
|
let _ = cmd_tx.send(WorkerCmd::FetchBody { seq });
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue