Skip to main content

io_imap/rfc2971/
id.rs

1//! IMAP ID coroutine returning the server's identification parameters.
2//!
3//! # Example
4//!
5//! ```rust,no_run
6//! use std::{
7//!     io::{Read, Write},
8//!     net::TcpStream,
9//! };
10//!
11//! use io_imap::{
12//!     codec::fragmentizer::Fragmentizer,
13//!     coroutine::{ImapCoroutine, ImapCoroutineState, ImapYield},
14//!     rfc2971::id::{ImapServerId, ImapServerIdOptions},
15//! };
16//!
17//! // Ready stream needed (TCP-connected, TLS-negociated, IMAP-authenticated)
18//! let mut stream = TcpStream::connect("localhost:143").unwrap();
19//!
20//! let mut fragmentizer = Fragmentizer::new(50 * 1024 * 1024);
21//! let mut buf = [0u8; 4096];
22//!
23//! let mut coroutine = ImapServerId::new(ImapServerIdOptions::default());
24//! let mut arg = None;
25//!
26//! let server_id = loop {
27//!     match coroutine.resume(&mut fragmentizer, arg.take()) {
28//!         ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => {
29//!             stream.write_all(&bytes).unwrap();
30//!         }
31//!         ImapCoroutineState::Yielded(ImapYield::WantsRead) => {
32//!             let n = stream.read(&mut buf).unwrap();
33//!             arg = Some(&buf[..n]);
34//!         }
35//!         ImapCoroutineState::Complete(Ok(server_id)) => break server_id,
36//!         ImapCoroutineState::Complete(Err(err)) => panic!("{err}"),
37//!     }
38//! };
39//!
40//! println!("{server_id:?}");
41//! ```
42
43use 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/// Failure causes during the IMAP ID flow.
62#[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/// Options for [`ImapServerId::new`].
79#[derive(Clone, Debug, Default, Eq, PartialEq)]
80pub struct ImapServerIdOptions {
81    /// `None` sends `ID NIL`; `Some(_)` sends `ID (key val ...)`.
82    pub parameters: Option<Vec<(IString<'static>, NString<'static>)>>,
83}
84
85/// I/O-free IMAP ID coroutine returning the server's identification.
86pub 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    // --- utils
264
265    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}