# Copyright (C) 1998-2009 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.
"""Track pending actions which require confirmation."""
import os
import time
import errno
import random
import cPickle
from Mailman import mm_cfg
from Mailman import UserDesc
from Mailman.Utils import sha_new
# Types of pending records
SUBSCRIPTION = 'S'
UNSUBSCRIPTION = 'U'
CHANGE_OF_ADDRESS = 'C'
HELD_MESSAGE = 'H'
RE_ENABLE = 'E'
PROBE_BOUNCE = 'P'
_ALLKEYS = (SUBSCRIPTION, UNSUBSCRIPTION,
CHANGE_OF_ADDRESS, HELD_MESSAGE,
RE_ENABLE, PROBE_BOUNCE,
)
try:
True, False
except NameError:
True = 1
False = 0
_missing = []
class Pending:
def InitTempVars(self):
self.__pendfile = os.path.join(self.fullpath(), 'pending.pck')
def pend_new(self, op, *content, **kws):
"""Create a new entry in the pending database, returning cookie for it.
"""
assert op in _ALLKEYS, 'op: %s' % op
lifetime = kws.get('lifetime', mm_cfg.PENDING_REQUEST_LIFE)
# We try the main loop several times. If we get a lock error somewhere
# (for instance because someone broke the lock) we simply try again.
assert self.Locked()
# Load the database
db = self.__load()
# Calculate a unique cookie. Algorithm vetted by the Timbot. time()
# has high resolution on Linux, clock() on Windows. random gives us
# about 45 bits in Python 2.2, 53 bits on Python 2.3. The time and
# clock values basically help obscure the random number generator, as
# does the hash calculation. The integral parts of the time values
# are discarded because they're the most predictable bits.
while True:
now = time.time()
x = random.random() + now % 1.0 + time.clock() % 1.0
cookie = sha_new(repr(x)).hexdigest()
# We'll never get a duplicate, but we'll be anal about checking
# anyway.
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] = (op,) + content
evictions = db.setdefault('evictions', {})
evictions[cookie] = now + lifetime
self.__save(db)
return cookie
def __load(self):
try:
fp = open(self.__pendfile)
except IOError, e:
if e.errno <> errno.ENOENT: raise
return {'evictions': {}}
try:
return cPickle.load(fp)
finally:
fp.close()
def __save(self, db):
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
tmpfile = '%s.tmp.%d.%d' % (self.__pendfile, os.getpid(), now)
omask = os.umask(007)
try:
fp = open(tmpfile, 'w')
try:
cPickle.dump(db, fp)
fp.flush()
os.fsync(fp.fileno())
finally:
fp.close()
os.rename(tmpfile, self.__pendfile)
finally:
os.umask(omask)
def pend_confirm(self, cookie, expunge=True):
"""Return data for cookie, or None if not found.
If optional expunge is True (the default), the record is also removed
from the database.
"""
db = self.__load()
# If we're not expunging, the database is read-only.
if not expunge:
return db.get(cookie)
# Since we're going to modify the database, we must make sure the list
# is locked, since it's the list lock that protects pending.pck.
assert self.Locked()
content = db.get(cookie, _missing)
if content is _missing:
return None
# Do the expunge
del db[cookie]
del db['evictions'][cookie]
self.__save(db)
return content
def pend_repend(self, cookie, data, lifetime=mm_cfg.PENDING_REQUEST_LIFE):
assert self.Locked()
db = self.__load()
db[cookie] = data
db['evictions'][cookie] = time.time() + lifetime
self.__save(db)
def _update(olddb):
db = {}
# We don't need this entry anymore
if olddb.has_key('lastculltime'):
del olddb['lastculltime']
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. We also need a fullname because confirmation processing
# references all those UserDesc attributes.
ud = UserDesc.UserDesc(address=data[0],
fullname='',
password=data[1],
digest=data[2],
lang=mm_cfg.DEFAULT_SERVER_LANGUAGE,
)
db[cookie] = (SUBSCRIPTION, ud)
# 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
return db