use super::{FetchResponse, Flag, MailboxInfo, StatusItem};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Response {
Greeting(GreetingResponse),
Tagged(TaggedResponse),
Untagged(Box<UntaggedResponse>),
Continuation(ContinuationRequest),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GreetingResponse {
pub status: GreetingStatus,
pub code: Option<ResponseCode>,
pub text: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GreetingStatus {
Ok,
PreAuth,
Bye,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TaggedResponse {
pub tag: String,
pub status: StatusKind,
pub code: Option<ResponseCode>,
pub text: String,
}
impl TaggedResponse {
pub(crate) fn require_ok(self) -> Result<Self, crate::error::Error> {
match self.status {
StatusKind::Ok => Ok(self),
StatusKind::No => Err(crate::error::Error::no_with_code(self.text, self.code)),
StatusKind::Bad => Err(crate::error::Error::bad_with_code(self.text, self.code)),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StatusKind {
Ok,
No,
Bad,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UntaggedStatus {
Ok,
No,
Bad,
Bye,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum UntaggedResponse {
Status {
status: UntaggedStatus,
code: Option<ResponseCode>,
text: String,
},
Exists(u32),
Recent(u32),
Expunge(u32),
Fetch(Box<FetchResponse>),
List(MailboxInfo),
Lsub(MailboxInfo),
Flags(Vec<Flag>),
Search {
uids: Vec<u32>,
mod_seq: Option<u64>,
},
Esearch(EsearchResponse),
MailboxStatus {
mailbox: String,
items: Vec<StatusItem>,
},
Capability(Vec<Capability>),
Enabled(Vec<String>),
Vanished { earlier: bool, uids: Vec<UidRange> },
Id(Vec<(String, Option<String>)>),
Namespace {
personal: Vec<NamespaceDescriptor>,
other: Vec<NamespaceDescriptor>,
shared: Vec<NamespaceDescriptor>,
},
Quota {
root: String,
resources: Vec<QuotaResource>,
},
QuotaRoot { mailbox: String, roots: Vec<String> },
Acl {
mailbox: String,
entries: Vec<AclEntry>,
},
MyRights { mailbox: String, rights: String },
ListRights {
mailbox: String,
identifier: String,
required: String,
optional: Vec<String>,
},
Metadata {
mailbox: String,
entries: Vec<MetadataEntry>,
},
Thread(Vec<ThreadNode>),
Sort {
nums: Vec<u32>,
mod_seq: Option<u64>,
},
Unknown(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ContinuationRequest {
pub code: Option<ResponseCode>,
pub data: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ResponseCode {
Alert,
BadCharset(Vec<String>),
Capability(Vec<Capability>),
Parse,
PermanentFlags(Vec<Flag>),
ReadOnly,
ReadWrite,
TryCreate,
UidNext(u32),
UidValidity(u32),
Unseen(u32),
AppendUid {
uid_validity: u32,
uids: Vec<UidRange>,
},
CopyUid {
uid_validity: u32,
source_uids: Vec<UidRange>,
dest_uids: Vec<UidRange>,
},
HighestModSeq(u64),
Modified(Vec<UidRange>),
NoModSeq,
Closed,
MailboxId(String),
Unavailable,
AuthenticationFailed,
AuthorizationFailed,
Expired,
PrivacyRequired,
ContactAdmin,
NoPerm,
InUse,
ExpungeIssued,
Corruption,
ServerBug,
ClientBug,
Cannot,
Limit,
OverQuota,
AlreadyExists,
NonExistent,
UidNotSticky,
NotSaved,
HasChildren,
UnknownCte,
TooBig,
CompressionActive,
UseAttr,
MetadataLongEntries(u64),
MetadataMaxSize(u64),
MetadataTooMany,
MetadataNoPrivate,
Other { name: String, value: Option<String> },
}
#[derive(Debug, Clone)]
pub enum Capability {
Imap4Rev1,
Imap4Rev2,
Acl,
AppendLimit(Option<u64>),
Binary,
Children,
CompressDeflate,
Condstore,
CreateSpecialUse,
Enable,
Esearch,
Id,
Idle,
ListExtended,
ListStatus,
LiteralPlus,
LoginDisabled,
LiteralMinus,
Metadata,
MetadataServer,
Move,
MultiAppend,
Namespace,
ObjectId,
QResync,
Quota,
Rights(String),
Preview,
SaslIr,
SaveDate,
SearchRes,
Sort,
SortDisplay(String),
StartTls,
SpecialUse,
Thread(String),
StatusSize,
UidPlus,
Unauthenticate,
Unselect,
Utf8Accept,
Utf8Only,
Within,
Auth(String),
Other(String),
}
impl Capability {
pub fn as_imap_str(&self) -> String {
match self {
Self::Imap4Rev1 => "IMAP4rev1".to_owned(),
Self::Imap4Rev2 => "IMAP4rev2".to_owned(),
Self::Acl => "ACL".to_owned(),
Self::AppendLimit(Some(n)) => format!("APPENDLIMIT={n}"),
Self::AppendLimit(None) => "APPENDLIMIT".to_owned(),
Self::Binary => "BINARY".to_owned(),
Self::Children => "CHILDREN".to_owned(),
Self::CompressDeflate => "COMPRESS=DEFLATE".to_owned(),
Self::Condstore => "CONDSTORE".to_owned(),
Self::CreateSpecialUse => "CREATE-SPECIAL-USE".to_owned(),
Self::Enable => "ENABLE".to_owned(),
Self::Esearch => "ESEARCH".to_owned(),
Self::Id => "ID".to_owned(),
Self::Idle => "IDLE".to_owned(),
Self::ListExtended => "LIST-EXTENDED".to_owned(),
Self::ListStatus => "LIST-STATUS".to_owned(),
Self::LiteralPlus => "LITERAL+".to_owned(),
Self::LoginDisabled => "LOGINDISABLED".to_owned(),
Self::LiteralMinus => "LITERAL-".to_owned(),
Self::Metadata => "METADATA".to_owned(),
Self::MetadataServer => "METADATA-SERVER".to_owned(),
Self::Move => "MOVE".to_owned(),
Self::MultiAppend => "MULTIAPPEND".to_owned(),
Self::Namespace => "NAMESPACE".to_owned(),
Self::ObjectId => "OBJECTID".to_owned(),
Self::Preview => "PREVIEW".to_owned(),
Self::QResync => "QRESYNC".to_owned(),
Self::Quota => "QUOTA".to_owned(),
Self::Rights(s) => format!("RIGHTS={s}"),
Self::SaslIr => "SASL-IR".to_owned(),
Self::SaveDate => "SAVEDATE".to_owned(),
Self::SearchRes => "SEARCHRES".to_owned(),
Self::Sort => "SORT".to_owned(),
Self::SortDisplay(s) => format!("SORT={s}"),
Self::StartTls => "STARTTLS".to_owned(),
Self::SpecialUse => "SPECIAL-USE".to_owned(),
Self::Thread(s) => format!("THREAD={s}"),
Self::StatusSize => "STATUS=SIZE".to_owned(),
Self::Unauthenticate => "UNAUTHENTICATE".to_owned(),
Self::UidPlus => "UIDPLUS".to_owned(),
Self::Unselect => "UNSELECT".to_owned(),
Self::Within => "WITHIN".to_owned(),
Self::Utf8Accept => "UTF8=ACCEPT".to_owned(),
Self::Utf8Only => "UTF8=ONLY".to_owned(),
Self::Auth(s) => format!("AUTH={s}"),
Self::Other(s) => s.clone(),
}
}
}
impl PartialEq for Capability {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Imap4Rev1, Self::Imap4Rev1)
| (Self::Imap4Rev2, Self::Imap4Rev2)
| (Self::Acl, Self::Acl)
| (Self::Binary, Self::Binary)
| (Self::Children, Self::Children)
| (Self::CompressDeflate, Self::CompressDeflate)
| (Self::Condstore, Self::Condstore)
| (Self::CreateSpecialUse, Self::CreateSpecialUse)
| (Self::Enable, Self::Enable)
| (Self::Esearch, Self::Esearch)
| (Self::Id, Self::Id)
| (Self::Idle, Self::Idle)
| (Self::ListExtended, Self::ListExtended)
| (Self::ListStatus, Self::ListStatus)
| (Self::LiteralPlus, Self::LiteralPlus)
| (Self::LoginDisabled, Self::LoginDisabled)
| (Self::LiteralMinus, Self::LiteralMinus)
| (Self::Metadata, Self::Metadata)
| (Self::MetadataServer, Self::MetadataServer)
| (Self::Move, Self::Move)
| (Self::MultiAppend, Self::MultiAppend)
| (Self::Namespace, Self::Namespace)
| (Self::ObjectId, Self::ObjectId)
| (Self::Preview, Self::Preview)
| (Self::QResync, Self::QResync)
| (Self::Quota, Self::Quota)
| (Self::SaslIr, Self::SaslIr)
| (Self::SaveDate, Self::SaveDate)
| (Self::SearchRes, Self::SearchRes)
| (Self::Sort, Self::Sort)
| (Self::StartTls, Self::StartTls)
| (Self::SpecialUse, Self::SpecialUse)
| (Self::StatusSize, Self::StatusSize)
| (Self::Unauthenticate, Self::Unauthenticate)
| (Self::UidPlus, Self::UidPlus)
| (Self::Unselect, Self::Unselect)
| (Self::Within, Self::Within)
| (Self::Utf8Accept, Self::Utf8Accept)
| (Self::Utf8Only, Self::Utf8Only) => true,
(Self::AppendLimit(a), Self::AppendLimit(b)) => a == b,
(Self::Auth(a), Self::Auth(b))
| (Self::Thread(a), Self::Thread(b))
| (Self::SortDisplay(a), Self::SortDisplay(b))
| (Self::Rights(a), Self::Rights(b))
| (Self::Other(a), Self::Other(b)) => a.eq_ignore_ascii_case(b),
(Self::Other(s), known) | (known, Self::Other(s)) => {
s.eq_ignore_ascii_case(&known.as_imap_str())
}
_ => false,
}
}
}
impl Eq for Capability {}
impl std::hash::Hash for Capability {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
for byte in self.as_imap_str().as_bytes() {
byte.to_ascii_lowercase().hash(state);
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NamespaceDescriptor {
pub prefix: String,
pub delimiter: Option<char>,
pub extensions: Vec<(String, Vec<String>)>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct EsearchResponse {
pub tag: Option<String>,
pub uid: bool,
pub min: Option<u32>,
pub max: Option<u32>,
pub count: Option<u32>,
pub all: Vec<UidRange>,
pub mod_seq: Option<u64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct UidRange {
pub start: u32,
pub end: Option<u32>,
}
impl UidRange {
pub const fn single(uid: u32) -> Self {
debug_assert!(
uid != 0,
"UID must be non-zero (RFC 3501 Section 9: uniqueid = nz-number)"
);
Self {
start: uid,
end: None,
}
}
pub const fn range(start: u32, end: u32) -> Self {
debug_assert!(
start != 0,
"UID start must be non-zero (RFC 3501 Section 9: uniqueid = nz-number)"
);
debug_assert!(
end != 0,
"UID end must be non-zero (RFC 3501 Section 9: uniqueid = nz-number)"
);
Self {
start,
end: Some(end),
}
}
pub const fn try_single(uid: u32) -> Option<Self> {
if uid == 0 {
None
} else {
Some(Self {
start: uid,
end: None,
})
}
}
pub const fn try_range(start: u32, end: u32) -> Option<Self> {
if start == 0 || end == 0 {
None
} else {
Some(Self {
start,
end: Some(end),
})
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ExpungeResult {
Expunged(Vec<u32>),
Vanished(Vec<UidRange>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MoveResult {
pub code: Option<ResponseCode>,
pub expunged: ExpungeResult,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct QresyncParams {
pub uid_validity: u32,
pub mod_seq: u64,
pub known_uids: Option<String>,
pub seq_match_data: Option<(String, String)>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct QuotaResource {
pub name: String,
pub usage: u64,
pub limit: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AclEntry {
pub identifier: String,
pub rights: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MetadataEntry {
pub name: String,
pub value: Option<Vec<u8>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ThreadNode {
pub id: Option<u32>,
pub children: Vec<Self>,
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use crate::types::{FetchResponse, Flag, MailboxInfo, StatusItem};
#[test]
fn greeting_status_variants() {
assert_eq!(GreetingStatus::Ok, GreetingStatus::Ok);
assert_ne!(GreetingStatus::Ok, GreetingStatus::PreAuth);
assert_ne!(GreetingStatus::PreAuth, GreetingStatus::Bye);
}
#[test]
fn greeting_status_copy() {
let s = GreetingStatus::PreAuth;
let s2 = s; assert_eq!(s, s2);
}
#[test]
fn status_kind_variants() {
assert_eq!(StatusKind::Ok, StatusKind::Ok);
assert_ne!(StatusKind::Ok, StatusKind::No);
assert_ne!(StatusKind::No, StatusKind::Bad);
}
#[test]
fn status_kind_copy() {
let s = StatusKind::Bad;
let s2 = s;
assert_eq!(s, s2);
}
#[test]
fn untagged_status_variants() {
assert_ne!(UntaggedStatus::Ok, UntaggedStatus::No);
assert_ne!(UntaggedStatus::Bad, UntaggedStatus::Bye);
}
#[test]
fn greeting_response_ok() {
let greeting = GreetingResponse {
status: GreetingStatus::Ok,
code: None,
text: "Dovecot ready.".into(),
};
assert_eq!(greeting.status, GreetingStatus::Ok);
assert!(greeting.code.is_none());
assert_eq!(greeting.text, "Dovecot ready.");
}
#[test]
fn greeting_response_preauth_with_code() {
let greeting = GreetingResponse {
status: GreetingStatus::PreAuth,
code: Some(ResponseCode::Alert),
text: "Authenticated via TLS cert".into(),
};
assert_eq!(greeting.status, GreetingStatus::PreAuth);
assert_eq!(greeting.code, Some(ResponseCode::Alert));
}
#[test]
fn greeting_response_bye() {
let greeting = GreetingResponse {
status: GreetingStatus::Bye,
code: None,
text: "Server shutting down".into(),
};
assert_eq!(greeting.status, GreetingStatus::Bye);
}
#[test]
fn tagged_response_ok() {
let resp = TaggedResponse {
tag: "A001".into(),
status: StatusKind::Ok,
code: None,
text: "LOGIN completed".into(),
};
assert_eq!(resp.tag, "A001");
assert_eq!(resp.status, StatusKind::Ok);
assert!(resp.code.is_none());
}
#[test]
fn tagged_response_no_with_code() {
let resp = TaggedResponse {
tag: "A002".into(),
status: StatusKind::No,
code: Some(ResponseCode::AuthenticationFailed),
text: "Invalid credentials".into(),
};
assert_eq!(resp.status, StatusKind::No);
assert_eq!(resp.code, Some(ResponseCode::AuthenticationFailed));
}
#[test]
fn tagged_response_bad() {
let resp = TaggedResponse {
tag: "A003".into(),
status: StatusKind::Bad,
code: Some(ResponseCode::ClientBug),
text: "Syntax error".into(),
};
assert_eq!(resp.status, StatusKind::Bad);
assert_eq!(resp.code, Some(ResponseCode::ClientBug));
}
#[test]
fn continuation_request() {
let cont = ContinuationRequest {
code: None,
data: "Ready for literal data".into(),
};
assert_eq!(cont.data, "Ready for literal data");
assert_eq!(cont.code, None);
}
#[test]
fn continuation_request_empty() {
let cont = ContinuationRequest {
code: None,
data: String::new(),
};
assert!(cont.data.is_empty());
assert_eq!(cont.code, None);
}
#[test]
fn response_code_simple_variants() {
assert_eq!(ResponseCode::Alert, ResponseCode::Alert);
assert_eq!(ResponseCode::Parse, ResponseCode::Parse);
assert_eq!(ResponseCode::ReadOnly, ResponseCode::ReadOnly);
assert_eq!(ResponseCode::ReadWrite, ResponseCode::ReadWrite);
assert_eq!(ResponseCode::TryCreate, ResponseCode::TryCreate);
assert_eq!(ResponseCode::NoModSeq, ResponseCode::NoModSeq);
assert_eq!(ResponseCode::Closed, ResponseCode::Closed);
assert_eq!(
ResponseCode::MailboxId("abc".into()),
ResponseCode::MailboxId("abc".into())
);
}
#[test]
fn response_code_uid_next() {
let code = ResponseCode::UidNext(42);
assert_eq!(code, ResponseCode::UidNext(42));
assert_ne!(code, ResponseCode::UidNext(99));
}
#[test]
fn response_code_uid_validity() {
let code = ResponseCode::UidValidity(1_234_567_890);
assert_eq!(code, ResponseCode::UidValidity(1_234_567_890));
}
#[test]
fn response_code_unseen() {
let code = ResponseCode::Unseen(5);
assert_eq!(code, ResponseCode::Unseen(5));
}
#[test]
fn response_code_append_uid() {
let code = ResponseCode::AppendUid {
uid_validity: 100,
uids: vec![UidRange::single(200)],
};
match &code {
ResponseCode::AppendUid { uid_validity, uids } => {
assert_eq!(*uid_validity, 100);
assert_eq!(uids, &[UidRange::single(200)]);
}
_ => panic!("expected AppendUid"),
}
}
#[test]
fn response_code_copy_uid() {
let code = ResponseCode::CopyUid {
uid_validity: 100,
source_uids: vec![UidRange::single(1), UidRange::range(3, 5)],
dest_uids: vec![UidRange::single(10)],
};
match &code {
ResponseCode::CopyUid {
uid_validity,
source_uids,
dest_uids,
} => {
assert_eq!(*uid_validity, 100);
assert_eq!(source_uids.len(), 2);
assert_eq!(dest_uids.len(), 1);
}
_ => panic!("expected CopyUid"),
}
}
#[test]
fn response_code_highest_mod_seq() {
let code = ResponseCode::HighestModSeq(9999);
assert_eq!(code, ResponseCode::HighestModSeq(9999));
}
#[test]
fn response_code_bad_charset() {
let code = ResponseCode::BadCharset(vec!["utf-8".into(), "us-ascii".into()]);
match &code {
ResponseCode::BadCharset(charsets) => {
assert_eq!(charsets.len(), 2);
assert_eq!(charsets[0], "utf-8");
}
_ => panic!("expected BadCharset"),
}
}
#[test]
fn response_code_permanent_flags() {
let code = ResponseCode::PermanentFlags(vec![Flag::Seen, Flag::Flagged]);
match &code {
ResponseCode::PermanentFlags(flags) => {
assert_eq!(flags.len(), 2);
assert_eq!(flags[0], Flag::Seen);
}
_ => panic!("expected PermanentFlags"),
}
}
#[test]
fn response_code_capability() {
let code = ResponseCode::Capability(vec![Capability::Imap4Rev1, Capability::Idle]);
match &code {
ResponseCode::Capability(caps) => {
assert_eq!(caps.len(), 2);
assert_eq!(caps[0], Capability::Imap4Rev1);
}
_ => panic!("expected Capability"),
}
}
#[test]
fn response_code_modified() {
let code = ResponseCode::Modified(vec![UidRange::range(1, 10)]);
match &code {
ResponseCode::Modified(ranges) => {
assert_eq!(ranges.len(), 1);
assert_eq!(ranges[0], UidRange::range(1, 10));
}
_ => panic!("expected Modified"),
}
}
#[test]
fn response_code_rfc5530_variants() {
let codes = [
ResponseCode::Unavailable,
ResponseCode::AuthenticationFailed,
ResponseCode::AuthorizationFailed,
ResponseCode::Expired,
ResponseCode::PrivacyRequired,
ResponseCode::ContactAdmin,
ResponseCode::NoPerm,
ResponseCode::InUse,
ResponseCode::ExpungeIssued,
ResponseCode::Corruption,
ResponseCode::ServerBug,
ResponseCode::ClientBug,
ResponseCode::Cannot,
ResponseCode::Limit,
ResponseCode::OverQuota,
ResponseCode::AlreadyExists,
ResponseCode::NonExistent,
];
for code in &codes {
assert_eq!(code, &code.clone());
}
assert_ne!(codes[0], codes[1]);
}
#[test]
fn response_code_other() {
let code = ResponseCode::Other {
name: "CUSTOM".into(),
value: Some("data".into()),
};
match &code {
ResponseCode::Other { name, value } => {
assert_eq!(name, "CUSTOM");
assert_eq!(value.as_deref(), Some("data"));
}
_ => panic!("expected Other"),
}
}
#[test]
fn response_code_other_no_value() {
let code = ResponseCode::Other {
name: "XFOO".into(),
value: None,
};
match &code {
ResponseCode::Other { name, value } => {
assert_eq!(name, "XFOO");
assert!(value.is_none());
}
_ => panic!("expected Other"),
}
}
#[test]
fn response_greeting_variant() {
let resp = Response::Greeting(GreetingResponse {
status: GreetingStatus::Ok,
code: None,
text: "Ready".into(),
});
assert!(matches!(resp, Response::Greeting(_)));
}
#[test]
fn response_tagged_variant() {
let resp = Response::Tagged(TaggedResponse {
tag: "T1".into(),
status: StatusKind::Ok,
code: None,
text: "Done".into(),
});
assert!(matches!(resp, Response::Tagged(_)));
}
#[test]
fn response_continuation_variant() {
let resp = Response::Continuation(ContinuationRequest {
code: None,
data: "+".into(),
});
assert!(matches!(resp, Response::Continuation(_)));
}
#[test]
fn untagged_exists() {
let resp = UntaggedResponse::Exists(42);
assert_eq!(resp, UntaggedResponse::Exists(42));
assert_ne!(resp, UntaggedResponse::Exists(0));
}
#[test]
fn untagged_recent() {
let resp = UntaggedResponse::Recent(5);
assert_eq!(resp, UntaggedResponse::Recent(5));
}
#[test]
fn untagged_expunge() {
let resp = UntaggedResponse::Expunge(7);
assert_eq!(resp, UntaggedResponse::Expunge(7));
}
#[test]
fn untagged_flags() {
let resp = UntaggedResponse::Flags(vec![
Flag::Seen,
Flag::Answered,
Flag::Custom("$Junk".into()),
]);
match &resp {
UntaggedResponse::Flags(flags) => {
assert_eq!(flags.len(), 3);
assert_eq!(flags[2], Flag::Custom("$Junk".into()));
}
_ => panic!("expected Flags"),
}
}
#[test]
fn untagged_search() {
let resp = UntaggedResponse::Search {
uids: vec![1, 5, 10, 42],
mod_seq: None,
};
match &resp {
UntaggedResponse::Search { uids, mod_seq } => {
assert_eq!(uids, &[1, 5, 10, 42]);
assert!(mod_seq.is_none());
}
_ => panic!("expected Search"),
}
}
#[test]
fn untagged_search_empty() {
let resp = UntaggedResponse::Search {
uids: vec![],
mod_seq: None,
};
match &resp {
UntaggedResponse::Search { uids, .. } => assert!(uids.is_empty()),
_ => panic!("expected Search"),
}
}
#[test]
fn untagged_status() {
let resp = UntaggedResponse::Status {
status: UntaggedStatus::Ok,
code: Some(ResponseCode::UidValidity(12345)),
text: "selected".into(),
};
match &resp {
UntaggedResponse::Status { status, code, text } => {
assert_eq!(*status, UntaggedStatus::Ok);
assert_eq!(code, &Some(ResponseCode::UidValidity(12345)));
assert_eq!(text, "selected");
}
_ => panic!("expected Status"),
}
}
#[test]
fn untagged_mailbox_status() {
let resp = UntaggedResponse::MailboxStatus {
mailbox: "INBOX".into(),
items: vec![StatusItem::Messages(42), StatusItem::Unseen(3)],
};
match &resp {
UntaggedResponse::MailboxStatus { mailbox, items } => {
assert_eq!(mailbox, "INBOX");
assert_eq!(items.len(), 2);
}
_ => panic!("expected MailboxStatus"),
}
}
#[test]
fn untagged_capability() {
let resp = UntaggedResponse::Capability(vec![
Capability::Imap4Rev1,
Capability::Idle,
Capability::Auth("PLAIN".into()),
]);
match &resp {
UntaggedResponse::Capability(caps) => {
assert_eq!(caps.len(), 3);
assert_eq!(caps[2], Capability::Auth("PLAIN".into()));
}
_ => panic!("expected Capability"),
}
}
#[test]
fn untagged_enabled() {
let resp = UntaggedResponse::Enabled(vec!["CONDSTORE".into(), "QRESYNC".into()]);
match &resp {
UntaggedResponse::Enabled(exts) => {
assert_eq!(exts, &["CONDSTORE", "QRESYNC"]);
}
_ => panic!("expected Enabled"),
}
}
#[test]
fn untagged_fetch() {
let fetch = FetchResponse {
seq: 1,
uid: Some(100),
..Default::default()
};
let resp = UntaggedResponse::Fetch(Box::new(fetch));
match &resp {
UntaggedResponse::Fetch(f) => {
assert_eq!(f.seq, 1);
assert_eq!(f.uid, Some(100));
}
_ => panic!("expected Fetch"),
}
}
#[test]
fn untagged_list() {
let info = MailboxInfo {
name: "INBOX".into(),
delimiter: Some('/'),
attributes: vec![],
..Default::default()
};
let resp = UntaggedResponse::List(info);
match &resp {
UntaggedResponse::List(i) => {
assert_eq!(i.name, "INBOX");
assert_eq!(i.delimiter, Some('/'));
}
_ => panic!("expected List"),
}
}
#[test]
fn untagged_vanished() {
let resp = UntaggedResponse::Vanished {
earlier: true,
uids: vec![UidRange::range(1, 5), UidRange::single(10)],
};
match &resp {
UntaggedResponse::Vanished { earlier, uids } => {
assert!(*earlier);
assert_eq!(uids.len(), 2);
}
_ => panic!("expected Vanished"),
}
}
#[test]
fn untagged_id() {
let resp = UntaggedResponse::Id(vec![
("name".into(), Some("Dovecot".into())),
("version".into(), None),
]);
match &resp {
UntaggedResponse::Id(pairs) => {
assert_eq!(pairs.len(), 2);
assert_eq!(pairs[0].1.as_deref(), Some("Dovecot"));
assert!(pairs[1].1.is_none());
}
_ => panic!("expected Id"),
}
}
#[test]
fn untagged_namespace() {
let resp = UntaggedResponse::Namespace {
personal: vec![NamespaceDescriptor {
prefix: String::new(),
delimiter: Some('/'),
extensions: vec![],
}],
other: vec![],
shared: vec![NamespaceDescriptor {
prefix: "#shared.".into(),
delimiter: Some('.'),
extensions: vec![],
}],
};
match &resp {
UntaggedResponse::Namespace {
personal,
other,
shared,
} => {
assert_eq!(personal.len(), 1);
assert!(other.is_empty());
assert_eq!(shared.len(), 1);
assert_eq!(shared[0].prefix, "#shared.");
}
_ => panic!("expected Namespace"),
}
}
#[test]
fn untagged_quota() {
let resp = UntaggedResponse::Quota {
root: String::new(),
resources: vec![QuotaResource {
name: "STORAGE".into(),
usage: 1024,
limit: 10240,
}],
};
match &resp {
UntaggedResponse::Quota { root, resources } => {
assert!(root.is_empty());
assert_eq!(resources.len(), 1);
assert_eq!(resources[0].usage, 1024);
assert_eq!(resources[0].limit, 10240);
}
_ => panic!("expected Quota"),
}
}
#[test]
fn untagged_quota_root() {
let resp = UntaggedResponse::QuotaRoot {
mailbox: "INBOX".into(),
roots: vec![String::new(), "user.alice".into()],
};
match &resp {
UntaggedResponse::QuotaRoot { mailbox, roots } => {
assert_eq!(mailbox, "INBOX");
assert_eq!(roots.len(), 2);
}
_ => panic!("expected QuotaRoot"),
}
}
#[test]
fn untagged_acl() {
let resp = UntaggedResponse::Acl {
mailbox: "INBOX".into(),
entries: vec![AclEntry {
identifier: "alice".into(),
rights: "lrswipkxte".into(),
}],
};
match &resp {
UntaggedResponse::Acl { mailbox, entries } => {
assert_eq!(mailbox, "INBOX");
assert_eq!(entries[0].identifier, "alice");
}
_ => panic!("expected Acl"),
}
}
#[test]
fn untagged_my_rights() {
let resp = UntaggedResponse::MyRights {
mailbox: "INBOX".into(),
rights: "lrs".into(),
};
match &resp {
UntaggedResponse::MyRights { mailbox, rights } => {
assert_eq!(mailbox, "INBOX");
assert_eq!(rights, "lrs");
}
_ => panic!("expected MyRights"),
}
}
#[test]
fn untagged_list_rights() {
let resp = UntaggedResponse::ListRights {
mailbox: "INBOX".into(),
identifier: "bob".into(),
required: "l".into(),
optional: vec!["r".into(), "s".into()],
};
match &resp {
UntaggedResponse::ListRights {
mailbox,
identifier,
required,
optional,
} => {
assert_eq!(mailbox, "INBOX");
assert_eq!(identifier, "bob");
assert_eq!(required, "l");
assert_eq!(optional.len(), 2);
}
_ => panic!("expected ListRights"),
}
}
#[test]
fn untagged_metadata() {
let resp = UntaggedResponse::Metadata {
mailbox: "INBOX".into(),
entries: vec![
MetadataEntry {
name: "/private/comment".into(),
value: Some(b"My notes".to_vec()),
},
MetadataEntry {
name: "/shared/vendor/foo".into(),
value: None,
},
],
};
match &resp {
UntaggedResponse::Metadata { mailbox, entries } => {
assert_eq!(mailbox, "INBOX");
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].value.as_deref(), Some(b"My notes".as_slice()));
assert!(entries[1].value.is_none());
}
_ => panic!("expected Metadata"),
}
}
#[test]
fn untagged_thread() {
let resp = UntaggedResponse::Thread(vec![ThreadNode {
id: Some(1),
children: vec![
ThreadNode {
id: Some(2),
children: vec![],
},
ThreadNode {
id: Some(3),
children: vec![ThreadNode {
id: Some(4),
children: vec![],
}],
},
],
}]);
match &resp {
UntaggedResponse::Thread(nodes) => {
assert_eq!(nodes.len(), 1);
assert_eq!(nodes[0].id, Some(1));
assert_eq!(nodes[0].children.len(), 2);
assert_eq!(nodes[0].children[1].children[0].id, Some(4));
}
_ => panic!("expected Thread"),
}
}
#[test]
fn capability_variants() {
assert_eq!(Capability::Imap4Rev1, Capability::Imap4Rev1);
assert_ne!(Capability::Imap4Rev1, Capability::Imap4Rev2);
assert_eq!(
Capability::Auth("PLAIN".into()),
Capability::Auth("PLAIN".into())
);
assert_ne!(
Capability::Auth("PLAIN".into()),
Capability::Auth("XOAUTH2".into())
);
}
#[test]
fn capability_other() {
let cap = Capability::Other("XSPECIAL".into());
assert_eq!(cap, Capability::Other("XSPECIAL".into()));
}
#[test]
fn capability_other_case_insensitive() {
assert_eq!(
Capability::Other("XYZZY".into()),
Capability::Other("xyzzy".into()),
"Capability::Other must compare case-insensitively per RFC 3501 Section 7.2.1"
);
}
#[test]
fn capability_auth_case_insensitive() {
assert_eq!(
Capability::Auth("PLAIN".into()),
Capability::Auth("plain".into()),
"Capability::Auth must compare case-insensitively per RFC 3501 Section 7.2.1"
);
}
#[test]
fn capability_thread_case_insensitive() {
assert_eq!(
Capability::Thread("REFERENCES".into()),
Capability::Thread("references".into()),
"Capability::Thread must compare case-insensitively per RFC 3501 Section 7.2.1"
);
}
#[test]
fn capability_other_case_insensitive_hash() {
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(Capability::Other("XYZZY".into()));
set.insert(Capability::Other("xyzzy".into()));
assert_eq!(
set.len(),
1,
"Case-insensitively equal Capability::Other must have the same Hash \
per RFC 3501 Section 7.2.1"
);
}
#[test]
fn capability_thread() {
let cap = Capability::Thread("REFERENCES".into());
assert_eq!(cap, Capability::Thread("REFERENCES".into()));
assert_ne!(cap, Capability::Thread("ORDEREDSUBJECT".into()));
}
#[test]
fn capability_append_limit() {
assert_eq!(
Capability::AppendLimit(Some(1024)),
Capability::AppendLimit(Some(1024))
);
assert_eq!(Capability::AppendLimit(None), Capability::AppendLimit(None));
assert_ne!(
Capability::AppendLimit(Some(1024)),
Capability::AppendLimit(None)
);
}
#[test]
fn capability_hash() {
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(Capability::Imap4Rev1);
set.insert(Capability::Idle);
set.insert(Capability::Imap4Rev1); assert_eq!(set.len(), 2);
}
#[test]
fn uid_range_single() {
let r = UidRange::single(42);
assert_eq!(r.start, 42);
assert!(r.end.is_none());
}
#[test]
fn uid_range_range() {
let r = UidRange::range(1, 100);
assert_eq!(r.start, 1);
assert_eq!(r.end, Some(100));
}
#[test]
fn uid_range_copy() {
let r = UidRange::range(5, 10);
let r2 = r; assert_eq!(r, r2);
}
#[test]
fn uid_range_try_single_zero_returns_none() {
assert!(UidRange::try_single(0).is_none());
}
#[test]
fn uid_range_try_single_nonzero_returns_some() {
let r = UidRange::try_single(1).unwrap();
assert_eq!(r.start, 1);
assert!(r.end.is_none());
}
#[test]
fn uid_range_try_range_zero_start_returns_none() {
assert!(UidRange::try_range(0, 5).is_none());
}
#[test]
fn uid_range_try_range_zero_end_returns_none() {
assert!(UidRange::try_range(5, 0).is_none());
}
#[test]
fn uid_range_try_range_nonzero_returns_some() {
let r = UidRange::try_range(1, 5).unwrap();
assert_eq!(r.start, 1);
assert_eq!(r.end, Some(5));
}
#[test]
fn namespace_descriptor() {
let ns = NamespaceDescriptor {
prefix: "INBOX.".into(),
delimiter: Some('.'),
extensions: vec![],
};
assert_eq!(ns.prefix, "INBOX.");
assert_eq!(ns.delimiter, Some('.'));
assert!(ns.extensions.is_empty());
}
#[test]
fn namespace_descriptor_no_delimiter() {
let ns = NamespaceDescriptor {
prefix: String::new(),
delimiter: None,
extensions: vec![],
};
assert!(ns.prefix.is_empty());
assert!(ns.delimiter.is_none());
assert!(ns.extensions.is_empty());
}
#[test]
fn quota_resource() {
let qr = QuotaResource {
name: "MESSAGE".into(),
usage: 50,
limit: 1000,
};
assert_eq!(qr.name, "MESSAGE");
assert_eq!(qr.usage, 50);
assert_eq!(qr.limit, 1000);
}
#[test]
fn acl_entry() {
let entry = AclEntry {
identifier: "admin".into(),
rights: "lrswipkxtea".into(),
};
assert_eq!(entry.identifier, "admin");
assert_eq!(entry.rights, "lrswipkxtea");
}
#[test]
fn metadata_entry_with_value() {
let entry = MetadataEntry {
name: "/private/comment".into(),
value: Some(b"hello".to_vec()),
};
assert_eq!(entry.name, "/private/comment");
assert_eq!(entry.value.as_deref(), Some(b"hello".as_slice()));
}
#[test]
fn metadata_entry_nil_value() {
let entry = MetadataEntry {
name: "/shared/something".into(),
value: None,
};
assert!(entry.value.is_none());
}
#[test]
fn thread_node_leaf() {
let node = ThreadNode {
id: Some(5),
children: vec![],
};
assert_eq!(node.id, Some(5));
assert!(node.children.is_empty());
}
#[test]
fn thread_node_dummy_parent() {
let node = ThreadNode {
id: None,
children: vec![ThreadNode {
id: Some(1),
children: vec![],
}],
};
assert_eq!(node.id, None);
assert_eq!(node.children.len(), 1);
}
#[test]
fn response_untagged_boxed() {
let untagged = UntaggedResponse::Exists(10);
let resp = Response::Untagged(Box::new(untagged));
match resp {
Response::Untagged(inner) => {
assert_eq!(*inner, UntaggedResponse::Exists(10));
}
_ => panic!("expected Untagged"),
}
}
#[test]
fn esearch_response_default() {
let esearch = EsearchResponse::default();
assert_eq!(esearch.tag, None);
assert!(!esearch.uid);
assert_eq!(esearch.min, None);
assert_eq!(esearch.max, None);
assert_eq!(esearch.count, None);
assert!(esearch.all.is_empty());
}
#[test]
fn esearch_response_clone_eq() {
let esearch = EsearchResponse {
tag: Some("A001".into()),
uid: true,
min: Some(1),
max: Some(100),
count: Some(50),
all: vec![UidRange::range(1, 50), UidRange::range(51, 100)],
mod_seq: None,
};
let cloned = esearch.clone();
assert_eq!(esearch, cloned);
}
#[test]
fn esearch_response_ne() {
let a = EsearchResponse {
min: Some(1),
..EsearchResponse::default()
};
let b = EsearchResponse {
min: Some(2),
..EsearchResponse::default()
};
assert_ne!(a, b);
}
#[test]
fn custom_idle_equals_idle_variant() {
assert_eq!(
Capability::Other("IDLE".into()),
Capability::Idle,
"Other(\"IDLE\") must equal Capability::Idle per RFC 3501 Section 7.2.1"
);
}
#[test]
fn custom_capability_cross_representation_hash() {
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(Capability::Idle);
set.insert(Capability::Other("IDLE".into()));
assert_eq!(
set.len(),
1,
"Other(\"IDLE\") and Capability::Idle must hash the same \
per RFC 3501 Section 7.2.1"
);
}
#[test]
fn custom_starttls_equals_starttls_variant() {
assert_eq!(
Capability::Other("STARTTLS".into()),
Capability::StartTls,
"Other(\"STARTTLS\") must equal Capability::StartTls \
per RFC 3501 Section 7.2.1"
);
}
#[test]
fn custom_auth_plain_equals_auth_variant() {
assert_eq!(
Capability::Other("AUTH=PLAIN".into()),
Capability::Auth("PLAIN".into()),
"Other(\"AUTH=PLAIN\") must equal Auth(\"PLAIN\") \
per RFC 3501 Section 7.2.1"
);
}
#[test]
fn sort_display_as_imap_str_round_trip() {
let cap = Capability::SortDisplay("DISPLAY".to_owned());
assert_eq!(cap.as_imap_str(), "SORT=DISPLAY");
}
#[test]
fn sort_display_cross_representation_equality() {
let structured = Capability::SortDisplay("DISPLAY".to_owned());
let other = Capability::Other("SORT=DISPLAY".to_owned());
assert_eq!(structured, other);
assert_eq!(other, structured);
}
#[test]
fn sort_display_hash_consistency() {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let a = Capability::SortDisplay("DISPLAY".to_owned());
let b = Capability::Other("SORT=DISPLAY".to_owned());
let hash = |c: &Capability| {
let mut h = DefaultHasher::new();
c.hash(&mut h);
h.finish()
};
assert_eq!(a, b);
assert_eq!(hash(&a), hash(&b));
}
#[test]
fn spec_audit_l12_literal_minus_capability() {
let input = b"* CAPABILITY IMAP4rev1 LITERAL-\r\n";
let (_, resp) = crate::codec::decode::parse_response(input).unwrap();
match resp {
Response::Untagged(inner) => match *inner {
UntaggedResponse::Capability(ref caps) => {
let has_dedicated = caps.iter().any(|c| {
!matches!(c, Capability::Other(_)) && !matches!(c, Capability::Imap4Rev1)
});
assert!(
has_dedicated,
"LITERAL- should have a dedicated Capability variant, \
not Capability::Other; got {caps:?}"
);
}
other => panic!("expected Capability, got {other:?}"),
},
other => panic!("expected Untagged, got {other:?}"),
}
}
#[test]
fn tagged_ok_returns_no_error() {
use crate::error::Error;
let tagged = TaggedResponse {
tag: "A001".into(),
status: StatusKind::Ok,
code: None,
text: "completed".into(),
};
let result: Result<(), Error> = match tagged.status {
StatusKind::Ok => Ok(()),
StatusKind::No => Err(Error::no_with_code(tagged.text, tagged.code)),
StatusKind::Bad => Err(Error::bad_with_code(tagged.text, tagged.code)),
};
assert!(result.is_ok());
}
#[test]
fn tagged_no_returns_no_error_variant() {
use crate::error::Error;
let tagged = TaggedResponse {
tag: "A002".into(),
status: StatusKind::No,
code: Some(ResponseCode::NonExistent),
text: "mailbox not found".into(),
};
let result: Result<(), Error> = match tagged.status {
StatusKind::Ok => Ok(()),
StatusKind::No => Err(Error::no_with_code(tagged.text, tagged.code)),
StatusKind::Bad => Err(Error::bad_with_code(tagged.text, tagged.code)),
};
assert!(result.is_err());
match result.unwrap_err() {
Error::No { text, code } => {
assert_eq!(text, "mailbox not found");
assert_eq!(code, Some(ResponseCode::NonExistent));
}
other => panic!("expected Error::No, got {other:?}"),
}
}
#[test]
fn tagged_bad_returns_bad_error() {
use crate::error::Error;
let tagged = TaggedResponse {
tag: "A003".into(),
status: StatusKind::Bad,
code: None,
text: "syntax error".into(),
};
let result: Result<(), Error> = match tagged.status {
StatusKind::Ok => Ok(()),
StatusKind::No => Err(Error::no_with_code(tagged.text, tagged.code)),
StatusKind::Bad => Err(Error::bad_with_code(tagged.text, tagged.code)),
};
assert!(result.is_err());
match result.unwrap_err() {
Error::Bad { text, code } => {
assert_eq!(text, "syntax error");
assert!(code.is_none());
}
other => panic!("expected Error::Bad, got {other:?}"),
}
}
#[test]
fn tagged_ok_with_response_code_succeeds() {
use crate::error::Error;
let tagged = TaggedResponse {
tag: "A004".into(),
status: StatusKind::Ok,
code: Some(ResponseCode::ReadWrite),
text: "SELECT completed".into(),
};
let result: Result<(), Error> = match tagged.status {
StatusKind::Ok => Ok(()),
StatusKind::No => Err(Error::no_with_code(tagged.text, tagged.code)),
StatusKind::Bad => Err(Error::bad_with_code(tagged.text, tagged.code)),
};
assert!(result.is_ok());
}
#[test]
fn capability_as_imap_str_all_variants() {
assert_eq!(Capability::Imap4Rev1.as_imap_str(), "IMAP4rev1");
assert_eq!(Capability::Imap4Rev2.as_imap_str(), "IMAP4rev2");
assert_eq!(Capability::Acl.as_imap_str(), "ACL");
assert_eq!(
Capability::AppendLimit(Some(1024)).as_imap_str(),
"APPENDLIMIT=1024"
);
assert_eq!(Capability::AppendLimit(None).as_imap_str(), "APPENDLIMIT");
assert_eq!(Capability::Binary.as_imap_str(), "BINARY");
assert_eq!(Capability::Children.as_imap_str(), "CHILDREN");
assert_eq!(
Capability::CompressDeflate.as_imap_str(),
"COMPRESS=DEFLATE"
);
assert_eq!(Capability::Condstore.as_imap_str(), "CONDSTORE");
assert_eq!(
Capability::CreateSpecialUse.as_imap_str(),
"CREATE-SPECIAL-USE"
);
assert_eq!(Capability::Enable.as_imap_str(), "ENABLE");
assert_eq!(Capability::Esearch.as_imap_str(), "ESEARCH");
assert_eq!(Capability::Id.as_imap_str(), "ID");
assert_eq!(Capability::Idle.as_imap_str(), "IDLE");
assert_eq!(Capability::ListExtended.as_imap_str(), "LIST-EXTENDED");
assert_eq!(Capability::ListStatus.as_imap_str(), "LIST-STATUS");
assert_eq!(Capability::LiteralPlus.as_imap_str(), "LITERAL+");
assert_eq!(Capability::LoginDisabled.as_imap_str(), "LOGINDISABLED");
assert_eq!(Capability::LiteralMinus.as_imap_str(), "LITERAL-");
assert_eq!(Capability::Metadata.as_imap_str(), "METADATA");
assert_eq!(Capability::MetadataServer.as_imap_str(), "METADATA-SERVER");
assert_eq!(Capability::Move.as_imap_str(), "MOVE");
assert_eq!(Capability::MultiAppend.as_imap_str(), "MULTIAPPEND");
assert_eq!(Capability::Namespace.as_imap_str(), "NAMESPACE");
assert_eq!(Capability::ObjectId.as_imap_str(), "OBJECTID");
assert_eq!(Capability::Preview.as_imap_str(), "PREVIEW");
assert_eq!(Capability::QResync.as_imap_str(), "QRESYNC");
assert_eq!(Capability::Quota.as_imap_str(), "QUOTA");
assert_eq!(
Capability::Rights("texk".into()).as_imap_str(),
"RIGHTS=texk"
);
assert_eq!(Capability::SaslIr.as_imap_str(), "SASL-IR");
assert_eq!(Capability::SaveDate.as_imap_str(), "SAVEDATE");
assert_eq!(Capability::SearchRes.as_imap_str(), "SEARCHRES");
assert_eq!(Capability::Sort.as_imap_str(), "SORT");
assert_eq!(
Capability::SortDisplay("DISPLAY".into()).as_imap_str(),
"SORT=DISPLAY"
);
assert_eq!(Capability::StartTls.as_imap_str(), "STARTTLS");
assert_eq!(Capability::SpecialUse.as_imap_str(), "SPECIAL-USE");
assert_eq!(
Capability::Thread("REFERENCES".into()).as_imap_str(),
"THREAD=REFERENCES"
);
assert_eq!(Capability::StatusSize.as_imap_str(), "STATUS=SIZE");
assert_eq!(Capability::Unauthenticate.as_imap_str(), "UNAUTHENTICATE");
assert_eq!(Capability::UidPlus.as_imap_str(), "UIDPLUS");
assert_eq!(Capability::Unselect.as_imap_str(), "UNSELECT");
assert_eq!(Capability::Within.as_imap_str(), "WITHIN");
assert_eq!(Capability::Utf8Accept.as_imap_str(), "UTF8=ACCEPT");
assert_eq!(Capability::Utf8Only.as_imap_str(), "UTF8=ONLY");
assert_eq!(
Capability::Auth("XOAUTH2".into()).as_imap_str(),
"AUTH=XOAUTH2"
);
assert_eq!(
Capability::Other("XSPECIAL".into()).as_imap_str(),
"XSPECIAL"
);
}
#[test]
fn capability_sort_display_equality() {
assert_eq!(
Capability::SortDisplay("DISPLAY".into()),
Capability::SortDisplay("display".into()),
"SortDisplay must compare case-insensitively per RFC 3501 Section 7.2.1"
);
assert_ne!(
Capability::SortDisplay("DISPLAY".into()),
Capability::SortDisplay("OTHER".into()),
);
}
#[test]
fn capability_rights_equality() {
assert_eq!(
Capability::Rights("texk".into()),
Capability::Rights("TEXK".into()),
"Rights must compare case-insensitively per RFC 3501 Section 7.2.1"
);
assert_ne!(
Capability::Rights("texk".into()),
Capability::Rights("abc".into()),
);
}
#[test]
fn require_ok_succeeds_on_ok() {
let tagged = TaggedResponse {
tag: "A001".into(),
status: StatusKind::Ok,
code: Some(ResponseCode::ReadWrite),
text: "SELECT completed".into(),
};
let result = tagged.require_ok();
assert!(result.is_ok());
let resp = result.unwrap();
assert_eq!(resp.tag, "A001");
assert_eq!(resp.code, Some(ResponseCode::ReadWrite));
}
#[test]
fn require_ok_returns_error_on_no() {
use crate::error::Error;
let tagged = TaggedResponse {
tag: "A002".into(),
status: StatusKind::No,
code: Some(ResponseCode::NonExistent),
text: "mailbox not found".into(),
};
let result = tagged.require_ok();
assert!(result.is_err());
match result.unwrap_err() {
Error::No { text, code } => {
assert_eq!(text, "mailbox not found");
assert_eq!(code, Some(ResponseCode::NonExistent));
}
other => panic!("expected Error::No, got {other:?}"),
}
}
#[test]
fn require_ok_returns_error_on_bad() {
use crate::error::Error;
let tagged = TaggedResponse {
tag: "A003".into(),
status: StatusKind::Bad,
code: None,
text: "syntax error in command".into(),
};
let result = tagged.require_ok();
assert!(result.is_err());
match result.unwrap_err() {
Error::Bad { text, code } => {
assert_eq!(text, "syntax error in command");
assert!(code.is_none());
}
other => panic!("expected Error::Bad, got {other:?}"),
}
}
#[test]
fn require_ok_no_without_code() {
use crate::error::Error;
let tagged = TaggedResponse {
tag: "A004".into(),
status: StatusKind::No,
code: None,
text: "operation failed".into(),
};
let result = tagged.require_ok();
assert!(result.is_err());
match result.unwrap_err() {
Error::No { text, code } => {
assert_eq!(text, "operation failed");
assert!(code.is_none());
}
other => panic!("expected Error::No, got {other:?}"),
}
}
#[test]
fn require_ok_bad_with_code() {
use crate::error::Error;
let tagged = TaggedResponse {
tag: "A005".into(),
status: StatusKind::Bad,
code: Some(ResponseCode::ClientBug),
text: "invalid arguments".into(),
};
let result = tagged.require_ok();
assert!(result.is_err());
match result.unwrap_err() {
Error::Bad { text, code } => {
assert_eq!(text, "invalid arguments");
assert_eq!(code, Some(ResponseCode::ClientBug));
}
other => panic!("expected Error::Bad, got {other:?}"),
}
}
}