eml-codec/src/rfc5322/datetime.rs

580 lines
18 KiB
Rust
Raw Normal View History

2023-06-22 15:08:50 +02:00
use crate::error::IMFError;
use crate::fragments::lazy;
use crate::fragments::whitespace::{cfws, fws};
2023-06-22 11:15:01 +02:00
use chrono::{DateTime, FixedOffset, NaiveDate, NaiveTime};
2023-06-16 18:16:55 +02:00
use nom::{
2023-06-18 17:27:01 +02:00
branch::alt,
2023-06-22 15:08:50 +02:00
bytes::complete::{is_a, tag, tag_no_case, take_while_m_n},
2023-06-18 17:27:01 +02:00
character,
2023-06-22 15:08:50 +02:00
character::complete::{alphanumeric1, digit0, one_of},
2023-06-18 17:27:01 +02:00
combinator::{map, opt, value},
2023-06-22 15:08:50 +02:00
sequence::{delimited, preceded, terminated, tuple},
IResult,
2023-06-16 18:16:55 +02:00
};
2023-06-16 09:58:07 +02:00
2023-06-18 17:27:01 +02:00
const MIN: i32 = 60;
const HOUR: i32 = 60 * MIN;
2023-06-16 18:16:55 +02:00
2023-06-22 10:48:07 +02:00
impl<'a> TryFrom<&'a lazy::DateTime<'a>> for DateTime<FixedOffset> {
2023-06-20 15:56:06 +02:00
type Error = IMFError<'a>;
2023-06-22 10:48:07 +02:00
fn try_from(value: &'a lazy::DateTime<'a>) -> Result<Self, Self::Error> {
2023-06-20 15:56:06 +02:00
match section(value.0) {
Ok((_, Some(dt))) => Ok(dt),
Err(e) => Err(IMFError::DateTimeParse(e)),
_ => Err(IMFError::DateTimeLogic),
}
}
}
2023-06-18 17:27:01 +02:00
/// Read datetime
///
/// ```abnf
/// date-time = [ day-of-week "," ] date time [CFWS]
/// time = time-of-day zone
/// ```
///
/// ## @FIXME - known bugs
///
/// - if chrono fails, Option::None is silently returned instead of failing the parser
/// - `-0000` means NaiveDateTime, a date without a timezone
/// while this library interprets it as +0000 aka UTC.
/// - Obsolete military zones should be considered as NaiveTime
/// due to an error in RFC0822 but are interpreted as their respective
/// timezone according to the RFC5322 definition
pub fn section(input: &str) -> IResult<&str, Option<DateTime<FixedOffset>>> {
2023-06-22 15:08:50 +02:00
map(
terminated(
2023-06-18 18:28:50 +02:00
alt((
2023-06-22 15:08:50 +02:00
tuple((
opt(terminated(strict_day_of_week, tag(","))),
strict_date,
strict_time_of_day,
strict_zone,
)),
tuple((
opt(terminated(obs_day_of_week, tag(","))),
obs_date,
obs_time_of_day,
alt((strict_zone, obs_zone)),
)),
2023-06-18 18:28:50 +02:00
)),
2023-06-22 15:08:50 +02:00
opt(cfws),
),
|res| match res {
(_, Some(date), Some(time), Some(tz)) => {
date.and_time(time).and_local_timezone(tz).earliest()
2023-06-18 17:27:01 +02:00
}
2023-06-22 15:08:50 +02:00
_ => None,
},
)(input)
2023-06-16 09:58:07 +02:00
}
2023-06-16 18:16:55 +02:00
/// day-of-week = ([FWS] day-name) / obs-day-of-week
2023-06-18 18:28:50 +02:00
fn strict_day_of_week(input: &str) -> IResult<&str, &str> {
2023-06-16 18:16:55 +02:00
preceded(opt(fws), day_name)(input)
}
2023-06-18 18:28:50 +02:00
/// obs-day-of-week = [CFWS] day-name [CFWS]
2023-06-16 18:16:55 +02:00
fn obs_day_of_week(input: &str) -> IResult<&str, &str> {
2023-06-18 17:27:01 +02:00
delimited(opt(cfws), day_name, opt(cfws))(input)
2023-06-16 18:16:55 +02:00
}
/// day-name = "Mon" / "Tue" / "Wed" / "Thu" /
/// "Fri" / "Sat" / "Sun"
fn day_name(input: &str) -> IResult<&str, &str> {
alt((
tag_no_case("Mon"),
tag_no_case("Tue"),
tag_no_case("Wed"),
tag_no_case("Thu"),
tag_no_case("Fri"),
tag_no_case("Sat"),
tag_no_case("Sun"),
))(input)
}
/// date = day month year
2023-06-18 18:28:50 +02:00
fn strict_date(input: &str) -> IResult<&str, Option<NaiveDate>> {
2023-06-22 15:08:50 +02:00
map(tuple((strict_day, month, strict_year)), |(d, m, y)| {
NaiveDate::from_ymd_opt(y, m, d)
})(input)
2023-06-16 18:16:55 +02:00
}
2023-06-18 18:28:50 +02:00
/// date = day month year
fn obs_date(input: &str) -> IResult<&str, Option<NaiveDate>> {
2023-06-22 15:08:50 +02:00
map(tuple((obs_day, month, obs_year)), |(d, m, y)| {
NaiveDate::from_ymd_opt(y, m, d)
})(input)
2023-06-16 18:16:55 +02:00
}
2023-06-18 18:28:50 +02:00
/// day = ([FWS] 1*2DIGIT FWS) / obs-day
fn strict_day(input: &str) -> IResult<&str, u32> {
2023-06-18 17:27:01 +02:00
delimited(opt(fws), character::complete::u32, fws)(input)
2023-06-16 18:16:55 +02:00
}
2023-06-18 18:28:50 +02:00
/// obs-day = [CFWS] 1*2DIGIT [CFWS]
2023-06-18 17:27:01 +02:00
fn obs_day(input: &str) -> IResult<&str, u32> {
delimited(opt(cfws), character::complete::u32, opt(cfws))(input)
2023-06-16 18:16:55 +02:00
}
/// month = "Jan" / "Feb" / "Mar" / "Apr" /
/// "May" / "Jun" / "Jul" / "Aug" /
/// "Sep" / "Oct" / "Nov" / "Dec"
2023-06-18 17:27:01 +02:00
fn month(input: &str) -> IResult<&str, u32> {
2023-06-16 18:16:55 +02:00
alt((
value(1, tag_no_case("Jan")),
value(2, tag_no_case("Feb")),
value(3, tag_no_case("Mar")),
value(4, tag_no_case("Apr")),
value(5, tag_no_case("May")),
value(6, tag_no_case("Jun")),
value(7, tag_no_case("Jul")),
value(8, tag_no_case("Aug")),
value(9, tag_no_case("Sep")),
value(10, tag_no_case("Oct")),
value(11, tag_no_case("Nov")),
value(12, tag_no_case("Dec")),
))(input)
}
/// year = (FWS 4*DIGIT FWS) / obs-year
2023-06-18 17:27:01 +02:00
fn strict_year(input: &str) -> IResult<&str, i32> {
2023-06-18 18:28:50 +02:00
delimited(
2023-06-22 15:08:50 +02:00
fws,
2023-06-18 18:28:50 +02:00
map(
2023-06-22 15:08:50 +02:00
terminated(take_while_m_n(4, 9, |c| c >= '\x30' && c <= '\x39'), digit0),
|d: &str| d.parse::<i32>().unwrap(),
),
2023-06-18 18:28:50 +02:00
fws,
)(input)
2023-06-17 11:43:54 +02:00
}
2023-06-18 18:28:50 +02:00
/// obs-year = [CFWS] 2*DIGIT [CFWS]
2023-06-17 11:43:54 +02:00
fn obs_year(input: &str) -> IResult<&str, i32> {
2023-06-22 15:08:50 +02:00
map(
delimited(
opt(cfws),
terminated(take_while_m_n(2, 7, |c| c >= '\x30' && c <= '\x39'), digit0),
opt(cfws),
),
|cap: &str| {
let d = cap.parse::<i32>().unwrap();
if d >= 0 && d <= 49 {
2000 + d
} else if d >= 50 && d <= 999 {
1900 + d
} else {
d
}
},
)(input)
2023-06-17 11:43:54 +02:00
}
/// time-of-day = hour ":" minute [ ":" second ]
2023-06-18 18:28:50 +02:00
fn strict_time_of_day(input: &str) -> IResult<&str, Option<NaiveTime>> {
map(
2023-06-22 15:08:50 +02:00
tuple((
strict_time_digit,
tag(":"),
strict_time_digit,
opt(preceded(tag(":"), strict_time_digit)),
)),
|(hour, _, minute, maybe_sec)| {
NaiveTime::from_hms_opt(hour, minute, maybe_sec.unwrap_or(0))
},
2023-06-18 18:28:50 +02:00
)(input)
}
/// time-of-day = hour ":" minute [ ":" second ]
fn obs_time_of_day(input: &str) -> IResult<&str, Option<NaiveTime>> {
2023-06-17 11:43:54 +02:00
map(
2023-06-22 15:08:50 +02:00
tuple((
obs_time_digit,
tag(":"),
obs_time_digit,
opt(preceded(tag(":"), obs_time_digit)),
)),
|(hour, _, minute, maybe_sec)| {
NaiveTime::from_hms_opt(hour, minute, maybe_sec.unwrap_or(0))
},
2023-06-18 17:27:01 +02:00
)(input)
2023-06-17 11:43:54 +02:00
}
2023-06-18 18:28:50 +02:00
fn strict_time_digit(input: &str) -> IResult<&str, u32> {
character::complete::u32(input)
}
fn obs_time_digit(input: &str) -> IResult<&str, u32> {
delimited(opt(cfws), character::complete::u32, opt(cfws))(input)
}
2023-06-18 17:27:01 +02:00
/// Obsolete zones
///
/// ```abnf
2023-06-18 18:28:50 +02:00
/// zone = (FWS ( "+" / "-" ) 4DIGIT) / (FWS obs-zone)
/// ```
fn strict_zone(input: &str) -> IResult<&str, Option<FixedOffset>> {
map(
2023-06-22 15:08:50 +02:00
tuple((
opt(fws),
is_a("+-"),
take_while_m_n(2, 2, |c| c >= '\x30' && c <= '\x39'),
take_while_m_n(2, 2, |c| c >= '\x30' && c <= '\x39'),
)),
2023-06-18 18:28:50 +02:00
|(_, op, dig_zone_hour, dig_zone_min)| {
let zone_hour = dig_zone_hour.parse::<i32>().unwrap() * HOUR;
let zone_min = dig_zone_min.parse::<i32>().unwrap() * MIN;
match op {
"+" => FixedOffset::east_opt(zone_hour + zone_min),
"-" => FixedOffset::west_opt(zone_hour + zone_min),
2023-06-22 15:08:50 +02:00
_ => unreachable!(),
}
},
2023-06-18 18:28:50 +02:00
)(input)
}
/// obsole zone
///
2023-06-18 17:27:01 +02:00
/// obs-zone = "UT" / "GMT" / ; Universal Time
/// ; North American UT
/// ; offsets
/// "EST" / "EDT" / ; Eastern: - 5/ - 4
/// "CST" / "CDT" / ; Central: - 6/ - 5
/// "MST" / "MDT" / ; Mountain: - 7/ - 6
/// "PST" / "PDT" / ; Pacific: - 8/ - 7
/// ;
/// %d65-73 / ; Military zones - "A"
/// %d75-90 / ; through "I" and "K"
/// %d97-105 / ; through "Z", both
/// %d107-122 / ; upper and lower case
/// ;
2023-06-22 15:08:50 +02:00
/// 1*(ALPHA / DIGIT) ; Unknown legacy timezones
2023-06-18 18:28:50 +02:00
fn obs_zone(input: &str) -> IResult<&str, Option<FixedOffset>> {
// The writing of this function is volontarily verbose
// to keep it straightforward to understand.
// @FIXME: Could return a TimeZone and not an Option<TimeZone>
// as it could be determined at compile time if values are correct
// and panic at this time if not. But not sure how to do it without unwrap.
preceded(
opt(fws),
alt((
// Legacy UTC/GMT
2023-06-22 15:08:50 +02:00
value(
FixedOffset::west_opt(0 * HOUR),
alt((tag("UTC"), tag("UT"), tag("GMT"))),
),
2023-06-18 18:28:50 +02:00
// USA Timezones
value(FixedOffset::west_opt(4 * HOUR), tag("EDT")),
2023-06-22 15:08:50 +02:00
value(
FixedOffset::west_opt(5 * HOUR),
alt((tag("EST"), tag("CDT"))),
),
value(
FixedOffset::west_opt(6 * HOUR),
alt((tag("CST"), tag("MDT"))),
),
value(
FixedOffset::west_opt(7 * HOUR),
alt((tag("MST"), tag("PDT"))),
),
2023-06-18 18:28:50 +02:00
value(FixedOffset::west_opt(8 * HOUR), tag("PST")),
// Military Timezone UTC
value(FixedOffset::west_opt(0 * HOUR), tag("Z")),
// Military Timezones East
map(one_of("ABCDEFGHIKLMabcdefghiklm"), |c| match c {
'A' | 'a' => FixedOffset::east_opt(1 * HOUR),
'B' | 'b' => FixedOffset::east_opt(2 * HOUR),
'C' | 'c' => FixedOffset::east_opt(3 * HOUR),
'D' | 'd' => FixedOffset::east_opt(4 * HOUR),
'E' | 'e' => FixedOffset::east_opt(5 * HOUR),
'F' | 'f' => FixedOffset::east_opt(6 * HOUR),
'G' | 'g' => FixedOffset::east_opt(7 * HOUR),
'H' | 'h' => FixedOffset::east_opt(8 * HOUR),
'I' | 'i' => FixedOffset::east_opt(9 * HOUR),
'K' | 'k' => FixedOffset::east_opt(10 * HOUR),
'L' | 'l' => FixedOffset::east_opt(11 * HOUR),
'M' | 'm' => FixedOffset::east_opt(12 * HOUR),
_ => unreachable!(),
}),
// Military Timezones West
map(one_of("nopqrstuvwxyNOPQRSTUVWXY"), |c| match c {
'N' | 'n' => FixedOffset::west_opt(1 * HOUR),
'O' | 'o' => FixedOffset::west_opt(2 * HOUR),
'P' | 'p' => FixedOffset::west_opt(3 * HOUR),
'Q' | 'q' => FixedOffset::west_opt(4 * HOUR),
'R' | 'r' => FixedOffset::west_opt(5 * HOUR),
'S' | 's' => FixedOffset::west_opt(6 * HOUR),
'T' | 't' => FixedOffset::west_opt(7 * HOUR),
'U' | 'u' => FixedOffset::west_opt(8 * HOUR),
'V' | 'v' => FixedOffset::west_opt(9 * HOUR),
'W' | 'w' => FixedOffset::west_opt(10 * HOUR),
'X' | 'x' => FixedOffset::west_opt(11 * HOUR),
'Y' | 'y' => FixedOffset::west_opt(12 * HOUR),
_ => unreachable!(),
}),
// Unknown timezone
value(FixedOffset::west_opt(0 * HOUR), alphanumeric1),
)),
2023-06-18 17:27:01 +02:00
)(input)
2023-06-17 11:43:54 +02:00
}
2023-06-18 18:28:50 +02:00
#[cfg(test)]
mod tests {
use super::*;
2023-06-22 12:10:25 +02:00
use chrono::TimeZone;
2023-06-18 18:28:50 +02:00
#[test]
fn test_section_rfc_strict() {
assert_eq!(
2023-06-22 15:08:50 +02:00
section("Fri, 21 Nov 1997 09:55:06 -0600"),
Ok((
"",
Some(
FixedOffset::west_opt(6 * HOUR)
.unwrap()
.with_ymd_and_hms(1997, 11, 21, 9, 55, 6)
.unwrap()
)
)),
2023-06-18 18:28:50 +02:00
);
}
#[test]
fn test_section_received() {
assert_eq!(
section("Sun, 18 Jun 2023 15:39:08 +0200 (CEST)"),
2023-06-22 15:08:50 +02:00
Ok((
"",
Some(
FixedOffset::east_opt(2 * HOUR)
.unwrap()
.with_ymd_and_hms(2023, 6, 18, 15, 39, 8)
.unwrap()
)
)),
2023-06-18 18:28:50 +02:00
);
}
#[test]
fn test_section_rfc_ws() {
assert_eq!(
section(
r#"Thu,
13
Feb
1969
23:32
2023-06-22 15:08:50 +02:00
-0330 (Newfoundland Time)"#
),
Ok((
"",
Some(
FixedOffset::west_opt(3 * HOUR + 30 * MIN)
.unwrap()
.with_ymd_and_hms(1969, 2, 13, 23, 32, 00)
.unwrap()
)
)),
2023-06-18 18:28:50 +02:00
);
}
#[test]
fn test_section_rfc_obs() {
assert_eq!(
section("21 Nov 97 09:55:06 GMT"),
2023-06-22 15:08:50 +02:00
Ok((
"",
Some(
FixedOffset::east_opt(0)
.unwrap()
.with_ymd_and_hms(1997, 11, 21, 9, 55, 6)
.unwrap()
)
)),
2023-06-18 18:28:50 +02:00
);
}
#[test]
fn test_section_3digit_year() {
assert_eq!(
section("21 Nov 103 09:55:06 UT"),
2023-06-22 15:08:50 +02:00
Ok((
"",
Some(
FixedOffset::east_opt(0)
.unwrap()
.with_ymd_and_hms(2003, 11, 21, 9, 55, 6)
.unwrap()
)
)),
2023-06-18 18:28:50 +02:00
);
}
#[test]
fn test_section_rfc_obs_ws() {
assert_eq!(
section("Fri, 21 Nov 1997 09(comment): 55 : 06 -0600"),
2023-06-22 15:08:50 +02:00
Ok((
"",
Some(
FixedOffset::west_opt(6 * HOUR)
.unwrap()
.with_ymd_and_hms(1997, 11, 21, 9, 55, 6)
.unwrap()
)
)),
2023-06-18 18:28:50 +02:00
);
}
#[test]
fn test_section_2digit_year() {
assert_eq!(
section("21 Nov 23 09:55:06Z"),
2023-06-22 15:08:50 +02:00
Ok((
"",
Some(
FixedOffset::east_opt(0)
.unwrap()
.with_ymd_and_hms(2023, 11, 21, 9, 55, 6)
.unwrap()
)
)),
2023-06-18 18:28:50 +02:00
);
}
#[test]
fn test_section_military_zone_east() {
2023-06-22 15:08:50 +02:00
["a", "B", "c", "D", "e", "F", "g", "H", "i", "K", "l", "M"]
.iter()
.enumerate()
.for_each(|(i, x)| {
assert_eq!(
section(format!("1 Jan 22 08:00:00 {}", x).as_str()),
Ok((
"",
Some(
FixedOffset::east_opt((i as i32 + 1) * HOUR)
.unwrap()
.with_ymd_and_hms(2022, 01, 01, 8, 0, 0)
.unwrap()
)
))
);
});
2023-06-18 18:28:50 +02:00
}
#[test]
fn test_section_military_zone_west() {
2023-06-22 15:08:50 +02:00
["N", "O", "P", "q", "r", "s", "T", "U", "V", "w", "x", "y"]
.iter()
.enumerate()
.for_each(|(i, x)| {
assert_eq!(
section(format!("1 Jan 22 08:00:00 {}", x).as_str()),
Ok((
"",
Some(
FixedOffset::west_opt((i as i32 + 1) * HOUR)
.unwrap()
.with_ymd_and_hms(2022, 01, 01, 8, 0, 0)
.unwrap()
)
))
);
});
2023-06-18 18:28:50 +02:00
}
#[test]
fn test_section_gmt() {
assert_eq!(
section("21 Nov 2023 07:07:07 +0000"),
2023-06-22 15:08:50 +02:00
Ok((
"",
Some(
FixedOffset::east_opt(0)
.unwrap()
.with_ymd_and_hms(2023, 11, 21, 7, 7, 7)
.unwrap()
)
)),
2023-06-18 18:28:50 +02:00
);
assert_eq!(
section("21 Nov 2023 07:07:07 -0000"),
2023-06-22 15:08:50 +02:00
Ok((
"",
Some(
FixedOffset::east_opt(0)
.unwrap()
.with_ymd_and_hms(2023, 11, 21, 7, 7, 7)
.unwrap()
)
)),
2023-06-18 18:28:50 +02:00
);
assert_eq!(
section("21 Nov 2023 07:07:07 Z"),
2023-06-22 15:08:50 +02:00
Ok((
"",
Some(
FixedOffset::east_opt(0)
.unwrap()
.with_ymd_and_hms(2023, 11, 21, 7, 7, 7)
.unwrap()
)
)),
2023-06-18 18:28:50 +02:00
);
assert_eq!(
section("21 Nov 2023 07:07:07 GMT"),
2023-06-22 15:08:50 +02:00
Ok((
"",
Some(
FixedOffset::east_opt(0)
.unwrap()
.with_ymd_and_hms(2023, 11, 21, 7, 7, 7)
.unwrap()
)
)),
2023-06-18 18:28:50 +02:00
);
assert_eq!(
section("21 Nov 2023 07:07:07 UT"),
2023-06-22 15:08:50 +02:00
Ok((
"",
Some(
FixedOffset::east_opt(0)
.unwrap()
.with_ymd_and_hms(2023, 11, 21, 7, 7, 7)
.unwrap()
)
)),
2023-06-18 18:28:50 +02:00
);
assert_eq!(
section("21 Nov 2023 07:07:07 UTC"),
2023-06-22 15:08:50 +02:00
Ok((
"",
Some(
FixedOffset::east_opt(0)
.unwrap()
.with_ymd_and_hms(2023, 11, 21, 7, 7, 7)
.unwrap()
)
)),
2023-06-18 18:28:50 +02:00
);
}
#[test]
fn test_section_usa() {
assert_eq!(
section("21 Nov 2023 4:4:4 CST"),
2023-06-22 15:08:50 +02:00
Ok((
"",
Some(
FixedOffset::west_opt(6 * HOUR)
.unwrap()
.with_ymd_and_hms(2023, 11, 21, 4, 4, 4)
.unwrap()
)
)),
2023-06-18 18:28:50 +02:00
);
}
2023-06-18 17:27:01 +02:00
}