use super::{address, get_header_value};
use crate::types::DateTime;
pub(crate) fn extract_date(headers: &[(String, String)]) -> Option<DateTime> {
get_header_value(headers, "date").and_then(|v| parse_rfc5322_date(&v))
}
pub(crate) fn parse_rfc5322_date(input: &str) -> Option<DateTime> {
let input = address::strip_comments(input);
let input = input.replace("\r\n ", " ").replace("\r\n\t", " ");
let input = input.trim();
let (day_of_week, input) = if let Some(comma_pos) = input.find(',') {
let before_comma = input[..comma_pos].trim();
if let Some(day_of_week) = parse_day_name(before_comma) {
(Some(day_of_week), input[comma_pos + 1..].trim())
} else {
(None, input)
}
} else {
(None, input)
};
let parts: Vec<&str> = input.split_whitespace().collect();
if parts.len() < 4 {
return None;
}
let day: u8 = parts[0].parse().ok()?;
let month_token = parts[1].trim_end_matches(|c: char| !c.is_ascii_alphabetic());
let month = parse_month_name(month_token)?;
let year: u16 = parse_year(parts[2])?;
let mut time_string = String::new();
let mut tz_index = parts.len(); for (i, &part) in parts.iter().enumerate().skip(3) {
if part
.bytes()
.all(|b| b.is_ascii_digit() || b == b':' || b == b'.')
{
time_string.push_str(part);
} else {
tz_index = i;
break;
}
}
let time_parts: Vec<&str> = time_string.split(':').collect();
if time_parts.len() < 2 {
return None;
}
let hour: u8 = time_parts[0].parse().ok()?;
let minute: u8 = time_parts[1].parse().ok()?;
let second: u8 = time_parts
.get(2)
.and_then(|s| {
let int_part = s.split('.').next().unwrap_or(s);
int_part.parse().ok()
})
.unwrap_or(0);
if day == 0 || day > 31 || hour > 23 || minute > 59 || second > 60 {
return None;
}
let _ = day_of_week;
let tz_offset_minutes = parts.get(tz_index).map_or(0, |tz| parse_timezone(tz));
Some(DateTime {
year,
month,
day,
hour,
minute,
second,
tz_offset_minutes,
})
}
fn parse_day_name(s: &str) -> Option<u8> {
match s.to_ascii_lowercase().as_str() {
"sun" => Some(0),
"mon" => Some(1),
"tue" => Some(2),
"wed" => Some(3),
"thu" => Some(4),
"fri" => Some(5),
"sat" => Some(6),
_ => None,
}
}
fn parse_month_name(s: &str) -> Option<u8> {
match s.to_ascii_lowercase().as_str() {
"jan" => Some(1),
"feb" => Some(2),
"mar" => Some(3),
"apr" => Some(4),
"may" => Some(5),
"jun" => Some(6),
"jul" => Some(7),
"aug" => Some(8),
"sep" => Some(9),
"oct" => Some(10),
"nov" => Some(11),
"dec" => Some(12),
_ => None,
}
}
pub(crate) fn parse_year(s: &str) -> Option<u16> {
let y: u16 = s.parse().ok()?;
let digit_count = s.len();
if digit_count <= 2 {
Some(if y >= 50 { 1900 + y } else { 2000 + y })
} else if digit_count == 3 {
Some(1900 + y)
} else {
Some(y)
}
}
pub(crate) fn parse_timezone(s: &str) -> i16 {
let s = s.trim();
if (s.starts_with('+') || s.starts_with('-')) && s.len() == 5 {
let sign: i16 = if s.starts_with('-') { -1 } else { 1 };
if let (Some(h_str), Some(m_str)) = (s.get(1..3), s.get(3..5)) {
if let (Ok(h), Ok(m)) = (h_str.parse::<i16>(), m_str.parse::<i16>()) {
if h <= 23 && m <= 59 {
return sign * (h * 60 + m);
}
}
}
}
#[allow(clippy::match_same_arms)]
match s.to_ascii_uppercase().as_str() {
"UT" | "GMT" | "UTC" => 0,
"Z" => 0,
"EDT" => -240,
"EST" | "CDT" => -300,
"CST" | "MDT" => -360,
"MST" | "PDT" => -420,
"PST" => -480,
_ => 0,
}
}