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