diff options
Diffstat (limited to 'src/mailman/database')
| -rw-r--r-- | src/mailman/database/__init__.py | 144 | ||||
| -rw-r--r-- | src/mailman/database/address.py | 96 | ||||
| -rw-r--r-- | src/mailman/database/autorespond.py | 93 | ||||
| -rw-r--r-- | src/mailman/database/digests.py | 53 | ||||
| -rw-r--r-- | src/mailman/database/domain.py | 163 | ||||
| -rw-r--r-- | src/mailman/database/language.py | 40 | ||||
| -rw-r--r-- | src/mailman/database/listmanager.py | 97 | ||||
| -rw-r--r-- | src/mailman/database/mailinglist.py | 465 | ||||
| -rw-r--r-- | src/mailman/database/member.py | 105 | ||||
| -rw-r--r-- | src/mailman/database/message.py | 53 | ||||
| -rw-r--r-- | src/mailman/database/messagestore.py | 137 | ||||
| -rw-r--r-- | src/mailman/database/mime.py | 52 | ||||
| -rw-r--r-- | src/mailman/database/pending.py | 175 | ||||
| -rw-r--r-- | src/mailman/database/preferences.py | 67 | ||||
| -rw-r--r-- | src/mailman/database/requests.py | 140 | ||||
| -rw-r--r-- | src/mailman/database/roster.py | 271 | ||||
| -rw-r--r-- | src/mailman/database/stock.py | 144 | ||||
| -rw-r--r-- | src/mailman/database/user.py | 94 | ||||
| -rw-r--r-- | src/mailman/database/usermanager.py | 104 | ||||
| -rw-r--r-- | src/mailman/database/version.py | 40 |
20 files changed, 144 insertions, 2389 deletions
diff --git a/src/mailman/database/__init__.py b/src/mailman/database/__init__.py index 748378ca7..e69de29bb 100644 --- a/src/mailman/database/__init__.py +++ b/src/mailman/database/__init__.py @@ -1,144 +0,0 @@ -# Copyright (C) 2006-2009 by the Free Software Foundation, Inc. -# -# This file is part of GNU Mailman. -# -# GNU Mailman 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 3 of the License, or (at your option) -# any later version. -# -# GNU Mailman 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 -# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. - -from __future__ import absolute_import, unicode_literals - -__metaclass__ = type -__all__ = [ - 'StockDatabase', - ] - -import os -import logging - -from locknix.lockfile import Lock -from lazr.config import as_boolean -from pkg_resources import resource_string -from storm.cache import GenerationalCache -from storm.locals import create_database, Store -from urlparse import urlparse -from zope.interface import implements - -import mailman.version - -from mailman.config import config -from mailman.database.messagestore import MessageStore -from mailman.database.pending import Pendings -from mailman.database.requests import Requests -from mailman.database.version import Version -from mailman.interfaces.database import IDatabase, SchemaVersionMismatchError -from mailman.utilities.string import expand - -log = logging.getLogger('mailman.config') - - - -class StockDatabase: - """The standard database, using Storm on top of SQLite.""" - - implements(IDatabase) - - def __init__(self): - self.url = None - self.store = None - - def initialize(self, debug=None): - """See `IDatabase`.""" - # Serialize this so we don't get multiple processes trying to create - # the database at the same time. - with Lock(os.path.join(config.LOCK_DIR, 'dbcreate.lck')): - self._create(debug) - - def begin(self): - """See `IDatabase`.""" - # Storm takes care of this for us. - pass - - def commit(self): - """See `IDatabase`.""" - self.store.commit() - - def abort(self): - """See `IDatabase`.""" - self.store.rollback() - - def _create(self, debug): - # Calculate the engine url. - url = expand(config.database.url, config.paths) - log.debug('Database url: %s', url) - # XXX By design of SQLite, database file creation does not honor - # umask. See their ticket #1193: - # http://www.sqlite.org/cvstrac/tktview?tn=1193,31 - # - # This sucks for us because the mailman.db file /must/ be group - # writable, however even though we guarantee our umask is 002 here, it - # still gets created without the necessary g+w permission, due to - # SQLite's policy. This should only affect SQLite engines because its - # the only one that creates a little file on the local file system. - # This kludges around their bug by "touch"ing the database file before - # SQLite has any chance to create it, thus honoring the umask and - # ensuring the right permissions. We only try to do this for SQLite - # engines, and yes, we could have chmod'd the file after the fact, but - # half dozen and all... - self.url = url - touch(url) - database = create_database(url) - store = Store(database, GenerationalCache()) - database.DEBUG = (as_boolean(config.database.debug) - if debug is None else debug) - # Check the sqlite master database to see if the version file exists. - # If so, then we assume the database schema is correctly initialized. - # Storm does not currently have schema creation. This is not an ideal - # way to handle creating the database, but it's cheap and easy for - # now. - table_names = [item[0] for item in - store.execute('select tbl_name from sqlite_master;')] - if 'version' not in table_names: - # Initialize the database. - sql = resource_string('mailman.database', 'mailman.sql') - for statement in sql.split(';'): - store.execute(statement + ';') - # Validate schema version. - v = store.find(Version, component='schema').one() - if not v: - # Database has not yet been initialized - v = Version(component='schema', - version=mailman.version.DATABASE_SCHEMA_VERSION) - store.add(v) - elif v.version <> mailman.version.DATABASE_SCHEMA_VERSION: - # XXX Update schema - raise SchemaVersionMismatchError(v.version) - self.store = store - store.commit() - - def _reset(self): - """See `IDatabase`.""" - from mailman.database.model import ModelMeta - self.store.rollback() - ModelMeta._reset(self.store) - - - -def touch(url): - parts = urlparse(url) - if parts.scheme <> 'sqlite': - return - path = os.path.normpath(parts.path) - fd = os.open(path, os.O_WRONLY | os.O_NONBLOCK | os.O_CREAT, 0666) - # Ignore errors - if fd > 0: - os.close(fd) diff --git a/src/mailman/database/address.py b/src/mailman/database/address.py deleted file mode 100644 index f63f03e14..000000000 --- a/src/mailman/database/address.py +++ /dev/null @@ -1,96 +0,0 @@ -# Copyright (C) 2006-2009 by the Free Software Foundation, Inc. -# -# This file is part of GNU Mailman. -# -# GNU Mailman 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 3 of the License, or (at your option) -# any later version. -# -# GNU Mailman 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 -# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. - -"""Model for addresses.""" - -from __future__ import absolute_import, unicode_literals - -__metaclass__ = type -__all__ = [ - 'Address', - ] - - -from email.utils import formataddr -from storm.locals import DateTime, Int, Reference, Store, Unicode -from zope.interface import implements - -from mailman.database.member import Member -from mailman.database.model import Model -from mailman.database.preferences import Preferences -from mailman.interfaces.member import AlreadySubscribedError -from mailman.interfaces.address import IAddress - - - -class Address(Model): - implements(IAddress) - - id = Int(primary=True) - address = Unicode() - _original = Unicode() - real_name = Unicode() - verified_on = DateTime() - registered_on = DateTime() - - user_id = Int() - user = Reference(user_id, 'User.id') - preferences_id = Int() - preferences = Reference(preferences_id, 'Preferences.id') - - def __init__(self, address, real_name): - super(Address, self).__init__() - lower_case = address.lower() - self.address = lower_case - self.real_name = real_name - self._original = (None if lower_case == address else address) - - def __str__(self): - addr = (self.address if self._original is None else self._original) - return formataddr((self.real_name, addr)) - - def __repr__(self): - verified = ('verified' if self.verified_on else 'not verified') - address_str = str(self) - if self._original is None: - return '<Address: {0} [{1}] at {2:#x}>'.format( - address_str, verified, id(self)) - else: - return '<Address: {0} [{1}] key: {2} at {3:#x}>'.format( - address_str, verified, self.address, id(self)) - - def subscribe(self, mailing_list, role): - # This member has no preferences by default. - store = Store.of(self) - member = store.find( - Member, - Member.role == role, - Member.mailing_list == mailing_list.fqdn_listname, - Member.address == self).one() - if member: - raise AlreadySubscribedError( - mailing_list.fqdn_listname, self.address, role) - member = Member(role=role, - mailing_list=mailing_list.fqdn_listname, - address=self) - member.preferences = Preferences() - store.add(member) - return member - - @property - def original_address(self): - return (self.address if self._original is None else self._original) diff --git a/src/mailman/database/autorespond.py b/src/mailman/database/autorespond.py deleted file mode 100644 index 0a84f30e3..000000000 --- a/src/mailman/database/autorespond.py +++ /dev/null @@ -1,93 +0,0 @@ -# Copyright (C) 2009 by the Free Software Foundation, Inc. -# -# This file is part of GNU Mailman. -# -# GNU Mailman 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 3 of the License, or (at your option) -# any later version. -# -# GNU Mailman 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 -# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. - -"""Module stuff.""" - -from __future__ import absolute_import, unicode_literals - -__metaclass__ = type -__all__ = [ - 'AutoResponseRecord', - 'AutoResponseSet', - ] - - -from storm.locals import And, Date, Desc, Int, Reference -from zope.interface import implements - -from mailman.config import config -from mailman.database.model import Model -from mailman.database.types import Enum -from mailman.interfaces.autorespond import ( - IAutoResponseRecord, IAutoResponseSet, Response) -from mailman.interfaces.mailinglist import IMailingList -from mailman.utilities.datetime import today - - - -class AutoResponseRecord(Model): - implements(IAutoResponseRecord) - - id = Int(primary=True) - - address_id = Int() - address = Reference(address_id, 'Address.id') - - mailing_list_id = Int() - mailing_list = Reference(mailing_list_id, 'MailingList.id') - - response_type = Enum() - date_sent = Date() - - def __init__(self, mailing_list, address, response_type): - self.mailing_list = mailing_list - self.address = address - self.response_type = response_type - self.date_sent = today() - - - -class AutoResponseSet: - implements(IAutoResponseSet) - - def __init__(self, mailing_list): - self._mailing_list = mailing_list - - def todays_count(self, address, response_type): - """See `IAutoResponseSet`.""" - return config.db.store.find( - AutoResponseRecord, - And(AutoResponseRecord.address == address, - AutoResponseRecord.mailing_list == self._mailing_list, - AutoResponseRecord.response_type == response_type, - AutoResponseRecord.date_sent == today())).count() - - def response_sent(self, address, response_type): - """See `IAutoResponseSet`.""" - response = AutoResponseRecord( - self._mailing_list, address, response_type) - config.db.store.add(response) - - def last_response(self, address, response_type): - """See `IAutoResponseSet`.""" - results = config.db.store.find( - AutoResponseRecord, - And(AutoResponseRecord.address == address, - AutoResponseRecord.mailing_list == self._mailing_list, - AutoResponseRecord.response_type == response_type) - ).order_by(Desc(AutoResponseRecord.date_sent)) - return (None if results.count() == 0 else results.first()) diff --git a/src/mailman/database/digests.py b/src/mailman/database/digests.py deleted file mode 100644 index 291dafa28..000000000 --- a/src/mailman/database/digests.py +++ /dev/null @@ -1,53 +0,0 @@ -# Copyright (C) 2009 by the Free Software Foundation, Inc. -# -# This file is part of GNU Mailman. -# -# GNU Mailman 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 3 of the License, or (at your option) -# any later version. -# -# GNU Mailman 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 -# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. - -"""One last digest.""" - -from __future__ import absolute_import, unicode_literals - -__metaclass__ = type -__all__ = [ - 'OneLastDigest', - ] - - -from storm.locals import Int, Reference -from zope.interface import implements - -from mailman.database.model import Model -from mailman.database.types import Enum -from mailman.interfaces.digests import IOneLastDigest - - - -class OneLastDigest(Model): - implements(IOneLastDigest) - - id = Int(primary=True) - - mailing_list_id = Int() - mailing_list = Reference(mailing_list_id, 'MailingList.id') - - address_id = Int() - address = Reference(address_id, 'Address.id') - - delivery_mode = Enum() - - def __init__(self, mailing_list, address, delivery_mode): - self.mailing_list = mailing_list - self.address = address - self.delivery_mode = delivery_mode diff --git a/src/mailman/database/domain.py b/src/mailman/database/domain.py deleted file mode 100644 index 9d4e714bc..000000000 --- a/src/mailman/database/domain.py +++ /dev/null @@ -1,163 +0,0 @@ -# Copyright (C) 2008-2009 by the Free Software Foundation, Inc. -# -# This file is part of GNU Mailman. -# -# GNU Mailman 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 3 of the License, or (at your option) -# any later version. -# -# GNU Mailman 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 -# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. - -"""Domains.""" - -from __future__ import unicode_literals - -__metaclass__ = type -__all__ = [ - 'Domain', - 'DomainManager', - ] - -from urlparse import urljoin, urlparse -from storm.locals import Int, Unicode -from zope.interface import implements - -from mailman.database.model import Model -from mailman.interfaces.domain import ( - BadDomainSpecificationError, IDomain, IDomainManager) - - - -class Domain(Model): - """Domains.""" - - implements(IDomain) - - id = Int(primary=True) - - email_host = Unicode() - base_url = Unicode() - description = Unicode() - contact_address = Unicode() - - def __init__(self, email_host, - description=None, - base_url=None, - contact_address=None): - """Create and register a domain. - - :param email_host: The host name for the email interface. - :type email_host: string - :param description: An optional description of the domain. - :type description: string - :param base_url: The optional base url for the domain, including - scheme. If not given, it will be constructed from the - `email_host` using the http protocol. - :type base_url: string - :param contact_address: The email address to contact a human for this - domain. If not given, postmaster@`email_host` will be used. - :type contact_address: string - """ - self.email_host = email_host - self.base_url = (base_url - if base_url is not None - else 'http://' + email_host) - self.description = description - self.contact_address = (contact_address - if contact_address is not None - else 'postmaster@' + email_host) - - @property - def url_host(self): - # pylint: disable-msg=E1101 - # no netloc member; yes it does - return urlparse(self.base_url).netloc - - def confirm_address(self, token=''): - """See `IDomain`.""" - return 'confirm-{0}@{1}'.format(token, self.email_host) - - def confirm_url(self, token=''): - """See `IDomain`.""" - return urljoin(self.base_url, 'confirm/' + token) - - def __repr__(self): - """repr(a_domain)""" - if self.description is None: - return ('<Domain {0.email_host}, base_url: {0.base_url}, ' - 'contact_address: {0.contact_address}>').format(self) - else: - return ('<Domain {0.email_host}, {0.description}, ' - 'base_url: {0.base_url}, ' - 'contact_address: {0.contact_address}>').format(self) - - - -class DomainManager: - """Domain manager.""" - - implements(IDomainManager) - - def __init__(self, config): - """Create a domain manager. - - :param config: The configuration object. - :type config: `IConfiguration` - """ - self.config = config - self.store = config.db.store - - def add(self, email_host, - description=None, - base_url=None, - contact_address=None): - """See `IDomainManager`.""" - # Be sure the email_host is not already registered. This is probably - # a constraint that should (also) be maintained in the database. - if self.get(email_host) is not None: - raise BadDomainSpecificationError( - 'Duplicate email host: %s' % email_host) - domain = Domain(email_host, description, base_url, contact_address) - self.store.add(domain) - return domain - - def remove(self, email_host): - domain = self[email_host] - self.store.remove(domain) - return domain - - def get(self, email_host, default=None): - """See `IDomainManager`.""" - domains = self.store.find(Domain, email_host=email_host) - if domains.count() < 1: - return default - assert domains.count() == 1, ( - 'Too many matching domains: %s' % email_host) - return domains.one() - - def __getitem__(self, email_host): - """See `IDomainManager`.""" - missing = object() - domain = self.get(email_host, missing) - if domain is missing: - raise KeyError(email_host) - return domain - - def __len__(self): - return self.store.find(Domain).count() - - def __iter__(self): - """See `IDomainManager`.""" - for domain in self.store.find(Domain): - yield domain - - def __contains__(self, email_host): - """See `IDomainManager`.""" - return self.store.find(Domain, email_host=email_host).count() > 0 diff --git a/src/mailman/database/language.py b/src/mailman/database/language.py deleted file mode 100644 index 8adc5c4a5..000000000 --- a/src/mailman/database/language.py +++ /dev/null @@ -1,40 +0,0 @@ -# Copyright (C) 2006-2009 by the Free Software Foundation, Inc. -# -# This file is part of GNU Mailman. -# -# GNU Mailman 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 3 of the License, or (at your option) -# any later version. -# -# GNU Mailman 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 -# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. - -"""Model for languages.""" - -from __future__ import absolute_import, unicode_literals - -__metaclass__ = type -__all__ = [ - 'Language', - ] - - -from storm.locals import * -from zope.interface import implements - -from mailman.database import Model -from mailman.interfaces import ILanguage - - - -class Language(Model): - implements(ILanguage) - - id = Int(primary=True) - code = Unicode() diff --git a/src/mailman/database/listmanager.py b/src/mailman/database/listmanager.py deleted file mode 100644 index 929f00351..000000000 --- a/src/mailman/database/listmanager.py +++ /dev/null @@ -1,97 +0,0 @@ -# Copyright (C) 2007-2009 by the Free Software Foundation, Inc. -# -# This file is part of GNU Mailman. -# -# GNU Mailman 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 3 of the License, or (at your option) -# any later version. -# -# GNU Mailman 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 -# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. - -"""A mailing list manager.""" - -from __future__ import absolute_import, unicode_literals - -__metaclass__ = type -__all__ = [ - 'ListManager', - ] - - -import datetime - -from zope.interface import implements - -from mailman.config import config -from mailman.core.errors import InvalidEmailAddress -from mailman.database.mailinglist import MailingList -from mailman.interfaces.listmanager import IListManager, ListAlreadyExistsError -from mailman.interfaces.rest import IResolvePathNames - - - -class ListManager: - """An implementation of the `IListManager` interface.""" - - implements(IListManager, IResolvePathNames) - - # pylint: disable-msg=R0201 - def create(self, fqdn_listname): - """See `IListManager`.""" - listname, at, hostname = fqdn_listname.partition('@') - if len(hostname) == 0: - raise InvalidEmailAddress(fqdn_listname) - mlist = config.db.store.find( - MailingList, - MailingList.list_name == listname, - MailingList.host_name == hostname).one() - if mlist: - raise ListAlreadyExistsError(fqdn_listname) - mlist = MailingList(fqdn_listname) - mlist.created_at = datetime.datetime.now() - config.db.store.add(mlist) - return mlist - - def get(self, fqdn_listname): - """See `IListManager`.""" - listname, at, hostname = fqdn_listname.partition('@') - mlist = config.db.store.find(MailingList, - list_name=listname, - host_name=hostname).one() - if mlist is not None: - # XXX Fixme - mlist._restore() - return mlist - - def delete(self, mlist): - """See `IListManager`.""" - config.db.store.remove(mlist) - - @property - def mailing_lists(self): - """See `IListManager`.""" - for fqdn_listname in self.names: - yield self.get(fqdn_listname) - - @property - def names(self): - """See `IListManager`.""" - for mlist in config.db.store.find(MailingList): - yield '{0}@{1}'.format(mlist.list_name, mlist.host_name) - - def get_mailing_lists(self): - """See `IListManager`.""" - # lazr.restful will not allow this to be a generator. - return list(self.mailing_lists) - - def new(self, fqdn_listname): - """See `IListManager.""" - from mailman.app.lifecycle import create_list - return create_list(fqdn_listname) diff --git a/src/mailman/database/mailinglist.py b/src/mailman/database/mailinglist.py deleted file mode 100644 index 447d4657a..000000000 --- a/src/mailman/database/mailinglist.py +++ /dev/null @@ -1,465 +0,0 @@ -# Copyright (C) 2006-2009 by the Free Software Foundation, Inc. -# -# This file is part of GNU Mailman. -# -# GNU Mailman 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 3 of the License, or (at your option) -# any later version. -# -# GNU Mailman 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 -# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. - -"""Model for mailing lists.""" - -from __future__ import absolute_import, unicode_literals - -__metaclass__ = type -__all__ = [ - 'MailingList', - ] - - -import os -import string - -from storm.locals import ( - And, Bool, DateTime, Float, Int, Pickle, Reference, Store, TimeDelta, - Unicode) -from urlparse import urljoin -from zope.interface import implements - -from mailman.config import config -from mailman.database import roster -from mailman.database.digests import OneLastDigest -from mailman.database.mime import ContentFilter -from mailman.database.model import Model -from mailman.database.types import Enum -from mailman.interfaces.domain import IDomainManager -from mailman.interfaces.mailinglist import ( - IAcceptableAlias, IAcceptableAliasSet, IMailingList, Personalization) -from mailman.interfaces.mime import FilterType -from mailman.utilities.filesystem import makedirs -from mailman.utilities.string import expand - - -SPACE = ' ' -UNDERSCORE = '_' - - - -class MailingList(Model): - implements(IMailingList) - - id = Int(primary=True) - - # List identity - list_name = Unicode() - host_name = Unicode() - list_id = Unicode() - include_list_post_header = Bool() - include_rfc2369_headers = Bool() - # Attributes not directly modifiable via the web u/i - created_at = DateTime() - admin_member_chunksize = Int() - # Attributes which are directly modifiable via the web u/i. The more - # complicated attributes are currently stored as pickles, though that - # will change as the schema and implementation is developed. - next_request_id = Int() - next_digest_number = Int() - digest_last_sent_at = DateTime() - volume = Int() - last_post_time = DateTime() - # Implicit destination. - acceptable_aliases_id = Int() - acceptable_alias = Reference(acceptable_aliases_id, 'AcceptableAlias.id') - # Attributes which are directly modifiable via the web u/i. The more - # complicated attributes are currently stored as pickles, though that - # will change as the schema and implementation is developed. - accept_these_nonmembers = Pickle() - admin_immed_notify = Bool() - admin_notify_mchanges = Bool() - administrivia = Bool() - advertised = Bool() - anonymous_list = Bool() - archive = Bool() - archive_private = Bool() - archive_volume_frequency = Int() - # Automatic responses. - autoresponse_grace_period = TimeDelta() - autorespond_owner = Enum() - autoresponse_owner_text = Unicode() - autorespond_postings = Enum() - autoresponse_postings_text = Unicode() - autorespond_requests = Enum() - autoresponse_request_text = Unicode() - # Content filters. - filter_content = Bool() - collapse_alternatives = Bool() - convert_html_to_plaintext = Bool() - # Bounces and bans. - ban_list = Pickle() - bounce_info_stale_after = TimeDelta() - bounce_matching_headers = Unicode() - bounce_notify_owner_on_disable = Bool() - bounce_notify_owner_on_removal = Bool() - bounce_processing = Bool() - bounce_score_threshold = Int() - bounce_unrecognized_goes_to_list_owner = Bool() - bounce_you_are_disabled_warnings = Int() - bounce_you_are_disabled_warnings_interval = TimeDelta() - default_member_moderation = Bool() - description = Unicode() - digest_footer = Unicode() - digest_header = Unicode() - digest_is_default = Bool() - digest_send_periodic = Bool() - digest_size_threshold = Float() - digest_volume_frequency = Enum() - digestable = Bool() - discard_these_nonmembers = Pickle() - emergency = Bool() - encode_ascii_prefixes = Bool() - first_strip_reply_to = Bool() - forward_auto_discards = Bool() - gateway_to_mail = Bool() - gateway_to_news = Bool() - generic_nonmember_action = Int() - goodbye_msg = Unicode() - header_matches = Pickle() - hold_these_nonmembers = Pickle() - info = Unicode() - linked_newsgroup = Unicode() - max_days_to_hold = Int() - max_message_size = Int() - max_num_recipients = Int() - member_moderation_action = Enum() - member_moderation_notice = Unicode() - mime_is_default_digest = Bool() - moderator_password = Unicode() - msg_footer = Unicode() - msg_header = Unicode() - new_member_options = Int() - news_moderation = Enum() - news_prefix_subject_too = Bool() - nntp_host = Unicode() - nondigestable = Bool() - nonmember_rejection_notice = Unicode() - obscure_addresses = Bool() - personalize = Enum() - pipeline = Unicode() - post_id = Int() - _preferred_language = Unicode(name='preferred_language') - private_roster = Bool() - real_name = Unicode() - reject_these_nonmembers = Pickle() - reply_goes_to_list = Enum() - reply_to_address = Unicode() - require_explicit_destination = Bool() - respond_to_post_requests = Bool() - scrub_nondigest = Bool() - send_goodbye_msg = Bool() - send_reminders = Bool() - send_welcome_msg = Bool() - start_chain = Unicode() - subject_prefix = Unicode() - subscribe_auto_approval = Pickle() - subscribe_policy = Int() - topics = Pickle() - topics_bodylines_limit = Int() - topics_enabled = Bool() - unsubscribe_policy = Int() - welcome_msg = Unicode() - - def __init__(self, fqdn_listname): - super(MailingList, self).__init__() - listname, at, hostname = fqdn_listname.partition('@') - assert hostname, 'Bad list name: {0}'.format(fqdn_listname) - self.list_name = listname - self.host_name = hostname - # For the pending database - self.next_request_id = 1 - self._restore() - self.personalization = Personalization.none - self.real_name = string.capwords( - SPACE.join(listname.split(UNDERSCORE))) - makedirs(self.data_path) - - # XXX FIXME - def _restore(self): - self.owners = roster.OwnerRoster(self) - self.moderators = roster.ModeratorRoster(self) - self.administrators = roster.AdministratorRoster(self) - self.members = roster.MemberRoster(self) - self.regular_members = roster.RegularMemberRoster(self) - self.digest_members = roster.DigestMemberRoster(self) - self.subscribers = roster.Subscribers(self) - - def __repr__(self): - return '<mailing list "{0}" at {1:#x}>'.format( - self.fqdn_listname, id(self)) - - @property - def fqdn_listname(self): - """See `IMailingList`.""" - return '{0}@{1}'.format(self.list_name, self.host_name) - - @property - def web_host(self): - """See `IMailingList`.""" - return IDomainManager(config)[self.host_name] - - def script_url(self, target, context=None): - """See `IMailingList`.""" - # Find the domain for this mailing list. - domain = IDomainManager(config)[self.host_name] - # XXX Handle the case for when context is not None; those would be - # relative URLs. - return urljoin(domain.base_url, target + '/' + self.fqdn_listname) - - @property - def data_path(self): - """See `IMailingList`.""" - return os.path.join(config.LIST_DATA_DIR, self.fqdn_listname) - - # IMailingListAddresses - - @property - def posting_address(self): - """See `IMailingList`.""" - return self.fqdn_listname - - @property - def no_reply_address(self): - """See `IMailingList`.""" - return '{0}@{1}'.format(config.mailman.noreply_address, self.host_name) - - @property - def owner_address(self): - """See `IMailingList`.""" - return '{0}-owner@{1}'.format(self.list_name, self.host_name) - - @property - def request_address(self): - """See `IMailingList`.""" - return '{0}-request@{1}'.format(self.list_name, self.host_name) - - @property - def bounces_address(self): - """See `IMailingList`.""" - return '{0}-bounces@{1}'.format(self.list_name, self.host_name) - - @property - def join_address(self): - """See `IMailingList`.""" - return '{0}-join@{1}'.format(self.list_name, self.host_name) - - @property - def leave_address(self): - """See `IMailingList`.""" - return '{0}-leave@{1}'.format(self.list_name, self.host_name) - - @property - def subscribe_address(self): - """See `IMailingList`.""" - return '{0}-subscribe@{1}'.format(self.list_name, self.host_name) - - @property - def unsubscribe_address(self): - """See `IMailingList`.""" - return '{0}-unsubscribe@{1}'.format(self.list_name, self.host_name) - - def confirm_address(self, cookie): - """See `IMailingList`.""" - local_part = expand(config.mta.verp_confirm_format, dict( - address = '{0}-confirm'.format(self.list_name), - cookie = cookie)) - return '{0}@{1}'.format(local_part, self.host_name) - - @property - def preferred_language(self): - """See `IMailingList`.""" - return config.languages[self._preferred_language] - - @preferred_language.setter - def preferred_language(self, language): - """See `IMailingList`.""" - # Accept both a language code and a `Language` instance. - try: - self._preferred_language = language.code - except AttributeError: - self._preferred_language = language - - def send_one_last_digest_to(self, address, delivery_mode): - """See `IMailingList`.""" - digest = OneLastDigest(self, address, delivery_mode) - Store.of(self).add(digest) - - @property - def last_digest_recipients(self): - """See `IMailingList`.""" - results = Store.of(self).find( - OneLastDigest, - OneLastDigest.mailing_list == self) - recipients = [(digest.address, digest.delivery_mode) - for digest in results] - results.remove() - return recipients - - @property - def filter_types(self): - """See `IMailingList`.""" - results = Store.of(self).find( - ContentFilter, - And(ContentFilter.mailing_list == self, - ContentFilter.filter_type == FilterType.filter_mime)) - for content_filter in results: - yield content_filter.filter_pattern - - @filter_types.setter - def filter_types(self, sequence): - """See `IMailingList`.""" - # First, delete all existing MIME type filter patterns. - store = Store.of(self) - results = store.find( - ContentFilter, - And(ContentFilter.mailing_list == self, - ContentFilter.filter_type == FilterType.filter_mime)) - results.remove() - # Now add all the new filter types. - for mime_type in sequence: - content_filter = ContentFilter( - self, mime_type, FilterType.filter_mime) - store.add(content_filter) - - @property - def pass_types(self): - """See `IMailingList`.""" - results = Store.of(self).find( - ContentFilter, - And(ContentFilter.mailing_list == self, - ContentFilter.filter_type == FilterType.pass_mime)) - for content_filter in results: - yield content_filter.filter_pattern - - @pass_types.setter - def pass_types(self, sequence): - """See `IMailingList`.""" - # First, delete all existing MIME type pass patterns. - store = Store.of(self) - results = store.find( - ContentFilter, - And(ContentFilter.mailing_list == self, - ContentFilter.filter_type == FilterType.pass_mime)) - results.remove() - # Now add all the new filter types. - for mime_type in sequence: - content_filter = ContentFilter( - self, mime_type, FilterType.pass_mime) - store.add(content_filter) - - @property - def filter_extensions(self): - """See `IMailingList`.""" - results = Store.of(self).find( - ContentFilter, - And(ContentFilter.mailing_list == self, - ContentFilter.filter_type == FilterType.filter_extension)) - for content_filter in results: - yield content_filter.filter_pattern - - @filter_extensions.setter - def filter_extensions(self, sequence): - """See `IMailingList`.""" - # First, delete all existing file extensions filter patterns. - store = Store.of(self) - results = store.find( - ContentFilter, - And(ContentFilter.mailing_list == self, - ContentFilter.filter_type == FilterType.filter_extension)) - results.remove() - # Now add all the new filter types. - for mime_type in sequence: - content_filter = ContentFilter( - self, mime_type, FilterType.filter_extension) - store.add(content_filter) - - @property - def pass_extensions(self): - """See `IMailingList`.""" - results = Store.of(self).find( - ContentFilter, - And(ContentFilter.mailing_list == self, - ContentFilter.filter_type == FilterType.pass_extension)) - for content_filter in results: - yield content_filter.pass_pattern - - @pass_extensions.setter - def pass_extensions(self, sequence): - """See `IMailingList`.""" - # First, delete all existing file extensions pass patterns. - store = Store.of(self) - results = store.find( - ContentFilter, - And(ContentFilter.mailing_list == self, - ContentFilter.filter_type == FilterType.pass_extension)) - results.remove() - # Now add all the new filter types. - for mime_type in sequence: - content_filter = ContentFilter( - self, mime_type, FilterType.pass_extension) - store.add(content_filter) - - - -class AcceptableAlias(Model): - implements(IAcceptableAlias) - - id = Int(primary=True) - - mailing_list_id = Int() - mailing_list = Reference(mailing_list_id, MailingList.id) - - alias = Unicode() - - def __init__(self, mailing_list, alias): - self.mailing_list = mailing_list - self.alias = alias - - -class AcceptableAliasSet: - implements(IAcceptableAliasSet) - - def __init__(self, mailing_list): - self._mailing_list = mailing_list - - def clear(self): - """See `IAcceptableAliasSet`.""" - Store.of(self._mailing_list).find( - AcceptableAlias, - AcceptableAlias.mailing_list == self._mailing_list).remove() - - def add(self, alias): - if not (alias.startswith('^') or '@' in alias): - raise ValueError(alias) - alias = AcceptableAlias(self._mailing_list, alias.lower()) - Store.of(self._mailing_list).add(alias) - - def remove(self, alias): - Store.of(self._mailing_list).find( - AcceptableAlias, - And(AcceptableAlias.mailing_list == self._mailing_list, - AcceptableAlias.alias == alias.lower())).remove() - - @property - def aliases(self): - aliases = Store.of(self._mailing_list).find( - AcceptableAlias, - AcceptableAlias.mailing_list == self._mailing_list) - for alias in aliases: - yield alias.alias diff --git a/src/mailman/database/member.py b/src/mailman/database/member.py deleted file mode 100644 index 4a158a11e..000000000 --- a/src/mailman/database/member.py +++ /dev/null @@ -1,105 +0,0 @@ -# Copyright (C) 2007-2009 by the Free Software Foundation, Inc. -# -# This file is part of GNU Mailman. -# -# GNU Mailman 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 3 of the License, or (at your option) -# any later version. -# -# GNU Mailman 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 -# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. - -"""Model for members.""" - -from __future__ import absolute_import, unicode_literals - -__metaclass__ = type -__all__ = [ - 'Member', - ] - -from storm.locals import * -from zope.interface import implements - -from mailman.config import config -from mailman.constants import system_preferences -from mailman.database.model import Model -from mailman.database.types import Enum -from mailman.interfaces.member import IMember - - - -class Member(Model): - implements(IMember) - - id = Int(primary=True) - role = Enum() - mailing_list = Unicode() - is_moderated = Bool() - - address_id = Int() - address = Reference(address_id, 'Address.id') - preferences_id = Int() - preferences = Reference(preferences_id, 'Preferences.id') - - def __init__(self, role, mailing_list, address): - self.role = role - self.mailing_list = mailing_list - self.address = address - self.is_moderated = False - - def __repr__(self): - return '<Member: {0} on {1} as {2}>'.format( - self.address, self.mailing_list, self.role) - - def _lookup(self, preference): - pref = getattr(self.preferences, preference) - if pref is not None: - return pref - pref = getattr(self.address.preferences, preference) - if pref is not None: - return pref - if self.address.user: - pref = getattr(self.address.user.preferences, preference) - if pref is not None: - return pref - return getattr(system_preferences, preference) - - @property - def acknowledge_posts(self): - return self._lookup('acknowledge_posts') - - @property - def preferred_language(self): - return self._lookup('preferred_language') - - @property - def receive_list_copy(self): - return self._lookup('receive_list_copy') - - @property - def receive_own_postings(self): - return self._lookup('receive_own_postings') - - @property - def delivery_mode(self): - return self._lookup('delivery_mode') - - @property - def delivery_status(self): - return self._lookup('delivery_status') - - @property - def options_url(self): - # XXX Um, this is definitely wrong - return 'http://example.com/' + self.address.address - - def unsubscribe(self): - config.db.store.remove(self.preferences) - config.db.store.remove(self) diff --git a/src/mailman/database/message.py b/src/mailman/database/message.py deleted file mode 100644 index e77e11429..000000000 --- a/src/mailman/database/message.py +++ /dev/null @@ -1,53 +0,0 @@ -# Copyright (C) 2007-2009 by the Free Software Foundation, Inc. -# -# This file is part of GNU Mailman. -# -# GNU Mailman 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 3 of the License, or (at your option) -# any later version. -# -# GNU Mailman 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 -# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. - -"""Model for messages.""" - - -from __future__ import absolute_import, unicode_literals - -__metaclass__ = type -__all__ = [ - 'Message', - ] - -from storm.locals import * -from zope.interface import implements - -from mailman.config import config -from mailman.database.model import Model -from mailman.interfaces.messages import IMessage - - - -class Message(Model): - """A message in the message store.""" - - implements(IMessage) - - id = Int(primary=True, default=AutoReload) - message_id = Unicode() - message_id_hash = RawStr() - path = RawStr() - # This is a Messge-ID field representation, not a database row id. - - def __init__(self, message_id, message_id_hash, path): - super(Message, self).__init__() - self.message_id = message_id - self.message_id_hash = message_id_hash - self.path = path - config.db.store.add(self) diff --git a/src/mailman/database/messagestore.py b/src/mailman/database/messagestore.py deleted file mode 100644 index a129f47ec..000000000 --- a/src/mailman/database/messagestore.py +++ /dev/null @@ -1,137 +0,0 @@ -# Copyright (C) 2007-2009 by the Free Software Foundation, Inc. -# -# This file is part of GNU Mailman. -# -# GNU Mailman 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 3 of the License, or (at your option) -# any later version. -# -# GNU Mailman 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 -# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. - -"""Model for message stores.""" - - -from __future__ import absolute_import, unicode_literals - -__metaclass__ = type -__all__ = [ - 'MessageStore', - ] - -import os -import errno -import base64 -import hashlib -import cPickle as pickle - -from zope.interface import implements - -from mailman.config import config -from mailman.database.message import Message -from mailman.interfaces.messages import IMessageStore -from mailman.utilities.filesystem import makedirs - - -# It could be very bad if you have already stored files and you change this -# value. We'd need a script to reshuffle and resplit. -MAX_SPLITS = 2 -EMPTYSTRING = '' - - - -class MessageStore: - implements(IMessageStore) - - def add(self, message): - # Ensure that the message has the requisite headers. - message_ids = message.get_all('message-id', []) - if len(message_ids) <> 1: - raise ValueError('Exactly one Message-ID header required') - # Calculate and insert the X-Message-ID-Hash. - message_id = message_ids[0] - # Complain if the Message-ID already exists in the storage. - existing = config.db.store.find(Message, - Message.message_id == message_id).one() - if existing is not None: - raise ValueError( - 'Message ID already exists in message store: {0}'.format( - message_id)) - shaobj = hashlib.sha1(message_id) - hash32 = base64.b32encode(shaobj.digest()) - del message['X-Message-ID-Hash'] - message['X-Message-ID-Hash'] = hash32 - # Calculate the path on disk where we're going to store this message - # object, in pickled format. - parts = [] - split = list(hash32) - while split and len(parts) < MAX_SPLITS: - parts.append(split.pop(0) + split.pop(0)) - parts.append(hash32) - relpath = os.path.join(*parts) - # Store the message in the database. This relies on the database - # providing a unique serial number, but to get this information, we - # have to use a straight insert instead of relying on Elixir to create - # the object. - row = Message(message_id=message_id, - message_id_hash=hash32, - path=relpath) - # Now calculate the full file system path. - path = os.path.join(config.MESSAGES_DIR, relpath) - # Write the file to the path, but catch the appropriate exception in - # case the parent directories don't yet exist. In that case, create - # them and try again. - while True: - try: - with open(path, 'w') as fp: - # -1 says to use the highest protocol available. - pickle.dump(message, fp, -1) - break - except IOError as error: - if error.errno <> errno.ENOENT: - raise - makedirs(os.path.dirname(path)) - return hash32 - - def _get_message(self, row): - path = os.path.join(config.MESSAGES_DIR, row.path) - with open(path) as fp: - return pickle.load(fp) - - def get_message_by_id(self, message_id): - row = config.db.store.find(Message, message_id=message_id).one() - if row is None: - return None - return self._get_message(row) - - def get_message_by_hash(self, message_id_hash): - # It's possible the hash came from a message header, in which case it - # will be a Unicode. However when coming from source code, it may be - # an 8-string. Coerce to the latter if necessary; it must be - # US-ASCII. - if isinstance(message_id_hash, unicode): - message_id_hash = message_id_hash.encode('ascii') - row = config.db.store.find(Message, - message_id_hash=message_id_hash).one() - if row is None: - return None - return self._get_message(row) - - @property - def messages(self): - for row in config.db.store.find(Message): - yield self._get_message(row) - - def delete_message(self, message_id): - row = config.db.store.find(Message, message_id=message_id).one() - if row is None: - raise LookupError(message_id) - path = os.path.join(config.MESSAGES_DIR, row.path) - os.remove(path) - config.db.store.remove(row) diff --git a/src/mailman/database/mime.py b/src/mailman/database/mime.py deleted file mode 100644 index 31c2aacbe..000000000 --- a/src/mailman/database/mime.py +++ /dev/null @@ -1,52 +0,0 @@ -# Copyright (C) 2009 by the Free Software Foundation, Inc. -# -# This file is part of GNU Mailman. -# -# GNU Mailman 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 3 of the License, or (at your option) -# any later version. -# -# GNU Mailman 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 -# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. - -"""Module stuff.""" - -from __future__ import absolute_import, unicode_literals - -__metaclass__ = type -__all__ = [ - 'ContentFilter' - ] - - -from storm.locals import Bool, Int, Reference, Unicode -from zope.interface import implements - -from mailman.database.model import Model -from mailman.database.types import Enum -from mailman.interfaces.mime import IContentFilter - - - -class ContentFilter(Model): - """A single filter criteria.""" - implements(IContentFilter) - - id = Int(primary=True) - - mailing_list_id = Int() - mailing_list = Reference(mailing_list_id, 'MailingList.id') - - filter_type = Enum() - filter_pattern = Unicode() - - def __init__(self, mailing_list, filter_pattern, filter_type): - self.mailing_list = mailing_list - self.filter_pattern = filter_pattern - self.filter_type = filter_type diff --git a/src/mailman/database/pending.py b/src/mailman/database/pending.py deleted file mode 100644 index 2a0e5d09e..000000000 --- a/src/mailman/database/pending.py +++ /dev/null @@ -1,175 +0,0 @@ -# Copyright (C) 2007-2009 by the Free Software Foundation, Inc. -# -# This file is part of GNU Mailman. -# -# GNU Mailman 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 3 of the License, or (at your option) -# any later version. -# -# GNU Mailman 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 -# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. - -"""Implementations of the IPendable and IPending interfaces.""" - -from __future__ import absolute_import, unicode_literals - -__metaclass__ = type -__all__ = [ - 'Pended', - 'Pendings', - ] - -import sys -import time -import random -import hashlib -import datetime - -from lazr.config import as_timedelta -from storm.locals import * -from zope.interface import implements -from zope.interface.verify import verifyObject - -from mailman.config import config -from mailman.database.model import Model -from mailman.interfaces.pending import ( - IPendable, IPended, IPendedKeyValue, IPendings) -from mailman.utilities.modules import call_name - - - -class PendedKeyValue(Model): - """A pended key/value pair, tied to a token.""" - - implements(IPendedKeyValue) - - def __init__(self, key, value): - self.key = key - self.value = value - - id = Int(primary=True) - key = Unicode() - value = Unicode() - pended_id = Int() - - -class Pended(Model): - """A pended event, tied to a token.""" - - implements(IPended) - - def __init__(self, token, expiration_date): - super(Pended, self).__init__() - self.token = token - self.expiration_date = expiration_date - - id = Int(primary=True) - token = RawStr() - expiration_date = DateTime() - key_values = ReferenceSet(id, PendedKeyValue.pended_id) - - - -class UnpendedPendable(dict): - implements(IPendable) - - - -class Pendings: - """Implementation of the IPending interface.""" - - implements(IPendings) - - def add(self, pendable, lifetime=None): - verifyObject(IPendable, pendable) - # Calculate the token and the lifetime. - if lifetime is None: - lifetime = as_timedelta(config.mailman.pending_request_life) - # Calculate a unique token. 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. - for attempts in range(3): - now = time.time() - x = random.random() + now % 1.0 + time.clock() % 1.0 - # Use sha1 because it produces shorter strings. - token = hashlib.sha1(repr(x)).hexdigest() - # In practice, we'll never get a duplicate, but we'll be anal - # about checking anyway. - if config.db.store.find(Pended, token=token).count() == 0: - break - else: - raise AssertionError('Could not find a valid pendings token') - # Create the record, and then the individual key/value pairs. - pending = Pended( - token=token, - expiration_date=datetime.datetime.now() + lifetime) - for key, value in pendable.items(): - if isinstance(key, str): - key = unicode(key, 'utf-8') - if isinstance(value, str): - value = unicode(value, 'utf-8') - elif type(value) is int: - value = '__builtin__.int\1%s' % value - elif type(value) is float: - value = '__builtin__.float\1%s' % value - elif type(value) is bool: - value = '__builtin__.bool\1%s' % value - elif type(value) is list: - # We expect this to be a list of strings. - value = ('mailman.database.pending.unpack_list\1' + - '\2'.join(value)) - keyval = PendedKeyValue(key=key, value=value) - pending.key_values.add(keyval) - config.db.store.add(pending) - return token - - def confirm(self, token, expunge=True): - store = config.db.store - pendings = store.find(Pended, token=token) - if pendings.count() == 0: - return None - assert pendings.count() == 1, ( - 'Unexpected token count: {0}'.format(pendings.count())) - pending = pendings[0] - pendable = UnpendedPendable() - # Find all PendedKeyValue entries that are associated with the pending - # object's ID. Watch out for type conversions. - for keyvalue in store.find(PendedKeyValue, - PendedKeyValue.pended_id == pending.id): - if keyvalue.value is not None and '\1' in keyvalue.value: - type_name, value = keyvalue.value.split('\1', 1) - pendable[keyvalue.key] = call_name(type_name, value) - else: - pendable[keyvalue.key] = keyvalue.value - if expunge: - store.remove(keyvalue) - if expunge: - store.remove(pending) - return pendable - - def evict(self): - store = config.db.store - now = datetime.datetime.now() - for pending in store.find(Pended): - if pending.expiration_date < now: - # Find all PendedKeyValue entries that are associated with the - # pending object's ID. - q = store.find(PendedKeyValue, - PendedKeyValue.pended_id == pending.id) - for keyvalue in q: - store.remove(keyvalue) - store.remove(pending) - - - -def unpack_list(value): - return value.split('\2') diff --git a/src/mailman/database/preferences.py b/src/mailman/database/preferences.py deleted file mode 100644 index 31f9ce280..000000000 --- a/src/mailman/database/preferences.py +++ /dev/null @@ -1,67 +0,0 @@ -# Copyright (C) 2006-2009 by the Free Software Foundation, Inc. -# -# This file is part of GNU Mailman. -# -# GNU Mailman 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 3 of the License, or (at your option) -# any later version. -# -# GNU Mailman 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 -# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. - -"""Model for preferences.""" - -from __future__ import absolute_import, unicode_literals - -__metaclass__ = type -__all__ = [ - 'Preferences', - ] - - -from storm.locals import * -from zope.interface import implements - -from mailman.config import config -from mailman.database.model import Model -from mailman.database.types import Enum -from mailman.interfaces.preferences import IPreferences - - - -class Preferences(Model): - implements(IPreferences) - - id = Int(primary=True) - acknowledge_posts = Bool() - hide_address = Bool() - _preferred_language = Unicode(name='preferred_language') - receive_list_copy = Bool() - receive_own_postings = Bool() - delivery_mode = Enum() - delivery_status = Enum() - - def __repr__(self): - return '<Preferences object at {0:#x}>'.format(id(self)) - - @property - def preferred_language(self): - if self._preferred_language is None: - return None - return config.languages[self._preferred_language] - - @preferred_language.setter - def preferred_language(self, language): - if language is None: - self._preferred_language = None - # Accept both a language code and a `Language` instance. - try: - self._preferred_language = language.code - except AttributeError: - self._preferred_language = language diff --git a/src/mailman/database/requests.py b/src/mailman/database/requests.py deleted file mode 100644 index 538b97adb..000000000 --- a/src/mailman/database/requests.py +++ /dev/null @@ -1,140 +0,0 @@ -# Copyright (C) 2007-2009 by the Free Software Foundation, Inc. -# -# This file is part of GNU Mailman. -# -# GNU Mailman 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 3 of the License, or (at your option) -# any later version. -# -# GNU Mailman 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 -# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. - -"""Implementations of the IRequests and IListRequests interfaces.""" - -from __future__ import absolute_import, unicode_literals - -__metaclass__ = type -__all__ = [ - 'Requests', - ] - - -from datetime import timedelta -from storm.locals import * -from zope.component import getUtility -from zope.interface import implements - -from mailman.config import config -from mailman.database.model import Model -from mailman.database.types import Enum -from mailman.interfaces.pending import IPendable, IPendings -from mailman.interfaces.requests import IListRequests, IRequests, RequestType - - - -class DataPendable(dict): - implements(IPendable) - - - -class ListRequests: - implements(IListRequests) - - def __init__(self, mailing_list): - self.mailing_list = mailing_list - - @property - def count(self): - return config.db.store.find( - _Request, mailing_list=self.mailing_list).count() - - def count_of(self, request_type): - return config.db.store.find( - _Request, - mailing_list=self.mailing_list, request_type=request_type).count() - - @property - def held_requests(self): - results = config.db.store.find( - _Request, mailing_list=self.mailing_list) - for request in results: - yield request - - def of_type(self, request_type): - results = config.db.store.find( - _Request, - mailing_list=self.mailing_list, request_type=request_type) - for request in results: - yield request - - def hold_request(self, request_type, key, data=None): - if request_type not in RequestType: - raise TypeError(request_type) - if data is None: - data_hash = None - else: - # We're abusing the pending database as a way of storing arbitrary - # key/value pairs, where both are strings. This isn't ideal but - # it lets us get auxiliary data almost for free. We may need to - # lock this down more later. - pendable = DataPendable() - pendable.update(data) - token = getUtility(IPendings).add(pendable, timedelta(days=5000)) - data_hash = token - request = _Request(key, request_type, self.mailing_list, data_hash) - config.db.store.add(request) - return request.id - - def get_request(self, request_id): - result = config.db.store.get(_Request, request_id) - if result is None: - return None - if result.data_hash is None: - return result.key, result.data_hash - pendable = getUtility(IPendings).confirm( - result.data_hash, expunge=False) - data = dict() - data.update(pendable) - return result.key, data - - def delete_request(self, request_id): - request = config.db.store.get(_Request, request_id) - if request is None: - raise KeyError(request_id) - # Throw away the pended data. - getUtility(IPendings).confirm(request.data_hash) - config.db.store.remove(request) - - - -class Requests: - implements(IRequests) - - def get_list_requests(self, mailing_list): - return ListRequests(mailing_list) - - - -class _Request(Model): - """Table for mailing list hold requests.""" - - id = Int(primary=True, default=AutoReload) - key = Unicode() - request_type = Enum() - data_hash = RawStr() - - mailing_list_id = Int() - mailing_list = Reference(mailing_list_id, 'MailingList.id') - - def __init__(self, key, request_type, mailing_list, data_hash): - super(_Request, self).__init__() - self.key = key - self.request_type = request_type - self.mailing_list = mailing_list - self.data_hash = data_hash diff --git a/src/mailman/database/roster.py b/src/mailman/database/roster.py deleted file mode 100644 index 3b8ba4c4c..000000000 --- a/src/mailman/database/roster.py +++ /dev/null @@ -1,271 +0,0 @@ -# Copyright (C) 2007-2009 by the Free Software Foundation, Inc. -# -# This file is part of GNU Mailman. -# -# GNU Mailman 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 3 of the License, or (at your option) -# any later version. -# -# GNU Mailman 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 -# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. - -"""An implementation of an IRoster. - -These are hard-coded rosters which know how to filter a set of members to find -the ones that fit a particular role. These are used as the member, owner, -moderator, and administrator roster filters. -""" - -from __future__ import absolute_import, unicode_literals - -__metaclass__ = type -__all__ = [ - 'AdministratorRoster', - 'DigestMemberRoster', - 'MemberRoster', - 'Memberships', - 'ModeratorRoster', - 'OwnerRoster', - 'RegularMemberRoster', - 'Subscribers', - ] - - -from storm.expr import And, LeftJoin, Or -from zope.interface import implements - -from mailman.config import config -from mailman.database.address import Address -from mailman.database.member import Member -from mailman.database.preferences import Preferences -from mailman.interfaces.member import DeliveryMode, MemberRole -from mailman.interfaces.roster import IRoster - - - -class AbstractRoster: - """An abstract IRoster class. - - This class takes the simple approach of implemented the 'users' and - 'addresses' properties in terms of the 'members' property. This may not - be the most efficient way, but it works. - - This requires that subclasses implement the 'members' property. - """ - implements(IRoster) - - role = None - - def __init__(self, mlist): - self._mlist = mlist - - @property - def members(self): - for member in config.db.store.find( - Member, - mailing_list=self._mlist.fqdn_listname, - role=self.role): - yield member - - @property - def users(self): - # Members are linked to addresses, which in turn are linked to users. - # So while the 'members' attribute does most of the work, we have to - # keep a set of unique users. It's possible for the same user to be - # subscribed to a mailing list multiple times with different - # addresses. - users = set(member.address.user for member in self.members) - for user in users: - yield user - - @property - def addresses(self): - # Every Member is linked to exactly one address so the 'members' - # attribute does most of the work. - for member in self.members: - yield member.address - - def get_member(self, address): - results = config.db.store.find( - Member, - Member.mailing_list == self._mlist.fqdn_listname, - Member.role == self.role, - Address.address == address, - Member.address_id == Address.id) - if results.count() == 0: - return None - elif results.count() == 1: - return results[0] - else: - raise AssertionError( - 'Too many matching member results: {0}'.format( - results.count())) - - - -class MemberRoster(AbstractRoster): - """Return all the members of a list.""" - - name = 'member' - role = MemberRole.member - - - -class OwnerRoster(AbstractRoster): - """Return all the owners of a list.""" - - name = 'owner' - role = MemberRole.owner - - - -class ModeratorRoster(AbstractRoster): - """Return all the owners of a list.""" - - name = 'moderator' - role = MemberRole.moderator - - - -class AdministratorRoster(AbstractRoster): - """Return all the administrators of a list.""" - - name = 'administrator' - - @property - def members(self): - # Administrators are defined as the union of the owners and the - # moderators. - members = config.db.store.find( - Member, - Member.mailing_list == self._mlist.fqdn_listname, - Or(Member.role == MemberRole.owner, - Member.role == MemberRole.moderator)) - for member in members: - yield member - - def get_member(self, address): - results = config.db.store.find( - Member, - Member.mailing_list == self._mlist.fqdn_listname, - Or(Member.role == MemberRole.moderator, - Member.role == MemberRole.owner), - Address.address == address, - Member.address_id == Address.id) - if results.count() == 0: - return None - elif results.count() == 1: - return results[0] - else: - raise AssertionError( - 'Too many matching member results: {0}'.format(results)) - - - -class DeliveryMemberRoster(AbstractRoster): - """Return all the members having a particular kind of delivery.""" - - def _get_members(self, *delivery_modes): - """The set of members for a mailing list, filter by delivery mode. - - :param delivery_modes: The modes to filter on. - :type delivery_modes: sequence of `DeliveryMode`. - :return: A generator of members. - :rtype: generator - """ - results = config.db.store.find( - Member, - And(Member.mailing_list == self._mlist.fqdn_listname, - Member.role == MemberRole.member)) - for member in results: - if member.delivery_mode in delivery_modes: - yield member - - -class RegularMemberRoster(DeliveryMemberRoster): - """Return all the regular delivery members of a list.""" - - name = 'regular_members' - - @property - def members(self): - for member in self._get_members(DeliveryMode.regular): - yield member - - - -class DigestMemberRoster(DeliveryMemberRoster): - """Return all the regular delivery members of a list.""" - - name = 'digest_members' - - @property - def members(self): - for member in self._get_members(DeliveryMode.plaintext_digests, - DeliveryMode.mime_digests, - DeliveryMode.summary_digests): - yield member - - - -class Subscribers(AbstractRoster): - """Return all subscribed members regardless of their role.""" - - name = 'subscribers' - - @property - def members(self): - for member in config.db.store.find( - Member, - mailing_list=self._mlist.fqdn_listname): - yield member - - - -class Memberships: - """A roster of a single user's memberships.""" - - implements(IRoster) - - name = 'memberships' - - def __init__(self, user): - self._user = user - - @property - def members(self): - results = config.db.store.find( - Member, - Address.user_id == self._user.id, - Member.address_id == Address.id) - for member in results: - yield member - - @property - def users(self): - yield self._user - - @property - def addresses(self): - for address in self._user.addresses: - yield address - - def get_member(self, address): - results = config.db.store.find( - Member, - Member.address_id == Address.id, - Address.user_id == self._user.id) - if results.count() == 0: - return None - elif results.count() == 1: - return results[0] - else: - raise AssertionError( - 'Too many matching member results: {0}'.format( - results.count())) diff --git a/src/mailman/database/stock.py b/src/mailman/database/stock.py new file mode 100644 index 000000000..96d95fda4 --- /dev/null +++ b/src/mailman/database/stock.py @@ -0,0 +1,144 @@ +# Copyright (C) 2006-2009 by the Free Software Foundation, Inc. +# +# This file is part of GNU Mailman. +# +# GNU Mailman 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 3 of the License, or (at your option) +# any later version. +# +# GNU Mailman 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 +# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, unicode_literals + +__metaclass__ = type +__all__ = [ + 'StockDatabase', + ] + +import os +import logging + +from locknix.lockfile import Lock +from lazr.config import as_boolean +from pkg_resources import resource_string +from storm.cache import GenerationalCache +from storm.locals import create_database, Store +from urlparse import urlparse +from zope.interface import implements + +import mailman.version + +from mailman.config import config +from mailman.interfaces.database import IDatabase, SchemaVersionMismatchError +from mailman.model.messagestore import MessageStore +from mailman.model.pending import Pendings +from mailman.model.requests import Requests +from mailman.model.version import Version +from mailman.utilities.string import expand + +log = logging.getLogger('mailman.config') + + + +class StockDatabase: + """The standard database, using Storm on top of SQLite.""" + + implements(IDatabase) + + def __init__(self): + self.url = None + self.store = None + + def initialize(self, debug=None): + """See `IDatabase`.""" + # Serialize this so we don't get multiple processes trying to create + # the database at the same time. + with Lock(os.path.join(config.LOCK_DIR, 'dbcreate.lck')): + self._create(debug) + + def begin(self): + """See `IDatabase`.""" + # Storm takes care of this for us. + pass + + def commit(self): + """See `IDatabase`.""" + self.store.commit() + + def abort(self): + """See `IDatabase`.""" + self.store.rollback() + + def _create(self, debug): + # Calculate the engine url. + url = expand(config.database.url, config.paths) + log.debug('Database url: %s', url) + # XXX By design of SQLite, database file creation does not honor + # umask. See their ticket #1193: + # http://www.sqlite.org/cvstrac/tktview?tn=1193,31 + # + # This sucks for us because the mailman.db file /must/ be group + # writable, however even though we guarantee our umask is 002 here, it + # still gets created without the necessary g+w permission, due to + # SQLite's policy. This should only affect SQLite engines because its + # the only one that creates a little file on the local file system. + # This kludges around their bug by "touch"ing the database file before + # SQLite has any chance to create it, thus honoring the umask and + # ensuring the right permissions. We only try to do this for SQLite + # engines, and yes, we could have chmod'd the file after the fact, but + # half dozen and all... + self.url = url + touch(url) + database = create_database(url) + store = Store(database, GenerationalCache()) + database.DEBUG = (as_boolean(config.database.debug) + if debug is None else debug) + # Check the sqlite master database to see if the version file exists. + # If so, then we assume the database schema is correctly initialized. + # Storm does not currently have schema creation. This is not an ideal + # way to handle creating the database, but it's cheap and easy for + # now. + table_names = [item[0] for item in + store.execute('select tbl_name from sqlite_master;')] + if 'version' not in table_names: + # Initialize the database. + sql = resource_string('mailman.database', 'mailman.sql') + for statement in sql.split(';'): + store.execute(statement + ';') + # Validate schema version. + v = store.find(Version, component='schema').one() + if not v: + # Database has not yet been initialized + v = Version(component='schema', + version=mailman.version.DATABASE_SCHEMA_VERSION) + store.add(v) + elif v.version <> mailman.version.DATABASE_SCHEMA_VERSION: + # XXX Update schema + raise SchemaVersionMismatchError(v.version) + self.store = store + store.commit() + + def _reset(self): + """See `IDatabase`.""" + from mailman.database.model import ModelMeta + self.store.rollback() + ModelMeta._reset(self.store) + + + +def touch(url): + parts = urlparse(url) + if parts.scheme <> 'sqlite': + return + path = os.path.normpath(parts.path) + fd = os.open(path, os.O_WRONLY | os.O_NONBLOCK | os.O_CREAT, 0666) + # Ignore errors + if fd > 0: + os.close(fd) diff --git a/src/mailman/database/user.py b/src/mailman/database/user.py deleted file mode 100644 index 23701686b..000000000 --- a/src/mailman/database/user.py +++ /dev/null @@ -1,94 +0,0 @@ -# Copyright (C) 2007-2009 by the Free Software Foundation, Inc. -# -# This file is part of GNU Mailman. -# -# GNU Mailman 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 3 of the License, or (at your option) -# any later version. -# -# GNU Mailman 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 -# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. - -"""Model for users.""" - -from __future__ import absolute_import, unicode_literals - -__metaclass__ = type -__all__ = [ - 'User', - ] - -from storm.locals import * -from zope.interface import implements - -from mailman.config import config -from mailman.database.model import Model -from mailman.database.address import Address -from mailman.database.preferences import Preferences -from mailman.database.roster import Memberships -from mailman.interfaces.address import ( - AddressAlreadyLinkedError, AddressNotLinkedError) -from mailman.interfaces.user import IUser - - - -class User(Model): - """Mailman users.""" - - implements(IUser) - - id = Int(primary=True) - real_name = Unicode() - password = Unicode() - - addresses = ReferenceSet(id, 'Address.user_id') - preferences_id = Int() - preferences = Reference(preferences_id, 'Preferences.id') - - def __repr__(self): - return '<User "{0}" at {1:#x}>'.format(self.real_name, id(self)) - - def link(self, address): - """See `IUser`.""" - if address.user is not None: - raise AddressAlreadyLinkedError(address) - address.user = self - - def unlink(self, address): - """See `IUser`.""" - if address.user is None: - raise AddressNotLinkedError(address) - address.user = None - - def controls(self, address): - """See `IUser`.""" - found = config.db.store.find(Address, address=address) - if found.count() == 0: - return False - assert found.count() == 1, 'Unexpected count' - return found[0].user is self - - def register(self, address, real_name=None): - """See `IUser`.""" - # First, see if the address already exists - addrobj = config.db.store.find(Address, address=address).one() - if addrobj is None: - if real_name is None: - real_name = '' - addrobj = Address(address=address, real_name=real_name) - addrobj.preferences = Preferences() - # Link the address to the user if it is not already linked. - if addrobj.user is not None: - raise AddressAlreadyLinkedError(addrobj) - addrobj.user = self - return addrobj - - @property - def memberships(self): - return Memberships(self) diff --git a/src/mailman/database/usermanager.py b/src/mailman/database/usermanager.py deleted file mode 100644 index 0101bc8a5..000000000 --- a/src/mailman/database/usermanager.py +++ /dev/null @@ -1,104 +0,0 @@ -# Copyright (C) 2007-2009 by the Free Software Foundation, Inc. -# -# This file is part of GNU Mailman. -# -# GNU Mailman 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 3 of the License, or (at your option) -# any later version. -# -# GNU Mailman 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 -# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. - -"""A user manager.""" - -from __future__ import absolute_import, unicode_literals - -__metaclass__ = type -__all__ = [ - 'UserManager', - ] - - -from zope.interface import implements - -from mailman.config import config -from mailman.database.address import Address -from mailman.database.preferences import Preferences -from mailman.database.user import User -from mailman.interfaces.address import ExistingAddressError -from mailman.interfaces.usermanager import IUserManager - - - -class UserManager(object): - implements(IUserManager) - - def create_user(self, address=None, real_name=None): - user = User() - user.real_name = ('' if real_name is None else real_name) - if address: - addrobj = Address(address, user.real_name) - addrobj.preferences = Preferences() - user.link(addrobj) - user.preferences = Preferences() - config.db.store.add(user) - return user - - def delete_user(self, user): - config.db.store.remove(user) - - @property - def users(self): - for user in config.db.store.find(User): - yield user - - def get_user(self, address): - addresses = config.db.store.find(Address, address=address.lower()) - if addresses.count() == 0: - return None - elif addresses.count() == 1: - return addresses[0].user - else: - raise AssertionError('Unexpected query count') - - def create_address(self, address, real_name=None): - addresses = config.db.store.find(Address, address=address.lower()) - if addresses.count() == 1: - found = addresses[0] - raise ExistingAddressError(found.original_address) - assert addresses.count() == 0, 'Unexpected results' - if real_name is None: - real_name = '' - # It's okay not to lower case the 'address' argument because the - # constructor will do the right thing. - address = Address(address, real_name) - address.preferences = Preferences() - config.db.store.add(address) - return address - - def delete_address(self, address): - # If there's a user controlling this address, it has to first be - # unlinked before the address can be deleted. - if address.user: - address.user.unlink(address) - config.db.store.remove(address) - - def get_address(self, address): - addresses = config.db.store.find(Address, address=address.lower()) - if addresses.count() == 0: - return None - elif addresses.count() == 1: - return addresses[0] - else: - raise AssertionError('Unexpected query count') - - @property - def addresses(self): - for address in config.db.store.find(Address): - yield address diff --git a/src/mailman/database/version.py b/src/mailman/database/version.py deleted file mode 100644 index d15065395..000000000 --- a/src/mailman/database/version.py +++ /dev/null @@ -1,40 +0,0 @@ -# Copyright (C) 2007-2009 by the Free Software Foundation, Inc. -# -# This file is part of GNU Mailman. -# -# GNU Mailman 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 3 of the License, or (at your option) -# any later version. -# -# GNU Mailman 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 -# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. - -"""Model class for version numbers.""" - -from __future__ import absolute_import, unicode_literals - -__metaclass__ = type -__all__ = [ - 'Version', - ] - -from storm.locals import * -from mailman.database.model import Model - - - -class Version(Model): - id = Int(primary=True) - component = Unicode() - version = Int() - - def __init__(self, component, version): - super(Version, self).__init__() - self.component = component - self.version = version |
