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::io::{Error, Stdout};
|
||||||
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};
|
||||||
|
|
@ -12,6 +10,8 @@ use ratatui::widgets::{Block, Borders, List, ListItem, ListState, Paragraph};
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
|
|
||||||
pub mod config;
|
pub mod config;
|
||||||
|
mod connect;
|
||||||
|
mod inbox;
|
||||||
|
|
||||||
const POLL_INTERVAL: Duration = Duration::from_secs(30);
|
const POLL_INTERVAL: Duration = Duration::from_secs(30);
|
||||||
|
|
||||||
|
|
@ -21,136 +21,9 @@ struct Email {
|
||||||
date: String,
|
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> {
|
pub fn main(config: &Config, terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<(), Error> {
|
||||||
let mut session = connect(config).ok();
|
let mut session = connect::connect(config).ok();
|
||||||
let mut inbox = refresh_inbox(&mut session, config);
|
let mut inbox = inbox::refresh(&mut session, config);
|
||||||
let mut last_fetch = Instant::now();
|
let mut last_fetch = Instant::now();
|
||||||
let mut list_state = ListState::default();
|
let mut list_state = ListState::default();
|
||||||
if let Ok(emails) = &inbox {
|
if let Ok(emails) = &inbox {
|
||||||
|
|
@ -162,7 +35,7 @@ pub fn main(config: &Config, terminal: &mut Terminal<CrosstermBackend<Stdout>>)
|
||||||
// --- Main loop ---
|
// --- Main loop ---
|
||||||
loop {
|
loop {
|
||||||
if last_fetch.elapsed() >= POLL_INTERVAL {
|
if last_fetch.elapsed() >= POLL_INTERVAL {
|
||||||
inbox = refresh_inbox(&mut session, config);
|
inbox = inbox::refresh(&mut session, config);
|
||||||
last_fetch = Instant::now();
|
last_fetch = Instant::now();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -223,7 +96,7 @@ pub fn main(config: &Config, terminal: &mut Terminal<CrosstermBackend<Stdout>>)
|
||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Char('q') | KeyCode::Esc => break,
|
KeyCode::Char('q') | KeyCode::Esc => break,
|
||||||
KeyCode::Char('r') => {
|
KeyCode::Char('r') => {
|
||||||
inbox = refresh_inbox(&mut session, config);
|
inbox = inbox::refresh(&mut session, config);
|
||||||
last_fetch = Instant::now();
|
last_fetch = Instant::now();
|
||||||
if let Ok(emails) = &inbox {
|
if let Ok(emails) = &inbox {
|
||||||
if !emails.is_empty() {
|
if !emails.is_empty() {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue