summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorbwarsaw1999-11-10 20:18:29 +0000
committerbwarsaw1999-11-10 20:18:29 +0000
commit5b64f16172ca794a17aa5c81b2ac453a39481bfe (patch)
treec09d0f8efa19fc65419ad015b7a63798b794ac56
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).
-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)