3
3
import java .util .Arrays ;
4
4
import java .util .Collection ;
5
5
import java .util .Collections ;
6
- import java .util .HashSet ;
6
+ import java .util .HashMap ;
7
+ import java .util .Map ;
7
8
import java .util .Optional ;
8
- import java .util .Set ;
9
9
import java .util .StringJoiner ;
10
10
import java .util .function .Predicate ;
11
11
import java .util .stream .Collectors ;
@@ -34,9 +34,9 @@ public final class EmailValidator {
34
34
= ValidationRules ::disallowIpDomain ;
35
35
private static final Predicate <Email > REQUIRE_TOP_LEVEL_DOMAIN_PREDICATE
36
36
= ValidationRules ::requireTopLevelDomain ;
37
- private static final Predicate <Email > DISALLOW_EXPLICIT_SOURCE_ROUTING
37
+ private static final Predicate <Email > DISALLOW_EXPLICIT_SOURCE_ROUTING_PREDICATE
38
38
= ValidationRules ::disallowExplicitSourceRouting ;
39
- private static final Predicate <Email > DISALLOW_QUOTED_IDENTIFIERS
39
+ private static final Predicate <Email > DISALLOW_QUOTED_IDENTIFIERS_PREDICATE
40
40
= ValidationRules ::disallowQuotedIdentifiers ;
41
41
private static final Predicate <Email > DISALLOW_RESERVED_DOMAINS_PREDICATE
42
42
= ValidationRules ::disallowReservedDomains ;
@@ -47,14 +47,36 @@ public final class EmailValidator {
47
47
private static final Predicate <Email > REQUIRE_ASCII_PREDICATE
48
48
= ValidationRules ::requireAscii ;
49
49
50
- private final Set <Predicate <Email >> validationPredicates ;
50
+ private final Map <Predicate <Email >, FailureReason > validationPredicates ;
51
51
52
- EmailValidator (Set <Predicate <Email >> validationPredicates ) {
53
- this .validationPredicates = Collections .unmodifiableSet (validationPredicates );
52
+ EmailValidator (Map <Predicate <Email >, FailureReason > validationPredicates ) {
53
+ this .validationPredicates = Collections .unmodifiableMap (validationPredicates );
54
54
}
55
55
56
56
EmailValidator () {
57
- this (new HashSet <>());
57
+ this (new HashMap <>());
58
+ }
59
+
60
+ /**
61
+ * Create a new {@code EmailValidator} with all rules from the current instance and the
62
+ * additional provided custom validation rules.
63
+ *
64
+ * <p>Example usage:
65
+ *
66
+ * <pre>
67
+ * validator.withRules(Map.of(
68
+ * email -> email.domain().startsWith("test"), new FailureReason("MUST_START_WITH_TEST"),
69
+ * email -> email.localPart.contains("hello"), new FailureReason("MUST_CONTAIN_HELLO"));
70
+ * </pre>
71
+ *
72
+ * @param rules a collection of requirements that make a valid email address
73
+ * @return the new {@code EmailValidator} instance
74
+ */
75
+ public EmailValidator withRules (Map <Predicate <Email >, FailureReason > rules ) {
76
+ Map <Predicate <Email >, FailureReason > ruleMap = new HashMap <>(validationPredicates );
77
+ ruleMap .putAll (rules );
78
+
79
+ return new EmailValidator (ruleMap );
58
80
}
59
81
60
82
/**
@@ -73,10 +95,8 @@ public final class EmailValidator {
73
95
* @return the new {@code EmailValidator} instance
74
96
*/
75
97
public EmailValidator withRules (Collection <Predicate <Email >> rules ) {
76
- Set <Predicate <Email >> ruleSet = new HashSet <>(validationPredicates );
77
- ruleSet .addAll (rules );
78
-
79
- return new EmailValidator (ruleSet );
98
+ return withRules (rules .stream ()
99
+ .collect (Collectors .toMap (p -> p , p -> FailureReason .FAILED_CUSTOM_VALIDATION )));
80
100
}
81
101
82
102
/**
@@ -94,79 +114,116 @@ public EmailValidator withRules(Collection<Predicate<Email>> rules) {
94
114
* @return the new {@code EmailValidator} instance
95
115
*/
96
116
public EmailValidator withRule (Predicate <Email > rule ) {
97
- return withRules (Collections .singletonList (rule ));
117
+ return withRules (Collections .singletonMap (rule , FailureReason . FAILED_CUSTOM_VALIDATION ));
98
118
}
99
119
100
120
/**
101
- * Create a new {@code EmailValidator} with all rules from the current instance and the
121
+ * Create a new {@code EmailValidator} with all rules from the current instance and an
122
+ * additional provided custom validation rule.
123
+ *
124
+ * <p>Example usage:
125
+ *
126
+ * <pre>
127
+ * validator.withRule(
128
+ * email -> email.domain().startsWith("test"),
129
+ * new FailureReason("DOES_NOT_START_WITH_TEST"));
130
+ * </pre>
131
+ *
132
+ * @param rule the requirement for a valid email address. This must be a {@link Predicate} that
133
+ * accepts an {@link Email} object.
134
+ * @param failureReason the {@link FailureReason} to return in the {@link EmailValidationResult}
135
+ * if an email fails to pass this rule.
136
+ * @return the new {@code EmailValidator} instance
137
+ */
138
+ public EmailValidator withRule (Predicate <Email > rule , FailureReason failureReason ) {
139
+ return withRules (Collections .singletonMap (rule , failureReason ));
140
+ }
141
+
142
+ /**
143
+ * <p>Create a new {@code EmailValidator} with all rules from the current instance and the
102
144
* {@link ValidationRules#disallowIpDomain(Email)} rule.
103
- * Email addresses that have an IP address for a domain will fail validation.
145
+ * Email addresses that have an IP address for a domain will fail validation with
146
+ * {@link FailureReason#CONTAINS_IP_DOMAIN}.
104
147
*
105
148
* <p>For example, {@code "sample@[1.2.3.4]"} would be invalid.
106
149
*
107
150
* @return the new {@code EmailValidator} instance
108
151
*/
109
152
public EmailValidator disallowIpDomain () {
110
- return withRule (DISALLOW_IP_DOMAIN_PREDICATE );
153
+ return withRule (
154
+ DISALLOW_IP_DOMAIN_PREDICATE ,
155
+ FailureReason .CONTAINS_IP_DOMAIN );
111
156
}
112
157
113
158
/**
114
- * Create a new {@code EmailValidator} with all rules from the current instance and the
159
+ * <p> Create a new {@code EmailValidator} with all rules from the current instance and the
115
160
* {@link ValidationRules#requireTopLevelDomain(Email)} rule.
116
- * Email addresses that do not have a top level domain will fail validation.
161
+ * Email addresses that do not have a top level domain will fail validation
162
+ * with {@link FailureReason#MISSING_TOP_LEVEL_DOMAIN}.
117
163
*
118
164
* <p>For example, {@code "sample@mailserver"} would be invalid.
119
165
*
120
166
* @return the new {@code EmailValidator} instance
121
167
*/
122
168
public EmailValidator requireTopLevelDomain () {
123
- return withRule (REQUIRE_TOP_LEVEL_DOMAIN_PREDICATE );
169
+ return withRule (
170
+ REQUIRE_TOP_LEVEL_DOMAIN_PREDICATE ,
171
+ FailureReason .MISSING_TOP_LEVEL_DOMAIN );
124
172
}
125
173
126
174
/**
127
175
* Create a new {@code EmailValidator} with all rules from the current instance and the
128
176
* {@link ValidationRules#disallowExplicitSourceRouting(Email)} rule.
129
- * Email addresses that have explicit source routing will fail validation.
177
+ * Email addresses that have explicit source routing will fail validation with
178
+ * {@link FailureReason#CONTAINS_EXPLICIT_SOURCE_ROUTING}.
130
179
*
131
180
* <p>For example, {@code "@1st.relay,@2nd.relay:[email protected] "} would be invalid.
132
181
*
133
182
* @return the new {@code EmailValidator} instance
134
183
*/
135
184
public EmailValidator disallowExplicitSourceRouting () {
136
- return withRule (DISALLOW_EXPLICIT_SOURCE_ROUTING );
185
+ return withRule (
186
+ DISALLOW_EXPLICIT_SOURCE_ROUTING_PREDICATE ,
187
+ FailureReason .CONTAINS_EXPLICIT_SOURCE_ROUTING );
137
188
}
138
189
139
190
/**
140
191
* Create a new {@code EmailValidator} with all rules from the current instance and the
141
192
* {@link ValidationRules#disallowQuotedIdentifiers(Email)} rule.
142
- * Email addresses that have quoted identifiers will fail validation.
193
+ * Email addresses that have quoted identifiers will fail validation with
194
+ * {@link FailureReason#CONTAINS_QUOTED_IDENTIFIER}.
143
195
*
144
196
* <p>For example, {@code "John Smith <[email protected] >"} would be invalid.
145
197
*
146
198
* @return the new {@code EmailValidator} instance
147
199
*/
148
200
public EmailValidator disallowQuotedIdentifiers () {
149
- return withRule (DISALLOW_QUOTED_IDENTIFIERS );
201
+ return withRule (
202
+ DISALLOW_QUOTED_IDENTIFIERS_PREDICATE ,
203
+ FailureReason .CONTAINS_QUOTED_IDENTIFIER );
150
204
}
151
205
152
206
/**
153
207
* Create a new {@code EmailValidator} with all rules from the current instance and the
154
208
* {@link ValidationRules#disallowReservedDomains(Email)} rule.
155
- * Email addresses that have a reserved domain according to RFC 2606 will fail validation.
209
+ * Email addresses that have a reserved domain according to RFC 2606 will fail validation
210
+ * with {@link FailureReason#CONTAINS_RESERVED_DOMAIN}.
156
211
*
157
212
* <p>For example, {@code "[email protected] "} would be invalid.
158
213
*
159
214
* @return the new {@code EmailValidator} instance
160
215
*/
161
216
public EmailValidator disallowReservedDomains () {
162
- return withRule (DISALLOW_RESERVED_DOMAINS_PREDICATE );
217
+ return withRule (
218
+ DISALLOW_RESERVED_DOMAINS_PREDICATE ,
219
+ FailureReason .CONTAINS_RESERVED_DOMAIN );
163
220
}
164
221
165
222
/**
166
223
* Create a new {@code EmailValidator} with all rules from the current instance and the
167
- * {@link ValidationRules#requireOnlyTopLevelDomains(Email, Set)} rule.
224
+ * {@link ValidationRules#requireOnlyTopLevelDomains(Email, java.util. Set)} rule.
168
225
* Email addresses that have top level domains other than those provided will
169
- * fail validation.
226
+ * fail validation with {@link FailureReason#INVALID_TOP_LEVEL_DOMAIN} .
170
227
*
171
228
* <p>For example, if you require only {@link TopLevelDomain#DOT_COM}, the email address
172
229
* {@code "[email protected] "} would be invalid.
@@ -175,28 +232,33 @@ public EmailValidator disallowReservedDomains() {
175
232
* @return the new {@code EmailValidator} instance
176
233
*/
177
234
public EmailValidator requireOnlyTopLevelDomains (TopLevelDomain ... allowed ) {
178
- return withRule (email -> ValidationRules .requireOnlyTopLevelDomains (
179
- email , Arrays .stream (allowed ).collect (Collectors .toSet ())));
235
+ return withRule (
236
+ email -> ValidationRules .requireOnlyTopLevelDomains (
237
+ email , Arrays .stream (allowed ).collect (Collectors .toSet ())),
238
+ FailureReason .INVALID_TOP_LEVEL_DOMAIN );
180
239
}
181
240
182
241
/**
183
242
* Create a new {@code EmailValidator} with all rules from the current instance and the
184
243
* {@link ValidationRules#disallowObsoleteWhitespace(Email)} rule.
185
244
* Email addresses that have obsolete folding whitespace according to RFC 2822 will fail
186
- * validation.
245
+ * validation with {@link FailureReason#CONTAINS_OBSOLETE_WHITESPACE} .
187
246
*
188
247
* <p>For example, {@code "1234 @ local(blah) .com"} would be invalid.
189
248
*
190
249
* @return the new {@code EmailValidator} instance
191
250
*/
192
251
public EmailValidator disallowObsoleteWhitespace () {
193
- return withRule (DISALLOW_OBSOLETE_WHITESPACE_PREDICATE );
252
+ return withRule (
253
+ DISALLOW_OBSOLETE_WHITESPACE_PREDICATE ,
254
+ FailureReason .CONTAINS_OBSOLETE_WHITESPACE );
194
255
}
195
256
196
257
/**
197
258
* Create a new {@code EmailValidator} with all rules from the current instance and the
198
259
* {@link ValidationRules#requireValidMXRecord(Email)} rule.
199
- * Email addresses that have a domain without a valid MX record will fail validation.
260
+ * Email addresses that have a domain without a valid MX record will fail validation with
261
+ * {@link FailureReason#INVALID_MX_RECORD}.
200
262
*
201
263
* <p><strong>NOTE: Adding this rule to your EmailValidator may increase
202
264
* the amount of time it takes to validate email addresses, as the default initial timeout is
@@ -206,13 +268,16 @@ public EmailValidator disallowObsoleteWhitespace() {
206
268
* @return the new {@code EmailValidator} instance
207
269
*/
208
270
public EmailValidator requireValidMXRecord () {
209
- return withRule (REQUIRE_VALID_MX_RECORD_PREDICATE );
271
+ return withRule (
272
+ REQUIRE_VALID_MX_RECORD_PREDICATE ,
273
+ FailureReason .INVALID_MX_RECORD );
210
274
}
211
275
212
276
/**
213
277
* Create a new {@code EmailValidator} with all rules from the current instance and the
214
278
* {@link ValidationRules#requireValidMXRecord(Email, int, int)} rule.
215
- * Email addresses that have a domain without a valid MX record will fail validation.
279
+ * Email addresses that have a domain without a valid MX record will fail validation with
280
+ * {@link FailureReason#INVALID_MX_RECORD}.
216
281
*
217
282
* <p>This method allows you to customize the timeout and retries for performing DNS lookups.
218
283
* The initial timeout is supplied in milliseconds, and the number of retries indicate how many
@@ -224,22 +289,25 @@ public EmailValidator requireValidMXRecord() {
224
289
* @return the new {@code EmailValidator} instance
225
290
*/
226
291
public EmailValidator requireValidMXRecord (int initialTimeout , int numRetries ) {
227
- return withRule (email ->
228
- ValidationRules .requireValidMXRecord (email , initialTimeout , numRetries ));
292
+ return withRule (
293
+ email -> ValidationRules .requireValidMXRecord (email , initialTimeout , numRetries ),
294
+ FailureReason .INVALID_MX_RECORD );
229
295
}
230
296
231
297
/**
232
298
* Create a new {@code EmailValidator} with all rules from the current instance and the
233
299
* {@link ValidationRules#requireAscii(Email)} rule.
234
300
* Email addresses that contain characters other than those in the ASCII charset will fail
235
- * validation.
301
+ * validation with {@link FailureReason#NON_ASCII_ADDRESS}
236
302
*
237
303
* <p>For example, {@code "jø[email protected] "} would be invalid.
238
304
*
239
305
* @return the new {@code EmailValidator} instance
240
306
*/
241
307
public EmailValidator requireAscii () {
242
- return withRule (REQUIRE_ASCII_PREDICATE );
308
+ return withRule (
309
+ REQUIRE_ASCII_PREDICATE ,
310
+ FailureReason .NON_ASCII_ADDRESS );
243
311
}
244
312
245
313
/**
@@ -252,7 +320,7 @@ public EmailValidator requireAscii() {
252
320
*/
253
321
public boolean isValid (String email ) {
254
322
return JMail .tryParse (email )
255
- .filter (this :: passesPredicates )
323
+ .filter (e -> ! testPredicates ( e ). isPresent () )
256
324
.isPresent ();
257
325
}
258
326
@@ -298,12 +366,10 @@ public EmailValidationResult validate(String email) {
298
366
// If failed basic validation, just return it
299
367
if (!result .getEmail ().isPresent ()) return result ;
300
368
301
- // If the address fails custom validation, return failure
302
- if (!passesPredicates (result .getEmail ().get ())) {
303
- return EmailValidationResult .failure (FailureReason .FAILED_CUSTOM_VALIDATION );
304
- }
305
-
306
- return result ;
369
+ // If the address fails custom validation, return failure, otherwise return the original result
370
+ return testPredicates (result .getEmail ().get ())
371
+ .map (EmailValidationResult ::failure )
372
+ .orElse (result );
307
373
}
308
374
309
375
/**
@@ -316,7 +382,7 @@ public EmailValidationResult validate(String email) {
316
382
* is invalid according to all registered validation rules
317
383
*/
318
384
public Optional <Email > tryParse (String email ) {
319
- return JMail .tryParse (email ).filter (this :: passesPredicates );
385
+ return JMail .tryParse (email ).filter (e -> ! testPredicates ( e ). isPresent () );
320
386
}
321
387
322
388
/**
@@ -325,9 +391,11 @@ public Optional<Email> tryParse(String email) {
325
391
* @param email the email address to test
326
392
* @return true if it passes the predicates, false otherwise
327
393
*/
328
- private boolean passesPredicates (Email email ) {
329
- return validationPredicates .stream ()
330
- .allMatch (rule -> rule .test (email ));
394
+ private Optional <FailureReason > testPredicates (Email email ) {
395
+ return validationPredicates .entrySet ().stream ()
396
+ .filter (entry -> !entry .getKey ().test (email ))
397
+ .findFirst ()
398
+ .map (Map .Entry ::getValue );
331
399
}
332
400
333
401
@ Override
0 commit comments