add support for the imap quota extension (RFC 2087)

This commit is contained in:
Edward Rudd 2022-07-16 10:54:39 -04:00
parent 9c08e14523
commit 57ce6bb545
5 changed files with 663 additions and 3 deletions

View file

@ -12,6 +12,7 @@ use super::error::{Bad, Bye, Error, No, ParseError, Result, ValidateError};
use super::extensions; use super::extensions;
use super::parse::*; use super::parse::*;
use super::types::*; use super::types::*;
use super::utils::*;
static TAG_PREFIX: &str = "a"; static TAG_PREFIX: &str = "a";
const INITIAL_TAG: u32 = 0; const INITIAL_TAG: u32 = 0;
@ -1375,6 +1376,48 @@ impl<T: Read + Write> Session<T> {
.and_then(|lines| MyRightsResponse::parse(lines, &mut self.unsolicited_responses_tx)) .and_then(|lines| MyRightsResponse::parse(lines, &mut self.unsolicited_responses_tx))
} }
/// The [`SETQUOTA` command](https://datatracker.ietf.org/doc/html/rfc2087#section-4.1)
///
/// Updates the resource limits for a mailbox. Any previous limits for the named quota
/// are discarded.
///
/// Returns the updated quota.
pub fn set_quota(
&mut self,
quota_root: impl AsRef<str>,
limits: &[QuotaResourceLimit<'_>],
) -> Result<QuotaResponse> {
let limits = iter_join(limits.iter(), " ");
self.run_command_and_read_response(&format!(
"SETQUOTA {} ({})",
validate_str("SETQUOTA", "quota_root", quota_root.as_ref())?,
limits,
))
.and_then(|lines| QuotaResponse::parse(lines, &mut self.unsolicited_responses_tx))
}
/// The [`GETQUOTA` command](https://datatracker.ietf.org/doc/html/rfc2087#section-4.2)
///
/// Returns the quota information for the specified quota root
pub fn get_quota(&mut self, quota_root: impl AsRef<str>) -> Result<QuotaResponse> {
self.run_command_and_read_response(&format!(
"GETQUOTA {}",
validate_str("GETQUOTA", "quota_root", quota_root.as_ref())?
))
.and_then(|lines| QuotaResponse::parse(lines, &mut self.unsolicited_responses_tx))
}
/// The [`GETQUOTAROOT` command](https://datatracker.ietf.org/doc/html/rfc2087#section-4.3)
///
/// Returns the quota roots along with their quota information for the specified mailbox
pub fn get_quota_root(&mut self, mailbox_name: impl AsRef<str>) -> Result<QuotaRootResponse> {
self.run_command_and_read_response(&format!(
"GETQUOTAROOT {}",
validate_str("GETQUOTAROOT", "mailbox", mailbox_name.as_ref())?
))
.and_then(|lines| QuotaRootResponse::parse(lines, &mut self.unsolicited_responses_tx))
}
// these are only here because they are public interface, the rest is in `Connection` // these are only here because they are public interface, the rest is in `Connection`
/// Runs a command and checks if it returns OK. /// Runs a command and checks if it returns OK.
pub fn run_command_and_check_ok(&mut self, command: impl AsRef<str>) -> Result<()> { pub fn run_command_and_check_ok(&mut self, command: impl AsRef<str>) -> Result<()> {
@ -1634,6 +1677,17 @@ pub(crate) mod testutils {
.to_string() .to_string()
); );
} }
pub(crate) fn assert_quota_resource(
resource: &QuotaResource<'_>,
name: QuotaResourceName<'_>,
limit: u64,
usage: u64,
) {
assert_eq!(resource.name, name);
assert_eq!(resource.usage, usage);
assert_eq!(resource.limit, limit);
}
} }
#[cfg(test)] #[cfg(test)]
@ -2369,6 +2423,278 @@ mod tests {
assert!(matches!(acl, Err(Error::Parse(_)))); assert!(matches!(acl, Err(Error::Parse(_))));
} }
#[test]
fn set_quota() {
let response = b"* QUOTA my_root (STORAGE 10 500)\r\n\
a1 OK completed\r\n"
.to_vec();
let mock_stream = MockStream::new(response);
let mut session = mock_session!(mock_stream);
let quota = session
.set_quota(
"my_root",
&[QuotaResourceLimit {
name: QuotaResourceName::Storage,
amount: 500,
}],
)
.unwrap();
assert_eq!(
session.stream.get_ref().written_buf,
b"a1 SETQUOTA \"my_root\" (STORAGE 500)\r\n".to_vec(),
"Invalid setquota command"
);
let quota = quota.parsed().as_ref().unwrap();
assert_eq!(quota.root_name, "my_root");
assert_eq!(quota.resources.len(), 1);
assert_quota_resource(&quota.resources[0], QuotaResourceName::Storage, 500, 10);
}
#[test]
fn set_quota_no_such_quota_root() {
let response = b"a1 NO no such quota root\r\n".to_vec();
let mock_stream = MockStream::new(response);
let mut session = mock_session!(mock_stream);
let quota = session.set_quota(
"my_root",
&[QuotaResourceLimit {
name: QuotaResourceName::Storage,
amount: 500,
}],
);
assert_eq!(
session.stream.get_ref().written_buf,
b"a1 SETQUOTA \"my_root\" (STORAGE 500)\r\n".to_vec(),
"Invalid setquota command"
);
assert!(matches!(quota, Err(Error::No(_))));
}
#[test]
fn set_quota_invalid_no_quota_lines() {
let response = b"a1 OK completed\r\n".to_vec();
let mock_stream = MockStream::new(response);
let mut session = mock_session!(mock_stream);
let quota = session
.set_quota(
"my_root",
&[QuotaResourceLimit {
name: QuotaResourceName::Storage,
amount: 500,
}],
)
.unwrap();
assert_eq!(
session.stream.get_ref().written_buf,
b"a1 SETQUOTA \"my_root\" (STORAGE 500)\r\n".to_vec(),
"Invalid setquota command"
);
assert_eq!(quota.parsed(), &None);
}
#[test]
fn set_quota_invalid_too_many_quota_lines() {
let response = b"* QUOTA my_root (STORAGE 10 500)\r\n\
* QUOTA my_root2 (STORAGE 10 500)\r\n\
a1 OK completed\r\n"
.to_vec();
let mock_stream = MockStream::new(response);
let mut session = mock_session!(mock_stream);
let quota = session.set_quota(
"my_root",
&[QuotaResourceLimit {
name: QuotaResourceName::Storage,
amount: 500,
}],
);
assert_eq!(
session.stream.get_ref().written_buf,
b"a1 SETQUOTA \"my_root\" (STORAGE 500)\r\n".to_vec(),
"Invalid setquota command"
);
assert!(matches!(quota, Err(Error::Parse(_))));
}
#[test]
fn get_quota() {
let response = b"* QUOTA my_root ()\r\n\
a1 OK completed\r\n"
.to_vec();
let mock_stream = MockStream::new(response);
let mut session = mock_session!(mock_stream);
let quota = session.get_quota("my_root").unwrap();
assert_eq!(
session.stream.get_ref().written_buf,
b"a1 GETQUOTA \"my_root\"\r\n".to_vec(),
"Invalid getquota command"
);
let quota = quota.parsed().as_ref().unwrap();
assert_eq!(quota.root_name, "my_root");
assert_eq!(quota.resources, vec![]);
}
#[test]
fn get_quota_with_limits() {
let response = b"* QUOTA my_root (STORAGE 10 500)\r\n\
a1 OK completed\r\n"
.to_vec();
let mock_stream = MockStream::new(response);
let mut session = mock_session!(mock_stream);
let quota = session.get_quota("my_root").unwrap();
assert_eq!(
session.stream.get_ref().written_buf,
b"a1 GETQUOTA \"my_root\"\r\n".to_vec(),
"Invalid getquota command"
);
let quota = quota.parsed().as_ref().unwrap();
assert_eq!(quota.root_name, "my_root");
assert_eq!(quota.resources.len(), 1);
assert_quota_resource(&quota.resources[0], QuotaResourceName::Storage, 500, 10);
}
#[test]
fn get_quota_with_invalid_no_quota_lines() {
let response = b"a1 OK completed\r\n".to_vec();
let mock_stream = MockStream::new(response);
let mut session = mock_session!(mock_stream);
let quota = session.get_quota("my_root").unwrap();
assert_eq!(
session.stream.get_ref().written_buf,
b"a1 GETQUOTA \"my_root\"\r\n".to_vec(),
"Invalid getquota command"
);
assert_eq!(quota.parsed(), &None);
}
#[test]
fn get_quota_with_invalid_too_many_quota_lines() {
let response = b"* QUOTA my_root (STORAGE 10 500)\r\n\
* QUOTA my_root2 (STORAGE 10 500)\r\n\
a1 OK completed\r\n"
.to_vec();
let mock_stream = MockStream::new(response);
let mut session = mock_session!(mock_stream);
let quota = session.get_quota("my_root");
assert_eq!(
session.stream.get_ref().written_buf,
b"a1 GETQUOTA \"my_root\"\r\n".to_vec(),
"Invalid getquota command"
);
assert!(matches!(quota, Err(Error::Parse(_))));
}
#[test]
fn get_quota_with_no_such_quota_root() {
let response = b"a1 NO no such quota root\r\n".to_vec();
let mock_stream = MockStream::new(response);
let mut session = mock_session!(mock_stream);
let quota = session.get_quota("my_root");
assert_eq!(
session.stream.get_ref().written_buf,
b"a1 GETQUOTA \"my_root\"\r\n".to_vec(),
"Invalid getquota command"
);
assert!(matches!(quota, Err(Error::No(_))));
}
#[test]
fn get_quota_root_with_no_root() {
let response = b"* QUOTAROOT INBOX\r\n\
a1 OK completed\r\n"
.to_vec();
let mock_stream = MockStream::new(response);
let mut session = mock_session!(mock_stream);
let quota_root = session.get_quota_root("INBOX").unwrap();
assert_eq!(
session.stream.get_ref().written_buf,
b"a1 GETQUOTAROOT \"INBOX\"\r\n".to_vec(),
"Invalid getquotaroot command"
);
assert_eq!(quota_root.mailbox_name(), "INBOX");
assert_eq!(
quota_root.quota_root_names().collect::<Vec<_>>(),
Vec::<&str>::new()
);
assert_eq!(quota_root.quotas(), vec![]);
}
#[test]
fn get_quota_root_no_such_mailbox() {
let response = b"a1 NO no such mailboxd\r\n".to_vec();
let mock_stream = MockStream::new(response);
let mut session = mock_session!(mock_stream);
let quota_root = session.get_quota_root("INBOX");
assert_eq!(
session.stream.get_ref().written_buf,
b"a1 GETQUOTAROOT \"INBOX\"\r\n".to_vec(),
"Invalid getquotaroot command"
);
assert!(matches!(quota_root, Err(Error::No(_))));
}
#[test]
fn get_quota_root_invalid_no_quota_root_lines() {
let response = b"a1 OK completed\r\n".to_vec();
let mock_stream = MockStream::new(response);
let mut session = mock_session!(mock_stream);
let quota_root = session.get_quota_root("INBOX");
assert_eq!(
session.stream.get_ref().written_buf,
b"a1 GETQUOTAROOT \"INBOX\"\r\n".to_vec(),
"Invalid getquotaroot command"
);
assert!(matches!(quota_root, Err(Error::Parse(_))));
}
#[test]
fn get_quota_root_invalid_too_many_quota_root_lines() {
let response = b"* QUOTAROOT INBOX\r\n\
* QUOTAROOT INBOX\r\n\
a1 OK completed\r\n"
.to_vec();
let mock_stream = MockStream::new(response);
let mut session = mock_session!(mock_stream);
let quota_root = session.get_quota_root("INBOX");
println!("Resp: {:?}", quota_root);
assert_eq!(
session.stream.get_ref().written_buf,
b"a1 GETQUOTAROOT \"INBOX\"\r\n".to_vec(),
"Invalid getquotaroot command"
);
assert!(matches!(quota_root, Err(Error::Parse(_))));
}
#[test]
fn get_quota_root_with_root() {
let response = b"* QUOTAROOT INBOX my_root\r\n\
* QUOTA my_root (STORAGE 10 500)\r\n\
a1 OK completed\r\n"
.to_vec();
let mock_stream = MockStream::new(response);
let mut session = mock_session!(mock_stream);
let quota_root = session.get_quota_root("INBOX").unwrap();
assert_eq!(
session.stream.get_ref().written_buf,
b"a1 GETQUOTAROOT \"INBOX\"\r\n".to_vec(),
"Invalid getquotaroot command"
);
assert_eq!(quota_root.mailbox_name(), "INBOX");
assert_eq!(
quota_root.quota_root_names().collect::<Vec<_>>(),
vec!["my_root"]
);
assert_eq!(quota_root.quotas().len(), 1);
assert_eq!(quota_root.quotas().first().unwrap().root_name, "my_root");
assert_eq!(
quota_root.quotas().first().unwrap().resources,
vec![QuotaResource {
name: QuotaResourceName::Storage,
usage: 10,
limit: 500,
}]
);
}
#[test] #[test]
fn capability() { fn capability() {
let response = b"* CAPABILITY IMAP4rev1 STARTTLS AUTH=GSSAPI LOGINDISABLED\r\n\ let response = b"* CAPABILITY IMAP4rev1 STARTTLS AUTH=GSSAPI LOGINDISABLED\r\n\

View file

@ -78,6 +78,7 @@
#![cfg_attr(docsrs, feature(doc_cfg))] #![cfg_attr(docsrs, feature(doc_cfg))]
mod parse; mod parse;
mod utils;
pub mod types; pub mod types;

View file

@ -124,6 +124,9 @@ pub use self::deleted::Deleted;
mod acls; mod acls;
pub use self::acls::*; pub use self::acls::*;
mod quota;
pub use self::quota::*;
mod unsolicited_response; mod unsolicited_response;
pub use self::unsolicited_response::{AttributeValue, UnsolicitedResponse}; pub use self::unsolicited_response::{AttributeValue, UnsolicitedResponse};

221
src/types/quota.rs Normal file
View file

@ -0,0 +1,221 @@
use crate::error::{Error, ParseError};
use crate::parse::{parse_many_into2, parse_until_done, MapOrNot, MapOrNot2};
use crate::types::UnsolicitedResponse;
use imap_proto::Response;
use ouroboros::self_referencing;
use std::borrow::Cow;
use std::fmt::{Debug, Display, Formatter};
use std::sync::mpsc;
/// From [SETQUOTA Resource limit](https://datatracker.ietf.org/doc/html/rfc2087#section-4.1)
///
/// Used by [`Session::set_quota`]
#[derive(Debug, Eq, PartialEq)]
#[non_exhaustive]
pub struct QuotaResourceLimit<'a> {
/// The resource type
pub name: QuotaResourceName<'a>,
/// The amount for that resource
pub amount: u64,
}
impl<'a> QuotaResourceLimit<'a> {
/// Creates a new [`QuotaResourceLimit`]
pub fn new(name: QuotaResourceName<'a>, amount: u64) -> Self {
Self { name, amount }
}
}
impl Display for QuotaResourceLimit<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{} {}", self.name, self.amount)
}
}
/// From [Resources](https://datatracker.ietf.org/doc/html/rfc2087#section-3)
///
/// Used by [`QuotaLimit`], and [`QuotaResource`]
#[derive(Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum QuotaResourceName<'a> {
/// Sum of messages' RFC822.SIZE, in units of 1024 octets
Storage,
/// Number of messages
Message,
/// Any other string (for future RFCs)
Atom(Cow<'a, str>),
}
impl Display for QuotaResourceName<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::Storage => write!(f, "STORAGE"),
Self::Message => write!(f, "MESSAGE"),
Self::Atom(s) => write!(f, "{}", s),
}
}
}
/// From [QUOTA Response](https://datatracker.ietf.org/doc/html/rfc2087#section-5.1)
///
/// This is a wrapper around a single single [`Quota`].
///
/// Used by [`Session::get_quota`] and [`Session::set_quota`]
#[self_referencing]
pub struct QuotaResponse {
data: Vec<u8>,
#[borrows(data)]
#[covariant]
pub(crate) quota: Option<Quota<'this>>,
}
impl QuotaResponse {
/// Parse the [`Quota`] response from a response buffer.
pub fn parse(
owned: Vec<u8>,
unsolicited: &mut mpsc::Sender<UnsolicitedResponse>,
) -> Result<Self, Error> {
QuotaResponseTryBuilder {
data: owned,
quota_builder: |input| {
// There should zero or one QUOTA response
parse_until_done(input, true, unsolicited, |response| match response {
Response::Quota(q) => Ok(MapOrNot::Map(Quota::from_imap_proto(q))),
resp => Ok(MapOrNot::Not(resp)),
})
},
}
.try_build()
}
/// Access to the wrapped optional [`Quota`] struct
pub fn parsed(&self) -> &Option<Quota<'_>> {
self.borrow_quota()
}
}
/// From [QUOTA Response](https://datatracker.ietf.org/doc/html/rfc2087#section-5.1)
///
/// Used by [`QuotaResponse`] and [`QuotaRoot`]
#[derive(Debug, Eq, PartialEq)]
#[non_exhaustive]
pub struct Quota<'a> {
/// The quota root name
pub root_name: Cow<'a, str>,
/// The defined resources with their usage and limits (could be empty)
pub resources: Vec<QuotaResource<'a>>,
}
impl<'a> Quota<'a> {
fn from_imap_proto(q: imap_proto::Quota<'a>) -> Self {
Self {
root_name: q.root_name,
resources: q
.resources
.into_iter()
.map(|e| QuotaResource {
name: match e.name {
imap_proto::QuotaResourceName::Storage => QuotaResourceName::Storage,
imap_proto::QuotaResourceName::Message => QuotaResourceName::Message,
imap_proto::QuotaResourceName::Atom(e) => QuotaResourceName::Atom(e),
},
usage: e.usage,
limit: e.limit,
})
.collect(),
}
}
}
/// From [QUOTA Response](https://datatracker.ietf.org/doc/html/rfc2087#section-5.1)
///
/// The quota resource sub-pieces in a [`Quota`]
#[derive(Debug, Eq, PartialEq)]
#[non_exhaustive]
pub struct QuotaResource<'a> {
/// The resource type
pub name: QuotaResourceName<'a>,
/// current usage of the resource
pub usage: u64,
/// resource limit
pub limit: u64,
}
/// From [QUOTAROOT Response](https://datatracker.ietf.org/doc/html/rfc2087#section-5.2)
///
/// Used by [`Session::get_quota_root`]
#[self_referencing]
pub struct QuotaRootResponse {
data: Vec<u8>,
#[borrows(data)]
#[covariant]
pub(crate) inner: InnerQuotaRootResponse<'this>,
}
impl Debug for QuotaRootResponse {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self.borrow_inner())
}
}
/// Inner struct to manage storing the references for ouroboros
#[derive(Debug)]
pub(crate) struct InnerQuotaRootResponse<'a> {
pub(crate) quota_root: imap_proto::QuotaRoot<'a>,
pub(crate) quotas: Vec<Quota<'a>>,
}
impl QuotaRootResponse {
/// Parse the [`QuotaRoot`] response from a response buffer.
pub fn parse(
owned: Vec<u8>,
unsolicited: &mut mpsc::Sender<UnsolicitedResponse>,
) -> Result<Self, Error> {
QuotaRootResponseTryBuilder {
data: owned,
inner_builder: |input| {
let mut quota_roots = Vec::new();
let mut quotas = Vec::new();
parse_many_into2(
input,
&mut quota_roots,
&mut quotas,
unsolicited,
|response| match response {
Response::QuotaRoot(q) => Ok(MapOrNot2::Map1(q)),
Response::Quota(q) => Ok(MapOrNot2::Map2(Quota::from_imap_proto(q))),
resp => Ok(MapOrNot2::Not(resp)),
},
)?;
match quota_roots.len() {
1 => Ok(InnerQuotaRootResponse {
quota_root: quota_roots.remove(0),
quotas,
}),
_ => Err(Error::Parse(ParseError::Invalid(input.to_vec()))),
}
},
}
.try_build()
}
/// The mailbox name
pub fn mailbox_name(&self) -> &str {
&*self.borrow_inner().quota_root.mailbox_name
}
/// The list of quota roots for the mailbox name (could be empty)
pub fn quota_root_names(&self) -> impl Iterator<Item = &str> {
self.borrow_inner()
.quota_root
.quota_root_names
.iter()
.map(|e| &*e.as_ref())
}
/// The set of quotas for each named quota root (could be empty)
pub fn quotas(&self) -> &[Quota<'_>] {
&self.borrow_inner().quotas[..]
}
}

