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