diff --git a/Justfile b/Justfile index eada087..4a66c89 100644 --- a/Justfile +++ b/Justfile @@ -13,6 +13,13 @@ test: watch: cargo watch -c +afl-imap: + #!/bin/bash + cd imap/imap-parsing-fuzz-target + pwd + cargo afl build + cargo afl fuzz -i in -o out target/debug/imap-parsing-fuzz-target + fuzz-imap: #!/bin/bash cd imap diff --git a/imap/Justfile b/imap/Justfile deleted file mode 100644 index ebaadba..0000000 --- a/imap/Justfile +++ /dev/null @@ -1,6 +0,0 @@ -afl: - #!/bin/bash - cd imap-parsing-fuzz-target - pwd - cargo afl build - cargo afl fuzz -i in -o out target/debug/imap-parsing-fuzz-target \ No newline at end of file diff --git a/imap/imap-parsing-fuzz-target/Cargo.lock b/imap/imap-parsing-fuzz-target/Cargo.lock index 005458c..7b0aa58 100644 --- a/imap/imap-parsing-fuzz-target/Cargo.lock +++ b/imap/imap-parsing-fuzz-target/Cargo.lock @@ -558,6 +558,7 @@ dependencies = [ "format-bytes", "log", "nom", + "num-traits", ] [[package]] diff --git a/imap/imap-parsing-fuzz-target/in/response-highestmodseq b/imap/imap-parsing-fuzz-target/in/response-highestmodseq new file mode 100644 index 0000000..bde8f61 --- /dev/null +++ b/imap/imap-parsing-fuzz-target/in/response-highestmodseq @@ -0,0 +1 @@ +* OK [HIGHESTMODSEQ 694968] \ No newline at end of file diff --git a/imap/src/client/codec.rs b/imap/src/client/codec.rs index e7f2946..193258a 100644 --- a/imap/src/client/codec.rs +++ b/imap/src/client/codec.rs @@ -105,7 +105,7 @@ impl<'a> Encoder<&'a TaggedCommand> for ImapCodec { let cmd_bytes = format_bytes!(b"{}", command); dst.extend_from_slice(cmd_bytes.as_slice()); - debug!("C>>>S: {:?}", dst); + // debug!("C>>>S: {:?}", dst); Ok(()) } } diff --git a/imap/src/proto/response.rs b/imap/src/proto/response.rs index 09a101e..da04d1d 100644 --- a/imap/src/proto/response.rs +++ b/imap/src/proto/response.rs @@ -212,7 +212,7 @@ pub enum ResponseCode { Capabilities(Vec), HighestModSeq(u64), // RFC 4551, section 3.1.1 Parse, - PermanentFlags(Vec), + PermanentFlags(Vec), ReadOnly, ReadWrite, TryCreate, @@ -319,6 +319,7 @@ pub enum Flag { Recent, Keyword(Atom), Extension(Atom), + SpecialCreate, } impl DisplayBytes for Flag { @@ -332,6 +333,7 @@ impl DisplayBytes for Flag { Flag::Recent => write_bytes!(w, b"\\Recent"), Flag::Keyword(atom) => write_bytes!(w, b"{}", atom), Flag::Extension(atom) => write_bytes!(w, b"\\{}", atom), + Flag::SpecialCreate => write_bytes!(w, b"\\*"), } } } diff --git a/imap/src/proto/rfc3501/mod.rs b/imap/src/proto/rfc3501/mod.rs index ea66b97..5cb28e7 100644 --- a/imap/src/proto/rfc3501/mod.rs +++ b/imap/src/proto/rfc3501/mod.rs @@ -134,9 +134,11 @@ rule!(pub date_time : Timestamp => delimited(DQUOTE, SP, zone, )), |(d, _, m, _, y, _, time, _, zone)| { - eprintln!("{}-{}-{} {:?} {:?}", y, m, d, time, zone); - zone.ymd(y, m, d) + // eprintln!("{}-{}-{} {:?} {:?}", y, m, d, time, zone); + zone.ymd_opt(y, m, d) .and_time(time) + // TODO: what the hell + .earliest() .map(Timestamp) .ok_or_else(|| anyhow!("invalid time")) }), @@ -196,6 +198,11 @@ rule!(pub flag_keyword : Atom => atom); rule!(pub flag_list : Vec => paren!(sep_list!(?flag))); +rule!(pub flag_perm : Flag => alt(( + flag, + map(pair(byte(b'\\'), byte(b'*')), |_| Flag::SpecialCreate), +))); + pub(crate) fn is_list_wildcards(c: u8) -> bool { c == b'%' || c == b'*' } rule!(pub list_wildcards : u8 => satisfy(is_list_wildcards)); @@ -332,14 +339,30 @@ rule!(pub resp_cond_state : Condition => map( pub(crate) fn is_resp_specials(c: u8) -> bool { c == b']' } rule!(pub resp_specials : u8 => satisfy(is_resp_specials)); -rule!(pub resp_text : ResponseText => map(pair( - opt(terminated(delimited(byte(b'['), resp_text_code, byte(b']')), SP)), - text, -), |(code, info)| ResponseText { code, info })); +rule!(pub resp_text : ResponseText => alt(( + // FUCK YOU GMAIL + map(delimited(byte(b'['), resp_text_code, byte(b']')), + |code| ResponseText { code: Some(code), info: Bytes::from(b"") }), + + map(pair( + opt(terminated(delimited(byte(b'['), resp_text_code, byte(b']')), SP)), + text, + ), |(code, info)| ResponseText { code, info }), +))); rule!(pub resp_text_code : ResponseCode => alt(( map(tagi(b"ALERT"), |_| ResponseCode::Alert), map(capability_data, ResponseCode::Capabilities), + map(tagi(b"PARSE"), |_| ResponseCode::Parse), + map(preceded(pair(tagi(b"PERMANENTFLAGS"), SP), paren!(sep_list!(flag_perm))), ResponseCode::PermanentFlags), + map(tagi(b"READ-ONLY"), |_| ResponseCode::ReadOnly), + map(tagi(b"READ-WRITE"), |_| ResponseCode::ReadWrite), + map(tagi(b"TRYCREATE"), |_| ResponseCode::TryCreate), + map(preceded(pair(tagi(b"UIDNEXT"), SP), nz_number), ResponseCode::UidNext), + map(preceded(pair(tagi(b"UIDVALIDITY"), SP), nz_number), ResponseCode::UidValidity), + map(preceded(pair(tagi(b"UNSEEN"), SP), nz_number), ResponseCode::Unseen), + map(pair(atom, opt(preceded(SP, map(take_while1(|c| is_text_char(c) && c != b']'), Bytes::from)))), + |(a, b)| ResponseCode::Other(a, b)), ))); rule!(pub string : Bytes => alt((quoted, literal))); @@ -352,7 +375,7 @@ rule!(pub text : Bytes => map(take_while1(is_text_char), Bytes::from)); pub(crate) fn is_text_char(c: u8) -> bool { is_char(c) && !is_cr(c) && !is_lf(c) } rule!(pub TEXT_CHAR : u8 => satisfy(is_text_char)); -rule!(pub time : NaiveTime => map( +rule!(pub time : NaiveTime => map_res( tuple(( map_res(count(DIGIT, 2), parse_num::<_, u32>), byte(b':'), @@ -360,7 +383,7 @@ rule!(pub time : NaiveTime => map( byte(b':'), map_res(count(DIGIT, 2), parse_num::<_, u32>), )), - |(h, _, m, _, s)| NaiveTime::from_hms(h, m, s))); + |(h, _, m, _, s)| NaiveTime::from_hms_opt(h, m, s).ok_or_else(|| anyhow!("invalid time")))); rule!(pub uniqueid : u32 => nz_number); diff --git a/imap/src/proto/rfc3501/tests.rs b/imap/src/proto/rfc3501/tests.rs index 79a818a..70bb9de 100644 --- a/imap/src/proto/rfc3501/tests.rs +++ b/imap/src/proto/rfc3501/tests.rs @@ -20,7 +20,11 @@ fn test_literal() { } #[test] -fn afl() { let _ = response(Bytes::from(b"* 4544444444 444 ")); } +fn from_afl() { + let _ = response(Bytes::from(b"* 4544444444 444 ")); + + let _ = response(Bytes::from(b"* 8045 FETCH (UID 8225 ENVELOPE (\"Sun, 21 Mar 2021 18:44:10 -0700\" \"SUBJECT\" ((\"SENDER\" NIL \"sender\" \"example.com\")) ((\"SENDER\" NIL \"sender\" \"example.com\")) ((\"norepjy\" NIL \"noreply\" \"example.com\")) ((\"NAME\" NIL \"user\" \"gmail.com\")) NIL NIL NIL \"\") FLAGS () INTERNALDATE \"22-Mar-2021 01:64:12 \x7f0000\" RFC822.SIZE 13503)".to_vec())); +} #[test] fn test_date() { @@ -91,3 +95,20 @@ fn test_list() { &*mailbox == &b"Trash"[..] )); } + +#[test] +fn test_gmail_is_shit() { + // FUCK YOU GMAIL! + let res = response(Bytes::from(b"* OK [HIGHESTMODSEQ 694968]\r\n")) + .unwrap() + .1; + eprintln!("{:?}", res); + assert!(matches!(res, + Response::Condition(Condition { + status: Status::Ok, + code: Some(ResponseCode::Other(c, Some(d))), + info: e, + }) + if c == Bytes::from(b"HIGHESTMODSEQ") && d == Bytes::from(b"694968") && e.is_empty() + )); +}