I've created a patch to Mailman to add XML-RPC based remote procedure
calls to mailman.  This is just a first cut to show that it can be
done and to do a few basic operations I need to plug it into a kind of
hosting framework so it can be controlled from a separate web server. 
I plan to add functionality to it to fully meet my needs, but I
thought I'd throw it out there so that I can get some feedback.  It's
made against a pristine 2.1.6 tarball.

In order to reuse code, I had to refactor quite a few files.  Most
notably some of the Cgi interfaces that do real work were tied very
closely to the idea that you want to output HTML.  You don't with
XML-RPC, so I split out the work from the formatting and replaced said
formatting with some exception handling.  I welcome comments on this
part of the patch.  I'd like to know how to make this better.

You can see that I handle authentication in each XML-RPC call that
needs it, so it shouldn't be any less secure than the appropriate Cgi
call.

Finally, I'd like some discussion of licensing.  I think that the
xmlrpc.py file itself should be LGPL just so that there is no
ambiguity whatsoever about using XML-RPC calls from a non-GPL
application.  However part of me thinks that this is unnecessary
because there is no import required to use XML-RPC, and therefore,
calling these functions would not be considered "linking" in any
technical sense of the word, and therefore would not require making
the calling application GPL.  Thoughts?

-- 
Joseph Tate
Personal e-mail: jtate AT dragonstrider DOT com
Web: http://www.dragonstrider.com
--- mailman-2.1.6/bin/config_list       2003-11-24 18:08:19.000000000 -0500
+++ rpath-mailman-2.1.6/bin/config_list 2005-07-21 11:17:15.370952832 -0400
@@ -202,25 +202,6 @@
         print >> outfp
 
 
-
-def getPropertyMap(mlist):
-    guibyprop = {}
-    categories = mlist.GetConfigCategories()
-    for category, (label, gui) in categories.items():
-        if not hasattr(gui, 'GetConfigInfo'):
-            continue
-        subcats = mlist.GetConfigSubCategories(category)
-        if subcats is None:
-            subcats = [(None, None)]
-        for subcat, sclabel in subcats:
-            for element in gui.GetConfigInfo(mlist, category, subcat):
-                if not isinstance(element, TupleType):
-                    continue
-                propname = element[0]
-                wtype = element[1]
-                guibyprop[propname] = (gui, wtype)
-    return guibyprop
-
 
 class FakeDoc:
     # Fake the error reporting API for the htmlformat.Document class
@@ -232,7 +213,7 @@
     def set_language(self, val):
         pass
 
-
+
 def do_input(listname, infile, checkonly, verbose):
     fakedoc = FakeDoc()
     # open the specified list locked, unless checkonly is set
@@ -241,7 +222,7 @@
     except Errors.MMListError, e:
         usage(1, _('No such list "%(listname)s"\n%(e)s'))
     savelist = 0
-    guibyprop = getPropertyMap(mlist)
+    guibyprop = mlist.GetPropertyMap()
     try:
         globals = {'mlist': mlist}
         # Any exception that occurs in execfile() will cause the list to not
@@ -249,57 +230,13 @@
         execfile(infile, globals)
         savelist = 1
         for k, v in globals.items():
-            if k in ('mlist', '__builtins__'):
-                continue
-            if not hasattr(mlist, k):
-                print >> sys.stderr, _('attribute "%(k)s" ignored')
-                continue
-            if verbose:
-                print >> sys.stderr, _('attribute "%(k)s" changed')
-            missing = []
-            gui, wtype = guibyprop.get(k, (missing, missing))
-            if gui is missing:
-                # This isn't an official property of the list, but that's
-                # okay, we'll just restore it the old fashioned way
-                print >> sys.stderr, _('Non-standard property restored: %(k)s')
-                setattr(mlist, k, v)
-            else:
-                # BAW: This uses non-public methods.  This logic taken from
-                # the guts of GUIBase.handleForm().
-                try:
-                    validval = gui._getValidValue(mlist, k, wtype, v)
-                except ValueError:
-                    print >> sys.stderr, _('Invalid value for property: %(k)s')
-                except Errors.EmailAddressError:
-                    print >> sys.stderr, _(
-                        'Bad email address for option %(k)s: %(v)s')
-                else:
-                    # BAW: Horrible hack, but then this is special cased
-                    # everywhere anyway. :(  Privacy._setValue() knows that
-                    # when ALLOW_OPEN_SUBSCRIBE is false, the web values are
-                    # 0, 1, 2 but these really should be 1, 2, 3, so it adds
-                    # one.  But we really do provide [0..3] so we need to undo
-                    # the hack that _setValue adds. :( :(
-                    if k == 'subscribe_policy' and \
-                           not mm_cfg.ALLOW_OPEN_SUBSCRIBE:
-                        validval -= 1
-                    # BAW: Another horrible hack.  This one is just too hard
-                    # to fix in a principled way in Mailman 2.1
-                    elif k == 'new_member_options':
-                        # Because this is a Checkbox, _getValidValue()
-                        # transforms the value into a list of one item.
-                        validval = validval[0]
-                        validval = [bitfield for bitfield, bitval
-                                    in mm_cfg.OPTINFO.items()
-                                    if validval & bitval]
-                    gui._setValue(mlist, k, validval, fakedoc)
-            # BAW: when to do gui._postValidate()???
+            mlist.SetValue(guibyprop, k, v, fakedoc, verbose)
+        # BAW: when to do gui._postValidate()???
     finally:
         if savelist and not checkonly:
             mlist.Save()
         mlist.Unlock()
 
