Skip to main content

io_imap/sasl/
auth_plain.rs

1//! IMAP SASL PLAIN coroutine; supports both the non-IR and SASL-IR
2//! (RFC 4959) flows.
3//!
4//! PLAIN: <https://www.rfc-editor.org/rfc/rfc4616>
5//! SASL-IR: <https://www.rfc-editor.org/rfc/rfc4959>
6//!
7//! # Example
8//!
9//! ```rust,no_run
10//! use std::{
11//!     io::{Read, Write},
12//!     net::TcpStream,
13//! };
14//!
15//! use io_imap::{
16//!     codec::fragmentizer::Fragmentizer,
17//!     coroutine::{ImapCoroutine, ImapCoroutineState, ImapYield},
18//!     sasl::auth_plain::{ImapAuthPlain, ImapAuthPlainOptions},
19//! };
20//!
21//! // Ready stream needed (TCP-connected, TLS-negotiated)
22//! let mut stream = TcpStream::connect("localhost:143").unwrap();
23//!
24//! let mut fragmentizer = Fragmentizer::new(50 * 1024 * 1024);
25//! let mut buf = [0u8; 4096];
26//!
27//! let authzid: Option<&str> = None;
28//! let opts = ImapAuthPlainOptions::default();
29//! let mut coroutine = ImapAuthPlain::new(authzid, "alice", "secret", opts);
30//! let mut arg = None;
31//!
32//! let capability = loop {
33//!     match coroutine.resume(&mut fragmentizer, arg.take()) {
34//!         ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => {
35//!             stream.write_all(&bytes).unwrap();
36//!         }
37//!         ImapCoroutineState::Yielded(ImapYield::WantsRead) => {
38//!             let n = stream.read(&mut buf).unwrap();
39//!             arg = Some(&buf[..n]);
40//!         }
41//!         ImapCoroutineState::Complete(Ok(capability)) => break capability,
42//!         ImapCoroutineState::Complete(Err(err)) => panic!("{err}"),
43//!     }
44//! };
45//!
46//! println!("{capability:?}");
47//! ```
48
49use 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/// Failure causes during the SASL PLAIN flow.
75#[derive(Clone, Debug, Error)]
76pub enum ImapAuthPlainError {
77    /// The server rejected authentication with a tagged NO.
78    #[error("IMAP AUTHENTICATE PLAIN failed: NO {0}")]
79    No(String),
80    /// The server rejected the AUTHENTICATE command with a tagged BAD.
81    #[error("IMAP AUTHENTICATE PLAIN failed: BAD {0}")]
82    Bad(String),
83    /// The server closed the connection with an untagged BYE.
84    #[error("IMAP AUTHENTICATE PLAIN failed: BYE {0}")]
85    Bye(String),
86    /// The server never returned the final tagged response.
87    #[error("IMAP AUTHENTICATE PLAIN failed: server did not return a tagged response")]
88    MissingTagged,
89    /// The server never sent the expected continuation request.
90    #[error(
91        "IMAP AUTHENTICATE PLAIN failed: server did not send the expected continuation request"
92    )]
93    ExpectedContinuationRequest,
94    /// The server sent a continuation request after the exchange ended.
95    #[error("IMAP AUTHENTICATE PLAIN failed: server sent an unexpected continuation request")]
96    UnexpectedContinuationRequest,
97    /// The server returned OK before the mechanism could complete.
98    #[error(
99        "IMAP AUTHENTICATE PLAIN failed: server returned OK before the mechanism could complete"
100    )]
101    UnexpectedOk,
102    /// The underlying send coroutine failed.
103    #[error("IMAP AUTHENTICATE PLAIN failed: {0}")]
104    Send(#[from] ImapSendError),
105    /// The follow-up CAPABILITY command failed.
106    #[error(transparent)]
107    Capability(#[from] ImapCapabilityGetError),
108    /// The follow-up ID command failed.
109    #[error(transparent)]
110    ServerId(#[from] ImapServerIdError),
111}
112
113/// Options for [`ImapAuthPlain::new`].
114#[derive(Clone, Debug, Default, Eq, PartialEq)]
115pub struct ImapAuthPlainOptions {
116    /// `true` selects SASL-IR (RFC 4959, inline credentials);
117    /// `false` selects the non-IR upload-after-challenge flow.
118    pub initial_request: bool,
119    /// Fetch CAPABILITY after authentication when the tagged response
120    /// carries no capability data. Defaults to `false`.
121    pub ensure_capabilities: bool,
122    /// Chain an RFC 2971 ID round-trip right after authentication, as
123    /// required by some providers.
124    ///
125    /// Defaults to `None` (no ID); an empty list sends ID NIL.
126    pub auto_id: Option<Vec<(IString<'static>, NString<'static>)>>,
127}
128
129/// I/O-free SASL PLAIN coroutine.
130pub struct ImapAuthPlain {
131    state: State,
132    observed: Vec<Capability<'static>>,
133    opts: ImapAuthPlainOptions,
134}
135
136impl ImapAuthPlain {
137    /// Builds a SASL PLAIN coroutine authenticating `authcid` with
138    /// `password`.
139    ///
140    /// `authzid` is the optional RFC 4616 authorization identity;
141    /// `authcid` is the authentication identity (typically the
142    /// username). Depending on `opts.initial_request`, the credentials
143    /// go inline with the AUTHENTICATE command (SASL-IR) or are
144    /// uploaded after the server challenge.
145    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}