use std::fmt;
use std::net::{Ipv4Addr, Ipv6Addr};
pub use daaki_message::ValidationError;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(try_from = "String", into = "String"))]
pub struct Domain(String);
impl Domain {
pub fn new(s: impl Into<String>) -> Result<Self, ValidationError> {
let s = s.into();
validate_domain_syntax(&s)?;
Ok(Self(s))
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_inner(self) -> String {
self.0
}
}
impl AsRef<str> for Domain {
fn as_ref(&self) -> &str {
&self.0
}
}
impl fmt::Display for Domain {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl TryFrom<String> for Domain {
type Error = ValidationError;
fn try_from(s: String) -> Result<Self, ValidationError> {
Self::new(s)
}
}
impl TryFrom<&str> for Domain {
type Error = ValidationError;
fn try_from(s: &str) -> Result<Self, ValidationError> {
Self::new(s)
}
}
impl From<Domain> for String {
fn from(d: Domain) -> Self {
d.into_inner()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(try_from = "String", into = "String"))]
pub struct AddressLiteral(String);
impl AddressLiteral {
pub fn new(s: impl Into<String>) -> Result<Self, ValidationError> {
let s = s.into();
validate_address_literal_syntax(&s)?;
Ok(Self(s))
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_inner(self) -> String {
self.0
}
}
impl AsRef<str> for AddressLiteral {
fn as_ref(&self) -> &str {
&self.0
}
}
impl fmt::Display for AddressLiteral {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl TryFrom<String> for AddressLiteral {
type Error = ValidationError;
fn try_from(s: String) -> Result<Self, ValidationError> {
Self::new(s)
}
}
impl TryFrom<&str> for AddressLiteral {
type Error = ValidationError;
fn try_from(s: &str) -> Result<Self, ValidationError> {
Self::new(s)
}
}
impl From<AddressLiteral> for String {
fn from(a: AddressLiteral) -> Self {
a.into_inner()
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum DomainOrLiteral {
Domain(Domain),
Literal(AddressLiteral),
}
impl DomainOrLiteral {
pub fn new(s: impl Into<String>) -> Result<Self, ValidationError> {
let s = s.into();
if s.is_empty() {
return Err(ValidationError::new(
"SMTP greeting domain must not be empty (RFC 5321 Section 4.1.1.1)",
));
}
if s.starts_with('[') && s.ends_with(']') {
Ok(Self::Literal(AddressLiteral::new(s)?))
} else {
Ok(Self::Domain(Domain::new(s)?))
}
}
pub fn as_str(&self) -> &str {
match self {
Self::Domain(d) => d.as_str(),
Self::Literal(l) => l.as_str(),
}
}
}
impl AsRef<str> for DomainOrLiteral {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for DomainOrLiteral {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl TryFrom<String> for DomainOrLiteral {
type Error = ValidationError;
fn try_from(s: String) -> Result<Self, ValidationError> {
Self::new(s)
}
}
impl TryFrom<&str> for DomainOrLiteral {
type Error = ValidationError;
fn try_from(s: &str) -> Result<Self, ValidationError> {
Self::new(s)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(try_from = "String", into = "String"))]
pub struct Mailbox(String);
impl Mailbox {
pub fn new(s: impl Into<String>) -> Result<Self, ValidationError> {
let s = s.into();
validate_mailbox_syntax(&s)?;
Ok(Self(strip_obsolete_source_route(&s)?.to_string()))
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_inner(self) -> String {
self.0
}
pub fn requires_smtputf8(&self) -> bool {
!self.0.is_ascii()
}
}
impl AsRef<str> for Mailbox {
fn as_ref(&self) -> &str {
&self.0
}
}
impl fmt::Display for Mailbox {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl TryFrom<String> for Mailbox {
type Error = ValidationError;
fn try_from(s: String) -> Result<Self, ValidationError> {
Self::new(s)
}
}
impl TryFrom<&str> for Mailbox {
type Error = ValidationError;
fn try_from(s: &str) -> Result<Self, ValidationError> {
Self::new(s)
}
}
impl From<Mailbox> for String {
fn from(m: Mailbox) -> Self {
m.into_inner()
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ReversePath {
Null,
Mailbox(Mailbox),
}
impl ReversePath {
pub fn new(s: impl Into<String>) -> Result<Self, ValidationError> {
let s = s.into();
if s.is_empty() {
Ok(Self::Null)
} else {
Ok(Self::Mailbox(Mailbox::new(s)?))
}
}
pub fn as_str(&self) -> &str {
match self {
Self::Null => "",
Self::Mailbox(m) => m.as_str(),
}
}
pub fn requires_smtputf8(&self) -> bool {
match self {
Self::Null => false,
Self::Mailbox(m) => m.requires_smtputf8(),
}
}
}
impl fmt::Display for ReversePath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl TryFrom<String> for ReversePath {
type Error = ValidationError;
fn try_from(s: String) -> Result<Self, ValidationError> {
Self::new(s)
}
}
impl TryFrom<&str> for ReversePath {
type Error = ValidationError;
fn try_from(s: &str) -> Result<Self, ValidationError> {
Self::new(s)
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ForwardPath {
Postmaster,
Mailbox(Mailbox),
}
impl ForwardPath {
pub fn new(s: impl Into<String>) -> Result<Self, ValidationError> {
let s = s.into();
if s.eq_ignore_ascii_case("postmaster") {
Ok(Self::Postmaster)
} else {
Ok(Self::Mailbox(Mailbox::new(s)?))
}
}
pub fn as_str(&self) -> &str {
match self {
Self::Postmaster => "Postmaster",
Self::Mailbox(m) => m.as_str(),
}
}
pub fn requires_smtputf8(&self) -> bool {
match self {
Self::Postmaster => false,
Self::Mailbox(m) => m.requires_smtputf8(),
}
}
}
impl fmt::Display for ForwardPath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl TryFrom<String> for ForwardPath {
type Error = ValidationError;
fn try_from(s: String) -> Result<Self, ValidationError> {
Self::new(s)
}
}
impl TryFrom<&str> for ForwardPath {
type Error = ValidationError;
fn try_from(s: &str) -> Result<Self, ValidationError> {
Self::new(s)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(try_from = "String", into = "String"))]
pub struct EnvidValue(String);
impl EnvidValue {
pub fn into_inner(self) -> String {
self.0
}
pub fn new(s: impl Into<String>) -> Result<Self, ValidationError> {
let s = s.into();
if s.is_empty() {
return Err(ValidationError::new(
"ENVID value must be non-empty (RFC 3461 Section 4.4: xtext = 1*xchar)",
));
}
if !s
.bytes()
.all(|b| b.is_ascii() && (0x20..=0x7E).contains(&b))
{
return Err(ValidationError::new(
"ENVID source value must contain only printable US-ASCII before xtext encoding \
(RFC 3461 Section 4.4)",
));
}
if s.len() > 100 {
return Err(ValidationError::new(
"ENVID value must be at most 100 characters before xtext encoding \
(RFC 3461 Section 4.4)",
));
}
Ok(Self(s))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl AsRef<str> for EnvidValue {
fn as_ref(&self) -> &str {
&self.0
}
}
impl fmt::Display for EnvidValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl TryFrom<String> for EnvidValue {
type Error = ValidationError;
fn try_from(s: String) -> Result<Self, ValidationError> {
Self::new(s)
}
}
impl TryFrom<&str> for EnvidValue {
type Error = ValidationError;
fn try_from(s: &str) -> Result<Self, ValidationError> {
Self::new(s)
}
}
impl From<EnvidValue> for String {
fn from(e: EnvidValue) -> Self {
e.into_inner()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(try_from = "String", into = "String"))]
pub struct XtextSafe(String);
impl XtextSafe {
pub fn into_inner(self) -> String {
self.0
}
pub fn new(s: impl Into<String>) -> Result<Self, ValidationError> {
let s = s.into();
if s.is_empty() {
return Err(ValidationError::new(
"SMTP query argument must not be empty \
(RFC 5321 Sections 4.1.1.6-4.1.1.7)",
));
}
for &b in s.as_bytes() {
if b < 0x20 || b == 0x7F {
return Err(ValidationError::new(format!(
"SMTP argument contains control character (byte 0x{b:02X}); \
only printable US-ASCII is permitted (RFC 5321 Section 4.1.2)"
)));
}
if b > 0x7F {
return Err(ValidationError::new(
"SMTP argument contains non-ASCII characters; \
only printable US-ASCII is permitted (RFC 5321 Section 4.1.2)",
));
}
}
Ok(Self(s))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl AsRef<str> for XtextSafe {
fn as_ref(&self) -> &str {
&self.0
}
}
impl fmt::Display for XtextSafe {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl TryFrom<String> for XtextSafe {
type Error = ValidationError;
fn try_from(s: String) -> Result<Self, ValidationError> {
Self::new(s)
}
}
impl TryFrom<&str> for XtextSafe {
type Error = ValidationError;
fn try_from(s: &str) -> Result<Self, ValidationError> {
Self::new(s)
}
}
impl From<XtextSafe> for String {
fn from(x: XtextSafe) -> Self {
x.into_inner()
}
}
fn validate_domain_syntax(domain: &str) -> Result<(), ValidationError> {
if domain.is_empty() {
return Err(ValidationError::new(
"domain must not be empty (RFC 5321 Section 4.1.2)",
));
}
if domain.len() > 255 {
return Err(ValidationError::new(format!(
"domain exceeds 255-octet limit (RFC 5321 Section 4.5.3.1.2): {} octets",
domain.len()
)));
}
if domain.starts_with('[') && domain.ends_with(']') {
return Err(ValidationError::new(
"value is an address-literal, not a domain (RFC 5321 Section 4.1.2)",
));
}
for label in domain.split('.') {
validate_domain_label(label)?;
}
Ok(())
}
pub(crate) fn validate_smtputf8_domain_syntax(domain: &str) -> Result<(), ValidationError> {
if domain.is_empty() {
return Err(ValidationError::new(
"domain must not be empty (RFC 5321 Section 4.1.2 / RFC 6531 Section 3.3)",
));
}
if domain.len() > 255 {
return Err(ValidationError::new(format!(
"domain exceeds 255-octet limit (RFC 5321 Section 4.5.3.1.2): {} octets",
domain.len()
)));
}
if domain.starts_with('[') && domain.ends_with(']') {
return Err(ValidationError::new(
"value is an address-literal, not a domain (RFC 5321 Section 4.1.2)",
));
}
for label in domain.split('.') {
if label.is_empty() {
return Err(ValidationError::new(
"domain contains an empty label (RFC 5321 Section 4.1.2 / RFC 6531 Section 3.3)",
));
}
if label.is_ascii() {
validate_domain_label(label)?;
continue;
}
if label.len() > 63 {
return Err(ValidationError::new(format!(
"domain label exceeds 63-octet limit \
(RFC 5321 Section 4.5.3.1.2 / RFC 6531 Section 3.3): {} octets",
label.len()
)));
}
if label.starts_with('-') || label.ends_with('-') {
return Err(ValidationError::new(
"domain labels must begin and end with a letter or digit \
(RFC 5321 Section 4.1.2 / RFC 6531 Section 3.3)",
));
}
for ch in label.chars() {
if ch.is_ascii() && !(ch.is_ascii_alphanumeric() || ch == '-') {
return Err(ValidationError::new(format!(
"domain label contains invalid character {ch:?}; \
only letters, digits, '-', and UTF-8 U-label code points \
are permitted (RFC 5321 Section 4.1.2 / RFC 6531 Section 3.3)"
)));
}
}
}
Ok(())
}
fn validate_domain_label(label: &str) -> Result<(), ValidationError> {
if label.is_empty() {
return Err(ValidationError::new(
"domain contains an empty label (RFC 5321 Section 4.1.2)",
));
}
if label.len() > 63 {
return Err(ValidationError::new(format!(
"domain label exceeds 63-octet limit \
(RFC 1035 Section 2.3.4 / RFC 5321 Section 4.1.2): {} octets",
label.len()
)));
}
let bytes = label.as_bytes();
if !bytes[0].is_ascii_alphanumeric() || !bytes[bytes.len() - 1].is_ascii_alphanumeric() {
return Err(ValidationError::new(
"domain labels must begin and end with a letter or digit \
(RFC 5321 Section 4.1.2)",
));
}
for &b in bytes {
if !(b.is_ascii_alphanumeric() || b == b'-') {
return Err(ValidationError::new(format!(
"domain label contains invalid character {:?}; \
only letters, digits, and '-' are permitted (RFC 5321 Section 4.1.2)",
b as char
)));
}
}
Ok(())
}
fn validate_address_literal_syntax(literal: &str) -> Result<(), ValidationError> {
if literal.len() > 255 {
return Err(ValidationError::new(format!(
"address-literal exceeds 255-octet limit \
(RFC 5321 Section 4.5.3.1.2): {} octets",
literal.len()
)));
}
let Some(body) = literal.strip_prefix('[').and_then(|s| s.strip_suffix(']')) else {
return Err(ValidationError::new(
"address-literal must be enclosed in '[' and ']' (RFC 5321 Section 4.1.3)",
));
};
if body.is_empty() {
return Err(ValidationError::new(
"address-literal must not be empty (RFC 5321 Section 4.1.3)",
));
}
if body.parse::<Ipv4Addr>().is_ok() {
return Ok(());
}
if body.len() >= 5 && body[..5].eq_ignore_ascii_case("IPv6:") {
return body[5..].parse::<Ipv6Addr>().map(|_| ()).map_err(|_| {
ValidationError::new("invalid IPv6 address in address-literal (RFC 5321 Section 4.1.3)")
});
}
let Some((tag, value)) = body.split_once(':') else {
return Err(ValidationError::new(
"address-literal must be IPv4, IPv6, or generalized tag:value syntax \
(RFC 5321 Section 4.1.3)",
));
};
validate_ldh_str(tag)?;
if value.is_empty() {
return Err(ValidationError::new(
"generalized address-literal must contain data after ':' \
(RFC 5321 Section 4.1.3)",
));
}
for &b in value.as_bytes() {
if !((33..=90).contains(&b) || (94..=126).contains(&b)) {
return Err(ValidationError::new(format!(
"address-literal contains invalid data byte 0x{b:02X} \
(RFC 5321 Section 4.1.3)"
)));
}
}
Ok(())
}
fn validate_ldh_str(tag: &str) -> Result<(), ValidationError> {
if tag.is_empty() {
return Err(ValidationError::new(
"generalized address-literal tag must not be empty (RFC 5321 Section 4.1.3)",
));
}
let bytes = tag.as_bytes();
if !bytes[bytes.len() - 1].is_ascii_alphanumeric() {
return Err(ValidationError::new(
"generalized address-literal tag must end with a letter or digit \
(RFC 5321 Section 4.1.3: Standardized-tag = Ldh-str)",
));
}
for &b in bytes {
if !(b.is_ascii_alphanumeric() || b == b'-') {
return Err(ValidationError::new(format!(
"generalized address-literal tag contains invalid character {:?} \
(RFC 5321 Section 4.1.3)",
b as char
)));
}
}
Ok(())
}
fn strip_obsolete_source_route(address: &str) -> Result<&str, ValidationError> {
if !address.starts_with('@') {
return Ok(address);
}
let Some((route, mailbox)) = address.split_once(':') else {
return Err(ValidationError::new(
"obsolete source route must end with ':' before the mailbox \
(RFC 5321 Section 4.1.2)",
));
};
if mailbox.is_empty() {
return Err(ValidationError::new(
"obsolete source route must be followed by a mailbox \
(RFC 5321 Section 4.1.2)",
));
}
for at_domain in route.split(',') {
let Some(domain) = at_domain.strip_prefix('@') else {
return Err(ValidationError::new(
"obsolete source route entries must be @domain tokens \
(RFC 5321 Section 4.1.2)",
));
};
validate_smtputf8_domain_syntax(domain)?;
}
Ok(mailbox)
}
fn validate_mailbox_syntax(address: &str) -> Result<(), ValidationError> {
if address.len() > 254 {
return Err(ValidationError::new(format!(
"mailbox exceeds 256-octet path limit including <> \
(RFC 5321 Section 4.5.3.1.3): {} octets",
address.len() + 2
)));
}
let address = strip_obsolete_source_route(address)?;
if address != address.trim() {
return Err(ValidationError::new(
"mailbox must not contain leading or trailing whitespace \
(RFC 5321 Section 4.1.2)",
));
}
match address.parse::<daaki_message::Address>() {
Ok(parsed) if parsed.name.is_none() && parsed.email == address => {}
Ok(_) => {
return Err(ValidationError::new(
"mailbox must be a bare addr-spec, not a name-addr or comment form \
(RFC 5321 Section 4.1.2)",
));
}
Err(err) => {
return Err(ValidationError::new(format!(
"mailbox is syntactically invalid: {err} (RFC 5321 Section 4.1.2)"
)));
}
}
if let Some(at_pos) = address.rfind('@') {
let local_part = &address[..at_pos];
let domain = &address[at_pos + 1..];
if local_part.starts_with('"') && local_part.ends_with('"') {
validate_smtp_quoted_local_part(local_part)?;
}
if domain.starts_with('[') && domain.ends_with(']') {
validate_address_literal_syntax(domain)?;
}
if local_part.len() > 64 {
return Err(ValidationError::new(format!(
"local-part exceeds 64-octet limit \
(RFC 5321 Section 4.5.3.1.1): {} octets",
local_part.len()
)));
}
}
Ok(())
}
fn validate_smtp_quoted_local_part(local_part: &str) -> Result<(), ValidationError> {
let Some(inner) = local_part
.strip_prefix('"')
.and_then(|value| value.strip_suffix('"'))
else {
return Err(ValidationError::new(
"quoted local-part must be enclosed in double quotes \
(RFC 5321 Section 4.1.2)",
));
};
let bytes = inner.as_bytes();
let mut index = 0;
while index < bytes.len() {
let byte = bytes[index];
if byte == b'\\' {
index += 1;
if index >= bytes.len() {
return Err(ValidationError::new(
"quoted local-part has trailing backslash \
(RFC 5321 Section 4.1.2 quoted-pairSMTP)",
));
}
let escaped = bytes[index];
if !(0x20..=0x7E).contains(&escaped) {
return Err(ValidationError::new(format!(
"quoted local-part contains invalid escaped byte 0x{escaped:02X}; \
quoted-pairSMTP permits only ASCII space or VCHAR \
(RFC 5321 Section 4.1.2)"
)));
}
} else if byte == b'"' || byte == b'\\' {
return Err(ValidationError::new(
"quoted local-part contains an unescaped quote or backslash \
(RFC 5321 Section 4.1.2 qtextSMTP)",
));
} else if byte == b'\t' || byte < 0x20 || byte == 0x7F {
return Err(ValidationError::new(format!(
"quoted local-part contains control character 0x{byte:02X}; \
qtextSMTP permits ASCII space/graphics or UTF-8 non-ASCII only \
(RFC 5321 Section 4.1.2 / RFC 6531 Section 3.3)"
)));
}
index += 1;
}
Ok(())
}
#[cfg(test)]
#[path = "validated_tests.rs"]
mod tests;