""" 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')
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."""
assert content[:1] in _ALLKEYS
lock = LockFile.LockFile(LOCKFILE,
withlogging=mm_cfg.PENDINGDB_LOCK_DEBUGGING)
retries = mm_cfg.PENDINGDB_LOCK_ATTEMPTS
try:
while retries:
retries -= 1
if not lock.locked():
try:
lock.lock(timeout=mm_cfg.PENDINGDB_LOCK_TIMEOUT)
except LockFile.TimeOutError:
continue
db = _load()
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
db[cookie] = content
evictions = db.setdefault('evictions', {})
evictions[cookie] = now + mm_cfg.PENDING_REQUEST_LIFE
try:
_save(db, lock)
except LockFile.NotLockedError:
continue
return cookie
else:
raise LockFile.TimeOutError
finally:
if lock.locked():
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.
"""
if not expunge:
db = _load()
missing = []
content = db.get(cookie, missing)
if content is missing:
return None
return content
lock = LockFile.LockFile(LOCKFILE,
withlogging=mm_cfg.PENDINGDB_LOCK_DEBUGGING)
retries = mm_cfg.PENDINGDB_LOCK_ATTEMPTS
try:
while retries:
retries -= 1
if not lock.locked():
try:
lock.lock(timeout=mm_cfg.PENDINGDB_LOCK_TIMEOUT)
except LockFile.TimeOutError:
continue
db = _load()
missing = []
content = db.get(cookie, missing)
if content is missing:
return None
del db[cookie]
del db['evictions'][cookie]
try:
_save(db, lock)
except LockFile.NotLockedError:
continue
return content
else:
raise LockFile.TimeOutError
finally:
if lock.locked():
lock.unlock()
def _load():
fp = None
try:
try:
fp = open(PCKFILE)
return cPickle.load(fp)
except IOError, e:
if e.errno <> errno.ENOENT: raise
try:
fp = open(DBFILE)
return marshal.load(fp)
except IOError, e:
if e.errno <> errno.ENOENT: raise
return {'evictions': {}}
finally:
if fp:
fp.close()
def _save(db, lock):
if not lock.locked():
raise LockFile.NotLockedError
evictions = db['evictions']
now = time.time()
for cookie, data in db.items():
if cookie in ('evictions', 'version'):
continue
timestamp = evictions[cookie]
if now > timestamp:
del db[cookie]
del evictions[cookie]
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)
tmpfile = '%s.tmp.%d.%d' % (PCKFILE, os.getpid(), now)
fp = None
try:
fp = open(tmpfile, 'w')
cPickle.dump(db, fp)
fp.close()
fp = None
if not lock.locked():
os.remove(tmpfile)
raise LockFile.NotLockedError
os.rename(tmpfile, PCKFILE)
if os.path.exists(DBFILE):
os.remove(DBFILE)
finally:
if fp:
fp.close()
os.umask(omask)
def _update(olddb):
lock = LockFile.LockFile(LOCKFILE,
withlogging=mm_cfg.PENDINGDB_LOCK_DEBUGGING)
lock.lock(timeout=mm_cfg.PENDINGDB_LOCK_TIMEOUT)
try:
if olddb.has_key('lastculltime'):
del olddb['lastculltime']
db = _load()
evictions = db.setdefault('evictions', {})
for cookie, data in olddb.items():
cookie = str(cookie)
db[cookie] = (SUBSCRIPTION,) + data[:-1] + \
(mm_cfg.DEFAULT_SERVER_LANGUAGE,)
evictions[cookie] = data[-1] + mm_cfg.PENDING_REQUEST_LIFE
_save(db, lock)
finally:
if lock.locked():
lock.unlock()