diff -urN mailman-2.1.7/Mailman/Cgi/admindb.py mailman-2.1.7-openid/Mailman/Cgi/admindb.py
--- mailman-2.1.7/Mailman/Cgi/admindb.py	2005-12-30 10:50:07.000000000 -0800
+++ mailman-2.1.7-openid/Mailman/Cgi/admindb.py	2006-01-23 12:09:46.000000000 -0800
@@ -101,6 +101,21 @@
                                   mm_cfg.AuthListModerator,
                                   mm_cfg.AuthSiteAdmin),
                                  cgidata.getvalue('adminpw', '')):
+        openid_url = cgidata.getvalue('openid_url', '')
+        if openid_url:
+            rdurl = mlist.OpenIDAuthenticate('admindb',
+                                    (mm_cfg.AuthListAdmin,
+                                     mm_cfg.AuthListModerator,
+                                     mm_cfg.AuthSiteAdmin), openid_url)
+            if rdurl:
+                Utils.redirect(rdurl)
+                return
+            else:
+                msg = Bold(FontSize('+1', 
+                    _('Invalid OpenID URL.'))).Format()
+                Auth.loginpage(mlist, 'admindb', msg=msg)
+                return
+
         if cgidata.has_key('adminpw'):
             # This is a re-authorization attempt
             msg = Bold(FontSize('+1', _('Authorization failed.'))).Format()
diff -urN mailman-2.1.7/Mailman/Cgi/admin.py mailman-2.1.7-openid/Mailman/Cgi/admin.py
--- mailman-2.1.7/Mailman/Cgi/admin.py	2005-12-30 10:50:07.000000000 -0800
+++ mailman-2.1.7-openid/Mailman/Cgi/admin.py	2006-01-23 12:09:46.000000000 -0800
@@ -85,6 +85,21 @@
     if not mlist.WebAuthenticate((mm_cfg.AuthListAdmin,
                                   mm_cfg.AuthSiteAdmin),
                                  cgidata.getvalue('adminpw', '')):
+        openid_url = cgidata.getvalue('openid_url', '')
+        if openid_url:
+            rdurl = mlist.OpenIDAuthenticate('admin',
+                                    (mm_cfg.AuthListAdmin,
+                                     mm_cfg.AuthSiteAdmin),
+                                    cgidata.getvalue('openid_url', ''))
+            if rdurl:
+                Utils.redirect(rdurl)
+                return
+            else:
+                msg = Bold(FontSize('+1', 
+                    _('Invalid OpenID URL.'))).Format()
+                Auth.loginpage(mlist, 'admin', msg=msg)
+                return
+            
         if cgidata.has_key('adminpw'):
             # This is a re-authorization attempt
             msg = Bold(FontSize('+1', _('Authorization failed.'))).Format()
@@ -658,6 +673,13 @@
             r, c = None, None
         res = NL.join(value)
         return TextArea(varname, res, r, c, wrap='off')
+    elif kind == mm_cfg.OpenIDList:
+        if params:
+            r, c = params
+        else:
+            r, c = None, None
+        res = NL.join(value)
+        return TextArea(varname, res, r, c, wrap='off')
     elif kind == mm_cfg.FileUpload:
         # like a text area, but also with uploading
         if params:
diff -urN mailman-2.1.7/Mailman/Cgi/create.py mailman-2.1.7-openid/Mailman/Cgi/create.py
--- mailman-2.1.7/Mailman/Cgi/create.py	2005-12-30 10:50:07.000000000 -0800
+++ mailman-2.1.7-openid/Mailman/Cgi/create.py	2006-01-23 12:09:46.000000000 -0800
@@ -94,6 +94,16 @@
     confirm  = cgidata.getvalue('confirm', '').strip()
     auth     = cgidata.getvalue('auth', '').strip()
     langs    = cgidata.getvalue('langs', [mm_cfg.DEFAULT_SERVER_LANGUAGE])
+    
+    owner_openid = cgidata.getvalue('owner_openid', '').strip()
+
+    if owner_openid:
+        try:
+            owner_openid = Utils.ValidateOpenID(owner_openid)
+        except Errors.OpenIDError:
+            request_creation(doc, cgidata,
+                _('Not a valid OpenID: %(owner_openid)s'))
+            return
 
     if not isinstance(langs, ListType):
         langs = [langs]
@@ -214,6 +224,9 @@
         mlist.default_member_moderation = moderate
         mlist.web_page_url = mm_cfg.DEFAULT_URL_PATTERN % hostname
         mlist.host_name = emailhost
+
+        mlist.owner_openid = [owner_openid]
+        
         mlist.Save()
     finally:
         # Now be sure to unlock the list.  It's okay if we get a signal here
@@ -333,6 +346,12 @@
     ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 0, bgcolor=GREY)
     ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 1, bgcolor=GREY)
 
+    safeowneroid = Utils.websafe(cgidata.getvalue('owner_openid', ''))
+    ftable.AddRow([Label(_('Initial admin OpenID:')),
+                   TextBox('owner_openid', safeowneroid)])
+    ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 0, bgcolor=GREY)
+    ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 1, bgcolor=GREY)
+ 
     try:
         autogen = int(cgidata.getvalue('autogen', '0'))
     except ValueError:
diff -urN mailman-2.1.7/Mailman/Cgi/listinfo.py mailman-2.1.7-openid/Mailman/Cgi/listinfo.py
--- mailman-2.1.7/Mailman/Cgi/listinfo.py	2005-08-26 18:40:15.000000000 -0700
+++ mailman-2.1.7-openid/Mailman/Cgi/listinfo.py	2006-01-23 12:09:46.000000000 -0800
@@ -178,6 +178,7 @@
         'email-button', text=_('Subscribe'))
     replacements['<mm-new-password-box>'] = mlist.FormatSecureBox('pw')
     replacements['<mm-confirm-password>'] = mlist.FormatSecureBox('pw-conf')
+    replacements['<mm-new-openid-box>'] = mlist.FormatOpenIDBox('openid_url')
     replacements['<mm-subscribe-form-start>'] = mlist.FormatFormStart(
         'subscribe')
     # Roster form substitutions
