imap_client/
capabilities.rs1use imap_core::ast::{DataResponse, Response, ResponseCode};
10use imap_core::parser::parse_response;
11
12#[derive(Debug, Clone, Default)]
17pub struct Capabilities {
18 pub imap4rev1: bool,
20 pub imap4rev2: bool,
22 pub starttls: bool,
24 pub login_disabled: bool,
26 pub idle: bool,
28 pub unselect: bool,
30 pub condstore: bool,
32 pub qresync: bool,
34 pub move_ext: bool,
36 pub uidplus: bool,
38 pub literal_plus: bool,
40 pub literal_minus: bool,
42 pub enable: bool,
44 pub auth_plain: bool,
46 pub auth_login: bool,
48 pub auth_xoauth2: bool,
50 pub auth_oauthbearer: bool,
52}
53
54impl Capabilities {
55 pub fn from_atoms<S: AsRef<str>>(atoms: &[S]) -> Self {
59 let mut caps = Capabilities::default();
60 for a in atoms {
61 caps.set_from_atom(a.as_ref());
62 }
63 caps
64 }
65
66 fn set_from_atom(&mut self, atom: &str) {
67 let upper = atom.to_ascii_uppercase();
68 match upper.as_str() {
69 "IMAP4REV1" => self.imap4rev1 = true,
70 "IMAP4REV2" => self.imap4rev2 = true,
71 "STARTTLS" => self.starttls = true,
72 "LOGINDISABLED" => self.login_disabled = true,
73 "IDLE" => self.idle = true,
74 "UNSELECT" => self.unselect = true,
75 "CONDSTORE" => self.condstore = true,
76 "QRESYNC" => self.qresync = true,
77 "MOVE" => self.move_ext = true,
78 "UIDPLUS" => self.uidplus = true,
79 "LITERAL+" => self.literal_plus = true,
80 "LITERAL-" => self.literal_minus = true,
81 "ENABLE" => self.enable = true,
82 "AUTH=PLAIN" => self.auth_plain = true,
83 "AUTH=LOGIN" => self.auth_login = true,
84 "AUTH=XOAUTH2" => self.auth_xoauth2 = true,
85 "AUTH=OAUTHBEARER" => self.auth_oauthbearer = true,
86 _ => {}
87 }
88 }
89
90 pub fn try_update_from(&mut self, response: &Response<'_>) -> bool {
94 match response {
95 Response::Data(DataResponse::Capability(caps)) => {
96 *self = Capabilities::from_atoms(caps);
97 true
98 }
99 Response::Status(s) => match &s.code {
100 Some(ResponseCode::Capability(caps)) => {
101 *self = Capabilities::from_atoms(caps);
102 true
103 }
104 _ => false,
105 },
106 _ => false,
107 }
108 }
109
110 pub fn from_frame(frame: &[u8]) -> Option<Self> {
113 let (_, response) = parse_response(frame).ok()?;
114 let mut caps = Capabilities::default();
115 if caps.try_update_from(&response) {
116 Some(caps)
117 } else {
118 None
119 }
120 }
121
122 pub fn parse(response: &str) -> Self {
126 if let Some(caps) = Capabilities::from_frame(response.as_bytes()) {
128 return caps;
129 }
130 let mut caps = Capabilities::default();
132 for atom in response.split_ascii_whitespace() {
133 caps.set_from_atom(atom);
134 }
135 caps
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142
143 #[test]
144 fn test_parse_gmail_caps() {
145 let cap_str = "* CAPABILITY IMAP4rev1 UNSELECT IDLE NAMESPACE QUOTA ID XLIST MOVE CONDSTORE ENABLE UTF8=ACCEPT\r\n";
146 let caps = Capabilities::parse(cap_str);
147 assert!(caps.imap4rev1);
148 assert!(!caps.imap4rev2);
149 assert!(caps.move_ext);
150 assert!(caps.condstore);
151 assert!(caps.idle);
152 assert!(caps.unselect);
153 assert!(caps.enable);
154 }
155
156 #[test]
157 fn test_parse_rev2_caps() {
158 let cap_str = "* CAPABILITY IMAP4rev2 MOVE UIDPLUS LITERAL+\r\n";
159 let caps = Capabilities::parse(cap_str);
160 assert!(caps.imap4rev2);
161 assert!(caps.move_ext);
162 assert!(caps.uidplus);
163 assert!(caps.literal_plus);
164 }
165
166 #[test]
167 fn test_capabilities_from_atoms() {
168 let caps = Capabilities::from_atoms(&["IMAP4REV2", "STARTTLS", "AUTH=PLAIN"]);
169 assert!(caps.imap4rev2);
170 assert!(caps.starttls);
171 assert!(caps.auth_plain);
172 }
173
174 #[test]
175 fn test_capabilities_from_response_code() {
176 let frame = b"A1 OK [CAPABILITY IMAP4rev2 STARTTLS] welcome\r\n";
178 let caps = Capabilities::from_frame(frame).unwrap();
179 assert!(caps.imap4rev2);
180 assert!(caps.starttls);
181 assert!(!caps.imap4rev1);
182 }
183
184 #[test]
185 fn test_no_false_prefix_match() {
186 let caps = Capabilities::from_atoms(&["IMAP4REV2X"]);
188 assert!(!caps.imap4rev2);
189 }
190
191 #[test]
192 fn test_parse_legacy_string_falls_back() {
193 let caps = Capabilities::parse("IMAP4rev2 IDLE STARTTLS");
195 assert!(caps.imap4rev2);
196 assert!(caps.idle);
197 assert!(caps.starttls);
198 }
199}