Skip to main content

io_imap/rfc7628/
auth_oauthbearer.rs

1//! IMAP SASL OAUTHBEARER coroutine; supports both the non-IR and
2//! SASL-IR (RFC 4959) flows.
3//!
4//! SASL-IR: <https://www.rfc-editor.org/rfc/rfc4959>
5//!
6//! # Example
7//!
8//! ```rust,no_run
9//! use std::{
10//!     io::{Read, Write},
11//!     net::TcpStream,
12//! };
13//!
14//! use io_imap::{
15//!     codec::fragmentizer::Fragmentizer,
16//!     coroutine::{ImapCoroutine, ImapCoroutineState, ImapYield},
17//!     rfc7628::auth_oauthbearer::{ImapAuthOauthbearer, ImapAuthOauthbearerOptions},
18//! };
19//!
20//! // Ready stream needed (TCP-connected, TLS-negotiated)
21//! let mut stream = TcpStream::connect("localhost:143").unwrap();
22//!
23//! let mut fragmentizer = Fragmentizer::new(50 * 1024 * 1024);
24//! let mut buf = [0u8; 4096];
25//!
26//! let opts = ImapAuthOauthbearerOptions::default();
27//! let mut coroutine = ImapAuthOauthbearer::new(
28//!     "alice@example.org",
29//!     "imap.example.org",
30//!     993,
31//!     "oauth-token",
32//!     opts,
33//! );
34//! let mut arg = None;
35//!
36//! let capability = loop {
37//!     match coroutine.resume(&mut fragmentizer, arg.take()) {
38//!         ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => {
39//!             stream.write_all(&bytes).unwrap();
40//!         }
41//!         ImapCoroutineState::Yielded(ImapYield::WantsRead) => {
42//!             let n = stream.read(&mut buf).unwrap();
43//!             arg = Some(&buf[..n]);
44//!         }
45//!         ImapCoroutineState::Complete(Ok(capability)) => break capability,
46//!         ImapCoroutineState::Complete(Err(err)) => panic!("{err}"),
47//!     }
48//! };
49//!
50//! println!("{capability:?}");
51//! ```
52
53use core::{fmt, mem};
54
55use alloc::{
56    borrow::Cow,
57    format,
58    string::{String, ToString},
59    vec,
60    vec::Vec,
61};
62
63use imap_codec::{
64    AuthenticateDataCodec, CommandCodec,
65    fragmentizer::Fragmentizer,
66    imap_types::{
67        auth::{AuthMechanism, AuthenticateData},
68        command::{Command, CommandBody},
69        core::{IString, NString, TagGenerator},
70        response::{
71            Capability, Code, CommandContinuationRequest, Data, StatusBody, StatusKind, Tagged,
72        },
73        secret::Secret,
74    },
75};
76use log::{debug, trace};
77use thiserror::Error;
78
79use crate::{coroutine::*, imap_try, rfc2971::id::*, rfc3501::capability::*, send::*};
80
81/// Failure causes during the SASL OAUTHBEARER flow.
82#[derive(Clone, Debug, Error)]
83pub enum ImapAuthOauthbearerError {
84    /// The server rejected authentication with a tagged NO.
85    #[error("IMAP AUTHENTICATE OAUTHBEARER failed: NO {0}")]
86    No(String),
87    /// The server rejected authentication with a tagged NO after
88    /// returning an error payload in a challenge.
89    #[error("IMAP AUTHENTICATE OAUTHBEARER failed: NO {info} ({err})")]
90    NoWithError {
91        /// The tagged NO response text.
92        info: String,
93        /// The error payload extracted from the challenge.
94        err: String,
95    },
96    /// The server rejected the AUTHENTICATE command with a tagged BAD.
97    #[error("IMAP AUTHENTICATE OAUTHBEARER failed: BAD {0}")]
98    Bad(String),
99    /// The server closed the connection with an untagged BYE.
100    #[error("IMAP AUTHENTICATE OAUTHBEARER failed: BYE {0}")]
101    Bye(String),
102    /// The server never returned the final tagged response.
103    #[error("IMAP AUTHENTICATE OAUTHBEARER failed: server did not return a tagged response")]
104    MissingTagged,
105    /// The server never sent the expected continuation request.
106    #[error(
107        "IMAP AUTHENTICATE OAUTHBEARER failed: server did not send the expected continuation request"
108    )]
109    ExpectedContinuationRequest,
110    /// The server answered the error acknowledgement with a status
111    /// other than the expected NO.
112    #[error("IMAP AUTHENTICATE OAUTHBEARER failed: expected NO got {kind:?} ({info})")]
113    UnexpectedStatus {
114        /// The status kind the server answered instead of NO.
115        kind: StatusKind,
116        /// The status response text.
117        info: String,
118    },
119    /// The server returned OK before the mechanism could complete.
120    #[error(
121        "IMAP AUTHENTICATE OAUTHBEARER failed: server returned OK before the mechanism could complete"
122    )]
123    UnexpectedOk,
124    /// The underlying send coroutine failed.
125    #[error("IMAP AUTHENTICATE OAUTHBEARER failed: {0}")]
126    Send(#[from] ImapSendError),
127    /// The follow-up CAPABILITY command failed.
128    #[error(transparent)]
129    Capability(#[from] ImapCapabilityGetError),
130    /// The follow-up ID command failed.
131    #[error(transparent)]
132    ServerId(#[from] ImapServerIdError),
133}
134
135/// Options for [`ImapAuthOauthbearer::new`].
136#[derive(Clone, Debug, Default, Eq, PartialEq)]
137pub struct ImapAuthOauthbearerOptions {
138    /// `true` selects SASL-IR (RFC 4959, inline credentials);
139    /// `false` selects the non-IR upload-after-challenge flow.
140    pub initial_request: bool,
141    /// Fetch CAPABILITY after authentication when the tagged response
142    /// carries no capability data. Defaults to `false`.
143    pub ensure_capabilities: bool,
144    /// Chain an RFC 2971 ID round-trip right after authentication, as
145    /// required by some providers.
146    ///
147    /// Defaults to `None` (no ID); an empty list sends ID NIL.
148    pub auto_id: Option<Vec<(IString<'static>, NString<'static>)>>,
149}
150
151/// I/O-free SASL OAUTHBEARER coroutine.
152pub struct ImapAuthOauthbearer {
153    state: State,
154    error: Option<String>,
155    observed: Vec<Capability<'static>>,
156    opts: ImapAuthOauthbearerOptions,
157}
158
159impl ImapAuthOauthbearer {
160    /// Builds a SASL OAUTHBEARER coroutine authenticating `user` with
161    /// the bearer `token`, targeting the server at `host` and `port`.
162    ///
163    /// Depending on `opts.initial_request`, the credentials go inline
164    /// with the AUTHENTICATE command (SASL-IR) or are uploaded after
165    /// the server challenge.
166    pub fn new(
167        user: impl AsRef<str>,
168        host: impl AsRef<str>,
169        port: u16,
170        token: impl AsRef<str>,
171        opts: ImapAuthOauthbearerOptions,
172    ) -> Self {
173        let tag = TagGenerator::new().generate();
174
175        let u = user.as_ref();
176        let h = host.as_ref();
177        let t = token.as_ref();
178
179        let payload = format!("n,a={u},\x01host={h}\x01port={port}\x01auth=Bearer {t}\x01\x01");
180        let payload = payload.into_bytes().into();
181
182        let state = if opts.initial_request {
183            let body = CommandBody::Authenticate {
184                mechanism: AuthMechanism::OAuthBearer,
185                initial_response: Some(Secret::new(payload)),
186            };
187            let cmd = Command { tag, body };
188            trace!("send IMAP command {cmd:?}");
189            State::SendIr(ImapSend::new(CommandCodec::new(), cmd))
190        } else {
191            let body = CommandBody::Authenticate {
192                mechanism: AuthMechanism::OAuthBearer,
193                initial_response: None,
194            };
195            let cmd = Command { tag, body };
196            trace!("send IMAP command {cmd:?}");
197            let send = ImapSend::new(CommandCodec::new(), cmd);
198            State::Send { payload, send }
199        };
200
201        Self {
202            state,
203            error: None,
204            observed: Vec::new(),
205            opts,
206        }
207    }
208
209    fn wants_capability(
210        &mut self,
211        code: Option<Code<'static>>,
212        data: Vec<Data<'static>>,
213        untagged: Vec<StatusBody<'static>>,
214    ) -> Option<State> {
215        let mut new_capability = None;
216
217        if let Some(Code::Capability(capability)) = code {
218            new_capability.replace(capability);
219        }
220
221        for data in data {
222            if let Data::Capability(capability) = data {
223                new_capability.replace(capability);
224            }
225        }
226
227        for StatusBody { code, .. } in untagged {
228            if let Some(Code::Capability(capability)) = code {
229                new_capability.replace(capability);
230            }
231        }
232
233        if let Some(capability) = new_capability {
234            self.observed = capability.into_iter().collect();
235        }
236
237        (self.opts.ensure_capabilities && self.observed.is_empty())
238            .then(|| State::Capability(ImapCapabilityGet::new()))
239    }
240
241    fn wants_id(&mut self) -> Option<State> {
242        let params = self.opts.auto_id.take()?;
243        let wire = (!params.is_empty()).then_some(params);
244        Some(State::Id(ImapServerId::new(ImapServerIdOptions {
245            parameters: wire,
246        })))
247    }
248
249    fn extract_json_error(cr: &CommandContinuationRequest<'_>) -> String {
250        let err = match cr {
251            CommandContinuationRequest::Basic(err) => err.text().to_string().into(),
252            CommandContinuationRequest::Base64(err) => String::from_utf8_lossy(err),
253        };
254
255        err.to_string()
256    }
257}
258
259impl ImapCoroutine for ImapAuthOauthbearer {
260    type Yield = ImapYield;
261    type Return = Result<Vec<Capability<'static>>, ImapAuthOauthbearerError>;
262
263    fn resume(
264        &mut self,
265        fragmentizer: &mut Fragmentizer,
266        arg: Option<&[u8]>,
267    ) -> ImapCoroutineState<Self::Yield, Self::Return> {
268        loop {
269            match &mut self.state {
270                State::Send { send, payload } => {
271                    let out = imap_try!(send, fragmentizer, arg);
272
273                    if let Some(bye) = out.bye {
274                        let err = ImapAuthOauthbearerError::Bye(bye.text.to_string());
275                        return ImapCoroutineState::Complete(Err(err));
276                    }
277
278                    if out.continuation_request.is_some() {
279                        let payload = mem::take(payload).into_owned();
280                        let auth = AuthenticateData::r#continue(payload);
281                        let codec = AuthenticateDataCodec::new();
282                        self.state = State::Continue(ImapSend::new(codec, auth));
283                        debug!("{}", self.state);
284                        continue;
285                    }
286
287                    if let Some(Tagged { body, .. }) = out.tagged {
288                        let err = match body.kind {
289                            StatusKind::Ok => ImapAuthOauthbearerError::UnexpectedOk,
290                            StatusKind::No => ImapAuthOauthbearerError::No(body.text.to_string()),
291                            StatusKind::Bad => ImapAuthOauthbearerError::Bad(body.text.to_string()),
292                        };
293
294                        return ImapCoroutineState::Complete(Err(err));
295                    }
296
297                    let err = ImapAuthOauthbearerError::ExpectedContinuationRequest;
298                    return ImapCoroutineState::Complete(Err(err));
299                }
300                State::SendIr(send) => {
301                    let out = imap_try!(send, fragmentizer, arg);
302
303                    if let Some(bye) = out.bye {
304                        let err = ImapAuthOauthbearerError::Bye(bye.text.to_string());
305                        return ImapCoroutineState::Complete(Err(err));
306                    }
307
308                    if let Some(cr) = out.continuation_request {
309                        self.error.replace(Self::extract_json_error(&cr));
310                        let auth = AuthenticateData::r#continue(vec![0x01]);
311                        let codec = AuthenticateDataCodec::new();
312                        self.state = State::AcknowledgeError(ImapSend::new(codec, auth));
313                        debug!("{}", self.state);
314                        continue;
315                    }
316
317                    let Some(Tagged { body, .. }) = out.tagged else {
318                        let err = ImapAuthOauthbearerError::MissingTagged;
319                        return ImapCoroutineState::Complete(Err(err));
320                    };
321
322                    let code = match body.kind {
323                        StatusKind::Ok => body.code,
324                        StatusKind::No => {
325                            let err = ImapAuthOauthbearerError::No(body.text.to_string());
326                            return ImapCoroutineState::Complete(Err(err));
327                        }
328                        StatusKind::Bad => {
329                            let err = ImapAuthOauthbearerError::Bad(body.text.to_string());
330                            return ImapCoroutineState::Complete(Err(err));
331                        }
332                    };
333
334                    if let Some(next) = self.wants_capability(code, out.data, out.untagged) {
335                        self.state = next;
336                        debug!("{}", self.state);
337                        continue;
338                    }
339
340                    if let Some(next) = self.wants_id() {
341                        self.state = next;
342                        debug!("{}", self.state);
343                        continue;
344                    }
345
346                    let capability = mem::take(&mut self.observed);
347                    return ImapCoroutineState::Complete(Ok(capability));
348                }
349                State::Continue(send) => {
350                    let out = imap_try!(send, fragmentizer, arg);
351
352                    if let Some(bye) = out.bye {
353                        let err = ImapAuthOauthbearerError::Bye(bye.text.to_string());
354                        return ImapCoroutineState::Complete(Err(err));
355                    }
356
357                    if let Some(cr) = out.continuation_request {
358                        self.error.replace(Self::extract_json_error(&cr));
359                        let auth = AuthenticateData::r#continue(vec![0x01]);
360                        let codec = AuthenticateDataCodec::new();
361                        self.state = State::AcknowledgeError(ImapSend::new(codec, auth));
362                        debug!("{}", self.state);
363                        continue;
364                    }
365
366                    let Some(Tagged { body, .. }) = out.tagged else {
367                        let err = ImapAuthOauthbearerError::MissingTagged;
368                        return ImapCoroutineState::Complete(Err(err));
369                    };
370
371                    let code = match body.kind {
372                        StatusKind::Ok => body.code,
373                        StatusKind::No => {
374                            let err = ImapAuthOauthbearerError::No(body.text.to_string());
375                            return ImapCoroutineState::Complete(Err(err));
376                        }
377                        StatusKind::Bad => {
378                            let err = ImapAuthOauthbearerError::Bad(body.text.to_string());
379                            return ImapCoroutineState::Complete(Err(err));
380                        }
381                    };
382
383                    if let Some(next) = self.wants_capability(code, out.data, out.untagged) {
384                        self.state = next;
385                        debug!("{}", self.state);
386                        continue;
387                    }
388
389                    if let Some(next) = self.wants_id() {
390                        self.state = next;
391                        debug!("{}", self.state);
392                        continue;
393                    }
394
395                    let capability = mem::take(&mut self.observed);
396                    return ImapCoroutineState::Complete(Ok(capability));
397                }
398                State::AcknowledgeError(send) => {
399                    let out = imap_try!(send, fragmentizer, arg);
400
401                    if let Some(bye) = out.bye {
402                        let err = ImapAuthOauthbearerError::Bye(bye.text.to_string());
403                        return ImapCoroutineState::Complete(Err(err));
404                    }
405
406                    let Some(Tagged { body, .. }) = out.tagged else {
407                        let err = ImapAuthOauthbearerError::MissingTagged;
408                        return ImapCoroutineState::Complete(Err(err));
409                    };
410
411                    let info = body.text.to_string();
412
413                    let StatusKind::No = body.kind else {
414                        let kind = body.kind;
415                        let err = ImapAuthOauthbearerError::UnexpectedStatus { kind, info };
416                        return ImapCoroutineState::Complete(Err(err));
417                    };
418
419                    let err = match self.error.take() {
420                        Some(err) => ImapAuthOauthbearerError::NoWithError { info, err },
421                        None => ImapAuthOauthbearerError::No(info),
422                    };
423
424                    return ImapCoroutineState::Complete(Err(err));
425                }
426                State::Capability(capability) => {
427                    self.observed = imap_try!(capability, fragmentizer, arg);
428
429                    if let Some(next) = self.wants_id() {
430                        self.state = next;
431                        debug!("{}", self.state);
432                        continue;
433                    }
434
435                    let capability = mem::take(&mut self.observed);
436                    return ImapCoroutineState::Complete(Ok(capability));
437                }
438                State::Id(id) => {
439                    imap_try!(id, fragmentizer, arg);
440                    let capability = mem::take(&mut self.observed);
441                    return ImapCoroutineState::Complete(Ok(capability));
442                }
443            }
444        }
445    }
446}
447
448enum State {
449    Send {
450        send: ImapSend<CommandCodec>,
451        payload: Cow<'static, [u8]>,
452    },
453    SendIr(ImapSend<CommandCodec>),
454    Continue(ImapSend<AuthenticateDataCodec>),
455    AcknowledgeError(ImapSend<AuthenticateDataCodec>),
456    Capability(ImapCapabilityGet),
457    Id(ImapServerId),
458}
459
460impl fmt::Display for State {
461    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
462        match self {
463            Self::Send { .. } => f.write_str("send auth"),
464            Self::SendIr(_) => f.write_str("send auth with ir"),
465            Self::Continue(_) => f.write_str("send credentials"),
466            Self::AcknowledgeError(_) => f.write_str("acknowledge error"),
467            Self::Capability(_) => f.write_str("fetch capabilities"),
468            Self::Id(_) => f.write_str("send id"),
469        }
470    }
471}
472
473#[cfg(test)]
474mod tests {
475    use core::str;
476
477    use crate::rfc7628::auth_oauthbearer::*;
478
479    #[test]
480    fn ir_success_returns_ok() {
481        let opts = ImapAuthOauthbearerOptions {
482            initial_request: true,
483            ..Default::default()
484        };
485
486        let mut auth = ImapAuthOauthbearer::new(
487            "user@example.org",
488            "imap.example.org",
489            993,
490            "oauth-token",
491            opts,
492        );
493        let mut frag = Fragmentizer::new(50 * 1024 * 1024);
494
495        let bytes = expect_wants_write(&mut auth, &mut frag, None);
496        let line = str::from_utf8(&bytes).expect("utf8 command");
497        let tag = first_word(line);
498        assert!(line.contains("AUTHENTICATE OAUTHBEARER "));
499
500        expect_wants_read(&mut auth, &mut frag);
501
502        let reply = format!("{tag} OK AUTHENTICATE completed\r\n");
503        expect_complete_ok(&mut auth, &mut frag, reply.as_bytes());
504    }
505
506    #[test]
507    fn ir_invalid_token_returns_no_with_error() {
508        let opts = ImapAuthOauthbearerOptions {
509            initial_request: true,
510            ..Default::default()
511        };
512
513        let mut auth = ImapAuthOauthbearer::new(
514            "user@example.org",
515            "imap.example.org",
516            993,
517            "expired-token",
518            opts,
519        );
520        let mut frag = Fragmentizer::new(50 * 1024 * 1024);
521
522        let bytes = expect_wants_write(&mut auth, &mut frag, None);
523        let tag = first_word(str::from_utf8(&bytes).expect("utf8 command"));
524
525        expect_wants_read(&mut auth, &mut frag);
526
527        let (err_json_b64, err_json) = fake_json_error();
528        let challenge = format!("+ {err_json_b64}\r\n");
529        let ack = expect_wants_write(&mut auth, &mut frag, Some(challenge.as_bytes()));
530        assert_eq!(b"AQ==\r\n", &*ack);
531
532        expect_wants_read(&mut auth, &mut frag);
533
534        let reply = format!("{tag} NO SASL authentication failed\r\n");
535        let err = expect_complete_err(&mut auth, &mut frag, reply.as_bytes());
536        let ImapAuthOauthbearerError::NoWithError { info, err } = err else {
537            panic!("expected ImapAuthOauthbearerError::NoWithError, got {err:?}");
538        };
539        assert_eq!(info, "SASL authentication failed");
540        assert_eq!(err, err_json);
541    }
542
543    #[test]
544    fn ir_tagged_bad_returns_bad_error() {
545        let opts = ImapAuthOauthbearerOptions {
546            initial_request: true,
547            ..Default::default()
548        };
549
550        let mut auth = ImapAuthOauthbearer::new(
551            "user@example.org",
552            "imap.example.org",
553            993,
554            "oauth-token",
555            opts,
556        );
557        let mut frag = Fragmentizer::new(50 * 1024 * 1024);
558
559        let bytes = expect_wants_write(&mut auth, &mut frag, None);
560        let tag = first_word(str::from_utf8(&bytes).expect("utf8 command"));
561
562        expect_wants_read(&mut auth, &mut frag);
563
564        let reply = format!("{tag} BAD AUTHENTICATE not enabled\r\n");
565        let err = expect_complete_err(&mut auth, &mut frag, reply.as_bytes());
566        let ImapAuthOauthbearerError::Bad(text) = err else {
567            panic!("expected ImapAuthOauthbearerError::Bad, got {err:?}");
568        };
569        assert_eq!(text, "AUTHENTICATE not enabled");
570    }
571
572    #[test]
573    fn non_ir_success_returns_ok() {
574        let opts = ImapAuthOauthbearerOptions::default();
575        let mut auth = ImapAuthOauthbearer::new(
576            "user@example.org",
577            "imap.example.org",
578            993,
579            "oauth-token",
580            opts,
581        );
582        let mut frag = Fragmentizer::new(50 * 1024 * 1024);
583
584        let bytes = expect_wants_write(&mut auth, &mut frag, None);
585        let line = str::from_utf8(&bytes).expect("utf8 command");
586        let tag = first_word(line);
587        assert!(line.trim_end().ends_with("AUTHENTICATE OAUTHBEARER"));
588
589        expect_wants_read(&mut auth, &mut frag);
590
591        let creds = expect_wants_write(&mut auth, &mut frag, Some(b"+ \r\n"));
592        assert!(creds.ends_with(b"\r\n"));
593
594        expect_wants_read(&mut auth, &mut frag);
595
596        let reply = format!("{tag} OK AUTHENTICATE completed\r\n");
597        expect_complete_ok(&mut auth, &mut frag, reply.as_bytes());
598    }
599
600    #[test]
601    fn non_ir_invalid_token_returns_no_with_error() {
602        let opts = ImapAuthOauthbearerOptions::default();
603        let mut auth = ImapAuthOauthbearer::new(
604            "user@example.org",
605            "imap.example.org",
606            993,
607            "expired-token",
608            opts,
609        );
610        let mut frag = Fragmentizer::new(50 * 1024 * 1024);
611
612        let bytes = expect_wants_write(&mut auth, &mut frag, None);
613        let tag = first_word(str::from_utf8(&bytes).expect("utf8 command"));
614
615        expect_wants_read(&mut auth, &mut frag);
616        expect_wants_write(&mut auth, &mut frag, Some(b"+ \r\n"));
617        expect_wants_read(&mut auth, &mut frag);
618
619        let (err_json_b64, err_json) = fake_json_error();
620        let challenge = format!("+ {err_json_b64}\r\n");
621        let ack = expect_wants_write(&mut auth, &mut frag, Some(challenge.as_bytes()));
622        assert_eq!(b"AQ==\r\n", &*ack);
623
624        expect_wants_read(&mut auth, &mut frag);
625
626        let reply = format!("{tag} NO SASL authentication failed\r\n");
627        let err = expect_complete_err(&mut auth, &mut frag, reply.as_bytes());
628        let ImapAuthOauthbearerError::NoWithError { info, err } = err else {
629            panic!("expected ImapAuthOauthbearerError::NoWithError, got {err:?}");
630        };
631        assert_eq!(info, "SASL authentication failed");
632        assert_eq!(err, err_json);
633    }
634
635    fn expect_wants_write(
636        cor: &mut ImapAuthOauthbearer,
637        frag: &mut Fragmentizer,
638        arg: Option<&[u8]>,
639    ) -> Vec<u8> {
640        match cor.resume(frag, arg) {
641            ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => bytes,
642            state => panic!("expected WantsWrite, got {state:?}"),
643        }
644    }
645
646    fn expect_wants_read(cor: &mut ImapAuthOauthbearer, frag: &mut Fragmentizer) {
647        match cor.resume(frag, None) {
648            ImapCoroutineState::Yielded(ImapYield::WantsRead) => {}
649            state => panic!("expected WantsRead, got {state:?}"),
650        }
651    }
652
653    fn expect_complete_ok(cor: &mut ImapAuthOauthbearer, frag: &mut Fragmentizer, reply: &[u8]) {
654        match cor.resume(frag, Some(reply)) {
655            ImapCoroutineState::Complete(Ok(_)) => {}
656            state => panic!("expected Complete(Ok), got {state:?}"),
657        }
658    }
659
660    fn expect_complete_err(
661        cor: &mut ImapAuthOauthbearer,
662        frag: &mut Fragmentizer,
663        reply: &[u8],
664    ) -> ImapAuthOauthbearerError {
665        match cor.resume(frag, Some(reply)) {
666            ImapCoroutineState::Complete(Err(err)) => err,
667            state => panic!("expected Complete(Err), got {state:?}"),
668        }
669    }
670
671    fn first_word(line: &str) -> &str {
672        line.split_whitespace()
673            .next()
674            .expect("first whitespace-separated token")
675    }
676
677    fn fake_json_error() -> (&'static str, &'static str) {
678        (
679            "eyJzdGF0dXMiOiJpbnZhbGlkX3Rva2VuIiwic2NvcGUiOiJleGFtcGxlX3Njb3BlIiwib3BlbmlkLWNvbmZpZ3VyYXRpb24iOiJodHRwczovL2V4YW1wbGUuY29tLy53ZWxsLWtub3duL29wZW5pZC1jb25maWd1cmF0aW9uIn0=",
680            "{\"status\":\"invalid_token\",\"scope\":\"example_scope\",\"openid-configuration\":\"https://example.com/.well-known/openid-configuration\"}",
681        )
682    }
683}