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