1use core::fmt;
50
51use alloc::{
52 borrow::Cow,
53 string::{String, ToString},
54 vec,
55 vec::Vec,
56};
57
58use base64::{Engine, engine::general_purpose::STANDARD as base64};
59use bounded_static::IntoBoundedStatic;
60use log::debug;
61use secrecy::{ExposeSecret, SecretBox, SecretString};
62use thiserror::Error;
63
64use crate::{
65 coroutine::*,
66 rfc4954::{auth::SmtpAuthCommand, auth_data::SmtpAuthData},
67 rfc5321::{
68 SmtpEhloDomain, SmtpReplyCode,
69 ehlo::{SmtpEhlo, SmtpEhloError},
70 },
71 send::*,
72 smtp_try,
73};
74
75pub const OAUTHBEARER: &str = "OAUTHBEARER";
77
78#[derive(Clone, Debug, Eq, PartialEq)]
80pub struct SmtpAuthOauthbearerOptions {
81 pub initial_request: bool,
84 pub ensure_capabilities: bool,
87}
88
89impl Default for SmtpAuthOauthbearerOptions {
90 fn default() -> Self {
91 Self {
92 initial_request: true,
93 ensure_capabilities: false,
94 }
95 }
96}
97
98#[derive(Debug, Error)]
100pub enum SmtpAuthOauthbearerError {
101 #[error("SMTP AUTH OAUTHBEARER failed: rejected {code} {message}")]
103 Rejected {
104 code: u16,
106 message: String,
108 },
109 #[error("SMTP AUTH OAUTHBEARER failed: server did not send the expected continuation request")]
111 ExpectedContinuationRequest,
112 #[error("SMTP AUTH OAUTHBEARER failed: {0}")]
114 Send(#[from] SmtpCommandSendError),
115 #[error(transparent)]
117 Ehlo(#[from] SmtpEhloError),
118}
119
120pub struct SmtpAuthOauthbearer {
124 state: State,
125 domain: Option<SmtpEhloDomain<'static>>,
126 payload: Option<Vec<u8>>,
127 error_detail: Option<String>,
128 opts: SmtpAuthOauthbearerOptions,
129}
130
131impl SmtpAuthOauthbearer {
132 pub fn new(
136 token: &SecretString,
137 username: Option<&str>,
138 domain: SmtpEhloDomain<'_>,
139 opts: SmtpAuthOauthbearerOptions,
140 ) -> Self {
141 let payload = build_payload(token, username);
142
143 let state = if opts.initial_request {
144 let cmd = SmtpAuthCommand {
145 mechanism: Cow::Borrowed(OAUTHBEARER),
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(OAUTHBEARER),
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 SmtpAuthOauthbearer {
168 type Yield = SmtpYield;
169 type Return = Result<(), SmtpAuthOauthbearerError>;
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 SmtpAuthOauthbearerError::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();
190 if let Ok(detail_bytes) = base64.decode(text.as_bytes()) {
191 self.error_detail = String::from_utf8(detail_bytes).ok();
192 }
193
194 let ack = SmtpAuthData::r#continue(vec![0x01u8]);
195 self.state = State::AckError(SmtpCommandSend::new(ack));
196 debug!("error detail received, acknowledging");
197 continue;
198 }
199
200 let payload = self.payload.take().expect("payload taken twice");
201 let data = SmtpAuthData::r#continue(payload.into_boxed_slice());
202 self.state = State::Continue(SmtpCommandSend::new(data));
203 debug!("challenge received, sending credentials");
204 continue;
205 }
206
207 let code = out.response.code.code();
208 let message = out.response.text().to_string();
209 return SmtpCoroutineState::Complete(Err(SmtpAuthOauthbearerError::Rejected {
210 code,
211 message,
212 }));
213 }
214 State::Continue(send) => {
215 let out = smtp_try!(send, arg);
216
217 if out.response.code == SmtpReplyCode::AUTH_SUCCESSFUL {
218 self.advance_after_auth();
219 continue;
220 }
221
222 if out.response.code == SmtpReplyCode::AUTH_CONTINUE {
223 let text = out.response.text().0.trim_start();
224 if let Ok(detail_bytes) = base64.decode(text.as_bytes()) {
225 self.error_detail = String::from_utf8(detail_bytes).ok();
226 }
227
228 let ack = SmtpAuthData::r#continue(vec![0x01u8]);
229 self.state = State::AckError(SmtpCommandSend::new(ack));
230 debug!("error detail received, acknowledging");
231 continue;
232 }
233
234 let code = out.response.code.code();
235 let message = out.response.text().to_string();
236 return SmtpCoroutineState::Complete(Err(SmtpAuthOauthbearerError::Rejected {
237 code,
238 message,
239 }));
240 }
241 State::AckError(send) => {
242 let _ = smtp_try!(send, arg);
243
244 let message = self
245 .error_detail
246 .take()
247 .unwrap_or_else(|| "authentication failed".into());
248
249 return SmtpCoroutineState::Complete(Err(SmtpAuthOauthbearerError::Rejected {
250 code: 535,
251 message,
252 }));
253 }
254 State::Ehlo(ehlo) => {
255 let _ = smtp_try!(ehlo, arg);
256 debug!("capabilities refreshed");
257 return SmtpCoroutineState::Complete(Ok(()));
258 }
259 State::Done => return SmtpCoroutineState::Complete(Ok(())),
260 }
261 }
262 }
263}
264
265impl SmtpAuthOauthbearer {
266 fn advance_after_auth(&mut self) {
267 let _ = self.payload.take();
268 debug!("authenticated");
269 if self.opts.ensure_capabilities {
270 let domain = self.domain.take().expect("domain taken twice");
271 self.state = State::Ehlo(SmtpEhlo::new(domain));
272 } else {
273 self.state = State::Done;
274 }
275 }
276}
277
278enum State {
279 Send(SmtpCommandSend<SmtpAuthCommand<'static>>),
280 Continue(SmtpCommandSend<SmtpAuthData>),
281 AckError(SmtpCommandSend<SmtpAuthData>),
282 Ehlo(SmtpEhlo),
283 Done,
284}
285
286impl fmt::Display for State {
287 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
288 match self {
289 Self::Send(_) => f.write_str("send auth oauthbearer"),
290 Self::Continue(_) => f.write_str("send credentials"),
291 Self::AckError(_) => f.write_str("ack error detail"),
292 Self::Ehlo(_) => f.write_str("refresh capabilities"),
293 Self::Done => f.write_str("done"),
294 }
295 }
296}
297
298fn build_payload(token: &SecretString, username: Option<&str>) -> Vec<u8> {
301 let mut payload = Vec::new();
302 payload.extend_from_slice(b"n,");
303 if let Some(user) = username {
304 payload.extend_from_slice(b"a=");
305 payload.extend_from_slice(user.as_bytes());
306 }
307 payload.push(b',');
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 rfc7628::auth_oauthbearer::*,
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 = SmtpAuthOauthbearerOptions::default();
340 let mut auth =
341 SmtpAuthOauthbearer::new(&token(), Some("alice@example.com"), domain(), opts);
342
343 let _ = expect_wants_write(&mut auth, None);
344 expect_wants_read(&mut auth);
345 expect_complete_ok(&mut auth, b"235 OK\r\n");
346 }
347
348 #[test]
349 fn ir_success_with_ehlo_returns_ok() {
350 let opts = SmtpAuthOauthbearerOptions {
351 initial_request: true,
352 ensure_capabilities: true,
353 };
354 let mut auth =
355 SmtpAuthOauthbearer::new(&token(), Some("alice@example.com"), domain(), opts);
356
357 let _ = expect_wants_write(&mut auth, None);
358 expect_wants_read(&mut auth);
359 let _ehlo = expect_wants_write(&mut auth, Some(b"235 OK\r\n"));
360 expect_wants_read(&mut auth);
361 expect_complete_ok(&mut auth, b"250 server.example.com\r\n");
362 }
363
364 #[test]
365 fn ir_success_without_ehlo_returns_ok() {
366 let opts = SmtpAuthOauthbearerOptions {
367 initial_request: true,
368 ensure_capabilities: false,
369 };
370 let mut auth = SmtpAuthOauthbearer::new(&token(), None, domain(), opts);
371 let _ = expect_wants_write(&mut auth, None);
372 expect_wants_read(&mut auth);
373 expect_complete_ok(&mut auth, b"235 OK\r\n");
374 }
375
376 #[test]
377 fn error_detail_returns_rejected() {
378 let opts = SmtpAuthOauthbearerOptions {
379 initial_request: true,
380 ensure_capabilities: false,
381 };
382 let mut auth = SmtpAuthOauthbearer::new(&token(), None, domain(), opts);
383 let _ = expect_wants_write(&mut auth, None);
384 expect_wants_read(&mut auth);
385
386 let challenge = b"334 eyJzdGF0dXMiOiI0MDEifQ==\r\n";
387 let _ack = expect_wants_write(&mut auth, Some(challenge));
388 expect_wants_read(&mut auth);
389
390 let err = expect_complete_err(&mut auth, b"535 authentication failed\r\n");
391 let SmtpAuthOauthbearerError::Rejected { code, message } = err else {
392 panic!("expected SmtpAuthOauthbearerError::Rejected, got {err:?}");
393 };
394 assert_eq!(code, 535);
395 assert!(message.contains("status") || message.contains("401"));
396 }
397
398 #[test]
399 fn rejected_returns_rejected_error() {
400 let opts = SmtpAuthOauthbearerOptions::default();
401 let mut auth = SmtpAuthOauthbearer::new(&token(), None, domain(), opts);
402 let _ = expect_wants_write(&mut auth, None);
403 expect_wants_read(&mut auth);
404
405 let err = expect_complete_err(&mut auth, b"504 mechanism disabled\r\n");
406 let SmtpAuthOauthbearerError::Rejected { code, .. } = err else {
407 panic!("expected SmtpAuthOauthbearerError::Rejected, got {err:?}");
408 };
409 assert_eq!(code, 504);
410 }
411
412 #[test]
413 fn eof_returns_eof_error() {
414 let opts = SmtpAuthOauthbearerOptions::default();
415 let mut auth = SmtpAuthOauthbearer::new(&token(), None, domain(), opts);
416 let _ = expect_wants_write(&mut auth, None);
417 expect_wants_read(&mut auth);
418
419 let err = expect_complete_err(&mut auth, b"");
420 assert!(matches!(
421 err,
422 SmtpAuthOauthbearerError::Send(SmtpCommandSendError::Eof)
423 ));
424 }
425
426 fn expect_wants_write(cor: &mut SmtpAuthOauthbearer, arg: Option<&[u8]>) -> Vec<u8> {
427 match cor.resume(arg) {
428 SmtpCoroutineState::Yielded(SmtpYield::WantsWrite(bytes)) => bytes,
429 state => panic!("expected WantsWrite, got {state:?}"),
430 }
431 }
432
433 fn expect_wants_read(cor: &mut SmtpAuthOauthbearer) {
434 match cor.resume(None) {
435 SmtpCoroutineState::Yielded(SmtpYield::WantsRead) => {}
436 state => panic!("expected WantsRead, got {state:?}"),
437 }
438 }
439
440 fn expect_complete_ok(cor: &mut SmtpAuthOauthbearer, reply: &[u8]) {
441 match cor.resume(Some(reply)) {
442 SmtpCoroutineState::Complete(Ok(())) => {}
443 state => panic!("expected Complete(Ok), got {state:?}"),
444 }
445 }
446
447 fn expect_complete_err(
448 cor: &mut SmtpAuthOauthbearer,
449 reply: &[u8],
450 ) -> SmtpAuthOauthbearerError {
451 match cor.resume(Some(reply)) {
452 SmtpCoroutineState::Complete(Err(err)) => err,
453 state => panic!("expected Complete(Err), got {state:?}"),
454 }
455 }
456}