diff --git a/rules/Makefile.am b/rules/Makefile.am index 73226efb3a..10f9af3715 100644 --- a/rules/Makefile.am +++ b/rules/Makefile.am @@ -12,6 +12,7 @@ files.rules \ ftp-events.rules \ http-events.rules \ http2-events.rules \ +imap-events.rules \ ipsec-events.rules \ kerberos-events.rules \ ldap-events.rules \ diff --git a/rules/README.md b/rules/README.md index 90a266e320..44e17ae17d 100644 --- a/rules/README.md +++ b/rules/README.md @@ -39,6 +39,7 @@ signature IDs. | PGSQL | 2241000 | 2241999 | | mDNS | 2242000 | 2242999 | | Bittorent| 2243000 | 2243999 | +| IMAP | 2244000 | 2244999 | | MODBUS | 2250000 | 2250999 | | DNP3 | 2270000 | 2270999 | | SIP | 2280000 | 2280999 | diff --git a/rules/imap-events.rules b/rules/imap-events.rules new file mode 100644 index 0000000000..87f6d953a2 --- /dev/null +++ b/rules/imap-events.rules @@ -0,0 +1,8 @@ +# IMAP app layer event rules +# +# SID's fall in the 2244000+ range. See rules/README.md + +alert imap any any -> any any (msg:"SURICATA IMAP too many transactions"; app-layer-event:imap.too_many_transactions; classtype:protocol-command-decode; sid:2244000; rev:1;) +alert imap any any -> any any (msg:"SURICATA IMAP invalid data"; app-layer-event:imap.invalid_data; classtype:protocol-command-decode; sid:2244001; rev:1;) +alert imap any any -> any any (msg:"SURICATA IMAP line too long"; app-layer-event:imap.line_too_long; classtype:protocol-command-decode; sid:2244002; rev:1;) +alert imap any any -> any any (msg:"SURICATA IMAP data limit reached"; app-layer-event:imap.data_limit_reached; classtype:protocol-command-decode; sid:2244003; rev:1;) diff --git a/rust/src/imap/imap.rs b/rust/src/imap/imap.rs new file mode 100644 index 0000000000..204495bca8 --- /dev/null +++ b/rust/src/imap/imap.rs @@ -0,0 +1,1394 @@ +/* Copyright (C) 2026 Open Information Security Foundation + * + * You can copy, redistribute or modify this Program under the terms of + * the GNU General Public License version 2 as published by the Free + * Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * version 2 along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +// Author: Giuseppe Longo + +use crate::applayer::{self, *}; +use crate::conf::conf_get; +use crate::core::*; +use crate::direction::Direction; +use crate::flow::Flow; +use crate::frames::*; +use suricata_derive::AppLayerState; + +use crate::imap::parser::{ + parse_command, parse_command_continuation, parse_continuation_data, parse_response, + parse_response_continuation, peek_untagged, probe_command_prefix, sequence_set_contains, + untagged_trailing_literal, CommandLiteral, ImapCommand, ImapMessage, ImapMessageType, + ImapResponseStatus, LiteralInfo, IMAP_MAX_LINE_SIZE, +}; +use nom8::error::{Error, ErrorKind}; +use nom8::Err; +use std; +use std::collections::VecDeque; +use std::ffi::CString; +use std::os::raw::{c_char, c_int, c_void}; +use suricata_sys::sys::{ + AppLayerParserState, AppProto, SCAppLayerParserConfParserEnabled, + SCAppLayerParserRegisterLogger, SCAppLayerParserStateIssetFlag, + SCAppLayerProtoDetectConfProtoDetectionEnabled, SCAppLayerProtoDetectGetStreamDataSize, + SCAppLayerProtoDetectPMRegisterPatternCI, SCAppLayerProtoDetectPMRegisterPatternCIwPP, + SCAppLayerRequestProtocolTLSUpgrade, SCFlowGetAppProtocolToClient, +}; + +const IMAP_MAX_TX_DEFAULT: usize = 256; +const IMAP_MAX_LINES: usize = 512; +const IMAP_MAX_MSGS_PER_TX: usize = 512; +const IMAP_MAX_LINE_BYTES_PER_STATE: usize = 100 * 1024 * 1024; + +static mut IMAP_MAX_TX: usize = IMAP_MAX_TX_DEFAULT; + +pub(super) static mut ALPROTO_IMAP: AppProto = ALPROTO_UNKNOWN; + +#[derive(AppLayerFrameType)] +pub enum ImapFrameType { + Pdu, +} + +#[derive(AppLayerEvent)] +enum ImapEvent { + TooManyTransactions, + InvalidData, + LineTooLong, + DataLimitReached, +} + +fn retained_request_bytes(request: &ImapMessage) -> usize { + let mut bytes = request.raw_line.capacity() + request.tag.as_ref().map_or(0, Vec::capacity); + if let ImapMessageType::Command { + command, arguments, .. + } = &request.message + { + bytes += arguments.capacity() * std::mem::size_of::>() + + arguments.iter().map(Vec::capacity).sum::(); + if let ImapCommand::Unknown(name) = command { + bytes += name.capacity(); + } + } + bytes +} + +fn retain_item(items: &mut Vec, item: T, mut bytes: usize, budget: &mut usize) -> bool { + if bytes > *budget { + return false; + } + if items.len() == items.capacity() { + let slots = ((*budget - bytes) / std::mem::size_of::()).min(items.len().max(1)); + if slots == 0 { + return false; + } + let mut grown = Vec::with_capacity(items.len() + slots); + bytes += (grown.capacity() - items.capacity()) * std::mem::size_of::(); + if bytes > *budget { + return false; + } + grown.append(items); + *items = grown; + } + items.push(item); + *budget -= bytes; + true +} + +fn extract_command(request: &ImapMessage) -> Vec { + if let ImapMessageType::Command { + command, arguments, .. + } = &request.message + { + // UID is a prefix, the actual command (FETCH, STORE, etc.) is in arguments[0]. + if matches!(command, ImapCommand::Uid) { + arguments + .first() + .map(|arg| arg.to_ascii_uppercase()) + .unwrap_or_default() + } else { + command.to_string().into_bytes() + } + } else { + Vec::new() + } +} + +#[derive(AppLayerState, Copy, Clone, Debug, PartialOrd, PartialEq, Eq)] +#[suricata(alstate_strip_prefix = "ImapState")] +pub enum ImapStateProgress { + ImapStateInProgress = 0, + ImapStateComplete = 1, +} + +#[derive(Debug)] +pub struct ImapTransaction { + pub tx_id: u64, + + progress_ts: ImapStateProgress, + progress_tc: ImapStateProgress, + + pub requests: Vec, + pub response_lines: Vec>, + + request_tag: Option>, + pub command: Vec, + line_bytes: usize, + data_limit_event_set: bool, + + tx_data: AppLayerTxData, +} + +impl ImapTransaction { + pub fn new() -> ImapTransaction { + Self { + tx_id: 0, + progress_ts: ImapStateProgress::ImapStateInProgress, + progress_tc: ImapStateProgress::ImapStateInProgress, + requests: Vec::new(), + response_lines: Vec::new(), + request_tag: None, + command: Vec::new(), + line_bytes: 0, + data_limit_event_set: false, + tx_data: AppLayerTxData::new(), + } + } + + fn complete(&self) -> bool { + self.progress_tc == ImapStateProgress::ImapStateComplete + } + + fn awaiting_response(&self) -> bool { + !self.complete() && self.request_tag.is_some() + } + + fn update_completion_from_response(&mut self, response: &ImapMessage) { + let completes = match &self.request_tag { + Some(request_tag) => { + response.tag.as_deref() == Some(request_tag.as_slice()) + && matches!(response.message, ImapMessageType::Response { .. }) + } + None => true, + }; + if completes { + if self.request_tag.is_some() { + self.complete_request(); + } + self.progress_tc = ImapStateProgress::ImapStateComplete; + } + } + + fn complete_request(&mut self) { + if self.progress_ts != ImapStateProgress::ImapStateComplete { + self.progress_ts = ImapStateProgress::ImapStateComplete; + self.tx_data.0.updated_ts = true; + } + } + + fn complete_response(&mut self) { + if self.progress_tc != ImapStateProgress::ImapStateComplete { + self.progress_tc = ImapStateProgress::ImapStateComplete; + self.tx_data.0.updated_tc = true; + } + } + + fn mark_data_limit(&mut self) { + if !self.data_limit_event_set { + self.tx_data.set_event(ImapEvent::DataLimitReached as u8); + self.data_limit_event_set = true; + } + } + + fn add_request(&mut self, req: ImapMessage, line_budget: usize) -> bool { + self.tx_data.0.updated_ts = true; + if req.line_truncated { + self.tx_data.set_event(ImapEvent::LineTooLong as u8); + } + let mut rem = line_budget; + if self.request_tag.is_none() && matches!(req.message, ImapMessageType::Command { .. }) { + let tag = req.tag.clone(); + let command = extract_command(&req); + let bytes = tag.as_ref().map_or(0, Vec::capacity) + command.capacity(); + if bytes > rem { + self.mark_data_limit(); + return false; + } + rem -= bytes; + self.request_tag = tag; + self.command = command; + } + let request_bytes = retained_request_bytes(&req); + if self.requests.len() >= IMAP_MAX_MSGS_PER_TX + || !retain_item(&mut self.requests, req, request_bytes, &mut rem) + { + self.mark_data_limit(); + } + self.line_bytes += line_budget - rem; + true + } + + fn add_response(&mut self, mut resp: ImapMessage, line_budget: usize) { + self.tx_data.0.updated_tc = true; + let mut rem = line_budget; + let bytes = resp.raw_line.capacity(); + if !resp.raw_line.is_empty() + && (self.response_lines.len() >= IMAP_MAX_LINES + || !retain_item( + &mut self.response_lines, + std::mem::take(&mut resp.raw_line), + bytes, + &mut rem, + )) + { + self.mark_data_limit(); + } + self.line_bytes += line_budget - rem; + if resp.line_truncated { + self.tx_data.set_event(ImapEvent::LineTooLong as u8); + } + self.update_completion_from_response(&resp); + } +} + +impl Default for ImapTransaction { + fn default() -> Self { + Self::new() + } +} + +impl Transaction for ImapTransaction { + fn id(&self) -> u64 { + self.tx_id + } +} + +#[derive(Debug)] +struct PendingLiteral { + tx_id: u64, + literal: LiteralInfo, + continuation_received: bool, + awaiting_line_rest: bool, + parenthesis_depth: usize, +} + +impl PendingLiteral { + fn new(tx_id: u64, literal: CommandLiteral) -> Self { + Self { + tx_id, + literal: LiteralInfo::new(literal.size, literal.is_literal_plus), + continuation_received: false, + awaiting_line_rest: false, + parenthesis_depth: literal.parenthesis_depth, + } + } + + fn is_ready(&self) -> bool { + self.literal.is_literal_plus || self.continuation_received + } +} + +#[derive(Debug)] +struct PendingResponseLiteral { + tx_id: Option, + // Untagged response announcing the literal. The text following the + // literal octets is appended to its line until the response ends. + response: ImapMessage, + literal: LiteralInfo, + awaiting_line_rest: bool, +} + +impl PendingResponseLiteral { + fn new(tx_id: Option, response: ImapMessage, size: u64) -> Self { + Self { + tx_id, + response, + literal: LiteralInfo::new(size, false), + awaiting_line_rest: size == 0, + } + } + + fn append_line_rest(&mut self, rest: &[u8]) { + let line = &mut self.response.raw_line; + let room = IMAP_MAX_LINE_SIZE.saturating_sub(line.len()); + if rest.len() > room { + self.response.line_truncated = true; + } + line.extend_from_slice(&rest[..rest.len().min(room)]); + } +} + +const UNTAGGED_KEYWORD_COMMANDS: &[(&[u8], &[&[u8]])] = &[ + (b"FETCH", &[b"FETCH"]), + (b"SEARCH", &[b"SEARCH"]), + (b"ESEARCH", &[b"SEARCH"]), + (b"FLAGS", &[b"SELECT", b"EXAMINE"]), + (b"EXISTS", &[b"SELECT", b"EXAMINE"]), + (b"RECENT", &[b"SELECT", b"EXAMINE"]), + (b"CAPABILITY", &[b"CAPABILITY"]), + (b"LIST", &[b"LIST"]), + (b"LSUB", &[b"LSUB"]), + (b"STATUS", &[b"STATUS"]), + (b"ID", &[b"ID"]), + (b"NAMESPACE", &[b"NAMESPACE"]), + (b"ENABLED", &[b"ENABLE"]), + (b"SORT", &[b"SORT"]), + (b"THREAD", &[b"THREAD"]), + (b"QUOTA", &[b"GETQUOTA", b"GETQUOTAROOT", b"SETQUOTA"]), + (b"QUOTAROOT", &[b"GETQUOTAROOT"]), +]; + +pub struct ImapState { + state_data: AppLayerStateData, + tx_id: u64, + transactions: VecDeque, + request_frame: Option, + response_frame: Option, + request_gap: bool, + response_gap: bool, + // Transaction currently receiving a group of untagged responses. + active_response_tx_id: Option, + // Transaction that owns the next client continuation line. + continuation_tx_id: Option, + // Literal announced by the current client command line, until its line ends. + pending_literal: Option, + // Literal announced by an untagged non-FETCH response, until its line ends. + pending_response_literal: Option, +} + +impl State for ImapState { + fn get_transaction_count(&self) -> usize { + self.transactions.len() + } + + fn get_transaction_by_index(&self, index: usize) -> Option<&ImapTransaction> { + self.transactions.get(index) + } +} + +impl Default for ImapState { + fn default() -> Self { + Self::new() + } +} + +impl ImapState { + pub fn new() -> Self { + Self { + state_data: AppLayerStateData::default(), + tx_id: 0, + transactions: VecDeque::new(), + request_frame: None, + response_frame: None, + request_gap: false, + response_gap: false, + active_response_tx_id: None, + continuation_tx_id: None, + pending_literal: None, + pending_response_literal: None, + } + } + + fn line_retain_budget(&self) -> usize { + let used: usize = self.transactions.iter().map(|tx| tx.line_bytes).sum(); + IMAP_MAX_LINE_BYTES_PER_STATE.saturating_sub(used) + } + + fn tx_by_id(&self, tx_id: u64) -> Option<&ImapTransaction> { + self.transactions.iter().find(|tx| tx.tx_id == tx_id) + } + + fn tx_by_id_mut(&mut self, tx_id: u64) -> Option<&mut ImapTransaction> { + self.transactions.iter_mut().find(|tx| tx.tx_id == tx_id) + } + + fn active_response_tx_id(&self) -> Option { + self.active_response_tx_id + .and_then(|tx_id| self.tx_by_id(tx_id)) + .filter(|tx| tx.awaiting_response()) + .map(|tx| tx.tx_id) + } + + fn single_pending_tx_id(&self) -> Option { + let mut outstanding = self.transactions.iter().filter(|tx| tx.awaiting_response()); + match (outstanding.next(), outstanding.next()) { + (Some(tx), None) => Some(tx.tx_id), + _ => None, + } + } + + fn fetch_request_may_include_sequence( + tx: &ImapTransaction, sequence_number: Option, + ) -> bool { + let Some(sequence_number) = sequence_number else { + return true; + }; + let Some(request) = tx.requests.first() else { + return true; + }; + + match &request.message { + ImapMessageType::Command { + command: ImapCommand::Fetch, + arguments, + .. + } => arguments + .first() + .and_then(|set| sequence_set_contains(set, sequence_number)) + .unwrap_or(true), + // An untagged UID FETCH response starts with the message sequence + // number, not the UID requested by the client. + _ => true, + } + } + + fn resolve_fetch_response_target(&self, sequence_number: Option) -> Option { + let mut candidate = None; + let mut found_fetch_request = false; + + for tx in self + .transactions + .iter() + .filter(|tx| tx.awaiting_response() && tx.command.eq_ignore_ascii_case(b"FETCH")) + { + found_fetch_request = true; + if !Self::fetch_request_may_include_sequence(tx, sequence_number) { + continue; + } + if candidate.is_some() { + return None; + } + candidate = Some(tx.tx_id); + } + + if found_fetch_request { + candidate + } else { + self.single_pending_tx_id() + } + } + + fn resolve_untagged_response_target( + &self, sequence_number: Option, keyword: &[u8], + ) -> Option { + if keyword.eq_ignore_ascii_case(b"FETCH") { + return self.resolve_fetch_response_target(sequence_number); + } + + if let Some((_, expected_commands)) = UNTAGGED_KEYWORD_COMMANDS + .iter() + .find(|(untagged, _)| keyword.eq_ignore_ascii_case(untagged)) + { + let mut candidate = None; + for tx in self.transactions.iter().filter(|tx| tx.awaiting_response()) { + if expected_commands + .iter() + .any(|command| tx.command.eq_ignore_ascii_case(command)) + { + if candidate.is_some() { + return None; + } + candidate = Some(tx.tx_id); + } + } + return candidate.or_else(|| self.single_pending_tx_id()); + } + + if let Some(tx_id) = self.active_response_tx_id() { + return Some(tx_id); + } + + self.single_pending_tx_id() + } + + fn commit_untagged_response_target(&mut self, target: Option) -> Option { + self.active_response_tx_id = target; + target + } + + fn continuation_response_tx_id(&self) -> Option { + self.pending_literal + .as_ref() + .map(|pending| pending.tx_id) + .or_else(|| { + self.transactions + .iter() + .rev() + .find(|tx| { + !tx.complete() + && tx.progress_ts == ImapStateProgress::ImapStateInProgress + && (tx.command.eq_ignore_ascii_case(b"AUTHENTICATE") + || tx.command.eq_ignore_ascii_case(b"IDLE")) + }) + .map(|tx| tx.tx_id) + }) + .or_else(|| { + self.transactions + .iter() + .rev() + .find(|tx| !tx.complete()) + .map(|tx| tx.tx_id) + }) + } + + fn forget_tx(&mut self, tx_id: u64) { + if self.active_response_tx_id == Some(tx_id) { + self.active_response_tx_id = None; + } + if self.continuation_tx_id == Some(tx_id) { + self.continuation_tx_id = None; + } + if self + .pending_literal + .as_ref() + .is_some_and(|pending| pending.tx_id == tx_id) + { + self.pending_literal = None; + } + if let Some(pending) = self + .pending_response_literal + .as_mut() + .filter(|pending| pending.tx_id == Some(tx_id)) + { + pending.tx_id = None; + } + } + + fn free_tx(&mut self, tx_id: u64) { + if let Some(index) = self + .transactions + .iter() + .position(|tx| tx.tx_id == tx_id + 1) + { + self.forget_tx(tx_id + 1); + self.transactions.remove(index); + } + } + + pub fn get_tx(&mut self, tx_id: u64) -> Option<&ImapTransaction> { + self.transactions.iter().find(|tx| tx.tx_id == tx_id + 1) + } + + pub fn new_tx(&mut self) -> Option { + if self.transactions.len() >= unsafe { IMAP_MAX_TX } { + self.active_response_tx_id = None; + self.continuation_tx_id = None; + self.pending_literal = None; + self.pending_response_literal = None; + self.response_frame = None; + for tx_old in &mut self.transactions { + if !tx_old.complete() { + tx_old.tx_data.0.updated_tc = true; + tx_old.tx_data.0.updated_ts = true; + tx_old.progress_ts = ImapStateProgress::ImapStateComplete; + tx_old.progress_tc = ImapStateProgress::ImapStateComplete; + tx_old + .tx_data + .set_event(ImapEvent::TooManyTransactions as u8); + } + } + return None; + } + let mut tx = ImapTransaction::new(); + self.tx_id += 1; + tx.tx_id = self.tx_id; + return Some(tx); + } + + /// At end of stream, complete the given direction of every open + /// transaction so buffers that are already populated (e.g. the response + /// lines) are inspected even without a tagged completion reply. + fn complete_transactions_at_eof(&mut self, direction: Direction) { + if matches!(direction, Direction::ToClient) { + if let Some(pending) = self.pending_response_literal.take() { + self.complete_pending_response(pending); + } + } + for tx in &mut self.transactions { + match direction { + Direction::ToServer => tx.complete_request(), + Direction::ToClient => tx.complete_response(), + } + } + } + + fn push_response_tx(&mut self) -> Option<&mut ImapTransaction> { + let mut tx = self.new_tx()?; + tx.tx_data = AppLayerTxData::for_direction(Direction::ToClient); + tx.progress_ts = ImapStateProgress::ImapStateComplete; + self.transactions.push_back(tx); + self.transactions.back_mut() + } + + fn set_event(&mut self, e: ImapEvent) { + if let Some(tx) = self.transactions.back_mut() { + tx.tx_data.set_event(e as u8); + } + } + + fn reject_oversized_tag(&mut self, direction: Direction) -> AppLayerResult { + if let Some(mut tx) = self.new_tx() { + tx.tx_data = AppLayerTxData::for_direction(direction); + tx.tx_data.set_event(ImapEvent::LineTooLong as u8); + tx.progress_ts = ImapStateProgress::ImapStateComplete; + tx.progress_tc = ImapStateProgress::ImapStateComplete; + self.transactions.push_back(tx); + } + AppLayerResult::err() + } + + fn find_request(&mut self, tag: &[u8]) -> Option<&mut ImapTransaction> { + self.transactions + .iter_mut() + .find(|tx| tx.request_tag.as_deref() == Some(tag) && !tx.complete()) + } + + fn parse_request(&mut self, flow: *mut Flow, stream_slice: StreamSlice) -> AppLayerResult { + let input = stream_slice.as_slice(); + if input.is_empty() { + return AppLayerResult::ok(); + } + + if self.request_gap { + if parse_command(input).is_err() { + return AppLayerResult::ok(); + } + self.request_gap = false; + } + + let incomplete = |rest: &[u8]| { + AppLayerResult::incomplete((input.len() - rest.len()) as u32, (rest.len() + 1) as u32) + }; + let mut start = input; + while !start.is_empty() { + if let Some((tx_id, depth)) = self + .pending_literal + .as_ref() + .filter(|pending| pending.awaiting_line_rest) + .map(|pending| (pending.tx_id, pending.parenthesis_depth)) + { + match parse_command_continuation(start, depth) { + Ok((rem, (request, literal))) => { + if !request.raw_line.is_empty() { + let line_budget = self.line_retain_budget(); + if let Some(tx) = self.tx_by_id_mut(tx_id) { + tx.add_request(request, line_budget); + } + } + if let Some(literal) = literal { + self.pending_literal = Some(PendingLiteral::new(tx_id, literal)); + } else { + self.pending_literal = None; + if let Some(tx) = self.tx_by_id_mut(tx_id) { + tx.complete_request(); + } + } + start = rem; + continue; + } + Err(Err::Incomplete(_)) => return incomplete(start), + Err(_) => { + self.set_event(ImapEvent::InvalidData); + return AppLayerResult::err(); + } + } + } + + if self.request_frame.is_none() { + self.request_frame = Frame::new( + flow, + &stream_slice, + start, + -1_i64, + ImapFrameType::Pdu as u8, + None, + ); + SCLogDebug!("ts: pdu {:?}", self.request_frame); + } + + if let Some(mut pending) = self.pending_literal.take_if(|pending| pending.is_ready()) { + let consumed = pending.literal.consume_chunk(start); + if pending.literal.remaining() != 0 { + self.pending_literal = Some(pending); + return AppLayerResult::ok(); + } + + let tx_id = pending.tx_id; + if let Some(tx) = self.tx_by_id_mut(tx_id) { + tx.tx_data.0.updated_ts = true; + let frame_len = i64::try_from(pending.literal.size).unwrap_or(i64::MAX); + self.set_frame_ts(flow, tx_id, frame_len); + } + + pending.awaiting_line_rest = true; + self.pending_literal = Some(pending); + start = &start[consumed..]; + continue; + } + + if let Some(continuation_tx_id) = self.continuation_tx_id { + match parse_continuation_data(start) { + Ok((rem, request)) => { + let consumed = start.len() - rem.len(); + + self.continuation_tx_id = None; + + let line_budget = self.line_retain_budget(); + if let Some(tx) = self + .tx_by_id_mut(continuation_tx_id) + .filter(|tx| !tx.complete()) + { + let tx_id = tx.id(); + let data = request.raw_line.as_slice(); + let completes_request = (tx.command.eq_ignore_ascii_case(b"IDLE") + && data.eq_ignore_ascii_case(b"DONE")) + || (tx.command.eq_ignore_ascii_case(b"AUTHENTICATE") + && data == b"*"); + tx.add_request(request, line_budget); + if completes_request { + tx.complete_request(); + } + start = rem; + self.set_frame_ts(flow, tx_id, consumed as i64); + continue; + } + } + Err(Err::Incomplete(_)) => return incomplete(start), + Err(_) => {} + } + self.continuation_tx_id = None; + if let Some(pos) = start.iter().position(|&c| c == b'\n') { + start = &start[pos + 1..]; + continue; + } else { + break; + } + } + + match parse_command(start) { + Ok((rem, request)) => { + let consumed = start.len() - rem.len(); + + let mut setup_literal = None; + let mut client_data_pending = false; + if let ImapMessageType::Command { + command, literal, .. + } = &request.message + { + if let Some(literal) = literal { + setup_literal = Some(*literal); + client_data_pending = true; + } + if matches!(command, ImapCommand::Authenticate | ImapCommand::Idle) { + client_data_pending = true; + } + } + + let line_budget = self.line_retain_budget(); + let Some(mut tx) = self.new_tx() else { + return AppLayerResult::err(); + }; + let tx_id = tx.id(); + if !tx.add_request(request, line_budget) { + tx.progress_ts = ImapStateProgress::ImapStateComplete; + tx.progress_tc = ImapStateProgress::ImapStateComplete; + self.transactions.push_back(tx); + return AppLayerResult::err(); + } + if !client_data_pending { + tx.complete_request(); + } + self.transactions.push_back(tx); + start = rem; + self.set_frame_ts(flow, tx_id, consumed as i64); + + if let Some(literal) = setup_literal { + self.pending_literal = Some(PendingLiteral::new(tx_id, literal)); + } + } + Err(Err::Incomplete(_)) => return incomplete(start), + Err(Err::Error(e)) if e.code == ErrorKind::Eof => { + break; + } + Err(Err::Failure(e)) if e.code == ErrorKind::TooLarge => { + return self.reject_oversized_tag(Direction::ToServer); + } + Err(_e) => { + self.set_event(ImapEvent::InvalidData); + return AppLayerResult::err(); + } + } + } + + return AppLayerResult::ok(); + } + + fn parse_response(&mut self, flow: *mut Flow, stream_slice: StreamSlice) -> AppLayerResult { + let input = stream_slice.as_slice(); + if input.is_empty() { + return AppLayerResult::ok(); + } + + if self.response_gap { + if probe_response(input).is_err() { + return AppLayerResult::ok(); + } + self.response_gap = false; + } + + let incomplete = |rest: &[u8]| { + AppLayerResult::incomplete((input.len() - rest.len()) as u32, (rest.len() + 1) as u32) + }; + let mut start = input; + while !start.is_empty() { + if let Some(mut pending) = self + .pending_response_literal + .take_if(|pending| pending.awaiting_line_rest) + { + match parse_response_continuation(start) { + Ok((rem, (rest, literal_size))) => { + pending.append_line_rest(rest); + if let Some(size) = literal_size { + self.pending_response_literal = Some(PendingResponseLiteral::new( + pending.tx_id, + pending.response, + size, + )); + } else { + self.complete_pending_response(pending); + } + start = rem; + continue; + } + Err(Err::Incomplete(_)) => { + self.pending_response_literal = Some(pending); + return incomplete(start); + } + Err(_) => { + self.set_event(ImapEvent::InvalidData); + return AppLayerResult::err(); + } + } + } + + if self.response_frame.is_none() { + self.response_frame = Frame::new( + flow, + &stream_slice, + start, + -1_i64, + ImapFrameType::Pdu as u8, + None, + ); + SCLogDebug!("tc: pdu {:?}", self.response_frame); + } + + if let Some(pending) = self + .pending_response_literal + .as_mut() + .filter(|pending| !pending.awaiting_line_rest) + { + let consumed = pending.literal.consume_chunk(start); + if pending.literal.remaining() != 0 { + return AppLayerResult::ok(); + } + + pending.awaiting_line_rest = true; + let tx_id = pending.tx_id; + let frame_len = i64::try_from(pending.literal.size).unwrap_or(i64::MAX); + if let Some(tx_id) = tx_id { + self.set_frame_tc(flow, tx_id, frame_len); + } else { + self.response_frame = None; + } + start = &start[consumed..]; + continue; + } + + let untagged = peek_untagged(start); + let untagged_response_target = untagged.and_then(|(sequence_number, keyword)| { + self.resolve_untagged_response_target(sequence_number, keyword) + }); + let line_budget = self.line_retain_budget(); + + match parse_response(start) { + Ok((rem, response)) => { + let consumed = start.len() - rem.len(); + + if let Some(ref tag) = response.tag { + if let Some(tx) = self.find_request(tag) { + let tx_id = tx.id(); + let starttls_ok = tx.command.eq_ignore_ascii_case(b"STARTTLS") + && matches!( + response.message, + ImapMessageType::Response { + status: ImapResponseStatus::Ok + } + ); + tx.add_response(response, line_budget); + if starttls_ok { + SCLogDebug!("IMAP: STARTTLS completed"); + unsafe { + SCAppLayerRequestProtocolTLSUpgrade(flow); + } + } + /* The tagged reply completes the command, so no + * pending client or server state may refer to it. */ + self.forget_tx(tx_id); + self.set_frame_tc(flow, tx_id, consumed as i64); + } else { + let Some(tx) = self.push_response_tx() else { + return AppLayerResult::err(); + }; + let tx_id = tx.id(); + tx.add_response(response, line_budget); + self.set_frame_tc(flow, tx_id, consumed as i64); + } + } else { + let is_continuation = + matches!(response.message, ImapMessageType::Continuation); + + let untagged_target = if let ImapMessageType::Untagged { + seq_number, + keyword, + .. + } = &response.message + { + Some(untagged_response_target.or_else(|| { + self.resolve_untagged_response_target(*seq_number, keyword) + })) + } else { + None + }; + let response_tx_id = if let Some(target) = untagged_target { + self.commit_untagged_response_target(target) + } else if is_continuation { + self.continuation_response_tx_id() + } else { + self.transactions + .iter() + .rev() + .find(|tx| !tx.complete()) + .map(|tx| tx.tx_id) + }; + + if is_continuation { + if let Some(pending) = self.pending_literal.as_mut() { + if !pending.literal.is_literal_plus { + pending.continuation_received = true; + } + } else { + self.continuation_tx_id = response_tx_id; + } + } + + let tx_id = + match response_tx_id.filter(|&tx_id| self.tx_by_id(tx_id).is_some()) { + Some(tx_id) => tx_id, + None => { + if untagged_target.is_some() { + self.active_response_tx_id = None; + } + if is_continuation { + self.continuation_tx_id = None; + } + let Some(tx) = self.push_response_tx() else { + return AppLayerResult::err(); + }; + tx.id() + } + }; + + /* A data response (LIST, STATUS, ID, ...) may end in a + * literal specifier: its octets follow the CRLF. */ + let literal_size = match &response.message { + ImapMessageType::Untagged { keyword, .. } => untagged_trailing_literal( + keyword, + &start[..consumed.saturating_sub(2)], + ), + _ => None, + }; + + if let Some(size) = literal_size { + self.pending_response_literal = + Some(PendingResponseLiteral::new(Some(tx_id), response, size)); + } else if let Some(tx) = self.tx_by_id_mut(tx_id) { + tx.add_response(response, line_budget); + } + self.set_frame_tc(flow, tx_id, consumed as i64); + } + start = rem; + } + Err(Err::Incomplete(_)) => return incomplete(start), + Err(Err::Error(e)) if e.code == ErrorKind::Eof => { + break; + } + Err(Err::Failure(e)) if e.code == ErrorKind::TooLarge => { + return self.reject_oversized_tag(Direction::ToClient); + } + Err(_e) => { + self.set_event(ImapEvent::InvalidData); + return AppLayerResult::err(); + } + } + } + + return AppLayerResult::ok(); + } + + fn set_frame_ts(&mut self, flow: *const Flow, tx_id: u64, consumed: i64) { + if let Some(frame) = &self.request_frame { + frame.set_len(flow, consumed); + frame.set_tx(flow, tx_id - 1); + self.request_frame = None; + } + } + + fn set_frame_tc(&mut self, flow: *const Flow, tx_id: u64, consumed: i64) { + if let Some(frame) = &self.response_frame { + frame.set_len(flow, consumed); + frame.set_tx(flow, tx_id - 1); + self.response_frame = None; + } + } + + fn on_request_gap(&mut self, _size: u32) { + self.request_gap = true; + self.continuation_tx_id = None; + self.pending_literal = None; + self.request_frame = None; + } + + fn on_response_gap(&mut self, _size: u32) { + self.response_gap = true; + self.active_response_tx_id = None; + self.continuation_tx_id = None; + self.pending_literal = None; + if let Some(pending) = self.pending_response_literal.take() { + self.complete_pending_response(pending); + } + self.response_frame = None; + } + + fn complete_pending_response(&mut self, pending: PendingResponseLiteral) { + let line_budget = self.line_retain_budget(); + if let Some(tx) = pending.tx_id.and_then(|tx_id| self.tx_by_id_mut(tx_id)) { + tx.add_response(pending.response, line_budget); + } + } +} + +#[derive(Debug, PartialEq, Eq)] +enum ProbeResult { + Match { flip: bool }, + Incomplete, + Mismatch, +} + +fn probe_request(input: &[u8]) -> Result>> { + match parse_command(input) { + Ok((_, request)) => Ok(!matches!( + request.message, + ImapMessageType::Command { + command: ImapCommand::Unknown(_), + .. + } + )), + Err(Err::Incomplete(needed)) => { + if matches!(probe_command_prefix(input), Ok((_, true))) { + Ok(true) + } else { + Err(Err::Incomplete(needed)) + } + } + Err(e) => Err(e), + } +} + +fn probe_response(input: &[u8]) -> Result>> { + parse_response(input).map(|_| true) +} + +fn probe(input: &[u8], dir: Direction) -> ProbeResult { + let to_server = dir.is_to_server(); + let res = if to_server { + probe_request(input) + } else { + probe_response(input) + }; + match res { + Ok(true) => return ProbeResult::Match { flip: false }, + Err(Err::Incomplete(_)) => return ProbeResult::Incomplete, + Ok(false) | Err(_) => {} + } + let res = if to_server { + probe_response(input) + } else { + probe_request(input) + }; + match res { + Ok(true) => ProbeResult::Match { flip: true }, + Err(Err::Incomplete(_)) => ProbeResult::Incomplete, + Ok(false) | Err(_) => ProbeResult::Mismatch, + } +} + +unsafe extern "C" fn imap_probing_parser( + _flow: *const Flow, direction: u8, input: *const u8, input_len: u32, rdir: *mut u8, +) -> AppProto { + if input_len > 1 && !input.is_null() { + let slice = build_slice!(input, input_len as usize); + let dir: Direction = direction.into(); + return match probe(slice, dir) { + ProbeResult::Match { flip } => { + if flip { + *rdir = match dir { + Direction::ToServer => Direction::ToClient, + Direction::ToClient => Direction::ToServer, + } + .into(); + } + ALPROTO_IMAP + } + ProbeResult::Incomplete => ALPROTO_UNKNOWN, + ProbeResult::Mismatch => ALPROTO_FAILED, + }; + } + return ALPROTO_UNKNOWN; +} + +fn capability_tag_has_digit(input: &[u8]) -> bool { + input + .iter() + .take_while(|&&b| b != b' ') + .any(|b| b.is_ascii_digit()) +} + +unsafe extern "C" fn imap_capability_probing_parser( + flow: *const Flow, _direction: u8, input: *const u8, input_len: u32, _rdir: *mut u8, +) -> AppProto { + let alproto_tc = SCFlowGetAppProtocolToClient(flow); + if alproto_tc == ALPROTO_IMAP { + return ALPROTO_IMAP; + } + if !input.is_null() && capability_tag_has_digit(build_slice!(input, input_len as usize)) { + return ALPROTO_IMAP; + } + let mut size_tc: u32 = 0; + if !SCAppLayerProtoDetectGetStreamDataSize(flow, STREAM_TOCLIENT, &mut size_tc) { + return ALPROTO_FAILED; + } + if size_tc < 8 && alproto_tc == ALPROTO_UNKNOWN { + return ALPROTO_UNKNOWN; + } + return ALPROTO_FAILED; +} + +extern "C" fn imap_state_new(_orig_state: *mut c_void, _orig_proto: AppProto) -> *mut c_void { + let state = ImapState::new(); + let boxed = Box::new(state); + return Box::into_raw(boxed) as *mut c_void; +} + +unsafe extern "C" fn imap_state_free(state: *mut c_void) { + std::mem::drop(Box::from_raw(state as *mut ImapState)); +} + +unsafe extern "C" fn imap_state_tx_free(state: *mut c_void, tx_id: u64) { + let state = cast_pointer!(state, ImapState); + state.free_tx(tx_id); +} + +unsafe extern "C" fn imap_parse_request( + flow: *mut Flow, state: *mut c_void, pstate: *mut AppLayerParserState, + stream_slice: StreamSlice, _data: *mut c_void, +) -> AppLayerResult { + let eof = SCAppLayerParserStateIssetFlag(pstate, APP_LAYER_PARSER_EOF_TS) > 0; + if stream_slice.is_empty() && !eof { + return AppLayerResult::err(); + } + let state = cast_pointer!(state, ImapState); + + let result = if stream_slice.is_gap() { + state.on_request_gap(stream_slice.gap_size()); + AppLayerResult::ok() + } else { + state.parse_request(flow, stream_slice) + }; + if eof { + state.complete_transactions_at_eof(Direction::ToServer); + } + result +} + +unsafe extern "C" fn imap_parse_response( + flow: *mut Flow, state: *mut c_void, pstate: *mut AppLayerParserState, + stream_slice: StreamSlice, _data: *mut c_void, +) -> AppLayerResult { + let eof = SCAppLayerParserStateIssetFlag(pstate, APP_LAYER_PARSER_EOF_TC) > 0; + if stream_slice.is_empty() && !eof { + return AppLayerResult::err(); + } + let state = cast_pointer!(state, ImapState); + let result = if stream_slice.is_gap() { + state.on_response_gap(stream_slice.gap_size()); + AppLayerResult::ok() + } else { + state.parse_response(flow, stream_slice) + }; + if eof { + state.complete_transactions_at_eof(Direction::ToClient); + } + result +} + +unsafe extern "C" fn imap_state_get_tx(state: *mut c_void, tx_id: u64) -> *mut c_void { + let state = cast_pointer!(state, ImapState); + match state.get_tx(tx_id) { + Some(tx) => { + return tx as *const _ as *mut _; + } + None => { + return std::ptr::null_mut(); + } + } +} + +unsafe extern "C" fn imap_state_get_tx_count(state: *mut c_void) -> u64 { + let state = cast_pointer!(state, ImapState); + return state.tx_id; +} + +unsafe extern "C" fn imap_tx_get_alstate_progress(tx: *mut c_void, direction: u8) -> c_int { + let tx = cast_pointer!(tx, ImapTransaction); + if direction == STREAM_TOSERVER { + return tx.progress_ts as c_int; + } + return tx.progress_tc as c_int; +} + +export_tx_data_get!(imap_get_tx_data, ImapTransaction); +export_state_data_get!(imap_get_state_data, ImapState); + +const PARSER_NAME: &[u8] = b"imap\0"; + +fn register_pattern_probe() -> i8 { + const PATTERNS: &[&[u8]] = &[ + b"* OK \0", + b"* NO \0", + b"* BAD \0", + b"* LIST \0", + b"* ESEARCH \0", + b"* STATUS \0", + b"* FLAGS \0", + ]; + for pattern in PATTERNS { + let depth = (pattern.len() - 1) as u16; + let r = unsafe { + SCAppLayerProtoDetectPMRegisterPatternCI( + IPPROTO_TCP, + ALPROTO_IMAP, + pattern.as_ptr() as *const c_char, + depth, + 0, + Direction::ToClient as u8, + ) + }; + if r < 0 { + return -1; + } + } + // The client CAPABILITY command has no fixed server response to key on, so + // detect it on the request side, independent of port. Depth 17 allows up to + // a 6-char tag + space + "CAPABILITY", matching the historical detector. + // "USER CAPABILITY" may be FTP as well, so a probing parser disambiguates. + let capability = b" CAPABILITY\0"; + let r = unsafe { + SCAppLayerProtoDetectPMRegisterPatternCIwPP( + IPPROTO_TCP, + ALPROTO_IMAP, + capability.as_ptr() as *const c_char, + 17, + 0, + Direction::ToServer as u8, + Some(imap_capability_probing_parser), + 12, + 17, + ) + }; + if r < 0 { + return -1; + } + 0 +} + +#[no_mangle] +pub unsafe extern "C" fn SCRegisterImapParser() { + let default_port = CString::new("[143]").unwrap(); + let parser = RustParser { + name: PARSER_NAME.as_ptr() as *const c_char, + default_port: default_port.as_ptr(), + ipproto: IPPROTO_TCP, + probe_ts: Some(imap_probing_parser), + probe_tc: Some(imap_probing_parser), + min_depth: 0, + max_depth: 16, + state_new: imap_state_new, + state_free: imap_state_free, + tx_free: imap_state_tx_free, + parse_ts: imap_parse_request, + parse_tc: imap_parse_response, + get_tx_count: imap_state_get_tx_count, + get_tx: imap_state_get_tx, + tx_comp_st_ts: 1, + tx_comp_st_tc: 1, + tx_get_progress: imap_tx_get_alstate_progress, + get_eventinfo: Some(ImapEvent::get_event_info), + get_eventinfo_byid: Some(ImapEvent::get_event_info_by_id), + localstorage_new: None, + localstorage_free: None, + get_tx_files: None, + get_tx_iterator: Some(applayer::state_get_tx_iterator::), + get_tx_data: imap_get_tx_data, + get_state_data: imap_get_state_data, + apply_tx_config: None, + flags: APP_LAYER_PARSER_OPT_ACCEPT_GAPS, + get_frame_id_by_name: Some(ImapFrameType::ffi_id_from_name), + get_frame_name_by_id: Some(ImapFrameType::ffi_name_from_id), + get_state_id_by_name: Some(ImapStateProgress::ffi_id_from_name), + get_state_name_by_id: Some(ImapStateProgress::ffi_name_from_id), + }; + + let ip_proto_str = CString::new("tcp").unwrap(); + if SCAppLayerProtoDetectConfProtoDetectionEnabled(ip_proto_str.as_ptr(), parser.name) != 0 { + let alproto = applayer_register_protocol_detection(&parser, 1); + ALPROTO_IMAP = alproto; + if register_pattern_probe() < 0 { + return; + } + if SCAppLayerParserConfParserEnabled(ip_proto_str.as_ptr(), parser.name) != 0 { + let _ = AppLayerRegisterParser(&parser, alproto); + } + if let Some(val) = conf_get("app-layer.protocols.imap.max-tx") { + if let Ok(v) = val.parse::() { + if IMAP_MAX_TX == IMAP_MAX_TX_DEFAULT { + IMAP_MAX_TX = v; + } + } else { + SCLogError!("Invalid value for imap.max-tx"); + } + } + SCAppLayerParserRegisterLogger(IPPROTO_TCP, ALPROTO_IMAP); + } else { + SCLogDebug!("Protocol detection and parser disabled for IMAP/TCP."); + } +} diff --git a/src/app-layer-imap.h b/rust/src/imap/mod.rs similarity index 72% rename from src/app-layer-imap.h rename to rust/src/imap/mod.rs index 497992f71a..91f3c0d300 100644 --- a/src/app-layer-imap.h +++ b/rust/src/imap/mod.rs @@ -1,4 +1,4 @@ -/* Copyright (C) 2024 Open Information Security Foundation +/* Copyright (C) 2026 Open Information Security Foundation * * You can copy, redistribute or modify this Program under the terms of * the GNU General Public License version 2 as published by the Free @@ -15,14 +15,7 @@ * 02110-1301, USA. */ -/** - * \file - * - * \author Mahmoud Maatuq - * - */ +// Author: Giuseppe Longo -#ifndef SURICATA_APP_LAYER_IMAP_H -#define SURICATA_APP_LAYER_IMAP_H -void RegisterIMAPParsers(void); -#endif +pub mod imap; +pub mod parser; diff --git a/rust/src/imap/parser.rs b/rust/src/imap/parser.rs new file mode 100644 index 0000000000..6faec74d3a --- /dev/null +++ b/rust/src/imap/parser.rs @@ -0,0 +1,1471 @@ +/* Copyright (C) 2026 Open Information Security Foundation + * + * You can copy, redistribute or modify this Program under the terms of + * the GNU General Public License version 2 as published by the Free + * Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * version 2 along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +// Author: Giuseppe Longo + +use nom8::branch::alt; +use nom8::bytes::streaming::{tag, tag_no_case, take_till, take_while1}; +use nom8::character::complete::{char as complete_char, u32 as complete_u32}; +use nom8::character::streaming::{char, crlf, space0, space1, u64 as streaming_u64}; +use nom8::combinator::{all_consuming, complete, map, opt, value, verify}; +use nom8::error::{Error, ErrorKind}; +use nom8::sequence::preceded; +use nom8::{Err, IResult, Needed, Parser}; +use std::borrow::Cow; +use std::fmt; + +pub const IMAP_MAX_LINE_SIZE: usize = 8 * 1024; +const IMAP_MAX_ARGUMENTS: usize = IMAP_MAX_LINE_SIZE / 2; +const IMAP_MAX_LITERAL_SIZE: u64 = i64::MAX as u64; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ImapCommand { + // Any state commands + Capability, + Noop, + Logout, + + // Not authenticated state + StartTls, + Authenticate, + Login, + + // Authenticated state + Select, + Examine, + Create, + Delete, + Rename, + Subscribe, + Unsubscribe, + List, + Lsub, + Status, + Append, + + // Selected state + Check, + Close, + Expunge, + Search, + Fetch, + Store, + Copy, + Uid, + + // Extensions + Idle, + Id, + + // Unknown command + Unknown(Vec), +} + +const COMMANDS: &[(&str, ImapCommand)] = &[ + ("CAPABILITY", ImapCommand::Capability), + ("NOOP", ImapCommand::Noop), + ("LOGOUT", ImapCommand::Logout), + ("STARTTLS", ImapCommand::StartTls), + ("AUTHENTICATE", ImapCommand::Authenticate), + ("LOGIN", ImapCommand::Login), + ("SELECT", ImapCommand::Select), + ("EXAMINE", ImapCommand::Examine), + ("CREATE", ImapCommand::Create), + ("DELETE", ImapCommand::Delete), + ("RENAME", ImapCommand::Rename), + ("SUBSCRIBE", ImapCommand::Subscribe), + ("UNSUBSCRIBE", ImapCommand::Unsubscribe), + ("LIST", ImapCommand::List), + ("LSUB", ImapCommand::Lsub), + ("STATUS", ImapCommand::Status), + ("APPEND", ImapCommand::Append), + ("CHECK", ImapCommand::Check), + ("CLOSE", ImapCommand::Close), + ("EXPUNGE", ImapCommand::Expunge), + ("SEARCH", ImapCommand::Search), + ("FETCH", ImapCommand::Fetch), + ("STORE", ImapCommand::Store), + ("COPY", ImapCommand::Copy), + ("UID", ImapCommand::Uid), + ("IDLE", ImapCommand::Idle), + ("ID", ImapCommand::Id), +]; + +impl fmt::Display for ImapCommand { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let ImapCommand::Unknown(bytes) = self { + return write!(f, "{}", String::from_utf8_lossy(bytes)); + } + let name = COMMANDS + .iter() + .find(|(_, command)| command == self) + .map_or("", |(name, _)| name); + f.write_str(name) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ImapResponseStatus { + Ok, + No, + Bad, + PreAuth, + Bye, +} + +impl fmt::Display for ImapResponseStatus { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ImapResponseStatus::Ok => write!(f, "OK"), + ImapResponseStatus::No => write!(f, "NO"), + ImapResponseStatus::Bad => write!(f, "BAD"), + ImapResponseStatus::PreAuth => write!(f, "PREAUTH"), + ImapResponseStatus::Bye => write!(f, "BYE"), + } + } +} + +#[derive(Debug, Clone)] +pub struct LiteralInfo { + pub size: u64, + pub is_literal_plus: bool, + pub bytes_consumed: u64, +} + +impl LiteralInfo { + pub fn new(size: u64, is_literal_plus: bool) -> Self { + Self { + size, + is_literal_plus, + bytes_consumed: 0, + } + } + + pub fn remaining(&self) -> u64 { + self.size.saturating_sub(self.bytes_consumed) + } + + pub fn consume_chunk(&mut self, chunk: &[u8]) -> usize { + let consumed = match usize::try_from(self.remaining()) { + Ok(remaining) => std::cmp::min(chunk.len(), remaining), + Err(_) => chunk.len(), + }; + self.bytes_consumed = self + .bytes_consumed + .saturating_add(u64::try_from(consumed).unwrap_or(u64::MAX)); + consumed + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct CommandLiteral { + pub size: u64, + pub is_literal_plus: bool, + pub parenthesis_depth: usize, +} + +#[derive(Clone, Debug, PartialEq)] +pub enum ImapMessageType { + Command { + command: ImapCommand, + arguments: Vec>, + literal: Option, + }, + Response { + status: ImapResponseStatus, + }, + Untagged { + seq_number: Option, + keyword: Vec, + }, + Continuation, + ContinuationData, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct ImapMessage { + pub tag: Option>, + pub message: ImapMessageType, + pub raw_line: Vec, + pub line_truncated: bool, +} + +impl ImapMessage { + pub fn tag_str(&self) -> Cow<'_, str> { + self.tag + .as_deref() + .map(String::from_utf8_lossy) + .unwrap_or_default() + } +} + +#[inline] +fn is_line_ending(b: u8) -> bool { + b == b'\r' || b == b'\n' +} + +#[inline] +fn is_tag_char(c: u8) -> bool { + c.is_ascii_graphic() && !b"(){}%*\"\\+ ".contains(&c) +} + +#[inline] +fn is_atom_char(c: u8) -> bool { + c.is_ascii_graphic() && !b"(){}%*\"\\ ".contains(&c) +} + +fn cap_raw_line(line: &[u8]) -> (Vec, bool) { + if line.len() > IMAP_MAX_LINE_SIZE { + (line[..IMAP_MAX_LINE_SIZE].to_vec(), true) + } else { + (line.to_vec(), false) + } +} + +fn parse_tag(i: &[u8]) -> IResult<&[u8], &[u8]> { + if i.iter() + .take(IMAP_MAX_LINE_SIZE + 1) + .take_while(|&&b| is_tag_char(b)) + .count() + > IMAP_MAX_LINE_SIZE + { + return Err(Err::Failure(Error::new(i, ErrorKind::TooLarge))); + } + take_while1(is_tag_char).parse(i) +} + +fn parse_command_keyword(i: &[u8]) -> IResult<&[u8], ImapCommand> { + let (i, cmd) = parse_atom(i)?; + let command = COMMANDS + .iter() + .find(|(name, _)| cmd.eq_ignore_ascii_case(name.as_bytes())) + .map_or_else( + || ImapCommand::Unknown(cmd[..cmd.len().min(IMAP_MAX_LINE_SIZE)].to_vec()), + |(_, command)| command.clone(), + ); + Ok((i, command)) +} + +pub fn probe_command_prefix(i: &[u8]) -> IResult<&[u8], bool> { + let (i, _tag) = parse_tag(i)?; + let (i, _) = space1.parse(i)?; + let (i, command) = parse_command_keyword(i)?; + Ok((i, !matches!(command, ImapCommand::Unknown(_)))) +} + +fn parse_status(i: &[u8]) -> IResult<&[u8], ImapResponseStatus> { + alt(( + value(ImapResponseStatus::Ok, tag_no_case("OK")), + value(ImapResponseStatus::No, tag_no_case("NO")), + value(ImapResponseStatus::Bad, tag_no_case("BAD")), + value(ImapResponseStatus::PreAuth, tag_no_case("PREAUTH")), + value(ImapResponseStatus::Bye, tag_no_case("BYE")), + )) + .parse(i) +} + +fn parse_quoted_string(i: &[u8], retain_limit: usize) -> IResult<&[u8], Vec> { + let (mut rem, _) = char('"').parse(i)?; + let mut value = Vec::new(); + + loop { + let Some((&byte, after_byte)) = rem.split_first() else { + return Err(Err::Incomplete(Needed::new(1))); + }; + + match byte { + b'"' => { + value.shrink_to_fit(); + return Ok((after_byte, value)); + } + b'\\' => { + let Some((&escaped, after_escape)) = after_byte.split_first() else { + return Err(Err::Incomplete(Needed::new(1))); + }; + if escaped != b'"' && escaped != b'\\' { + return Err(Err::Error(Error::new(rem, ErrorKind::Escaped))); + } + if value.len() < retain_limit { + value.push(escaped); + } + rem = after_escape; + } + b'\0' | b'\r' | b'\n' => { + return Err(Err::Error(Error::new(rem, ErrorKind::Char))); + } + _ => { + if value.len() < retain_limit { + value.push(byte); + } + rem = after_byte; + } + } + } +} + +fn parse_atom(i: &[u8]) -> IResult<&[u8], &[u8]> { + take_while1(is_atom_char).parse(i) +} + +#[derive(Debug, Default)] +struct ParenthesisScanner { + depth: usize, + in_quoted: bool, + escaped: bool, + saw_open: bool, +} + +impl ParenthesisScanner { + fn scan(&mut self, i: &[u8]) -> Result, ErrorKind> { + for (pos, &b) in i.iter().enumerate() { + if self.in_quoted { + if self.escaped { + if b != b'"' && b != b'\\' { + return Err(ErrorKind::Escaped); + } + self.escaped = false; + continue; + } + + match b { + b'\\' => self.escaped = true, + b'"' => self.in_quoted = false, + b'\0' | b'\r' | b'\n' => return Err(ErrorKind::Char), + _ => {} + } + continue; + } + + match b { + b'"' => self.in_quoted = true, + b'(' => { + self.depth = self.depth.checked_add(1).ok_or(ErrorKind::TooLarge)?; + self.saw_open = true; + } + b')' => { + if self.depth == 0 { + return Err(ErrorKind::Char); + } + self.depth -= 1; + if self.depth == 0 { + return Ok(Some(pos)); + } + } + b'\r' | b'\n' => return Err(ErrorKind::Char), + _ => {} + } + } + Ok(None) + } + + fn quote_is_closed(&self) -> bool { + !self.in_quoted && !self.escaped + } +} + +fn parse_list( + i: &[u8], depth: usize, retain_limit: usize, +) -> IResult<&[u8], (Vec, Option)> { + if depth == 0 { + let _ = char('(').parse(i)?; + } else if let Some(&first) = i.first() { + if !matches!(first, b' ' | b'\t' | b')') { + return Err(Err::Error(Error::new(i, ErrorKind::Char))); + } + } + let mut scanner = ParenthesisScanner { + depth, + ..Default::default() + }; + let line_end = i.iter().position(|&b| is_line_ending(b)).unwrap_or(i.len()); + if let Some(pos) = scanner + .scan(&i[..line_end]) + .map_err(|kind| Err::Error(Error::new(i, kind)))? + { + return Ok(( + &i[pos + 1..], + (i[..(pos + 1).min(retain_limit)].to_vec(), None), + )); + } + if line_end == i.len() { + return Err(Err::Incomplete(Needed::new(1))); + } + let line = &i[..line_end]; + if scanner.quote_is_closed() { + if let Some((prefix, (size, is_literal_plus))) = detect_trailing_literal(line) { + if matches!(prefix.last(), Some(b' ' | b'\t' | b'(')) { + let literal = CommandLiteral { + size, + is_literal_plus, + parenthesis_depth: scanner.depth, + }; + return Ok(( + &i[line.len()..], + (line[..line.len().min(retain_limit)].to_vec(), Some(literal)), + )); + } + } + } + Err(Err::Error(Error::new(i, ErrorKind::Char))) +} + +fn parse_literal_as_argument( + i: &[u8], retain_limit: usize, +) -> IResult<&[u8], (Vec, Option)> { + let start = i; + let (i, (size, is_literal_plus)) = parse_literal_specifier(i)?; + let len = start.len() - i.len(); + let literal = CommandLiteral { + size, + is_literal_plus, + parenthesis_depth: 0, + }; + Ok((i, (start[..len.min(retain_limit)].to_vec(), Some(literal)))) +} + +#[inline] +fn is_list_char(c: u8) -> bool { + is_atom_char(c) || c == b'%' || c == b'*' || c == b'\\' +} + +fn parse_sequence_value(i: &[u8]) -> Option> { + all_consuming(alt(( + value(None, complete_char::<_, Error<_>>('*')), + map(verify(complete_u32::<_, Error<_>>, |num| *num != 0), Some), + ))) + .parse(i) + .ok() + .map(|(_, val)| val) +} + +pub fn sequence_set_contains(set: &[u8], sequence_number: u32) -> Option { + let mut has_unknown_item = false; + + for item in set.split(|b| *b == b',') { + let mut vals = item.split(|b| *b == b':'); + let first_val = parse_sequence_value(vals.next()?)?; + let second_val = match vals.next() { + Some(val) => Some(parse_sequence_value(val)?), + None => None, + }; + if vals.next().is_some() { + return None; + } + + let item_contains = match (first_val, second_val) { + (Some(num), None) => Some(sequence_number == num), + (None, None) => None, + (Some(first_val), Some(Some(second_val))) => { + let lower = first_val.min(second_val); + let upper = first_val.max(second_val); + Some((lower..=upper).contains(&sequence_number)) + } + (Some(val), Some(None)) | (None, Some(Some(val))) => { + if sequence_number >= val { + Some(true) + } else { + None + } + } + (None, Some(None)) => None, + }; + + match item_contains { + Some(true) => return Some(true), + Some(false) => {} + None => has_unknown_item = true, + } + } + + if has_unknown_item { + None + } else { + Some(false) + } +} + +fn parse_body_section_argument( + i: &[u8], retain_limit: usize, +) -> IResult<&[u8], (Vec, Option)> { + let (_, line) = take_till(is_line_ending).parse(i)?; + let (rem, _) = complete(parse_body_section).parse(line)?; + if rem.first().is_some_and(|b| !matches!(b, b' ' | b'\t')) { + return Err(Err::Error(Error::new(i, ErrorKind::Verify))); + } + let consumed = line.len() - rem.len(); + Ok(( + &i[consumed..], + (i[..consumed.min(retain_limit)].to_vec(), None), + )) +} + +fn parse_argument( + i: &[u8], retain_limit: usize, +) -> IResult<&[u8], (Vec, Option)> { + alt(( + map(|i| parse_quoted_string(i, retain_limit), |arg| (arg, None)), + |i| parse_list(i, 0, retain_limit), + |i| parse_literal_as_argument(i, retain_limit), + map(take_while1(is_list_char), |s: &[u8]| { + (s[..s.len().min(retain_limit)].to_vec(), None) + }), + )) + .parse(i) +} + +type CommandArguments = (Vec>, Option); + +fn parse_arguments<'a>( + mut i: &'a [u8], depth: usize, command: Option<&ImapCommand>, +) -> IResult<&'a [u8], CommandArguments> { + let mut arguments = Vec::new(); + let mut retain_left = if command.is_some() { + IMAP_MAX_LINE_SIZE + } else { + 0 + }; + let mut position = 0; + let mut literal = None; + if depth != 0 { + let (rem, (argument, pending)) = parse_list(i, depth, retain_left)?; + if retain_left != 0 { + retain_left -= argument.len(); + arguments.push(argument); + } + position += 1; + literal = pending; + i = rem; + } + while literal.is_none() { + let fetch_attribute = match command { + Some(ImapCommand::Fetch) => position == 1, + Some(ImapCommand::Uid) => { + position == 2 + && arguments + .first() + .is_some_and(|arg| arg.eq_ignore_ascii_case(b"FETCH")) + } + _ => false, + }; + let (rem, argument) = opt(preceded(space1, |input| { + if fetch_attribute { + alt(( + |i| parse_body_section_argument(i, retain_left), + |i| parse_argument(i, retain_left), + )) + .parse(input) + } else { + parse_argument(input, retain_left) + } + })) + .parse(i)?; + let Some((argument, pending)) = argument else { + break; + }; + if retain_left != 0 { + retain_left -= argument.len(); + arguments.push(argument); + if arguments.len() == IMAP_MAX_ARGUMENTS { + retain_left = 0; + } + } + position += 1; + literal = pending; + i = rem; + } + arguments.shrink_to_fit(); + Ok((i, (arguments, literal))) +} + +pub fn parse_command_continuation( + i: &[u8], depth: usize, +) -> IResult<&[u8], (ImapMessage, Option)> { + let start = i; + let (i, (_, literal)) = parse_arguments(i, depth, None)?; + let (i, _) = crlf.parse(i)?; + + let raw_len = start.len() - i.len() - 2; + let suffix = &start[..raw_len]; + let offset = suffix + .iter() + .position(|&b| b != b' ') + .unwrap_or(suffix.len()); + let (raw_line, line_truncated) = cap_raw_line(&suffix[offset..]); + Ok(( + i, + ( + ImapMessage { + tag: None, + message: ImapMessageType::ContinuationData, + raw_line, + line_truncated, + }, + literal, + ), + )) +} + +pub fn parse_response_continuation(i: &[u8]) -> IResult<&[u8], (&[u8], Option)> { + let (i, rest) = take_till(is_line_ending).parse(i)?; + let (i, _) = crlf.parse(i)?; + Ok(( + i, + ( + rest, + detect_trailing_literal(rest).map(|(_, (size, _))| size), + ), + )) +} + +fn detect_trailing_literal(line: &[u8]) -> Option<(&[u8], (u64, bool))> { + let brace_pos = line.iter().rposition(|&c| c == b'{')?; + match parse_literal_specifier(&line[brace_pos..]) { + Ok(([], literal)) => Some((&line[..brace_pos], literal)), + _ => None, + } +} + +pub fn untagged_trailing_literal(keyword: &[u8], line: &[u8]) -> Option { + if all_consuming(parse_status).parse(keyword).is_ok() { + return None; + } + detect_trailing_literal(line).map(|(_, (size, _))| size) +} + +fn parse_partial_offset(i: &[u8]) -> IResult<&[u8], u64> { + let (i, _) = complete_char('<').parse(i)?; + let (i, origin) = complete_u32::<_, Error<_>>(i)?; + let (i, _) = opt(preceded(complete_char('.'), complete_u32::<_, Error<_>>)).parse(i)?; + let (i, _) = complete_char('>').parse(i)?; + Ok((i, u64::from(origin))) +} + +fn parse_body_section(i: &[u8]) -> IResult<&[u8], ()> { + let (i, _) = alt((tag_no_case("BODY.PEEK"), tag_no_case("BODY"))).parse(i)?; + let (i, _) = char('[').parse(i)?; + let (i, _) = take_till(|c| c == b']').parse(i)?; + let (i, _) = char(']').parse(i)?; + let (i, _) = opt(parse_partial_offset).parse(i)?; + Ok((i, ())) +} + +pub fn parse_command(i: &[u8]) -> IResult<&[u8], ImapMessage> { + let start = i; + let (i, tag_bytes) = parse_tag(i)?; + let (i, _) = space1.parse(i)?; + let (i, command) = parse_command_keyword(i)?; + let (i, (arguments, literal)) = parse_arguments(i, 0, Some(&command))?; + let (i, _) = crlf.parse(i)?; + + let raw_len = start.len() - i.len() - 2; + let (raw_line, line_truncated) = cap_raw_line(&start[..raw_len]); + + Ok(( + i, + ImapMessage { + tag: Some(tag_bytes.to_vec()), + message: ImapMessageType::Command { + command, + arguments, + literal, + }, + raw_line, + line_truncated, + }, + )) +} + +fn parse_tagged_response(i: &[u8]) -> IResult<&[u8], ImapMessage> { + let start = i; + let (i, tag_bytes) = parse_tag(i)?; + let (i, _) = space1.parse(i)?; + let (i, status) = parse_status(i)?; + let (i, _) = opt(preceded(space1, take_till(is_line_ending))).parse(i)?; + let (i, _) = crlf.parse(i)?; + + let raw_len = start.len() - i.len() - 2; + let (raw_line, line_truncated) = cap_raw_line(&start[..raw_len]); + + Ok(( + i, + ImapMessage { + tag: Some(tag_bytes.to_vec()), + message: ImapMessageType::Response { status }, + raw_line, + line_truncated, + }, + )) +} + +fn parse_untagged_prefix(i: &[u8]) -> IResult<&[u8], (Option, &[u8])> { + let (i, _) = tag("* ").parse(i)?; + let (i, first_token) = take_while1(|c: u8| c.is_ascii_alphanumeric()).parse(i)?; + + if first_token.iter().all(|c| c.is_ascii_digit()) { + let (i, _) = space1.parse(i)?; + let (i, keyword) = take_while1(|c: u8| c.is_ascii_alphanumeric()).parse(i)?; + let seq: u32 = std::str::from_utf8(first_token) + .ok() + .and_then(|s| s.parse().ok()) + .unwrap_or(0); + Ok((i, (Some(seq), keyword))) + } else { + Ok((i, (None, first_token))) + } +} + +pub fn peek_untagged(i: &[u8]) -> Option<(Option, &[u8])> { + parse_untagged_prefix(i).ok().map(|(_, prefix)| prefix) +} + +fn parse_untagged_response(i: &[u8]) -> IResult<&[u8], ImapMessage> { + let start = i; + let (rem, (seq_number, keyword)) = parse_untagged_prefix(i)?; + + let (i, _) = space0.parse(rem)?; + let (i, _) = take_till(is_line_ending).parse(i)?; + let (i, _) = crlf.parse(i)?; + let raw_len = start.len() - i.len() - 2; + let (raw_line, line_truncated) = cap_raw_line(&start[..raw_len]); + + Ok(( + i, + ImapMessage { + tag: None, + message: ImapMessageType::Untagged { + seq_number, + keyword: keyword.to_vec(), + }, + raw_line, + line_truncated, + }, + )) +} + +fn parse_continuation(i: &[u8]) -> IResult<&[u8], ImapMessage> { + let start = i; + let (i, _) = tag("+").parse(i)?; + let (i, _) = space0.parse(i)?; + let (i, _) = take_till(is_line_ending).parse(i)?; + let (i, _) = crlf.parse(i)?; + + let raw_len = start.len() - i.len() - 2; + let (raw_line, line_truncated) = cap_raw_line(&start[..raw_len]); + + Ok(( + i, + ImapMessage { + tag: None, + message: ImapMessageType::Continuation, + raw_line, + line_truncated, + }, + )) +} + +pub fn parse_continuation_data(i: &[u8]) -> IResult<&[u8], ImapMessage> { + let (i, data) = take_till(is_line_ending).parse(i)?; + let (i, _) = crlf.parse(i)?; + + let (raw_line, line_truncated) = cap_raw_line(data); + + Ok(( + i, + ImapMessage { + tag: None, + message: ImapMessageType::ContinuationData, + raw_line, + line_truncated, + }, + )) +} + +pub fn parse_response(i: &[u8]) -> IResult<&[u8], ImapMessage> { + alt(( + parse_untagged_response, + parse_continuation, + parse_tagged_response, + )) + .parse(i) +} + +fn parse_number64(i: &[u8]) -> IResult<&[u8], u64> { + verify(streaming_u64, |value| *value <= IMAP_MAX_LITERAL_SIZE).parse(i) +} + +pub fn parse_literal_specifier(i: &[u8]) -> IResult<&[u8], (u64, bool)> { + let (i, _) = char('{').parse(i)?; + let (i, size) = parse_number64(i)?; + let (i, is_plus) = opt(char('+')).parse(i)?; + let (i, _) = char('}').parse(i)?; + + Ok((i, (size, is_plus.is_some()))) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn assert_incomplete_at_each_boundary( + input: &[u8], parser: for<'a> fn(&'a [u8]) -> IResult<&'a [u8], O>, + ) { + for split in 1..input.len() { + assert!( + matches!(parser(&input[..split]), Err(Err::Incomplete(_))), + "expected incomplete at byte boundary {split}" + ); + } + + let (rem, _) = parser(input).expect("complete input should parse"); + assert!(rem.is_empty()); + } + + #[test] + fn test_command_incomplete_at_each_boundary() { + let input = b"A001 UID FETCH 1:* (FLAGS BODY.PEEK[HEADER.FIELDS (FROM TO)])\r\n"; + assert_incomplete_at_each_boundary(input, parse_command); + } + + #[test] + fn test_responses_incomplete_at_each_boundary() { + for input in [ + b"A001 OK FETCH completed\r\n".as_slice(), + b"* 23 EXISTS\r\n".as_slice(), + ] { + assert_incomplete_at_each_boundary(input, parse_response); + } + } + + #[test] + fn test_unterminated_list_at_line_end_is_invalid() { + assert!(matches!( + parse_command(b"A001 FETCH 1 (FLAGS\r\n"), + Err(Err::Error(_)) + )); + } + + #[test] + fn test_parse_command_metadata_limits() { + let large = "x".repeat(1_000_000); + for command in [ + format!("A1 CREATE \"{large}\""), + format!("A1 CREATE {large}"), + format!("A1 SEARCH ({large})"), + format!("A1 FETCH 1 BODY[HEADER.FIELDS ({large})]"), + format!("A1 FETCH {large} BODY[TEXT]"), + format!("A1 {large}"), + format!("A1 SEARCH{}", " \"\"".repeat(100_000)), + ] { + let input = format!("{command}\r\nA2 NOOP\r\n"); + let (rem, msg) = parse_command(input.as_bytes()).unwrap(); + assert_eq!(rem, b"A2 NOOP\r\n"); + assert!(msg.line_truncated); + assert!(msg.raw_line.capacity() <= IMAP_MAX_LINE_SIZE); + let ImapMessageType::Command { + command, arguments, .. + } = msg.message + else { + panic!("Expected Command"); + }; + if let ImapCommand::Unknown(name) = command { + assert_eq!(name, vec![b'x'; IMAP_MAX_LINE_SIZE]); + assert_eq!(name.capacity(), IMAP_MAX_LINE_SIZE); + } else { + assert!(!arguments.is_empty()); + } + assert!(arguments.capacity() <= IMAP_MAX_ARGUMENTS); + assert!(arguments.iter().map(Vec::capacity).sum::() <= IMAP_MAX_LINE_SIZE); + assert!(parse_command(rem).unwrap().0.is_empty()); + } + } + + #[test] + fn test_command_limits_preserve_syntax_and_literals() { + for arguments in [ + format!(" {}", "x".repeat(IMAP_MAX_LINE_SIZE)), + " \"\"".repeat(IMAP_MAX_ARGUMENTS + 1), + ] { + let input = format!("A1 SEARCH{arguments} {{3+}}\r\nfoo SUBJECT secret\r\nA2 NOOP\r\n"); + let (rem, msg) = parse_command(input.as_bytes()).unwrap(); + let ImapMessageType::Command { + literal: Some(literal), + .. + } = msg.message + else { + panic!("Lost literal after retention limit"); + }; + assert_eq!(literal.size, 3); + assert!(literal.is_literal_plus); + let (rem, (suffix, next_literal)) = parse_command_continuation(&rem[3..], 0).unwrap(); + assert_eq!(suffix.raw_line, b"SUBJECT secret"); + assert!(next_literal.is_none()); + assert_eq!(rem, b"A2 NOOP\r\n"); + let invalid = format!("A1 SEARCH{arguments} \"bad\\escape\"\r\n"); + assert!(matches!( + parse_command(invalid.as_bytes()), + Err(Err::Error(_)) + )); + let incomplete = format!("A1 SEARCH{arguments} \"unfinished"); + assert!(matches!( + parse_command(incomplete.as_bytes()), + Err(Err::Incomplete(_)) + )); + let suffix = format!("{arguments} {{3+}}\r\n"); + let (_, (retained, literal)) = parse_arguments(suffix.as_bytes(), 0, None).unwrap(); + assert_eq!(retained.capacity(), 0); + assert!(literal.is_some()); + } + } + + #[test] + fn test_tag_size_limit() { + for len in [ + IMAP_MAX_LINE_SIZE - 1, + IMAP_MAX_LINE_SIZE, + IMAP_MAX_LINE_SIZE + 1, + 1_000_000, + ] { + let tag = vec![b'A'; len]; + for suffix in [b" NOOP\r\n".as_slice(), b" OK done\r\n"] { + let mut input = tag.clone(); + input.extend_from_slice(suffix); + let result = if suffix == b" NOOP\r\n" { + parse_command(&input) + } else { + parse_response(&input) + }; + if len <= IMAP_MAX_LINE_SIZE { + let (rem, msg) = result.unwrap(); + assert!(rem.is_empty()); + assert_eq!(msg.tag.as_deref(), Some(tag.as_slice())); + } else { + assert!(matches!( + result, + Err(Err::Failure(Error { + code: ErrorKind::TooLarge, + .. + })) + )); + } + } + let result = parse_tag(&tag); + if len <= IMAP_MAX_LINE_SIZE { + assert!(matches!(result, Err(Err::Incomplete(_)))); + } else { + assert!(matches!( + result, + Err(Err::Failure(Error { + code: ErrorKind::TooLarge, + .. + })) + )); + } + } + } + + #[test] + fn test_parse_fetch_arguments() { + for (input, expected) in [ + ( + b"A1 FETCH 1 BODY[HEADER.FIELDS (SUBJECT FROM)]\r\n".as_slice(), + b"BODY[HEADER.FIELDS (SUBJECT FROM)]".as_slice(), + ), + ( + b"A1 FETCH 1 BODY.PEEK[HEADER.FIELDS (TO)]\r\n", + b"BODY.PEEK[HEADER.FIELDS (TO)]", + ), + ( + b"A1 FETCH 1 BODY[HEADER.FIELDS (SUBJECT)]\r\n", + b"BODY[HEADER.FIELDS (SUBJECT)]", + ), + (b"A1 FETCH 1 BODY[TEXT]\r\n", b"BODY[TEXT]"), + (b"A1 FETCH 1 BODY[HEADER]\r\n", b"BODY[HEADER]"), + (b"A1 FETCH 1 BODYSTRUCTURE\r\n", b"BODYSTRUCTURE"), + ] { + let (rem, msg) = parse_command(input).unwrap(); + assert!(rem.is_empty(), "unconsumed input for {input:?}"); + match msg.message { + ImapMessageType::Command { arguments, .. } => { + assert_eq!(arguments, [b"1".to_vec(), expected.to_vec()]); + } + _ => panic!("Expected Command"), + } + } + } + + #[test] + fn test_parse_fetch_section_argument_streaming() { + for input in [ + b"A1 FETCH 1 BODY[]<0.128>\r\n".as_slice(), + b"A1 FETCH 1 BODY[HEADER.FIELDS (SUBJECT FROM)]<0.128>\r\n", + b"A1 UID FETCH 1 body.peek[header.fields.not (subject)]<123.456>\r\n", + b"A1 FETCH 1 BODY[TEXT]\r\n", + b"A1 FETCH 1 BODYSTRUCTURE\r\n", + ] { + assert_incomplete_at_each_boundary(input, parse_command); + let mut commands = input.to_vec(); + commands.extend_from_slice(b"A2 NOOP\r\n"); + let (rem, msg) = parse_command(&commands).unwrap(); + assert_eq!(msg.raw_line, &input[..input.len() - 2]); + assert_eq!(rem, b"A2 NOOP\r\n"); + } + } + + #[test] + fn test_parse_body_prefix_as_ordinary_argument() { + for (input, expected) in [ + ( + b"A1 SELECT BODY[Archive]suffix\r\n".as_slice(), + b"BODY[Archive]suffix".as_slice(), + ), + (b"A1 SELECT BODY[Archive\r\n", b"BODY[Archive"), + ( + b"A1 CREATE body.peek[TEXT]suffix\r\n", + b"body.peek[TEXT]suffix", + ), + (b"A1 CREATE BODY[]<0.\r\n", b"BODY[]<0."), + (b"A1 LOGIN user BODY[password\r\n", b"BODY[password"), + (b"A1 UID COPY 1 BODY[Archive\r\n", b"BODY[Archive"), + ( + b"A1 SEARCH BODY BODY[Archive]suffix\r\n", + b"BODY[Archive]suffix", + ), + ] { + assert_incomplete_at_each_boundary(input, parse_command); + let mut commands = input.to_vec(); + commands.extend_from_slice(b"A2 FETCH 1 BODY[TEXT]\r\n"); + let (rem, msg) = parse_command(&commands).unwrap(); + assert_eq!(rem, b"A2 FETCH 1 BODY[TEXT]\r\n"); + let ImapMessageType::Command { + arguments, literal, .. + } = msg.message + else { + panic!("Expected Command"); + }; + assert_eq!(arguments.last().unwrap(), expected); + assert_eq!(literal, None); + } + for input in [b" BODY[Archive\r\n".as_slice(), b" BODY[Archive]suffix\r\n"] { + assert_incomplete_at_each_boundary(input, |i| parse_command_continuation(i, 0)); + let (rem, (msg, literal)) = parse_command_continuation(input, 0).unwrap(); + assert!(rem.is_empty()); + assert_eq!(msg.raw_line, &input[1..input.len() - 2]); + assert_eq!(literal, None); + } + } + + #[test] + fn test_parse_fetch_section_argument_malformed() { + for input in [ + b"A1 FETCH 1 BODY[HEADER.FIELDS (SUBJECT\r\nA2 NOOP\r\n".as_slice(), + b"A1 FETCH 1 BODY[HEADER.FIELDS (SUBJECT)]\nA2 NOOP\r\n", + ] { + assert!( + matches!(parse_command(input), Err(Err::Error(_))), + "{input:?}" + ); + } + } + + #[test] + fn test_parse_login_quoted_args() { + let i = b"A001 LOGIN \"user name\" \"pass word\"\r\n"; + let (rem, msg) = parse_command(i).unwrap(); + assert!(rem.is_empty()); + match msg.message { + ImapMessageType::Command { + command, arguments, .. + } => { + assert_eq!(command, ImapCommand::Login); + assert_eq!(arguments.len(), 2); + assert_eq!(arguments[0], b"user name".to_vec()); + assert_eq!(arguments[1], b"pass word".to_vec()); + } + _ => panic!("Expected Command"), + } + } + + #[test] + fn test_parse_quoted_args_with_escapes() { + let i = b"A001 LOGIN \"user\\\"name\" \"pa\\\\ss\"\r\n"; + let (rem, msg) = parse_command(i).unwrap(); + assert!(rem.is_empty()); + assert_eq!(msg.raw_line, &i[..i.len() - 2]); + match msg.message { + ImapMessageType::Command { + command, arguments, .. + } => { + assert_eq!(command, ImapCommand::Login); + assert_eq!(arguments, [b"user\"name".to_vec(), b"pa\\ss".to_vec()]); + } + _ => panic!("Expected Command"), + } + } + + #[test] + fn test_parse_quoted_string_validation() { + assert_eq!( + parse_quoted_string(b"\"\"", IMAP_MAX_LINE_SIZE), + Ok((b"".as_slice(), Vec::new())) + ); + assert!(matches!( + parse_quoted_string(b"\"bad\\escape\"", IMAP_MAX_LINE_SIZE), + Err(Err::Error(_)) + )); + assert!(matches!( + parse_quoted_string(b"\"bad\0value\"", IMAP_MAX_LINE_SIZE), + Err(Err::Error(_)) + )); + assert!(matches!( + parse_quoted_string(b"\"bad\r\n", IMAP_MAX_LINE_SIZE), + Err(Err::Error(_)) + )); + assert!(matches!( + parse_quoted_string(b"\"unfinished\\", IMAP_MAX_LINE_SIZE), + Err(Err::Incomplete(_)) + )); + } + + #[test] + fn test_parse_list_ignores_quoted_parentheses() { + let i = b"A001 ID (\"name\" \"value ) ( \\\"quoted\\\" \\\\folder\")\r\n"; + assert_incomplete_at_each_boundary(i, parse_command); + let (rem, msg) = parse_command(i).unwrap(); + assert!(rem.is_empty()); + match msg.message { + ImapMessageType::Command { + command, arguments, .. + } => { + assert_eq!(command, ImapCommand::Id); + assert_eq!( + arguments, + [b"(\"name\" \"value ) ( \\\"quoted\\\" \\\\folder\")".to_vec()] + ); + } + _ => panic!("Expected Command"), + } + } + + #[test] + fn test_literal_info_consume_chunk_boundaries() { + let cases: &[(u64, &[u8], usize)] = &[ + (0, b"", 0), + (0, b"next", 0), + (4, b"", 0), + (4, b"ab", 2), + (4, b"abcd", 4), + (4, b"abcdnext", 4), + ]; + for &(size, input, expected) in cases { + for split in 0..=input.len() { + let mut literal = LiteralInfo::new(size, false); + let mut total = 0; + for (chunk, consumed) in [ + (&input[..split], split.min(expected)), + (&input[split..], expected.saturating_sub(split)), + ] { + assert_eq!(literal.consume_chunk(chunk), consumed); + total += consumed; + assert_eq!(literal.bytes_consumed, total as u64); + assert_eq!(literal.remaining(), size - total as u64); + } + } + } + } + + #[test] + fn test_command_quoted_literal_lookalikes() { + for input in [ + b"A1 CREATE INBOX\r\n".as_slice(), + b"A1 CREATE \"{3}\"\r\n", + b"A1 CREATE \"{3+}\"\r\n", + b"A1 SEARCH (SUBJECT \"{3}\")\r\n", + b"A1 SEARCH (SUBJECT \"{3+}\")\r\n", + ] { + assert_incomplete_at_each_boundary(input, parse_command); + let (rem, msg) = parse_command(input).unwrap(); + assert!(rem.is_empty()); + assert!(matches!( + msg.message, + ImapMessageType::Command { literal: None, .. } + )); + } + } + + #[test] + fn test_command_parenthesized_literals() { + for (input, size, is_literal_plus, depth) in [ + ( + b"A1 SEARCH (SUBJECT {3}\r\nfoo)\r\nA2 NOOP\r\n".as_slice(), + 3, + false, + 1, + ), + ( + b"A1 SEARCH ((SUBJECT {4+}\r\n)(\r\n)) UNSEEN\r\nA2 NOOP\r\n", + 4, + true, + 2, + ), + (b"A1 SEARCH (SUBJECT {0+}\r\n)\r\nA2 NOOP\r\n", 0, true, 1), + ] { + let (rem, msg) = parse_command(input).unwrap(); + let syntax_len = input.len() - rem.len(); + assert_incomplete_at_each_boundary(&input[..syntax_len], parse_command); + let ImapMessageType::Command { + command, literal, .. + } = msg.message + else { + panic!("Expected Command"); + }; + assert_eq!(command, ImapCommand::Search); + assert_eq!( + literal, + Some(CommandLiteral { + size, + is_literal_plus, + parenthesis_depth: depth, + }) + ); + + for split in 0..=size as usize { + let mut data = LiteralInfo::new(size, is_literal_plus); + assert_eq!(data.consume_chunk(&rem[..split]), split); + assert_eq!(data.consume_chunk(&rem[split..]), size as usize - split); + assert_eq!(data.remaining(), 0); + let (next, (_, literal)) = + parse_command_continuation(&rem[size as usize..], depth).unwrap(); + assert_eq!(literal, None); + assert_eq!(next, b"A2 NOOP\r\n"); + assert!(parse_command(next).unwrap().0.is_empty()); + } + } + } + + #[test] + fn test_command_nested_literal_continuations() { + for input in [b" BODY {0}\r\n".as_slice(), b") (TEXT {2+}\r\n"] { + assert_incomplete_at_each_boundary(input, |i| parse_command_continuation(i, 2)); + let (rem, (_, literal)) = parse_command_continuation(input, 2).unwrap(); + assert!(rem.is_empty()); + assert_eq!(literal.unwrap().parenthesis_depth, 2); + } + let input = b")) UNSEEN\r\n"; + assert_incomplete_at_each_boundary(input, |i| parse_command_continuation(i, 2)); + let (rem, (msg, literal)) = parse_command_continuation(input, 2).unwrap(); + assert!(rem.is_empty()); + assert_eq!(msg.raw_line, b")) UNSEEN"); + assert_eq!(literal, None); + } + + #[test] + fn test_command_continuation_quoted_literal_lookalikes() { + for (input, depth) in [ + (b" \"{5+}\"\r\n".as_slice(), 0), + (b" \"{5}\"\r\n", 0), + (b" SUBJECT \"{5+}\")\r\n", 1), + (b") (SUBJECT \"{5}\")\r\n", 1), + ] { + for end in 0..input.len() { + assert!(matches!( + parse_command_continuation(&input[..end], depth), + Err(Err::Incomplete(_)) + )); + } + let (rem, (_, literal)) = parse_command_continuation(input, depth).unwrap(); + assert!(rem.is_empty()); + assert_eq!(literal, None); + } + } + + #[test] + fn test_parse_command_continuation_caps_raw_line() { + for size in [ + IMAP_MAX_LINE_SIZE - 1, + IMAP_MAX_LINE_SIZE, + IMAP_MAX_LINE_SIZE + 1, + ] { + let mut input = b" ".to_vec(); + input.resize(input.len() + size, b'x'); + input.extend_from_slice(b"\r\nA2 NOOP\r\n"); + let (rem, (msg, literal)) = parse_command_continuation(&input, 0).unwrap(); + assert_eq!(rem, b"A2 NOOP\r\n"); + assert_eq!(msg.raw_line, vec![b'x'; size.min(IMAP_MAX_LINE_SIZE)]); + assert_eq!(msg.line_truncated, size > IMAP_MAX_LINE_SIZE); + assert_eq!(literal, None); + } + } + + #[test] + fn test_parse_command_continuation_literal_beyond_raw_line_limit() { + let mut input = b" ".to_vec(); + input.resize(input.len() + IMAP_MAX_LINE_SIZE + 1, b'x'); + input.extend_from_slice(b" {4+}\r\ndata\r\nA2 NOOP\r\n"); + let (rem, (msg, literal)) = parse_command_continuation(&input, 0).unwrap(); + assert_eq!(rem, b"data\r\nA2 NOOP\r\n"); + assert_eq!(msg.raw_line, vec![b'x'; IMAP_MAX_LINE_SIZE]); + assert!(msg.line_truncated); + assert_eq!( + literal, + Some(CommandLiteral { + size: 4, + is_literal_plus: true, + parenthesis_depth: 0, + }) + ); + } + + #[test] + fn test_parse_response_continuation_incomplete_at_each_boundary() { + for input in [ + b"\r\n".as_slice(), + b" (MESSAGES 1)\r\n".as_slice(), + b" {6}\r\n".as_slice(), + ] { + assert_incomplete_at_each_boundary(input, |i| { + parse_response_continuation(i).map(|(rem, (_, literal))| (rem, literal)) + }); + } + } + + #[test] + fn test_parse_response_continuation_malformed() { + for input in [b"\n".as_slice(), b" a\rb\r\n".as_slice()] { + assert!( + matches!(parse_response_continuation(input), Err(Err::Error(_))), + "expected error for {input:?}" + ); + } + } + + #[test] + fn test_parse_fetch_single_star() { + let i = b"A005 FETCH * FLAGS\r\n"; + let (rem, msg) = parse_command(i).unwrap(); + assert!(rem.is_empty()); + match msg.message { + ImapMessageType::Command { + command, arguments, .. + } => { + assert_eq!(command, ImapCommand::Fetch); + assert_eq!(arguments[0], b"*".to_vec()); + assert_eq!(arguments[1], b"FLAGS".to_vec()); + } + _ => panic!("Expected Command"), + } + } + + #[test] + fn test_parse_fetch_no_literal_still_works() { + let i = b"* 1 FETCH (UID 1 FLAGS (\\Seen))\r\n"; + let (rem, msg) = parse_response(i).unwrap(); + assert!(rem.is_empty()); + assert_eq!(msg.raw_line, b"* 1 FETCH (UID 1 FLAGS (\\Seen))".to_vec()); + match msg.message { + ImapMessageType::Untagged { + seq_number, + keyword, + } => { + assert_eq!(seq_number, Some(1)); + assert_eq!(keyword, b"FETCH".to_vec()); + } + _ => panic!("Expected Untagged"), + } + } + + #[test] + fn test_detect_trailing_literal() { + assert_eq!( + detect_trailing_literal(b"BODY[] {100}"), + Some((b"BODY[] ".as_slice(), (100, false))) + ); + assert_eq!( + detect_trailing_literal(b"{50+}"), + Some((b"".as_slice(), (50, true))) + ); + assert_eq!(detect_trailing_literal(b"no literal here"), None); + assert_eq!(detect_trailing_literal(b"middle {10} stuff"), None); + } + + #[test] + fn test_untagged_trailing_literal() { + assert_eq!( + untagged_trailing_literal(b"LIST", b"* LIST () \"/\" {5}"), + Some(5) + ); + assert_eq!( + untagged_trailing_literal(b"list", b"* list () \"/\" {5}"), + Some(5) + ); + assert_eq!(untagged_trailing_literal(b"ID", b"* ID ({4}"), Some(4)); + assert_eq!( + untagged_trailing_literal(b"STATUS", b"* STATUS INBOX (MESSAGES 1)"), + None + ); + assert_eq!( + untagged_trailing_literal(b"LIST", b"* LIST () \"/\" \"{5}\""), + None + ); + assert_eq!(untagged_trailing_literal(b"EXISTS", b"* 12 EXISTS"), None); + for status in [b"OK".as_slice(), b"ok", b"NO", b"BAD", b"BYE", b"PREAUTH"] { + assert_eq!(untagged_trailing_literal(status, b"* OK done {3}"), None); + } + } + + #[test] + fn test_parse_body_section() { + for input in [ + b"BODY[]<1024>".as_slice(), + b"BODY[HEADER]<50>", + b"BODY[]<0.512>", + b"BODY[]", + b"BODY.PEEK[]", + b"BODY[1.2]", + b"BODY[TEXT]", + b"body.peek[header.fields (subject)]", + ] { + let (rem, _) = parse_body_section(input).unwrap(); + assert!(rem.is_empty(), "unconsumed input for {input:?}"); + } + } + + #[test] + fn test_parse_untagged_response_stops_at_literal_specifier() { + for (input, keyword, raw_line, rem_expected) in [ + ( + b"* LIST () \"/\" {5}\r\nINBOX\r\n".as_slice(), + b"LIST".as_slice(), + b"* LIST () \"/\" {5}".as_slice(), + b"INBOX\r\n".as_slice(), + ), + ( + b"* 2 FETCH (BODY[] {10}\r\nHelloWorld)\r\n", + b"FETCH", + b"* 2 FETCH (BODY[] {10}", + b"HelloWorld)\r\n", + ), + ] { + let (rem, msg) = parse_response(input).unwrap(); + assert_eq!(rem, rem_expected); + assert_eq!(msg.raw_line, raw_line); + assert!(matches!( + msg.message, + ImapMessageType::Untagged { keyword: ref kw, .. } if kw == keyword + )); + } + } +} diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 0b39f5ed3c..f69fa2ddcf 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -140,6 +140,7 @@ pub mod util; pub mod websocket; pub mod x509; pub use suricata_ffi::direction; +pub mod imap; pub mod llmnr; #[allow(unused_imports)] diff --git a/src/Makefile.am b/src/Makefile.am index a606981fe1..491fcb4b03 100755 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -37,7 +37,6 @@ noinst_HEADERS = \ app-layer-htp.h \ app-layer-http2.h \ app-layer-ike.h \ - app-layer-imap.h \ app-layer-modbus.h \ app-layer-nfs-tcp.h \ app-layer-nfs-udp.h \ @@ -615,7 +614,6 @@ libsuricata_c_a_SOURCES = \ app-layer-htp.c \ app-layer-http2.c \ app-layer-ike.c \ - app-layer-imap.c \ app-layer-modbus.c \ app-layer-nfs-tcp.c \ app-layer-nfs-udp.c \ diff --git a/src/app-layer-imap.c b/src/app-layer-imap.c deleted file mode 100644 index d1264e72de..0000000000 --- a/src/app-layer-imap.c +++ /dev/null @@ -1,133 +0,0 @@ -/* Copyright (C) 2024 Open Information Security Foundation - * - * You can copy, redistribute or modify this Program under the terms of - * the GNU General Public License version 2 as published by the Free - * Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * version 2 along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA - * 02110-1301, USA. - */ - -/** - * \file - * - * \author Mahmoud Maatuq - * - */ - -#include "suricata-common.h" -#include "app-layer-detect-proto.h" -#include "app-layer-imap.h" -#include "app-layer-protos.h" -#include "rust.h" -#include "stream-tcp-private.h" -#include "stream-tcp-reassemble.h" -#include "util-debug.h" - -static AppProto ImapClientProbingParser( - const Flow *f, uint8_t direction, const uint8_t *input, uint32_t len, uint8_t *rdir) -{ - // This is called after " CAPABILITY" pattern has been found - if (f->alproto_tc == ALPROTO_IMAP) { - // server was already recognised as IMAP, so ok - return ALPROTO_IMAP; - } - for (size_t i = 0; i < len; i++) { - if (input[i] >= '0' && input[i] <= '9') { - // We check if the tag at the beginning of the input contains a number - return ALPROTO_IMAP; - } - if (input[i] == ' ') { - break; - } - } - - TcpSession *ssn = (TcpSession *)f->protoctx; - if (ssn == NULL) { - return ALPROTO_FAILED; - } - const uint32_t size_tc = StreamDataAvailableForProtoDetect(&ssn->server); - - if (size_tc < 8 && f->alproto_tc == ALPROTO_UNKNOWN) { - // wait for another packet from server - return ALPROTO_UNKNOWN; - } - return ALPROTO_FAILED; -} - -static int IMAPRegisterPatternsForProtocolDetection(void) -{ - if (SCAppLayerProtoDetectPMRegisterPatternCI( - IPPROTO_TCP, ALPROTO_IMAP, "* OK ", 5, 0, STREAM_TOCLIENT) < 0) { - return -1; - } - - if (SCAppLayerProtoDetectPMRegisterPatternCI( - IPPROTO_TCP, ALPROTO_IMAP, "* NO ", 5, 0, STREAM_TOCLIENT) < 0) { - return -1; - } - - if (SCAppLayerProtoDetectPMRegisterPatternCI( - IPPROTO_TCP, ALPROTO_IMAP, "* BAD ", 6, 0, STREAM_TOCLIENT) < 0) { - return -1; - } - - if (SCAppLayerProtoDetectPMRegisterPatternCI( - IPPROTO_TCP, ALPROTO_IMAP, "* LIST ", 7, 0, STREAM_TOCLIENT) < 0) { - return -1; - } - - if (SCAppLayerProtoDetectPMRegisterPatternCI( - IPPROTO_TCP, ALPROTO_IMAP, "* ESEARCH ", 10, 0, STREAM_TOCLIENT) < 0) { - return -1; - } - - if (SCAppLayerProtoDetectPMRegisterPatternCI( - IPPROTO_TCP, ALPROTO_IMAP, "* STATUS ", 9, 0, STREAM_TOCLIENT) < 0) { - return -1; - } - - if (SCAppLayerProtoDetectPMRegisterPatternCI( - IPPROTO_TCP, ALPROTO_IMAP, "* FLAGS ", 8, 0, STREAM_TOCLIENT) < 0) { - return -1; - } - - /** - * there is no official document that limits the length of the tag - * some practical implementations limit it to 20 characters - * but keeping depth equal to 31 fails unit tests such AppLayerTest10 - * so keeping depth 17 for now to pass unit tests, that might miss some detections - * until we find a better solution for the unit tests. - * - * AppLayerTest10 fails because it expects protocol detection to be completed with only 17 bytes - * as input, and with this new pattern, we would need more bytes to finish protocol detection. - */ - if (SCAppLayerProtoDetectPMRegisterPatternCIwPP(IPPROTO_TCP, ALPROTO_IMAP, " CAPABILITY", - 17 /*6 for max tag len + space + len(CAPABILITY)*/, 0, STREAM_TOSERVER, - ImapClientProbingParser, 12, 17) < 0) { - return -1; - } - - return 0; -} - -void RegisterIMAPParsers(void) -{ - const char *proto_name = "imap"; - - if (SCAppLayerProtoDetectConfProtoDetectionEnabled("tcp", proto_name)) { - SCLogDebug("IMAP protocol detection is enabled."); - AppLayerProtoDetectRegisterProtocol(ALPROTO_IMAP, proto_name); - if (IMAPRegisterPatternsForProtocolDetection() < 0) - SCLogError("Failed to register IMAP protocol detection patterns."); - } else { - SCLogDebug("Protocol detector and parser disabled for IMAP."); - } -} diff --git a/src/app-layer-parser.c b/src/app-layer-parser.c index dab0b17f53..95289c0898 100644 --- a/src/app-layer-parser.c +++ b/src/app-layer-parser.c @@ -55,7 +55,6 @@ #include "app-layer-tftp.h" #include "app-layer-ike.h" #include "app-layer-http2.h" -#include "app-layer-imap.h" #include "conf.h" #include "decode.h" #include "detect.h" @@ -2056,9 +2055,9 @@ void AppLayerParserRegisterProtocolParsers(void) SCRegisterRdpParser(); RegisterHTTP2Parsers(); SCRegisterTelnetParser(); - RegisterIMAPParsers(); SCRegisterLLMNRUdpParser(); SCRegisterLLMNRTcpParser(); + SCRegisterImapParser(); for (size_t i = 0; i < preregistered_callbacks_nb; i++) { PreRegisteredCallbacks[i](); diff --git a/suricata.yaml.in b/suricata.yaml.in index 49c1df696c..ed76e86c7e 100644 --- a/suricata.yaml.in +++ b/suricata.yaml.in @@ -1082,7 +1082,8 @@ app-layer: content-inspect-min-size: 32768 content-inspect-window: 4096 imap: - enabled: detection-only + enabled: yes + # max-tx: 256 pop3: enabled: yes detection-ports: