Code

That's gadfly done, mostly. Things left:
[roundup.git] / roundup / mailgw.py
index 53dc401d20dea4d9c4ba0613aa34621586ec9f67..edbfb89254cb66621090fc246985b0dd8fb93583 100644 (file)
@@ -73,7 +73,7 @@ are calling the create() method to create a new node). If an auditor raises
 an exception, the original message is bounced back to the sender with the
 explanatory message given in the exception. 
 
-$Id: mailgw.py,v 1.74 2002-05-29 01:16:17 richard Exp $
+$Id: mailgw.py,v 1.81 2002-08-19 00:21:56 richard Exp $
 '''
 
 
@@ -93,9 +93,21 @@ class MailUsageError(ValueError):
 class MailUsageHelp(Exception):
     pass
 
-class UnAuthorized(Exception):
+class Unauthorized(Exception):
     """ Access denied """
 
+def initialiseSecurity(security):
+    ''' Create some Permissions and Roles on the security object
+
+        This function is directly invoked by security.Security.__init__()
+        as a part of the Security object instantiation.
+    '''
+    security.addPermission(name="Email Registration",
+        description="Anonymous may register through e-mail")
+    p = security.addPermission(name="Email Access",
+        description="User may use the email interface")
+    security.addPermissionToRole('Admin', p)
+
 class Message(mimetools.Message):
     ''' subclass mimetools.Message so we can retrieve the parts of the
         message...
@@ -128,6 +140,10 @@ class MailGW:
         self.instance = instance
         self.db = db
 
+        # should we trap exceptions (normal usage) or pass them through
+        # (for testing)
+        self.trapExceptions = 1
+
     def main(self, fp):
         ''' fp - the file from which to read the Message.
         '''
@@ -146,6 +162,8 @@ class MailGW:
         # its way into here... try to handle it gracefully
         sendto = message.getaddrlist('from')
         if sendto:
+            if not self.trapExceptions:
+                return self.handle_message(message)
             try:
                 return self.handle_message(message)
             except MailUsageHelp:
@@ -166,7 +184,7 @@ class MailGW:
                 m.append('\n\nMail Gateway Help\n=================')
                 m.append(fulldoc)
                 m = self.bounce_message(message, sendto, m)
-            except UnAuthorized, value:
+            except Unauthorized, value:
                 # just inform the user that he is not authorized
                 sendto = [sendto[0][1]]
                 m = ['']
@@ -439,6 +457,12 @@ Subject was: "%s"
                         curvalue = []
 
                     # handle each add/remove in turn
+                    # keep an extra list for all items that are
+                    # definitely in the new list (in case of e.g.
+                    # <propname>=A,+B, which should replace the old
+                    # list with A,B)
+                    set = 0
+                    newvalue = []
                     for item in value.split(','):
                         item = item.strip()
 
@@ -449,6 +473,8 @@ Subject was: "%s"
                             item = item[1:]
                         elif item.startswith('+'):
                             item = item[1:]
+                        else:
+                            set = 1
 
                         # look up the value
                         try:
@@ -467,11 +493,22 @@ Subject was: "%s"
                                     'for %s.'%(item, propname))
                                 continue
                         else:
+                            newvalue.append(item)
                             if item not in curvalue:
                                 curvalue.append(item)
 
-                    # that's it, set the new Multilink property value
-                    props[propname] = curvalue
+                    # that's it, set the new Multilink property value,
+                    # or overwrite it completely
+                    if set:
+                        props[propname] = newvalue
+                    else:
+                        props[propname] = curvalue
+                elif isinstance(proptype, hyperdb.Boolean):
+                    value = value.strip()
+                    props[propname] = value.lower() in ('yes', 'true', 'on', '1')
+                elif isinstance(proptype, hyperdb.Number):
+                    value = value.strip()
+                    props[propname] = int(value)
 
             # handle any errors parsing the argument list
             if errors:
@@ -487,24 +524,29 @@ Subject was: "%s"
         # handle the users
         #
 
-        # Don't create users if ANONYMOUS_REGISTER_MAIL is denied
-        # ... fall back on ANONYMOUS_REGISTER if the other doesn't exist
+        # Don't create users if anonymous isn't allowed to register
         create = 1
-        if hasattr(self.instance, 'ANONYMOUS_REGISTER_MAIL'):
-            if self.instance.ANONYMOUS_REGISTER_MAIL == 'deny':
-                create = 0
-        elif self.instance.ANONYMOUS_REGISTER == 'deny':
+        anonid = self.db.user.lookup('anonymous')
+        if not self.db.security.hasPermission('Email Registration', anonid):
             create = 0
 
