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