[Checkins] SVN: CMF/trunk/C - improved email address validation

Yvo Schubbe y.2006_ at wcm-solutions.de
Sun Oct 22 13:59:48 EDT 2006


Log message for revision 70883:
  - improved email address validation
  - updated .pot file

Changed:
  U   CMF/trunk/CHANGES.txt
  U   CMF/trunk/CMFDefault/RegistrationTool.py
  U   CMF/trunk/CMFDefault/exceptions.py
  U   CMF/trunk/CMFDefault/locales/cmf_default.pot
  U   CMF/trunk/CMFDefault/tests/test_RegistrationTool.py
  U   CMF/trunk/CMFDefault/tests/test_utils.py
  U   CMF/trunk/CMFDefault/utils.py

-=-
Modified: CMF/trunk/CHANGES.txt
===================================================================
--- CMF/trunk/CHANGES.txt	2006-10-22 13:01:32 UTC (rev 70882)
+++ CMF/trunk/CHANGES.txt	2006-10-22 17:59:47 UTC (rev 70883)
@@ -2,6 +2,8 @@
 
   New Features
 
+    - CMFDefault utils: Added 'checkEmailAddress' function.
+
     - Added CMFCore.FSRestMethod:  ReST equivalent of FSSTXMethod.
 
     - CMFCore.FSSTXMethod:  Modernized, added tests, made customization
@@ -18,6 +20,9 @@
 
   Bug Fixes
 
+    - CMFDefault RegistrationTool: Fixed too restrictive email checking.
+      The new 'checkEmailAddress' function is now used.
+
     - Fixed test breakage induced by use of Z3 pagetempalates in Zope 2.10+.
 
     - CMFDefault skins: Fixed encoding issues in welcome and reminder emails.

Modified: CMF/trunk/CMFDefault/RegistrationTool.py
===================================================================
--- CMF/trunk/CMFDefault/RegistrationTool.py	2006-10-22 13:01:32 UTC (rev 70882)
+++ CMF/trunk/CMFDefault/RegistrationTool.py	2006-10-22 17:59:47 UTC (rev 70883)
@@ -20,12 +20,14 @@
 from AccessControl import ClassSecurityInfo
 from Acquisition import aq_base
 from Globals import InitializeClass
+from zope.schema import ValidationError
 
 from Products.CMFCore.RegistrationTool import RegistrationTool as BaseTool
 from Products.CMFCore.utils import _checkPermission
 from Products.CMFCore.utils import getToolByName
 
 from permissions import ManagePortal
+from utils import checkEmailAddress
 from utils import Message as _
 
 
@@ -40,6 +42,19 @@
 
     security = ClassSecurityInfo()
 
+    def _getValidEmailAddress(self, member):
+        email = member.getProperty('email')
+
+        # assert that we can actually get an email address, otherwise
+        # the template will be made with a blank To:, this is bad
+        if email is None:
+            msg = _(u'No email address is registered for member: '
+                    u'${member_id}', mapping={'member_id': new_member_id})
+            raise ValueError(msg)
+
+        checkEmailAddress(email)
+        return email
+
     #
     #   'portal_registration' interface
     #
@@ -85,17 +100,18 @@
             if email is None:
                 return _(u'You must enter an email address.')
 
-            ok, message =  _checkEmail( email )
-            if not ok:
+            try:
+                checkEmailAddress(email)
+            except ValidationError:
                 return _(u'You must enter a valid email address.')
 
         else: # Existing member.
             email = props.get('email')
 
             if email is not None:
-
-                ok, message =  _checkEmail( email )
-                if not ok:
+                try:
+                    checkEmailAddress(email)
+                except ValidationError:
                     return _(u'You must enter a valid email address.')
 
             # Not allowed to clear an existing non-empty email.
@@ -119,15 +135,8 @@
             raise ValueError(_(u'The username you entered could not be '
                                u'found.'))
 
-        # assert that we can actually get an email address, otherwise
-        # the template will be made with a blank To:, this is bad
-        if not member.getProperty('email'):
-            raise ValueError(_(u'That user does not have an email address.'))
+        email = self._getValidEmailAddress(member)
 
