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