Skip to main content

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 everywhere.
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 (requires `psl` feature).
51    Psl,
52    /// Require at least a recognized TLD suffix.
53    Tld,
54}
55
56/// Whether to strip +subaddress tags.
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
58pub enum SubaddressPolicy {
59    /// Keep subaddress in canonical form. Tag is still extracted and accessible.
60    #[default]
61    Preserve,
62    /// Strip subaddress from canonical form. Original still accessible.
63    Strip,
64}
65
66/// Configuration for email address parsing and normalization.
67///
68/// # Example
69///
70/// ```
71/// use structured_email_address::Config;
72///
73/// let config = Config::builder()
74///     .strip_subaddress()
75///     .dots_gmail_only()
76///     .lowercase_all()
77///     .build();
78/// ```
79#[derive(Debug, Clone)]
80pub struct Config {
81    pub(crate) strictness: Strictness,
82    pub(crate) dot_policy: DotPolicy,
83    pub(crate) case_policy: CasePolicy,
84    pub(crate) domain_check: DomainCheck,
85    pub(crate) subaddress: SubaddressPolicy,
86    pub(crate) subaddress_separator: char,
87    pub(crate) check_confusables: bool,
88    pub(crate) allow_domain_literal: bool,
89    pub(crate) allow_display_name: bool,
90    pub(crate) require_tld_dot: bool,
91}
92
93impl Default for Config {
94    fn default() -> Self {
95        Self {
96            strictness: Strictness::Standard,
97            dot_policy: DotPolicy::Preserve,
98            case_policy: CasePolicy::Domain,
99            domain_check: DomainCheck::Syntax,
100            subaddress: SubaddressPolicy::Preserve,
101            subaddress_separator: '+',
102            check_confusables: false,
103            allow_domain_literal: false,
104            allow_display_name: false,
105            require_tld_dot: true,
106        }
107    }
108}
109
110impl Config {
111    /// Create a builder with default settings.
112    pub fn builder() -> ConfigBuilder {
113        ConfigBuilder(Config::default())
114    }
115}
116
117/// Builder for [`Config`].
118pub struct ConfigBuilder(Config);
119
120impl ConfigBuilder {
121    /// Set RFC strictness level.
122    pub fn strictness(mut self, s: Strictness) -> Self {
123        self.0.strictness = s;
124        self
125    }
126
127    /// Strip subaddress from canonical form.
128    pub fn strip_subaddress(mut self) -> Self {
129        self.0.subaddress = SubaddressPolicy::Strip;
130        self
131    }
132
133    /// Keep subaddress in canonical form (default).
134    pub fn preserve_subaddress(mut self) -> Self {
135        self.0.subaddress = SubaddressPolicy::Preserve;
136        self
137    }
138
139    /// Set the subaddress separator character (default: `+`).
140    pub fn subaddress_separator(mut self, sep: char) -> Self {
141        self.0.subaddress_separator = sep;
142        self
143    }
144
145    /// Strip dots only for Gmail/Googlemail.
146    pub fn dots_gmail_only(mut self) -> Self {
147        self.0.dot_policy = DotPolicy::GmailOnly;
148        self
149    }
150
151    /// Always strip dots from local part.
152    pub fn dots_always_strip(mut self) -> Self {
153        self.0.dot_policy = DotPolicy::Always;
154        self
155    }
156
157    /// Preserve dots (default).
158    pub fn dots_preserve(mut self) -> Self {
159        self.0.dot_policy = DotPolicy::Preserve;
160        self
161    }
162
163    /// Lowercase both local part and domain.
164    pub fn lowercase_all(mut self) -> Self {
165        self.0.case_policy = CasePolicy::All;
166        self
167    }
168
169    /// Lowercase domain only (default, RFC-correct).
170    pub fn lowercase_domain(mut self) -> Self {
171        self.0.case_policy = CasePolicy::Domain;
172        self
173    }
174
175    /// Preserve original case.
176    pub fn preserve_case(mut self) -> Self {
177        self.0.case_policy = CasePolicy::Preserve;
178        self
179    }
180
181    /// Validate domain against Public Suffix List (requires `psl` feature).
182    pub fn domain_check_psl(mut self) -> Self {
183        self.0.domain_check = DomainCheck::Psl;
184        self
185    }
186
187    /// Validate domain has a recognized TLD.
188    pub fn domain_check_tld(mut self) -> Self {
189        self.0.domain_check = DomainCheck::Tld;
190        self
191    }
192
193    /// Enable anti-homoglyph confusable detection.
194    pub fn check_confusables(mut self) -> Self {
195        self.0.check_confusables = true;
196        self
197    }
198
199    /// Allow domain literals like `[192.168.1.1]`.
200    pub fn allow_domain_literal(mut self) -> Self {
201        self.0.allow_domain_literal = true;
202        self
203    }
204
205    /// Allow display names like `"John Doe" <john@example.com>`.
206    pub fn allow_display_name(mut self) -> Self {
207        self.0.allow_display_name = true;
208        self
209    }
210
211    /// Do not require a dot in the domain (allow single-label domains).
212    pub fn allow_single_label_domain(mut self) -> Self {
213        self.0.require_tld_dot = false;
214        self
215    }
216
217    /// Build the config.
218    pub fn build(self) -> Config {
219        self.0
220    }
221}