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