Skip to content

Commit 47e0865

Browse files
Merge pull request #40 from plone/maurits-emails-and-tests
Fix email validation, add tests
2 parents e1db8e7 + 9627aad commit 47e0865

9 files changed

+202
-18
lines changed

news/30.bugfix

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Fix email validation:
2+
* allow apostrophes
3+
* allow accented characters
4+
* allow ampersand in the user part
5+
* do not allows spaces.
6+
* accept TLDs with more than 4 characters
7+
[maurits]

news/5.tests.1

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Refactor the ``jsonfield`` doctest to a simpler unit test.
2+
[maurits]

news/5.tests.2

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add basic tests for the email field.
2+
[maurits]

plone/schema/email.py

+80-6
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,9 @@
55
from zope.schema.interfaces import INativeStringLine
66
from zope.schema.interfaces import ValidationError
77

8-
import re
9-
108

119
_ = MessageFactory("plone")
1210

13-
# Taken from http://www.regular-expressions.info/email.html
14-
_isemail = r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}"
15-
_isemail = re.compile(_isemail).match
16-
1711

1812
class IEmail(INativeStringLine):
1913
"""A field containing an email address"""
@@ -23,6 +17,78 @@ class InvalidEmail(ValidationError):
2317
__doc__ = _("""The specified email is not valid.""")
2418

2519

20+
def _isemail(value):
21+
r"""Is this a valid email?
22+
23+
https://www.regular-expressions.info/email.html has some hints on how to
24+
check for a valid email with regular expressions. It also gives reasons
25+
for why you may *not* want to use them. A too simple regex will work for
26+
most cases, but may give false negatives: it will treat a rare but valid
27+
email address as invalid. A complex regex may still allow some invalid
28+
email addresses, and is hard to debug in case of errors.
29+
30+
Originally we had this regex, unchanged between 2013 and 2024:
31+
32+
import re
33+
_isemail = r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}"
34+
_isemail = re.compile(_isemail).match
35+
36+
Problems:
37+
38+
1. It does not allow apostrophes.
39+
Example: o'[email protected]
40+
See: https://github.com/plone/plone.schema/issues/6
41+
42+
2. It does not allow accented characters.
43+
Example: jens@österreich.example.com
44+
See: https://github.com/plone/plone.schema/issues/9
45+
46+
3. It does not allow ampersand in the user part.
47+
Example: q&[email protected]
48+
See: https://github.com/plone/plone.schema/issues/15
49+
50+
4. It allows spaces.
51+
Example: "[email protected] hello"
52+
See: https://github.com/plone/plone.schema/issues/30
53+
54+
5. It correctly accepts TLDs with more than 4 characters,
55+
but this seems by accident, so it could get lost in an update.
56+
57+
See: https://github.com/plone/plone.schema/issues/30
58+
"""
59+
# We only accept a string.
60+
if not isinstance(value, str):
61+
return False
62+
63+
# It is up to the caller to strip spaces, newlines, etc.
64+
if value != value.strip():
65+
return False
66+
if len(value.split()) != 1:
67+
return False
68+
69+
# only one @ sign
70+
if value.count("@") != 1:
71+
return False
72+
73+
# At least one dot in the domain. And when split on dot,
74+
# each part must not be empty.
75+
user, domain = value.split("@")
76+
if not all(domain.partition(".")):
77+
return False
78+
79+
# user part must not be empty
80+
if not user:
81+
return False
82+
83+
# The maximum length of an email address that can be handled by SMTP
84+
# is 254 characters.
85+
if len(value) > 254:
86+
return False
87+
88+
# We have found no problems.
89+
return True
90+
91+
2692
@implementer(IEmail, IFromUnicode)
2793
class Email(NativeStringLine):
2894
"""Email schema field"""
@@ -34,6 +100,14 @@ def _validate(self, value):
34100

35101
raise InvalidEmail(value)
36102

103+
def fromBytes(self, value):
104+
# Originally, fromBytes was not defined.
105+
# Upstream NativeStringLine had it, but without the 'strip'
106+
# that we added in fromUnicode.
107+
value = value.strip().decode("utf-8")
108+
self.validate(value)
109+
return value
110+
37111
def fromUnicode(self, value):
38112
v = str(value.strip())
39113
self.validate(v)

plone/schema/jsonfield.py

