lots of shit

This commit is contained in:
Michael Zhang 2021-08-09 06:29:48 -05:00
parent f8a402ab6e
commit e4bc3e4b98
Signed by: michael
GPG key ID: BDA47A31A3C8EE6B
18 changed files with 380 additions and 60 deletions

41
Cargo.lock generated
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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",
));
}