Change to 2 spaces

This commit is contained in:
Michael Zhang 2021-10-28 22:44:16 -05:00
parent 1ab69aadb9
commit ff18e98eff
24 changed files with 2051 additions and 2088 deletions

View file

@ -14,50 +14,50 @@ pub use self::watcher::{spawn_config_watcher_system, ConfigWatcher};
/// Configuration /// Configuration
#[derive(Default, Serialize, Deserialize, Clone, Debug)] #[derive(Default, Serialize, Deserialize, Clone, Debug)]
pub struct Config { pub struct Config {
/// Version of the config to use /// Version of the config to use
/// (potentially for migration later?) /// (potentially for migration later?)
pub version: String, pub version: String,
/// Directory to store panorama-related data in /// Directory to store panorama-related data in
pub data_dir: PathBuf, pub data_dir: PathBuf,
/// Mail accounts /// Mail accounts
#[serde(rename = "mail")] #[serde(rename = "mail")]
pub mail_accounts: HashMap<String, MailAccountConfig>, pub mail_accounts: HashMap<String, MailAccountConfig>,
} }
impl Config { impl Config {
pub async fn from_file(path: impl AsRef<Path>) -> Result<Self> { pub async fn from_file(path: impl AsRef<Path>) -> Result<Self> {
let mut file = File::open(path.as_ref())?; let mut file = File::open(path.as_ref())?;
let mut contents = Vec::new(); let mut contents = Vec::new();
file.read_to_end(&mut contents)?; file.read_to_end(&mut contents)?;
let config = toml::from_slice(&contents)?; let config = toml::from_slice(&contents)?;
Ok(config) Ok(config)
} }
} }
/// Configuration for a single mail account /// Configuration for a single mail account
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug)]
pub struct MailAccountConfig { pub struct MailAccountConfig {
/// Imap /// Imap
pub imap: ImapConfig, pub imap: ImapConfig,
} }
/// Configuring an IMAP server /// Configuring an IMAP server
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ImapConfig { pub struct ImapConfig {
/// Host of the IMAP server (needs to be hostname for TLS) /// Host of the IMAP server (needs to be hostname for TLS)
pub server: String, pub server: String,
/// Port of the IMAP server /// Port of the IMAP server
pub port: u16, pub port: u16,
/// TLS /// TLS
pub tls: TlsMethod, pub tls: TlsMethod,
/// Auth /// Auth
#[serde(flatten)] #[serde(flatten)]
pub auth: ImapAuth, pub auth: ImapAuth,
} }
/// Method of authentication for the IMAP server /// Method of authentication for the IMAP server
@ -65,28 +65,28 @@ pub struct ImapConfig {
#[derivative(Debug)] #[derivative(Debug)]
#[serde(tag = "auth")] #[serde(tag = "auth")]
pub enum ImapAuth { pub enum ImapAuth {
/// Use plain username/password authentication /// Use plain username/password authentication
#[serde(rename = "plain")] #[serde(rename = "plain")]
Plain { Plain {
username: String, username: String,
#[derivative(Debug = "ignore")] #[derivative(Debug = "ignore")]
password: String, password: String,
}, },
} }
/// Describes when to perform the TLS handshake /// Describes when to perform the TLS handshake
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug)]
pub enum TlsMethod { pub enum TlsMethod {
/// Perform TLS handshake immediately upon connection /// Perform TLS handshake immediately upon connection
#[serde(rename = "on")] #[serde(rename = "on")]
On, On,
/// Perform TLS handshake after issuing the STARTTLS command /// Perform TLS handshake after issuing the STARTTLS command
#[serde(rename = "starttls")] #[serde(rename = "starttls")]
Starttls, Starttls,
/// Don't perform TLS handshake at all (unsecured) /// Don't perform TLS handshake at all (unsecured)
#[serde(rename = "off")] #[serde(rename = "off")]
Off, Off,
} }

View file

@ -5,8 +5,8 @@ use std::sync::mpsc as stdmpsc;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use futures::future::TryFutureExt; use futures::future::TryFutureExt;
use notify::{ use notify::{
recommended_watcher, Event as NotifyEvent, RecommendedWatcher, recommended_watcher, Event as NotifyEvent, RecommendedWatcher, RecursiveMode,
RecursiveMode, Watcher, Watcher,
}; };
use tokio::{sync::watch, task::JoinHandle}; use tokio::{sync::watch, task::JoinHandle};
use xdg::BaseDirectories; use xdg::BaseDirectories;
@ -20,91 +20,86 @@ pub type ConfigWatcher = watch::Receiver<Config>;
/// config update events. /// config update events.
pub fn spawn_config_watcher_system() -> Result<(JoinHandle<()>, ConfigWatcher)> pub fn spawn_config_watcher_system() -> Result<(JoinHandle<()>, ConfigWatcher)>
{ {
let (tx, rx) = stdmpsc::channel(); let (tx, rx) = stdmpsc::channel();
let mut dir_watcher = recommended_watcher(move |res| match res { let mut dir_watcher = recommended_watcher(move |res| match res {
Ok(event) => { Ok(event) => {
tx.send(event).unwrap(); tx.send(event).unwrap();
}
Err(_) => {}
})?;
let xdg = BaseDirectories::new()?;
let config_home = xdg.get_config_home().join("panorama");
if !config_home.exists() {
fs::create_dir_all(&config_home)?;
} }
Err(_) => {}
})?;
dir_watcher let xdg = BaseDirectories::new()?;
.watch(&config_home, RecursiveMode::Recursive) let config_home = xdg.get_config_home().join("panorama");
.context("adding watch for config home")?; if !config_home.exists() {
fs::create_dir_all(&config_home)?;
}
debug!("watching {:?}", config_home); dir_watcher
let (config_tx, config_update) = watch::channel(Config::default()); .watch(&config_home, RecursiveMode::Recursive)
let handle = tokio::spawn( .context("adding watch for config home")?;
start_notify_stream(dir_watcher, rx, config_home, config_tx)
.unwrap_or_else(|_err| todo!()), debug!("watching {:?}", config_home);
); let (config_tx, config_update) = watch::channel(Config::default());
Ok((handle, config_update)) let handle = tokio::spawn(
start_notify_stream(dir_watcher, rx, config_home, config_tx)
.unwrap_or_else(|_err| todo!()),
);
Ok((handle, config_update))
} }
async fn start_notify_stream( async fn start_notify_stream(
_watcher: RecommendedWatcher, _watcher: RecommendedWatcher,
rx: stdmpsc::Receiver<NotifyEvent>, rx: stdmpsc::Receiver<NotifyEvent>,
config_home: impl AsRef<Path>, config_home: impl AsRef<Path>,
config_tx: watch::Sender<Config>, config_tx: watch::Sender<Config>,
) -> Result<()> { ) -> Result<()> {
let config_home = config_home.as_ref().to_path_buf(); let config_home = config_home.as_ref().to_path_buf();
let config_path = config_home.join("panorama.toml"); let config_path = config_home.join("panorama.toml");
// first shot // first shot
{ {
let config = Config::from_file(&config_path).await?; let config = Config::from_file(&config_path).await?;
config_tx.send(config)?; config_tx.send(config)?;
} }
debug!("listening for inotify events"); debug!("listening for inotify events");
loop { loop {
use notify::EventKind; use notify::EventKind;
let event = rx.recv()?; let event = rx.recv()?;
debug!("notify event: {:?}", event); debug!("notify event: {:?}", event);
match event.kind { match event.kind {
EventKind::Create(_) | EventKind::Modify(_) => { EventKind::Create(_) | EventKind::Modify(_) => {
let path_expect = config_home let path_expect = config_home
.clone() .clone()
.join("panorama.toml") .join("panorama.toml")
.canonicalize() .canonicalize()
.context("osu")?; .context("osu")?;
if !path_expect.exists() { if !path_expect.exists() {
debug!("path {:?} doesn't exist", path_expect); debug!("path {:?} doesn't exist", path_expect);
continue; continue;
}
match event.paths.iter().find(|p| *p == &path_expect) {
Some(path) => path.to_path_buf(),
None => continue,
};
// TODO: any better way to do this?
let config_path_c =
config_path.canonicalize().context("cfg_path")?;
if config_path_c != path_expect {
debug!(
"did not match {:?} {:?}",
config_path_c, path_expect
);
continue;
}
debug!("reading config from {:?}", path_expect);
let config =
Config::from_file(path_expect).await.context("read")?;
// debug!("sending config {:?}", config);
config_tx.send(config)?;
}
_ => {}
} }
match event.paths.iter().find(|p| *p == &path_expect) {
Some(path) => path.to_path_buf(),
None => continue,
};
// TODO: any better way to do this?
let config_path_c = config_path.canonicalize().context("cfg_path")?;
if config_path_c != path_expect {
debug!("did not match {:?} {:?}", config_path_c, path_expect);
continue;
}
debug!("reading config from {:?}", path_expect);
let config = Config::from_file(path_expect).await.context("read")?;
// debug!("sending config {:?}", config);
config_tx.send(config)?;
}
_ => {}
} }
}
} }

View file

