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 auth::AuthMechanism,
52 command::{Command, CommandBody},
53 core::TagGenerator,
54 response::{Capability, Code, Data, StatusBody, StatusKind, Tagged},
55 },
56};
57use log::trace;
58use pimalaya_stream::sasl::SaslMechanism;
59use thiserror::Error;
60
61use crate::{coroutine::*, imap_try, send::*};
62
63#[derive(Clone, Debug, Error)]
65pub enum ImapCapabilityGetError {
66 #[error("IMAP CAPABILITY failed: NO {0}")]
68 No(String),
69 #[error("IMAP CAPABILITY failed: BAD {0}")]
71 Bad(String),
72 #[error("IMAP CAPABILITY failed: BYE {0}")]
74 Bye(String),
75 #[error("IMAP CAPABILITY failed: server did not return a tagged response")]
77 MissingTagged,
78 #[error("IMAP CAPABILITY failed: server did not advertise any capability")]
81 MissingCapability,
82 #[error("IMAP CAPABILITY failed: {0}")]
84 Send(#[from] ImapSendError),
85}
86
87pub struct ImapCapabilityGet {
89 state: State,
90}
91
92impl ImapCapabilityGet {
93 pub fn new() -> Self {
96 let command = Command {
97 tag: TagGenerator::new().generate(),
98 body: CommandBody::Capability,
99 };
100
101 trace!("send IMAP command {command:?}");
102
103 let state = State::Send(ImapSend::new(CommandCodec::new(), command));
104
105 Self { state }
106 }
107}
108
109impl Default for ImapCapabilityGet {
110 fn default() -> Self {
111 Self::new()
112 }
113}
114
115impl ImapCoroutine for ImapCapabilityGet {
116 type Yield = ImapYield;
117 type Return = Result<Vec<Capability<'static>>, ImapCapabilityGetError>;
118
119 fn resume(
120 &mut self,
121 fragmentizer: &mut Fragmentizer,
122 arg: Option<&[u8]>,
123 ) -> ImapCoroutineState<Self::Yield, Self::Return> {
124 match &mut self.state {
125 State::Send(send) => {
126 let out = imap_try!(send, fragmentizer, arg);
127
128 if let Some(bye) = out.bye {
129 let err = ImapCapabilityGetError::Bye(bye.text.to_string());
130 return ImapCoroutineState::Complete(Err(err));
131 }
132
133 let Some(Tagged { body, .. }) = out.tagged else {
134 let err = ImapCapabilityGetError::MissingTagged;
135 return ImapCoroutineState::Complete(Err(err));
136 };
137
138 let code = match body.kind {
139 StatusKind::Ok => body.code,
140 StatusKind::No => {
141 let err = ImapCapabilityGetError::No(body.text.to_string());
142 return ImapCoroutineState::Complete(Err(err));
143 }
144 StatusKind::Bad => {
145 let err = ImapCapabilityGetError::Bad(body.text.to_string());
146 return ImapCoroutineState::Complete(Err(err));
147 }
148 };
149
150 let mut new_capability = None;
151
152 if let Some(Code::Capability(capability)) = code {
153 new_capability.replace(capability);
154 }
155
156 for data in out.data {
157 if let Data::Capability(capability) = data {
158 new_capability.replace(capability);
159 }
160 }
161
162 for StatusBody { code, .. } in out.untagged {
163 if let Some(Code::Capability(capability)) = code {
164 new_capability.replace(capability);
165 }
166 }
167
168 let Some(capability) = new_capability else {
169 let err = ImapCapabilityGetError::MissingCapability;
170 return ImapCoroutineState::Complete(Err(err));
171 };
172
173 ImapCoroutineState::Complete(Ok(capability.into_iter().collect()))
174 }
175 }
176 }
177}
178
179pub fn available_auth_mechanisms(capabilities: &[Capability]) -> Vec<SaslMechanism> {
190 let advertises = |name: &str| {
191 capabilities.iter().any(|capability| match capability {
192 Capability::Auth(mechanism) => {
193 AuthMechanism::as_ref(mechanism).eq_ignore_ascii_case(name)
194 }
195 _ => false,
196 })
197 };
198
199 let mut mechanisms = Vec::new();
200
201 if advertises("SCRAM-SHA-256") {
202 mechanisms.push(SaslMechanism::ScramSha256);
203 }
204 if advertises("PLAIN") {
205 mechanisms.push(SaslMechanism::Plain);
206 }
207 if advertises("OAUTHBEARER") {
208 mechanisms.push(SaslMechanism::OAuthBearer);
209 }
210 if advertises("XOAUTH2") {
211 mechanisms.push(SaslMechanism::XOAuth2);
212 }
213 if advertises("ANONYMOUS") {
214 mechanisms.push(SaslMechanism::Anonymous);
215 }
216
217 let login_disabled = capabilities
220 .iter()
221 .any(|capability| matches!(capability, Capability::LoginDisabled));
222 if !login_disabled {
223 mechanisms.push(SaslMechanism::Login);
224 }
225
226 mechanisms
227}
228
229enum State {
230 Send(ImapSend<CommandCodec>),
231}
232
233impl fmt::Display for State {
234 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
235 match self {
236 Self::Send(_) => f.write_str("send capability"),
237 }
238 }
239}
240
241#[cfg(test)]
242mod tests {
243 use core::str;
244
245 use alloc::{borrow::ToOwned, format};
246
247 use crate::rfc3501::capability::*;
248
249 #[test]
250 fn data_capability_returns_capabilities() {
251 let mut cap = ImapCapabilityGet::new();
252 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
253
254 let bytes = expect_wants_write(&mut cap, &mut frag, None);
255 let line = str::from_utf8(&bytes).expect("utf8 command");
256 let tag = first_word(line).to_owned();
257 assert!(line.trim_end().ends_with("CAPABILITY"));
258
259 expect_wants_read(&mut cap, &mut frag);
260
261 let reply =
262 format!("* CAPABILITY IMAP4REV1 STARTTLS IDLE\r\n{tag} OK CAPABILITY completed\r\n");
263 let caps = expect_complete_ok(&mut cap, &mut frag, reply.as_bytes());
264 assert_eq!(3, caps.len());
265 assert!(caps.contains(&Capability::Imap4Rev1));
266 assert!(caps.contains(&Capability::StartTls));
267 assert!(caps.contains(&Capability::Idle));
268 }
269
270 #[test]
271 fn tagged_code_capability_returns_capabilities() {
272 let mut cap = ImapCapabilityGet::new();
273 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
274
275 let bytes = expect_wants_write(&mut cap, &mut frag, None);
276 let tag = first_word(str::from_utf8(&bytes).expect("utf8 command")).to_owned();
277
278 expect_wants_read(&mut cap, &mut frag);
279
280 let reply = format!("{tag} OK [CAPABILITY IMAP4REV1 IDLE] done\r\n");
281 let caps = expect_complete_ok(&mut cap, &mut frag, reply.as_bytes());
282 assert_eq!(2, caps.len());
283 }
284
285 #[test]
286 fn no_capability_returns_missing_error() {
287 let mut cap = ImapCapabilityGet::new();
288 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
289
290 let bytes = expect_wants_write(&mut cap, &mut frag, None);
291 let tag = first_word(str::from_utf8(&bytes).expect("utf8 command")).to_owned();
292
293 expect_wants_read(&mut cap, &mut frag);
294
295 let reply = format!("{tag} OK CAPABILITY completed\r\n");
296 let err = expect_complete_err(&mut cap, &mut frag, reply.as_bytes());
297 assert!(matches!(err, ImapCapabilityGetError::MissingCapability));
298 }
299
300 #[test]
301 fn tagged_no_returns_no_error() {
302 let mut cap = ImapCapabilityGet::new();
303 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
304
305 let bytes = expect_wants_write(&mut cap, &mut frag, None);
306 let tag = first_word(str::from_utf8(&bytes).expect("utf8 command")).to_owned();
307
308 expect_wants_read(&mut cap, &mut frag);
309
310 let reply = format!("{tag} NO server is sulking\r\n");
311 let err = expect_complete_err(&mut cap, &mut frag, reply.as_bytes());
312 let ImapCapabilityGetError::No(text) = err else {
313 panic!("expected ImapCapabilityGetError::No, got {err:?}");
314 };
315 assert_eq!(text, "server is sulking");
316 }
317
318 #[test]
319 fn bye_returns_bye_error() {
320 let mut cap = ImapCapabilityGet::new();
321 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
322
323 let _ = expect_wants_write(&mut cap, &mut frag, None);
324 expect_wants_read(&mut cap, &mut frag);
325
326 let err = expect_complete_err(&mut cap, &mut frag, b"* BYE going down\r\n");
327 let ImapCapabilityGetError::Bye(text) = err else {
328 panic!("expected ImapCapabilityGetError::Bye, got {err:?}");
329 };
330 assert_eq!(text, "going down");
331 }
332
333 fn expect_wants_write(
334 cor: &mut ImapCapabilityGet,
335 frag: &mut Fragmentizer,
336 arg: Option<&[u8]>,
337 ) -> Vec<u8> {
338 match cor.resume(frag, arg) {
339 ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => bytes,
340 state => panic!("expected WantsWrite, got {state:?}"),
341 }
342 }
343
344 fn expect_wants_read(cor: &mut ImapCapabilityGet, frag: &mut Fragmentizer) {
345 match cor.resume(frag, None) {
346 ImapCoroutineState::Yielded(ImapYield::WantsRead) => {}
347 state => panic!("expected WantsRead, got {state:?}"),
348 }
349 }
350
351 fn expect_complete_ok(
352 cor: &mut ImapCapabilityGet,
353 frag: &mut Fragmentizer,
354 reply: &[u8],
355 ) -> Vec<Capability<'static>> {
356 match cor.resume(frag, Some(reply)) {
357 ImapCoroutineState::Complete(Ok(value)) => value,
358 state => panic!("expected Complete(Ok), got {state:?}"),
359 }
360 }
361
362 fn expect_complete_err(
363 cor: &mut ImapCapabilityGet,
364 frag: &mut Fragmentizer,
365 reply: &[u8],
366 ) -> ImapCapabilityGetError {
367 match cor.resume(frag, Some(reply)) {
368 ImapCoroutineState::Complete(Err(err)) => err,
369 state => panic!("expected Complete(Err), got {state:?}"),
370 }
371 }
372
373 fn first_word(line: &str) -> &str {
374 line.split_whitespace()
375 .next()
376 .expect("first whitespace-separated token")
377 }
378
379 fn auth(name: &str) -> Capability<'static> {
380 Capability::Auth(AuthMechanism::try_from(name.to_owned()).expect("valid mechanism"))
381 }
382
383 #[test]
384 fn bare_capabilities_offer_only_the_login_command() {
385 let caps = [Capability::Imap4Rev1];
387 assert!(matches!(
388 available_auth_mechanisms(&caps).as_slice(),
389 [SaslMechanism::Login]
390 ));
391 }
392
393 #[test]
394 fn advertised_mechanisms_are_ordered_by_preference_with_login_last() {
395 let caps = [Capability::Imap4Rev1, auth("PLAIN"), auth("SCRAM-SHA-256")];
398 assert!(matches!(
399 available_auth_mechanisms(&caps).as_slice(),
400 [
401 SaslMechanism::ScramSha256,
402 SaslMechanism::Plain,
403 SaslMechanism::Login,
404 ]
405 ));
406 }
407
408 #[test]
409 fn login_disabled_drops_the_login_command() {
410 let caps = [auth("PLAIN"), Capability::LoginDisabled];
411 assert!(matches!(
412 available_auth_mechanisms(&caps).as_slice(),
413 [SaslMechanism::Plain]
414 ));
415 }
416
417 #[test]
418 fn token_and_anonymous_mechanisms_are_recognised() {
419 let caps = [auth("XOAUTH2"), auth("OAUTHBEARER"), auth("ANONYMOUS")];
420 assert!(matches!(
421 available_auth_mechanisms(&caps).as_slice(),
422 [
423 SaslMechanism::OAuthBearer,
424 SaslMechanism::XOAuth2,
425 SaslMechanism::Anonymous,
426 SaslMechanism::Login,
427 ]
428 ));
429 }
430}