1use core::fmt;
52
53use alloc::{
54 borrow::Cow,
55 string::{String, ToString},
56 vec,
57 vec::Vec,
58};
59
60use base64::{Engine, engine::general_purpose::STANDARD as base64};
61use bounded_static::IntoBoundedStatic;
62use log::debug;
63use secrecy::{ExposeSecret, SecretBox, SecretString};
64use thiserror::Error;
65
66use crate::{
67 coroutine::*,
68 rfc4954::{auth::SmtpAuthCommand, auth_data::SmtpAuthData},
69 rfc5321::{
70 SmtpEhloDomain, SmtpReplyCode,
71 ehlo::{SmtpEhlo, SmtpEhloError},
72 },
73 send::*,
74 smtp_try,
75};
76
77pub const XOAUTH2: &str = "XOAUTH2";
79
80#[derive(Clone, Debug, Eq, PartialEq)]
82pub struct SmtpAuthXoauth2Options {
83 pub initial_request: bool,
86 pub ensure_capabilities: bool,
89}
90
91impl Default for SmtpAuthXoauth2Options {
92 fn default() -> Self {
93 Self {
94 initial_request: true,
95 ensure_capabilities: false,
96 }
97 }
98}
99
100#[derive(Debug, Error)]
102pub enum SmtpAuthXoauth2Error {
103 #[error("SMTP AUTH XOAUTH2 failed: rejected {code} {message}")]
105 Rejected {
106 code: u16,
108 message: String,
110 },
111 #[error("SMTP AUTH XOAUTH2 failed: server did not send the expected continuation request")]
113 ExpectedContinuationRequest,
114 #[error("SMTP AUTH XOAUTH2 failed: {0}")]
116 Send(#[from] SmtpCommandSendError),
117 #[error(transparent)]
119 Ehlo(#[from] SmtpEhloError),
120}
121
122pub struct SmtpAuthXoauth2 {
125 state: State,
126 domain: Option<SmtpEhloDomain<'static>>,
127 payload: Option<Vec<u8>>,
128 error_detail: Option<String>,
129 opts: SmtpAuthXoauth2Options,
130}
131
132impl SmtpAuthXoauth2 {
133 pub fn new(
136 username: &str,
137 token: &SecretString,
138 domain: SmtpEhloDomain<'_>,
139 opts: SmtpAuthXoauth2Options,
140 ) -> Self {
141 let payload = build_payload(username, token);
142
143 let state = if opts.initial_request {
144 let cmd = SmtpAuthCommand {
145 mechanism: Cow::Borrowed(XOAUTH2),
146 initial_response: Some(SecretBox::new(payload.clone().into_boxed_slice())),
147 };
148 State::Send(SmtpCommandSend::new(cmd))
149 } else {
150 let cmd = SmtpAuthCommand {
151 mechanism: Cow::Borrowed(XOAUTH2),
152 initial_response: None,
153 };
154 State::Send(SmtpCommandSend::new(cmd))
155 };
156
157 Self {
158 state,
159 domain: Some(domain.into_static()),
160 payload: Some(payload),
161 error_detail: None,
162 opts,
163 }
164 }
165}
166
167impl SmtpCoroutine for SmtpAuthXoauth2 {
168 type Yield = SmtpYield;
169 type Return = Result<(), SmtpAuthXoauth2Error>;
170
171 fn resume(&mut self, arg: Option<&[u8]>) -> SmtpCoroutineState<Self::Yield, Self::Return> {
172 loop {
173 match &mut self.state {
174 State::Send(send) => {
175 let out = smtp_try!(send, arg);
176
177 if out.response.code == SmtpReplyCode::AUTH_SUCCESSFUL {
178 if self.opts.initial_request {
179 self.advance_after_auth();
180 continue;
181 }
182 return SmtpCoroutineState::Complete(Err(
183 SmtpAuthXoauth2Error::ExpectedContinuationRequest,
184 ));
185 }
186
187 if out.response.code == SmtpReplyCode::AUTH_CONTINUE {
188 if self.opts.initial_request {
189 let text = out.response.text().0.trim_start();
193 if let Ok(detail_bytes) = base64.decode(text.as_bytes()) {
194 self.error_detail = String::from_utf8(detail_bytes).ok();
195 }
196
197 let ack = SmtpAuthData::r#continue(vec![0x01u8]);
198 self.state = State::AckError(SmtpCommandSend::new(ack));
199 debug!("error detail received, acknowledging");
200 continue;
201 }
202
203 let payload = self.payload.take().expect("payload taken twice");
205 let data = SmtpAuthData::r#continue(payload.into_boxed_slice());
206 self.state = State::Continue(SmtpCommandSend::new(data));
207 debug!("challenge received, sending credentials");
208 continue;
209 }
210
211 let code = out.response.code.code();
212 let message = out.response.text().to_string();
213 return SmtpCoroutineState::Complete(Err(SmtpAuthXoauth2Error::Rejected {
214 code,
215 message,
216 }));
217 }
218 State::Continue(send) => {
219 let out = smtp_try!(send, arg);
220
221 if out.response.code == SmtpReplyCode::AUTH_SUCCESSFUL {
222 self.advance_after_auth();
223 continue;
224 }
225
226 if out.response.code == SmtpReplyCode::AUTH_CONTINUE {
227 let text = out.response.text().0.trim_start();
228 if let Ok(detail_bytes) = base64.decode(text.as_bytes()) {
229 self.error_detail = String::from_utf8(detail_bytes).ok();
230 }
231
232 let ack = SmtpAuthData::r#continue(vec![0x01u8]);
233 self.state = State::AckError(SmtpCommandSend::new(ack));
234 debug!("error detail received, acknowledging");
235 continue;
236 }
237
238 let code = out.response.code.code();
239 let message = out.response.text().to_string();
240 return SmtpCoroutineState::Complete(Err(SmtpAuthXoauth2Error::Rejected {
241 code,
242 message,
243 }));
244 }
245 State::AckError(send) => {
246 let _ = smtp_try!(send, arg);
247
248 let message = self
249 .error_detail
250 .take()
251 .unwrap_or_else(|| "authentication failed".into());
252
253 return SmtpCoroutineState::Complete(Err(SmtpAuthXoauth2Error::Rejected {
254 code: 535,
255 message,
256 }));
257 }
258 State::Ehlo(ehlo) => {
259 let _ = smtp_try!(ehlo, arg);
260 debug!("capabilities refreshed");
261 return SmtpCoroutineState::Complete(Ok(()));
262 }
263 State::Done => return SmtpCoroutineState::Complete(Ok(())),
264 }
265 }
266 }
267}
268
269impl SmtpAuthXoauth2 {
270 fn advance_after_auth(&mut self) {
271 let _ = self.payload.take();
272 debug!("authenticated");
273 if self.opts.ensure_capabilities {
274 let domain = self.domain.take().expect("domain taken twice");
275 self.state = State::Ehlo(SmtpEhlo::new(domain));
276 } else {
277 self.state = State::Done;
278 }
279 }
280}
281
282enum State {
283 Send(SmtpCommandSend<SmtpAuthCommand<'static>>),
284 Continue(SmtpCommandSend<SmtpAuthData>),
285 AckError(SmtpCommandSend<SmtpAuthData>),
286 Ehlo(SmtpEhlo),
287 Done,
288}
289
290impl fmt::Display for State {
291 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
292 match self {
293 Self::Send(_) => f.write_str("send auth xoauth2"),
294 Self::Continue(_) => f.write_str("send credentials"),
295 Self::AckError(_) => f.write_str("ack error detail"),
296 Self::Ehlo(_) => f.write_str("refresh capabilities"),
297 Self::Done => f.write_str("done"),
298 }
299 }
300}
301
302fn build_payload(username: &str, token: &SecretString) -> Vec<u8> {
305 let mut payload = Vec::new();
306 payload.extend_from_slice(b"user=");
307 payload.extend_from_slice(username.as_bytes());
308 payload.push(0x01);
309 payload.extend_from_slice(b"auth=Bearer ");
310 payload.extend_from_slice(token.expose_secret().as_bytes());
311 payload.push(0x01);
312 payload.push(0x01);
313 payload
314}
315
316#[cfg(test)]
317mod tests {
318 use alloc::{borrow::Cow, string::ToString, vec::Vec};
319
320 use secrecy::SecretString;
321
322 use crate::{
323 coroutine::*,
324 rfc5321::{SmtpDomain, SmtpEhloDomain},
325 sasl::auth_xoauth2::*,
326 send::SmtpCommandSendError,
327 };
328
329 fn domain() -> SmtpEhloDomain<'static> {
330 SmtpEhloDomain::SmtpDomain(SmtpDomain(Cow::Borrowed("example.com")))
331 }
332
333 fn token() -> SecretString {
334 SecretString::from("ya29.tokenvalue".to_string())
335 }
336
337 #[test]
338 fn ir_success_does_not_send_ehlo_by_default() {
339 let opts = SmtpAuthXoauth2Options::default();
340 let mut auth = SmtpAuthXoauth2::new("alice@example.com", &token(), domain(), opts);
341
342 let _ = expect_wants_write(&mut auth, None);
343 expect_wants_read(&mut auth);
344 expect_complete_ok(&mut auth, b"235 OK\r\n");
345 }
346
347 #[test]
348 fn ir_success_with_ehlo_returns_ok() {
349 let opts = SmtpAuthXoauth2Options {
350 initial_request: true,
351 ensure_capabilities: true,
352 };
353 let mut auth = SmtpAuthXoauth2::new("alice@example.com", &token(), domain(), opts);
354
355 let _ = expect_wants_write(&mut auth, None);
356 expect_wants_read(&mut auth);
357 let _ehlo = expect_wants_write(&mut auth, Some(b"235 OK\r\n"));
358 expect_wants_read(&mut auth);
359 expect_complete_ok(&mut auth, b"250 server.example.com\r\n");
360 }
361
362 #[test]
363 fn ir_success_without_ehlo_returns_ok() {
364 let opts = SmtpAuthXoauth2Options {
365 initial_request: true,
366 ensure_capabilities: false,
367 };
368 let mut auth = SmtpAuthXoauth2::new("alice@example.com", &token(), domain(), opts);
369 let _ = expect_wants_write(&mut auth, None);
370 expect_wants_read(&mut auth);
371 expect_complete_ok(&mut auth, b"235 OK\r\n");
372 }
373
374 #[test]
375 fn error_detail_returns_rejected() {
376 let opts = SmtpAuthXoauth2Options {
377 initial_request: true,
378 ensure_capabilities: false,
379 };
380 let mut auth = SmtpAuthXoauth2::new("alice@example.com", &token(), domain(), opts);
381 let _ = expect_wants_write(&mut auth, None);
382 expect_wants_read(&mut auth);
383
384 let challenge = b"334 eyJzdGF0dXMiOiI0MDEifQ==\r\n";
385 let _ack = expect_wants_write(&mut auth, Some(challenge));
386 expect_wants_read(&mut auth);
387
388 let err = expect_complete_err(&mut auth, b"535 authentication failed\r\n");
389 let SmtpAuthXoauth2Error::Rejected { code, message } = err else {
390 panic!("expected SmtpAuthXoauth2Error::Rejected, got {err:?}");
391 };
392 assert_eq!(code, 535);
393 assert!(message.contains("status") || message.contains("401"));
394 }
395
396 #[test]
397 fn rejected_returns_rejected_error() {
398 let opts = SmtpAuthXoauth2Options::default();
399 let mut auth = SmtpAuthXoauth2::new("alice@example.com", &token(), domain(), opts);
400 let _ = expect_wants_write(&mut auth, None);
401 expect_wants_read(&mut auth);
402
403 let err = expect_complete_err(&mut auth, b"504 mechanism disabled\r\n");
404 let SmtpAuthXoauth2Error::Rejected { code, message } = err else {
405 panic!("expected SmtpAuthXoauth2Error::Rejected, got {err:?}");
406 };
407 assert_eq!(code, 504);
408 assert_eq!(message, "mechanism disabled");
409 }
410
411 #[test]
412 fn eof_returns_eof_error() {
413 let opts = SmtpAuthXoauth2Options::default();
414 let mut auth = SmtpAuthXoauth2::new("alice@example.com", &token(), domain(), opts);
415 let _ = expect_wants_write(&mut auth, None);
416 expect_wants_read(&mut auth);
417
418 let err = expect_complete_err(&mut auth, b"");
419 assert!(matches!(
420 err,
421 SmtpAuthXoauth2Error::Send(SmtpCommandSendError::Eof)
422 ));
423 }
424
425 fn expect_wants_write(cor: &mut SmtpAuthXoauth2, arg: Option<&[u8]>) -> Vec<u8> {
426 match cor.resume(arg) {
427 SmtpCoroutineState::Yielded(SmtpYield::WantsWrite(bytes)) => bytes,
428 state => panic!("expected WantsWrite, got {state:?}"),
429 }
430 }
431
432 fn expect_wants_read(cor: &mut SmtpAuthXoauth2) {
433 match cor.resume(None) {
434 SmtpCoroutineState::Yielded(SmtpYield::WantsRead) => {}
435 state => panic!("expected WantsRead, got {state:?}"),
436 }
437 }
438
439 fn expect_complete_ok(cor: &mut SmtpAuthXoauth2, reply: &[u8]) {
440 match cor.resume(Some(reply)) {
441 SmtpCoroutineState::Complete(Ok(())) => {}
442 state => panic!("expected Complete(Ok), got {state:?}"),
443 }
444 }
445
446 fn expect_complete_err(cor: &mut SmtpAuthXoauth2, reply: &[u8]) -> SmtpAuthXoauth2Error {
447 match cor.resume(Some(reply)) {
448 SmtpCoroutineState::Complete(Err(err)) => err,
449 state => panic!("expected Complete(Err), got {state:?}"),
450 }
451 }
452}