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