async_imap/extensions/
id.rs

1//! IMAP ID extension specified in [RFC2971](https://datatracker.ietf.org/doc/html/rfc2971)
2
3use async_channel as channel;
4use futures::io;
5use futures::prelude::*;
6use imap_proto::{self, RequestId, Response};
7use std::collections::HashMap;
8
9use crate::types::ResponseData;
10use crate::types::*;
11use crate::{
12    error::Result,
13    parse::{filter, handle_unilateral},
14};
15
16fn 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.
23pub(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
38pub(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());
60            }
61        }
62    }
63
64    Ok(id)
65}
66
67#[cfg(test)]
68mod 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}