email-parser 0.5.0

The fastest and lightest email parsing Rust library. Supports MIME.
Documentation
use crate::prelude::*;

pub fn day_name(input: &[u8]) -> Res<Day> {
    if let (Some(input), Some(letters)) = (input.get(3..), input.get(..3)) {
        let letters = letters.to_ascii_lowercase();
        match letters.as_slice() {
            b"mon" => Ok((input, Day::Monday)),
            b"tue" => Ok((input, Day::Tuesday)),
            b"wed" => Ok((input, Day::Wednesday)),
            b"thu" => Ok((input, Day::Thursday)),
            b"fri" => Ok((input, Day::Friday)),
            b"sat" => Ok((input, Day::Saturday)),
            b"sun" => Ok((input, Day::Sunday)),
            _ => Err(Error::Unknown("Not a valid day_name")),
        }
    } else {
        Err(Error::Unknown(
            "Expected day_name, but characters are missing (at least 3).",
        ))
    }
}

pub fn month(input: &[u8]) -> Res<Month> {
    if let (Some(input), Some(letters)) = (input.get(3..), input.get(..3)) {
        let letters = letters.to_ascii_lowercase();
        match letters.as_slice() {
            b"jan" => Ok((input, Month::January)),
            b"feb" => Ok((input, Month::February)),
            b"mar" => Ok((input, Month::March)),
            b"apr" => Ok((input, Month::April)),
            b"may" => Ok((input, Month::May)),
            b"jun" => Ok((input, Month::June)),
            b"jul" => Ok((input, Month::July)),
            b"aug" => Ok((input, Month::August)),
            b"sep" => Ok((input, Month::September)),
            b"oct" => Ok((input, Month::October)),
            b"nov" => Ok((input, Month::November)),
            b"dec" => Ok((input, Month::December)),
            _ => Err(Error::Unknown("Not a valid month")),
        }
    } else {
        Err(Error::Unknown(
            "Expected month, but characters are missing (at least 3).",
        ))
    }
}

pub fn day_of_week(input: &[u8]) -> Res<Day> {
    let (input, _fws) = optional(input, fws);
    let (input, day) = day_name(input)?;
    let (input, ()) = tag(input, b",")?;
    Ok((input, day))
}

pub fn year(input: &[u8]) -> Res<usize> {
    let (input, _) = fws(input)?;

    let (input, year) =
        take_while1(input, is_digit).map_err(|_e| Error::Unknown("no digit in year"))?;
    if year.len() < 4 {
        return Err(Error::Unknown("year is expected to have 4 digits or more"));
    }
    let year: usize = year
        .parse()
        .map_err(|_e| Error::Unknown("Failed to parse year"))?;

    if year < 1990 {
        return Err(Error::Unknown("year must be after 1990"));
    }

    let (input, _) = fws(input)?;

    Ok((input, year))
}

pub fn day(input: &[u8]) -> Res<u8> {
    let (input, _fws) = optional(input, fws);
    let (mut input, mut day) = digit(input)?;
    if let Ok((new_input, digit)) = digit(input) {
        day *= 10;
        day += digit;
        input = new_input;
    }
    if day > 31 {
        return Err(Error::Unknown("day must be less than 31"));
    }
    let (input, _) = fws(input)?;
    Ok((input, day))
}

pub fn time_of_day(input: &[u8]) -> Res<Time> {
    let (input, hour) = two_digits(input)?;
    if hour > 23 {
        return Err(Error::Unknown("There is only 24 hours in a day"));
    }
    let (input, ()) = tag(input, b":")?;

    let (input, minute) = two_digits(input)?;
    if minute > 59 {
        return Err(Error::Unknown("There is only 60 minutes per hour"));
    }

    if input.starts_with(b":") {
        let new_input = &input[1..];
        if let Ok((new_input, second)) = two_digits(new_input) {
            if second > 60 {
                // leap second allowed
                return Err(Error::Unknown("There is only 60 seconds in a minute"));
            }
            return Ok((
                new_input,
                Time {
                    hour,
                    minute,
                    second,
                },
            ));
        }
    }

    Ok((
        input,
        Time {
            hour,
            minute,
            second: 0,
        },
    ))
}

pub fn zone(input: &[u8]) -> Res<Zone> {
    let (mut input, _fws) = fws(input)?;

    let sign = match input.get(0) {
        Some(b'+') => true,
        Some(b'-') => false,
        None => return Err(Error::Unknown("Expected more characters in zone")),
        _ => return Err(Error::Unknown("Invalid sign character in zone")),
    };
    input = &input[1..];

    let (input, hour_offset) = two_digits(input)?;
    let (input, minute_offset) = two_digits(input)?;

    if minute_offset > 59 {
        return Err(Error::Unknown("zone minute_offset out of range"));
    }

    Ok((
        input,
        Zone {
            sign,
            hour_offset,
            minute_offset,
        },
    ))
}

pub fn time(input: &[u8]) -> Res<TimeWithZone> {
    let (input, time) = time_of_day(input)?;
    let (input, zone) = zone(input)?;
    Ok((input, TimeWithZone { time, zone }))
}

