Skip to content

Commit e76cd70

Browse files
authored
Merge QUOTA support
2 parents 066a0b9 + f976dc0 commit e76cd70

File tree

7 files changed

+210
-2
lines changed

7 files changed

+210
-2
lines changed

src/client.rs

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,14 @@ use async_std::channel;
99
use async_std::io::{self, Read, Write};
1010
use async_std::net::{TcpStream, ToSocketAddrs};
1111
use async_std::prelude::*;
12+
use extensions::quota::parse_get_quota_root;
1213
use imap_proto::{RequestId, Response};
1314

1415
use super::authenticator::Authenticator;
1516
use super::error::{Error, ParseError, Result, ValidateError};
1617
use super::parse::*;
1718
use super::types::*;
18-
use crate::extensions;
19+
use crate::extensions::{self, quota::parse_get_quota};
1920
use crate::imap_stream::ImapStream;
2021

2122
macro_rules! quote {
@@ -1245,6 +1246,37 @@ impl<T: Read + Write + Unpin + fmt::Debug + Send> Session<T> {
12451246
Ok(uids)
12461247
}
12471248

1249+
/// The [`GETQUOTA` command](https://tools.ietf.org/html/rfc2087#section-4.2)
1250+
pub async fn get_quota(&mut self, quota_root: &str) -> Result<Quota> {
1251+
let id = self
1252+
.run_command(format!("GETQUOTA {}", quote!(quota_root)))
1253+
.await?;
1254+
let c = parse_get_quota(
1255+
&mut self.conn.stream,
1256+
self.unsolicited_responses_tx.clone(),
1257+
id,
1258+
)
1259+
.await?;
1260+
Ok(c)
1261+
}
1262+
1263+
/// The [`GETQUOTAROOT` command](https://tools.ietf.org/html/rfc2087#section-4.3)
1264+
pub async fn get_quota_root(
1265+
&mut self,
1266+
mailbox_name: &str,
1267+
) -> Result<(Vec<QuotaRoot>, Vec<Quota>)> {
1268+
let id = self
1269+
.run_command(format!("GETQUOTAROOT {}", quote!(mailbox_name)))
1270+
.await?;
1271+
let c = parse_get_quota_root(
1272+
&mut self.conn.stream,
1273+
self.unsolicited_responses_tx.clone(),
1274+
id,
1275+
)
1276+
.await?;
1277+
Ok(c)
1278+
}
1279+
12481280
// these are only here because they are public interface, the rest is in `Connection`
12491281
/// Runs a command and checks if it returns OK.
12501282
pub async fn run_command_and_check_ok<S: AsRef<str>>(&mut self, command: S) -> Result<()> {

src/error.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ pub enum ParseError {
5555
/// The client received data that was not UTF-8 encoded.
5656
#[error("unable to parse data ({0:?}) as UTF-8 text: {1:?}")]
5757
DataNotUtf8(Vec<u8>, #[source] Utf8Error),
58+
/// The expected response for X was not found
59+
#[error("expected response not found for: {0}")]
60+
ExpectedResponseNotFound(String),
5861
}
5962

6063
/// An [invalid character](https://tools.ietf.org/html/rfc3501#section-4.3) was found in an input

src/extensions/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
//! Implementations of various IMAP extensions.
22
pub mod idle;
3+
4+
pub mod quota;

src/extensions/quota.rs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
//! Adds support for the GETQUOTA and GETQUOTAROOT commands specificed in [RFC2087](https://tools.ietf.org/html/rfc2087).
2+
3+
use async_std::channel;
4+
use async_std::io;
5+
use async_std::prelude::*;
6+
use async_std::stream::Stream;
7+
use imap_proto::{self, RequestId, Response};
8+
9+
use crate::types::*;
10+
use crate::{
11+
error::Result,
12+
parse::{filter_sync, handle_unilateral},
13+
};
14+
use crate::{
15+
error::{Error, ParseError},
16+
types::{Quota, QuotaRoot, ResponseData},
17+
};
18+
19+
pub(crate) async fn parse_get_quota<T: Stream<Item = io::Result<ResponseData>> + Unpin>(
20+
stream: &mut T,
21+
unsolicited: channel::Sender<UnsolicitedResponse>,
22+
command_tag: RequestId,
23+
) -> Result<Quota> {
24+
let mut quota = None;
25+
while let Some(resp) = stream
26+
.take_while(|res| filter_sync(res, &command_tag))
27+
.next()
28+
.await
29+
{
30+
let resp = resp?;
31+
match resp.parsed() {
32+
Response::Quota(q) => quota = Some(q.clone().into()),
33+
_ => {
34+
handle_unilateral(resp, unsolicited.clone()).await;
35+
}
36+
}
37+
}
38+
39+
match quota {
40+
Some(q) => Ok(q),
41+
None => Err(Error::Parse(ParseError::ExpectedResponseNotFound(
42+
"Quota, no quota response found".to_string(),
43+
))),
44+
}
45+
}
46+
47+
pub(crate) async fn parse_get_quota_root<T: Stream<Item = io::Result<ResponseData>> + Unpin>(
48+
stream: &mut T,
49+
unsolicited: channel::Sender<UnsolicitedResponse>,
50+
command_tag: RequestId,
51+
) -> Result<(Vec<QuotaRoot>, Vec<Quota>)> {
52+
let mut roots: Vec<QuotaRoot> = Vec::new();
53+
let mut quotas: Vec<Quota> = Vec::new();
54+
55+
while let Some(resp) = stream
56+
.take_while(|res| filter_sync(res, &command_tag))
57+
.next()
58+
.await
59+
{
60+
let resp = resp?;
61+
match resp.parsed() {
62+
Response::QuotaRoot(qr) => {
63+
roots.push(qr.clone().into());
64+
}
65+
Response::Quota(q) => {
66+
quotas.push(q.clone().into());
67+
}
68+
_ => {
69+
handle_unilateral(resp, unsolicited.clone()).await;
70+
}
71+
}
72+
}
73+
74+
Ok((roots, quotas))
75+
}

src/parse.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ fn filter(res: &io::Result<ResponseData>, command_tag: &RequestId) -> impl Futur
4646
futures::future::ready(val)
4747
}
4848

49-
fn filter_sync(res: &io::Result<ResponseData>, command_tag: &RequestId) -> bool {
49+
pub(crate) fn filter_sync(res: &io::Result<ResponseData>, command_tag: &RequestId) -> bool {
5050
match res {
5151
Ok(res) => match res.parsed() {
5252
Response::Done { tag, .. } => tag != command_tag,

src/types/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,9 @@ pub(crate) use self::response_data::ResponseData;
213213
mod request;
214214
pub(crate) use self::request::Request;
215215

216+
mod quota;
217+
pub use self::quota::*;
218+
216219
/// Responses that the server sends that are not related to the current command.
217220
/// [RFC 3501](https://tools.ietf.org/html/rfc3501#section-7) states that clients need to be able
218221
/// to accept any response at any time. These are the ones we've encountered in the wild.

src/types/quota.rs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
use imap_proto::types::Quota as QuotaRef;
2+
use imap_proto::types::QuotaResource as QuotaResourceRef;
3+
use imap_proto::types::QuotaResourceName as QuotaResourceNameRef;
4+
use imap_proto::types::QuotaRoot as QuotaRootRef;
5+
6+
/// https://tools.ietf.org/html/rfc2087#section-3
7+
#[derive(Debug, Eq, PartialEq, Hash, Clone)]
8+
pub enum QuotaResourceName {
9+
/// Sum of messages' RFC822.SIZE, in units of 1024 octets
10+
Storage,
11+
/// Number of messages
12+
Message,
13+
/// A different/custom resource
14+
Atom(String),
15+
}
16+
17+
impl<'a> From<QuotaResourceNameRef<'a>> for QuotaResourceName {
18+
fn from(name: QuotaResourceNameRef<'_>) -> Self {
19+
match name {
20+
QuotaResourceNameRef::Message => QuotaResourceName::Message,
21+
QuotaResourceNameRef::Storage => QuotaResourceName::Storage,
22+
QuotaResourceNameRef::Atom(v) => QuotaResourceName::Atom(v.to_string()),
23+
}
24+
}
25+
}
26+
27+
/// 5.1. QUOTA Response (https://tools.ietf.org/html/rfc2087#section-5.1)
28+
#[derive(Debug, Eq, PartialEq, Hash, Clone)]
29+
pub struct QuotaResource {
30+
/// name of the resource
31+
pub name: QuotaResourceName,
32+
/// current usage of the resource
33+
pub usage: u64,
34+
/// resource limit
35+
pub limit: u64,
36+
}
37+
38+
impl<'a> From<QuotaResourceRef<'a>> for QuotaResource {
39+
fn from(resource: QuotaResourceRef<'_>) -> Self {
40+
Self {
41+
name: resource.name.into(),
42+
usage: resource.usage,
43+
limit: resource.limit,
44+
}
45+
}
46+
}
47+
48+
impl QuotaResource {
49+
/// gets the usage percentage of a QuotaResource
50+
pub fn get_usage_percentage(&self) -> u64 {
51+
self.usage.saturating_mul(100) / self.limit
52+
}
53+
}
54+
55+
/// 5.1. QUOTA Response (https://tools.ietf.org/html/rfc2087#section-5.1)
56+
#[derive(Debug, Eq, PartialEq, Hash, Clone)]
57+
pub struct Quota {
58+
/// quota root name
59+
pub root_name: String,
60+
/// quota resources for this quota
61+
pub resources: Vec<QuotaResource>,
62+
}
63+
64+
impl<'a> From<QuotaRef<'a>> for Quota {
65+
fn from(quota: QuotaRef<'_>) -> Self {
66+
Self {
67+
root_name: quota.root_name.to_string(),
68+
resources: quota.resources.iter().map(|r| r.clone().into()).collect(),
69+
}
70+
}
71+
}
72+
73+
/// 5.2. QUOTAROOT Response (https://tools.ietf.org/html/rfc2087#section-5.2)
74+
#[derive(Debug, Eq, PartialEq, Hash, Clone)]
75+
pub struct QuotaRoot {
76+
/// mailbox name
77+
pub mailbox_name: String,
78+
/// zero or more quota root names
79+
pub quota_root_names: Vec<String>,
80+
}
81+
82+
impl<'a> From<QuotaRootRef<'a>> for QuotaRoot {
83+
fn from(root: QuotaRootRef<'_>) -> Self {
84+
Self {
85+
mailbox_name: root.mailbox_name.to_string(),
86+
quota_root_names: root
87+
.quota_root_names
88+
.iter()
89+
.map(|n| n.to_string())
90+
.collect(),
91+
}
92+
}
93+
}

0 commit comments

Comments
 (0)