Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add GETMETADATA command #102

Merged
merged 1 commit into from
Jan 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 128 additions & 1 deletion src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use base64::Engine as _;
use extensions::id::{format_identification, parse_id};
use extensions::quota::parse_get_quota_root;
use futures::{io, Stream, StreamExt};
use imap_proto::{RequestId, Response};
use imap_proto::{Metadata, RequestId, Response};
#[cfg(feature = "runtime-tokio")]
use tokio::io::{AsyncRead as Read, AsyncWrite as Write, AsyncWriteExt};

Expand Down Expand Up @@ -1250,6 +1250,36 @@ impl<T: Read + Write + Unpin + fmt::Debug + Send> Session<T> {
Ok(c)
}

/// The [`GETMETADATA` command](https://datatracker.ietf.org/doc/html/rfc5464.html#section-4.2)
pub async fn get_metadata(
&mut self,
mailbox_name: &str,
options: &str,
entry_specifier: &str,
) -> Result<Vec<Metadata>> {
let options = if options.is_empty() {
String::new()
} else {
format!(" {options}")
};
let id = self
.run_command(format!(
"GETMETADATA {} {}{}",
quote!(mailbox_name),
options,
entry_specifier
))
.await?;
let metadata = parse_metadata(
&mut self.conn.stream,
mailbox_name,
self.unsolicited_responses_tx.clone(),
id,
)
.await?;
Ok(metadata)
}

/// The [`ID` command](https://datatracker.ietf.org/doc/html/rfc2971)
///
/// `identification` is an iterable sequence of pairs such as `("name", Some("MyMailClient"))`.
Expand Down Expand Up @@ -2315,4 +2345,101 @@ mod tests {
assert_eq!(status.exists, 231);
}
}

#[cfg_attr(feature = "runtime-tokio", tokio::test)]
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
async fn get_metadata() {
{
let response = b"* METADATA \"INBOX\" (/private/comment \"My own comment\")\r\n\
A0001 OK GETMETADATA complete\r\n"
.to_vec();

let mock_stream = MockStream::new(response);
let mut session = mock_session!(mock_stream);
let metadata = session
.get_metadata("INBOX", "", "/private/comment")
.await
.unwrap();
assert_eq!(
session.stream.inner.written_buf,
b"A0001 GETMETADATA \"INBOX\" /private/comment\r\n".to_vec()
);
assert_eq!(metadata.len(), 1);
assert_eq!(metadata[0].entry, "/private/comment");
assert_eq!(metadata[0].value.as_ref().unwrap(), "My own comment");
}

{
let response = b"* METADATA \"INBOX\" (/shared/comment \"Shared comment\" /private/comment \"My own comment\")\r\n\
A0001 OK GETMETADATA complete\r\n"
.to_vec();

let mock_stream = MockStream::new(response);
let mut session = mock_session!(mock_stream);
let metadata = session
.get_metadata("INBOX", "", "(/shared/comment /private/comment)")
.await
.unwrap();
assert_eq!(
session.stream.inner.written_buf,
b"A0001 GETMETADATA \"INBOX\" (/shared/comment /private/comment)\r\n".to_vec()
);
assert_eq!(metadata.len(), 2);
assert_eq!(metadata[0].entry, "/shared/comment");
assert_eq!(metadata[0].value.as_ref().unwrap(), "Shared comment");
assert_eq!(metadata[1].entry, "/private/comment");
assert_eq!(metadata[1].value.as_ref().unwrap(), "My own comment");
}

{
let response = b"* METADATA \"\" (/shared/comment {15}\r\nChatmail server /shared/admin {28}\r\nmailto:root@nine.testrun.org)\r\n\
A0001 OK OK Getmetadata completed (0.001 + 0.000 secs).\r\n"
.to_vec();

let mock_stream = MockStream::new(response);
let mut session = mock_session!(mock_stream);
let metadata = session
.get_metadata("", "", "(/shared/comment /shared/admin)")
.await
.unwrap();
assert_eq!(
session.stream.inner.written_buf,
b"A0001 GETMETADATA \"\" (/shared/comment /shared/admin)\r\n".to_vec()
);
assert_eq!(metadata.len(), 2);
assert_eq!(metadata[0].entry, "/shared/comment");
assert_eq!(metadata[0].value.as_ref().unwrap(), "Chatmail server");
assert_eq!(metadata[1].entry, "/shared/admin");
assert_eq!(
metadata[1].value.as_ref().unwrap(),
"mailto:root@nine.testrun.org"
);
}

{
let response = b"* METADATA \"\" (/shared/comment \"Chatmail server\")\r\n\
* METADATA \"\" (/shared/admin \"mailto:root@nine.testrun.org\")\r\n\
A0001 OK OK Getmetadata completed (0.001 + 0.000 secs).\r\n"
.to_vec();

let mock_stream = MockStream::new(response);
let mut session = mock_session!(mock_stream);
let metadata = session
.get_metadata("", "", "(/shared/comment /shared/admin)")
.await
.unwrap();
assert_eq!(
session.stream.inner.written_buf,
b"A0001 GETMETADATA \"\" (/shared/comment /shared/admin)\r\n".to_vec()
);
assert_eq!(metadata.len(), 2);
assert_eq!(metadata[0].entry, "/shared/comment");
assert_eq!(metadata[0].value.as_ref().unwrap(), "Chatmail server");
assert_eq!(metadata[1].entry, "/shared/admin");
assert_eq!(
metadata[1].value.as_ref().unwrap(),
"mailto:root@nine.testrun.org"
);
}
}
}
36 changes: 35 additions & 1 deletion src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use async_channel as channel;
use futures::io;
use futures::prelude::*;
use futures::stream::Stream;
use imap_proto::{self, MailboxDatum, RequestId, Response};
use imap_proto::{self, MailboxDatum, Metadata, RequestId, Response};

use crate::error::{Error, Result};
use crate::types::ResponseData;
Expand Down Expand Up @@ -394,6 +394,40 @@ pub(crate) async fn parse_ids<T: Stream<Item = io::Result<ResponseData>> + Unpin
Ok(ids)
}

/// Parses [GETMETADATA](https://www.rfc-editor.org/info/rfc5464) response.
pub(crate) async fn parse_metadata<T: Stream<Item = io::Result<ResponseData>> + Unpin>(
stream: &mut T,
mailbox_name: &str,
unsolicited: channel::Sender<UnsolicitedResponse>,
command_tag: RequestId,
) -> Result<Vec<Metadata>> {
let mut res_values = Vec::new();
while let Some(resp) = stream
.take_while(|res| filter(res, &command_tag))
.next()
.await
{
let resp = resp?;
match resp.parsed() {
// METADATA Response with Values
// <https://datatracker.ietf.org/doc/html/rfc5464.html#section-4.4.1>
Response::MailboxData(MailboxDatum::MetadataSolicited { mailbox, values })
if mailbox == mailbox_name =>
{
res_values.extend_from_slice(values.as_slice());
}

// We are not interested in
// [Unsolicited METADATA Response without Values](https://datatracker.ietf.org/doc/html/rfc5464.html#section-4.4.2),
// they go to unsolicited channel with other unsolicited responses.
_ => {
handle_unilateral(resp, unsolicited.clone()).await;
}
}
}
Ok(res_values)
}

// check if this is simply a unilateral server response
// (see Section 7 of RFC 3501):
pub(crate) async fn handle_unilateral(
Expand Down
Loading