Skip to content

Commit ea7f777

Browse files
author
Justus Pendleton
committed
fuller email validition (request [SF#216291])
Checks email syntax more rigorously. Perform address checks against "address" and any "alternate_addresses". Changed all of the unit tests to have addresses that pass this new check.
1 parent e112527 commit ea7f777

File tree

6 files changed

+232
-158
lines changed

6 files changed

+232
-158
lines changed

CHANGES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ Fixed:
4949
- respect umask on filestorage backends (Ulrik Mikaelsson) (sf bug 1744328)
5050
- cope with spam robots posting multiple instances of the same form
5151
- include the author of property-only changes in generated messages
52+
- fuller email validation in templates (sf feature 1216291)
5253

5354

5455
2007-02-15 1.3.3

templates/classic/detectors/userauditor.py

Lines changed: 54 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -18,44 +18,73 @@
1818
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
1919
# SOFTWARE.
2020
#
21-
#$Id: userauditor.py,v 1.8 2007-09-11 21:28:29 jpend Exp $
21+
#$Id: userauditor.py,v 1.9 2007-09-12 21:11:13 jpend Exp $
22+
23+
import re
24+
25+
# regular expression thanks to: http://www.regular-expressions.info/email.html
26+
# this is the "99.99% solution for syntax only".
27+
email_regexp = (r"[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*", r"(localhost|(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9]))")
28+
email_rfc = re.compile('^' + email_regexp[0] + '@' + email_regexp[1] + '$', re.IGNORECASE)
29+
email_local = re.compile('^' + email_regexp[0] + '$', re.IGNORECASE)
30+
31+
def valid_address(address):
32+
''' If we see an @-symbol in the address then check against the full
33+
RFC syntax. Otherwise it is a local-only address so only check
34+
the local part of the RFC syntax.
35+
'''
36+
if '@' in address:
37+
return email_rfc.match(address)
38+
else:
39+
return email_local.match(address)
40+
41+
def get_addresses(user):
42+
''' iterate over all known addresses in a newvalues dict
43+
this takes of the address/alterate_addresses handling
44+
'''
45+
if user.has_key('address'):
46+
yield user['address']
47+
if user.get('alternate_addresses', None):
48+
for address in user['alternate_addresses'].split('\n'):
49+
yield address
2250

2351
def audit_user_fields(db, cl, nodeid, newvalues):
2452
''' Make sure user properties are valid.
2553
26-
- email address has no spaces in it
54+
- email address is syntactically valid
2755
- email address is unique
2856
- roles specified exist
2957
- timezone is valid
3058
'''
31-
if newvalues.has_key('address'):
32-
address = newvalues['address']
33-
if address:
34-
if ' ' in address:
35-
raise ValueError, 'Email address must not contain spaces'
36-
user = db.user.stringFind(address=address)
37-
if len(user):
38-
raise ValueError, 'Email address already in use'
59+
60+
for address in get_addresses(newvalues):
61+
if not valid_address(address):
62+
raise ValueError, 'Email address syntax is invalid'
63+
64+
check_main = db.user.stringFind(address=address)
65+
# make sure none of the alts are owned by anyone other than us (x!=nodeid)
66+
check_alts = [x for x in db.user.filter(None, {'alternate_addresses' : address}) if x != nodeid]
67+
if check_main or check_alts:
68+
raise ValueError, 'Email address %s already in use' % address
3969

4070
for rolename in [r.lower().strip() for r in newvalues.get('roles', '').split(',')]:
4171
if rolename and not db.security.role.has_key(rolename):
4272
raise ValueError, 'Role "%s" does not exist'%rolename
4373

44-
if newvalues.has_key('timezone'):
45-
tz = newvalues['timezone']
46-
if tz:
47-
# if they set a new timezone validate the timezone by attempting to
48-
# use it before we store it to the db.
49-
import roundup.date
50-
import datetime
51-
try:
52-
TZ = roundup.date.get_timezone(tz)
53-
dt = datetime.datetime.now()
54-
local = TZ.localize(dt).utctimetuple()
55-
except IOError:
56-
raise ValueError, 'Timezone "%s" does not exist' % tz
57-
except ValueError:
58-
raise ValueError, 'Timezone "%s" exceeds valid range [-23...23]' % tz
74+
tz = newvalues.get('timezone', None)
75+
if tz:
76+
# if they set a new timezone validate the timezone by attempting to
77+
# use it before we store it to the db.
78+
import roundup.date
79+
import datetime
80+
try:
81+
TZ = roundup.date.get_timezone(tz)
82+
dt = datetime.datetime.now()
83+
local = TZ.localize(dt).utctimetuple()
84+
except IOError:
85+
raise ValueError, 'Timezone "%s" does not exist' % tz
86+
except ValueError:
87+
raise ValueError, 'Timezone "%s" exceeds valid range [-23...23]' % tz
5988

6089
def init(db):
6190
# fire before changes are made

templates/minimal/detectors/userauditor.py

Lines changed: 54 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -18,43 +18,73 @@
1818
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
1919
# SOFTWARE.
2020
#
21-
#$Id: userauditor.py,v 1.7 2007-09-11 21:28:29 jpend Exp $
21+
#$Id: userauditor.py,v 1.8 2007-09-12 21:11:14 jpend Exp $
22+
23+
import re
24+
25+
# regular expression thanks to: http://www.regular-expressions.info/email.html
26+
# this is the "99.99% solution for syntax only".
27+
email_regexp = (r"[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*", r"(localhost|(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9]))")
28+
email_rfc = re.compile('^' + email_regexp[0] + '@' + email_regexp[1] + '$', re.IGNORECASE)
29+
email_local = re.compile('^' + email_regexp[0] + '$', re.IGNORECASE)
30+
31+
def valid_address(address):
32+
''' If we see an @-symbol in the address then check against the full
33+
RFC syntax. Otherwise it is a local-only address so only check
34+
the local part of the RFC syntax.
35+
'''
36+
if '@' in address:
37+
return email_rfc.match(address)
38+
else:
39+
return email_local.match(address)
40+
41+
def get_addresses(user):
42+
''' iterate over all known addresses in a newvalues dict
43+
this takes of the address/alterate_addresses handling
44+
'''
45+
if user.has_key('address'):
46+
yield user['address']
47+
if user.get('alternate_addresses', None):
48+
for address in user['alternate_addresses'].split('\n'):
49+
yield address
2250

2351
def audit_user_fields(db, cl, nodeid, newvalues):
2452
''' Make sure user properties are valid.
2553
26-
- email address has no spaces in it
54+
- email address is syntactically valid
2755
- email address is unique
2856
- roles specified exist
2957
- timezone is valid
3058
'''
31-
if newvalues.has_key('address'):
32-
address = newvalues['address']
33-
if ' ' in address:
34-
raise ValueError, 'Email address must not contain spaces'
35-
user = db.user.stringFind(address=address)
36-
if len(user):
37-
raise ValueError, 'Email address already in use'
59+
60+
for address in get_addresses(newvalues):
61+
if not valid_address(address):
62+
raise ValueError, 'Email address syntax is invalid'
63+
64+
check_main = db.user.stringFind(address=address)
65+
# make sure none of the alts are owned by anyone other than us (x!=nodeid)
66+
check_alts = [x for x in db.user.filter(None, {'alternate_addresses' : address}) if x != nodeid]
67+
if check_main or check_alts:
68+
raise ValueError, 'Email address %s already in use' % address
3869

3970
for rolename in [r.lower().strip() for r in newvalues.get('roles', '').split(',')]:
4071
if rolename and not db.security.role.has_key(rolename):
4172
raise ValueError, 'Role "%s" does not exist'%rolename
4273

43-
if newvalues.has_key('timezone'):
44-
tz = newvalues['timezone']
45-
if tz:
46-
# if they set a new timezone validate the timezone by attempting to
47-
# use it before we store it to the db.
48-
import roundup.date
49-
import datetime
50-
try:
51-
TZ = roundup.date.get_timezone(tz)
52-
dt = datetime.datetime.now()
53-
local = TZ.localize(dt).utctimetuple()
54-
except IOError:
55-
raise ValueError, 'Timezone "%s" does not exist' % tz
56-
except ValueError:
57-
raise ValueError, 'Timezone "%s" exceeds valid range [-23...23]' % tz
74+
tz = newvalues.get('timezone', None)
75+
if tz:
76+
# if they set a new timezone validate the timezone by attempting to
77+
# use it before we store it to the db.
78+
import roundup.date
79+
import datetime
80+
try:
81+
TZ = roundup.date.get_timezone(tz)
82+
dt = datetime.datetime.now()
83+
local = TZ.localize(dt).utctimetuple()
84+
except IOError:
85+
raise ValueError, 'Timezone "%s" does not exist' % tz
86+
except ValueError:
87+
raise ValueError, 'Timezone "%s" exceeds valid range [-23...23]' % tz
5888

5989
def init(db):
6090
# fire before changes are made

test/test_cgi.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
# but WITHOUT ANY WARRANTY; without even the implied warranty of
99
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
1010
#
11-
# $Id: test_cgi.py,v 1.30 2007-07-05 19:21:57 schlatterbeck Exp $
11+
# $Id: test_cgi.py,v 1.31 2007-09-12 21:11:14 jpend Exp $
1212

1313
import unittest, os, shutil, errno, sys, difflib, cgi, re
1414

@@ -70,7 +70,7 @@ def setUp(self):
7070
self.db = self.instance.open('admin')
7171
self.db.user.create(username='Chef', address='[email protected]',
7272
realname='Bork, Chef', roles='User')
73-
self.db.user.create(username='mary', address='mary@test',
73+
self.db.user.create(username='mary', address='mary@test.test',
7474
roles='User', realname='Contrary, Mary')
7575

7676
test = self.instance.backend.Class(self.db, "test",

0 commit comments

Comments
 (0)