diff -urN mailman-2.1.7/Mailman/Cgi/oidconfirm.py mailman-2.1.7-openid/Mailman/Cgi/oidconfirm.py
--- mailman-2.1.7/Mailman/Cgi/oidconfirm.py	1969-12-31 16:00:00.000000000 -0800
+++ mailman-2.1.7-openid/Mailman/Cgi/oidconfirm.py	2006-01-23 12:09:46.000000000 -0800
@@ -0,0 +1,211 @@
+# Copyright (C) 2001-2006 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+"""Confirm a pending action via URL."""
+
+import os
+import signal
+import cgi
+import time
+import urllib
+
+from Mailman import mm_cfg
+from Mailman import Errors
+from Mailman import i18n
+from Mailman import MailList
+from Mailman import Pending
+from Mailman.UserDesc import UserDesc
+from Mailman.htmlformat import *
+from Mailman.Logging.Syslog import syslog
+
+# Set up i18n
+_ = i18n._
+i18n.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE)
+
+try:
+    True, False
+except NameError:
+    True = 1
+    False = 0
+
+
+
+def main():
+    doc = Document()
+    doc.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE)
+
+    parts = Utils.GetPathPieces()
+    if not parts or len(parts) != 2:
+        bad_confirmation(doc)
+        doc.AddItem(MailmanLogo())
+        print doc.Format()
+        return
+    
+    query = dict(cgi.parse_qsl(os.environ.get('QUERY_STRING')))
+
+    listname = parts[0].lower()
+    try:
+        mlist = MailList.MailList(listname, lock=0)
+    except Errors.MMListError, e:
+        # Avoid cross-site scripting attacks
+        safelistname = Utils.websafe(listname)
+        bad_confirmation(doc, _('No such list <em>%(safelistname)s</em>'))
+        doc.AddItem(MailmanLogo())
+        print doc.Format()
+        syslog('error', 'No such list "%s": %s', listname, e)
+        return
+
+    # Set the language for the list
+    i18n.set_language(mlist.preferred_language)
+    doc.set_language(mlist.preferred_language)
+
+    cookie = parts[1]
+    
+    if not cookie:
+        bad_confirmation(doc)
+        doc.AddItem(mlist.GetMailmanFooter())
+        print doc.Format()
+        return
+
+    days = int(mm_cfg.PENDING_REQUEST_LIFE / mm_cfg.days(1) + 0.5)
+    badconfirmstr = _('''<b>Invalid confirmation URL</b>
+
+    <p>Note that confirmation strings expire approximately
+    %(days)s days after the initial subscription request.  If your
+    confirmation has expired, please try to re-submit your subscription.
+    Otherwise, try the link in your confirmation email again.</p>''')
+
+    content = mlist.pend_confirm(cookie, expunge=False)
+    if content is None:
+        bad_confirmation(doc, badconfirmstr)
+        doc.AddItem(mlist.GetMailmanFooter())
+        print doc.Format()
+        return
+
+    try:
+        if content[0] == Pending.SUBSCRIPTION:
+            userdesc = content[1]
+            if query.get('openid.identity'):
+                # We are getting back from the server.
+                openid_url = mlist.OpenIDComplete(query)
+                
+                if openid_url == userdesc.openid:
+                    subscription_confirm(mlist, doc, cookie)
+                else:
+                    # maliciousness
+                    bad_confirmation(doc, 
+                        _("""The OpenID you verified: %(openid_url)s does not
+                            match the OpenID for this cookie."""))
+                    syslog('mischief', 
+                        "Unauthorized use of confirmation page by %(openid_url)s")
+                
+            else:
+                # redirect to the server
+                rd_url = mlist.OpenIDSubscribe(userdesc.openid, cookie)
+
+                if not rd_url:
+                    bad_confirmation(doc, _('Not a valid OpenID'))
+                    doc.AddItem(mlist.GetMailmanFooter())
+                    print doc.Format()
+                    return
+
+                Utils.redirect(rd_url)
+                return
+            
+            
+        else:
+            bad_confirmation(doc, _('System error, bad content: %(content)s'))
+    except Errors.MMBadConfirmation:
+        bad_confirmation(doc, badconfirmstr)
+
+    doc.AddItem(mlist.GetMailmanFooter())
+    print doc.Format()
+
+
+
+def bad_confirmation(doc, extra=''):
+    title = _('Bad confirmation string')
+    doc.SetTitle(title)
+    doc.AddItem(Header(3, Bold(FontAttr(title, color='#ff0000', size='+2'))))
+    doc.AddItem(extra)
+
+
+
+
+def subscription_confirm(mlist, doc, cookie):
+    # See the comment in admin.py about the need for the signal
+    # handler.
+    def sigterm_handler(signum, frame, mlist=mlist):
+        mlist.Unlock()
+        sys.exit(0)
+
+    listname = mlist.real_name
+    mlist.Lock()
+    try:
+        try:
+            userdesc = mlist.pend_confirm(cookie, expunge=False)[1]
+            op, addr, pw, digest, lang = mlist.ProcessConfirmation(
+                cookie, userdesc)
+        except Errors.MMNeedApproval:
+            title = _('Awaiting moderator approval')
+            doc.SetTitle(title)
+            doc.AddItem(Header(3, Bold(FontAttr(title, size='+2'))))
+            doc.AddItem(_("""\
+            You have successfully confirmed your subscription request to the
+            mailing list %(listname)s, however final approval is required from
+            the list moderator before you will be subscribed.  Your request
+            has been forwarded to the list moderator, and you will be notified
+            of the moderator's decision."""))
+        except Errors.NotAMemberError:
+            bad_confirmation(doc, _('''Invalid confirmation string.  It is
+            possible that you are attempting to confirm a request for an
+            address that has already been unsubscribed.'''))
+        except Errors.MMAlreadyAMember:
+            doc.addError(_("You are already a member of this mailing list!"))
+        except Errors.MembershipIsBanned:
+            owneraddr = mlist.GetOwnerEmail()
+            doc.addError(_("""You are currently banned from subscribing to
+            this list.  If you think this restriction is erroneous, please
+            contact the list owners at %(owneraddr)s."""))
+        except Errors.HostileSubscriptionError:
+            doc.addError(_("""\
+            You were not invited to this mailing list.  The invitation has
+            been discarded, and both list administrators have been
+            alerted."""))
+        else:
+            # Use the user's preferred language
+            i18n.set_language(lang)
+            doc.set_language(lang)
+            # The response
+            listname = mlist.real_name
+            title = _('Subscription request confirmed')
+            optionsurl = mlist.GetOptionsURL(addr, absolute=1)
+            doc.SetTitle(title)
+            doc.AddItem(Header(3, Bold(FontAttr(title, size='+2'))))
+            doc.AddItem(_('''\
+            You have successfully confirmed your subscription request for
+            "%(addr)s" to the %(listname)s mailing list.  A separate
+            confirmation message will be sent to your email address, along
+            with your password, and other useful information and links.
+
+            <p>You can now
+            <a href="%(optionsurl)s">proceed to your membership login
+            page</a>.'''))
+        mlist.Save()
+    finally:
+        mlist.Unlock()
+
+
diff -urN mailman-2.1.7/Mailman/Cgi/openid.py mailman-2.1.7-openid/Mailman/Cgi/openid.py
--- mailman-2.1.7/Mailman/Cgi/openid.py	1969-12-31 16:00:00.000000000 -0800
+++ mailman-2.1.7-openid/Mailman/Cgi/openid.py	2006-01-23 12:09:46.000000000 -0800
@@ -0,0 +1,144 @@
+# Copyright (C) 1998-2005 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+"""Handle return from OpenID servers.  Provided the OpenID was valid,
+give an auth cookie and redirect to the specified page.
+"""
+
+# For Python 2.1.x compatibility
+from __future__ import nested_scopes
+
+import sys
+import os
+import cgi
+import sha
+import urllib
+import signal
+
+from types import *
+from string import lowercase, digits
+
+from email.Utils import unquote, parseaddr, formataddr
+
+from Mailman import mm_cfg
+from Mailman import Utils
+from Mailman import MailList
+from Mailman import Errors
+from Mailman import MemberAdaptor
+from Mailman import i18n
+from Mailman.UserDesc import UserDesc
+from Mailman.htmlformat import *
+from Mailman.Logging.Syslog import syslog
+
+
+def main():
+    parts = Utils.GetPathPieces()
+    query = dict(cgi.parse_qsl(os.environ.get('QUERY_STRING')))
+    user = query.get('user')
+    if user:
+        user = urllib.unquote(user)
+    ac = query.get('ac')
+    scriptname = query.get('script')
+    if scriptname:
+        scriptname = urllib.unquote(scriptname)
+
+    if not scriptname or not ac:
+         syslog('error', 'openid.py accessed with improper query')
+         display_failure("Improper query to openid script.")
+         return
+    
+    # Get the list object
+    listname = parts[0].lower()
+    try:
+        mlist = MailList.MailList(listname, lock=0)
+    except Errors.MMListError, e:
+        # Avoid cross-site scripting attacks
+        safelistname = Utils.websafe(listname)
+        syslog('error', 'openid.py accessed for non-existent list: %s',
+               listname)
+
+        display_failure("Unknown List '%s'"%safelistname)
+        return
+
+    openid_url = mlist.OpenIDComplete(query)
+    if openid_url:
+        authcontexts = decodeAuthContexts(ac)
+        # verify that this openid is valid for this authcontext and user.
+        for ac in authcontexts:
+            if ac in [mm_cfg.AuthCreator, mm_cfg.AuthSiteAdmin]:
+                #disabled for now
+                ok = False
+            elif ac == mm_cfg.AuthListAdmin:
+                ok = (openid_url in mlist.owner_openid)
+            elif ac == mm_cfg.AuthListModerator:
+                ok = (openid_url in mlist.moderator_openid)
+            elif ac == mm_cfg.AuthUser:
+                if not user:
+                    ok = user = mlist.getMemberByOpenID(openid_url)
+                else:
+                    ok = (openid_url == mlist.getMemberOpenID(user))
+
+            if ok:
+                cookie = mlist.MakeCookie(ac, user)
+                url = mlist.GetScriptURL(scriptname)
+                # hack to the max for the options page, which is a pain
+                if scriptname == 'options' and user:
+                    url = url + "/%s"%user
+                Utils.redirect(url, cookie.output(), 
+                    "Authentication Successful.  You are being redirected.")
+                return
+
+        #Failed to authenticate in all contexts
+        display_failure("%s is not authorized to view this page."%openid_url)
+        return
+    else:
+        display_failure("OpenID Verification Failed.")
+
+def display_failure(message):
+    """Print an HTTP Response indicating failure with the given message."""
+    # TODO: make the page look more official mailman-like
+    print "Content-Type: text/html"
+    print "\n\n"
+    print """
+<html><head><title>
+Authentication Failure
+</title></head><body>
+<h1>Authentication Failed</h1>
+<p>%s</p>
+</body></html>
+"""%message
+
+def decodeAuthContexts(acchars):
+    """Given a string, return a corresponding sequence of Authentication
+    Contexts.
+    """
+
+    acs = [];
+
+    for c in acchars:
+        if c == 'U':
+            acs.append(mm_cfg.AuthUser)
+        elif c == 'C':
+            acs.append(mm_cfg.AuthCreator)
+        elif c == 'A':
+            acs.append(mm_cfg.AuthListAdmin)
+        elif c == 'M':
+            acs.append(mm_cfg.AuthListModerator)
+        elif c == 'S':
+            acs.append(mm_cfg.AuthSiteAdmin)
+
+    return acs
+
diff -urN mailman-2.1.7/Mailman/Cgi/options.py mailman-2.1.7-openid/Mailman/Cgi/options.py
--- mailman-2.1.7/Mailman/Cgi/options.py	2005-12-02 17:07:13.000000000 -0800
+++ mailman-2.1.7-openid/Mailman/Cgi/options.py	2006-01-23 12:26:24.000000000 -0800
@@ -95,6 +95,23 @@
     i18n.set_language(language)
     doc.set_language(language)
 
+    if cgidata.has_key('login'):
+        openid_url = cgidata.getvalue('openid_url')
+        user = cgidata.getvalue('email')
+        if not user:
+            user = Utils.LCDomain(Utils.UnobscureEmail(SLASH.join(parts[1:])))
+        if openid_url:
+            rd_url = mlist.OpenIDAuthenticate('options',
+                                            (mm_cfg.AuthUser,
+                                            mm_cfg.AuthListAdmin),
+                                            openid_url, 
+                                            user)
+            if rd_url:
+                Utils.redirect(rd_url)
+                return
+            else:
+                doc.addError(_('Invalid OpenID URL.'))          
+
     if lenparts < 2:
         user = cgidata.getvalue('email')
         if not user:
@@ -880,6 +897,8 @@
         ptable.AddRow([Hidden('email', user)])
     ptable.AddRow([Label(_('Password:')),
                    PasswordBox('password', size=20)])
+    ptable.AddRow([Label(_('OpenID:')),
+                   mlist.FormatOpenIDBox('openid_url', size=20)]) 
     ptable.AddRow([Center(SubmitButton('login', _('Log in')))])
     ptable.AddCellInfo(ptable.GetCurrentRowIndex(), 0, colspan=2)
     table.AddRow([Center(ptable)])
Binary files mailman-2.1.7/Mailman/Cgi/.options.py.swp and mailman-2.1.7-openid/Mailman/Cgi/.options.py.swp differ
diff -urN mailman-2.1.7/Mailman/Cgi/private.py mailman-2.1.7-openid/Mailman/Cgi/private.py
--- mailman-2.1.7/Mailman/Cgi/private.py	2005-12-30 10:50:07.000000000 -0800
+++ mailman-2.1.7-openid/Mailman/Cgi/private.py	2006-01-23 12:09:46.000000000 -0800
@@ -118,6 +118,7 @@
     cgidata = cgi.FieldStorage()
     username = cgidata.getvalue('username', '')
     password = cgidata.getvalue('password', '')
+    openid_url = cgidata.getvalue('openid_url', '')
 
     is_auth = 0
     realname = mlist.real_name
@@ -128,6 +129,14 @@
                                   mm_cfg.AuthListAdmin,
                                   mm_cfg.AuthSiteAdmin),
                                  password, username):
+        if openid_url:
+            rdurl = mlist.OpenIDAuthenticate((mm_cfg.AuthListAdmin,
+                                     mm_cfg.AuthSiteAdmin),
+                                    openid_url, username)
+            if rdurl:
+                Utils.redirect(rdurl)
+                return
+    
         if cgidata.has_key('submit'):
             # This is a re-authorization attempt
             message = Bold(FontSize('+1', _('Authorization failed.'))).Format()
diff -urN mailman-2.1.7/Mailman/Cgi/roster.py mailman-2.1.7-openid/Mailman/Cgi/roster.py
--- mailman-2.1.7/Mailman/Cgi/roster.py	2005-08-26 18:40:15.000000000 -0700
+++ mailman-2.1.7-openid/Mailman/Cgi/roster.py	2006-01-23 12:09:46.000000000 -0800
@@ -78,18 +78,39 @@
         # Members only
         addr = cgidata.getvalue('roster-email', '')
         password = cgidata.getvalue('roster-pw', '')
+        openid = cgidata.getvalue('roster-openid', '')
         ok = mlist.WebAuthenticate((mm_cfg.AuthUser,
                                     mm_cfg.AuthListModerator,
                                     mm_cfg.AuthListAdmin,
                                     mm_cfg.AuthSiteAdmin),
                                    password, addr)
+        if not ok and openid:
+            rdurl = mlist.OpenIDAuthenticate('roster',
+                                    (mm_cfg.AuthUser,
+                                    mm_cfg.AuthListModerator,
+                                    mm_cfg.AuthListAdmin),
+                                    openid)
+            if rdurl:
+                Utils.redirect(rdurl)
+                return
+
     else:
         # Admin only, so we can ignore the address field
         password = cgidata.getvalue('roster-pw', '')
+        openid = cgidata.getvalue('roster-openid', '')
         ok = mlist.WebAuthenticate((mm_cfg.AuthListModerator,
                                     mm_cfg.AuthListAdmin,
                                     mm_cfg.AuthSiteAdmin),
                                    password)
+        if not ok and openid:
+            rdurl = mlist.OpenIDAuthenticate('roster'
+                                    (mm_cfg.AuthListModerator,
+                                    mm_cfg.AuthListAdmin),
+                                    openid)
+            if rdurl:
+                Utils.redirect(rdurl)
+                return
+
     if not ok:
         realname = mlist.real_name
         doc = Document()
diff -urN mailman-2.1.7/Mailman/Cgi/subscribe.py mailman-2.1.7-openid/Mailman/Cgi/subscribe.py
--- mailman-2.1.7/Mailman/Cgi/subscribe.py	2005-08-26 18:40:15.000000000 -0700
+++ mailman-2.1.7-openid/Mailman/Cgi/subscribe.py	2006-01-23 12:09:46.000000000 -0800
@@ -124,6 +124,7 @@
     # If the user did not supply a password, generate one for him
     password = cgidata.getvalue('pw')
     confirmed = cgidata.getvalue('pw-conf')
+    openid = cgidata.getvalue('openid_url')
 
     if password is None and confirmed is None:
         password = Utils.MakeRandomPassword()
@@ -172,7 +173,7 @@
 email which contains further instructions.""")
 
     try:
