Begin service work...
This commit is contained in:
parent
7c36d9ef7d
commit
0899cf3276
7 changed files with 202 additions and 188 deletions
|
@ -1,3 +1,4 @@
|
||||||
[*.rs]
|
[*.rs]
|
||||||
indent_style = space
|
indent_style = space
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
|
max_line_length = 80
|
||||||
|
|
10
Justfile
10
Justfile
|
@ -1,11 +1,17 @@
|
||||||
|
set dotenv-load := false
|
||||||
|
|
||||||
ct:
|
ct:
|
||||||
tokei
|
tokei
|
||||||
|
|
||||||
fmt:
|
fmt:
|
||||||
cargo +nightly fmt --all
|
cargo +nightly fmt --all
|
||||||
|
|
||||||
doc:
|
doc shouldOpen="":
|
||||||
cargo doc --workspace --no-deps
|
#!/bin/bash
|
||||||
|
if [[ -n "{{shouldOpen}}" ]]; then
|
||||||
|
OPEN=--open
|
||||||
|
fi
|
||||||
|
cargo doc --workspace --document-private-items --no-deps $OPEN
|
||||||
|
|
||||||
test:
|
test:
|
||||||
cargo test --all
|
cargo test --all
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
//! Everything related to config handling within the panorama daemon.
|
||||||
|
|
||||||
#[cfg(feature = "config-watch")]
|
#[cfg(feature = "config-watch")]
|
||||||
mod watcher;
|
mod watcher;
|
||||||
|
|
||||||
|
|
15
daemon/src/exit.rs
Normal file
15
daemon/src/exit.rs
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
use tokio::sync::oneshot;
|
||||||
|
|
||||||
|
/// Receiver whose sole purpose is to receive an "exit" notification.
|
||||||
|
///
|
||||||
|
/// This exit "pattern", along with message passing over channels, allows
|
||||||
|
/// panorama to introspect on running loops. For example, a server listen loop
|
||||||
|
/// would select over the `ExitListener`, and break the loop once it receives
|
||||||
|
/// something on that channel. Then it would perform any clean up it needs and
|
||||||
|
/// exit, returning any resources that can be reused.
|
||||||
|
///
|
||||||
|
/// This is useful, for example, during TLS upgrade with STARTTLS, where the
|
||||||
|
/// connection needs to stay open. We can send the existing loop an "exit"
|
||||||
|
/// notification, then it returns the open channel so we can perform TLS
|
||||||
|
/// negotiation and create a new tunneled connection.
|
||||||
|
pub type ExitListener = oneshot::Receiver<()>;
|
|
@ -1,3 +1,7 @@
|
||||||
|
//! Library functions used by the panorama daemon.
|
||||||
|
|
||||||
|
// #![deny(missing_docs)]
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate serde;
|
extern crate serde;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
|
@ -12,6 +16,9 @@ extern crate derivative;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod mail;
|
pub mod mail;
|
||||||
|
|
||||||
|
mod exit;
|
||||||
|
mod service;
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
|
@ -24,10 +31,9 @@ use tokio::{
|
||||||
use xdg::BaseDirectories;
|
use xdg::BaseDirectories;
|
||||||
|
|
||||||
use crate::config::{Config, MailAccountConfig};
|
use crate::config::{Config, MailAccountConfig};
|
||||||
|
pub use crate::exit::ExitListener;
|
||||||
use crate::mail::{mail_main, MailStore};
|
use crate::mail::{mail_main, MailStore};
|
||||||
|
|
||||||
type ExitListener = oneshot::Receiver<()>;
|
|
||||||
|
|
||||||
/// The panorama daemon runs in the background and communicates with other
|
/// The panorama daemon runs in the background and communicates with other
|
||||||
/// panorama components over Unix sockets.
|
/// panorama components over Unix sockets.
|
||||||
#[derive(Debug, Parser)]
|
#[derive(Debug, Parser)]
|
||||||
|
@ -41,6 +47,11 @@ struct Options {
|
||||||
verbose: usize,
|
verbose: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Primary entrypoint; this is the function that is called by main with no
|
||||||
|
/// arguments.
|
||||||
|
///
|
||||||
|
/// The purpose of this function is to parse command line arguments
|
||||||
|
/// and set up config watching, then call [`run_with_config`] with the config.
|
||||||
pub async fn run() -> Result<()> {
|
pub async fn run() -> Result<()> {
|
||||||
let opt = Options::parse();
|
let opt = Options::parse();
|
||||||
|
|
||||||
|
@ -118,8 +129,8 @@ async fn run_with_config(config: Config, exit: ExitListener) -> Result<()> {
|
||||||
|
|
||||||
/// The main loop for a single mail account.
|
/// The main loop for a single mail account.
|
||||||
///
|
///
|
||||||
/// This loop is restarted each time the config watcher gets a new config, at
|
/// The exit listener may allow the loop to be interrupted by the outside. When
|
||||||
/// which point `exit` is sent a single value telling us to break the loop.
|
/// anything is received on the exit listener, the loop exits gracefully.
|
||||||
async fn run_single_mail_account(
|
async fn run_single_mail_account(
|
||||||
account_name: String,
|
account_name: String,
|
||||||
account: MailAccountConfig,
|
account: MailAccountConfig,
|
||||||
|
|
|
@ -6,7 +6,7 @@ mod store;
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use futures::stream::StreamExt;
|
use futures::stream::StreamExt;
|
||||||
use panorama_imap::{
|
use panorama_imap::{
|
||||||
client::{auth::Login, ConfigBuilder},
|
client::{ClientAuthenticated, ConfigBuilder},
|
||||||
pool::{ImapPool, PoolConfig},
|
pool::{ImapPool, PoolConfig},
|
||||||
proto::{
|
proto::{
|
||||||
command::FetchItems,
|
command::FetchItems,
|
||||||
|
@ -16,14 +16,18 @@ use panorama_imap::{
|
||||||
use sqlx::migrate::Migrator;
|
use sqlx::migrate::Migrator;
|
||||||
use tokio::sync::mpsc::UnboundedSender;
|
use tokio::sync::mpsc::UnboundedSender;
|
||||||
|
|
||||||
use crate::config::{ImapAuth, MailAccountConfig, TlsMethod};
|
use crate::config::{MailAccountConfig, TlsMethod};
|
||||||
|
|
||||||
pub use self::event::MailEvent;
|
pub use self::event::MailEvent;
|
||||||
pub use self::store::MailStore;
|
pub use self::store::MailStore;
|
||||||
|
|
||||||
static MIGRATOR: Migrator = sqlx::migrate!();
|
static MIGRATOR: Migrator = sqlx::migrate!();
|
||||||
|
|
||||||
/// The main function for the IMAP syncing thread
|
/// The main function for the IMAP thread.
|
||||||
|
///
|
||||||
|
/// This spawns two processes:
|
||||||
|
/// - A background synchronization thread
|
||||||
|
/// - An event listener / handler
|
||||||
pub async fn mail_main(
|
pub async fn mail_main(
|
||||||
acct_name: impl AsRef<str>,
|
acct_name: impl AsRef<str>,
|
||||||
acct: MailAccountConfig,
|
acct: MailAccountConfig,
|
||||||
|
@ -50,51 +54,21 @@ pub async fn mail_main(
|
||||||
// grab one connection from that pool and start running a background
|
// grab one connection from that pool and start running a background
|
||||||
// synchronization thread
|
// synchronization thread
|
||||||
let sync_conn = pool.acquire().await?;
|
let sync_conn = pool.acquire().await?;
|
||||||
|
let sync_loop = run_sync_loop(sync_conn);
|
||||||
|
|
||||||
// let the rest of the pool respond to requests coming from the channel
|
// let the rest of the pool respond to requests coming from the channel
|
||||||
// TODO:
|
// TODO:
|
||||||
|
|
||||||
return Ok(());
|
sync_loop.await?;
|
||||||
|
|
||||||
// loop ensures that the connection is retried after it dies
|
Ok(())
|
||||||
loop {
|
}
|
||||||
let client = ConfigBuilder::default()
|
|
||||||
.hostname(acct.imap.server.clone())
|
|
||||||
.port(acct.imap.port)
|
|
||||||
.tls(matches!(acct.imap.tls, TlsMethod::On))
|
|
||||||
.open()
|
|
||||||
.await
|
|
||||||
.map_err(|err| anyhow!("err: {}", err))?;
|
|
||||||
debug!("Connected to {}:{}.", &acct.imap.server, acct.imap.port);
|
|
||||||
|
|
||||||
debug!("TLS Upgrade option: {:?}", acct.imap.tls);
|
async fn run_sync_loop(mut conn: ClientAuthenticated) -> Result<()> {
|
||||||
let unauth = if matches!(acct.imap.tls, TlsMethod::Starttls) {
|
// get the list of folders first
|
||||||
debug!("attempting to upgrade");
|
debug!(target: "run_sync_loop", "Retrieving folder list...");
|
||||||
let client = client
|
let folder_list = conn.list().await?;
|
||||||
.upgrade()
|
|
||||||
.await
|
|
||||||
.context("could not upgrade connection")?;
|
|
||||||
debug!("upgrade successful");
|
|
||||||
client
|
|
||||||
} else {
|
|
||||||
warn!("Continuing with unencrypted connection!");
|
|
||||||
client
|
|
||||||
};
|
|
||||||
|
|
||||||
debug!("preparing to auth");
|
|
||||||
// check if the authentication method is supported
|
|
||||||
let mut authed = match &acct.imap.auth {
|
|
||||||
ImapAuth::Plain { username, password } => {
|
|
||||||
let login = Login {
|
|
||||||
username: username.clone(),
|
|
||||||
password: password.clone(),
|
|
||||||
};
|
|
||||||
unauth.auth(login).await?
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
debug!("authentication successful!");
|
|
||||||
let folder_list = authed.list().await?;
|
|
||||||
// let _ = mail2ui_tx.send(MailEvent::FolderList(
|
// let _ = mail2ui_tx.send(MailEvent::FolderList(
|
||||||
// acct_name.clone(),
|
// acct_name.clone(),
|
||||||
// folder_list.clone(),
|
// folder_list.clone(),
|
||||||
|
@ -103,7 +77,7 @@ pub async fn mail_main(
|
||||||
debug!("mailbox list: {:?}", folder_list);
|
debug!("mailbox list: {:?}", folder_list);
|
||||||
for folder in folder_list.iter() {
|
for folder in folder_list.iter() {
|
||||||
debug!("folder: {:?}", folder);
|
debug!("folder: {:?}", folder);
|
||||||
let select = authed.select("INBOX").await?;
|
let select = conn.select("INBOX").await?;
|
||||||
debug!("select response: {:?}", select);
|
debug!("select response: {:?}", select);
|
||||||
if let (Some(_exists), Some(_uidvalidity)) =
|
if let (Some(_exists), Some(_uidvalidity)) =
|
||||||
(select.exists, select.uid_validity)
|
(select.exists, select.uid_validity)
|
||||||
|
@ -124,7 +98,7 @@ pub async fn mail_main(
|
||||||
// <Vec<_>>().await?;
|
// <Vec<_>>().await?;
|
||||||
if !new_uids.is_empty() {
|
if !new_uids.is_empty() {
|
||||||
debug!("fetching uids {:?}", new_uids);
|
debug!("fetching uids {:?}", new_uids);
|
||||||
let _fetched = authed
|
let _fetched = conn
|
||||||
.uid_fetch(&new_uids, &[], FetchItems::Envelope)
|
.uid_fetch(&new_uids, &[], FetchItems::Envelope)
|
||||||
.await
|
.await
|
||||||
.context("error fetching uids")?;
|
.context("error fetching uids")?;
|
||||||
|
@ -147,18 +121,18 @@ pub async fn mail_main(
|
||||||
// mailbox later?
|
// mailbox later?
|
||||||
|
|
||||||
debug!("selecting the INBOX mailbox");
|
debug!("selecting the INBOX mailbox");
|
||||||
let select = authed.select("INBOX").await?;
|
let select = conn.select("INBOX").await?;
|
||||||
debug!("select result: {:?}", select);
|
debug!("select result: {:?}", select);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let message_uids = authed.uid_search().await?;
|
let message_uids = conn.uid_search().await?;
|
||||||
let message_uids = message_uids.into_iter().take(30).collect::<Vec<_>>();
|
let message_uids = message_uids.into_iter().take(30).collect::<Vec<_>>();
|
||||||
// let _ = mail2ui_tx.send(MailEvent::MessageUids(
|
// let _ = mail2ui_tx.send(MailEvent::MessageUids(
|
||||||
// acct_name.clone(),
|
// acct_name.clone(),
|
||||||
// message_uids.clone(),
|
// message_uids.clone(),
|
||||||
// ));
|
// ));
|
||||||
// TODO: make this happen concurrently with the main loop?
|
// TODO: make this happen concurrently with the main loop?
|
||||||
let mut message_list = authed
|
let mut message_list = conn
|
||||||
.uid_fetch(&message_uids, &[], FetchItems::All)
|
.uid_fetch(&message_uids, &[], FetchItems::All)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -172,7 +146,7 @@ pub async fn mail_main(
|
||||||
let supports_idle = true; // authed.has_capability("IDLE").await?;
|
let supports_idle = true; // authed.has_capability("IDLE").await?;
|
||||||
|
|
||||||
if supports_idle {
|
if supports_idle {
|
||||||
let mut idle_stream = authed.idle().await?;
|
let mut idle_stream = conn.idle().await?;
|
||||||
loop {
|
loop {
|
||||||
let evt = match idle_stream.next().await {
|
let evt = match idle_stream.next().await {
|
||||||
Some(v) => v,
|
Some(v) => v,
|
||||||
|
@ -191,7 +165,7 @@ pub async fn mail_main(
|
||||||
// .icon("firefox")
|
// .icon("firefox")
|
||||||
// .timeout(Timeout::Milliseconds(6000))
|
// .timeout(Timeout::Milliseconds(6000))
|
||||||
// .show()?;
|
// .show()?;
|
||||||
let message_uids = authed.uid_search().await?;
|
let message_uids = conn.uid_search().await?;
|
||||||
let message_uids =
|
let message_uids =
|
||||||
message_uids.into_iter().take(20).collect::<Vec<_>>();
|
message_uids.into_iter().take(20).collect::<Vec<_>>();
|
||||||
// let _ = mail2ui_tx.send(MailEvent::MessageUids(
|
// let _ = mail2ui_tx.send(MailEvent::MessageUids(
|
||||||
|
@ -200,7 +174,7 @@ pub async fn mail_main(
|
||||||
// ));
|
// ));
|
||||||
// TODO: make this happen concurrently with the main
|
// TODO: make this happen concurrently with the main
|
||||||
// loop?
|
// loop?
|
||||||
let mut message_list = authed
|
let mut message_list = conn
|
||||||
.uid_fetch(&message_uids, &[], FetchItems::All)
|
.uid_fetch(&message_uids, &[], FetchItems::All)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -211,7 +185,7 @@ pub async fn mail_main(
|
||||||
// mail2ui_tx.send(evt);
|
// mail2ui_tx.send(evt);
|
||||||
}
|
}
|
||||||
|
|
||||||
idle_stream = authed.idle().await?;
|
idle_stream = conn.idle().await?;
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
@ -233,5 +207,6 @@ pub async fn mail_main(
|
||||||
// TODO: some kind of smart exponential backoff that considers some time
|
// TODO: some kind of smart exponential backoff that considers some time
|
||||||
// threshold to be a failing case?
|
// threshold to be a failing case?
|
||||||
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
|
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
|
||||||
}
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
4
daemon/src/service.rs
Normal file
4
daemon/src/service.rs
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
pub trait Service<Request> {
|
||||||
|
type Response;
|
||||||
|
type Error;
|
||||||
|
}
|
Loading…
Reference in a new issue