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&file=faq01.027.htp