use crate::client::*;
use crate::error::{Error, ParseError, Result};
use crate::parse::try_handle_unilateral;
use crate::types::*;
use imap_proto::types::{MailboxDatum, Metadata, Response, ResponseCode};
use std::io::{Read, Write};
use std::sync::mpsc;
#[allow(unused_imports)]
use crate::error::No;
trait CmdListItemFormat {
fn format_as_cmd_list_item(&self, item_index: usize) -> Result<String>;
}
impl CmdListItemFormat for Metadata {
fn format_as_cmd_list_item(&self, item_index: usize) -> Result<String> {
let synopsis = "SETMETADATA";
Ok(format!(
"{} {}",
validate_str(
synopsis,
format!("entry#{}", item_index + 1),
self.entry.as_str()
)?,
self.value
.as_ref()
.map(|v| validate_str(synopsis, format!("value#{}", item_index + 1), v.as_str()))
.unwrap_or_else(|| Ok("NIL".to_string()))?
))
}
}
#[derive(Debug, Copy, Clone)]
pub enum MetadataDepth {
Zero,
One,
Infinity,
}
impl Default for MetadataDepth {
fn default() -> Self {
Self::Zero
}
}
impl MetadataDepth {
fn depth_str<'a>(self) -> &'a str {
match self {
MetadataDepth::Zero => "0",
MetadataDepth::One => "1",
MetadataDepth::Infinity => "infinity",
}
}
}
fn parse_metadata<'a>(
mut lines: &'a [u8],
unsolicited: &'a mut mpsc::Sender<UnsolicitedResponse>,
) -> Result<Vec<Metadata>> {
let mut res: Vec<Metadata> = Vec::new();
loop {
if lines.is_empty() {
break Ok(res);
}
match imap_proto::parser::parse_response(lines) {
Ok((rest, resp)) => {
lines = rest;
match resp {
Response::MailboxData(MailboxDatum::MetadataSolicited {
mailbox: _,
mut values,
}) => {
res.append(&mut values);
}
_ => {
if let Some(unhandled) = try_handle_unilateral(resp, unsolicited) {
break Err(unhandled.into());
}
}
}
}
Err(_) => {
return Err(Error::Parse(ParseError::Invalid(lines.to_vec())));
}
}
}
}
impl<T: Read + Write> Session<T> {
pub fn get_metadata(
&mut self,
mailbox: Option<&str>,
entries: &[impl AsRef<str>],
depth: MetadataDepth,
maxsize: Option<usize>,
) -> Result<(Vec<Metadata>, Option<u64>)> {
let synopsis = "GETMETADATA";
let v: Vec<String> = entries
.iter()
.enumerate()
.map(|(i, e)| validate_str(synopsis, format!("entry#{}", i + 1), e.as_ref()))
.collect::<Result<_>>()?;
let s = v.as_slice().join(" ");
let mut command = format!("GETMETADATA (DEPTH {}", depth.depth_str());
if let Some(size) = maxsize {
command.push_str(format!(" MAXSIZE {}", size).as_str());
}
command.push_str(
format!(
") {} ({})",
mailbox
.map(|mbox| validate_str(synopsis, "mailbox", mbox))
.unwrap_or_else(|| Ok("\"\"".to_string()))?,
s
)
.as_str(),
);
let (lines, ok) = self.run(command)?;
let meta = parse_metadata(&lines[..ok], &mut self.unsolicited_responses_tx)?;
let missed = if maxsize.is_some() {
if let Ok((_, Response::Done { code, .. })) =
imap_proto::parser::parse_response(&lines[ok..])
{
match code {
None => None,
Some(ResponseCode::MetadataLongEntries(v)) => Some(v),
Some(_) => None,
}
} else {
unreachable!("already parsed as Done by Client::run");
}
} else {
None
};
Ok((meta, missed))
}
pub fn set_metadata(&mut self, mbox: impl AsRef<str>, annotations: &[Metadata]) -> Result<()> {
let v: Vec<String> = annotations
.iter()
.enumerate()
.map(|(i, metadata)| metadata.format_as_cmd_list_item(i))
.collect::<Result<_>>()?;
let s = v.as_slice().join(" ");
let command = format!(
"SETMETADATA {} ({})",
validate_str("SETMETADATA", "mailbox", mbox.as_ref())?,
s
);
self.run_command_and_check_ok(command)
}
}
#[cfg(test)]
mod tests {
use crate::extensions::metadata::*;
use crate::mock_stream::MockStream;
use crate::*;
#[test]
fn test_getmetadata() {
let response = "a1 OK Logged in.\r\n* METADATA \"\" (/shared/vendor/vendor.coi/a {3}\r\nAAA /shared/vendor/vendor.coi/b {3}\r\nBBB /shared/vendor/vendor.coi/c {3}\r\nCCC)\r\na2 OK GETMETADATA Completed\r\n";
let mock_stream = MockStream::new(response.as_bytes().to_vec());
let client = Client::new(mock_stream);
let mut session = client.login("testuser", "pass").unwrap();
let r = session.get_metadata(
None,
&["/shared/vendor/vendor.coi", "/shared/comment"],
MetadataDepth::Infinity,
Option::None,
);
match r {
Ok((v, missed)) => {
assert_eq!(missed, None);
assert_eq!(v.len(), 3);
assert_eq!(v[0].entry, "/shared/vendor/vendor.coi/a");
assert_eq!(v[0].value.as_ref().expect("None is not expected"), "AAA");
assert_eq!(v[1].entry, "/shared/vendor/vendor.coi/b");
assert_eq!(v[1].value.as_ref().expect("None is not expected"), "BBB");
assert_eq!(v[2].entry, "/shared/vendor/vendor.coi/c");
assert_eq!(v[2].value.as_ref().expect("None is not expected"), "CCC");
}
Err(e) => panic!("Unexpected error: {:?}", e),
}
}
use crate::client::testutils::assert_validation_error_session;
#[test]
fn test_getmetadata_validation_entry1() {
assert_validation_error_session(
|mut session| {
session.get_metadata(
None,
&[
"/shared/vendor\n/vendor.coi",
"/shared/comment",
"/some/other/entry",
],
MetadataDepth::Infinity,
None,
)
},
"GETMETADATA",
"entry#1",
'\n',
)
}
#[test]
fn test_getmetadata_validation_entry2() {
assert_validation_error_session(
|mut session| {
session.get_metadata(
Some("INBOX"),
&["/shared/vendor/vendor.coi", "/\rshared/comment"],
MetadataDepth::Infinity,
None,
)
},
"GETMETADATA",
"entry#2",
'\r',
)
}
#[test]
fn test_getmetadata_validation_mailbox() {
assert_validation_error_session(
|mut session| {
session.get_metadata(
Some("INB\nOX"),
&["/shared/vendor/vendor.coi", "/shared/comment"],
MetadataDepth::Infinity,
None,
)
},
"GETMETADATA",
"mailbox",
'\n',
);
}
#[test]
fn test_setmetadata_validation_mailbox() {
assert_validation_error_session(
|mut session| {
session.set_metadata(
"INB\nOX",
&[
Metadata {
entry: "/shared/vendor/vendor.coi".to_string(),
value: None,
},
Metadata {
entry: "/shared/comment".to_string(),
value: Some("value".to_string()),
},
],
)
},
"SETMETADATA",
"mailbox",
'\n',
);
}
#[test]
fn test_setmetadata_validation_entry1() {
assert_validation_error_session(
|mut session| {
session.set_metadata(
"INBOX",
&[
Metadata {
entry: "/shared/\nvendor/vendor.coi".to_string(),
value: None,
},
Metadata {
entry: "/shared/comment".to_string(),
value: Some("value".to_string()),
},
],
)
},
"SETMETADATA",
"entry#1",
'\n',
);
}
#[test]
fn test_setmetadata_validation_entry2_key() {
assert_validation_error_session(
|mut session| {
session.set_metadata(
"INBOX",
&[
Metadata {
entry: "/shared/vendor/vendor.coi".to_string(),
value: None,
},
Metadata {
entry: "/shared\r/comment".to_string(),
value: Some("value".to_string()),
},
],
)
},
"SETMETADATA",
"entry#2",
'\r',
);
}
#[test]
fn test_setmetadata_validation_entry2_value() {
assert_validation_error_session(
|mut session| {
session.set_metadata(
"INBOX",
&[
Metadata {
entry: "/shared/vendor/vendor.coi".to_string(),
value: None,
},
Metadata {
entry: "/shared/comment".to_string(),
value: Some("va\nlue".to_string()),
},
],
)
},
"SETMETADATA",
"value#2",
'\n',
);
}
#[test]
fn test_setmetadata_validation_entry() {
assert_validation_error_session(
|mut session| {
session.set_metadata(
"INBOX",
&[Metadata {
entry: "/shared/\nvendor/vendor.coi".to_string(),
value: None,
}],
)
},
"SETMETADATA",
"entry#1",
'\n',
);
}
}