-        check, msg = _checkEmail(member.getProperty('email'))
-        if not check:
-            raise ValueError, msg
-
         # Rather than have the template try to use the mailhost, we will
         # render the message ourselves and send it from here (where we
         # don't need to worry about 'UseMailHost' permissions).
@@ -158,17 +167,8 @@
         if password is None:
             password = member.getPassword()
 
-        email = member.getProperty( 'email' )
+        email = self._getValidEmailAddress(member)
 
-        if email is None:
-            msg = _(u'No email address is registered for member: '
-                    u'${member_id}', mapping={'member_id': new_member_id})
-            raise ValueError(msg)
-
-        check, msg = _checkEmail(email)
-        if not check:
-            raise ValueError(msg)
-
         # Rather than have the template try to use the mailhost, we will
         # render the message ourselves and send it from here (where we
         # don't need to worry about 'UseMailHost' permissions).
@@ -205,53 +205,3 @@
         return member
 
 InitializeClass(RegistrationTool)
-
-# See URL: http://www.zopelabs.com/cookbook/1033402597 and
-#          http://aspn.activestate.com/ASPN/Cookbook/Rx/Recipe/68432
-
-_TESTS = ( 
-           # characters allowed on local-part: 0-9a-Z-._+' on domain: 0-9a-Z-.
-           # on between: @
-           ( re.compile("^[0-9a-zA-Z\.\-\_\+\']+\@[0-9a-zA-Z\.\-]+$")
-           , True
-           , "Failed a"
-           )
-           # must start or end with alpha or num
-         , ( re.compile("^[^0-9a-zA-Z]|[^0-9a-zA-Z]$")
-           , False
-           , "Failed b"
-           )
-           # local-part must end with alpha or num or _
-         , ( re.compile("([0-9a-zA-Z_]{1})\@.")
-           , True
-           , "Failed c"
-           )
-           # domain must start with alpha or num
-         , ( re.compile(".\@([0-9a-zA-Z]{1})")
-           , True
-           , "Failed d"
-           )
-           # pair .- or -. or .. or -- not allowed
-         , ( re.compile(".\.\-.|.\-\..|.\.\..|.\-\-.")
-           , False
-           , "Failed e"
-           )
-           # pair ._ or -_ or _. or _- or __ not allowed
-         , ( re.compile(".\.\_.|.\-\_.|.\_\..|.\_\-.|.\_\_.")
-           , False
-           , "Failed f"
-           )
-           # domain must end with '.' plus 2, 3 or 4 alpha for TopLevelDomain
-           # (MUST be modified in future!)
-         , ( re.compile(".\.([a-zA-Z]{2,3})$|.\.([a-zA-Z]{2,4})$")
-           , True
-           , "Failed g"
-           )
-         )
-
-def _checkEmail( address ):
-    for pattern, expected, message in _TESTS:
-        matched = pattern.search( address ) is not None
-        if matched != expected:
-            return False, message
-    return True, ''

Modified: CMF/trunk/CMFDefault/exceptions.py
===================================================================
--- CMF/trunk/CMFDefault/exceptions.py	2006-10-22 13:01:32 UTC (rev 70882)
+++ CMF/trunk/CMFDefault/exceptions.py	2006-10-22 17:59:47 UTC (rev 70883)
@@ -15,6 +15,10 @@
 $Id$
 """
 
+from zope.schema import ValidationError
+from zope.i18nmessageid import MessageFactory
+_ = MessageFactory('cmf_default')
+
 from AccessControl import ModuleSecurityInfo
 security = ModuleSecurityInfo('Products.CMFDefault.exceptions')
 
@@ -59,3 +63,6 @@
 class MetadataError(Exception):
     """ Metadata error.
     """
+
+class EmailAddressInvalid(ValidationError):
+    __doc__ = _(u'Invalid email address.')

Modified: CMF/trunk/CMFDefault/locales/cmf_default.pot
===================================================================
--- CMF/trunk/CMFDefault/locales/cmf_default.pot	2006-10-22 13:01:32 UTC (rev 70882)
+++ CMF/trunk/CMFDefault/locales/cmf_default.pot	2006-10-22 17:59:47 UTC (rev 70883)
@@ -20,7 +20,7 @@
 "Content-Transfer-Encoding: 8bit\n"
 
 #: CMFCore/RegistrationTool.py:144
-#: CMFDefault/RegistrationTool.py:81
+#: CMFDefault/RegistrationTool.py:96
 msgid "The login name you selected is already in use or is not valid. Please choose another."
 msgstr ""
 
@@ -53,46 +53,42 @@
 msgid "Intervening changes from elsewhere detected. Please refetch the document and reapply your changes. (You may be able to recover your version using the browser 'back' button, but will have to apply them to a freshly fetched copy.)"
 msgstr ""
 
-#: CMFDefault/RegistrationTool.py:119
-#: CMFDefault/RegistrationTool.py:155
-msgid "The username you entered could not be found."
+#: CMFDefault/RegistrationTool.py:101
+msgid "You must enter an email address."
 msgstr ""
 
-#: CMFDefault/RegistrationTool.py:125
-msgid "That user does not have an email address."
+#: CMFDefault/RegistrationTool.py:106
+#: CMFDefault/RegistrationTool.py:115
+#: CMFDefault/RegistrationTool.py:121
+msgid "You must enter a valid email address."
 msgstr ""
 
+#: CMFDefault/RegistrationTool.py:135
 #: CMFDefault/RegistrationTool.py:164
+msgid "The username you entered could not be found."
+msgstr ""
+
+#: CMFDefault/RegistrationTool.py:51
 # Default: ""
 msgid "No email address is registered for member: ${member_id}"
 msgstr ""
 
-#: CMFDefault/RegistrationTool.py:55
+#: CMFDefault/RegistrationTool.py:70
 msgid "You must enter a password."
 msgstr ""
 
-#: CMFDefault/RegistrationTool.py:58
+#: CMFDefault/RegistrationTool.py:73
 msgid "Your password must contain at least 5 characters."
 msgstr ""
 
-#: CMFDefault/RegistrationTool.py:61
+#: CMFDefault/RegistrationTool.py:76
 msgid "Your password and confirmation did not match. Please try again."
 msgstr ""
 
-#: CMFDefault/RegistrationTool.py:78
+#: CMFDefault/RegistrationTool.py:93
 msgid "You must enter a valid name."
 msgstr ""
 
-#: CMFDefault/RegistrationTool.py:86
-msgid "You must enter an email address."
-msgstr ""
-
-#: CMFDefault/RegistrationTool.py:90
-#: CMFDefault/RegistrationTool.py:99
-#: CMFDefault/RegistrationTool.py:105
-msgid "You must enter a valid email address."
-msgstr ""
-
 #: CMFDefault/browser/document.py:107
 #: CMFDefault/skins/zpt_content/document_edit_control.py:10
 msgid "Document changed."
@@ -642,34 +638,38 @@
 msgid "Body"
 msgstr ""
 
-#: CMFDefault/browser/utils.py:269
+#: CMFDefault/browser/utils.py:274
 msgid "Previous item"
 msgstr ""
 
-#: CMFDefault/browser/utils.py:271
+#: CMFDefault/browser/utils.py:276
 # Default: ""
 msgid "Previous ${count} items"
 msgstr ""
 
-#: CMFDefault/browser/utils.py:283
+#: CMFDefault/browser/utils.py:288
 msgid "Next item"
 msgstr ""
 
-#: CMFDefault/browser/utils.py:285
+#: CMFDefault/browser/utils.py:290
 # Default: ""
 msgid "Next ${count} items"
 msgstr ""
 
-#: CMFDefault/browser/utils.py:296
+#: CMFDefault/browser/utils.py:301
 #: CMFDefault/skins/zpt_generic/search.py:47
 msgid "item"
 msgstr ""
 
-#: CMFDefault/browser/utils.py:296
+#: CMFDefault/browser/utils.py:301
 #: CMFDefault/skins/zpt_generic/search.py:47
 msgid "items"
 msgstr ""
 
+#: CMFDefault/exceptions.py:68
+msgid "Invalid email address."
+msgstr ""
+
 #: CMFDefault/profiles/default/actions.xml:10
 #: CMFDefault/skins/zpt_generic/login_form.pt:44
 msgid "Login"
@@ -691,37 +691,45 @@
 msgid "Syndication"
 msgstr ""
 
-#: CMFDefault/profiles/default/actions.xml:141
+#: CMFDefault/profiles/default/actions.xml:137
+msgid "Interfaces"
+msgstr ""
+
+#: CMFDefault/profiles/default/actions.xml:138
+msgid "Assign marker interfaces"
+msgstr ""
+
+#: CMFDefault/profiles/default/actions.xml:153
 msgid "Folder contents"
 msgstr ""
 
-#: CMFDefault/profiles/default/actions.xml:156
+#: CMFDefault/profiles/default/actions.xml:168
 msgid "Manage members"
 msgstr ""
 
-#: CMFDefault/profiles/default/actions.xml:157
+#: CMFDefault/profiles/default/actions.xml:169
 msgid "Manage portal members"
 msgstr ""
 
-#: CMFDefault/profiles/default/actions.xml:170
+#: CMFDefault/profiles/default/actions.xml:182
 msgid "Delete members"
 msgstr ""
 
-#: CMFDefault/profiles/default/actions.xml:171
+#: CMFDefault/profiles/default/actions.xml:183
 msgid "Delete portal members"
 msgstr ""
 
-#: CMFDefault/profiles/default/actions.xml:183
+#: CMFDefault/profiles/default/actions.xml:195
 #: CMFDefault/skins/zpt_generic/undo_form.pt:44
 #: CMFDefault/skins/zpt_generic/undo_form.pt:100
 msgid "Undo"
 msgstr ""
 
-#: CMFDefault/profiles/default/actions.xml:195
+#: CMFDefault/profiles/default/actions.xml:207
 msgid "Reconfigure Portal"
 msgstr ""
 
-#: CMFDefault/profiles/default/actions.xml:196
+#: CMFDefault/profiles/default/actions.xml:208
 msgid "Reconfigure the portal"
 msgstr ""
 
@@ -849,14 +857,6 @@
 msgid "News Items contain short text articles and carry a title as well as an optional description."
 msgstr ""
 
-#: CMFDefault/profiles/views_support/actions.xml:6
-msgid "Interfaces"
-msgstr ""
-
-#: CMFDefault/profiles/views_support/actions.xml:7
-msgid "Assign marker interfaces"
-msgstr ""
-
 #: CMFDefault/skins/zpt_content/content_hide_form.pt:10
 msgid "Use this form to hide a content item by setting its status to <b>Private</b>, thereby making it unavailable to other portal members and visitors."
 msgstr ""
@@ -1100,7 +1100,7 @@
 msgid "Selected members deleted."
 msgstr ""
 
-#: CMFDefault/skins/zpt_control/portal_config_control.py:12
+#: CMFDefault/skins/zpt_control/portal_config_control.py:14
 msgid "CMF Settings changed."
 msgstr ""
 
@@ -1356,13 +1356,13 @@
 #: CMFDefault/skins/zpt_generic/join_template.pt:57
 #: CMFDefault/skins/zpt_generic/login_form.pt:22
 #: CMFDefault/skins/zpt_generic/password_form_template.pt:26
-#: CMFDefault/skins/zpt_generic/registered_email_template.pt:27
+#: CMFDefault/skins/zpt_generic/registered_email_template.pt:20
 msgid "Member ID"
 msgstr ""
 
 #: CMFDefault/skins/zpt_generic/join_template.pt:71
 #: CMFDefault/skins/zpt_generic/login_form.pt:29
-#: CMFDefault/skins/zpt_generic/registered_email_template.pt:29
+#: CMFDefault/skins/zpt_generic/registered_email_template.pt:22
 msgid "Password"
 msgstr ""
 
@@ -1598,16 +1598,17 @@
 msgid "News"
 msgstr ""
 
-#: CMFDefault/skins/zpt_generic/password_email_template.pt:13
-msgid "Request made by IP ${ip} at ${time}"
+#: CMFDefault/skins/zpt_generic/password_email.py:18
+# Default: ""
+msgid "${portal_title}: Membership reminder"
 msgstr ""
 
 #: CMFDefault/skins/zpt_generic/password_email_template.pt:2
-msgid "${portal_title}: Membership reminder"
+msgid "Your password: ${password}"
 msgstr ""
 
-#: CMFDefault/skins/zpt_generic/password_email_template.pt:8
-msgid "Your password: ${password}"
+#: CMFDefault/skins/zpt_generic/password_email_template.pt:7
+msgid "Request made by IP ${ip} at ${time}"
 msgstr ""
 
 #: CMFDefault/skins/zpt_generic/password_form.py:29
@@ -1694,6 +1695,10 @@
 msgid "This form is used to set the portal configuration options."
 msgstr ""
 
+#: CMFDefault/skins/zpt_generic/reconfig_template.pt:102
+msgid "Charset used to encode emails send by the portal. If empty, 'utf-8' is used if necessary."
+msgstr ""
+
 #: CMFDefault/skins/zpt_generic/reconfig_template.pt:17
 msgid "Portal 'From' name"
 msgstr ""
@@ -1758,34 +1763,39 @@
 msgid "Charset used to decode portal content strings. If empty, 'ascii' is used."
 msgstr ""
 
-#: CMFDefault/skins/zpt_generic/registered_email_template.pt:10
-msgid "You have been registered as a member of \"${portal_title}\", which allows you to personalize your view of the website and participate in the community."
+#: CMFDefault/skins/zpt_generic/reconfig_template.pt:97
+msgid "Portal email encoding"
 msgstr ""
 
-#: CMFDefault/skins/zpt_generic/registered_email_template.pt:15
-msgid "This describes the purpose of the website:"
-msgstr ""
-
-#: CMFDefault/skins/zpt_generic/registered_email_template.pt:2
+#: CMFDefault/skins/zpt_generic/registered_email.py:31
+# Default: ""
 msgid "${portal_title}: Your Membership Information"
 msgstr ""
 
-#: CMFDefault/skins/zpt_generic/registered_email_template.pt:21
+#: CMFDefault/skins/zpt_generic/registered_email_template.pt:14
 msgid "Visit us at ${portal_url}"
 msgstr ""
 
-#: CMFDefault/skins/zpt_generic/registered_email_template.pt:24
+#: CMFDefault/skins/zpt_generic/registered_email_template.pt:17
 msgid "Here is your login data (mind upper and lower case):"
 msgstr ""
 
-#: CMFDefault/skins/zpt_generic/registered_email_template.pt:32
+#: CMFDefault/skins/zpt_generic/registered_email_template.pt:25
 msgid "You can use this URL to log in:"
 msgstr ""
 
-#: CMFDefault/skins/zpt_generic/registered_email_template.pt:37
+#: CMFDefault/skins/zpt_generic/registered_email_template.pt:3
+msgid "You have been registered as a member of \"${portal_title}\", which allows you to personalize your view of the website and participate in the community."
+msgstr ""
+
+#: CMFDefault/skins/zpt_generic/registered_email_template.pt:30
 msgid "Be aware that this URL might wrap over two lines. If your browser shows an error message when you try to access the URL please make sure that you put in the complete string."
 msgstr ""
 
+#: CMFDefault/skins/zpt_generic/registered_email_template.pt:8
+msgid "This describes the purpose of the website:"
+msgstr ""
+
 #: CMFDefault/skins/zpt_generic/review.pt:14
 msgid "Items pending review"
 msgstr ""
@@ -1942,7 +1952,7 @@
 msgstr ""
 
 #: CMFDefault/skins/zpt_generic/standard_error_message.pt:44
-msgid "For more detailed information about the error, please refer to the HTML source for this page."
+msgid "For more detailed information about the error, please refer to the error log."
 msgstr ""
 
 #: CMFDefault/skins/zpt_generic/standard_error_message.pt:48
@@ -2061,17 +2071,17 @@
 msgid "Comments:"
 msgstr ""
 
-#: CMFDefault/utils.py:324
+#: CMFDefault/utils.py:329
 # Default: ""
 msgid "JavaScript event '${attribute}' not allowed."
 msgstr ""
 
-#: CMFDefault/utils.py:329
+#: CMFDefault/utils.py:334
 # Default: ""
 msgid "JavaScript URI '${value}' not allowed."
 msgstr ""
 
-#: CMFDefault/utils.py:342
+#: CMFDefault/utils.py:347
 # Default: ""
 msgid "Dynamic tag '${tag}' not allowed."
 msgstr ""
@@ -2104,16 +2114,16 @@
 msgid "Query Parameters:"
 msgstr ""
 
-#: DCWorkflow/DCWorkflow.py:279
+#: DCWorkflow/DCWorkflow.py:280
 msgid "Object is in an undefined state."
 msgstr ""
 
-#: DCWorkflow/DCWorkflow.py:284
+#: DCWorkflow/DCWorkflow.py:285
 # Default: ""
 msgid "Transition '${action_id}' is not triggered by a user action."
 msgstr ""
 
-#: DCWorkflow/DCWorkflow.py:467
+#: DCWorkflow/DCWorkflow.py:468
 # Default: ""
 msgid "Destination state undefined: ${state_id}"
 msgstr ""

Modified: CMF/trunk/CMFDefault/tests/test_RegistrationTool.py
===================================================================
--- CMF/trunk/CMFDefault/tests/test_RegistrationTool.py	2006-10-22 13:01:32 UTC (rev 70882)
+++ CMF/trunk/CMFDefault/tests/test_RegistrationTool.py	2006-10-22 17:59:47 UTC (rev 70883)
@@ -57,62 +57,6 @@
         verifyClass(IActionProvider, self._getTargetClass())
         verifyClass(IRegistrationTool, self._getTargetClass())
 
-    def test_testPropertiesValidity_email_with_plus( self ):
-
-        # CMF Collector #322
-
-        tool = self._makeOne().__of__( self.root )
-        self.root.portal_membership = FauxMembershipTool()
-
-        props = { 'email' : 'user+site at example.com'
-                , 'username' : 'username'
-                }
-
-        result = tool.testPropertiesValidity( props, None )
-
-        self.failUnless( result is None )
-
-    def test_testPropertiesValidity_email_with_underscore( self ):
-        # Test for collector item 326: Email alias ending w/underscore
-        tool = self._makeOne().__of__( self.root )
-        self.root.portal_membership = FauxMembershipTool()
-
-        props = { 'email' : 'username_ at example.com'
-                , 'username' : 'username'
-                }
-
-        result = tool.testPropertiesValidity( props, None )
-
-        self.failUnless( result is None )
-
-    def test_testPropertiesValidity_email_with_singlequote( self ):
-
-        # CMF Collector #401
-
-        tool = self._makeOne().__of__( self.root )
-        self.root.portal_membership = FauxMembershipTool()
-
-        props = { 'email' : "user'site at example.com"
-                , 'username' : 'username'
-                }
-
-        result = tool.testPropertiesValidity( props, None )
-
-        self.failUnless( result is None )
-
-    def test_testPropertiesValidity_new_invalid_email( self ):
-
-        tool = self._makeOne().__of__( self.root )
-        self.root.portal_membership = FauxMembershipTool()
-
-        props = { 'email' : 'this is not an e-mail address'
-                , 'username' : 'username'
-                }
-
-        result = tool.testPropertiesValidity( props, None )
-
-        self.failIf( result is None, 'Invalid e-mail passed inspection' )
-
     def test_spamcannon_collector_243( self ):
 
         INJECTED_HEADERS = """

Modified: CMF/trunk/CMFDefault/tests/test_utils.py
===================================================================
--- CMF/trunk/CMFDefault/tests/test_utils.py	2006-10-22 13:01:32 UTC (rev 70882)
+++ CMF/trunk/CMFDefault/tests/test_utils.py	2006-10-22 17:59:47 UTC (rev 70883)
@@ -203,6 +203,38 @@
         self.assertEqual( toUnicode( {'foo': 'bar'}, 'iso-8859-1' ),
                           {'foo': u'bar'} )
 
+    def test_checkEmailAddress(self):
+        from Products.CMFDefault.exceptions import EmailAddressInvalid
+        from Products.CMFDefault.utils import checkEmailAddress
+
+        self.assertEqual(checkEmailAddress('foo at example.com'), None)
+        self.assertEqual(checkEmailAddress('$.- at example.com'), None)
+        self.assertEqual(checkEmailAddress(u'foo at example.com'), None)
+        # CMF Collector issue #322
+        self.assertEqual(checkEmailAddress('user+site at example.com'), None)
+        # CMF Collector issue #326
+        self.assertEqual(checkEmailAddress('username_ at example.com'), None)
+        # CMF Collector issue #401
+        self.assertEqual(checkEmailAddress("user'site at example.com"), None)
+        self.assertRaises(EmailAddressInvalid, checkEmailAddress,
+                          'this is not an e-mail address')
+        self.assertRaises(EmailAddressInvalid, checkEmailAddress,
+                          'foo at example.com, bar at example.com')
+        self.assertRaises(EmailAddressInvalid, checkEmailAddress,
+                          'foo. at example.com')
+        self.assertRaises(EmailAddressInvalid, checkEmailAddress,
+                          '.foo at example.com')
+        self.assertRaises(EmailAddressInvalid, checkEmailAddress,
+                          'foo at 1bar.example.com')
+        self.assertRaises(EmailAddressInvalid, checkEmailAddress,
+                          'foo at -bar.example.com')
+        # RFC 2821 local-part: max 64 characters
+        self.assertRaises(EmailAddressInvalid, checkEmailAddress,
+                          'f'*63+'oo at example.com')
+        # RFC 2821 domain: max 255 characters
+        self.assertRaises(EmailAddressInvalid, checkEmailAddress,
+                          'foo@'+'b'*242+'ar.example.com')
+
     def test_formatRFC822Headers_simple(self):
         from Products.CMFDefault.utils import formatRFC822Headers
 

