Change to 2 spaces
This commit is contained in:
parent
1ab69aadb9
commit
ff18e98eff
24 changed files with 2051 additions and 2088 deletions
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(())
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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};
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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:*"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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),
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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};
|
||||||
|
|
||||||
|
|
|
@ -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));
|
||||||
|
|
||||||
|
|
|
@ -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(_)));
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() }
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
fn main() {
|
fn main() {
|
||||||
println!("Hello, world!");
|
println!("Hello, world!");
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue