implement ACL extension

This commit is contained in:
Edward Rudd 2022-04-23 15:25:45 -04:00
parent 21ea164e3e
commit b12eda4924
7 changed files with 869 additions and 7 deletions

View file

@ -48,7 +48,7 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: test
args: -- --ignored
args: --features test-full-imap
services:
cyrus_imapd:
image: outoforder/cyrus-imapd-tester:latest

View file

@ -16,13 +16,15 @@ categories = ["email", "network-programming"]
[features]
rustls-tls = ["rustls-connector"]
default = ["native-tls"]
# Used to activate full integration tests when running against a more complete IMAP server
test-full-imap = []
[dependencies]
native-tls = { version = "0.2.2", optional = true }
rustls-connector = { version = "0.16.1", optional = true }
regex = "1.0"
bufstream = "0.1.3"
imap-proto = "0.16.0"
imap-proto = "0.16.1"
nom = { version = "7.1.0", default-features = false }
base64 = "0.13"
chrono = { version = "0.4", default-features = false, features = ["std"]}

View file

@ -432,10 +432,10 @@ impl<T: Read + Write> Client<T> {
/// };
/// }
/// ```
pub fn authenticate(
pub fn authenticate<A: Authenticator>(
mut self,
auth_type: impl AsRef<str>,
authenticator: &impl Authenticator,
authenticator: &A,
) -> ::std::result::Result<Session<T>, (Error, Client<T>)> {
ok_or_unauth_client_err!(
self.run_command(&format!("AUTHENTICATE {}", auth_type.as_ref())),
@ -1284,6 +1284,104 @@ impl<T: Read + Write> Session<T> {
.and_then(|lines| parse_id_seq(&lines, &mut self.unsolicited_responses_tx))
}
/// The [`SETACL` command](https://datatracker.ietf.org/doc/html/rfc4314#section-3.1)
///
/// Modifies the ACLs on the given mailbox for the specified identifier.
/// Return [`Error::No`] if the logged in user does not have `a` rights on the mailbox.
///
/// This method only works against a server with the ACL capability. Otherwise [`Error::Bad`]
/// will be returned
pub fn set_acl(
&mut self,
mailbox_name: impl AsRef<str>,
identifier: impl AsRef<str>,
rights: &AclRights,
modification: AclModifyMode,
) -> Result<()> {
let mod_str = match modification {
AclModifyMode::Replace => "",
AclModifyMode::Add => "+",
AclModifyMode::Remove => "-",
};
self.run_command_and_check_ok(&format!(
"SETACL {} {} {}{}",
validate_str("SETACL", "mailbox", mailbox_name.as_ref())?,
validate_str("SETACL", "identifier", identifier.as_ref())?,
mod_str,
rights,
))
}
/// The [`DELETEACL` command](https://datatracker.ietf.org/doc/html/rfc4314#section-3.2)
///
/// Removes the ACL for the given identifier from the given mailbox.
/// Return [`Error::No`] if the logged in user does not have `a` rights on the mailbox.
///
/// This method only works against a server with the ACL capability. Otherwise [`Error::Bad`]
/// will be returned
pub fn delete_acl(
&mut self,
mailbox_name: impl AsRef<str>,
identifier: impl AsRef<str>,
) -> Result<()> {
self.run_command_and_check_ok(&format!(
"DELETEACL {} {}",
validate_str("DELETEACL", "mailbox", mailbox_name.as_ref())?,
validate_str("DELETEACL", "identifier", identifier.as_ref())?,
))
}
/// The [`GETACL` command](https://datatracker.ietf.org/doc/html/rfc4314#section-3.3)
///
/// Returns the ACLs on the given mailbox. A set ot `ACL` responses are returned if the
/// logged in user has `a` rights on the mailbox. Otherwise, will return [`Error::No`].
///
/// This method only works against a server with the ACL capability. Otherwise [`Error::Bad`]
/// will be returned
pub fn get_acl(&mut self, mailbox_name: impl AsRef<str>) -> Result<AclResponse> {
self.run_command_and_read_response(&format!(
"GETACL {}",
validate_str("GETACL", "mailbox", mailbox_name.as_ref())?
))
.and_then(|lines| AclResponse::parse(lines, &mut self.unsolicited_responses_tx))
}
/// The [`LISTRIGHTS` command](https://datatracker.ietf.org/doc/html/rfc4314#section-3.4)
///
/// Returns the always granted and optionally granted rights on the given mailbox for the
/// specified identifier (login). A set ot `LISTRIGHTS` responses are returned if the
/// logged in user has `a` rights on the mailbox. Otherwise, will return [`Error::No`].
///
/// This method only works against a server with the ACL capability. Otherwise [`Error::Bad`]
/// will be returned
pub fn list_rights(
&mut self,
mailbox_name: impl AsRef<str>,
identifier: impl AsRef<str>,
) -> Result<ListRightsResponse> {
self.run_command_and_read_response(&format!(
"LISTRIGHTS {} {}",
validate_str("LISTRIGHTS", "mailbox", mailbox_name.as_ref())?,
validate_str("LISTRIGHTS", "identifier", identifier.as_ref())?
))
.and_then(|lines| ListRightsResponse::parse(lines, &mut self.unsolicited_responses_tx))
}
/// The [`MYRIGHTS` command](https://datatracker.ietf.org/doc/html/rfc4314#section-3.5)
///
/// Returns the list of rights the logged in user has on the given mailbox.
///
/// This method only works against a server with the ACL capability. Otherwise [`Error::Bad`]
/// will be returned
pub fn my_rights(&mut self, mailbox_name: impl AsRef<str>) -> Result<MyRightsResponse> {
self.run_command_and_read_response(&format!(
"MYRIGHTS {}",
validate_str("MYRIGHTS", "mailbox", mailbox_name.as_ref())?,
))
.and_then(|lines| MyRightsResponse::parse(lines, &mut self.unsolicited_responses_tx))
}
// these are only here because they are public interface, the rest is in `Connection`
/// Runs a command and checks if it returns OK.
pub fn run_command_and_check_ok(&mut self, command: impl AsRef<str>) -> Result<()> {
@ -1550,7 +1648,7 @@ mod tests {
use super::super::error::Result;
use super::super::mock_stream::MockStream;
use super::*;
use imap_proto::types::*;
use imap_proto::types::Capability;
use std::borrow::Cow;
use super::testutils::*;
@ -2024,6 +2122,260 @@ mod tests {
assert_eq!(ids, [1, 2, 3, 4, 5].iter().cloned().collect::<Vec<_>>());
}
#[test]
fn set_acl_replace() {
let response = b"a1 OK completed\r\n".to_vec();
let mock_stream = MockStream::new(response);
let mut session = mock_session!(mock_stream);
session
.set_acl(
"INBOX",
"myuser",
&"x".try_into().unwrap(),
AclModifyMode::Replace,
)
.unwrap();
assert_eq!(
session.stream.get_ref().written_buf,
b"a1 SETACL \"INBOX\" \"myuser\" x\r\n".to_vec(),
"Invalid setacl command"
);
}
#[test]
fn set_acl_add() {
let response = b"a1 OK completed\r\n".to_vec();
let mock_stream = MockStream::new(response);
let mut session = mock_session!(mock_stream);
session
.set_acl(
"INBOX",
"myuser",
&"x".try_into().unwrap(),
AclModifyMode::Add,
)
.unwrap();
assert_eq!(
session.stream.get_ref().written_buf,
b"a1 SETACL \"INBOX\" \"myuser\" +x\r\n".to_vec(),
"Invalid setacl command"
);
}
#[test]
fn set_acl_remove() {
let response = b"a1 OK completed\r\n".to_vec();
let mock_stream = MockStream::new(response);
let mut session = mock_session!(mock_stream);
session
.set_acl(
"INBOX",
"myuser",
&"x".try_into().unwrap(),
AclModifyMode::Remove,
)
.unwrap();
assert_eq!(
session.stream.get_ref().written_buf,
b"a1 SETACL \"INBOX\" \"myuser\" -x\r\n".to_vec(),
"Invalid setacl command"
);
}
#[test]
fn delete_acl() {
let response = b"a1 OK completed\r\n".to_vec();
let mock_stream = MockStream::new(response);
let mut session = mock_session!(mock_stream);
session.delete_acl("INBOX", "myuser").unwrap();
assert_eq!(
session.stream.get_ref().written_buf,
b"a1 DELETEACL \"INBOX\" \"myuser\"\r\n".to_vec(),
"Invalid deleteacl command"
);
}
#[test]
fn get_acl() {
let response = b"* ACL INBOX myuser lr\r\n\
a1 OK completed\r\n"
.to_vec();
let mock_stream = MockStream::new(response);
let mut session = mock_session!(mock_stream);
let acl = session.get_acl("INBOX").unwrap();
assert_eq!(
session.stream.get_ref().written_buf,
b"a1 GETACL \"INBOX\"\r\n".to_vec(),
"Invalid getacl command"
);
assert_eq!(acl.parsed().mailbox(), "INBOX");
assert_eq!(
acl.parsed().acls(),
vec![AclEntry {
identifier: "myuser".into(),
rights: "lr".try_into().unwrap(),
}]
);
}
#[test]
fn get_acl_invalid_no_acl_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 acl = session.get_acl("INBOX");
assert_eq!(
session.stream.get_ref().written_buf,
b"a1 GETACL \"INBOX\"\r\n".to_vec(),
"Invalid getacl command"
);
assert!(matches!(acl, Err(Error::Parse(_))));
}
#[test]
fn get_acl_invalid_too_many_acl_lines() {
let response = b"* ACL INBOX myuser lr\r\n\
* ACL INBOX myuser lr\r\n\
a1 OK completed\r\n"
.to_vec();
let mock_stream = MockStream::new(response);
let mut session = mock_session!(mock_stream);
let acl = session.get_acl("INBOX");
assert_eq!(
session.stream.get_ref().written_buf,
b"a1 GETACL \"INBOX\"\r\n".to_vec(),
"Invalid getacl command"
);
assert!(matches!(acl, Err(Error::Parse(_))));
}
#[test]
fn get_acl_multiple_users() {
let response = b"* ACL INBOX myuser lr other_user lr\r\n\
a1 OK completed\r\n"
.to_vec();
let mock_stream = MockStream::new(response);
let mut session = mock_session!(mock_stream);
let acl = session.get_acl("INBOX").unwrap();
assert_eq!(
session.stream.get_ref().written_buf,
b"a1 GETACL \"INBOX\"\r\n".to_vec(),
"Invalid getacl command"
);
assert_eq!(acl.parsed().mailbox(), "INBOX");
assert_eq!(
acl.parsed().acls(),
vec![
AclEntry {
identifier: "myuser".into(),
rights: "lr".try_into().unwrap(),
},
AclEntry {
identifier: "other_user".into(),
rights: "lr".try_into().unwrap(),
},
]
);
}
#[test]
fn list_rights() {
let response = b"* LISTRIGHTS INBOX myuser lr x k\r\n\
a1 OK completed\r\n"
.to_vec();
let mock_stream = MockStream::new(response);
let mut session = mock_session!(mock_stream);
let acl = session.list_rights("INBOX", "myuser").unwrap();
assert_eq!(
session.stream.get_ref().written_buf,
b"a1 LISTRIGHTS \"INBOX\" \"myuser\"\r\n".to_vec(),
"Invalid listrights command"
);
assert_eq!(acl.parsed().mailbox(), "INBOX");
assert_eq!(acl.parsed().identifier(), "myuser");
assert_eq!(*acl.parsed().required(), "lr".try_into().unwrap());
assert_eq!(*acl.parsed().optional(), "kx".try_into().unwrap());
}
#[test]
fn list_rights_invalid_no_rights_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 acl = session.list_rights("INBOX", "myuser");
assert_eq!(
session.stream.get_ref().written_buf,
b"a1 LISTRIGHTS \"INBOX\" \"myuser\"\r\n".to_vec(),
"Invalid listrights command"
);
assert!(matches!(acl, Err(Error::Parse(_))));
}
#[test]
fn list_rights_invalid_too_many_rights_lines() {
let response = b"* LISTRIGHTS INBOX myuser lr x k\r\n\
* LISTRIGHTS INBOX myuser lr x k\r\n\
a1 OK completed\r\n"
.to_vec();
let mock_stream = MockStream::new(response);
let mut session = mock_session!(mock_stream);
let acl = session.list_rights("INBOX", "myuser");
assert_eq!(
session.stream.get_ref().written_buf,
b"a1 LISTRIGHTS \"INBOX\" \"myuser\"\r\n".to_vec(),
"Invalid listrights command"
);
assert!(matches!(acl, Err(Error::Parse(_))));
}
#[test]
fn my_rights() {
let response = b"* MYRIGHTS INBOX lrxk\r\n\
a1 OK completed\r\n"
.to_vec();
let mock_stream = MockStream::new(response);
let mut session = mock_session!(mock_stream);
let acl = session.my_rights("INBOX").unwrap();
assert_eq!(
session.stream.get_ref().written_buf,
b"a1 MYRIGHTS \"INBOX\"\r\n".to_vec(),
"Invalid myrights command"
);
assert_eq!(acl.parsed().mailbox(), "INBOX");
assert_eq!(*acl.parsed().rights(), "lrkx".try_into().unwrap())
}
#[test]
fn my_rights_invalid_no_rights_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 acl = session.my_rights("INBOX");
assert_eq!(
session.stream.get_ref().written_buf,
b"a1 MYRIGHTS \"INBOX\"\r\n".to_vec(),
"Invalid myrights command"
);
assert!(matches!(acl, Err(Error::Parse(_))));
}
#[test]
fn my_rights_invalid_too_many_rights_lines() {
let response = b"* MYRIGHTS INBOX lrxk\r\n\
* MYRIGHTS INBOX lrxk\r\n\
a1 OK completed\r\n"
.to_vec();
let mock_stream = MockStream::new(response);
let mut session = mock_session!(mock_stream);
let acl = session.my_rights("INBOX");
assert_eq!(
session.stream.get_ref().written_buf,
b"a1 MYRIGHTS \"INBOX\"\r\n".to_vec(),
"Invalid myrights command"
);
assert!(matches!(acl, Err(Error::Parse(_))));
}
#[test]
fn capability() {
let response = b"* CAPABILITY IMAP4rev1 STARTTLS AUTH=GSSAPI LOGINDISABLED\r\n\

View file

@ -71,6 +71,28 @@ where
}
}
/// 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` is 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>>,
{
let mut temp_output = Vec::<T>::new();
parse_many_into(input, &mut temp_output, unsolicited, map)?;
match temp_output.len() {
1 => Ok(temp_output.remove(0)),
_ => Err(Error::Parse(ParseError::Invalid(input.to_vec()))),
}
}
pub fn parse_expunge(
lines: Vec<u8>,
unsolicited: &mut mpsc::Sender<UnsolicitedResponse>,

391
src/types/acls.rs Normal file
View file

@ -0,0 +1,391 @@
use crate::error::Error;
use crate::parse::{parse_until_done, MapOrNot};
use crate::types::UnsolicitedResponse;
#[cfg(doc)]
use crate::Session;
use imap_proto::types::AclRight;
use imap_proto::Response;
use ouroboros::self_referencing;
use std::borrow::Cow;
use std::collections::HashSet;
use std::fmt::{Display, Formatter};
use std::sync::mpsc;
/// Specifies how [`Session::set_acl`] should modify an existing permission set.
#[derive(Debug, Clone, Copy)]
pub enum AclModifyMode {
/// Replace all ACLs on the identifier for the mailbox
Replace,
/// Add the given ACLs to the identifier for the mailbox
Add,
/// Remove the given ACLs from the identifier for the mailbox
Remove,
}
/// A set of [`imap_proto::AclRight`]s.
///
/// Used as input for [`Session::set_acl`] as output in [`ListRights`], [`MyRights`], and [`AclEntry`]
#[derive(Debug, Eq, PartialEq, Clone)]
pub struct AclRights {
pub(crate) data: HashSet<AclRight>,
}
impl AclRights {
/// Returns true if the AclRights has the provided ACL (either as a char or an AclRight enum)
pub fn contains<T: Into<AclRight>>(&self, right: T) -> bool {
self.data.contains(&right.into())
}
}
impl Display for AclRights {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let mut v: Vec<char> = self.data.iter().map(|c| char::from(*c)).collect();
v.sort_unstable();
write!(f, "{}", v.into_iter().collect::<String>())
}
}
impl From<HashSet<AclRight>> for AclRights {
fn from(hash: HashSet<AclRight>) -> Self {
Self { data: hash }
}
}
impl From<Vec<AclRight>> for AclRights {
fn from(vec: Vec<AclRight>) -> Self {
AclRights {
data: vec.into_iter().collect(),
}
}
}
impl TryFrom<&str> for AclRights {
type Error = AclRightError;
fn try_from(input: &str) -> Result<Self, Self::Error> {
if !input
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit())
{
return Err(AclRightError::InvalidRight);
}
Ok(input
.chars()
.map(|c| c.into())
.collect::<HashSet<AclRight>>()
.into())
}
}
/// Error from parsing AclRight strings
#[derive(Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum AclRightError {
/// Returned when a non-lower-case alpha numeric is provided in the rights list string.
InvalidRight,
}
impl Display for AclRightError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match *self {
AclRightError::InvalidRight => {
write!(f, "Rights may only be lowercase alpha numeric characters")
}
}
}
}
impl std::error::Error for AclRightError {}
/// From [section 3.6 of RFC 4313](https://datatracker.ietf.org/doc/html/rfc4314#section-3.6).
///
/// This is a wrapper around a single [`Acl`].
///
/// The ACL response from the [`Session::get_acl`] IMAP command
#[self_referencing]
pub struct AclResponse {
data: Vec<u8>,
#[borrows(data)]
#[covariant]
pub(crate) acl: Acl<'this>,
}
impl AclResponse {
/// Parse the given input into a [`Acl`] response.
pub fn parse(
owned: Vec<u8>,
unsolicited: &mut mpsc::Sender<UnsolicitedResponse>,
) -> Result<Self, Error> {
AclResponseTryBuilder {
data: owned,
acl_builder: |input| {
// There should only be ONE single ACL response
parse_until_done(input, unsolicited, |response| match response {
Response::Acl(a) => Ok(MapOrNot::Map(Acl {
mailbox: a.mailbox,
acls: a
.acls
.into_iter()
.map(|e| AclEntry {
identifier: e.identifier,
rights: e.rights.into(),
})
.collect(),
})),
resp => Ok(MapOrNot::Not(resp)),
})
},
}
.try_build()
}
/// Access to the wrapped [`ListRights`] struct
pub fn parsed(&self) -> &Acl<'_> {
self.borrow_acl()
}
}
/// From [section 3.6 of RFC 4313](https://datatracker.ietf.org/doc/html/rfc4314#section-3.6).
///
/// Used by [`AclResponse`].
#[derive(Debug, Eq, PartialEq)]
pub struct Acl<'a> {
/// The mailbox the ACL Entries belong to
pub(crate) mailbox: Cow<'a, str>,
/// The list of identifier/rights pairs for the mailbox
pub(crate) acls: Vec<AclEntry<'a>>,
}
impl<'a> Acl<'a> {
/// Return the mailbox the ACL entries belong to
pub fn mailbox(&self) -> &str {
&*self.mailbox
}
/// Returns a list of identifier/rights pairs for the mailbox
pub fn acls(&self) -> &[AclEntry<'_>] {
&*self.acls
}
}
/// From [section 3.6 of RFC 4313](https://datatracker.ietf.org/doc/html/rfc4314#section-3.6).
///
/// The list of identifiers and rights for the [`Acl`] response
#[derive(Debug, Eq, PartialEq, Clone)]
#[non_exhaustive]
pub struct AclEntry<'a> {
/// The user identifier the rights are for
pub identifier: Cow<'a, str>,
/// the rights for the provided identifier
pub rights: AclRights,
}
/// From [section 3.7 of RFC 4313](https://datatracker.ietf.org/doc/html/rfc4314#section-3.7).
///
/// This is a wrapper around a single [`ListRights`].
///
/// The LISTRIGHTS response from the [`Session::list_rights`] IMAP command
#[self_referencing]
pub struct ListRightsResponse {
data: Vec<u8>,
#[borrows(data)]
#[covariant]
pub(crate) rights: ListRights<'this>,
}
impl ListRightsResponse {
/// Parse the given input into a [`ListRights`] response.
pub fn parse(
owned: Vec<u8>,
unsolicited: &mut mpsc::Sender<UnsolicitedResponse>,
) -> Result<Self, Error> {
ListRightsResponseTryBuilder {
data: owned,
rights_builder: |input| {
// There should only be ONE single LISTRIGHTS response
parse_until_done(input, unsolicited, |response| match response {
Response::ListRights(a) => Ok(MapOrNot::Map(ListRights {
mailbox: a.mailbox,
identifier: a.identifier,
required: a.required.into(),
optional: a.optional.into(),
})),
resp => Ok(MapOrNot::Not(resp)),
})
},
}
.try_build()
}
/// Access to the wrapped [`ListRights`] struct
pub fn parsed(&self) -> &ListRights<'_> {
self.borrow_rights()
}
}
/// From [section 3.7 of RFC 4313](https://datatracker.ietf.org/doc/html/rfc4314#section-3.7).
///
/// Used by [`ListRightsResponse`].
#[derive(Debug, Eq, PartialEq)]
pub struct ListRights<'a> {
/// The mailbox for the rights
pub(crate) mailbox: Cow<'a, str>,
/// The user identifier for the rights
pub(crate) identifier: Cow<'a, str>,
/// The set of rights that are always provided for this identifier
pub(crate) required: AclRights,
/// The set of rights that can be granted to the identifier
pub(crate) optional: AclRights,
}
impl ListRights<'_> {
/// Returns the mailbox for the rights
pub fn mailbox(&self) -> &str {
&*self.mailbox
}
/// Returns the user identifier for the rights
pub fn identifier(&self) -> &str {
&*self.identifier
}
/// Returns the set of rights that are always provided for this identifier
pub fn required(&self) -> &AclRights {
&self.required
}
/// Returns the set of rights that can be granted to the identifier
pub fn optional(&self) -> &AclRights {
&self.optional
}
}
/// From [section 3.8 of RFC 4313](https://datatracker.ietf.org/doc/html/rfc4314#section-3.8).
///
/// This is a wrapper around a single [`MyRights`].
///
/// The MYRIGHTS response from the [`Session::my_rights`] IMAP command
#[self_referencing]
pub struct MyRightsResponse {
data: Vec<u8>,
#[borrows(data)]
#[covariant]
pub(crate) rights: MyRights<'this>,
}
impl MyRightsResponse {
/// Parse the given input into a [`MyRights`] response.
pub fn parse(
owned: Vec<u8>,
unsolicited: &mut mpsc::Sender<UnsolicitedResponse>,
) -> Result<Self, Error> {
MyRightsResponseTryBuilder {
data: owned,
rights_builder: |input| {
// There should only be ONE single MYRIGHTS response
parse_until_done(input, unsolicited, |response| match response {
Response::MyRights(a) => Ok(MapOrNot::Map(MyRights {
mailbox: a.mailbox,
rights: a.rights.into(),
})),
resp => Ok(MapOrNot::Not(resp)),
})
},
}
.try_build()
}
/// Access to the wrapped [`MyRights`] struct
pub fn parsed(&self) -> &MyRights<'_> {
self.borrow_rights()
}
}
/// From [section 3.8 of RFC 4313](https://datatracker.ietf.org/doc/html/rfc4314#section-3.8).
///
/// Used by [`MyRightsResponse`].
#[derive(Debug, Eq, PartialEq)]
pub struct MyRights<'a> {
/// The mailbox for the rights
pub(crate) mailbox: Cow<'a, str>,
/// The rights for the mailbox
pub(crate) rights: AclRights,
}
impl MyRights<'_> {
/// Returns the mailbox for the rights
pub fn mailbox(&self) -> &str {
&*self.mailbox
}
/// Returns the rights for the mailbox
pub fn rights(&self) -> &AclRights {
&self.rights
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_acl_rights_to_string() {
let rights: AclRights = vec![
AclRight::Lookup,
AclRight::Read,
AclRight::Seen,
AclRight::Custom('0'),
]
.into();
let expected = "0lrs";
assert_eq!(rights.to_string(), expected);
}
#[test]
fn test_str_to_acl_rights() {
let right_string = "lrskx0";
let rights: Result<AclRights, _> = right_string.try_into();
assert_eq!(
rights,
Ok(vec![
AclRight::Lookup,
AclRight::Read,
AclRight::Seen,
AclRight::CreateMailbox,
AclRight::DeleteMailbox,
AclRight::Custom('0'),
]
.into())
);
}
#[test]
fn test_str_to_acl_rights_invalid_right_character() {
let right_string = "l_";
let rights: Result<AclRights, _> = right_string.try_into();
assert_eq!(rights, Err(AclRightError::InvalidRight));
assert_eq!(
format!("{}", rights.unwrap_err()),
"Rights may only be lowercase alpha numeric characters"
);
}
#[test]
fn test_acl_rights_contains() {
let rights: AclRights = "lrskx".try_into().unwrap();
assert!(rights.contains('l'));
assert!(rights.contains(AclRight::Lookup));
assert!(!rights.contains('0'));
assert!(!rights.contains(AclRight::Custom('0')));
}
}

View file

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

View file

@ -27,6 +27,7 @@ fn test_smtp_host() -> 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 {
std::env::var("TEST_IMAP_PORT")
.unwrap_or("3143".to_string())
@ -109,10 +110,10 @@ fn smtp(user: &str) -> lettre::SmtpTransport {
}
#[test]
#[ignore]
#[cfg(feature = "test-full-imap")]
fn connect_insecure_then_secure() {
let host = test_host();
// ignored because of https://github.com/greenmail-mail-test/greenmail/issues/135
// Not supported on greenmail because of https://github.com/greenmail-mail-test/greenmail/issues/135
imap::ClientBuilder::new(&host, test_imap_port())
.starttls()
.connect(|domain, tcp| {
@ -509,6 +510,97 @@ fn append_with_flags_and_date() {
assert_eq!(inbox.len(), 0);
}
#[test]
#[cfg(feature = "test-full-imap")]
fn acl_tests() {
use imap::types::AclModifyMode;
let user_friend = "inbox-acl-friend@localhost";
let user_me = "inbox-acl@localhost";
// ensure we have this user by logging in once
session(user_friend);
let mut s_me = session(user_me);
let acl = s_me.get_acl("INBOX").unwrap();
// one ACL
// assert_eq!(acl.acls().len(), 1);
// ACL is for me
assert_eq!(acl.parsed().acls()[0].identifier, user_me);
// ACL has administration rights
assert!(acl.parsed().acls()[0].rights.contains('a'));
// Grant read to friend
let ret = s_me.set_acl(
"INBOX",
user_friend,
&"lr".try_into().unwrap(),
AclModifyMode::Replace,
);
assert!(ret.is_ok());
// Check rights again
let acl = s_me.get_acl("INBOX").unwrap();
assert_eq!(acl.parsed().acls().len(), 2);
let idx = acl
.parsed()
.acls()
.binary_search_by(|e| (*e.identifier).cmp(user_friend))
.unwrap();
assert_eq!(acl.parsed().acls()[idx].rights, "lr".try_into().unwrap());
// Add "p" right (post)
let ret = s_me.set_acl(
"INBOX",
user_friend,
&"p".try_into().unwrap(),
AclModifyMode::Add,
);
assert!(ret.is_ok());
// Check rights again
let acl = s_me.get_acl("INBOX").unwrap();
assert_eq!(acl.parsed().acls().len(), 2);
let idx = acl
.parsed()
.acls()
.binary_search_by(|e| (*e.identifier).cmp(user_friend))
.unwrap();
assert_eq!(acl.parsed().acls()[idx].rights, "lrp".try_into().unwrap());
// remove "p" right (post)
let ret = s_me.set_acl(
"INBOX",
user_friend,
&"p".try_into().unwrap(),
AclModifyMode::Remove,
);
assert!(ret.is_ok());
// Check rights again
let acl = s_me.get_acl("INBOX").unwrap();
assert_eq!(acl.parsed().acls().len(), 2);
let idx = acl
.parsed()
.acls()
.binary_search_by(|e| (*e.identifier).cmp(user_friend))
.unwrap();
assert_eq!(acl.parsed().acls()[idx].rights, "lr".try_into().unwrap());
// Delete rights for friend
let ret = s_me.delete_acl("INBOX", user_friend);
assert!(ret.is_ok());
// Check rights again
let acl = s_me.get_acl("INBOX").unwrap();
assert_eq!(acl.parsed().acls().len(), 1);
assert_eq!(acl.parsed().acls()[0].identifier, user_me);
// List rights
let acl = s_me.list_rights("INBOX", user_friend).unwrap();
assert_eq!(acl.parsed().mailbox(), "INBOX");
assert_eq!(acl.parsed().identifier(), user_friend);
assert!(acl.parsed().optional().contains('0'));
assert!(!acl.parsed().required().contains('0'));
// My Rights
let acl = s_me.my_rights("INBOX").unwrap();
assert_eq!(acl.parsed().mailbox(), "INBOX");
assert!(acl.parsed().rights().contains('a'));
}
#[test]
fn status() {
let mut s = session("readonly-test@localhost");