lots of shit
This commit is contained in:
parent
f8a402ab6e
commit
e4bc3e4b98
18 changed files with 380 additions and 60 deletions
41
Cargo.lock
generated
41
Cargo.lock
generated
|
@ -66,6 +66,17 @@ dependencies = [
|
||||||
"wyz",
|
"wyz",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bstr"
|
||||||
|
version = "0.2.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a40b47ad93e1a5404e6c18dec46b628214fee441c70f4ab5d6942142cc268a3d"
|
||||||
|
dependencies = [
|
||||||
|
"lazy_static",
|
||||||
|
"memchr",
|
||||||
|
"regex-automata",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bumpalo"
|
name = "bumpalo"
|
||||||
version = "3.7.0"
|
version = "3.7.0"
|
||||||
|
@ -175,6 +186,28 @@ version = "1.0.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "format-bytes"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1c4e89040c7fd7b4e6ba2820ac705a45def8a0c098ec78d170ae88f1ef1d5762"
|
||||||
|
dependencies = [
|
||||||
|
"format-bytes-macros",
|
||||||
|
"proc-macro-hack",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "format-bytes-macros"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b05089e341a0460449e2210c3bf7b61597860b07f0deae58da38dbed0a4c6b6d"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro-hack",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "funty"
|
name = "funty"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
|
@ -443,9 +476,11 @@ dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"bitflags",
|
"bitflags",
|
||||||
|
"bstr",
|
||||||
"bytes",
|
"bytes",
|
||||||
"chrono",
|
"chrono",
|
||||||
"derive_builder",
|
"derive_builder",
|
||||||
|
"format-bytes",
|
||||||
"futures",
|
"futures",
|
||||||
"log",
|
"log",
|
||||||
"nom",
|
"nom",
|
||||||
|
@ -538,6 +573,12 @@ dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-automata"
|
||||||
|
version = "0.1.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ring"
|
name = "ring"
|
||||||
version = "0.16.20"
|
version = "0.16.20"
|
||||||
|
|
|
@ -28,14 +28,16 @@ rfc6154 = [] # list
|
||||||
anyhow = "1.0.42"
|
anyhow = "1.0.42"
|
||||||
async-trait = "0.1.51"
|
async-trait = "0.1.51"
|
||||||
bitflags = "1.2.1"
|
bitflags = "1.2.1"
|
||||||
|
bstr = "0.2.15"
|
||||||
bytes = "1.0.1"
|
bytes = "1.0.1"
|
||||||
chrono = "0.4.19"
|
chrono = "0.4.19"
|
||||||
derive_builder = "0.10.2"
|
derive_builder = "0.10.2"
|
||||||
|
format-bytes = "0.2.2"
|
||||||
futures = "0.3.16"
|
futures = "0.3.16"
|
||||||
log = "0.4.14"
|
log = "0.4.14"
|
||||||
nom = "6.2.1"
|
nom = "6.2.1"
|
||||||
|
stderrlog = { version = "0.5.1", optional = true }
|
||||||
tokio = { version = "1.9.0", features = ["full"] }
|
tokio = { version = "1.9.0", features = ["full"] }
|
||||||
tokio-rustls = { version = "0.22.0", features = ["dangerous_configuration"] }
|
tokio-rustls = { version = "0.22.0", features = ["dangerous_configuration"] }
|
||||||
tokio-util = { version = "0.6.7", features = ["codec"] }
|
tokio-util = { version = "0.6.7", features = ["codec"] }
|
||||||
webpki-roots = "0.21.1"
|
webpki-roots = "0.21.1"
|
||||||
stderrlog = { version = "0.5.1", optional = true }
|
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
use tokio::io::{AsyncRead, AsyncWrite};
|
use tokio::io::{AsyncRead, AsyncWrite};
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
use crate::client::inner::Inner;
|
use crate::client::inner::Inner;
|
||||||
use crate::proto::{
|
use crate::proto::{
|
||||||
bytes::Bytes,
|
bytes::Bytes,
|
||||||
|
@ -7,10 +9,11 @@ use crate::proto::{
|
||||||
};
|
};
|
||||||
|
|
||||||
pub trait Client: AsyncRead + AsyncWrite + Unpin + Sync + Send + 'static {}
|
pub trait Client: AsyncRead + AsyncWrite + Unpin + Sync + Send + 'static {}
|
||||||
|
impl<C> Client for C where 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>)
|
async fn perform_auth<C>(&self, inner: &mut Inner<C>) -> Result<()>
|
||||||
where
|
where
|
||||||
C: Client;
|
C: Client;
|
||||||
}
|
}
|
||||||
|
@ -22,15 +25,18 @@ pub struct Login {
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl AuthMethod for Login {
|
impl AuthMethod for Login {
|
||||||
async fn perform_auth<C>(&self, inner: &mut Inner<C>)
|
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 {
|
||||||
username: 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?);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ use crate::proto::{
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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::upgrade::upgrade;
|
||||||
|
@ -61,11 +62,17 @@ impl ConfigBuilder {
|
||||||
if config.tls {
|
if config.tls {
|
||||||
let conn = upgrade(conn, hostname).await?;
|
let conn = upgrade(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");
|
||||||
|
|
||||||
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");
|
||||||
|
|
||||||
return Ok(ClientUnauthenticated::Unencrypted(inner));
|
return Ok(ClientUnauthenticated::Unencrypted(inner));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -88,6 +95,20 @@ impl ClientUnauthenticated {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn auth(self, auth: impl AuthMethod) -> Result<ClientAuthenticated> {
|
||||||
|
match self {
|
||||||
|
// this is a no-op, we don't need to upgrade
|
||||||
|
ClientUnauthenticated::Encrypted(mut inner) => {
|
||||||
|
auth.perform_auth(&mut inner).await?;
|
||||||
|
Ok(ClientAuthenticated::Encrypted(inner))
|
||||||
|
}
|
||||||
|
ClientUnauthenticated::Unencrypted(mut inner) => {
|
||||||
|
auth.perform_auth(&mut inner).await?;
|
||||||
|
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>);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
use std::io;
|
use std::io::{self};
|
||||||
|
|
||||||
use bytes::{Buf, BufMut, BytesMut};
|
use bytes::{BufMut, BytesMut};
|
||||||
use nom::Needed;
|
use nom::Needed;
|
||||||
use tokio_util::codec::{Decoder, Encoder};
|
use tokio_util::codec::{Decoder, Encoder};
|
||||||
|
|
||||||
use crate::proto::{
|
use crate::proto::{
|
||||||
|
bytes::Bytes,
|
||||||
command::Command,
|
command::Command,
|
||||||
|
convert_error::convert_error,
|
||||||
response::{Response, Tag},
|
response::{Response, Tag},
|
||||||
rfc3501::response as parse_response,
|
rfc3501::response as parse_response,
|
||||||
};
|
};
|
||||||
|
@ -19,15 +21,22 @@ pub struct ImapCodec {
|
||||||
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(&mut self, buf: &mut BytesMut) -> Result<Option<Self::Item>, io::Error> {
|
fn decode(&mut self, buf: &mut BytesMut) -> Result<Option<Self::Item>, io::Error> {
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
let buf2 = buf.split();
|
let buf2 = buf.split();
|
||||||
let buf3 = buf2.clone().freeze();
|
let buf3 = buf2.clone().freeze();
|
||||||
let (response, len) = match parse_response(buf3.clone().into()) {
|
debug!("going to parse a response since buffer len: {}", buf3.len());
|
||||||
Ok((remaining, response)) => (response, buf.len() - remaining.len()),
|
// trace!("buf: {:?}", buf3);
|
||||||
|
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()),
|
||||||
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);
|
||||||
|
@ -35,17 +44,21 @@ impl<'a> Decoder for ImapCodec {
|
||||||
Err(nom::Err::Incomplete(_)) => {
|
Err(nom::Err::Incomplete(_)) => {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
Err(nom::Err::Error(nom::error::Error { code, .. }))
|
Err(Err::Error(err)) | Err(Err::Failure(err)) => {
|
||||||
| Err(nom::Err::Failure(nom::error::Error { code, .. })) => {
|
let buf4 = buf3.clone().into();
|
||||||
|
error!("failed to parse: {:?}", buf4);
|
||||||
|
error!("code: {}", convert_error(buf4, err));
|
||||||
return Err(io::Error::new(
|
return Err(io::Error::new(
|
||||||
io::ErrorKind::Other,
|
io::ErrorKind::Other,
|
||||||
format!("{:?} during parsing of {:?}", code, buf),
|
format!("error during parsing of {:?}", buf),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
info!("success, parsed as {:?}", response);
|
||||||
buf.unsplit(buf2);
|
buf.unsplit(buf2);
|
||||||
buf.advance(len);
|
let _ = buf.split_to(len);
|
||||||
|
debug!("buf: {:?}", buf);
|
||||||
|
|
||||||
self.decode_need_message_bytes = 0;
|
self.decode_need_message_bytes = 0;
|
||||||
Ok(Some(response))
|
Ok(Some(response))
|
||||||
|
@ -53,18 +66,24 @@ impl<'a> Decoder for ImapCodec {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A command with its accompanying tag.
|
/// A command with its accompanying tag.
|
||||||
|
#[derive(Debug)]
|
||||||
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(&mut self, tagged_cmd: &TaggedCommand, dst: &mut BytesMut) -> Result<(), io::Error> {
|
|
||||||
let tag = &tagged_cmd.0;
|
|
||||||
let _command = &tagged_cmd.1;
|
|
||||||
|
|
||||||
dst.put(&*tag.0);
|
fn encode(&mut self, tagged_cmd: &TaggedCommand, dst: &mut BytesMut) -> Result<(), io::Error> {
|
||||||
|
let tag = &*tagged_cmd.0 .0;
|
||||||
|
let command = &tagged_cmd.1;
|
||||||
|
|
||||||
|
dst.put(tag);
|
||||||
dst.put_u8(b' ');
|
dst.put_u8(b' ');
|
||||||
// TODO: write command
|
|
||||||
dst.put_slice(b"\r\n");
|
// TODO: don't allocate here! use a stream writer
|
||||||
|
let cmd_bytes = format_bytes!(b"{}", command);
|
||||||
|
dst.extend_from_slice(cmd_bytes.as_slice());
|
||||||
|
|
||||||
|
debug!("C>>>S: {:?}", dst);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,21 +3,20 @@ use std::sync::atomic::{AtomicU32, Ordering};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use futures::{
|
use futures::{
|
||||||
future::{self, FutureExt, TryFutureExt},
|
future::{self, FutureExt, TryFutureExt},
|
||||||
sink::SinkExt,
|
|
||||||
stream::StreamExt,
|
stream::StreamExt,
|
||||||
};
|
};
|
||||||
use tokio::{
|
use tokio::{
|
||||||
io::{split, AsyncRead, AsyncWrite, ReadHalf, WriteHalf},
|
io::{split, AsyncRead, AsyncWrite, AsyncWriteExt, BufWriter, ReadHalf, WriteHalf},
|
||||||
sync::{mpsc, oneshot},
|
sync::{mpsc, oneshot},
|
||||||
task::JoinHandle,
|
task::JoinHandle,
|
||||||
};
|
};
|
||||||
use tokio_rustls::client::TlsStream;
|
use tokio_rustls::client::TlsStream;
|
||||||
use tokio_util::codec::{FramedRead, FramedWrite};
|
use tokio_util::codec::FramedRead;
|
||||||
|
|
||||||
use crate::proto::{
|
use crate::proto::{
|
||||||
bytes::Bytes,
|
bytes::Bytes,
|
||||||
command::Command,
|
command::Command,
|
||||||
response::{Response, Tag},
|
response::{Condition, Response, Status, Tag},
|
||||||
rfc3501::capability as parse_capability,
|
rfc3501::capability as parse_capability,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -135,9 +134,13 @@ where
|
||||||
Ok(false)
|
Ok(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn upgrade(self) -> Result<Inner<TlsStream<C>>> {
|
pub async fn upgrade(mut self) -> Result<Inner<TlsStream<C>>> {
|
||||||
|
debug!("preparing to upgrade using STARTTLS");
|
||||||
// TODO: check that this capability exists??
|
// TODO: check that this capability exists??
|
||||||
// TODO: issue the STARTTLS command to the server
|
// TODO: issue the STARTTLS command to the server
|
||||||
|
let resp = self.execute(Command::Starttls).await?;
|
||||||
|
dbg!(resp.wait().await?);
|
||||||
|
debug!("received OK from server");
|
||||||
|
|
||||||
// issue exit to the read loop and retrieve the read half
|
// issue exit to the read loop and retrieve the read half
|
||||||
let _ = self.read_exit.send(());
|
let _ = self.read_exit.send(());
|
||||||
|
@ -188,11 +191,13 @@ where
|
||||||
let exit = exit.fuse();
|
let exit = exit.fuse();
|
||||||
pin_mut!(exit);
|
pin_mut!(exit);
|
||||||
loop {
|
loop {
|
||||||
|
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(_) = curr_cmd {
|
let mut cmd_fut = if let Some(ref cmd) = curr_cmd {
|
||||||
|
debug!("current command: {:?}", cmd);
|
||||||
// if there is one, just make a future that never resolves so it'll always pick
|
// if there is one, just make a future that never resolves so it'll always pick
|
||||||
// the other options in the select.
|
// the other options in the select.
|
||||||
future::pending().boxed().fuse()
|
future::pending().boxed().fuse()
|
||||||
|
@ -202,14 +207,15 @@ where
|
||||||
|
|
||||||
select! {
|
select! {
|
||||||
// read a command from the command list
|
// read a command from the command list
|
||||||
mut command = cmd_fut => {
|
command = cmd_fut => {
|
||||||
if curr_cmd.is_none() {
|
if curr_cmd.is_none() {
|
||||||
if let Some(CommandContainer { tag, command, .. }) = command.take() {
|
if let Some(CommandContainer { ref tag, ref command, .. }) = command {
|
||||||
let _ = write_tx.send(TaggedCommand(tag, command));
|
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;
|
curr_cmd = command;
|
||||||
|
debug!("new command: {:?}", curr_cmd);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -232,6 +238,12 @@ where
|
||||||
let _ = channel.send(resp);
|
let _ = channel.send(resp);
|
||||||
// debug!("res0: {:?}", res);
|
// 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() {
|
} 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
|
// we got a response from the server for this command, so send it over the
|
||||||
// channel
|
// channel
|
||||||
|
@ -241,6 +253,7 @@ where
|
||||||
// debug!("res1: {:?}", res);
|
// debug!("res1: {:?}", res);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = exit => break,
|
_ = exit => break,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -257,8 +270,9 @@ where
|
||||||
C: AsyncWrite,
|
C: AsyncWrite,
|
||||||
{
|
{
|
||||||
// set up framed communication
|
// set up framed communication
|
||||||
let codec = ImapCodec::default();
|
// let codec = ImapCodec::default();
|
||||||
let mut framed = FramedWrite::new(stream, codec);
|
let mut stream = BufWriter::new(stream);
|
||||||
|
// 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 {
|
||||||
|
@ -269,15 +283,20 @@ where
|
||||||
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 _ = framed.send(&command).await;
|
let cmd = format_bytes!(b"{} {}\r\n", &*command.0.0, command.1);
|
||||||
|
debug!("sending command: {:?}", String::from_utf8_lossy(&cmd));
|
||||||
|
let _ = stream.write_all(&cmd).await;
|
||||||
|
let _ = stream.flush().await;
|
||||||
|
// let _ = framed.send(&command).await;
|
||||||
|
// let _ = framed.flush().await;
|
||||||
}
|
}
|
||||||
// let _ = stream.write_all(line.as_bytes()).await;
|
// let _ = stream.write_all(line.as_bytes()).await;
|
||||||
// let _ = stream.flush().await;
|
// let _ = stream.flush().await;
|
||||||
// trace!("C>>>S: {:?}", line);
|
|
||||||
}
|
}
|
||||||
_ = exit_rx => break,
|
_ = exit_rx => break,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
framed.into_inner()
|
// framed.into_inner()
|
||||||
|
stream.into_inner()
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,11 +3,13 @@ extern crate anyhow;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate async_trait;
|
extern crate async_trait;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate log;
|
extern crate derive_builder;
|
||||||
|
#[macro_use]
|
||||||
|
extern crate format_bytes;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate futures;
|
extern crate futures;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate derive_builder;
|
extern crate log;
|
||||||
// #[macro_use]
|
// #[macro_use]
|
||||||
// extern crate bitflags;
|
// extern crate bitflags;
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
|
use std::io::{self, Write};
|
||||||
use std::ops::{Deref, RangeBounds};
|
use std::ops::{Deref, RangeBounds};
|
||||||
|
|
||||||
|
use format_bytes::DisplayBytes;
|
||||||
use nom::{
|
use nom::{
|
||||||
error::{ErrorKind, ParseError},
|
error::{ErrorKind, ParseError},
|
||||||
CompareResult, Err, IResult, InputLength, Needed,
|
CompareResult, Err, IResult, InputLength, Needed,
|
||||||
|
@ -13,6 +15,10 @@ impl Bytes {
|
||||||
pub fn len(&self) -> usize { self.0.len() }
|
pub fn len(&self) -> usize { self.0.len() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl DisplayBytes for Bytes {
|
||||||
|
fn display_bytes(&self, w: &mut dyn Write) -> io::Result<()> { w.write(&*self.0).map(|_| ()) }
|
||||||
|
}
|
||||||
|
|
||||||
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) }
|
||||||
}
|
}
|
||||||
|
@ -168,7 +174,7 @@ impl ShitCompare<&[u8]> for Bytes {
|
||||||
match self
|
match self
|
||||||
.iter()
|
.iter()
|
||||||
.zip(other.iter())
|
.zip(other.iter())
|
||||||
.any(|(a, b)| (a & 0x20) != (b & 0x20))
|
.any(|(a, b)| (a | 0x20) != (b | 0x20))
|
||||||
{
|
{
|
||||||
true => CompareResult::Error,
|
true => CompareResult::Error,
|
||||||
false if self.len() < other.len() => CompareResult::Incomplete,
|
false if self.len() < other.len() => CompareResult::Incomplete,
|
||||||
|
@ -213,7 +219,9 @@ array_impls! {
|
||||||
0 1 2 3 4 5 6 7 8 9
|
0 1 2 3 4 5 6 7 8 9
|
||||||
10 11 12 13 14 15 16 17 18 19
|
10 11 12 13 14 15 16 17 18 19
|
||||||
20 21 22 23 24 25 26 27 28 29
|
20 21 22 23 24 25 26 27 28 29
|
||||||
30 31 32
|
30 31 32 33 34 35 36 37 38 39
|
||||||
|
40 41 42 43 44 45 46 47 48 49
|
||||||
|
50 51 52 53 54 55 56 57 58 59
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Bytes {
|
impl Bytes {
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
use crate::proto::bytes::Bytes;
|
use std::io::{self, Write};
|
||||||
|
|
||||||
|
use format_bytes::DisplayBytes;
|
||||||
|
|
||||||
|
use crate::proto::{bytes::Bytes, formatter::quote_string as q};
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum Command {
|
pub enum Command {
|
||||||
|
@ -45,6 +49,34 @@ pub enum Command {
|
||||||
Done,
|
Done,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl DisplayBytes for Command {
|
||||||
|
fn display_bytes(&self, w: &mut dyn Write) -> io::Result<()> {
|
||||||
|
match self {
|
||||||
|
// command-any
|
||||||
|
Command::Capability => write_bytes!(w, b"CAPABILITY"),
|
||||||
|
Command::Logout => write_bytes!(w, b"LOGOUT"),
|
||||||
|
Command::Noop => write_bytes!(w, b"NOOP"),
|
||||||
|
|
||||||
|
// command-nonauth
|
||||||
|
Command::Login(login) => {
|
||||||
|
write_bytes!(w, b"LOGIN {} {}", q(&login.userid), q(&login.password))
|
||||||
|
}
|
||||||
|
Command::Starttls => write_bytes!(w, b"STARTTLS"),
|
||||||
|
|
||||||
|
// command-auth
|
||||||
|
Command::List(list) => {
|
||||||
|
write_bytes!(w, b"LIST {} {}", q(&list.reference), q(&list.mailbox))
|
||||||
|
}
|
||||||
|
Command::Select(select) => write_bytes!(w, b"SELECT {}", q(&select.mailbox)),
|
||||||
|
|
||||||
|
#[cfg(feature = "rfc2177")]
|
||||||
|
Command::Idle => write_bytes!(w, b"IDLE"),
|
||||||
|
|
||||||
|
_ => Ok(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct CommandFetch {
|
pub struct CommandFetch {
|
||||||
pub ids: Vec<u32>,
|
pub ids: Vec<u32>,
|
||||||
|
@ -59,7 +91,7 @@ pub struct CommandList {
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct CommandLogin {
|
pub struct CommandLogin {
|
||||||
pub username: Bytes,
|
pub userid: Bytes,
|
||||||
pub password: Bytes,
|
pub password: Bytes,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
120
imap/src/proto/convert_error.rs
Normal file
120
imap/src/proto/convert_error.rs
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
use std::fmt::Debug;
|
||||||
|
use std::fmt::Write;
|
||||||
|
use std::ops::Deref;
|
||||||
|
|
||||||
|
use bstr::ByteSlice;
|
||||||
|
use nom::{
|
||||||
|
error::{VerboseError, VerboseErrorKind},
|
||||||
|
Offset,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Same as nom's convert_error, except operates on u8
|
||||||
|
pub fn convert_error<I: Deref<Target = [u8]> + Debug>(input: I, e: VerboseError<I>) -> String {
|
||||||
|
let mut result = String::new();
|
||||||
|
debug!("e: {:?}", e);
|
||||||
|
|
||||||
|
for (i, (substring, kind)) in e.errors.iter().enumerate() {
|
||||||
|
let offset = input.offset(substring);
|
||||||
|
|
||||||
|
if input.is_empty() {
|
||||||
|
match kind {
|
||||||
|
VerboseErrorKind::Char(c) => {
|
||||||
|
write!(&mut result, "{}: expected '{}', got empty input\n\n", i, c)
|
||||||
|
}
|
||||||
|
VerboseErrorKind::Context(s) => {
|
||||||
|
write!(&mut result, "{}: in {}, got empty input\n\n", i, s)
|
||||||
|
}
|
||||||
|
VerboseErrorKind::Nom(e) => {
|
||||||
|
write!(&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
|
||||||
|
let line_number = prefix.iter().filter(|&&b| b == b'\n').count() + 1;
|
||||||
|
|
||||||
|
// Find the line that includes the subslice:
|
||||||
|
// Find the *last* newline before the substring starts
|
||||||
|
let line_begin = prefix
|
||||||
|
.iter()
|
||||||
|
.rev()
|
||||||
|
.position(|&b| b == b'\n')
|
||||||
|
.map(|pos| offset - pos)
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
// Find the full line after that newline
|
||||||
|
let line = input[line_begin..]
|
||||||
|
.lines()
|
||||||
|
.next()
|
||||||
|
.unwrap_or(&input[line_begin..])
|
||||||
|
.trim_end();
|
||||||
|
|
||||||
|
// The (1-indexed) column number is the offset of our substring into that line
|
||||||
|
let column_number = line.offset(substring) + 1;
|
||||||
|
|
||||||
|
match kind {
|
||||||
|
VerboseErrorKind::Char(c) => {
|
||||||
|
if let Some(actual) = substring.chars().next() {
|
||||||
|
write!(
|
||||||
|
&mut result,
|
||||||
|
"{i}: at line {line_number}:\n\
|
||||||
|
{line}\n\
|
||||||
|
{caret:>column$}\n\
|
||||||
|
expected '{expected}', found {actual}\n\n",
|
||||||
|
i = i,
|
||||||
|
line_number = line_number,
|
||||||
|
line = String::from_utf8_lossy(line),
|
||||||
|
caret = '^',
|
||||||
|
column = column_number,
|
||||||
|
expected = c,
|
||||||
|
actual = actual,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
write!(
|
||||||
|
&mut result,
|
||||||
|
"{i}: at line {line_number}:\n\
|
||||||
|
{line}\n\
|
||||||
|
{caret:>column$}\n\
|
||||||
|
expected '{expected}', got end of input\n\n",
|
||||||
|
i = i,
|
||||||
|
line_number = line_number,
|
||||||
|
line = String::from_utf8_lossy(line),
|
||||||
|
caret = '^',
|
||||||
|
column = column_number,
|
||||||
|
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.
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
17
imap/src/proto/formatter.rs
Normal file
17
imap/src/proto/formatter.rs
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
use super::rfc3501::is_quoted_specials;
|
||||||
|
|
||||||
|
pub fn quote_string(input: impl AsRef<[u8]>) -> Vec<u8> {
|
||||||
|
let input = input.as_ref();
|
||||||
|
let mut ret = Vec::with_capacity(input.len() + 2);
|
||||||
|
|
||||||
|
ret.push(b'\x22');
|
||||||
|
for c in input {
|
||||||
|
if is_quoted_specials(*c) {
|
||||||
|
ret.push(b'\\');
|
||||||
|
}
|
||||||
|
ret.push(*c);
|
||||||
|
}
|
||||||
|
ret.push(b'\x22');
|
||||||
|
|
||||||
|
ret
|
||||||
|
}
|
|
@ -1,6 +1,8 @@
|
||||||
macro_rules! rule {
|
macro_rules! rule {
|
||||||
($vis:vis $name:ident : $ret:ty => $expr:expr) => {
|
($vis:vis $name:ident : $ret:ty => $expr:expr) => {
|
||||||
$vis fn $name(i: crate::proto::bytes::Bytes) -> nom::IResult<crate::proto::bytes::Bytes, $ret> {
|
$vis fn $name (
|
||||||
|
i: crate::proto::bytes::Bytes
|
||||||
|
) -> crate::proto::parsers::VResult<crate::proto::bytes::Bytes, $ret> {
|
||||||
$expr(i)
|
$expr(i)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -8,6 +8,8 @@ mod macros;
|
||||||
pub mod bytes;
|
pub mod bytes;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
pub mod parsers;
|
pub mod parsers;
|
||||||
|
pub mod convert_error;
|
||||||
|
pub mod formatter;
|
||||||
|
|
||||||
// data types
|
// data types
|
||||||
pub mod command;
|
pub mod command;
|
||||||
|
@ -20,3 +22,7 @@ pub mod rfc6154;
|
||||||
|
|
||||||
#[cfg(feature = "rfc2177")]
|
#[cfg(feature = "rfc2177")]
|
||||||
pub mod rfc2177;
|
pub mod rfc2177;
|
||||||
|
|
||||||
|
// tests
|
||||||
|
#[cfg(test)]
|
||||||
|
pub mod test_rfc3501;
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use nom::{
|
use nom::{
|
||||||
error::{Error, ErrorKind, ParseError},
|
error::{ErrorKind, ParseError, VerboseError},
|
||||||
CompareResult, Err, IResult, InputLength, Needed, Parser, ToUsize,
|
CompareResult, Err, IResult, InputLength, Needed, Parser, ToUsize,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::bytes::{ShitCompare, ShitNeededForParsing};
|
use super::bytes::{ShitCompare, ShitNeededForParsing};
|
||||||
use super::rfc2234::is_digit;
|
use super::rfc2234::is_digit;
|
||||||
|
|
||||||
|
pub type VResult<I, O> = IResult<I, O, VerboseError<I>>;
|
||||||
|
|
||||||
/// `sep_list!(t, d)` represents `t *(d t)` and automatically collapses it into
|
/// `sep_list!(t, d)` represents `t *(d t)` and automatically collapses it into
|
||||||
/// `Vec<T>`.
|
/// `Vec<T>`.
|
||||||
///
|
///
|
||||||
|
@ -79,7 +81,12 @@ pub fn parse_u32(s: impl AsRef<[u8]>) -> Result<u32> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Always fails, used as a no-op.
|
/// Always fails, used as a no-op.
|
||||||
pub fn never<I, O>(i: I) -> IResult<I, O> { Err(Err::Error(Error::new(i, ErrorKind::Not))) }
|
pub fn never<I, O, E>(i: I) -> IResult<I, O, E>
|
||||||
|
where
|
||||||
|
E: ParseError<I>,
|
||||||
|
{
|
||||||
|
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>
|
||||||
|
|
|
@ -128,7 +128,7 @@ pub enum Capability {
|
||||||
pub enum MailboxData {
|
pub enum MailboxData {
|
||||||
Flags(Vec<Flag>),
|
Flags(Vec<Flag>),
|
||||||
List(MailboxList),
|
List(MailboxList),
|
||||||
Lsub,
|
Lsub(MailboxList),
|
||||||
Search(Vec<u32>),
|
Search(Vec<u32>),
|
||||||
Status,
|
Status,
|
||||||
Exists(u32),
|
Exists(u32),
|
||||||
|
|
|
@ -3,7 +3,11 @@
|
||||||
//!
|
//!
|
||||||
//! Grammar from <https://tools.ietf.org/html/rfc2234#section-6.1>
|
//! Grammar from <https://tools.ietf.org/html/rfc2234#section-6.1>
|
||||||
|
|
||||||
use nom::{branch::alt, multi::many0, sequence::pair};
|
use nom::{
|
||||||
|
branch::alt,
|
||||||
|
multi::many0,
|
||||||
|
sequence::{pair, preceded},
|
||||||
|
};
|
||||||
|
|
||||||
use super::parsers::{byte, satisfy, skip};
|
use super::parsers::{byte, satisfy, skip};
|
||||||
|
|
||||||
|
@ -35,7 +39,7 @@ rule!(pub HTAB : u8 => byte(b'\x09'));
|
||||||
pub(crate) fn is_lf(c: u8) -> bool { c == b'\x0a' }
|
pub(crate) fn is_lf(c: u8) -> bool { c == b'\x0a' }
|
||||||
rule!(pub LF : u8 => satisfy(is_lf));
|
rule!(pub LF : u8 => satisfy(is_lf));
|
||||||
|
|
||||||
rule!(pub LWSP : () => skip(many0(alt((skip(WSP), skip(pair(CRLF, WSP)))))));
|
rule!(pub LWSP : () => skip(many0(alt((WSP, preceded(CRLF, WSP))))));
|
||||||
|
|
||||||
// rule!(pub OCTET : char => anychar);
|
// rule!(pub OCTET : char => anychar);
|
||||||
|
|
||||||
|
|
|
@ -8,11 +8,10 @@ use nom::{
|
||||||
combinator::{map, map_res, opt, verify},
|
combinator::{map, map_res, opt, verify},
|
||||||
multi::{many0, many1},
|
multi::{many0, many1},
|
||||||
sequence::{delimited, pair, preceded, separated_pair, terminated, tuple},
|
sequence::{delimited, pair, preceded, separated_pair, terminated, tuple},
|
||||||
IResult,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::bytes::Bytes;
|
use super::bytes::Bytes;
|
||||||
use super::parsers::{byte, never, parse_u32, satisfy, tagi, take, take_while1};
|
use super::parsers::{byte, never, parse_u32, satisfy, tagi, take, take_while1, VResult};
|
||||||
use super::response::{
|
use super::response::{
|
||||||
Address, Atom, Capability, Condition, Envelope, Flag, Mailbox, MailboxData, MailboxList,
|
Address, Atom, Capability, Condition, Envelope, Flag, Mailbox, MailboxData, MailboxList,
|
||||||
MailboxListFlag, MessageAttribute, Response, ResponseCode, ResponseText, Status, Tag,
|
MailboxListFlag, MessageAttribute, Response, ResponseCode, ResponseText, Status, Tag,
|
||||||
|
@ -118,6 +117,7 @@ rule!(pub flag : Flag => alt((
|
||||||
map(tagi(b"\\Deleted"), |_| Flag::Deleted),
|
map(tagi(b"\\Deleted"), |_| Flag::Deleted),
|
||||||
map(tagi(b"\\Seen"), |_| Flag::Seen),
|
map(tagi(b"\\Seen"), |_| Flag::Seen),
|
||||||
map(tagi(b"\\Draft"), |_| Flag::Draft),
|
map(tagi(b"\\Draft"), |_| Flag::Draft),
|
||||||
|
map(flag_keyword, Flag::Keyword),
|
||||||
map(flag_extension, Flag::Extension),
|
map(flag_extension, Flag::Extension),
|
||||||
)));
|
)));
|
||||||
|
|
||||||
|
@ -125,6 +125,8 @@ rule!(pub flag_extension : Atom => preceded(byte(b'\\'), atom));
|
||||||
|
|
||||||
rule!(pub flag_fetch : Flag => alt((flag, map(tagi(b"\\Recent"), |_| Flag::Recent))));
|
rule!(pub flag_fetch : Flag => alt((flag, map(tagi(b"\\Recent"), |_| Flag::Recent))));
|
||||||
|
|
||||||
|
rule!(pub flag_keyword : Atom => atom);
|
||||||
|
|
||||||
rule!(pub flag_list : Vec<Flag> => paren!(sep_list!(?flag)));
|
rule!(pub flag_list : Vec<Flag> => paren!(sep_list!(?flag)));
|
||||||
|
|
||||||
pub(crate) fn is_list_wildcards(c: u8) -> bool { c == b'%' || c == b'*' }
|
pub(crate) fn is_list_wildcards(c: u8) -> bool { c == b'%' || c == b'*' }
|
||||||
|
@ -135,24 +137,13 @@ rule!(pub list_wildcards : u8 => satisfy(is_list_wildcards));
|
||||||
// TODO: Future work, could possibly initialize writing to file if the length is
|
// TODO: Future work, could possibly initialize writing to file if the length is
|
||||||
// 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) -> IResult<Bytes, Bytes> {
|
pub fn literal(i: Bytes) -> VResult<Bytes, Bytes> {
|
||||||
let mut length_of = terminated(delimited(byte(b'{'), number, byte(b'}')), CRLF);
|
let mut length_of = 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_literal() {
|
|
||||||
assert_eq!(
|
|
||||||
literal(Bytes::from(b"{13}\r\nHello, world!"))
|
|
||||||
.unwrap()
|
|
||||||
.1
|
|
||||||
.as_ref(),
|
|
||||||
b"Hello, world!"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
rule!(pub mailbox : Mailbox => alt((
|
rule!(pub mailbox : Mailbox => alt((
|
||||||
map(tagi(b"INBOX"), |_| Mailbox::Inbox),
|
map(tagi(b"INBOX"), |_| Mailbox::Inbox),
|
||||||
map(astring, Mailbox::Name),
|
map(astring, Mailbox::Name),
|
||||||
|
@ -161,6 +152,9 @@ rule!(pub mailbox : Mailbox => alt((
|
||||||
rule!(pub mailbox_data : MailboxData => alt((
|
rule!(pub mailbox_data : MailboxData => alt((
|
||||||
map(preceded(pair(tagi(b"FLAGS"), SP), flag_list), MailboxData::Flags),
|
map(preceded(pair(tagi(b"FLAGS"), SP), flag_list), MailboxData::Flags),
|
||||||
map(preceded(pair(tagi(b"LIST"), SP), mailbox_list), MailboxData::List),
|
map(preceded(pair(tagi(b"LIST"), SP), mailbox_list), MailboxData::List),
|
||||||
|
map(preceded(pair(tagi(b"LSUB"), SP), mailbox_list), MailboxData::Lsub),
|
||||||
|
map(terminated(number, pair(SP, tagi(b"EXISTS"))), MailboxData::Exists),
|
||||||
|
map(terminated(number, pair(SP, tagi(b"RECENT"))), MailboxData::Recent),
|
||||||
)));
|
)));
|
||||||
|
|
||||||
rule!(pub mailbox_list : MailboxList => map(separated_pair(
|
rule!(pub mailbox_list : MailboxList => map(separated_pair(
|
||||||
|
@ -217,7 +211,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) -> IResult<Bytes, u32> {
|
pub(crate) fn number(i: Bytes) -> VResult<Bytes, u32> {
|
||||||
map_res(take_while1(is_digit), parse_u32)(i)
|
map_res(take_while1(is_digit), parse_u32)(i)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
20
imap/src/proto/test_rfc3501.rs
Normal file
20
imap/src/proto/test_rfc3501.rs
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
use super::bytes::Bytes;
|
||||||
|
use super::rfc3501::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_literal() {
|
||||||
|
assert_eq!(
|
||||||
|
literal(Bytes::from(b"{13}\r\nHello, world!"))
|
||||||
|
.unwrap()
|
||||||
|
.1
|
||||||
|
.as_ref(),
|
||||||
|
b"Hello, world!"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_list() {
|
||||||
|
let _ = response(Bytes::from(
|
||||||
|
b"* LIST (\\HasChildren \\UnMarked \\Trash) \".\" Trash\r\n",
|
||||||
|
));
|
||||||
|
}
|
Loading…
Reference in a new issue