@ -16,8 +16,8 @@ use anyhow::{Context, Result};
use clap::Parser; use clap::Parser;
use futures::future::FutureExt; use futures::future::FutureExt;
use tokio::{ use tokio::{
fs::{self, OpenOptions}, fs::{self, OpenOptions},
sync::{mpsc, oneshot}, sync::{mpsc, oneshot},
}; };
use xdg::BaseDirectories; use xdg::BaseDirectories;
@ -30,89 +30,88 @@ type ExitListener = oneshot::Receiver<()>;
/// panorama components over Unix sockets. /// panorama components over Unix sockets.
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
struct Options { struct Options {
// /// Config file path (defaults to XDG) // /// Config file path (defaults to XDG)
// #[clap(long = "config", short = 'c')] // #[clap(long = "config", short = 'c')]
// config_file: Option<PathBuf>, // config_file: Option<PathBuf>,
/// Verbose mode (-v, -vv, -vvv, etc) /// Verbose mode (-v, -vv, -vvv, etc)
#[clap(short = 'v', long = "verbose", parse(from_occurrences))] #[clap(short = 'v', long = "verbose", parse(from_occurrences))]
verbose: usize, verbose: usize,
} }
#[tokio::main] #[tokio::main]
pub async fn run() -> Result<()> { pub async fn run() -> Result<()> {
let opt = Options::parse(); let opt = Options::parse();
stderrlog::new() stderrlog::new()
.module(module_path!()) .module(module_path!())
.module("panorama_daemon") .module("panorama_daemon")
.module("panorama_imap") .module("panorama_imap")
.verbosity(opt.verbose) .verbosity(opt.verbose)
.init() .init()
.unwrap(); .unwrap();
// if we're using a config-watcher, then start the watcher system // if we're using a config-watcher, then start the watcher system
#[cfg(feature = "config-watch")] #[cfg(feature = "config-watch")]
{ {
let (_, mut config_watcher) = config::spawn_config_watcher_system()?; let (_, mut config_watcher) = config::spawn_config_watcher_system()?;
loop { loop {
let (exit_tx, exit_rx) = oneshot::channel(); let (exit_tx, exit_rx) = oneshot::channel();
let new_config = config_watcher.borrow().clone(); let new_config = config_watcher.borrow().clone();
tokio::spawn(run_with_config(new_config, exit_rx)); tokio::spawn(run_with_config(new_config, exit_rx));
// wait till the config has changed, then tell the current thread to // wait till the config has changed, then tell the current thread to
// stop // stop
config_watcher.changed().await?; config_watcher.changed().await?;
let _ = exit_tx.send(()); let _ = exit_tx.send(());
} }
}
// TODO: handle SIGHUP on Unix? pretty common for systemd to send this when
// reloading config files
// if not, just read the config once and run the daemon
#[cfg(not(feature = "config-watch"))]
{
let xdg = BaseDirectories::new()?;
let config_home = xdg.get_config_home().join("panorama");
if !config_home.exists() {
fs::create_dir_all(&config_home).await?;
} }
// TODO: handle SIGHUP on Unix? pretty common for systemd to send this when let config_path = config_home.join("panorama.toml");
// reloading config files let config_path_c = config_path.canonicalize().context("cfg_path")?;
let config = Config::from_file(config_path_c).await.context("read")?;
// if not, just read the config once and run the daemon let (_, exit_rx) = oneshot::channel();
#[cfg(not(feature = "config-watch"))] run_with_config(config, exit_rx).await
{ }
let xdg = BaseDirectories::new()?;
let config_home = xdg.get_config_home().join("panorama");
if !config_home.exists() {
fs::create_dir_all(&config_home).await?;
}
let config_path = config_home.join("panorama.toml");
let config_path_c = config_path.canonicalize().context("cfg_path")?;
let config = Config::from_file(config_path_c).await.context("read")?;
let (_, exit_rx) = oneshot::channel();
run_with_config(config, exit_rx).await
}
} }
async fn run_with_config(config: Config, exit: ExitListener) -> Result<()> { async fn run_with_config(config: Config, exit: ExitListener) -> Result<()> {
debug!("New config: {:?}", config); debug!("New config: {:?}", config);
// keep track of which threads need to be stopped when this function is // keep track of which threads need to be stopped when this function is
// stopped // stopped
let mut notify_mail_threads = Vec::new(); let mut notify_mail_threads = Vec::new();
for (account_name, account) in config.mail_accounts { for (account_name, account) in config.mail_accounts {
let (exit_tx, exit_rx) = oneshot::channel(); let (exit_tx, exit_rx) = oneshot::channel();
tokio::spawn(async { tokio::spawn(async {
match run_single_mail_account(account_name, account, exit_rx).await match run_single_mail_account(account_name, account, exit_rx).await {
{ Ok(_) => {}
Ok(_) => {} Err(err) => panic!("failed: {:?}", err),
Err(err) => panic!("failed: {:?}", err), }
} });
}); notify_mail_threads.push(exit_tx);
notify_mail_threads.push(exit_tx); }
}
exit.await?; exit.await?;
for exit_tx in notify_mail_threads { for exit_tx in notify_mail_threads {
let _ = exit_tx.send(()); let _ = exit_tx.send(());
} }
Ok(()) Ok(())
} }
/// The main loop for a single mail account. /// The main loop for a single mail account.
@ -120,68 +119,68 @@ async fn run_with_config(config: Config, exit: ExitListener) -> Result<()> {
/// This loop is restarted each time the config watcher gets a new config, at /// This loop is restarted each time the config watcher gets a new config, at
/// which point `exit` is sent a single value telling us to break the loop. /// which point `exit` is sent a single value telling us to break the loop.
async fn run_single_mail_account( async fn run_single_mail_account(
account_name: String, account_name: String,
account: MailAccountConfig, account: MailAccountConfig,
exit: ExitListener, exit: ExitListener,
) -> Result<()> { ) -> Result<()> {
debug!("run_single_mail_account({}, {:?})", account_name, account); debug!("run_single_mail_account({}, {:?})", account_name, account);
let mut exit = exit.fuse(); let mut exit = exit.fuse();
let xdg = BaseDirectories::new()?; let xdg = BaseDirectories::new()?;
let data_home = xdg.get_data_home().join("panorama"); let data_home = xdg.get_data_home().join("panorama");
if !data_home.exists() { if !data_home.exists() {
fs::create_dir_all(&data_home) fs::create_dir_all(&data_home)
.await .await
.context("could not create config directory")?; .context("could not create config directory")?;
}
let db_path = data_home.join("db.sqlite3");
debug!("Opening database at path: {:?}", db_path);
if !db_path.exists() {
OpenOptions::new()
.create(true)
.write(true)
.open(&db_path)
.await
.context("could not touch db path")?;
}
let db_path = db_path
.canonicalize()
.context("could not canonicalize db path")?;
let store =
MailStore::open(format!("sqlite://{}", db_path.to_string_lossy()))
.await
.context("couldn't open mail store")?;
let (tx, mut rx) = mpsc::unbounded_channel();
let sync_fut = sync_main(&account_name, account, tx, store).fuse();
pin_mut!(sync_fut);
debug!("Mail account loop for {}.", account_name);
loop {
select! {
res = sync_fut => match res {
Ok(_) => {},
Err(err) => {
error!("sync_main died with: {:?}", err);
break;
}
},
evt_opt = rx.recv().fuse() => {
let evt = match evt_opt {
Some(evt) => evt,
None => break,
};
debug!("Event: {:?}", evt);
},
// we're being told to exit the loop
_ = exit => break,
} }
let db_path = data_home.join("db.sqlite3"); }
debug!("Opening database at path: {:?}", db_path);
if !db_path.exists() {
OpenOptions::new()
.create(true)
.write(true)
.open(&db_path)
.await
.context("could not touch db path")?;
}
let db_path = db_path
.canonicalize()
.context("could not canonicalize db path")?;
let store =
MailStore::open(format!("sqlite://{}", db_path.to_string_lossy()))
.await
.context("couldn't open mail store")?;
let (tx, mut rx) = mpsc::unbounded_channel(); debug!("disconnecting from account {}", account_name);
Ok(())
let sync_fut = sync_main(&account_name, account, tx, store).fuse();
pin_mut!(sync_fut);
debug!("Mail account loop for {}.", account_name);
loop {
select! {
res = sync_fut => match res {
Ok(_) => {},
Err(err) => {
error!("sync_main died with: {:?}", err);
break;
}
},
evt_opt = rx.recv().fuse() => {
let evt = match evt_opt {
Some(evt) => evt,
None => break,
};
debug!("Event: {:?}", evt);
},
// we're being told to exit the loop
_ = exit => break,
}
}
debug!("disconnecting from account {}", account_name);
Ok(())
} }

View file

@ -6,11 +6,11 @@ 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::{auth::Login, ConfigBuilder},
proto::{ proto::{
command::FetchItems, command::FetchItems,
response::{MailboxData, Response}, response::{MailboxData, Response},
}, },
}; };
use sqlx::migrate::Migrator; use sqlx::migrate::Migrator;
use tokio::sync::mpsc::UnboundedSender; use tokio::sync::mpsc::UnboundedSender;
@ -24,197 +24,190 @@ static MIGRATOR: Migrator = sqlx::migrate!();
/// The main function for the IMAP syncing thread /// The main function for the IMAP syncing thread
pub async fn sync_main( pub async fn sync_main(
acct_name: impl AsRef<str>, acct_name: impl AsRef<str>,
acct: MailAccountConfig, acct: MailAccountConfig,
_mail2ui_tx: UnboundedSender<MailEvent>, _mail2ui_tx: UnboundedSender<MailEvent>,
_mail_store: MailStore, _mail_store: MailStore,
) -> Result<()> { ) -> Result<()> {
let acct_name = acct_name.as_ref(); let acct_name = acct_name.as_ref();
debug!("Starting main synchronization procedure for {}", acct_name); debug!("Starting main synchronization procedure for {}", acct_name);
// loop ensures that the connection is retried after it dies // loop ensures that the connection is retried after it dies
loop { loop {
let client = ConfigBuilder::default() let client = ConfigBuilder::default()
.hostname(acct.imap.server.clone()) .hostname(acct.imap.server.clone())
.port(acct.imap.port) .port(acct.imap.port)
.tls(matches!(acct.imap.tls, TlsMethod::On)) .tls(matches!(acct.imap.tls, TlsMethod::On))
.open() .open()
.await
.map_err(|err| anyhow!("err: {}", err))?;
debug!("Connected to {}:{}.", &acct.imap.server, acct.imap.port);
debug!("TLS Upgrade option: {:?}", acct.imap.tls);
let unauth = if matches!(acct.imap.tls, TlsMethod::Starttls) {
debug!("attempting to upgrade");
let client = client
.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(
// acct_name.clone(),
// folder_list.clone(),
// ));
debug!("mailbox list: {:?}", folder_list);
for folder in folder_list.iter() {
debug!("folder: {:?}", folder);
let select = authed.select("INBOX").await?;
debug!("select response: {:?}", select);
if let (Some(_exists), Some(_uidvalidity)) =
(select.exists, select.uid_validity)
{
// figure out which uids don't exist locally yet
let new_uids = vec![];
// let new_uids =
// stream::iter(1..exists).map(Ok).try_filter_map(|uid| {
// todo!()
// // mail_store.try_identify_email(&acct_name, &folder,
// uid, uidvalidity, None) // //
// invert the option to only select uids that
// haven't been downloaded //
// .map_ok(move |o| o.map_or_else(move || Some(uid), |v| None))
// // .map_err(|err| err.context("error checking if
// the email is already downloaded
// [try_identify_email]")) }).try_collect::
// <Vec<_>>().await?;
if !new_uids.is_empty() {
debug!("fetching uids {:?}", new_uids);
let _fetched = authed
.uid_fetch(&new_uids, &[], FetchItems::Envelope)
.await .await
.map_err(|err| anyhow!("err: {}", err))?; .context("error fetching uids")?;
debug!("Connected to {}:{}.", &acct.imap.server, acct.imap.port); // fetched
// .map(Ok)
debug!("TLS Upgrade option: {:?}", acct.imap.tls); // .try_for_each_concurrent(None, |(uid, attrs)| {
let unauth = if matches!(acct.imap.tls, TlsMethod::Starttls) { // mail_store.store_email(&acct_name, &folder, uid,
debug!("attempting to upgrade"); // uidvalidity, attrs) })
let client = client // .await
.upgrade() // .context("error during fetch-store")?;
.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(
// acct_name.clone(),
// folder_list.clone(),
// ));
debug!("mailbox list: {:?}", folder_list);
for folder in folder_list.iter() {
debug!("folder: {:?}", folder);
let select = authed.select("INBOX").await?;
debug!("select response: {:?}", select);
if let (Some(_exists), Some(_uidvalidity)) =
(select.exists, select.uid_validity)
{
// figure out which uids don't exist locally yet
let new_uids = vec![];
// let new_uids =
// stream::iter(1..exists).map(Ok).try_filter_map(|uid| {
// todo!()
// // mail_store.try_identify_email(&acct_name, &folder,
// uid, uidvalidity, None) // //
// invert the option to only select uids that
// haven't been downloaded //
// .map_ok(move |o| o.map_or_else(move || Some(uid), |v| None))
// // .map_err(|err| err.context("error checking if
// the email is already downloaded
// [try_identify_email]")) }).try_collect::
// <Vec<_>>().await?;
if !new_uids.is_empty() {
debug!("fetching uids {:?}", new_uids);
let _fetched = authed
.uid_fetch(&new_uids, &[], FetchItems::Envelope)
.await
.context("error fetching uids")?;
// fetched
// .map(Ok)
// .try_for_each_concurrent(None, |(uid, attrs)| {
// mail_store.store_email(&acct_name, &folder, uid,
// uidvalidity, attrs) })
// .await
// .context("error during fetch-store")?;
}
}
} }
tokio::time::sleep(std::time::Duration::from_secs(50)).await; }
}
tokio::time::sleep(std::time::Duration::from_secs(50)).await;
// TODO: remove this later // TODO: remove this later
// continue; // continue;
// let's just select INBOX for now, maybe have a config for default
// mailbox later?
debug!("selecting the INBOX mailbox"); // let's just select INBOX for now, maybe have a config for default
let select = authed.select("INBOX").await?; // mailbox later?
debug!("select result: {:?}", select);
debug!("selecting the INBOX mailbox");
let select = authed.select("INBOX").await?;
debug!("select result: {:?}", select);
loop {
let message_uids = authed.uid_search().await?;
let message_uids = message_uids.into_iter().take(30).collect::<Vec<_>>();
// let _ = mail2ui_tx.send(MailEvent::MessageUids(
// acct_name.clone(),
// message_uids.clone(),
// ));
// TODO: make this happen concurrently with the main loop?
let mut message_list = authed
.uid_fetch(&message_uids, &[], FetchItems::All)
.await
.unwrap();
while let Some((_uid, _attrs)) = message_list.next().await {
// let evt = MailEvent::UpdateUid(acct_name.clone(), uid,
// attrs); TODO: probably odn't care about this?
// let _ = mail2ui_tx.send(evt);
}
// TODO: check if IDLE is supported
let supports_idle = true; // authed.has_capability("IDLE").await?;
if supports_idle {
let mut idle_stream = authed.idle().await?;
loop { loop {
let message_uids = authed.uid_search().await?; let evt = match idle_stream.next().await {
let message_uids = Some(v) => v,
message_uids.into_iter().take(30).collect::<Vec<_>>(); None => break,
// let _ = mail2ui_tx.send(MailEvent::MessageUids( };
// acct_name.clone(),
// message_uids.clone(), debug!("got an event: {:?}", evt);
// )); match evt {
// TODO: make this happen concurrently with the main loop? Response::MailboxData(MailboxData::Exists(uid)) => {
let mut message_list = authed 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("TODO")
// .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(
// acct_name.clone(),
// message_uids.clone(),
// ));
// TODO: make this happen concurrently with the main
// loop?
let mut message_list = authed
.uid_fetch(&message_uids, &[], FetchItems::All) .uid_fetch(&message_uids, &[], FetchItems::All)
.await .await
.unwrap(); .unwrap();
while let Some((_uid, _attrs)) = message_list.next().await { while let Some((_uid, _attrs)) = message_list.next().await {
// let evt = MailEvent::UpdateUid(acct_name.clone(), uid, // let evt = MailEvent::UpdateUid(acct_name.
// attrs); TODO: probably odn't care about this? // clone(), uid, attrs);
// let _ = mail2ui_tx.send(evt); // debug!("sent {:?}", evt);
} // mail2ui_tx.send(evt);
}
// TODO: check if IDLE is supported
let supports_idle = true; // authed.has_capability("IDLE").await?; idle_stream = authed.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("TODO")
// .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(
// acct_name.clone(),
// message_uids.clone(),
// ));
// TODO: make this happen concurrently with the main
// loop?
let mut message_list = authed
.uid_fetch(&message_uids, &[], FetchItems::All)
.await
.unwrap();
while let Some((_uid, _attrs)) =
message_list.next().await
{
// let evt = MailEvent::UpdateUid(acct_name.
// clone(), 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;
} }
_ => {}
}
} }
} else {
// wait a bit so we're not hitting the server really fast if the fail loop {
// happens early on tokio::time::sleep(std::time::Duration::from_secs(20)).await;
// debug!("heartbeat");
// 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; 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

@ -4,20 +4,20 @@ use sqlx::sqlite::{SqlitePool, SqlitePoolOptions};
use super::MIGRATOR; use super::MIGRATOR;
pub struct MailStore { pub struct MailStore {
pool: SqlitePool, pool: SqlitePool,
} }
impl MailStore { impl MailStore {
/// Creates a new connection to a SQLite database. /// Creates a new store tied to a SQLite database.
pub async fn open(uri: impl AsRef<str>) -> Result<Self> { pub async fn open(uri: impl AsRef<str>) -> Result<Self> {
let pool = SqlitePoolOptions::new().connect(uri.as_ref()).await?; let pool = SqlitePoolOptions::new().connect(uri.as_ref()).await?;
// run migrations, if available // run migrations, if available
MIGRATOR MIGRATOR
.run(&pool) .run(&pool)
.await .await
.context("could not run migrations on the pool")?; .context("could not run migrations on the pool")?;
Ok(MailStore { pool }) Ok(MailStore { pool })
} }
} }

View file

@ -7,40 +7,40 @@ use crate::client::inner::Inner;
use crate::proto::command::{Command, CommandLogin}; use crate::proto::command::{Command, CommandLogin};
pub trait Client: pub trait Client:
AsyncRead + AsyncWrite + Unpin + Sync + Send + 'static AsyncRead + AsyncWrite + Unpin + Sync + Send + 'static
{ {
} }
impl<C> Client for C where impl<C> Client for C where
C: Send + Sync + Unpin + AsyncWrite + AsyncRead + 'static C: Send + Sync + Unpin + AsyncWrite + AsyncRead + 'static
{ {
} }
#[async_trait] #[async_trait]
pub trait AuthMethod { pub trait AuthMethod {
async fn perform_auth<C>(&self, inner: &mut Inner<C>) -> Result<()> async fn perform_auth<C>(&self, inner: &mut Inner<C>) -> Result<()>
where where
C: Client; C: Client;
} }
pub struct Login { pub struct Login {
pub username: String, pub username: String,
pub password: String, pub password: String,
} }
#[async_trait] #[async_trait]
impl AuthMethod for Login { impl AuthMethod for Login {
async fn perform_auth<C>(&self, inner: &mut Inner<C>) -> Result<()> async fn perform_auth<C>(&self, inner: &mut Inner<C>) -> Result<()>
where where
C: Client, C: Client,
{ {
let command = Command::Login(CommandLogin { let command = Command::Login(CommandLogin {
userid: Bytes::from(self.username.clone()), userid: Bytes::from(self.username.clone()),
password: Bytes::from(self.password.clone()), password: Bytes::from(self.password.clone()),
}); });
let result = inner.execute(command).await?; let result = inner.execute(command).await?;
info!("result: {:?}", result.wait().await?); info!("result: {:?}", result.wait().await?);
Ok(()) Ok(())
} }
} }

View file

@ -4,22 +4,22 @@ use std::task::{Context, Poll};
use anyhow::Result; use anyhow::Result;
use futures::{ use futures::{
future::{self, FutureExt}, future::{self, FutureExt},
stream::{Stream, StreamExt}, stream::{Stream, StreamExt},
}; };
use panorama_proto_common::Bytes; use panorama_proto_common::Bytes;
use tokio::net::TcpStream; use tokio::net::TcpStream;
use tokio_rustls::client::TlsStream; use tokio_rustls::client::TlsStream;
use crate::proto::{ use crate::proto::{
command::{ command::{
Command, CommandFetch, CommandList, CommandSearch, CommandSelect, Command, CommandFetch, CommandList, CommandSearch, CommandSelect,
FetchItems, SearchCriteria, Sequence, FetchItems, SearchCriteria, Sequence,
}, },
response::{ response::{
Condition, Flag, Mailbox, MailboxData, MailboxList, MessageAttribute, Condition, Flag, Mailbox, MailboxData, MailboxList, MessageAttribute,
Response, ResponseCode, Status, Response, ResponseCode, Status,
}, },
}; };
use super::auth::AuthMethod; use super::auth::AuthMethod;
@ -31,278 +31,270 @@ use super::tls::wrap_tls;
#[derive(Builder, Clone, Debug)] #[derive(Builder, Clone, Debug)]
#[builder(build_fn(private))] #[builder(build_fn(private))]
pub struct Config { pub struct Config {
/// The hostname of the IMAP server. If using TLS, must be an address /// The hostname of the IMAP server. If using TLS, must be an address
pub hostname: String, pub hostname: String,
/// The port of the IMAP server. /// The port of the IMAP server.
pub port: u16, pub port: u16,
/// Whether or not the client is using an encrypted stream. /// Whether or not the client is using an encrypted stream.
/// ///
/// To upgrade the connection later, use the upgrade method. /// To upgrade the connection later, use the upgrade method.
#[builder(default = "true")] #[builder(default = "true")]
pub tls: bool, pub tls: bool,
/// Whether or not to verify hostname /// Whether or not to verify hostname
#[builder(default = "true")] #[builder(default = "true")]
pub verify_hostname: bool, pub verify_hostname: bool,
} }
impl Config { impl Config {
pub fn builder() -> ConfigBuilder { ConfigBuilder::default() } pub fn builder() -> ConfigBuilder { ConfigBuilder::default() }
} }
impl ConfigBuilder { impl ConfigBuilder {
pub async fn open(&self) -> Result<ClientUnauthenticated> { pub async fn open(&self) -> Result<ClientUnauthenticated> {
let config = self.build()?; let config = self.build()?;
let hostname = config.hostname.as_ref(); let hostname = config.hostname.as_ref();
let port = config.port; let port = config.port;
trace!("connecting to {}:{}...", hostname, port); trace!("connecting to {}:{}...", hostname, port);
let conn = TcpStream::connect((hostname, port)).await?; let conn = TcpStream::connect((hostname, port)).await?;
trace!("connected."); trace!("connected.");
if config.tls { if config.tls {
let conn = wrap_tls(conn, hostname).await?; let conn = wrap_tls(conn, hostname).await?;
let mut inner = Inner::new(conn, config).await?; let mut inner = Inner::new(conn, config).await?;
inner.wait_for_greeting().await?; inner.wait_for_greeting().await?;
debug!("received greeting"); debug!("received greeting");
return Ok(ClientUnauthenticated::Encrypted(inner)); return Ok(ClientUnauthenticated::Encrypted(inner));
} else { } else {
let mut inner = Inner::new(conn, config).await?; let mut inner = Inner::new(conn, config).await?;
inner.wait_for_greeting().await?; inner.wait_for_greeting().await?;
debug!("received greeting"); debug!("received greeting");
return Ok(ClientUnauthenticated::Unencrypted(inner)); return Ok(ClientUnauthenticated::Unencrypted(inner));
}
} }
}
} }
/// A client that hasn't been authenticated. /// A client that hasn't been authenticated.
pub enum ClientUnauthenticated { pub enum ClientUnauthenticated {
Encrypted(Inner<TlsStream<TcpStream>>), Encrypted(Inner<TlsStream<TcpStream>>),
Unencrypted(Inner<TcpStream>), Unencrypted(Inner<TcpStream>),
} }
impl ClientUnauthenticated { impl ClientUnauthenticated {
pub async fn upgrade(self) -> Result<ClientUnauthenticated> { pub async fn upgrade(self) -> Result<ClientUnauthenticated> {
match self { match self {
// this is a no-op, we don't need to upgrade // this is a no-op, we don't need to upgrade
ClientUnauthenticated::Encrypted(_) => Ok(self), ClientUnauthenticated::Encrypted(_) => Ok(self),
ClientUnauthenticated::Unencrypted(e) => { ClientUnauthenticated::Unencrypted(e) => {
Ok(ClientUnauthenticated::Encrypted(e.upgrade().await?)) Ok(ClientUnauthenticated::Encrypted(e.upgrade().await?))
} }
}
} }
}
pub async fn auth( pub async fn auth(
self, self,
auth: impl AuthMethod, auth: impl AuthMethod,
) -> Result<ClientAuthenticated> { ) -> Result<ClientAuthenticated> {
match self { match self {
// this is a no-op, we don't need to upgrade // this is a no-op, we don't need to upgrade
ClientUnauthenticated::Encrypted(mut inner) => { ClientUnauthenticated::Encrypted(mut inner) => {
auth.perform_auth(&mut inner).await?; auth.perform_auth(&mut inner).await?;
Ok(ClientAuthenticated::Encrypted(inner)) Ok(ClientAuthenticated::Encrypted(inner))
} }
ClientUnauthenticated::Unencrypted(mut inner) => { ClientUnauthenticated::Unencrypted(mut inner) => {
auth.perform_auth(&mut inner).await?; auth.perform_auth(&mut inner).await?;
Ok(ClientAuthenticated::Unencrypted(inner)) Ok(ClientAuthenticated::Unencrypted(inner))
} }
}
} }
}
client_expose!(async execute(cmd: Command) -> Result<ResponseStream>); client_expose!(async execute(cmd: Command) -> Result<ResponseStream>);
client_expose!(async has_capability(cap: impl AsRef<str>) -> Result<bool>); client_expose!(async has_capability(cap: impl AsRef<str>) -> Result<bool>);
} }
/// A client that has been authenticated. /// A client that has been authenticated.
pub enum ClientAuthenticated { pub enum ClientAuthenticated {
Encrypted(Inner<TlsStream<TcpStream>>), Encrypted(Inner<TlsStream<TcpStream>>),
Unencrypted(Inner<TcpStream>), Unencrypted(Inner<TcpStream>),
} }
impl ClientAuthenticated { impl ClientAuthenticated {
client_expose!(async execute(cmd: Command) -> Result<ResponseStream>); client_expose!(async execute(cmd: Command) -> Result<ResponseStream>);
client_expose!(async has_capability(cap: impl AsRef<str>) -> Result<bool>); client_expose!(async has_capability(cap: impl AsRef<str>) -> Result<bool>);
pub async fn search(&mut self) -> Result<()> { pub async fn search(&mut self) -> Result<()> {
let cmd = Command::Examine; let cmd = Command::Examine;
let res = self.execute(cmd).await?; let res = self.execute(cmd).await?;
let (done, data) = res.wait().await?; let (done, data) = res.wait().await?;
println!("done = {:?}", done); println!("done = {:?}", done);
println!("data = {:?}", data); println!("data = {:?}", data);
Ok(()) Ok(())
}
/// Runs the LIST command
pub async fn list(&mut self) -> Result<Vec<Mailbox>> {
let cmd = Command::List(CommandList {
reference: Bytes::from(""),
mailbox: Bytes::from("*"),
});
let res = self.execute(cmd).await?;
let (_, data) = res.wait().await?;
let mut folders = Vec::new();
for resp in data {
if let Response::MailboxData(MailboxData::List(MailboxList {
mailbox,
..
})) = resp
{
folders.push(mailbox);
}
} }
/// Runs the LIST command Ok(folders)
pub async fn list(&mut self) -> Result<Vec<Mailbox>> { }
let cmd = Command::List(CommandList {
reference: Bytes::from(""),
mailbox: Bytes::from("*"),
});
let res = self.execute(cmd).await?; /// Runs the SELECT command
let (_, data) = res.wait().await?; pub async fn select(
&mut self,
mailbox: impl AsRef<str>,
) -> Result<SelectResponse> {
let cmd = Command::Select(CommandSelect {
mailbox: Bytes::from(mailbox.as_ref().to_owned()),
});
let mut folders = Vec::new(); let stream = self.execute(cmd).await?;
for resp in data { let (_, data) = stream.wait().await?;
if let Response::MailboxData(MailboxData::List(MailboxList {
mailbox, let mut select = SelectResponse::default();
.. for resp in data {
})) = resp match resp {
{ Response::MailboxData(MailboxData::Flags(flags)) => {
folders.push(mailbox); select.flags = flags
}
} }
Response::MailboxData(MailboxData::Exists(exists)) => {
Ok(folders) select.exists = Some(exists)
}
Response::MailboxData(MailboxData::Recent(recent)) => {
select.recent = Some(recent)
}
Response::Tagged(
_,
Condition {
status: Status::Ok,
code: Some(code),
..
},
)
| Response::Condition(Condition {
status: Status::Ok,
code: Some(code),
..
}) => match code {
ResponseCode::Unseen(value) => select.unseen = Some(value),
ResponseCode::UidNext(value) => select.uid_next = Some(value),
ResponseCode::UidValidity(value) => select.uid_validity = Some(value),
_ => {}
},
_ => warn!("unknown response {:?}", resp),
}
} }
/// Runs the SELECT command Ok(select)
pub async fn select( }
&mut self,
mailbox: impl AsRef<str>,
) -> Result<SelectResponse> {
let cmd = Command::Select(CommandSelect {
mailbox: Bytes::from(mailbox.as_ref().to_owned()),
});
let stream = self.execute(cmd).await?; /// Runs the SEARCH command
let (_, data) = stream.wait().await?; pub async fn uid_search(&mut self) -> Result<Vec<u32>> {
let cmd = Command::UidSearch(CommandSearch {
let mut select = SelectResponse::default(); criteria: SearchCriteria::all(),
for resp in data { });
match resp { let stream = self.execute(cmd).await?;
Response::MailboxData(MailboxData::Flags(flags)) => { let (_, data) = stream.wait().await?;
select.flags = flags for resp in data {
} if let Response::MailboxData(MailboxData::Search(uids)) = resp {
Response::MailboxData(MailboxData::Exists(exists)) => { return Ok(uids);
select.exists = Some(exists) }
}
Response::MailboxData(MailboxData::Recent(recent)) => {
select.recent = Some(recent)
}
Response::Tagged(
_,
Condition {
status: Status::Ok,
code: Some(code),
..
},
)
| Response::Condition(Condition {
status: Status::Ok,
code: Some(code),
..
}) => match code {
ResponseCode::Unseen(value) => select.unseen = Some(value),
ResponseCode::UidNext(value) => {
select.uid_next = Some(value)
}
ResponseCode::UidValidity(value) => {
select.uid_validity = Some(value)
}
_ => {}
},
_ => warn!("unknown response {:?}", resp),
}
}
Ok(select)
} }
bail!("could not find the SEARCH response")
}
/// Runs the SEARCH command /// Runs the FETCH command
pub async fn uid_search(&mut self) -> Result<Vec<u32>> { pub async fn fetch(
let cmd = Command::UidSearch(CommandSearch { &mut self,
criteria: SearchCriteria::all(), uids: &[u32],
}); uid_seqs: &[Range<u32>],
let stream = self.execute(cmd).await?; items: FetchItems,
let (_, data) = stream.wait().await?; ) -> Result<impl Stream<Item = (u32, Vec<MessageAttribute>)>> {
for resp in data { let mut ids = Vec::new();
if let Response::MailboxData(MailboxData::Search(uids)) = resp { for uid in uids {
return Ok(uids); ids.push(Sequence::Single(*uid));
}
}
bail!("could not find the SEARCH response")
} }
for seq in uid_seqs {
ids.push(Sequence::Range(seq.start, seq.end));
}
let cmd = Command::Fetch(CommandFetch { ids, items });
debug!("fetch: {:?}", cmd);
let stream = self.execute(cmd).await?;
// let (done, data) = stream.wait().await?;
Ok(stream.filter_map(|resp| match resp {
Response::Fetch(n, attrs) => future::ready(Some((n, attrs))).boxed(),
Response::Done(_) => future::ready(None).boxed(),
_ => future::pending().boxed(),
}))
}
/// Runs the FETCH command /// Runs the UID FETCH command
pub async fn fetch( pub async fn uid_fetch(
&mut self, &mut self,
uids: &[u32], uids: &[u32],
uid_seqs: &[Range<u32>], uid_seqs: &[Range<u32>],
items: FetchItems, items: FetchItems,
) -> Result<impl Stream<Item = (u32, Vec<MessageAttribute>)>> { ) -> Result<impl Stream<Item = (u32, Vec<MessageAttribute>)>> {
let mut ids = Vec::new(); let mut ids = Vec::new();
for uid in uids { for uid in uids {
ids.push(Sequence::Single(*uid)); ids.push(Sequence::Single(*uid));
}
for seq in uid_seqs {
ids.push(Sequence::Range(seq.start, seq.end));
}
let cmd = Command::Fetch(CommandFetch { ids, items });
debug!("fetch: {:?}", cmd);
let stream = self.execute(cmd).await?;
// let (done, data) = stream.wait().await?;
Ok(stream.filter_map(|resp| match resp {
Response::Fetch(n, attrs) => {
future::ready(Some((n, attrs))).boxed()
}
Response::Done(_) => future::ready(None).boxed(),
_ => future::pending().boxed(),
}))
} }
for seq in uid_seqs {
ids.push(Sequence::Range(seq.start, seq.end));
}
let cmd = Command::UidFetch(CommandFetch { ids, items });
debug!("uid fetch: {:?}", cmd);
let stream = self.execute(cmd).await?;
// let (done, data) = stream.wait().await?;
Ok(stream.filter_map(|resp| match resp {
Response::Fetch(n, attrs) => future::ready(Some((n, attrs))).boxed(),
Response::Done(_) => future::ready(None).boxed(),
_ => future::pending().boxed(),
}))
}
/// Runs the UID FETCH command /// Runs the IDLE command
pub async fn uid_fetch( #[cfg(feature = "rfc2177")]
&mut self, #[cfg_attr(docsrs, doc(cfg(feature = "rfc2177")))]
uids: &[u32], pub async fn idle(&mut self) -> Result<IdleToken> {
uid_seqs: &[Range<u32>], let cmd = Command::Idle;
items: FetchItems, let stream = self.execute(cmd).await?;
) -> Result<impl Stream<Item = (u32, Vec<MessageAttribute>)>> { Ok(IdleToken { stream })
let mut ids = Vec::new(); }
for uid in uids {
ids.push(Sequence::Single(*uid));
}
for seq in uid_seqs {
ids.push(Sequence::Range(seq.start, seq.end));
}
let cmd = Command::UidFetch(CommandFetch { ids, items });
debug!("uid fetch: {:?}", cmd);
let stream = self.execute(cmd).await?;
// let (done, data) = stream.wait().await?;
Ok(stream.filter_map(|resp| match resp {
Response::Fetch(n, attrs) => {
future::ready(Some((n, attrs))).boxed()
}
Response::Done(_) => future::ready(None).boxed(),
_ => future::pending().boxed(),
}))
}
/// Runs the IDLE command
#[cfg(feature = "rfc2177")]
#[cfg_attr(docsrs, doc(cfg(feature = "rfc2177")))]
pub async fn idle(&mut self) -> Result<IdleToken> {
let cmd = Command::Idle;
let stream = self.execute(cmd).await?;
Ok(IdleToken { stream })
}
} }
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct SelectResponse { pub struct SelectResponse {
pub flags: Vec<Flag>, pub flags: Vec<Flag>,
pub exists: Option<u32>, pub exists: Option<u32>,
pub recent: Option<u32>, pub recent: Option<u32>,
pub uid_next: Option<u32>, pub uid_next: Option<u32>,
pub uid_validity: Option<u32>, pub uid_validity: Option<u32>,
pub unseen: Option<u32>, pub unseen: Option<u32>,
} }
/// A token that represents an idling connection. /// A token that represents an idling connection.
@ -312,28 +304,28 @@ pub struct SelectResponse {
#[cfg(feature = "rfc2177")] #[cfg(feature = "rfc2177")]
#[cfg_attr(docsrs, doc(cfg(feature = "rfc2177")))] #[cfg_attr(docsrs, doc(cfg(feature = "rfc2177")))]
pub struct IdleToken { pub struct IdleToken {
pub stream: ResponseStream, pub stream: ResponseStream,
// sender: mpsc::UnboundedSender<TaggedCommand>, // sender: mpsc::UnboundedSender<TaggedCommand>,
} }
#[cfg(feature = "rfc2177")] #[cfg(feature = "rfc2177")]
#[cfg_attr(docsrs, doc(cfg(feature = "rfc2177")))] #[cfg_attr(docsrs, doc(cfg(feature = "rfc2177")))]
impl Drop for IdleToken { impl Drop for IdleToken {
fn drop(&mut self) { fn drop(&mut self) {
// TODO: put this into a channel instead // TODO: put this into a channel instead
// tokio::spawn(self.client.execute(Command::Done)); // tokio::spawn(self.client.execute(Command::Done));
} }
} }
#[cfg(feature = "rfc2177")] #[cfg(feature = "rfc2177")]
#[cfg_attr(docsrs, doc(cfg(feature = "rfc2177")))] #[cfg_attr(docsrs, doc(cfg(feature = "rfc2177")))]
impl Stream for IdleToken { impl Stream for IdleToken {
type Item = Response; type Item = Response;
fn poll_next( fn poll_next(
mut self: Pin<&mut Self>, mut self: Pin<&mut Self>,
cx: &mut Context, cx: &mut Context,
) -> Poll<Option<Self::Item>> { ) -> Poll<Option<Self::Item>> {
let stream = Pin::new(&mut self.stream); let stream = Pin::new(&mut self.stream);
Stream::poll_next(stream, cx) Stream::poll_next(stream, cx)
} }
} }

View file

@ -6,90 +6,90 @@ use panorama_proto_common::{convert_error, Bytes};
use tokio_util::codec::{Decoder, Encoder}; use tokio_util::codec::{Decoder, Encoder};
use crate::proto::{ use crate::proto::{
command::Command, command::Command,
response::{Response, Tag}, response::{Response, Tag},
rfc3501::response as parse_response, rfc3501::response as parse_response,
}; };
/// A codec that can be used for decoding `Response`s and encoding `Command`s. /// A codec that can be used for decoding `Response`s and encoding `Command`s.
#[derive(Default)] #[derive(Default)]
pub struct ImapCodec { pub struct ImapCodec {
decode_need_message_bytes: usize, decode_need_message_bytes: usize,
} }
impl<'a> Decoder for ImapCodec { impl<'a> Decoder for ImapCodec {
type Item = Response; type Item = Response;
type Error = io::Error; type Error = io::Error;
fn decode( fn decode(
&mut self, &mut self,
buf: &mut BytesMut, buf: &mut BytesMut,
) -> Result<Option<Self::Item>, io::Error> { ) -> Result<Option<Self::Item>, io::Error> {
use nom::Err; use nom::Err;
if self.decode_need_message_bytes > buf.len() { if self.decode_need_message_bytes > buf.len() {
return Ok(None); return Ok(None);
}
// this is a pretty hot mess so here's my best attempt at explaining
// buf, or buf1, is the original message
// "split" mutably removes all the bytes from the self, and returns a
// new BytesMut with the contents. so buf2 now has all the
// original contents and buf1 is now empty
let buf2 = buf.split();
// now we're going to clone buf2 here, calling "freeze" turns the
// BytesMut back into Bytes so we can manipulate it. remember,
// none of this should be actually copying anything
let buf3 = buf2.clone().freeze();
debug!("going to parse a response since buffer len: {}", buf3.len());
// trace!("buf: {:?}", buf3);
// we don't know how long the message is going to be yet, so parse it
// out of the Bytes right now, and since the buffer is being
// consumed, subtracting the remainder of the string from the
// original total (buf4_len) will tell us how long the payload
// was. this also avoids unnecessary cloning
let buf4: Bytes = buf3.clone().into();
let buf4_len = buf4.len();
let (response, len) = match parse_response(buf4) {
Ok((remaining, response)) => (response, buf4_len - remaining.len()),
// the incomplete cases: set the decoded bytes and quit early
Err(nom::Err::Incomplete(Needed::Size(min))) => {
self.decode_need_message_bytes = min.get();
return Ok(None);
}
Err(nom::Err::Incomplete(_)) => {
return Ok(None);
}
// shit
Err(Err::Error(err)) | Err(Err::Failure(err)) => {
let buf4 = buf3.clone().into();
error!("failed to parse: {:?}", buf4);
error!("code: {}", convert_error(buf4, &err));
return Err(io::Error::new(
io::ErrorKind::Other,
format!("error during parsing of {:?}", buf),
));
}
};
info!("success, parsed as {:?}", response);
// "unsplit" is the opposite of split, we're getting back the original
// data here
buf.unsplit(buf2);
// and then move to after the message we just parsed
let first_part = buf.split_to(len);
trace!("parsed from: {:?}", first_part);
// since we're done parsing a complete message, set this to zero
self.decode_need_message_bytes = 0;
Ok(Some(response))
} }
// this is a pretty hot mess so here's my best attempt at explaining
// buf, or buf1, is the original message
// "split" mutably removes all the bytes from the self, and returns a
// new BytesMut with the contents. so buf2 now has all the
// original contents and buf1 is now empty
let buf2 = buf.split();
// now we're going to clone buf2 here, calling "freeze" turns the
// BytesMut back into Bytes so we can manipulate it. remember,
// none of this should be actually copying anything
let buf3 = buf2.clone().freeze();
debug!("going to parse a response since buffer len: {}", buf3.len());
// trace!("buf: {:?}", buf3);
// we don't know how long the message is going to be yet, so parse it
// out of the Bytes right now, and since the buffer is being
// consumed, subtracting the remainder of the string from the
// original total (buf4_len) will tell us how long the payload
// was. this also avoids unnecessary cloning
let buf4: Bytes = buf3.clone().into();
let buf4_len = buf4.len();
let (response, len) = match parse_response(buf4) {
Ok((remaining, response)) => (response, buf4_len - remaining.len()),
// the incomplete cases: set the decoded bytes and quit early
Err(nom::Err::Incomplete(Needed::Size(min))) => {
self.decode_need_message_bytes = min.get();
return Ok(None);
}
Err(nom::Err::Incomplete(_)) => {
return Ok(None);
}
// shit
Err(Err::Error(err)) | Err(Err::Failure(err)) => {
let buf4 = buf3.clone().into();
error!("failed to parse: {:?}", buf4);
error!("code: {}", convert_error(buf4, &err));
return Err(io::Error::new(
io::ErrorKind::Other,
format!("error during parsing of {:?}", buf),
));
}
};
info!("success, parsed as {:?}", response);
// "unsplit" is the opposite of split, we're getting back the original
// data here
buf.unsplit(buf2);
// and then move to after the message we just parsed
let first_part = buf.split_to(len);
trace!("parsed from: {:?}", first_part);
// since we're done parsing a complete message, set this to zero
self.decode_need_message_bytes = 0;
Ok(Some(response))
}
} }
/// A command with its accompanying tag. /// A command with its accompanying tag.
@ -97,24 +97,24 @@ impl<'a> Decoder for ImapCodec {
pub struct TaggedCommand(pub Tag, pub Command); pub struct TaggedCommand(pub Tag, pub Command);
impl<'a> Encoder<&'a TaggedCommand> for ImapCodec { impl<'a> Encoder<&'a TaggedCommand> for ImapCodec {
type Error = io::Error; type Error = io::Error;
fn encode( fn encode(
&mut self, &mut self,
tagged_cmd: &TaggedCommand, tagged_cmd: &TaggedCommand,
dst: &mut BytesMut, dst: &mut BytesMut,
) -> Result<(), io::Error> { ) -> Result<(), io::Error> {
let tag = &*tagged_cmd.0 .0; let tag = &*tagged_cmd.0 .0;
let command = &tagged_cmd.1; let command = &tagged_cmd.1;
dst.put(tag); dst.put(tag);
dst.put_u8(b' '); dst.put_u8(b' ');
// TODO: don't allocate here! use a stream writer // TODO: don't allocate here! use a stream writer
let cmd_bytes = format_bytes!(b"{}", command); let cmd_bytes = format_bytes!(b"{}", command);
dst.extend_from_slice(cmd_bytes.as_slice()); dst.extend_from_slice(cmd_bytes.as_slice());
// debug!("C>>>S: {:?}", dst); // debug!("C>>>S: {:?}", dst);
Ok(()) Ok(())
} }
} }

View file

@ -1,30 +1,29 @@
use std::collections::HashSet; use std::collections::HashSet;
use std::sync::{ use std::sync::{
atomic::{AtomicU32, Ordering}, atomic::{AtomicU32, Ordering},
Arc, Arc,
}; };
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use futures::{ use futures::{
future::{self, FutureExt, TryFutureExt}, future::{self, FutureExt, TryFutureExt},
stream::StreamExt, stream::StreamExt,
}; };
use panorama_proto_common::Bytes; use panorama_proto_common::Bytes;
use tokio::{ use tokio::{
io::{ io::{
split, AsyncRead, AsyncWrite, AsyncWriteExt, BufWriter, ReadHalf, split, AsyncRead, AsyncWrite, AsyncWriteExt, BufWriter, ReadHalf, WriteHalf,
WriteHalf, },
}, sync::{mpsc, oneshot, RwLock},
sync::{mpsc, oneshot, RwLock}, task::JoinHandle,
task::JoinHandle,
}; };
use tokio_rustls::client::TlsStream; use tokio_rustls::client::TlsStream;
use tokio_util::codec::FramedRead; use tokio_util::codec::FramedRead;
use crate::proto::{ use crate::proto::{
command::Command, command::Command,
response::{Capability, Condition, Response, Status, Tag}, response::{Capability, Condition, Response, Status, Tag},
rfc3501::capability as parse_capability, rfc3501::capability as parse_capability,
}; };
use super::client::Config; use super::client::Config;
@ -42,193 +41,187 @@ type GreetingWaiter = oneshot::Receiver<()>;
/// Low-level client, can directly read from and write to the stream /// Low-level client, can directly read from and write to the stream
/// without the additional type-safety of the higher-level state machine. /// without the additional type-safety of the higher-level state machine.
pub struct Inner<C> { pub struct Inner<C> {
config: Config, config: Config,
tag_number: AtomicU32, tag_number: AtomicU32,
command_tx: mpsc::UnboundedSender<CommandContainer>, command_tx: mpsc::UnboundedSender<CommandContainer>,
read_exit: ExitSender, read_exit: ExitSender,
read_handle: JoinHandle<ReadHalf<C>>, read_handle: JoinHandle<ReadHalf<C>>,
write_exit: ExitSender, write_exit: ExitSender,
write_handle: JoinHandle<WriteHalf<C>>, write_handle: JoinHandle<WriteHalf<C>>,
_write_tx: mpsc::UnboundedSender<TaggedCommand>, _write_tx: mpsc::UnboundedSender<TaggedCommand>,
greeting_rx: Option<GreetingWaiter>, greeting_rx: Option<GreetingWaiter>,
capabilities: Arc<RwLock<Option<HashSet<Capability>>>>, capabilities: Arc<RwLock<Option<HashSet<Capability>>>>,
} }
#[derive(Debug)] #[derive(Debug)]
struct CommandContainer { struct CommandContainer {
tag: Tag, tag: Tag,
command: Command, command: Command,
channel: mpsc::UnboundedSender<Response>, channel: mpsc::UnboundedSender<Response>,
} }
impl<C> Inner<C> impl<C> Inner<C>
where where
C: AsyncRead + AsyncWrite + Unpin + Send + 'static, C: AsyncRead + AsyncWrite + Unpin + Send + 'static,
{ {
pub async fn new(c: C, config: Config) -> Result<Self> { pub async fn new(c: C, config: Config) -> Result<Self> {
let (command_tx, command_rx) = mpsc::unbounded_channel(); let (command_tx, command_rx) = mpsc::unbounded_channel();
// break the stream of bytes into a reader and a writer // break the stream of bytes into a reader and a writer
// the read_half represents the server->client connection // the read_half represents the server->client connection
// the write_half represents the client->server connection // the write_half represents the client->server connection
let (read_half, write_half) = split(c); let (read_half, write_half) = split(c);
// this channel is used to inform clients when we receive the // this channel is used to inform clients when we receive the
// initial greeting from the server // initial greeting from the server
let (greeting_tx, greeting_rx) = oneshot::channel(); let (greeting_tx, greeting_rx) = oneshot::channel();
// spawn the server->client loop // spawn the server->client loop
let (read_exit, exit_rx) = oneshot::channel(); let (read_exit, exit_rx) = oneshot::channel();
let (write_tx, write_rx) = mpsc::unbounded_channel(); // TODO: maybe an arbitrary/configurable limit here would be better? let (write_tx, write_rx) = mpsc::unbounded_channel(); // TODO: maybe an arbitrary/configurable limit here would be better?
let read_handle = tokio::spawn(read_loop( let read_handle = tokio::spawn(read_loop(
read_half, read_half,
exit_rx, exit_rx,
greeting_tx, greeting_tx,
write_tx.clone(), write_tx.clone(),
command_rx, command_rx,
)); ));
// spawn the client->server loop // spawn the client->server loop
let (write_exit, exit_rx) = oneshot::channel(); let (write_exit, exit_rx) = oneshot::channel();
let write_handle = let write_handle = tokio::spawn(write_loop(write_half, exit_rx, write_rx));
tokio::spawn(write_loop(write_half, exit_rx, write_rx));
let tag_number = AtomicU32::new(0); let tag_number = AtomicU32::new(0);
let capabilities = Arc::new(RwLock::new(None)); let capabilities = Arc::new(RwLock::new(None));
Ok(Inner { Ok(Inner {
config, config,
tag_number, tag_number,
command_tx, command_tx,
read_exit, read_exit,
read_handle, read_handle,
write_exit, write_exit,
write_handle, write_handle,
_write_tx: write_tx, _write_tx: write_tx,
greeting_rx: Some(greeting_rx), greeting_rx: Some(greeting_rx),
capabilities, capabilities,
}) })
} }
pub async fn execute( pub async fn execute(&mut self, command: Command) -> Result<ResponseStream> {
&mut self, let id = self.tag_number.fetch_add(1, Ordering::SeqCst);
command: Command, let tag = Tag(Bytes::from(format!("{}{}", TAG_PREFIX, id)));
) -> Result<ResponseStream> {
let id = self.tag_number.fetch_add(1, Ordering::SeqCst);
let tag = Tag(Bytes::from(format!("{}{}", TAG_PREFIX, id)));
let (channel, rx) = mpsc::unbounded_channel(); let (channel, rx) = mpsc::unbounded_channel();
self.command_tx.send(CommandContainer { self.command_tx.send(CommandContainer {
tag, tag,
command, command,
channel, channel,
})?; })?;
let stream = ResponseStream { inner: rx }; let stream = ResponseStream { inner: rx };
Ok(stream) Ok(stream)
} }
pub async fn has_capability( pub async fn has_capability(&mut self, cap: impl AsRef<str>) -> Result<bool> {
&mut self, let mut cap_slice = cap.as_ref().as_bytes().to_vec();
cap: impl AsRef<str>,
) -> Result<bool> {
let mut cap_slice = cap.as_ref().as_bytes().to_vec();
// since we're doing incremental parsing, we have to finish this off // since we're doing incremental parsing, we have to finish this off
// with something that's invalid // with something that's invalid
cap_slice.push(b'\n'); cap_slice.push(b'\n');
let cap_bytes = Bytes::from(cap_slice); let cap_bytes = Bytes::from(cap_slice);
trace!("CAP_BYTES: {:?}", cap_bytes); trace!("CAP_BYTES: {:?}", cap_bytes);
let (_, cap) = parse_capability(cap_bytes) let (_, cap) =
.context("could not parse capability")?; parse_capability(cap_bytes).context("could not parse capability")?;
let contains = { let contains = {
let read = self.capabilities.read().await; let read = self.capabilities.read().await;
if let Some(read) = &*read { if let Some(read) = &*read {
read.contains(&cap) read.contains(&cap)
} else { } else {
std::mem::drop(read); std::mem::drop(read);
let cmd = self.execute(Command::Capability).await?; let cmd = self.execute(Command::Capability).await?;
let (_, res) = cmd.wait().await?; let (_, res) = cmd.wait().await?;
let mut capabilities = HashSet::new(); let mut capabilities = HashSet::new();
// todo!("done: {:?} {:?}", done, res); // todo!("done: {:?} {:?}", done, res);
for caps in res.iter().filter_map(|res| match res { for caps in res.iter().filter_map(|res| match res {
Response::Capabilities(caps) => Some(caps), Response::Capabilities(caps) => Some(caps),
_ => None, _ => None,
}) { }) {
capabilities.extend(caps.clone()); capabilities.extend(caps.clone());
}
let mut write = self.capabilities.write().await;
*write = Some(capabilities);
true
}
};
Ok(contains)
}
pub async fn upgrade(mut self) -> Result<Inner<TlsStream<C>>> {
debug!("preparing to upgrade using STARTTLS");
// check that this capability exists
// if it doesn't exist, then it's not an IMAP4-compliant server
if !self
.has_capability("STARTTLS")
.await
.context("could not check starttls capability")?
{
bail!("Server does not have the STARTTLS capability");
} }
// issue the STARTTLS command to the server let mut write = self.capabilities.write().await;
let resp = self *write = Some(capabilities);
.execute(Command::Starttls) true
.await }
.context("could not send starttls command")?; };
resp.wait()
.await
.context("could not receive starttls response")?;
debug!("received OK from server");
// issue exit to the read loop and retrieve the read half Ok(contains)
let _ = self.read_exit.send(()); }
let read_half = self
.read_handle
.await
.context("could not retrieve read half of connection")?;
// issue exit to the write loop and retrieve the write half pub async fn upgrade(mut self) -> Result<Inner<TlsStream<C>>> {
let _ = self.write_exit.send(()); debug!("preparing to upgrade using STARTTLS");
let write_half = self
.write_handle
.await
.context("could not retrieve write half of connection")?;
// put the read half and write half back together // check that this capability exists
let stream = read_half.unsplit(write_half); // if it doesn't exist, then it's not an IMAP4-compliant server
let tls_stream = wrap_tls(stream, &self.config.hostname) if !self
.await .has_capability("STARTTLS")
.context("could not initialize tls stream")?; .await
.context("could not check starttls capability")?
Inner::new(tls_stream, self.config) {
.await bail!("Server does not have the STARTTLS capability");
.context("could not construct new client")
} }
pub async fn wait_for_greeting(&mut self) -> Result<()> { // issue the STARTTLS command to the server
if let Some(greeting_rx) = self.greeting_rx.take() { let resp = self
greeting_rx.await?; .execute(Command::Starttls)
} .await
Ok(()) .context("could not send starttls command")?;
resp
.wait()
.await
.context("could not receive starttls response")?;
debug!("received OK from server");
// issue exit to the read loop and retrieve the read half
let _ = self.read_exit.send(());
let read_half = self
.read_handle
.await
.context("could not retrieve read half of connection")?;
// issue exit to the write loop and retrieve the write half
let _ = self.write_exit.send(());
let write_half = self
.write_handle
.await
.context("could not retrieve write half of connection")?;
// put the read half and write half back together
let stream = read_half.unsplit(write_half);
let tls_stream = wrap_tls(stream, &self.config.hostname)
.await
.context("could not initialize tls stream")?;
Inner::new(tls_stream, self.config)
.await
.context("could not construct new client")
}
pub async fn wait_for_greeting(&mut self) -> Result<()> {
if let Some(greeting_rx) = self.greeting_rx.take() {
greeting_rx.await?;
} }
Ok(())
}
} }
// exit is a channel that will notify this loop when some external // exit is a channel that will notify this loop when some external
@ -236,133 +229,133 @@ where
// //
// when the loop exits, the read half of the stream will be returned // when the loop exits, the read half of the stream will be returned
async fn read_loop<C>( async fn read_loop<C>(
stream: ReadHalf<C>, stream: ReadHalf<C>,
exit: ExitListener, exit: ExitListener,
greeting_tx: GreetingSender, greeting_tx: GreetingSender,
write_tx: mpsc::UnboundedSender<TaggedCommand>, write_tx: mpsc::UnboundedSender<TaggedCommand>,
mut command_rx: mpsc::UnboundedReceiver<CommandContainer>, mut command_rx: mpsc::UnboundedReceiver<CommandContainer>,
) -> ReadHalf<C> ) -> ReadHalf<C>
where where
C: AsyncRead, C: AsyncRead,
{ {
// this lets us "use up" the greeting sender // this lets us "use up" the greeting sender
let mut greeting_tx = Some(greeting_tx); let mut greeting_tx = Some(greeting_tx);
let mut curr_cmd: Option<CommandContainer> = None; let mut curr_cmd: Option<CommandContainer> = None;
// set up framed communication // set up framed communication
let codec = ImapCodec::default(); let codec = ImapCodec::default();
let mut framed = FramedRead::new(stream, codec); let mut framed = FramedRead::new(stream, codec);
let exit = exit.fuse(); let exit = exit.fuse();
pin_mut!(exit); pin_mut!(exit);
loop { loop {
debug!("READ LOOP ITER"); debug!("READ LOOP ITER");
let next = framed.next().fuse(); let next = framed.next().fuse();
pin_mut!(next); pin_mut!(next);
// only listen for a new command if there isn't one already // only listen for a new command if there isn't one already
let mut cmd_fut = if let Some(ref cmd) = curr_cmd { let mut cmd_fut = if let Some(ref cmd) = curr_cmd {
debug!("current command: {:?}", cmd); debug!("current command: {:?}", cmd);
// if there is one, just make a future that never resolves so it'll // if there is one, just make a future that never resolves so it'll
// always pick the other options in the select. // always pick the other options in the select.
future::pending().boxed().fuse() future::pending().boxed().fuse()
} else { } else {
command_rx.recv().boxed().fuse() command_rx.recv().boxed().fuse()
}; };
select! { select! {
// read a command from the command list // read a command from the command list
command = cmd_fut => { command = cmd_fut => {
if curr_cmd.is_none() { if curr_cmd.is_none() {
if let Some(CommandContainer { ref tag, ref command, .. }) = command { if let Some(CommandContainer { ref tag, ref command, .. }) = command {
let _ = write_tx.send(TaggedCommand(tag.clone(), command.clone())); let _ = write_tx.send(TaggedCommand(tag.clone(), command.clone()));
// let cmd_str = format!("{} {:?}\r\n", tag, cmd); // let cmd_str = format!("{} {:?}\r\n", tag, cmd);
// write_tx.send(cmd_str); // write_tx.send(cmd_str);
}
curr_cmd = command;
debug!("new command: {:?}", curr_cmd);
} }
curr_cmd = command;
debug!("new command: {:?}", curr_cmd);
} }
// new message from the server
resp = next => {
let resp = match resp {
Some(Ok(v)) => v,
a => { error!("failed: {:?}", a); todo!("fuck"); },
};
trace!("S>>>C: {:?}", resp);
// if this is the very first response, then it's a greeting
if let Some(greeting_tx) = greeting_tx.take() {
greeting_tx.send(()).unwrap();
}
if let Response::Done(_) = resp {
// since this is the DONE message, clear curr_cmd so another one can be sent
if let Some(CommandContainer { channel, .. }) = curr_cmd.take() {
let _ = channel.send(resp);
// debug!("res0: {:?}", res);
}
} else if let Response::Tagged(_, Condition { status: Status::Ok, ..}) = resp {
// clear curr_cmd so another one can be sent
if let Some(CommandContainer { channel, .. }) = curr_cmd.take() {
let _ = channel.send(resp);
// debug!("res0: {:?}", res);
}
} else if let Some(CommandContainer { channel, .. }) = curr_cmd.as_mut() {
// we got a response from the server for this command, so send it over the
// channel
// debug!("sending {:?} to tag {}", resp, tag);
let _res = channel.send(resp);
// debug!("res1: {:?}", res);
}
}
_ = exit => break,
} }
}
framed.into_inner() // new message from the server
resp = next => {
let resp = match resp {
Some(Ok(v)) => v,
a => { error!("failed: {:?}", a); todo!("fuck"); },
};
trace!("S>>>C: {:?}", resp);
// if this is the very first response, then it's a greeting
if let Some(greeting_tx) = greeting_tx.take() {
greeting_tx.send(()).unwrap();
}
if let Response::Done(_) = resp {
// since this is the DONE message, clear curr_cmd so another one can be sent
if let Some(CommandContainer { channel, .. }) = curr_cmd.take() {
let _ = channel.send(resp);
// debug!("res0: {:?}", res);
}
} else if let Response::Tagged(_, Condition { status: Status::Ok, ..}) = resp {
// clear curr_cmd so another one can be sent
if let Some(CommandContainer { channel, .. }) = curr_cmd.take() {
let _ = channel.send(resp);
// debug!("res0: {:?}", res);
}
} else if let Some(CommandContainer { channel, .. }) = curr_cmd.as_mut() {
// we got a response from the server for this command, so send it over the
// channel
// debug!("sending {:?} to tag {}", resp, tag);
let _res = channel.send(resp);
// debug!("res1: {:?}", res);
}
}
_ = exit => break,
}
}
framed.into_inner()
} }
async fn write_loop<C>( async fn write_loop<C>(
stream: WriteHalf<C>, stream: WriteHalf<C>,
exit_rx: ExitListener, exit_rx: ExitListener,
mut command_rx: mpsc::UnboundedReceiver<TaggedCommand>, mut command_rx: mpsc::UnboundedReceiver<TaggedCommand>,
) -> WriteHalf<C> ) -> WriteHalf<C>
where where
C: AsyncWrite, C: AsyncWrite,
{ {
// set up framed communication // set up framed communication
// let codec = ImapCodec::default(); // let codec = ImapCodec::default();
let mut stream = BufWriter::new(stream); let mut stream = BufWriter::new(stream);
// let mut framed = FramedWrite::new(stream, codec); // let mut framed = FramedWrite::new(stream, codec);
let mut exit_rx = exit_rx.map_err(|_| ()).shared(); let mut exit_rx = exit_rx.map_err(|_| ()).shared();
loop { loop {
let command_fut = command_rx.recv().fuse(); let command_fut = command_rx.recv().fuse();
pin_mut!(command_fut); pin_mut!(command_fut);
select! { select! {
command = command_fut => { command = command_fut => {
// TODO: handle errors here // TODO: handle errors here
if let Some(command) = command { if let Some(command) = command {
let cmd = format_bytes!(b"{} {}\r\n", &*command.0.0, command.1); let cmd = format_bytes!(b"{} {}\r\n", &*command.0.0, command.1);
debug!("sending command: {:?}", String::from_utf8_lossy(&cmd)); debug!("sending command: {:?}", String::from_utf8_lossy(&cmd));
let _ = stream.write_all(&cmd).await; let _ = stream.write_all(&cmd).await;
let _ = stream.flush().await; let _ = stream.flush().await;
// let _ = framed.send(&command).await; // let _ = framed.send(&command).await;
// let _ = framed.flush().await; // let _ = framed.flush().await;
}
// let _ = stream.write_all(line.as_bytes()).await;
// let _ = stream.flush().await;
} }
_ = exit_rx => break, // let _ = stream.write_all(line.as_bytes()).await;
// let _ = stream.flush().await;
} }
_ = exit_rx => break,
} }
}
// framed.into_inner() // framed.into_inner()
stream.into_inner() stream.into_inner()
} }

View file

@ -24,7 +24,7 @@ mod inner;
mod tls; mod tls;
pub use self::client::{ pub use self::client::{
ClientAuthenticated, ClientUnauthenticated, Config, ConfigBuilder, ClientAuthenticated, ClientUnauthenticated, Config, ConfigBuilder,
}; };
pub use self::codec::{ImapCodec, TaggedCommand}; pub use self::codec::{ImapCodec, TaggedCommand};

View file

@ -9,45 +9,43 @@ use crate::proto::response::{Response, ResponseDone};
/// A series of responses that follow an /// A series of responses that follow an
pub struct ResponseStream { pub struct ResponseStream {
pub(crate) inner: mpsc::UnboundedReceiver<Response>, pub(crate) inner: mpsc::UnboundedReceiver<Response>,
} }
impl ResponseStream { impl ResponseStream {
/// Retrieves just the DONE item in the stream, discarding the rest /// Retrieves just the DONE item in the stream, discarding the rest
pub async fn done(mut self) -> Result<Option<ResponseDone>> { pub async fn done(mut self) -> Result<Option<ResponseDone>> {
while let Some(resp) = self.inner.recv().await { while let Some(resp) = self.inner.recv().await {
if let Response::Done(done) = resp { if let Response::Done(done) = resp {
return Ok(Some(done)); return Ok(Some(done));
} }
}
Ok(None)
} }
Ok(None)
}
/// Waits for the entire stream to finish, returning the DONE status and the /// Waits for the entire stream to finish, returning the DONE status and the
/// stream /// stream
pub async fn wait( pub async fn wait(mut self) -> Result<(Option<ResponseDone>, Vec<Response>)> {
mut self, let mut done = None;
) -> Result<(Option<ResponseDone>, Vec<Response>)> { let mut vec = Vec::new();
let mut done = None; while let Some(resp) = self.inner.recv().await {
let mut vec = Vec::new(); if let Response::Done(d) = resp {
while let Some(resp) = self.inner.recv().await { done = Some(d);
if let Response::Done(d) = resp { break;
done = Some(d); } else {
break; vec.push(resp);
} else { }
vec.push(resp);
}
}
Ok((done, vec))
} }
Ok((done, vec))
}
} }
impl Stream for ResponseStream { impl Stream for ResponseStream {
type Item = Response; type Item = Response;
fn poll_next( fn poll_next(
mut self: Pin<&mut Self>, mut self: Pin<&mut Self>,
cx: &mut Context, cx: &mut Context,
) -> Poll<Option<Self::Item>> { ) -> Poll<Option<Self::Item>> {
self.inner.poll_recv(cx) self.inner.poll_recv(cx)
} }
} }

View file

@ -3,43 +3,42 @@ use std::{convert::TryFrom, sync::Arc};
use anyhow::Result; use anyhow::Result;
use tokio::io::{AsyncRead, AsyncWrite}; use tokio::io::{AsyncRead, AsyncWrite};
use tokio_rustls::{ use tokio_rustls::{
client::TlsStream, client::TlsStream,
rustls::{ rustls::{
ClientConfig as RustlsConfig, OwnedTrustAnchor, RootCertStore, ClientConfig as RustlsConfig, OwnedTrustAnchor, RootCertStore, ServerName,
ServerName, },
}, TlsConnector,
TlsConnector,
}; };
/// Wraps the given async stream in TLS with the given hostname (required) /// Wraps the given async stream in TLS with the given hostname (required)
pub async fn wrap_tls<C>( pub async fn wrap_tls<C>(
c: C, c: C,
hostname: impl AsRef<str>, hostname: impl AsRef<str>,
) -> Result<TlsStream<C>> ) -> Result<TlsStream<C>>
where where
C: AsyncRead + AsyncWrite + Unpin, C: AsyncRead + AsyncWrite + Unpin,
{ {
let server_name = hostname.as_ref(); let server_name = hostname.as_ref();
let mut root_store = RootCertStore::empty(); let mut root_store = RootCertStore::empty();
root_store.add_server_trust_anchors( root_store.add_server_trust_anchors(
webpki_roots::TLS_SERVER_ROOTS.0.iter().map(|ta| { webpki_roots::TLS_SERVER_ROOTS.0.iter().map(|ta| {
OwnedTrustAnchor::from_subject_spki_name_constraints( OwnedTrustAnchor::from_subject_spki_name_constraints(
ta.subject, ta.subject,
ta.spki, ta.spki,
ta.name_constraints, ta.name_constraints,
) )
}), }),
); );
let tls_config = RustlsConfig::builder() let tls_config = RustlsConfig::builder()
.with_safe_defaults() .with_safe_defaults()
.with_root_certificates(root_store) .with_root_certificates(root_store)
.with_no_client_auth(); .with_no_client_auth();
let tls_config = TlsConnector::from(Arc::new(tls_config)); let tls_config = TlsConnector::from(Arc::new(tls_config));
let server_name = ServerName::try_from(server_name).unwrap(); let server_name = ServerName::try_from(server_name).unwrap();
let stream = tls_config.connect(server_name, c).await?; let stream = tls_config.connect(server_name, c).await?;
Ok(stream) Ok(stream)
} }

View file

@ -1,7 +1,7 @@
use std::{ use std::{
collections::HashSet, collections::HashSet,
io::{self, Write}, io::{self, Write},
ops::{Bound, RangeBounds}, ops::{Bound, RangeBounds},
}; };
use format_bytes::DisplayBytes; use format_bytes::DisplayBytes;
@ -11,169 +11,169 @@ use super::rfc3501::is_quoted_specials;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum Command { pub enum Command {
// Any state // Any state
Capability, Capability,
Noop, Noop,
Logout, Logout,
// Not authenticated // Not authenticated
Login(CommandLogin), Login(CommandLogin),
Starttls, Starttls,
Authenticate, Authenticate,
// Authenticated // Authenticated
Select(CommandSelect), Select(CommandSelect),
Examine, Examine,
Create, Create,
Delete, Delete,
Rename, Rename,
Subscribe, Subscribe,
Unsubscribe, Unsubscribe,
List(CommandList), List(CommandList),
Lsub, Lsub,
Status, Status,
Append, Append,
// Selected // Selected
Check, Check,
Close, Close,
Expunge, Expunge,
Search, Search,
Copy, Copy,
Fetch(CommandFetch), Fetch(CommandFetch),
Store, Store,
UidCopy, UidCopy,
UidFetch(CommandFetch), UidFetch(CommandFetch),
UidStore, UidStore,
UidSearch(CommandSearch), UidSearch(CommandSearch),
// Extensions // Extensions
#[cfg(feature = "rfc2177")] #[cfg(feature = "rfc2177")]
Idle, Idle,
#[cfg(feature = "rfc2177")] #[cfg(feature = "rfc2177")]
Done, Done,
} }
impl DisplayBytes for Command { impl DisplayBytes for Command {
fn display_bytes(&self, w: &mut dyn Write) -> io::Result<()> { fn display_bytes(&self, w: &mut dyn Write) -> io::Result<()> {
let quote = quote_string(b'\x22', b'\\', is_quoted_specials); let quote = quote_string(b'\x22', b'\\', is_quoted_specials);
match self { match self {
// command-any // command-any
Command::Capability => write_bytes!(w, b"CAPABILITY"), Command::Capability => write_bytes!(w, b"CAPABILITY"),
Command::Logout => write_bytes!(w, b"LOGOUT"), Command::Logout => write_bytes!(w, b"LOGOUT"),
Command::Noop => write_bytes!(w, b"NOOP"), Command::Noop => write_bytes!(w, b"NOOP"),
// command-nonauth // command-nonauth
Command::Login(login) => { Command::Login(login) => {
write_bytes!( write_bytes!(
w, w,
b"LOGIN {} {}", b"LOGIN {} {}",
quote(&login.userid), quote(&login.userid),
quote(&login.password) quote(&login.password)
) )
} }
Command::Starttls => write_bytes!(w, b"STARTTLS"), Command::Starttls => write_bytes!(w, b"STARTTLS"),
// command-auth // command-auth
Command::List(list) => { Command::List(list) => {
write_bytes!( write_bytes!(
w, w,
b"LIST {} {}", b"LIST {} {}",
quote(&list.reference), quote(&list.reference),
quote(&list.mailbox) quote(&list.mailbox)
) )
} }
Command::Select(select) => { Command::Select(select) => {
write_bytes!(w, b"SELECT {}", quote(&select.mailbox)) write_bytes!(w, b"SELECT {}", quote(&select.mailbox))
} }
// selected // selected
Command::UidFetch(fetch) => write_bytes!(w, b"UID FETCH {}", fetch), Command::UidFetch(fetch) => write_bytes!(w, b"UID FETCH {}", fetch),
// extensions // extensions
#[cfg(feature = "rfc2177")] #[cfg(feature = "rfc2177")]
Command::Idle => write_bytes!(w, b"IDLE"), Command::Idle => write_bytes!(w, b"IDLE"),
_ => todo!("unimplemented command: {:?}", self), _ => todo!("unimplemented command: {:?}", self),
}
} }
}
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct CommandFetch { pub struct CommandFetch {
pub ids: Vec<Sequence>, pub ids: Vec<Sequence>,
pub items: FetchItems, pub items: FetchItems,
} }
impl DisplayBytes for CommandFetch { impl DisplayBytes for CommandFetch {
fn display_bytes(&self, w: &mut dyn Write) -> io::Result<()> { fn display_bytes(&self, w: &mut dyn Write) -> io::Result<()> {
for (i, seq) in self.ids.iter().enumerate() { for (i, seq) in self.ids.iter().enumerate() {
if i != 0 { if i != 0 {
write_bytes!(w, b",")?; write_bytes!(w, b",")?;
} }
match seq { match seq {
Sequence::Single(n) => write_bytes!(w, b"{}", n)?, Sequence::Single(n) => write_bytes!(w, b"{}", n)?,
Sequence::Range(m, n) => write_bytes!(w, b"{}:{}", m, n)?, Sequence::Range(m, n) => write_bytes!(w, b"{}:{}", m, n)?,
} }
}
write_bytes!(w, b" {}", self.items)
} }
write_bytes!(w, b" {}", self.items)
}
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum Sequence { pub enum Sequence {
Single(u32), Single(u32),
Range(u32, u32), Range(u32, u32),
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct CommandList { pub struct CommandList {
pub reference: Bytes, pub reference: Bytes,
pub mailbox: Bytes, pub mailbox: Bytes,
} }
#[derive(Clone, Derivative)] #[derive(Clone, Derivative)]
#[derivative(Debug)] #[derivative(Debug)]
pub struct CommandLogin { pub struct CommandLogin {
pub userid: Bytes, pub userid: Bytes,
#[derivative(Debug = "ignore")] #[derivative(Debug = "ignore")]
pub password: Bytes, pub password: Bytes,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct CommandSearch { pub struct CommandSearch {
pub criteria: SearchCriteria, pub criteria: SearchCriteria,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct CommandSelect { pub struct CommandSelect {
pub mailbox: Bytes, pub mailbox: Bytes,
} }
// //
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum FetchItems { pub enum FetchItems {
All, All,
Fast, Fast,
Full, Full,
Flags, Flags,
Envelope, Envelope,
} }
impl DisplayBytes for FetchItems { impl DisplayBytes for FetchItems {
fn display_bytes(&self, w: &mut dyn Write) -> io::Result<()> { fn display_bytes(&self, w: &mut dyn Write) -> io::Result<()> {
match self { match self {
FetchItems::All => write_bytes!(w, b"ALL"), FetchItems::All => write_bytes!(w, b"ALL"),
FetchItems::Fast => write_bytes!(w, b"FAST"), FetchItems::Fast => write_bytes!(w, b"FAST"),
FetchItems::Full => write_bytes!(w, b"FULL"), FetchItems::Full => write_bytes!(w, b"FULL"),
FetchItems::Flags => write_bytes!(w, b"FLAGS"), FetchItems::Flags => write_bytes!(w, b"FLAGS"),
FetchItems::Envelope => write_bytes!(w, b"ENVELOPE"), FetchItems::Envelope => write_bytes!(w, b"ENVELOPE"),
}
} }
}
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -185,74 +185,74 @@ pub enum FetchAttr {}
pub struct SearchCriteria(Vec<(Bound<u32>, Bound<u32>)>); pub struct SearchCriteria(Vec<(Bound<u32>, Bound<u32>)>);
impl SearchCriteria { impl SearchCriteria {
pub fn all() -> Self { pub fn all() -> Self {
let mut set = Vec::new(); let mut set = Vec::new();
set.push((Bound::Unbounded, Bound::Unbounded)); set.push((Bound::Unbounded, Bound::Unbounded));
SearchCriteria(set) SearchCriteria(set)
}
pub fn contains(&self, n: u32) -> bool {
for range in self.0.iter() {
if range.contains(&n) {
return true;
}
} }
pub fn contains(&self, n: u32) -> bool { false
for range in self.0.iter() { }
if range.contains(&n) {
return true;
}
}
false pub fn with_range(&mut self, range: impl RangeBounds<u32>) -> &mut Self {
} let range = (range.start_bound().cloned(), range.end_bound().cloned());
self.0.push(range);
pub fn with_range(&mut self, range: impl RangeBounds<u32>) -> &mut Self { self
let range = (range.start_bound().cloned(), range.end_bound().cloned()); }
self.0.push(range);
self
}
} }
impl DisplayBytes for SearchCriteria { impl DisplayBytes for SearchCriteria {
fn display_bytes(&self, w: &mut dyn Write) -> io::Result<()> { fn display_bytes(&self, w: &mut dyn Write) -> io::Result<()> {
// TODO: is it faster to batch these up or not? // TODO: is it faster to batch these up or not?
for (i, range) in self.0.iter().enumerate() { for (i, range) in self.0.iter().enumerate() {
if i != 0 { if i != 0 {
write_bytes!(w, b",")?; write_bytes!(w, b",")?;
} }
match range.0 { match range.0 {
Bound::Excluded(n) => write_bytes!(w, b"{}", &(n + 1))?, Bound::Excluded(n) => write_bytes!(w, b"{}", &(n + 1))?,
Bound::Included(n) => write_bytes!(w, b"{}", &n)?, Bound::Included(n) => write_bytes!(w, b"{}", &n)?,
Bound::Unbounded => write_bytes!(w, b"1")?, Bound::Unbounded => write_bytes!(w, b"1")?,
} }
write_bytes!(w, b":")?; write_bytes!(w, b":")?;
match range.1 { match range.1 {
Bound::Excluded(n) => write_bytes!(w, b"{}", &(n - 1))?, Bound::Excluded(n) => write_bytes!(w, b"{}", &(n - 1))?,
Bound::Included(n) => write_bytes!(w, b"{}", &n)?, Bound::Included(n) => write_bytes!(w, b"{}", &n)?,
Bound::Unbounded => write_bytes!(w, b"*")?, Bound::Unbounded => write_bytes!(w, b"*")?,
} }
}
Ok(())
} }
Ok(())
}
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
#[test] #[test]
fn display_search_criteria() { fn display_search_criteria() {
// TODO: is there a trivial case? // TODO: is there a trivial case?
assert_eq!(format_bytes!(b"{}", SearchCriteria::all()), b"1:*"); assert_eq!(format_bytes!(b"{}", SearchCriteria::all()), b"1:*");
assert_eq!( assert_eq!(
format_bytes!( format_bytes!(
b"{}", b"{}",
SearchCriteria(vec![ SearchCriteria(vec![
(Bound::Unbounded, Bound::Included(4)), (Bound::Unbounded, Bound::Included(4)),
(Bound::Excluded(5), Bound::Unbounded), (Bound::Excluded(5), Bound::Unbounded),
]) ])
), ),
b"1:4,6:*" b"1:4,6:*"
); );
} }
} }

View file

@ -17,9 +17,9 @@ pub type Atom = Bytes;
pub struct Tag(pub Bytes); pub struct Tag(pub Bytes);
impl DisplayBytes for Tag { impl DisplayBytes for Tag {
fn display_bytes(&self, w: &mut dyn Write) -> io::Result<()> { fn display_bytes(&self, w: &mut dyn Write) -> io::Result<()> {
write_bytes!(w, b"{}", self.0) write_bytes!(w, b"{}", self.0)
} }
} }
#[derive(Clone, Debug, PartialEq, Eq, Hash)] #[derive(Clone, Debug, PartialEq, Eq, Hash)]
@ -27,366 +27,366 @@ pub struct Timestamp(pub(crate) DateTime<FixedOffset>);
#[cfg(feature = "fuzzing")] #[cfg(feature = "fuzzing")]
impl<'a> Arbitrary<'a> for Timestamp { impl<'a> Arbitrary<'a> for Timestamp {
fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> { fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
let (y, m, d, H, M, S) = ( let (y, m, d, H, M, S) = (
// TODO: year range? // TODO: year range?
u.int_in_range(-4000..=4000i32)?, u.int_in_range(-4000..=4000i32)?,
u.int_in_range(1..=12u32)?, u.int_in_range(1..=12u32)?,
u.int_in_range(1..=28u32)?, u.int_in_range(1..=28u32)?,
u.int_in_range(0..=23u32)?, u.int_in_range(0..=23u32)?,
u.int_in_range(0..=59u32)?, u.int_in_range(0..=59u32)?,
u.int_in_range(0..=59u32)?, u.int_in_range(0..=59u32)?,
); );
println!("{:?}", (y, m, d, H, M, S)); println!("{:?}", (y, m, d, H, M, S));
Ok(Timestamp( Ok(Timestamp(
// TODO: introduce offset // TODO: introduce offset
FixedOffset::west(0).ymd(y, m, d).and_hms(H, M, S), FixedOffset::west(0).ymd(y, m, d).and_hms(H, M, S),
)) ))
} }
} }
#[derive(Debug)] #[derive(Debug)]
#[non_exhaustive] #[non_exhaustive]
#[cfg_attr(feature = "fuzzing", derive(Arbitrary))] #[cfg_attr(feature = "fuzzing", derive(Arbitrary))]
pub enum Response { pub enum Response {
Capabilities(Vec<Capability>), Capabilities(Vec<Capability>),
Continue(ResponseText), Continue(ResponseText),
Condition(Condition), Condition(Condition),
Done(ResponseDone), Done(ResponseDone),
MailboxData(MailboxData), MailboxData(MailboxData),
Fetch(u32, Vec<MessageAttribute>), Fetch(u32, Vec<MessageAttribute>),
Expunge(u32), Expunge(u32),
Fatal(Condition), Fatal(Condition),
Tagged(Tag, Condition), Tagged(Tag, Condition),
} }
impl DisplayBytes for Response { impl DisplayBytes for Response {
fn display_bytes(&self, w: &mut dyn Write) -> io::Result<()> { fn display_bytes(&self, w: &mut dyn Write) -> io::Result<()> {
#[allow(unreachable_patterns)] #[allow(unreachable_patterns)]
match self { match self {
Response::Capabilities(caps) => { Response::Capabilities(caps) => {
write_bytes!(w, b"CAPABILITY")?; write_bytes!(w, b"CAPABILITY")?;
for cap in caps { for cap in caps {
write_bytes!(w, b" {}", cap)?; write_bytes!(w, b" {}", cap)?;
}
Ok(())
}
Response::Continue(cont) => write_bytes!(w, b"+ {}\r\n", cont),
Response::Condition(cond) => write_bytes!(w, b"* {}\r\n", cond),
Response::Done(_) => write_bytes!(w, b""),
Response::MailboxData(data) => write_bytes!(w, b"* {}\r\n", data),
Response::Fetch(n, attrs) => {
write_bytes!(w, b"{} FETCH (", n)?;
for (i, attr) in attrs.iter().enumerate() {
if i != 0 {
write_bytes!(w, b" ")?;
}
write_bytes!(w, b"{}", attr)?;
}
write_bytes!(w, b")\r\n")
}
Response::Expunge(n) => write_bytes!(w, b"{} EXPUNGE\r\n", n),
Response::Fatal(cond) => write_bytes!(w, b"* {}\r\n", cond),
Response::Tagged(tag, cond) => {
write_bytes!(w, b"{} {}\r\n", tag, cond)
}
_ => todo!(),
} }
Ok(())
}
Response::Continue(cont) => write_bytes!(w, b"+ {}\r\n", cont),
Response::Condition(cond) => write_bytes!(w, b"* {}\r\n", cond),
Response::Done(_) => write_bytes!(w, b""),
Response::MailboxData(data) => write_bytes!(w, b"* {}\r\n", data),
Response::Fetch(n, attrs) => {
write_bytes!(w, b"{} FETCH (", n)?;
for (i, attr) in attrs.iter().enumerate() {
if i != 0 {
write_bytes!(w, b" ")?;
}
write_bytes!(w, b"{}", attr)?;
}
write_bytes!(w, b")\r\n")
}
Response::Expunge(n) => write_bytes!(w, b"{} EXPUNGE\r\n", n),
Response::Fatal(cond) => write_bytes!(w, b"* {}\r\n", cond),
Response::Tagged(tag, cond) => {
write_bytes!(w, b"{} {}\r\n", tag, cond)
}
_ => todo!(),
} }
}
} }
#[derive(Debug)] #[derive(Debug)]
#[cfg_attr(feature = "fuzzing", derive(Arbitrary))] #[cfg_attr(feature = "fuzzing", derive(Arbitrary))]
pub struct ResponseText { pub struct ResponseText {
pub code: Option<ResponseCode>, pub code: Option<ResponseCode>,
pub info: Bytes, pub info: Bytes,
} }
impl DisplayBytes for ResponseText { impl DisplayBytes for ResponseText {
fn display_bytes(&self, w: &mut dyn Write) -> io::Result<()> { fn display_bytes(&self, w: &mut dyn Write) -> io::Result<()> {
if let Some(code) = &self.code { if let Some(code) = &self.code {
write_bytes!(w, b"[{}] ", code)?; write_bytes!(w, b"[{}] ", code)?;
}
write_bytes!(w, b"{}", self.info)
} }
write_bytes!(w, b"{}", self.info)
}
} }
#[derive(Debug)] #[derive(Debug)]
#[cfg_attr(feature = "fuzzing", derive(Arbitrary))] #[cfg_attr(feature = "fuzzing", derive(Arbitrary))]
pub enum MessageAttribute { pub enum MessageAttribute {
BodySection, BodySection,
BodyStructure, BodyStructure,
Envelope(Envelope), Envelope(Envelope),
Flags(Vec<Flag>), Flags(Vec<Flag>),
InternalDate(Timestamp), InternalDate(Timestamp),
ModSeq(u64), // RFC 4551, section 3.3.2 ModSeq(u64), // RFC 4551, section 3.3.2
Rfc822(Option<String>), Rfc822(Option<String>),
Rfc822Header(Option<String>), Rfc822Header(Option<String>),
Rfc822Size(u32), Rfc822Size(u32),
Rfc822Text(Option<String>), Rfc822Text(Option<String>),
Uid(u32), Uid(u32),
} }
impl DisplayBytes for MessageAttribute { impl DisplayBytes for MessageAttribute {
fn display_bytes(&self, _: &mut dyn Write) -> io::Result<()> { fn display_bytes(&self, _: &mut dyn Write) -> io::Result<()> {
match self { match self {
_ => todo!(), _ => todo!(),
}
} }
}
} }
#[derive(Debug)] #[derive(Debug)]
#[cfg_attr(feature = "fuzzing", derive(Arbitrary))] #[cfg_attr(feature = "fuzzing", derive(Arbitrary))]
pub struct Envelope { pub struct Envelope {
pub date: Option<Bytes>, pub date: Option<Bytes>,
pub subject: Option<Bytes>, pub subject: Option<Bytes>,
pub from: Option<Vec<Address>>, pub from: Option<Vec<Address>>,
pub sender: Option<Vec<Address>>, pub sender: Option<Vec<Address>>,
pub reply_to: Option<Vec<Address>>, pub reply_to: Option<Vec<Address>>,
pub to: Option<Vec<Address>>, pub to: Option<Vec<Address>>,
pub cc: Option<Vec<Address>>, pub cc: Option<Vec<Address>>,
pub bcc: Option<Vec<Address>>, pub bcc: Option<Vec<Address>>,
pub in_reply_to: Option<Bytes>, pub in_reply_to: Option<Bytes>,
pub message_id: Option<Bytes>, pub message_id: Option<Bytes>,
} }
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
#[cfg_attr(feature = "fuzzing", derive(Arbitrary))] #[cfg_attr(feature = "fuzzing", derive(Arbitrary))]
pub struct Address { pub struct Address {
pub name: Option<Bytes>, pub name: Option<Bytes>,
pub adl: Option<Bytes>, pub adl: Option<Bytes>,
pub mailbox: Option<Bytes>, pub mailbox: Option<Bytes>,
pub host: Option<Bytes>, pub host: Option<Bytes>,
} }
#[derive(Debug)] #[derive(Debug)]
#[cfg_attr(feature = "fuzzing", derive(Arbitrary))] #[cfg_attr(feature = "fuzzing", derive(Arbitrary))]
pub struct ResponseDone { pub struct ResponseDone {
pub tag: Tag, pub tag: Tag,
pub status: Status, pub status: Status,
pub code: Option<ResponseCode>, pub code: Option<ResponseCode>,
pub info: Option<Bytes>, pub info: Option<Bytes>,
} }
#[derive(Debug)] #[derive(Debug)]
#[cfg_attr(feature = "fuzzing", derive(Arbitrary))] #[cfg_attr(feature = "fuzzing", derive(Arbitrary))]
pub struct Condition { pub struct Condition {
pub status: Status, pub status: Status,
pub code: Option<ResponseCode>, pub code: Option<ResponseCode>,
pub info: Bytes, pub info: Bytes,
} }
impl DisplayBytes for Condition { impl DisplayBytes for Condition {
fn display_bytes(&self, w: &mut dyn Write) -> io::Result<()> { fn display_bytes(&self, w: &mut dyn Write) -> io::Result<()> {
write_bytes!(w, b"{} ", self.status)?; write_bytes!(w, b"{} ", self.status)?;
if let Some(code) = &self.code { if let Some(code) = &self.code {
write_bytes!(w, b"[{}] ", code)?; write_bytes!(w, b"[{}] ", code)?;
}
write_bytes!(w, b"{}", self.info)
} }
write_bytes!(w, b"{}", self.info)
}
} }
#[derive(Debug)] #[derive(Debug)]
#[cfg_attr(feature = "fuzzing", derive(Arbitrary))] #[cfg_attr(feature = "fuzzing", derive(Arbitrary))]
pub enum Status { pub enum Status {
Ok, Ok,
No, No,
Bad, Bad,
PreAuth, PreAuth,
Bye, Bye,
} }
impl DisplayBytes for Status { impl DisplayBytes for Status {
fn display_bytes(&self, w: &mut dyn Write) -> io::Result<()> { fn display_bytes(&self, w: &mut dyn Write) -> io::Result<()> {
match self { match self {
Status::Ok => write_bytes!(w, b"OK"), Status::Ok => write_bytes!(w, b"OK"),
Status::No => write_bytes!(w, b"NO"), Status::No => write_bytes!(w, b"NO"),
Status::Bad => write_bytes!(w, b"BAD"), Status::Bad => write_bytes!(w, b"BAD"),
Status::PreAuth => write_bytes!(w, b"PREAUTH"), Status::PreAuth => write_bytes!(w, b"PREAUTH"),
Status::Bye => write_bytes!(w, b"BYE"), Status::Bye => write_bytes!(w, b"BYE"),
}
} }
}
} }
#[derive(Debug)] #[derive(Debug)]
#[non_exhaustive] #[non_exhaustive]
#[cfg_attr(feature = "fuzzing", derive(Arbitrary))] #[cfg_attr(feature = "fuzzing", derive(Arbitrary))]
pub enum ResponseCode { pub enum ResponseCode {
Alert, Alert,
BadCharset(Option<Vec<Bytes>>), BadCharset(Option<Vec<Bytes>>),
Capabilities(Vec<Capability>), Capabilities(Vec<Capability>),
HighestModSeq(u64), // RFC 4551, section 3.1.1 HighestModSeq(u64), // RFC 4551, section 3.1.1
Parse, Parse,
PermanentFlags(Vec<Flag>), PermanentFlags(Vec<Flag>),
ReadOnly, ReadOnly,
ReadWrite, ReadWrite,
TryCreate, TryCreate,
UidNext(u32), UidNext(u32),
UidValidity(u32), UidValidity(u32),
Unseen(u32), Unseen(u32),
AppendUid(u32, Vec<UidSetMember>), AppendUid(u32, Vec<UidSetMember>),
CopyUid(u32, Vec<UidSetMember>, Vec<UidSetMember>), CopyUid(u32, Vec<UidSetMember>, Vec<UidSetMember>),
UidNotSticky, UidNotSticky,
Other(Bytes, Option<Bytes>), Other(Bytes, Option<Bytes>),
} }
impl DisplayBytes for ResponseCode { impl DisplayBytes for ResponseCode {
fn display_bytes(&self, w: &mut dyn Write) -> io::Result<()> { fn display_bytes(&self, w: &mut dyn Write) -> io::Result<()> {
match self { match self {
ResponseCode::Alert => write_bytes!(w, b"ALERT"), ResponseCode::Alert => write_bytes!(w, b"ALERT"),
_ => todo!(), _ => todo!(),
}
} }
}
} }
#[derive(Debug)] #[derive(Debug)]
#[cfg_attr(feature = "fuzzing", derive(Arbitrary))] #[cfg_attr(feature = "fuzzing", derive(Arbitrary))]
pub enum UidSetMember { pub enum UidSetMember {
UidRange(RangeInclusive<u32>), UidRange(RangeInclusive<u32>),
Uid(u32), Uid(u32),
} }
#[derive(Clone, Debug, PartialEq, Eq, Hash)] #[derive(Clone, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "fuzzing", derive(Arbitrary))] #[cfg_attr(feature = "fuzzing", derive(Arbitrary))]
pub enum Capability { pub enum Capability {
Imap4rev1, Imap4rev1,
Auth(Atom), Auth(Atom),
Atom(Atom), Atom(Atom),
} }
impl DisplayBytes for Capability { impl DisplayBytes for Capability {
fn display_bytes(&self, w: &mut dyn Write) -> io::Result<()> { fn display_bytes(&self, w: &mut dyn Write) -> io::Result<()> {
match self { match self {
Capability::Imap4rev1 => write_bytes!(w, b"IMAP4rev1"), Capability::Imap4rev1 => write_bytes!(w, b"IMAP4rev1"),
Capability::Auth(a) => write_bytes!(w, b"AUTH={}", a), Capability::Auth(a) => write_bytes!(w, b"AUTH={}", a),
Capability::Atom(a) => write_bytes!(w, b"{}", a), Capability::Atom(a) => write_bytes!(w, b"{}", a),
}
} }
}
} }
#[derive(Debug)] #[derive(Debug)]
#[cfg_attr(feature = "fuzzing", derive(Arbitrary))] #[cfg_attr(feature = "fuzzing", derive(Arbitrary))]
pub enum MailboxData { pub enum MailboxData {
Flags(Vec<Flag>), Flags(Vec<Flag>),
List(MailboxList), List(MailboxList),
Lsub(MailboxList), Lsub(MailboxList),
Search(Vec<u32>), Search(Vec<u32>),
Status, Status,
Exists(u32), Exists(u32),
Recent(u32), Recent(u32),
} }
impl DisplayBytes for MailboxData { impl DisplayBytes for MailboxData {
fn display_bytes(&self, w: &mut dyn Write) -> io::Result<()> { fn display_bytes(&self, w: &mut dyn Write) -> io::Result<()> {
match self { match self {
MailboxData::Flags(flags) => { MailboxData::Flags(flags) => {
write_bytes!(w, b"(")?; write_bytes!(w, b"(")?;
for (i, flag) in flags.iter().enumerate() { for (i, flag) in flags.iter().enumerate() {
if i != 0 { if i != 0 {
write_bytes!(w, b" ")?; write_bytes!(w, b" ")?;
} }
write_bytes!(w, b"{}", flag)?; write_bytes!(w, b"{}", flag)?;
}
write_bytes!(w, b")")
}
MailboxData::List(list) => write_bytes!(w, b"{}", list),
MailboxData::Exists(n) => write_bytes!(w, b"{} EXISTS", n),
MailboxData::Recent(n) => write_bytes!(w, b"{} RECENT", n),
_ => todo!(),
} }
write_bytes!(w, b")")
}
MailboxData::List(list) => write_bytes!(w, b"{}", list),
MailboxData::Exists(n) => write_bytes!(w, b"{} EXISTS", n),
MailboxData::Recent(n) => write_bytes!(w, b"{} RECENT", n),
_ => todo!(),
} }
}
} }
#[derive(Debug)] #[derive(Debug)]
#[cfg_attr(feature = "fuzzing", derive(Arbitrary))] #[cfg_attr(feature = "fuzzing", derive(Arbitrary))]
pub enum Mailbox { pub enum Mailbox {
Inbox, Inbox,
Name(Bytes), Name(Bytes),
} }
impl DisplayBytes for Mailbox { impl DisplayBytes for Mailbox {
fn display_bytes(&self, w: &mut dyn Write) -> io::Result<()> { fn display_bytes(&self, w: &mut dyn Write) -> io::Result<()> {
match self { match self {
Mailbox::Inbox => write_bytes!(w, b"INBOX"), Mailbox::Inbox => write_bytes!(w, b"INBOX"),
Mailbox::Name(b) => write_bytes!(w, b"{}", b), Mailbox::Name(b) => write_bytes!(w, b"{}", b),
}
} }
}
} }
#[derive(Debug)] #[derive(Debug)]
#[cfg_attr(feature = "fuzzing", derive(Arbitrary))] #[cfg_attr(feature = "fuzzing", derive(Arbitrary))]
pub enum Flag { pub enum Flag {
Answered, Answered,
Flagged, Flagged,
Deleted, Deleted,
Seen, Seen,
Draft, Draft,
Recent, Recent,
Keyword(Atom), Keyword(Atom),
Extension(Atom), Extension(Atom),
SpecialCreate, SpecialCreate,
} }
impl DisplayBytes for Flag { impl DisplayBytes for Flag {
fn display_bytes(&self, w: &mut dyn Write) -> io::Result<()> { fn display_bytes(&self, w: &mut dyn Write) -> io::Result<()> {
match self { match self {
Flag::Answered => write_bytes!(w, b"\\Answered"), Flag::Answered => write_bytes!(w, b"\\Answered"),
Flag::Flagged => write_bytes!(w, b"\\Flagged"), Flag::Flagged => write_bytes!(w, b"\\Flagged"),
Flag::Deleted => write_bytes!(w, b"\\Deleted"), Flag::Deleted => write_bytes!(w, b"\\Deleted"),
Flag::Seen => write_bytes!(w, b"\\Seen"), Flag::Seen => write_bytes!(w, b"\\Seen"),
Flag::Draft => write_bytes!(w, b"\\Draft"), Flag::Draft => write_bytes!(w, b"\\Draft"),
Flag::Recent => write_bytes!(w, b"\\Recent"), Flag::Recent => write_bytes!(w, b"\\Recent"),
Flag::Keyword(atom) => write_bytes!(w, b"{}", atom), Flag::Keyword(atom) => write_bytes!(w, b"{}", atom),
Flag::Extension(atom) => write_bytes!(w, b"\\{}", atom), Flag::Extension(atom) => write_bytes!(w, b"\\{}", atom),
Flag::SpecialCreate => write_bytes!(w, b"\\*"), Flag::SpecialCreate => write_bytes!(w, b"\\*"),
}
} }
}
} }
#[derive(Debug)] #[derive(Debug)]
#[cfg_attr(feature = "fuzzing", derive(Arbitrary))] #[cfg_attr(feature = "fuzzing", derive(Arbitrary))]
pub struct MailboxList { pub struct MailboxList {
pub flags: Vec<MailboxListFlag>, pub flags: Vec<MailboxListFlag>,
pub delimiter: Option<u8>, pub delimiter: Option<u8>,
pub mailbox: Mailbox, pub mailbox: Mailbox,
} }
impl DisplayBytes for MailboxList { impl DisplayBytes for MailboxList {
fn display_bytes(&self, w: &mut dyn Write) -> io::Result<()> { fn display_bytes(&self, w: &mut dyn Write) -> io::Result<()> {
write_bytes!(w, b"(")?; write_bytes!(w, b"(")?;
for (i, flag) in self.flags.iter().enumerate() { for (i, flag) in self.flags.iter().enumerate() {
if i != 0 { if i != 0 {
write_bytes!(w, b" ")?; write_bytes!(w, b" ")?;
} }
write_bytes!(w, b"{}", flag)?; write_bytes!(w, b"{}", flag)?;
}
write_bytes!(w, b") ")?;
match self.delimiter {
Some(d) => write_bytes!(w, b"\"{}\"", d)?,
None => write_bytes!(w, b"NIL")?,
}
write_bytes!(w, b" {}", self.mailbox)
} }
write_bytes!(w, b") ")?;
match self.delimiter {
Some(d) => write_bytes!(w, b"\"{}\"", d)?,
None => write_bytes!(w, b"NIL")?,
}
write_bytes!(w, b" {}", self.mailbox)
}
} }
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
#[cfg_attr(feature = "fuzzing", derive(Arbitrary))] #[cfg_attr(feature = "fuzzing", derive(Arbitrary))]
pub enum MailboxListFlag { pub enum MailboxListFlag {
NoInferiors, NoInferiors,
NoSelect, NoSelect,
Marked, Marked,
Unmarked, Unmarked,
Extension(Atom), Extension(Atom),
} }
impl DisplayBytes for MailboxListFlag { impl DisplayBytes for MailboxListFlag {
fn display_bytes(&self, w: &mut dyn Write) -> io::Result<()> { fn display_bytes(&self, w: &mut dyn Write) -> io::Result<()> {
match self { match self {
MailboxListFlag::NoInferiors => write_bytes!(w, b"\\Noinferiors"), MailboxListFlag::NoInferiors => write_bytes!(w, b"\\Noinferiors"),
MailboxListFlag::NoSelect => write_bytes!(w, b"\\NoSelect"), MailboxListFlag::NoSelect => write_bytes!(w, b"\\NoSelect"),
MailboxListFlag::Marked => write_bytes!(w, b"\\Marked"), MailboxListFlag::Marked => write_bytes!(w, b"\\Marked"),
MailboxListFlag::Unmarked => write_bytes!(w, b"\\Unmarked"), MailboxListFlag::Unmarked => write_bytes!(w, b"\\Unmarked"),
MailboxListFlag::Extension(a) => write_bytes!(w, b"\\{}", a), MailboxListFlag::Extension(a) => write_bytes!(w, b"\\{}", a),
}
} }
}
} }

View file

@ -4,9 +4,9 @@
//! Grammar from <https://tools.ietf.org/html/rfc2234#section-6.1> //! Grammar from <https://tools.ietf.org/html/rfc2234#section-6.1>
use nom::{ use nom::{
branch::alt, branch::alt,
multi::many0, multi::many0,
sequence::{pair, preceded}, sequence::{pair, preceded},
}; };
use panorama_proto_common::{byte, satisfy, skip}; use panorama_proto_common::{byte, satisfy, skip};

View file

@ -8,30 +8,30 @@ pub mod tests;
use chrono::{FixedOffset, NaiveTime, TimeZone}; use chrono::{FixedOffset, NaiveTime, TimeZone};
use nom::{ use nom::{
branch::alt, branch::alt,
combinator::{map, map_res, opt, verify}, combinator::{map, map_res, opt, verify},
multi::{count, many0, many1, many_m_n}, multi::{count, many0, many1, many_m_n},
sequence::{delimited, pair, preceded, separated_pair, terminated, tuple}, sequence::{delimited, pair, preceded, separated_pair, terminated, tuple},
}; };
use panorama_proto_common::{ use panorama_proto_common::{
byte, never, parse_num, satisfy, tagi, take, take_while1, Bytes, VResult, byte, never, parse_num, satisfy, tagi, take, take_while1, Bytes, VResult,
}; };
use super::response::{ use super::response::{
Address, Atom, Capability, Condition, Envelope, Flag, Mailbox, MailboxData, Address, Atom, Capability, Condition, Envelope, Flag, Mailbox, MailboxData,
MailboxList, MailboxListFlag, MessageAttribute, Response, ResponseCode, MailboxList, MailboxListFlag, MessageAttribute, Response, ResponseCode,
ResponseText, Status, Tag, Timestamp, ResponseText, Status, Tag, Timestamp,
}; };
use super::rfc2234::{ use super::rfc2234::{
is_char, is_cr, is_ctl, is_digit, is_dquote, is_lf, is_sp, CRLF, DIGIT, is_char, is_cr, is_ctl, is_digit, is_dquote, is_lf, is_sp, CRLF, DIGIT,
DQUOTE, SP, DQUOTE, SP,
}; };
/// Grammar rule `T / nil` produces `Option<T>` /// Grammar rule `T / nil` produces `Option<T>`
macro_rules! opt_nil { macro_rules! opt_nil {
($t:expr) => { ($t:expr) => {
alt((map($t, Some), map(crate::proto::rfc3501::nil, |_| None))) alt((map($t, Some), map(crate::proto::rfc3501::nil, |_| None)))
}; };
} }
rule!(pub address : Address => map(paren!(tuple(( rule!(pub address : Address => map(paren!(tuple((
@ -52,7 +52,7 @@ rule!(pub addr_name : Option<Bytes> => nstring);
rule!(pub astring : Bytes => alt((take_while1(is_astring_char), string))); rule!(pub astring : Bytes => alt((take_while1(is_astring_char), string)));
pub(crate) fn is_astring_char(c: u8) -> bool { pub(crate) fn is_astring_char(c: u8) -> bool {
is_atom_char(c) || is_resp_specials(c) is_atom_char(c) || is_resp_specials(c)
} }
rule!(pub ASTRING_CHAR : u8 => alt((ATOM_CHAR, resp_specials))); rule!(pub ASTRING_CHAR : u8 => alt((ATOM_CHAR, resp_specials)));
@ -66,14 +66,14 @@ pub(crate) fn is_atom_char(c: u8) -> bool { is_char(c) && !is_atom_specials(c) }
rule!(pub ATOM_CHAR : u8 => satisfy(is_atom_char)); rule!(pub ATOM_CHAR : u8 => satisfy(is_atom_char));
pub(crate) fn is_atom_specials(c: u8) -> bool { pub(crate) fn is_atom_specials(c: u8) -> bool {
c == b'(' c == b'('
|| c == b')' || c == b')'
|| c == b'{' || c == b'{'
|| is_sp(c) || is_sp(c)
|| is_ctl(c) || is_ctl(c)
|| is_list_wildcards(c) || is_list_wildcards(c)
|| is_quoted_specials(c) || is_quoted_specials(c)
|| is_resp_specials(c) || is_resp_specials(c)
} }
rule!(pub atom_specials : u8 => satisfy(is_atom_specials)); rule!(pub atom_specials : u8 => satisfy(is_atom_specials));
@ -215,11 +215,11 @@ rule!(pub list_wildcards : u8 => satisfy(is_list_wildcards));
// determined to exceed a certain threshold so we don't have insane amounts of // determined to exceed a certain threshold so we don't have insane amounts of
// data in memory // data in memory
pub fn literal(i: Bytes) -> VResult<Bytes, Bytes> { pub fn literal(i: Bytes) -> VResult<Bytes, Bytes> {
let mut length_of = let mut length_of =
terminated(delimited(byte(b'{'), number, byte(b'}')), CRLF); terminated(delimited(byte(b'{'), number, byte(b'}')), CRLF);
let (i, length) = length_of(i)?; let (i, length) = length_of(i)?;
debug!("length is: {:?}", length); debug!("length is: {:?}", length);
map(take(length), Bytes::from)(i) map(take(length), Bytes::from)(i)
} }
rule!(pub mailbox : Mailbox => alt(( rule!(pub mailbox : Mailbox => alt((
@ -292,7 +292,7 @@ rule!(pub nil : Bytes => tagi(b"NIL"));
rule!(pub nstring : Option<Bytes> => opt_nil!(string)); rule!(pub nstring : Option<Bytes> => opt_nil!(string));
pub(crate) fn number(i: Bytes) -> VResult<Bytes, u32> { pub(crate) fn number(i: Bytes) -> VResult<Bytes, u32> {
map_res(take_while1(is_digit), parse_num::<_, u32>)(i) map_res(take_while1(is_digit), parse_num::<_, u32>)(i)
} }
rule!(pub nz_number : u32 => verify(number, |n| *n != 0)); rule!(pub nz_number : u32 => verify(number, |n| *n != 0));
@ -378,7 +378,7 @@ rule!(pub tag : Tag => map(take_while1(is_tag_char), Tag));
rule!(pub text : Bytes => map(take_while1(is_text_char), Bytes::from)); rule!(pub text : Bytes => map(take_while1(is_text_char), Bytes::from));
pub(crate) fn is_text_char(c: u8) -> bool { pub(crate) fn is_text_char(c: u8) -> bool {
is_char(c) && !is_cr(c) && !is_lf(c) is_char(c) && !is_cr(c) && !is_lf(c)
} }
rule!(pub TEXT_CHAR : u8 => satisfy(is_text_char)); rule!(pub TEXT_CHAR : u8 => satisfy(is_text_char));

View file

@ -10,114 +10,114 @@ use crate::proto::rfc3501::*;
#[test] #[test]
fn test_literal() { fn test_literal() {
assert_eq!( assert_eq!(
literal(Bytes::from(b"{13}\r\nHello, world!")) literal(Bytes::from(b"{13}\r\nHello, world!"))
.unwrap() .unwrap()
.1 .1
.as_ref(), .as_ref(),
b"Hello, world!" b"Hello, world!"
); );
} }
#[test] #[test]
fn from_afl() { fn from_afl() {
let _ = response(Bytes::from(b"* 4544444444 444 ")); let _ = response(Bytes::from(b"* 4544444444 444 "));
let _ = response(Bytes::from(b"* 8045 FETCH (UID 8225 ENVELOPE (\"Sun, 21 Mar 2021 18:44:10 -0700\" \"SUBJECT\" ((\"SENDER\" NIL \"sender\" \"example.com\")) ((\"SENDER\" NIL \"sender\" \"example.com\")) ((\"norepjy\" NIL \"noreply\" \"example.com\")) ((\"NAME\" NIL \"user\" \"gmail.com\")) NIL NIL NIL \"<Hmple.com>\") FLAGS () INTERNALDATE \"22-Mar-2021 01:64:12 \x7f0000\" RFC822.SIZE 13503)".to_vec())); let _ = response(Bytes::from(b"* 8045 FETCH (UID 8225 ENVELOPE (\"Sun, 21 Mar 2021 18:44:10 -0700\" \"SUBJECT\" ((\"SENDER\" NIL \"sender\" \"example.com\")) ((\"SENDER\" NIL \"sender\" \"example.com\")) ((\"norepjy\" NIL \"noreply\" \"example.com\")) ((\"NAME\" NIL \"user\" \"gmail.com\")) NIL NIL NIL \"<Hmple.com>\") FLAGS () INTERNALDATE \"22-Mar-2021 01:64:12 \x7f0000\" RFC822.SIZE 13503)".to_vec()));
} }
#[test] #[test]
fn test_date() { fn test_date() {
assert_eq!(date_year(Bytes::from(b"2021")).unwrap().1, 2021); assert_eq!(date_year(Bytes::from(b"2021")).unwrap().1, 2021);
assert_eq!( assert_eq!(
date_time(Bytes::from(b"\"22-Mar-2021 01:44:12 +0000\"")) date_time(Bytes::from(b"\"22-Mar-2021 01:44:12 +0000\""))
.unwrap() .unwrap()
.1, .1,
Timestamp(FixedOffset::east(0).ymd(2021, 3, 22).and_hms(1, 44, 12)), Timestamp(FixedOffset::east(0).ymd(2021, 3, 22).and_hms(1, 44, 12)),
); );
} }
#[test] #[test]
fn test_fetch() { fn test_fetch() {
assert!(flag_list(Bytes::from(b"()")).unwrap().1.is_empty()); assert!(flag_list(Bytes::from(b"()")).unwrap().1.is_empty());
use nom::Err; use nom::Err;
use panorama_proto_common::convert_error; use panorama_proto_common::convert_error;
let buf = Bytes::from(b"* 8045 FETCH (UID 8225 ENVELOPE (\"Sun, 21 Mar 2021 18:44:10 -0700\" \"SUBJECT\" ((\"SENDER\" NIL \"sender\" \"example.com\")) ((\"SENDER\" NIL \"sender\" \"example.com\")) ((\"noreply\" NIL \"noreply\" \"example.com\")) ((\"NAME\" NIL \"user\" \"gmail.com\")) NIL NIL NIL \"<HASH-99c91810@example.com>\") FLAGS () INTERNALDATE \"22-Mar-2021 01:44:12 +0000\" RFC822.SIZE 13503)\r\n".to_vec()); let buf = Bytes::from(b"* 8045 FETCH (UID 8225 ENVELOPE (\"Sun, 21 Mar 2021 18:44:10 -0700\" \"SUBJECT\" ((\"SENDER\" NIL \"sender\" \"example.com\")) ((\"SENDER\" NIL \"sender\" \"example.com\")) ((\"noreply\" NIL \"noreply\" \"example.com\")) ((\"NAME\" NIL \"user\" \"gmail.com\")) NIL NIL NIL \"<HASH-99c91810@example.com>\") FLAGS () INTERNALDATE \"22-Mar-2021 01:44:12 +0000\" RFC822.SIZE 13503)\r\n".to_vec());
let res = response(buf.clone()); let res = response(buf.clone());
println!("response: {:?}", res); println!("response: {:?}", res);
assert!(matches!(res.unwrap().1, Response::Fetch(8045, _))); assert!(matches!(res.unwrap().1, Response::Fetch(8045, _)));
} }
#[test] #[test]
fn test_capabilities() { fn test_capabilities() {
assert_eq!( assert_eq!(
capability(Bytes::from(b"UNSELECT\r\n")).unwrap().1, capability(Bytes::from(b"UNSELECT\r\n")).unwrap().1,
Capability::Atom(Bytes::from(b"UNSELECT")) Capability::Atom(Bytes::from(b"UNSELECT"))
); );
// trivial case // trivial case
assert_eq!( assert_eq!(
capability_data(Bytes::from(b"CAPABILITY IMAP4rev1\r\n")) capability_data(Bytes::from(b"CAPABILITY IMAP4rev1\r\n"))
.unwrap() .unwrap()
.1, .1,
vec![] vec![]
); );
assert_eq!( assert_eq!(
capability_data(Bytes::from( capability_data(Bytes::from(
b"CAPABILITY UNSELECT IMAP4rev1 NAMESPACE\r\n" b"CAPABILITY UNSELECT IMAP4rev1 NAMESPACE\r\n"
)) ))
.unwrap() .unwrap()
.1, .1,
vec![ vec![
Capability::Atom(Bytes::from(b"UNSELECT")), Capability::Atom(Bytes::from(b"UNSELECT")),
Capability::Atom(Bytes::from(b"NAMESPACE")) Capability::Atom(Bytes::from(b"NAMESPACE"))
] ]
); );
} }
#[test] #[test]
fn test_list() { fn test_list() {
assert!(matches!( assert!(matches!(
response(Bytes::from( response(Bytes::from(
b"* LIST (\\HasChildren \\UnMarked \\Trash) \".\" Trash\r\n", b"* LIST (\\HasChildren \\UnMarked \\Trash) \".\" Trash\r\n",
)) ))
.unwrap() .unwrap()
.1, .1,
Response::MailboxData(MailboxData::List(MailboxList { Response::MailboxData(MailboxData::List(MailboxList {
flags, flags,
delimiter: Some(b'.'), delimiter: Some(b'.'),
mailbox: Mailbox::Name(mailbox), mailbox: Mailbox::Name(mailbox),
}) ) if flags.len() == 3 && }) ) if flags.len() == 3 &&
flags.contains(&MailboxListFlag::Extension(Atom::from(b"HasChildren"))) && flags.contains(&MailboxListFlag::Extension(Atom::from(b"HasChildren"))) &&
flags.contains(&MailboxListFlag::Extension(Atom::from(b"UnMarked"))) && flags.contains(&MailboxListFlag::Extension(Atom::from(b"UnMarked"))) &&
flags.contains(&MailboxListFlag::Extension(Atom::from(b"Trash"))) && flags.contains(&MailboxListFlag::Extension(Atom::from(b"Trash"))) &&
&*mailbox == &b"Trash"[..] &*mailbox == &b"Trash"[..]
)); ));
} }
#[test] #[test]
fn test_gmail_is_shit() { fn test_gmail_is_shit() {
// FUCK YOU GMAIL! // FUCK YOU GMAIL!
let res = response(Bytes::from(b"* OK [HIGHESTMODSEQ 694968]\r\n")) let res = response(Bytes::from(b"* OK [HIGHESTMODSEQ 694968]\r\n"))
.unwrap() .unwrap()
.1; .1;
assert!(matches!(res, assert!(matches!(res,
Response::Condition(Condition { Response::Condition(Condition {
status: Status::Ok, status: Status::Ok,
code: Some(ResponseCode::Other(c, Some(d))), code: Some(ResponseCode::Other(c, Some(d))),
info: e, info: e,
}) })
if c == Bytes::from(b"HIGHESTMODSEQ") && d == Bytes::from(b"694968") && e.is_empty() if c == Bytes::from(b"HIGHESTMODSEQ") && d == Bytes::from(b"694968") && e.is_empty()
)); ));
let res = resp_text(Bytes::from(b"[PERMANENTFLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen $NotPhishing $Phishing \\*)] Flags permitted.\r".to_vec())).unwrap().1; let res = resp_text(Bytes::from(b"[PERMANENTFLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen $NotPhishing $Phishing \\*)] Flags permitted.\r".to_vec())).unwrap().1;
eprintln!("{:?}", res); eprintln!("{:?}", res);
eprintln!(); eprintln!();
let res = response(Bytes::from(b"* OK [PERMANENTFLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen $NotPhishing $Phishing \\*)] Flags permitted.\r\n".to_vec())).unwrap().1; let res = response(Bytes::from(b"* OK [PERMANENTFLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen $NotPhishing $Phishing \\*)] Flags permitted.\r\n".to_vec())).unwrap().1;
eprintln!("{:?}", res); eprintln!("{:?}", res);
assert!(matches!(res, Response::Condition(_))); assert!(matches!(res, Response::Condition(_)));
} }

View file

@ -3,8 +3,8 @@ use std::ops::{Deref, RangeBounds};
use format_bytes::DisplayBytes; use format_bytes::DisplayBytes;
use nom::{ use nom::{
error::{ErrorKind, ParseError}, error::{ErrorKind, ParseError},
CompareResult, Err, HexDisplay, IResult, InputLength, Needed, CompareResult, Err, HexDisplay, IResult, InputLength, Needed,
}; };
#[cfg(feature = "fuzzing")] #[cfg(feature = "fuzzing")]
@ -16,254 +16,254 @@ pub struct Bytes(bytes::Bytes);
#[cfg(feature = "fuzzing")] #[cfg(feature = "fuzzing")]
impl<'a> Arbitrary<'a> for Bytes { impl<'a> Arbitrary<'a> for Bytes {
fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> { fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
Ok(Bytes::from(<Vec<u8>>::arbitrary(u)?)) Ok(Bytes::from(<Vec<u8>>::arbitrary(u)?))
} }
} }
impl Bytes { impl Bytes {
/// Length of the internal `Bytes`. /// Length of the internal `Bytes`.
/// ///
/// # Example /// # Example
/// ///
/// ``` /// ```
/// # use panorama_proto_common::Bytes; /// # use panorama_proto_common::Bytes;
/// // the &, [..] is needed since &[u8; N] doesn't coerce automatically to &[u8] /// // the &, [..] is needed since &[u8; N] doesn't coerce automatically to &[u8]
/// let b = Bytes::from(&b"hello"[..]); /// let b = Bytes::from(&b"hello"[..]);
/// assert_eq!(b.len(), 5); /// assert_eq!(b.len(), 5);
/// ``` /// ```
pub fn len(&self) -> usize { self.0.len() } pub fn len(&self) -> usize { self.0.len() }
/// Consumes the wrapper, returning the original `Bytes`. /// Consumes the wrapper, returning the original `Bytes`.
pub fn inner(self) -> bytes::Bytes { self.0 } pub fn inner(self) -> bytes::Bytes { self.0 }
} }
impl DisplayBytes for Bytes { impl DisplayBytes for Bytes {
fn display_bytes(&self, w: &mut dyn Write) -> io::Result<()> { fn display_bytes(&self, w: &mut dyn Write) -> io::Result<()> {
w.write(&*self.0).map(|_| ()) w.write(&*self.0).map(|_| ())
} }
} }
static CHARS: &[u8] = b"0123456789abcdef"; static CHARS: &[u8] = b"0123456789abcdef";
impl HexDisplay for Bytes { impl HexDisplay for Bytes {
fn to_hex(&self, chunk_size: usize) -> String { fn to_hex(&self, chunk_size: usize) -> String {
self.to_hex_from(chunk_size, 0) self.to_hex_from(chunk_size, 0)
} }
fn to_hex_from(&self, chunk_size: usize, from: usize) -> String { fn to_hex_from(&self, chunk_size: usize, from: usize) -> String {
let mut v = Vec::with_capacity(self.len() * 3); let mut v = Vec::with_capacity(self.len() * 3);
let mut i = from; let mut i = from;
for chunk in self.chunks(chunk_size) { for chunk in self.chunks(chunk_size) {
let s = format!("{:08x}", i); let s = format!("{:08x}", i);
for &ch in s.as_bytes().iter() { for &ch in s.as_bytes().iter() {
v.push(ch); v.push(ch);
} }
v.push(b'\t'); v.push(b'\t');
i += chunk_size; i += chunk_size;
for &byte in chunk { for &byte in chunk {
v.push(CHARS[(byte >> 4) as usize]); v.push(CHARS[(byte >> 4) as usize]);
v.push(CHARS[(byte & 0xf) as usize]); v.push(CHARS[(byte & 0xf) as usize]);
v.push(b' '); v.push(b' ');
} }
if chunk_size > chunk.len() { if chunk_size > chunk.len() {
for _ in 0..(chunk_size - chunk.len()) { for _ in 0..(chunk_size - chunk.len()) {
v.push(b' '); v.push(b' ');
v.push(b' '); v.push(b' ');
v.push(b' '); v.push(b' ');
}
}
v.push(b'\t');
for &byte in chunk {
if (byte >= 32 && byte <= 126) || byte >= 128 {
v.push(byte);
} else {
v.push(b'.');
}
}
v.push(b'\n');
} }
}
v.push(b'\t');
String::from_utf8_lossy(&v[..]).into_owned() for &byte in chunk {
if (byte >= 32 && byte <= 126) || byte >= 128 {
v.push(byte);
} else {
v.push(b'.');
}
}
v.push(b'\n');
} }
String::from_utf8_lossy(&v[..]).into_owned()
}
} }
impl From<bytes::Bytes> for Bytes { impl From<bytes::Bytes> for Bytes {
fn from(b: bytes::Bytes) -> Self { Bytes(b) } fn from(b: bytes::Bytes) -> Self { Bytes(b) }
} }
impl From<&'static [u8]> for Bytes { impl From<&'static [u8]> for Bytes {
fn from(slice: &'static [u8]) -> Self { Bytes(bytes::Bytes::from(slice)) } fn from(slice: &'static [u8]) -> Self { Bytes(bytes::Bytes::from(slice)) }
} }
impl From<Vec<u8>> for Bytes { impl From<Vec<u8>> for Bytes {
fn from(slice: Vec<u8>) -> Self { Bytes(bytes::Bytes::from(slice)) } fn from(slice: Vec<u8>) -> Self { Bytes(bytes::Bytes::from(slice)) }
} }
impl From<&'static str> for Bytes { impl From<&'static str> for Bytes {
fn from(s: &'static str) -> Self { Bytes(bytes::Bytes::from(s.as_bytes())) } fn from(s: &'static str) -> Self { Bytes(bytes::Bytes::from(s.as_bytes())) }
} }
impl From<String> for Bytes { impl From<String> for Bytes {
fn from(slice: String) -> Self { Bytes(bytes::Bytes::from(slice)) } fn from(slice: String) -> Self { Bytes(bytes::Bytes::from(slice)) }
} }
pub trait ShitNeededForParsing: Sized { pub trait ShitNeededForParsing: Sized {
type Item; type Item;
type Sliced; type Sliced;
fn slice<R: RangeBounds<usize>>(&self, range: R) -> Self::Sliced; fn slice<R: RangeBounds<usize>>(&self, range: R) -> Self::Sliced;
fn first(&self) -> Option<Self::Item>; fn first(&self) -> Option<Self::Item>;
fn slice_index(&self, count: usize) -> Result<usize, Needed>; fn slice_index(&self, count: usize) -> Result<usize, Needed>;
// InputTake // InputTake
fn take(&self, count: usize) -> Self; fn take(&self, count: usize) -> Self;
fn take_split(&self, count: usize) -> (Self, Self); fn take_split(&self, count: usize) -> (Self, Self);
// InputTakeAtPosition // InputTakeAtPosition
fn split_at_position<P, E: ParseError<Self>>( fn split_at_position<P, E: ParseError<Self>>(
&self, &self,
predicate: P, predicate: P,
) -> IResult<Self, Self, E> ) -> IResult<Self, Self, E>
where where
P: Fn(Self::Item) -> bool; P: Fn(Self::Item) -> bool;
fn split_at_position1<P, E: ParseError<Self>>( fn split_at_position1<P, E: ParseError<Self>>(
&self, &self,
predicate: P, predicate: P,
e: ErrorKind, e: ErrorKind,
) -> IResult<Self, Self, E> ) -> IResult<Self, Self, E>
where where
P: Fn(Self::Item) -> bool; P: Fn(Self::Item) -> bool;
fn split_at_position_complete<P, E: ParseError<Self>>( fn split_at_position_complete<P, E: ParseError<Self>>(
&self, &self,
predicate: P, predicate: P,
) -> IResult<Self, Self, E> ) -> IResult<Self, Self, E>
where where
P: Fn(Self::Item) -> bool; P: Fn(Self::Item) -> bool;
fn split_at_position1_complete<P, E: ParseError<Self>>( fn split_at_position1_complete<P, E: ParseError<Self>>(
&self, &self,
predicate: P, predicate: P,
e: ErrorKind, e: ErrorKind,
) -> IResult<Self, Self, E> ) -> IResult<Self, Self, E>
where where
P: Fn(Self::Item) -> bool; P: Fn(Self::Item) -> bool;
} }
impl ShitNeededForParsing for Bytes { impl ShitNeededForParsing for Bytes {
type Item = u8; type Item = u8;
type Sliced = Bytes; type Sliced = Bytes;
fn slice<R: RangeBounds<usize>>(&self, range: R) -> Self::Sliced { fn slice<R: RangeBounds<usize>>(&self, range: R) -> Self::Sliced {
Self(self.0.slice(range)) Self(self.0.slice(range))
}
fn first(&self) -> Option<Self::Item> { self.0.first().copied() }
fn slice_index(&self, count: usize) -> Result<usize, Needed> {
if self.len() >= count {
Ok(count)
} else {
Err(Needed::new(count - self.len()))
} }
}
fn first(&self) -> Option<Self::Item> { self.0.first().copied() } // InputTake
fn slice_index(&self, count: usize) -> Result<usize, Needed> { fn take(&self, count: usize) -> Self { self.slice(..count) }
if self.len() >= count { fn take_split(&self, count: usize) -> (Self, Self) {
Ok(count) let mut prefix = self.clone();
let suffix = Self(prefix.0.split_off(count));
(suffix, prefix)
}
// InputTakeAtPosition
fn split_at_position<P, E: ParseError<Self>>(
&self,
predicate: P,
) -> IResult<Self, Self, E>
where
P: Fn(Self::Item) -> bool,
{
match (0..self.len()).find(|b| predicate(self[*b])) {
Some(i) => Ok((self.slice(i..), self.slice(..i))),
None => Err(Err::Incomplete(Needed::new(1))),
}
}
fn split_at_position1<P, E: ParseError<Self>>(
&self,
predicate: P,
e: ErrorKind,
) -> IResult<Self, Self, E>
where
P: Fn(Self::Item) -> bool,
{
match (0..self.len()).find(|b| predicate(self[*b])) {
Some(0) => Err(Err::Error(E::from_error_kind(self.clone(), e))),
Some(i) => Ok((self.slice(i..), self.slice(..i))),
None => Err(Err::Incomplete(Needed::new(1))),
}
}
fn split_at_position_complete<P, E: ParseError<Self>>(
&self,
predicate: P,
) -> IResult<Self, Self, E>
where
P: Fn(Self::Item) -> bool,
{
match (0..self.len()).find(|b| predicate(self[*b])) {
Some(i) => Ok((self.slice(i..), self.slice(..i))),
None => Ok(self.take_split(self.input_len())),
}
}
fn split_at_position1_complete<P, E: ParseError<Self>>(
&self,
predicate: P,
e: ErrorKind,
) -> IResult<Self, Self, E>
where
P: Fn(Self::Item) -> bool,
{
match (0..self.len()).find(|b| predicate(self[*b])) {
Some(0) => Err(Err::Error(E::from_error_kind(self.clone(), e))),
Some(i) => Ok((self.slice(i..), self.slice(..i))),
None => {
if self.is_empty() {
Err(Err::Error(E::from_error_kind(self.clone(), e)))
} else { } else {
Err(Needed::new(count - self.len())) Ok(self.take_split(self.input_len()))
}
}
// InputTake
fn take(&self, count: usize) -> Self { self.slice(..count) }
fn take_split(&self, count: usize) -> (Self, Self) {
let mut prefix = self.clone();
let suffix = Self(prefix.0.split_off(count));
(suffix, prefix)
}
// InputTakeAtPosition
fn split_at_position<P, E: ParseError<Self>>(
&self,
predicate: P,
) -> IResult<Self, Self, E>
where
P: Fn(Self::Item) -> bool,
{
match (0..self.len()).find(|b| predicate(self[*b])) {
Some(i) => Ok((self.slice(i..), self.slice(..i))),
None => Err(Err::Incomplete(Needed::new(1))),
}
}
fn split_at_position1<P, E: ParseError<Self>>(
&self,
predicate: P,
e: ErrorKind,
) -> IResult<Self, Self, E>
where
P: Fn(Self::Item) -> bool,
{
match (0..self.len()).find(|b| predicate(self[*b])) {
Some(0) => Err(Err::Error(E::from_error_kind(self.clone(), e))),
Some(i) => Ok((self.slice(i..), self.slice(..i))),
None => Err(Err::Incomplete(Needed::new(1))),
}
}
fn split_at_position_complete<P, E: ParseError<Self>>(
&self,
predicate: P,
) -> IResult<Self, Self, E>
where
P: Fn(Self::Item) -> bool,
{
match (0..self.len()).find(|b| predicate(self[*b])) {
Some(i) => Ok((self.slice(i..), self.slice(..i))),
None => Ok(self.take_split(self.input_len())),
}
}
fn split_at_position1_complete<P, E: ParseError<Self>>(
&self,
predicate: P,
e: ErrorKind,
) -> IResult<Self, Self, E>
where
P: Fn(Self::Item) -> bool,
{
match (0..self.len()).find(|b| predicate(self[*b])) {
Some(0) => Err(Err::Error(E::from_error_kind(self.clone(), e))),
Some(i) => Ok((self.slice(i..), self.slice(..i))),
None => {
if self.is_empty() {
Err(Err::Error(E::from_error_kind(self.clone(), e)))
} else {
Ok(self.take_split(self.input_len()))
}
}
} }
}
} }
}
} }
pub trait ShitCompare<T> { pub trait ShitCompare<T> {
fn compare(&self, t: T) -> CompareResult; fn compare(&self, t: T) -> CompareResult;
fn compare_no_case(&self, t: T) -> CompareResult; fn compare_no_case(&self, t: T) -> CompareResult;
} }
impl ShitCompare<&[u8]> for Bytes { impl ShitCompare<&[u8]> for Bytes {
fn compare(&self, other: &[u8]) -> CompareResult { fn compare(&self, other: &[u8]) -> CompareResult {
match self.iter().zip(other.iter()).any(|(a, b)| a != b) { match self.iter().zip(other.iter()).any(|(a, b)| a != b) {
true => CompareResult::Error, true => CompareResult::Error,
false if self.len() < other.len() => CompareResult::Incomplete, false if self.len() < other.len() => CompareResult::Incomplete,
false => CompareResult::Ok, false => CompareResult::Ok,
}
} }
fn compare_no_case(&self, other: &[u8]) -> CompareResult { }
match self fn compare_no_case(&self, other: &[u8]) -> CompareResult {
.iter() match self
.zip(other.iter()) .iter()
.any(|(a, b)| (a | 0x20) != (b | 0x20)) .zip(other.iter())
{ .any(|(a, b)| (a | 0x20) != (b | 0x20))
true => CompareResult::Error, {
false if self.len() < other.len() => CompareResult::Incomplete, true => CompareResult::Error,
false => CompareResult::Ok, false if self.len() < other.len() => CompareResult::Incomplete,
} false => CompareResult::Ok,
} }
}
} }
macro_rules! array_impls { macro_rules! array_impls {
@ -308,14 +308,14 @@ array_impls! {
} }
impl Deref for Bytes { impl Deref for Bytes {
type Target = [u8]; type Target = [u8];
fn deref(&self) -> &Self::Target { &*self.0 } fn deref(&self) -> &Self::Target { &*self.0 }
} }
impl AsRef<[u8]> for Bytes { impl AsRef<[u8]> for Bytes {
fn as_ref(&self) -> &[u8] { &*self.0 } fn as_ref(&self) -> &[u8] { &*self.0 }
} }
impl InputLength for Bytes { impl InputLength for Bytes {
fn input_len(&self) -> usize { self.0.len() } fn input_len(&self) -> usize { self.0.len() }
} }

View file

@ -4,155 +4,146 @@ use std::ops::Deref;
use bstr::ByteSlice; use bstr::ByteSlice;
use nom::{ use nom::{
error::{VerboseError, VerboseErrorKind}, error::{VerboseError, VerboseErrorKind},
Err, HexDisplay, Offset, Err, HexDisplay, Offset,
}; };
use crate::VResult; use crate::VResult;
/// Same as nom's dbg_dmp, except operates on Bytes /// Same as nom's dbg_dmp, except operates on Bytes
pub fn dbg_dmp<'a, T, F, O>( pub fn dbg_dmp<'a, T, F, O>(
mut f: F, mut f: F,
context: &'static str, context: &'static str,
) -> impl FnMut(T) -> VResult<T, O> ) -> impl FnMut(T) -> VResult<T, O>
where where
F: FnMut(T) -> VResult<T, O>, F: FnMut(T) -> VResult<T, O>,
T: AsRef<[u8]> + HexDisplay + Clone + Debug + Deref<Target = [u8]>, T: AsRef<[u8]> + HexDisplay + Clone + Debug + Deref<Target = [u8]>,
{ {
move |i: T| match f(i.clone()) { move |i: T| match f(i.clone()) {
Err(Err::Failure(e)) => { Err(Err::Failure(e)) => {
println!( println!(
"{}: Error({}) at:\n{}", "{}: Error({}) at:\n{}",
context, context,
convert_error(i.clone(), &e), convert_error(i.clone(), &e),
i.to_hex(16) i.to_hex(16)
); );
Err(Err::Failure(e)) Err(Err::Failure(e))
}
a => a,
} }
a => a,
}
} }
/// Same as nom's convert_error, except operates on u8 /// Same as nom's convert_error, except operates on u8
pub fn convert_error<I: Deref<Target = [u8]> + Debug>( pub fn convert_error<I: Deref<Target = [u8]> + Debug>(
input: I, input: I,
e: &VerboseError<I>, e: &VerboseError<I>,
) -> String { ) -> String {
let mut result = String::new(); let mut result = String::new();
debug!("e: {:?}", e); debug!("e: {:?}", e);
for (i, (substring, kind)) in e.errors.iter().enumerate() { for (i, (substring, kind)) in e.errors.iter().enumerate() {
let offset = input.offset(substring); let offset = input.offset(substring);
if input.is_empty() { if input.is_empty() {
match kind { match kind {
VerboseErrorKind::Char(c) => { VerboseErrorKind::Char(c) => {
write!( write!(&mut result, "{}: expected '{}', got empty input\n\n", i, c)
&mut result, }
"{}: expected '{}', got empty input\n\n", VerboseErrorKind::Context(s) => {
i, c write!(&mut result, "{}: in {}, got empty input\n\n", i, s)
) }
} VerboseErrorKind::Nom(e) => {
VerboseErrorKind::Context(s) => { write!(&mut result, "{}: in {:?}, got empty input\n\n", i, e)
write!(&mut result, "{}: in {}, got empty input\n\n", i, s) }
} }
VerboseErrorKind::Nom(e) => { } else {
write!( let prefix = &input.as_bytes()[..offset];
&mut result,
"{}: in {:?}, got empty input\n\n",
i, e
)
}
}
} else {
let prefix = &input.as_bytes()[..offset];
// Count the number of newlines in the first `offset` bytes of input // Count the number of newlines in the first `offset` bytes of input
let line_number = let line_number = prefix.iter().filter(|&&b| b == b'\n').count() + 1;
prefix.iter().filter(|&&b| b == b'\n').count() + 1;
// Find the line that includes the subslice: // Find the line that includes the subslice:
// Find the *last* newline before the substring starts // Find the *last* newline before the substring starts
let line_begin = prefix let line_begin = prefix
.iter() .iter()
.rev() .rev()
.position(|&b| b == b'\n') .position(|&b| b == b'\n')
.map(|pos| offset - pos) .map(|pos| offset - pos)
.unwrap_or(0); .unwrap_or(0);
// Find the full line after that newline // Find the full line after that newline
let line = input[line_begin..] let line = input[line_begin..]
.lines() .lines()
.next() .next()
.unwrap_or(&input[line_begin..]) .unwrap_or(&input[line_begin..])
.trim_end(); .trim_end();
// The (1-indexed) column number is the offset of our substring into // The (1-indexed) column number is the offset of our substring into
// that line // that line
let column_number = line.offset(substring) + 1; let column_number = line.offset(substring) + 1;
match kind { match kind {
VerboseErrorKind::Char(c) => { VerboseErrorKind::Char(c) => {
if let Some(actual) = substring.chars().next() { if let Some(actual) = substring.chars().next() {
write!( write!(
&mut result, &mut result,
"{i}: at line {line_number}:\n\ "{i}: at line {line_number}:\n\
{line}\n\ {line}\n\
{caret:>column$}\n\ {caret:>column$}\n\
expected '{expected}', found {actual}\n\n", expected '{expected}', found {actual}\n\n",
i = i, i = i,
line_number = line_number, line_number = line_number,
line = String::from_utf8_lossy(line), line = String::from_utf8_lossy(line),
caret = '^', caret = '^',
column = column_number, column = column_number,
expected = c, expected = c,
actual = actual, actual = actual,
) )
} else { } else {
write!( write!(
&mut result, &mut result,
"{i}: at line {line_number}:\n\ "{i}: at line {line_number}:\n\
{line}\n\ {line}\n\
{caret:>column$}\n\ {caret:>column$}\n\
expected '{expected}', got end of input\n\n", expected '{expected}', got end of input\n\n",
i = i, i = i,
line_number = line_number, line_number = line_number,
line = String::from_utf8_lossy(line), line = String::from_utf8_lossy(line),
caret = '^', caret = '^',
column = column_number, column = column_number,
expected = c, expected = c,
) )
} }
}
VerboseErrorKind::Context(s) => write!(
&mut result,
"{i}: at line {line_number}, in {context}:\n\
{line}\n\
{caret:>column$}\n\n",
i = i,
line_number = line_number,
context = s,
line = String::from_utf8_lossy(line),
caret = '^',
column = column_number,
),
VerboseErrorKind::Nom(e) => write!(
&mut result,
"{i}: at line {line_number}, in {nom_err:?}:\n\
{line}\n\
{caret:>column$}\n\n",
i = i,
line_number = line_number,
nom_err = e,
line = String::from_utf8_lossy(line),
caret = '^',
column = column_number,
),
}
} }
// Because `write!` to a `String` is infallible, this `unwrap` is fine. VerboseErrorKind::Context(s) => write!(
.unwrap(); &mut result,
"{i}: at line {line_number}, in {context}:\n\
{line}\n\
{caret:>column$}\n\n",
i = i,
line_number = line_number,
context = s,
line = String::from_utf8_lossy(line),
caret = '^',
column = column_number,
),
VerboseErrorKind::Nom(e) => write!(
&mut result,
"{i}: at line {line_number}, in {nom_err:?}:\n\
{line}\n\
{caret:>column$}\n\n",
i = i,
line_number = line_number,
nom_err = e,
line = String::from_utf8_lossy(line),
caret = '^',
column = column_number,
),
}
} }
// Because `write!` to a `String` is infallible, this `unwrap` is fine.
.unwrap();
}
result result
} }

View file

@ -8,27 +8,27 @@
/// assert_eq!(quote(b"hello \"' world"), b"\"hello \\\"\\' world\""); /// assert_eq!(quote(b"hello \"' world"), b"\"hello \\\"\\' world\"");
/// ``` /// ```
pub fn quote_string<B, F>( pub fn quote_string<B, F>(
quote: u8, quote: u8,
escape: u8, escape: u8,
should_escape: F, should_escape: F,
) -> impl Fn(B) -> Vec<u8> ) -> impl Fn(B) -> Vec<u8>
where where
B: AsRef<[u8]>, B: AsRef<[u8]>,
F: Fn(u8) -> bool, F: Fn(u8) -> bool,
{ {
move |input: B| { move |input: B| {
let input = input.as_ref(); let input = input.as_ref();
let mut ret = Vec::with_capacity(input.len() + 2); let mut ret = Vec::with_capacity(input.len() + 2);
ret.push(quote); ret.push(quote);
for c in input { for c in input {
if should_escape(*c) { if should_escape(*c) {
ret.push(escape); ret.push(escape);
} }
ret.push(*c); ret.push(*c);
}
ret.push(quote);
ret
} }
ret.push(quote);
ret
}
} }

View file

@ -13,5 +13,5 @@ pub use crate::bytes::{Bytes, ShitCompare, ShitNeededForParsing};
pub use crate::convert_error::{convert_error, dbg_dmp}; pub use crate::convert_error::{convert_error, dbg_dmp};
pub use crate::formatter::quote_string; pub use crate::formatter::quote_string;
pub use crate::parsers::{ pub use crate::parsers::{
byte, never, parse_num, satisfy, skip, tagi, take, take_while1, VResult, byte, never, parse_num, satisfy, skip, tagi, take, take_while1, VResult,
}; };

View file

@ -1,7 +1,7 @@
use anyhow::Result; use anyhow::Result;
use nom::{ use nom::{
error::{ErrorKind, ParseError, VerboseError}, error::{ErrorKind, ParseError, VerboseError},
CompareResult, Err, IResult, InputLength, Needed, Parser, ToUsize, CompareResult, Err, IResult, InputLength, Needed, Parser, ToUsize,
}; };
use num_traits::{CheckedAdd, CheckedMul, FromPrimitive, Zero}; use num_traits::{CheckedAdd, CheckedMul, FromPrimitive, Zero};
@ -19,83 +19,85 @@ pub type VResult<I, O> = IResult<I, O, VerboseError<I>>;
/// If `d` is not passed then it defaults to `SP`. /// If `d` is not passed then it defaults to `SP`.
#[macro_export] #[macro_export]
macro_rules! sep_list { macro_rules! sep_list {
($t:expr) => { ($t:expr) => {
map( map(
pair( pair(
$t, $t,
many0(preceded(panorama_proto_common::byte(b'\x20'), $t)), many0(preceded(panorama_proto_common::byte(b'\x20'), $t)),
), ),
|(hd, mut tl)| { |(hd, mut tl)| {
tl.insert(0, hd); tl.insert(0, hd);
tl tl
}, },
) )
}; };
($t:expr, $d:expr) => { ($t:expr, $d:expr) => {
map(pair($t, many0(preceded($d, $t))), |(hd, mut tl)| { map(pair($t, many0(preceded($d, $t))), |(hd, mut tl)| {
tl.insert(0, hd);
tl
})
};
(? $t:expr) => {
map(
opt(pair(
$t,
many0(preceded(panorama_proto_common::byte(b'\x20'), $t)),
)),
|opt| {
opt
.map(|(hd, mut tl)| {
tl.insert(0, hd); tl.insert(0, hd);
tl tl
})
.unwrap_or_else(Vec::new)
},
)
};
(? $t:expr, $d:expr) => {
map(opt(pair($t, many0(preceded($d, $t)))), |opt| {
opt
.map(|(hd, mut tl)| {
tl.insert(0, hd);
tl
}) })
}; .unwrap_or_else(Vec::new)
(? $t:expr) => { })
map( };
opt(pair(
$t,
many0(preceded(panorama_proto_common::byte(b'\x20'), $t)),
)),
|opt| {
opt.map(|(hd, mut tl)| {
tl.insert(0, hd);
tl
})
.unwrap_or_else(Vec::new)
},
)
};
(? $t:expr, $d:expr) => {
map(opt(pair($t, many0(preceded($d, $t)))), |opt| {
opt.map(|(hd, mut tl)| {
tl.insert(0, hd);
tl
})
.unwrap_or_else(Vec::new)
})
};
} }
/// Helper macro for wrapping a parser in parentheses. /// Helper macro for wrapping a parser in parentheses.
#[macro_export] #[macro_export]
macro_rules! paren { macro_rules! paren {
($t:expr) => { ($t:expr) => {
delimited(byte(b'('), $t, byte(b')')) delimited(byte(b'('), $t, byte(b')'))
}; };
} }
/// Parse from a [u8] into a u32 without first decoding it to UTF-8. /// Parse from a [u8] into a u32 without first decoding it to UTF-8.
pub fn parse_num<S, T>(s: S) -> Result<T> pub fn parse_num<S, T>(s: S) -> Result<T>
where where
S: AsRef<[u8]>, S: AsRef<[u8]>,
T: CheckedMul + Zero + CheckedAdd + FromPrimitive, T: CheckedMul + Zero + CheckedAdd + FromPrimitive,
{ {
let mut total = T::zero(); let mut total = T::zero();
let ten = T::from_u8(10).unwrap(); let ten = T::from_u8(10).unwrap();
let s = s.as_ref(); let s = s.as_ref();
for digit in s.iter() { for digit in s.iter() {
let digit = *digit; let digit = *digit;
total = match total.checked_mul(&ten) { total = match total.checked_mul(&ten) {
Some(v) => v, Some(v) => v,
None => bail!("number {:?} overflow", s), None => bail!("number {:?} overflow", s),
}; };
if !(digit >= b'0' && digit <= b'9') { if !(digit >= b'0' && digit <= b'9') {
bail!("invalid digit {}", digit) bail!("invalid digit {}", digit)
}
let new_digit = T::from_u8(digit - b'\x30').unwrap();
total = match total.checked_add(&new_digit) {
Some(v) => v,
None => bail!("number {:?} overflow", s),
};
} }
Ok(total) let new_digit = T::from_u8(digit - b'\x30').unwrap();
total = match total.checked_add(&new_digit) {
Some(v) => v,
None => bail!("number {:?} overflow", s),
};
}
Ok(total)
} }
// /// Parse from a [u8] into a u32 without first decoding it to UTF-8. // /// Parse from a [u8] into a u32 without first decoding it to UTF-8.
@ -119,96 +121,96 @@ where
/// Always fails, used as a no-op. /// Always fails, used as a no-op.
pub fn never<I, O, E>(i: I) -> IResult<I, O, E> pub fn never<I, O, E>(i: I) -> IResult<I, O, E>
where where
E: ParseError<I>, E: ParseError<I>,
{ {
Err(Err::Error(E::from_error_kind(i, ErrorKind::Not))) Err(Err::Error(E::from_error_kind(i, ErrorKind::Not)))
} }
/// Skip the part of the input matched by the given parser. /// Skip the part of the input matched by the given parser.
pub fn skip<E, F, I, O>(mut f: F) -> impl FnMut(I) -> IResult<I, (), E> pub fn skip<E, F, I, O>(mut f: F) -> impl FnMut(I) -> IResult<I, (), E>
where where
I: Clone, I: Clone,
F: Parser<I, O, E>, F: Parser<I, O, E>,
{ {
move |i: I| match f.parse(i.clone()) { move |i: I| match f.parse(i.clone()) {
Ok(_) => Ok((i, ())), Ok(_) => Ok((i, ())),
Err(err) => Err(err), Err(err) => Err(err),
} }
} }
/// Same as nom's streaming take, but operates on Bytes /// Same as nom's streaming take, but operates on Bytes
pub fn take<C, I, E>(count: C) -> impl Fn(I) -> IResult<I, I, E> pub fn take<C, I, E>(count: C) -> impl Fn(I) -> IResult<I, I, E>
where where
I: ShitNeededForParsing + InputLength, I: ShitNeededForParsing + InputLength,
E: ParseError<I>, E: ParseError<I>,
C: ToUsize, C: ToUsize,
{ {
let c = count.to_usize(); let c = count.to_usize();
move |i: I| match i.slice_index(c) { move |i: I| match i.slice_index(c) {
Err(i) => Err(Err::Incomplete(i)), Err(i) => Err(Err::Incomplete(i)),
Ok(index) => Ok(i.take_split(index)), Ok(index) => Ok(i.take_split(index)),
} }
} }
/// Same as nom's streaming take_while1, but operates on Bytes /// Same as nom's streaming take_while1, but operates on Bytes
pub fn take_while1<F, I, E, T>(cond: F) -> impl Fn(I) -> IResult<I, I, E> pub fn take_while1<F, I, E, T>(cond: F) -> impl Fn(I) -> IResult<I, I, E>
where where
I: ShitNeededForParsing<Item = T>, I: ShitNeededForParsing<Item = T>,
E: ParseError<I>, E: ParseError<I>,
F: Fn(T) -> bool, F: Fn(T) -> bool,
{ {
move |i: I| { move |i: I| {
let e: ErrorKind = ErrorKind::TakeWhile1; let e: ErrorKind = ErrorKind::TakeWhile1;
i.split_at_position1(|c| !cond(c), e) i.split_at_position1(|c| !cond(c), e)
} }
} }
/// Tag (case-)Insensitive: Same as nom's tag_no_case, but operates on Bytes /// Tag (case-)Insensitive: Same as nom's tag_no_case, but operates on Bytes
pub fn tagi<T, I, E>(tag: T) -> impl Fn(I) -> IResult<I, I, E> pub fn tagi<T, I, E>(tag: T) -> impl Fn(I) -> IResult<I, I, E>
where where
I: ShitNeededForParsing<Sliced = I> + InputLength + ShitCompare<T>, I: ShitNeededForParsing<Sliced = I> + InputLength + ShitCompare<T>,
T: InputLength + Clone, T: InputLength + Clone,
E: ParseError<I>, E: ParseError<I>,
{ {
let tag_len = tag.input_len(); let tag_len = tag.input_len();
move |i: I| match i.compare_no_case(tag.clone()) { move |i: I| match i.compare_no_case(tag.clone()) {
CompareResult::Ok => { CompareResult::Ok => {
let fst = i.slice(0..tag_len); let fst = i.slice(0..tag_len);
let snd = i.slice(tag_len..); let snd = i.slice(tag_len..);
Ok((snd, fst)) Ok((snd, fst))
}
CompareResult::Incomplete => {
Err(Err::Incomplete(Needed::new(tag_len - i.input_len())))
}
CompareResult::Error => {
let e: ErrorKind = ErrorKind::Tag;
Err(Err::Error(E::from_error_kind(i, e)))
}
} }
CompareResult::Incomplete => {
Err(Err::Incomplete(Needed::new(tag_len - i.input_len())))
}
CompareResult::Error => {
let e: ErrorKind = ErrorKind::Tag;
Err(Err::Error(E::from_error_kind(i, e)))
}
}
} }
/// Same as nom's satisfy, but operates on `Bytes` instead of `&str`. /// Same as nom's satisfy, but operates on `Bytes` instead of `&str`.
pub fn satisfy<F, I, E, T>(f: F) -> impl Fn(I) -> IResult<I, T, E> pub fn satisfy<F, I, E, T>(f: F) -> impl Fn(I) -> IResult<I, T, E>
where where
I: ShitNeededForParsing<Item = T, Sliced = I>, I: ShitNeededForParsing<Item = T, Sliced = I>,
F: Fn(T) -> bool, F: Fn(T) -> bool,
E: ParseError<I>, E: ParseError<I>,
T: Copy, T: Copy,
{ {
move |i: I| match i.first().map(|t| (f(t), t)) { move |i: I| match i.first().map(|t| (f(t), t)) {
Some((true, ft)) => Ok((i.slice(1..), ft)), Some((true, ft)) => Ok((i.slice(1..), ft)),
Some((false, _)) => Err(Err::Error(E::from_error_kind( Some((false, _)) => Err(Err::Error(E::from_error_kind(
i.slice(1..), i.slice(1..),
ErrorKind::Satisfy, ErrorKind::Satisfy,
))), ))),
None => Err(Err::Incomplete(Needed::Unknown)), None => Err(Err::Incomplete(Needed::Unknown)),
} }
} }
/// Match a single byte exactly. /// Match a single byte exactly.
pub fn byte<I, E: ParseError<I>>(b: u8) -> impl Fn(I) -> IResult<I, u8, E> pub fn byte<I, E: ParseError<I>>(b: u8) -> impl Fn(I) -> IResult<I, u8, E>
where where
I: ShitNeededForParsing<Item = u8, Sliced = I>, I: ShitNeededForParsing<Item = u8, Sliced = I>,
{ {
satisfy(move |c| c == b) satisfy(move |c| c == b)
} }

View file

@ -1,3 +1,4 @@
fn_single_line = true fn_single_line = true
max_width = 80 max_width = 80
wrap_comments = true wrap_comments = true
tab_spaces = 2

View file

@ -1,3 +1,3 @@
fn main() { fn main() {
println!("Hello, world!"); println!("Hello, world!");
} }