Upstream commit: 474dd0229cb20cf513036619781ed97bd8073c3f Enterprise-only files removed or emptied: 63 Enterprise-only snippets removed: 117 in 50 files Dangling module declarations removed: 5 Cargo edits turning enterprise off: 14 Verification: clean Enterprise feature gates left for rebuilt features: 19 in 18 files Produced by tools/fork/strip.py. The full report is in docs/fork/strip-reports/ on main.
1174 lines
44 KiB
Rust
1174 lines
44 KiB
Rust
/*
|
|
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
|
*
|
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
|
*/
|
|
|
|
use super::{ResponseCode, ResponseType};
|
|
use compact_str::{CompactString, format_compact};
|
|
use std::fmt::Display;
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum Error {
|
|
NeedsMoreData,
|
|
NeedsLiteral { size: u32 },
|
|
Error { response: trc::Error },
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct Request<T: CommandParser> {
|
|
pub tag: String,
|
|
pub command: T,
|
|
pub tokens: Vec<Token>,
|
|
}
|
|
|
|
pub trait CommandParser: Sized + Default {
|
|
fn parse(bytes: &[u8], is_uid: bool) -> Option<Self>;
|
|
fn tokenize_brackets(&self) -> bool;
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub enum Token {
|
|
Argument(Vec<u8>),
|
|
ParenthesisOpen, // (
|
|
ParenthesisClose, // )
|
|
BracketOpen, // [
|
|
BracketClose, // ]
|
|
Lt, // <
|
|
Gt, // >
|
|
Dot, // .
|
|
Nil, // NIL
|
|
}
|
|
|
|
impl<T: CommandParser> Default for Request<T> {
|
|
fn default() -> Self {
|
|
Self {
|
|
tag: String::new(),
|
|
command: T::default(),
|
|
tokens: Vec::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
|
pub enum State {
|
|
Start,
|
|
Tag,
|
|
Command { is_uid: bool },
|
|
Argument { last_ch: u8 },
|
|
ArgumentQuoted { escaped: bool },
|
|
Literal { non_sync: bool },
|
|
LiteralSeek { size: u32, non_sync: bool },
|
|
LiteralData { remaining: u32 },
|
|
LiteralDiscard { remaining: u32 },
|
|
}
|
|
|
|
pub struct Receiver<T: CommandParser> {
|
|
buf: ArgumentBuffer,
|
|
pub request: Request<T>,
|
|
pub state: State,
|
|
pub max_request_size: usize,
|
|
pub current_request_size: usize,
|
|
pub start_state: State,
|
|
}
|
|
|
|
const ARG_MAX_LEN: usize = 8000;
|
|
|
|
struct ArgumentBuffer {
|
|
buf: Vec<u8>,
|
|
}
|
|
|
|
impl<T: CommandParser> Receiver<T> {
|
|
pub fn new() -> Self {
|
|
Receiver {
|
|
max_request_size: 25 * 1024 * 1024, // 25MB
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
pub fn with_start_state(mut self, state: State) -> Self {
|
|
self.state = state;
|
|
self.start_state = state;
|
|
self
|
|
}
|
|
|
|
pub fn with_max_request_size(max_request_size: usize) -> Self {
|
|
Receiver {
|
|
max_request_size,
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
pub fn error_reset(&mut self, message: impl Into<trc::Value>) -> Error {
|
|
let request = std::mem::take(&mut self.request);
|
|
let err = Error::err(
|
|
if !request.tag.is_empty() {
|
|
request.tag.into()
|
|
} else {
|
|
None
|
|
},
|
|
message,
|
|
);
|
|
self.buf = ArgumentBuffer::default();
|
|
self.state = self.start_state;
|
|
self.current_request_size = 0;
|
|
err
|
|
}
|
|
|
|
fn push_argument(&mut self, in_quote: bool) -> Result<(), Error> {
|
|
if !self.buf.is_empty() {
|
|
self.current_request_size += self.buf.len();
|
|
if self.current_request_size > self.max_request_size {
|
|
return Err(self.error_reset(format_compact!(
|
|
"Request exceeds maximum limit of {} bytes.",
|
|
self.max_request_size
|
|
)));
|
|
}
|
|
self.request.tokens.push(Token::Argument(self.buf.take()));
|
|
} else if in_quote {
|
|
self.request.tokens.push(Token::Nil);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn push_token(&mut self, token: Token) -> Result<(), Error> {
|
|
self.current_request_size += 1;
|
|
if self.current_request_size > self.max_request_size {
|
|
return Err(self.error_reset(format_compact!(
|
|
"Request exceeds maximum limit of {} bytes.",
|
|
self.max_request_size
|
|
)));
|
|
}
|
|
self.request.tokens.push(token);
|
|
Ok(())
|
|
}
|
|
|
|
pub fn parse(&mut self, bytes: &mut std::slice::Iter<'_, u8>) -> Result<Request<T>, Error> {
|
|
#[allow(clippy::while_let_on_iterator)]
|
|
while let Some(&ch) = bytes.next() {
|
|
match self.state {
|
|
State::Start => {
|
|
if !ch.is_ascii_whitespace() {
|
|
// SAFETY: This called just once
|
|
self.buf.push_unchecked(ch);
|
|
self.state = State::Tag;
|
|
}
|
|
}
|
|
State::Tag => match ch {
|
|
b' ' => {
|
|
if !self.buf.is_empty() {
|
|
self.request.tag =
|
|
String::from_utf8(self.buf.take()).map_err(|_| {
|
|
self.error_reset("Tag is not a valid UTF-8 string.")
|
|
})?;
|
|
self.state = State::Command { is_uid: false };
|
|
}
|
|
}
|
|
b'\t' | b'\r' => {}
|
|
b'\n' => {
|
|
return Err(self.error_reset(format_compact!(
|
|
"Missing command after tag {:?}, found CRLF instead.",
|
|
self.buf.as_str()
|
|
)));
|
|
}
|
|
_ => {
|
|
self.buf.push_checked(ch, 128).map_err(|_| {
|
|
self.error_reset("Tag exceeds maximum length of 128 characters.")
|
|
})?;
|
|
}
|
|
},
|
|
State::Command { is_uid } => {
|
|
if ch.is_ascii_alphanumeric() {
|
|
self.buf
|
|
.push_checked(ch.to_ascii_uppercase(), 15)
|
|
.map_err(|_| {
|
|
self.error_reset("Command exceeds maximum length of 15 characters.")
|
|
})?;
|
|
} else if ch.is_ascii_whitespace() {
|
|
if !self.buf.is_empty() {
|
|
if !self.buf.as_ref().eq_ignore_ascii_case(b"UID") {
|
|
self.request.command = T::parse(self.buf.as_ref(), is_uid)
|
|
.ok_or_else(|| {
|
|
let err = format_compact!(
|
|
"Unrecognized command '{}'.",
|
|
String::from_utf8_lossy(self.buf.as_ref())
|
|
);
|
|
self.error_reset(err)
|
|
})?;
|
|
self.buf.clear();
|
|
if ch != b'\n' {
|
|
self.state = State::Argument { last_ch: b' ' };
|
|
} else {
|
|
self.state = self.start_state;
|
|
self.current_request_size = 0;
|
|
return Ok(std::mem::take(&mut self.request));
|
|
}
|
|
} else {
|
|
self.buf.clear();
|
|
self.state = State::Command { is_uid: true };
|
|
}
|
|
}
|
|
} else {
|
|
return Err(self.error_reset(format_compact!(
|
|
"Invalid character {:?} in command name.",
|
|
ch as char
|
|
)));
|
|
}
|
|
}
|
|
State::Argument { last_ch } => match ch {
|
|
b'\"' if last_ch.is_ascii_whitespace() => {
|
|
self.push_argument(false)?;
|
|
self.state = State::ArgumentQuoted { escaped: false };
|
|
}
|
|
b'{' if last_ch.is_ascii_whitespace()
|
|
|| (last_ch == b'~' && self.buf.len() == 1) =>
|
|
{
|
|
if last_ch != b'~' {
|
|
self.push_argument(false)?;
|
|
} else {
|
|
self.buf.clear();
|
|
}
|
|
self.state = State::Literal { non_sync: false };
|
|
}
|
|
b'(' => {
|
|
self.push_argument(false)?;
|
|
self.push_token(Token::ParenthesisOpen)?;
|
|
}
|
|
b')' => {
|
|
self.push_argument(false)?;
|
|
self.push_token(Token::ParenthesisClose)?;
|
|
}
|
|
b'[' if self.request.command.tokenize_brackets() => {
|
|
self.push_argument(false)?;
|
|
self.push_token(Token::BracketOpen)?;
|
|
}
|
|
b']' if self.request.command.tokenize_brackets() => {
|
|
self.push_argument(false)?;
|
|
self.push_token(Token::BracketClose)?;
|
|
}
|
|
b'<' if self.request.command.tokenize_brackets() => {
|
|
self.push_argument(false)?;
|
|
self.push_token(Token::Lt)?;
|
|
}
|
|
b'>' if self.request.command.tokenize_brackets() => {
|
|
self.push_argument(false)?;
|
|
self.push_token(Token::Gt)?;
|
|
}
|
|
b'.' if self.request.command.tokenize_brackets() => {
|
|
self.push_argument(false)?;
|
|
self.push_token(Token::Dot)?;
|
|
}
|
|
b'\n' => {
|
|
self.push_argument(false)?;
|
|
self.state = self.start_state;
|
|
self.current_request_size = 0;
|
|
return Ok(std::mem::take(&mut self.request));
|
|
}
|
|
_ if ch.is_ascii_whitespace() => {
|
|
self.push_argument(false)?;
|
|
self.state = State::Argument { last_ch: ch };
|
|
}
|
|
_ => {
|
|
self.buf.push_checked(ch, ARG_MAX_LEN).map_err(|_| {
|
|
self.error_reset("Argument exceeds maximum length of 8000 bytes.")
|
|
})?;
|
|
self.state = State::Argument { last_ch: ch };
|
|
}
|
|
},
|
|
State::ArgumentQuoted { escaped } => match ch {
|
|
b'\"' => {
|
|
if !escaped {
|
|
self.push_argument(true)?;
|
|
self.state = State::Argument { last_ch: b' ' };
|
|
} else {
|
|
self.buf
|
|
.push_checked(ch, ARG_MAX_LEN)
|
|
.map_err(|_| self.error_reset("Quoted argument too long."))?;
|
|
self.state = State::ArgumentQuoted { escaped: false };
|
|
}
|
|
}
|
|
b'\\' => {
|
|
if escaped {
|
|
self.buf
|
|
.push_checked(ch, ARG_MAX_LEN)
|
|
.map_err(|_| self.error_reset("Quoted argument too long."))?;
|
|
}
|
|
self.state = State::ArgumentQuoted { escaped: !escaped };
|
|
}
|
|
b'\n' => {
|
|
return Err(self.error_reset("Unterminated quoted argument."));
|
|
}
|
|
_ => {
|
|
if escaped {
|
|
// SAFETY: We check the size below
|
|
self.buf.push_unchecked(b'\\');
|
|
}
|
|
self.buf
|
|
.push_checked(ch, ARG_MAX_LEN)
|
|
.map_err(|_| self.error_reset("Quoted argument too long."))?;
|
|
self.state = State::ArgumentQuoted { escaped: false };
|
|
}
|
|
},
|
|
State::Literal { non_sync } => {
|
|
match ch {
|
|
b'}' => {
|
|
if !self.buf.is_empty() {
|
|
let size = self.buf.as_str().parse::<u32>().map_err(|_| {
|
|
self.error_reset("Literal size is not a valid number.")
|
|
})?;
|
|
let oversize = self.current_request_size + size as usize
|
|
> self.max_request_size;
|
|
if oversize && !non_sync {
|
|
return Err(self.error_reset(format_compact!(
|
|
"Literal exceeds the maximum request size of {} bytes.",
|
|
self.max_request_size
|
|
)));
|
|
}
|
|
self.state = State::LiteralSeek { size, non_sync };
|
|
if !oversize {
|
|
self.buf.resize_buffer(size as usize);
|
|
}
|
|
self.buf.clear();
|
|
} else {
|
|
return Err(self.error_reset("Invalid empty literal."));
|
|
}
|
|
}
|
|
b'+' => {
|
|
if !self.buf.is_empty() {
|
|
self.state = State::Literal { non_sync: true };
|
|
} else {
|
|
return Err(self.error_reset("Invalid non-sync literal."));
|
|
}
|
|
}
|
|
_ if ch.is_ascii_digit() => {
|
|
if !non_sync {
|
|
self.buf.push_checked(ch, 15).map_err(|_| {
|
|
self.error_reset("Literal size exceeds maximum of 15 digits.")
|
|
})?;
|
|
} else {
|
|
// Digit found after non-sync '+' flag
|
|
return Err(self.error_reset("Invalid literal."));
|
|
}
|
|
}
|
|
_ => {
|
|
return Err(self.error_reset(format_compact!(
|
|
"Invalid character {:?} in literal.",
|
|
ch as char
|
|
)));
|
|
}
|
|
}
|
|
}
|
|
State::LiteralSeek { size, non_sync } => {
|
|
if ch == b'\n' {
|
|
if non_sync
|
|
&& self.current_request_size + size as usize > self.max_request_size
|
|
{
|
|
self.state = State::LiteralDiscard { remaining: size };
|
|
} else if size > 0 {
|
|
self.state = State::LiteralData { remaining: size };
|
|
if !non_sync {
|
|
return Err(Error::NeedsLiteral { size });
|
|
}
|
|
} else {
|
|
self.state = State::Argument { last_ch: b' ' };
|
|
self.push_token(Token::Nil)?;
|
|
if !non_sync {
|
|
return Err(Error::NeedsLiteral { size });
|
|
}
|
|
}
|
|
} else if !ch.is_ascii_whitespace() {
|
|
return Err(
|
|
self.error_reset("Expected CRLF after literal, found an invalid char.")
|
|
);
|
|
}
|
|
}
|
|
State::LiteralDiscard { remaining } => {
|
|
if remaining > 1 {
|
|
self.state = State::LiteralDiscard {
|
|
remaining: remaining - 1,
|
|
};
|
|
} else {
|
|
return Err(self.error_reset(format_compact!(
|
|
"Literal exceeds the maximum request size of {} bytes.",
|
|
self.max_request_size
|
|
)));
|
|
}
|
|
}
|
|
State::LiteralData { remaining } => {
|
|
// SAFETY: We checked the size before entering this state
|
|
self.buf.push_unchecked(ch);
|
|
|
|
if remaining > 1 {
|
|
self.state = State::LiteralData {
|
|
remaining: remaining - 1,
|
|
};
|
|
} else {
|
|
self.push_argument(false)?;
|
|
self.state = State::Argument { last_ch: b' ' };
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Err(Error::NeedsMoreData)
|
|
}
|
|
}
|
|
|
|
impl ArgumentBuffer {
|
|
pub fn new() -> Self {
|
|
ArgumentBuffer {
|
|
buf: Vec::with_capacity(10),
|
|
}
|
|
}
|
|
|
|
pub fn resize_buffer(&mut self, size: usize) {
|
|
if self.buf.capacity() < size {
|
|
self.buf.reserve(size - self.buf.capacity());
|
|
}
|
|
}
|
|
|
|
#[inline(always)]
|
|
pub fn push_checked(&mut self, byte: u8, limit: usize) -> Result<(), ()> {
|
|
if self.buf.len() < limit {
|
|
self.buf.push(byte);
|
|
Ok(())
|
|
} else {
|
|
Err(())
|
|
}
|
|
}
|
|
|
|
#[inline(always)]
|
|
pub fn push_unchecked(&mut self, byte: u8) {
|
|
self.buf.push(byte);
|
|
}
|
|
|
|
pub fn take(&mut self) -> Vec<u8> {
|
|
let buf = self.buf.clone();
|
|
self.buf.clear();
|
|
buf
|
|
}
|
|
|
|
#[inline(always)]
|
|
pub fn len(&self) -> usize {
|
|
self.buf.len()
|
|
}
|
|
|
|
#[inline(always)]
|
|
pub fn is_empty(&self) -> bool {
|
|
self.buf.is_empty()
|
|
}
|
|
|
|
#[inline(always)]
|
|
pub fn clear(&mut self) {
|
|
self.buf.clear();
|
|
}
|
|
|
|
#[inline(always)]
|
|
pub fn as_str(&self) -> &str {
|
|
std::str::from_utf8(&self.buf).unwrap_or_default()
|
|
}
|
|
}
|
|
|
|
impl Token {
|
|
pub fn unwrap_string(self) -> crate::parser::Result<String> {
|
|
match self {
|
|
Token::Argument(value) => {
|
|
String::from_utf8(value).map_err(|_| "Invalid UTF-8 in argument.".into())
|
|
}
|
|
other => Ok(other.to_string()),
|
|
}
|
|
}
|
|
|
|
pub fn unwrap_bytes(self) -> Vec<u8> {
|
|
match self {
|
|
Token::Argument(value) => value,
|
|
other => other.as_bytes().to_vec(),
|
|
}
|
|
}
|
|
|
|
pub fn eq_ignore_ascii_case(&self, bytes: &[u8]) -> bool {
|
|
match self {
|
|
Token::Argument(argument) => argument.eq_ignore_ascii_case(bytes),
|
|
Token::ParenthesisOpen => bytes.eq(b"("),
|
|
Token::ParenthesisClose => bytes.eq(b")"),
|
|
Token::BracketOpen => bytes.eq(b"["),
|
|
Token::BracketClose => bytes.eq(b"]"),
|
|
Token::Gt => bytes.eq(b">"),
|
|
Token::Lt => bytes.eq(b"<"),
|
|
Token::Dot => bytes.eq(b"."),
|
|
Token::Nil => bytes.is_empty(),
|
|
}
|
|
}
|
|
|
|
pub fn is_parenthesis_open(&self) -> bool {
|
|
matches!(self, Token::ParenthesisOpen)
|
|
}
|
|
|
|
pub fn is_parenthesis_close(&self) -> bool {
|
|
matches!(self, Token::ParenthesisClose)
|
|
}
|
|
|
|
pub fn is_bracket_open(&self) -> bool {
|
|
matches!(self, Token::BracketOpen)
|
|
}
|
|
|
|
pub fn is_bracket_close(&self) -> bool {
|
|
matches!(self, Token::BracketClose)
|
|
}
|
|
|
|
pub fn is_dot(&self) -> bool {
|
|
matches!(self, Token::Dot)
|
|
}
|
|
|
|
pub fn is_lt(&self) -> bool {
|
|
matches!(self, Token::Lt)
|
|
}
|
|
|
|
pub fn is_gt(&self) -> bool {
|
|
matches!(self, Token::Gt)
|
|
}
|
|
}
|
|
|
|
impl AsRef<[u8]> for ArgumentBuffer {
|
|
fn as_ref(&self) -> &[u8] {
|
|
&self.buf
|
|
}
|
|
}
|
|
|
|
impl Default for ArgumentBuffer {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl Display for Token {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
f.write_str(&String::from_utf8_lossy(self.as_bytes()))
|
|
}
|
|
}
|
|
|
|
impl Token {
|
|
pub fn as_bytes(&self) -> &[u8] {
|
|
match self {
|
|
Token::Argument(value) => value,
|
|
Token::ParenthesisOpen => b"(",
|
|
Token::ParenthesisClose => b")",
|
|
Token::BracketOpen => b"[",
|
|
Token::BracketClose => b"]",
|
|
Token::Gt => b">",
|
|
Token::Lt => b"<",
|
|
Token::Dot => b".",
|
|
Token::Nil => b"",
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Error {
|
|
pub fn err(tag: Option<impl Into<CompactString>>, message: impl Into<trc::Value>) -> Self {
|
|
Error::Error {
|
|
response: trc::ImapEvent::Error
|
|
.ctx(trc::Key::Details, message)
|
|
.ctx_opt(trc::Key::Id, tag.map(Into::into))
|
|
.ctx(trc::Key::Type, ResponseType::Bad)
|
|
.code(ResponseCode::Parse),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<T: CommandParser> Default for Receiver<T> {
|
|
fn default() -> Self {
|
|
Self {
|
|
buf: Default::default(),
|
|
request: Default::default(),
|
|
state: State::Start,
|
|
start_state: State::Start,
|
|
max_request_size: 25 * 1024 * 1024,
|
|
current_request_size: 0,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<T: CommandParser> Request<T> {
|
|
pub fn into_error(self, message: impl Into<trc::Value>) -> trc::Error {
|
|
trc::ImapEvent::Error
|
|
.ctx(trc::Key::Details, message)
|
|
.ctx(trc::Key::Id, CompactString::from_string_buffer(self.tag))
|
|
}
|
|
|
|
pub fn into_parse_error(self, message: impl Into<trc::Value>) -> trc::Error {
|
|
trc::ImapEvent::Error
|
|
.ctx(trc::Key::Details, message)
|
|
.ctx(trc::Key::Id, CompactString::from_string_buffer(self.tag))
|
|
.ctx(trc::Key::Code, ResponseCode::Parse)
|
|
.ctx(trc::Key::Type, ResponseType::Bad)
|
|
}
|
|
}
|
|
|
|
pub(crate) fn bad(tag: impl Into<trc::Value>, message: impl Into<trc::Value>) -> trc::Error {
|
|
trc::ImapEvent::Error
|
|
.ctx(trc::Key::Details, message)
|
|
.ctx(trc::Key::Id, tag)
|
|
.ctx(trc::Key::Type, ResponseType::Bad)
|
|
}
|
|
|
|
/*
|
|
|
|
astring = 1*ASTRING-CHAR / string
|
|
|
|
string = quoted / literal
|
|
|
|
literal = "{" number64 ["+"] "}" CRLF *CHAR8
|
|
|
|
quoted = DQUOTE *QUOTED-CHAR DQUOTE
|
|
|
|
ASTRING-CHAR = ATOM-CHAR / resp-specials
|
|
|
|
atom = 1*ATOM-CHAR
|
|
|
|
ATOM-CHAR = <any CHAR except atom-specials>
|
|
|
|
atom-specials = "(" / ")" / "{" / SP / CTL / list-wildcards /
|
|
quoted-specials / resp-specials
|
|
|
|
resp-specials = "]"
|
|
|
|
list-wildcards = "%" / "*"
|
|
|
|
quoted-specials = DQUOTE / "\"
|
|
|
|
DQUOTE = %x22 ; " (Double Quote)
|
|
|
|
*/
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
|
|
use crate::Command;
|
|
|
|
use super::{Error, Receiver, Request, Token};
|
|
|
|
#[test]
|
|
fn receiver_parse_ok() {
|
|
let mut receiver = Receiver::new();
|
|
|
|
for (frames, expected_requests) in [
|
|
(
|
|
vec!["abcd CAPABILITY\r\n"],
|
|
vec![Request {
|
|
tag: "abcd".into(),
|
|
command: Command::Capability,
|
|
tokens: vec![],
|
|
}],
|
|
),
|
|
(
|
|
vec!["A023 LO", "GOUT\r\n"],
|
|
vec![Request {
|
|
tag: "A023".into(),
|
|
command: Command::Logout,
|
|
tokens: vec![],
|
|
}],
|
|
),
|
|
(
|
|
vec![" A001 AUTHENTICATE GSSAPI \r\n"],
|
|
vec![Request {
|
|
tag: "A001".into(),
|
|
command: Command::Authenticate,
|
|
tokens: vec![Token::Argument(b"GSSAPI".to_vec())],
|
|
}],
|
|
),
|
|
(
|
|
vec!["A03 AUTHENTICATE ", "PLAIN dGVzdAB0ZXN", "0AHRlc3Q=\r\n"],
|
|
vec![Request {
|
|
tag: "A03".into(),
|
|
command: Command::Authenticate,
|
|
tokens: vec![
|
|
Token::Argument(b"PLAIN".to_vec()),
|
|
Token::Argument(b"dGVzdAB0ZXN0AHRlc3Q=".to_vec()),
|
|
],
|
|
}],
|
|
),
|
|
(
|
|
vec!["A003 CREATE owatagusiam/\r\n"],
|
|
vec![Request {
|
|
tag: "A003".into(),
|
|
command: Command::Create,
|
|
tokens: vec![Token::Argument(b"owatagusiam/".to_vec())],
|
|
}],
|
|
),
|
|
(
|
|
vec!["A682 LIST \"\" *\r\n"],
|
|
vec![Request {
|
|
tag: "A682".into(),
|
|
command: Command::List,
|
|
tokens: vec![Token::Nil, Token::Argument(b"*".to_vec())],
|
|
}],
|
|
),
|
|
(
|
|
vec!["A03 LIST () \"\" \"%\" RETURN (CHILDREN)\r\n"],
|
|
vec![Request {
|
|
tag: "A03".into(),
|
|
command: Command::List,
|
|
tokens: vec![
|
|
Token::ParenthesisOpen,
|
|
Token::ParenthesisClose,
|
|
Token::Nil,
|
|
Token::Argument(b"%".to_vec()),
|
|
Token::Argument(b"RETURN".to_vec()),
|
|
Token::ParenthesisOpen,
|
|
Token::Argument(b"CHILDREN".to_vec()),
|
|
Token::ParenthesisClose,
|
|
],
|
|
}],
|
|
),
|
|
(
|
|
vec!["A05 LIST (REMOTE SUBSCRIBED) \"\" \"*\"\r\n"],
|
|
vec![Request {
|
|
tag: "A05".into(),
|
|
command: Command::List,
|
|
tokens: vec![
|
|
Token::ParenthesisOpen,
|
|
Token::Argument(b"REMOTE".to_vec()),
|
|
Token::Argument(b"SUBSCRIBED".to_vec()),
|
|
Token::ParenthesisClose,
|
|
Token::Nil,
|
|
Token::Argument(b"*".to_vec()),
|
|
],
|
|
}],
|
|
),
|
|
(
|
|
vec!["a1 list \"\" (\"foo\")\r\n"],
|
|
vec![Request {
|
|
tag: "a1".into(),
|
|
command: Command::List,
|
|
tokens: vec![
|
|
Token::Nil,
|
|
Token::ParenthesisOpen,
|
|
Token::Argument(b"foo".to_vec()),
|
|
Token::ParenthesisClose,
|
|
],
|
|
}],
|
|
),
|
|
(
|
|
vec!["a3.1 LIST \"\" (% music/rock)\r\n"],
|
|
vec![Request {
|
|
tag: "a3.1".into(),
|
|
command: Command::List,
|
|
tokens: vec![
|
|
Token::Nil,
|
|
Token::ParenthesisOpen,
|
|
Token::Argument(b"%".to_vec()),
|
|
Token::Argument(b"music/rock".to_vec()),
|
|
Token::ParenthesisClose,
|
|
],
|
|
}],
|
|
),
|
|
(
|
|
vec!["A01 LIST \"\" % RETURN (STATUS (MESSAGES UNSEEN))\r\n"],
|
|
vec![Request {
|
|
tag: "A01".into(),
|
|
command: Command::List,
|
|
tokens: vec![
|
|
Token::Nil,
|
|
Token::Argument(b"%".to_vec()),
|
|
Token::Argument(b"RETURN".to_vec()),
|
|
Token::ParenthesisOpen,
|
|
Token::Argument(b"STATUS".to_vec()),
|
|
Token::ParenthesisOpen,
|
|
Token::Argument(b"MESSAGES".to_vec()),
|
|
Token::Argument(b"UNSEEN".to_vec()),
|
|
Token::ParenthesisClose,
|
|
Token::ParenthesisClose,
|
|
],
|
|
}],
|
|
),
|
|
(
|
|
vec![" A01 LiSt \"\" % RETURN ( STATUS ( MESSAGES UNSEEN ) ) \r\n"],
|
|
vec![Request {
|
|
tag: "A01".into(),
|
|
command: Command::List,
|
|
tokens: vec![
|
|
Token::Nil,
|
|
Token::Argument(b"%".to_vec()),
|
|
Token::Argument(b"RETURN".to_vec()),
|
|
Token::ParenthesisOpen,
|
|
Token::Argument(b"STATUS".to_vec()),
|
|
Token::ParenthesisOpen,
|
|
Token::Argument(b"MESSAGES".to_vec()),
|
|
Token::Argument(b"UNSEEN".to_vec()),
|
|
Token::ParenthesisClose,
|
|
Token::ParenthesisClose,
|
|
],
|
|
}],
|
|
),
|
|
(
|
|
vec!["A02 LIST (SUBSCRIBED RECURSIVEMATCH) \"\" % RETURN (STATUS (MESSAGES))\r\n"],
|
|
vec![Request {
|
|
tag: "A02".into(),
|
|
command: Command::List,
|
|
tokens: vec![
|
|
Token::ParenthesisOpen,
|
|
Token::Argument(b"SUBSCRIBED".to_vec()),
|
|
Token::Argument(b"RECURSIVEMATCH".to_vec()),
|
|
Token::ParenthesisClose,
|
|
Token::Nil,
|
|
Token::Argument(b"%".to_vec()),
|
|
Token::Argument(b"RETURN".to_vec()),
|
|
Token::ParenthesisOpen,
|
|
Token::Argument(b"STATUS".to_vec()),
|
|
Token::ParenthesisOpen,
|
|
Token::Argument(b"MESSAGES".to_vec()),
|
|
Token::ParenthesisClose,
|
|
Token::ParenthesisClose,
|
|
],
|
|
}],
|
|
),
|
|
(
|
|
vec!["A002 CREATE \"INBOX.Sent Mail\"\r\n"],
|
|
vec![Request {
|
|
tag: "A002".into(),
|
|
command: Command::Create,
|
|
tokens: vec![Token::Argument(b"INBOX.Sent Mail".to_vec())],
|
|
}],
|
|
),
|
|
(
|
|
vec!["A002 CREATE \"Maibox \\\"quo\\\\ted\\\" \"\r\n"],
|
|
vec![Request {
|
|
tag: "A002".into(),
|
|
command: Command::Create,
|
|
tokens: vec![Token::Argument(b"Maibox \"quo\\ted\" ".to_vec())],
|
|
}],
|
|
),
|
|
(
|
|
vec!["A004 COPY 2:4 meeting\r\n"],
|
|
vec![Request {
|
|
tag: "A004".into(),
|
|
command: Command::Copy(false),
|
|
tokens: vec![
|
|
Token::Argument(b"2:4".to_vec()),
|
|
Token::Argument(b"meeting".to_vec()),
|
|
],
|
|
}],
|
|
),
|
|
(
|
|
vec![
|
|
"A282 SEARCH RETURN (MIN COU",
|
|
"NT) FLAGGED SINCE 1-Feb-1994 ",
|
|
"NOT FROM \"Smith\"\r\n",
|
|
],
|
|
vec![Request {
|
|
tag: "A282".into(),
|
|
command: Command::Search(false),
|
|
tokens: vec![
|
|
Token::Argument(b"RETURN".to_vec()),
|
|
Token::ParenthesisOpen,
|
|
Token::Argument(b"MIN".to_vec()),
|
|
Token::Argument(b"COUNT".to_vec()),
|
|
Token::ParenthesisClose,
|
|
Token::Argument(b"FLAGGED".to_vec()),
|
|
Token::Argument(b"SINCE".to_vec()),
|
|
Token::Argument(b"1-Feb-1994".to_vec()),
|
|
Token::Argument(b"NOT".to_vec()),
|
|
Token::Argument(b"FROM".to_vec()),
|
|
Token::Argument(b"Smith".to_vec()),
|
|
],
|
|
}],
|
|
),
|
|
(
|
|
vec!["F284 UID STORE $ +FLAGS.Silent (\\Deleted)\r\n"],
|
|
vec![Request {
|
|
tag: "F284".into(),
|
|
command: Command::Store(true),
|
|
tokens: vec![
|
|
Token::Argument(b"$".to_vec()),
|
|
Token::Argument(b"+FLAGS.Silent".to_vec()),
|
|
Token::ParenthesisOpen,
|
|
Token::Argument(b"\\Deleted".to_vec()),
|
|
Token::ParenthesisClose,
|
|
],
|
|
}],
|
|
),
|
|
(
|
|
vec!["A654 FETCH 2:4 (FLAGS BODY[HEADER.FIELDS (DATE FROM)])\r\n"],
|
|
vec![Request {
|
|
tag: "A654".into(),
|
|
command: Command::Fetch(false),
|
|
tokens: vec![
|
|
Token::Argument(b"2:4".to_vec()),
|
|
Token::ParenthesisOpen,
|
|
Token::Argument(b"FLAGS".to_vec()),
|
|
Token::Argument(b"BODY".to_vec()),
|
|
Token::BracketOpen,
|
|
Token::Argument(b"HEADER".to_vec()),
|
|
Token::Dot,
|
|
Token::Argument(b"FIELDS".to_vec()),
|
|
Token::ParenthesisOpen,
|
|
Token::Argument(b"DATE".to_vec()),
|
|
Token::Argument(b"FROM".to_vec()),
|
|
Token::ParenthesisClose,
|
|
Token::BracketClose,
|
|
Token::ParenthesisClose,
|
|
],
|
|
}],
|
|
),
|
|
(
|
|
vec![
|
|
"B283 UID SEARCH RETURN (SAVE) CHARSET ",
|
|
"KOI8-R (OR $ 1,3000:3021) TEXT \"hello world\"\r\n",
|
|
],
|
|
vec![Request {
|
|
tag: "B283".into(),
|
|
command: Command::Search(true),
|
|
tokens: vec![
|
|
Token::Argument(b"RETURN".to_vec()),
|
|
Token::ParenthesisOpen,
|
|
Token::Argument(b"SAVE".to_vec()),
|
|
Token::ParenthesisClose,
|
|
Token::Argument(b"CHARSET".to_vec()),
|
|
Token::Argument(b"KOI8-R".to_vec()),
|
|
Token::ParenthesisOpen,
|
|
Token::Argument(b"OR".to_vec()),
|
|
Token::Argument(b"$".to_vec()),
|
|
Token::Argument(b"1,3000:3021".to_vec()),
|
|
Token::ParenthesisClose,
|
|
Token::Argument(b"TEXT".to_vec()),
|
|
Token::Argument(b"hello world".to_vec()),
|
|
],
|
|
}],
|
|
),
|
|
(
|
|
vec![
|
|
"P283 SEARCH CHARSET UTF-8 (OR $ 1,3000:3021) ",
|
|
"TEXT {8+}\r\nмать\r\n",
|
|
],
|
|
vec![Request {
|
|
tag: "P283".into(),
|
|
command: Command::Search(false),
|
|
tokens: vec![
|
|
Token::Argument(b"CHARSET".to_vec()),
|
|
Token::Argument(b"UTF-8".to_vec()),
|
|
Token::ParenthesisOpen,
|
|
Token::Argument(b"OR".to_vec()),
|
|
Token::Argument(b"$".to_vec()),
|
|
Token::Argument(b"1,3000:3021".to_vec()),
|
|
Token::ParenthesisClose,
|
|
Token::Argument(b"TEXT".to_vec()),
|
|
Token::Argument("мать".to_string().into_bytes()),
|
|
],
|
|
}],
|
|
),
|
|
(
|
|
vec!["A001 LOGIN {11}\r\n", "FRED FOOBAR {7}\r\n", "fat man\r\n"],
|
|
vec![Request {
|
|
tag: "A001".into(),
|
|
command: Command::Login,
|
|
tokens: vec![
|
|
Token::Argument(b"FRED FOOBAR".to_vec()),
|
|
Token::Argument(b"fat man".to_vec()),
|
|
],
|
|
}],
|
|
),
|
|
(
|
|
vec!["TAG3 CREATE \"Test-ąęć-Test\"\r\n"],
|
|
vec![Request {
|
|
tag: "TAG3".into(),
|
|
command: Command::Create,
|
|
tokens: vec![Token::Argument("Test-ąęć-Test".as_bytes().to_vec())],
|
|
}],
|
|
),
|
|
(
|
|
vec!["abc LOGIN {0}\r\n", "\r\n"],
|
|
vec![Request {
|
|
tag: "abc".into(),
|
|
command: Command::Login,
|
|
tokens: vec![Token::Nil],
|
|
}],
|
|
),
|
|
(
|
|
vec!["abc LOGIN {0+}\r\n\r\n"],
|
|
vec![Request {
|
|
tag: "abc".into(),
|
|
command: Command::Login,
|
|
tokens: vec![Token::Nil],
|
|
}],
|
|
),
|
|
(
|
|
vec![
|
|
"A003 APPEND saved-messages (\\Seen) {297+}\r\n",
|
|
"Date: Mon, 7 Feb 1994 21:52:25 -0800 (PST)\r\n",
|
|
"From: Fred Foobar <[email protected]>\r\n",
|
|
"Subject: afternoon meeting\r\n",
|
|
"To: [email protected]\r\n",
|
|
"Message-Id: <[email protected]>\r\n",
|
|
"MIME-Version: 1.0\r\n",
|
|
"Content-Type: TEXT/PLAIN; CHARSET=US-ASCII\r\n",
|
|
"\r\n",
|
|
"Hello Joe, do you think we can meet at 3:30 tomorrow?\r\n\r\n",
|
|
],
|
|
vec![Request {
|
|
tag: "A003".into(),
|
|
command: Command::Append,
|
|
tokens: vec![
|
|
Token::Argument(b"saved-messages".to_vec()),
|
|
Token::ParenthesisOpen,
|
|
Token::Argument(b"\\Seen".to_vec()),
|
|
Token::ParenthesisClose,
|
|
Token::Argument(
|
|
concat!(
|
|
"Date: Mon, 7 Feb 1994 21:52:25 -0800 (PST)\r\n",
|
|
"From: Fred Foobar <[email protected]>\r\n",
|
|
"Subject: afternoon meeting\r\n",
|
|
"To: [email protected]\r\n",
|
|
"Message-Id: <[email protected]>\r\n",
|
|
"MIME-Version: 1.0\r\n",
|
|
"Content-Type: TEXT/PLAIN; CHARSET=US-ASCII\r\n",
|
|
"\r\n",
|
|
"Hello Joe, do you think we can meet at 3:30 tomorrow?\r\n"
|
|
)
|
|
.as_bytes()
|
|
.to_vec(),
|
|
),
|
|
],
|
|
}],
|
|
),
|
|
(
|
|
vec![
|
|
"A003 APPEND saved-messages (\\Seen) {326}\r\n",
|
|
"Date: Mon, 7 Feb 1994 21:52:25 -0800 (PST)\r\n",
|
|
"From: Fred Foobar <[email protected]>\r\n",
|
|
"Subject: afternoon meeting\r\n",
|
|
"To: [email protected]\r\n",
|
|
"Message-Id: <[email protected]>\r\n",
|
|
"MIME-Version: 1.0\r\n",
|
|
"Content-Type: TEXT/PLAIN; CHARSET=US-ASCII\r\n",
|
|
"\r\n",
|
|
"Hello Joe, do you think we can meet at 3:30 tomorrow?\r\n\r\n",
|
|
],
|
|
vec![Request {
|
|
tag: "A003".into(),
|
|
command: Command::Append,
|
|
tokens: vec![
|
|
Token::Argument(b"saved-messages".to_vec()),
|
|
Token::ParenthesisOpen,
|
|
Token::Argument(b"\\Seen".to_vec()),
|
|
Token::ParenthesisClose,
|
|
Token::Argument(
|
|
concat!(
|
|
"Date: Mon, 7 Feb 1994 21:52:25 -0800 (PST)\r\n",
|
|
"From: Fred Foobar <[email protected]>\r\n",
|
|
"Subject: afternoon meeting\r\n",
|
|
"To: [email protected]\r\n",
|
|
"Message-Id: <[email protected]>\r\n",
|
|
"MIME-Version: 1.0\r\n",
|
|
"Content-Type: TEXT/PLAIN; CHARSET=US-ASCII\r\n",
|
|
"\r\n",
|
|
"Hello Joe, do you think we can meet at 3:30 tomorrow?\r\n",
|
|
)
|
|
.as_bytes()
|
|
.to_vec(),
|
|
),
|
|
],
|
|
}],
|
|
),
|
|
(
|
|
vec!["001 NOOP\r\n002 CAPABILITY\r\nabc LOGIN hello world\r\n"],
|
|
vec![
|
|
Request {
|
|
tag: "001".into(),
|
|
command: Command::Noop,
|
|
tokens: vec![],
|
|
},
|
|
Request {
|
|
tag: "002".into(),
|
|
command: Command::Capability,
|
|
tokens: vec![],
|
|
},
|
|
Request {
|
|
tag: "abc".into(),
|
|
command: Command::Login,
|
|
tokens: vec![
|
|
Token::Argument(b"hello".to_vec()),
|
|
Token::Argument(b"world".to_vec()),
|
|
],
|
|
},
|
|
],
|
|
),
|
|
] {
|
|
let mut requests = Vec::new();
|
|
for frame in &frames {
|
|
let mut bytes = frame.as_bytes().iter();
|
|
loop {
|
|
match receiver.parse(&mut bytes) {
|
|
Ok(request) => requests.push(request),
|
|
Err(Error::NeedsMoreData | Error::NeedsLiteral { .. }) => break,
|
|
Err(err) => panic!("{:?} for frames {:#?}", err, frames),
|
|
}
|
|
}
|
|
}
|
|
assert_eq!(requests, expected_requests, "{:#?}", frames);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn receiver_parse_invalid() {
|
|
let mut receiver = Receiver::<Command>::new();
|
|
for invalid in [
|
|
//"\r\n",
|
|
//" \r \n",
|
|
"a001\r\n",
|
|
"a001 unknown\r\n",
|
|
"a001 login {abc}\r\n",
|
|
"a001 login {+30}\r\n",
|
|
"a001 login {30} junk\r\n",
|
|
] {
|
|
match receiver.parse(&mut invalid.as_bytes().iter()) {
|
|
Err(Error::Error { .. }) => {}
|
|
result => panic!("Expecter error, got: {:?}", result),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn receiver_discard_oversized_non_sync_literal() {
|
|
for frames in [
|
|
vec![
|
|
"a1 APPEND inbox (\\Draft) {40+}\r\n",
|
|
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\r\n",
|
|
"b2 NOOP\r\n",
|
|
],
|
|
vec![
|
|
"a1 APPEND inbox (\\Draft) {40+}\r\nAAAAAAAAAAAAAAAAAAAA",
|
|
"AAAAAAAAAAAAAAAAAAAA\r\nb2 NOOP\r\n",
|
|
],
|
|
vec![
|
|
"a1 APPEND inbox (\\Draft) {40+}\r\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\r\nb2 NOOP\r\n",
|
|
],
|
|
] {
|
|
let mut receiver = Receiver::<Command>::with_max_request_size(30);
|
|
let mut requests = Vec::new();
|
|
let mut errors = 0;
|
|
for frame in &frames {
|
|
let mut bytes = frame.as_bytes().iter();
|
|
loop {
|
|
match receiver.parse(&mut bytes) {
|
|
Ok(request) => requests.push(request),
|
|
Err(Error::NeedsMoreData | Error::NeedsLiteral { .. }) => break,
|
|
Err(Error::Error { .. }) => errors += 1,
|
|
}
|
|
}
|
|
}
|
|
|
|
assert_eq!(errors, 1, "expected a single error for {:#?}", frames);
|
|
assert_eq!(
|
|
requests,
|
|
vec![Request {
|
|
tag: "b2".into(),
|
|
command: Command::Noop,
|
|
tokens: vec![],
|
|
}],
|
|
"connection did not resync for {:#?}",
|
|
frames
|
|
);
|
|
}
|
|
}
|
|
}
|