-        userdesc = UserDesc(email, fullname, password, digest, lang)
+        userdesc = UserDesc(email, fullname, password, openid, digest, lang)
         mlist.AddMember(userdesc, remote)
         results = ''
     # Check for all the errors that mlist.AddMember can throw options on the
@@ -210,6 +211,8 @@
 Your subscription request was deferred because %(x)s.  Your request has been
 forwarded to the list moderator.  You will receive email informing you of the
 moderator's decision when they get to your request.""")
+    except Errors.OpenIDError:
+        results = _('The OpenID URL you supplied was invalid.')
     except Errors.MMAlreadyAMember:
         # Results string depends on whether we have private rosters or not
         if not privacy_results:
diff -urN mailman-2.1.7/Mailman/Defaults.py.in mailman-2.1.7-openid/Mailman/Defaults.py.in
--- mailman-2.1.7/Mailman/Defaults.py.in	2005-12-30 10:50:07.000000000 -0800
+++ mailman-2.1.7-openid/Mailman/Defaults.py.in	2006-01-23 12:09:46.000000000 -0800
@@ -1204,6 +1204,9 @@
 EmailListEx = 13
 # Extended spam filter widget
 HeaderFilter  = 14
+# The OpenID fields - canonicalize the URLs entered in these types
+OpenID = 15
+OpenIDList = 16
 
 # Actions
 DEFER = 0
@@ -1278,6 +1281,7 @@
 MESSAGES_DIR    = os.path.join(PREFIX, 'messages')
 PUBLIC_ARCHIVE_FILE_DIR  = os.path.join(VAR_PREFIX, 'archives', 'public')
 PRIVATE_ARCHIVE_FILE_DIR = os.path.join(VAR_PREFIX, 'archives', 'private')
+OID_STORE_DIR = os.path.join(DATA_DIR, 'oidstore')
 
 # Directories used by the qrunner subsystem
 QUEUE_DIR       = os.path.join(VAR_PREFIX, 'qfiles')
diff -urN mailman-2.1.7/Mailman/Errors.py mailman-2.1.7-openid/Mailman/Errors.py
--- mailman-2.1.7/Mailman/Errors.py	2005-08-26 18:40:15.000000000 -0700
+++ mailman-2.1.7-openid/Mailman/Errors.py	2006-01-23 12:09:46.000000000 -0800
@@ -77,6 +77,9 @@
     """Post already went through this list!"""
     pass
 
+class OpenIDError(MailmanError):
+    """Could not resolve an OpenID URL"""
+    pass
 
 # Exception hierarchy for bad email address errors that can be raised from
 # Utils.ValidateEmail()
diff -urN mailman-2.1.7/Mailman/Gui/General.py mailman-2.1.7-openid/Mailman/Gui/General.py
--- mailman-2.1.7/Mailman/Gui/General.py	2005-10-23 01:10:22.000000000 -0700
+++ mailman-2.1.7-openid/Mailman/Gui/General.py	2006-01-23 12:09:46.000000000 -0800
@@ -91,9 +91,41 @@
              administrators and moderators, you must
              <a href="passwords">set a separate moderator password</a>,
              and also provide the <a href="?VARHELP=general/moderator">email
-             addresses of the list moderators</a>.  Note that the field you
-             are changing here specifies the list administrators.''')),
+             addresses of the list moderators</a>. You may provide lists
+             of OpenIDs for the two roles, in which case the administrators
+             or moderators need not know the password.
+             Note that the field you are
+             changing here specifies the emails of the list 
+             administrators.''')),
+
+            ('owner_openid', mm_cfg.OpenIDList, (3, WIDTH), 0,
+             _("""The list administrator OpenID identity URLs.  Multiple
+             administrator identities, each on separate line is okay."""),
 
+             _('''There are two ownership roles associated with each mailing
+             list.  The <em>list administrators</em> are the people who have
+             ultimate control over all parameters of this mailing list.  They
+             are able to change any list configuration variable available
+             through these administration web pages.
+
+             <p>The <em>list moderators</em> have more limited permissions;
+             they are not able to change any list configuration variable, but
+             they are allowed to tend to pending administration requests,
+             including approving or rejecting held subscription requests, and
+             disposing of held postings.  Of course, the <em>list
+             administrators</em> can also tend to pending requests.
+
+             <p>In order to split the list ownership duties into
+             administrators and moderators, you must
+             <a href="passwords">set a separate moderator password</a>,
+             and also provide the <a href="?VARHELP=general/moderator">email
+             addresses of the list moderators</a>. You may provide lists
+             of OpenIDs for the two roles, in which case the administrators
+             or moderators need not know the password.
+             Note that the field you are
+             changing here specifies the OpenIDs of the list 
+             administrators.''')),
+             
             ('moderator', mm_cfg.EmailList, (3, WIDTH), 0,
              _("""The list moderator email addresses.  Multiple
              moderator addresses, each on separate line is okay."""),
@@ -112,12 +144,41 @@
              administrators</em> can also tend to pending requests.
 
              <p>In order to split the list ownership duties into
-             administrators and moderators, you must
+             administrators and moderators, you must 
              <a href="passwords">set a separate moderator password</a>,
-             and also provide the email addresses of the list moderators in
-             this section.  Note that the field you are changing here
-             specifies the list moderators.''')),
+             and also provide the <a href="?VARHELP=general/moderator">email
+             addresses of the list moderators</a>.  You may provide lists
+             of OpenIDs for the two roles, in which case the administrators
+             or moderators need not know the password.  Note that the field you are
+             changing here specifies the emails of the list moderators.''')),
+
+            ('moderator_openid', mm_cfg.OpenIDList, (3, WIDTH), 0,
+             _("""The list moderator OpenID identity URLs.  Multiple
+             moderator identities, each on separate line is okay."""),
+
+             _('''There are two ownership roles associated with each mailing
+             list.  The <em>list administrators</em> are the people who have
+             ultimate control over all parameters of this mailing list.  They
+             are able to change any list configuration variable available
+             through these administration web pages.
+
+             <p>The <em>list moderators</em> have more limited permissions;
+             they are not able to change any list configuration variable, but
+             they are allowed to tend to pending administration requests,
+             including approving or rejecting held subscription requests, and
+             disposing of held postings.  Of course, the <em>list
+             administrators</em> can also tend to pending requests.
 
+             <p>In order to split the list ownership duties into
+             administrators and moderators, you must
+             <a href="passwords">set a separate moderator password</a>,
+             and also provide the <a href="?VARHELP=general/moderator">email
+             addresses of the list moderators</a>. You may provide lists
+             of OpenIDs for the two roles, in which case the administrators
+             or moderators need not know the password.
+             Note that the field you are
+             changing here specifies the OpenIDs of the list moderators.''')),
+             
             ('description', mm_cfg.String, WIDTH, 0,
              _('A terse phrase identifying this list.'),
 
diff -urN mailman-2.1.7/Mailman/Gui/GUIBase.py mailman-2.1.7-openid/Mailman/Gui/GUIBase.py
--- mailman-2.1.7/Mailman/Gui/GUIBase.py	2005-08-26 18:40:15.000000000 -0700
+++ mailman-2.1.7-openid/Mailman/Gui/GUIBase.py	2006-01-23 12:09:46.000000000 -0800
@@ -81,6 +81,20 @@
                         raise
                 addrs.append(addr)
             return addrs
+        if wtype == mm_cfg.OpenID:
+            if val:
+                # makes sure it is an OpenID URL but doesn't authenticate
+                val = Utils.ValidateOpenID(val)
+            return val
+        if wtype == mm_cfg.OpenIDList:
+            id_urls = []
+            for idurl in [s.strip() for s in val.split(NL)]:
+                if not idurl:
+                    continue
+                # Raises an exception if it's not a good openid
+                idurl = Utils.ValidateOpenID(idurl)
+                id_urls.append(idurl)
+            return id_urls
         # This is a host name, i.e. verbatim
         if wtype == mm_cfg.Host:
             return val
@@ -158,6 +172,9 @@
             except Errors.EmailAddressError:
                 doc.addError(
                     _('Bad email address for option %(property)s: %(val)s'))
+            except Errors.OpenIDError:
+                doc.addError(
+                    _('Bad OpenID for option %(property)s: %(val)s'))
             else:
                 # Set the attribute, which will normally delegate to the mlist
                 self._setValue(mlist, property, val, doc)
diff -urN mailman-2.1.7/Mailman/htmlformat.py mailman-2.1.7-openid/Mailman/htmlformat.py
--- mailman-2.1.7/Mailman/htmlformat.py	2005-08-26 18:40:15.000000000 -0700
+++ mailman-2.1.7-openid/Mailman/htmlformat.py	2006-01-23 12:09:46.000000000 -0800
@@ -316,6 +316,18 @@
             if self.title:
                 output.append('%s<TITLE>%s</TITLE>' % (tab, self.title))
             output.append('%s</HEAD>' % tab)
+
+            # Provide for fancy OpenID entry boxes.  More inline CSS can go here
+            output.append('''<style type="text/css">
+            input.openid {
+               background: url(/icons/openid-bg.gif) no-repeat; 
+               background-color: #fff; 
+               background-position: 0 50%;
+               color: #000;
+               padding-left: 18px; 
+            }
+            </style>''')
+            
             quals = []
             # Default link colors
             if mm_cfg.WEB_VLINK_COLOR:
diff -urN mailman-2.1.7/Mailman/HTMLFormatter.py mailman-2.1.7-openid/Mailman/HTMLFormatter.py
--- mailman-2.1.7/Mailman/HTMLFormatter.py	2005-08-26 18:40:15.000000000 -0700
+++ mailman-2.1.7-openid/Mailman/HTMLFormatter.py	2006-01-23 12:09:46.000000000 -0800
@@ -304,7 +304,7 @@
                                            self.private_roster)
                               + _(" <p>Enter your ")
                               + whom[:-1].lower()
-                              + _(" and password to visit"
+                              + _(" and password, or your OpenID to visit"
                               "  the subscribers list: <p><center> ")
                               + whom
                               + " ")
@@ -312,6 +312,9 @@
             container.AddItem(_("Password: ")
                               + self.FormatSecureBox('roster-pw')
                               + "&nbsp;&nbsp;")
+            container.AddItem(_("OpenID: ")
+                              + self.FormatOpenIDBox('roster-openid')
+                              + "&nbsp;&nbsp;")
             container.AddItem(SubmitButton('SubscriberRoster',
                                            _('Visit Subscriber List')))
             container.AddItem("</center>")
@@ -341,6 +344,10 @@
     def FormatButton(self, name, text='Submit'):
         return '<INPUT type="Submit" name="%s" value="%s">' % (name, text)
 
+    def FormatOpenIDBox(self, name, size=20, value=''):
+        return '<INPUT class="openid" name="%s" size="%d" value="%s">' % (
+            name, size, value)
+
     def FormatReminder(self, lang):
         if self.send_reminders:
             return _('Once a month, your password will be emailed to you as'
Binary files mailman-2.1.7/Mailman/.HTMLFormatter.py.swp and mailman-2.1.7-openid/Mailman/.HTMLFormatter.py.swp differ
diff -urN mailman-2.1.7/Mailman/MailList.py mailman-2.1.7-openid/Mailman/MailList.py
--- mailman-2.1.7/Mailman/MailList.py	2005-12-30 10:50:07.000000000 -0800
+++ mailman-2.1.7-openid/Mailman/MailList.py	2006-01-23 12:09:46.000000000 -0800
@@ -308,6 +308,8 @@
         self.language = {}
         self.usernames = {}
         self.passwords = {}
+        self.openids = {} # openids keyed by email
+        self.oidlookup = {} # emails keyed by openid
         self.new_member_options = mm_cfg.DEFAULT_NEW_MEMBER_OPTIONS
 
         # This stuff is configurable
@@ -322,7 +324,9 @@
             mm_cfg.DEFAULT_URL or
             mm_cfg.DEFAULT_URL_PATTERN % mm_cfg.DEFAULT_URL_HOST)
         self.owner = [admin]
+        self.owner_openid = []
         self.moderator = []
+        self.moderator_openid = []
         self.reply_goes_to_list = mm_cfg.DEFAULT_REPLY_GOES_TO_LIST
         self.reply_to_address = ''
         self.first_strip_reply_to = mm_cfg.DEFAULT_FIRST_STRIP_REPLY_TO
@@ -814,6 +818,7 @@
             digest   -- a flag indicating whether the user wants digests or not
             language -- the requested default language for the user
             password -- the user's password
+            openid   -- the user's openid as entered
 
         Other attributes may be defined later.  Only address is required; the
         others all have defaults (fullname='', digests=0, language=list's
@@ -830,6 +835,7 @@
         lang = getattr(userdesc, 'language', self.preferred_language)
         digest = getattr(userdesc, 'digest', None)
         password = getattr(userdesc, 'password', Utils.MakeRandomPassword())
+        openid = getattr(userdesc, 'openid', None)
         if digest is None:
             if self.nondigestable:
                 digest = 0
@@ -855,11 +861,15 @@
         elif not digest and not self.nondigestable:
             raise Errors.MMMustDigestError
 
+        # Check the validity of the OpenID URL and canonicalize it
+        openid = Utils.ValidateOpenID(openid)
+
         userdesc.address = email
         userdesc.fullname = name
         userdesc.digest = digest
         userdesc.language = lang
         userdesc.password = password
+        userdesc.openid = openid
 
         # Apply the list's subscription policy.  0 means open subscriptions; 1
         # means the user must confirm; 2 means the admin must approve; 3 means
@@ -878,19 +888,42 @@
                 remote = _(' from %(remote)s')
 
             recipient = self.GetMemberAdminEmail(email)
-            confirmurl = '%s/%s' % (self.GetScriptURL('confirm', absolute=1),
-                                    cookie)
-            text = Utils.maketext(
-                'verify.txt',
-                {'email'       : email,
-                 'listaddr'    : self.GetListEmail(),
-                 'listname'    : realname,
-                 'cookie'      : cookie,
-                 'requestaddr' : self.getListAddress('request'),
-                 'remote'      : remote,
-                 'listadmin'   : self.GetOwnerEmail(),
-                 'confirmurl'  : confirmurl,
-                 }, lang=lang, mlist=self)
+            if openid:
+                # If the user subscribes with an openid, we send them a
+                # link to verify it which they are required to click on
+                # to be subscribed (no email confirmation)
+
+                cookie = self.pend_new(Pending.SUBSCRIPTION, 
+                                            userdesc)
+                confirmurl = self.GetScriptURL('oidconfirm', absolute=1)
+                confirmurl += '/' + cookie
+
+                text = Utils.maketext(
+                    'oidverify.txt',
+                    {'email'       : email,
+                     'listaddr'    : self.GetListEmail(),
+                     'listname'    : realname,
+                     'remote'      : remote,
+                     'listadmin'   : self.GetOwnerEmail(),
+                     'confirmurl'  : confirmurl,
+                     'openid'      : openid,
+                     }, lang=lang, mlist=self)
+
+            else:
+                confirmurl = '%s/%s' % (self.GetScriptURL('confirm',
+                                absolute=1), cookie)
+                text = Utils.maketext(
+                    'verify.txt',
+                    {'email'       : email,
+                     'listaddr'    : self.GetListEmail(),
+                     'listname'    : realname,
+                     'cookie'      : cookie,
+                     'requestaddr' : self.getListAddress('request'),
+                     'remote'      : remote,
+                     'listadmin'   : self.GetOwnerEmail(),
+                     'confirmurl'  : confirmurl,
+                     }, lang=lang, mlist=self)
+                     
             msg = Message.UserNotification(
                 recipient, self.GetRequestEmail(cookie),
                 text=text, lang=lang)
@@ -940,6 +973,7 @@
         lang = getattr(userdesc, 'language', self.preferred_language)
         digest = getattr(userdesc, 'digest', None)
         password = getattr(userdesc, 'password', Utils.MakeRandomPassword())
+        openid = getattr(userdesc, 'openid', None)
         if digest is None:
             if self.nondigestable:
                 digest = 0
@@ -956,11 +990,13 @@
             raise Errors.MembershipIsBanned, pattern
         # Do the actual addition
         self.addNewMember(email, realname=name, digest=digest,
-                          password=password, language=lang)
+                          password=password, language=lang, openid=openid)
         self.setMemberOption(email, mm_cfg.DisableMime,
                              1 - self.mime_is_default_digest)
         self.setMemberOption(email, mm_cfg.Moderate,
                              self.default_member_moderation)
+        if openid:
+            self.setMemberOption(email, mm_cfg.SuppressPasswordReminder, 1)
         # Now send and log results
         if digest:
             kind = ' (digest)'
@@ -969,7 +1005,12 @@
         syslog('subscribe', '%s: new%s %s, %s', self.internal_name(),
                kind, formataddr((email, name)), whence)
         if ack:
