had some trouble with capability matching IMAP4rev1 as an atom

This commit is contained in:
Michael Zhang 2021-08-23 00:01:05 -05:00
parent ff98685afb
commit b0c8423968
Signed by: michael
GPG key ID: BDA47A31A3C8EE6B
19 changed files with 219 additions and 56 deletions

75
Cargo.lock generated
View file

@ -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"

View file

@ -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"

View file

@ -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?;

View file

@ -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))
} }

View file

@ -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
} }

View file

@ -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};

View file

@ -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,
{ {

View file

@ -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;

View file

@ -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,
} }

View file

@ -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),

View file

@ -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 })
})); }));

View file

@ -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!(

View file

@ -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
View file

@ -0,0 +1,2 @@
mbsync
===

View file

@ -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;

View file

@ -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;

View file

@ -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?;

View file

@ -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");

View file

@ -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())) }
} }