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",
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "bumpalo"
|
||||
version = "3.7.0"
|
||||
|
@ -175,6 +186,28 @@ version = "1.0.7"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "funty"
|
||||
version = "1.1.0"
|
||||
|
@ -443,9 +476,11 @@ dependencies = [
|
|||
"anyhow",
|
||||
"async-trait",
|
||||
"bitflags",
|
||||
"bstr",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"derive_builder",
|
||||
"format-bytes",
|
||||
"futures",
|
||||
"log",
|
||||
"nom",
|
||||
|
@ -538,6 +573,12 @@ dependencies = [
|
|||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.16.20"
|
||||
|
|
|
@ -28,14 +28,16 @@ rfc6154 = [] # list
|
|||
anyhow = "1.0.42"
|
||||
async-trait = "0.1.51"
|
||||
bitflags = "1.2.1"
|
||||
bstr = "0.2.15"
|
||||
bytes = "1.0.1"
|
||||
chrono = "0.4.19"
|
||||
derive_builder = "0.10.2"
|
||||
format-bytes = "0.2.2"
|
||||
futures = "0.3.16"
|
||||
log = "0.4.14"
|
||||
nom = "6.2.1"
|
||||
stderrlog = { version = "0.5.1", optional = true }
|
||||
tokio = { version = "1.9.0", features = ["full"] }
|
||||
tokio-rustls = { version = "0.22.0", features = ["dangerous_configuration"] }
|
||||
tokio-util = { version = "0.6.7", features = ["codec"] }
|
||||
webpki-roots = "0.21.1"
|
||||
stderrlog = { version = "0.5.1", optional = true }
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
use tokio::io::{AsyncRead, AsyncWrite};
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::client::inner::Inner;
|
||||
use crate::proto::{
|
||||
bytes::Bytes,
|
||||
|
@ -7,10 +9,11 @@ use crate::proto::{
|
|||
};
|
||||
|
||||
pub trait Client: AsyncRead + AsyncWrite + Unpin + Sync + Send + 'static {}
|
||||
impl<C> Client for C where C: Send + Sync + Unpin + AsyncWrite + AsyncRead + 'static {}
|
||||
|
||||
#[async_trait]
|
||||
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
|
||||
C: Client;
|
||||
}
|
||||
|
@ -22,15 +25,18 @@ pub struct Login {
|
|||
|
||||
#[async_trait]
|
||||
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
|
||||
C: Client,
|
||||
{
|
||||
let command = Command::Login(CommandLogin {
|
||||
username: Bytes::from(self.username.clone()),
|
||||
userid: Bytes::from(self.username.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::response_stream::ResponseStream;
|
||||
use super::upgrade::upgrade;
|
||||
|
@ -61,11 +62,17 @@ impl ConfigBuilder {
|
|||
if config.tls {
|
||||
let conn = upgrade(conn, hostname).await?;
|
||||
let mut inner = Inner::new(conn, config).await?;
|
||||
|
||||
inner.wait_for_greeting().await?;
|
||||
debug!("received greeting");
|
||||
|
||||
return Ok(ClientUnauthenticated::Encrypted(inner));
|
||||
} else {
|
||||
let mut inner = Inner::new(conn, config).await?;
|
||||
|
||||
inner.wait_for_greeting().await?;
|
||||
debug!("received greeting");
|
||||
|
||||
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 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 tokio_util::codec::{Decoder, Encoder};
|
||||
|
||||
use crate::proto::{
|
||||
bytes::Bytes,
|
||||
command::Command,
|
||||
convert_error::convert_error,
|
||||
response::{Response, Tag},
|
||||
rfc3501::response as parse_response,
|
||||
};
|
||||
|
@ -19,15 +21,22 @@ pub struct ImapCodec {
|
|||
impl<'a> Decoder for ImapCodec {
|
||||
type Item = Response;
|
||||
type Error = 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() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let buf2 = buf.split();
|
||||
let buf3 = buf2.clone().freeze();
|
||||
let (response, len) = match parse_response(buf3.clone().into()) {
|
||||
Ok((remaining, response)) => (response, buf.len() - remaining.len()),
|
||||
debug!("going to parse a response since buffer len: {}", buf3.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))) => {
|
||||
self.decode_need_message_bytes = min.get();
|
||||
return Ok(None);
|
||||
|
@ -35,17 +44,21 @@ impl<'a> Decoder for ImapCodec {
|
|||
Err(nom::Err::Incomplete(_)) => {
|
||||
return Ok(None);
|
||||
}
|
||||
Err(nom::Err::Error(nom::error::Error { code, .. }))
|
||||
| Err(nom::Err::Failure(nom::error::Error { code, .. })) => {
|
||||
Err(Err::Error(err)) | Err(Err::Failure(err)) => {
|
||||
let buf4 = buf3.clone().into();
|
||||
error!("failed to parse: {:?}", buf4);
|
||||
error!("code: {}", convert_error(buf4, err));
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("{:?} during parsing of {:?}", code, buf),
|
||||
format!("error during parsing of {:?}", buf),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
info!("success, parsed as {:?}", response);
|
||||
buf.unsplit(buf2);
|
||||
buf.advance(len);
|
||||
let _ = buf.split_to(len);
|
||||
debug!("buf: {:?}", buf);
|
||||
|
||||
self.decode_need_message_bytes = 0;
|
||||
Ok(Some(response))
|
||||
|
@ -53,18 +66,24 @@ impl<'a> Decoder for ImapCodec {
|
|||
}
|
||||
|
||||
/// A command with its accompanying tag.
|
||||
#[derive(Debug)]
|
||||
pub struct TaggedCommand(pub Tag, pub Command);
|
||||
|
||||
impl<'a> Encoder<&'a TaggedCommand> for ImapCodec {
|
||||
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' ');
|
||||
// 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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,21 +3,20 @@ use std::sync::atomic::{AtomicU32, Ordering};
|
|||
use anyhow::Result;
|
||||
use futures::{
|
||||
future::{self, FutureExt, TryFutureExt},
|
||||
sink::SinkExt,
|
||||
stream::StreamExt,
|
||||
};
|
||||
use tokio::{
|
||||
io::{split, AsyncRead, AsyncWrite, ReadHalf, WriteHalf},
|
||||
io::{split, AsyncRead, AsyncWrite, AsyncWriteExt, BufWriter, ReadHalf, WriteHalf},
|
||||
sync::{mpsc, oneshot},
|
||||
task::JoinHandle,
|
||||
};
|
||||
use tokio_rustls::client::TlsStream;
|
||||
use tokio_util::codec::{FramedRead, FramedWrite};
|
||||
use tokio_util::codec::FramedRead;
|
||||
|
||||
use crate::proto::{
|
||||
bytes::Bytes,
|
||||
command::Command,
|
||||
response::{Response, Tag},
|
||||
response::{Condition, Response, Status, Tag},
|
||||
rfc3501::capability as parse_capability,
|
||||
};
|
||||
|
||||
|
@ -135,9 +134,13 @@ where
|
|||
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: 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
|
||||
let _ = self.read_exit.send(());
|
||||
|
@ -188,11 +191,13 @@ where
|
|||
let exit = exit.fuse();
|
||||
pin_mut!(exit);
|
||||
loop {
|
||||
debug!("READ LOOP ITER");
|
||||
let next = framed.next().fuse();
|
||||
pin_mut!(next);
|
||||
|
||||
// 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
|
||||
// the other options in the select.
|
||||
future::pending().boxed().fuse()
|
||||
|
@ -202,14 +207,15 @@ where
|
|||
|
||||
select! {
|
||||
// read a command from the command list
|
||||
mut command = cmd_fut => {
|
||||
command = cmd_fut => {
|
||||
if curr_cmd.is_none() {
|
||||
if let Some(CommandContainer { tag, command, .. }) = command.take() {
|
||||
let _ = write_tx.send(TaggedCommand(tag, command));
|
||||
if let Some(CommandContainer { ref tag, ref command, .. }) = command {
|
||||
let _ = write_tx.send(TaggedCommand(tag.clone(), command.clone()));
|
||||
// let cmd_str = format!("{} {:?}\r\n", tag, cmd);
|
||||
// write_tx.send(cmd_str);
|
||||
}
|
||||
curr_cmd = command;
|
||||
debug!("new command: {:?}", curr_cmd);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -232,6 +238,12 @@ where
|
|||
let _ = channel.send(resp);
|
||||
// debug!("res0: {:?}", res);
|
||||
}
|
||||
} else if let Response::Tagged(_, Condition { status: Status::Ok, ..}) = resp {
|
||||
// clear curr_cmd so another one can be sent
|
||||
if let Some(CommandContainer { channel, .. }) = curr_cmd.take() {
|
||||
let _ = channel.send(resp);
|
||||
// debug!("res0: {:?}", res);
|
||||
}
|
||||
} else if let Some(CommandContainer { channel, .. }) = curr_cmd.as_mut() {
|
||||
// we got a response from the server for this command, so send it over the
|
||||
// channel
|
||||
|
@ -241,6 +253,7 @@ where
|
|||
// debug!("res1: {:?}", res);
|
||||
}
|
||||
}
|
||||
|
||||
_ = exit => break,
|
||||
}
|
||||
}
|
||||
|
@ -257,8 +270,9 @@ where
|
|||
C: AsyncWrite,
|
||||
{
|
||||
// set up framed communication
|
||||
let codec = ImapCodec::default();
|
||||
let mut framed = FramedWrite::new(stream, codec);
|
||||
// let codec = ImapCodec::default();
|
||||
let mut stream = BufWriter::new(stream);
|
||||
// let mut framed = FramedWrite::new(stream, codec);
|
||||
|
||||
let mut exit_rx = exit_rx.map_err(|_| ()).shared();
|
||||
loop {
|
||||
|
@ -269,15 +283,20 @@ where
|
|||
command = command_fut => {
|
||||
// TODO: handle errors here
|
||||
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.flush().await;
|
||||
// trace!("C>>>S: {:?}", line);
|
||||
}
|
||||
_ = exit_rx => break,
|
||||
}
|
||||
}
|
||||
|
||||
framed.into_inner()
|
||||
// framed.into_inner()
|
||||
stream.into_inner()
|
||||
}
|
||||
|
|
|
@ -3,11 +3,13 @@ extern crate anyhow;
|
|||
#[macro_use]
|
||||
extern crate async_trait;
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
extern crate derive_builder;
|
||||
#[macro_use]
|
||||
extern crate format_bytes;
|
||||
#[macro_use]
|
||||
extern crate futures;
|
||||
#[macro_use]
|
||||
extern crate derive_builder;
|
||||
extern crate log;
|
||||
// #[macro_use]
|
||||
// extern crate bitflags;
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
use std::io::{self, Write};
|
||||
use std::ops::{Deref, RangeBounds};
|
||||
|
||||
use format_bytes::DisplayBytes;
|
||||
use nom::{
|
||||
error::{ErrorKind, ParseError},
|
||||
CompareResult, Err, IResult, InputLength, Needed,
|
||||
|
@ -13,6 +15,10 @@ impl Bytes {
|
|||
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 {
|
||||
fn from(b: bytes::Bytes) -> Self { Bytes(b) }
|
||||
}
|
||||
|
@ -168,7 +174,7 @@ impl ShitCompare<&[u8]> for Bytes {
|
|||
match self
|
||||
.iter()
|
||||
.zip(other.iter())
|
||||
.any(|(a, b)| (a & 0x20) != (b & 0x20))
|
||||
.any(|(a, b)| (a | 0x20) != (b | 0x20))
|
||||
{
|
||||
true => CompareResult::Error,
|
||||
false if self.len() < other.len() => CompareResult::Incomplete,
|
||||
|
@ -213,7 +219,9 @@ array_impls! {
|
|||
0 1 2 3 4 5 6 7 8 9
|
||||
10 11 12 13 14 15 16 17 18 19
|
||||
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 {
|
||||
|
|
|
@ -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)]
|
||||
pub enum Command {
|
||||
|
@ -45,6 +49,34 @@ pub enum Command {
|
|||
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)]
|
||||
pub struct CommandFetch {
|
||||
pub ids: Vec<u32>,
|
||||
|
@ -59,7 +91,7 @@ pub struct CommandList {
|
|||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CommandLogin {
|
||||
pub username: Bytes,
|
||||
pub userid: 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 {
|
||||
($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)
|
||||
}
|
||||
};
|
||||
|
|
|
@ -8,6 +8,8 @@ mod macros;
|
|||
pub mod bytes;
|
||||
#[macro_use]
|
||||
pub mod parsers;
|
||||
pub mod convert_error;
|
||||
pub mod formatter;
|
||||
|
||||
// data types
|
||||
pub mod command;
|
||||
|
@ -20,3 +22,7 @@ pub mod rfc6154;
|
|||
|
||||
#[cfg(feature = "rfc2177")]
|
||||
pub mod rfc2177;
|
||||
|
||||
// tests
|
||||
#[cfg(test)]
|
||||
pub mod test_rfc3501;
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
use anyhow::Result;
|
||||
use nom::{
|
||||
error::{Error, ErrorKind, ParseError},
|
||||
error::{ErrorKind, ParseError, VerboseError},
|
||||
CompareResult, Err, IResult, InputLength, Needed, Parser, ToUsize,
|
||||
};
|
||||
|
||||
use super::bytes::{ShitCompare, ShitNeededForParsing};
|
||||
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
|
||||
/// `Vec<T>`.
|
||||
///
|
||||
|
@ -79,7 +81,12 @@ pub fn parse_u32(s: impl AsRef<[u8]>) -> Result<u32> {
|
|||
}
|
||||
|
||||
/// 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.
|
||||
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 {
|
||||
Flags(Vec<Flag>),
|
||||
List(MailboxList),
|
||||
Lsub,
|
||||
Lsub(MailboxList),
|
||||
Search(Vec<u32>),
|
||||
Status,
|
||||
Exists(u32),
|
||||
|
|
|
@ -3,7 +3,11 @@
|
|||
//!
|
||||
//! 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};
|
||||
|
||||
|
@ -35,7 +39,7 @@ rule!(pub HTAB : u8 => byte(b'\x09'));
|
|||
pub(crate) fn is_lf(c: u8) -> bool { c == b'\x0a' }
|
||||
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);
|
||||
|
||||
|
|
|
@ -8,11 +8,10 @@ use nom::{
|
|||
combinator::{map, map_res, opt, verify},
|
||||
multi::{many0, many1},
|
||||
sequence::{delimited, pair, preceded, separated_pair, terminated, tuple},
|
||||
IResult,
|
||||
};
|
||||
|
||||
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::{
|
||||
Address, Atom, Capability, Condition, Envelope, Flag, Mailbox, MailboxData, MailboxList,
|
||||
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"\\Seen"), |_| Flag::Seen),
|
||||
map(tagi(b"\\Draft"), |_| Flag::Draft),
|
||||
map(flag_keyword, Flag::Keyword),
|
||||
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_keyword : Atom => atom);
|
||||
|
||||
rule!(pub flag_list : Vec<Flag> => paren!(sep_list!(?flag)));
|
||||
|
||||
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
|
||||
// determined to exceed a certain threshold so we don't have insane amounts of
|
||||
// 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 (i, length) = length_of(i)?;
|
||||
debug!("length is: {:?}", length);
|
||||
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((
|
||||
map(tagi(b"INBOX"), |_| Mailbox::Inbox),
|
||||
map(astring, Mailbox::Name),
|
||||
|
@ -161,6 +152,9 @@ rule!(pub mailbox : Mailbox => alt((
|
|||
rule!(pub mailbox_data : MailboxData => alt((
|
||||
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"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(
|
||||
|
@ -217,7 +211,7 @@ rule!(pub nil : Bytes => tagi(b"NIL"));
|
|||
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
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