#![allow(clippy::unwrap_used, clippy::expect_used)]
use super::*;
#[test]
fn domain_valid() {
assert!(Domain::new("example.com").is_ok());
assert!(Domain::new("mail.example.co.uk").is_ok());
assert!(Domain::new("a").is_ok());
assert!(Domain::new("a-b.example").is_ok());
}
#[test]
fn domain_rejects_empty() {
assert!(Domain::new("").is_err());
}
#[test]
fn domain_rejects_overlong_label() {
let long = "a".repeat(64);
assert!(Domain::new(format!("{long}.com")).is_err());
}
#[test]
fn domain_rejects_empty_label() {
assert!(Domain::new("mail..example.com").is_err());
}
#[test]
fn domain_rejects_leading_hyphen() {
assert!(Domain::new("-example.com").is_err());
}
#[test]
fn domain_rejects_address_literal() {
assert!(Domain::new("[127.0.0.1]").is_err());
}
#[test]
fn address_literal_ipv4() {
assert!(AddressLiteral::new("[127.0.0.1]").is_ok());
assert!(AddressLiteral::new("[192.168.1.1]").is_ok());
}
#[test]
fn address_literal_ipv6() {
assert!(AddressLiteral::new("[IPv6:::1]").is_ok());
assert!(AddressLiteral::new("[IPv6:2001:db8::1]").is_ok());
}
#[test]
fn address_literal_rejects_no_brackets() {
assert!(AddressLiteral::new("127.0.0.1").is_err());
}
#[test]
fn address_literal_rejects_empty_body() {
assert!(AddressLiteral::new("[]").is_err());
}
#[test]
fn address_literal_rejects_invalid_ipv6() {
assert!(AddressLiteral::new("[IPv6:not-valid]").is_err());
}
#[test]
fn address_literal_generalized_tag_allows_leading_hyphen() {
assert!(AddressLiteral::new("[-tag:opaque]").is_ok());
}
#[test]
fn domain_or_literal_domain() {
let d = DomainOrLiteral::new("example.com").unwrap();
assert!(matches!(d, DomainOrLiteral::Domain(_)));
}
#[test]
fn domain_or_literal_ipv4() {
let d = DomainOrLiteral::new("[127.0.0.1]").unwrap();
assert!(matches!(d, DomainOrLiteral::Literal(_)));
}
#[test]
fn mailbox_valid() {
assert!(Mailbox::new("user@example.com").is_ok());
assert!(Mailbox::new("a.b@example.com").is_ok());
}
#[test]
fn mailbox_accepts_quoted_local_part_with_at_sign() {
let mailbox = Mailbox::new("\"user@inner\"@example.com").unwrap();
assert_eq!(mailbox.as_str(), "\"user@inner\"@example.com");
assert!(!mailbox.requires_smtputf8());
}
#[test]
fn mailbox_accepts_quoted_local_part_with_parentheses() {
let mailbox = Mailbox::new("\"user(comment)\"@example.com").unwrap();
assert_eq!(mailbox.as_str(), "\"user(comment)\"@example.com");
}
#[test]
fn mailbox_rejects_tab_in_quoted_local_part() {
assert!(
Mailbox::new("\"user\tinner\"@example.com").is_err(),
"RFC 5321 Section 4.1.2 qtextSMTP excludes HTAB in quoted local-parts"
);
assert!(
Mailbox::new("\"user\\\tinner\"@example.com").is_err(),
"RFC 5321 Section 4.1.2 quoted-pairSMTP permits only ASCII SP/VCHAR after '\\\\'"
);
}
#[test]
fn mailbox_rejects_display_name() {
assert!(Mailbox::new("User <user@example.com>").is_err());
}
#[test]
fn mailbox_rejects_overlong_local() {
let long_local = "a".repeat(65);
assert!(Mailbox::new(format!("{long_local}@example.com")).is_err());
}
#[test]
fn mailbox_rejects_whitespace() {
assert!(Mailbox::new(" user@example.com").is_err());
assert!(Mailbox::new("user@example.com ").is_err());
}
#[test]
fn mailbox_rejects_rfc5322_only_domain_literal() {
assert!(
Mailbox::new("user@[10,0,0,1]").is_err(),
"SMTP mailbox syntax must reject RFC 5322-only domain-literals \
that are not valid RFC 5321 address-literals"
);
}
#[test]
fn mailbox_requires_smtputf8() {
let m = Mailbox::new("user@example.com").unwrap();
assert!(!m.requires_smtputf8());
}
#[test]
fn reverse_path_null() {
let rp = ReversePath::new("").unwrap();
assert!(matches!(rp, ReversePath::Null));
assert_eq!(rp.as_str(), "");
}
#[test]
fn reverse_path_mailbox() {
let rp = ReversePath::new("user@example.com").unwrap();
assert!(matches!(rp, ReversePath::Mailbox(_)));
assert_eq!(rp.as_str(), "user@example.com");
}
#[test]
fn reverse_path_accepts_quoted_local_part_with_at_sign() {
let rp = ReversePath::new("\"user@inner\"@example.com").unwrap();
assert_eq!(rp.as_str(), "\"user@inner\"@example.com");
}
#[test]
fn reverse_path_accepts_and_ignores_source_route() {
let rp = ReversePath::new("@old.example,@relay.example:user@example.com").unwrap();
assert_eq!(rp.as_str(), "user@example.com");
}
#[test]
fn reverse_path_accepts_utf8_source_route_domains() {
let rp = ReversePath::new("@例え.jp,@domínio.example:user@example.com").unwrap();
assert_eq!(rp.as_str(), "user@example.com");
}
#[test]
fn reverse_path_rejects_overlong_source_route_path() {
let route_domain = format!("{}.{}.{}", "u".repeat(63), "v".repeat(63), "w".repeat(62));
let address = format!("@{route_domain},@{route_domain}:user@example.com");
assert!(
address.len() + 2 > 256,
"test precondition: source-routed reverse-path must exceed the RFC 5321 256-octet limit"
);
let err = ReversePath::new(&address).expect_err(
"source-routed reverse-path exceeding 256 octets must be rejected \
per RFC 5321 Section 4.5.3.1.3",
);
let msg = err.to_string();
assert!(
msg.contains("256") || msg.contains("path"),
"error should mention the path-length limit: {msg}"
);
}
#[test]
fn forward_path_postmaster() {
let fp = ForwardPath::new("Postmaster").unwrap();
assert!(matches!(fp, ForwardPath::Postmaster));
let fp2 = ForwardPath::new("postmaster").unwrap();
assert!(matches!(fp2, ForwardPath::Postmaster));
}
#[test]
fn forward_path_mailbox() {
let fp = ForwardPath::new("user@example.com").unwrap();
assert!(matches!(fp, ForwardPath::Mailbox(_)));
}
#[test]
fn forward_path_accepts_quoted_local_part_with_at_sign() {
let fp = ForwardPath::new("\"user@inner\"@example.com").unwrap();
assert_eq!(fp.as_str(), "\"user@inner\"@example.com");
}
#[test]
fn forward_path_accepts_and_ignores_source_route() {
let fp = ForwardPath::new("@old.example,@relay.example:user@example.com").unwrap();
assert_eq!(fp.as_str(), "user@example.com");
}
#[test]
fn forward_path_accepts_utf8_source_route_domains() {
let fp = ForwardPath::new("@例え.jp,@domínio.example:user@example.com").unwrap();
assert_eq!(fp.as_str(), "user@example.com");
}
#[test]
fn envid_valid() {
assert!(EnvidValue::new("abc123").is_ok());
assert!(EnvidValue::new("msg-001@server").is_ok());
}
#[test]
fn envid_rejects_empty() {
assert!(EnvidValue::new("").is_err());
}
#[test]
fn envid_rejects_overlong() {
assert!(EnvidValue::new("a".repeat(101)).is_err());
}
#[test]
fn envid_rejects_control_chars() {
assert!(EnvidValue::new("abc\x00def").is_err());
}
#[test]
fn xtext_safe_valid() {
assert!(XtextSafe::new("hello").is_ok());
assert!(XtextSafe::new("user@example.com").is_ok());
}
#[test]
fn xtext_safe_rejects_empty() {
assert!(XtextSafe::new("").is_err());
}
#[test]
fn xtext_safe_rejects_control_chars() {
assert!(XtextSafe::new("abc\x00").is_err());
assert!(XtextSafe::new("abc\n").is_err());
}
#[test]
fn xtext_safe_rejects_cr_lf() {
assert!(
XtextSafe::new("hello\rworld").is_err(),
"bare CR must be rejected"
);
assert!(
XtextSafe::new("hello\nworld").is_err(),
"bare LF must be rejected"
);
assert!(
XtextSafe::new("hello\r\nworld").is_err(),
"CRLF must be rejected"
);
}
#[test]
fn xtext_safe_rejects_non_ascii() {
assert!(XtextSafe::new("café").is_err());
}
#[test]
fn smtputf8_domain_rejects_overlong_non_ascii_label() {
let label = "é".repeat(32);
assert_eq!(label.len(), 64, "test precondition: label must be 64 bytes");
let domain = format!("{label}.com");
let err = validate_smtputf8_domain_syntax(&domain)
.expect_err("non-ASCII label of 64 bytes must be rejected");
let msg = err.to_string();
assert!(
msg.contains("63") || msg.contains("label"),
"error should mention the label-length limit: {msg}"
);
}
#[test]
fn smtputf8_domain_accepts_max_non_ascii_label() {
let label = format!("{}a", "é".repeat(31));
assert_eq!(label.len(), 63, "test precondition: label must be 63 bytes");
let domain = format!("{label}.com");
assert!(
validate_smtputf8_domain_syntax(&domain).is_ok(),
"non-ASCII label of exactly 63 bytes must be accepted"
);
}