Apply 01_defaults.debian.patch
[mspang/vmailman.git] / Mailman / Pending.py
1 # Copyright (C) 1998-2004 by the Free Software Foundation, Inc.
2 #
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
7 #
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 # GNU General Public License for more details.
12 #
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software
15 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16
17 """Track pending actions which require confirmation."""
18
19 import os
20 import sha
21 import time
22 import errno
23 import random
24 import cPickle
25
26 from Mailman import mm_cfg
27
28 # Types of pending records
29 SUBSCRIPTION = 'S'
30 UNSUBSCRIPTION = 'U'
31 CHANGE_OF_ADDRESS = 'C'
32 HELD_MESSAGE = 'H'
33 RE_ENABLE = 'E'
34 PROBE_BOUNCE = 'P'
35
36 _ALLKEYS = (SUBSCRIPTION, UNSUBSCRIPTION,
37             CHANGE_OF_ADDRESS, HELD_MESSAGE,
38             RE_ENABLE, PROBE_BOUNCE,
39             )
40
41 try:
42     True, False
43 except NameError:
44     True = 1
45     False = 0
46
47
48 _missing = []
49
50
51 \f
52 class Pending:
53     def InitTempVars(self):
54         self.__pendfile = os.path.join(self.fullpath(), 'pending.pck')
55
56     def pend_new(self, op, *content, **kws):
57         """Create a new entry in the pending database, returning cookie for it.
58         """
59         assert op in _ALLKEYS, 'op: %s' % op
60         lifetime = kws.get('lifetime', mm_cfg.PENDING_REQUEST_LIFE)
61         # We try the main loop several times. If we get a lock error somewhere
62         # (for instance because someone broke the lock) we simply try again.
63         assert self.Locked()
64         # Load the database
65         db = self.__load()
66         # Calculate a unique cookie.  Algorithm vetted by the Timbot.  time()
67         # has high resolution on Linux, clock() on Windows.  random gives us
68         # about 45 bits in Python 2.2, 53 bits on Python 2.3.  The time and
69         # clock values basically help obscure the random number generator, as
70         # does the hash calculation.  The integral parts of the time values
71         # are discarded because they're the most predictable bits.
72         while True:
73             now = time.time()
74             x = random.random() + now % 1.0 + time.clock() % 1.0
75             cookie = sha.new(repr(x)).hexdigest()
76             # We'll never get a duplicate, but we'll be anal about checking
77             # anyway.
78             if not db.has_key(cookie):
79                 break
80         # Store the content, plus the time in the future when this entry will
81         # be evicted from the database, due to staleness.
82         db[cookie] = (op,) + content
83         evictions = db.setdefault('evictions', {})
84         evictions[cookie] = now + lifetime
85         self.__save(db)
86         return cookie
87
88     def __load(self):
89         try:
90             fp = open(self.__pendfile)
91         except IOError, e:
92             if e.errno <> errno.ENOENT: raise
93             return {'evictions': {}}
94         try:
95             return cPickle.load(fp)
96         finally:
97             fp.close()
98
99     def __save(self, db):
100         evictions = db['evictions']
101         now = time.time()
102         for cookie, data in db.items():
103             if cookie in ('evictions', 'version'):
104                 continue
105             timestamp = evictions[cookie]
106             if now > timestamp:
107                 # The entry is stale, so remove it.
108                 del db[cookie]
109                 del evictions[cookie]
110         # Clean out any bogus eviction entries.
111         for cookie in evictions.keys():
112             if not db.has_key(cookie):
113                 del evictions[cookie]
114         db['version'] = mm_cfg.PENDING_FILE_SCHEMA_VERSION
115         tmpfile = '%s.tmp.%d.%d' % (self.__pendfile, os.getpid(), now)
116         omask = os.umask(007)
117         try:
118             fp = open(tmpfile, 'w')
119             try:
120                 cPickle.dump(db, fp)
121                 fp.flush()
122                 os.fsync(fp.fileno())
123             finally:
124                 fp.close()
125             os.rename(tmpfile, self.__pendfile)
126         finally:
127             os.umask(omask)
128
129     def pend_confirm(self, cookie, expunge=True):
130         """Return data for cookie, or None if not found.
131
132         If optional expunge is True (the default), the record is also removed
133         from the database.
134         """
135         db = self.__load()
136         # If we're not expunging, the database is read-only.
137         if not expunge:
138             return db.get(cookie)
139         # Since we're going to modify the database, we must make sure the list
140         # is locked, since it's the list lock that protects pending.pck.
141         assert self.Locked()
142         content = db.get(cookie, _missing)
143         if content is _missing:
144             return None
145         # Do the expunge
146         del db[cookie]
147         del db['evictions'][cookie]
148         self.__save(db)
149         return content
150
151     def pend_repend(self, cookie, data, lifetime=mm_cfg.PENDING_REQUEST_LIFE):
152         assert self.Locked()
153         db = self.__load()
154         db[cookie] = data
155         db['evictions'][cookie] = time.time() + lifetime
156         self.__save(db)
157
158
159 \f
160 def _update(olddb):
161     db = {}
162     # We don't need this entry anymore
163     if olddb.has_key('lastculltime'):
164         del olddb['lastculltime']
165     evictions = db.setdefault('evictions', {})
166     for cookie, data in olddb.items():
167         # The cookies used to be kept as a 6 digit integer.  We now keep the
168         # cookies as a string (sha in our case, but it doesn't matter for
169         # cookie matching).
170         cookie = str(cookie)
171         # The old format kept the content as a tuple and tacked the timestamp
172         # on as the last element of the tuple.  We keep the timestamps
173         # separate, but require the prepending of a record type indicator.  We
174         # know that the only things that were kept in the old format were
175         # subscription requests.  Also, the old request format didn't have the
176         # subscription language.  Best we can do here is use the server
177         # default.
178         db[cookie] = (SUBSCRIPTION,) + data[:-1] + \
179                      (mm_cfg.DEFAULT_SERVER_LANGUAGE,)
180         # The old database format kept the timestamp as the time the request
181         # was made.  The new format keeps it as the time the request should be
182         # evicted.
183         evictions[cookie] = data[-1] + mm_cfg.PENDING_REQUEST_LIFE
184     return db