htmlmail/
lib.rs

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/// Letter-Digit
14#[inline]
15fn let_dig(ch: char) -> bool {
16    ch.is_ascii_alphanumeric()
17}
18
19/// Letter-Digit-Hyphen
20#[inline]
21fn let_dig_hyp(ch: char) -> bool {
22    let_dig(ch) || ch == '-'
23}
24
25/// Label (which is like the sub-domain part)
26#[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/// ASCII text
40#[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/// Local part of the email
67#[inline]
68fn local(ch: char) -> bool {
69    atext(ch) || ch == '.'
70}
71
72/// Validate whether an email is well-formed in accordance with the WHATWG specification
73///
74/// # Examples
75///
76/// ```
77/// assert!(htmlmail::is_valid("user@example.com"));
78/// assert!(!htmlmail::is_valid("invalid@"));
79/// ```
80#[inline]
81#[must_use]
82pub fn is_valid(email: &str) -> bool {
83    // Max length SMTP can do.
84    // Later on we validate that the local part may only be 64 octets in length.
85    //
86    // This then, combined with the rest of the parser, leads us to this calculation:
87    // 64 (local) + 1 (@) + 255 = 320
88    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        // Max length from the SMTP RFC
96        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}