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