refactor
This commit is contained in:
parent
96e170f1c9
commit
1412ecfbb8
4 changed files with 140 additions and 139 deletions
|
|
@ -1,6 +0,0 @@
|
|||
[imap]
|
||||
host = "imap.gmail.com"
|
||||
port = 993
|
||||
username =
|
||||
password =
|
||||
use_tls = true
|
||||
51
src/connect.rs
Normal file
51
src/connect.rs
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
use std::net::TcpStream;
|
||||
use native_tls::TlsStream;
|
||||
use crate::config::Config;
|
||||
|
||||
pub(crate) enum ImapSession {
|
||||
Plain(imap::Session<TcpStream>),
|
||||
Tls(imap::Session<TlsStream<TcpStream>>),
|
||||
}
|
||||
|
||||
impl ImapSession {
|
||||
pub(crate) fn noop(&mut self) -> imap::error::Result<()> {
|
||||
match self {
|
||||
Self::Plain(s) => s.noop(),
|
||||
Self::Tls(s) => s.noop(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn logout(&mut self) -> imap::error::Result<()> {
|
||||
match self {
|
||||
Self::Plain(s) => s.logout(),
|
||||
Self::Tls(s) => s.logout(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn connect(config: &Config) -> Result<ImapSession, String> {
|
||||
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 =
|
||||
TcpStream::connect((&*imap_cfg.host, imap_cfg.port)).map_err(|e| e.to_string())?;
|
||||
let client = imap::Client::new(stream);
|
||||
let session = client
|
||||
.login(&imap_cfg.username, &imap_cfg.password)
|
||||
.map_err(|(e, _)| e.to_string())?;
|
||||
Ok(ImapSession::Plain(session))
|
||||
}
|
||||
}
|
||||
83
src/inbox.rs
Normal file
83
src/inbox.rs
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
use crate::config::Config;
|
||||
use crate::connect::ImapSession;
|
||||
use crate::{connect, Email};
|
||||
|
||||
const MAX_FETCH: u32 = 50;
|
||||
|
||||
/// Refresh inbox using NOOP + fetch. Reconnects on error.
|
||||
pub(crate) fn refresh(
|
||||
session: &mut Option<ImapSession>,
|
||||
config: &Config,
|
||||
) -> Result<Vec<Email>, String> {
|
||||
// If we have a session, try NOOP to keep alive / detect changes
|
||||
if let Some(s) = session.as_mut() {
|
||||
if s.noop().is_ok() {
|
||||
return fetch_inbox(s);
|
||||
}
|
||||
}
|
||||
|
||||
// Session is dead or missing — reconnect
|
||||
*session = None;
|
||||
let mut new_session = connect::connect(config)?;
|
||||
let result = fetch_inbox(&mut new_session);
|
||||
*session = Some(new_session);
|
||||
result
|
||||
}
|
||||
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_emails(fetches: &[imap::types::Fetch]) -> Vec<Email> {
|
||||
let mut emails = Vec::new();
|
||||
for message in fetches {
|
||||
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 });
|
||||
}
|
||||
}
|
||||
emails
|
||||
}
|
||||
139
src/lib.rs
139
src/lib.rs
|
|
@ -1,9 +1,7 @@
|
|||
use std::io::{Error, Stdout};
|
||||
use std::net::TcpStream;
|
||||
use std::time::{Duration, Instant};
|
||||
use crossterm::event;
|
||||
use crossterm::event::{Event, KeyCode};
|
||||
use native_tls::TlsStream;
|
||||
use ratatui::backend::CrosstermBackend;
|
||||
use ratatui::layout::{Constraint, Direction, Layout};
|
||||
use ratatui::prelude::{Color, Modifier, Style};
|
||||
|
|
@ -12,6 +10,8 @@ use ratatui::widgets::{Block, Borders, List, ListItem, ListState, Paragraph};
|
|||
use crate::config::Config;
|
||||
|
||||
pub mod config;
|
||||
mod connect;
|
||||
mod inbox;
|
||||
|
||||
const POLL_INTERVAL: Duration = Duration::from_secs(30);
|
||||
|
||||
|
|
@ -21,136 +21,9 @@ struct Email {
|
|||
date: String,
|
||||
}
|
||||
|
||||
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> {
|
||||
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 =
|
||||
TcpStream::connect((&*imap_cfg.host, imap_cfg.port)).map_err(|e| e.to_string())?;
|
||||
let client = imap::Client::new(stream);
|
||||
let session = client
|
||||
.login(&imap_cfg.username, &imap_cfg.password)
|
||||
.map_err(|(e, _)| e.to_string())?;
|
||||
Ok(ImapSession::Plain(session))
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_emails(fetches: &[imap::types::Fetch]) -> Vec<Email> {
|
||||
let mut emails = Vec::new();
|
||||
for message in fetches {
|
||||
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 });
|
||||
}
|
||||
}
|
||||
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.
|
||||
fn refresh_inbox(
|
||||
session: &mut Option<ImapSession>,
|
||||
config: &Config,
|
||||
) -> Result<Vec<Email>, String> {
|
||||
// If we have a session, try NOOP to keep alive / detect changes
|
||||
if let Some(s) = session.as_mut() {
|
||||
if s.noop().is_ok() {
|
||||
return fetch_inbox(s);
|
||||
}
|
||||
}
|
||||
|
||||
// Session is dead or missing — reconnect
|
||||
*session = None;
|
||||
let mut new_session = connect(config)?;
|
||||
let result = fetch_inbox(&mut new_session);
|
||||
*session = Some(new_session);
|
||||
result
|
||||
}
|
||||
|
||||
pub fn main(config: &Config, terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<(), Error> {
|
||||
let mut session = connect(config).ok();
|
||||
let mut inbox = refresh_inbox(&mut session, config);
|
||||
let mut session = connect::connect(config).ok();
|
||||
let mut inbox = inbox::refresh(&mut session, config);
|
||||
let mut last_fetch = Instant::now();
|
||||
let mut list_state = ListState::default();
|
||||
if let Ok(emails) = &inbox {
|
||||
|
|
@ -162,7 +35,7 @@ pub fn main(config: &Config, terminal: &mut Terminal<CrosstermBackend<Stdout>>)
|
|||
// --- Main loop ---
|
||||
loop {
|
||||
if last_fetch.elapsed() >= POLL_INTERVAL {
|
||||
inbox = refresh_inbox(&mut session, config);
|
||||
inbox = inbox::refresh(&mut session, config);
|
||||
last_fetch = Instant::now();
|
||||
}
|
||||
|
||||
|
|
@ -223,7 +96,7 @@ pub fn main(config: &Config, terminal: &mut Terminal<CrosstermBackend<Stdout>>)
|
|||
match key.code {
|
||||
KeyCode::Char('q') | KeyCode::Esc => break,
|
||||
KeyCode::Char('r') => {
|
||||
inbox = refresh_inbox(&mut session, config);
|
||||
inbox = inbox::refresh(&mut session, config);
|
||||
last_fetch = Instant::now();
|
||||
if let Ok(emails) = &inbox {
|
||||
if !emails.is_empty() {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue