Skip to main content

structured_email_address/
error.rs

1//! Error types for email address parsing and validation.
2
3use core::fmt;
4
5/// Error returned when parsing or validating an email address fails.
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct Error {
8    kind: ErrorKind,
9    position: usize,
10}
11
12/// The specific kind of error that occurred.
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum ErrorKind {
15    /// Input is empty or whitespace-only.
16    Empty,
17    /// Missing `@` separator.
18    MissingAtSign,
19    /// Local part is empty (nothing before `@`).
20    EmptyLocalPart,
21    /// Domain is empty (nothing after `@`).
22    EmptyDomain,
23    /// Local part exceeds 64 octets (RFC 5321 §4.5.3.1.1).
24    LocalPartTooLong { len: usize },
25    /// Total address exceeds 254 octets (RFC 5321 §4.5.3.1.3).
26    AddressTooLong { len: usize },
27    /// Domain label exceeds 63 octets (RFC 1035 §2.3.4).
28    DomainLabelTooLong { label: String, len: usize },
29    /// Invalid character in local part.
30    InvalidLocalPartChar { ch: char },
31    /// Invalid character in domain.
32    InvalidDomainChar { ch: char },
33    /// Domain label starts or ends with hyphen.
34    DomainLabelHyphen,
35    /// Domain has no dot (single label, not a valid internet domain).
36    DomainNoDot,
37    /// Unterminated quoted string.
38    UnterminatedQuotedString,
39    /// Invalid quoted-pair sequence.
40    InvalidQuotedPair,
41    /// Unterminated comment.
42    UnterminatedComment,
43    /// Unterminated domain literal `[...]`.
44    UnterminatedDomainLiteral,
45    /// Domain literal `[...]` is not a valid IPv4 or `IPv6:` address literal
46    /// (RFC 5321 §4.1.3). General domain literals are not accepted.
47    InvalidAddressLiteral,
48    /// IDNA encoding failed for domain.
49    IdnaError(String),
50    /// Domain not in Public Suffix List (when PSL validation enabled).
51    UnknownTld(String),
52    /// Generic parse failure at position.
53    Unexpected { ch: char },
54}
55
56impl Error {
57    /// Create a new error of the given kind at the given byte position.
58    pub(crate) fn new(kind: ErrorKind, position: usize) -> Self {
59        Self { kind, position }
60    }
61
62    /// The kind of error.
63    pub fn kind(&self) -> &ErrorKind {
64        &self.kind
65    }
66
67    /// Byte offset in the input where the error was detected.
68    pub fn position(&self) -> usize {
69        self.position
70    }
71}
72
73impl fmt::Display for Error {
74    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75        match &self.kind {
76            ErrorKind::Empty => write!(f, "empty input"),
77            ErrorKind::MissingAtSign => write!(f, "missing '@' separator"),
78            ErrorKind::EmptyLocalPart => write!(f, "empty local part"),
79            ErrorKind::EmptyDomain => write!(f, "empty domain"),
80            ErrorKind::LocalPartTooLong { len } => {
81                write!(f, "local part too long: {len} octets (max 64)")
82            }
83            ErrorKind::AddressTooLong { len } => {
84                write!(f, "address too long: {len} octets (max 254)")
85            }
86            ErrorKind::DomainLabelTooLong { label, len } => {
87                write!(f, "domain label '{label}' too long: {len} octets (max 63)")
88            }
89            ErrorKind::InvalidLocalPartChar { ch } => {
90                write!(f, "invalid character in local part: '{ch}'")
91            }
92            ErrorKind::InvalidDomainChar { ch } => {
93                write!(f, "invalid character in domain: '{ch}'")
94            }
95            ErrorKind::DomainLabelHyphen => {
96                write!(f, "domain label starts or ends with hyphen")
97            }
98            ErrorKind::DomainNoDot => write!(f, "domain has no dot"),
99            ErrorKind::UnterminatedQuotedString => write!(f, "unterminated quoted string"),
100            ErrorKind::InvalidQuotedPair => write!(f, "invalid quoted-pair escape"),
101            ErrorKind::UnterminatedComment => write!(f, "unterminated comment"),
102            ErrorKind::UnterminatedDomainLiteral => write!(f, "unterminated domain literal"),
103            ErrorKind::InvalidAddressLiteral => {
104                write!(
105                    f,
106                    "domain literal is not a valid IPv4 or IPv6 address literal"
107                )
108            }
109            ErrorKind::IdnaError(msg) => write!(f, "IDNA encoding failed: {msg}"),
110            ErrorKind::UnknownTld(tld) => write!(f, "unknown TLD: .{tld}"),
111            ErrorKind::Unexpected { ch } => {
112                write!(
113                    f,
114                    "unexpected character '{ch}' at position {}",
115                    self.position
116                )
117            }
118        }
119    }
120}
121
122impl std::error::Error for Error {}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    #[test]
129    fn display_and_accessors_cover_all_kinds() {
130        // Every ErrorKind must render a non-empty message, and the accessors
131        // must round-trip the kind and position.
132        let kinds = [
133            ErrorKind::Empty,
134            ErrorKind::MissingAtSign,
135            ErrorKind::EmptyLocalPart,
136            ErrorKind::EmptyDomain,
137            ErrorKind::LocalPartTooLong { len: 65 },
138            ErrorKind::AddressTooLong { len: 300 },
139            ErrorKind::DomainLabelTooLong {
140                label: "x".to_string(),
141                len: 64,
142            },
143            ErrorKind::InvalidLocalPartChar { ch: '(' },
144            ErrorKind::InvalidDomainChar { ch: '[' },
145            ErrorKind::DomainLabelHyphen,
146            ErrorKind::DomainNoDot,
147            ErrorKind::UnterminatedQuotedString,
148            ErrorKind::InvalidQuotedPair,
149            ErrorKind::UnterminatedComment,
150            ErrorKind::UnterminatedDomainLiteral,
151            ErrorKind::InvalidAddressLiteral,
152            ErrorKind::IdnaError("boom".to_string()),
153            ErrorKind::UnknownTld("zzz".to_string()),
154            ErrorKind::Unexpected { ch: '!' },
155        ];
156        for (pos, kind) in kinds.into_iter().enumerate() {
157            let err = Error::new(kind.clone(), pos);
158            assert!(!err.to_string().is_empty(), "empty Display for {kind:?}");
159            assert_eq!(err.kind(), &kind);
160            assert_eq!(err.position(), pos);
161        }
162    }
163}