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
|
//! 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:
|
||||||
//!
|
//!
|
||||||
|
|
5
notes.md
5
notes.md
|
@ -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
|
|
||||||
|
|
|
@ -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
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
|
//! 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in a new issue