Skip to content

Commit 0ec3db5

Browse files
committed
Make email validation same or stricter as Django
1 parent 6c82786 commit 0ec3db5

File tree

4 files changed

+139
-3
lines changed

4 files changed

+139
-3
lines changed

libpretixsync/src/main/java/eu/pretix/libpretixsync/db/QuestionLike.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import java.util.regex.Pattern;
1111

1212
import eu.pretix.libpretixsync.check.QuestionType;
13+
import eu.pretix.libpretixsync.utils.EmailValidator;
1314
import eu.pretix.libpretixsync.utils.Patterns;
1415

1516
public abstract class QuestionLike {
@@ -74,7 +75,7 @@ public String clean_answer(String answer, List<QuestionOption> opts, boolean all
7475
throw new ValidationException("Invalid file path supplied");
7576
}
7677
} else if (type == QuestionType.EMAIL) {
77-
if (!Patterns.EMAIL_ADDRESS.matcher(answer).matches()) {
78+
if (!(new EmailValidator()).isValidEmail(answer)) {
7879
throw new ValidationException("Invalid email address supplied");
7980
}
8081
} else if (type == QuestionType.B) {
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package eu.pretix.libpretixsync.utils
2+
3+
import java.lang.IllegalArgumentException
4+
import java.net.IDN
5+
import java.util.regex.Pattern
6+
7+
class EmailValidator {
8+
// Same validation as django/core/validators.py
9+
val DOT_ATOM = "^[-!#\\$%&'*+/=?^_`{}|~0-9A-Z]+(\\.[-!#\\$%&'*+/=?^_`{}|~0-9A-Z]+)*\\Z"
10+
val QUOTED_STRING = "^\"([\\001-\\010\\013\\014\\016-\\037!#-\\[\\]-\\0177]|\\\\[\\001-\\011\\013\\014\\016-\\0177])*\"\\Z"
11+
val USER_REGEX = Pattern.compile("($DOT_ATOM|$QUOTED_STRING)", Pattern.CASE_INSENSITIVE)
12+
val DOMAIN_REGEX = Pattern.compile(
13+
// max length for domain name labels is 63 characters per RFC 1034
14+
"((?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\\.)+)(?:[A-Z0-9-]{2,63}(?<!-))\\Z",
15+
Pattern.CASE_INSENSITIVE
16+
)
17+
18+
fun isValidEmail(value: String): Boolean {
19+
// The maximum length of an email is 320 characters per RFC 3696
20+
// section 3.
21+
if (value.isBlank() || !value.contains("@") || value.length > 320) {
22+
return false
23+
}
24+
25+
val userPart = value.substringBeforeLast("@")
26+
val domainPart = value.substringAfterLast("@")
27+
28+
if (!USER_REGEX.matcher(userPart).matches()) {
29+
return false
30+
}
31+
32+
if (!validateDomainPart(domainPart)) {
33+
try {
34+
val asciiDomainPart = IDN.toASCII(domainPart)
35+
if (validateDomainPart(asciiDomainPart)) {
36+
return true
37+
}
38+
} catch (e: IllegalArgumentException) {
39+
// ignore
40+
}
41+
return false
42+
}
43+
return true
44+
}
45+
46+
fun validateDomainPart(domainPart: String): Boolean {
47+
return DOMAIN_REGEX.matcher(domainPart).matches()
48+
// Django also checks for literal form here, such as @[127.0.0.1] here, but we are stricter
49+
// here and do not accept that
50+
}
51+
}

libpretixsync/src/test/java/eu/pretix/libpretixsync/db/QuestionAnswerValidationTest.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,10 @@ class QuestionAnswerValidationTest(private val questionType: QuestionType, priva
6969
arrayOf(QuestionType.W, "2016-02-29T14:30", "2016-02-29T14:30"),
7070
arrayOf(QuestionType.W, "2016-02-29T25:59", null),
7171
arrayOf(QuestionType.W, "2017-02-01", null),
72-
arrayOf(QuestionType.W, "fooobar", null)
73-
// TODO: Date, time, datetime
72+
arrayOf(QuestionType.W, "fooobar", null),
73+
arrayOf(QuestionType.EMAIL, "foobar@example.org", "foobar@example.org"),
74+
arrayOf(QuestionType.EMAIL, "foobar", null),
75+
arrayOf(QuestionType.EMAIL, "foobar@example.com.k", null)
7476
)
7577
}
7678
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package eu.pretix.libpretixsync.utils
2+
3+
import org.junit.Assert.assertEquals
4+
import org.junit.Test
5+
import org.junit.runner.RunWith
6+
import org.junit.runners.Parameterized
7+
8+
@RunWith(Parameterized::class)
9+
class EmailValidatorTests(private val input: String, private val result: Boolean) {
10+
11+
@Test
12+
fun test() {
13+
assertEquals(input, result, EmailValidator().isValidEmail(input))
14+
}
15+
16+
companion object {
17+
18+
@JvmStatic
19+
@Parameterized.Parameters
20+
fun data() = listOf(
21+
// These are from https://github.com/django/django/blob/1b5338d03ecc962af8ab4678426bc60b0672b8dd/tests/validators/tests.py#L279
22+
arrayOf("email@here.com", true),
23+
arrayOf("weirder-email@here.and.there.com", true),
24+
arrayOf("example@valid-----hyphens.com", true),
25+
arrayOf("example@valid-with-hyphens.com", true),
26+
arrayOf("test@domain.with.idn.tld.उदाहरण.परीक्षा", true),
27+
arrayOf(
28+
"email@localhost",
29+
false
30+
), // difference to django since we did not implement the whitelist
31+
arrayOf("\"test@test\"@example.com", true),
32+
arrayOf("example@atm.${"a".repeat(63)}", true),
33+
arrayOf("example@${"a".repeat(63)}.atm", true),
34+
arrayOf("example@${"a".repeat(63)}.${"b".repeat(10)}.atm", true),
35+
arrayOf("example@atm.${"a".repeat(64)}", false),
36+
arrayOf("example@${"b".repeat(64)}.atm.${"a".repeat(63)}", false),
37+
arrayOf("example@${("a".repeat(63) + ".").repeat(100)}com", false),
38+
arrayOf("", false),
39+
arrayOf("abc", false),
40+
arrayOf("abc@", false),
41+
arrayOf("abc@bar", false),
42+
arrayOf("a @x.cz", false),
43+
arrayOf("abc@.com", false),
44+
arrayOf("something@@somewhere.com", false),
45+
arrayOf("example@invalid-.com", false),
46+
arrayOf("example@-invalid.com", false),
47+
arrayOf("example@invalid.com-", false),
48+
arrayOf("example@inv-.alid-.com", false),
49+
arrayOf("example@inv-.-alid.com", false),
50+
arrayOf("test@example.com\n\n<script src=\"x.js\">", false),
51+
// Quoted-string format (CR not allowed)
52+
arrayOf("\"\\\t\"@here.com", true),
53+
arrayOf("\"\\\r\"@here.com", false),
54+
arrayOf("trailingdot@shouldfail.com.", false),
55+
// Max length of domain name labels is 63 characters per RFC 1034.
56+
arrayOf("a@${"a".repeat(63)}.us", true),
57+
arrayOf("a@${"a".repeat(64)}.us", false),
58+
// Trailing newlines in username or domain not allowed
59+
arrayOf("a@b.com\n", false),
60+
arrayOf("a\n@b.com", false),
61+
arrayOf("\"test@test\"\n@example.com", false),
62+
63+
// We are even stricter than Django and do not allow any IP addresses
64+
arrayOf("email@[127.0.0.1]", false),
65+
arrayOf("email@[2001:dB8::1]", false),
66+
arrayOf("email@[2001:dB8:0:0:0:0:0:1]", false),
67+
arrayOf("email@[::fffF:127.0.0.1]", false),
68+
arrayOf("email@127.0.0.1", false),
69+
arrayOf("email@[127.0.0.256]", false),
70+
arrayOf("email@[2001:db8::12345]", false),
71+
arrayOf("email@[2001:db8:0:0:0:0:1]", false),
72+
arrayOf("email@[::ffff:127.0.0.256]", false),
73+
arrayOf("email@[2001:dg8::1]", false),
74+
arrayOf("email@[2001:dG8:0:0:0:0:0:1]", false),
75+
arrayOf("email@[::fTzF:127.0.0.1]", false),
76+
arrayOf("a@[127.0.0.1]\n", false),
77+
78+
// Real-world find
79+
arrayOf("foobar@example.com.k", false),
80+
)
81+
}
82+
}

0 commit comments

Comments
 (0)