pop3_codec/parse/
mod.rs

1use std::str::from_utf8;
2
3use abnf_core::streaming::{is_ALPHA, is_VCHAR, SP};
4use nom::{
5    branch::alt,
6    bytes::streaming::{tag, tag_no_case, take_while, take_while1, take_while_m_n},
7    character::{
8        is_alphanumeric,
9        streaming::{digit1, line_ending},
10    },
11    combinator::{map_res, opt, recognize},
12    multi::many0,
13    sequence::{preceded, terminated, tuple},
14    IResult,
15};
16
17use crate::{
18    parse::{command::*, response::*},
19    types::{
20        command::Command,
21        response::{
22            Capability, DropListing, Greeting, LanguageListing, MultiLine, Response, ScanListing,
23            SingleLine, UniqueIdListing,
24        },
25    },
26};
27
28mod command;
29mod response;
30
31/// Parses the server greeting.
32pub fn greeting(input: &[u8]) -> IResult<&[u8], Greeting> {
33    // greeting = "+OK" [resp-code] *gchar [timestamp] *gchar CRLF
34    //
35    // Corrections:
36    // * [resp-code] -> [SP resp-code]
37    //
38    // TODO: 512 octets maximum (?)
39    let mut parser = tuple((
40        tag_no_case("+OK"),
41        opt(preceded(SP, resp_code)),
42        opt(preceded(
43            SP,
44            tuple((
45                map_res(take_while(is_gchar), from_utf8),
46                opt(timestamp),
47                map_res(take_while(is_gchar), from_utf8),
48            )),
49        )),
50        line_ending,
51    ));
52
53    let (rem, (_, maybe_code, maybe_body, _)) = parser(input)?;
54
55    let code = maybe_code
56        .unwrap_or_default()
57        .into_iter()
58        .map(|lvl| lvl.to_owned())
59        .collect();
60
61    let res = match maybe_body {
62        Some((comment1, maybe_timestamp, comment2)) => {
63            let timestamp = maybe_timestamp.map(ToOwned::to_owned);
64
65            let comment = match timestamp.as_ref() {
66                Some(_) => format!("{}<>{}", comment1, comment2),
67                None => format!("{}{}", comment1, comment2),
68            };
69
70            Greeting {
71                code,
72                comment,
73                timestamp,
74            }
75        }
76        None => Greeting {
77            code,
78            comment: "".into(),
79            timestamp: None,
80        },
81    };
82
83    Ok((rem, res))
84}
85
86/// Parses any command.
87///
88/// See the [Command](crate::types::Command) enum for supported commands.
89pub fn command(input: &[u8]) -> IResult<&[u8], Command> {
90    terminated(
91        alt((
92            user, pass, apop, stls, // AUTHORIZATION
93            capa, quit, // AUTHORIZATION + TRANSACTION
94            stat, list, retr, dele, noop, rset, top, uidl, // TRANSACTION
95            auth, utf8, lang, // not sorted yet
96        )),
97        line_ending,
98    )(input)
99}
100
101/// Parses the response to the [User](crate::types::Command::User) command.
102pub fn response_user(input: &[u8]) -> IResult<&[u8], Response<SingleLine, SingleLine>> {
103    single_line(input, head, false)
104}
105
106/// Parses the response to the [Pass](crate::types::Command::Pass) command.
107pub fn response_pass(input: &[u8]) -> IResult<&[u8], Response<SingleLine, SingleLine>> {
108    single_line(input, head, false)
109}
110
111/// Parses the response to the [Stat](crate::types::Command::Stat) command.
112pub fn response_stat(input: &[u8]) -> IResult<&[u8], Response<DropListing, SingleLine>> {
113    single_line(input, drop_listing, true)
114}
115
116/// Parses the response to the [ListAll](crate::types::Command::ListAll) command, i.e. LIST without a parameter.
117pub fn response_list_all(
118    input: &[u8],
119) -> IResult<&[u8], Response<MultiLine<ScanListing>, SingleLine>> {
120    multi_line(input, scan_listing)
121}
122
123/// Parses the response to the [List](crate::types::Command::List) command, i.e. LIST with a parameter.
124pub fn response_list(input: &[u8]) -> IResult<&[u8], Response<ScanListing, SingleLine>> {
125    single_line(input, scan_listing, true)
126}
127
128/// Parses the response to the [Retr](crate::types::Command::Retr) command.
129pub fn response_retr(input: &[u8]) -> IResult<&[u8], Response<MultiLine<String>, SingleLine>> {
130    multi_line(input, dot_stuffed)
131}
132
133/// Parses the response to the [Dele](crate::types::Command::Dele) command.
134pub fn response_dele(input: &[u8]) -> IResult<&[u8], Response<SingleLine, SingleLine>> {
135    single_line(input, head, false)
136}
137
138/// Parses the response to the [Noop](crate::types::Command::Noop) command.
139pub fn response_noop(input: &[u8]) -> IResult<&[u8], Response<SingleLine, SingleLine>> {
140    single_line(input, head, false)
141}
142
143/// Parses the response to the [Rset](crate::types::Command::Rset) command.
144pub fn response_rset(input: &[u8]) -> IResult<&[u8], Response<SingleLine, SingleLine>> {
145    single_line(input, head, false)
146}
147
148/// Parses the response to the [Quit](crate::types::Command::Quit) command.
149pub fn response_quit(input: &[u8]) -> IResult<&[u8], Response<SingleLine, SingleLine>> {
150    single_line(input, head, false)
151}
152
153/// Parses the response to the [Apop](crate::types::Command::Apop) command.
154pub fn response_apop(input: &[u8]) -> IResult<&[u8], Response<SingleLine, SingleLine>> {
155    single_line(input, head, false)
156}
157
158/// Parses the response to the [Top](crate::types::Command::Top) command.
159pub fn response_top(input: &[u8]) -> IResult<&[u8], Response<MultiLine<String>, SingleLine>> {
160    multi_line(input, dot_stuffed)
161}
162
163/// Parses the response to the [UidlAll](crate::types::Command::UidlAll) command, i.e. UIDL when used without a parameter.
164pub fn response_uidl_all(
165    input: &[u8],
166) -> IResult<&[u8], Response<MultiLine<UniqueIdListing>, SingleLine>> {
167    multi_line(input, unique_id_listing)
168}
169
170/// Parses the response to the [Uidl](crate::types::Command::Uidl) command, i.e. UIDL when used with a parameter.
171pub fn response_uidl(input: &[u8]) -> IResult<&[u8], Response<UniqueIdListing, SingleLine>> {
172    single_line(input, unique_id_listing, true)
173}
174
175/// Parses the response to the [Capa](crate::types::Command::Capa) command.
176pub fn response_capa(input: &[u8]) -> IResult<&[u8], Response<MultiLine<Capability>, SingleLine>> {
177    // capa-resp = single-line *capability "." CRLF
178    multi_line(input, capability)
179}
180
181/// Parses the response to the [Stls](crate::types::Command::Stls) command.
182pub fn response_stls(input: &[u8]) -> IResult<&[u8], Response<SingleLine, SingleLine>> {
183    single_line(input, head, false)
184}
185
186/// Parses the response to the [AuthAll](crate::types::Command::AuthAll) command, i.e. AUTH when used without a parameter.
187///
188/// Note: This command appears to be non-standard. However, MUAs use it and popular POP3 servers understand it.
189pub fn response_auth_all(input: &[u8]) -> IResult<&[u8], Response<MultiLine<String>, SingleLine>> {
190    multi_line(input, dot_stuffed)
191}
192
193// TODO: response_auth
194
195/// Parses the response to the [Utf8](crate::types::Command::Utf8) command.
196pub fn response_utf8(input: &[u8]) -> IResult<&[u8], Response<SingleLine, SingleLine>> {
197    single_line(input, head, false)
198}
199
200/// Parses the response to the [LangAll](crate::types::Command::LangAll) command, i.e. LANG when used without a parameter.
201pub fn response_lang_all(
202    input: &[u8],
203) -> IResult<&[u8], Response<MultiLine<LanguageListing>, SingleLine>> {
204    multi_line(input, language_listing)
205}
206
207/// Parses the response to the [Lang](crate::types::Command::Lang) command, i.e. LANG when used with a parameter.
208pub fn response_lang(input: &[u8]) -> IResult<&[u8], Response<SingleLine, SingleLine>> {
209    single_line(input, head, false)
210}
211
212// -------------------------------------------------------------------------------------------------
213
214pub(crate) fn number(input: &[u8]) -> IResult<&[u8], u32> {
215    map_res(map_res(digit1, from_utf8), str::parse::<u32>)(input)
216}
217
218// -------------------------------------------------------------------------------------------------
219
220/// language-range = (1*8ALPHA *("-" 1*8alphanum)) / "*"
221///
222/// alphanum       = ALPHA / DIGIT
223///
224/// Note: don't use wildcard here, because it is only useful in command?
225pub(crate) fn language(input: &[u8]) -> IResult<&[u8], &str> {
226    map_res(
227        recognize(tuple((
228            take_while_m_n(1, 8, is_ALPHA),
229            many0(tuple((tag(b"-"), take_while_m_n(1, 8, is_alphanumeric)))),
230        ))),
231        from_utf8,
232    )(input)
233}
234
235// -------------------------------------------------------------------------------------------------
236//
237// ABNF from RFC2449
238
239// ----- Generic -----
240
241// param = 1*VCHAR
242pub(crate) fn param(input: &[u8]) -> IResult<&[u8], &str> {
243    map_res(take_while1(is_VCHAR), from_utf8)(input)
244}
245
246// VCHAR = <from ABNF core rules>
247
248// -------------------------------------------------------------------------------------------------
249
250#[cfg(test)]
251mod test {
252    use super::*;
253
254    #[test]
255    fn test_greeting() {
256        let tests: &[(&[u8], Greeting)] = &[
257            (
258                b"+OK\r\n",
259                Greeting {
260                    code: vec![],
261                    comment: "".into(),
262                    timestamp: None,
263                },
264            ),
265            (
266                b"+OK \r\n",
267                Greeting {
268                    code: vec![],
269                    comment: "".into(),
270                    timestamp: None,
271                },
272            ),
273            (
274                b"+OK A\r\n",
275                Greeting {
276                    code: vec![],
277                    comment: "A".into(),
278                    timestamp: None,
279                },
280            ),
281            (
282                b"+OK Z\r\n",
283                Greeting {
284                    code: vec![],
285                    comment: "Z".into(),
286                    timestamp: None,
287                },
288            ),
289            (
290                b"+ok Hello World!\r\n",
291                Greeting {
292                    code: vec![],
293                    comment: "Hello World!".into(),
294                    timestamp: None,
295                },
296            ),
297            (
298                b"+ok Hello <123> World!\r\n",
299                Greeting {
300                    code: vec![],
301                    comment: "Hello <> World!".into(),
302                    timestamp: Some("123".into()),
303                },
304            ),
305            (
306                b"+ok [a] Hello World!\r\n",
307                Greeting {
308                    code: vec!["a".into()],
309                    comment: "Hello World!".into(),
310                    timestamp: None,
311                },
312            ),
313            (
314                b"+ok [a] Hello <123> World!\r\n",
315                Greeting {
316                    code: vec!["a".into()],
317                    comment: "Hello <> World!".into(),
318                    timestamp: Some("123".into()),
319                },
320            ),
321        ];
322
323        for (test, expected) in tests {
324            let (rem, got) = greeting(test).unwrap();
325            assert!(rem.is_empty());
326            assert_eq!(*expected, got);
327        }
328    }
329
330    #[test]
331    fn test_command() {
332        // Extracted via "C: ([^\n]*\n)" regex from RFC 1939
333        let commands = "\
334QUIT
335STAT
336LIST
337LIST 2
338LIST 3
339RETR 1
340DELE 1
341DELE 2
342NOOP
343RSET
344QUIT
345QUIT
346TOP 1 10
347TOP 100 3
348UIDL
349UIDL 2
350UIDL 3
351USER frated
352USER mrose
353USER mrose
354PASS secret
355USER mrose
356PASS secret
357APOP mrose c4c9334bac560ecc979e58001b3e22fb
358APOP mrose c4c9334bac560ecc979e58001b3e22fb
359STAT
360LIST
361RETR 1
362DELE 1
363RETR 2
364DELE 2
365QUIT
366LANG MUL
367LANG
368LANG
369LANG es
370LANG uga
371LANG sv
372LANG *
373UTF8
374";
375
376        for cmd in commands.lines() {
377            let cmd = cmd.to_string() + "\r\n";
378            print!("C: {}", cmd);
379            let (rem, cmd) = super::command(cmd.as_bytes()).unwrap();
380            assert!(rem.is_empty());
381            println!("{:?}\n", cmd);
382        }
383    }
384
385    #[test]
386    fn test_response() {
387        println!(
388            "{:#?}",
389            response_quit(b"+OK dewey POP3 server signing off\r\n")
390                .unwrap()
391                .1
392        );
393        println!("{:#?}", response_stat(b"+OK 2 320\r\n").unwrap().1);
394        println!(
395            "{:#?}",
396            response_list_all(
397                b"+OK 2 messages (320 octets)
3981 120
3992 200
400.
401",
402            )
403            .unwrap()
404            .1
405        );
406        println!("{:#?}", response_list(b"+OK 2 200\r\n").unwrap().1);
407        println!(
408            "{:#?}",
409            response_list(b"-ERR no such message, only 2 messages in maildrop\r\n")
410                .unwrap()
411                .1
412        );
413        println!(
414            "{:#?}",
415            response_retr(
416                b"+OK 120 octets
417<the POP3 server sends the entire message here>
418.
419",
420            )
421            .unwrap()
422            .1
423        );
424        println!(
425            "{:#?}",
426            response_dele(b"+OK message 1 deleted\r\n").unwrap().1
427        );
428        println!(
429            "{:#?}",
430            response_dele(b"-ERR message 2 already deleted\r\n")
431                .unwrap()
432                .1
433        );
434        println!("{:#?}", response_noop(b"+OK\r\n").unwrap().1);
435        println!(
436            "{:#?}",
437            response_rset(b"+OK maildrop has 2 messages (320 octets)\r\n")
438                .unwrap()
439                .1
440        );
441        println!(
442            "{:#?}",
443            response_quit(b"+OK dewey POP3 server signing off (maildrop empty)\r\n")
444                .unwrap()
445                .1
446        );
447        println!(
448            "{:#?}",
449            response_quit(b"+OK dewey POP3 server signing off (2 messages left)\r\n")
450                .unwrap()
451                .1
452        );
453        println!("{:#?}", response_top(b"+OK
454<the POP3 server sends the headers of the message, a blank line, and the first 10 lines of the body of the message>
455.
456").unwrap().1);
457        println!(
458            "{:#?}",
459            response_top(b"-ERR no such message\r\n").unwrap().1
460        );
461        println!(
462            "{:#?}",
463            response_uidl_all(
464                b"+OK
4651 whqtswO00WBw418f9t5JxYwZ
4662 QhdPYR:00WBw1Ph7x7
467.
468",
469            )
470            .unwrap()
471            .1
472        );
473        println!(
474            "{:#?}",
475            response_uidl(b"+OK 2 QhdPYR:00WBw1Ph7x7\r\n").unwrap().1
476        );
477        println!(
478            "{:#?}",
479            response_uidl(b"-ERR no such message, only 2 messages in maildrop\r\n")
480                .unwrap()
481                .1
482        );
483        println!(
484            "{:#?}",
485            response_user(b"-ERR sorry, no mailbox for frated here\r\n")
486                .unwrap()
487                .1
488        );
489        println!(
490            "{:#?}",
491            response_user(b"+OK mrose is a real hoopy frood\r\n")
492                .unwrap()
493                .1
494        );
495        println!(
496            "{:#?}",
497            response_pass(b"-ERR maildrop already locked\r\n")
498                .unwrap()
499                .1
500        );
501        println!(
502            "{:#?}",
503            response_pass(b"+OK mrose's maildrop has 2 messages (320 octets)\r\n")
504                .unwrap()
505                .1
506        );
507        println!(
508            "{:#?}",
509            response_apop(b"+OK maildrop has 1 message (369 octets)\r\n")
510                .unwrap()
511                .1
512        );
513
514        println!(
515            "{:#?}",
516            response_lang(b"-ERR invalid language MUL\r\n").unwrap().1
517        );
518        println!(
519            "{:#?}",
520            response_lang_all(
521                b"+OK Language listing follows:
522en English
523en-boont English Boontling dialect
524de Deutsch
525it Italiano
526es Espanol
527sv Svenska
528i-default Default language
529.
530"
531            )
532            .unwrap()
533            .1
534        );
535        println!(
536            "{:#?}",
537            response_lang_all(b"-ERR Server is unable to list languages\r\n")
538                .unwrap()
539                .1
540        );
541        println!(
542            "{:#?}",
543            response_lang(b"+OK es Idioma cambiado\r\n").unwrap().1
544        );
545        println!(
546            "{:#?}",
547            response_lang(b"-ERR es Idioma <<UGA>> no es conocido\r\n")
548                .unwrap()
549                .1
550        );
551        println!(
552            "{:#?}",
553            response_lang(b"+OK sv Kommandot \"LANG\" lyckades\r\n")
554                .unwrap()
555                .1
556        );
557        println!(
558            "{:#?}",
559            response_lang(b"+OK es Idioma cambiado\r\n").unwrap().1
560        );
561    }
562
563    #[test]
564    fn test_example_session() {
565        let client = b"\
566APOP mrose c4c9334bac560ecc979e58001b3e22fb
567STAT
568LIST
569RETR 1
570DELE 1
571RETR 2
572DELE 2
573QUIT
574";
575
576        let server = b"\
577+OK POP3 server ready <1896.697170952@dbc.mtview.ca.us>
578+OK mrose's maildrop has 2 messages (320 octets)
579+OK 2 320
580+OK 2 messages (320 octets)
5811 120
5822 200
583.
584+OK 120 octets
585<the POP3 server sends message 1>
586.
587+OK message 1 deleted
588+OK 200 octets
589<the POP3 server sends message 2>
590.
591+OK message 2 deleted
592+OK dewey POP3 server signing off (maildrop empty)
593";
594
595        let mut rem_client = client.as_ref();
596        let mut rem_server = server.as_ref();
597
598        let (rem, greeting) = greeting(rem_server).unwrap();
599        println!("{:#?}", greeting);
600        rem_server = rem;
601
602        while !rem_client.is_empty() {
603            let (rem, cmd) = command(rem_client).unwrap();
604            println!("{:#?}", cmd);
605            rem_client = rem;
606
607            use Command::*;
608            match cmd {
609                User(_) => {
610                    let (rem, resp) = response_user(rem_server).unwrap();
611                    println!("{:#?}", resp);
612                    rem_server = rem;
613                }
614                Pass(_) => {
615                    let (rem, resp) = response_pass(rem_server).unwrap();
616                    println!("{:#?}", resp);
617                    rem_server = rem;
618                }
619                Stat => {
620                    let (rem, resp) = response_stat(rem_server).unwrap();
621                    println!("{:#?}", resp);
622                    rem_server = rem;
623                }
624                ListAll => {
625                    let (rem, resp) = response_list_all(rem_server).unwrap();
626                    println!("{:#?}", resp);
627                    rem_server = rem;
628                }
629                List { .. } => {
630                    let (rem, resp) = response_list(rem_server).unwrap();
631                    println!("{:#?}", resp);
632                    rem_server = rem;
633                }
634                Retr { .. } => {
635                    let (rem, resp) = response_retr(rem_server).unwrap();
636                    println!("{:#?}", resp);
637                    rem_server = rem;
638                }
639                Dele { .. } => {
640                    let (rem, resp) = response_dele(rem_server).unwrap();
641                    println!("{:#?}", resp);
642                    rem_server = rem;
643                }
644                Noop => {
645                    let (rem, resp) = response_noop(rem_server).unwrap();
646                    println!("{:#?}", resp);
647                    rem_server = rem;
648                }
649                Rset => {
650                    let (rem, resp) = response_rset(rem_server).unwrap();
651                    println!("{:#?}", resp);
652                    rem_server = rem;
653                }
654                Quit => {
655                    let (rem, resp) = response_quit(rem_server).unwrap();
656                    println!("{:#?}", resp);
657                    rem_server = rem;
658                }
659                Apop { .. } => {
660                    let (rem, resp) = response_apop(rem_server).unwrap();
661                    println!("{:#?}", resp);
662                    rem_server = rem;
663                }
664                Top { .. } => {
665                    let (rem, resp) = response_top(rem_server).unwrap();
666                    println!("{:#?}", resp);
667                    rem_server = rem;
668                }
669                UidlAll => {
670                    let (rem, resp) = response_uidl_all(rem_server).unwrap();
671                    println!("{:#?}", resp);
672                    rem_server = rem;
673                }
674                Uidl { .. } => {
675                    let (rem, resp) = response_uidl(rem_server).unwrap();
676                    println!("{:#?}", resp);
677                    rem_server = rem;
678                }
679                Capa => {
680                    let (rem, resp) = response_user(rem_server).unwrap();
681                    println!("{:#?}", resp);
682                    rem_server = rem;
683                }
684                Stls => {
685                    let (rem, resp) = response_stls(rem_server).unwrap();
686                    println!("{:#?}", resp);
687                    rem_server = rem;
688                }
689                AuthAll => {
690                    let (rem, resp) = response_auth_all(rem_server).unwrap();
691                    println!("{:#?}", resp);
692                    rem_server = rem;
693                }
694                Auth { .. } => {
695                    unimplemented!()
696                }
697                Utf8 => {
698                    let (rem, resp) = response_utf8(rem_server).unwrap();
699                    println!("{:#?}", resp);
700                    rem_server = rem;
701                }
702                LangAll => {
703                    let (rem, resp) = response_lang_all(rem_server).unwrap();
704                    println!("{:#?}", resp);
705                    rem_server = rem;
706                }
707                Lang { .. } => {
708                    let (rem, resp) = response_lang(rem_server).unwrap();
709                    println!("{:#?}", resp);
710                    rem_server = rem;
711                }
712            }
713        }
714    }
715}