-
 
 def main():
     try:
--- mailman-2.1.6/Mailman/Cgi/create.py 2004-12-30 15:49:30.000000000 -0500
+++ rpath-mailman-2.1.6/Mailman/Cgi/create.py   2005-07-20 16:27:22.627625320 
-0400
@@ -69,78 +69,31 @@
     doc.AddItem(MailmanLogo())
     print doc.Format()
 
-
 
-def process_request(doc, cgidata):
-    # Lowercase the listname since this is treated as the "internal" name.
-    listname = cgidata.getvalue('listname', '').strip().lower()
-    owner    = cgidata.getvalue('owner', '').strip()
-    try:
-        autogen  = int(cgidata.getvalue('autogen', '0'))
-    except ValueError:
-        autogen = 0
-    try:
-        notify  = int(cgidata.getvalue('notify', '0'))
-    except ValueError:
-        notify = 0
-    try:
-        moderate = int(cgidata.getvalue('moderate', '0'))
-    except ValueError:
-        moderate = mm_cfg.DEFAULT_DEFAULT_MEMBER_MODERATION
-
-    password = cgidata.getvalue('password', '').strip()
-    confirm  = cgidata.getvalue('confirm', '').strip()
-    auth     = cgidata.getvalue('auth', '').strip()
-    langs    = cgidata.getvalue('langs', [mm_cfg.DEFAULT_SERVER_LANGUAGE])
-
+class CreateException(Exception):
+    def __init__(self, msg):
+        self.msg = msg
+    def __str__(self):
+        return self.msg
+
+def add_list(auth, listname, owner, password,
+            notify = False, moderate = False,
+            langs = [mm_cfg.DEFAULT_SERVER_LANGUAGE]):
     if not isinstance(langs, ListType):
         langs = [langs]
     # Sanity check
     safelistname = Utils.websafe(listname)
     if '@' in listname:
