Skip to content

Commit

Permalink
Make email validation same or stricter as Django (#36)
Browse files Browse the repository at this point in the history
  • Loading branch information
raphaelm authored Feb 14, 2024
1 parent 8b53c11 commit 34a18e3
Show file tree
Hide file tree
Showing 4 changed files with 139 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import java.util.regex.Pattern;

import eu.pretix.libpretixsync.check.QuestionType;
import eu.pretix.libpretixsync.utils.EmailValidator;
import eu.pretix.libpretixsync.utils.Patterns;

public abstract class QuestionLike {
Expand Down Expand Up @@ -74,7 +75,7 @@ public String clean_answer(String answer, List<QuestionOption> opts, boolean all
throw new ValidationException("Invalid file path supplied");
}
} else if (type == QuestionType.EMAIL) {
if (!Patterns.EMAIL_ADDRESS.matcher(answer).matches()) {
if (!(new EmailValidator()).isValidEmail(answer)) {
throw new ValidationException("Invalid email address supplied");
}
} else if (type == QuestionType.B) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package eu.pretix.libpretixsync.utils

import java.lang.IllegalArgumentException
import java.net.IDN
import java.util.regex.Pattern

class EmailValidator {
// Same validation as django/core/validators.py
val DOT_ATOM = "^[-!#\\$%&'*+/=?^_`{}|~0-9A-Z]+(\\.[-!#\\$%&'*+/=?^_`{}|~0-9A-Z]+)*\\Z"
val QUOTED_STRING = "^\"([\\001-\\010\\013\\014\\016-\\037!#-\\[\\]-\\0177]|\\\\[\\001-\\011\\013\\014\\016-\\0177])*\"\\Z"
val USER_REGEX = Pattern.compile("($DOT_ATOM|$QUOTED_STRING)", Pattern.CASE_INSENSITIVE)
val DOMAIN_REGEX = Pattern.compile(
// max length for domain name labels is 63 characters per RFC 1034
"((?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\\.)+)(?:[A-Z0-9-]{2,63}(?<!-))\\Z",
Pattern.CASE_INSENSITIVE
)

fun isValidEmail(value: String): Boolean {
// The maximum length of an email is 320 characters per RFC 3696
// section 3.
if (value.isBlank() || !value.contains("@") || value.length > 320) {
return false
}

val userPart = value.substringBeforeLast("@")
val domainPart = value.substringAfterLast("@")

if (!USER_REGEX.matcher(userPart).matches()) {
return false
}

if (!validateDomainPart(domainPart)) {
try {
val asciiDomainPart = IDN.toASCII(domainPart)
if (validateDomainPart(asciiDomainPart)) {
return true
}
} catch (e: IllegalArgumentException) {
// ignore
}
return false
}
return true
}

fun validateDomainPart(domainPart: String): Boolean {
return DOMAIN_REGEX.matcher(domainPart).matches()
// Django also checks for literal form here, such as @[127.0.0.1] here, but we are stricter
// here and do not accept that
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,10 @@ class QuestionAnswerValidationTest(private val questionType: QuestionType, priva
arrayOf(QuestionType.W, "2016-02-29T14:30", "2016-02-29T14:30"),
arrayOf(QuestionType.W, "2016-02-29T25:59", null),
arrayOf(QuestionType.W, "2017-02-01", null),
arrayOf(QuestionType.W, "fooobar", null)
// TODO: Date, time, datetime
arrayOf(QuestionType.W, "fooobar", null),
arrayOf(QuestionType.EMAIL, "[email protected]", "[email protected]"),
arrayOf(QuestionType.EMAIL, "foobar", null),
arrayOf(QuestionType.EMAIL, "[email protected]", null)
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package eu.pretix.libpretixsync.utils

import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized

@RunWith(Parameterized::class)
class EmailValidatorTests(private val input: String, private val result: Boolean) {

@Test
fun test() {
assertEquals(input, result, EmailValidator().isValidEmail(input))
}

companion object {

@JvmStatic
@Parameterized.Parameters
fun data() = listOf(
// These are from https://github.com/django/django/blob/1b5338d03ecc962af8ab4678426bc60b0672b8dd/tests/validators/tests.py#L279
arrayOf("[email protected]", true),
arrayOf("[email protected]", true),
arrayOf("[email protected]", true),
arrayOf("[email protected]", true),
arrayOf("[email protected].उदाहरण.परीक्षा", true),
arrayOf(
"email@localhost",
false
), // difference to django since we did not implement the whitelist
arrayOf("\"test@test\"@example.com", true),
arrayOf("example@atm.${"a".repeat(63)}", true),
arrayOf("example@${"a".repeat(63)}.atm", true),
arrayOf("example@${"a".repeat(63)}.${"b".repeat(10)}.atm", true),
arrayOf("example@atm.${"a".repeat(64)}", false),
arrayOf("example@${"b".repeat(64)}.atm.${"a".repeat(63)}", false),
arrayOf("example@${("a".repeat(63) + ".").repeat(100)}com", false),
arrayOf("", false),
arrayOf("abc", false),
arrayOf("abc@", false),
arrayOf("abc@bar", false),
arrayOf("a @x.cz", false),
arrayOf("[email protected]", false),
arrayOf("something@@somewhere.com", false),
arrayOf("[email protected]", false),
arrayOf("[email protected]", false),
arrayOf("[email protected]", false),
arrayOf("[email protected]", false),
arrayOf("[email protected]", false),
arrayOf("[email protected]\n\n<script src=\"x.js\">", false),
// Quoted-string format (CR not allowed)
arrayOf("\"\\\t\"@here.com", true),
arrayOf("\"\\\r\"@here.com", false),
arrayOf("[email protected].", false),
// Max length of domain name labels is 63 characters per RFC 1034.
arrayOf("a@${"a".repeat(63)}.us", true),
arrayOf("a@${"a".repeat(64)}.us", false),
// Trailing newlines in username or domain not allowed
arrayOf("[email protected]\n", false),
arrayOf("a\n@b.com", false),
arrayOf("\"test@test\"\n@example.com", false),

// We are even stricter than Django and do not allow any IP addresses
arrayOf("email@[127.0.0.1]", false),
arrayOf("email@[2001:dB8::1]", false),
arrayOf("email@[2001:dB8:0:0:0:0:0:1]", false),
arrayOf("email@[::fffF:127.0.0.1]", false),
arrayOf("[email protected]", false),
arrayOf("email@[127.0.0.256]", false),
arrayOf("email@[2001:db8::12345]", false),
arrayOf("email@[2001:db8:0:0:0:0:1]", false),
arrayOf("email@[::ffff:127.0.0.256]", false),
arrayOf("email@[2001:dg8::1]", false),
arrayOf("email@[2001:dG8:0:0:0:0:0:1]", false),
arrayOf("email@[::fTzF:127.0.0.1]", false),
arrayOf("a@[127.0.0.1]\n", false),

// Real-world find
arrayOf("[email protected]", false),
)
}
}

0 comments on commit 34a18e3

Please sign in to comment.