Line data Source code
1 : // Copyright 2026 The Authors
2 : //
3 : // Licensed under the Apache License, Version 2.0 (the "License");
4 : // you may not use this file except in compliance with the License.
5 : // You may obtain a copy of the License at
6 : //
7 : // https://www.apache.org/licenses/LICENSE-2.0
8 : //
9 : // Unless required by applicable law or agreed to in writing, software
10 : // distributed under the License is distributed on an "AS IS" BASIS,
11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 : // See the License for the specific language governing permissions and
13 : // limitations under the License.
14 :
15 : /// An email address.
16 : ///
17 : /// See:
18 : ///
19 : /// - [RFC 5321](https://www.rfc-editor.org/rfc/rfc5321.html#section-4.1.2)
20 : /// - [RFC 5322](https://www.rfc-editor.org/rfc/rfc5322#section-3.4.1)
21 : class Email {
22 : final String localPart;
23 : final String domain;
24 :
25 2 : Email._({required this.localPart, required this.domain});
26 :
27 0 : @override
28 0 : String toString() => '$localPart@$domain';
29 :
30 0 : Uri toUri() => Uri(scheme: 'mailto', path: toString());
31 :
32 : static const _localPartRegex =
33 : r"(?<localPart>[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+)";
34 :
35 : static const _domainRegex =
36 : r"(?<domain>[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*)";
37 :
38 : static const _addrSpec = '$_localPartRegex@$_domainRegex';
39 :
40 6 : static final _emailRegex = RegExp('^$_addrSpec\$');
41 :
42 : /// Parse [input], returning a valid email address.
43 : ///
44 : /// This is based on the HTML Living Standard, section 4.10.5 The input element.
45 : ///
46 : /// See: https://html.spec.whatwg.org/multipage/input.html#email-state-(type=email)
47 : ///
48 : /// The approach is a pragmatic subset of RFC 5322.
49 2 : static Email? tryParse(String input, {int maxInputLength = 30}) {
50 4 : if (input.length > maxInputLength) {
51 : return null;
52 : }
53 :
54 4 : var match = _emailRegex.firstMatch(input);
55 : if (match == null) {
56 : return null;
57 : }
58 2 : var localPart = match.namedGroup('localPart');
59 2 : var domain = match.namedGroup('domain');
60 :
61 : if (localPart == null || domain == null) {
62 : return null;
63 : }
64 2 : return Email._(localPart: localPart, domain: domain);
65 : }
66 :
67 4 : static bool isValid(String value) => tryParse(value) != null;
68 : }
|