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    /// Multiple `@` outside of quoted local-part.
20    MultipleAtSigns,
21    /// Local part is empty (nothing before `@`).
22    EmptyLocalPart,
23    /// Domain is empty (nothing after `@`).
24    EmptyDomain,
25    /// Local part exceeds 64 octets (RFC 5321 §4.5.3.1.1).
26    LocalPartTooLong { len: usize },
27    /// Total address exceeds 254 octets (RFC 5321 §4.5.3.1.3).
28    AddressTooLong { len: usize },
29    /// Domain label exceeds 63 octets (RFC 1035 §2.3.4).
30    DomainLabelTooLong { label: String, len: usize },
31    /// Invalid character in local part.
32    InvalidLocalPartChar { ch: char },
33    /// Invalid character in domain.
34    InvalidDomainChar { ch: char },
35    /// Domain label starts or ends with hyphen.
36    DomainLabelHyphen,
37    /// Domain has no dot (single label, not a valid internet domain).
38    DomainNoDot,
39    /// Unterminated quoted string.
40    UnterminatedQuotedString,
41    /// Invalid quoted-pair sequence.
42    InvalidQuotedPair,
43    /// Unterminated comment.
44    UnterminatedComment,
45    /// Unterminated domain literal `[...]`.
46    UnterminatedDomainLiteral,
47    /// IDNA encoding failed for domain.
48    IdnaError(String),
49    /// Domain not in Public Suffix List (when PSL validation enabled).
50    UnknownTld(String),
51    /// Generic parse failure at position.
52    Unexpected { ch: char },
53}
54
55impl Error {
56    /// Create a new error of the given kind at the given byte position.
57    pub(crate) fn new(kind: ErrorKind, position: usize) -> Self {
58        Self { kind, position }
59    }
60
61    /// The kind of error.
62    pub fn kind(&self) -> &ErrorKind {
63        &self.kind
64    }
65
66    /// Byte offset in the input where the error was detected.
67    pub fn position(&self) -> usize {
68        self.position
69    }
70}
71
72impl fmt::Display for Error {
73    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74        match &self.kind {
75            ErrorKind::Empty => write!(f, "empty input"),
76            ErrorKind::MissingAtSign => write!(f, "missing '@' separator"),
77            ErrorKind::MultipleAtSigns => write!(f, "multiple unquoted '@' signs"),
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::IdnaError(msg) => write!(f, "IDNA error: {msg}"),
104            ErrorKind::UnknownTld(tld) => write!(f, "unknown TLD: .{tld}"),
105            ErrorKind::Unexpected { ch } => {
106                write!(
107                    f,
108                    "unexpected character '{ch}' at position {}",
109                    self.position
110                )
111            }
112        }
113    }
114}
115
116impl std::error::Error for Error {}