1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
//! IMAP ID extension specified in [RFC2971](https://datatracker.ietf.org/doc/html/rfc2971)

use async_channel as channel;
use futures::io;
use futures::prelude::*;
use imap_proto::{self, RequestId, Response};
use std::collections::HashMap;

use crate::types::ResponseData;
use crate::types::*;
use crate::{
    error::Result,
    parse::{filter, handle_unilateral},
};

fn escape(s: &str) -> String {
    s.replace('\\', r"\\").replace('\"', "\\\"")
}

/// Formats list of key-value pairs for ID command.
///
/// Returned list is not wrapped in parenthesis, the caller should do it.
pub(crate) fn format_identification<'a, 'b>(
    id: impl IntoIterator<Item = (&'a str, Option<&'b str>)>,
) -> String {
    id.into_iter()
        .map(|(k, v)| {
            format!(
                "\"{}\" {}",
                escape(k),
                v.map_or("NIL".to_string(), |v| format!("\"{}\"", escape(v)))
            )
        })
        .collect::<Vec<String>>()
        .join(" ")
}

pub(crate) async fn parse_id<T: Stream<Item = io::Result<ResponseData>> + Unpin>(
    stream: &mut T,
    unsolicited: channel::Sender<UnsolicitedResponse>,
    command_tag: RequestId,
) -> Result<Option<HashMap<String, String>>> {
    let mut id = None;
    while let Some(resp) = stream
        .take_while(|res| filter(res, &command_tag))
        .next()
        .await
    {
        let resp = resp?;
        match resp.parsed() {
            Response::Id(res) => {
                id = res.as_ref().map(|m| {
                    m.iter()
                        .map(|(k, v)| (k.to_string(), v.to_string()))
                        .collect()
                })
            }
            _ => {
                handle_unilateral(resp, unsolicited.clone()).await;
            }
        }
    }

    Ok(id)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_format_identification() {
        assert_eq!(
            format_identification([("name", Some("MyClient"))]),
            r#""name" "MyClient""#
        );

        assert_eq!(
            format_identification([("name", Some(r#""MyClient"\"#))]),
            r#""name" "\"MyClient\"\\""#
        );

        assert_eq!(
            format_identification([("name", Some("MyClient")), ("version", Some("2.0"))]),
            r#""name" "MyClient" "version" "2.0""#
        );

        assert_eq!(
            format_identification([("name", None), ("version", Some("2.0"))]),
            r#""name" NIL "version" "2.0""#
        );
    }
}