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