mod commands;
mod string_helpers;
#[cfg(test)]
#[path = "tests.rs"]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests;
use base64::Engine as _;
use bytes::BytesMut;
use std::collections::HashSet;
use crate::types::response::Capability;
use crate::types::{Command, MailboxAttribute, QresyncParams};
#[cfg(test)]
pub(crate) use commands::encode_multi_append_header;
pub(crate) use commands::encode_multi_append_header_with_literal8;
pub(crate) use string_helpers::{encode_quoted_or_literal, encode_quoted_or_literal_utf8};
pub(crate) use commands::encode_mailbox_str;
use commands::{
encode_authenticate, encode_create_special_use, encode_fetch, encode_getmetadata, encode_id,
encode_list_extended, encode_list_status, encode_login, encode_mailbox_cmd,
encode_mailbox_name, encode_notify_set, encode_search, encode_select_or_examine,
encode_set_acl, encode_set_quota, encode_setmetadata, encode_simple, encode_status,
encode_store, encode_thread_or_sort_cmd, encode_two_arg, encode_two_quoted_args,
encode_uid_expunge, encode_uid_fetch,
};
use string_helpers::encode_metadata_value;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum LiteralMode {
Synchronizing,
LiteralPlus,
LiteralMinus,
}
#[derive(Debug, Clone)]
pub(crate) struct EncodeOptions {
pub(crate) utf8_mode: bool,
pub(crate) literal_mode: LiteralMode,
pub(crate) capabilities: Vec<Capability>,
}
impl EncodeOptions {
fn has_capability(&self, cap: &Capability) -> bool {
self.capabilities.contains(cap)
}
fn has_condstore(&self) -> bool {
self.has_capability(&Capability::Condstore) || self.has_capability(&Capability::QResync)
}
}
#[derive(Debug, Clone, thiserror::Error)]
pub(crate) enum EncodeError {
#[error("command {cmd} requires capability {cap} which is not available")]
MissingCapability {
cmd: &'static str,
cap: String,
},
#[error("encode validation error: {0}")]
Validation(String),
}
impl From<crate::Error> for EncodeError {
fn from(e: crate::Error) -> Self {
Self::Validation(e.to_string())
}
}
const LITERAL_MINUS_MAX: usize = 4096;
#[derive(Debug, Clone)]
pub(crate) struct EncodedCommand {
segments: Vec<BytesMut>,
}
impl EncodedCommand {
pub(crate) fn segments(&self) -> &[BytesMut] {
&self.segments
}
pub(crate) fn into_buf(mut self) -> BytesMut {
if self.segments.len() == 1 {
return self.segments.swap_remove(0);
}
let total: usize = self.segments.iter().map(BytesMut::len).sum();
let mut buf = BytesMut::with_capacity(total);
for seg in &self.segments {
buf.extend_from_slice(seg);
}
buf
}
fn from_flat_buffer(buf: &[u8]) -> Self {
let mut segments = Vec::new();
let mut seg_start = 0;
let mut scan_pos = 0;
while scan_pos < buf.len() {
if let Some((marker_end_rel, literal_size)) =
find_sync_literal_boundary(&buf[scan_pos..])
{
let abs_marker_end = scan_pos + marker_end_rel;
segments.push(BytesMut::from(&buf[seg_start..abs_marker_end]));
seg_start = abs_marker_end;
scan_pos = abs_marker_end + literal_size;
} else {
break;
}
}
if seg_start < buf.len() {
segments.push(BytesMut::from(&buf[seg_start..]));
}
Self { segments }
}
}
fn find_sync_literal_boundary(buf: &[u8]) -> Option<(usize, usize)> {
let mut i = 0;
while i < buf.len() {
if buf[i] == b'{' {
let start = i + 1;
let mut j = start;
while j < buf.len() && buf[j].is_ascii_digit() {
j += 1;
}
if j > start
&& j + 2 < buf.len()
&& buf[j] == b'}'
&& buf[j + 1] == b'\r'
&& buf[j + 2] == b'\n'
{
let Ok(size_str) = std::str::from_utf8(&buf[start..j]) else {
i += 1;
continue;
};
let Ok(size) = size_str.parse::<usize>() else {
i += 1;
continue;
};
return Some((j + 3, size));
}
}
i += 1;
}
None
}
fn validate_no_crlf(s: &str, context: &str) -> Result<(), crate::Error> {
if s.bytes().any(|b| b == b'\r' || b == b'\n') {
return Err(crate::Error::Protocol(format!(
"{context} must not contain CR or LF — IMAP commands are \
CRLF-delimited (RFC 3501 Section 2.2)"
)));
}
Ok(())
}
fn validate_search_criteria_crlf(
criteria: &str,
context: &str,
literal_mode: LiteralMode,
) -> Result<(), crate::Error> {
let bytes = criteria.as_bytes();
let mut i = 0usize;
while i < bytes.len() {
if bytes[i] == b'{' {
let mut j = i + 1;
let digits_start = j;
while j < bytes.len() && bytes[j].is_ascii_digit() {
j += 1;
}
let has_plus = j > digits_start && j < bytes.len() && bytes[j] == b'+';
if has_plus {
j += 1;
}
if j > digits_start
&& j + 2 < bytes.len()
&& bytes[j] == b'}'
&& bytes[j + 1] == b'\r'
&& bytes[j + 2] == b'\n'
{
let size = std::str::from_utf8(
&bytes[digits_start..j - usize::from(bytes[j - 1] == b'+')],
)
.ok()
.and_then(|s| s.parse::<usize>().ok())
.ok_or_else(|| {
crate::Error::Protocol(format!(
"{context} contains an invalid literal octet count \
(RFC 3501 Section 4.3)"
))
})?;
if has_plus {
match literal_mode {
LiteralMode::Synchronizing => {
return Err(crate::Error::Protocol(format!(
"{context} uses a non-synchronizing literal \
without negotiated LITERAL+/LITERAL- support \
(RFC 7888 Section 3)"
)));
}
LiteralMode::LiteralMinus if size > LITERAL_MINUS_MAX => {
return Err(crate::Error::Protocol(format!(
"{context} uses a non-synchronizing literal of {size} octets, \
which exceeds the 4096-octet limit for LITERAL- / IMAP4rev2 \
mode (RFC 7888 Section 5 / RFC 9051 Section 4.3)"
)));
}
LiteralMode::LiteralMinus | LiteralMode::LiteralPlus => {}
}
}
let data_start = j + 3;
let data_end = data_start.checked_add(size).ok_or_else(|| {
crate::Error::Protocol(format!(
"{context} literal length overflows usize \
(RFC 3501 Section 4.3)"
))
})?;
if data_end > bytes.len() {
return Err(crate::Error::Protocol(format!(
"{context} literal declares {size} octets but the provided \
criteria fragment ends early (RFC 3501 Section 4.3)"
)));
}
i = data_end;
continue;
}
}
if matches!(bytes[i], b'\r' | b'\n') {
return Err(crate::Error::Protocol(format!(
"{context} must not contain raw CR or LF except inside an IMAP literal \
(RFC 3501 Sections 2.2 and 4.3)"
)));
}
i += 1;
}
Ok(())
}
fn search_criteria_starts_with_charset(criteria: &str) -> bool {
criteria
.split_whitespace()
.next()
.is_some_and(|token| token.eq_ignore_ascii_case("CHARSET"))
}
fn validate_login_credential_ascii(value: &str, field: &str) -> Result<(), crate::Error> {
if !value.is_ascii() {
return Err(crate::Error::Protocol(format!(
"LOGIN {field} must be ASCII-only; RFC 6855 Section 5 requires \
AUTHENTICATE for non-ASCII credentials"
)));
}
Ok(())
}
fn validate_sasl_initial_response(ir: &str) -> Result<(), crate::Error> {
validate_no_crlf(ir, "AUTHENTICATE initial response")?;
if ir.is_empty() || ir == "=" {
return Ok(());
}
base64::engine::general_purpose::STANDARD
.decode(ir.as_bytes())
.map(|_| ())
.map_err(|_| {
crate::Error::Protocol(
"AUTHENTICATE initial response must be RFC 4648 base64 or the special \
\"=\" empty marker (RFC 4959 Section 3)"
.into(),
)
})
}
const MOD_SEQ_MAX: u64 = i64::MAX as u64;
fn validate_mod_sequence_value(val: u64, context: &str) -> Result<(), crate::Error> {
if val == 0 {
return Err(crate::Error::Protocol(format!(
"{context} mod-sequence-value must be >= 1 per RFC 7162 Section 7, got 0"
)));
}
if val > MOD_SEQ_MAX {
return Err(crate::Error::Protocol(format!(
"{context} mod-sequence-value must be <= {MOD_SEQ_MAX} per RFC 7162 Section 7, got {val}"
)));
}
Ok(())
}
fn validate_mod_sequence_valzer(val: u64, context: &str) -> Result<(), crate::Error> {
if val > MOD_SEQ_MAX {
return Err(crate::Error::Protocol(format!(
"{context} mod-sequence-valzer must be <= {MOD_SEQ_MAX} per RFC 7162 Section 7, got {val}"
)));
}
Ok(())
}
fn encode_changedsince_modifier(
buf: &mut BytesMut,
modseq: u64,
vanished: bool,
) -> Result<(), crate::Error> {
validate_mod_sequence_value(modseq, "CHANGEDSINCE")?;
buf.extend_from_slice(b" (CHANGEDSINCE ");
buf.extend_from_slice(modseq.to_string().as_bytes());
if vanished {
buf.extend_from_slice(b" VANISHED");
}
buf.extend_from_slice(b")");
Ok(())
}
fn encode_enable(
buf: &mut BytesMut,
tag: &str,
capabilities: &[String],
) -> Result<(), EncodeError> {
if capabilities.is_empty() {
return Err(EncodeError::Validation(
"ENABLE requires at least one capability (RFC 5161 Section 3.1)".into(),
));
}
for cap in capabilities {
validate_atom(cap, "ENABLE capability")?;
}
buf.extend_from_slice(tag.as_bytes());
buf.extend_from_slice(b" ENABLE");
for cap in capabilities {
buf.extend_from_slice(b" ");
buf.extend_from_slice(cap.as_bytes());
}
buf.extend_from_slice(b"\r\n");
Ok(())
}
fn validate_atom(s: &str, context: &str) -> Result<(), crate::Error> {
crate::types::validated::validate_atom_bytes(s.as_bytes(), context)?;
Ok(())
}
fn validate_sort_thread_charset(charset: &str) -> Result<(), crate::Error> {
if charset.starts_with('"') || charset.ends_with('"') {
return validate_imap_quoted_string(charset, "THREAD/SORT charset");
}
validate_atom(charset, "THREAD/SORT charset")
}
fn validate_imap_quoted_string(s: &str, context: &str) -> Result<(), crate::Error> {
let bytes = s.as_bytes();
if bytes.len() < 2 || bytes[0] != b'"' || bytes[bytes.len() - 1] != b'"' {
return Err(crate::Error::Protocol(format!(
"{context} must be either an atom or a quoted-string \
(RFC 5256 Section 5 / RFC 3501 Section 9): {s:?}"
)));
}
let inner = &bytes[1..bytes.len() - 1];
if inner.is_empty() {
return Err(crate::Error::Protocol(format!(
"{context} must not be empty (RFC 5256 Section 5: charset values \
name an IANA-registered charset)"
)));
}
let mut idx = 0usize;
while idx < inner.len() {
match inner[idx] {
b'\\' => {
let Some(&escaped) = inner.get(idx + 1) else {
return Err(crate::Error::Protocol(format!(
"{context} quoted-string must not end with a bare backslash \
(RFC 3501 Section 9)"
)));
};
if escaped != b'"' && escaped != b'\\' {
return Err(crate::Error::Protocol(format!(
"{context} quoted-string may only escape DQUOTE or backslash \
(RFC 3501 Section 9): {s:?}"
)));
}
idx += 2;
}
b'"' => {
return Err(crate::Error::Protocol(format!(
"{context} quoted-string contains an unescaped DQUOTE \
(RFC 3501 Section 9): {s:?}"
)));
}
b'\r' | b'\n' | 0x00 => {
return Err(crate::Error::Protocol(format!(
"{context} quoted-string contains CR, LF, or NUL \
(RFC 3501 Section 9): {s:?}"
)));
}
byte if !byte.is_ascii() => {
return Err(crate::Error::Protocol(format!(
"{context} quoted-string must be ASCII-only \
(RFC 3501 Section 9 CHAR): {s:?}"
)));
}
_ => {
idx += 1;
}
}
}
Ok(())
}
pub(crate) fn validate_flag_keyword(s: &str) -> Result<(), crate::Error> {
validate_atom(s, "flag keyword")
}
fn validate_metadata_entry_name(entry: &str, context: &str) -> Result<(), crate::Error> {
if entry.contains("//") {
return Err(crate::Error::Protocol(format!(
"{context} must not contain consecutive '/' characters \
(RFC 5464 Section 3.2): {entry:?}"
)));
}
if entry.ends_with('/') {
return Err(crate::Error::Protocol(format!(
"{context} must not end with '/' (RFC 5464 Section 3.2): {entry:?}"
)));
}
if !entry.is_ascii() {
return Err(crate::Error::Protocol(format!(
"{context} must not contain non-ASCII characters \
(RFC 5464 Section 3.2): {entry:?}"
)));
}
if entry
.bytes()
.any(|byte| matches!(byte, 0x00..=0x19) || byte == b'*' || byte == b'%')
{
return Err(crate::Error::Protocol(format!(
"{context} must not contain '*', '%', or control octets \
0x00..=0x19 (RFC 5464 Section 3.2): {entry:?}"
)));
}
let Some(stripped) = entry.strip_prefix('/') else {
return Err(crate::Error::Protocol(format!(
"{context} must start with '/' and use the /private or /shared \
scope prefixes (RFC 5464 Section 3.2): {entry:?}"
)));
};
let scope = stripped.split('/').next().unwrap_or_default();
if !matches!(scope.to_ascii_lowercase().as_str(), "private" | "shared") {
return Err(crate::Error::Protocol(format!(
"{context} must use the /private or /shared scope prefixes \
(RFC 5464 Section 3.2): {entry:?}"
)));
}
Ok(())
}
fn validate_and_filter_flags<'a>(
flags: &'a [crate::types::Flag],
context: &str,
) -> Result<Vec<&'a crate::types::Flag>, crate::Error> {
let valid_flags: Vec<_> = flags
.iter()
.filter(|f| {
if matches!(f, crate::types::Flag::Recent | crate::types::Flag::Wildcard) {
tracing::debug!(
flag = %f,
"Skipping flag not permitted in {context} per RFC 3501 Section 9 (flag production)"
);
false
} else {
true
}
})
.collect();
for flag in &valid_flags {
if let crate::types::Flag::Custom(s) = flag {
validate_flag_keyword(s)?;
}
}
Ok(valid_flags)
}
#[allow(clippy::too_many_lines)]
pub(crate) fn validate_append_datetime(date: &str) -> Result<(), crate::Error> {
const MONTHS: [&[u8]; 12] = [
b"Jan", b"Feb", b"Mar", b"Apr", b"May", b"Jun", b"Jul", b"Aug", b"Sep", b"Oct", b"Nov",
b"Dec",
];
let b = date.as_bytes();
if b.len() != 26 {
return Err(crate::Error::InvalidAppendDate(format!(
"expected 26 characters, got {} — \
date-time must be date-day-fixed \"-\" date-month \"-\" date-year \
SP time SP zone (RFC 3501 Section 9)",
b.len()
)));
}
let day_hi = b[0];
let day_lo = b[1];
let valid_day = match day_hi {
b' ' => day_lo.is_ascii_digit() && day_lo != b'0',
b'0' => (b'1'..=b'9').contains(&day_lo),
b'1'..=b'2' => day_lo.is_ascii_digit(),
b'3' => day_lo == b'0' || day_lo == b'1',
_ => false,
};
if !valid_day {
return Err(crate::Error::InvalidAppendDate(format!(
"invalid day {:?} — date-day-fixed must be (SP DIGIT) / 2DIGIT \
with values 1-31 (RFC 3501 Section 9)",
std::str::from_utf8(&b[0..2]).unwrap_or("??")
)));
}
if b[2] != b'-' {
return Err(crate::Error::InvalidAppendDate(format!(
"expected '-' at position 2, got {:?} (RFC 3501 Section 9)",
b[2] as char
)));
}
let month = &b[3..6];
if !MONTHS.iter().any(|m| m.eq_ignore_ascii_case(month)) {
return Err(crate::Error::InvalidAppendDate(format!(
"invalid month {:?} — date-month must be Jan/Feb/.../Dec (RFC 3501 Section 9)",
std::str::from_utf8(month).unwrap_or("???")
)));
}
let day: u8 = match day_hi {
b' ' => day_lo - b'0',
_ => (day_hi - b'0') * 10 + (day_lo - b'0'),
};
if b[6] != b'-' {
return Err(crate::Error::InvalidAppendDate(format!(
"expected '-' at position 6, got {:?} (RFC 3501 Section 9)",
b[6] as char
)));
}
if !b[7..11].iter().all(u8::is_ascii_digit) {
return Err(crate::Error::InvalidAppendDate(format!(
"invalid year {:?} — date-year must be 4DIGIT (RFC 3501 Section 9)",
std::str::from_utf8(&b[7..11]).unwrap_or("????")
)));
}
let year: u16 = u16::from(b[7] - b'0') * 1000
+ u16::from(b[8] - b'0') * 100
+ u16::from(b[9] - b'0') * 10
+ u16::from(b[10] - b'0');
let max_day: u8 = match month[0].to_ascii_lowercase() {
b'f' => {
let y = u32::from(year);
if (y % 4 == 0 && y % 100 != 0) || y % 400 == 0 {
29
} else {
28
}
}
b'a' if month[1].eq_ignore_ascii_case(&b'p') => 30,
b'j' if month[1].eq_ignore_ascii_case(&b'u') && month[2].eq_ignore_ascii_case(&b'n') => 30,
b's' | b'n' => 30,
_ => 31,
};
if day > max_day {
return Err(crate::Error::InvalidAppendDate(format!(
"invalid day {} for month {:?} year {} — maximum is {} (RFC 3501 Section 9)",
day,
std::str::from_utf8(month).unwrap_or("???"),
year,
max_day
)));
}
if b[11] != b' ' {
return Err(crate::Error::InvalidAppendDate(format!(
"expected SP at position 11, got {:?} (RFC 3501 Section 9)",
b[11] as char
)));
}
let time = &b[12..20];
let valid_time = time[0].is_ascii_digit()
&& time[1].is_ascii_digit()
&& time[2] == b':'
&& time[3].is_ascii_digit()
&& time[4].is_ascii_digit()
&& time[5] == b':'
&& time[6].is_ascii_digit()
&& time[7].is_ascii_digit();
if !valid_time {
return Err(crate::Error::InvalidAppendDate(format!(
"invalid time {:?} — time must be 2DIGIT \":\" 2DIGIT \":\" 2DIGIT \
(RFC 3501 Section 9)",
std::str::from_utf8(time).unwrap_or("????????")
)));
}
let hour = (time[0] - b'0') * 10 + (time[1] - b'0');
let minute = (time[3] - b'0') * 10 + (time[4] - b'0');
let second = (time[6] - b'0') * 10 + (time[7] - b'0');
if hour > 23 {
return Err(crate::Error::InvalidAppendDate(format!(
"invalid hour {hour:02} — must be 00-23 (RFC 3501 Section 9, \
RFC 5322 Section 3.3)"
)));
}
if minute > 59 {
return Err(crate::Error::InvalidAppendDate(format!(
"invalid minute {minute:02} — must be 00-59 (RFC 3501 Section 9, \
RFC 5322 Section 3.3)"
)));
}
if second > 60 {
return Err(crate::Error::InvalidAppendDate(format!(
"invalid second {second:02} — must be 00-60 (RFC 3501 Section 9, \
RFC 5322 Section 3.3; 60 permits leap seconds)"
)));
}
if b[20] != b' ' {
return Err(crate::Error::InvalidAppendDate(format!(
"expected SP at position 20, got {:?} (RFC 3501 Section 9)",
b[20] as char
)));
}
let zone = &b[21..26];
let valid_zone =
(zone[0] == b'+' || zone[0] == b'-') && zone[1..].iter().all(u8::is_ascii_digit);
if !valid_zone {
return Err(crate::Error::InvalidAppendDate(format!(
"invalid zone {:?} — zone must be (\"+\" / \"-\") 4DIGIT (RFC 3501 Section 9)",
std::str::from_utf8(zone).unwrap_or("?????")
)));
}
let zone_hh = (zone[1] - b'0') * 10 + (zone[2] - b'0');
let zone_mm = (zone[3] - b'0') * 10 + (zone[4] - b'0');
if zone_hh > 14 {
return Err(crate::Error::InvalidAppendDate(format!(
"invalid zone hour {zone_hh:02} — must be 00-14 \
(maximum real UTC offset is ±14:00, RFC 3501 Section 9)"
)));
}
if zone_mm > 59 {
return Err(crate::Error::InvalidAppendDate(format!(
"invalid zone minute {zone_mm:02} — must be 00-59 \
(RFC 3501 Section 9)"
)));
}
Ok(())
}
#[allow(clippy::too_many_lines)]
pub(crate) fn encode_command(
tag: &str,
command: &Command,
opts: &EncodeOptions,
) -> Result<EncodedCommand, EncodeError> {
let mut buf = BytesMut::new();
encode_command_to_buf(&mut buf, tag, command, opts)?;
Ok(EncodedCommand::from_flat_buffer(&buf))
}
#[allow(clippy::too_many_lines)]
fn encode_command_to_buf(
buf: &mut BytesMut,
tag: &str,
command: &Command,
opts: &EncodeOptions,
) -> Result<(), EncodeError> {
let literal_mode = opts.literal_mode;
let utf8 = opts.utf8_mode;
match command {
Command::Login { user, pass } => {
encode_login(buf, tag, user, pass, utf8, literal_mode)?;
}
Command::Authenticate {
mechanism,
initial_response,
} => {
validate_atom(mechanism, "AUTHENTICATE mechanism")?;
encode_authenticate(buf, tag, mechanism, initial_response.as_deref())?;
}
Command::StartTls => {
if !opts.has_capability(&Capability::StartTls) {
return Err(EncodeError::MissingCapability {
cmd: "STARTTLS",
cap: "STARTTLS".into(),
});
}
encode_simple(buf, tag, "STARTTLS");
}
Command::Logout => {
encode_simple(buf, tag, "LOGOUT");
}
Command::Enable { capabilities } => {
if !opts.has_capability(&Capability::Enable) {
return Err(EncodeError::MissingCapability {
cmd: "ENABLE",
cap: "ENABLE".into(),
});
}
encode_enable(buf, tag, capabilities)?;
}
Command::List { reference, pattern } => {
let wire_ref = encode_mailbox_str(reference, utf8);
let wire_pat = encode_mailbox_str(pattern, utf8);
encode_two_quoted_args(buf, tag, "LIST", &wire_ref, &wire_pat, utf8, literal_mode);
}
Command::ListExtended {
selection_options,
reference,
patterns,
return_options,
} => {
encode_list_extended(
buf,
tag,
selection_options,
reference,
patterns,
return_options,
utf8,
literal_mode,
)?;
}
Command::ListStatus {
reference,
pattern,
status_items,
} => {
encode_list_status(
buf,
tag,
reference,
pattern,
status_items,
utf8,
literal_mode,
)?;
}
Command::Select {
mailbox,
condstore,
qresync,
} => {
if *condstore && !opts.has_condstore() {
return Err(EncodeError::MissingCapability {
cmd: "SELECT (CONDSTORE)",
cap: "CONDSTORE".into(),
});
}
if qresync.is_some() && !opts.has_capability(&Capability::QResync) {
return Err(EncodeError::MissingCapability {
cmd: "SELECT (QRESYNC)",
cap: "QRESYNC".into(),
});
}
let wire = encode_mailbox_name(mailbox, utf8);
encode_select_or_examine(
buf,
tag,
"SELECT",
&wire,
*condstore,
qresync.as_ref(),
utf8,
literal_mode,
)?;
}
Command::Examine {
mailbox,
condstore,
qresync,
} => {
if *condstore && !opts.has_condstore() {
return Err(EncodeError::MissingCapability {
cmd: "EXAMINE (CONDSTORE)",
cap: "CONDSTORE".into(),
});
}
if qresync.is_some() && !opts.has_capability(&Capability::QResync) {
return Err(EncodeError::MissingCapability {
cmd: "EXAMINE (QRESYNC)",
cap: "QRESYNC".into(),
});
}
let wire = encode_mailbox_name(mailbox, utf8);
encode_select_or_examine(
buf,
tag,
"EXAMINE",
&wire,
*condstore,
qresync.as_ref(),
utf8,
literal_mode,
)?;
}
Command::Create { mailbox } => {
let wire = encode_mailbox_name(mailbox, utf8);
encode_mailbox_cmd(buf, tag, "CREATE", &wire, utf8, literal_mode);
}
Command::CreateSpecialUse {
mailbox,
special_use,
} => {
if !opts.has_capability(&Capability::CreateSpecialUse) {
return Err(EncodeError::MissingCapability {
cmd: "CREATE (USE)",
cap: "CREATE-SPECIAL-USE".into(),
});
}
let wire = encode_mailbox_name(mailbox, utf8);
encode_create_special_use(buf, tag, &wire, special_use, utf8, literal_mode);
}
Command::Delete { mailbox } => {
let wire = encode_mailbox_name(mailbox, utf8);
encode_mailbox_cmd(buf, tag, "DELETE", &wire, utf8, literal_mode);
}
Command::Rename { mailbox, new_name } => {
let wire_old = encode_mailbox_name(mailbox, utf8);
let wire_new = encode_mailbox_name(new_name, utf8);
encode_two_quoted_args(buf, tag, "RENAME", &wire_old, &wire_new, utf8, literal_mode);
}
Command::Subscribe { mailbox } => {
let wire = encode_mailbox_name(mailbox, utf8);
encode_mailbox_cmd(buf, tag, "SUBSCRIBE", &wire, utf8, literal_mode);
}
Command::Unsubscribe { mailbox } => {
let wire = encode_mailbox_name(mailbox, utf8);
encode_mailbox_cmd(buf, tag, "UNSUBSCRIBE", &wire, utf8, literal_mode);
}
Command::Lsub { reference, pattern } => {
let wire_ref = encode_mailbox_str(reference, utf8);
let wire_pat = encode_mailbox_str(pattern, utf8);
encode_two_quoted_args(buf, tag, "LSUB", &wire_ref, &wire_pat, utf8, literal_mode);
}
Command::Close => {
encode_simple(buf, tag, "CLOSE");
}
Command::Unauthenticate => {
encode_simple(buf, tag, "UNAUTHENTICATE");
}
Command::Unselect => {
if !opts.has_capability(&Capability::Unselect) {
return Err(EncodeError::MissingCapability {
cmd: "UNSELECT",
cap: "UNSELECT".into(),
});
}
encode_simple(buf, tag, "UNSELECT");
}
Command::Status { mailbox, items } => {
let wire = encode_mailbox_name(mailbox, utf8);
encode_status(buf, tag, &wire, items, utf8, literal_mode)?;
}
Command::Search { criteria } => {
encode_search(buf, tag, "SEARCH", criteria, None, utf8, literal_mode)?;
}
Command::SearchReturn {
criteria,
return_opts,
} => {
encode_search(
buf,
tag,
"SEARCH",
criteria,
Some(return_opts),
utf8,
literal_mode,
)?;
}
Command::SearchSave { criteria } => {
encode_search(
buf,
tag,
"SEARCH RETURN (SAVE)",
criteria,
None,
utf8,
literal_mode,
)?;
}
Command::Fetch {
sequence_set,
items,
changed_since,
} => {
if changed_since.is_some() && !opts.has_condstore() {
return Err(EncodeError::MissingCapability {
cmd: "FETCH (CHANGEDSINCE)",
cap: "CONDSTORE".into(),
});
}
encode_fetch(buf, tag, sequence_set.as_str(), items, *changed_since)?;
}
Command::Store {
sequence_set,
operation,
flags,
unchanged_since,
} => {
if unchanged_since.is_some() && !opts.has_condstore() {
return Err(EncodeError::MissingCapability {
cmd: "STORE (UNCHANGEDSINCE)",
cap: "CONDSTORE".into(),
});
}
encode_store(
buf,
tag,
false,
sequence_set.as_str(),
*operation,
flags,
*unchanged_since,
)?;
}
Command::Copy {
sequence_set,
mailbox,
} => {
let wire = encode_mailbox_name(mailbox, utf8);
encode_two_arg(
buf,
tag,
false,
"COPY",
sequence_set.as_str(),
&wire,
utf8,
literal_mode,
)?;
}
Command::Move {
sequence_set,
mailbox,
} => {
if !opts.has_capability(&Capability::Move) {
return Err(EncodeError::MissingCapability {
cmd: "MOVE",
cap: "MOVE".into(),
});
}
let wire = encode_mailbox_name(mailbox, utf8);
encode_two_arg(
buf,
tag,
false,
"MOVE",
sequence_set.as_str(),
&wire,
utf8,
literal_mode,
)?;
}
Command::UidSearch { criteria } => {
encode_search(buf, tag, "UID SEARCH", criteria, None, utf8, literal_mode)?;
}
Command::UidSearchReturn {
criteria,
return_opts,
} => {
encode_search(
buf,
tag,
"UID SEARCH",
criteria,
Some(return_opts),
utf8,
literal_mode,
)?;
}
Command::UidSearchSave { criteria } => {
encode_search(
buf,
tag,
"UID SEARCH RETURN (SAVE)",
criteria,
None,
utf8,
literal_mode,
)?;
}
Command::UidFetch {
sequence_set,
items,
changed_since,
vanished,
} => {
if changed_since.is_some() && !opts.has_condstore() {
return Err(EncodeError::MissingCapability {
cmd: "UID FETCH (CHANGEDSINCE)",
cap: "CONDSTORE".into(),
});
}
if *vanished && !opts.has_capability(&Capability::QResync) {
return Err(EncodeError::MissingCapability {
cmd: "UID FETCH (VANISHED)",
cap: "QRESYNC".into(),
});
}
encode_uid_fetch(
buf,
tag,
sequence_set.as_str(),
items,
*changed_since,
*vanished,
)?;
}
Command::UidStore {
sequence_set,
operation,
flags,
unchanged_since,
} => {
if unchanged_since.is_some() && !opts.has_condstore() {
return Err(EncodeError::MissingCapability {
cmd: "UID STORE (UNCHANGEDSINCE)",
cap: "CONDSTORE".into(),
});
}
encode_store(
buf,
tag,
true,
sequence_set.as_str(),
*operation,
flags,
*unchanged_since,
)?;
}
Command::UidMove {
sequence_set,
mailbox,
} => {
if !opts.has_capability(&Capability::Move) {
return Err(EncodeError::MissingCapability {
cmd: "UID MOVE",
cap: "MOVE".into(),
});
}
let wire = encode_mailbox_name(mailbox, utf8);
encode_two_arg(
buf,
tag,
true,
"MOVE",
sequence_set.as_str(),
&wire,
utf8,
literal_mode,
)?;
}
Command::UidCopy {
sequence_set,
mailbox,
} => {
let wire = encode_mailbox_name(mailbox, utf8);
encode_two_arg(
buf,
tag,
true,
"COPY",
sequence_set.as_str(),
&wire,
utf8,
literal_mode,
)?;
}
Command::UidExpunge { sequence_set } => {
if !opts.has_capability(&Capability::UidPlus) {
return Err(EncodeError::MissingCapability {
cmd: "UID EXPUNGE",
cap: "UIDPLUS".into(),
});
}
encode_uid_expunge(buf, tag, sequence_set.as_str())?;
}
Command::Namespace => {
if !opts.has_capability(&Capability::Namespace) {
return Err(EncodeError::MissingCapability {
cmd: "NAMESPACE",
cap: "NAMESPACE".into(),
});
}
encode_simple(buf, tag, "NAMESPACE");
}
Command::Check => {
encode_simple(buf, tag, "CHECK");
}
Command::Expunge => {
encode_simple(buf, tag, "EXPUNGE");
}
Command::Idle => {
if !opts.has_capability(&Capability::Idle) {
return Err(EncodeError::MissingCapability {
cmd: "IDLE",
cap: "IDLE".into(),
});
}
encode_simple(buf, tag, "IDLE");
}
Command::Capability => {
encode_simple(buf, tag, "CAPABILITY");
}
Command::Noop => {
encode_simple(buf, tag, "NOOP");
}
Command::Id(params) => {
if !opts.has_capability(&Capability::Id) {
return Err(EncodeError::MissingCapability {
cmd: "ID",
cap: "ID".into(),
});
}
encode_id(buf, tag, params, utf8, literal_mode)?;
}
Command::GetMetadata {
mailbox,
entries,
max_size,
depth,
} => {
if !opts.has_capability(&Capability::Metadata)
&& !opts.has_capability(&Capability::MetadataServer)
{
return Err(EncodeError::MissingCapability {
cmd: "GETMETADATA",
cap: "METADATA".into(),
});
}
let wire = encode_mailbox_name(mailbox, utf8);
encode_getmetadata(
buf,
tag,
&wire,
entries,
*max_size,
depth.as_deref(),
utf8,
literal_mode,
)?;
}
Command::SetMetadata { mailbox, entries } => {
if !opts.has_capability(&Capability::Metadata)
&& !opts.has_capability(&Capability::MetadataServer)
{
return Err(EncodeError::MissingCapability {
cmd: "SETMETADATA",
cap: "METADATA".into(),
});
}
let wire = encode_mailbox_name(mailbox, utf8);
encode_setmetadata(buf, tag, &wire, entries, utf8, literal_mode)?;
}
Command::Thread {
algorithm,
charset,
criteria,
} => {
encode_thread_or_sort_cmd(
buf,
tag,
"THREAD",
algorithm,
charset,
criteria,
false,
literal_mode,
)?;
}
Command::UidThread {
algorithm,
charset,
criteria,
} => {
encode_thread_or_sort_cmd(
buf,
tag,
"UID THREAD",
algorithm,
charset,
criteria,
false,
literal_mode,
)?;
}
Command::Sort {
sort_criteria,
charset,
criteria,
} => {
encode_thread_or_sort_cmd(
buf,
tag,
"SORT",
sort_criteria,
charset,
criteria,
true,
literal_mode,
)?;
}
Command::UidSort {
sort_criteria,
charset,
criteria,
} => {
encode_thread_or_sort_cmd(
buf,
tag,
"UID SORT",
sort_criteria,
charset,
criteria,
true,
literal_mode,
)?;
}
Command::Compress => {
if !opts.has_capability(&Capability::CompressDeflate) {
return Err(EncodeError::MissingCapability {
cmd: "COMPRESS",
cap: "COMPRESS=DEFLATE".into(),
});
}
buf.extend_from_slice(tag.as_bytes());
buf.extend_from_slice(b" COMPRESS DEFLATE\r\n");
}
Command::GetQuota { root } => {
if !opts.has_capability(&Capability::Quota) {
return Err(EncodeError::MissingCapability {
cmd: "GETQUOTA",
cap: "QUOTA".into(),
});
}
encode_mailbox_cmd(buf, tag, "GETQUOTA", root, utf8, literal_mode);
}
Command::GetQuotaRoot { mailbox } => {
if !opts.has_capability(&Capability::Quota) {
return Err(EncodeError::MissingCapability {
cmd: "GETQUOTAROOT",
cap: "QUOTA".into(),
});
}
let wire = encode_mailbox_name(mailbox, utf8);
encode_mailbox_cmd(buf, tag, "GETQUOTAROOT", &wire, utf8, literal_mode);
}
Command::SetQuota { root, resources } => {
if !opts.has_capability(&Capability::QuotaSet) {
return Err(EncodeError::MissingCapability {
cmd: "SETQUOTA",
cap: "QUOTASET".into(),
});
}
encode_set_quota(buf, tag, root, resources, utf8, literal_mode)?;
}
Command::SetAcl {
mailbox,
identifier,
rights,
} => {
if !opts.has_capability(&Capability::Acl) {
return Err(EncodeError::MissingCapability {
cmd: "SETACL",
cap: "ACL".into(),
});
}
let wire = encode_mailbox_name(mailbox, utf8);
encode_set_acl(buf, tag, &wire, identifier, rights, utf8, literal_mode);
}
Command::DeleteAcl {
mailbox,
identifier,
} => {
if !opts.has_capability(&Capability::Acl) {
return Err(EncodeError::MissingCapability {
cmd: "DELETEACL",
cap: "ACL".into(),
});
}
let wire = encode_mailbox_name(mailbox, utf8);
encode_two_quoted_args(buf, tag, "DELETEACL", &wire, identifier, utf8, literal_mode);
}
Command::GetAcl { mailbox } => {
if !opts.has_capability(&Capability::Acl) {
return Err(EncodeError::MissingCapability {
cmd: "GETACL",
cap: "ACL".into(),
});
}
let wire = encode_mailbox_name(mailbox, utf8);
encode_mailbox_cmd(buf, tag, "GETACL", &wire, utf8, literal_mode);
}
Command::ListRights {
mailbox,
identifier,
} => {
if !opts.has_capability(&Capability::Acl) {
return Err(EncodeError::MissingCapability {
cmd: "LISTRIGHTS",
cap: "ACL".into(),
});
}
let wire = encode_mailbox_name(mailbox, utf8);
encode_two_quoted_args(
buf,
tag,
"LISTRIGHTS",
&wire,
identifier,
utf8,
literal_mode,
);
}
Command::MyRights { mailbox } => {
if !opts.has_capability(&Capability::Acl) {
return Err(EncodeError::MissingCapability {
cmd: "MYRIGHTS",
cap: "ACL".into(),
});
}
let wire = encode_mailbox_name(mailbox, utf8);
encode_mailbox_cmd(buf, tag, "MYRIGHTS", &wire, utf8, literal_mode);
}
Command::NotifySet(params) => {
if !opts.has_capability(&Capability::Notify) {
return Err(EncodeError::MissingCapability {
cmd: "NOTIFY SET",
cap: "NOTIFY".into(),
});
}
encode_notify_set(buf, tag, params, utf8, literal_mode)?;
}
Command::NotifyNone => {
if !opts.has_capability(&Capability::Notify) {
return Err(EncodeError::MissingCapability {
cmd: "NOTIFY NONE",
cap: "NOTIFY".into(),
});
}
encode_simple(buf, tag, "NOTIFY NONE");
}
}
Ok(())
}