This commit is contained in:
Michael Zhang 2021-03-11 23:07:40 -06:00
parent 7ec5c510a3
commit fd1cb9f927
Signed by: michael
GPG key ID: BDA47A31A3C8EE6B
7 changed files with 185 additions and 139 deletions

View file

@ -1,10 +1,10 @@
//! Panorama/IMAP //! Panorama/IMAP
//! === //! ===
//! //!
//! This is a library that implements the IMAP protocol according to RFC 3501 and several //! This is a library that implements parts of the IMAP protocol according to RFC 3501 and several
//! extensions. Although its primary purpose is to be used in panorama, it should be usable for //! extensions. Although its primary purpose is to be used in panorama, it should be usable for
//! general-purpose IMAP usage. See the [client][crate::client] module for more information on how //! general-purpose IMAP client development. See the [client][crate::client] module for more
//! to get started with a client quickly. //! information on how to get started with a client quickly.
//! //!
//! RFCs: //! RFCs:
//! //!

View file

@ -36,7 +36,4 @@ imap routine
list of shit to do list of shit to do
--- ---
- [x] starttls impl - profile the client to see what needs to be improved?
- [ ] auth impl
- [ ] auth plain impl
- [ ] fetch impl

View file

@ -3,6 +3,7 @@
//! One of the primary goals of panorama is to be able to always hot-reload configuration files. //! One of the primary goals of panorama is to be able to always hot-reload configuration files.
use std::fs::{self, File}; use std::fs::{self, File};
use std::collections::HashMap;
use std::io::Read; use std::io::Read;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@ -26,7 +27,7 @@ pub struct Config {
/// Mail accounts /// Mail accounts
#[serde(rename = "mail")] #[serde(rename = "mail")]
pub mail_accounts: Vec<MailAccountConfig>, pub mail_accounts: HashMap<String, MailAccountConfig>,
} }
/// Configuration for a single mail account /// Configuration for a single mail account

147
src/mail/client.rs Normal file
View file

