Add reply to email (r key)
- Email struct gains from_addr (bare address extracted from From: header) - r opens compose pre-filled: To = sender, Subject = Re: ..., cursor in Body - Quoted original shown below a dimmed separator in the Body field; sent as part of the message body when the user hits Ctrl+S - Refresh rebound from r → u / F5 to free r for reply - Status bar updated accordingly Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
2ef6a0fd24
commit
3ce0067f69
2 changed files with 79 additions and 5 deletions
18
src/inbox.rs
18
src/inbox.rs
|
|
@ -91,6 +91,16 @@ fn fetch_range_emails(session: &mut ImapSession, range: &str) -> Result<Vec<Emai
|
|||
}
|
||||
}
|
||||
|
||||
/// Extract the bare email address from a `From:` header value.
|
||||
/// Handles both `"Name <addr>"` and bare `"addr"` formats.
|
||||
fn extract_addr(s: &str) -> String {
|
||||
if let (Some(start), Some(end)) = (s.find('<'), s.rfind('>')) {
|
||||
s[start + 1..end].trim().to_string()
|
||||
} else {
|
||||
s.trim().to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_emails(fetches: &[imap::types::Fetch]) -> Vec<Email> {
|
||||
let mut emails = Vec::new();
|
||||
for message in fetches {
|
||||
|
|
@ -114,7 +124,13 @@ fn parse_emails(fetches: &[imap::types::Fetch]) -> Vec<Email> {
|
|||
}
|
||||
}
|
||||
|
||||
emails.push(Email { seq: message.message, subject, from, date });
|
||||
emails.push(Email {
|
||||
seq: message.message,
|
||||
subject,
|
||||
from_addr: extract_addr(&from),
|
||||
from,
|
||||
date,
|
||||
});
|
||||
}
|
||||
}
|
||||
emails
|
||||
|
|
|
|||
66
src/lib.rs
66
src/lib.rs
|
|
@ -44,6 +44,7 @@ pub(crate) struct Email {
|
|||
pub seq: u32,
|
||||
pub subject: String,
|
||||
pub from: String,
|
||||
pub from_addr: String,
|
||||
pub date: String,
|
||||
}
|
||||
|
||||
|
|
@ -155,6 +156,7 @@ pub fn main(config: &Config, terminal: &mut Terminal<CrosstermBackend<Stdout>>)
|
|||
let mut compose_to = String::new();
|
||||
let mut compose_subject = String::new();
|
||||
let mut compose_body = String::new();
|
||||
let mut compose_reply_quote = String::new(); // quoted original for replies
|
||||
let mut compose_field = ComposeField::To;
|
||||
let mut send_status: Option<String> = None;
|
||||
|
||||
|
|
@ -346,6 +348,19 @@ pub fn main(config: &Config, terminal: &mut Terminal<CrosstermBackend<Stdout>>)
|
|||
last.spans.push(Span::styled("_", body_style));
|
||||
}
|
||||
}
|
||||
// Show quoted original below the editable area (dimmed, read-only).
|
||||
if !compose_reply_quote.is_empty() {
|
||||
body_lines.push(Line::from(Span::styled(
|
||||
"─────────────────────────────",
|
||||
Style::default().fg(Color::DarkGray),
|
||||
)));
|
||||
for line in compose_reply_quote.lines() {
|
||||
body_lines.push(Line::from(Span::styled(
|
||||
line.to_string(),
|
||||
Style::default().fg(Color::DarkGray),
|
||||
)));
|
||||
}
|
||||
}
|
||||
let body_block = Paragraph::new(body_lines)
|
||||
.block(Block::default()
|
||||
.title(Line::from(Span::styled("Body", body_style)))
|
||||
|
|
@ -553,7 +568,7 @@ pub fn main(config: &Config, terminal: &mut Terminal<CrosstermBackend<Stdout>>)
|
|||
} else if search_active {
|
||||
" / new search | Esc clear | q quit | ↑/↓ navigate | Tab switch pane".to_string()
|
||||
} else {
|
||||
" c compose | / search | q quit | r refresh | d delete | ↑/↓ navigate | Tab switch pane".to_string()
|
||||
" c compose | r reply | / search | q quit | u/F5 refresh | d delete | ↑/↓ navigate | Tab switch pane".to_string()
|
||||
};
|
||||
let status = Paragraph::new(status_text)
|
||||
.style(Style::default().fg(Color::DarkGray));
|
||||
|
|
@ -565,7 +580,10 @@ pub fn main(config: &Config, terminal: &mut Terminal<CrosstermBackend<Stdout>>)
|
|||
if let Event::Key(key) = event::read()? {
|
||||
if mode == Mode::Compose {
|
||||
match key.code {
|
||||
KeyCode::Esc => { mode = Mode::Normal; }
|
||||
KeyCode::Esc => {
|
||||
mode = Mode::Normal;
|
||||
compose_reply_quote.clear();
|
||||
}
|
||||
KeyCode::Tab => {
|
||||
compose_field = match compose_field {
|
||||
ComposeField::To => ComposeField::Subject,
|
||||
|
|
@ -582,12 +600,18 @@ pub fn main(config: &Config, terminal: &mut Terminal<CrosstermBackend<Stdout>>)
|
|||
KeyCode::Enter => { compose_body.push('\n'); }
|
||||
KeyCode::Char('s') if key.modifiers.contains(KeyModifiers::CONTROL) => {
|
||||
if !compose_to.is_empty() {
|
||||
let full_body = if compose_reply_quote.is_empty() {
|
||||
compose_body.clone()
|
||||
} else {
|
||||
format!("{}\n\n{}", compose_body, compose_reply_quote)
|
||||
};
|
||||
let _ = cmd_tx.send(WorkerCmd::SendEmail {
|
||||
to: compose_to.clone(),
|
||||
subject: compose_subject.clone(),
|
||||
body: compose_body.clone(),
|
||||
body: full_body,
|
||||
});
|
||||
mode = Mode::Normal;
|
||||
compose_reply_quote.clear();
|
||||
}
|
||||
}
|
||||
KeyCode::Backspace => {
|
||||
|
|
@ -655,13 +679,47 @@ pub fn main(config: &Config, terminal: &mut Terminal<CrosstermBackend<Stdout>>)
|
|||
Focus::Message => Focus::Inbox,
|
||||
};
|
||||
}
|
||||
KeyCode::Char('r') => {
|
||||
KeyCode::Char('u') | KeyCode::F(5) => {
|
||||
if !loading && !search_active {
|
||||
let _ = cmd_tx.send(WorkerCmd::Refresh);
|
||||
loading = true;
|
||||
last_fetch = Instant::now();
|
||||
}
|
||||
}
|
||||
KeyCode::Char('r') if focus == Focus::Inbox => {
|
||||
// Reply to the currently selected email.
|
||||
let email_opt = if search_active {
|
||||
search_results_state.selected().and_then(|i| search_results.get(i))
|
||||
} else {
|
||||
list_state.selected().and_then(|i| emails.get(i))
|
||||
};
|
||||
if let Some(email) = email_opt {
|
||||
compose_to = email.from_addr.clone();
|
||||
compose_subject = if email.subject.to_ascii_lowercase().starts_with("re:") {
|
||||
email.subject.clone()
|
||||
} else {
|
||||
format!("Re: {}", email.subject)
|
||||
};
|
||||
compose_body.clear();
|
||||
// Pre-fill quoted original if the body is already loaded.
|
||||
if message_seq == Some(email.seq) && !message_body.is_empty() {
|
||||
let quoted = message_body
|
||||
.lines()
|
||||
.map(|l| format!("> {l}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
compose_reply_quote = format!(
|
||||
"On {}, {} wrote:\n{}",
|
||||
email.date, email.from, quoted
|
||||
);
|
||||
} else {
|
||||
compose_reply_quote.clear();
|
||||
}
|
||||
compose_field = ComposeField::Body;
|
||||
send_status = None;
|
||||
mode = Mode::Compose;
|
||||
}
|
||||
}
|
||||
KeyCode::Down | KeyCode::Char('j') => match focus {
|
||||
Focus::Inbox => {
|
||||
if search_active {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue