Skip to content

Commit 8755b66

Browse files
committed
feat: add ID extension support
1 parent c94c378 commit 8755b66

File tree

3 files changed

+130
-1
lines changed

3 files changed

+130
-1
lines changed

src/client.rs

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::collections::HashSet;
1+
use std::collections::{HashMap, HashSet};
22
use std::fmt;
33
use std::ops::{Deref, DerefMut};
44
use std::pin::Pin;
@@ -11,6 +11,7 @@ use async_std::{
1111
io::{Read, Write, WriteExt},
1212
net::{TcpStream, ToSocketAddrs},
1313
};
14+
use extensions::id::{format_identification, parse_id};
1415
use extensions::quota::parse_get_quota_root;
1516
use futures::{io, Stream, StreamExt};
1617
use imap_proto::{RequestId, Response};
@@ -1307,6 +1308,39 @@ impl<T: Read + Write + Unpin + fmt::Debug + Send> Session<T> {
13071308
Ok(c)
13081309
}
13091310

1311+
/// The [`ID` command](https://datatracker.ietf.org/doc/html/rfc2971)
1312+
///
1313+
/// `identification` is an iterable sequence of pairs such as `("name", Some("MyMailClient"))`.
1314+
pub async fn id(
1315+
&mut self,
1316+
identification: impl IntoIterator<Item = (&str, Option<&str>)>,
1317+
) -> Result<Option<HashMap<String, String>>> {
1318+
let id = self
1319+
.run_command(format!("ID ({})", format_identification(identification)))
1320+
.await?;
1321+
let server_identification = parse_id(
1322+
&mut self.conn.stream,
1323+
self.unsolicited_responses_tx.clone(),
1324+
id,
1325+
)
1326+
.await?;
1327+
Ok(server_identification)
1328+
}
1329+
1330+
/// Similar to `id`, but don't identify ourselves.
1331+
///
1332+
/// Sends `ID NIL` command and returns server response.
1333+
pub async fn id_nil(&mut self) -> Result<Option<HashMap<String, String>>> {
1334+
let id = self.run_command("ID NIL").await?;
1335+
let server_identification = parse_id(
1336+
&mut self.conn.stream,
1337+
self.unsolicited_responses_tx.clone(),
1338+
id,
1339+
)
1340+
.await?;
1341+
Ok(server_identification)
1342+
}
1343+
13101344
// these are only here because they are public interface, the rest is in `Connection`
13111345
/// Runs a command and checks if it returns OK.
13121346
pub async fn run_command_and_check_ok<S: AsRef<str>>(&mut self, command: S) -> Result<()> {

src/extensions/id.rs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
//! IMAP ID extension specified in [RFC2971](https://datatracker.ietf.org/doc/html/rfc2971)
2+
3+
use async_channel as channel;
4+
use futures::io;
5+
use futures::prelude::*;
6+
use imap_proto::{self, RequestId, Response};
7+
use std::collections::HashMap;
8+
9+
use crate::types::ResponseData;
10+
use crate::types::*;
11+
use crate::{
12+
error::Result,
13+
parse::{filter, handle_unilateral},
14+
};
15+
16+
fn escape(s: &str) -> String {
17+
s.replace('\\', r"\\").replace('\"', "\\\"")
18+
}
19+
20+
/// Formats list of key-value pairs for ID command.
21+
///
22+
/// Returned list is not wrapped in parenthesis, the caller should do it.
23+
pub(crate) fn format_identification<'a, 'b>(
24+
id: impl IntoIterator<Item = (&'a str, Option<&'b str>)>,
25+
) -> String {
26+
id.into_iter()
27+
.map(|(k, v)| {
28+
format!(
29+
"\"{}\" {}",
30+
escape(k),
31+
v.map_or("NIL".to_string(), |v| format!("\"{}\"", escape(v)))
32+
)
33+
})
34+
.collect::<Vec<String>>()
35+
.join(" ")
36+
}
37+
38+
pub(crate) async fn parse_id<T: Stream<Item = io::Result<ResponseData>> + Unpin>(
39+
stream: &mut T,
40+
unsolicited: channel::Sender<UnsolicitedResponse>,
41+
command_tag: RequestId,
42+
) -> Result<Option<HashMap<String, String>>> {
43+
let mut id = None;
44+
while let Some(resp) = stream
45+
.take_while(|res| filter(res, &command_tag))
46+
.next()
47+
.await
48+
{
49+
let resp = resp?;
50+
match resp.parsed() {
51+
Response::Id(res) => {
52+
id = res.as_ref().map(|m| {
53+
m.iter()
54+
.map(|(k, v)| (k.to_string(), v.to_string()))
55+
.collect()
56+
})
57+
}
58+
_ => {
59+
handle_unilateral(resp, unsolicited.clone()).await;
60+
}
61+
}
62+
}
63+
64+
Ok(id)
65+
}
66+
67+
#[cfg(test)]
68+
mod tests {
69+
use super::*;
70+
71+
#[test]
72+
fn test_format_identification() {
73+
assert_eq!(
74+
format_identification([("name", Some("MyClient"))]),
75+
r#""name" "MyClient""#
76+
);
77+
78+
assert_eq!(
79+
format_identification([("name", Some(r#""MyClient"\"#))]),
80+
r#""name" "\"MyClient\"\\""#
81+
);
82+
83+
assert_eq!(
84+
format_identification([("name", Some("MyClient")), ("version", Some("2.0"))]),
85+
r#""name" "MyClient" "version" "2.0""#
86+
);
87+
88+
assert_eq!(
89+
format_identification([("name", None), ("version", Some("2.0"))]),
90+
r#""name" NIL "version" "2.0""#
91+
);
92+
}
93+
}

src/extensions/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@
22
pub mod idle;
33

44
pub mod quota;
5+
6+
pub mod id;

0 commit comments

Comments
 (0)