@ -0,0 +1,147 @@
use anyhow::Result;
use futures::{
future::FutureExt,
stream::{Stream, StreamExt},
};
use notify_rust::{Notification, Timeout};
use panorama_imap::{
client::{
auth::{self, Auth},
ClientBuilder, ClientConfig,
},
command::Command as ImapCommand,
response::{AttributeValue, Envelope, MailboxData, Response},
};
use tokio::{
sync::mpsc::{UnboundedReceiver, UnboundedSender},
task::JoinHandle,
};
use crate::config::{Config, ConfigWatcher, ImapAuth, MailAccountConfig, TlsMethod};
use super::{MailCommand, MailEvent};
/// The main sequence of steps for the IMAP thread to follow
pub async fn imap_main(
acct: MailAccountConfig,
mail2ui_tx: UnboundedSender<MailEvent>,
) -> Result<()> {
// loop ensures that the connection is retried after it dies
loop {
let builder: ClientConfig = ClientBuilder::default()
.hostname(acct.imap.server.clone())
.port(acct.imap.port)
.tls(matches!(acct.imap.tls, TlsMethod::On))
.build()
.map_err(|err| anyhow!("err: {}", err))?;
debug!("connecting to {}:{}", &acct.imap.server, acct.imap.port);
let unauth = builder.open().await?;
let unauth = if matches!(acct.imap.tls, TlsMethod::Starttls) {
debug!("attempting to upgrade");
let client = unauth.upgrade().await?;
debug!("upgrade successful");
client
} else {
unauth
};
debug!("preparing to auth");
// check if the authentication method is supported
let mut authed = match &acct.imap.auth {
ImapAuth::Plain { username, password } => {
let auth = auth::Plain {
username: username.clone(),
password: password.clone(),
};
auth.perform_auth(unauth).await?
}
};
debug!("authentication successful!");
// let's just select INBOX for now, maybe have a config for default mailbox later?
debug!("selecting the INBOX mailbox");
authed.select("INBOX").await?;
loop {
let folder_list = authed.list().await?;
debug!("mailbox list: {:?}", folder_list);
let _ = mail2ui_tx.send(MailEvent::FolderList(folder_list));
let message_uids = authed.uid_search().await?;
let message_uids = message_uids.into_iter().take(30).collect::<Vec<_>>();
let _ = mail2ui_tx.send(MailEvent::MessageUids(message_uids.clone()));
// TODO: make this happen concurrently with the main loop?
let mut message_list = authed.uid_fetch(&message_uids).await.unwrap();
while let Some((uid, attrs)) = message_list.next().await {
let evt = MailEvent::UpdateUid(uid, attrs);
debug!("sent {:?}", evt);
mail2ui_tx.send(evt);
}
// check if IDLE is supported
let supports_idle = authed.has_capability("IDLE").await?;
if supports_idle {
let mut idle_stream = authed.idle().await?;
loop {
let evt = match idle_stream.next().await {
Some(v) => v,
None => break,
};
debug!("got an event: {:?}", evt);
match evt {
Response::MailboxData(MailboxData::Exists(uid)) => {
debug!("NEW MESSAGE WITH UID {:?}, droping everything", uid);
// send DONE to stop the idle
std::mem::drop(idle_stream);
let handle = Notification::new()
.summary("New Email")
.body("holy Shit,")
.icon("firefox")
.timeout(Timeout::Milliseconds(6000))
.show()?;
let message_uids = authed.uid_search().await?;
let message_uids =
message_uids.into_iter().take(20).collect::<Vec<_>>();
let _ = mail2ui_tx.send(MailEvent::MessageUids(message_uids.clone()));
// TODO: make this happen concurrently with the main loop?
let mut message_list = authed.uid_fetch(&message_uids).await.unwrap();
while let Some((uid, attrs)) = message_list.next().await {
let evt = MailEvent::UpdateUid(uid, attrs);
debug!("sent {:?}", evt);
mail2ui_tx.send(evt);
}
idle_stream = authed.idle().await?;
}
_ => {}
}
}
} else {
loop {
tokio::time::sleep(std::time::Duration::from_secs(20)).await;
debug!("heartbeat");
}
}
if false {
break;
}
}
// wait a bit so we're not hitting the server really fast if the fail happens
// early on
//
// TODO: some kind of smart exponential backoff that considers some time
// threshold to be a failing case?
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
}
}

View file

