had some trouble with capability matching IMAP4rev1 as an atom
This commit is contained in:
parent
ff98685afb
commit
b0c8423968
19 changed files with 219 additions and 56 deletions
75
Cargo.lock
generated
75
Cargo.lock
generated
|
@ -2,6 +2,15 @@
|
||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 3
|
version = 3
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aho-corasick"
|
||||||
|
version = "0.7.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anyhow"
|
name = "anyhow"
|
||||||
version = "1.0.43"
|
version = "1.0.43"
|
||||||
|
@ -222,6 +231,19 @@ dependencies = [
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "env_logger"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3"
|
||||||
|
dependencies = [
|
||||||
|
"atty",
|
||||||
|
"humantime",
|
||||||
|
"log",
|
||||||
|
"regex",
|
||||||
|
"termcolor",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fnv"
|
name = "fnv"
|
||||||
version = "1.0.7"
|
version = "1.0.7"
|
||||||
|
@ -427,6 +449,12 @@ version = "1.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6456b8a6c8f33fee7d958fcd1b60d55b11940a79e63ae87013e6d22e26034440"
|
checksum = "6456b8a6c8f33fee7d958fcd1b60d55b11940a79e63ae87013e6d22e26034440"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "humantime"
|
||||||
|
version = "2.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper"
|
name = "hyper"
|
||||||
version = "0.14.11"
|
version = "0.14.11"
|
||||||
|
@ -555,19 +583,6 @@ dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "mbsync"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"anyhow",
|
|
||||||
"bitflags",
|
|
||||||
"clap",
|
|
||||||
"derivative",
|
|
||||||
"derive_builder",
|
|
||||||
"panorama-imap",
|
|
||||||
"tokio",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.3.4"
|
version = "2.3.4"
|
||||||
|
@ -690,6 +705,7 @@ dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"bytes",
|
"bytes",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"derivative",
|
||||||
"derive_builder",
|
"derive_builder",
|
||||||
"format-bytes",
|
"format-bytes",
|
||||||
"futures",
|
"futures",
|
||||||
|
@ -703,6 +719,22 @@ dependencies = [
|
||||||
"webpki-roots",
|
"webpki-roots",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "panorama-mbsync"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"bitflags",
|
||||||
|
"clap",
|
||||||
|
"derivative",
|
||||||
|
"derive_builder",
|
||||||
|
"env_logger",
|
||||||
|
"log",
|
||||||
|
"panorama-imap",
|
||||||
|
"serde",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "panorama-proto-common"
|
name = "panorama-proto-common"
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
|
@ -829,12 +861,29 @@ dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex"
|
||||||
|
version = "1.4.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2a26af418b574bd56588335b3a3659a65725d4e636eb1016c2f9e3b38c7cc759"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex-automata"
|
name = "regex-automata"
|
||||||
version = "0.1.10"
|
version = "0.1.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
|
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-syntax"
|
||||||
|
version = "0.6.25"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ring"
|
name = "ring"
|
||||||
version = "0.16.20"
|
version = "0.16.20"
|
||||||
|
|
|
@ -25,6 +25,7 @@ async-trait = "0.1.51"
|
||||||
bitflags = "1.2.1"
|
bitflags = "1.2.1"
|
||||||
bytes = "1.0.1"
|
bytes = "1.0.1"
|
||||||
chrono = "0.4.19"
|
chrono = "0.4.19"
|
||||||
|
derivative = "2.2.0"
|
||||||
derive_builder = "0.10.2"
|
derive_builder = "0.10.2"
|
||||||
format-bytes = "0.2.2"
|
format-bytes = "0.2.2"
|
||||||
futures = "0.3.16"
|
futures = "0.3.16"
|
||||||
|
|
|
@ -24,7 +24,7 @@ use crate::proto::{
|
||||||
use super::auth::AuthMethod;
|
use super::auth::AuthMethod;
|
||||||
use super::inner::Inner;
|
use super::inner::Inner;
|
||||||
use super::response_stream::ResponseStream;
|
use super::response_stream::ResponseStream;
|
||||||
use super::upgrade::upgrade;
|
use super::tls::wrap_tls;
|
||||||
|
|
||||||
/// An IMAP client that hasn't been connected yet.
|
/// An IMAP client that hasn't been connected yet.
|
||||||
#[derive(Builder, Clone, Debug)]
|
#[derive(Builder, Clone, Debug)]
|
||||||
|
@ -57,10 +57,12 @@ impl ConfigBuilder {
|
||||||
|
|
||||||
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);
|
||||||
let conn = TcpStream::connect((hostname, port)).await?;
|
let conn = TcpStream::connect((hostname, port)).await?;
|
||||||
|
trace!("connected.");
|
||||||
|
|
||||||
if config.tls {
|
if config.tls {
|
||||||
let conn = upgrade(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?;
|
||||||
|
|
|
@ -28,14 +28,31 @@ impl<'a> Decoder for ImapCodec {
|
||||||
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();
|
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();
|
let buf3 = buf2.clone().freeze();
|
||||||
debug!("going to parse a response since buffer len: {}", buf3.len());
|
debug!("going to parse a response since buffer len: {}", buf3.len());
|
||||||
// trace!("buf: {:?}", buf3);
|
// 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: Bytes = buf3.clone().into();
|
||||||
let buf4_len = buf4.len();
|
let buf4_len = buf4.len();
|
||||||
let (response, len) = match parse_response(buf4) {
|
let (response, len) = match parse_response(buf4) {
|
||||||
Ok((remaining, response)) => (response, buf4_len - remaining.len()),
|
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))) => {
|
Err(nom::Err::Incomplete(Needed::Size(min))) => {
|
||||||
self.decode_need_message_bytes = min.get();
|
self.decode_need_message_bytes = min.get();
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
|
@ -43,6 +60,8 @@ impl<'a> Decoder for ImapCodec {
|
||||||
Err(nom::Err::Incomplete(_)) => {
|
Err(nom::Err::Incomplete(_)) => {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// shit
|
||||||
Err(Err::Error(err)) | Err(Err::Failure(err)) => {
|
Err(Err::Error(err)) | Err(Err::Failure(err)) => {
|
||||||
let buf4 = buf3.clone().into();
|
let buf4 = buf3.clone().into();
|
||||||
error!("failed to parse: {:?}", buf4);
|
error!("failed to parse: {:?}", buf4);
|
||||||
|
@ -55,10 +74,14 @@ impl<'a> Decoder for ImapCodec {
|
||||||
};
|
};
|
||||||
|
|
||||||
info!("success, parsed as {:?}", response);
|
info!("success, parsed as {:?}", response);
|
||||||
|
// "unsplit" is the opposite of split, we're getting back the original data here
|
||||||
buf.unsplit(buf2);
|
buf.unsplit(buf2);
|
||||||
|
|
||||||
|
// and then move to after the message we just parsed
|
||||||
let _ = buf.split_to(len);
|
let _ = buf.split_to(len);
|
||||||
debug!("buf: {:?}", buf);
|
debug!("buf: {:?}", buf);
|
||||||
|
|
||||||
|
// since we're done parsing a complete message, set this to zero
|
||||||
self.decode_need_message_bytes = 0;
|
self.decode_need_message_bytes = 0;
|
||||||
Ok(Some(response))
|
Ok(Some(response))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
use std::sync::atomic::{AtomicU32, Ordering};
|
use std::collections::HashSet;
|
||||||
|
use std::sync::{
|
||||||
|
atomic::{AtomicU32, Ordering},
|
||||||
|
Arc,
|
||||||
|
};
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use futures::{
|
use futures::{
|
||||||
|
@ -8,7 +12,7 @@ use futures::{
|
||||||
use panorama_proto_common::Bytes;
|
use panorama_proto_common::Bytes;
|
||||||
use tokio::{
|
use tokio::{
|
||||||
io::{split, AsyncRead, AsyncWrite, AsyncWriteExt, BufWriter, ReadHalf, WriteHalf},
|
io::{split, AsyncRead, AsyncWrite, AsyncWriteExt, BufWriter, ReadHalf, WriteHalf},
|
||||||
sync::{mpsc, oneshot},
|
sync::{mpsc, oneshot, RwLock},
|
||||||
task::JoinHandle,
|
task::JoinHandle,
|
||||||
};
|
};
|
||||||
use tokio_rustls::client::TlsStream;
|
use tokio_rustls::client::TlsStream;
|
||||||
|
@ -16,14 +20,14 @@ use tokio_util::codec::FramedRead;
|
||||||
|
|
||||||
use crate::proto::{
|
use crate::proto::{
|
||||||
command::Command,
|
command::Command,
|
||||||
response::{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;
|
||||||
use super::codec::{ImapCodec, TaggedCommand};
|
use super::codec::{ImapCodec, TaggedCommand};
|
||||||
use super::response_stream::ResponseStream;
|
use super::response_stream::ResponseStream;
|
||||||
use super::upgrade::upgrade;
|
use super::tls::wrap_tls;
|
||||||
|
|
||||||
const TAG_PREFIX: &str = "panotag";
|
const TAG_PREFIX: &str = "panotag";
|
||||||
|
|
||||||
|
@ -47,6 +51,7 @@ pub struct Inner<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>>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -88,6 +93,7 @@ where
|
||||||
let write_handle = tokio::spawn(write_loop(write_half, exit_rx, write_rx));
|
let write_handle = 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));
|
||||||
Ok(Inner {
|
Ok(Inner {
|
||||||
config,
|
config,
|
||||||
tag_number,
|
tag_number,
|
||||||
|
@ -98,6 +104,7 @@ where
|
||||||
write_handle,
|
write_handle,
|
||||||
_write_tx: write_tx,
|
_write_tx: write_tx,
|
||||||
greeting_rx: Some(greeting_rx),
|
greeting_rx: Some(greeting_rx),
|
||||||
|
capabilities,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,27 +124,37 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn has_capability(&mut self, cap: impl AsRef<str>) -> Result<bool> {
|
pub async fn has_capability(&mut self, cap: impl AsRef<str>) -> Result<bool> {
|
||||||
// TODO: cache capabilities if needed?
|
let cap_bytes = cap.as_ref().as_bytes().to_vec();
|
||||||
let cap = cap.as_ref().to_owned();
|
let (_, cap) = parse_capability(Bytes::from(cap_bytes))?;
|
||||||
let (_, cap) = parse_capability(Bytes::from(cap))?;
|
|
||||||
|
|
||||||
let resp = self.execute(Command::Capability).await?;
|
let contains = {
|
||||||
let (_, data) = resp.wait().await?;
|
let read = self.capabilities.read().await;
|
||||||
|
if let Some(read) = &*read {
|
||||||
|
read.contains(&cap)
|
||||||
|
} else {
|
||||||
|
std::mem::drop(read);
|
||||||
|
|
||||||
for resp in data {
|
let cmd = self.execute(Command::Capability).await?;
|
||||||
if let Response::Capabilities(caps) = resp {
|
let done = cmd.done().await?;
|
||||||
return Ok(caps.contains(&cap));
|
todo!("done: {:?}", done);
|
||||||
|
|
||||||
|
// todo!()
|
||||||
}
|
}
|
||||||
// debug!("cap: {:?}", resp);
|
};
|
||||||
}
|
|
||||||
|
|
||||||
Ok(false)
|
Ok(contains)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn upgrade(mut self) -> Result<Inner<TlsStream<C>>> {
|
pub async fn upgrade(mut self) -> Result<Inner<TlsStream<C>>> {
|
||||||
debug!("preparing to upgrade using STARTTLS");
|
debug!("preparing to upgrade using STARTTLS");
|
||||||
// TODO: check that this capability exists??
|
|
||||||
// TODO: issue the STARTTLS command to the server
|
// check that this capability exists
|
||||||
|
// if it doesn't exist, then it's not an IMAP4-compliant server
|
||||||
|
if !self.has_capability("STARTTLS").await? {
|
||||||
|
bail!("Server does not have the STARTTLS capability");
|
||||||
|
}
|
||||||
|
|
||||||
|
// issue the STARTTLS command to the server
|
||||||
let resp = self.execute(Command::Starttls).await?;
|
let resp = self.execute(Command::Starttls).await?;
|
||||||
dbg!(resp.wait().await?);
|
dbg!(resp.wait().await?);
|
||||||
debug!("received OK from server");
|
debug!("received OK from server");
|
||||||
|
@ -152,7 +169,7 @@ where
|
||||||
|
|
||||||
// put the read half and write half back together
|
// put the read half and write half back together
|
||||||
let stream = read_half.unsplit(write_half);
|
let stream = read_half.unsplit(write_half);
|
||||||
let tls_stream = upgrade(stream, &self.config.hostname).await?;
|
let tls_stream = wrap_tls(stream, &self.config.hostname).await?;
|
||||||
|
|
||||||
Inner::new(tls_stream, self.config).await
|
Inner::new(tls_stream, self.config).await
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,7 @@ pub mod response_stream;
|
||||||
mod client;
|
mod client;
|
||||||
mod codec;
|
mod codec;
|
||||||
mod inner;
|
mod inner;
|
||||||
mod upgrade;
|
mod tls;
|
||||||
|
|
||||||
pub use self::client::{ClientAuthenticated, ClientUnauthenticated, Config, ConfigBuilder};
|
pub use self::client::{ClientAuthenticated, ClientUnauthenticated, Config, ConfigBuilder};
|
||||||
pub use self::codec::{ImapCodec, TaggedCommand};
|
pub use self::codec::{ImapCodec, TaggedCommand};
|
||||||
|
|
|
@ -6,7 +6,8 @@ use tokio_rustls::{
|
||||||
client::TlsStream, rustls::ClientConfig as RustlsConfig, webpki::DNSNameRef, TlsConnector,
|
client::TlsStream, rustls::ClientConfig as RustlsConfig, webpki::DNSNameRef, TlsConnector,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub async fn upgrade<C>(c: C, hostname: impl AsRef<str>) -> Result<TlsStream<C>>
|
/// Wraps the given async stream in TLS with the given hostname (required)
|
||||||
|
pub async fn wrap_tls<C>(c: C, hostname: impl AsRef<str>) -> Result<TlsStream<C>>
|
||||||
where
|
where
|
||||||
C: AsyncRead + AsyncWrite + Unpin,
|
C: AsyncRead + AsyncWrite + Unpin,
|
||||||
{
|
{
|
|
@ -5,6 +5,8 @@ extern crate async_trait;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate derive_builder;
|
extern crate derive_builder;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
|
extern crate derivative;
|
||||||
|
#[macro_use]
|
||||||
extern crate format_bytes;
|
extern crate format_bytes;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate futures;
|
extern crate futures;
|
||||||
|
|
|
@ -101,9 +101,12 @@ pub struct CommandList {
|
||||||
pub mailbox: Bytes,
|
pub mailbox: Bytes,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Derivative)]
|
||||||
|
#[derivative(Debug)]
|
||||||
pub struct CommandLogin {
|
pub struct CommandLogin {
|
||||||
pub userid: Bytes,
|
pub userid: Bytes,
|
||||||
|
|
||||||
|
#[derivative(Debug = "ignore")]
|
||||||
pub password: Bytes,
|
pub password: Bytes,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -116,7 +116,7 @@ pub enum UidSetMember {
|
||||||
Uid(u32),
|
Uid(u32),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||||
pub enum Capability {
|
pub enum Capability {
|
||||||
Imap4rev1,
|
Imap4rev1,
|
||||||
Auth(Atom),
|
Auth(Atom),
|
||||||
|
|
|
@ -46,6 +46,10 @@ rule!(pub astring : Bytes => alt((take_while1(is_astring_char), string)));
|
||||||
pub(crate) fn is_astring_char(c: u8) -> bool { is_atom_char(c) || is_resp_specials(c) }
|
pub(crate) fn is_astring_char(c: u8) -> bool { 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)));
|
||||||
|
|
||||||
|
// really odd behavior about take_while1 is that if there isn't a character that's
|
||||||
|
// not is_atom_char, then it's actually going to error out and require another character
|
||||||
|
// in case there's more. makes sense, just need to keep in mind that we need more
|
||||||
|
// content in order to satisfy this
|
||||||
rule!(pub atom : Bytes => take_while1(is_atom_char));
|
rule!(pub atom : Bytes => take_while1(is_atom_char));
|
||||||
|
|
||||||
pub(crate) fn is_atom_char(c: u8) -> bool { is_char(c) && !is_atom_specials(c) }
|
pub(crate) fn is_atom_char(c: u8) -> bool { is_char(c) && !is_atom_specials(c) }
|
||||||
|
@ -67,14 +71,14 @@ rule!(pub auth_type : Atom => atom);
|
||||||
|
|
||||||
rule!(pub capability : Capability => alt((
|
rule!(pub capability : Capability => alt((
|
||||||
map(preceded(tagi(b"AUTH="), auth_type), Capability::Auth),
|
map(preceded(tagi(b"AUTH="), auth_type), Capability::Auth),
|
||||||
map(atom, Capability::Atom),
|
map(verify(atom, |s| &s[..] != b"IMAP4rev1"), Capability::Atom),
|
||||||
)));
|
)));
|
||||||
|
|
||||||
rule!(pub capability_data : Vec<Capability> => preceded(tagi(b"CAPABILITY"), {
|
rule!(pub capability_data : Vec<Capability> => preceded(tagi(b"CAPABILITY"), {
|
||||||
map(separated_pair(
|
map(separated_pair(
|
||||||
many0(preceded(SP, capability)),
|
many0(preceded(SP, capability)),
|
||||||
pair(SP, tagi(b"IMAP4rev1")),
|
pair(SP, tagi(b"IMAP4rev1")),
|
||||||
many0(preceded(SP, capability))
|
many0(preceded(SP, capability)),
|
||||||
), |(mut a, b)| { a.extend(b); a })
|
), |(mut a, b)| { a.extend(b); a })
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
use panorama_proto_common::Bytes;
|
#![allow(unused_imports)]
|
||||||
|
|
||||||
|
use panorama_proto_common::*;
|
||||||
|
use nom::{sequence::*, multi::*};
|
||||||
|
|
||||||
use super::response::*;
|
use super::response::*;
|
||||||
use super::rfc3501::*;
|
use super::rfc3501::*;
|
||||||
|
use super::rfc2234::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_literal() {
|
fn test_literal() {
|
||||||
|
@ -14,6 +18,17 @@ fn test_literal() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_capabilities() {
|
||||||
|
assert_eq!(capability(Bytes::from(b"UNSELECT\r\n")).unwrap().1, Capability::Atom(Bytes::from(b"UNSELECT")));
|
||||||
|
|
||||||
|
// trivial case
|
||||||
|
assert_eq!(capability_data(Bytes::from(b"CAPABILITY IMAP4rev1\r\n")).unwrap().1, vec![]);
|
||||||
|
|
||||||
|
assert_eq!(capability_data(Bytes::from(b"CAPABILITY UNSELECT IMAP4rev1 NAMESPACE\r\n")).unwrap().1,
|
||||||
|
vec![Capability::Atom(Bytes::from(b"UNSELECT")), Capability::Atom(Bytes::from(b"NAMESPACE"))]);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_list() {
|
fn test_list() {
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
|
|
|
@ -1,7 +1,15 @@
|
||||||
[package]
|
[package]
|
||||||
name = "mbsync"
|
name = "panorama-mbsync"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
description = "mbsync written using panorama's IMAP library"
|
||||||
|
authors = ["Michael Zhang <mail@mzhang.io>"]
|
||||||
|
keywords = ["imap", "email", "mbsync"]
|
||||||
|
license = "GPL-3.0-or-later"
|
||||||
|
categories = ["email"]
|
||||||
|
repository = "https://git.mzhang.io/michael/panorama"
|
||||||
|
readme = "README.md"
|
||||||
|
workspace = ".."
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.42"
|
anyhow = "1.0.42"
|
||||||
|
@ -10,4 +18,7 @@ clap = "3.0.0-beta.2"
|
||||||
derivative = "2.2.0"
|
derivative = "2.2.0"
|
||||||
derive_builder = "0.10.2"
|
derive_builder = "0.10.2"
|
||||||
panorama-imap = { path = "../imap" }
|
panorama-imap = { path = "../imap" }
|
||||||
|
serde = { version = "1.0.127", features = ["derive"] }
|
||||||
tokio = { version = "1.9.0", features = ["full"] }
|
tokio = { version = "1.9.0", features = ["full"] }
|
||||||
|
log = "0.4.14"
|
||||||
|
env_logger = "0.9.0"
|
||||||
|
|
2
mbsync/README.md
Normal file
2
mbsync/README.md
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
mbsync
|
||||||
|
===
|
|
@ -14,6 +14,7 @@ pub fn read_from_file(path: impl AsRef<Path>) -> Result<Config> {
|
||||||
pub fn read_from_reader<R: Read>(r: R) -> Result<Config> {
|
pub fn read_from_reader<R: Read>(r: R) -> Result<Config> {
|
||||||
let r = BufReader::new(r);
|
let r = BufReader::new(r);
|
||||||
|
|
||||||
|
// store each element here
|
||||||
let mut accounts = HashMap::new();
|
let mut accounts = HashMap::new();
|
||||||
let mut stores = HashMap::new();
|
let mut stores = HashMap::new();
|
||||||
let mut channels = HashMap::new();
|
let mut channels = HashMap::new();
|
||||||
|
@ -25,6 +26,9 @@ pub fn read_from_reader<R: Read>(r: R) -> Result<Config> {
|
||||||
Channel(ChannelBuilder),
|
Channel(ChannelBuilder),
|
||||||
}
|
}
|
||||||
let mut current_section = None;
|
let mut current_section = None;
|
||||||
|
|
||||||
|
// this is called whenever we encounter a new section
|
||||||
|
// it wraps up the current builder and adds it to the appropriate table
|
||||||
let finish_section = |accounts: &mut HashMap<_, _>,
|
let finish_section = |accounts: &mut HashMap<_, _>,
|
||||||
stores: &mut HashMap<_, _>,
|
stores: &mut HashMap<_, _>,
|
||||||
channels: &mut HashMap<_, _>,
|
channels: &mut HashMap<_, _>,
|
||||||
|
@ -55,6 +59,8 @@ pub fn read_from_reader<R: Read>(r: R) -> Result<Config> {
|
||||||
|
|
||||||
for line in r.lines() {
|
for line in r.lines() {
|
||||||
let line = line?.trim().to_string();
|
let line = line?.trim().to_string();
|
||||||
|
|
||||||
|
// ignore empty lines and comments
|
||||||
if line.is_empty() || line.starts_with('#') {
|
if line.is_empty() || line.starts_with('#') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -202,6 +208,7 @@ pub fn read_from_reader<R: Read>(r: R) -> Result<Config> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// finish again at the end
|
||||||
finish_section(
|
finish_section(
|
||||||
&mut accounts,
|
&mut accounts,
|
||||||
&mut stores,
|
&mut stores,
|
||||||
|
@ -218,6 +225,7 @@ pub fn read_from_reader<R: Read>(r: R) -> Result<Config> {
|
||||||
Ok(config)
|
Ok(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Double check that all the names being referred to actually exist
|
||||||
fn check_config(config: &Config) -> Result<()> {
|
fn check_config(config: &Config) -> Result<()> {
|
||||||
for store in config.stores.values() {
|
for store in config.stores.values() {
|
||||||
if let Store::Imap(store) = store {
|
if let Store::Imap(store) = store {
|
||||||
|
@ -233,19 +241,19 @@ fn check_config(config: &Config) -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub accounts: HashMap<String, Account>,
|
pub accounts: HashMap<String, Account>,
|
||||||
pub stores: HashMap<String, Store>,
|
pub stores: HashMap<String, Store>,
|
||||||
pub channels: HashMap<String, Channel>,
|
pub channels: HashMap<String, Channel>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub enum Account {
|
pub enum Account {
|
||||||
Imap(ImapAccount),
|
Imap(ImapAccount),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Derivative, Builder)]
|
#[derive(Derivative, Builder, Serialize, Deserialize)]
|
||||||
#[derivative(Debug)]
|
#[derivative(Debug)]
|
||||||
pub struct ImapAccount {
|
pub struct ImapAccount {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
@ -261,20 +269,20 @@ pub struct ImapAccount {
|
||||||
pub pass: String,
|
pub pass: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub enum SslType {
|
pub enum SslType {
|
||||||
None,
|
None,
|
||||||
Starttls,
|
Starttls,
|
||||||
Imaps,
|
Imaps,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub enum Store {
|
pub enum Store {
|
||||||
Maildir(MaildirStore),
|
Maildir(MaildirStore),
|
||||||
Imap(ImapStore),
|
Imap(ImapStore),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Builder)]
|
#[derive(Debug, Builder, Serialize, Deserialize)]
|
||||||
pub struct MaildirStore {
|
pub struct MaildirStore {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
|
@ -282,20 +290,20 @@ pub struct MaildirStore {
|
||||||
pub subfolders: MaildirSubfolderStyle,
|
pub subfolders: MaildirSubfolderStyle,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub enum MaildirSubfolderStyle {
|
pub enum MaildirSubfolderStyle {
|
||||||
Verbatim,
|
Verbatim,
|
||||||
Maildirpp,
|
Maildirpp,
|
||||||
Legacy,
|
Legacy,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Builder)]
|
#[derive(Debug, Builder, Serialize, Deserialize)]
|
||||||
pub struct ImapStore {
|
pub struct ImapStore {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub account: String,
|
pub account: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Builder)]
|
#[derive(Debug, Builder, Serialize, Deserialize)]
|
||||||
pub struct Channel {
|
pub struct Channel {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub far: String,
|
pub far: String,
|
||||||
|
@ -304,6 +312,7 @@ pub struct Channel {
|
||||||
}
|
}
|
||||||
|
|
||||||
bitflags! {
|
bitflags! {
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct ChannelSyncOps: u32 {
|
pub struct ChannelSyncOps: u32 {
|
||||||
const PULL = 1 << 1;
|
const PULL = 1 << 1;
|
||||||
const PUSH = 1 << 2;
|
const PUSH = 1 << 2;
|
||||||
|
|
|
@ -6,6 +6,10 @@ extern crate bitflags;
|
||||||
extern crate derivative;
|
extern crate derivative;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate derive_builder;
|
extern crate derive_builder;
|
||||||
|
#[macro_use]
|
||||||
|
extern crate serde;
|
||||||
|
#[macro_use]
|
||||||
|
extern crate log;
|
||||||
|
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod store;
|
pub mod store;
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
|
#[macro_use]
|
||||||
|
extern crate log;
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use clap::Clap;
|
use clap::Clap;
|
||||||
use mbsync::{
|
use panorama_mbsync::{
|
||||||
config::{self, ChannelSyncOps},
|
config::{self, ChannelSyncOps},
|
||||||
store,
|
store,
|
||||||
};
|
};
|
||||||
|
@ -12,24 +15,30 @@ struct Opt {
|
||||||
/// The path to the config file (defaults to ~/.mbsyncrc).
|
/// The path to the config file (defaults to ~/.mbsyncrc).
|
||||||
#[clap(name = "config", long = "config", short = 'c')]
|
#[clap(name = "config", long = "config", short = 'c')]
|
||||||
config_path: Option<PathBuf>,
|
config_path: Option<PathBuf>,
|
||||||
|
|
||||||
|
/// Verbose mode (-v, -vv, -vvv, etc)
|
||||||
|
#[clap(short = 'v', long = "verbose", parse(from_occurrences))]
|
||||||
|
verbose: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
let opt = Opt::parse();
|
let opt = Opt::parse();
|
||||||
println!("opts: {:?}", opt);
|
info!("opts: {:?}", opt);
|
||||||
|
|
||||||
|
env_logger::init();
|
||||||
|
|
||||||
let config_path = match opt.config_path {
|
let config_path = match opt.config_path {
|
||||||
Some(path) => path,
|
Some(path) => path,
|
||||||
None => PathBuf::from(env!("HOME")).join(".mbsyncrc"),
|
None => PathBuf::from(env!("HOME")).join(".mbsyncrc"),
|
||||||
};
|
};
|
||||||
println!("config path: {:?}", config_path);
|
info!("config path: {:?}", config_path);
|
||||||
|
|
||||||
let config = config::read_from_file(&config_path)?;
|
let config = config::read_from_file(&config_path)?;
|
||||||
println!("config: {:?}", config);
|
info!("config: {:?}", config);
|
||||||
|
|
||||||
for channel in config.channels.values() {
|
for channel in config.channels.values() {
|
||||||
println!("beginning to sync {}", channel.name);
|
info!("beginning to sync {}", channel.name);
|
||||||
|
|
||||||
let far = store::open(&config, &channel.far).await?;
|
let far = store::open(&config, &channel.far).await?;
|
||||||
let near = store::open(&config, &channel.near).await?;
|
let near = store::open(&config, &channel.near).await?;
|
||||||
|
|
|
@ -17,6 +17,8 @@ pub async fn open(config: &Config, store_name: impl AsRef<str>) -> Result<Opened
|
||||||
.accounts
|
.accounts
|
||||||
.get(&store.account)
|
.get(&store.account)
|
||||||
.expect("already checked by config reader");
|
.expect("already checked by config reader");
|
||||||
|
debug!("account: {:?}", account);
|
||||||
|
|
||||||
match account {
|
match account {
|
||||||
Account::Imap(account) => {
|
Account::Imap(account) => {
|
||||||
let port = account.port.unwrap_or_else(|| match account.ssltype {
|
let port = account.port.unwrap_or_else(|| match account.ssltype {
|
||||||
|
@ -27,15 +29,20 @@ pub async fn open(config: &Config, store_name: impl AsRef<str>) -> Result<Opened
|
||||||
SslType::None | SslType::Starttls => false,
|
SslType::None | SslType::Starttls => false,
|
||||||
SslType::Imaps => true,
|
SslType::Imaps => true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
trace!("opening client...");
|
||||||
let mut client = ConfigBuilder::default()
|
let mut client = ConfigBuilder::default()
|
||||||
.hostname(account.host.clone())
|
.hostname(account.host.clone())
|
||||||
.port(port)
|
.port(port)
|
||||||
.tls(tls)
|
.tls(tls)
|
||||||
.open()
|
.open()
|
||||||
.await?;
|
.await?;
|
||||||
|
trace!("opened.");
|
||||||
|
|
||||||
if let SslType::Starttls = account.ssltype {
|
if let SslType::Starttls = account.ssltype {
|
||||||
|
trace!("upgrading client...");
|
||||||
client = client.upgrade().await?;
|
client = client.upgrade().await?;
|
||||||
|
trace!("upgraded.");
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("connected");
|
println!("connected");
|
||||||
|
|
|
@ -8,7 +8,7 @@ use nom::{
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Glue code between nom and Bytes so they work together.
|
/// Glue code between nom and Bytes so they work together.
|
||||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
|
||||||
pub struct Bytes(bytes::Bytes);
|
pub struct Bytes(bytes::Bytes);
|
||||||
|
|
||||||
impl Bytes {
|
impl Bytes {
|
||||||
|
@ -40,6 +40,10 @@ 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 {
|
||||||
|
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())) }
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue