#![allow(clippy::unwrap_used)]
use super::*;
#[test]
fn address_display_bare_email() {
let addr = Address {
name: None,
email: "user@example.com".into(),
};
assert_eq!(addr.to_string(), "user@example.com");
}
#[test]
fn address_display_with_name() {
let addr = Address {
name: Some("John Doe".into()),
email: "john@example.com".into(),
};
assert_eq!(addr.to_string(), "John Doe <john@example.com>");
}
#[test]
fn address_display_name_with_specials_quoted() {
let addr = Address {
name: Some("Doe, John".into()),
email: "john@example.com".into(),
};
assert_eq!(addr.to_string(), "\"Doe, John\" <john@example.com>");
}
#[test]
fn address_display_name_with_quotes_escaped() {
let addr = Address {
name: Some("John \"Doc\" Doe".into()),
email: "john@example.com".into(),
};
assert_eq!(
addr.to_string(),
"\"John \\\"Doc\\\" Doe\" <john@example.com>"
);
}
#[test]
fn address_from_str_bare_email() {
let addr: Address = "user@example.com".parse().unwrap();
assert_eq!(addr.email, "user@example.com");
assert!(addr.name.is_none());
}
#[test]
fn address_from_str_with_name() {
let addr: Address = "John Doe <john@example.com>".parse().unwrap();
assert_eq!(addr.email, "john@example.com");
assert_eq!(addr.name.as_deref(), Some("John Doe"));
}
#[test]
fn address_from_str_quoted_name() {
let addr: Address = "\"Doe, John\" <john@example.com>".parse().unwrap();
assert_eq!(addr.email, "john@example.com");
assert_eq!(addr.name.as_deref(), Some("Doe, John"));
}
#[test]
fn address_from_str_escaped_quotes_in_name() {
let addr: Address = "\"John \\\"Doc\\\" Doe\" <john@example.com>"
.parse()
.unwrap();
assert_eq!(addr.email, "john@example.com");
assert_eq!(addr.name.as_deref(), Some("John \"Doc\" Doe"));
}
#[test]
fn address_from_str_empty_rejected() {
let result: Result<Address, _> = "".parse();
assert!(result.is_err());
}
#[test]
fn address_from_str_no_at_rejected() {
let result: Result<Address, _> = "not-an-email".parse();
assert!(result.is_err());
}
#[test]
fn address_round_trip_display_from_str() {
let original = Address {
name: Some("Doe, John".into()),
email: "john@example.com".into(),
};
let displayed = original.to_string();
let parsed: Address = displayed.parse().unwrap();
assert_eq!(original, parsed);
}
#[test]
fn address_display_quotes_encoded_word_syntax() {
let addr = Address {
name: Some("=?UTF-8?B?SGVsbG8=?=".into()),
email: "user@example.com".into(),
};
let formatted = addr.to_string();
assert!(
formatted.starts_with('"'),
"display name containing =? must be quoted, got: {formatted}"
);
assert_eq!(formatted, "\"=?UTF-8?B?SGVsbG8=?=\" <user@example.com>");
}
#[test]
fn address_round_trip_encoded_word_syntax_preserved() {
let original = Address {
name: Some("=?UTF-8?B?SGVsbG8=?=".into()),
email: "user@example.com".into(),
};
let displayed = original.to_string();
let parsed: Address = displayed.parse().unwrap();
assert_eq!(original, parsed);
}
#[test]
fn datetime_now_returns_plausible_date() {
let now = DateTime::now();
assert!(now.year >= 2025, "DateTime::now() year is {}", now.year);
assert!((1..=12).contains(&now.month));
assert!((1..=31).contains(&now.day));
assert!(now.hour <= 23);
assert!(now.minute <= 59);
assert!(now.second <= 60);
assert_eq!(now.tz_offset_minutes, 0, "now() should return UTC");
}
#[test]
fn datetime_weekday_known_dates() {
let dt = DateTime {
year: 2025,
month: 2,
day: 13,
hour: 0,
minute: 0,
second: 0,
tz_offset_minutes: 0,
};
assert_eq!(dt.weekday(), 4, "2025-02-13 should be Thursday (4)");
let epoch = DateTime::from_unix_timestamp(0, 0);
assert_eq!(epoch.weekday(), 4, "1970-01-01 should be Thursday (4)");
let sunday = DateTime {
year: 2025,
month: 3,
day: 16,
hour: 0,
minute: 0,
second: 0,
tz_offset_minutes: 0,
};
assert_eq!(sunday.weekday(), 0, "2025-03-16 should be Sunday (0)");
}
#[test]
fn datetime_eq_consistent_with_ord() {
let utc = DateTime {
year: 2025,
month: 1,
day: 15,
hour: 12,
minute: 0,
second: 0,
tz_offset_minutes: 0,
};
let plus_five = DateTime {
year: 2025,
month: 1,
day: 15,
hour: 17,
minute: 0,
second: 0,
tz_offset_minutes: 300, };
assert_eq!(utc.to_unix_timestamp(), plus_five.to_unix_timestamp());
assert_eq!(
utc.cmp(&plus_five),
std::cmp::Ordering::Equal,
"cmp should consider same-UTC-instant values equal"
);
assert_eq!(
utc, plus_five,
"PartialEq must agree with Ord: same UTC instant should be =="
);
}
#[test]
fn datetime_hash_consistent_with_eq() {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
fn hash_of(dt: &DateTime) -> u64 {
let mut hasher = DefaultHasher::new();
dt.hash(&mut hasher);
hasher.finish()
}
let utc = DateTime {
year: 2025,
month: 1,
day: 15,
hour: 12,
minute: 0,
second: 0,
tz_offset_minutes: 0,
};
let plus_five = DateTime {
year: 2025,
month: 1,
day: 15,
hour: 17,
minute: 0,
second: 0,
tz_offset_minutes: 300,
};
assert_eq!(utc, plus_five);
assert_eq!(
hash_of(&utc),
hash_of(&plus_five),
"Hash must be consistent with Eq: same UTC instant must hash the same"
);
}
#[test]
fn datetime_to_rfc5322_string_utc() {
let dt = DateTime {
year: 2025,
month: 2,
day: 13,
hour: 15,
minute: 47,
second: 33,
tz_offset_minutes: 0,
};
assert_eq!(dt.to_rfc5322_string(), "Thu, 13 Feb 2025 15:47:33 +0000");
}
#[test]
fn datetime_to_rfc5322_string_positive_offset() {
let dt = DateTime {
year: 2025,
month: 2,
day: 13,
hour: 21,
minute: 17,
second: 33,
tz_offset_minutes: 330, };
assert_eq!(dt.to_rfc5322_string(), "Thu, 13 Feb 2025 21:17:33 +0530");
}
#[test]
fn datetime_to_rfc5322_string_negative_offset() {
let dt = DateTime {
year: 2025,
month: 3,
day: 16,
hour: 9,
minute: 0,
second: 0,
tz_offset_minutes: -480, };
assert_eq!(dt.to_rfc5322_string(), "Sun, 16 Mar 2025 09:00:00 -0800");
}
#[test]
fn datetime_parse_rfc5322_basic() {
let dt = DateTime::parse_rfc5322("Thu, 13 Feb 2025 15:47:33 +0000").unwrap();
assert_eq!(dt.year, 2025);
assert_eq!(dt.month, 2);
assert_eq!(dt.day, 13);
assert_eq!(dt.hour, 15);
assert_eq!(dt.minute, 47);
assert_eq!(dt.second, 33);
assert_eq!(dt.tz_offset_minutes, 0);
}
#[test]
fn datetime_parse_rfc5322_with_offset() {
let dt = DateTime::parse_rfc5322("Fri, 14 Feb 2025 09:15:00 -0800").unwrap();
assert_eq!(dt.year, 2025);
assert_eq!(dt.tz_offset_minutes, -480);
}
#[test]
fn datetime_parse_rfc5322_round_trip() {
let original = DateTime {
year: 2025,
month: 12,
day: 25,
hour: 0,
minute: 0,
second: 0,
tz_offset_minutes: 330,
};
let s = original.to_rfc5322_string();
let parsed = DateTime::parse_rfc5322(&s).unwrap();
assert_eq!(original, parsed);
}
#[test]
fn datetime_parse_rfc5322_invalid() {
assert!(DateTime::parse_rfc5322("not a date").is_none());
assert!(DateTime::parse_rfc5322("").is_none());
}
#[test]
fn datetime_to_iso8601_string_utc() {
let dt = DateTime {
year: 2025,
month: 2,
day: 13,
hour: 15,
minute: 47,
second: 33,
tz_offset_minutes: 0,
};
assert_eq!(dt.to_iso8601_string(), "2025-02-13T15:47:33+00:00");
}
#[test]
fn datetime_to_iso8601_string_positive_offset() {
let dt = DateTime {
year: 2025,
month: 2,
day: 13,
hour: 21,
minute: 17,
second: 33,
tz_offset_minutes: 330, };
assert_eq!(dt.to_iso8601_string(), "2025-02-13T21:17:33+05:30");
}
#[test]
fn datetime_to_iso8601_string_negative_offset() {
let dt = DateTime {
year: 2025,
month: 3,
day: 16,
hour: 9,
minute: 0,
second: 0,
tz_offset_minutes: -480, };
assert_eq!(dt.to_iso8601_string(), "2025-03-16T09:00:00-08:00");
}
#[test]
fn address_display_non_ascii_is_rfc2047_encoded() {
let addr = Address {
name: Some("José García".into()),
email: "jose@example.com".into(),
};
let displayed = addr.to_string();
assert!(
displayed.is_ascii(),
"Display output must be pure ASCII, got: {displayed}"
);
assert!(
displayed.contains("=?UTF-8?B?"),
"Non-ASCII name must be RFC 2047 encoded, got: {displayed}"
);
assert!(
displayed.contains("<jose@example.com>"),
"Email must appear in angle brackets, got: {displayed}"
);
}
#[test]
fn address_display_ascii_name_unchanged() {
let addr = Address {
name: Some("John Doe".into()),
email: "john@example.com".into(),
};
let displayed = addr.to_string();
assert_eq!(displayed, "John Doe <john@example.com>");
assert!(
!displayed.contains("=?"),
"ASCII name should not be RFC 2047 encoded, got: {displayed}"
);
}
#[test]
fn address_display_non_ascii_round_trip() {
let original = Address {
name: Some("José García".into()),
email: "jose@example.com".into(),
};
let displayed = original.to_string();
let parsed: Address = displayed.parse().unwrap();
assert_eq!(
original, parsed,
"Round-trip failed: displayed as '{displayed}', parsed name = {:?}",
parsed.name
);
}
#[test]
fn datetime_invalid_month_no_panic() {
let dt_zero = DateTime {
year: 2025,
month: 0,
day: 15,
hour: 12,
minute: 0,
second: 0,
tz_offset_minutes: 0,
};
let _ = dt_zero.to_rfc5322_string();
let _ = dt_zero.weekday();
let dt_thirteen = DateTime {
year: 2025,
month: 13,
day: 15,
hour: 12,
minute: 0,
second: 0,
tz_offset_minutes: 0,
};
let _ = dt_thirteen.to_rfc5322_string();
let _ = dt_thirteen.weekday();
let dt_max = DateTime {
year: 2025,
month: 255,
day: 15,
hour: 12,
minute: 0,
second: 0,
tz_offset_minutes: 0,
};
let _ = dt_max.to_rfc5322_string();
let _ = dt_max.weekday();
}
#[test]
fn datetime_from_str_basic() {
let dt: DateTime = "Thu, 13 Feb 2025 15:47:33 +0000".parse().unwrap();
assert_eq!(dt.year, 2025);
assert_eq!(dt.month, 2);
assert_eq!(dt.day, 13);
assert_eq!(dt.hour, 15);
assert_eq!(dt.minute, 47);
assert_eq!(dt.second, 33);
assert_eq!(dt.tz_offset_minutes, 0);
}
#[test]
fn datetime_from_str_with_offset() {
let dt: DateTime = "Fri, 14 Feb 2025 09:15:00 -0800".parse().unwrap();
assert_eq!(dt.year, 2025);
assert_eq!(dt.tz_offset_minutes, -480);
}
#[test]
fn datetime_from_str_invalid() {
let result: Result<DateTime, _> = "not a date".parse();
assert!(result.is_err());
}
#[test]
fn datetime_from_str_empty() {
let result: Result<DateTime, _> = "".parse();
assert!(result.is_err());
}
#[test]
fn datetime_from_str_round_trip() {
let original = DateTime {
year: 2025,
month: 7,
day: 4,
hour: 12,
minute: 30,
second: 0,
tz_offset_minutes: -300,
};
let s = original.to_rfc5322_string();
let parsed: DateTime = s.parse().unwrap();
assert_eq!(original, parsed);
}
#[test]
fn datetime_display_utc() {
let dt = DateTime {
year: 2025,
month: 2,
day: 13,
hour: 15,
minute: 47,
second: 33,
tz_offset_minutes: 0,
};
assert_eq!(dt.to_string(), "2025-02-13 15:47:33 +0000");
}
#[test]
fn datetime_display_positive_offset() {
let dt = DateTime {
year: 2025,
month: 2,
day: 13,
hour: 21,
minute: 17,
second: 33,
tz_offset_minutes: 330, };
assert_eq!(dt.to_string(), "2025-02-13 21:17:33 +0530");
}
#[test]
fn datetime_display_negative_offset() {
let dt = DateTime {
year: 2025,
month: 3,
day: 16,
hour: 9,
minute: 0,
second: 0,
tz_offset_minutes: -480, };
assert_eq!(dt.to_string(), "2025-03-16 09:00:00 -0800");
}
#[test]
fn datetime_display_non_half_hour_offset() {
let dt = DateTime {
year: 2025,
month: 6,
day: 1,
hour: 12,
minute: 0,
second: 0,
tz_offset_minutes: 345, };
assert_eq!(dt.to_string(), "2025-06-01 12:00:00 +0545");
}
#[test]
fn datetime_display_extreme_offset() {
let dt = DateTime {
year: 2025,
month: 1,
day: 1,
hour: 0,
minute: 0,
second: 0,
tz_offset_minutes: 720, };
assert_eq!(dt.to_string(), "2025-01-01 00:00:00 +1200");
let dt_neg = DateTime {
year: 2025,
month: 1,
day: 1,
hour: 0,
minute: 0,
second: 0,
tz_offset_minutes: -720, };
assert_eq!(dt_neg.to_string(), "2025-01-01 00:00:00 -1200");
}
#[test]
fn address_from_str_empty_angle_brackets_rejected() {
let result: Result<Address, _> = "<>".parse();
let err = result.unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("empty email in angle brackets"),
"expected 'empty email in angle brackets' error, got: {msg}"
);
}
#[test]
fn address_from_str_angle_brackets_no_name() {
let addr: Address = "<user@example.com>".parse().unwrap();
assert_eq!(addr.email, "user@example.com");
assert!(
addr.name.is_none(),
"expected name to be None for bare angle-bracket address, got: {:?}",
addr.name
);
}
#[test]
fn address_from_str_quoted_empty_name_is_none() {
let addr: Address = "\"\" <user@example.com>".parse().unwrap();
assert_eq!(addr.email, "user@example.com");
assert!(
addr.name.is_none(),
"expected name to be None for quoted empty name, got: {:?}",
addr.name
);
}
#[test]
fn address_from_str_whitespace_only_name_is_none() {
let addr: Address = " <user@example.com>".parse().unwrap();
assert_eq!(addr.email, "user@example.com");
assert!(
addr.name.is_none(),
"expected name to be None for whitespace-only prefix, got: {:?}",
addr.name
);
}
#[test]
fn address_from_str_unclosed_angle_bracket() {
let result: Result<Address, _> = "<user@example.com".parse();
if let Ok(addr) = result {
assert!(addr.name.is_none());
assert!(addr.email.contains("user@example.com"));
}
}
#[test]
fn address_display_name_with_special_chars() {
let addr = Address {
name: Some("O'Brien \\test".into()),
email: "obrien@example.com".into(),
};
let displayed = addr.to_string();
assert!(
displayed.contains("\\\\"),
"backslash should be escaped in quoted name, got: {displayed}"
);
assert!(
displayed.starts_with('"'),
"name with specials should be quoted, got: {displayed}"
);
assert!(
displayed.contains("<obrien@example.com>"),
"email must appear in angle brackets, got: {displayed}"
);
}
#[test]
fn unescape_trailing_backslash() {
let result = unescape_quoted_string("hello\\");
assert_eq!(
result, "hello\\",
"trailing backslash with no following char should be preserved"
);
}
#[test]
fn address_display_whitespace_only_name_treated_as_absent() {
let addr = Address {
name: Some(" ".to_string()),
email: "test@example.com".into(),
};
assert_eq!(addr.to_string(), "test@example.com");
}
#[test]
fn datetime_leap_second_clamped_in_timestamp() {
let leap = DateTime {
year: 2016,
month: 12,
day: 31,
hour: 23,
minute: 59,
second: 60, tz_offset_minutes: 0,
};
let clamped = DateTime {
year: 2016,
month: 12,
day: 31,
hour: 23,
minute: 59,
second: 59,
tz_offset_minutes: 0,
};
assert_eq!(
leap.to_unix_timestamp(),
clamped.to_unix_timestamp(),
"leap second (60) must clamp to 59 in timestamp calculation"
);
assert_eq!(
leap, clamped,
"leap second DateTime must be equal to the clamped form"
);
}
#[test]
fn datetime_rfc5322_string_clamps_invalid_time_fields() {
let dt = DateTime {
year: 2025,
month: 1,
day: 15,
hour: 25,
minute: 61,
second: 99,
tz_offset_minutes: 0,
};
let s = dt.to_rfc5322_string();
assert!(
s.contains("23:59:60"),
"Invalid time fields must be clamped, got: {s}"
);
}
#[test]
fn datetime_weekday_consistent_with_clamped_month() {
let dt = DateTime {
year: 2025,
month: 0,
day: 15,
hour: 12,
minute: 0,
second: 0,
tz_offset_minutes: 0,
};
let s = dt.to_rfc5322_string();
assert!(
s.starts_with("Wed,"),
"Weekday must match clamped date, got: {s}"
);
}
#[test]
fn datetime_display_clamps_fields() {
let dt = DateTime {
year: 2025,
month: 0,
day: 15,
hour: 99,
minute: 0,
second: 0,
tz_offset_minutes: 0,
};
let display = dt.to_string();
assert!(
display.contains("-01-") && display.contains(" 23:"),
"Display must clamp fields like to_rfc5322_string: {display:?}"
);
}
#[test]
fn datetime_to_unix_timestamp_clamps_hour_minute() {
let dt = DateTime {
year: 2025,
month: 1,
day: 1,
hour: 25,
minute: 0,
second: 0,
tz_offset_minutes: 0,
};
let clamped = DateTime {
year: 2025,
month: 1,
day: 1,
hour: 23,
minute: 0,
second: 0,
tz_offset_minutes: 0,
};
assert_eq!(
dt.to_unix_timestamp(),
clamped.to_unix_timestamp(),
"to_unix_timestamp must clamp hour to 0-23 for consistency with formatters"
);
}
#[test]
fn datetime_from_unix_timestamp_clamps_extreme_years() {
let ancient = DateTime::from_unix_timestamp(-62_200_000_000, 0);
assert!(
ancient.year <= 9999,
"Negative year must be clamped, got {}",
ancient.year
);
assert_eq!(ancient.year, 0, "Years before 0 CE should clamp to year 0");
let distant = DateTime::from_unix_timestamp(253_402_300_800 * 100, 0);
assert!(
distant.year <= 9999,
"Years above 9999 must be clamped, got {}",
distant.year
);
assert_eq!(distant.year, 9999, "Years above 9999 should clamp to 9999");
let ts = 1_700_000_000_i64; let dt = DateTime::from_unix_timestamp(ts, 0);
assert_eq!(
dt.to_unix_timestamp(),
ts,
"Normal timestamps must round-trip"
);
}
#[test]
fn address_from_str_quoted_string_encoded_word_not_decoded() {
let addr: Address = "\"=?UTF-8?B?SGVsbG8=?=\" <user@example.com>"
.parse()
.unwrap();
assert_eq!(addr.email, "user@example.com");
assert_eq!(
addr.name.as_deref(),
Some("=?UTF-8?B?SGVsbG8=?="),
"encoded-word inside quoted-string must be preserved literally \
(RFC 2047 Section 5)"
);
}
#[test]
fn address_from_str_rejects_empty_local_part() {
let result: Result<Address, _> = "@example.com".parse();
assert!(
result.is_err(),
"bare email with empty local-part must be rejected (RFC 5322 Section 3.4.1)"
);
}
#[test]
fn address_from_str_rejects_empty_domain() {
let result: Result<Address, _> = "user@".parse();
assert!(
result.is_err(),
"bare email with empty domain must be rejected (RFC 5322 Section 3.4.1)"
);
}
#[test]
fn address_from_str_rejects_bare_at() {
let result: Result<Address, _> = "@".parse();
assert!(
result.is_err(),
"bare '@' must be rejected (RFC 5322 Section 3.4.1)"
);
}
#[test]
fn address_from_str_rejects_whitespace_in_addr_spec() {
for raw in [
"user @example.com",
"user@ example.com",
"user@example .com",
"Display Name <user @example.com>",
] {
let result: Result<Address, _> = raw.parse();
assert!(
result.is_err(),
"embedded whitespace in addr-spec must be rejected: {raw}"
);
}
}
#[test]
fn address_from_str_rejects_trailing_garbage_after_angle_addr() {
let result: Result<Address, _> = "Alice <user@example.com> garbage".parse();
assert!(
result.is_err(),
"trailing non-CFWS text after angle-addr must be rejected \
(RFC 5322 Section 3.4 / Section 3.2.2)"
);
}
#[test]
fn address_from_str_accepts_trailing_comment_after_angle_addr() {
let addr: Address = "Alice <user@example.com> (Team Inbox)".parse().unwrap();
assert_eq!(addr.email, "user@example.com");
assert_eq!(addr.name.as_deref(), Some("Alice"));
}
#[test]
fn address_from_str_strips_comments_from_name_addr_display_name() {
let addr: Address = "John (Boss) Doe <user@example.com>".parse().unwrap();
assert_eq!(addr.email, "user@example.com");
assert_eq!(addr.name.as_deref(), Some("John Doe"));
}
#[test]
fn address_from_str_unquotes_mixed_phrase_words() {
let addr: Address = "\"John\" Doe <user@example.com>".parse().unwrap();
assert_eq!(addr.email, "user@example.com");
assert_eq!(addr.name.as_deref(), Some("John Doe"));
}
#[test]
fn address_from_str_rejects_trailing_garbage_after_addr_spec_comment() {
let result: Result<Address, _> = "user@example.com (Team Inbox) garbage".parse();
assert!(
result.is_err(),
"trailing non-CFWS text after addr-spec comment must be rejected \
(RFC 5322 Section 3.4.1 / Section 3.2.2)"
);
}
#[test]
fn test_clamped_fields_respects_days_in_month() {
let dt_feb_leap = DateTime {
year: 2024,
month: 2,
day: 31,
hour: 12,
minute: 0,
second: 0,
tz_offset_minutes: 0,
};
let s = dt_feb_leap.to_rfc5322_string();
assert!(
s.contains("29 Feb 2024"),
"Feb 31 in leap year 2024 should clamp to 29 Feb, got: {s}"
);
let dt_feb_non_leap = DateTime {
year: 2023,
month: 2,
day: 31,
hour: 12,
minute: 0,
second: 0,
tz_offset_minutes: 0,
};
let s = dt_feb_non_leap.to_rfc5322_string();
assert!(
s.contains("28 Feb 2023"),
"Feb 31 in non-leap year 2023 should clamp to 28 Feb, got: {s}"
);
let dt_apr = DateTime {
year: 2024,
month: 4,
day: 31,
hour: 12,
minute: 0,
second: 0,
tz_offset_minutes: 0,
};
let s = dt_apr.to_rfc5322_string();
assert!(
s.contains("30 Apr 2024"),
"Apr 31 should clamp to 30 Apr, got: {s}"
);
}
#[test]
fn test_tz_offset_clamped_for_rfc5322_round_trip() {
let dt = DateTime {
year: 2025,
month: 6,
day: 15,
hour: 12,
minute: 0,
second: 0,
tz_offset_minutes: 1500,
};
let formatted = dt.to_rfc5322_string();
let parsed = DateTime::parse_rfc5322(&formatted);
assert!(
parsed.is_some(),
"to_rfc5322_string() with tz_offset_minutes=1500 must produce parseable output, got: {formatted}"
);
let parsed = parsed.unwrap();
assert_eq!(
parsed.tz_offset_minutes, 1439,
"tz_offset_minutes should clamp to 1439, got: {}",
parsed.tz_offset_minutes
);
let dt_neg = DateTime {
year: 2025,
month: 6,
day: 15,
hour: 12,
minute: 0,
second: 0,
tz_offset_minutes: -1500,
};
let formatted_neg = dt_neg.to_rfc5322_string();
let parsed_neg = DateTime::parse_rfc5322(&formatted_neg);
assert!(
parsed_neg.is_some(),
"to_rfc5322_string() with tz_offset_minutes=-1500 must produce parseable output, got: {formatted_neg}"
);
let parsed_neg = parsed_neg.unwrap();
assert_eq!(
parsed_neg.tz_offset_minutes, -1439,
"tz_offset_minutes should clamp to -1439, got: {}",
parsed_neg.tz_offset_minutes
);
}
#[test]
fn test_address_from_str_parenthesized_comment() {
let addr: Address = "user@example.com (John Doe)".parse().unwrap();
assert_eq!(addr.email, "user@example.com");
assert_eq!(addr.name.as_deref(), Some("John Doe"));
}
#[test]
fn test_address_from_str_nested_parenthesized_comment() {
let addr: Address = "user@example.com (John (Jack) Doe)".parse().unwrap();
assert_eq!(addr.email, "user@example.com");
assert_eq!(addr.name.as_deref(), Some("John (Jack) Doe"));
}
#[test]
fn test_address_from_str_bare_email_unchanged() {
let addr: Address = "user@example.com".parse().unwrap();
assert_eq!(addr.email, "user@example.com");
assert_eq!(addr.name, None);
}
#[test]
fn test_address_from_str_leading_comment() {
let addr: Address = "(John Doe) user@example.com".parse().unwrap();
assert_eq!(addr.email, "user@example.com");
assert_eq!(
addr.name.as_deref(),
Some("John Doe"),
"leading comment should be used as display name (RFC 5322 Section 3.2.2)"
);
}
#[test]
fn test_address_from_str_leading_comment_nested() {
let addr: Address = "(John (Jack) Doe) user@example.com".parse().unwrap();
assert_eq!(addr.email, "user@example.com");
assert_eq!(addr.name.as_deref(), Some("John (Jack) Doe"));
}
#[test]
fn address_display_encodes_control_chars() {
let addr = Address {
name: Some("Hello\x7FWorld".into()),
email: "user@example.com".into(),
};
let displayed = addr.to_string();
assert!(
!displayed.contains('\x7F'),
"DEL char must not appear raw in output"
);
assert!(
displayed.contains("=?"),
"Control chars should trigger RFC 2047 encoding"
);
}
#[test]
fn address_from_str_comment_unescapes_quoted_pairs() {
let addr: Address = "(John \\(Jack\\) Doe) user@example.com".parse().unwrap();
assert_eq!(addr.name.as_deref(), Some("John (Jack) Doe"));
}
#[test]
fn address_from_str_unquoted_preserves_backslash() {
let addr: Address = "John \\Doe <user@example.com>".parse().unwrap();
assert_eq!(addr.name.as_deref(), Some("John \\Doe"));
}
#[test]
fn address_from_str_quoted_local_part_with_at_sign() {
let addr: Address = "\"user@inner\"@example.com".parse().unwrap();
assert_eq!(addr.email, "\"user@inner\"@example.com");
assert_eq!(addr.name, None);
}
#[test]
fn address_from_str_quoted_local_part_with_parentheses() {
let addr: Address = "\"user(comment)\"@example.com".parse().unwrap();
assert_eq!(addr.email, "\"user(comment)\"@example.com");
assert_eq!(addr.name, None);
}
#[test]
fn fuzz_address_from_str_paren_at_no_panic() {
assert!("(@".parse::<Address>().is_err());
assert!("(@)".parse::<Address>().is_err());
assert!("(user@host)".parse::<Address>().is_err());
}
#[test]
fn header_name_valid() {
let h = HeaderName::new("X-Custom-Header").unwrap();
assert_eq!(h.as_str(), "X-Custom-Header");
assert_eq!(h.to_string(), "X-Custom-Header");
}
#[test]
fn header_name_empty_rejected() {
assert!(HeaderName::new("").is_err());
}
#[test]
fn header_name_colon_rejected() {
assert!(HeaderName::new("X-Bad:Name").is_err());
}
#[test]
fn header_name_space_rejected() {
assert!(HeaderName::new("X Bad").is_err());
}
#[test]
fn header_name_control_char_rejected() {
assert!(HeaderName::new("X-Bad\x01").is_err());
}
#[test]
fn header_name_non_ascii_rejected() {
assert!(HeaderName::new("X-Caf\u{00E9}").is_err());
}
#[test]
fn header_name_try_from_string() {
let h: Result<HeaderName, _> = "X-Valid".to_string().try_into();
assert!(h.is_ok());
assert_eq!(h.unwrap().as_str(), "X-Valid");
}
#[test]
fn header_name_try_from_str() {
let h: Result<HeaderName, _> = "X-Valid".try_into();
assert!(h.is_ok());
}
#[test]
fn header_name_unchecked_allows_anything() {
let h = HeaderName::new_unchecked("bad: name");
assert_eq!(h.as_str(), "bad: name");
}
#[test]
fn header_name_as_ref() {
let h = HeaderName::new("X-Test").unwrap();
let s: &str = h.as_ref();
assert_eq!(s, "X-Test");
}
#[test]
fn header_name_into_inner() {
let h = HeaderName::new("X-Test").unwrap();
let s: String = h.into_inner();
assert_eq!(s, "X-Test");
}
#[test]
fn header_name_equality_is_case_insensitive() {
let upper = HeaderName::new("Subject").unwrap();
let lower = HeaderName::new("subject").unwrap();
assert_eq!(
upper, lower,
"header field names must compare case-insensitively"
);
}
#[test]
fn header_name_hash_is_case_insensitive() {
let mut set = std::collections::HashSet::new();
set.insert(HeaderName::new("X-Trace").unwrap());
set.insert(HeaderName::new("x-trace").unwrap());
assert_eq!(
set.len(),
1,
"header field names that differ only by ASCII case must hash identically"
);
}
#[test]
fn message_id_valid() {
let m = MessageId::new("abc123@example.com").unwrap();
assert_eq!(m.as_str(), "abc123@example.com");
assert_eq!(m.to_string(), "abc123@example.com");
}
#[test]
fn message_id_quoted_id_left_rejected_for_public_api() {
assert!(MessageId::new("\"user@inner\"@example.com").is_err());
}
#[test]
fn message_id_empty_rejected() {
assert!(MessageId::new("").is_err());
}
#[test]
fn message_id_no_at_rejected() {
assert!(MessageId::new("no-at-sign").is_err());
}
#[test]
fn message_id_multiple_at_rejected() {
assert!(MessageId::new("a@b@c").is_err());
}
#[test]
fn message_id_empty_id_left_rejected() {
assert!(MessageId::new("@example.com").is_err());
}
#[test]
fn message_id_empty_id_right_rejected() {
assert!(MessageId::new("abc@").is_err());
}
#[test]
fn message_id_whitespace_rejected() {
assert!(MessageId::new("abc @example.com").is_err());
}
#[test]
fn message_id_angle_brackets_rejected() {
assert!(MessageId::new("<abc@example.com>").is_err());
}
#[test]
fn message_id_control_char_rejected() {
assert!(MessageId::new("abc\x01@example.com").is_err());
}
#[test]
fn message_id_invalid_dot_atom_rejected() {
assert!(MessageId::new(".user@example.com").is_err());
assert!(MessageId::new("user.@example.com").is_err());
assert!(MessageId::new("user..name@example.com").is_err());
assert!(MessageId::new("user@.example.com").is_err());
assert!(MessageId::new("user@example.com.").is_err());
assert!(MessageId::new("user@example..com").is_err());
}
#[test]
fn message_id_no_fold_literal_id_right_accepted() {
let m = MessageId::new("user@[127.0.0.1]");
assert!(m.is_ok(), "no-fold-literal id-right should be accepted");
}
#[test]
fn message_id_try_from_string() {
let m: Result<MessageId, _> = "valid@host.com".to_string().try_into();
assert!(m.is_ok());
}
#[test]
fn message_id_try_from_str() {
let m: Result<MessageId, _> = "valid@host.com".try_into();
assert!(m.is_ok());
}
#[test]
fn message_id_unchecked_allows_anything() {
let m = MessageId::new_unchecked("no-at");
assert_eq!(m.as_str(), "no-at");
}
#[test]
fn message_id_as_ref() {
let m = MessageId::new("test@host.com").unwrap();
let s: &str = m.as_ref();
assert_eq!(s, "test@host.com");
}
#[test]
fn message_id_into_inner() {
let m = MessageId::new("test@host.com").unwrap();
let s: String = m.into_inner();
assert_eq!(s, "test@host.com");
}
#[test]
fn message_id_non_ascii_accepted() {
let m = MessageId::new("réponse@example.com");
assert!(
m.is_ok(),
"Non-ASCII message-ID should be accepted per RFC 6532"
);
}
#[test]
fn validated_extra_headers_converts_valid_names() {
let email = ParsedEmail {
extra_headers: vec![
("X-Mailer".into(), "TestApp/1.0".into()),
("X-Priority".into(), "3".into()),
],
..Default::default()
};
let result = email.validated_extra_headers();
assert_eq!(result.len(), 2);
assert_eq!(result[0].0.as_ref(), "X-Mailer");
assert_eq!(result[0].1, "TestApp/1.0");
assert_eq!(result[1].0.as_ref(), "X-Priority");
assert_eq!(result[1].1, "3");
}
#[test]
fn validated_extra_headers_drops_invalid_names() {
let email = ParsedEmail {
extra_headers: vec![
("X-Valid".into(), "keep".into()),
("Bad Header".into(), "space in name".into()),
("Also:Bad".into(), "colon in name".into()),
(String::new(), "empty name".into()),
("X-Also-Valid".into(), "also keep".into()),
],
..Default::default()
};
let result = email.validated_extra_headers();
assert_eq!(result.len(), 2);
assert_eq!(result[0].0.as_ref(), "X-Valid");
assert_eq!(result[0].1, "keep");
assert_eq!(result[1].0.as_ref(), "X-Also-Valid");
assert_eq!(result[1].1, "also keep");
}
#[test]
fn validated_extra_headers_empty_input() {
let email = ParsedEmail::default();
assert!(email.validated_extra_headers().is_empty());
}
#[test]
fn validation_error_message_returns_inner_string() {
let err = ValidationError::new("bad input");
assert_eq!(err.message(), "bad input");
}
#[test]
fn validation_error_message_matches_display() {
let err = ValidationError::new("something went wrong");
assert_eq!(err.message(), err.to_string());
}