diff options
Diffstat (limited to 'Mailman/BDBMemberAdaptor.py')
-rw-r--r-- | Mailman/BDBMemberAdaptor.py | 637 |
1 files changed, 637 insertions, 0 deletions
diff --git a/Mailman/BDBMemberAdaptor.py b/Mailman/BDBMemberAdaptor.py new file mode 100644 index 00000000..589be626 --- /dev/null +++ b/Mailman/BDBMemberAdaptor.py @@ -0,0 +1,637 @@ +# Copyright (C) 2003 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +"""A MemberAdaptor based on the Berkeley database wrapper for Python. + +Requires Python 2.2.2 or newer, and PyBSDDB3 4.1.3 or newer. +""" + +# To use, put the following in a file called extend.py in the mailing list's +# directory: +# +# from Mailman.BDBMemberAdaptor import extend +# +# that's it! + +import os +import new +import time +import errno +import struct +import cPickle as pickle + +try: + # Python 2.3 + from bsddb import db +except ImportError: + # earlier Pythons + from bsddb3 import db + +from Mailman import mm_cfg +from Mailman import Utils +from Mailman import Errors +from Mailman import MemberAdaptor +from Mailman.MailList import MailList +from Mailman.Logging.Syslog import syslog + +STORAGE_VERSION = 'BA01' +FMT = '>BHB' +FMTSIZE = struct.calcsize(FMT) + +REGDELIV = 1 +DIGDELIV = 2 +REGFLAG = struct.pack('>B', REGDELIV) +DIGFLAG = struct.pack('>B', DIGDELIV) + +# Positional arguments for _unpack() +CPADDR = 0 +PASSWD = 1 +LANG = 2 +NAME = 3 +DIGEST = 4 +OPTIONS = 5 +STATUS = 6 + + + +class BDBMemberAdaptor(MemberAdaptor.MemberAdaptor): + def __init__(self, mlist): + self._mlist = mlist + # metainfo -- {key -> value} + # This table contains storage metadata information. The keys and + # values are simple strings of variable length. Here are the + # valid keys: + # + # version - the version of the database + # + # members -- {address | rec} + # For all regular delivery members, this maps from the member's + # key to their data record, which is a string concatenated of the + # following: + # + # -- fixed data (as a packed struct) + # + 1-byte digest or regular delivery flag + # + 2-byte option flags + # + 1-byte delivery status + # -- variable data (as a pickle of a tuple) + # + their case preserved address or '' + # + their plaintext password + # + their chosen language + # + their realname or '' + # + # status -- {address | status+time} + # Maps the member's key to their delivery status and change time. + # These are passed as a tuple and are pickled for storage. + # + # topics -- {address | topicstrings} + # Maps the member's key to their topic strings, concatenated and + # separated by SEP + # + # bounceinfo -- {address | bounceinfo} + # Maps the member's key to their bounceinfo, as a pickle + # + # Make sure the database directory exists + path = os.path.join(mlist.fullpath(), 'member.db') + exists = False + try: + os.mkdir(path, 02775) + except OSError, e: + if e.errno <> errno.EEXIST: raise + exists = True + # Create the environment + self._env = env = db.DBEnv() + if exists: + # We must join an existing environment, otherwise we'll get + # DB_RUNRECOVERY errors when the second process to open the + # environment begins a transaction. I don't get it. + env.open(path, db.DB_JOINENV) + else: + env.open(path, + db.DB_CREATE | + db.DB_RECOVER | + db.DB_INIT_MPOOL | + db.DB_INIT_TXN + ) + self._txn = None + self._tables = [] + self._metainfo = self._setupDB('metainfo') + self._members = self._setupDB('members') + self._status = self._setupDB('status') + self._topics = self._setupDB('topics') + self._bounceinfo = self._setupDB('bounceinfo') + # Check the database version number + version = self._metainfo.get('version') + if version is None: + # Initialize + try: + self.txn_begin() + self._metainfo.put('version', STORAGE_VERSION, txn=self._txn) + except: + self.txn_abort() + raise + else: + self.txn_commit() + else: + # Currently there's nothing to upgrade + assert version == STORAGE_VERSION + + def _setupDB(self, name): + d = db.DB(self._env) + openflags = db.DB_CREATE + # db 4.1 requires that databases be opened in a transaction. We'll + # use auto commit, but only if that flag exists (i.e. we're using at + # least db 4.1). + try: + openflags |= db.DB_AUTO_COMMIT + except AttributeError: + pass + d.open(name, db.DB_BTREE, openflags) + self._tables.append(d) + return d + + def _close(self): + self.txn_abort() + for d in self._tables: + d.close() + # Checkpoint the database twice, as recommended by Sleepycat + self._checkpoint() + self._checkpoint() + self._env.close() + + def _checkpoint(self): + self._env.txn_checkpoint(0, 0, db.DB_FORCE) + + def txn_begin(self): + assert self._txn is None + self._txn = self._env.txn_begin() + + def txn_commit(self): + assert self._txn is not None + self._txn.commit() + self._checkpoint() + self._txn = None + + def txn_abort(self): + if self._txn is not None: + self._txn.abort() + self._checkpoint() + self._txn = None + + def _unpack(self, member): + # Assume member is a LCE (i.e. lowercase key) + rec = self._members.get(member.lower()) + assert rec is not None + fixed = struct.unpack(FMT, rec[:FMTSIZE]) + vari = pickle.loads(rec[FMTSIZE:]) + return vari + fixed + + def _pack(self, member, cpaddr, passwd, lang, name, digest, flags, status): + # Assume member is a LCE (i.e. lowercase key) + fixed = struct.pack(FMT, digest, flags, status) + vari = pickle.dumps((cpaddr, passwd, lang, name)) + self._members.put(member.lower(), fixed+vari, txn=self._txn) + + # MemberAdaptor writeable interface + + def addNewMember(self, member, **kws): + assert self._mlist.Locked() + # Make sure this address isn't already a member + if self.isMember(member): + raise Errors.MMAlreadyAMember, member + # Parse the keywords + digest = False + password = Utils.MakeRandomPassword() + language = self._mlist.preferred_language + realname = None + if kws.has_key('digest'): + digest = kws['digest'] + del kws['digest'] + if kws.has_key('password'): + password = kws['password'] + del kws['password'] + if kws.has_key('language'): + language = kws['language'] + del kws['language'] + if kws.has_key('realname'): + realname = kws['realname'] + del kws['realname'] + # Assert that no other keywords are present + if kws: + raise ValueError, kws.keys() + # Should we store the case-preserved address? + if Utils.LCDomain(member) == member.lower(): + cpaddress = '' + else: + cpaddress = member + # Calculate the realname + if realname is None: + realname = '' + # Calculate the digest flag + if digest: + digest = DIGDELIV + else: + digest = REGDELIV + self._pack(member.lower(), + cpaddress, password, language, realname, + digest, self._mlist.new_member_options, + MemberAdaptor.ENABLED) + + def removeMember(self, member): + txn = self._txn + assert txn is not None + assert self._mlist.Locked() + self.__assertIsMember(member) + key = member.lower() + # Remove the table entries + self._members.delete(key, txn=txn) + if self._status.has_key(key): + self._status.delete(key, txn=txn) + if self._topics.has_key(key): + self._topics.delete(key, txn=txn) + if self._bounceinfo.has_key(key): + self._bounceinfo.delete(key, txn=txn) + + def changeMemberAddress(self, member, newaddress, nodelete=0): + assert self._mlist.Locked() + self.__assertIsMember(member) + okey = member.lower() + nkey = newaddress.lower() + txn = self._txn + assert txn is not None + # First, store a new member record, changing the case preserved addr. + # Then delete the old record. + cpaddr, passwd, lang, name, digest, flags, sts = self._unpack(okey) + self._pack(nkey, newaddress, passwd, lang, name, digest, flags, sts) + if not nodelete: + self._members.delete(okey, txn) + # Copy over the status times, topics, and bounce info, if present + timestr = self._status.get(okey) + if timestr is not None: + self._status.put(nkey, timestr, txn=txn) + if not nodelete: + self._status.delete(okey, txn) + topics = self._topics.get(okey) + if topics is not None: + self._topics.put(nkey, topics, txn=txn) + if not nodelete: + self._topics.delete(okey, txn) + binfo = self._bounceinfo.get(nkey) + if binfo is not None: + self._binfo.put(nkey, binfo, txn=txn) + if not nodelete: + self._binfo.delete(okey, txn) + + def setMemberPassword(self, member, password): + assert self._mlist.Locked() + self.__assertIsMember(member) + member = member.lower() + cpaddr, oldpw, lang, name, digest, flags, status = self._unpack(member) + self._pack(member, cpaddr, password, lang, name, digest, flags, status) + + def setMemberLanguage(self, member, language): + assert self._mlist.Locked() + self.__assertIsMember(member) + member = member.lower() + cpaddr, passwd, olang, name, digest, flags, sts = self._unpack(member) + self._pack(member, cpaddr, passwd, language, name, digest, flags, sts) + + def setMemberOption(self, member, flag, value): + assert self._mlist.Locked() + self.__assertIsMember(member) + member = member.lower() + cpaddr, passwd, lang, name, digest, options, sts = self._unpack(member) + # Sanity check for the digest flag + if flag == mm_cfg.Digests: + if value: + # Be sure the list supports digest delivery + if not self._mlist.digestable: + raise Errors.CantDigestError + digest = DIGDELIV + else: + # Be sure the list supports regular delivery + if not self._mlist.nondigestable: + raise Errors.MustDigestError + # When toggling off digest delivery, we want to be sure to set + # things up so that the user receives one last digest, + # otherwise they may lose some email + self._mlist.one_last_digest[member] = cpaddr + digest = REGDELIV + else: + if value: + options |= flag + else: + options &= ~flag + self._pack(member, cpaddr, passwd, lang, name, digest, options, sts) + + def setMemberName(self, member, realname): + assert self._mlist.Locked() + self.__assertIsMember(member) + member = member.lower() + cpaddr, passwd, lang, oldname, digest, flags, sts = self._unpack( + member) + self._pack(member, cpaddr, passwd, lang, realname, digest, flags, sts) + + def setMemberTopics(self, member, topics): + assert self._mlist.Locked() + self.__assertIsMember(member) + member = member.lower() + if topics: + self._topics.put(member, SEP.join(topics), txn=self._txn) + elif self._topics.has_key(member): + # No record is the same as no topics + self._topics.delete(member, self._txn) + + def setDeliveryStatus(self, member, status): + assert status in (MemberAdaptor.ENABLED, MemberAdaptor.UNKNOWN, + MemberAdaptor.BYUSER, MemberAdaptor.BYADMIN, + MemberAdaptor.BYBOUNCE) + assert self._mlist.Locked() + self.__assertIsMember(member) + if status == MemberAdaptor.ENABLED: + # Enable by resetting their bounce info + self.setBounceInfo(member, None) + else: + # Pickle up the status an the current time and store that in the + # database. Use binary mode. + data = pickle.dumps((status, time.time()), 1) + self._status.put(member.lower(), data, txn=self._txn) + + def setBounceInfo(self, member, info): + assert self._mlist.Locked() + self.__assertIsMember(member) + member = member.lower() + if info is None: + # This means to reset the bounce and delivery status information + if self._bounceinfo.has_key(member): + self._bounceinfo.delete(member, self._txn) + if self._status.has_key(member): + self._status.delete(member, self._txn) + else: + # Use binary mode + data = pickle.dumps(info, 1) + self._status.put(member, data, txn=self._txn) + + # The readable interface + + # BAW: It would be more efficient to simply return the iterator, but + # modules like admin.py can't handle that yet. They requires lists. + def getMembers(self): + return list(_AllMembersIterator(self._members)) + + def getRegularMemberKeys(self): + return list(_DeliveryMemberIterator(self._members, REGFLAG)) + + def getDigestMemberKeys(self): + return list(_DeliveryMemberIterator(self._members, DIGFLAG)) + + def __assertIsMember(self, member): + if not self.isMember(member): + raise Errors.NotAMemberError, member + + def isMember(self, member): + return self._members.has_key(member.lower()) + + def getMemberKey(self, member): + self.__assertIsMember(member) + return member.lower() + + def getMemberCPAddress(self, member): + self.__assertIsMember(member) + cpaddr = self._unpack(member)[CPADDR] + if cpaddr: + return cpaddr + return member + + def getMemberCPAddresses(self, members): + rtn = [] + for member in members: + member = member.lower() + if self._members.has_key(member): + rtn.append(self._unpack(member)[CPADDR]) + else: + rtn.append(None) + return rtn + + def authenticateMember(self, member, response): + self.__assertIsMember(member) + passwd = self._unpack(member)[PASSWD] + if passwd == response: + return passwd + return False + + def getMemberPassword(self, member): + self.__assertIsMember(member) + return self._unpack(member)[PASSWD] + + def getMemberLanguage(self, member): + if not self.isMember(member): + return self._mlist.preferred_language + lang = self._unpack(member)[LANG] + if lang in self._mlist.GetAvailableLanguages(): + return lang + return self._mlist.preferred_language + + def getMemberOption(self, member, flag): + self.__assertIsMember(member) + if flag == mm_cfg.Digests: + return self._unpack(member)[DIGEST] == DIGDELIV + options = self._unpack(member)[OPTIONS] + return bool(options & flag) + + def getMemberName(self, member): + self.__assertIsMember(member) + name = self._unpack(member)[NAME] + return name or None + + def getMemberTopics(self, member): + self.__assertIsMember(member) + topics = self._topics.get(member.lower(), '') + if not topics: + return [] + return topics.split(SEP) + + def getDeliveryStatus(self, member): + self.__assertIsMember(member) + data = self._status.get(member.lower()) + if data is None: + return MemberAdaptor.ENABLED + status, when = pickle.loads(data) + return status + + def getDeliveryStatusChangeTime(self, member): + self.__assertIsMember(member) + data = self._status.get(member.lower()) + if data is None: + return 0 + status, when = pickle.loads(data) + return when + + # BAW: see above, re iterators + def getDeliveryStatusMembers(self, status=(MemberAdaptor.UNKNOWN, + MemberAdaptor.BYUSER, + MemberAdaptor.BYADMIN, + MemberAdaptor.BYBOUNCE)): + return list(_StatusMemberIterator(self._members, self._status, status)) + + def getBouncingMembers(self): + return list(_BouncingMembersIterator(self._bounceinfo)) + + def getBounceInfo(self, member): + self.__assertIsMember(member) + return self._bounceinfo.get(member.lower()) + + + +class _MemberIterator: + def __init__(self, table): + self._table = table + self._c = table.cursor() + + def __iter__(self): + raise NotImplementedError + + def next(self): + raise NotImplementedError + + def close(self): + if self._c: + self._c.close() + self._c = None + + def __del__(self): + self.close() + + +class _AllMembersIterator(_MemberIterator): + def __iter__(self): + return _AllMembersIterator(self._table) + + def next(self): + rec = self._c.next() + if rec: + return rec[0] + self.close() + raise StopIteration + + +class _DeliveryMemberIterator(_MemberIterator): + def __init__(self, table, flag): + _MemberIterator.__init__(self, table) + self._flag = flag + + def __iter__(self): + return _DeliveryMemberIterator(self._table, self._flag) + + def next(self): + rec = self._c.next() + while rec: + addr, data = rec + if data[0] == self._flag: + return addr + rec = self._c.next() + self.close() + raise StopIteration + + +class _StatusMemberIterator(_MemberIterator): + def __init__(self, table, statustab, status): + _MemberIterator.__init__(self, table) + self._statustab = statustab + self._status = status + + def __iter__(self): + return _StatusMemberIterator(self._table, + self._statustab, + self._status) + + def next(self): + rec = self._c.next() + while rec: + addr = rec[0] + data = self._statustab.get(addr) + if data is None: + status = MemberAdaptor.ENABLED + else: + status, when = pickle.loads(data) + if status in self._status: + return addr + rec = self._c.next() + self.close() + raise StopIteration + + +class _BouncingMembersIterator(_MemberIterator): + def __iter__(self): + return _BouncingMembersIterator(self._table) + + def next(self): + rec = self._c.next() + if rec: + return rec[0] + self.close() + raise StopIteration + + + +# For extend.py +def fixlock(mlist): + def Lock(self, timeout=0): + MailList.Lock(self, timeout) + try: + self._memberadaptor.txn_begin() + except: + MailList.Unlock(self) + raise + mlist.Lock = new.instancemethod(Lock, mlist, MailList) + + +def fixsave(mlist): + def Save(self): + self._memberadaptor.txn_commit() + MailList.Save(self) + mlist.Save = new.instancemethod(Save, mlist, MailList) + + +def fixunlock(mlist): + def Unlock(self): + # It's fine to abort the transaction even if there isn't one in + # process, say because the Save() already committed it + self._memberadaptor.txn_abort() + MailList.Unlock(self) + mlist.Unlock = new.instancemethod(Unlock, mlist, MailList) + + +def extend(mlist): + mlist._memberadaptor = BDBMemberAdaptor(mlist) + fixlock(mlist) + fixsave(mlist) + fixunlock(mlist) + # To make sure we got everything, let's actually delete the + # OldStyleMemberships dictionaries. Assume if it has one, it has all + # attributes. + try: + del mlist.members + del mlist.digest_members + del mlist.passwords + del mlist.language + del mlist.user_options + del mlist.usernames + del mlist.topics_userinterest + del mlist.delivery_status + del mlist.bounce_info + except AttributeError: + pass + # BAW: How can we ensure that the BDBMemberAdaptor is closed? |