# 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 .
"""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.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.model import roster
from mailman.model.digests import OneLastDigest
from mailman.model.mime import ContentFilter
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 ''.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