|
20 | 20 | import bisq.core.locale.Res; |
21 | 21 | import bisq.core.util.validation.InputValidator; |
22 | 22 |
|
| 23 | +import java.util.regex.Matcher; |
| 24 | +import java.util.regex.Pattern; |
| 25 | + |
23 | 26 | /* |
24 | | - * Mail addresses consist of localPart @ domainPart |
| 27 | + * Email addresses consist of localPart @ domainPart |
25 | 28 | * |
26 | 29 | * Local part: |
27 | | - * May contain lots of symbols A-Za-z0-9.!#$%&'*+-/=?^_`{|}~ |
28 | | - * but cannot begin or end with a dot (.) |
29 | | - * between double quotes many more symbols are allowed: |
30 | | - * "(),:;<>@[\] (ASCII: 32, 34, 40, 41, 44, 58, 59, 60, 62, 64, 91–93) |
| 30 | + * - Practical subset allowed: A-Za-z0-9._%+- (max 64 characters) |
| 31 | + * - Leading, trailing, and consecutive dots are not allowed |
| 32 | + * - Quoted strings and exotic RFC constructs are intentionally excluded |
31 | 33 | * |
32 | 34 | * Domain part: |
33 | | - * Consists of name dot TLD |
34 | | - * name can but usually doesn't (compatibility reasons) contain non-ASCII |
35 | | - * symbols. |
36 | | - * TLD is at least two letters long |
| 35 | + * - Labels separated by dots, each 1..63 characters |
| 36 | + * - Each label starts and ends with alphanumeric character |
| 37 | + * - Hyphens allowed internally |
| 38 | + * - At least one dot required |
| 39 | + * - Last label (TLD) must be at least 2 characters, letters-only (or punycode starting with "xn--") |
| 40 | + * |
| 41 | + * Conservative validator: |
| 42 | + * - Accepts common, interoperable email addresses |
| 43 | + * - Rejects edge-case RFC addresses that break UIs, JSON, or payment rails |
37 | 44 | */ |
38 | 45 | public final class EmailValidator extends InputValidator { |
39 | 46 |
|
40 | | - /////////////////////////////////////////////////////////////////////////////////////////// |
41 | | - // Public methods |
42 | | - /////////////////////////////////////////////////////////////////////////////////////////// |
| 47 | + // Precompiled regex pattern for efficient validation |
| 48 | + // Local part: labels separated by single dots (prevents leading/trailing/consecutive dots) |
| 49 | + // Domain part: labels with internal hyphens only, at least one dot |
| 50 | + private static final Pattern SIMPLE_EMAIL_PATTERN = Pattern.compile( |
| 51 | + "^[A-Za-z0-9_%+\\-]+(?:\\.[A-Za-z0-9_%+\\-]+)*@" + // local-part |
| 52 | + "[A-Za-z0-9](?:[A-Za-z0-9\\-]{0,61}[A-Za-z0-9])?" + // first domain label |
| 53 | + "(?:\\.[A-Za-z0-9](?:[A-Za-z0-9\\-]{0,61}[A-Za-z0-9])?)+$" // additional labels |
| 54 | + ); |
43 | 55 |
|
44 | | - private final ValidationResult invalidAddress = new ValidationResult(false, Res.get("validation.email.invalidAddress")); |
| 56 | + private static final ValidationResult INVALID_ADDRESS = new ValidationResult(false, Res.get("validation.email.invalidAddress")); |
45 | 57 |
|
46 | 58 | @Override |
47 | 59 | public ValidationResult validate(String input) { |
48 | | - if (input == null || input.length() < 6 || input.length() > 100) // shortest address is l@d.cc, max length 100 |
49 | | - return invalidAddress; |
50 | | - String[] subStrings; |
51 | | - String local, domain; |
52 | | - |
53 | | - subStrings = input.split("@", -1); |
54 | | - |
55 | | - if (subStrings.length == 1) // address does not contain '@' |
56 | | - return invalidAddress; |
57 | | - if (subStrings.length > 2) // multiple @'s included -> check for valid double quotes |
58 | | - if (!checkForValidQuotes(subStrings)) // around @'s -> "..@..@.." and concatenate local part |
59 | | - return invalidAddress; |
60 | | - local = subStrings[0]; |
61 | | - domain = subStrings[subStrings.length - 1]; |
62 | | - |
63 | | - if (local.isEmpty()) |
64 | | - return invalidAddress; |
65 | | - |
66 | | - // local part cannot begin or end with '.' |
67 | | - if (local.startsWith(".") || local.endsWith(".")) |
68 | | - return invalidAddress; |
69 | | - |
70 | | - String[] splitDomain = domain.split("\\.", -1); // '.' is a regex in java and has to be escaped |
71 | | - String tld = splitDomain[splitDomain.length - 1]; |
72 | | - if (splitDomain.length < 2) |
73 | | - return invalidAddress; |
74 | | - |
75 | | - if (splitDomain[0] == null || splitDomain[0].isEmpty()) |
76 | | - return invalidAddress; |
77 | | - |
78 | | - // TLD length is at least two |
79 | | - if (tld.length() < 2) |
80 | | - return invalidAddress; |
81 | | - |
82 | | - // TLD is letters only |
83 | | - for (int k = 0; k < tld.length(); k++) |
84 | | - if (!Character.isLetter(tld.charAt(k))) |
85 | | - return invalidAddress; |
86 | | - return new ValidationResult(true); |
87 | | - } |
88 | | - |
89 | | - /////////////////////////////////////////////////////////////////////////////////////////// |
90 | | - // Private methods |
91 | | - /////////////////////////////////////////////////////////////////////////////////////////// |
92 | | - |
93 | | - private boolean checkForValidQuotes(String[] subStrings) { |
94 | | - int length = subStrings.length - 2; // is index on last substring of local part |
95 | | - |
96 | | - // check for odd number of double quotes before first and after last '@' |
97 | | - if ((subStrings[0].split("\"", -1).length % 2 == 1) || (subStrings[length].split("\"", -1).length % 2 == 1)) |
98 | | - return false; |
99 | | - for (int k = 1; k < length; k++) { |
100 | | - if (subStrings[k].split("\"", -1).length % 2 == 0) |
101 | | - return false; |
| 60 | + if (input == null) return INVALID_ADDRESS; |
| 61 | + |
| 62 | + String email = input.trim(); |
| 63 | + |
| 64 | + // Practical length limits: shortest plausible a@b.cc (6), longest allowed by spec 254 |
| 65 | + if (email.length() < 6 || email.length() > 254) return INVALID_ADDRESS; |
| 66 | + |
| 67 | + // Apply regex pattern |
| 68 | + Matcher matcher = SIMPLE_EMAIL_PATTERN.matcher(email); |
| 69 | + if (!matcher.matches()) return INVALID_ADDRESS; |
| 70 | + |
| 71 | + // Check TLD (last label after final dot) |
| 72 | + int lastDot = email.lastIndexOf('.'); |
| 73 | + if (lastDot < 0 || lastDot == email.length() - 1) return INVALID_ADDRESS; |
| 74 | + |
| 75 | + String tld = email.substring(lastDot + 1); |
| 76 | + |
| 77 | + // TLD length must be at least 2 characters |
| 78 | + if (tld.length() < 2) return INVALID_ADDRESS; |
| 79 | + |
| 80 | + // allow punycode TLDs for IDN support |
| 81 | + if (tld.startsWith("xn--")) { |
| 82 | + // basic sanity check for punycode TLD |
| 83 | + if (tld.length() > 63) return INVALID_ADDRESS; |
| 84 | + for (int i = 4; i < tld.length(); i++) { |
| 85 | + char c = tld.charAt(i); |
| 86 | + if (!(Character.isLetterOrDigit(c) || c == '-')) return INVALID_ADDRESS; |
| 87 | + } |
| 88 | + } else { |
| 89 | + // ASCII letters only |
| 90 | + for (int i = 0; i < tld.length(); i++) { |
| 91 | + if (!Character.isLetter(tld.charAt(i))) return INVALID_ADDRESS; |
| 92 | + } |
102 | 93 | } |
103 | 94 |
|
104 | | - String patchLocal = ""; |
105 | | - for (int k = 0; k <= length; k++) // remember: length is last index not array length |
106 | | - patchLocal = patchLocal.concat(subStrings[k]); // @'s are not reinstalled, since not needed for further checks |
107 | | - subStrings[0] = patchLocal; |
108 | | - return true; |
| 95 | + return new ValidationResult(true); |
109 | 96 | } |
110 | 97 | } |
0 commit comments