1use core::fmt;
45
46use alloc::{
47 string::{String, ToString},
48 vec::Vec,
49};
50
51use bounded_static::IntoBoundedStatic;
52use log::debug;
53use thiserror::Error;
54
55use crate::{
56 coroutine::*,
57 rfc5321::{SmtpDomain, SmtpReplyCode},
58 send::*,
59 smtp_try,
60};
61
62pub struct SmtpHeloCommand<'a> {
64 pub domain: SmtpDomain<'a>,
66}
67
68impl<'a> From<SmtpHeloCommand<'a>> for Vec<u8> {
69 fn from(cmd: SmtpHeloCommand<'a>) -> Vec<u8> {
70 let mut buf = String::from("HELO ");
71 buf.push_str(cmd.domain.as_ref());
72 buf.push_str("\r\n");
73 buf.into_bytes()
74 }
75}
76
77#[derive(Clone, Debug, Error)]
79pub enum SmtpHeloError {
80 #[error("SMTP HELO failed: rejected {code} {message}")]
82 Rejected {
83 code: u16,
85 message: String,
87 },
88 #[error("SMTP HELO failed: {0}")]
90 Send(#[from] SmtpCommandSendError),
91}
92
93pub struct SmtpHelo {
95 state: State,
96}
97
98impl SmtpHelo {
99 pub fn new(domain: SmtpDomain<'_>) -> Self {
102 let cmd = SmtpHeloCommand {
103 domain: domain.into_static(),
104 };
105
106 Self {
107 state: State::Send(SmtpCommandSend::new(cmd)),
108 }
109 }
110}
111
112impl SmtpCoroutine for SmtpHelo {
113 type Yield = SmtpYield;
114 type Return = Result<(), SmtpHeloError>;
115
116 fn resume(&mut self, arg: Option<&[u8]>) -> SmtpCoroutineState<Self::Yield, Self::Return> {
117 match &mut self.state {
118 State::Send(send) => {
119 let out = smtp_try!(send, arg);
120
121 if out.response.code == SmtpReplyCode::OK {
122 debug!("helo accepted");
123 return SmtpCoroutineState::Complete(Ok(()));
124 }
125
126 let code = out.response.code.code();
127 let message = out.response.text().to_string();
128 SmtpCoroutineState::Complete(Err(SmtpHeloError::Rejected { code, message }))
129 }
130 }
131 }
132}
133
134enum State {
135 Send(SmtpCommandSend<SmtpHeloCommand<'static>>),
136}
137
138impl fmt::Display for State {
139 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140 match self {
141 Self::Send(_) => f.write_str("send helo"),
142 }
143 }
144}
145
146#[cfg(test)]
147mod tests {
148 use alloc::{borrow::Cow, vec::Vec};
149
150 use crate::{
151 coroutine::*,
152 rfc5321::{SmtpDomain, helo::*},
153 send::SmtpCommandSendError,
154 };
155
156 fn domain() -> SmtpDomain<'static> {
157 SmtpDomain(Cow::Borrowed("example.com"))
158 }
159
160 #[test]
161 fn success_returns_ok() {
162 let mut helo = SmtpHelo::new(domain());
163
164 let bytes = expect_wants_write(&mut helo, None);
165 assert_eq!(bytes, b"HELO example.com\r\n");
166
167 expect_wants_read(&mut helo);
168 expect_complete_ok(&mut helo, b"250 server.example.com\r\n");
169 }
170
171 #[test]
172 fn rejected_returns_rejected_error() {
173 let mut helo = SmtpHelo::new(domain());
174 let _ = expect_wants_write(&mut helo, None);
175 expect_wants_read(&mut helo);
176
177 let err = expect_complete_err(&mut helo, b"550 bad domain\r\n");
178 let SmtpHeloError::Rejected { code, message } = err else {
179 panic!("expected SmtpHeloError::Rejected, got {err:?}");
180 };
181 assert_eq!(code, 550);
182 assert_eq!(message, "bad domain");
183 }
184
185 #[test]
186 fn eof_returns_eof_error() {
187 let mut helo = SmtpHelo::new(domain());
188 let _ = expect_wants_write(&mut helo, None);
189 expect_wants_read(&mut helo);
190
191 let err = expect_complete_err(&mut helo, b"");
192 assert!(matches!(
193 err,
194 SmtpHeloError::Send(SmtpCommandSendError::Eof)
195 ));
196 }
197
198 fn expect_wants_write(cor: &mut SmtpHelo, arg: Option<&[u8]>) -> Vec<u8> {
199 match cor.resume(arg) {
200 SmtpCoroutineState::Yielded(SmtpYield::WantsWrite(bytes)) => bytes,
201 state => panic!("expected WantsWrite, got {state:?}"),
202 }
203 }
204
205 fn expect_wants_read(cor: &mut SmtpHelo) {
206 match cor.resume(None) {
207 SmtpCoroutineState::Yielded(SmtpYield::WantsRead) => {}
208 state => panic!("expected WantsRead, got {state:?}"),
209 }
210 }
211
212 fn expect_complete_ok(cor: &mut SmtpHelo, reply: &[u8]) {
213 match cor.resume(Some(reply)) {
214 SmtpCoroutineState::Complete(Ok(())) => {}
215 state => panic!("expected Complete(Ok), got {state:?}"),
216 }
217 }
218
219 fn expect_complete_err(cor: &mut SmtpHelo, reply: &[u8]) -> SmtpHeloError {
220 match cor.resume(Some(reply)) {
221 SmtpCoroutineState::Complete(Err(err)) => err,
222 state => panic!("expected Complete(Err), got {state:?}"),
223 }
224 }
225}