imap_codec/
datetime.rs

1use abnf_core::{
2    is_digit,
3    streaming::{dquote, sp},
4};
5use chrono::{
6    FixedOffset, LocalResult, NaiveDate as ChronoNaiveDate, NaiveDateTime, NaiveTime, TimeZone,
7};
8use imap_types::datetime::{DateTime, NaiveDate};
9use nom::{
10    branch::alt,
11    bytes::streaming::{tag, tag_no_case, take_while_m_n},
12    character::streaming::char,
13    combinator::{map, map_res, value},
14    sequence::{delimited, preceded, tuple},
15};
16
17use crate::decode::{IMAPErrorKind, IMAPParseError, IMAPResult};
18
19/// ```abnf
20/// date = date-text / DQUOTE date-text DQUOTE
21/// ```
22pub(crate) fn date(input: &[u8]) -> IMAPResult<&[u8], Option<NaiveDate>> {
23    alt((date_text, delimited(dquote, date_text, dquote)))(input)
24}
25
26/// ```abnf
27/// date-text = date-day "-" date-month "-" date-year
28/// ```
29pub(crate) fn date_text(input: &[u8]) -> IMAPResult<&[u8], Option<NaiveDate>> {
30    let mut parser = tuple((date_day, tag(b"-"), date_month, tag(b"-"), date_year));
31
32    let (remaining, (d, _, m, _, y)) = parser(input)?;
33
34    Ok((
35        remaining,
36        ChronoNaiveDate::from_ymd_opt(y.into(), m.into(), d.into()).map(NaiveDate::unvalidated),
37    ))
38}
39
40/// Day of month.
41///
42/// ```abnf
43/// date-day = 1*2DIGIT
44/// ```
45pub(crate) fn date_day(input: &[u8]) -> IMAPResult<&[u8], u8> {
46    digit_1_2(input)
47}
48
49/// ```abnf
50/// date-month = "Jan" / "Feb" / "Mar" / "Apr" /
51///              "May" / "Jun" / "Jul" / "Aug" /
52///              "Sep" / "Oct" / "Nov" / "Dec"
53/// ```
54pub(crate) fn date_month(input: &[u8]) -> IMAPResult<&[u8], u8> {
55    alt((
56        value(1, tag_no_case(b"Jan")),
57        value(2, tag_no_case(b"Feb")),
58        value(3, tag_no_case(b"Mar")),
59        value(4, tag_no_case(b"Apr")),
60        value(5, tag_no_case(b"May")),
61        value(6, tag_no_case(b"Jun")),
62        value(7, tag_no_case(b"Jul")),
63        value(8, tag_no_case(b"Aug")),
64        value(9, tag_no_case(b"Sep")),
65        value(10, tag_no_case(b"Oct")),
66        value(11, tag_no_case(b"Nov")),
67        value(12, tag_no_case(b"Dec")),
68    ))(input)
69}
70
71/// ```abnf
72/// date-year = 4DIGIT
73/// ```
74pub(crate) fn date_year(input: &[u8]) -> IMAPResult<&[u8], u16> {
75    digit_4(input)
76}
77
78/// Hours minutes seconds.
79///
80/// ```abnf
81/// time = 2DIGIT ":" 2DIGIT ":" 2DIGIT
82/// ```
83pub(crate) fn time(input: &[u8]) -> IMAPResult<&[u8], Option<NaiveTime>> {
84    let mut parser = tuple((digit_2, tag(b":"), digit_2, tag(b":"), digit_2));
85
86    let (remaining, (h, _, m, _, s)) = parser(input)?;
87
88    Ok((
89        remaining,
90        NaiveTime::from_hms_opt(h.into(), m.into(), s.into()),
91    ))
92}
93
94/// ```abnf
95/// date-time = DQUOTE
96///              date-day-fixed "-" date-month "-" date-year SP
97///              time SP
98///              zone
99///             DQUOTE
100/// ```
101pub(crate) fn date_time(input: &[u8]) -> IMAPResult<&[u8], DateTime> {
102    let mut parser = delimited(
103        dquote,
104        tuple((
105            date_day_fixed,
106            tag(b"-"),
107            date_month,
108            tag(b"-"),
109            date_year,
110            sp,
111            time,
112            sp,
113            zone,
114        )),
115        dquote,
116    );
117
118    let (remaining, (d, _, m, _, y, _, time, _, zone)) = parser(input)?;
119
120    let date = ChronoNaiveDate::from_ymd_opt(y.into(), m.into(), d.into());
121
122    match (date, time, zone) {
123        (Some(date), Some(time), Some(zone)) => {
124            let local_datetime = NaiveDateTime::new(date, time);
125
126            if let LocalResult::Single(datetime) = zone.from_local_datetime(&local_datetime) {
127                Ok((remaining, DateTime::unvalidated(datetime)))
128            } else {
129                Err(nom::Err::Failure(IMAPParseError {
130                    input,
131                    kind: IMAPErrorKind::BadDateTime,
132                }))
133            }
134        }
135        _ => Err(nom::Err::Failure(IMAPParseError {
136            input,
137            kind: IMAPErrorKind::BadDateTime,
138        })),
139    }
140}
141
142/// Fixed-format version of date-day.
143///
144/// ```abnf
145/// date-day-fixed = (SP DIGIT) / 2DIGIT
146/// ```
147pub(crate) fn date_day_fixed(input: &[u8]) -> IMAPResult<&[u8], u8> {
148    alt((
149        map(
150            preceded(sp, take_while_m_n(1, 1, is_digit)),
151            |bytes: &[u8]| bytes[0] - b'0',
152        ),
153        digit_2,
154    ))(input)
155}
156
157/// Signed four-digit value of hhmm representing hours and minutes east of Greenwich (that is, the
158/// amount that the given time differs from Universal Time).
159///
160/// Subtracting the timezone from the given time will give the UT form. The Universal Time zone is
161/// "+0000".
162///
163/// ```abnf
164/// zone = ("+" / "-") 4DIGIT
165/// ```
166pub(crate) fn zone(input: &[u8]) -> IMAPResult<&[u8], Option<FixedOffset>> {
167    let mut parser = tuple((alt((char('+'), char('-'))), digit_2, digit_2));
168
169    let (remaining, (sign, hh, mm)) = parser(input)?;
170
171    let offset = 3600 * (hh as i32) + 60 * (mm as i32);
172
173    let zone = match sign {
174        '+' => FixedOffset::east_opt(offset),
175        '-' => FixedOffset::west_opt(offset),
176        _ => unreachable!(),
177    };
178
179    Ok((remaining, zone))
180}
181
182fn digit_1_2(input: &[u8]) -> IMAPResult<&[u8], u8> {
183    map_res(
184        map(take_while_m_n(1, 2, is_digit), |bytes| {
185            // # Safety
186            //
187            // `bytes` is always UTF-8.
188            std::str::from_utf8(bytes).unwrap()
189        }),
190        str::parse::<u8>,
191    )(input)
192}
193
194fn digit_2(input: &[u8]) -> IMAPResult<&[u8], u8> {
195    map_res(
196        map(take_while_m_n(2, 2, is_digit), |bytes| {
197            // # Safety
198            //
199            // `bytes` is always UTF-8.
200            std::str::from_utf8(bytes).unwrap()
201        }),
202        str::parse::<u8>,
203    )(input)
204}
205
206fn digit_4(input: &[u8]) -> IMAPResult<&[u8], u16> {
207    map_res(
208        map(take_while_m_n(4, 4, is_digit), |bytes| {
209            // # Safety
210            //
211            // `bytes` is always UTF-8.
212            std::str::from_utf8(bytes).unwrap()
213        }),
214        str::parse::<u16>,
215    )(input)
216}
217
218#[cfg(test)]
219mod tests {
220    use std::str::from_utf8;
221
222    use super::*;
223    use crate::testing::known_answer_test_encode;
224
225    #[test]
226    fn test_encode_date_time() {
227        let tests = [
228            (
229                DateTime::try_from(
230                    chrono::DateTime::parse_from_rfc2822("Mon, 7 Feb 1994 21:52:25 -0800 (PST)")
231                        .unwrap(),
232                )
233                .unwrap(),
234                b"\"07-Feb-1994 21:52:25 -0800\"".as_ref(),
235            ),
236            (
237                DateTime::try_from(
238                    chrono::DateTime::parse_from_rfc2822("Mon, 7 Feb 0000 21:52:25 -0800 (PST)")
239                        .unwrap(),
240                )
241                .unwrap(),
242                b"\"07-Feb-0000 21:52:25 -0800\"".as_ref(),
243            ),
244        ];
245
246        for test in tests {
247            known_answer_test_encode(test);
248        }
249    }
250
251    #[test]
252    fn test_date() {
253        let (rem, val) = date(b"1-Feb-2020xxx").unwrap();
254        assert_eq!(rem, b"xxx");
255        assert_eq!(
256            val,
257            ChronoNaiveDate::from_ymd_opt(2020, 2, 1).map(NaiveDate::unvalidated)
258        );
259
260        let (rem, val) = date(b"\"1-Feb-2020\"xxx").unwrap();
261        assert_eq!(rem, b"xxx");
262        assert_eq!(
263            val,
264            ChronoNaiveDate::from_ymd_opt(2020, 2, 1).map(NaiveDate::unvalidated)
265        );
266
267        let (rem, val) = date(b"\"01-Feb-2020\"xxx").unwrap();
268        assert_eq!(rem, b"xxx");
269        assert_eq!(
270            val,
271            ChronoNaiveDate::from_ymd_opt(2020, 2, 1).map(NaiveDate::unvalidated)
272        );
273    }
274
275    #[test]
276    fn test_date_text() {
277        let (rem, val) = date_text(b"1-Feb-2020").unwrap();
278        assert_eq!(rem, b"");
279        assert_eq!(
280            val,
281            ChronoNaiveDate::from_ymd_opt(2020, 2, 1).map(NaiveDate::unvalidated)
282        );
283    }
284
285    #[test]
286    fn test_date_day() {
287        let (rem, val) = date_day(b"1xxx").unwrap();
288        assert_eq!(rem, b"xxx");
289        assert_eq!(val, 1);
290
291        let (rem, val) = date_day(b"01xxx").unwrap();
292        assert_eq!(rem, b"xxx");
293        assert_eq!(val, 1);
294
295        let (rem, val) = date_day(b"999xxx").unwrap();
296        assert_eq!(rem, b"9xxx");
297        assert_eq!(val, 99);
298    }
299
300    #[test]
301    fn test_date_month() {
302        let (rem, val) = date_month(b"jAn").unwrap();
303        assert_eq!(rem, b"");
304        assert_eq!(val, 1);
305
306        let (rem, val) = date_month(b"DeCxxx").unwrap();
307        assert_eq!(rem, b"xxx");
308        assert_eq!(val, 12);
309    }
310
311    #[test]
312    fn test_date_year() {
313        let (rem, val) = date_year(b"1985xxx").unwrap();
314        assert_eq!(rem, b"xxx");
315        assert_eq!(val, 1985);
316
317        let (rem, val) = date_year(b"1991xxx").unwrap();
318        assert_eq!(rem, b"xxx");
319        assert_eq!(val, 1991);
320    }
321
322    #[test]
323    fn test_date_day_fixed() {
324        let (rem, val) = date_day_fixed(b"00").unwrap();
325        assert_eq!(rem, b"");
326        assert_eq!(val, 0);
327
328        let (rem, val) = date_day_fixed(b" 0").unwrap();
329        assert_eq!(rem, b"");
330        assert_eq!(val, 0);
331
332        let (rem, val) = date_day_fixed(b"99").unwrap();
333        assert_eq!(rem, b"");
334        assert_eq!(val, 99);
335
336        let (rem, val) = date_day_fixed(b" 9").unwrap();
337        assert_eq!(rem, b"");
338        assert_eq!(val, 9);
339    }
340
341    #[test]
342    fn test_time() {
343        assert!(time(b"1:34:56xxx").is_err());
344        assert!(time(b"12:3:56xxx").is_err());
345        assert!(time(b"12:34:5xxx").is_err());
346
347        let (rem, val) = time(b"12:34:56xxx").unwrap();
348        assert_eq!(rem, b"xxx");
349        assert_eq!(val, NaiveTime::from_hms_opt(12, 34, 56));
350
351        let (rem, val) = time(b"99:99:99 ").unwrap();
352        assert_eq!(rem, b" ");
353        assert_eq!(val, NaiveTime::from_hms_opt(99, 99, 99));
354
355        let (rem, val) = time(b"12:34:56").unwrap();
356        assert_eq!(rem, b"");
357        assert_eq!(val, NaiveTime::from_hms_opt(12, 34, 56));
358
359        let (rem, val) = time(b"99:99:99").unwrap();
360        assert_eq!(rem, b"");
361        assert_eq!(val, NaiveTime::from_hms_opt(99, 99, 99));
362    }
363
364    #[test]
365    fn test_date_time() {
366        let (rem, val) = date_time(b"\" 1-Feb-1985 12:34:56 +0100\"xxx").unwrap();
367        assert_eq!(rem, b"xxx");
368
369        let local_datetime = NaiveDateTime::new(
370            ChronoNaiveDate::from_ymd_opt(1985, 2, 1).unwrap(),
371            NaiveTime::from_hms_opt(12, 34, 56).unwrap(),
372        );
373
374        let datetime = DateTime::try_from(
375            FixedOffset::east_opt(3600)
376                .unwrap()
377                .from_local_datetime(&local_datetime)
378                .unwrap(),
379        )
380        .unwrap();
381
382        println!("{:?} == \n{:?}", val, datetime);
383
384        assert_eq!(val, datetime);
385    }
386
387    #[test]
388    fn test_date_time_invalid() {
389        let tests = [
390            b"\" 1-Feb-0000 12:34:56 +0000\"xxx".as_ref(), // ok
391            b"\" 1-Feb-9999 12:34:56 +0000\"xxx",          // ok
392            b"\" 1-Feb-0000 12:34:56 -0000\"xxx",          // ok
393            b"\" 1-Feb-9999 12:34:56 -0000\"xxx",          // ok
394            b"\" 1-Feb-2020 00:00:00 +0100\"xxx",          // ok
395            b"\" 1-Feb-0000 12:34:56 +9999\"xxx",
396            b"\" 1-Feb-9999 12:34:56 +9999\"xxx",
397            b"\" 1-Feb-0000 12:34:56 -9999\"xxx",
398            b"\" 1-Feb-9999 12:34:56 -9999\"xxx",
399            b"\" 1-Feb-2020 99:99:99 +0100\"xxx",
400            b"\"31-Feb-2020 00:00:00 +0100\"xxx",
401            b"\"99-Feb-2020 99:99:99 +0100\"xxx",
402        ];
403
404        for test in &tests[..5] {
405            let (rem, datetime) = date_time(test).unwrap();
406            assert_eq!(rem, b"xxx");
407            println!("{} -> {:?}", from_utf8(test).unwrap(), datetime);
408        }
409
410        for test in &tests[5..] {
411            assert!(date_time(test).is_err());
412        }
413    }
414
415    #[test]
416    fn test_zone() {
417        let (rem, val) = zone(b"+0000xxx").unwrap();
418        eprintln!("{:?}", val);
419        assert_eq!(rem, b"xxx");
420        assert_eq!(val, FixedOffset::east_opt(0));
421
422        let (rem, val) = zone(b"+0000").unwrap();
423        eprintln!("{:?}", val);
424        assert_eq!(rem, b"");
425        assert_eq!(val, FixedOffset::east_opt(0));
426
427        let (rem, val) = zone(b"-0205xxx").unwrap();
428        eprintln!("{:?}", val);
429        assert_eq!(rem, b"xxx");
430        assert_eq!(val, FixedOffset::west_opt(2 * 3600 + 5 * 60));
431
432        let (rem, val) = zone(b"-1159").unwrap();
433        eprintln!("{:?}", val);
434        assert_eq!(rem, b"");
435        assert_eq!(val, FixedOffset::west_opt(11 * 3600 + 59 * 60));
436
437        let (rem, val) = zone(b"-1159").unwrap();
438        eprintln!("{:?}", val);
439        assert_eq!(rem, b"");
440        assert_eq!(val, FixedOffset::west_opt(11 * 3600 + 59 * 60));
441    }
442}