imap_proto/builders/
command.rs

1use std::borrow::Cow;
2use std::marker::PhantomData;
3use std::ops::{RangeFrom, RangeInclusive};
4use std::str;
5
6use crate::types::{AttrMacro, Attribute, State};
7
8pub struct CommandBuilder {}
9
10impl CommandBuilder {
11    pub fn check() -> Command {
12        let args = b"CHECK".to_vec();
13        Command {
14            args,
15            next_state: None,
16        }
17    }
18
19    pub fn close() -> Command {
20        let args = b"CLOSE".to_vec();
21        Command {
22            args,
23            next_state: Some(State::Authenticated),
24        }
25    }
26
27    pub fn examine(mailbox: &str) -> SelectCommand<select::NoParams> {
28        let args = format!("EXAMINE \"{}\"", quoted_string(mailbox).unwrap()).into_bytes();
29        SelectCommand {
30            args,
31            state: PhantomData,
32        }
33    }
34
35    pub fn fetch() -> FetchCommand<fetch::Empty> {
36        FetchCommand {
37            args: b"FETCH ".to_vec(),
38            state: PhantomData,
39        }
40    }
41
42    pub fn list(reference: &str, glob: &str) -> Command {
43        let args = format!(
44            "LIST \"{}\" \"{}\"",
45            quoted_string(reference).unwrap(),
46            quoted_string(glob).unwrap()
47        )
48        .into_bytes();
49        Command {
50            args,
51            next_state: None,
52        }
53    }
54
55    pub fn login(user_name: &str, password: &str) -> Command {
56        let args = format!(
57            "LOGIN \"{}\" \"{}\"",
58            quoted_string(user_name).unwrap(),
59            quoted_string(password).unwrap()
60        )
61        .into_bytes();
62        Command {
63            args,
64            next_state: Some(State::Authenticated),
65        }
66    }
67
68    pub fn select(mailbox: &str) -> SelectCommand<select::NoParams> {
69        let args = format!("SELECT \"{}\"", quoted_string(mailbox).unwrap()).into_bytes();
70        SelectCommand {
71            args,
72            state: PhantomData,
73        }
74    }
75
76    pub fn uid_fetch() -> FetchCommand<fetch::Empty> {
77        FetchCommand {
78            args: b"UID FETCH ".to_vec(),
79            state: PhantomData,
80        }
81    }
82}
83
84pub struct Command {
85    pub args: Vec<u8>,
86    pub next_state: Option<State>,
87}
88
89pub struct SelectCommand<T> {
90    args: Vec<u8>,
91    state: PhantomData<T>,
92}
93
94impl SelectCommand<select::NoParams> {
95    // RFC 4551 CONDSTORE parameter (based on RFC 4466 `select-param`)
96    pub fn cond_store(mut self) -> SelectCommand<select::Params> {
97        self.args.extend(b" (CONDSTORE");
98        SelectCommand {
99            args: self.args,
100            state: PhantomData,
101        }
102    }
103}
104
105impl From<SelectCommand<select::NoParams>> for Command {
106    fn from(cmd: SelectCommand<select::NoParams>) -> Command {
107        Command {
108            args: cmd.args,
109            next_state: Some(State::Selected),
110        }
111    }
112}
113
114impl From<SelectCommand<select::Params>> for Command {
115    fn from(mut cmd: SelectCommand<select::Params>) -> Command {
116        cmd.args.push(b')');
117        Command {
118            args: cmd.args,
119            next_state: Some(State::Selected),
120        }
121    }
122}
123
124pub mod select {
125    pub struct NoParams;
126    pub struct Params;
127}
128
129pub mod fetch {
130    pub struct Empty;
131    pub struct Messages;
132    pub struct Attributes;
133    pub struct Modifiers;
134}
135
136pub struct FetchCommand<T> {
137    args: Vec<u8>,
138    state: PhantomData<T>,
139}
140
141impl FetchCommand<fetch::Empty> {
142    pub fn num(mut self, num: u32) -> FetchCommand<fetch::Messages> {
143        sequence_num(&mut self.args, num);
144        FetchCommand {
145            args: self.args,
146            state: PhantomData,
147        }
148    }
149
150    pub fn range(mut self, range: RangeInclusive<u32>) -> FetchCommand<fetch::Messages> {
151        sequence_range(&mut self.args, range);
152        FetchCommand {
153            args: self.args,
154            state: PhantomData,
155        }
156    }
157
158    pub fn range_from(mut self, range: RangeFrom<u32>) -> FetchCommand<fetch::Messages> {
159        range_from(&mut self.args, range);
160        FetchCommand {
161            args: self.args,
162            state: PhantomData,
163        }
164    }
165}
166
167impl FetchCommand<fetch::Messages> {
168    pub fn num(mut self, num: u32) -> FetchCommand<fetch::Messages> {
169        self.args.extend(b",");
170        sequence_num(&mut self.args, num);
171        self
172    }
173
174    pub fn range(mut self, range: RangeInclusive<u32>) -> FetchCommand<fetch::Messages> {
175        self.args.extend(b",");
176        sequence_range(&mut self.args, range);
177        self
178    }
179
180    pub fn range_from(mut self, range: RangeFrom<u32>) -> FetchCommand<fetch::Messages> {
181        self.args.extend(b",");
182        range_from(&mut self.args, range);
183        self
184    }
185
186    pub fn attr_macro(mut self, named: AttrMacro) -> FetchCommand<fetch::Modifiers> {
187        self.args.push(b' ');
188        self.args.extend(
189            match named {
190                AttrMacro::All => "ALL",
191                AttrMacro::Fast => "FAST",
192                AttrMacro::Full => "FULL",
193            }
194            .as_bytes(),
195        );
196        FetchCommand {
197            args: self.args,
198            state: PhantomData,
199        }
200    }
201
202    pub fn attr(mut self, attr: Attribute) -> FetchCommand<fetch::Attributes> {
203        self.args.extend(b" (");
204        push_attr(&mut self.args, attr);
205        FetchCommand {
206            args: self.args,
207            state: PhantomData,
208        }
209    }
210}
211
212fn sequence_num(cmd: &mut Vec<u8>, num: u32) {
213    cmd.extend(num.to_string().as_bytes());
214}
215
216fn sequence_range(cmd: &mut Vec<u8>, range: RangeInclusive<u32>) {
217    cmd.extend(range.start().to_string().as_bytes());
218    cmd.push(b':');
219    cmd.extend(range.end().to_string().as_bytes());
220}
221
222fn range_from(cmd: &mut Vec<u8>, range: RangeFrom<u32>) {
223    cmd.extend(range.start.to_string().as_bytes());
224    cmd.extend(b":*");
225}
226
227impl FetchCommand<fetch::Attributes> {
228    pub fn attr(mut self, attr: Attribute) -> FetchCommand<fetch::Attributes> {
229        self.args.push(b' ');
230        push_attr(&mut self.args, attr);
231        self
232    }
233
234    pub fn changed_since(mut self, seq: u64) -> FetchCommand<fetch::Modifiers> {
235        self.args.push(b')');
236        changed_since(&mut self.args, seq);
237        FetchCommand {
238            args: self.args,
239            state: PhantomData,
240        }
241    }
242}
243
244fn push_attr(cmd: &mut Vec<u8>, attr: Attribute) {
245    cmd.extend(
246        match attr {
247            Attribute::Body => "BODY",
248            Attribute::Envelope => "ENVELOPE",
249            Attribute::Flags => "FLAGS",
250            Attribute::InternalDate => "INTERNALDATE",
251            Attribute::ModSeq => "MODSEQ",
252            Attribute::Rfc822 => "RFC822",
253            Attribute::Rfc822Size => "RFC822.SIZE",
254            Attribute::Rfc822Text => "RFC822.TEXT",
255            Attribute::Uid => "UID",
256            Attribute::GmailLabels => "X-GM-LABELS",
257            Attribute::GmailMsgId => "X-GM-MSGID",
258            Attribute::GmailThrId => "X-GM-THRID",
259        }
260        .as_bytes(),
261    );
262}
263
264impl From<FetchCommand<fetch::Attributes>> for Command {
265    fn from(mut cmd: FetchCommand<fetch::Attributes>) -> Command {
266        cmd.args.push(b')');
267        Command {
268            args: cmd.args,
269            next_state: None,
270        }
271    }
272}
273
274impl From<FetchCommand<fetch::Modifiers>> for Command {
275    fn from(cmd: FetchCommand<fetch::Modifiers>) -> Command {
276        Command {
277            args: cmd.args,
278            next_state: None,
279        }
280    }
281}
282
283impl FetchCommand<fetch::Modifiers> {
284    pub fn changed_since(mut self, seq: u64) -> FetchCommand<fetch::Modifiers> {
285        changed_since(&mut self.args, seq);
286        self
287    }
288}
289
290fn changed_since(cmd: &mut Vec<u8>, seq: u64) {
291    cmd.extend(b" (CHANGEDSINCE ");
292    cmd.extend(seq.to_string().as_bytes());
293    cmd.push(b')');
294}
295
296/// Returns an escaped string if necessary for use as a "quoted" string per
297/// the IMAPv4 RFC. Return value does not include surrounding quote characters.
298/// Will return Err if the argument contains illegal characters.
299///
300/// Relevant definitions from RFC 3501 formal syntax:
301///
302/// string = quoted / literal [literal elided here]
303/// quoted = DQUOTE *QUOTED-CHAR DQUOTE
304/// QUOTED-CHAR = <any TEXT-CHAR except quoted-specials> / "\" quoted-specials
305/// quoted-specials = DQUOTE / "\"
306/// TEXT-CHAR = <any CHAR except CR and LF>
307fn quoted_string(s: &str) -> Result<Cow<'_, str>, &'static str> {
308    let bytes = s.as_bytes();
309    let (mut start, mut new) = (0, Vec::<u8>::new());
310    for (i, b) in bytes.iter().enumerate() {
311        match *b {
312            b'\r' | b'\n' => {
313                return Err("CR and LF not allowed in quoted strings");
314            }
315            b'\\' | b'"' => {
316                if start < i {
317                    new.extend(&bytes[start..i]);
318                }
319                new.push(b'\\');
320                new.push(*b);
321                start = i + 1;
322            }
323            _ => {}
324        };
325    }
326    if start == 0 {
327        Ok(Cow::Borrowed(s))
328    } else {
329        if start < bytes.len() {
330            new.extend(&bytes[start..]);
331        }
332        // Since the argument is a str, it must contain valid UTF-8. Since
333        // this function's transformation preserves the UTF-8 validity,
334        // unwrapping here should be okay.
335        Ok(Cow::Owned(String::from_utf8(new).unwrap()))
336    }
337}
338
339#[cfg(test)]
340mod tests {
341    use super::{quoted_string, Attribute, Command, CommandBuilder};
342
343    #[test]
344    fn login() {
345        assert_eq!(
346            CommandBuilder::login("djc", "s3cr3t").args,
347            b"LOGIN \"djc\" \"s3cr3t\""
348        );
349        assert_eq!(
350            CommandBuilder::login("djc", "domain\\password").args,
351            b"LOGIN \"djc\" \"domain\\\\password\""
352        );
353    }
354
355    #[test]
356    fn select() {
357        let cmd = Command::from(CommandBuilder::select("INBOX"));
358        assert_eq!(&cmd.args, br#"SELECT "INBOX""#);
359        let cmd = Command::from(CommandBuilder::examine("INBOX").cond_store());
360        assert_eq!(&cmd.args, br#"EXAMINE "INBOX" (CONDSTORE)"#);
361    }
362
363    #[test]
364    fn fetch() {
365        let cmd: Command = CommandBuilder::fetch()
366            .range_from(1..)
367            .attr(Attribute::Uid)
368            .attr(Attribute::ModSeq)
369            .changed_since(13)
370            .into();
371        assert_eq!(cmd.args, &b"FETCH 1:* (UID MODSEQ) (CHANGEDSINCE 13)"[..]);
372
373        let cmd: Command = CommandBuilder::fetch()
374            .num(1)
375            .num(2)
376            .attr(Attribute::Uid)
377            .attr(Attribute::ModSeq)
378            .into();
379        assert_eq!(cmd.args, &b"FETCH 1,2 (UID MODSEQ)"[..]);
380    }
381
382    #[test]
383    fn test_quoted_string() {
384        assert_eq!(quoted_string("a").unwrap(), "a");
385        assert_eq!(quoted_string("").unwrap(), "");
386        assert_eq!(quoted_string("a\"b\\c").unwrap(), "a\\\"b\\\\c");
387        assert_eq!(quoted_string("\"foo\\").unwrap(), "\\\"foo\\\\");
388        assert!(quoted_string("\n").is_err());
389    }
390}