diff --git a/Cargo.lock b/Cargo.lock index 1001617..80e2df5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/imap/Cargo.toml b/imap/Cargo.toml index 3e502e2..514258d 100644 --- a/imap/Cargo.toml +++ b/imap/Cargo.toml @@ -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 } diff --git a/imap/src/client/auth.rs b/imap/src/client/auth.rs index 904f933..9022475 100644 --- a/imap/src/client/auth.rs +++ b/imap/src/client/auth.rs @@ -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 Client for C where C: Send + Sync + Unpin + AsyncWrite + AsyncRead + 'static {} #[async_trait] pub trait AuthMethod { - async fn perform_auth(&self, inner: &mut Inner) + async fn perform_auth(&self, inner: &mut Inner) -> Result<()> where C: Client; } @@ -22,15 +25,18 @@ pub struct Login { #[async_trait] impl AuthMethod for Login { - async fn perform_auth(&self, inner: &mut Inner) + async fn perform_auth(&self, inner: &mut Inner) -> 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(()) } } diff --git a/imap/src/client/client.rs b/imap/src/client/client.rs index 05db555..d05de79 100644 --- a/imap/src/client/client.rs +++ b/imap/src/client/client.rs @@ -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 { + 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); client_expose!(async has_capability(cap: impl AsRef) -> Result); } diff --git a/imap/src/client/codec.rs b/imap/src/client/codec.rs index b1144e3..5134d83 100644 --- a/imap/src/client/codec.rs +++ b/imap/src/client/codec.rs @@ -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, 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(()) } } diff --git a/imap/src/client/inner.rs b/imap/src/client/inner.rs index 5d14dd4..656349f 100644 --- a/imap/src/client/inner.rs +++ b/imap/src/client/inner.rs @@ -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>> { + pub async fn upgrade(mut self) -> Result>> { + 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() } diff --git a/imap/src/lib.rs b/imap/src/lib.rs index 0d7e9c1..d3c11f8 100644 --- a/imap/src/lib.rs +++ b/imap/src/lib.rs @@ -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; diff --git a/imap/src/proto/bytes.rs b/imap/src/proto/bytes.rs index babc5a2..8641627 100644 --- a/imap/src/proto/bytes.rs +++ b/imap/src/proto/bytes.rs @@ -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 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 { diff --git a/imap/src/proto/command.rs b/imap/src/proto/command.rs index 26192f4..a54380e 100644 --- a/imap/src/proto/command.rs +++ b/imap/src/proto/command.rs @@ -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, @@ -59,7 +91,7 @@ pub struct CommandList { #[derive(Clone, Debug)] pub struct CommandLogin { - pub username: Bytes, + pub userid: Bytes, pub password: Bytes, } diff --git a/imap/src/proto/convert_error.rs b/imap/src/proto/convert_error.rs new file mode 100644 index 0000000..b0f687b --- /dev/null +++ b/imap/src/proto/convert_error.rs @@ -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 + Debug>(input: I, e: VerboseError) -> 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 +} diff --git a/imap/src/proto/formatter.rs b/imap/src/proto/formatter.rs new file mode 100644 index 0000000..407824b --- /dev/null +++ b/imap/src/proto/formatter.rs @@ -0,0 +1,17 @@ +use super::rfc3501::is_quoted_specials; + +pub fn quote_string(input: impl AsRef<[u8]>) -> Vec { + 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 +} diff --git a/imap/src/proto/macros.rs b/imap/src/proto/macros.rs index 9806e33..0101d44 100644 --- a/imap/src/proto/macros.rs +++ b/imap/src/proto/macros.rs @@ -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 { + $vis fn $name ( + i: crate::proto::bytes::Bytes + ) -> crate::proto::parsers::VResult { $expr(i) } }; diff --git a/imap/src/proto/mod.rs b/imap/src/proto/mod.rs index 617c5dd..710ab52 100644 --- a/imap/src/proto/mod.rs +++ b/imap/src/proto/mod.rs @@ -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; diff --git a/imap/src/proto/parsers.rs b/imap/src/proto/parsers.rs index 2a12bba..5456b8f 100644 --- a/imap/src/proto/parsers.rs +++ b/imap/src/proto/parsers.rs @@ -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 = IResult>; + /// `sep_list!(t, d)` represents `t *(d t)` and automatically collapses it into /// `Vec`. /// @@ -79,7 +81,12 @@ pub fn parse_u32(s: impl AsRef<[u8]>) -> Result { } /// Always fails, used as a no-op. -pub fn never(i: I) -> IResult { Err(Err::Error(Error::new(i, ErrorKind::Not))) } +pub fn never(i: I) -> IResult +where + E: ParseError, +{ + Err(Err::Error(E::from_error_kind(i, ErrorKind::Not))) +} /// Skip the part of the input matched by the given parser. pub fn skip(mut f: F) -> impl FnMut(I) -> IResult diff --git a/imap/src/proto/response.rs b/imap/src/proto/response.rs index c320950..8329b39 100644 --- a/imap/src/proto/response.rs +++ b/imap/src/proto/response.rs @@ -128,7 +128,7 @@ pub enum Capability { pub enum MailboxData { Flags(Vec), List(MailboxList), - Lsub, + Lsub(MailboxList), Search(Vec), Status, Exists(u32), diff --git a/imap/src/proto/rfc2234.rs b/imap/src/proto/rfc2234.rs index a8e6405..689e7f5 100644 --- a/imap/src/proto/rfc2234.rs +++ b/imap/src/proto/rfc2234.rs @@ -3,7 +3,11 @@ //! //! Grammar from -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); diff --git a/imap/src/proto/rfc3501.rs b/imap/src/proto/rfc3501.rs index 501670d..0a39403 100644 --- a/imap/src/proto/rfc3501.rs +++ b/imap/src/proto/rfc3501.rs @@ -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 => 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 { +pub fn literal(i: Bytes) -> VResult { 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 => opt_nil!(string)); -pub(crate) fn number(i: Bytes) -> IResult { +pub(crate) fn number(i: Bytes) -> VResult { map_res(take_while1(is_digit), parse_u32)(i) } diff --git a/imap/src/proto/test_rfc3501.rs b/imap/src/proto/test_rfc3501.rs new file mode 100644 index 0000000..5dd26bd --- /dev/null +++ b/imap/src/proto/test_rfc3501.rs @@ -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", + )); +}