Modified: CMF/trunk/CMFDefault/utils.py
===================================================================
--- CMF/trunk/CMFDefault/utils.py	2006-10-22 13:01:32 UTC (rev 70882)
+++ CMF/trunk/CMFDefault/utils.py	2006-10-22 17:59:47 UTC (rev 70883)
@@ -34,6 +34,7 @@
 
 from Products.CMFCore.utils import getToolByName
 
+from exceptions import EmailAddressInvalid
 from exceptions import IllegalHTML
 
 
@@ -488,5 +489,29 @@
         msg[k] = str(header)
     return msg.as_string()
 
+# RFC 2822 local-part: dot-atom or quoted-string
+# characters allowed in atom: A-Za-z0-9!#$%&'*+-/=?^_`{|}~
+# RFC 2821 domain: max 255 characters
+_LOCAL_RE = re.compile(r'([A-Za-z0-9!#$%&\'*+\-/=?^_`{|}~]+'
+                     r'(\.[A-Za-z0-9!#$%&\'*+\-/=?^_`{|}~]+)*|'
+                     r'"[^(\|")]*")@[^@]{3,255}$')
+
+# RFC 2821 local-part: max 64 characters
+# RFC 2821 domain: sequence of dot-separated labels
+# characters allowed in label: A-Za-z0-9-, first is a letter
+_DOMAIN_RE = re.compile(r'[^@]{1,64}@[A-Za-z][A-Za-z0-9-]*'
+                                r'(\.[A-Za-z][A-Za-z0-9-]*)+$')
+
+security.declarePublic('checkEmailAddress')
+def checkEmailAddress(address):
+    """ Check email address.
+
+    This should catch most invalid but no valid addresses.
+    """
+    if not _LOCAL_RE.match(address):
+        raise EmailAddressInvalid
+    if not _DOMAIN_RE.match(address):
+        raise EmailAddressInvalid
+
 security.declarePublic('Message')
 Message = _ = MessageFactory('cmf_default')



More information about the Checkins mailing list