use std::{
borrow::Cow,
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 thiserror::Error;
#[cfg(feature = "ext_compress")]
use crate::extensions::compress::CompressionAlgorithm;
#[cfg(feature = "ext_enable")]
use crate::extensions::enable::CapabilityEnable;
#[cfg(feature = "ext_literal")]
use crate::extensions::literal::LiteralCapability;
use crate::{
auth::AuthMechanism,
core::{impl_try_from, Atom, Charset, NonEmptyVec, QuotedChar, Tag, Text, TextError},
fetch::MessageDataItem,
flag::{Flag, FlagNameAttribute, FlagPerm},
mailbox::Mailbox,
status::StatusDataItem,
};
#[cfg(feature = "ext_quota")]
use crate::{
core::AString,
extensions::quota::{QuotaGet, Resource},
};
#[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, TextError> {
Ok(Greeting {
kind,
code,
text: text.try_into()?,
})
}
pub fn ok(code: Option<Code<'a>>, text: &'a str) -> Result<Self, TextError> {
Ok(Greeting {
kind: GreetingKind::Ok,
code,
text: text.try_into()?,
})
}
pub fn preauth(code: Option<Code<'a>>, text: &'a str) -> Result<Self, TextError> {
Ok(Greeting {
kind: GreetingKind::PreAuth,
code,
text: text.try_into()?,
})
}
pub fn bye(code: Option<Code<'a>>, text: &'a str) -> Result<Self, TextError> {
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> {
Status(Status<'a>),
Data(Data<'a>),
Continue(Continue<'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>>,
},
#[cfg(feature = "ext_enable")]
#[cfg_attr(docsrs, doc(cfg(feature = "ext_enable")))]
Enabled {
capabilities: Vec<CapabilityEnable<'a>>,
},
#[cfg(feature = "ext_quota")]
#[cfg_attr(docsrs, doc(cfg(feature = "ext_quota")))]
Quota {
root: AString<'a>,
quotas: NonEmptyVec<QuotaGet<'a>>,
},
#[cfg(feature = "ext_quota")]
#[cfg_attr(docsrs, doc(cfg(feature = "ext_quota")))]
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 })
}
}
#[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_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 = "Continuation")]
#[doc(alias = "ContinuationRequest")]
#[doc(alias = "CommandContinuationRequest")]
pub enum Continue<'a> {
Basic(ContinueBasic<'a>),
Base64(Cow<'a, [u8]>),
}
impl<'a> Continue<'a> {
pub fn basic<T>(code: Option<Code<'a>>, text: T) -> Result<Self, ContinueError<T::Error>>
where
T: TryInto<Text<'a>>,
{
Ok(Continue::Basic(ContinueBasic::new(code, text)?))
}
pub fn base64<'data: 'a, D>(data: D) -> Self
where
D: Into<Cow<'data, [u8]>>,
{
Continue::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 ContinueBasic<'a> {
code: Option<Code<'a>>,
text: Text<'a>,
}
impl<'a> ContinueBasic<'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() && _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
}
}
#[derive(Clone, Debug, Eq, Error, Hash, Ord, PartialEq, PartialOrd)]
pub enum ContinueError<T> {
#[error("invalid text")]
Text(T),
#[error("using no code and valid base64 may result in ambiguities")]
Ambiguity,
}
#[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>),
#[cfg(feature = "ext_compress")]
#[cfg_attr(docsrs, doc(cfg(feature = "ext_compress")))]
CompressionActive,
#[cfg(feature = "ext_quota")]
#[cfg_attr(docsrs, doc(cfg(feature = "ext_quota")))]
OverQuota,
#[cfg(feature = "ext_literal")]
#[cfg_attr(docsrs, doc(cfg(feature = "ext_literal")))]
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(Debug, Clone, PartialEq, Eq, Hash)]
pub struct CodeOther<'a>(Cow<'a, [u8]>);
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)]
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,
#[cfg(feature = "ext_idle")]
#[cfg_attr(docsrs, doc(cfg(feature = "ext_idle")))]
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,
#[cfg(feature = "ext_sasl_ir")]
#[cfg_attr(docsrs, doc(cfg(feature = "ext_sasl_ir")))]
SaslIr,
#[cfg(feature = "ext_enable")]
#[cfg_attr(docsrs, doc(cfg(feature = "ext_enable")))]
Enable,
#[cfg(feature = "ext_compress")]
#[cfg_attr(docsrs, doc(cfg(feature = "ext_compress")))]
Compress {
algorithm: CompressionAlgorithm,
},
#[cfg(feature = "ext_quota")]
#[cfg_attr(docsrs, doc(cfg(feature = "ext_quota")))]
Quota,
#[cfg(feature = "ext_quota")]
#[cfg_attr(docsrs, doc(cfg(feature = "ext_quota")))]
QuotaRes(Resource<'a>),
#[cfg(feature = "ext_quota")]
#[cfg_attr(docsrs, doc(cfg(feature = "ext_quota")))]
QuotaSet,
#[cfg(feature = "ext_literal")]
#[cfg_attr(docsrs, doc(cfg(feature = "ext_literal")))]
Literal(LiteralCapability),
#[cfg(feature = "ext_move")]
#[cfg_attr(docsrs, doc(cfg(feature = "ext_move")))]
Move,
Other(CapabilityOther<'a>),
}
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,
#[cfg(feature = "ext_idle")]
"idle" => Self::Idle,
#[cfg(feature = "ext_mailbox_referrals")]
"mailbox-referrals" => Self::MailboxReferrals,
#[cfg(feature = "ext_login_referrals")]
"login-referrals" => Self::LoginReferrals,
#[cfg(feature = "ext_sasl_ir")]
"sasl-ir" => Self::SaslIr,
#[cfg(feature = "ext_enable")]
"enable" => Self::Enable,
#[cfg(feature = "ext_quota")]
"quota" => Self::Quota,
#[cfg(feature = "ext_quota")]
"quotaset" => Self::QuotaSet,
#[cfg(feature = "ext_literal")]
"literal+" => Self::Literal(LiteralCapability::Plus),
#[cfg(feature = "ext_literal")]
"literal-" => Self::Literal(LiteralCapability::Minus),
#[cfg(feature = "ext_move")]
"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);
}
}
#[cfg(feature = "ext_compress")]
"compress" => {
if let Ok(atom) = Atom::try_from(right) {
if let Ok(algorithm) = CompressionAlgorithm::try_from(atom) {
return Self::Compress { algorithm };
}
}
}
#[cfg(feature = "ext_quota")]
"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>(pub(crate) Atom<'a>);
impl<'a> CapabilityOther<'a> {
#[cfg(feature = "unvalidated")]
#[cfg_attr(docsrs, doc(cfg(feature = "unvalidated")))]
pub fn unvalidated<C>(inner: C) -> Self
where
C: Into<Cow<'a, str>>,
{
Self(Atom::unvalidated(inner))
}
pub fn inner(&self) -> &Atom<'a> {
&self.0
}
}
#[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 = [
Continue::basic(None, ""),
Continue::basic(Some(Code::ReadWrite), ""),
];
for test in tests {
println!("{:?}", test);
assert!(test.is_err());
}
}
}