use std::{
borrow::Cow,
fmt::{Debug, Display, Formatter},
num::{NonZeroU32, TryFromIntError},
};
#[cfg(feature = "arbitrary")]
use arbitrary::Arbitrary;
use base64::{engine::general_purpose::STANDARD as _base64, Engine};
#[cfg(feature = "bounded-static")]
use bounded_static::ToStatic;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use crate::{
auth::AuthMechanism,
core::{impl_try_from, AString, Atom, Charset, NonEmptyVec, QuotedChar, Tag, Text},
error::ValidationError,
extensions::{
compress::CompressionAlgorithm,
enable::CapabilityEnable,
quota::{QuotaGet, Resource},
},
fetch::MessageDataItem,
flag::{Flag, FlagNameAttribute, FlagPerm},
mailbox::Mailbox,
response::error::{ContinueError, FetchError},
status::StatusDataItem,
};
#[cfg_attr(feature = "bounded-static", derive(ToStatic))]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Greeting<'a> {
pub kind: GreetingKind,
pub code: Option<Code<'a>>,
pub text: Text<'a>,
}
impl<'a> Greeting<'a> {
pub fn new(
kind: GreetingKind,
code: Option<Code<'a>>,
text: &'a str,
) -> Result<Self, ValidationError> {
Ok(Greeting {
kind,
code,
text: text.try_into()?,
})
}
pub fn ok(code: Option<Code<'a>>, text: &'a str) -> Result<Self, ValidationError> {
Ok(Greeting {
kind: GreetingKind::Ok,
code,
text: text.try_into()?,
})
}
pub fn preauth(code: Option<Code<'a>>, text: &'a str) -> Result<Self, ValidationError> {
Ok(Greeting {
kind: GreetingKind::PreAuth,
code,
text: text.try_into()?,
})
}
pub fn bye(code: Option<Code<'a>>, text: &'a str) -> Result<Self, ValidationError> {
Ok(Greeting {
kind: GreetingKind::Bye,
code,
text: text.try_into()?,
})
}
}
#[cfg_attr(feature = "arbitrary", derive(Arbitrary))]
#[cfg_attr(feature = "bounded-static", derive(ToStatic))]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum GreetingKind {
Ok,
PreAuth,
Bye,
}
#[cfg_attr(feature = "arbitrary", derive(Arbitrary))]
#[cfg_attr(feature = "bounded-static", derive(ToStatic))]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Response<'a> {
CommandContinuationRequest(CommandContinuationRequest<'a>),
Data(Data<'a>),
Status(Status<'a>),
}
#[cfg_attr(feature = "bounded-static", derive(ToStatic))]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Status<'a> {
Ok {
tag: Option<Tag<'a>>,
code: Option<Code<'a>>,
text: Text<'a>,
},
No {
tag: Option<Tag<'a>>,
code: Option<Code<'a>>,
text: Text<'a>,
},
Bad {
tag: Option<Tag<'a>>,
code: Option<Code<'a>>,
text: Text<'a>,
},
Bye {
code: Option<Code<'a>>,
text: Text<'a>,
},
}
impl<'a> Status<'a> {
pub fn ok<T>(tag: Option<Tag<'a>>, code: Option<Code<'a>>, text: T) -> Result<Self, T::Error>
where
T: TryInto<Text<'a>>,
{
Ok(Status::Ok {
tag,
code,
text: text.try_into()?,
})
}
pub fn no<T>(tag: Option<Tag<'a>>, code: Option<Code<'a>>, text: T) -> Result<Self, T::Error>
where
T: TryInto<Text<'a>>,
{
Ok(Status::No {
tag,
code,
text: text.try_into()?,
})
}
pub fn bad<T>(tag: Option<Tag<'a>>, code: Option<Code<'a>>, text: T) -> Result<Self, T::Error>
where
T: TryInto<Text<'a>>,
{
Ok(Status::Bad {
tag,
code,
text: text.try_into()?,
})
}
pub fn bye<T>(code: Option<Code<'a>>, text: T) -> Result<Self, T::Error>
where
T: TryInto<Text<'a>>,
{
Ok(Status::Bye {
code,
text: text.try_into()?,
})
}
pub fn tag(&self) -> Option<&Tag> {
match self {
Status::Ok { tag, .. } | Status::No { tag, .. } | Status::Bad { tag, .. } => {
tag.as_ref()
}
Status::Bye { .. } => None,
}
}
pub fn code(&self) -> Option<&Code> {
match self {
Status::Ok { code, .. }
| Status::No { code, .. }
| Status::Bad { code, .. }
| Status::Bye { code, .. } => code.as_ref(),
}
}
pub fn text(&self) -> &Text {
match self {
Status::Ok { text, .. }
| Status::No { text, .. }
| Status::Bad { text, .. }
| Status::Bye { text, .. } => text,
}
}
}
#[cfg_attr(feature = "arbitrary", derive(Arbitrary))]
#[cfg_attr(feature = "bounded-static", derive(ToStatic))]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Data<'a> {
Capability(NonEmptyVec<Capability<'a>>),
List {
items: Vec<FlagNameAttribute<'a>>,
delimiter: Option<QuotedChar>,
mailbox: Mailbox<'a>,
},
Lsub {
items: Vec<FlagNameAttribute<'a>>,
delimiter: Option<QuotedChar>,
mailbox: Mailbox<'a>,
},
Status {
mailbox: Mailbox<'a>,
items: Cow<'a, [StatusDataItem]>,
},
Search(Vec<NonZeroU32>),
Flags(Vec<Flag<'a>>),
Exists(u32),
Recent(u32),
Expunge(NonZeroU32),
Fetch {
seq: NonZeroU32,
items: NonEmptyVec<MessageDataItem<'a>>,
},
Enabled {
capabilities: Vec<CapabilityEnable<'a>>,
},
Quota {
root: AString<'a>,
quotas: NonEmptyVec<QuotaGet<'a>>,
},
QuotaRoot {
mailbox: Mailbox<'a>,
roots: Vec<AString<'a>>,
},
}
impl<'a> Data<'a> {
pub fn capability<C>(caps: C) -> Result<Self, C::Error>
where
C: TryInto<NonEmptyVec<Capability<'a>>>,
{
Ok(Self::Capability(caps.try_into()?))
}
pub fn expunge(seq: u32) -> Result<Self, TryFromIntError> {
Ok(Self::Expunge(NonZeroU32::try_from(seq)?))
}
pub fn fetch<S, I>(seq: S, items: I) -> Result<Self, FetchError<S::Error, I::Error>>
where
S: TryInto<NonZeroU32>,
I: TryInto<NonEmptyVec<MessageDataItem<'a>>>,
{
let seq = seq.try_into().map_err(FetchError::SeqOrUid)?;
let items = items.try_into().map_err(FetchError::InvalidItems)?;
Ok(Self::Fetch { seq, items })
}
}
#[cfg_attr(feature = "arbitrary", derive(Arbitrary))]
#[cfg_attr(feature = "bounded-static", derive(ToStatic))]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[doc(alias = "Continue")]
#[doc(alias = "Continuation")]
#[doc(alias = "ContinuationRequest")]
pub enum CommandContinuationRequest<'a> {
Basic(CommandContinuationRequestBasic<'a>),
Base64(Cow<'a, [u8]>),
}
impl<'a> CommandContinuationRequest<'a> {
pub fn basic<T>(code: Option<Code<'a>>, text: T) -> Result<Self, ContinueError<T::Error>>
where
T: TryInto<Text<'a>>,
{
Ok(Self::Basic(CommandContinuationRequestBasic::new(
code, text,
)?))
}
pub fn base64<'data: 'a, D>(data: D) -> Self
where
D: Into<Cow<'data, [u8]>>,
{
Self::Base64(data.into())
}
}
#[cfg_attr(feature = "bounded-static", derive(ToStatic))]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct CommandContinuationRequestBasic<'a> {
code: Option<Code<'a>>,
text: Text<'a>,
}
impl<'a> CommandContinuationRequestBasic<'a> {
pub fn new<T>(code: Option<Code<'a>>, text: T) -> Result<Self, ContinueError<T::Error>>
where
T: TryInto<Text<'a>>,
{
let text = text.try_into().map_err(ContinueError::Text)?;
if code.is_none() && text.as_ref().starts_with('[') {
return Err(ContinueError::Ambiguity);
}
if code.is_none() && _base64.decode(text.inner()).is_ok() {
return Err(ContinueError::Ambiguity);
}
Ok(Self { code, text })
}
pub fn code(&self) -> Option<&Code<'a>> {
self.code.as_ref()
}
pub fn text(&self) -> &Text<'a> {
&self.text
}
}
#[cfg_attr(feature = "arbitrary", derive(Arbitrary))]
#[cfg_attr(feature = "bounded-static", derive(ToStatic))]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Code<'a> {
Alert,
BadCharset {
allowed: Vec<Charset<'a>>,
},
Capability(NonEmptyVec<Capability<'a>>),
Parse,
PermanentFlags(Vec<FlagPerm<'a>>),
ReadOnly,
ReadWrite,
TryCreate,
UidNext(NonZeroU32),
UidValidity(NonZeroU32),
Unseen(NonZeroU32),
#[cfg(any(feature = "ext_mailbox_referrals", feature = "ext_login_referrals"))]
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "ext_mailbox_referrals", feature = "ext_login_referrals")))
)]
Referral(Cow<'a, str>),
CompressionActive,
OverQuota,
TooBig,
Other(CodeOther<'a>),
}
impl<'a> Code<'a> {
pub fn badcharset(allowed: Vec<Charset<'a>>) -> Self {
Self::BadCharset { allowed }
}
pub fn capability<C>(caps: C) -> Result<Self, C::Error>
where
C: TryInto<NonEmptyVec<Capability<'a>>>,
{
Ok(Self::Capability(caps.try_into()?))
}
pub fn permanentflags(flags: Vec<FlagPerm<'a>>) -> Self {
Self::PermanentFlags(flags)
}
pub fn uidnext(uidnext: u32) -> Result<Self, TryFromIntError> {
Ok(Self::UidNext(NonZeroU32::try_from(uidnext)?))
}
pub fn uidvalidity(uidnext: u32) -> Result<Self, TryFromIntError> {
Ok(Self::UidValidity(NonZeroU32::try_from(uidnext)?))
}
pub fn unseen(uidnext: u32) -> Result<Self, TryFromIntError> {
Ok(Self::Unseen(NonZeroU32::try_from(uidnext)?))
}
}
#[cfg_attr(feature = "bounded-static", derive(ToStatic))]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Clone, PartialEq, Eq, Hash)]
pub struct CodeOther<'a>(Cow<'a, [u8]>);
impl<'a> Debug for CodeOther<'a> {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
struct BStr<'a>(&'a Cow<'a, [u8]>);
impl<'a> Debug for BStr<'a> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"b\"{}\"",
crate::utils::escape_byte_string(self.0.as_ref())
)
}
}
f.debug_tuple("CodeOther").field(&BStr(&self.0)).finish()
}
}
impl<'a> CodeOther<'a> {
#[cfg(feature = "unvalidated")]
#[cfg_attr(docsrs, doc(cfg(feature = "unvalidated")))]
pub fn unvalidated<D: 'a>(data: D) -> Self
where
D: Into<Cow<'a, [u8]>>,
{
Self(data.into())
}
pub fn inner(&self) -> &[u8] {
self.0.as_ref()
}
}
#[cfg_attr(feature = "bounded-static", derive(ToStatic))]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Capability<'a> {
Imap4Rev1,
Auth(AuthMechanism<'a>),
#[cfg(feature = "starttls")]
#[cfg_attr(docsrs, doc(cfg(feature = "starttls")))]
LoginDisabled,
#[cfg(feature = "starttls")]
#[cfg_attr(docsrs, doc(cfg(feature = "starttls")))]
StartTls,
Idle,
#[cfg(feature = "ext_mailbox_referrals")]
#[cfg_attr(docsrs, doc(cfg(feature = "ext_mailbox_referrals")))]
MailboxReferrals,
#[cfg(feature = "ext_login_referrals")]
#[cfg_attr(docsrs, doc(cfg(feature = "ext_login_referrals")))]
LoginReferrals,
SaslIr,
Enable,
Compress {
algorithm: CompressionAlgorithm,
},
Quota,
QuotaRes(Resource<'a>),
QuotaSet,
LiteralPlus,
LiteralMinus,
Move,
Other(CapabilityOther<'a>),
}
impl<'a> Display for Capability<'a> {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
match self {
Self::Imap4Rev1 => write!(f, "IMAP4REV1"),
Self::Auth(mechanism) => write!(f, "AUTH={}", mechanism),
#[cfg(feature = "starttls")]
Self::LoginDisabled => write!(f, "LOGINDISABLED"),
#[cfg(feature = "starttls")]
Self::StartTls => write!(f, "STARTTLS"),
#[cfg(feature = "ext_mailbox_referrals")]
Self::MailboxReferrals => write!(f, "MAILBOX-REFERRALS"),
#[cfg(feature = "ext_login_referrals")]
Self::LoginReferrals => write!(f, "LOGIN-REFERRALS"),
Self::SaslIr => write!(f, "SASL-IR"),
Self::Idle => write!(f, "IDLE"),
Self::Enable => write!(f, "ENABLE"),
Self::Compress { algorithm } => write!(f, "COMPRESS={}", algorithm),
Self::Quota => write!(f, "QUOTA"),
Self::QuotaRes(resource) => write!(f, "QUOTA=RES-{}", resource),
Self::QuotaSet => write!(f, "QUOTASET"),
Self::LiteralPlus => write!(f, "LITERAL+"),
Self::LiteralMinus => write!(f, "LITERAL-"),
Self::Move => write!(f, "MOVE"),
Self::Other(other) => write!(f, "{}", other.0),
}
}
}
impl_try_from!(Atom<'a>, 'a, &'a [u8], Capability<'a>);
impl_try_from!(Atom<'a>, 'a, Vec<u8>, Capability<'a>);
impl_try_from!(Atom<'a>, 'a, &'a str, Capability<'a>);
impl_try_from!(Atom<'a>, 'a, String, Capability<'a>);
impl<'a> From<Atom<'a>> for Capability<'a> {
fn from(atom: Atom<'a>) -> Self {
fn split_once_cow<'a>(
cow: Cow<'a, str>,
pattern: &str,
) -> Option<(Cow<'a, str>, Cow<'a, str>)> {
match cow {
Cow::Borrowed(str) => {
if let Some((left, right)) = str.split_once(pattern) {
return Some((Cow::Borrowed(left), Cow::Borrowed(right)));
}
None
}
Cow::Owned(string) => {
if let Some((left, right)) = string.split_once(pattern) {
return Some((Cow::Owned(left.to_owned()), Cow::Owned(right.to_owned())));
}
None
}
}
}
let cow = atom.into_inner();
match cow.to_ascii_lowercase().as_ref() {
"imap4rev1" => Self::Imap4Rev1,
#[cfg(feature = "starttls")]
"logindisabled" => Self::LoginDisabled,
#[cfg(feature = "starttls")]
"starttls" => Self::StartTls,
"idle" => Self::Idle,
#[cfg(feature = "ext_mailbox_referrals")]
"mailbox-referrals" => Self::MailboxReferrals,
#[cfg(feature = "ext_login_referrals")]
"login-referrals" => Self::LoginReferrals,
"sasl-ir" => Self::SaslIr,
"enable" => Self::Enable,
"quota" => Self::Quota,
"quotaset" => Self::QuotaSet,
"literal+" => Self::LiteralPlus,
"literal-" => Self::LiteralMinus,
"move" => Self::Move,
_ => {
if let Some((left, right)) = split_once_cow(cow.clone(), "=") {
match left.as_ref().to_ascii_lowercase().as_ref() {
"auth" => {
if let Ok(mechanism) = AuthMechanism::try_from(right) {
return Self::Auth(mechanism);
}
}
"compress" => {
if let Ok(atom) = Atom::try_from(right) {
if let Ok(algorithm) = CompressionAlgorithm::try_from(atom) {
return Self::Compress { algorithm };
}
}
}
"quota" => {
if let Some((_, right)) =
right.as_ref().to_ascii_lowercase().split_once("res-")
{
if let Ok(resource) = Resource::try_from(right.to_owned()) {
return Self::QuotaRes(resource);
}
}
}
_ => {}
}
}
Self::Other(CapabilityOther(Atom(cow)))
}
}
}
}
#[cfg_attr(feature = "bounded-static", derive(ToStatic))]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct CapabilityOther<'a>(Atom<'a>);
pub mod error {
use thiserror::Error;
#[derive(Clone, Debug, Eq, Error, Hash, Ord, PartialEq, PartialOrd)]
pub enum ContinueError<T> {
#[error("invalid text")]
Text(T),
#[error("ambiguity detected")]
Ambiguity,
}
#[derive(Clone, Debug, Eq, Error, Hash, Ord, PartialEq, PartialOrd)]
pub enum FetchError<S, I> {
#[error("Invalid sequence or UID: {0:?}")]
SeqOrUid(S),
#[error("Invalid items: {0:?}")]
InvalidItems(I),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_conversion_data() {
let _ = Data::capability(vec![Capability::Imap4Rev1]).unwrap();
let _ = Data::fetch(1, vec![MessageDataItem::Rfc822Size(123)]).unwrap();
}
#[test]
fn test_conversion_continue_failing() {
let tests = [
CommandContinuationRequest::basic(None, ""),
CommandContinuationRequest::basic(Some(Code::ReadWrite), ""),
];
for test in tests {
println!("{:?}", test);
assert!(test.is_err());
}
}
}