-        author = self.db.uidFromAddress(message.getaddrlist('from')[0],
+        # ok, now figure out who the author is - create a new user if the
+        # "create" flag is true
+        author = uidFromAddress(self.db, message.getaddrlist('from')[0],
             create=create)
+
+        # no author? means we're not author
         if not author:
-            raise UnAuthorized, '''
+            raise Unauthorized, '''
 You are not a registered user.
 
 Unknown address: %s
 '''%message.getaddrlist('from')[0][1]
 
+        # make sure the author has permission to use the email interface
+        if not self.db.security.hasPermission('Email Access', author):
+            raise Unauthorized, 'You are not permitted to access this tracker.'
+
         # the author may have been created - make sure the change is
         # committed before we reopen the database
         self.db.commit()
@@ -526,7 +568,7 @@ Unknown address: %s
 
             # look up the recipient - create if necessary (and we're
             # allowed to)
-            recipient = self.db.uidFromAddress(recipient, create)
+            recipient = uidFromAddress(self.db, recipient, create)
 
             # if all's well, add the recipient to the list
             if recipient:
@@ -696,6 +738,53 @@ There was a problem with the message you sent:
 
         return nodeid
 
+def extractUserFromList(userClass, users):
+    '''Given a list of users, try to extract the first non-anonymous user
+       and return that user, otherwise return None
+    '''
+    if len(users) > 1:
+        for user in users:
+            # make sure we don't match the anonymous or admin user
+            if userClass.get(user, 'username') in ('admin', 'anonymous'):
+                continue
+            # first valid match will do
+            return user
+        # well, I guess we have no choice
+        return user[0]
+    elif users:
+        return users[0]
+    return None
+
+def uidFromAddress(db, address, create=1):
+    ''' address is from the rfc822 module, and therefore is (name, addr)
+
+        user is created if they don't exist in the db already
+    '''
+    (realname, address) = address
+
+    # try a straight match of the address
+    user = extractUserFromList(db.user, db.user.stringFind(address=address))
+    if user is not None: return user
+
+    # try the user alternate addresses if possible
+    props = db.user.getprops()
+    if props.has_key('alternate_addresses'):
+        users = db.user.filter(None, {'alternate_addresses': address},
+            [], [])
+        user = extractUserFromList(db.user, users)
+        if user is not None: return user
+
+    # try to match the username to the address (for local
+    # submissions where the address is empty)
+    user = extractUserFromList(db.user, db.user.stringFind(username=address))
+
+    # couldn't match address or username, so create a new user
+    if create:
+        return db.user.create(username=address, address=address,
+            realname=realname, roles=db.config.NEW_EMAIL_USER_ROLES)
+    else:
+        return 0
+
 def parseContent(content, keep_citations, keep_body,
         blank_line=re.compile(r'[\r\n]+\s*[\r\n]+'),
         eol=re.compile(r'[\r\n]+'), 
@@ -765,6 +854,49 @@ def parseContent(content, keep_citations, keep_body,
 
 #
 # $Log: not supported by cvs2svn $
+# Revision 1.80  2002/08/01 00:56:22  richard
+# Added the web access and email access permissions, so people can restrict
+# access to users who register through the email interface (for example).
+# Also added "security" command to the roundup-admin interface to display the
+# Role/Permission config for an instance.
+#
+# Revision 1.79  2002/07/26 08:26:59  richard
+# Very close now. The cgi and mailgw now use the new security API. The two
+# templates have been migrated to that setup. Lots of unit tests. Still some
+# issue in the web form for editing Roles assigned to users.
+#
+# Revision 1.78  2002/07/25 07:14:06  richard
+# Bugger it. Here's the current shape of the new security implementation.
+# Still to do:
+#  . call the security funcs from cgi and mailgw
+#  . change shipped templates to include correct initialisation and remove
+#    the old config vars
+# ... that seems like a lot. The bulk of the work has been done though. Honest :)
+#
+# Revision 1.77  2002/07/18 11:17:31  gmcm
+# Add Number and Boolean types to hyperdb.
+# Add conversion cases to web, mail & admin interfaces.
+# Add storage/serialization cases to back_anydbm & back_metakit.
+#
+# Revision 1.76  2002/07/10 06:39:37  richard
+#  . made mailgw handle set and modify operations on multilinks (bug #579094)
+#
+# Revision 1.75  2002/07/09 01:21:24  richard
+# Added ability for unit tests to turn off exception handling in mailgw so
+# that exceptions are reported earlier (and hence make sense).
+#
+# Revision 1.74  2002/05/29 01:16:17  richard
+# Sorry about this huge checkin! It's fixing a lot of related stuff in one go
+# though.
+#
+# . #541941 ] changing multilink properties by mail
+# . #526730 ] search for messages capability
+# . #505180 ] split MailGW.handle_Message
+#   - also changed cgi client since it was duplicating the functionality
+# . build htmlbase if tests are run using CVS checkout (removed note from
+#   installation.txt)
+# . don't create an empty message on email issue creation if the email is empty
+#
 # Revision 1.73  2002/05/22 04:12:05  richard
 #  . applied patch #558876 ] cgi client customization
 #    ... with significant additions and modifications ;)