Skip to main content

structured_email_address/
error.rs

1//! Error types for email address parsing and validation.
2
3use alloc::string::String;
4use core::fmt;
5
6/// Error returned when parsing or validating an email address fails.
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct Error {
9    kind: ErrorKind,
10    position: usize,
11}
12
13/// The specific kind of error that occurred.
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub enum ErrorKind {
16    /// Input is empty or whitespace-only.
17    Empty,
18    /// Missing `@` separator.
19    MissingAtSign,
20    /// Local part is empty (nothing before `@`).
21    EmptyLocalPart,
22    /// Domain is empty (nothing after `@`).
23    EmptyDomain,
24    /// Local part exceeds 64 octets (RFC 5321 §4.5.3.1.1).
25    LocalPartTooLong { len: usize },
26    /// Total address exceeds 254 octets (RFC 5321 §4.5.3.1.3).
27    AddressTooLong { len: usize },
28    /// Domain label exceeds 63 octets (RFC 1035 §2.3.4).
29    DomainLabelTooLong { label: String, len: usize },
30    /// Invalid character in local part.
31    InvalidLocalPartChar { ch: char },
32    /// Invalid character in domain.
33    InvalidDomainChar { ch: char },
34    /// Domain label starts or ends with hyphen.
35    DomainLabelHyphen,
36    /// Domain has no dot (single label, not a valid internet domain).
37    DomainNoDot,
38    /// Unterminated quoted string.
39    UnterminatedQuotedString,
40    /// Invalid quoted-pair sequence.
41    InvalidQuotedPair,
42    /// Unterminated comment.
43    UnterminatedComment,
44    /// Unterminated domain literal `[...]`.
45    UnterminatedDomainLiteral,
46    /// Domain literal `[...]` is not a valid IPv4 or `IPv6:` address literal
47    /// (RFC 5321 §4.1.3). General domain literals are not accepted.
48    InvalidAddressLiteral,
49    /// IDNA encoding failed for domain.
50    IdnaError(String),
51    /// Domain not in Public Suffix List (when PSL validation enabled).
52    UnknownTld(String),
53    /// Generic parse failure at position.
54    Unexpected { ch: char },
55}
56
57impl Error {
58    /// Create a new error of the given kind at the given byte position.
59    pub(crate) fn new(kind: ErrorKind, position: usize) -> Self {
60        Self { kind, position }
61    }
62
63    /// The kind of error.
64    pub fn kind(&self) -> &ErrorKind {
65        &self.kind
66    }
67
68    /// Byte offset in the input where the error was detected.
69    pub fn position(&self) -> usize {
70        self.position
71    }
72}
73
74impl fmt::Display for Error {
75    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76        match &self.kind {
77            ErrorKind::Empty => write!(f, "empty input"),
78            ErrorKind::MissingAtSign => write!(f, "missing '@' separator"),
79            ErrorKind::EmptyLocalPart => write!(f, "empty local part"),
80            ErrorKind::EmptyDomain => write!(f, "empty domain"),
81            ErrorKind::LocalPartTooLong { len } => {
82                write!(f, "local part too long: {len} octets (max 64)")
83            }
84            ErrorKind::AddressTooLong { len } => {
85                write!(f, "address too long: {len} octets (max 254)")
86            }
87            ErrorKind::DomainLabelTooLong { label, len } => {
88                write!(f, "domain label '{label}' too long: {len} octets (max 63)")
89            }
90            ErrorKind::InvalidLocalPartChar { ch } => {
91                write!(f, "invalid character in local part: '{ch}'")
92            }
93            ErrorKind::InvalidDomainChar { ch } => {
94                write!(f, "invalid character in domain: '{ch}'")
95            }
96            ErrorKind::DomainLabelHyphen => {
97                write!(f, "domain label starts or ends with hyphen")
98            }
99            ErrorKind::DomainNoDot => write!(f, "domain has no dot"),
100            ErrorKind::UnterminatedQuotedString => write!(f, "unterminated quoted string"),
101            ErrorKind::InvalidQuotedPair => write!(f, "invalid quoted-pair escape"),
102            ErrorKind::UnterminatedComment => write!(f, "unterminated comment"),
103            ErrorKind::UnterminatedDomainLiteral => write!(f, "unterminated domain literal"),
104            ErrorKind::InvalidAddressLiteral => {
105                write!(
106                    f,
107                    "domain literal is not a valid IPv4 or IPv6 address literal"
108                )
109            }
110            ErrorKind::IdnaError(msg) => write!(f, "IDNA encoding failed: {msg}"),
111            ErrorKind::UnknownTld(tld) => write!(f, "unknown TLD: .{tld}"),
112            ErrorKind::Unexpected { ch } => {
113                write!(
114                    f,
115                    "unexpected character '{ch}' at position {}",
116                    self.position
117                )
118            }
119        }
120    }
121}
122
123// `core::error::Error` rather than `std::error::Error`: the same trait, reachable
124// without an operating system (stable since Rust 1.81, below this crate's MSRV),
125// so `?` into a `Box<dyn Error>` keeps working for std callers unchanged.
126impl core::error::Error for Error {}
127
128#[cfg(test)]
129mod tests;