Add TLS support and limit inbox fetch to last 50 messages

Support both plain and TLS IMAP connections via an ImapSession enum,
enabling use with Gmail and other TLS-only servers. Limit fetch range
to the most recent 50 messages to avoid hanging on large mailboxes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Shautvast 2026-02-17 20:25:37 +01:00
parent 7bb8aaec32
commit cc7eeba7f8
3 changed files with 82 additions and 17 deletions

1
Cargo.lock generated
View file

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

View file

@ -7,5 +7,6 @@ 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

@ -3,6 +3,7 @@ use std::net::TcpStream;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use crossterm::event; use crossterm::event;
use crossterm::event::{Event, KeyCode}; use crossterm::event::{Event, KeyCode};
use native_tls::TlsStream;
use ratatui::backend::CrosstermBackend; use ratatui::backend::CrosstermBackend;
use ratatui::layout::{Constraint, Direction, Layout}; use ratatui::layout::{Constraint, Direction, Layout};
use ratatui::prelude::{Color, Modifier, Style}; use ratatui::prelude::{Color, Modifier, Style};
@ -20,28 +21,57 @@ struct Email {
date: String, date: String,
} }
type ImapSession = imap::Session<TcpStream>; enum ImapSession {
Plain(imap::Session<TcpStream>),
Tls(imap::Session<TlsStream<TcpStream>>),
}
impl ImapSession {
fn noop(&mut self) -> imap::error::Result<()> {
match self {
Self::Plain(s) => s.noop(),
Self::Tls(s) => s.noop(),
}
}
fn logout(&mut self) -> imap::error::Result<()> {
match self {
Self::Plain(s) => s.logout(),
Self::Tls(s) => s.logout(),
}
}
}
fn connect(config: &Config) -> Result<ImapSession, String> { fn connect(config: &Config) -> Result<ImapSession, String> {
let imap_cfg = &config.imap; let imap_cfg = &config.imap;
if imap_cfg.use_tls {
let tls = native_tls::TlsConnector::builder()
.build()
.map_err(|e| e.to_string())?;
let client = imap::connect(
(&*imap_cfg.host, imap_cfg.port),
&imap_cfg.host,
&tls,
)
.map_err(|e| e.to_string())?;
let session = client
.login(&imap_cfg.username, &imap_cfg.password)
.map_err(|(e, _)| e.to_string())?;
Ok(ImapSession::Tls(session))
} else {
let stream = let stream =
TcpStream::connect((&*imap_cfg.host, imap_cfg.port)).map_err(|e| e.to_string())?; 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 session = client let session = client
.login(&imap_cfg.username, &imap_cfg.password) .login(&imap_cfg.username, &imap_cfg.password)
.map_err(|(e, _)| e.to_string())?; .map_err(|(e, _)| e.to_string())?;
Ok(session) Ok(ImapSession::Plain(session))
}
} }
fn fetch_inbox(session: &mut ImapSession) -> Result<Vec<Email>, String> { fn parse_emails(fetches: &[imap::types::Fetch]) -> Vec<Email> {
session.select("INBOX").map_err(|e| e.to_string())?;
let messages = session
.fetch("1:*", "BODY.PEEK[HEADER.FIELDS (SUBJECT FROM DATE)]")
.map_err(|e| e.to_string())?;
let mut emails = Vec::new(); let mut emails = Vec::new();
for message in messages.iter() { for message in fetches {
if let Some(body) = message.header() { if let Some(body) = message.header() {
let header = String::from_utf8_lossy(body); let header = String::from_utf8_lossy(body);
let mut subject = String::new(); let mut subject = String::new();
@ -61,8 +91,41 @@ fn fetch_inbox(session: &mut ImapSession) -> Result<Vec<Email>, String> {
emails.push(Email { subject, from, date }); emails.push(Email { subject, from, date });
} }
} }
emails
}
Ok(emails) const MAX_FETCH: u32 = 50;
fn fetch_range(exists: u32) -> String {
let start = exists.saturating_sub(MAX_FETCH - 1).max(1);
format!("{}:{}", start, exists)
}
fn fetch_inbox(session: &mut ImapSession) -> Result<Vec<Email>, String> {
match session {
ImapSession::Plain(s) => {
let mailbox = s.select("INBOX").map_err(|e| e.to_string())?;
if mailbox.exists == 0 {
return Ok(Vec::new());
}
let range = fetch_range(mailbox.exists);
let messages = s
.fetch(range, "BODY.PEEK[HEADER.FIELDS (SUBJECT FROM DATE)]")
.map_err(|e| e.to_string())?;
Ok(parse_emails(&messages))
}
ImapSession::Tls(s) => {
let mailbox = s.select("INBOX").map_err(|e| e.to_string())?;
if mailbox.exists == 0 {
return Ok(Vec::new());
}
let range = fetch_range(mailbox.exists);
let messages = s
.fetch(range, "BODY.PEEK[HEADER.FIELDS (SUBJECT FROM DATE)]")
.map_err(|e| e.to_string())?;
Ok(parse_emails(&messages))
}
}
} }
/// Refresh inbox using NOOP + fetch. Reconnects on error. /// Refresh inbox using NOOP + fetch. Reconnects on error.