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