structured_email_address/config.rs
1//! Configuration for email address parsing, validation, and normalization.
2//!
3//! The builder pattern allows fine-grained control over every aspect of
4//! email handling — from RFC strictness level to provider-aware normalization.
5
6/// How strictly to validate RFC grammar.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
8pub enum Strictness {
9 /// RFC 5321 envelope: dot-atom only, no comments, no quoted strings, no obs-*.
10 /// Rejects technically valid but practically useless addresses.
11 Strict,
12 /// RFC 5322 header: full grammar including quoted strings, comments, CFWS.
13 /// This is the correct conformant mode.
14 #[default]
15 Standard,
16 /// Standard + obs-local-part, obs-domain for legacy compatibility.
17 Lax,
18}
19
20/// How to handle dots in the local part.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
22pub enum DotPolicy {
23 /// Do not strip dots.
24 #[default]
25 Preserve,
26 /// Strip dots only for known providers that ignore them (Gmail, Googlemail).
27 GmailOnly,
28 /// Always strip dots from local part.
29 Always,
30}
31
32/// How to handle letter case.
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
34pub enum CasePolicy {
35 /// Lowercase domain only (RFC says local part is case-sensitive, but domain is not).
36 #[default]
37 Domain,
38 /// Lowercase both local part and domain. Most providers are case-insensitive.
39 All,
40 /// Preserve original case for local part (domain is always lowercased per RFC 5321).
41 Preserve,
42}
43
44/// How to validate the domain.
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
46pub enum DomainCheck {
47 /// No domain validation beyond RFC syntax.
48 #[default]
49 Syntax,
50 /// Validate against Public Suffix List.
51 ///
52 /// **Requires the `psl` feature.** Falls back to [`Tld`](Self::Tld) check
53 /// when the `psl` feature is disabled.
54 Psl,
55 /// Require that the final label is syntactically TLD-like.
56 ///
57 /// Checks that the last label is at least two ASCII alphabetic characters
58 /// (e.g., `com`, `net`). Does *not* verify against a real TLD list —
59 /// use [`Psl`](Self::Psl) for semantic validation.
60 Tld,
61}
62
63/// Whether to strip +subaddress tags.
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
65pub enum SubaddressPolicy {
66 /// Keep subaddress in canonical form. Tag is still extracted and accessible.
67 #[default]
68 Preserve,
69 /// Strip subaddress from canonical form. Original still accessible.
70 Strip,
71}
72
73/// Configuration for email address parsing and normalization.
74///
75/// # Example
76///
77/// ```
78/// use structured_email_address::Config;
79///
80/// let config = Config::builder()
81/// .strip_subaddress()
82/// .dots_gmail_only()
83/// .lowercase_all()
84/// .build();
85/// ```
86#[derive(Debug, Clone)]
87pub struct Config {
88 pub(crate) strictness: Strictness,
89 pub(crate) dot_policy: DotPolicy,
90 pub(crate) case_policy: CasePolicy,
91 pub(crate) domain_check: DomainCheck,
92 pub(crate) subaddress: SubaddressPolicy,
93 pub(crate) subaddress_separator: char,
94 pub(crate) check_confusables: bool,
95 pub(crate) allow_domain_literal: bool,
96 pub(crate) allow_display_name: bool,
97 pub(crate) require_tld_dot: bool,
98}
99
100impl Default for Config {
101 fn default() -> Self {
102 Self {
103 strictness: Strictness::Standard,
104 dot_policy: DotPolicy::Preserve,
105 case_policy: CasePolicy::Domain,
106 domain_check: DomainCheck::Syntax,
107 subaddress: SubaddressPolicy::Preserve,
108 subaddress_separator: '+',
109 check_confusables: false,
110 allow_domain_literal: false,
111 allow_display_name: false,
112 require_tld_dot: true,
113 }
114 }
115}
116
117impl Config {
118 /// Create a builder with default settings.
119 pub fn builder() -> ConfigBuilder {
120 ConfigBuilder(Config::default())
121 }
122}
123
124/// Builder for [`Config`].
125pub struct ConfigBuilder(Config);
126
127impl ConfigBuilder {
128 /// Set RFC strictness level.
129 pub fn strictness(mut self, s: Strictness) -> Self {
130 self.0.strictness = s;
131 self
132 }
133
134 /// Strip subaddress from canonical form.
135 pub fn strip_subaddress(mut self) -> Self {
136 self.0.subaddress = SubaddressPolicy::Strip;
137 self
138 }
139
140 /// Keep subaddress in canonical form (default).
141 pub fn preserve_subaddress(mut self) -> Self {
142 self.0.subaddress = SubaddressPolicy::Preserve;
143 self
144 }
145
146 /// Set the subaddress separator character (default: `+`).
147 pub fn subaddress_separator(mut self, sep: char) -> Self {
148 self.0.subaddress_separator = sep;
149 self
150 }
151
152 /// Strip dots only for Gmail/Googlemail.
153 pub fn dots_gmail_only(mut self) -> Self {
154 self.0.dot_policy = DotPolicy::GmailOnly;
155 self
156 }
157
158 /// Always strip dots from local part.
159 pub fn dots_always_strip(mut self) -> Self {
160 self.0.dot_policy = DotPolicy::Always;
161 self
162 }
163
164 /// Preserve dots (default).
165 pub fn dots_preserve(mut self) -> Self {
166 self.0.dot_policy = DotPolicy::Preserve;
167 self
168 }
169
170 /// Lowercase both local part and domain.
171 pub fn lowercase_all(mut self) -> Self {
172 self.0.case_policy = CasePolicy::All;
173 self
174 }
175
176 /// Lowercase domain only (default, RFC-correct).
177 pub fn lowercase_domain(mut self) -> Self {
178 self.0.case_policy = CasePolicy::Domain;
179 self
180 }
181
182 /// Preserve original case for local part (domain is always lowercased per RFC 5321).
183 pub fn preserve_case(mut self) -> Self {
184 self.0.case_policy = CasePolicy::Preserve;
185 self
186 }
187
188 /// Validate domain against Public Suffix List (requires `psl` feature).
189 pub fn domain_check_psl(mut self) -> Self {
190 self.0.domain_check = DomainCheck::Psl;
191 self
192 }
193
194 /// Validate domain has a recognized TLD.
195 pub fn domain_check_tld(mut self) -> Self {
196 self.0.domain_check = DomainCheck::Tld;
197 self
198 }
199
200 /// Enable anti-homoglyph confusable detection.
201 pub fn check_confusables(mut self) -> Self {
202 self.0.check_confusables = true;
203 self
204 }
205
206 /// Allow domain literals like `[192.168.1.1]`.
207 pub fn allow_domain_literal(mut self) -> Self {
208 self.0.allow_domain_literal = true;
209 self
210 }
211
212 /// Allow display names like `"John Doe" <john@example.com>`.
213 pub fn allow_display_name(mut self) -> Self {
214 self.0.allow_display_name = true;
215 self
216 }
217
218 /// Do not require a dot in the domain (allow single-label domains).
219 pub fn allow_single_label_domain(mut self) -> Self {
220 self.0.require_tld_dot = false;
221 self
222 }
223
224 /// Syntax-only domain check (default). Resets from `Psl`/`Tld` back to syntax.
225 pub fn domain_check_syntax(mut self) -> Self {
226 self.0.domain_check = DomainCheck::Syntax;
227 self
228 }
229
230 /// Build the config.
231 pub fn build(self) -> Config {
232 self.0
233 }
234}