Skip to content

Commit da4a9d9

Browse files
committed
v0.4.5
1 parent c3b36dd commit da4a9d9

File tree

5 files changed

+177
-6
lines changed

5 files changed

+177
-6
lines changed

CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
mail-parser 0.4.5
2+
================================
3+
- DateTime to UNIX timestamp conversion.
4+
- Ord, PartialOrd support for DateTime (#13).
5+
- Fixed Message::parse() panic on duplicate Content-Type headers (#14).
6+
17
mail-parser 0.4.4
28
================================
39
- Support for multi-line headers.

Cargo.lock

+79-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
name = "mail-parser"
33
description = "Fast and robust e-mail parsing library for Rust"
4-
version = "0.4.4"
4+
version = "0.4.5"
55
edition = "2018"
66
authors = [ "Stalwart Labs <hello@stalw.art>"]
77
license = "Apache-2.0 OR MIT"
@@ -19,6 +19,7 @@ serde = { version = "1.0", features = ["derive"], optional=true}
1919
serde_yaml = "0.8"
2020
serde_json = "1.0"
2121
bincode = "1.3.3"
22+
chrono = "0.4"
2223

2324
[features]
2425
default = ["serde_support", "full_encoding"]

src/lib.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -779,7 +779,7 @@ pub struct ContentType<'x> {
779779
}
780780

781781
/// An RFC5322 datetime.
782-
#[derive(Debug, PartialEq)]
782+
#[derive(Debug, PartialEq, Eq)]
783783
#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
784784
pub struct DateTime {
785785
pub year: u32,

src/parsers/fields/date.rs

+89-3
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,64 @@ impl DateTime {
2929
self.tz_minute
3030
)
3131
}
32+
33+
/// Returns true if the date is vald
34+
pub fn is_valid(&self) -> bool {
35+
(0..23).contains(&self.tz_hour)
36+
&& (0..59).contains(&self.tz_minute)
37+
&& (1970..2500).contains(&self.year)
38+
&& (1..12).contains(&self.month)
39+
&& (1..31).contains(&self.day)
40+
&& (0..23).contains(&self.hour)
41+
&& (0..59).contains(&self.minute)
42+
&& (0..59).contains(&self.second)
43+
}
44+
45+
/// Returns the numbers of seconds since 1970-01-01T00:00:00Z (Unix epoch)
46+
/// or None if the date is invalid.
47+
pub fn to_timestamp(&self) -> Option<i64> {
48+
// Ported from https://github.com/protocolbuffers/upb/blob/22182e6e/upb/json_decode.c#L982-L992
49+
if self.is_valid() {
50+
let year_base = 4800; /* Before min year, multiple of 400. */
51+
let m_adj = self.month.overflowing_sub(3).0; /* March-based month. */
52+
let carry = if m_adj > self.month { 1 } else { 0 };
53+
let adjust = if carry > 0 { 12 } else { 0 };
54+
let y_adj = self.year as i64 + year_base - carry;
55+
let month_days = ((m_adj.overflowing_add(adjust).0) * 62719 + 769) / 2048;
56+
let leap_days = y_adj / 4 - y_adj / 100 + y_adj / 400;
57+
((y_adj * 365 + leap_days + month_days as i64 + (self.day as i64 - 1) - 2472632)
58+
* 86400
59+
+ self.hour as i64 * 3600
60+
+ self.minute as i64 * 60
61+
+ self.second as i64
62+
+ ((self.tz_hour as i64 * 3600 + self.tz_minute as i64 * 60)
63+
* if self.tz_before_gmt { 1 } else { -1 }))
64+
.into()
65+
} else {
66+
None
67+
}
68+
}
69+
}
70+
71+
impl PartialOrd for DateTime {
72+
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
73+
match self.to_timestamp()? - other.to_timestamp()? {
74+
0 => std::cmp::Ordering::Equal,
75+
x if x > 0 => std::cmp::Ordering::Greater,
76+
_ => std::cmp::Ordering::Less,
77+
}
78+
.into()
79+
}
80+
}
81+
82+
impl Ord for DateTime {
83+
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
84+
match self.to_timestamp().unwrap_or_default() - other.to_timestamp().unwrap_or_default() {
85+
0 => std::cmp::Ordering::Equal,
86+
x if x > 0 => std::cmp::Ordering::Greater,
87+
_ => std::cmp::Ordering::Less,
88+
}
89+
}
3290
}
3391

3492
impl fmt::Display for DateTime {
@@ -195,6 +253,8 @@ pub static MONTH_MAP: &[u8; 31] = &[
195253

196254
#[cfg(test)]
197255
mod tests {
256+
use chrono::{FixedOffset, LocalResult, SecondsFormat, TimeZone, Utc};
257+
198258
use crate::{
199259
parsers::{fields::date::parse_date, message::MessageStream},
200260
HeaderValue,
@@ -265,9 +325,35 @@ mod tests {
265325
for input in inputs {
266326
let str = input.0.to_string();
267327
match parse_date(&mut MessageStream::new(str.as_bytes())) {
268-
HeaderValue::DateTime(date) => {
269-
//println!("{} -> {}", input.0.escape_debug(), date.to_iso8601());
270-
assert_eq!(input.1, date.to_iso8601());
328+
HeaderValue::DateTime(datetime) => {
329+
assert_eq!(input.1, datetime.to_iso8601());
330+
331+
if datetime.is_valid() {
332+
if let LocalResult::Single(chrono_datetime)
333+
| LocalResult::Ambiguous(chrono_datetime, _) = FixedOffset::west_opt(
334+
((datetime.tz_hour as i32 * 3600i32) + datetime.tz_minute as i32 * 60)
335+
* if datetime.tz_before_gmt { 1i32 } else { -1i32 },
336+
)
337+
.unwrap_or_else(|| FixedOffset::east(0))
338+
.ymd_opt(datetime.year as i32, datetime.month, datetime.day)
339+
.and_hms_opt(datetime.hour, datetime.minute, datetime.second)
340+
{
341+
assert_eq!(
342+
chrono_datetime.timestamp(),
343+
datetime.to_timestamp().unwrap(),
344+
"{} -> {} ({}) -> {} ({})",
345+
input.0.escape_debug(),
346+
datetime.to_timestamp().unwrap(),
347+
Utc.timestamp_opt(datetime.to_timestamp().unwrap(), 0)
348+
.unwrap()
349+
.to_rfc3339_opts(SecondsFormat::Secs, true),
350+
chrono_datetime.timestamp(),
351+
Utc.timestamp_opt(chrono_datetime.timestamp(), 0)
352+
.unwrap()
353+
.to_rfc3339_opts(SecondsFormat::Secs, true)
354+
);
355+
}
356+
}
271357
}
272358
HeaderValue::Empty => {
273359
//println!("{} -> None", input.0.escape_debug());

0 commit comments

Comments
 (0)