View file

@ -27,7 +27,6 @@ fn test_smtp_host() -> String {
.unwrap_or_else(|_| std::env::var("TEST_HOST").unwrap_or("127.0.0.1".to_string())) .unwrap_or_else(|_| std::env::var("TEST_HOST").unwrap_or("127.0.0.1".to_string()))
} }
#[cfg(feature = "test-full-imap")]
fn test_imap_port() -> u16 { fn test_imap_port() -> u16 {
std::env::var("TEST_IMAP_PORT") std::env::var("TEST_IMAP_PORT")
.unwrap_or("3143".to_string()) .unwrap_or("3143".to_string())
@ -71,7 +70,10 @@ fn wait_for_delivery() {
std::thread::sleep(std::time::Duration::from_millis(500)); std::thread::sleep(std::time::Duration::from_millis(500));
} }
fn session(user: &str) -> imap::Session<native_tls::TlsStream<TcpStream>> { fn session_with_options(
user: &str,
clean: bool,
) -> imap::Session<native_tls::TlsStream<TcpStream>> {
let host = test_host(); let host = test_host();
let mut s = imap::ClientBuilder::new(&host, test_imaps_port()) let mut s = imap::ClientBuilder::new(&host, test_imaps_port())
.connect(|domain, tcp| { .connect(|domain, tcp| {
@ -82,10 +84,37 @@ fn session(user: &str) -> imap::Session<native_tls::TlsStream<TcpStream>> {
.login(user, user) .login(user, user)
.unwrap(); .unwrap();
s.debug = true; s.debug = true;
clean_mailbox(&mut s); if clean {
clean_mailbox(&mut s);
}
s s
} }
fn get_greeting() -> String {
let host = test_host();
let tcp = TcpStream::connect((host.as_ref(), test_imap_port())).unwrap();
let mut client = imap::Client::new(tcp);
let greeting = client.read_greeting().unwrap();
String::from_utf8(greeting).unwrap()
}
fn delete_mailbox(s: &mut imap::Session<native_tls::TlsStream<TcpStream>>, mailbox: &str) {
// we are silently eating any error (e.g. mailbox does not exist
s.set_acl(
mailbox,
"cyrus",
&"x".try_into().unwrap(),
imap::types::AclModifyMode::Replace,
)
.unwrap_or(());
s.delete(mailbox).unwrap_or(());
}
fn session(user: &str) -> imap::Session<native_tls::TlsStream<TcpStream>> {
session_with_options(user, true)
}
fn smtp(user: &str) -> lettre::SmtpTransport { fn smtp(user: &str) -> lettre::SmtpTransport {
use lettre::{ use lettre::{
transport::smtp::{ transport::smtp::{
@ -670,3 +699,83 @@ fn qresync() {
// Assert that the new highest mod sequence is being returned // Assert that the new highest mod sequence is being returned
assert_ne!(r.mod_seq, None); assert_ne!(r.mod_seq, None);
} }
fn assert_quota_resource(
resource: &imap::types::QuotaResource,
name: imap::types::QuotaResourceName,
limit: u64,
usage: Option<u64>,
) {
assert_eq!(resource.name, name);
if let Some(usage) = usage {
assert_eq!(resource.usage, usage);
}
assert_eq!(resource.limit, limit);
}
#[test]
fn quota() {
use imap::types::{QuotaResourceLimit, QuotaResourceName};
let greeting = get_greeting();
let is_greenmail = greeting.find("Cyrus").is_none();
let to = "inbox-quota@localhost";
if is_greenmail {
let mut c = session(to);
let quota_root = c.get_quota_root("INBOX").unwrap();
assert_eq!(quota_root.mailbox_name(), "INBOX");
let root_names = quota_root.quota_root_names().collect::<Vec<&str>>();
assert_eq!(root_names, Vec::<&str>::new());
// TODO build tests for greenmail
} else {
// because we are cyrus we can "test" the admin account for checking the GET/SET commands
let mut admin = session_with_options("cyrus", false);
// purge mailbox from previous run
delete_mailbox(&mut admin, "user/inbox-quota@localhost");
let mut c = session(to);
let quota_root = c.get_quota_root("INBOX").unwrap();
assert_eq!(quota_root.mailbox_name(), "INBOX");
let root_names = quota_root.quota_root_names().collect::<Vec<&str>>();
assert_eq!(root_names, vec!["INBOX"]);
let quota = quota_root.quotas().first().unwrap();
assert_eq!(quota.root_name, "INBOX");
assert_quota_resource(
&quota.resources[0],
QuotaResourceName::Storage,
1000,
Some(0),
);
let update = admin
.set_quota(
"user/inbox-quota@localhost",
&[QuotaResourceLimit::new(QuotaResourceName::Storage, 500)],
)
.unwrap();
// Cyrus does not return the quota root definition on modification
assert_eq!(update.parsed(), &None);
let quota_response = admin.get_quota("user/inbox-quota@localhost").unwrap();
let quota = quota_response.parsed().as_ref().unwrap();
assert_eq!(quota.root_name, "user/inbox-quota@localhost");
assert_eq!(quota.resources.len(), 1);
assert_quota_resource(
&quota.resources[0],
QuotaResourceName::Storage,
500,
Some(0),
);
}
}