1use core::{fmt, mem};
49
50use alloc::{
51 string::{String, ToString},
52 vec::Vec,
53};
54
55use imap_codec::{
56 AuthenticateDataCodec, CommandCodec,
57 fragmentizer::Fragmentizer,
58 imap_types::{
59 auth::{AuthMechanism, AuthenticateData},
60 command::{Command, CommandBody},
61 core::{IString, NString, TagGenerator},
62 response::{Capability, Code, Data, StatusBody, StatusKind, Tagged},
63 secret::Secret,
64 },
65};
66use log::{debug, trace};
67use thiserror::Error;
68
69use crate::{coroutine::*, imap_try, rfc2971::id::*, rfc3501::capability::*, send::*};
70
71#[derive(Clone, Debug, Error)]
73pub enum ImapAuthLoginError {
74 #[error("IMAP AUTHENTICATE LOGIN failed: NO {0}")]
76 No(String),
77 #[error("IMAP AUTHENTICATE LOGIN failed: BAD {0}")]
79 Bad(String),
80 #[error("IMAP AUTHENTICATE LOGIN failed: BYE {0}")]
82 Bye(String),
83 #[error("IMAP AUTHENTICATE LOGIN failed: server did not return a tagged response")]
85 MissingTagged,
86 #[error(
88 "IMAP AUTHENTICATE LOGIN failed: server did not send the expected continuation request"
89 )]
90 ExpectedContinuationRequest,
91 #[error("IMAP AUTHENTICATE LOGIN failed: server sent an unexpected continuation request")]
93 UnexpectedContinuationRequest,
94 #[error(
96 "IMAP AUTHENTICATE LOGIN failed: server returned OK before the mechanism could complete"
97 )]
98 UnexpectedOk,
99 #[error("IMAP AUTHENTICATE LOGIN failed: {0}")]
101 Send(#[from] ImapSendError),
102 #[error(transparent)]
104 Capability(#[from] ImapCapabilityGetError),
105 #[error(transparent)]
107 ServerId(#[from] ImapServerIdError),
108}
109
110#[derive(Clone, Debug, Default, Eq, PartialEq)]
112pub struct ImapAuthLoginOptions {
113 pub initial_request: bool,
116 pub ensure_capabilities: bool,
119 pub auto_id: Option<Vec<(IString<'static>, NString<'static>)>>,
124}
125
126pub struct ImapAuthLogin {
128 state: State,
129 password: String,
130 observed: Vec<Capability<'static>>,
131 opts: ImapAuthLoginOptions,
132}
133
134impl ImapAuthLogin {
135 pub fn new(
142 user: impl AsRef<str>,
143 password: impl AsRef<str>,
144 opts: ImapAuthLoginOptions,
145 ) -> Self {
146 let user = user.as_ref();
147 let password = password.as_ref().to_string();
148 let tag = TagGenerator::new().generate();
149
150 let state = if opts.initial_request {
151 let body = CommandBody::Authenticate {
152 mechanism: AuthMechanism::Login,
153 initial_response: Some(Secret::new(user.as_bytes().to_vec().into())),
154 };
155 let cmd = Command { tag, body };
156 trace!("send IMAP command {cmd:?}");
157 State::SendIr(ImapSend::new(CommandCodec::new(), cmd))
158 } else {
159 let body = CommandBody::Authenticate {
160 mechanism: AuthMechanism::Login,
161 initial_response: None,
162 };
163 let cmd = Command { tag, body };
164 trace!("send IMAP command {cmd:?}");
165 State::Send {
166 send: ImapSend::new(CommandCodec::new(), cmd),
167 user: user.to_string(),
168 }
169 };
170
171 Self {
172 state,
173 password,
174 observed: Vec::new(),
175 opts,
176 }
177 }
178
179 fn wants_capability(
180 &mut self,
181 code: Option<Code<'static>>,
182 data: Vec<Data<'static>>,
183 untagged: Vec<StatusBody<'static>>,
184 ) -> Option<State> {
185 let mut new_capability = None;
186
187 if let Some(Code::Capability(capability)) = code {
188 new_capability.replace(capability);
189 }
190
191 for data in data {
192 if let Data::Capability(capability) = data {
193 new_capability.replace(capability);
194 }
195 }
196
197 for StatusBody { code, .. } in untagged {
198 if let Some(Code::Capability(capability)) = code {
199 new_capability.replace(capability);
200 }
201 }
202
203 if let Some(capability) = new_capability {
204 self.observed = capability.into_iter().collect();
205 }
206
207 (self.opts.ensure_capabilities && self.observed.is_empty())
208 .then(|| State::Capability(ImapCapabilityGet::new()))
209 }
210
211 fn wants_id(&mut self) -> Option<State> {
212 let params = self.opts.auto_id.take()?;
213 let wire = (!params.is_empty()).then_some(params);
214 Some(State::Id(ImapServerId::new(ImapServerIdOptions {
215 parameters: wire,
216 })))
217 }
218
219 fn next_continue_password(&mut self) -> State {
220 let password = mem::take(&mut self.password).into_bytes();
221 let auth = AuthenticateData::r#continue(password);
222 let codec = AuthenticateDataCodec::new();
223 State::ContinuePassword(ImapSend::new(codec, auth))
224 }
225}
226
227impl ImapCoroutine for ImapAuthLogin {
228 type Yield = ImapYield;
229 type Return = Result<Vec<Capability<'static>>, ImapAuthLoginError>;
230
231 fn resume(
232 &mut self,
233 fragmentizer: &mut Fragmentizer,
234 arg: Option<&[u8]>,
235 ) -> ImapCoroutineState<Self::Yield, Self::Return> {
236 loop {
237 match &mut self.state {
238 State::Send { send, user } => {
239 let out = imap_try!(send, fragmentizer, arg);
240
241 if let Some(bye) = out.bye {
242 let err = ImapAuthLoginError::Bye(bye.text.to_string());
243 return ImapCoroutineState::Complete(Err(err));
244 }
245
246 if out.continuation_request.is_some() {
247 let user = mem::take(user).into_bytes();
248 let auth = AuthenticateData::r#continue(user);
249 let codec = AuthenticateDataCodec::new();
250 self.state = State::ContinueUsername(ImapSend::new(codec, auth));
251 debug!("{}", self.state);
252 continue;
253 }
254
255 if let Some(Tagged { body, .. }) = out.tagged {
256 let err = match body.kind {
257 StatusKind::Ok => ImapAuthLoginError::UnexpectedOk,
258 StatusKind::No => ImapAuthLoginError::No(body.text.to_string()),
259 StatusKind::Bad => ImapAuthLoginError::Bad(body.text.to_string()),
260 };
261
262 return ImapCoroutineState::Complete(Err(err));
263 }
264
265 let err = ImapAuthLoginError::ExpectedContinuationRequest;
266 return ImapCoroutineState::Complete(Err(err));
267 }
268 State::SendIr(send) => {
269 let out = imap_try!(send, fragmentizer, arg);
270
271 if let Some(bye) = out.bye {
272 let err = ImapAuthLoginError::Bye(bye.text.to_string());
273 return ImapCoroutineState::Complete(Err(err));
274 }
275
276 if out.continuation_request.is_some() {
277 self.state = self.next_continue_password();
278 debug!("{}", self.state);
279 continue;
280 }
281
282 if let Some(Tagged { body, .. }) = out.tagged {
283 let err = match body.kind {
284 StatusKind::Ok => ImapAuthLoginError::UnexpectedOk,
285 StatusKind::No => ImapAuthLoginError::No(body.text.to_string()),
286 StatusKind::Bad => ImapAuthLoginError::Bad(body.text.to_string()),
287 };
288
289 return ImapCoroutineState::Complete(Err(err));
290 }
291
292 let err = ImapAuthLoginError::ExpectedContinuationRequest;
293 return ImapCoroutineState::Complete(Err(err));
294 }
295 State::ContinueUsername(send) => {
296 let out = imap_try!(send, fragmentizer, arg);
297
298 if let Some(bye) = out.bye {
299 let err = ImapAuthLoginError::Bye(bye.text.to_string());
300 return ImapCoroutineState::Complete(Err(err));
301 }
302
303 if out.continuation_request.is_some() {
304 self.state = self.next_continue_password();
305 debug!("{}", self.state);
306 continue;
307 }
308
309 if let Some(Tagged { body, .. }) = out.tagged {
310 let err = match body.kind {
311 StatusKind::Ok => ImapAuthLoginError::UnexpectedOk,
312 StatusKind::No => ImapAuthLoginError::No(body.text.to_string()),
313 StatusKind::Bad => ImapAuthLoginError::Bad(body.text.to_string()),
314 };
315
316 return ImapCoroutineState::Complete(Err(err));
317 }
318
319 let err = ImapAuthLoginError::ExpectedContinuationRequest;
320 return ImapCoroutineState::Complete(Err(err));
321 }
322 State::ContinuePassword(send) => {
323 let out = imap_try!(send, fragmentizer, arg);
324
325 if let Some(bye) = out.bye {
326 let err = ImapAuthLoginError::Bye(bye.text.to_string());
327 return ImapCoroutineState::Complete(Err(err));
328 }
329
330 if out.continuation_request.is_some() {
331 let err = ImapAuthLoginError::UnexpectedContinuationRequest;
332 return ImapCoroutineState::Complete(Err(err));
333 }
334
335 let Some(Tagged { body, .. }) = out.tagged else {
336 let err = ImapAuthLoginError::MissingTagged;
337 return ImapCoroutineState::Complete(Err(err));
338 };
339
340 let code = match body.kind {
341 StatusKind::Ok => body.code,
342 StatusKind::No => {
343 let err = ImapAuthLoginError::No(body.text.to_string());
344 return ImapCoroutineState::Complete(Err(err));
345 }
346 StatusKind::Bad => {
347 let err = ImapAuthLoginError::Bad(body.text.to_string());
348 return ImapCoroutineState::Complete(Err(err));
349 }
350 };
351
352 if let Some(next) = self.wants_capability(code, out.data, out.untagged) {
353 self.state = next;
354 debug!("{}", self.state);
355 continue;
356 }
357
358 if let Some(next) = self.wants_id() {
359 self.state = next;
360 debug!("{}", self.state);
361 continue;
362 }
363
364 let capability = mem::take(&mut self.observed);
365 return ImapCoroutineState::Complete(Ok(capability));
366 }
367 State::Capability(capability) => {
368 self.observed = imap_try!(capability, fragmentizer, arg);
369
370 if let Some(next) = self.wants_id() {
371 self.state = next;
372 debug!("{}", self.state);
373 continue;
374 }
375
376 let capability = mem::take(&mut self.observed);
377 return ImapCoroutineState::Complete(Ok(capability));
378 }
379 State::Id(id) => {
380 imap_try!(id, fragmentizer, arg);
381 let capability = mem::take(&mut self.observed);
382 return ImapCoroutineState::Complete(Ok(capability));
383 }
384 }
385 }
386 }
387}
388
389enum State {
390 Send {
391 send: ImapSend<CommandCodec>,
392 user: String,
393 },
394 SendIr(ImapSend<CommandCodec>),
395 ContinueUsername(ImapSend<AuthenticateDataCodec>),
396 ContinuePassword(ImapSend<AuthenticateDataCodec>),
397 Capability(ImapCapabilityGet),
398 Id(ImapServerId),
399}
400
401impl fmt::Display for State {
402 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
403 match self {
404 Self::Send { .. } => f.write_str("send auth"),
405 Self::SendIr(_) => f.write_str("send auth with ir"),
406 Self::ContinueUsername(_) => f.write_str("send username"),
407 Self::ContinuePassword(_) => f.write_str("send password"),
408 Self::Capability(_) => f.write_str("fetch capabilities"),
409 Self::Id(_) => f.write_str("send id"),
410 }
411 }
412}
413
414#[cfg(test)]
415mod tests {
416 use core::str;
417
418 use alloc::format;
419
420 use crate::sasl::auth_login::*;
421
422 #[test]
423 fn ir_success_returns_ok() {
424 let opts = ImapAuthLoginOptions {
425 initial_request: true,
426 ..Default::default()
427 };
428
429 let mut auth = ImapAuthLogin::new("alice", "secret", opts);
430 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
431
432 let bytes = expect_wants_write(&mut auth, &mut frag, None);
433 let line = str::from_utf8(&bytes).expect("utf8 command");
434 let tag = first_word(line);
435 assert!(line.contains("AUTHENTICATE LOGIN "));
436
437 expect_wants_read(&mut auth, &mut frag);
438
439 let pass = expect_wants_write(&mut auth, &mut frag, Some(b"+ UGFzc3dvcmQ6\r\n"));
441 assert!(pass.ends_with(b"\r\n"));
442
443 expect_wants_read(&mut auth, &mut frag);
444
445 let reply = format!("{tag} OK AUTHENTICATE completed\r\n");
446 expect_complete_ok(&mut auth, &mut frag, reply.as_bytes());
447 }
448
449 #[test]
450 fn ir_invalid_password_returns_no_error() {
451 let opts = ImapAuthLoginOptions {
452 initial_request: true,
453 ..Default::default()
454 };
455
456 let mut auth = ImapAuthLogin::new("alice", "wrong", opts);
457 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
458
459 let bytes = expect_wants_write(&mut auth, &mut frag, None);
460 let tag = first_word(str::from_utf8(&bytes).expect("utf8 command"));
461
462 expect_wants_read(&mut auth, &mut frag);
463 expect_wants_write(&mut auth, &mut frag, Some(b"+ UGFzc3dvcmQ6\r\n"));
464 expect_wants_read(&mut auth, &mut frag);
465
466 let reply = format!("{tag} NO authentication failed\r\n");
467 let err = expect_complete_err(&mut auth, &mut frag, reply.as_bytes());
468 let ImapAuthLoginError::No(text) = err else {
469 panic!("expected ImapAuthLoginError::No, got {err:?}");
470 };
471 assert_eq!(text, "authentication failed");
472 }
473
474 #[test]
475 fn ir_tagged_bad_returns_bad_error() {
476 let opts = ImapAuthLoginOptions {
477 initial_request: true,
478 ..Default::default()
479 };
480
481 let mut auth = ImapAuthLogin::new("alice", "secret", opts);
482 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
483
484 let bytes = expect_wants_write(&mut auth, &mut frag, None);
485 let tag = first_word(str::from_utf8(&bytes).expect("utf8 command"));
486
487 expect_wants_read(&mut auth, &mut frag);
488
489 let reply = format!("{tag} BAD AUTHENTICATE not enabled\r\n");
490 let err = expect_complete_err(&mut auth, &mut frag, reply.as_bytes());
491 let ImapAuthLoginError::Bad(text) = err else {
492 panic!("expected ImapAuthLoginError::Bad, got {err:?}");
493 };
494 assert_eq!(text, "AUTHENTICATE not enabled");
495 }
496
497 #[test]
498 fn non_ir_success_returns_ok() {
499 let opts = ImapAuthLoginOptions::default();
500 let mut auth = ImapAuthLogin::new("alice", "secret", opts);
501 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
502
503 let bytes = expect_wants_write(&mut auth, &mut frag, None);
504 let line = str::from_utf8(&bytes).expect("utf8 command");
505 let tag = first_word(line);
506 assert!(line.trim_end().ends_with("AUTHENTICATE LOGIN"));
507
508 expect_wants_read(&mut auth, &mut frag);
509
510 let user = expect_wants_write(&mut auth, &mut frag, Some(b"+ VXNlcm5hbWU6\r\n"));
512 assert!(user.ends_with(b"\r\n"));
513
514 expect_wants_read(&mut auth, &mut frag);
515
516 let pass = expect_wants_write(&mut auth, &mut frag, Some(b"+ UGFzc3dvcmQ6\r\n"));
518 assert!(pass.ends_with(b"\r\n"));
519
520 expect_wants_read(&mut auth, &mut frag);
521
522 let reply = format!("{tag} OK AUTHENTICATE completed\r\n");
523 expect_complete_ok(&mut auth, &mut frag, reply.as_bytes());
524 }
525
526 #[test]
527 fn non_ir_invalid_password_returns_no_error() {
528 let opts = ImapAuthLoginOptions::default();
529 let mut auth = ImapAuthLogin::new("alice", "wrong", opts);
530 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
531
532 let bytes = expect_wants_write(&mut auth, &mut frag, None);
533 let tag = first_word(str::from_utf8(&bytes).expect("utf8 command"));
534
535 expect_wants_read(&mut auth, &mut frag);
536 expect_wants_write(&mut auth, &mut frag, Some(b"+ VXNlcm5hbWU6\r\n"));
537 expect_wants_read(&mut auth, &mut frag);
538 expect_wants_write(&mut auth, &mut frag, Some(b"+ UGFzc3dvcmQ6\r\n"));
539 expect_wants_read(&mut auth, &mut frag);
540
541 let reply = format!("{tag} NO authentication failed\r\n");
542 let err = expect_complete_err(&mut auth, &mut frag, reply.as_bytes());
543 let ImapAuthLoginError::No(text) = err else {
544 panic!("expected ImapAuthLoginError::No, got {err:?}");
545 };
546 assert_eq!(text, "authentication failed");
547 }
548
549 fn expect_wants_write(
550 cor: &mut ImapAuthLogin,
551 frag: &mut Fragmentizer,
552 arg: Option<&[u8]>,
553 ) -> Vec<u8> {
554 match cor.resume(frag, arg) {
555 ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => bytes,
556 state => panic!("expected WantsWrite, got {state:?}"),
557 }
558 }
559
560 fn expect_wants_read(cor: &mut ImapAuthLogin, frag: &mut Fragmentizer) {
561 match cor.resume(frag, None) {
562 ImapCoroutineState::Yielded(ImapYield::WantsRead) => {}
563 state => panic!("expected WantsRead, got {state:?}"),
564 }
565 }
566
567 fn expect_complete_ok(cor: &mut ImapAuthLogin, frag: &mut Fragmentizer, reply: &[u8]) {
568 match cor.resume(frag, Some(reply)) {
569 ImapCoroutineState::Complete(Ok(_)) => {}
570 state => panic!("expected Complete(Ok), got {state:?}"),
571 }
572 }
573
574 fn expect_complete_err(
575 cor: &mut ImapAuthLogin,
576 frag: &mut Fragmentizer,
577 reply: &[u8],
578 ) -> ImapAuthLoginError {
579 match cor.resume(frag, Some(reply)) {
580 ImapCoroutineState::Complete(Err(err)) => err,
581 state => panic!("expected Complete(Err), got {state:?}"),
582 }
583 }
584
585 fn first_word(line: &str) -> &str {
586 line.split_whitespace()
587 .next()
588 .expect("first whitespace-separated token")
589 }
590}