1#![doc = include_str!("../README.md")]
2#![no_std]
3#![forbid(unsafe_code)]
4#![deny(missing_docs)]
5#![warn(clippy::all, clippy::pedantic)]
6
7use winnow::{
8 Parser,
9 combinator::{opt, repeat},
10 token::take_while,
11};
12
13#[inline]
15fn let_dig(ch: char) -> bool {
16 ch.is_ascii_alphanumeric()
17}
18
19#[inline]
21fn let_dig_hyp(ch: char) -> bool {
22 let_dig(ch) || ch == '-'
23}
24
25#[inline]
27fn label(input: &mut &str) -> winnow::ModalResult<()> {
28 take_while(1..=63, let_dig_hyp)
29 .verify(|string: &str| {
30 let first = string.chars().next().unwrap();
31 let last = string.chars().last().unwrap();
32 let_dig(first) && let_dig(last)
33 })
34 .parse_next(input)?;
35
36 Ok(())
37}
38
39#[inline]
41fn atext(ch: char) -> bool {
42 ch.is_ascii_alphanumeric()
43 || matches!(
44 ch,
45 '!' | '#'
46 | '$'
47 | '%'
48 | '&'
49 | '\''
50 | '*'
51 | '+'
52 | '-'
53 | '/'
54 | '='
55 | '?'
56 | '^'
57 | '_'
58 | '`'
59 | '{'
60 | '|'
61 | '}'
62 | '~'
63 )
64}
65
66#[inline]
68fn local(ch: char) -> bool {
69 atext(ch) || ch == '.'
70}
71
72#[inline]
81#[must_use]
82pub fn is_valid(email: &str) -> bool {
83 if email.len() > 320 {
89 return false;
90 }
91
92 let mut email = email;
93
94 let result: winnow::ModalResult<(&str, _, _, Option<()>)> = winnow::seq!(
95 take_while(1..=64, local),
97 "@",
98 label,
99 opt(repeat(1.., winnow::seq!(".", label)))
100 )
101 .parse_next(&mut email);
102
103 result.is_ok() && email.is_empty()
104}