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