@ -1,5 +1,6 @@
//! Mail //! Mail
mod client;
mod metadata; mod metadata;
use anyhow::Result; use anyhow::Result;
@ -82,14 +83,14 @@ pub async fn run_mail(
conn.abort(); conn.abort();
} }
for acct in config.mail_accounts.into_iter() { for (acct_name, acct) in config.mail_accounts.into_iter() {
let mail2ui_tx = mail2ui_tx.clone(); let mail2ui_tx = mail2ui_tx.clone();
let handle = tokio::spawn(async move { let handle = tokio::spawn(async move {
// debug!("opening imap connection for {:?}", acct); // debug!("opening imap connection for {:?}", acct);
// this loop is to make sure accounts are restarted on error // this loop is to make sure accounts are restarted on error
loop { loop {
match imap_main(acct.clone(), mail2ui_tx.clone()).await { match client::imap_main(acct.clone(), mail2ui_tx.clone()).await {
Ok(_) => {} Ok(_) => {}
Err(err) => { Err(err) => {
error!("IMAP Error: {}", err); error!("IMAP Error: {}", err);
@ -112,125 +113,3 @@ pub async fn run_mail(
Ok(()) Ok(())
} }
/// The main sequence of steps for the IMAP thread to follow
async fn imap_main(acct: MailAccountConfig, mail2ui_tx: UnboundedSender<MailEvent>) -> Result<()> {
// loop ensures that the connection is retried after it dies
loop {
let builder: ClientConfig = ClientBuilder::default()
.hostname(acct.imap.server.clone())
.port(acct.imap.port)
.tls(matches!(acct.imap.tls, TlsMethod::On))
.build()
.map_err(|err| anyhow!("err: {}", err))?;
debug!("connecting to {}:{}", &acct.imap.server, acct.imap.port);
let unauth = builder.open().await?;
let unauth = if matches!(acct.imap.tls, TlsMethod::Starttls) {
debug!("attempting to upgrade");
let client = unauth.upgrade().await?;
debug!("upgrade successful");
client
} else {
unauth
};
debug!("preparing to auth");
// check if the authentication method is supported
let mut authed = match &acct.imap.auth {
ImapAuth::Plain { username, password } => {
let auth = auth::Plain {
username: username.clone(),
password: password.clone(),
};
auth.perform_auth(unauth).await?
}
};
debug!("authentication successful!");
// let's just select INBOX for now, maybe have a config for default mailbox later?
debug!("selecting the INBOX mailbox");
authed.select("INBOX").await?;
loop {
let folder_list = authed.list().await?;
debug!("mailbox list: {:?}", folder_list);
let _ = mail2ui_tx.send(MailEvent::FolderList(folder_list));
let message_uids = authed.uid_search().await?;
let message_uids = message_uids.into_iter().take(30).collect::<Vec<_>>();
let _ = mail2ui_tx.send(MailEvent::MessageUids(message_uids.clone()));
// TODO: make this happen concurrently with the main loop?
let mut message_list = authed.uid_fetch(&message_uids).await.unwrap();
while let Some((uid, attrs)) = message_list.next().await {
let evt = MailEvent::UpdateUid(uid, attrs);
debug!("sent {:?}", evt);
mail2ui_tx.send(evt);
}
// check if IDLE is supported
let supports_idle = authed.has_capability("IDLE").await?;
if supports_idle {
let mut idle_stream = authed.idle().await?;
loop {
let evt = match idle_stream.next().await {
Some(v) => v,
None => break,
};
debug!("got an event: {:?}", evt);
match evt {
Response::MailboxData(MailboxData::Exists(uid)) => {
debug!("NEW MESSAGE WITH UID {:?}, droping everything", uid);
// send DONE to stop the idle
std::mem::drop(idle_stream);
let handle = Notification::new()
.summary("New Email")
.body("holy Shit,")
.icon("firefox")
.timeout(Timeout::Milliseconds(6000))
.show()?;
let message_uids = authed.uid_search().await?;
let message_uids =
message_uids.into_iter().take(20).collect::<Vec<_>>();
let _ = mail2ui_tx.send(MailEvent::MessageUids(message_uids.clone()));
// TODO: make this happen concurrently with the main loop?
let mut message_list = authed.uid_fetch(&message_uids).await.unwrap();
while let Some((uid, attrs)) = message_list.next().await {
let evt = MailEvent::UpdateUid(uid, attrs);
debug!("sent {:?}", evt);
mail2ui_tx.send(evt);
}
idle_stream = authed.idle().await?;
}
_ => {}
}
}
} else {
loop {
tokio::time::sleep(std::time::Duration::from_secs(20)).await;
debug!("heartbeat");
}
}
if false {
break;
}
}
// wait a bit so we're not hitting the server really fast if the fail happens
// early on
//
// TODO: some kind of smart exponential backoff that considers some time
// threshold to be a failing case?
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
}
}

View file

@ -3,7 +3,7 @@ use std::thread;
use anyhow::Result; use anyhow::Result;
use fern::colors::{Color, ColoredLevelConfig}; use fern::colors::{Color, ColoredLevelConfig};
use futures::future::{FutureExt, TryFutureExt}; use futures::future::{TryFutureExt};
use panorama::{ use panorama::{
config::spawn_config_watcher_system, config::spawn_config_watcher_system,
mail::{self, MailEvent}, mail::{self, MailEvent},
@ -54,13 +54,13 @@ async fn run(opt: Opt) -> Result<()> {
let (exit_tx, mut exit_rx) = mpsc::channel::<()>(1); let (exit_tx, mut exit_rx) = mpsc::channel::<()>(1);
// send messages from the UI thread to the mail thread // send messages from the UI thread to the mail thread
let (ui2mail_tx, ui2mail_rx) = mpsc::unbounded_channel(); let (_ui2mail_tx, ui2mail_rx) = mpsc::unbounded_channel();
// send messages from the mail thread to the UI thread // send messages from the mail thread to the UI thread
let (mail2ui_tx, mail2ui_rx) = mpsc::unbounded_channel(); let (mail2ui_tx, mail2ui_rx) = mpsc::unbounded_channel();
// send messages from the UI thread to the vm thread // send messages from the UI thread to the vm thread
let (ui2vm_tx, ui2vm_rx) = mpsc::unbounded_channel(); let (ui2vm_tx, _ui2vm_rx) = mpsc::unbounded_channel();
tokio::spawn(async move { tokio::spawn(async move {
let config_update = config_update.clone(); let config_update = config_update.clone();
@ -86,7 +86,7 @@ async fn run(opt: Opt) -> Result<()> {
fn run_ui( fn run_ui(
exit_tx: mpsc::Sender<()>, exit_tx: mpsc::Sender<()>,
mail2ui_rx: mpsc::UnboundedReceiver<MailEvent>, mail2ui_rx: mpsc::UnboundedReceiver<MailEvent>,
ui2vm_tx: mpsc::UnboundedSender<()>, _ui2vm_tx: mpsc::UnboundedSender<()>,
) { ) {
let stdout = std::io::stdout(); let stdout = std::io::stdout();

View file

@ -4,9 +4,11 @@ use std::sync::{
Arc, Arc,
}; };
use anyhow::Result;
use chrono::{DateTime, Datelike, Duration, Local}; use chrono::{DateTime, Datelike, Duration, Local};
use chrono_humanize::HumanTime; use chrono_humanize::HumanTime;
use panorama_imap::response::Envelope; use panorama_imap::response::Envelope;
use crossterm::event::{KeyEvent, KeyCode};
use tui::{ use tui::{
buffer::Buffer, buffer::Buffer,
layout::{Constraint, Direction, Layout, Rect}, layout::{Constraint, Direction, Layout, Rect},
@ -17,7 +19,7 @@ use tui::{
use crate::mail::EmailMetadata; use crate::mail::EmailMetadata;
use super::{FrameType, HandlesInput, Window, UI}; use super::{FrameType, HandlesInput, TermType, InputResult, Window, UI};
#[derive(Default, Debug)] #[derive(Default, Debug)]
pub struct MailView { pub struct MailView {
@ -29,7 +31,23 @@ pub struct MailView {
pub change: Arc<AtomicI8>, pub change: Arc<AtomicI8>,
} }
impl HandlesInput for MailView {} impl HandlesInput for MailView {
fn handle_key(&mut self, term: TermType, evt: KeyEvent) -> Result<InputResult> {
let KeyEvent { code, .. } = evt;
match code {
// KeyCode::Char('q') => self.0.store(true, Ordering::Relaxed),
// KeyCode::Char('j') => self.1.store(1, Ordering::Relaxed),
// KeyCode::Char('k') => self.1.store(-1, Ordering::Relaxed),
KeyCode::Char(':') => {
// let colon_prompt = Box::new(ColonPrompt::init(term));
// return Ok(InputResult::Push(colon_prompt));
}
_ => {}
}
Ok(InputResult::Ok)
}
}
impl Window for MailView { impl Window for MailView {
fn name(&self) -> String { fn name(&self) -> String {
@ -87,7 +105,6 @@ impl Window for MailView {
row row
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let table = Table::new(rows) let table = Table::new(rows)
.style(Style::default().fg(Color::White)) .style(Style::default().fg(Color::White))
.widths(&[ .widths(&[
@ -108,6 +125,11 @@ impl Window for MailView {
} }
} }
/// Turn a timestamp into a format that a human might read when viewing it in a table.
///
/// This means, for dates within the past 24 hours, report it in a relative format.
///
/// For dates sent this year, omit the year entirely.
fn humanize_timestamp(date: DateTime<Local>) -> String { fn humanize_timestamp(date: DateTime<Local>) -> String {
let now = Local::now(); let now = Local::now();
let diff = now - date; let diff = now - date;