pub fn date(input: &[u8]) -> Res<Date> {
    let (input, day) = day(input)?;
    let (input, month) = month(input)?;
    let (input, year) = year(input)?;
    Ok((input, Date { day, month, year }))
}

pub fn date_time(input: &[u8]) -> Res<DateTime> {
    let (input, day) = optional(input, day_of_week);
    let (input, date) = date(input)?;
    let (input, time) = time(input)?;
    let (input, _cfws) = optional(input, cfws);
    Ok((
        input,
        DateTime {
            day_name: day,
            date,
            time,
        },
    ))
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn test_day() {
        assert_eq!(day_name(b"Mon ").unwrap().1, Day::Monday);
        assert_eq!(day_name(b"moN ").unwrap().1, Day::Monday);
        assert_eq!(day_name(b"thu").unwrap().1, Day::Thursday);

        assert_eq!(day_of_week(b"   thu, ").unwrap().1, Day::Thursday);
        assert_eq!(day_of_week(b"wed, ").unwrap().1, Day::Wednesday);
        assert_eq!(day_of_week(b" Sun,").unwrap().1, Day::Sunday);

        assert_eq!(day(b"31 ").unwrap().1, 31);
        assert_eq!(day(b"9 ").unwrap().1, 9);
        assert_eq!(day(b"05 ").unwrap().1, 5);
        assert_eq!(day(b"23 ").unwrap().1, 23);
    }

    #[test]
    fn test_month_and_year() {
        assert_eq!(month(b"Apr ").unwrap().1, Month::April);
        assert_eq!(month(b"may ").unwrap().1, Month::May);
        assert_eq!(month(b"deC ").unwrap().1, Month::December);

        assert_eq!(year(b" 2020 ").unwrap().1, 2020);
        assert_eq!(year(b"\r\n 1995 ").unwrap().1, 1995);
        assert_eq!(year(b" 250032 ").unwrap().1, 250032);
    }

    #[test]
    fn test_date() {
        assert_eq!(
            date(b"1 nov 2020 ").unwrap().1,
            Date {
                day: 1,
                month: Month::November,
                year: 2020
            }
        );
        assert_eq!(
            date(b"25 dec 2038 ").unwrap().1,
            Date {
                day: 25,
                month: Month::December,
                year: 2038
            }
        );

        assert_eq!(
            date_time(b"Mon, 12 Apr 2023 10:25:03 +0000").unwrap().1,
            DateTime {
                day_name: Some(Day::Monday),
                date: Date {
                    day: 12,
                    month: Month::April,
                    year: 2023
                },
                time: TimeWithZone {
                    time: Time {
                        hour: 10,
                        minute: 25,
                        second: 3
                    },
                    zone: Zone {
                        sign: true,
                        hour_offset: 0,
                        minute_offset: 0
                    }
                },
            }
        );
        assert_eq!(
            date_time(b"5 May 2003 18:59:03 +0000").unwrap().1,
            DateTime {
                day_name: None,
                date: Date {
                    day: 5,
                    month: Month::May,
                    year: 2003
                },
                time: TimeWithZone {
                    time: Time {
                        hour: 18,
                        minute: 59,
                        second: 3
                    },
                    zone: Zone {
                        sign: true,
                        hour_offset: 0,
                        minute_offset: 0
                    }
                }
            }
        );
    }

    #[test]
    fn test_time() {
        assert_eq!(
            time_of_day(b"10:40:29").unwrap().1,
            Time {
                hour: 10,
                minute: 40,
                second: 29
            }
        );
        assert_eq!(
            time_of_day(b"10:40 ").unwrap().1,
            Time {
                hour: 10,
                minute: 40,
                second: 0
            }
        );
        assert_eq!(
            time_of_day(b"05:23 ").unwrap().1,
            Time {
                hour: 5,
                minute: 23,
                second: 0
            }
        );

        assert_eq!(
            zone(b" +1000 ").unwrap().1,
            Zone {
                sign: true,
                hour_offset: 10,
                minute_offset: 0
            }
        );
        assert_eq!(
            zone(b" -0523 ").unwrap().1,
            Zone {
                sign: false,
                hour_offset: 5,
                minute_offset: 23
            }
        );

        assert_eq!(
            time(b"06:44 +0100").unwrap().1,
            TimeWithZone {
                time: Time {
                    hour: 6,
                    minute: 44,
                    second: 0
                },
                zone: Zone {
                    sign: true,
                    hour_offset: 1,
                    minute_offset: 0
                }
            }
        );
        assert_eq!(
            time(b"23:57 +0000").unwrap().1,
            TimeWithZone {
                time: Time {
                    hour: 23,
                    minute: 57,
                    second: 0
                },
                zone: Zone {
                    sign: true,
                    hour_offset: 0,
                    minute_offset: 0
                }
            }
        );
        assert_eq!(
            time(b"08:23:02 -0500").unwrap().1,
            TimeWithZone {
                time: Time {
                    hour: 8,
                    minute: 23,
                    second: 2
                },
                zone: Zone {
                    sign: false,
                    hour_offset: 5,
                    minute_offset: 0
                }
            }
        );
    }
}