use std::fmt;
pub use daaki_message::ValidationError;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(try_from = "String", into = "String"))]
pub struct SequenceSet(String);
impl SequenceSet {
pub fn into_inner(self) -> String {
self.0
}
pub fn new(s: impl Into<String>) -> Result<Self, ValidationError> {
let s = s.into();
if !is_valid_sequence_set(&s) {
return Err(ValidationError::new(format!(
"invalid sequence set per RFC 3501 Section 9: {s:?}"
)));
}
Ok(Self(s))
}
pub fn new_known(s: impl Into<String>) -> Result<Self, ValidationError> {
let s = s.into();
if s.contains('*') {
return Err(ValidationError::new(
"\"*\" is not allowed in QRESYNC known-uids/sequence-set \
(RFC 7162 Section 3.2.5.2)",
));
}
if s.contains('$') {
return Err(ValidationError::new(
"\"$\" (search result reference) is not allowed in QRESYNC \
known-uids/sequence-set (RFC 7162 Section 7)",
));
}
if !is_valid_sequence_set(&s) {
return Err(ValidationError::new(
"QRESYNC known-uids/sequence-set is not a valid sequence-set \
(RFC 7162 Section 7: known-uids = sequence-set)",
));
}
Ok(Self(s))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl AsRef<str> for SequenceSet {
fn as_ref(&self) -> &str {
&self.0
}
}
impl fmt::Display for SequenceSet {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl TryFrom<String> for SequenceSet {
type Error = ValidationError;
fn try_from(s: String) -> Result<Self, Self::Error> {
Self::new(s)
}
}
impl TryFrom<&str> for SequenceSet {
type Error = ValidationError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
Self::new(s)
}
}
impl From<SequenceSet> for String {
fn from(s: SequenceSet) -> Self {
s.into_inner()
}
}
pub(crate) fn is_valid_sequence_set(s: &str) -> bool {
if s.is_empty() {
return false;
}
s.split(',').all(|part| {
if part.is_empty() {
return false;
}
if part == "$" {
return true;
}
part.split(':').all(|num| {
if num == "*" {
return true;
}
if num.is_empty() || num.starts_with('0') || !num.chars().all(|c| c.is_ascii_digit()) {
return false;
}
num.parse::<u32>().is_ok()
}) && part.matches(':').count() <= 1
})
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(try_from = "String", into = "String"))]
pub struct ImapAtom(String);
impl ImapAtom {
pub fn into_inner(self) -> String {
self.0
}
pub fn new(s: impl Into<String>) -> Result<Self, ValidationError> {
let s = s.into();
validate_atom_bytes(s.as_bytes(), "atom")?;
Ok(Self(s))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl AsRef<str> for ImapAtom {
fn as_ref(&self) -> &str {
&self.0
}
}
impl fmt::Display for ImapAtom {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl TryFrom<String> for ImapAtom {
type Error = ValidationError;
fn try_from(s: String) -> Result<Self, Self::Error> {
Self::new(s)
}
}
impl TryFrom<&str> for ImapAtom {
type Error = ValidationError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
Self::new(s)
}
}
impl From<ImapAtom> for String {
fn from(a: ImapAtom) -> Self {
a.into_inner()
}
}
pub(crate) fn validate_atom_bytes(bytes: &[u8], context: &str) -> Result<(), ValidationError> {
if bytes.is_empty() {
return Err(ValidationError::new(format!(
"{context} must be at least one character \
(RFC 3501 Section 9: atom = 1*ATOM-CHAR)"
)));
}
for &b in bytes {
let is_atom_special = matches!(
b,
b'(' | b')' | b'{' | b' ' | b'%' | b'*' | b'"' | b'\\' | b']'
);
let is_ctl = b < 0x20 || b == 0x7F;
let is_outside_char = b == 0 || b > 0x7F;
if is_atom_special || is_ctl || is_outside_char {
return Err(ValidationError::new(format!(
"{context} contains invalid byte 0x{b:02X} — must be an atom \
(RFC 3501 Section 9: ATOM-CHAR excludes atom-specials, CTL, non-ASCII)"
)));
}
}
Ok(())
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct MailboxName(String);
impl MailboxName {
pub fn new(s: impl Into<String>) -> Result<Self, ValidationError> {
let s = s.into();
if s.bytes().any(|b| matches!(b, b'\0' | b'\r' | b'\n')) {
return Err(ValidationError::new(
"mailbox name must not contain NUL, CR, or LF — IMAP strings \
forbid NUL (RFC 3501 Section 9 / RFC 9051 Section 9) and \
commands are CRLF-delimited (RFC 3501 Section 2.2 / \
RFC 9051 Section 2.2)",
));
}
Ok(Self(s))
}
pub fn as_str(&self) -> &str {
&self.0
}
pub(crate) fn from_decoded(s: String) -> Self {
Self(s)
}
}
impl Default for MailboxName {
fn default() -> Self {
Self(String::new())
}
}
impl AsRef<str> for MailboxName {
fn as_ref(&self) -> &str {
&self.0
}
}
impl fmt::Display for MailboxName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl TryFrom<String> for MailboxName {
type Error = ValidationError;
fn try_from(s: String) -> Result<Self, Self::Error> {
Self::new(s)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(try_from = "String", into = "String"))]
pub struct ObjectId(String);
impl ObjectId {
pub fn into_inner(self) -> String {
self.0
}
pub fn new(s: impl Into<String>) -> Result<Self, ValidationError> {
let s = s.into();
if s.is_empty() {
return Err(ValidationError::new(
"object identifier must not be empty (RFC 8474 Section 4)",
));
}
if s.len() > 255 {
return Err(ValidationError::new(format!(
"object identifier exceeds 255 characters ({} bytes) \
(RFC 8474 Section 4: objectid = 1*255(...))",
s.len()
)));
}
for &b in s.as_bytes() {
let valid = b.is_ascii_alphanumeric() || b == b'-' || b == b'_';
if !valid {
return Err(ValidationError::new(format!(
"object identifier contains invalid byte 0x{b:02X} — \
only ALPHA / DIGIT / \"_\" / \"-\" are allowed \
(RFC 8474 Section 4)"
)));
}
}
Ok(Self(s))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl AsRef<str> for ObjectId {
fn as_ref(&self) -> &str {
&self.0
}
}
impl fmt::Display for ObjectId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl TryFrom<String> for ObjectId {
type Error = ValidationError;
fn try_from(s: String) -> Result<Self, Self::Error> {
Self::new(s)
}
}
impl TryFrom<&str> for ObjectId {
type Error = ValidationError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
Self::new(s)
}
}
impl From<ObjectId> for String {
fn from(o: ObjectId) -> Self {
o.into_inner()
}
}
use crate::types::UidRange;
pub(crate) struct ParsedUidSet(Vec<(u32, u32)>);
impl ParsedUidSet {
pub(crate) fn new(set: &SequenceSet) -> Option<Self> {
let s = set.as_str();
let mut intervals: Vec<(u32, u32)> = Vec::new();
for part in s.split(',') {
if part.contains('$') {
return None;
}
if let Some((left, right)) = part.split_once(':') {
let start = Self::parse_seq_number(left)?;
let end = Self::parse_seq_number(right)?;
let (lo, hi) = if start <= end {
(start, end)
} else {
(end, start)
};
intervals.push((lo, hi));
} else {
let n = Self::parse_seq_number(part)?;
intervals.push((n, n));
}
}
intervals.sort_unstable_by_key(|&(start, _)| start);
let mut merged: Vec<(u32, u32)> = Vec::with_capacity(intervals.len());
for (lo, hi) in intervals {
if let Some(last) = merged.last_mut() {
if lo <= last.1 || lo == last.1.saturating_add(1) {
last.1 = last.1.max(hi);
continue;
}
}
merged.push((lo, hi));
}
Some(Self(merged))
}
fn parse_seq_number(s: &str) -> Option<u32> {
if s == "*" {
Some(u32::MAX)
} else {
s.parse::<u32>().ok()
}
}
pub(crate) fn intersect_uid_ranges(&self, ranges: &[UidRange]) -> (Vec<UidRange>, usize) {
let mut result: Vec<UidRange> = Vec::new();
let mut total_input_uids: u64 = 0;
let mut total_output_uids: u64 = 0;
for range in ranges {
let v_start = range.start;
let v_end = range.end.unwrap_or(range.start);
let (v_lo, v_hi) = if v_start <= v_end {
(v_start, v_end)
} else {
(v_end, v_start)
};
total_input_uids += u64::from(v_hi) - u64::from(v_lo) + 1;
for &(s_lo, s_hi) in &self.0 {
if s_lo > v_hi {
break;
}
if s_hi < v_lo {
continue;
}
let overlap_start = v_lo.max(s_lo);
let overlap_end = v_hi.min(s_hi);
total_output_uids += u64::from(overlap_end) - u64::from(overlap_start) + 1;
if overlap_start == overlap_end {
result.push(UidRange::single(overlap_start));
} else {
result.push(UidRange::range(overlap_start, overlap_end));
}
}
}
let dropped_u64 = total_input_uids - total_output_uids;
let dropped = usize::try_from(dropped_u64).unwrap_or(usize::MAX);
(result, dropped)
}
}
#[cfg(test)]
#[path = "validated_tests.rs"]
mod tests;