diff options
Diffstat (limited to '')
-rw-r--r-- | Mailman/Pending.py | 204 |
1 files changed, 204 insertions, 0 deletions
diff --git a/Mailman/Pending.py b/Mailman/Pending.py new file mode 100644 index 00000000..be1c6cac --- /dev/null +++ b/Mailman/Pending.py @@ -0,0 +1,204 @@ +# Copyright (C) 1998,1999,2000,2001,2002 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +""" Track pending confirmation of subscriptions. + +new(stuff...) places an item's data in the db, returning its cookie. + +confirmed(cookie) returns a tuple for the data, removing the item +from the db. It returns None if the cookie is not registered. +""" + +import os +import time +import sha +import marshal +import cPickle +import random +import errno + +from Mailman import mm_cfg +from Mailman import LockFile + +DBFILE = os.path.join(mm_cfg.DATA_DIR, 'pending.db') +PCKFILE = os.path.join(mm_cfg.DATA_DIR, 'pending.pck') +LOCKFILE = os.path.join(mm_cfg.LOCK_DIR, 'pending.lock') + +# Types of pending records +SUBSCRIPTION = 'S' +UNSUBSCRIPTION = 'U' +CHANGE_OF_ADDRESS = 'C' +HELD_MESSAGE = 'H' +RE_ENABLE = 'E' + +_ALLKEYS = [(x,) for x in (SUBSCRIPTION, UNSUBSCRIPTION, + CHANGE_OF_ADDRESS, HELD_MESSAGE, + RE_ENABLE, + )] + + + +def new(*content): + """Create a new entry in the pending database, returning cookie for it.""" + # It's a programming error if this assertion fails! We do it this way so + # the assert test won't fail if the sequence is empty. + assert content[:1] in _ALLKEYS + # Acquire the pending database lock, letting TimeOutError percolate up. + lock = LockFile.LockFile(LOCKFILE) + lock.lock(timeout=30) + try: + # Load the current database + db = _load() + # Calculate a unique cookie + while 1: + n = random.random() + now = time.time() + hashfood = str(now) + str(n) + str(content) + cookie = sha.new(hashfood).hexdigest() + if not db.has_key(cookie): + break + # Store the content, plus the time in the future when this entry will + # be evicted from the database, due to staleness. + db[cookie] = content + evictions = db.setdefault('evictions', {}) + evictions[cookie] = now + mm_cfg.PENDING_REQUEST_LIFE + _save(db) + return cookie + finally: + lock.unlock() + + + +def confirm(cookie, expunge=1): + """Return data for cookie, or None if not found. + + If optional expunge is true (the default), the record is also removed from + the database. + """ + # Acquire the pending database lock, letting TimeOutError percolate up. + # BAW: we perhaps shouldn't acquire the lock if expunge==0. + lock = LockFile.LockFile(LOCKFILE) + lock.lock(timeout=30) + try: + # Load the database + db = _load() + missing = [] + content = db.get(cookie, missing) + if content is missing: + return None + # Remove the entry from the database + if expunge: + del db[cookie] + del db['evictions'][cookie] + _save(db) + return content + finally: + lock.unlock() + + + +def _load(): + # The list's lock must be acquired. + # + # First try to load the pickle file + fp = None + try: + try: + fp = open(PCKFILE) + return cPickle.load(fp) + except IOError, e: + if e.errno <> errno.ENOENT: raise + try: + # Try to load the old DBFILE + fp = open(DBFILE) + return marshal.load(fp) + except IOError, e: + if e.errno <> errno.ENOENT: raise + # Fresh pendings database + return {'evictions': {}} + finally: + if fp: + fp.close() + + +def _save(db): + # Lock must be acquired. + evictions = db['evictions'] + now = time.time() + for cookie, data in db.items(): + if cookie in ('evictions', 'version'): + continue + timestamp = evictions[cookie] + if now > timestamp: + # The entry is stale, so remove it. + del db[cookie] + del evictions[cookie] + # Clean out any bogus eviction entries. + for cookie in evictions.keys(): + if not db.has_key(cookie): + del evictions[cookie] + db['version'] = mm_cfg.PENDING_FILE_SCHEMA_VERSION + omask = os.umask(007) + # Always save this as a pickle (safely), and after that succeeds, blow + # away any old marshal file. + tmpfile = PCKFILE + '.tmp' + fp = None + try: + fp = open(tmpfile, 'w') + cPickle.dump(db, fp) + fp.close() + fp = None + os.rename(tmpfile, PCKFILE) + if os.path.exists(DBFILE): + os.remove(DBFILE) + finally: + if fp: + fp.close() + os.umask(omask) + + + +def _update(olddb): + # Update an old pending_subscriptions.db database to the new format + lock = LockFile.LockFile(LOCKFILE) + lock.lock(timeout=30) + try: + # We don't need this entry anymore + if olddb.has_key('lastculltime'): + del olddb['lastculltime'] + db = _load() + evictions = db.setdefault('evictions', {}) + for cookie, data in olddb.items(): + # The cookies used to be kept as a 6 digit integer. We now keep + # the cookies as a string (sha in our case, but it doesn't matter + # for cookie matching). + cookie = str(cookie) + # The old format kept the content as a tuple and tacked the + # timestamp on as the last element of the tuple. We keep the + # timestamps separate, but require the prepending of a record type + # indicator. We know that the only things that were kept in the + # old format were subscription requests. Also, the old request + # format didn't have the subscription language. Best we can do + # here is use the server default. + db[cookie] = (SUBSCRIPTION,) + data[:-1] + \ + (mm_cfg.DEFAULT_SERVER_LANGUAGE,) + # The old database format kept the timestamp as the time the + # request was made. The new format keeps it as the time the + # request should be evicted. + evictions[cookie] = data[-1] + mm_cfg.PENDING_REQUEST_LIFE + _save(db) + finally: + lock.unlock() |