Merge pull request #227 from urkle/feat-acl-protocol
add in ACL extension support
This commit is contained in:
commit
1a81abbdb8
7 changed files with 926 additions and 64 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"]}
|
||||
|
|
|
|||
472
src/client.rs
472
src/client.rs
|
|
@ -374,10 +374,10 @@ impl<T: Read + Write> Client<T> {
|
|||
/// }
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn login<U: AsRef<str>, P: AsRef<str>>(
|
||||
pub fn login(
|
||||
mut self,
|
||||
username: U,
|
||||
password: P,
|
||||
username: impl AsRef<str>,
|
||||
password: impl AsRef<str>,
|
||||
) -> ::std::result::Result<Session<T>, (Error, Client<T>)> {
|
||||
let synopsis = "LOGIN";
|
||||
let u =
|
||||
|
|
@ -432,9 +432,9 @@ impl<T: Read + Write> Client<T> {
|
|||
/// };
|
||||
/// }
|
||||
/// ```
|
||||
pub fn authenticate<A: Authenticator, S: AsRef<str>>(
|
||||
pub fn authenticate<A: Authenticator>(
|
||||
mut self,
|
||||
auth_type: S,
|
||||
auth_type: impl AsRef<str>,
|
||||
authenticator: &A,
|
||||
) -> ::std::result::Result<Session<T>, (Error, Client<T>)> {
|
||||
ok_or_unauth_client_err!(
|
||||
|
|
@ -528,7 +528,7 @@ impl<T: Read + Write> Session<T> {
|
|||
/// [`Connection::run_command_and_read_response`], you *may* see additional untagged `RECENT`,
|
||||
/// `EXISTS`, `FETCH`, and `EXPUNGE` responses. You can get them from the
|
||||
/// `unsolicited_responses` channel of the [`Session`](struct.Session.html).
|
||||
pub fn select<S: AsRef<str>>(&mut self, mailbox_name: S) -> Result<Mailbox> {
|
||||
pub fn select(&mut self, mailbox_name: impl AsRef<str>) -> Result<Mailbox> {
|
||||
self.run(&format!(
|
||||
"SELECT {}",
|
||||
validate_str("SELECT", "mailbox", mailbox_name.as_ref())?
|
||||
|
|
@ -540,7 +540,7 @@ impl<T: Read + Write> Session<T> {
|
|||
/// however, the selected mailbox is identified as read-only. No changes to the permanent state
|
||||
/// of the mailbox, including per-user state, will happen in a mailbox opened with `examine`;
|
||||
/// in particular, messagess cannot lose [`Flag::Recent`] in an examined mailbox.
|
||||
pub fn examine<S: AsRef<str>>(&mut self, mailbox_name: S) -> Result<Mailbox> {
|
||||
pub fn examine(&mut self, mailbox_name: impl AsRef<str>) -> Result<Mailbox> {
|
||||
self.run(&format!(
|
||||
"EXAMINE {}",
|
||||
validate_str("EXAMINE", "mailbox", mailbox_name.as_ref())?
|
||||
|
|
@ -606,11 +606,11 @@ impl<T: Read + Write> Session<T> {
|
|||
/// - `RFC822.HEADER`: Functionally equivalent to `BODY.PEEK[HEADER]`.
|
||||
/// - `RFC822.SIZE`: The [RFC-2822](https://tools.ietf.org/html/rfc2822) size of the message.
|
||||
/// - `UID`: The unique identifier for the message.
|
||||
pub fn fetch<S1, S2>(&mut self, sequence_set: S1, query: S2) -> Result<Fetches>
|
||||
where
|
||||
S1: AsRef<str>,
|
||||
S2: AsRef<str>,
|
||||
{
|
||||
pub fn fetch(
|
||||
&mut self,
|
||||
sequence_set: impl AsRef<str>,
|
||||
query: impl AsRef<str>,
|
||||
) -> Result<Fetches> {
|
||||
if sequence_set.as_ref().is_empty() {
|
||||
Fetches::parse(vec![], &mut self.unsolicited_responses_tx)
|
||||
} else {
|
||||
|
|
@ -626,11 +626,11 @@ impl<T: Read + Write> Session<T> {
|
|||
|
||||
/// Equivalent to [`Session::fetch`], except that all identifiers in `uid_set` are
|
||||
/// [`Uid`]s. See also the [`UID` command](https://tools.ietf.org/html/rfc3501#section-6.4.8).
|
||||
pub fn uid_fetch<S1, S2>(&mut self, uid_set: S1, query: S2) -> Result<Fetches>
|
||||
where
|
||||
S1: AsRef<str>,
|
||||
S2: AsRef<str>,
|
||||
{
|
||||
pub fn uid_fetch(
|
||||
&mut self,
|
||||
uid_set: impl AsRef<str>,
|
||||
query: impl AsRef<str>,
|
||||
) -> Result<Fetches> {
|
||||
if uid_set.as_ref().is_empty() {
|
||||
Fetches::parse(vec![], &mut self.unsolicited_responses_tx)
|
||||
} else {
|
||||
|
|
@ -688,7 +688,7 @@ impl<T: Read + Write> Session<T> {
|
|||
/// the mailbox UNLESS the new incarnation has a different unique identifier validity value.
|
||||
/// See the description of the [`UID`
|
||||
/// command](https://tools.ietf.org/html/rfc3501#section-6.4.8) for more detail.
|
||||
pub fn create<S: AsRef<str>>(&mut self, mailbox_name: S) -> Result<()> {
|
||||
pub fn create(&mut self, mailbox_name: impl AsRef<str>) -> Result<()> {
|
||||
self.run_command_and_check_ok(&format!(
|
||||
"CREATE {}",
|
||||
validate_str("CREATE", "mailbox", mailbox_name.as_ref())?
|
||||
|
|
@ -714,7 +714,7 @@ impl<T: Read + Write> Session<T> {
|
|||
/// incarnation, UNLESS the new incarnation has a different unique identifier validity value.
|
||||
/// See the description of the [`UID`
|
||||
/// command](https://tools.ietf.org/html/rfc3501#section-6.4.8) for more detail.
|
||||
pub fn delete<S: AsRef<str>>(&mut self, mailbox_name: S) -> Result<()> {
|
||||
pub fn delete(&mut self, mailbox_name: impl AsRef<str>) -> Result<()> {
|
||||
self.run_command_and_check_ok(&format!(
|
||||
"DELETE {}",
|
||||
validate_str("DELETE", "mailbox", mailbox_name.as_ref())?
|
||||
|
|
@ -746,7 +746,7 @@ impl<T: Read + Write> Session<T> {
|
|||
/// to a new mailbox with the given name, leaving `INBOX` empty. If the server implementation
|
||||
/// supports inferior hierarchical names of `INBOX`, these are unaffected by a rename of
|
||||
/// `INBOX`.
|
||||
pub fn rename<S1: AsRef<str>, S2: AsRef<str>>(&mut self, from: S1, to: S2) -> Result<()> {
|
||||
pub fn rename(&mut self, from: impl AsRef<str>, to: impl AsRef<str>) -> Result<()> {
|
||||
self.run_command_and_check_ok(&format!(
|
||||
"RENAME {} {}",
|
||||
quote!(from.as_ref()),
|
||||
|
|
@ -762,7 +762,7 @@ impl<T: Read + Write> Session<T> {
|
|||
/// The server may validate the mailbox argument to `SUBSCRIBE` to verify that it exists.
|
||||
/// However, it will not unilaterally remove an existing mailbox name from the subscription
|
||||
/// list even if a mailbox by that name no longer exists.
|
||||
pub fn subscribe<S: AsRef<str>>(&mut self, mailbox: S) -> Result<()> {
|
||||
pub fn subscribe(&mut self, mailbox: impl AsRef<str>) -> Result<()> {
|
||||
self.run_command_and_check_ok(&format!("SUBSCRIBE {}", quote!(mailbox.as_ref())))
|
||||
}
|
||||
|
||||
|
|
@ -770,7 +770,7 @@ impl<T: Read + Write> Session<T> {
|
|||
/// specified mailbox name from the server's set of "active" or "subscribed" mailboxes as
|
||||
/// returned by [`Session::lsub`]. This command returns `Ok` only if the unsubscription is
|
||||
/// successful.
|
||||
pub fn unsubscribe<S: AsRef<str>>(&mut self, mailbox: S) -> Result<()> {
|
||||
pub fn unsubscribe(&mut self, mailbox: impl AsRef<str>) -> Result<()> {
|
||||
self.run_command_and_check_ok(&format!("UNSUBSCRIBE {}", quote!(mailbox.as_ref())))
|
||||
}
|
||||
|
||||
|
|
@ -813,7 +813,7 @@ impl<T: Read + Write> Session<T> {
|
|||
///
|
||||
/// Alternatively, the client may fall back to using just [`Session::expunge`], risking the
|
||||
/// unintended removal of some messages.
|
||||
pub fn uid_expunge<S: AsRef<str>>(&mut self, uid_set: S) -> Result<Deleted> {
|
||||
pub fn uid_expunge(&mut self, uid_set: impl AsRef<str>) -> Result<Deleted> {
|
||||
self.run_command(&format!("UID EXPUNGE {}", uid_set.as_ref()))?;
|
||||
self.read_response()
|
||||
.and_then(|(lines, _)| parse_expunge(lines, &mut self.unsolicited_responses_tx))
|
||||
|
|
@ -896,11 +896,11 @@ impl<T: Read + Write> Session<T> {
|
|||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
pub fn store<S1, S2>(&mut self, sequence_set: S1, query: S2) -> Result<Fetches>
|
||||
where
|
||||
S1: AsRef<str>,
|
||||
S2: AsRef<str>,
|
||||
{
|
||||
pub fn store(
|
||||
&mut self,
|
||||
sequence_set: impl AsRef<str>,
|
||||
query: impl AsRef<str>,
|
||||
) -> Result<Fetches> {
|
||||
self.run_command_and_read_response(&format!(
|
||||
"STORE {} {}",
|
||||
sequence_set.as_ref(),
|
||||
|
|
@ -911,11 +911,11 @@ impl<T: Read + Write> Session<T> {
|
|||
|
||||
/// Equivalent to [`Session::store`], except that all identifiers in `sequence_set` are
|
||||
/// [`Uid`]s. See also the [`UID` command](https://tools.ietf.org/html/rfc3501#section-6.4.8).
|
||||
pub fn uid_store<S1, S2>(&mut self, uid_set: S1, query: S2) -> Result<Fetches>
|
||||
where
|
||||
S1: AsRef<str>,
|
||||
S2: AsRef<str>,
|
||||
{
|
||||
pub fn uid_store(
|
||||
&mut self,
|
||||
uid_set: impl AsRef<str>,
|
||||
query: impl AsRef<str>,
|
||||
) -> Result<Fetches> {
|
||||
self.run_command_and_read_response(&format!(
|
||||
"UID STORE {} {}",
|
||||
uid_set.as_ref(),
|
||||
|
|
@ -931,10 +931,10 @@ impl<T: Read + Write> Session<T> {
|
|||
///
|
||||
/// If the `COPY` command is unsuccessful for any reason, the server restores the destination
|
||||
/// mailbox to its state before the `COPY` attempt.
|
||||
pub fn copy<S1: AsRef<str>, S2: AsRef<str>>(
|
||||
pub fn copy(
|
||||
&mut self,
|
||||
sequence_set: S1,
|
||||
mailbox_name: S2,
|
||||
sequence_set: impl AsRef<str>,
|
||||
mailbox_name: impl AsRef<str>,
|
||||
) -> Result<()> {
|
||||
self.run_command_and_check_ok(&format!(
|
||||
"COPY {} {}",
|
||||
|
|
@ -945,10 +945,10 @@ impl<T: Read + Write> Session<T> {
|
|||
|
||||
/// Equivalent to [`Session::copy`], except that all identifiers in `sequence_set` are
|
||||
/// [`Uid`]s. See also the [`UID` command](https://tools.ietf.org/html/rfc3501#section-6.4.8).
|
||||
pub fn uid_copy<S1: AsRef<str>, S2: AsRef<str>>(
|
||||
pub fn uid_copy(
|
||||
&mut self,
|
||||
uid_set: S1,
|
||||
mailbox_name: S2,
|
||||
uid_set: impl AsRef<str>,
|
||||
mailbox_name: impl AsRef<str>,
|
||||
) -> Result<()> {
|
||||
self.run_command_and_check_ok(&format!(
|
||||
"UID COPY {} {}",
|
||||
|
|
@ -987,10 +987,10 @@ impl<T: Read + Write> Session<T> {
|
|||
/// orphaned). The server will generally not leave any message in both mailboxes (it would be
|
||||
/// bad for a partial failure to result in a bunch of duplicate messages). This is true even
|
||||
/// if the server returns with [`Error::No`].
|
||||
pub fn mv<S1: AsRef<str>, S2: AsRef<str>>(
|
||||
pub fn mv(
|
||||
&mut self,
|
||||
sequence_set: S1,
|
||||
mailbox_name: S2,
|
||||
sequence_set: impl AsRef<str>,
|
||||
mailbox_name: impl AsRef<str>,
|
||||
) -> Result<()> {
|
||||
self.run_command_and_check_ok(&format!(
|
||||
"MOVE {} {}",
|
||||
|
|
@ -1003,10 +1003,10 @@ impl<T: Read + Write> Session<T> {
|
|||
/// [`Uid`]s. See also the [`UID` command](https://tools.ietf.org/html/rfc3501#section-6.4.8)
|
||||
/// and the [semantics of `MOVE` and `UID
|
||||
/// MOVE`](https://tools.ietf.org/html/rfc6851#section-3.3).
|
||||
pub fn uid_mv<S1: AsRef<str>, S2: AsRef<str>>(
|
||||
pub fn uid_mv(
|
||||
&mut self,
|
||||
uid_set: S1,
|
||||
mailbox_name: S2,
|
||||
uid_set: impl AsRef<str>,
|
||||
mailbox_name: impl AsRef<str>,
|
||||
) -> Result<()> {
|
||||
self.run_command_and_check_ok(&format!(
|
||||
"UID MOVE {} {}",
|
||||
|
|
@ -1121,10 +1121,10 @@ impl<T: Read + Write> Session<T> {
|
|||
/// - `UNSEEN`: The number of messages which do not have [`Flag::Seen`] set.
|
||||
///
|
||||
/// `data_items` is a space-separated list enclosed in parentheses.
|
||||
pub fn status<S1: AsRef<str>, S2: AsRef<str>>(
|
||||
pub fn status(
|
||||
&mut self,
|
||||
mailbox_name: S1,
|
||||
data_items: S2,
|
||||
mailbox_name: impl AsRef<str>,
|
||||
data_items: impl AsRef<str>,
|
||||
) -> Result<Mailbox> {
|
||||
let mailbox_name = mailbox_name.as_ref();
|
||||
self.run_command_and_read_response(&format!(
|
||||
|
|
@ -1233,7 +1233,7 @@ impl<T: Read + Write> Session<T> {
|
|||
///
|
||||
/// - `BEFORE <date>`: Messages whose internal date (disregarding time and timezone) is earlier than the specified date.
|
||||
/// - `SINCE <date>`: Messages whose internal date (disregarding time and timezone) is within or later than the specified date.
|
||||
pub fn search<S: AsRef<str>>(&mut self, query: S) -> Result<HashSet<Seq>> {
|
||||
pub fn search(&mut self, query: impl AsRef<str>) -> Result<HashSet<Seq>> {
|
||||
self.run_command_and_read_response(&format!("SEARCH {}", query.as_ref()))
|
||||
.and_then(|lines| parse_id_set(&lines, &mut self.unsolicited_responses_tx))
|
||||
}
|
||||
|
|
@ -1241,7 +1241,7 @@ impl<T: Read + Write> Session<T> {
|
|||
/// Equivalent to [`Session::search`], except that the returned identifiers
|
||||
/// are [`Uid`] instead of [`Seq`]. See also the [`UID`
|
||||
/// command](https://tools.ietf.org/html/rfc3501#section-6.4.8).
|
||||
pub fn uid_search<S: AsRef<str>>(&mut self, query: S) -> Result<HashSet<Uid>> {
|
||||
pub fn uid_search(&mut self, query: impl AsRef<str>) -> Result<HashSet<Uid>> {
|
||||
self.run_command_and_read_response(&format!("UID SEARCH {}", query.as_ref()))
|
||||
.and_then(|lines| parse_id_set(&lines, &mut self.unsolicited_responses_tx))
|
||||
}
|
||||
|
|
@ -1251,11 +1251,11 @@ impl<T: Read + Write> Session<T> {
|
|||
///
|
||||
/// This command is like [`Session::search`], except that
|
||||
/// the results are also sorted according to the supplied criteria (subject to the given charset).
|
||||
pub fn sort<S: AsRef<str>>(
|
||||
pub fn sort(
|
||||
&mut self,
|
||||
criteria: &[extensions::sort::SortCriterion<'_>],
|
||||
charset: extensions::sort::SortCharset<'_>,
|
||||
query: S,
|
||||
query: impl AsRef<str>,
|
||||
) -> Result<Vec<Seq>> {
|
||||
self.run_command_and_read_response(&format!(
|
||||
"SORT {} {} {}",
|
||||
|
|
@ -1269,11 +1269,11 @@ impl<T: Read + Write> Session<T> {
|
|||
/// Equivalent to [`Session::sort`], except that it returns [`Uid`]s.
|
||||
///
|
||||
/// See also [`Session::uid_search`].
|
||||
pub fn uid_sort<S: AsRef<str>>(
|
||||
pub fn uid_sort(
|
||||
&mut self,
|
||||
criteria: &[extensions::sort::SortCriterion<'_>],
|
||||
charset: extensions::sort::SortCharset<'_>,
|
||||
query: S,
|
||||
query: impl AsRef<str>,
|
||||
) -> Result<Vec<Uid>> {
|
||||
self.run_command_and_read_response(&format!(
|
||||
"UID SORT {} {} {}",
|
||||
|
|
@ -1284,14 +1284,112 @@ 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<S: AsRef<str>>(&mut self, command: S) -> Result<()> {
|
||||
pub fn run_command_and_check_ok(&mut self, command: impl AsRef<str>) -> Result<()> {
|
||||
self.run_command_and_read_response(command).map(|_| ())
|
||||
}
|
||||
|
||||
/// Runs any command passed to it.
|
||||
pub fn run_command<S: AsRef<str>>(&mut self, untagged_command: S) -> Result<()> {
|
||||
pub fn run_command(&mut self, untagged_command: impl AsRef<str>) -> Result<()> {
|
||||
self.conn.run_command(untagged_command.as_ref())
|
||||
}
|
||||
|
||||
|
|
@ -1303,7 +1401,7 @@ impl<T: Read + Write> Session<T> {
|
|||
/// additional untagged `RECENT`, `EXISTS`, `FETCH`, and `EXPUNGE` responses!
|
||||
///
|
||||
/// The response includes the final [`Response::Done`], which starts at the returned index.
|
||||
pub fn run<S: AsRef<str>>(&mut self, untagged_command: S) -> Result<(Vec<u8>, usize)> {
|
||||
pub fn run(&mut self, untagged_command: impl AsRef<str>) -> Result<(Vec<u8>, usize)> {
|
||||
self.conn.run(untagged_command.as_ref())
|
||||
}
|
||||
|
||||
|
|
@ -1315,9 +1413,9 @@ impl<T: Read + Write> Session<T> {
|
|||
/// additional untagged `RECENT`, `EXISTS`, `FETCH`, and `EXPUNGE` responses!
|
||||
///
|
||||
/// The response does not include the final [`Response::Done`].
|
||||
pub fn run_command_and_read_response<S: AsRef<str>>(
|
||||
pub fn run_command_and_read_response(
|
||||
&mut self,
|
||||
untagged_command: S,
|
||||
untagged_command: impl AsRef<str>,
|
||||
) -> Result<Vec<u8>> {
|
||||
let (mut data, ok) = self.run(untagged_command)?;
|
||||
data.truncate(ok);
|
||||
|
|
@ -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