Merge pull request #235 from urkle/feat-quota-protocol
add support for the imap quota extension (RFC 2087)
This commit is contained in:
commit
1a1b5ea269
7 changed files with 868 additions and 22 deletions
357
src/client.rs
357
src/client.rs
|
|
@ -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;
|
||||||
|
|
@ -216,14 +217,7 @@ impl<'a, T: Read + Write> AppendCmd<'a, T> {
|
||||||
/// Note: be sure to set flags and optional date before you
|
/// Note: be sure to set flags and optional date before you
|
||||||
/// finish the command.
|
/// finish the command.
|
||||||
pub fn finish(&mut self) -> Result<Appended> {
|
pub fn finish(&mut self) -> Result<Appended> {
|
||||||
let flagstr = self
|
let flagstr = iter_join(self.flags.iter().filter(|f| **f != Flag::Recent), " ");
|
||||||
.flags
|
|
||||||
.clone()
|
|
||||||
.into_iter()
|
|
||||||
.filter(|f| *f != Flag::Recent)
|
|
||||||
.map(|f| f.to_string())
|
|
||||||
.collect::<Vec<String>>()
|
|
||||||
.join(" ");
|
|
||||||
|
|
||||||
let datestr = if let Some(date) = self.date {
|
let datestr = if let Some(date) = self.date {
|
||||||
format!(" \"{}\"", date.format("%d-%h-%Y %T %z"))
|
format!(" \"{}\"", date.format("%d-%h-%Y %T %z"))
|
||||||
|
|
@ -1382,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<()> {
|
||||||
|
|
@ -1641,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)]
|
||||||
|
|
@ -2376,6 +2423,300 @@ 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("a.resources[0], QuotaResourceName::Storage, 500, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_quota_via_quota_resource_limit_new() {
|
||||||
|
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::new("STORAGE", 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("a.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
|
||||||
|
a1 OK completed\r
|
||||||
|
"
|
||||||
|
.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("a.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\
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
98
src/parse.rs
98
src/parse.rs
|
|
@ -42,6 +42,50 @@ pub(crate) fn parse_many_into<'input, T, F>(
|
||||||
) -> Result<()>
|
) -> Result<()>
|
||||||
where
|
where
|
||||||
F: FnMut(Response<'input>) -> Result<MapOrNot<'input, T>>,
|
F: FnMut(Response<'input>) -> Result<MapOrNot<'input, T>>,
|
||||||
|
{
|
||||||
|
let mut other = Vec::new();
|
||||||
|
|
||||||
|
parse_many_into2::<_, (), _, _, _>(
|
||||||
|
input,
|
||||||
|
into,
|
||||||
|
&mut other,
|
||||||
|
unsolicited,
|
||||||
|
|response| match map(response)? {
|
||||||
|
MapOrNot::Map(t) => Ok(MapOrNot2::Map1(t)),
|
||||||
|
MapOrNot::MapVec(t) => Ok(MapOrNot2::MapVec1(t)),
|
||||||
|
MapOrNot::Not(t) => Ok(MapOrNot2::Not(t)),
|
||||||
|
MapOrNot::Ignore => Ok(MapOrNot2::Ignore),
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
|
||||||
|
assert_eq!(other.len(), 0);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) enum MapOrNot2<'a, T, U> {
|
||||||
|
Map1(T),
|
||||||
|
Map2(U),
|
||||||
|
MapVec1(Vec<T>),
|
||||||
|
#[allow(dead_code)]
|
||||||
|
MapVec2(Vec<U>),
|
||||||
|
Not(Response<'a>),
|
||||||
|
Ignore,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse many `T` or `U` Responses with `F` and extend `into1` or `into2` with them.
|
||||||
|
/// Responses other than `T` or `U` go into the `unsolicited` channel.
|
||||||
|
pub(crate) fn parse_many_into2<'input, T, U, F, IU, IT>(
|
||||||
|
input: &'input [u8],
|
||||||
|
into1: &mut IT,
|
||||||
|
into2: &mut IU,
|
||||||
|
unsolicited: &mut mpsc::Sender<UnsolicitedResponse>,
|
||||||
|
mut map: F,
|
||||||
|
) -> Result<()>
|
||||||
|
where
|
||||||
|
IT: Extend<T>,
|
||||||
|
IU: Extend<U>,
|
||||||
|
F: FnMut(Response<'input>) -> Result<MapOrNot2<'input, T, U>>,
|
||||||
{
|
{
|
||||||
let mut lines = input;
|
let mut lines = input;
|
||||||
loop {
|
loop {
|
||||||
|
|
@ -54,14 +98,16 @@ where
|
||||||
lines = rest;
|
lines = rest;
|
||||||
|
|
||||||
match map(resp)? {
|
match map(resp)? {
|
||||||
MapOrNot::Map(t) => into.extend(std::iter::once(t)),
|
MapOrNot2::Map1(t) => into1.extend(std::iter::once(t)),
|
||||||
MapOrNot::MapVec(t) => into.extend(t),
|
MapOrNot2::Map2(t) => into2.extend(std::iter::once(t)),
|
||||||
MapOrNot::Not(resp) => match try_handle_unilateral(resp, unsolicited) {
|
MapOrNot2::MapVec1(t) => into1.extend(t),
|
||||||
|
MapOrNot2::MapVec2(t) => into2.extend(t),
|
||||||
|
MapOrNot2::Not(resp) => match try_handle_unilateral(resp, unsolicited) {
|
||||||
Some(Response::Fetch(..)) => continue,
|
Some(Response::Fetch(..)) => continue,
|
||||||
Some(resp) => break Err(resp.into()),
|
Some(resp) => break Err(resp.into()),
|
||||||
None => {}
|
None => {}
|
||||||
},
|
},
|
||||||
MapOrNot::Ignore => continue,
|
MapOrNot2::Ignore => continue,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
|
|
@ -71,15 +117,12 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse and return an expected single `T` Response with `F`.
|
fn parse_until_done_internal<'input, T, F>(
|
||||||
/// Responses other than `T` go into the `unsolicited` channel.
|
|
||||||
///
|
|
||||||
/// If zero or more than one `T` is found then [`Error::Parse`] is returned
|
|
||||||
pub(crate) fn parse_until_done<'input, T, F>(
|
|
||||||
input: &'input [u8],
|
input: &'input [u8],
|
||||||
|
optional: bool,
|
||||||
unsolicited: &mut mpsc::Sender<UnsolicitedResponse>,
|
unsolicited: &mut mpsc::Sender<UnsolicitedResponse>,
|
||||||
map: F,
|
map: F,
|
||||||
) -> Result<T>
|
) -> Result<Option<T>>
|
||||||
where
|
where
|
||||||
F: FnMut(Response<'input>) -> Result<MapOrNot<'input, T>>,
|
F: FnMut(Response<'input>) -> Result<MapOrNot<'input, T>>,
|
||||||
{
|
{
|
||||||
|
|
@ -88,11 +131,44 @@ where
|
||||||
parse_many_into(input, &mut temp_output, unsolicited, map)?;
|
parse_many_into(input, &mut temp_output, unsolicited, map)?;
|
||||||
|
|
||||||
match temp_output.len() {
|
match temp_output.len() {
|
||||||
1 => Ok(temp_output.remove(0)),
|
1 => Ok(Some(temp_output.remove(0))),
|
||||||
|
0 if optional => Ok(None),
|
||||||
_ => Err(Error::Parse(ParseError::Invalid(input.to_vec()))),
|
_ => Err(Error::Parse(ParseError::Invalid(input.to_vec()))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parse and return an optional single `T` Response with `F`.
|
||||||
|
/// Responses other than `T` go into the `unsolicited` channel.
|
||||||
|
///
|
||||||
|
/// If more than one `T` are found then [`Error::Parse`] is returned
|
||||||
|
pub(crate) fn parse_until_done_optional<'input, T, F>(
|
||||||
|
input: &'input [u8],
|
||||||
|
unsolicited: &mut mpsc::Sender<UnsolicitedResponse>,
|
||||||
|
map: F,
|
||||||
|
) -> Result<Option<T>>
|
||||||
|
where
|
||||||
|
F: FnMut(Response<'input>) -> Result<MapOrNot<'input, T>>,
|
||||||
|
{
|
||||||
|
parse_until_done_internal(input, true, unsolicited, map)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse and return an expected single `T` Response with `F`.
|
||||||
|
/// Responses other than `T` go into the `unsolicited` channel.
|
||||||
|
///
|
||||||
|
/// If zero or more than one `T` are found then [`Error::Parse`] is returned.
|
||||||
|
pub(crate) fn parse_until_done<'input, T, F>(
|
||||||
|
input: &'input [u8],
|
||||||
|
unsolicited: &mut mpsc::Sender<UnsolicitedResponse>,
|
||||||
|
map: F,
|
||||||
|
) -> Result<T>
|
||||||
|
where
|
||||||
|
F: FnMut(Response<'input>) -> Result<MapOrNot<'input, T>>,
|
||||||
|
{
|
||||||
|
parse_until_done_internal(input, false, unsolicited, map).map(|e| {
|
||||||
|
e.expect("optional = false, so Err(Invalid) would be returned instead of Ok(None)")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn parse_expunge(
|
pub fn parse_expunge(
|
||||||
lines: Vec<u8>,
|
lines: Vec<u8>,
|
||||||
unsolicited: &mut mpsc::Sender<UnsolicitedResponse>,
|
unsolicited: &mut mpsc::Sender<UnsolicitedResponse>,
|
||||||
|
|
|
||||||
|
|
@ -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};
|
||||||
|
|
||||||
|
|
|
||||||
265
src/types/quota.rs
Normal file
265
src/types/quota.rs
Normal file
|
|
@ -0,0 +1,265 @@
|
||||||
|
use crate::error::{Error, ParseError};
|
||||||
|
use crate::parse::{parse_many_into2, parse_until_done_optional, 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(Clone, 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: impl Into<QuotaResourceName<'a>>, amount: u64) -> Self {
|
||||||
|
let name = name.into();
|
||||||
|
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, Clone)]
|
||||||
|
#[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<'a> From<&'a str> for QuotaResourceName<'a> {
|
||||||
|
fn from(input: &'a str) -> Self {
|
||||||
|
match input {
|
||||||
|
"STORAGE" => QuotaResourceName::Storage,
|
||||||
|
"MESSAGE" => QuotaResourceName::Message,
|
||||||
|
_ => QuotaResourceName::Atom(Cow::from(input)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> QuotaResourceName<'a> {
|
||||||
|
/// Get an owned version of the [`QuotaResourceName`].
|
||||||
|
pub fn into_owned(self) -> QuotaResourceName<'static> {
|
||||||
|
match self {
|
||||||
|
QuotaResourceName::Storage => QuotaResourceName::Storage,
|
||||||
|
QuotaResourceName::Message => QuotaResourceName::Message,
|
||||||
|
QuotaResourceName::Atom(n) => QuotaResourceName::Atom(Cow::Owned(n.into_owned())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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_optional(input, 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(Clone, 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[..]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_quota_resource_name_into_owned() {
|
||||||
|
let name = "TEST";
|
||||||
|
let borrowed = QuotaResourceName::Atom(Cow::Borrowed(name));
|
||||||
|
|
||||||
|
let new_owned = borrowed.into_owned();
|
||||||
|
assert!(matches!(new_owned, QuotaResourceName::Atom(Cow::Owned(_))));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_quota_resource_limit_new() {
|
||||||
|
let limit = QuotaResourceLimit::new("STORAGE", 1000);
|
||||||
|
|
||||||
|
assert_eq!(limit.name, QuotaResourceName::Storage);
|
||||||
|
assert_eq!(limit.amount, 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
31
src/utils.rs
Normal file
31
src/utils.rs
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
/// Lovingly borrowed from the cargo crate
|
||||||
|
///
|
||||||
|
/// Joins an iterator of [std::fmt::Display]'ables into an output writable
|
||||||
|
pub(crate) fn iter_join_onto<W, I, T>(mut w: W, iter: I, delim: &str) -> std::fmt::Result
|
||||||
|
where
|
||||||
|
W: std::fmt::Write,
|
||||||
|
I: IntoIterator<Item = T>,
|
||||||
|
T: std::fmt::Display,
|
||||||
|
{
|
||||||
|
let mut it = iter.into_iter().peekable();
|
||||||
|
while let Some(n) = it.next() {
|
||||||
|
write!(w, "{}", n)?;
|
||||||
|
if it.peek().is_some() {
|
||||||
|
write!(w, "{}", delim)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lovingly borrowed from the cargo crate
|
||||||
|
///
|
||||||
|
/// Joins an iterator of [std::fmt::Display]'ables to a new [std::string::String].
|
||||||
|
pub(crate) fn iter_join<I, T>(iter: I, delim: &str) -> String
|
||||||
|
where
|
||||||
|
I: IntoIterator<Item = T>,
|
||||||
|
T: std::fmt::Display,
|
||||||
|
{
|
||||||
|
let mut s = String::new();
|
||||||
|
let _ = iter_join_onto(&mut s, iter, delim);
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
if clean {
|
||||||
clean_mailbox(&mut s);
|
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,103 @@ 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);
|
||||||
|
|
||||||
|
// Set a quota
|
||||||
|
c.set_quota("INBOX", &[QuotaResourceLimit::new("STORAGE", 1000)])
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Check it
|
||||||
|
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>>();
|
||||||
|
// not sure why, but greenmail returns no quota root names
|
||||||
|
assert_eq!(root_names, Vec::<&str>::new());
|
||||||
|
|
||||||
|
assert_eq!(quota_root.quotas().len(), 1);
|
||||||
|
|
||||||
|
let quota = quota_root.quotas().first().unwrap();
|
||||||
|
assert_eq!(quota.root_name, "INBOX");
|
||||||
|
assert_quota_resource(
|
||||||
|
"a.resources[0],
|
||||||
|
QuotaResourceName::Storage,
|
||||||
|
1000,
|
||||||
|
Some(0),
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO no reliable way to delete a quota from greenmail other than resetting the whole system
|
||||||
|
// Deleting a mailbox or user in greenmail does not remove the quota
|
||||||
|
} else {
|
||||||
|
// because we are cyrus we can "test" the admin account for checking the GET/SET commands
|
||||||
|
// the clean: false is because the cyrus admin user has no INBOX.
|
||||||
|
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(
|
||||||
|
"a.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(
|
||||||
|
"a.resources[0],
|
||||||
|
QuotaResourceName::Storage,
|
||||||
|
500,
|
||||||
|
Some(0),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue