implement ACL extension
This commit is contained in:
parent
21ea164e3e
commit
b12eda4924
7 changed files with 869 additions and 7 deletions
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"]}
|
||||
|
|
|
|||
358
src/client.rs
358
src/client.rs
|
|
@ -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\
|
||||
|
|
|
|||
22
src/parse.rs
22
src/parse.rs
|
|
@ -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
391
src/types/acls.rs
Normal 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')));
|
||||
}
|
||||
}
|
||||
|
|
@ -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};
|
||||
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue