summaryrefslogtreecommitdiff
path: root/Mailman/ListAdmin.py
diff options
context:
space:
mode:
authorbwarsaw1999-11-10 20:18:29 +0000
committerbwarsaw1999-11-10 20:18:29 +0000
commit5b64f16172ca794a17aa5c81b2ac453a39481bfe (patch)
treec09d0f8efa19fc65419ad015b7a63798b794ac56 /Mailman/ListAdmin.py
parent20f94db70e6b1520010c787e5a0bc2e1affbeaa9 (diff)
downloadmailman-5b64f16172ca794a17aa5c81b2ac453a39481bfe.tar.gz
mailman-5b64f16172ca794a17aa5c81b2ac453a39481bfe.tar.zst
mailman-5b64f16172ca794a17aa5c81b2ac453a39481bfe.zip
Massively restructured, the key improvement is that request data are
no longer kept with the MailList object, but instead in a separate lists/<listname>/request.db file. This means that the potentially huge amounts of data assosicated with held messages need not be loaded in unless administrative requests are actually being changed (added to, handled).
Diffstat (limited to 'Mailman/ListAdmin.py')
-rw-r--r--Mailman/ListAdmin.py363
1 files changed, 208 insertions, 155 deletions
diff --git a/Mailman/ListAdmin.py b/Mailman/ListAdmin.py
index 6db0d405b..65d260e8e 100644
--- a/Mailman/ListAdmin.py
+++ b/Mailman/ListAdmin.py
@@ -14,140 +14,169 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+"""Mixin class for MailList which handles administrative requests.
-"""Mixin class which handles of administrative requests."""
+Two types of admin requests are currently supported: adding members to a
+closed or semi-closed list, and moderated posts.
+Pending subscriptions which are requiring a user's confirmation are handled
+elsewhere (currently).
-# When an operation can't be completed, and is sent to the list admin for
-# Handling, we consider that an error condition, and raise MMNeedApproval
+"""
import os
-import time
import string
-import Errors
-import Message
-import Utils
+import time
+import anydbm
+import marshal
+
+from Mailman.pythonlib.StringIO import StringIO
+from Mailman import Message
+from Mailman import mm_cfg
+from Mailman import Utils
+from Mailman import Errors
+
class ListAdmin:
def InitVars(self):
- # Non-configurable data:
- self.requests = {}
- self.next_request_id = 1
+ # non-configurable data
+ self.requests = {}
+ self.next_request_id = 1
- def AddRequest(self, request, *args):
- now = time.time()
- request_id = self.GetRequestId()
- if not self.requests.has_key(request):
- self.requests[request] = [(request_id, now) + args]
- else:
- self.requests[request].append( (request_id, now) + args )
- self.Save()
- if request == 'add_member':
- who = args[1]
- self.LogMsg("vette", ("%s: Subscribe request: %s"
- % (self.real_name, who)))
- if self.admin_immed_notify:
- subj = 'New %s subscription request: %s' % (self.real_name,
- who)
- text = Utils.maketext(
- 'subauth.txt',
- {'username' : who,
- 'listname' : self.real_name,
- 'hostname' : self.host_name,
- 'admindb_url': self.GetAbsoluteScriptURL('admindb'),
- })
- self.SendTextToUser(subject = subj,
- recipient = self.GetAdminEmail(),
- text = text)
- raise Errors.MMNeedApproval, "Admin approval required to subscribe"
+ def InitTempVars(self):
+ self.__db = None
+ self.__dbflags = None
- elif request == 'post':
- sender = args[0][0]
- reason = args[1]
- subject = args[2]
- self.LogMsg("vette", ("%s: %s post hold\n\t%s"
- % (self.real_name, sender, `reason`)))
- if self.admin_immed_notify:
- subj = '%s post approval required for %s' % (self.real_name,
- sender)
- text = Utils.maketext(
- 'postauth.txt',
- {'listname' : self.real_name,
- 'hostname' : self.host_name,
- 'reason' : reason,
- 'sender' : sender,
- 'subject' : subject,
- 'admindb_url': self.GetAbsoluteScriptURL('admindb'),
- })
- self.SendTextToUser(subject = subj,
- recipient = self.GetAdminEmail(),
- text = text)
- raise Errors.MMNeedApproval, args[1]
+ def __opendb(self, flags='r'):
+ if self.__db is not None:
+ # if we want the database writeable, but it currently is not, then
+ # close it and re-open it with the given flags. we don't need to
+ # re-open it if it's already open writeable and we just want to
+ # read it.
+ if 'w' in self.__dbflags or 'w' not in flags:
+ return
+ self.__closedb()
+ dbfilename = os.path.join(self.fullpath(), 'request.db')
+ # For all writes we need to make sure the database is locked. We
+ # assert this by using the list's lock as the database lock.
+ if 'w' in flags:
+ assert self.Locked()
+ try:
+ omask = os.umask(002)
+ try:
+ self.__db = anydbm.open(dbfilename, flags)
+ finally:
+ os.umask(omask)
+ except anydbm.error:
+ # perhaps the db file exists but is zero sized?
+ from stat import ST_SIZE
+ if os.stat(dbfilename)[ST_SIZE] == 0:
+ os.unlink(dbfilename)
+ omask = os.umask(002)
+ try:
+ self.__db = anydbm.open(dbfilename, flags)
+ finally:
+ os.umask(omask)
+ else:
+ raise
+ self.__dbflags = flags
- def CleanRequests(self):
- for (key, val) in self.requests.items():
- if not len(val):
- del self.requests[key]
+ def __closedb(self):
+ if self.__db is not None:
+ self.__db.close()
+ self.__db = None
- def GetRequest(self, id):
- for (key, val) in self.requests.items():
- for i in range(len(val)):
- if val[i][0] == id:
- return (key, i)
- raise Errors.MMBadRequestId
+ def __request_id(self):
+ id = self.next_request_id
+ self.next_request_id = self.next_request_id + 1
+ return id
- def RemoveRequest(self, id):
- for (key, val) in self.requests.items():
- for item in val:
- if item[0] == id:
- val.remove(item)
- return
- raise Errors.MMBadRequestId
+ def SaveRequestsDb(self):
+ if self.__db and 'w' in self.__dbflags:
+ self.__closedb()
def NumRequestsPending(self):
- self.CleanRequests()
- total = 0
- for (k,v) in self.requests.items():
- total = total + len(v)
- return total
+ return len(self.requests)
- def HandleRequest(self, request_info, value, comment=None):
- request = request_info[0]
- index = request_info[1]
- request_data = self.requests[request][index]
- if request == 'add_member':
- self.HandleAddMemberRequest(request_data[2:], value, comment)
- elif request == 'post':
- self.HandlePostRequest(request_data[2:], value, comment)
- self.RemoveRequest(request_data[0])
+ def __getmsgids(self, rtype):
+ ids = []
+ for k, v in self.requests.items():
+ if v == rtype:
+ ids.append(k)
+ return ids
- def HandlePostRequest(self, data, value, comment):
- destination_email = data[0][0]
- msg = Message.IncomingMessage(data[0][1])
- rejection = None
- if not value:
- # Accept.
- self.Post(msg, 1)
- return
- elif value == 1:
- # Reject.
- rejection = "Refused"
- subj = msg.getheader('subject')
- if subj == None:
- request = 'Posting of your untitled message'
- else:
- request = ('Posting of your message entitled:\n\t\t %s'
- % subj)
- if not comment:
- comment = data[1]
- if not self.dont_respond_to_post_requests:
- self.RefuseRequest(request, destination_email,
- comment, msg)
- else:
- # Discard.
- rejection = "Discarded"
+ def GetHeldMessageIds(self):
+ return self.__getmsgids(mm_cfg.HELDMSG)
+
+ def GetSubscriptionIds(self):
+ return self.__getmsgids(mm_cfg.SUBSCRIPTION)
+
+ def GetRecord(self, id):
+ self.__opendb()
+ data = self.__db[`id`]
+ return marshal.loads(data)
+
+ def GetRecordType(self, id):
+ return self.requests[id]
+ def HandleRequest(self, id, value, comment):
+ self.__opendb('w')
+ record = self.GetRecord(id)
+ rtype = self.GetRecordType(id)
+ del self.__db[`id`]
+ del self.requests[id]
+ if rtype == mm_cfg.HELDMSG:
+ self.__handlepost(record, value, comment)
+ else:
+ assert rtype == mm_cfg.SUBSCRIPTION
+ self.__handlesubscription(record, value, comment)
+
+ def HoldMessage(self, msg, reason):
+ # assure that the database is open for writing
+ self.__opendb('cw')
+ # get the next unique id
+ id = self.__request_id()
+ dbid = `id`
+ assert not self.__db.has_key(dbid)
+ # flatten the message and suck out the sender address
+ sender, text = Utils.SnarfMessage(msg)
+ # save the information to the request database. for held message
+ # entries, each record in the database will be of the following
+ # format:
+ #
+ # the time the message was received
+ # the sender of the message
+ # the message's subject
+ # a string description of the problem
+ # the full text of the message
+ #
+ msgsubject = msg.get('subject', '(no subject)')
+ data = marshal.dumps((time.time(), sender, msgsubject, reason, text))
+ self.__db[dbid] = data
+ #
+ # remember the request type for later
+ self.requests[id] = mm_cfg.HELDMSG
+
+ def __handlepost(self, record, value, comment):
+ ptime, sender, subject, reason, text = record
+ rejection = None
+ if value == 0:
+ # Approved
+ msg = Message.Message(StringIO(text))
+ msg.approved = 1
+ self.Post(msg)
+ elif value == 1:
+ # Rejected
+ rejection = 'Refused'
+ if not self.dont_respond_to_post_requests:
+ self.__refuse('Posting of your message titled "%s"' % subject,
+ sender, comment or '[No reason given]')
+ else:
+ assert value == 2
+ # Discarded
+ rejection = 'Discarded'
+ # Log the rejection
def strquote(s):
return string.replace(s, '%', '%%')
@@ -155,59 +184,83 @@ class ListAdmin:
note = '''%(listname)s: %(rejection)s posting:
\tFrom: %(sender)s
\tSubject: %(subject)s''' % {
- 'listname' : self._internal_name,
+ 'listname' : self.internal_name(),
'rejection': rejection,
- 'sender' : msg.GetSender(),
- 'subject' : strquote(msg.getheader('subject', '<none>')),
+ 'sender' : sender,
+ 'subject' : strquote(subject),
}
- if data[1]:
- note = note + '\n\tHeld: ' + strquote(data[1])
- if comment:
- note = note + '\n\tDiscarded: ' + strquote(comment)
- self.LogMsg("vette", note)
-
- def HandleAddMemberRequest(self, data, value, comment):
- digest = data[0]
- destination_email = data[1]
- pw = data[2]
- if value == 0:
- if digest:
- digest_text = 'digest'
- else:
- digest_text = 'nodigest'
- self.RefuseRequest('subscribe %s %s' % (pw, digest_text),
- destination_email, comment)
- else:
- try:
- self.ApprovedAddMember(destination_email, pw, digest)
- self.Save()
- except Errors.MMAlreadyAMember:
- pass
+ if comment:
+ note = note + '\n\tReason: ' + strquote(comment)
+ self.LogMsg('vette', note)
+ def HoldSubscription(self, addr, password, digest):
+ # assure that the database is open for writing
+ self.__opendb('cw')
+ # get the next unique id
+ id = self.__request_id()
+ dbid = `id`
+ assert not self.__db.has_key(dbid)
+ #
+ # save the information to the request database. for held subscription
+ # entries, each record in the database will be one of the following
+ # format:
+ #
+ # the time the subscription request was received
+ # the subscriber's address
+ # the subscriber's selected password (TBD: is this safe???)
+ # the digest flag
+ #
+ data = marshal.dumps((time.time(), addr, password, digest))
+ self.__db[dbid] = data
+ #
+ # remember the request type for later
+ self.requests[id] = mm_cfg.SUBSCRIPTION
+ #
+ # TBD: this really shouldn't go here but I'm not sure where else is
+ # appropriate.
+ self.LogMsg('vette', '%s: held subscription request from %s' %
+ (self.real_name, addr))
+ # possibly notify the administrator
+ if self.admin_immed_notify:
+ subject = 'New subscription request to list %s from %s' % (
+ self.real_name, addr)
+ text = Utils.maketext(
+ 'subauth.txt',
+ {'username' : addr,
+ 'listname' : self.real_name,
+ 'hostname' : self.host_name,
+ 'admindb_url': self.GetAbsoluteScriptURL('admindb'),
+ })
+ self.SendTextToUser(subject=subject,
+ recipient=self.GetAdminEmail(),
+ text=text)
-# Don't call any methods below this point from outside this mixin.
+ def __handlesubscription(self, record, value, comment):
+ stime, addr, password, digest = record
+ if value == 0:
+ # refused
+ self.__refuse('Subscription request', addr, comment)
+ else:
+ # subscribe
+ assert value == 1
+ self.ApprovedAddMember(addr, password, digest)
- def GetRequestId(self):
- id = self.next_request_id
- self.next_request_id = self.next_request_id + 1
- # No need to save, we know it's about to be done.
- return id
- def RefuseRequest(self, request, destination_email, comment, msg=None):
+ def __refuse(self, request, recip, comment, msg=None):
text = Utils.maketext(
'refuse.txt',
{'listname' : self.real_name,
'request' : request,
- 'reason' : comment or '[No reason given]',
+ 'reason' : comment,
'adminaddr': self.GetAdminEmail(),
})
# add in original message, but not wrap/filled
if msg:
- text = text + string.join(msg.headers, '') + '\n\n' + msg.body
- else:
- text = text + '[Original message unavailable]'
- # send it
- self.SendTextToUser(subject = '%s request rejected' % self.real_name,
- recipient = destination_email,
- text = text)
+ text = text + \
+ '\n---------- Original Message ----------\n' + \
+ Utils.SnarfMessage(msg)[1]
+ subject = 'Request to mailing list %s rejected' % self.real_name
+ self.SendTextToUser(subject=subject,
+ recipient=recip,
+ text=text)