use super::{
encode_changedsince_modifier, encode_metadata_value, encode_quoted_or_literal,
encode_quoted_or_literal_utf8, search_criteria_starts_with_charset, validate_and_filter_flags,
validate_append_datetime, validate_atom, validate_login_credential_ascii,
validate_metadata_entry_name, validate_mod_sequence_value, validate_mod_sequence_valzer,
validate_no_crlf, validate_sasl_initial_response, validate_search_criteria_crlf,
validate_sort_thread_charset, BytesMut, HashSet, LiteralMode, MailboxAttribute, QresyncParams,
LITERAL_MINUS_MAX,
};
use crate::types::notify::{MailboxFilter, NotifyEvent, NotifySetParams};
use crate::types::validated::MailboxName;
pub(super) fn encode_simple(buf: &mut BytesMut, tag: &str, command: &str) {
buf.extend_from_slice(tag.as_bytes());
buf.extend_from_slice(b" ");
buf.extend_from_slice(command.as_bytes());
buf.extend_from_slice(b"\r\n");
}
pub(super) fn encode_login(
buf: &mut BytesMut,
tag: &str,
user: &str,
pass: &str,
utf8: bool,
literal_mode: LiteralMode,
) -> Result<(), crate::Error> {
validate_login_credential_ascii(user, "user")?;
validate_login_credential_ascii(pass, "password")?;
buf.extend_from_slice(tag.as_bytes());
buf.extend_from_slice(b" LOGIN ");
encode_quoted_or_literal_utf8(buf, user.as_bytes(), utf8, literal_mode);
buf.extend_from_slice(b" ");
encode_quoted_or_literal_utf8(buf, pass.as_bytes(), utf8, literal_mode);
buf.extend_from_slice(b"\r\n");
Ok(())
}
pub(super) fn encode_authenticate(
buf: &mut BytesMut,
tag: &str,
mechanism: &str,
initial_response: Option<&str>,
) -> Result<(), crate::Error> {
buf.extend_from_slice(tag.as_bytes());
buf.extend_from_slice(b" AUTHENTICATE ");
buf.extend_from_slice(mechanism.as_bytes());
if let Some(ir) = initial_response {
validate_sasl_initial_response(ir)?;
if ir.is_empty() {
buf.extend_from_slice(b" =");
} else {
buf.extend_from_slice(b" ");
buf.extend_from_slice(ir.as_bytes());
}
}
buf.extend_from_slice(b"\r\n");
Ok(())
}
pub(super) fn encode_status(
buf: &mut BytesMut,
tag: &str,
mailbox: &str,
items: &str,
utf8: bool,
literal_mode: LiteralMode,
) -> Result<(), crate::Error> {
validate_no_crlf(items, "STATUS items")?;
let status_items = normalize_status_items(items, "STATUS items")?;
buf.extend_from_slice(tag.as_bytes());
buf.extend_from_slice(b" STATUS ");
encode_quoted_or_literal_utf8(buf, mailbox.as_bytes(), utf8, literal_mode);
buf.extend_from_slice(b" ");
buf.extend_from_slice(status_items.as_bytes());
buf.extend_from_slice(b"\r\n");
Ok(())
}
pub(super) fn encode_search(
buf: &mut BytesMut,
tag: &str,
cmd: &str,
criteria: &str,
return_opts: Option<&[String]>,
utf8: bool,
literal_mode: LiteralMode,
) -> Result<(), crate::Error> {
validate_search_criteria_crlf(criteria, "SEARCH criteria", literal_mode)?;
validate_non_empty_search_criteria(criteria, cmd)?;
if utf8 && search_criteria_starts_with_charset(criteria) {
return Err(crate::Error::Protocol(format!(
"{cmd} must not include a CHARSET specification when UTF8=ACCEPT is active \
(RFC 6855 Section 3)"
)));
}
buf.extend_from_slice(tag.as_bytes());
buf.extend_from_slice(b" ");
buf.extend_from_slice(cmd.as_bytes());
if let Some(opts) = return_opts {
for opt in opts {
validate_no_crlf(opt, "SEARCH RETURN option")?;
validate_atom(opt.trim(), "SEARCH RETURN option")?;
}
buf.extend_from_slice(b" RETURN (");
for (i, opt) in opts.iter().enumerate() {
if i > 0 {
buf.extend_from_slice(b" ");
}
buf.extend_from_slice(opt.trim().as_bytes());
}
buf.extend_from_slice(b")");
}
buf.extend_from_slice(b" ");
buf.extend_from_slice(criteria.as_bytes());
buf.extend_from_slice(b"\r\n");
Ok(())
}
pub(super) fn encode_fetch(
buf: &mut BytesMut,
tag: &str,
sequence_set: &str,
items: &str,
changed_since: Option<u64>,
) -> Result<(), crate::Error> {
validate_no_crlf(sequence_set, "FETCH sequence set")?;
validate_no_crlf(items, "FETCH items")?;
validate_non_empty_fetch_items(items, "FETCH items")?;
buf.extend_from_slice(tag.as_bytes());
buf.extend_from_slice(b" FETCH ");
buf.extend_from_slice(sequence_set.as_bytes());
buf.extend_from_slice(b" ");
buf.extend_from_slice(items.as_bytes());
if let Some(modseq) = changed_since {
encode_changedsince_modifier(buf, modseq, false)?;
}
buf.extend_from_slice(b"\r\n");
Ok(())
}
pub(super) fn encode_store(
buf: &mut BytesMut,
tag: &str,
uid: bool,
sequence_set: &str,
operation: crate::types::StoreOperation,
flags: &[crate::types::Flag],
unchanged_since: Option<u64>,
) -> Result<(), crate::Error> {
validate_no_crlf(sequence_set, "STORE sequence set")?;
buf.extend_from_slice(tag.as_bytes());
if uid {
buf.extend_from_slice(b" UID STORE ");
} else {
buf.extend_from_slice(b" STORE ");
}
buf.extend_from_slice(sequence_set.as_bytes());
if let Some(modseq) = unchanged_since {
validate_mod_sequence_valzer(modseq, "UNCHANGEDSINCE")?;
buf.extend_from_slice(b" (UNCHANGEDSINCE ");
buf.extend_from_slice(modseq.to_string().as_bytes());
buf.extend_from_slice(b")");
}
encode_store_flags(buf, operation, flags)
}
#[allow(clippy::too_many_arguments)]
pub(super) fn encode_two_arg(
buf: &mut BytesMut,
tag: &str,
uid: bool,
cmd: &str,
arg1: &str,
arg2: &str,
utf8: bool,
literal_mode: LiteralMode,
) -> Result<(), crate::Error> {
validate_no_crlf(arg1, &format!("{cmd} sequence set"))?;
buf.extend_from_slice(tag.as_bytes());
if uid {
buf.extend_from_slice(b" UID ");
} else {
buf.extend_from_slice(b" ");
}
buf.extend_from_slice(cmd.as_bytes());
buf.extend_from_slice(b" ");
buf.extend_from_slice(arg1.as_bytes());
buf.extend_from_slice(b" ");
encode_quoted_or_literal_utf8(buf, arg2.as_bytes(), utf8, literal_mode);
buf.extend_from_slice(b"\r\n");
Ok(())
}
pub(super) fn encode_two_quoted_args(
buf: &mut BytesMut,
tag: &str,
cmd: &str,
arg1: &str,
arg2: &str,
utf8: bool,
literal_mode: LiteralMode,
) {
buf.extend_from_slice(tag.as_bytes());
buf.extend_from_slice(b" ");
buf.extend_from_slice(cmd.as_bytes());
buf.extend_from_slice(b" ");
encode_quoted_or_literal_utf8(buf, arg1.as_bytes(), utf8, literal_mode);
buf.extend_from_slice(b" ");
encode_quoted_or_literal_utf8(buf, arg2.as_bytes(), utf8, literal_mode);
buf.extend_from_slice(b"\r\n");
}
pub(super) fn encode_list_status(
buf: &mut BytesMut,
tag: &str,
reference: &str,
pattern: &str,
status_items: &str,
utf8: bool,
literal_mode: LiteralMode,
) -> Result<(), crate::Error> {
validate_no_crlf(status_items, "LIST-STATUS status items")?;
let status_items = normalize_status_items_body(status_items, "LIST-STATUS status items")?;
let wire_ref = encode_mailbox_str(reference, utf8);
let wire_pat = encode_mailbox_str(pattern, utf8);
buf.extend_from_slice(tag.as_bytes());
buf.extend_from_slice(b" LIST ");
encode_quoted_or_literal_utf8(buf, wire_ref.as_bytes(), utf8, literal_mode);
buf.extend_from_slice(b" ");
encode_quoted_or_literal_utf8(buf, wire_pat.as_bytes(), utf8, literal_mode);
buf.extend_from_slice(b" RETURN (STATUS (");
buf.extend_from_slice(status_items.as_bytes());
buf.extend_from_slice(b"))\r\n");
Ok(())
}
fn normalize_status_items(items: &str, context: &str) -> Result<String, crate::Error> {
let body = normalize_status_items_body(items, context)?;
Ok(format!("({body})"))
}
fn normalize_status_items_body(items: &str, context: &str) -> Result<String, crate::Error> {
let trimmed = items.trim();
if trimmed.is_empty() {
return Err(crate::Error::Protocol(format!(
"{context} must contain at least one status data item \
(RFC 3501 Section 6.3.10 / RFC 9051 Section 6.3.11)"
)));
}
let body = match (trimmed.strip_prefix('('), trimmed.strip_suffix(')')) {
(Some(without_open), Some(_)) => {
let inner = without_open
.strip_suffix(')')
.ok_or_else(|| {
crate::Error::Protocol(format!(
"{context} must be a single parenthesized status-att-list \
(RFC 3501 Section 6.3.10 / RFC 9051 Section 6.3.11)"
))
})?
.trim();
if inner.is_empty() {
return Err(crate::Error::Protocol(format!(
"{context} must contain at least one status data item \
(RFC 3501 Section 6.3.10 / RFC 9051 Section 6.3.11)"
)));
}
inner
}
(Some(_), None) | (None, Some(_)) => {
return Err(crate::Error::Protocol(format!(
"{context} must use balanced parentheses for status-att-list \
syntax (RFC 3501 Section 6.3.10 / RFC 9051 Section 6.3.11)"
)));
}
(None, None) => trimmed,
};
if body.contains('(') || body.contains(')') {
return Err(crate::Error::Protocol(format!(
"{context} must be a flat status-att-list without nested parentheses \
(RFC 3501 Section 6.3.10 / RFC 9051 Section 6.3.11)"
)));
}
Ok(body.to_owned())
}
fn validate_non_empty_search_criteria(criteria: &str, context: &str) -> Result<(), crate::Error> {
if criteria.trim().is_empty() {
return Err(crate::Error::Protocol(format!(
"{context} must contain at least one search criterion \
(RFC 3501 Section 6.4.4 / RFC 5256 Section 6)"
)));
}
Ok(())
}
fn validate_single_fetch_att(attr: &str) -> Result<(), crate::Error> {
let trimmed = attr.trim();
if trimmed.is_empty() {
return Err(crate::Error::Protocol(
"NOTIFY MessageNew fetch-att must not be empty \
(RFC 5465 Section 8)"
.into(),
));
}
if trimmed.starts_with('(') {
return Err(crate::Error::Protocol(
"NOTIFY MessageNew fetch-att must be a single item, not a \
parenthesized list — use separate vector elements for each \
fetch-att (RFC 5465 Section 8)"
.into(),
));
}
validate_non_empty_fetch_items(trimmed, "NOTIFY fetch-att")
}
fn validate_non_empty_fetch_items(items: &str, context: &str) -> Result<(), crate::Error> {
let trimmed = items.trim();
if trimmed.is_empty() {
return Err(crate::Error::Protocol(format!(
"{context} must contain at least one message data item \
(RFC 3501 Section 6.4.5 / RFC 9051 Section 6.4.5)"
)));
}
if let Some(inner) = trimmed.strip_prefix('(') {
let inner = inner.strip_suffix(')').ok_or_else(|| {
crate::Error::Protocol(format!(
"{context} must use balanced parentheses for fetch-att list \
syntax (RFC 3501 Section 6.4.5 / RFC 9051 Section 6.4.5)"
))
})?;
if inner.trim().is_empty() {
return Err(crate::Error::Protocol(format!(
"{context} must contain at least one message data item \
(RFC 3501 Section 6.4.5 / RFC 9051 Section 6.4.5)"
)));
}
}
let mut in_quote = false;
let mut escaped = false;
let mut bracket_depth = 0u32;
let mut angle_depth = 0u32;
let mut paren_depth = 0u32;
for ch in trimmed.chars() {
if in_quote {
if escaped {
escaped = false;
continue;
}
match ch {
'\\' => escaped = true,
'"' => in_quote = false,
_ => {}
}
continue;
}
match ch {
'"' => in_quote = true,
'[' => bracket_depth += 1,
']' => {
if bracket_depth == 0 {
return Err(crate::Error::Protocol(format!(
"{context} must use balanced quotes and delimiters for fetch-att \
syntax (RFC 3501 Section 6.4.5 / RFC 9051 Section 6.4.5)"
)));
}
bracket_depth -= 1;
}
'<' if bracket_depth == 0 => angle_depth += 1,
'>' if bracket_depth == 0 => {
if angle_depth == 0 {
return Err(crate::Error::Protocol(format!(
"{context} must use balanced quotes and delimiters for fetch-att \
syntax (RFC 3501 Section 6.4.5 / RFC 9051 Section 6.4.5)"
)));
}
angle_depth -= 1;
}
'(' if bracket_depth == 0 && angle_depth == 0 => paren_depth += 1,
')' if bracket_depth == 0 && angle_depth == 0 => {
if paren_depth == 0 {
return Err(crate::Error::Protocol(format!(
"{context} must use balanced quotes and delimiters for fetch-att \
syntax (RFC 3501 Section 6.4.5 / RFC 9051 Section 6.4.5)"
)));
}
paren_depth -= 1;
}
_ => {}
}
}
if in_quote || bracket_depth != 0 || angle_depth != 0 || paren_depth != 0 {
return Err(crate::Error::Protocol(format!(
"{context} must use balanced quotes and delimiters for fetch-att \
syntax (RFC 3501 Section 6.4.5 / RFC 9051 Section 6.4.5)"
)));
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub(super) fn encode_list_extended(
buf: &mut BytesMut,
tag: &str,
selection_options: &[String],
reference: &str,
patterns: &[String],
return_options: &[String],
utf8: bool,
literal_mode: LiteralMode,
) -> Result<(), crate::Error> {
if patterns.is_empty() {
return Err(crate::Error::Protocol(
"LIST-EXTENDED requires at least one mailbox pattern \
(RFC 5258 Section 3 / RFC 9051 Section 6.3.9)"
.into(),
));
}
for option in selection_options {
validate_no_crlf(option, "LIST-EXTENDED selection option")?;
}
for option in return_options {
validate_no_crlf(option, "LIST-EXTENDED return option")?;
}
validate_list_extended_option_syntax(selection_options, return_options)?;
buf.extend_from_slice(tag.as_bytes());
buf.extend_from_slice(b" LIST");
if !selection_options.is_empty() {
buf.extend_from_slice(b" (");
for (index, option) in selection_options.iter().enumerate() {
if index > 0 {
buf.extend_from_slice(b" ");
}
buf.extend_from_slice(option.trim().as_bytes());
}
buf.extend_from_slice(b")");
}
let wire_ref = encode_mailbox_str(reference, utf8);
buf.extend_from_slice(b" ");
encode_quoted_or_literal_utf8(buf, wire_ref.as_bytes(), utf8, literal_mode);
buf.extend_from_slice(b" ");
if patterns.len() == 1 {
let wire_pat = encode_mailbox_str(&patterns[0], utf8);
encode_quoted_or_literal_utf8(buf, wire_pat.as_bytes(), utf8, literal_mode);
} else {
buf.extend_from_slice(b"(");
for (index, pattern) in patterns.iter().enumerate() {
if index > 0 {
buf.extend_from_slice(b" ");
}
let wire_pat = encode_mailbox_str(pattern, utf8);
encode_quoted_or_literal_utf8(buf, wire_pat.as_bytes(), utf8, literal_mode);
}
buf.extend_from_slice(b")");
}
if !return_options.is_empty() {
buf.extend_from_slice(b" RETURN (");
for (index, option) in return_options.iter().enumerate() {
if index > 0 {
buf.extend_from_slice(b" ");
}
buf.extend_from_slice(option.trim().as_bytes());
}
buf.extend_from_slice(b")");
}
buf.extend_from_slice(b"\r\n");
Ok(())
}
fn validate_list_extended_option_syntax(
selection_options: &[String],
return_options: &[String],
) -> Result<(), crate::Error> {
let has_recursivematch = selection_options
.iter()
.any(|option| option.trim().eq_ignore_ascii_case("RECURSIVEMATCH"));
if has_recursivematch
&& !selection_options.iter().any(|option| {
let trimmed = option.trim();
!trimmed.is_empty()
&& !trimmed.eq_ignore_ascii_case("RECURSIVEMATCH")
&& !trimmed.eq_ignore_ascii_case("REMOTE")
})
{
return Err(crate::Error::Protocol(
"LIST-EXTENDED selection option RECURSIVEMATCH requires another \
non-REMOTE selection option (RFC 5258 Section 3 / RFC 9051 Section 6.3.9)"
.into(),
));
}
for option in selection_options {
if option.trim().is_empty() {
return Err(crate::Error::Protocol(
"LIST-EXTENDED selection options must not be empty \
(RFC 5258 Section 3 / RFC 9051 Section 6.3.9)"
.into(),
));
}
}
for option in return_options {
let trimmed = option.trim();
if trimmed.is_empty() {
return Err(crate::Error::Protocol(
"LIST-EXTENDED return options must not be empty \
(RFC 5258 Section 3 / RFC 9051 Section 6.3.9)"
.into(),
));
}
if let Some(status_items) = list_status_return_option_items(trimmed).transpose()? {
let wrapped = format!("({status_items})");
let _ = normalize_status_items_body(&wrapped, "LIST-EXTENDED STATUS return option")?;
}
}
Ok(())
}
fn list_status_return_option_items(option: &str) -> Option<Result<&str, crate::Error>> {
if !option
.get(..6)
.is_some_and(|prefix| prefix.eq_ignore_ascii_case("STATUS"))
{
return None;
}
match option.as_bytes().get(6).copied() {
Some(next) if next != b' ' && !next.is_ascii_whitespace() && next != b'(' => return None,
_ => {}
}
Some(if let Some(suffix) = option[6..].strip_prefix(" (") {
if suffix.ends_with(')') && suffix.len() >= 2 {
Ok(&suffix[1..suffix.len() - 1])
} else {
Err(crate::Error::Protocol(
"LIST-EXTENDED STATUS return option must be STATUS (<items>) \
per RFC 5819 Section 4 / RFC 9051 Section 7"
.into(),
))
}
} else {
Err(crate::Error::Protocol(
"LIST-EXTENDED STATUS return option must be STATUS (<items>) \
per RFC 5819 Section 4 / RFC 9051 Section 7"
.into(),
))
})
}
pub(super) fn encode_create_special_use(
buf: &mut BytesMut,
tag: &str,
mailbox: &str,
special_use: &[MailboxAttribute],
utf8: bool,
literal_mode: LiteralMode,
) {
buf.extend_from_slice(tag.as_bytes());
buf.extend_from_slice(b" CREATE ");
encode_quoted_or_literal_utf8(buf, mailbox.as_bytes(), utf8, literal_mode);
buf.extend_from_slice(b" (USE (");
for (i, attr) in special_use.iter().enumerate() {
if i > 0 {
buf.extend_from_slice(b" ");
}
buf.extend_from_slice(attr.as_imap_str().as_bytes());
}
buf.extend_from_slice(b"))\r\n");
}
pub(crate) fn encode_mailbox_str(name: &str, utf8: bool) -> String {
if name.eq_ignore_ascii_case("INBOX") {
return "INBOX".to_owned();
}
if name.len() > 5 && name.is_char_boundary(5) {
let prefix = &name[..5];
let sep = name[5..].chars().next();
if prefix.eq_ignore_ascii_case("INBOX")
&& sep.is_some_and(|ch| ch.is_ascii() && !ch.is_ascii_alphanumeric())
{
let child = &name[5..];
let encoded_child = if utf8 {
child.to_owned()
} else {
crate::codec::utf7::encode_utf7(child)
};
return format!("INBOX{encoded_child}");
}
}
if utf8 {
name.to_owned()
} else {
crate::codec::utf7::encode_utf7(name)
}
}
pub(super) fn encode_mailbox_name(name: &MailboxName, utf8: bool) -> String {
encode_mailbox_str(name.as_str(), utf8)
}
pub(super) fn encode_mailbox_cmd(
buf: &mut BytesMut,
tag: &str,
cmd: &str,
mailbox: &str,
utf8: bool,
literal_mode: LiteralMode,
) {
buf.extend_from_slice(tag.as_bytes());
buf.extend_from_slice(b" ");
buf.extend_from_slice(cmd.as_bytes());
buf.extend_from_slice(b" ");
encode_quoted_or_literal_utf8(buf, mailbox.as_bytes(), utf8, literal_mode);
buf.extend_from_slice(b"\r\n");
}
#[allow(clippy::too_many_arguments)]
pub(super) fn encode_select_or_examine(
buf: &mut BytesMut,
tag: &str,
cmd: &str,
mailbox: &str,
condstore: bool,
qresync: Option<&QresyncParams>,
utf8: bool,
literal_mode: LiteralMode,
) -> Result<(), crate::Error> {
buf.extend_from_slice(tag.as_bytes());
buf.extend_from_slice(b" ");
buf.extend_from_slice(cmd.as_bytes());
buf.extend_from_slice(b" ");
encode_quoted_or_literal_utf8(buf, mailbox.as_bytes(), utf8, literal_mode);
if condstore && qresync.is_none() {
buf.extend_from_slice(b" (CONDSTORE)");
}
if let Some(params) = qresync {
if params.uid_validity == 0 {
return Err(crate::Error::Protocol(
"QRESYNC uid_validity must be non-zero (nz-number per RFC 3501 Section 9)".into(),
));
}
validate_mod_sequence_value(params.mod_seq, "QRESYNC mod_seq")?;
buf.extend_from_slice(b" (QRESYNC (");
buf.extend_from_slice(params.uid_validity.to_string().as_bytes());
buf.extend_from_slice(b" ");
buf.extend_from_slice(params.mod_seq.to_string().as_bytes());
if let Some(known_uids) = ¶ms.known_uids {
validate_no_crlf(known_uids, "QRESYNC known-uids")?;
crate::types::SequenceSet::new_known(known_uids.as_str())?;
buf.extend_from_slice(b" ");
buf.extend_from_slice(known_uids.as_bytes());
}
if let Some((seq_set, uid_set)) = ¶ms.seq_match_data {
if params.known_uids.is_none() {
return Err(crate::Error::Protocol(
"seq-match-data requires known-uids (RFC 7162 Section 3.2.5.2)".into(),
));
}
validate_no_crlf(seq_set, "QRESYNC seq-match-data sequence set")?;
validate_no_crlf(uid_set, "QRESYNC seq-match-data UID set")?;
crate::types::SequenceSet::new_known(seq_set.as_str())?;
crate::types::SequenceSet::new_known(uid_set.as_str())?;
buf.extend_from_slice(b" (");
buf.extend_from_slice(seq_set.as_bytes());
buf.extend_from_slice(b" ");
buf.extend_from_slice(uid_set.as_bytes());
buf.extend_from_slice(b")");
}
buf.extend_from_slice(b"))");
}
buf.extend_from_slice(b"\r\n");
Ok(())
}
pub(super) fn encode_uid_fetch(
buf: &mut BytesMut,
tag: &str,
sequence_set: &str,
items: &str,
changed_since: Option<u64>,
vanished: bool,
) -> Result<(), crate::Error> {
if vanished && changed_since.is_none() {
return Err(crate::Error::Protocol(
"VANISHED modifier requires CHANGEDSINCE per RFC 7162 Section 3.2.6".into(),
));
}
validate_no_crlf(sequence_set, "UID FETCH sequence set")?;
validate_no_crlf(items, "UID FETCH items")?;
validate_non_empty_fetch_items(items, "UID FETCH items")?;
buf.extend_from_slice(tag.as_bytes());
buf.extend_from_slice(b" UID FETCH ");
buf.extend_from_slice(sequence_set.as_bytes());
buf.extend_from_slice(b" ");
buf.extend_from_slice(items.as_bytes());
if let Some(modseq) = changed_since {
encode_changedsince_modifier(buf, modseq, vanished)?;
}
buf.extend_from_slice(b"\r\n");
Ok(())
}
fn encode_store_flags(
buf: &mut BytesMut,
operation: crate::types::StoreOperation,
flags: &[crate::types::Flag],
) -> Result<(), crate::Error> {
buf.extend_from_slice(b" ");
match operation {
crate::types::StoreOperation::Add => buf.extend_from_slice(b"+FLAGS"),
crate::types::StoreOperation::Remove => buf.extend_from_slice(b"-FLAGS"),
crate::types::StoreOperation::Replace => buf.extend_from_slice(b"FLAGS"),
crate::types::StoreOperation::AddSilent => buf.extend_from_slice(b"+FLAGS.SILENT"),
crate::types::StoreOperation::RemoveSilent => buf.extend_from_slice(b"-FLAGS.SILENT"),
crate::types::StoreOperation::ReplaceSilent => buf.extend_from_slice(b"FLAGS.SILENT"),
}
let valid_flags = validate_and_filter_flags(flags, "STORE")?;
if valid_flags.is_empty() {
match operation {
crate::types::StoreOperation::Replace | crate::types::StoreOperation::ReplaceSilent => {
}
_ => {
return Err(crate::Error::Protocol(
"STORE +FLAGS/-FLAGS requires at least one flag; adding or \
removing zero flags is a no-op (RFC 3501 Section 6.4.6)"
.into(),
));
}
}
}
buf.extend_from_slice(b" (");
for (i, flag) in valid_flags.iter().enumerate() {
if i > 0 {
buf.extend_from_slice(b" ");
}
buf.extend_from_slice(flag.as_imap_str().as_bytes());
}
buf.extend_from_slice(b")\r\n");
Ok(())
}
pub(super) fn encode_uid_expunge(
buf: &mut BytesMut,
tag: &str,
sequence_set: &str,
) -> Result<(), crate::Error> {
validate_no_crlf(sequence_set, "UID EXPUNGE sequence set")?;
buf.extend_from_slice(tag.as_bytes());
buf.extend_from_slice(b" UID EXPUNGE ");
buf.extend_from_slice(sequence_set.as_bytes());
buf.extend_from_slice(b"\r\n");
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub(super) fn encode_getmetadata(
buf: &mut BytesMut,
tag: &str,
mailbox: &str,
entries: &[String],
max_size: Option<u64>,
depth: Option<&str>,
utf8: bool,
literal_mode: LiteralMode,
) -> Result<(), crate::Error> {
if let Some(n) = max_size {
if n > u64::from(u32::MAX) {
return Err(crate::Error::Protocol(format!(
"GETMETADATA MAXSIZE must fit in number (u32) per RFC 5464 Section 5 / RFC 3501 Section 9, got {n}"
)));
}
}
if entries.is_empty() {
return Err(crate::Error::Protocol(
"GETMETADATA requires at least one entry (RFC 5464 Section 4.2)".into(),
));
}
for entry in entries {
validate_metadata_entry_name(entry, "GETMETADATA entry name")?;
}
if let Some(d) = depth {
if d != "0" && d != "1" && d != "infinity" {
return Err(crate::Error::Protocol(format!(
"GETMETADATA DEPTH must be \"0\", \"1\", or \"infinity\" \
(RFC 5464 Section 4.2.2), got: {d:?}"
)));
}
}
buf.extend_from_slice(tag.as_bytes());
buf.extend_from_slice(b" GETMETADATA");
if max_size.is_some() || depth.is_some() {
buf.extend_from_slice(b" (");
let first_opt = if let Some(n) = max_size {
buf.extend_from_slice(b"MAXSIZE ");
buf.extend_from_slice(n.to_string().as_bytes());
false
} else {
true
};
if let Some(d) = depth {
if !first_opt {
buf.extend_from_slice(b" ");
}
buf.extend_from_slice(b"DEPTH ");
buf.extend_from_slice(d.as_bytes());
}
buf.extend_from_slice(b")");
}
buf.extend_from_slice(b" ");
encode_quoted_or_literal_utf8(buf, mailbox.as_bytes(), utf8, literal_mode);
buf.extend_from_slice(b" ");
if entries.len() == 1 {
encode_quoted_or_literal_utf8(buf, entries[0].as_bytes(), utf8, literal_mode);
} else {
buf.extend_from_slice(b"(");
for (i, entry) in entries.iter().enumerate() {
if i > 0 {
buf.extend_from_slice(b" ");
}
encode_quoted_or_literal_utf8(buf, entry.as_bytes(), utf8, literal_mode);
}
buf.extend_from_slice(b")");
}
buf.extend_from_slice(b"\r\n");
Ok(())
}
pub(super) fn encode_setmetadata(
buf: &mut BytesMut,
tag: &str,
mailbox: &str,
entries: &[(String, Option<Vec<u8>>)],
utf8: bool,
literal_mode: LiteralMode,
) -> Result<(), crate::Error> {
if entries.is_empty() {
return Err(crate::Error::Protocol(
"SETMETADATA requires at least one entry (RFC 5464 Section 5)".into(),
));
}
for (name, _) in entries {
validate_metadata_entry_name(name, "SETMETADATA entry name")?;
}
buf.extend_from_slice(tag.as_bytes());
buf.extend_from_slice(b" SETMETADATA ");
encode_quoted_or_literal_utf8(buf, mailbox.as_bytes(), utf8, literal_mode);
buf.extend_from_slice(b" (");
for (i, (name, value)) in entries.iter().enumerate() {
if i > 0 {
buf.extend_from_slice(b" ");
}
encode_quoted_or_literal_utf8(buf, name.as_bytes(), utf8, literal_mode);
buf.extend_from_slice(b" ");
match value {
Some(v) => encode_metadata_value(buf, v, literal_mode),
None => buf.extend_from_slice(b"NIL"),
}
}
buf.extend_from_slice(b")\r\n");
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub(super) fn encode_thread_or_sort_cmd(
buf: &mut BytesMut,
tag: &str,
cmd: &str,
algorithm: &str,
charset: &str,
criteria: &str,
parenthesize_algo: bool,
literal_mode: LiteralMode,
) -> Result<(), crate::Error> {
if parenthesize_algo {
for key in algorithm.split(' ') {
validate_atom(key, "SORT sort-key")?;
}
} else {
validate_atom(algorithm, "THREAD algorithm")?;
}
validate_sort_thread_charset(charset)?;
validate_search_criteria_crlf(criteria, &format!("{cmd} criteria"), literal_mode)?;
validate_non_empty_search_criteria(criteria, cmd)?;
buf.extend_from_slice(tag.as_bytes());
buf.extend_from_slice(b" ");
buf.extend_from_slice(cmd.as_bytes());
if parenthesize_algo {
buf.extend_from_slice(b" (");
buf.extend_from_slice(algorithm.as_bytes());
buf.extend_from_slice(b") ");
} else {
buf.extend_from_slice(b" ");
buf.extend_from_slice(algorithm.as_bytes());
buf.extend_from_slice(b" ");
}
buf.extend_from_slice(charset.as_bytes());
buf.extend_from_slice(b" ");
buf.extend_from_slice(criteria.as_bytes());
buf.extend_from_slice(b"\r\n");
Ok(())
}
pub(super) fn encode_id(
buf: &mut BytesMut,
tag: &str,
params: &[(String, Option<String>)],
utf8: bool,
literal_mode: LiteralMode,
) -> Result<(), crate::Error> {
if params.len() > 30 {
return Err(crate::Error::Protocol(format!(
"ID command has {} field-value pairs, but RFC 2971 Section 3.3 \
allows at most 30",
params.len()
)));
}
for (key, value) in params {
if key.len() > 30 {
return Err(crate::Error::Protocol(format!(
"ID field name is {} octets, but RFC 2971 Section 3.3 \
allows at most 30",
key.len()
)));
}
if let Some(v) = value {
if v.len() > 1024 {
return Err(crate::Error::Protocol(format!(
"ID value is {} octets, but RFC 2971 Section 3.3 \
allows at most 1024",
v.len()
)));
}
}
}
let mut seen_keys = HashSet::with_capacity(params.len());
for (key, _) in params {
let normalized = key.to_ascii_lowercase();
if !seen_keys.insert(normalized) {
return Err(crate::Error::Protocol(format!(
"ID command repeats the same field name more than once: {key} \
(RFC 2971 Section 3.3)"
)));
}
}
buf.extend_from_slice(tag.as_bytes());
if params.is_empty() {
buf.extend_from_slice(b" ID NIL\r\n");
} else {
buf.extend_from_slice(b" ID (");
for (i, (key, value)) in params.iter().enumerate() {
if i > 0 {
buf.extend_from_slice(b" ");
}
encode_quoted_or_literal_utf8(buf, key.as_bytes(), utf8, literal_mode);
buf.extend_from_slice(b" ");
match value {
Some(v) => encode_quoted_or_literal_utf8(buf, v.as_bytes(), utf8, literal_mode),
None => buf.extend_from_slice(b"NIL"),
}
}
buf.extend_from_slice(b")\r\n");
}
Ok(())
}
pub(super) fn encode_set_quota(
buf: &mut BytesMut,
tag: &str,
root: &str,
resources: &[(String, u64)],
utf8: bool,
literal_mode: LiteralMode,
) -> Result<(), crate::Error> {
for (resource, limit) in resources {
validate_atom(resource, "SETQUOTA resource name")?;
if *limit > u64::from(u32::MAX) {
return Err(crate::Error::Protocol(format!(
"SETQUOTA resource limit {limit} for \"{resource}\" exceeds u32::MAX \
(RFC 2087 Section 4.1: number is constrained to 32 bits per RFC 3501 Section 9)"
)));
}
}
buf.extend_from_slice(tag.as_bytes());
buf.extend_from_slice(b" SETQUOTA ");
encode_quoted_or_literal_utf8(buf, root.as_bytes(), utf8, literal_mode);
buf.extend_from_slice(b" (");
for (i, (resource, limit)) in resources.iter().enumerate() {
if i > 0 {
buf.extend_from_slice(b" ");
}
buf.extend_from_slice(resource.as_bytes());
buf.extend_from_slice(b" ");
buf.extend_from_slice(limit.to_string().as_bytes());
}
buf.extend_from_slice(b")\r\n");
Ok(())
}
pub(super) fn encode_set_acl(
buf: &mut BytesMut,
tag: &str,
mailbox: &str,
identifier: &str,
rights: &str,
utf8: bool,
literal_mode: LiteralMode,
) {
buf.extend_from_slice(tag.as_bytes());
buf.extend_from_slice(b" SETACL ");
encode_quoted_or_literal_utf8(buf, mailbox.as_bytes(), utf8, literal_mode);
buf.extend_from_slice(b" ");
encode_quoted_or_literal_utf8(buf, identifier.as_bytes(), utf8, literal_mode);
buf.extend_from_slice(b" ");
encode_quoted_or_literal_utf8(buf, rights.as_bytes(), utf8, literal_mode);
buf.extend_from_slice(b"\r\n");
}
#[cfg(test)]
#[allow(clippy::too_many_arguments)]
pub(crate) fn encode_multi_append_header(
buf: &mut BytesMut,
tag: &str,
mailbox: &str,
flags: &[crate::types::Flag],
date: Option<&str>,
message_len: usize,
first: bool,
literal_mode: LiteralMode,
utf8: bool,
) -> Result<(), crate::Error> {
encode_multi_append_header_with_literal8(
buf,
tag,
mailbox,
flags,
date,
message_len,
first,
literal_mode,
utf8,
utf8,
)
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn encode_multi_append_header_with_literal8(
buf: &mut BytesMut,
tag: &str,
mailbox: &str,
flags: &[crate::types::Flag],
date: Option<&str>,
message_len: usize,
first: bool,
literal_mode: LiteralMode,
utf8: bool,
literal8: bool,
) -> Result<(), crate::Error> {
if first {
buf.extend_from_slice(tag.as_bytes());
buf.extend_from_slice(b" APPEND ");
encode_quoted_or_literal_utf8(buf, mailbox.as_bytes(), utf8, literal_mode);
}
let valid_flags = validate_and_filter_flags(flags, "APPEND")?;
if !valid_flags.is_empty() {
buf.extend_from_slice(b" (");
for (i, flag) in valid_flags.iter().enumerate() {
if i > 0 {
buf.extend_from_slice(b" ");
}
buf.extend_from_slice(flag.as_imap_str().as_bytes());
}
buf.extend_from_slice(b")");
}
if let Some(d) = date {
validate_append_datetime(d)?;
buf.extend_from_slice(b" ");
encode_quoted_or_literal(buf, d.as_bytes(), literal_mode);
}
if utf8 {
buf.extend_from_slice(b" UTF8 (~{");
} else if literal8 {
buf.extend_from_slice(b" ~{");
} else {
buf.extend_from_slice(b" {");
}
buf.extend_from_slice(message_len.to_string().as_bytes());
let use_non_sync = !utf8
&& !literal8
&& match literal_mode {
LiteralMode::LiteralPlus => true,
LiteralMode::LiteralMinus => message_len <= LITERAL_MINUS_MAX,
LiteralMode::Synchronizing => false,
};
if use_non_sync {
buf.extend_from_slice(b"+");
}
buf.extend_from_slice(b"}\r\n");
Ok(())
}
pub(super) fn encode_notify_set(
buf: &mut BytesMut,
tag: &str,
params: &NotifySetParams,
utf8: bool,
literal_mode: LiteralMode,
) -> Result<(), crate::Error> {
if params.event_groups.is_empty() {
return Err(crate::Error::Protocol(
"NOTIFY SET requires at least one event group (RFC 5465 Section 8)".into(),
));
}
{
let selected_count = params
.event_groups
.iter()
.filter(|g| {
matches!(
g.filter,
MailboxFilter::Selected | MailboxFilter::SelectedDelayed
)
})
.count();
if selected_count > 1 {
return Err(crate::Error::Protocol(
"NOTIFY SET must not contain more than one event group with a \
selected or selected-delayed filter (RFC 5465 Section 3)"
.into(),
));
}
}
for group in ¶ms.event_groups {
validate_notify_event_group(group)?;
}
buf.extend_from_slice(tag.as_bytes());
buf.extend_from_slice(b" NOTIFY SET");
if params.status {
buf.extend_from_slice(b" STATUS");
}
for group in ¶ms.event_groups {
buf.extend_from_slice(b" (");
encode_mailbox_filter(buf, &group.filter, utf8, literal_mode)?;
buf.extend_from_slice(b" ");
encode_events(buf, &group.events)?;
buf.extend_from_slice(b")");
}
buf.extend_from_slice(b"\r\n");
Ok(())
}
fn validate_notify_event_group(
group: &crate::types::notify::NotifyEventGroup,
) -> Result<(), crate::Error> {
let is_selected_filter = matches!(
group.filter,
MailboxFilter::Selected | MailboxFilter::SelectedDelayed
);
if is_selected_filter {
for event in &group.events {
if !is_message_event(event) {
return Err(crate::Error::Protocol(format!(
"selected/selected-delayed filters only accept message events \
(MessageNew, MessageExpunge, FlagChange, AnnotationChange), \
got {event:?} (RFC 5465 Section 6.1)"
)));
}
}
}
if !is_selected_filter {
for event in &group.events {
if let NotifyEvent::MessageNew { fetch_attrs } = event {
if !fetch_attrs.is_empty() {
return Err(crate::Error::Protocol(
"MessageNew fetch attributes are only valid with \
selected/selected-delayed filters (RFC 5465 Section 8)"
.into(),
));
}
}
}
}
let has_new = group
.events
.iter()
.any(|e| matches!(e, NotifyEvent::MessageNew { .. }));
let has_expunge = group
.events
.iter()
.any(|e| matches!(e, NotifyEvent::MessageExpunge));
let has_flag_change = group
.events
.iter()
.any(|e| matches!(e, NotifyEvent::FlagChange));
let has_annotation_change = group
.events
.iter()
.any(|e| matches!(e, NotifyEvent::AnnotationChange));
if has_new && !has_expunge {
return Err(crate::Error::Protocol(
"MessageNew requires MessageExpunge to also be specified \
(RFC 5465 Section 5)"
.into(),
));
}
if has_expunge && !has_new {
return Err(crate::Error::Protocol(
"MessageExpunge requires MessageNew to also be specified \
(RFC 5465 Section 5)"
.into(),
));
}
if has_flag_change && (!has_new || !has_expunge) {
return Err(crate::Error::Protocol(
"FlagChange requires both MessageNew and MessageExpunge to also \
be specified (RFC 5465 Section 5)"
.into(),
));
}
if has_annotation_change && (!has_new || !has_expunge) {
return Err(crate::Error::Protocol(
"AnnotationChange requires both MessageNew and MessageExpunge to \
also be specified (RFC 5465 Section 5)"
.into(),
));
}
Ok(())
}
fn is_message_event(event: &NotifyEvent) -> bool {
matches!(
event,
NotifyEvent::MessageNew { .. }
| NotifyEvent::MessageExpunge
| NotifyEvent::FlagChange
| NotifyEvent::AnnotationChange
)
}
fn encode_mailbox_filter(
buf: &mut BytesMut,
filter: &MailboxFilter,
utf8: bool,
literal_mode: LiteralMode,
) -> Result<(), crate::Error> {
match filter {
MailboxFilter::Selected => buf.extend_from_slice(b"selected"),
MailboxFilter::SelectedDelayed => buf.extend_from_slice(b"selected-delayed"),
MailboxFilter::Inboxes => buf.extend_from_slice(b"inboxes"),
MailboxFilter::Personal => buf.extend_from_slice(b"personal"),
MailboxFilter::Subscribed => buf.extend_from_slice(b"subscribed"),
MailboxFilter::Subtree(mailboxes) => {
if mailboxes.is_empty() {
return Err(crate::Error::Protocol(
"subtree filter requires at least one mailbox (RFC 5465 Section 8)".into(),
));
}
buf.extend_from_slice(b"subtree");
encode_one_or_more_mailbox(buf, mailboxes, utf8, literal_mode);
}
MailboxFilter::Mailboxes(mailboxes) => {
if mailboxes.is_empty() {
return Err(crate::Error::Protocol(
"mailboxes filter requires at least one mailbox (RFC 5465 Section 8)".into(),
));
}
buf.extend_from_slice(b"mailboxes");
encode_one_or_more_mailbox(buf, mailboxes, utf8, literal_mode);
}
}
Ok(())
}
fn encode_one_or_more_mailbox(
buf: &mut BytesMut,
mailboxes: &[String],
utf8: bool,
literal_mode: LiteralMode,
) {
if mailboxes.len() == 1 {
let wire = encode_mailbox_str(&mailboxes[0], utf8);
buf.extend_from_slice(b" ");
encode_quoted_or_literal_utf8(buf, wire.as_bytes(), utf8, literal_mode);
} else {
buf.extend_from_slice(b" (");
for (i, mbox) in mailboxes.iter().enumerate() {
if i > 0 {
buf.extend_from_slice(b" ");
}
let wire = encode_mailbox_str(mbox, utf8);
encode_quoted_or_literal_utf8(buf, wire.as_bytes(), utf8, literal_mode);
}
buf.extend_from_slice(b")");
}
}
fn encode_events(buf: &mut BytesMut, events: &[NotifyEvent]) -> Result<(), crate::Error> {
if events.is_empty() {
buf.extend_from_slice(b"NONE");
return Ok(());
}
buf.extend_from_slice(b"(");
for (i, event) in events.iter().enumerate() {
if i > 0 {
buf.extend_from_slice(b" ");
}
encode_single_event(buf, event)?;
}
buf.extend_from_slice(b")");
Ok(())
}
fn encode_single_event(buf: &mut BytesMut, event: &NotifyEvent) -> Result<(), crate::Error> {
match event {
NotifyEvent::MessageNew { fetch_attrs } => {
buf.extend_from_slice(b"MessageNew");
if !fetch_attrs.is_empty() {
buf.extend_from_slice(b" (");
for (i, attr) in fetch_attrs.iter().enumerate() {
if i > 0 {
buf.extend_from_slice(b" ");
}
validate_no_crlf(attr, "NOTIFY fetch-att")?;
validate_single_fetch_att(attr)?;
buf.extend_from_slice(attr.as_bytes());
}
buf.extend_from_slice(b")");
}
}
NotifyEvent::MessageExpunge => buf.extend_from_slice(b"MessageExpunge"),
NotifyEvent::FlagChange => buf.extend_from_slice(b"FlagChange"),
NotifyEvent::AnnotationChange => buf.extend_from_slice(b"AnnotationChange"),
NotifyEvent::MailboxName => buf.extend_from_slice(b"MailboxName"),
NotifyEvent::SubscriptionChange => buf.extend_from_slice(b"SubscriptionChange"),
NotifyEvent::MailboxMetadataChange => buf.extend_from_slice(b"MailboxMetadataChange"),
NotifyEvent::ServerMetadataChange => buf.extend_from_slice(b"ServerMetadataChange"),
NotifyEvent::Other(name) => {
validate_atom(name, "NOTIFY event-ext")?;
buf.extend_from_slice(name.as_bytes());
}
}
Ok(())
}