1use core::fmt;
44
45use alloc::{string::String, string::ToString, vec::Vec};
46
47use imap_codec::{
48 CommandCodec,
49 fragmentizer::Fragmentizer,
50 imap_types::{
51 command::{Command, CommandBody},
52 core::TagGenerator,
53 response::{Capability, Code, Data, StatusBody, StatusKind, Tagged},
54 },
55};
56use log::trace;
57use thiserror::Error;
58
59use crate::{coroutine::*, imap_try, send::*};
60
61#[derive(Clone, Debug, Error)]
63pub enum ImapCapabilityGetError {
64 #[error("IMAP CAPABILITY failed: NO {0}")]
66 No(String),
67 #[error("IMAP CAPABILITY failed: BAD {0}")]
69 Bad(String),
70 #[error("IMAP CAPABILITY failed: BYE {0}")]
72 Bye(String),
73 #[error("IMAP CAPABILITY failed: server did not return a tagged response")]
75 MissingTagged,
76 #[error("IMAP CAPABILITY failed: server did not advertise any capability")]
79 MissingCapability,
80 #[error("IMAP CAPABILITY failed: {0}")]
82 Send(#[from] ImapSendError),
83}
84
85pub struct ImapCapabilityGet {
87 state: State,
88}
89
90impl ImapCapabilityGet {
91 pub fn new() -> Self {
94 let command = Command {
95 tag: TagGenerator::new().generate(),
96 body: CommandBody::Capability,
97 };
98
99 trace!("send IMAP command {command:?}");
100
101 let state = State::Send(ImapSend::new(CommandCodec::new(), command));
102
103 Self { state }
104 }
105}
106
107impl Default for ImapCapabilityGet {
108 fn default() -> Self {
109 Self::new()
110 }
111}
112
113impl ImapCoroutine for ImapCapabilityGet {
114 type Yield = ImapYield;
115 type Return = Result<Vec<Capability<'static>>, ImapCapabilityGetError>;
116
117 fn resume(
118 &mut self,
119 fragmentizer: &mut Fragmentizer,
120 arg: Option<&[u8]>,
121 ) -> ImapCoroutineState<Self::Yield, Self::Return> {
122 match &mut self.state {
123 State::Send(send) => {
124 let out = imap_try!(send, fragmentizer, arg);
125
126 if let Some(bye) = out.bye {
127 let err = ImapCapabilityGetError::Bye(bye.text.to_string());
128 return ImapCoroutineState::Complete(Err(err));
129 }
130
131 let Some(Tagged { body, .. }) = out.tagged else {
132 let err = ImapCapabilityGetError::MissingTagged;
133 return ImapCoroutineState::Complete(Err(err));
134 };
135
136 let code = match body.kind {
137 StatusKind::Ok => body.code,
138 StatusKind::No => {
139 let err = ImapCapabilityGetError::No(body.text.to_string());
140 return ImapCoroutineState::Complete(Err(err));
141 }
142 StatusKind::Bad => {
143 let err = ImapCapabilityGetError::Bad(body.text.to_string());
144 return ImapCoroutineState::Complete(Err(err));
145 }
146 };
147
148 let mut new_capability = None;
149
150 if let Some(Code::Capability(capability)) = code {
151 new_capability.replace(capability);
152 }
153
154 for data in out.data {
155 if let Data::Capability(capability) = data {
156 new_capability.replace(capability);
157 }
158 }
159
160 for StatusBody { code, .. } in out.untagged {
161 if let Some(Code::Capability(capability)) = code {
162 new_capability.replace(capability);
163 }
164 }
165
166 let Some(capability) = new_capability else {
167 let err = ImapCapabilityGetError::MissingCapability;
168 return ImapCoroutineState::Complete(Err(err));
169 };
170
171 ImapCoroutineState::Complete(Ok(capability.into_iter().collect()))
172 }
173 }
174 }
175}
176
177enum State {
178 Send(ImapSend<CommandCodec>),
179}
180
181impl fmt::Display for State {
182 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
183 match self {
184 Self::Send(_) => f.write_str("send capability"),
185 }
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use core::str;
192
193 use alloc::{borrow::ToOwned, format};
194
195 use crate::rfc3501::capability::*;
196
197 #[test]
198 fn data_capability_returns_capabilities() {
199 let mut cap = ImapCapabilityGet::new();
200 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
201
202 let bytes = expect_wants_write(&mut cap, &mut frag, None);
203 let line = str::from_utf8(&bytes).expect("utf8 command");
204 let tag = first_word(line).to_owned();
205 assert!(line.trim_end().ends_with("CAPABILITY"));
206
207 expect_wants_read(&mut cap, &mut frag);
208
209 let reply =
210 format!("* CAPABILITY IMAP4REV1 STARTTLS IDLE\r\n{tag} OK CAPABILITY completed\r\n");
211 let caps = expect_complete_ok(&mut cap, &mut frag, reply.as_bytes());
212 assert_eq!(3, caps.len());
213 assert!(caps.contains(&Capability::Imap4Rev1));
214 assert!(caps.contains(&Capability::StartTls));
215 assert!(caps.contains(&Capability::Idle));
216 }
217
218 #[test]
219 fn tagged_code_capability_returns_capabilities() {
220 let mut cap = ImapCapabilityGet::new();
221 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
222
223 let bytes = expect_wants_write(&mut cap, &mut frag, None);
224 let tag = first_word(str::from_utf8(&bytes).expect("utf8 command")).to_owned();
225
226 expect_wants_read(&mut cap, &mut frag);
227
228 let reply = format!("{tag} OK [CAPABILITY IMAP4REV1 IDLE] done\r\n");
229 let caps = expect_complete_ok(&mut cap, &mut frag, reply.as_bytes());
230 assert_eq!(2, caps.len());
231 }
232
233 #[test]
234 fn no_capability_returns_missing_error() {
235 let mut cap = ImapCapabilityGet::new();
236 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
237
238 let bytes = expect_wants_write(&mut cap, &mut frag, None);
239 let tag = first_word(str::from_utf8(&bytes).expect("utf8 command")).to_owned();
240
241 expect_wants_read(&mut cap, &mut frag);
242
243 let reply = format!("{tag} OK CAPABILITY completed\r\n");
244 let err = expect_complete_err(&mut cap, &mut frag, reply.as_bytes());
245 assert!(matches!(err, ImapCapabilityGetError::MissingCapability));
246 }
247
248 #[test]
249 fn tagged_no_returns_no_error() {
250 let mut cap = ImapCapabilityGet::new();
251 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
252
253 let bytes = expect_wants_write(&mut cap, &mut frag, None);
254 let tag = first_word(str::from_utf8(&bytes).expect("utf8 command")).to_owned();
255
256 expect_wants_read(&mut cap, &mut frag);
257
258 let reply = format!("{tag} NO server is sulking\r\n");
259 let err = expect_complete_err(&mut cap, &mut frag, reply.as_bytes());
260 let ImapCapabilityGetError::No(text) = err else {
261 panic!("expected ImapCapabilityGetError::No, got {err:?}");
262 };
263 assert_eq!(text, "server is sulking");
264 }
265
266 #[test]
267 fn bye_returns_bye_error() {
268 let mut cap = ImapCapabilityGet::new();
269 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
270
271 let _ = expect_wants_write(&mut cap, &mut frag, None);
272 expect_wants_read(&mut cap, &mut frag);
273
274 let err = expect_complete_err(&mut cap, &mut frag, b"* BYE going down\r\n");
275 let ImapCapabilityGetError::Bye(text) = err else {
276 panic!("expected ImapCapabilityGetError::Bye, got {err:?}");
277 };
278 assert_eq!(text, "going down");
279 }
280
281 fn expect_wants_write(
282 cor: &mut ImapCapabilityGet,
283 frag: &mut Fragmentizer,
284 arg: Option<&[u8]>,
285 ) -> Vec<u8> {
286 match cor.resume(frag, arg) {
287 ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => bytes,
288 state => panic!("expected WantsWrite, got {state:?}"),
289 }
290 }
291
292 fn expect_wants_read(cor: &mut ImapCapabilityGet, frag: &mut Fragmentizer) {
293 match cor.resume(frag, None) {
294 ImapCoroutineState::Yielded(ImapYield::WantsRead) => {}
295 state => panic!("expected WantsRead, got {state:?}"),
296 }
297 }
298
299 fn expect_complete_ok(
300 cor: &mut ImapCapabilityGet,
301 frag: &mut Fragmentizer,
302 reply: &[u8],
303 ) -> Vec<Capability<'static>> {
304 match cor.resume(frag, Some(reply)) {
305 ImapCoroutineState::Complete(Ok(value)) => value,
306 state => panic!("expected Complete(Ok), got {state:?}"),
307 }
308 }
309
310 fn expect_complete_err(
311 cor: &mut ImapCapabilityGet,
312 frag: &mut Fragmentizer,
313 reply: &[u8],
314 ) -> ImapCapabilityGetError {
315 match cor.resume(frag, Some(reply)) {
316 ImapCoroutineState::Complete(Err(err)) => err,
317 state => panic!("expected Complete(Err), got {state:?}"),
318 }
319 }
320
321 fn first_word(line: &str) -> &str {
322 line.split_whitespace()
323 .next()
324 .expect("first whitespace-separated token")
325 }
326}