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