-            self.SendSubscribeAck(email, self.getMemberPassword(email),
+            if openid:
+                self.SendSubscribeAck(email, 
+                    "(password omitted) OpenID: %s"%openid,
+                                  digest, text)
+            else:
+                self.SendSubscribeAck(email, self.getMemberPassword(email),
                                   digest, text)
         if admin_notif:
             lang = self.preferred_language
diff -urN mailman-2.1.7/Mailman/MemberAdaptor.py mailman-2.1.7-openid/Mailman/MemberAdaptor.py
--- mailman-2.1.7/Mailman/MemberAdaptor.py	2005-08-26 18:40:15.000000000 -0700
+++ mailman-2.1.7-openid/Mailman/MemberAdaptor.py	2006-01-23 12:09:46.000000000 -0800
@@ -123,6 +123,21 @@
         """
         raise NotImplementedError
 
+    def getMemberOpenID(self, member):
+        """Return the member's OpenID.
+
+        If the member KEY/LCE is not a member of the list, raise
+        NotAMemberError.
+        """
+        raise NotImplementedError
+        
+    def getMemberByOpenID(self, openid):
+        """Look up a user's email with their OpenID.
+
+        Return None if the OpenID is not associated with any member.
+        """
+        raise NotImplementedError
+
     def getMemberLanguage(self, member):
         """Return the preferred language for the member KEY/LCE.
 
diff -urN mailman-2.1.7/Mailman/OldStyleMemberships.py mailman-2.1.7-openid/Mailman/OldStyleMemberships.py
--- mailman-2.1.7/Mailman/OldStyleMemberships.py	2005-08-26 18:40:15.000000000 -0700
+++ mailman-2.1.7-openid/Mailman/OldStyleMemberships.py	2006-01-23 12:09:46.000000000 -0800
@@ -102,6 +102,15 @@
             raise Errors.NotAMemberError, member
         return secret
 
+    def getMemberOpenID(self, member):
+        id = self.__mlist.openids.get(member.lower())
+        if id is None:
+            raise Errors.NotAMemberError, member
+        return id
+
+    def getMemberByOpenID(self, openid):
+        return self.__mlist.oidlookup.get(openid)
+    
     def authenticateMember(self, member, response):
         secret = self.getMemberPassword(member)
         if secret == response:
@@ -178,12 +187,16 @@
         password = Utils.MakeRandomPassword()
         language = self.__mlist.preferred_language
         realname = None
+        openid = None
         if kws.has_key('digest'):
             digest = kws['digest']
             del kws['digest']
         if kws.has_key('password'):
             password = kws['password']
             del kws['password']
+        if kws.has_key('openid'):
+            openid = kws['openid']
+            del kws['openid']
         if kws.has_key('language'):
             language = kws['language']
             del kws['language']
@@ -208,6 +221,9 @@
             self.__mlist.members[member] = value
         self.setMemberPassword(member, password)
 
+        if openid:
+            self.setMemberOpenID(member, openid)
+
         self.setMemberLanguage(member, language)
         if realname:
             self.setMemberName(member, realname)
@@ -258,6 +274,16 @@
         self.__assertIsMember(memberkey)
         self.__mlist.passwords[memberkey.lower()] = password
 
+    def setMemberOpenID(self, memberkey, openid):
+        # The openid must be verified by the time this is called.
+        assert self.__mlist.Locked()
+        self.__assertIsMember(memberkey)
+        old_openid = self.__mlist.openids.get(memberkey.lower())
+        if old_openid:
+            del self.__mlist.oidlookup[old_openid]
+        self.__mlist.openids[memberkey.lower()] = openid
+        self.__mlist.oidlookup[openid] = memberkey.lower()
+
     def setMemberLanguage(self, memberkey, language):
         assert self.__mlist.Locked()
         self.__assertIsMember(memberkey)
diff -urN mailman-2.1.7/Mailman/SecurityManager.py mailman-2.1.7-openid/Mailman/SecurityManager.py
--- mailman-2.1.7/Mailman/SecurityManager.py	2005-08-26 18:40:15.000000000 -0700
+++ mailman-2.1.7-openid/Mailman/SecurityManager.py	2006-01-23 12:09:46.000000000 -0800
@@ -68,6 +68,9 @@
 from Mailman import Errors
 from Mailman.Logging.Syslog import syslog
 
+from openid.consumer import consumer
+from openid.store.filestore import FileOpenIDStore
+
 try:
     True, False
 except NameError:
@@ -85,6 +88,7 @@
         self.mod_password = None
         # Non configurable
         self.passwords = {}
+        self.openids = {}
 
     def AuthContextInfo(self, authcontext, user=None):
         # authcontext may be one of AuthUser, AuthListModerator,
@@ -210,6 +214,75 @@
                 raise ValueError, 'Bad authcontext: %s' % ac
         return mm_cfg.UnAuthorized
 
+
+    def OpenIDAuthenticate(self, script, authcontexts, oid_url, user=None):
+        # Start the OpenID Authentication dance.
+        # returns a URL for a redirect or None
+        oidstore = FileOpenIDStore(mm_cfg.OID_STORE_DIR)
+        oidconsumer = consumer.OpenIDConsumer(oidstore)
+        # Fetch the URL and get the OpenID info out of the page, and
+        # associate with the openid server if we haven't already.
+        status, info = oidconsumer.beginAuth(oid_url)
+        if status == consumer.SUCCESS:
+            # Construct the query for the return to URL.
+            querystring = "?ac="
+            for ac in authcontexts:
+                if ac == mm_cfg.AuthUser:
+                    querystring += 'U'
+                elif ac == mm_cfg.AuthListAdmin:
+                    querystring += 'A'
+                elif ac == mm_cfg.AuthListModerator:
+                    querystring += 'M'
+                elif ac in [mm_cfg.AuthSiteAdmin, mm_cfg.AuthCreator]:
+                    #disabled for now
+                    pass
+            querystring += "&script=%s"%urllib.quote(script,'')
+            querystring += "&token=%s"%urllib.quote(info.token,'')
+            if user:
+                querystring += "&user=%s"%urllib.quote(user,'')
+                
+            return_to = self.GetScriptURL('openid', absolute=True) + \
+                querystring                 
+                
+            trust_root = mm_cfg.DEFAULT_URL_PATTERN % Utils.get_domain()
+            # Construct an openid query URL to send the user to the server
+            redirect_url = oidconsumer.constructRedirect(
+                                info, return_to, trust_root)
+            return redirect_url
+        else: # beginAuth failed.
+           return None
+           
+    def OpenIDSubscribe(self, oid_url, cookie):
+        # The OpenID dance for subscription is different
+        oidstore = FileOpenIDStore(mm_cfg.OID_STORE_DIR)
+        oidconsumer = consumer.OpenIDConsumer(oidstore)
+        status, info = oidconsumer.beginAuth(oid_url)
+        if status == consumer.SUCCESS:
+            return_to = self.GetScriptURL('oidconfirm', absolute=True) + \
+                '/' + cookie + "?token=%s"%urllib.quote(info.token,'')
+                
+            trust_root = mm_cfg.DEFAULT_URL_PATTERN % Utils.get_domain()
+            redirect_url = oidconsumer.constructRedirect(
+                                info, return_to, trust_root)
+            return redirect_url
+        else: # beginAuth failed.
+           return None
+
+    def OpenIDComplete(self, query):
+        # Do the final steps of OpenID verification
+        # On success return the OpenID URL, on failure None
+        # These objects are lightweight - easier to construct them than to
+        # keep them around
+        oidstore = FileOpenIDStore(mm_cfg.OID_STORE_DIR)
+        oidconsumer = consumer.OpenIDConsumer(oidstore)
+        token = urllib.unquote(query.get('token'))
+        # verify the identity assertion
+        status, openid_url = oidconsumer.completeAuth(token, query)
+        if status == consumer.SUCCESS:
+            return openid_url
+        else:
+            return None
+
     def WebAuthenticate(self, authcontexts, response, user=None):
         # Given a list of authentication contexts, check to see if the cookie
         # contains a matching authorization, falling back to checking whether
diff -urN mailman-2.1.7/Mailman/UserDesc.py mailman-2.1.7-openid/Mailman/UserDesc.py
--- mailman-2.1.7/Mailman/UserDesc.py	2005-08-26 18:40:15.000000000 -0700
+++ mailman-2.1.7-openid/Mailman/UserDesc.py	2006-01-23 12:09:46.000000000 -0800
@@ -23,13 +23,15 @@
 
 class UserDesc:
     def __init__(self, address=None, fullname=None, password=None,
-                 digest=None, lang=None):
+                 openid=None, digest=None, lang=None):
         if address is not None:
             self.address = address
         if fullname is not None:
             self.fullname = fullname
         if password is not None:
             self.password = password
+        if openid is not None:
+            self.openid = openid
         if digest is not None:
             self.digest = digest
         if lang is not None:
@@ -52,6 +54,7 @@
         address = getattr(self, 'address', 'n/a')
         fullname = getattr(self, 'fullname', 'n/a')
         password = getattr(self, 'password', 'n/a')
+        openid = getattr(self, 'openid', 'n/a')
         digest = getattr(self, 'digest', 'n/a')
         if digest == 0:
             digest = 'no'
@@ -63,5 +66,5 @@
             fullname = fullname.encode('ascii', 'replace')
         if isinstance(password, UnicodeType):
             password = password.encode('ascii', 'replace')
-        return '<UserDesc %s (%s) [%s] [digest? %s] [%s]>' % (
-            address, fullname, password, digest, language)
+        return '<UserDesc %s (%s) [%s] [%s] [digest? %s] [%s]>' % (
+            address, fullname, password, openid, digest, language)
diff -urN mailman-2.1.7/Mailman/Utils.py mailman-2.1.7-openid/Mailman/Utils.py
--- mailman-2.1.7/Mailman/Utils.py	2005-08-26 18:40:15.000000000 -0700
+++ mailman-2.1.7-openid/Mailman/Utils.py	2006-01-23 12:09:46.000000000 -0800
@@ -53,6 +53,9 @@
 from Mailman import Site
 from Mailman.SafeDict import SafeDict
 
+from openid.consumer import consumer
+from openid.store.filestore import FileOpenIDStore
+
 try:
     True, False
 except NameError:
