1use core::{fmt, mem};
57
58use alloc::{
59 string::{String, ToString},
60 vec,
61 vec::Vec,
62};
63
64use imap_codec::{
65 AuthenticateDataCodec, CommandCodec,
66 fragmentizer::Fragmentizer,
67 imap_types::{
68 auth::{AuthMechanism, AuthenticateData},
69 command::{Command, CommandBody},
70 core::{IString, NString, TagGenerator},
71 response::{
72 Capability, Code, CommandContinuationRequest, Data, StatusBody, StatusKind, Tagged,
73 },
74 secret::Secret,
75 },
76};
77use io_sasl::{
78 coroutine::*,
79 rfc4505::anonymous::{SaslAnonymous, SaslAnonymousCreds, SaslAnonymousError},
80};
81use log::{debug, trace};
82use thiserror::Error;
83
84use crate::{coroutine::*, imap_try, rfc2971::id::*, rfc3501::capability::*, send::*};
85
86#[derive(Clone, Debug, Error)]
88pub enum ImapAuthAnonymousError {
89 #[error("IMAP AUTHENTICATE ANONYMOUS failed: NO {0}")]
91 No(String),
92 #[error("IMAP AUTHENTICATE ANONYMOUS failed: BAD {0}")]
94 Bad(String),
95 #[error("IMAP AUTHENTICATE ANONYMOUS failed: BYE {0}")]
97 Bye(String),
98 #[error("IMAP AUTHENTICATE ANONYMOUS failed: server did not return a tagged response")]
100 MissingTagged,
101 #[error(
103 "IMAP AUTHENTICATE ANONYMOUS failed: server did not send the expected continuation request"
104 )]
105 ExpectedContinuationRequest,
106 #[error(
108 "IMAP AUTHENTICATE ANONYMOUS failed: server returned OK before the mechanism could complete"
109 )]
110 UnexpectedOk,
111 #[error("IMAP AUTHENTICATE ANONYMOUS failed: {0}")]
117 Mechanism(#[from] SaslAnonymousError),
118 #[error("IMAP AUTHENTICATE ANONYMOUS failed: {0}")]
120 Send(#[from] ImapSendError),
121 #[error(transparent)]
123 Capability(#[from] ImapCapabilityGetError),
124 #[error(transparent)]
126 ServerId(#[from] ImapServerIdError),
127}
128
129#[derive(Clone, Debug, Default, Eq, PartialEq)]
131pub struct ImapAuthAnonymousOptions {
132 pub initial_request: bool,
135 pub ensure_capabilities: bool,
138 pub auto_id: Option<Vec<(IString<'static>, NString<'static>)>>,
143}
144
145pub struct ImapAuthAnonymous {
147 state: State,
148 mechanism: SaslAnonymous,
149 observed: Vec<Capability<'static>>,
150 opts: ImapAuthAnonymousOptions,
151}
152
153impl ImapAuthAnonymous {
154 pub fn new(message: Option<impl AsRef<str>>, opts: ImapAuthAnonymousOptions) -> Self {
161 let mechanism = SaslAnonymous::new(SaslAnonymousCreds {
162 message: message.map(|message| message.as_ref().to_string()),
163 });
164
165 Self {
166 state: State::Start,
167 mechanism,
168 observed: Vec::new(),
169 opts,
170 }
171 }
172
173 fn wants_capability(
176 &mut self,
177 code: Option<Code<'static>>,
178 data: Vec<Data<'static>>,
179 untagged: Vec<StatusBody<'static>>,
180 ) -> Option<State> {
181 let mut new_capability = None;
182
183 if let Some(Code::Capability(capability)) = code {
184 new_capability.replace(capability);
185 }
186
187 for data in data {
188 if let Data::Capability(capability) = data {
189 new_capability.replace(capability);
190 }
191 }
192
193 for StatusBody { code, .. } in untagged {
194 if let Some(Code::Capability(capability)) = code {
195 new_capability.replace(capability);
196 }
197 }
198
199 if let Some(capability) = new_capability {
200 self.observed = capability.into_iter().collect();
201 }
202
203 (self.opts.ensure_capabilities && self.observed.is_empty())
204 .then(|| State::Capability(ImapCapabilityGet::new()))
205 }
206
207 fn wants_id(&mut self) -> Option<State> {
209 let params = self.opts.auto_id.take()?;
210 let wire = (!params.is_empty()).then_some(params);
211 Some(State::Id(ImapServerId::new(ImapServerIdOptions {
212 parameters: wire,
213 })))
214 }
215
216 fn wants_continue(payload: Vec<u8>) -> State {
218 let auth = AuthenticateData::r#continue(payload);
219 let codec = AuthenticateDataCodec::new();
220 State::Continue(ImapSend::new(codec, auth))
221 }
222
223 fn resume_sasl(&mut self, arg: SaslArg<'_>) -> Result<Option<Vec<u8>>, ImapAuthAnonymousError> {
225 match self.mechanism.resume(arg) {
226 SaslCoroutineState::Yielded(SaslYield::WantsWrite(payload)) => Ok(Some(payload)),
227 SaslCoroutineState::Yielded(SaslYield::WantsRead) => Ok(None),
228 SaslCoroutineState::Complete(result) => result.map(|()| None).map_err(Into::into),
229 }
230 }
231}
232
233impl ImapCoroutine for ImapAuthAnonymous {
234 type Yield = ImapYield;
235 type Return = Result<Vec<Capability<'static>>, ImapAuthAnonymousError>;
236
237 fn resume(
238 &mut self,
239 fragmentizer: &mut Fragmentizer,
240 arg: Option<&[u8]>,
241 ) -> ImapCoroutineState<Self::Yield, Self::Return> {
242 loop {
243 match &mut self.state {
244 State::Start => {
245 let payload = match self.resume_sasl(SaslArg::None) {
246 Ok(payload) => payload,
247 Err(err) => return ImapCoroutineState::Complete(Err(err)),
248 };
249
250 let (initial_response, pending) = match payload {
255 Some(payload) if self.opts.initial_request => {
256 (Some(Secret::new(payload.into())), None)
257 }
258 payload => (None, payload),
259 };
260
261 let tag = TagGenerator::new().generate();
262 let body = CommandBody::Authenticate {
263 mechanism: AuthMechanism::try_from("ANONYMOUS").unwrap(),
265 initial_response,
266 };
267 let cmd = Command { tag, body };
268 trace!("send IMAP command {cmd:?}");
269
270 self.state = State::Send {
271 send: ImapSend::new(CommandCodec::new(), cmd),
272 pending,
273 };
274 debug!("{}", self.state);
275 }
276 State::Send { send, pending } => {
277 let out = imap_try!(send, fragmentizer, arg);
278
279 if let Some(bye) = out.bye {
280 let err = ImapAuthAnonymousError::Bye(bye.text.to_string());
281 return ImapCoroutineState::Complete(Err(err));
282 }
283
284 if let Some(cr) = out.continuation_request {
285 let payload = match pending.take() {
290 Some(payload) => payload,
291 None => {
292 match self.resume_sasl(SaslArg::Input(&extract_challenge(cr))) {
293 Ok(payload) => payload.unwrap_or_default(),
294 Err(err) => return ImapCoroutineState::Complete(Err(err)),
295 }
296 }
297 };
298
299 self.state = Self::wants_continue(payload);
300 debug!("{}", self.state);
301 continue;
302 }
303
304 let inlined = pending.is_none();
309
310 let Some(Tagged { body, .. }) = out.tagged else {
311 let err = ImapAuthAnonymousError::ExpectedContinuationRequest;
312 return ImapCoroutineState::Complete(Err(err));
313 };
314
315 let code = match body.kind {
316 StatusKind::Ok if inlined => body.code,
317 StatusKind::Ok => {
318 let err = ImapAuthAnonymousError::UnexpectedOk;
319 return ImapCoroutineState::Complete(Err(err));
320 }
321 StatusKind::No => {
322 let err = ImapAuthAnonymousError::No(body.text.to_string());
323 return ImapCoroutineState::Complete(Err(err));
324 }
325 StatusKind::Bad => {
326 let err = ImapAuthAnonymousError::Bad(body.text.to_string());
327 return ImapCoroutineState::Complete(Err(err));
328 }
329 };
330
331 if let Err(err) = self.resume_sasl(SaslArg::Done) {
332 return ImapCoroutineState::Complete(Err(err));
333 }
334
335 if let Some(next) = self.wants_capability(code, out.data, out.untagged) {
336 self.state = next;
337 debug!("{}", self.state);
338 continue;
339 }
340
341 if let Some(next) = self.wants_id() {
342 self.state = next;
343 debug!("{}", self.state);
344 continue;
345 }
346
347 let capability = mem::take(&mut self.observed);
348 return ImapCoroutineState::Complete(Ok(capability));
349 }
350 State::Continue(send) => {
351 let out = imap_try!(send, fragmentizer, arg);
352
353 if let Some(bye) = out.bye {
354 let err = ImapAuthAnonymousError::Bye(bye.text.to_string());
355 return ImapCoroutineState::Complete(Err(err));
356 }
357
358 if let Some(cr) = out.continuation_request {
359 let payload = match self.resume_sasl(SaslArg::Input(&extract_challenge(cr)))
360 {
361 Ok(payload) => payload.unwrap_or_default(),
362 Err(err) => return ImapCoroutineState::Complete(Err(err)),
363 };
364
365 self.state = Self::wants_continue(payload);
366 debug!("{}", self.state);
367 continue;
368 }
369
370 let Some(Tagged { body, .. }) = out.tagged else {
371 let err = ImapAuthAnonymousError::MissingTagged;
372 return ImapCoroutineState::Complete(Err(err));
373 };
374
375 let code = match body.kind {
376 StatusKind::Ok => body.code,
377 StatusKind::No => {
378 let err = ImapAuthAnonymousError::No(body.text.to_string());
379 return ImapCoroutineState::Complete(Err(err));
380 }
381 StatusKind::Bad => {
382 let err = ImapAuthAnonymousError::Bad(body.text.to_string());
383 return ImapCoroutineState::Complete(Err(err));
384 }
385 };
386
387 if let Err(err) = self.resume_sasl(SaslArg::Done) {
393 return ImapCoroutineState::Complete(Err(err));
394 }
395
396 if let Some(next) = self.wants_capability(code, out.data, out.untagged) {
397 self.state = next;
398 debug!("{}", self.state);
399 continue;
400 }
401
402 if let Some(next) = self.wants_id() {
403 self.state = next;
404 debug!("{}", self.state);
405 continue;
406 }
407
408 let capability = mem::take(&mut self.observed);
409 return ImapCoroutineState::Complete(Ok(capability));
410 }
411 State::Capability(capability) => {
412 self.observed = imap_try!(capability, fragmentizer, arg);
413
414 if let Some(next) = self.wants_id() {
415 self.state = next;
416 debug!("{}", self.state);
417 continue;
418 }
419
420 let capability = mem::take(&mut self.observed);
421 return ImapCoroutineState::Complete(Ok(capability));
422 }
423 State::Id(id) => {
424 imap_try!(id, fragmentizer, arg);
425 let capability = mem::take(&mut self.observed);
426 return ImapCoroutineState::Complete(Ok(capability));
427 }
428 }
429 }
430 }
431}
432
433enum State {
434 Start,
435 Send {
436 send: ImapSend<CommandCodec>,
437 pending: Option<Vec<u8>>,
438 },
439 Continue(ImapSend<AuthenticateDataCodec>),
440 Capability(ImapCapabilityGet),
441 Id(ImapServerId),
442}
443
444impl fmt::Display for State {
445 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
446 match self {
447 Self::Start => f.write_str("start mechanism"),
448 Self::Send { pending, .. } if pending.is_some() => f.write_str("send auth"),
449 Self::Send { .. } => f.write_str("send auth with ir"),
450 Self::Continue(_) => f.write_str("send trace"),
451 Self::Capability(_) => f.write_str("fetch capabilities"),
452 Self::Id(_) => f.write_str("send id"),
453 }
454 }
455}
456
457fn extract_challenge(cr: CommandContinuationRequest<'static>) -> Vec<u8> {
458 match cr {
459 CommandContinuationRequest::Base64(data) => data.as_ref().to_vec(),
460 CommandContinuationRequest::Basic(_) => vec![],
461 }
462}
463
464#[cfg(test)]
465mod tests {
466 use core::str;
467
468 use alloc::format;
469
470 use crate::sasl::auth_anonymous::*;
471
472 #[test]
473 fn ir_success_returns_ok() {
474 let opts = ImapAuthAnonymousOptions {
475 initial_request: true,
476 ..Default::default()
477 };
478
479 let mut auth = ImapAuthAnonymous::new(Some("trace@example.org"), opts);
480 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
481
482 let bytes = expect_wants_write(&mut auth, &mut frag, None);
483 let line = str::from_utf8(&bytes).expect("utf8 command");
484 let tag = first_word(line);
485 assert!(line.contains("AUTHENTICATE ANONYMOUS "));
486
487 expect_wants_read(&mut auth, &mut frag);
488
489 let reply = format!("{tag} OK AUTHENTICATE completed\r\n");
490 expect_complete_ok(&mut auth, &mut frag, reply.as_bytes());
491 }
492
493 #[test]
494 fn ir_rejected_returns_no_error() {
495 let opts = ImapAuthAnonymousOptions {
496 initial_request: true,
497 ..Default::default()
498 };
499
500 let mut auth = ImapAuthAnonymous::new(None::<&str>, opts);
501 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
502
503 let bytes = expect_wants_write(&mut auth, &mut frag, None);
504 let tag = first_word(str::from_utf8(&bytes).expect("utf8 command"));
505
506 expect_wants_read(&mut auth, &mut frag);
507
508 let reply = format!("{tag} NO anonymous access disabled\r\n");
509 let err = expect_complete_err(&mut auth, &mut frag, reply.as_bytes());
510 let ImapAuthAnonymousError::No(text) = err else {
511 panic!("expected ImapAuthAnonymousError::No, got {err:?}");
512 };
513 assert_eq!(text, "anonymous access disabled");
514 }
515
516 #[test]
517 fn ir_tagged_bad_returns_bad_error() {
518 let opts = ImapAuthAnonymousOptions {
519 initial_request: true,
520 ..Default::default()
521 };
522
523 let mut auth = ImapAuthAnonymous::new(None::<&str>, opts);
524 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
525
526 let bytes = expect_wants_write(&mut auth, &mut frag, None);
527 let tag = first_word(str::from_utf8(&bytes).expect("utf8 command"));
528
529 expect_wants_read(&mut auth, &mut frag);
530
531 let reply = format!("{tag} BAD AUTHENTICATE not enabled\r\n");
532 let err = expect_complete_err(&mut auth, &mut frag, reply.as_bytes());
533 let ImapAuthAnonymousError::Bad(text) = err else {
534 panic!("expected ImapAuthAnonymousError::Bad, got {err:?}");
535 };
536 assert_eq!(text, "AUTHENTICATE not enabled");
537 }
538
539 #[test]
540 fn ir_extra_challenge_returns_mechanism_error() {
541 let opts = ImapAuthAnonymousOptions {
542 initial_request: true,
543 ..Default::default()
544 };
545
546 let mut auth = ImapAuthAnonymous::new(None::<&str>, opts);
547 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
548
549 expect_wants_write(&mut auth, &mut frag, None);
550 expect_wants_read(&mut auth, &mut frag);
551
552 let err = expect_complete_err(&mut auth, &mut frag, b"+ \r\n");
556 let ImapAuthAnonymousError::Mechanism(SaslAnonymousError::UnexpectedChallenge) = err else {
557 panic!("expected ImapAuthAnonymousError::Mechanism, got {err:?}");
558 };
559 }
560
561 #[test]
562 fn non_ir_success_returns_ok() {
563 let opts = ImapAuthAnonymousOptions::default();
564 let mut auth = ImapAuthAnonymous::new(None::<&str>, opts);
565 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
566
567 let bytes = expect_wants_write(&mut auth, &mut frag, None);
568 let line = str::from_utf8(&bytes).expect("utf8 command");
569 let tag = first_word(line);
570 assert!(line.trim_end().ends_with("AUTHENTICATE ANONYMOUS"));
571
572 expect_wants_read(&mut auth, &mut frag);
573
574 let trace = expect_wants_write(&mut auth, &mut frag, Some(b"+ \r\n"));
575 assert!(trace.ends_with(b"\r\n"));
576
577 expect_wants_read(&mut auth, &mut frag);
578
579 let reply = format!("{tag} OK AUTHENTICATE completed\r\n");
580 expect_complete_ok(&mut auth, &mut frag, reply.as_bytes());
581 }
582
583 #[test]
584 fn non_ir_rejected_returns_no_error() {
585 let opts = ImapAuthAnonymousOptions::default();
586 let mut auth = ImapAuthAnonymous::new(Some("trace@example.org"), opts);
587 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
588
589 let bytes = expect_wants_write(&mut auth, &mut frag, None);
590 let tag = first_word(str::from_utf8(&bytes).expect("utf8 command"));
591
592 expect_wants_read(&mut auth, &mut frag);
593 expect_wants_write(&mut auth, &mut frag, Some(b"+ \r\n"));
594 expect_wants_read(&mut auth, &mut frag);
595
596 let reply = format!("{tag} NO anonymous access disabled\r\n");
597 let err = expect_complete_err(&mut auth, &mut frag, reply.as_bytes());
598 let ImapAuthAnonymousError::No(text) = err else {
599 panic!("expected ImapAuthAnonymousError::No, got {err:?}");
600 };
601 assert_eq!(text, "anonymous access disabled");
602 }
603
604 fn expect_wants_write(
605 cor: &mut ImapAuthAnonymous,
606 frag: &mut Fragmentizer,
607 arg: Option<&[u8]>,
608 ) -> Vec<u8> {
609 match cor.resume(frag, arg) {
610 ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => bytes,
611 state => panic!("expected WantsWrite, got {state:?}"),
612 }
613 }
614
615 fn expect_wants_read(cor: &mut ImapAuthAnonymous, frag: &mut Fragmentizer) {
616 match cor.resume(frag, None) {
617 ImapCoroutineState::Yielded(ImapYield::WantsRead) => {}
618 state => panic!("expected WantsRead, got {state:?}"),
619 }
620 }
621
622 fn expect_complete_ok(cor: &mut ImapAuthAnonymous, frag: &mut Fragmentizer, reply: &[u8]) {
623 match cor.resume(frag, Some(reply)) {
624 ImapCoroutineState::Complete(Ok(_)) => {}
625 state => panic!("expected Complete(Ok), got {state:?}"),
626 }
627 }
628
629 fn expect_complete_err(
630 cor: &mut ImapAuthAnonymous,
631 frag: &mut Fragmentizer,
632 reply: &[u8],
633 ) -> ImapAuthAnonymousError {
634 match cor.resume(frag, Some(reply)) {
635 ImapCoroutineState::Complete(Err(err)) => err,
636 state => panic!("expected Complete(Err), got {state:?}"),
637 }
638 }
639
640 fn first_word(line: &str) -> &str {
641 line.split_whitespace()
642 .next()
643 .expect("first whitespace-separated token")
644 }
645}