-        request_creation(doc, cgidata,
+        raise CreateException(
                          _('List name must not include "@": %(safelistname)s'))
         return
     if Utils.list_exists(listname):
         # BAW: should we tell them the list already exists?  This could be
         # used to mine/guess the existance of non-advertised lists.  Then
         # again, that can be done in other ways already, so oh well.
-        request_creation(doc, cgidata,
+        raise CreateException(
                          _('List already exists: %(safelistname)s'))
         return
-    if not listname:
-        request_creation(doc, cgidata,
-                         _('You forgot to enter the list name'))
-        return
-    if not owner:
-        request_creation(doc, cgidata,
-                         _('You forgot to specify the list owner'))
-        return
-
-    if autogen:
-        if password or confirm:
-            request_creation(
-                doc, cgidata,
-                _('''Leave the initial password (and confirmation) fields
-                blank if you want Mailman to autogenerate the list
-                passwords.'''))
-            return
-        password = confirm = Utils.MakeRandomPassword(
-            mm_cfg.ADMIN_PASSWORD_LENGTH)
-    else:
-        if password <> confirm:
-            request_creation(doc, cgidata,
-                             _('Initial list passwords do not match'))
-            return
-        if not password:
-            request_creation(
-                doc, cgidata,
-                # The little <!-- ignore --> tag is used so that this string
-                # differs from the one in bin/newlist.  The former is destined
-                # for the web while the latter is destined for email, so they
-                # must be different entries in the message catalog.
-                _('The list password cannot be empty<!-- ignore -->'))
-            return
     # The authorization password must be non-empty, and it must match either
     # the list creation password or the site admin password
     ok = 0
@@ -149,8 +102,7 @@
         if not ok:
             ok = Utils.check_global_password(auth)
     if not ok:
-        request_creation(
-            doc, cgidata,
+        raise CreateException(
             _('You are not authorized to create new mailing lists'))
         return
     # Make sure the web hostname matches one of our virtual domains
@@ -158,7 +110,7 @@
     if mm_cfg.VIRTUAL_HOST_OVERVIEW and \
            not mm_cfg.VIRTUAL_HOSTS.has_key(hostname):
         safehostname = Utils.websafe(hostname)
-        request_creation(doc, cgidata,
+        raise CreateException(
                          _('Unknown virtual host: %(safehostname)s'))
         return
     emailhost = mm_cfg.VIRTUAL_HOSTS.get(hostname, mm_cfg.DEFAULT_EMAIL_HOST)
@@ -189,20 +141,19 @@
             finally:
                 os.umask(oldmask)
         except Errors.EmailAddressError, s:
-            request_creation(doc, cgidata,
+            raise CreateException(
                              _('Bad owner email address: %(s)s'))
             return
         except Errors.MMListAlreadyExistsError:
-            request_creation(doc, cgidata,
+            raise CreateException(
                              _('List already exists: %(listname)s'))
             return
         except Errors.BadListNameError, s:
-            request_creation(doc, cgidata,
+            raise CreateException(
                              _('Illegal list name: %(s)s'))
             return
         except Errors.MMListError:
-            request_creation(
-                doc, cgidata,
+            raise CreateException(
                 _('''Some unknown error occurred while creating the list.
                 Please contact the site administrator for assistance.'''))
             return
@@ -244,6 +195,70 @@
             text, mlist.preferred_language)
         msg.send(mlist)
 
+    return True
+
+
+def process_request(doc, cgidata):
+    # Lowercase the listname since this is treated as the "internal" name.
+    listname = cgidata.getvalue('listname', '').strip().lower()
+    owner    = cgidata.getvalue('owner', '').strip()
+    try:
+        autogen  = int(cgidata.getvalue('autogen', '0'))
+    except ValueError:
+        autogen = 0
+    try:
+        notify  = int(cgidata.getvalue('notify', '0'))
+    except ValueError:
+        notify = 0
+    try:
+        moderate = int(cgidata.getvalue('moderate', '0'))
+    except ValueError:
+        moderate = mm_cfg.DEFAULT_DEFAULT_MEMBER_MODERATION
+
+    password = cgidata.getvalue('password', '').strip()
+    confirm  = cgidata.getvalue('confirm', '').strip()
+    auth     = cgidata.getvalue('auth', '').strip()
+    langs    = cgidata.getvalue('langs', [mm_cfg.DEFAULT_SERVER_LANGUAGE])
+
+    if not listname:
+        request_creation(doc, cgidata,
+                         _('You forgot to enter the list name'))
+        return
+    if not owner:
+        request_creation(doc, cgidata,
+                         _('You forgot to specify the list owner'))
+        return
+
+    if autogen:
+        if password or confirm:
+            request_creation(
+                doc, cgidata,
+                _('''Leave the initial password (and confirmation) fields
+                blank if you want Mailman to autogenerate the list
+                passwords.'''))
+            return
+        password = confirm = Utils.MakeRandomPassword(
+            mm_cfg.ADMIN_PASSWORD_LENGTH)
+    else:
+        if password <> confirm:
+            request_creation(doc, cgidata,
+                             _('Initial list passwords do not match'))
+            return
+        if not password:
+            request_creation(
+                doc, cgidata,
+                # The little <!-- ignore --> tag is used so that this string
+                # differs from the one in bin/newlist.  The former is destined
+                # for the web while the latter is destined for email, so they
+                # must be different entries in the message catalog.
+                _('The list password cannot be empty<!-- ignore -->'))
+            return
+
+    try:
+        add_list(auth, listname, owner, password, notify, moderate, langs)
+    except CreateException, e:
+        request_creation(doc, cgidata, str(e))
+        return
     # Success!
     listinfo_url = mlist.GetScriptURL('listinfo', absolute=1)
     admin_url = mlist.GetScriptURL('admin', absolute=1)
--- mailman-2.1.6/Mailman/Cgi/rmlist.py 2002-12-02 09:32:11.000000000 -0500
+++ rpath-mailman-2.1.6/Mailman/Cgi/rmlist.py   2005-07-20 16:21:25.957847368 
-0400
@@ -97,27 +97,30 @@
     doc.AddItem(mlist.GetMailmanFooter())
     print doc.Format()
 
+class RemoveException(Exception):
+    def __init__(self, msg):
+        self.msg = msg
 
-
-def process_request(doc, cgidata, mlist):
-    password = cgidata.getvalue('password', '').strip()
-    try:
-        delarchives = int(cgidata.getvalue('delarchives', '0'))
-    except ValueError:
-        delarchives = 0
+    def __str__(self):
+        return msg
 
+
+def delete_list(password, mlist, delarchives = False):
     # Removing a list is limited to the list-creator (a.k.a. list-destroyer),
     # the list-admin, or the site-admin.  Don't use WebAuthenticate here
     # because we want to be sure the actual typed password is valid, not some
     # password sitting in a cookie.
-    if mlist.Authenticate((mm_cfg.AuthCreator,
-                           mm_cfg.AuthListAdmin,
-                           mm_cfg.AuthSiteAdmin),
-                          password) == mm_cfg.UnAuthorized:
-        request_deletion(
-            doc, mlist,
+    authTypes = ()
+    if mm_cfg.OWNERS_CAN_DELETE_THEIR_OWN_LISTS:
+        authTypes = (mm_cfg.AuthCreator,
+                     mm_cfg.AuthListAdmin,
+                     mm_cfg.AuthSiteAdmin)
+    else:
+        authTypes = (mm_cfg.AuthCreator,
+                     mm_cfg.AuthSiteAdmin)
+    if mlist.Authenticate( authTypes, password) == mm_cfg.UnAuthorized:
+        raise RemoveException(
             _('You are not authorized to delete this mailing list'))
-        return
 
     # Do the MTA-specific list deletion tasks
     if mm_cfg.MTA:
@@ -157,6 +160,22 @@
                        'directory %s not deleted due to permission problems',
                        dir)
 
+    return problems
+
+
+def process_request(doc, cgidata, mlist):
+    password = cgidata.getvalue('password', '').strip()
+    try:
+        delarchives = int(cgidata.getvalue('delarchives', '0'))
+    except ValueError:
+        delarchives = 0
+
+    try:
+        problems = delete_list(password, mlist, delarchives)
+    except RemoveException, e:
+        request_deletion(doc, mlist, str(e))
+        return
+
     title = _('Mailing list deletion results')
     doc.SetTitle(title)
     table = Table(border=0, width='100%')
--- mailman-2.1.6/Mailman/Cgi/xmlrpc.py 1969-12-31 19:00:00.000000000 -0500
+++ rpath-mailman-2.1.6/Mailman/Cgi/xmlrpc.py   2005-07-21 11:41:07.974164040 
-0400
@@ -0,0 +1,179 @@
+#!/usr/bin/python
+
+# This code is lgpl in order to link against the Mailman tree
+
+import sys
+import os
+import sha
+import xmlrpclib
+from SimpleXMLRPCServer import CGIXMLRPCRequestHandler
+
+#Mailman imports
+from Mailman import Utils, MailList, Errors, i18n, mm_cfg
+from Mailman.Cgi import create, rmlist
+from Mailman.Logging.Syslog import syslog
+
+_ = i18n._
+
+#Possible errors: ListAlreadyExists, PermissionDenied, MMUnknownListError
+
+
+def reportError(errormsg):
+    """Abstraction function.  This allows us to change how errors
+    are reported to the caller
+    """
+    return [True, errormsg]
+
+
+class GenericException(Exception):
+    """Simple Exception class which allows us to pass readable error messages
+    back and forth
+    """
+    def __init__(self, msg):
+        self.msg = msg
+    def __str__(self):
+        return self.msg
+
+
+class XMLDoc:
+    #Fake error reporting API for the htmlformat.Document class
+    def __init__(self):
+        self.errors = []
+    def addError(self, s, tag=None, *args):
+        if tag:
+            s = tag + " " + s
+        self.errors.append(s % args)
+
+    def set_language(self, val):
+        pass
+
+
+class MailmanXMLRPC(object):
+    """This class implements the functionality available from XML-RPC calls.
+    At the moment it only contains a handful of methods, but could be easily
+    extended to complete functionality."""
+
+
+    def _checkListPassword(self, pw, mlist):
+        """Do the permissions granted to the password supplied allow for
+        changing list settings?
+        """
+        return mlist.Authenticate((mm_cfg.AuthListAdmin, 
+                                      mm_cfg.AuthSiteAdmin), pw)
+
+
+    def list_lists(self, filter = ''):
+        """List mailing lists on this server, optionally filtering by filter
+        """
+        listnames = Utils.list_names()
+        returner = []
+        for list in listnames:
+            if list.startswith(filter):
+                returner.append(list)
+        return returner
+
+
+    def set_list_settings(self, pw, listname, settings = {}):
+        """Set individual list settings.  settings must be passed in as a
+        dictionary containing setting name/value pairs.
+        """
+        mlist = MailList.MailList(listname, lock=False)
+        if not self._checkListPassword(pw, mlist):
+            raise GenericException(_('Authorization failed.'))
+        #Do the work
+        errordoc = XMLDoc()
+        guibyprop = mlist.GetPropertyMap()
+        mlist.Lock()
+        for setting, value in settings.items():
+            mlist.SetValue(guibyprop, setting, value, errordoc)
+        mlist.Save()
+        mlist.Unlock()
+        if errordoc:
+            return errordoc.errors
+        else
+            return True
+
+
+    def add_list(self, adminpw, listname, owners, listpw,
+                 notify = False, moderate = False):
+        """Create a new mailing list on this server.
+        """
+        try:
+            create.add_list(adminpw, listname, owners[0], listpw, 
+                            notify, moderate)
+        except create.CreateException, e:
+            raise Exception(str(e))
+        return self._set_owners(listname, owners)
+
+
+    def set_owners(self, adminpw, listname, owners):
+        """ This function should go away when set_list_settings is completed.
+        """
+        if _checkListPassword(adminpw):
+            return self.set_owners(listname, owners)
+        else:
+            raise GenericException(_('Authorization failed.'))
+
+
+    def _set_owners(self, listname, owners):
+        """Assumes that authentication has already taken place"""
+        mlist = MailList.MailList(listname, lock = True)
+        mlist.owner = owners
+        mlist.Save()
+        mlist.Unlock()
+        return True
+
+
+    def delete_list(self, password, listname, delarchives = False):
+        """Delete the list from the server and optionally delete the archives
+        """
+        mlist = MailList.MailList(listname, lock=True)
+        try:
+            rmlist.delete_list(password, mlist, delarchives)
+        except rmlist.RemoveException, e:
+            raise GenericException(str(e)) 
+        return True
+
+
+    def _dispatch(self, methodName, args):
+        """This function is the wrapper which calls the other methods
+        We override this so that we can wrap all results in a list/tuple
+        context so we can report reasonable errors.
+        """
+        # Find the method pointer
+        if methodName.startswith("_"):
+            return reportError(_("Cannot call private functions"))
+        try:
+            method = self.__getattribute__(methodName)
+        except AttributeError:
+            return reportError(_("MethodNotSupported: %(methodName)s"))
+        # Make the actual method call
+        try:
+            r = method(*args)
+            # XXX: add some individualized handling for the common exceptions
+        except Exception, e:
+            syslog('error', "Error calling %s(%s)", methodName, str(args))
+            return reportError(str(e))
+        return [False, r]
+
+
+def rawhandler():
+    #This method will handle XMLRPC calls in cases where
+    #CGIXMLRPCRequestHandler is not available
+    params, method =  xmlrpclib.loads(sys.stdin.read())
+    rpcserver = MailmanXMLRPC()
+    #make the call
+    results = rpcserver._dispatch(method, params)
+    resp = xmlrpclib.dumps(results)
+    print "Content-type: text/xml\n\n"
+    print resp
+
+
+def main():
+    handler = CGIXMLRPCRequestHandler()
+    handler.register_instance(MailmanXMLRPC())
+    handler.handle_request()
+
+
+if __name__=="__main__":
+    main()
--- mailman-2.1.6/Mailman/MailList.py   2005-02-15 19:21:41.000000000 -0500
+++ rpath-mailman-2.1.6/Mailman/MailList.py     2005-07-21 10:58:53.683434792 
-0400
@@ -493,6 +493,81 @@
         else:
             self.available_languages = langs
 
+
+    #
+    # Settings manipulation
+    #
+    def SetValue(mlist, guibyprop, property, value, errordoc, verbose = False):
+        """guibyprop is a big dictionary generated by getPropertyMap
+        This code is moved from config_list so that it can be more generally
+        available.
+        Value should be set according to how it's dumped from config_list.  For
+        the most part, the caller is required to ensure the correctness of the
+        data values.
+        """
+        if property in ('mlist', '__builtins__'):
+            return
+        if not hasattr(mlist, property):
+            errordoc.addError( _('attribute "%(property)s" ignored')) 
+            return
+        if verbose:
+            errordoc.addError( _('attribute "%(property)s" changed'))
+        missing = []
+        gui, wtype = guibyprop.get(property, (missing, missing))
+        if gui is missing:
+            # This isn't an official property of the list, but that's
+            # okay, we'll just restore it the old fashioned way
+            errordoc.addError( _('Non-standard property restored: 
%(property)s'))
+            setattr(mlist, property, value)
+        else:
+            # BAW: This uses non-public methods.  This logic taken from
+            # the guts of GUIBase.handleForm().
+            try:
+                validval = gui._getValidValue(mlist, property, wtype, value)
+            except ValueError:
+                errordoc.addError( _('Invalid value for property: 
%(property)s'))
+            except Errors.EmailAddressError:
+                errordoc.addError(_(
+                    'Bad email address for option %(property)s: %(value)s'))
+            else:
+                # BAW: Horrible hack, but then this is special cased
+                # everywhere anyway. :(  Privacy._setValue() knows that
+                # when ALLOW_OPEN_SUBSCRIBE is false, the web values are
+                # 0, 1, 2 but these really should be 1, 2, 3, so it adds
+                # one.  But we really do provide [0..3] so we need to undo
+                # the hack that _setValue adds. :( :(
+                if property == 'subscribe_policy' and \
+                       not mm_cfg.ALLOW_OPEN_SUBSCRIBE:
+                    validval -= 1
+                # BAW: Another horrible hack.  This one is just too hard
+                # to fix in a principled way in Mailman 2.1
+                elif property == 'new_member_options':
+                    # Because this is a Checkbox, _getValidValue()
+                    # transforms the value into a list of one item.
+                    validval = validval[0]
+                    validval = [bitfield for bitfield, bitval
+                                in mm_cfg.OPTINFO.items()
+                                if validval & bitval]
+                gui._setValue(mlist, property, validval, errordoc)
+
+    
+    def GetPropertyMap(mlist):
+        guibyprop = {}
+        categories = mlist.GetConfigCategories()
+        for category, (label, gui) in categories.items():
+            if not hasattr(gui, 'GetConfigInfo'):
+                continue
+            subcats = mlist.GetConfigSubCategories(category)
+            if subcats is None:
+                subcats = [(None, None)]
+            for subcat, sclabel in subcats:
+                for element in gui.GetConfigInfo(mlist, category, subcat):
+                    if not isinstance(element, TupleType):
+                        continue
+                    propname = element[0]
+                    wtype = element[1]
+                    guibyprop[propname] = (gui, wtype)
+        return guibyprop
 
 
     #
--- mailman-2.1.6/src/Makefile.in       2003-03-31 14:27:14.000000000 -0500
+++ rpath-mailman-2.1.6/src/Makefile.in 2005-07-20 12:09:33.037360504 -0400
@@ -71,7 +71,7 @@
 # Fixed definitions
 
 CGI_PROGS= admindb admin confirm create edithtml listinfo options \
-       private rmlist roster subscribe
+       private rmlist roster subscribe xmlrpc
 
 COMMONOBJS= common.o vsnprintf.o
 
_______________________________________________
Mailman-Developers mailing list
[email protected]
http://mail.python.org/mailman/listinfo/mailman-developers
Mailman FAQ: http://www.python.org/cgi-bin/faqw-mm.py
Searchable Archives: http://www.mail-archive.com/mailman-users%40python.org/
Unsubscribe: 
http://mail.python.org/mailman/options/mailman-developers/archive%40jab.org

Security Policy: 
http://www.python.org/cgi-bin/faqw-mm.py?req=show&amp;file=faq01.027.htp

Reply via email to