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