@@ -216,6 +219,32 @@
     if len(domain_parts) < 2:
         raise Errors.MMBadEmailError, s
 
+
+def ValidateOpenID(s):
+    store = FileOpenIDStore(mm_cfg.OID_STORE_DIR)
+    oidconsumer = consumer.OpenIDConsumer(store)
+
+    status, info = oidconsumer._findIdentityInfo(s)
+
+    if status != consumer.SUCCESS:
+        raise Errors.OpenIDError
+    # info[0] is the canonicalized claimed identity
+    # the "consumer id"
+    openid = info[0]
+    return openid
+
+
+def redirect(url, header_str=None, message=None):
+    # This probably isn't quite the right place for this, but...
+    #print "HTTP/1.1 302 Redirect"
+    if header_str is not None:
+        print header_str
+    print "Location: %s"% url
+    print "Content-Type: text/plain"
+    if message is None:
+        message = "You are being redirected."
+    print "\n\n%s"%message
+    return 
 
 
 def GetPathPieces(envar='PATH_INFO'):
diff -urN mailman-2.1.7/src/Makefile.in mailman-2.1.7-openid/src/Makefile.in
--- mailman-2.1.7/src/Makefile.in	2005-08-26 18:40:17.000000000 -0700
+++ mailman-2.1.7-openid/src/Makefile.in	2006-01-23 12:09:46.000000000 -0800
@@ -71,7 +71,7 @@
 # Fixed definitions
 
 CGI_PROGS= admindb admin confirm create edithtml listinfo options \
-	private rmlist roster subscribe
+	openid oidconfirm private rmlist roster subscribe
 
 COMMONOBJS= common.o vsnprintf.o
 
diff -urN mailman-2.1.7/templates/en/admlogin.html mailman-2.1.7-openid/templates/en/admlogin.html
--- mailman-2.1.7/templates/en/admlogin.html	2001-05-31 12:25:10.000000000 -0700
+++ mailman-2.1.7-openid/templates/en/admlogin.html	2006-01-23 12:09:46.000000000 -0800
@@ -2,6 +2,15 @@
 <head>
   <title>%(listname)s %(who)s Authentication</title>
 </head>
+<STYLE type="text/css">
+          input.openid {
+             background: url(/icons/openid-bg.gif) no-repeat; 
+             background-color: #fff; 
+             background-position: 0 50%%;
+             color: #000;
+             padding-left: 18px; 
+          }
+</STYLE>
 <body bgcolor="#ffffff">
 <FORM METHOD=POST ACTION="%(path)s">
 %(message)s
@@ -17,6 +26,11 @@
       <TD><INPUT TYPE="password" NAME="adminpw" SIZE="30"></TD>
     </tr>
     <tr>
+      <TD><div ALIGN="Right">Or List %(who)s OpenID:</div></TD>
+      <TD><INPUT CLASS="openid" NAME="openid_url" SIZE="30"></TD>
+    </tr>
+
+    <tr>
       <td colspan=2 align=middle><INPUT type="SUBMIT"
                                         name="admlogin"
 					value="Let me in...">
diff -urN mailman-2.1.7/templates/en/listinfo.html mailman-2.1.7-openid/templates/en/listinfo.html
--- mailman-2.1.7/templates/en/listinfo.html	2002-11-15 22:10:36.000000000 -0800
+++ mailman-2.1.7-openid/templates/en/listinfo.html	2006-01-23 12:09:46.000000000 -0800
@@ -4,6 +4,15 @@
     <TITLE><MM-List-Name> Info Page</TITLE>
   
   </HEAD>
+  <STYLE type="text/css">
+            input.openid {
+               background: url(/icons/openid-bg.gif) no-repeat; 
+               background-color: #fff; 
+               background-position: 0 50%%;
+               color: #000;
+               padding-left: 18px; 
+            }
+  </STYLE>
   <BODY BGCOLOR="#ffffff">
 
     <P>
@@ -77,18 +86,22 @@
         <td width="33%"><mm-fullname-box></td>
 	<TD WIDTH="12%">&nbsp;</TD></TR>
       <TR>
-	<TD COLSPAN="3"><FONT SIZE=-1>You may enter a
+        <TD COLSPAN="3"><FONT SIZE=-1>If you enter your OpenID
+        Identity here, you can use it to log in to change your
+        subscription options, and to view private rosters and archives.
+        No password will be sent to your email automatically.
+      <TR>
+	<TD BGCOLOR="#dddddd">Enter your OpenID:</TD>
+	<TD><MM-New-OpenID-Box></TD>
+	<TD>&nbsp;</TD></TR>
+      <TR>
+	<TD COLSPAN="3"><FONT SIZE=-1>Alternatively, enter a
 	    privacy password below. This provides only mild security,
 	    but should prevent others from messing with your
 	    subscription.  <b>Do not use a valuable password</b> as
-	    it will occasionally be emailed back to you in cleartext.
-
-            <p>If you choose not to enter a password, one will be
-            automatically generated for you, and it will be sent to
-            you once you've confirmed your subscription.  You can
-            always request a mail-back of your password when you edit
-            your personal options.
-	    <MM-Reminder>
+	    it will periodically be emailed back to you in cleartext.
+            If you enter neither an OpenID nor a password, a password
+            will be automatically generated and emailed to you.
 	</TD>
       </TR>  
       <TR>
diff -urN mailman-2.1.7/templates/en/oidverify.txt mailman-2.1.7-openid/templates/en/oidverify.txt
--- mailman-2.1.7/templates/en/oidverify.txt	1969-12-31 16:00:00.000000000 -0800
+++ mailman-2.1.7-openid/templates/en/oidverify.txt	2006-01-23 12:09:46.000000000 -0800
@@ -0,0 +1,19 @@
+Mailing list subscription confirmation notice for mailing list %(listname)s
+
+We have received a request%(remote)s for subscription of your email
+address, "%(email)s", to the %(listaddr)s mailing list.
+
+The request indicated that you wished to be able to log in to
+the list site with your openid, %(openid)s . To confirm that this
+is your OpenID, and that you want to be added to this mailing list,
+simply visit this web page:
+
+%(confirmurl)s
+
+If you made an error entering your OpenID, please initiate another
+request for subscription.
+
+If you do not wish to be subscribed to this list, please simply
+disregard this message.  If you think you are being maliciously
+subscribed to the list, or have any other questions, send them to
+%(listadmin)s.
diff -urN mailman-2.1.7/templates/en/options.html mailman-2.1.7-openid/templates/en/options.html
--- mailman-2.1.7/templates/en/options.html	2004-11-13 15:57:05.000000000 -0800
+++ mailman-2.1.7-openid/templates/en/options.html	2006-01-23 12:13:52.000000000 -0800
@@ -4,6 +4,16 @@
     <title><MM-Presentable-User> membership configuration for <MM-List-Name>
     </title>
 </head>
+<STYLE type="text/css">
+    input.openid {
+        background: url(/icons/openid-bg.gif) no-repeat;
+        background-color: #fff;
+        background-position: 0 50%%;
+        color: #000;
+        padding-left: 18px;
+    }                                                                           }
+</STYLE>
+
 <BODY BGCOLOR="#ffffff">
     <TABLE WIDTH="100%" BORDER="0" CELLSPACING="0" CELLPADDING="5">
     <TR><TD WIDTH="100%" BGCOLOR="#99CCFF"><B>
