mailparse/
dateparse.rs

1use crate::MailParseError;
2
3enum DateParseState {
4    Date,
5    Month,
6    Year,
7    Hour,
8    Minute,
9    Second,
10    Timezone,
11}
12
13fn days_in_month(month: i64, year: i64) -> i64 {
14    match month {
15        0 | 2 | 4 | 6 | 7 | 9 | 11 => 31,
16        3 | 5 | 8 | 10 => 30,
17        1 => {
18            if (year % 400) == 0 {
19                29
20            } else if (year % 100) == 0 {
21                28
22            } else if (year % 4) == 0 {
23                29
24            } else {
25                28
26            }
27        }
28        _ => 0,
29    }
30}
31
32fn seconds_to_date(year: i64, month: i64, day: i64) -> i64 {
33    let mut result: i64 = 0;
34    for y in 1970..2001 {
35        if y == year {
36            break;
37        }
38        result += 86400 * 365;
39        if (y % 4) == 0 {
40            result += 86400;
41        }
42    }
43    let mut y = 2001;
44    while y < year {
45        if year - y >= 400 {
46            result += (86400 * 365 * 400) + (86400 * 97);
47            y += 400;
48            continue;
49        }
50        if year - y >= 100 {
51            result += (86400 * 365 * 100) + (86400 * 24);
52            y += 100;
53            continue;
54        }
55        if year - y >= 4 {
56            result += (86400 * 365 * 4) + (86400);
57            y += 4;
58            continue;
59        }
60        result += 86400 * 365;
61        y += 1;
62    }
63    for m in 0..month {
64        result += 86400 * days_in_month(m, year)
65    }
66    result + 86400 * (day - 1)
67}
68
69/// Convert a date field from an email header into a UNIX epoch timestamp.
70/// This function handles the most common formatting of date fields found in
71/// email headers. It may fail to parse some of the more creative formattings.
72///
73/// # Examples
74/// ```
75///     use mailparse::dateparse;
76///     assert_eq!(dateparse("Sun, 02 Oct 2016 07:06:22 -0700 (PDT)").unwrap(), 1475417182);
77/// ```
78pub fn dateparse(date: &str) -> Result<i64, MailParseError> {
79    let mut result = 0;
80    let mut month = 0;
81    let mut day_of_month = 0;
82    let mut state = DateParseState::Date;
83    for tok in date.split(|c| c == ' ' || c == ':') {
84        if tok.is_empty() {
85            continue;
86        }
87        match state {
88            DateParseState::Date => {
89                if let Ok(v) = tok.parse::<u8>() {
90                    if !(1..=31).contains(&v) {
91                        return Err(MailParseError::Generic("Invalid day"));
92                    }
93                    day_of_month = v;
94                    state = DateParseState::Month;
95                };
96                continue;
97            }
98            DateParseState::Month => {
99                month = match tok.to_uppercase().as_str() {
100                    "JAN" | "JANUARY" => 0,
101                    "FEB" | "FEBRUARY" => 1,
102                    "MAR" | "MARCH" => 2,
103                    "APR" | "APRIL" => 3,
104                    "MAY" => 4,
105                    "JUN" | "JUNE" => 5,
106                    "JUL" | "JULY" => 6,
107                    "AUG" | "AUGUST" => 7,
108                    "SEP" | "SEPTEMBER" => 8,
109                    "OCT" | "OCTOBER" => 9,
110                    "NOV" | "NOVEMBER" => 10,
111                    "DEC" | "DECEMBER" => 11,
112                    _ => return Err(MailParseError::Generic("Unrecognized month")),
113                };
114                state = DateParseState::Year;
115                continue;
116            }
117            DateParseState::Year => {
118                let year = match tok.parse::<u32>() {
119                    Ok(v) if v < 70 => 2000 + v,
120                    Ok(v) if v < 100 => 1900 + v,
121                    Ok(v) if v < 1970 => return Err(MailParseError::Generic("Disallowed year")),
122                    Ok(v) => v,
123                    Err(_) => return Err(MailParseError::Generic("Invalid year")),
124                };
125                result =
126                    seconds_to_date(i64::from(year), i64::from(month), i64::from(day_of_month));
127                state = DateParseState::Hour;
128                continue;
129            }
130            DateParseState::Hour => {
131                let hour = match tok.parse::<u8>() {
132                    Ok(v) => v,
133                    Err(_) => return Err(MailParseError::Generic("Invalid hour")),
134                };
135                result += 3600 * i64::from(hour);
136                state = DateParseState::Minute;
137                continue;
138            }
139            DateParseState::Minute => {
140                let minute = match tok.parse::<u8>() {
141                    Ok(v) => v,
142                    Err(_) => return Err(MailParseError::Generic("Invalid minute")),
143                };
144                result += 60 * i64::from(minute);
145                state = DateParseState::Second;
146                continue;
147            }
148            DateParseState::Second => {
149                let second = match tok.parse::<u8>() {
150                    Ok(v) => v,
151                    Err(_) => return Err(MailParseError::Generic("Invalid second")),
152                };
153                result += i64::from(second);
154                state = DateParseState::Timezone;
155                continue;
156            }
157            DateParseState::Timezone => {
158                let (tz, tz_sign) = match tok.parse::<i32>() {
159                    Ok(v) if !(-2400..=2400).contains(&v) => {
160                        return Err(MailParseError::Generic("Invalid timezone"))
161                    }
162                    Ok(v) if v < 0 => (-v, -1),
163                    Ok(v) => (v, 1),
164                    Err(_) => {
165                        match tok.to_uppercase().as_str() {
166                            // This list taken from IETF RFC 822
167                            "UTC" | "UT" | "GMT" | "Z" => (0, 1),
168                            "EDT" => (400, -1),
169                            "EST" | "CDT" => (500, -1),
170                            "CST" | "MDT" => (600, -1),
171                            "MST" | "PDT" => (700, -1),
172                            "PST" => (800, -1),
173                            "A" => (100, -1),
174                            "M" => (1200, -1),
175                            "N" => (100, 1),
176                            "Y" => (1200, 1),
177                            _ => return Err(MailParseError::Generic("Invalid timezone")),
178                        }
179                    }
180                };
181                let tz_hours = tz / 100;
182                let tz_mins = tz % 100;
183                let tz_delta = (tz_hours * 3600) + (tz_mins * 60);
184                if tz_sign < 0 {
185                    result += i64::from(tz_delta);
186                } else {
187                    result -= i64::from(tz_delta);
188                }
189                break;
190            }
191        }
192    }
193    Ok(result)
194}
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199
200    #[test]
201    fn parse_dates() {
202        assert_eq!(
203            dateparse("Sun, 25 Sep 2016 18:36:33 -0400").unwrap(),
204            1474842993
205        );
206        assert_eq!(
207            dateparse("Fri, 01 Jan 2100 11:12:13 +0000").unwrap(),
208            4102485133
209        );
210        assert_eq!(
211            dateparse("Fri, 31 Dec 2100 00:00:00 +0000").unwrap(),
212            4133894400
213        );
214        assert_eq!(
215            dateparse("Fri, 31 Dec 2399 00:00:00 +0000").unwrap(),
216            13569379200
217        );
218        assert_eq!(
219            dateparse("Fri, 31 Dec 2400 00:00:00 +0000").unwrap(),
220            13601001600
221        );
222        assert_eq!(dateparse("17 Sep 2016 16:05:38 -1000").unwrap(), 1474164338);
223        assert_eq!(
224            dateparse("Fri, 30 Nov 2012 20:57:23 GMT").unwrap(),
225            1354309043
226        );
227
228        // Day cannot be zero.
229        assert!(dateparse("Wed, 0 Jan 1970 00:00:00 +0000").is_err());
230
231        // Regression test for integer overflow on invalid timezone.
232        assert!(dateparse("Thu, 1 Jan 1970 00:00:00 +2147483647").is_err());
233    }
234}