hellosu
This commit is contained in:
parent
7ec5c510a3
commit
fd1cb9f927
7 changed files with 185 additions and 139 deletions
|
@ -1,10 +1,10 @@
|
|||
//! 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
|
||||
//! general-purpose IMAP usage. See the [client][crate::client] module for more information on how
|
||||
//! to get started with a client quickly.
|
||||
//! general-purpose IMAP client development. See the [client][crate::client] module for more
|
||||
//! information on how to get started with a client quickly.
|
||||
//!
|
||||
//! RFCs:
|
||||
//!
|
||||
|
|
5
notes.md
5
notes.md
|
@ -36,7 +36,4 @@ imap routine
|
|||
list of shit to do
|
||||
---
|
||||
|
||||
- [x] starttls impl
|
||||
- [ ] auth impl
|
||||
- [ ] auth plain impl
|
||||
- [ ] fetch impl
|
||||
- profile the client to see what needs to be improved?
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
//! One of the primary goals of panorama is to be able to always hot-reload configuration files.
|
||||
|
||||
use std::fs::{self, File};
|
||||
use std::collections::HashMap;
|
||||
use std::io::Read;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
|
@ -26,7 +27,7 @@ pub struct Config {
|
|||
|
||||
/// Mail accounts
|
||||
#[serde(rename = "mail")]
|
||||
pub mail_accounts: Vec<MailAccountConfig>,
|
||||
pub mail_accounts: HashMap<String, MailAccountConfig>,
|
||||
}
|
||||
|
||||
/// Configuration for a single mail account
|
||||
|
|
147
src/mail/client.rs
Normal file
147
src/mail/client.rs
Normal 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;
|
||||
}
|
||||
}
|
127
src/mail/mod.rs
127
src/mail/mod.rs
|
@ -1,5 +1,6 @@
|
|||
//! Mail
|
||||
|
||||
mod client;
|
||||
mod metadata;
|
||||
|
||||
use anyhow::Result;
|
||||
|
@ -82,14 +83,14 @@ pub async fn run_mail(
|
|||
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 handle = tokio::spawn(async move {
|
||||
// debug!("opening imap connection for {:?}", acct);
|
||||
|
||||
// this loop is to make sure accounts are restarted on error
|
||||
loop {
|
||||
match imap_main(acct.clone(), mail2ui_tx.clone()).await {
|
||||
match client::imap_main(acct.clone(), mail2ui_tx.clone()).await {
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
error!("IMAP Error: {}", err);
|
||||
|
@ -112,125 +113,3 @@ pub async fn run_mail(
|
|||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ use std::thread;
|
|||
|
||||
use anyhow::Result;
|
||||
use fern::colors::{Color, ColoredLevelConfig};
|
||||
use futures::future::{FutureExt, TryFutureExt};
|
||||
use futures::future::{TryFutureExt};
|
||||
use panorama::{
|
||||
config::spawn_config_watcher_system,
|
||||
mail::{self, MailEvent},
|
||||
|
@ -54,13 +54,13 @@ async fn run(opt: Opt) -> Result<()> {
|
|||
let (exit_tx, mut exit_rx) = mpsc::channel::<()>(1);
|
||||
|
||||
// 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
|
||||
let (mail2ui_tx, mail2ui_rx) = mpsc::unbounded_channel();
|
||||
|
||||
// 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 {
|
||||
let config_update = config_update.clone();
|
||||
|
@ -86,7 +86,7 @@ async fn run(opt: Opt) -> Result<()> {
|
|||
fn run_ui(
|
||||
exit_tx: mpsc::Sender<()>,
|
||||
mail2ui_rx: mpsc::UnboundedReceiver<MailEvent>,
|
||||
ui2vm_tx: mpsc::UnboundedSender<()>,
|
||||
_ui2vm_tx: mpsc::UnboundedSender<()>,
|
||||
) {
|
||||
let stdout = std::io::stdout();
|
||||
|
||||
|
|
|
@ -4,9 +4,11 @@ use std::sync::{
|
|||
Arc,
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
use chrono::{DateTime, Datelike, Duration, Local};
|
||||
use chrono_humanize::HumanTime;
|
||||
use panorama_imap::response::Envelope;
|
||||
use crossterm::event::{KeyEvent, KeyCode};
|
||||
use tui::{
|
||||
buffer::Buffer,
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
|
@ -17,7 +19,7 @@ use tui::{
|
|||
|
||||
use crate::mail::EmailMetadata;
|
||||
|
||||
use super::{FrameType, HandlesInput, Window, UI};
|
||||
use super::{FrameType, HandlesInput, TermType, InputResult, Window, UI};
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct MailView {
|
||||
|
@ -29,7 +31,23 @@ pub struct MailView {
|
|||
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 {
|
||||
fn name(&self) -> String {
|
||||
|
@ -87,7 +105,6 @@ impl Window for MailView {
|
|||
row
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let table = Table::new(rows)
|
||||
.style(Style::default().fg(Color::White))
|
||||
.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 {
|
||||
let now = Local::now();
|
||||
let diff = now - date;
|
||||
|
|
Loading…
Reference in a new issue