1use core::fmt;
44
45use alloc::{string::String, string::ToString, vec::Vec};
46
47use imap_codec::{
48 CommandCodec,
49 fragmentizer::Fragmentizer,
50 imap_types::{
51 command::{Command, CommandBody},
52 core::{IString, NString, TagGenerator},
53 response::{Data, StatusKind, Tagged},
54 },
55};
56use log::trace;
57use thiserror::Error;
58
59use crate::{coroutine::*, imap_try, send::*};
60
61#[derive(Clone, Debug, Error)]
63pub enum ImapServerIdError {
64 #[error("IMAP ID failed: NO {0}")]
66 No(String),
67 #[error("IMAP ID failed: BAD {0}")]
69 Bad(String),
70 #[error("IMAP ID failed: BYE {0}")]
72 Bye(String),
73 #[error("IMAP ID failed: server did not return a tagged response")]
75 MissingTagged,
76 #[error("IMAP ID failed: {0}")]
78 Send(#[from] ImapSendError),
79}
80
81#[derive(Clone, Debug, Default, Eq, PartialEq)]
83pub struct ImapServerIdOptions {
84 pub parameters: Option<Vec<(IString<'static>, NString<'static>)>>,
86}
87
88pub struct ImapServerId {
90 state: State,
91}
92
93impl ImapServerId {
94 pub fn new(opts: ImapServerIdOptions) -> Self {
97 let command = Command {
98 tag: TagGenerator::new().generate(),
99 body: CommandBody::Id {
100 parameters: opts.parameters,
101 },
102 };
103
104 trace!("send IMAP command {command:?}");
105
106 let state = State::Send(ImapSend::new(CommandCodec::new(), command));
107
108 Self { state }
109 }
110}
111
112impl ImapCoroutine for ImapServerId {
113 type Yield = ImapYield;
114 type Return = Result<Option<Vec<(IString<'static>, NString<'static>)>>, ImapServerIdError>;
115
116 fn resume(
117 &mut self,
118 fragmentizer: &mut Fragmentizer,
119 arg: Option<&[u8]>,
120 ) -> ImapCoroutineState<Self::Yield, Self::Return> {
121 match &mut self.state {
122 State::Send(send) => {
123 let out = imap_try!(send, fragmentizer, arg);
124
125 if let Some(bye) = out.bye {
126 let err = ImapServerIdError::Bye(bye.text.to_string());
127 return ImapCoroutineState::Complete(Err(err));
128 }
129
130 let Some(Tagged { body, .. }) = out.tagged else {
131 return ImapCoroutineState::Complete(Err(ImapServerIdError::MissingTagged));
132 };
133
134 match body.kind {
135 StatusKind::No => {
136 let err = ImapServerIdError::No(body.text.to_string());
137 return ImapCoroutineState::Complete(Err(err));
138 }
139 StatusKind::Bad => {
140 let err = ImapServerIdError::Bad(body.text.to_string());
141 return ImapCoroutineState::Complete(Err(err));
142 }
143 StatusKind::Ok => {}
144 }
145
146 let mut server_id = None;
147 for data in out.data {
148 if let Data::Id { parameters } = data {
149 server_id = parameters;
150 }
151 }
152
153 ImapCoroutineState::Complete(Ok(server_id))
154 }
155 }
156 }
157}
158
159enum State {
160 Send(ImapSend<CommandCodec>),
161}
162
163impl fmt::Display for State {
164 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
165 match self {
166 Self::Send(_) => f.write_str("send id"),
167 }
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use core::str;
174
175 use alloc::{borrow::ToOwned, format};
176
177 use crate::rfc2971::id::*;
178
179 #[test]
180 fn nil_success_returns_none() {
181 let mut id = ImapServerId::new(ImapServerIdOptions::default());
182 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
183
184 let bytes = expect_wants_write(&mut id, &mut frag, None);
185 let line = str::from_utf8(&bytes).expect("utf8 command");
186 let tag = first_word(line).to_owned();
187 assert!(line.trim_end().ends_with("ID NIL"));
188
189 expect_wants_read(&mut id, &mut frag);
190
191 let reply = format!("* ID NIL\r\n{tag} OK ID completed\r\n");
192 let result = expect_complete_ok(&mut id, &mut frag, reply.as_bytes());
193 assert!(result.is_none());
194 }
195
196 #[test]
197 fn server_parameters_returns_some() {
198 let mut id = ImapServerId::new(ImapServerIdOptions::default());
199 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
200
201 let bytes = expect_wants_write(&mut id, &mut frag, None);
202 let tag = first_word(str::from_utf8(&bytes).expect("utf8 command")).to_owned();
203
204 expect_wants_read(&mut id, &mut frag);
205
206 let reply =
207 format!("* ID (\"name\" \"Dovecot\" \"version\" \"2.3\")\r\n{tag} OK ID completed\r\n");
208 let result = expect_complete_ok(&mut id, &mut frag, reply.as_bytes());
209 let params = result.expect("server returned parameters");
210 assert_eq!(2, params.len());
211 }
212
213 #[test]
214 fn tagged_no_returns_no_error() {
215 let mut id = ImapServerId::new(ImapServerIdOptions::default());
216 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
217
218 let bytes = expect_wants_write(&mut id, &mut frag, None);
219 let tag = first_word(str::from_utf8(&bytes).expect("utf8 command")).to_owned();
220
221 expect_wants_read(&mut id, &mut frag);
222
223 let reply = format!("{tag} NO ID rejected\r\n");
224 let err = expect_complete_err(&mut id, &mut frag, reply.as_bytes());
225 let ImapServerIdError::No(text) = err else {
226 panic!("expected ImapServerIdError::No, got {err:?}");
227 };
228 assert_eq!(text, "ID rejected");
229 }
230
231 #[test]
232 fn tagged_bad_returns_bad_error() {
233 let mut id = ImapServerId::new(ImapServerIdOptions::default());
234 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
235
236 let bytes = expect_wants_write(&mut id, &mut frag, None);
237 let tag = first_word(str::from_utf8(&bytes).expect("utf8 command")).to_owned();
238
239 expect_wants_read(&mut id, &mut frag);
240
241 let reply = format!("{tag} BAD ID not supported\r\n");
242 let err = expect_complete_err(&mut id, &mut frag, reply.as_bytes());
243 let ImapServerIdError::Bad(text) = err else {
244 panic!("expected ImapServerIdError::Bad, got {err:?}");
245 };
246 assert_eq!(text, "ID not supported");
247 }
248
249 #[test]
250 fn bye_returns_bye_error() {
251 let mut id = ImapServerId::new(ImapServerIdOptions::default());
252 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
253
254 let _ = expect_wants_write(&mut id, &mut frag, None);
255 expect_wants_read(&mut id, &mut frag);
256
257 let err = expect_complete_err(&mut id, &mut frag, b"* BYE shutting down\r\n");
258 let ImapServerIdError::Bye(text) = err else {
259 panic!("expected ImapServerIdError::Bye, got {err:?}");
260 };
261 assert_eq!(text, "shutting down");
262 }
263
264 fn expect_wants_write(
265 cor: &mut ImapServerId,
266 frag: &mut Fragmentizer,
267 arg: Option<&[u8]>,
268 ) -> Vec<u8> {
269 match cor.resume(frag, arg) {
270 ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => bytes,
271 state => panic!("expected WantsWrite, got {state:?}"),
272 }
273 }
274
275 fn expect_wants_read(cor: &mut ImapServerId, frag: &mut Fragmentizer) {
276 match cor.resume(frag, None) {
277 ImapCoroutineState::Yielded(ImapYield::WantsRead) => {}
278 state => panic!("expected WantsRead, got {state:?}"),
279 }
280 }
281
282 fn expect_complete_ok(
283 cor: &mut ImapServerId,
284 frag: &mut Fragmentizer,
285 reply: &[u8],
286 ) -> Option<Vec<(IString<'static>, NString<'static>)>> {
287 match cor.resume(frag, Some(reply)) {
288 ImapCoroutineState::Complete(Ok(value)) => value,
289 state => panic!("expected Complete(Ok), got {state:?}"),
290 }
291 }
292
293 fn expect_complete_err(
294 cor: &mut ImapServerId,
295 frag: &mut Fragmentizer,
296 reply: &[u8],
297 ) -> ImapServerIdError {
298 match cor.resume(frag, Some(reply)) {
299 ImapCoroutineState::Complete(Err(err)) => err,
300 state => panic!("expected Complete(Err), got {state:?}"),
301 }
302 }
303
304 fn first_word(line: &str) -> &str {
305 line.split_whitespace()
306 .next()
307 .expect("first whitespace-separated token")
308 }
309}