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