+5-4
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,15 @@ def fromUnicode(self, value):
5454
5555
Value can be a valid JSON object:
5656
57-
>>> JSONField().fromUnicode('{"items": []}')
58-
{'items': []}
57+
JSONField().fromUnicode('{"items": []}')
5958
6059
or it can be a Python dict stored as string:
6160
62-
>>> JSONField().fromUnicode("{'items': []}")
63-
{'items': []}
61+
JSONField().fromUnicode("{'items': []}")
6462
63+
In both cases the result is:
64+
65+
{"items": []}
6566
"""
6667
try:
6768
v = json.loads(value)

plone/schema/tests/test_doctests.py

-8
This file was deleted.

plone/schema/tests/test_email.py

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import unittest
2+
3+
4+
class TestEmail(unittest.TestCase):
5+
6+
def test_fromUnicode(self):
7+
from plone.schema.email import Email
8+
from plone.schema.email import InvalidEmail
9+
10+
# Value must be string
11+
self.assertEqual(
12+
Email().fromUnicode("[email protected]"),
13+
14+
)
15+
16+
# We strip spaces.
17+
self.assertEqual(
18+
Email().fromUnicode(" [email protected] "),
19+
20+
)
21+
22+
# We do some validation
23+
with self.assertRaises(InvalidEmail):
24+
Email().fromUnicode("arthur")
25+
with self.assertRaises(InvalidEmail):
26+
Email().fromUnicode("arthur@")
27+
with self.assertRaises(InvalidEmail):
28+
Email().fromUnicode("arthur@one")
29+
with self.assertRaises(InvalidEmail):
30+
Email().fromUnicode("@one.two")
31+
32+
def test_fromBytes(self):
33+
from plone.schema.email import Email
34+
from plone.schema.email import InvalidEmail
35+
36+
# Value must be bytes
37+
self.assertEqual(
38+
Email().fromBytes(b"[email protected]"),
39+
40+
)
41+
42+
# We strip spaces.
43+
self.assertEqual(
44+
Email().fromBytes(b" [email protected] "),
45+
46+
)
47+
48+
# We do some validation
49+
with self.assertRaises(InvalidEmail):
50+
Email().fromBytes(b"arthur@one")
51+
52+
def test_validation(self):
53+
# Let's test the email validation directly, without the field.
54+
from plone.schema.email import _isemail
55+
56+
# Some good:
57+
self.assertTrue(_isemail("[email protected]"))
58+
59+
# Some bad:
60+
self.assertFalse(_isemail(""))
61+
self.assertFalse(_isemail(" "))
62+
self.assertFalse(_isemail(" [email protected]"))
63+
self.assertFalse(_isemail("[email protected]\n"))
64+
self.assertFalse(_isemail("arthur\t@example.org"))
65+
self.assertFalse(_isemail("arthur@one"))
66+
self.assertFalse(_isemail("arthur@example@org"))
67+
self.assertFalse(_isemail("[email protected]"))
68+
self.assertFalse(_isemail("x" * 254 + "@too.long"))
69+
70+
# Explicitly test some examples from the docstring,
71+
# reported in the issue tracker.
72+
73+
# 1. allow apostrophes
74+
self.assertTrue(_isemail("o'[email protected]"))
75+
76+
# 2. allow accented characters
77+
self.assertTrue(_isemail("jens@österreich.example.com"))
78+
79+
# 3. allow ampersand in the user part
80+
self.assertTrue(_isemail("q&[email protected]"))
81+
82+
# 4. do not allows spaces.
83+
self.assertFalse(_isemail("[email protected] hello"))
84+
85+
# 5. accept TLDs with more than 4 characters
86+
self.assertTrue(_isemail("[email protected]"))
87+
self.assertTrue(_isemail("[email protected]"))

plone/schema/tests/test_jsonfield.py

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import unittest
2+
3+
4+
class TestJsonField(unittest.TestCase):
5+
6+
def test_fromUnicode(self):
7+
from plone.schema.jsonfield import JSONField
8+
9+
# Value can be a valid JSON object:
10+
self.assertDictEqual(
11+
JSONField().fromUnicode('{"items": []}'),
12+
{"items": []},
13+
)
14+
15+
# or it can be a Python dict stored as string:
16+
self.assertDictEqual(JSONField().fromUnicode("{'items': []}"), {"items": []})

setup.py

+3
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,15 @@
2222
"Framework :: Zope :: 5",
2323
"Framework :: Plone",
2424
"Framework :: Plone :: 6.0",
25+
"Framework :: Plone :: 6.1",
2526
"Framework :: Plone :: Core",
2627
"Programming Language :: Python",
2728
"Programming Language :: Python :: 3.8",
2829
"Programming Language :: Python :: 3.9",
2930
"Programming Language :: Python :: 3.10",
3031
"Programming Language :: Python :: 3.11",
32+
"Programming Language :: Python :: 3.12",
33+
"Programming Language :: Python :: 3.13",
3134
"Topic :: Software Development :: Libraries :: Python Modules",
3235
"License :: OSI Approved :: BSD License",
3336
],

0 commit comments

Comments
 (0)