'
- % (url_quote(self.prev.filename),
- self.quote(subject)))
- else:
- prev = prev_wsubj = ""
- return prev, prev_wsubj
-
- def _get_subject_enc(self, art):
- """Return the subject of art, decoded if possible.
-
- If the charset of the current message and art match and the
- article's subject is encoded, decode it.
- """
- return art.decoded.get('subject', art.subject)
-
- def _get_next(self):
- """Return the href and subject for the previous message"""
- if self.next:
- subject = self._get_subject_enc(self.next)
- next = (''
- % (url_quote(self.next.filename)))
- next_wsubj = ('
')
- else:
- # Do fancy formatting here
- if self.SHOWBR:
- lines = map(lambda x:x + " ", lines)
- else:
- for i in range(0, len(lines)):
- s = lines[i]
- if s[0:1] in ' \t\n':
- lines[i] = '
' + s
- article.html_body = lines
- return article
-
- def update_article(self, arcdir, article, prev, next):
- seq = article.sequence
- filename = os.path.join(arcdir, article.filename)
- self.message(_('Updating HTML for article %(seq)s'))
- try:
- f = open(filename)
- article.loadbody_fromHTML(f)
- f.close()
- except IOError, e:
- if e.errno <> errno.ENOENT: raise
- self.message(_('article file %(filename)s is missing!'))
- article.prev = prev
- article.next = next
- omask = os.umask(002)
- try:
- f = open(filename, 'w')
- finally:
- os.umask(omask)
- f.write(article.as_html())
- f.close()
diff --git a/mailman/Archiver/HyperDatabase.py b/mailman/Archiver/HyperDatabase.py
deleted file mode 100644
index 49928d7b3..000000000
--- a/mailman/Archiver/HyperDatabase.py
+++ /dev/null
@@ -1,339 +0,0 @@
-# Copyright (C) 1998-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 .
-
-#
-# site modules
-#
-import os
-import marshal
-import time
-import errno
-
-#
-# package/project modules
-#
-import pipermail
-from locknix import lockfile
-
-CACHESIZE = pipermail.CACHESIZE
-
-try:
- import cPickle
- pickle = cPickle
-except ImportError:
- import pickle
-
-#
-# we're using a python dict in place of
-# of bsddb.btree database. only defining
-# the parts of the interface used by class HyperDatabase
-# only one thing can access this at a time.
-#
-class DumbBTree:
- """Stores pickles of Article objects
-
- This dictionary-like object stores pickles of all the Article
- objects. The object itself is stored using marshal. It would be
- much simpler, and probably faster, to store the actual objects in
- the DumbBTree and pickle it.
-
- TBD: Also needs a more sensible name, like IteratableDictionary or
- SortedDictionary.
- """
-
- def __init__(self, path):
- self.current_index = 0
- self.path = path
- self.lockfile = lockfile.Lock(self.path + ".lock")
- self.lock()
- self.__dirty = 0
- self.dict = {}
- self.sorted = []
- self.load()
-
- def __repr__(self):
- return "DumbBTree(%s)" % self.path
-
- def __sort(self, dirty=None):
- if self.__dirty == 1 or dirty:
- self.sorted = self.dict.keys()
- self.sorted.sort()
- self.__dirty = 0
-
- def lock(self):
- self.lockfile.lock()
-
- def unlock(self):
- try:
- self.lockfile.unlock()
- except lockfile.NotLockedError:
- pass
-
- def __delitem__(self, item):
- # if first hasn't been called, we can skip the sort
- if self.current_index == 0:
- del self.dict[item]
- self.__dirty = 1
- return
- try:
- ci = self.sorted[self.current_index]
- except IndexError:
- ci = None
- if ci == item:
- try:
- ci = self.sorted[self.current_index + 1]
- except IndexError:
- ci = None
- del self.dict[item]
- self.__sort(dirty=1)
- if ci is not None:
- self.current_index = self.sorted.index(ci)
- else:
- self.current_index = self.current_index + 1
-
- def clear(self):
- # bulk clearing much faster than deleting each item, esp. with the
- # implementation of __delitem__() above :(
- self.dict = {}
-
- def first(self):
- self.__sort() # guarantee that the list is sorted
- if not self.sorted:
- raise KeyError
- else:
- key = self.sorted[0]
- self.current_index = 1
- return key, self.dict[key]
-
- def last(self):
- if not self.sorted:
- raise KeyError
- else:
- key = self.sorted[-1]
- self.current_index = len(self.sorted) - 1
- return key, self.dict[key]
-
- def next(self):
- try:
- key = self.sorted[self.current_index]
- except IndexError:
- raise KeyError
- self.current_index = self.current_index + 1
- return key, self.dict[key]
-
- def has_key(self, key):
- return self.dict.has_key(key)
-
- def set_location(self, loc):
- if not self.dict.has_key(loc):
- raise KeyError
- self.current_index = self.sorted.index(loc)
-
- def __getitem__(self, item):
- return self.dict[item]
-
- def __setitem__(self, item, val):
- # if first hasn't been called, then we don't need to worry
- # about sorting again
- if self.current_index == 0:
- self.dict[item] = val
- self.__dirty = 1
- return
- try:
- current_item = self.sorted[self.current_index]
- except IndexError:
- current_item = item
- self.dict[item] = val
- self.__sort(dirty=1)
- self.current_index = self.sorted.index(current_item)
-
- def __len__(self):
- return len(self.sorted)
-
- def load(self):
- try:
- fp = open(self.path)
- try:
- self.dict = marshal.load(fp)
- finally:
- fp.close()
- except IOError, e:
- if e.errno <> errno.ENOENT: raise
- pass
- except EOFError:
- pass
- else:
- self.__sort(dirty=1)
-
- def close(self):
- omask = os.umask(007)
- try:
- fp = open(self.path, 'w')
- finally:
- os.umask(omask)
- fp.write(marshal.dumps(self.dict))
- fp.close()
- self.unlock()
-
-
-# this is lifted straight out of pipermail with
-# the bsddb.btree replaced with above class.
-# didn't use inheritance because of all the
-# __internal stuff that needs to be here -scott
-#
-class HyperDatabase(pipermail.Database):
- __super_addArticle = pipermail.Database.addArticle
-
- def __init__(self, basedir, mlist):
- self.__cache = {}
- self.__currentOpenArchive = None # The currently open indices
- self._mlist = mlist
- self.basedir = os.path.expanduser(basedir)
- # Recently added articles, indexed only by message ID
- self.changed={}
-
- def firstdate(self, archive):
- self.__openIndices(archive)
- date = 'None'
- try:
- datekey, msgid = self.dateIndex.first()
- date = time.asctime(time.localtime(float(datekey[0])))
- except KeyError:
- pass
- return date
-
- def lastdate(self, archive):
- self.__openIndices(archive)
- date = 'None'
- try:
- datekey, msgid = self.dateIndex.last()
- date = time.asctime(time.localtime(float(datekey[0])))
- except KeyError:
- pass
- return date
-
- def numArticles(self, archive):
- self.__openIndices(archive)
- return len(self.dateIndex)
-
- def addArticle(self, archive, article, subject=None, author=None,
- date=None):
- self.__openIndices(archive)
- self.__super_addArticle(archive, article, subject, author, date)
-
- def __openIndices(self, archive):
- if self.__currentOpenArchive == archive:
- return
- self.__closeIndices()
- arcdir = os.path.join(self.basedir, 'database')
- omask = os.umask(0)
- try:
- try:
- os.mkdir(arcdir, 02770)
- except OSError, e:
- if e.errno <> errno.EEXIST: raise
- finally:
- os.umask(omask)
- for i in ('date', 'author', 'subject', 'article', 'thread'):
- t = DumbBTree(os.path.join(arcdir, archive + '-' + i))
- setattr(self, i + 'Index', t)
- self.__currentOpenArchive = archive
-
- def __closeIndices(self):
- for i in ('date', 'author', 'subject', 'thread', 'article'):
- attr = i + 'Index'
- if hasattr(self, attr):
- index = getattr(self, attr)
- if i == 'article':
- if not hasattr(self, 'archive_length'):
- self.archive_length = {}
- l = len(index)
- self.archive_length[self.__currentOpenArchive] = l
- index.close()
- delattr(self, attr)
- self.__currentOpenArchive = None
-
- def close(self):
- self.__closeIndices()
-
- def hasArticle(self, archive, msgid):
- self.__openIndices(archive)
- return self.articleIndex.has_key(msgid)
-
- def setThreadKey(self, archive, key, msgid):
- self.__openIndices(archive)
- self.threadIndex[key]=msgid
-
- def getArticle(self, archive, msgid):
- self.__openIndices(archive)
- if not self.__cache.has_key(msgid):
- # get the pickled object out of the DumbBTree
- buf = self.articleIndex[msgid]
- article = self.__cache[msgid] = pickle.loads(buf)
- # For upgrading older archives
- article.setListIfUnset(self._mlist)
- else:
- article = self.__cache[msgid]
- return article
-
- def first(self, archive, index):
- self.__openIndices(archive)
- index = getattr(self, index + 'Index')
- try:
- key, msgid = index.first()
- return msgid
- except KeyError:
- return None
-
- def next(self, archive, index):
- self.__openIndices(archive)
- index = getattr(self, index + 'Index')
- try:
- key, msgid = index.next()
- return msgid
- except KeyError:
- return None
-
- def getOldestArticle(self, archive, subject):
- self.__openIndices(archive)
- subject = subject.lower()
- try:
- key, tempid=self.subjectIndex.set_location(subject)
- self.subjectIndex.next()
- [subject2, date]= key.split('\0')
- if subject!=subject2: return None
- return tempid
- except KeyError:
- return None
-
- def newArchive(self, archive):
- pass
-
- def clearIndex(self, archive, index):
- self.__openIndices(archive)
- if hasattr(self.threadIndex, 'clear'):
- self.threadIndex.clear()
- return
- finished=0
- try:
- key, msgid=self.threadIndex.first()
- except KeyError: finished=1
- while not finished:
- del self.threadIndex[key]
- try:
- key, msgid=self.threadIndex.next()
- except KeyError: finished=1
diff --git a/mailman/Archiver/__init__.py b/mailman/Archiver/__init__.py
deleted file mode 100644
index 322010acb..000000000
--- a/mailman/Archiver/__init__.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Copyright (C) 1998-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 .
-
-from Archiver import *
diff --git a/mailman/Archiver/pipermail.py b/mailman/Archiver/pipermail.py
deleted file mode 100644
index 19bc05c3f..000000000
--- a/mailman/Archiver/pipermail.py
+++ /dev/null
@@ -1,874 +0,0 @@
-#! /usr/bin/env python
-
-import os
-import re
-import sys
-import time
-import logging
-import mailbox
-
-import cPickle as pickle
-
-from cStringIO import StringIO
-from email.Utils import parseaddr, parsedate_tz, mktime_tz, formatdate
-from string import lowercase
-
-__version__ = '0.11 (Mailman edition)'
-VERSION = __version__
-CACHESIZE = 100 # Number of slots in the cache
-
-from mailman.Mailbox import ArchiverMailbox
-from mailman.core import errors
-from mailman.i18n import _
-
-SPACE = ' '
-
-log = logging.getLogger('mailman.error')
-
-
-
-msgid_pat = re.compile(r'(<.*>)')
-def strip_separators(s):
- "Remove quotes or parenthesization from a Message-ID string"
- if not s:
- return ""
- if s[0] in '"<([' and s[-1] in '">)]':
- s = s[1:-1]
- return s
-
-smallNameParts = ['van', 'von', 'der', 'de']
-
-def fixAuthor(author):
- "Canonicalize a name into Last, First format"
- # If there's a comma, guess that it's already in "Last, First" format
- if ',' in author:
- return author
- L = author.split()
- i = len(L) - 1
- if i == 0:
- return author # The string's one word--forget it
- if author.upper() == author or author.lower() == author:
- # Damn, the name is all upper- or lower-case.
- while i > 0 and L[i-1].lower() in smallNameParts:
- i = i - 1
- else:
- # Mixed case; assume that small parts of the last name will be
- # in lowercase, and check them against the list.
- while i>0 and (L[i-1][0] in lowercase or
- L[i-1].lower() in smallNameParts):
- i = i - 1
- author = SPACE.join(L[-1:] + L[i:-1]) + ', ' + SPACE.join(L[:i])
- return author
-
-# Abstract class for databases
-
-class DatabaseInterface:
- def __init__(self): pass
- def close(self): pass
- def getArticle(self, archive, msgid): pass
- def hasArticle(self, archive, msgid): pass
- def addArticle(self, archive, article, subject=None, author=None,
- date=None): pass
- def firstdate(self, archive): pass
- def lastdate(self, archive): pass
- def first(self, archive, index): pass
- def next(self, archive, index): pass
- def numArticles(self, archive): pass
- def newArchive(self, archive): pass
- def setThreadKey(self, archive, key, msgid): pass
- def getOldestArticle(self, subject): pass
-
-class Database(DatabaseInterface):
- """Define the basic sorting logic for a database
-
- Assumes that the database internally uses dateIndex, authorIndex,
- etc.
- """
-
- # TBD Factor out more of the logic shared between BSDDBDatabase
- # and HyperDatabase and place it in this class.
-
- def __init__(self):
- # This method need not be called by subclasses that do their
- # own initialization.
- self.dateIndex = {}
- self.authorIndex = {}
- self.subjectIndex = {}
- self.articleIndex = {}
- self.changed = {}
-
- def addArticle(self, archive, article, subject=None, author=None,
- date=None):
- # create the keys; always end w/ msgid which will be unique
- authorkey = (author or article.author, article.date,
- article.msgid)
- subjectkey = (subject or article.subject, article.date,
- article.msgid)
- datekey = date or article.date, article.msgid
-
- # Add the new article
- self.dateIndex[datekey] = article.msgid
- self.authorIndex[authorkey] = article.msgid
- self.subjectIndex[subjectkey] = article.msgid
-
- self.store_article(article)
- self.changed[archive, article.msgid] = None
-
- parentID = article.parentID
- if parentID is not None and self.articleIndex.has_key(parentID):
- parent = self.getArticle(archive, parentID)
- myThreadKey = parent.threadKey + article.date + '-'
- else:
- myThreadKey = article.date + '-'
- article.threadKey = myThreadKey
- key = myThreadKey, article.msgid
- self.setThreadKey(archive, key, article.msgid)
-
- def store_article(self, article):
- """Store article without message body to save space"""
- # TBD this is not thread safe!
- temp = article.body
- temp2 = article.html_body
- article.body = []
- del article.html_body
- self.articleIndex[article.msgid] = pickle.dumps(article)
- article.body = temp
- article.html_body = temp2
-
-
-# The Article class encapsulates a single posting. The attributes
-# are:
-#
-# sequence : Sequence number, unique for each article in a set of archives
-# subject : Subject
-# datestr : The posting date, in human-readable format
-# date : The posting date, in purely numeric format
-# headers : Any other headers of interest
-# author : The author's name (and possibly organization)
-# email : The author's e-mail address
-# msgid : A unique message ID
-# in_reply_to: If != "", this is the msgid of the article being replied to
-# references : A (possibly empty) list of msgid's of earlier articles
-# in the thread
-# body : A list of strings making up the message body
-
-class Article:
- _last_article_time = time.time()
-
- def __init__(self, message = None, sequence = 0, keepHeaders = []):
- if message is None:
- return
- self.sequence = sequence
-
- self.parentID = None
- self.threadKey = None
- # otherwise the current sequence number is used.
- id = strip_separators(message['Message-Id'])
- if id == "":
- self.msgid = str(self.sequence)
- else: self.msgid = id
-
- if message.has_key('Subject'):
- self.subject = str(message['Subject'])
- else:
- self.subject = _('No subject')
- if self.subject == "": self.subject = _('No subject')
-
- self._set_date(message)
-
- # Figure out the e-mail address and poster's name. Use the From:
- # field first, followed by Reply-To:
- self.author, self.email = parseaddr(message.get('From', ''))
- e = message['Reply-To']
- if not self.email and e is not None:
- ignoreauthor, self.email = parseaddr(e)
- self.email = strip_separators(self.email)
- self.author = strip_separators(self.author)
-
- if self.author == "":
- self.author = self.email
-
- # Save the In-Reply-To:, References:, and Message-ID: lines
- #
- # TBD: The original code does some munging on these fields, which
- # shouldn't be necessary, but changing this may break code. For
- # safety, I save the original headers on different attributes for use
- # in writing the plain text periodic flat files.
- self._in_reply_to = message['in-reply-to']
- self._references = message['references']
- self._message_id = message['message-id']
-
- i_r_t = message['In-Reply-To']
- if i_r_t is None:
- self.in_reply_to = ''
- else:
- match = msgid_pat.search(i_r_t)
- if match is None: self.in_reply_to = ''
- else: self.in_reply_to = strip_separators(match.group(1))
-
- references = message['References']
- if references is None:
- self.references = []
- else:
- self.references = map(strip_separators, references.split())
-
- # Save any other interesting headers
- self.headers = {}
- for i in keepHeaders:
- if message.has_key(i):
- self.headers[i] = message[i]
-
- # Read the message body
- s = StringIO(message.get_payload(decode=True)\
- or message.as_string().split('\n\n',1)[1])
- self.body = s.readlines()
-
- def _set_date(self, message):
- def floatdate(header):
- missing = []
- datestr = message.get(header, missing)
- if datestr is missing:
- return None
- date = parsedate_tz(datestr)
- try:
- return mktime_tz(date)
- except (TypeError, ValueError, OverflowError):
- return None
- date = floatdate('date')
- if date is None:
- date = floatdate('x-list-received-date')
- if date is None:
- # What's left to try?
- date = self._last_article_time + 1
- self._last_article_time = date
- self.date = '%011i' % date
- self.datestr = message.get('date') \
- or message.get('x-list-received-date') \
- or formatdate(date)
-
- def __repr__(self):
- return ''
-
- def finished_update_article(self):
- pass
-
-# Pipermail formatter class
-
-class T:
- DIRMODE = 0755 # Mode to give to created directories
- FILEMODE = 0644 # Mode to give to created files
- INDEX_EXT = ".html" # Extension for indexes
-
- def __init__(self, basedir = None, reload = 1, database = None):
- # If basedir isn't provided, assume the current directory
- if basedir is None:
- self.basedir = os.getcwd()
- else:
- basedir = os.path.expanduser(basedir)
- self.basedir = basedir
- self.database = database
-
- # If the directory doesn't exist, create it. This code shouldn't get
- # run anymore, we create the directory in Archiver.py. It should only
- # get used by legacy lists created that are only receiving their first
- # message in the HTML archive now -- Marc
- try:
- os.stat(self.basedir)
- except os.error, errdata:
- errno, errmsg = errdata
- if errno != 2:
- raise os.error, errdata
- else:
- self.message(_('Creating archive directory ') + self.basedir)
- omask = os.umask(0)
- try:
- os.mkdir(self.basedir, self.DIRMODE)
- finally:
- os.umask(omask)
-
- # Try to load previously pickled state
- try:
- if not reload:
- raise IOError
- f = open(os.path.join(self.basedir, 'pipermail.pck'), 'r')
- self.message(_('Reloading pickled archive state'))
- d = pickle.load(f)
- f.close()
- for key, value in d.items():
- setattr(self, key, value)
- except (IOError, EOFError):
- # No pickled version, so initialize various attributes
- self.archives = [] # Archives
- self._dirty_archives = [] # Archives that will have to be updated
- self.sequence = 0 # Sequence variable used for
- # numbering articles
- self.update_TOC = 0 # Does the TOC need updating?
- #
- # make the basedir variable work when passed in as an __init__ arg
- # and different from the one in the pickle. Let the one passed in
- # as an __init__ arg take precedence if it's stated. This way, an
- # archive can be moved from one place to another and still work.
- #
- if basedir != self.basedir:
- self.basedir = basedir
-
- def close(self):
- "Close an archive, save its state, and update any changed archives."
- self.update_dirty_archives()
- self.update_TOC = 0
- self.write_TOC()
- # Save the collective state
- self.message(_('Pickling archive state into ')
- + os.path.join(self.basedir, 'pipermail.pck'))
- self.database.close()
- del self.database
-
- omask = os.umask(007)
- try:
- f = open(os.path.join(self.basedir, 'pipermail.pck'), 'w')
- finally:
- os.umask(omask)
- pickle.dump(self.getstate(), f)
- f.close()
-
- def getstate(self):
- # can override this in subclass
- return self.__dict__
-
- #
- # Private methods
- #
- # These will be neither overridden nor called by custom archivers.
- #
-
-
- # Create a dictionary of various parameters that will be passed
- # to the write_index_{header,footer} functions
- def __set_parameters(self, archive):
- # Determine the earliest and latest date in the archive
- firstdate = self.database.firstdate(archive)
- lastdate = self.database.lastdate(archive)
-
- # Get the current time
- now = time.asctime(time.localtime(time.time()))
- self.firstdate = firstdate
- self.lastdate = lastdate
- self.archivedate = now
- self.size = self.database.numArticles(archive)
- self.archive = archive
- self.version = __version__
-
- # Find the message ID of an article's parent, or return None
- # if no parent can be found.
-
- def __findParent(self, article, children = []):
- parentID = None
- if article.in_reply_to:
- parentID = article.in_reply_to
- elif article.references:
- # Remove article IDs that aren't in the archive
- refs = filter(self.articleIndex.has_key, article.references)
- if not refs:
- return None
- maxdate = self.database.getArticle(self.archive,
- refs[0])
- for ref in refs[1:]:
- a = self.database.getArticle(self.archive, ref)
- if a.date > maxdate.date:
- maxdate = a
- parentID = maxdate.msgid
- else:
- # Look for the oldest matching subject
- try:
- key, tempid = \
- self.subjectIndex.set_location(article.subject)
- print key, tempid
- self.subjectIndex.next()
- [subject, date] = key.split('\0')
- print article.subject, subject, date
- if subject == article.subject and tempid not in children:
- parentID = tempid
- except KeyError:
- pass
- return parentID
-
- # Update the threaded index completely
- def updateThreadedIndex(self):
- # Erase the threaded index
- self.database.clearIndex(self.archive, 'thread')
-
- # Loop over all the articles
- msgid = self.database.first(self.archive, 'date')
- while msgid is not None:
- try:
- article = self.database.getArticle(self.archive, msgid)
- except KeyError:
- pass
- else:
- if article.parentID is None or \
- not self.database.hasArticle(self.archive,
- article.parentID):
- # then
- pass
- else:
- parent = self.database.getArticle(self.archive,
- article.parentID)
- article.threadKey = parent.threadKey+article.date+'-'
- self.database.setThreadKey(self.archive,
- (article.threadKey, article.msgid),
- msgid)
- msgid = self.database.next(self.archive, 'date')
-
- #
- # Public methods:
- #
- # These are part of the public interface of the T class, but will
- # never be overridden (unless you're trying to do something very new).
-
- # Update a single archive's indices, whether the archive's been
- # dirtied or not.
- def update_archive(self, archive):
- self.archive = archive
- self.message(_("Updating index files for archive [%(archive)s]"))
- arcdir = os.path.join(self.basedir, archive)
- self.__set_parameters(archive)
-
- for hdr in ('Date', 'Subject', 'Author'):
- self._update_simple_index(hdr, archive, arcdir)
-
- self._update_thread_index(archive, arcdir)
-
- def _update_simple_index(self, hdr, archive, arcdir):
- self.message(" " + hdr)
- self.type = hdr
- hdr = hdr.lower()
-
- self._open_index_file_as_stdout(arcdir, hdr)
- self.write_index_header()
- count = 0
- # Loop over the index entries
- msgid = self.database.first(archive, hdr)
- while msgid is not None:
- try:
- article = self.database.getArticle(self.archive, msgid)
- except KeyError:
- pass
- else:
- count = count + 1
- self.write_index_entry(article)
- msgid = self.database.next(archive, hdr)
- # Finish up this index
- self.write_index_footer()
- self._restore_stdout()
-
- def _update_thread_index(self, archive, arcdir):
- self.message(_(" Thread"))
- self._open_index_file_as_stdout(arcdir, "thread")
- self.type = 'Thread'
- self.write_index_header()
-
- # To handle the prev./next in thread pointers, we need to
- # track articles 5 at a time.
-
- # Get the first 5 articles
- L = [None] * 5
- i = 2
- msgid = self.database.first(self.archive, 'thread')
-
- while msgid is not None and i < 5:
- L[i] = self.database.getArticle(self.archive, msgid)
- i = i + 1
- msgid = self.database.next(self.archive, 'thread')
-
- while L[2] is not None:
- article = L[2]
- artkey = None
- if article is not None:
- artkey = article.threadKey
- if artkey is not None:
- self.write_threadindex_entry(article, artkey.count('-') - 1)
- if self.database.changed.has_key((archive,article.msgid)):
- a1 = L[1]
- a3 = L[3]
- self.update_article(arcdir, article, a1, a3)
- if a3 is not None:
- self.database.changed[(archive, a3.msgid)] = None
- if a1 is not None:
- key = archive, a1.msgid
- if not self.database.changed.has_key(key):
- self.update_article(arcdir, a1, L[0], L[2])
- else:
- del self.database.changed[key]
- if L[0]:
- L[0].finished_update_article()
- L = L[1:] # Rotate the list
- if msgid is None:
- L.append(msgid)
- else:
- L.append(self.database.getArticle(self.archive, msgid))
- msgid = self.database.next(self.archive, 'thread')
-
- self.write_index_footer()
- self._restore_stdout()
-
- def _open_index_file_as_stdout(self, arcdir, index_name):
- path = os.path.join(arcdir, index_name + self.INDEX_EXT)
- omask = os.umask(002)
- try:
- self.__f = open(path, 'w')
- finally:
- os.umask(omask)
- self.__stdout = sys.stdout
- sys.stdout = self.__f
-
- def _restore_stdout(self):
- sys.stdout = self.__stdout
- self.__f.close()
- del self.__f
- del self.__stdout
-
- # Update only archives that have been marked as "changed".
- def update_dirty_archives(self):
- for i in self._dirty_archives:
- self.update_archive(i)
- self._dirty_archives = []
-
- # Read a Unix mailbox file from the file object ,
- # and create a series of Article objects. Each article
- # object will then be archived.
-
- def _makeArticle(self, msg, sequence):
- return Article(msg, sequence)
-
- def processUnixMailbox(self, input, start=None, end=None):
- mbox = ArchiverMailbox(input, self.maillist)
- if start is None:
- start = 0
- counter = 0
- while counter < start:
- try:
- m = mbox.next()
- except errors.DiscardMessage:
- continue
- if m is None:
- return
- counter += 1
- while 1:
- try:
- pos = input.tell()
- m = mbox.next()
- except errors.DiscardMessage:
- continue
- except Exception:
- log.error('uncaught archiver exception at filepos: %s', pos)
- raise
- if m is None:
- break
- if m == '':
- # It was an unparseable message
- continue
- msgid = m.get('message-id', 'n/a')
- self.message(_('#%(counter)05d %(msgid)s'))
- a = self._makeArticle(m, self.sequence)
- self.sequence += 1
- self.add_article(a)
- if end is not None and counter >= end:
- break
- counter += 1
-
- def new_archive(self, archive, archivedir):
- self.archives.append(archive)
- self.update_TOC = 1
- self.database.newArchive(archive)
- # If the archive directory doesn't exist, create it
- try:
- os.stat(archivedir)
- except os.error, errdata:
- errno, errmsg = errdata
- if errno == 2:
- omask = os.umask(0)
- try:
- os.mkdir(archivedir, self.DIRMODE)
- finally:
- os.umask(omask)
- else:
- raise os.error, errdata
- self.open_new_archive(archive, archivedir)
-
- def add_article(self, article):
- archives = self.get_archives(article)
- if not archives:
- return
- if type(archives) == type(''):
- archives = [archives]
-
- article.filename = filename = self.get_filename(article)
- temp = self.format_article(article)
- for arch in archives:
- self.archive = arch # why do this???
- archivedir = os.path.join(self.basedir, arch)
- if arch not in self.archives:
- self.new_archive(arch, archivedir)
-
- # Write the HTML-ized article
- self.write_article(arch, temp, os.path.join(archivedir,
- filename))
-
- if article.decoded.has_key('author'):
- author = fixAuthor(article.decoded['author'])
- else:
- author = fixAuthor(article.author)
- if article.decoded.has_key('stripped'):
- subject = article.decoded['stripped'].lower()
- else:
- subject = article.subject.lower()
-
- article.parentID = parentID = self.get_parent_info(arch, article)
- if parentID:
- parent = self.database.getArticle(arch, parentID)
- article.threadKey = parent.threadKey + article.date + '-'
- else:
- article.threadKey = article.date + '-'
- key = article.threadKey, article.msgid
-
- self.database.setThreadKey(arch, key, article.msgid)
- self.database.addArticle(arch, temp, author=author,
- subject=subject)
-
- if arch not in self._dirty_archives:
- self._dirty_archives.append(arch)
-
- def get_parent_info(self, archive, article):
- parentID = None
- if article.in_reply_to:
- parentID = article.in_reply_to
- elif article.references:
- refs = self._remove_external_references(article.references)
- if refs:
- maxdate = self.database.getArticle(archive, refs[0])
- for ref in refs[1:]:
- a = self.database.getArticle(archive, ref)
- if a.date > maxdate.date:
- maxdate = a
- parentID = maxdate.msgid
- else:
- # Get the oldest article with a matching subject, and
- # assume this is a follow-up to that article
- parentID = self.database.getOldestArticle(archive,
- article.subject)
-
- if parentID and not self.database.hasArticle(archive, parentID):
- parentID = None
- return parentID
-
- def write_article(self, index, article, path):
- omask = os.umask(002)
- try:
- f = open(path, 'w')
- finally:
- os.umask(omask)
- temp_stdout, sys.stdout = sys.stdout, f
- self.write_article_header(article)
- sys.stdout.writelines(article.body)
- self.write_article_footer(article)
- sys.stdout = temp_stdout
- f.close()
-
- def _remove_external_references(self, refs):
- keep = []
- for ref in refs:
- if self.database.hasArticle(self.archive, ref):
- keep.append(ref)
- return keep
-
- # Abstract methods: these will need to be overridden by subclasses
- # before anything useful can be done.
-
- def get_filename(self, article):
- pass
- def get_archives(self, article):
- """Return a list of indexes where the article should be filed.
- A string can be returned if the list only contains one entry,
- and the empty list is legal."""
- pass
- def format_article(self, article):
- pass
- def write_index_header(self):
- pass
- def write_index_footer(self):
- pass
- def write_index_entry(self, article):
- pass
- def write_threadindex_entry(self, article, depth):
- pass
- def write_article_header(self, article):
- pass
- def write_article_footer(self, article):
- pass
- def write_article_entry(self, article):
- pass
- def update_article(self, archivedir, article, prev, next):
- pass
- def write_TOC(self):
- pass
- def open_new_archive(self, archive, dir):
- pass
- def message(self, msg):
- pass
-
-
-class BSDDBdatabase(Database):
- __super_addArticle = Database.addArticle
-
- def __init__(self, basedir):
- self.__cachekeys = []
- self.__cachedict = {}
- self.__currentOpenArchive = None # The currently open indices
- self.basedir = os.path.expanduser(basedir)
- self.changed = {} # Recently added articles, indexed only by
- # message ID
-
- def firstdate(self, archive):
- self.__openIndices(archive)
- date = 'None'
- try:
- date, msgid = self.dateIndex.first()
- date = time.asctime(time.localtime(float(date)))
- except KeyError:
- pass
- return date
-
- def lastdate(self, archive):
- self.__openIndices(archive)
- date = 'None'
- try:
- date, msgid = self.dateIndex.last()
- date = time.asctime(time.localtime(float(date)))
- except KeyError:
- pass
- return date
-
- def numArticles(self, archive):
- self.__openIndices(archive)
- return len(self.dateIndex)
-
- def addArticle(self, archive, article, subject=None, author=None,
- date=None):
- self.__openIndices(archive)
- self.__super_addArticle(archive, article, subject, author, date)
-
- # Open the BSDDB files that are being used as indices
- # (dateIndex, authorIndex, subjectIndex, articleIndex)
- def __openIndices(self, archive):
- if self.__currentOpenArchive == archive:
- return
-
- import bsddb
- self.__closeIndices()
- arcdir = os.path.join(self.basedir, 'database')
- omask = os.umask(0)
- try:
- try:
- os.mkdir(arcdir, 02775)
- except OSError:
- # BAW: Hmm...
- pass
- finally:
- os.umask(omask)
- for hdr in ('date', 'author', 'subject', 'article', 'thread'):
- path = os.path.join(arcdir, archive + '-' + hdr)
- t = bsddb.btopen(path, 'c')
- setattr(self, hdr + 'Index', t)
- self.__currentOpenArchive = archive
-
- # Close the BSDDB files that are being used as indices (if they're
- # open--this is safe to call if they're already closed)
- def __closeIndices(self):
- if self.__currentOpenArchive is not None:
- pass
- for hdr in ('date', 'author', 'subject', 'thread', 'article'):
- attr = hdr + 'Index'
- if hasattr(self, attr):
- index = getattr(self, attr)
- if hdr == 'article':
- if not hasattr(self, 'archive_length'):
- self.archive_length = {}
- self.archive_length[self.__currentOpenArchive] = len(index)
- index.close()
- delattr(self,attr)
- self.__currentOpenArchive = None
-
- def close(self):
- self.__closeIndices()
- def hasArticle(self, archive, msgid):
- self.__openIndices(archive)
- return self.articleIndex.has_key(msgid)
- def setThreadKey(self, archive, key, msgid):
- self.__openIndices(archive)
- self.threadIndex[key] = msgid
- def getArticle(self, archive, msgid):
- self.__openIndices(archive)
- if self.__cachedict.has_key(msgid):
- self.__cachekeys.remove(msgid)
- self.__cachekeys.append(msgid)
- return self.__cachedict[msgid]
- if len(self.__cachekeys) == CACHESIZE:
- delkey, self.__cachekeys = (self.__cachekeys[0],
- self.__cachekeys[1:])
- del self.__cachedict[delkey]
- s = self.articleIndex[msgid]
- article = pickle.loads(s)
- self.__cachekeys.append(msgid)
- self.__cachedict[msgid] = article
- return article
-
- def first(self, archive, index):
- self.__openIndices(archive)
- index = getattr(self, index+'Index')
- try:
- key, msgid = index.first()
- return msgid
- except KeyError:
- return None
- def next(self, archive, index):
- self.__openIndices(archive)
- index = getattr(self, index+'Index')
- try:
- key, msgid = index.next()
- except KeyError:
- return None
- else:
- return msgid
-
- def getOldestArticle(self, archive, subject):
- self.__openIndices(archive)
- subject = subject.lower()
- try:
- key, tempid = self.subjectIndex.set_location(subject)
- self.subjectIndex.next()
- [subject2, date] = key.split('\0')
- if subject != subject2:
- return None
- return tempid
- except KeyError: # XXX what line raises the KeyError?
- return None
-
- def newArchive(self, archive):
- pass
-
- def clearIndex(self, archive, index):
- self.__openIndices(archive)
- index = getattr(self, index+'Index')
- finished = 0
- try:
- key, msgid = self.threadIndex.first()
- except KeyError:
- finished = 1
- while not finished:
- del self.threadIndex[key]
- try:
- key, msgid = self.threadIndex.next()
- except KeyError:
- finished = 1
-
-
diff --git a/mailman/Bouncers/BouncerAPI.py b/mailman/Bouncers/BouncerAPI.py
deleted file mode 100644
index f4712ec20..000000000
--- a/mailman/Bouncers/BouncerAPI.py
+++ /dev/null
@@ -1,64 +0,0 @@
-# Copyright (C) 1998-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 .
-
-"""Contains all the common functionality for msg bounce scanning API.
-
-This module can also be used as the basis for a bounce detection testing
-framework. When run as a script, it expects two arguments, the listname and
-the filename containing the bounce message.
-"""
-
-import sys
-
-# If a bounce detector returns Stop, that means to just discard the message.
-# An example is warning messages for temporary delivery problems. These
-# shouldn't trigger a bounce notification, but we also don't want to send them
-# on to the list administrator.
-Stop = object()
-
-
-BOUNCE_PIPELINE = [
- 'DSN',
- 'Qmail',
- 'Postfix',
- 'Yahoo',
- 'Caiwireless',
- 'Exchange',
- 'Exim',
- 'Netscape',
- 'Compuserve',
- 'Microsoft',
- 'GroupWise',
- 'SMTP32',
- 'SimpleMatch',
- 'SimpleWarning',
- 'Yale',
- 'LLNL',
- ]
-
-
-
-# msg must be a mimetools.Message
-def ScanMessages(mlist, msg):
- for module in BOUNCE_PIPELINE:
- modname = 'mailman.Bouncers.' + module
- __import__(modname)
- addrs = sys.modules[modname].process(msg)
- if addrs:
- # Return addrs even if it is Stop. BounceRunner needs this info.
- return addrs
- return []
diff --git a/mailman/Bouncers/Caiwireless.py b/mailman/Bouncers/Caiwireless.py
deleted file mode 100644
index 3bf03cc62..000000000
--- a/mailman/Bouncers/Caiwireless.py
+++ /dev/null
@@ -1,45 +0,0 @@
-# Copyright (C) 1998-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 .
-
-"""Parse mystery style generated by MTA at caiwireless.net."""
-
-import re
-import email
-
-tcre = re.compile(r'the following recipients did not receive this message:',
- re.IGNORECASE)
-acre = re.compile(r'<(?P[^>]*)>')
-
-
-
-def process(msg):
- if msg.get_content_type() <> 'multipart/mixed':
- return None
- # simple state machine
- # 0 == nothing seen
- # 1 == tag line seen
- state = 0
- # This format thinks it's a MIME, but it really isn't
- for line in email.Iterators.body_line_iterator(msg):
- line = line.strip()
- if state == 0 and tcre.match(line):
- state = 1
- elif state == 1 and line:
- mo = acre.match(line)
- if not mo:
- return None
- return [mo.group('addr')]
diff --git a/mailman/Bouncers/Compuserve.py b/mailman/Bouncers/Compuserve.py
deleted file mode 100644
index 2297a72a9..000000000
--- a/mailman/Bouncers/Compuserve.py
+++ /dev/null
@@ -1,46 +0,0 @@
-# Copyright (C) 1998-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 .
-
-"""Compuserve has its own weird format for bounces."""
-
-import re
-import email
-
-dcre = re.compile(r'your message could not be delivered', re.IGNORECASE)
-acre = re.compile(r'Invalid receiver address: (?P.*)')
-
-
-
-def process(msg):
- # simple state machine
- # 0 = nothing seen yet
- # 1 = intro line seen
- state = 0
- addrs = []
- for line in email.Iterators.body_line_iterator(msg):
- if state == 0:
- mo = dcre.search(line)
- if mo:
- state = 1
- elif state == 1:
- mo = dcre.search(line)
- if mo:
- break
- mo = acre.search(line)
- if mo:
- addrs.append(mo.group('addr'))
- return addrs
diff --git a/mailman/Bouncers/DSN.py b/mailman/Bouncers/DSN.py
deleted file mode 100644
index 37e5bcb83..000000000
--- a/mailman/Bouncers/DSN.py
+++ /dev/null
@@ -1,99 +0,0 @@
-# Copyright (C) 1998-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 .
-
-"""Parse RFC 3464 (i.e. DSN) bounce formats.
-
-RFC 3464 obsoletes 1894 which was the old DSN standard. This module has not
-been audited for differences between the two.
-"""
-
-from email.Iterators import typed_subpart_iterator
-from email.Utils import parseaddr
-
-from mailman.Bouncers.BouncerAPI import Stop
-
-
-
-def check(msg):
- # Iterate over each message/delivery-status subpart
- addrs = []
- for part in typed_subpart_iterator(msg, 'message', 'delivery-status'):
- if not part.is_multipart():
- # Huh?
- continue
- # Each message/delivery-status contains a list of Message objects
- # which are the header blocks. Iterate over those too.
- for msgblock in part.get_payload():
- # We try to dig out the Original-Recipient (which is optional) and
- # Final-Recipient (which is mandatory, but may not exactly match
- # an address on our list). Some MTA's also use X-Actual-Recipient
- # as a synonym for Original-Recipient, but some apparently use
- # that for other purposes :(
- #
- # Also grok out Action so we can do something with that too.
- action = msgblock.get('action', '').lower()
- # Some MTAs have been observed that put comments on the action.
- if action.startswith('delayed'):
- return Stop
- if not action.startswith('fail'):
- # Some non-permanent failure, so ignore this block
- continue
- params = []
- foundp = False
- for header in ('original-recipient', 'final-recipient'):
- for k, v in msgblock.get_params([], header):
- if k.lower() == 'rfc822':
- foundp = True
- else:
- params.append(k)
- if foundp:
- # Note that params should already be unquoted.
- addrs.extend(params)
- break
- else:
- # MAS: This is a kludge, but SMTP-GATEWAY01.intra.home.dk
- # has a final-recipient with an angle-addr and no
- # address-type parameter at all. Non-compliant, but ...
- for param in params:
- if param.startswith('<') and param.endswith('>'):
- addrs.append(param[1:-1])
- # Uniquify
- rtnaddrs = {}
- for a in addrs:
- if a is not None:
- realname, a = parseaddr(a)
- rtnaddrs[a] = True
- return rtnaddrs.keys()
-
-
-
-def process(msg):
- # A DSN has been seen wrapped with a "legal disclaimer" by an outgoing MTA
- # in a multipart/mixed outer part.
- if msg.is_multipart() and msg.get_content_subtype() == 'mixed':
- msg = msg.get_payload()[0]
- # The above will suffice if the original message 'parts' were wrapped with
- # the disclaimer added, but the original DSN can be wrapped as a
- # message/rfc822 part. We need to test that too.
- if msg.is_multipart() and msg.get_content_type() == 'message/rfc822':
- msg = msg.get_payload()[0]
- # The report-type parameter should be "delivery-status", but it seems that
- # some DSN generating MTAs don't include this on the Content-Type: header,
- # so let's relax the test a bit.
- if not msg.is_multipart() or msg.get_content_subtype() <> 'report':
- return None
- return check(msg)
diff --git a/mailman/Bouncers/Exchange.py b/mailman/Bouncers/Exchange.py
deleted file mode 100644
index cf8beefce..000000000
--- a/mailman/Bouncers/Exchange.py
+++ /dev/null
@@ -1,48 +0,0 @@
-# Copyright (C) 2002-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 .
-
-"""Recognizes (some) Microsoft Exchange formats."""
-
-import re
-import email.Iterators
-
-scre = re.compile('did not reach the following recipient')
-ecre = re.compile('MSEXCH:')
-a1cre = re.compile('SMTP=(?P[^;]+); on ')
-a2cre = re.compile('(?P[^ ]+) on ')
-
-
-
-def process(msg):
- addrs = {}
- it = email.Iterators.body_line_iterator(msg)
- # Find the start line
- for line in it:
- if scre.search(line):
- break
- else:
- return []
- # Search each line until we hit the end line
- for line in it:
- if ecre.search(line):
- break
- mo = a1cre.search(line)
- if not mo:
- mo = a2cre.search(line)
- if mo:
- addrs[mo.group('addr')] = 1
- return addrs.keys()
diff --git a/mailman/Bouncers/Exim.py b/mailman/Bouncers/Exim.py
deleted file mode 100644
index 0f4e7f4cf..000000000
--- a/mailman/Bouncers/Exim.py
+++ /dev/null
@@ -1,31 +0,0 @@
-# Copyright (C) 1998-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 .
-
-"""Parse bounce messages generated by Exim.
-
-Exim adds an X-Failed-Recipients: header to bounce messages containing
-an `addresslist' of failed addresses.
-
-"""
-
-from email.Utils import getaddresses
-
-
-
-def process(msg):
- all = msg.get_all('x-failed-recipients', [])
- return [a for n, a in getaddresses(all)]
diff --git a/mailman/Bouncers/GroupWise.py b/mailman/Bouncers/GroupWise.py
deleted file mode 100644
index e74291217..000000000
--- a/mailman/Bouncers/GroupWise.py
+++ /dev/null
@@ -1,71 +0,0 @@
-# Copyright (C) 1998-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 .
-
-"""This appears to be the format for Novell GroupWise and NTMail
-
-X-Mailer: Novell GroupWise Internet Agent 5.5.3.1
-X-Mailer: NTMail v4.30.0012
-X-Mailer: Internet Mail Service (5.5.2653.19)
-"""
-
-import re
-from email.Message import Message
-from cStringIO import StringIO
-
-acre = re.compile(r'<(?P[^>]*)>')
-
-
-
-def find_textplain(msg):
- if msg.get_content_type() == 'text/plain':
- return msg
- if msg.is_multipart:
- for part in msg.get_payload():
- if not isinstance(part, Message):
- continue
- ret = find_textplain(part)
- if ret:
- return ret
- return None
-
-
-
-def process(msg):
- if msg.get_content_type() <> 'multipart/mixed' or not msg['x-mailer']:
- return None
- addrs = {}
- # find the first text/plain part in the message
- textplain = find_textplain(msg)
- if not textplain:
- return None
- body = StringIO(textplain.get_payload())
- while 1:
- line = body.readline()
- if not line:
- break
- mo = acre.search(line)
- if mo:
- addrs[mo.group('addr')] = 1
- elif '@' in line:
- i = line.find(' ')
- if i == 0:
- continue
- if i < 0:
- addrs[line] = 1
- else:
- addrs[line[:i]] = 1
- return addrs.keys()
diff --git a/mailman/Bouncers/LLNL.py b/mailman/Bouncers/LLNL.py
deleted file mode 100644
index cc1a08542..000000000
--- a/mailman/Bouncers/LLNL.py
+++ /dev/null
@@ -1,32 +0,0 @@
-# Copyright (C) 2001-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 .
-
-"""LLNL's custom Sendmail bounce message."""
-
-import re
-import email
-
-acre = re.compile(r',\s*(?P\S+@[^,]+),', re.IGNORECASE)
-
-
-
-def process(msg):
- for line in email.Iterators.body_line_iterator(msg):
- mo = acre.search(line)
- if mo:
- return [mo.group('addr')]
- return []
diff --git a/mailman/Bouncers/Microsoft.py b/mailman/Bouncers/Microsoft.py
deleted file mode 100644
index 98d27d4ee..000000000
--- a/mailman/Bouncers/Microsoft.py
+++ /dev/null
@@ -1,53 +0,0 @@
-# Copyright (C) 1998-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 .
-
-"""Microsoft's `SMTPSVC' nears I kin tell."""
-
-import re
-from cStringIO import StringIO
-
-scre = re.compile(r'transcript of session follows', re.IGNORECASE)
-
-
-
-def process(msg):
- if msg.get_content_type() <> 'multipart/mixed':
- return None
- # Find the first subpart, which has no MIME type
- try:
- subpart = msg.get_payload(0)
- except IndexError:
- # The message *looked* like a multipart but wasn't
- return None
- data = subpart.get_payload()
- if isinstance(data, list):
- # The message is a multi-multipart, so not a matching bounce
- return None
- body = StringIO(data)
- state = 0
- addrs = []
- while 1:
- line = body.readline()
- if not line:
- break
- if state == 0:
- if scre.search(line):
- state = 1
- if state == 1:
- if '@' in line:
- addrs.append(line)
- return addrs
diff --git a/mailman/Bouncers/Netscape.py b/mailman/Bouncers/Netscape.py
deleted file mode 100644
index 319329e84..000000000
--- a/mailman/Bouncers/Netscape.py
+++ /dev/null
@@ -1,89 +0,0 @@
-# Copyright (C) 1998-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 .
-
-"""Netscape Messaging Server bounce formats.
-
-I've seen at least one NMS server version 3.6 (envy.gmp.usyd.edu.au) bounce
-messages of this format. Bounces come in DSN MIME format, but don't include
-any -Recipient: headers. Gotta just parse the text :(
-
-NMS 4.1 (dfw-smtpin1.email.verio.net) seems even worse, but we'll try to
-decipher the format here too.
-
-"""
-
-import re
-from cStringIO import StringIO
-
-pcre = re.compile(
- r'This Message was undeliverable due to the following reason:',
- re.IGNORECASE)
-
-acre = re.compile(
- r'(?Pplease reply to)?.*<(?P[^>]*)>',
- re.IGNORECASE)
-
-
-
-def flatten(msg, leaves):
- # give us all the leaf (non-multipart) subparts
- if msg.is_multipart():
- for part in msg.get_payload():
- flatten(part, leaves)
- else:
- leaves.append(msg)
-
-
-
-def process(msg):
- # Sigh. Some show NMS 3.6's show
- # multipart/report; report-type=delivery-status
- # and some show
- # multipart/mixed;
- if not msg.is_multipart():
- return None
- # We're looking for a text/plain subpart occuring before a
- # message/delivery-status subpart.
- plainmsg = None
- leaves = []
- flatten(msg, leaves)
- for i, subpart in zip(range(len(leaves)-1), leaves):
- if subpart.get_content_type() == 'text/plain':
- plainmsg = subpart
- break
- if not plainmsg:
- return None
- # Total guesswork, based on captured examples...
- body = StringIO(plainmsg.get_payload())
- addrs = []
- while 1:
- line = body.readline()
- if not line:
- break
- mo = pcre.search(line)
- if mo:
- # We found a bounce section, but I have no idea what the official
- # format inside here is. :( We'll just search for
- # strings.
- while 1:
- line = body.readline()
- if not line:
- break
- mo = acre.search(line)
- if mo and not mo.group('reply'):
- addrs.append(mo.group('addr'))
- return addrs
diff --git a/mailman/Bouncers/Postfix.py b/mailman/Bouncers/Postfix.py
deleted file mode 100644
index cfc97a05e..000000000
--- a/mailman/Bouncers/Postfix.py
+++ /dev/null
@@ -1,86 +0,0 @@
-# Copyright (C) 1998-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 .
-
-"""Parse bounce messages generated by Postfix.
-
-This also matches something called `Keftamail' which looks just like Postfix
-bounces with the word Postfix scratched out and the word `Keftamail' written
-in in crayon.
-
-It also matches something claiming to be `The BNS Postfix program', and
-`SMTP_Gateway'. Everybody's gotta be different, huh?
-"""
-
-import re
-from cStringIO import StringIO
-
-
-
-def flatten(msg, leaves):
- # give us all the leaf (non-multipart) subparts
- if msg.is_multipart():
- for part in msg.get_payload():
- flatten(part, leaves)
- else:
- leaves.append(msg)
-
-
-
-# are these heuristics correct or guaranteed?
-pcre = re.compile(r'[ \t]*the\s*(bns)?\s*(postfix|keftamail|smtp_gateway)',
- re.IGNORECASE)
-rcre = re.compile(r'failure reason:$', re.IGNORECASE)
-acre = re.compile(r'<(?P[^>]*)>:')
-
-def findaddr(msg):
- addrs = []
- body = StringIO(msg.get_payload())
- # simple state machine
- # 0 == nothing found
- # 1 == salutation found
- state = 0
- while 1:
- line = body.readline()
- if not line:
- break
- # preserve leading whitespace
- line = line.rstrip()
- # yes use match to match at beginning of string
- if state == 0 and (pcre.match(line) or rcre.match(line)):
- state = 1
- elif state == 1 and line:
- mo = acre.search(line)
- if mo:
- addrs.append(mo.group('addr'))
- # probably a continuation line
- return addrs
-
-
-
-def process(msg):
- if msg.get_content_type() not in ('multipart/mixed', 'multipart/report'):
- return None
- # We're looking for the plain/text subpart with a Content-Description: of
- # `notification'.
- leaves = []
- flatten(msg, leaves)
- for subpart in leaves:
- if subpart.get_content_type() == 'text/plain' and \
- subpart.get('content-description', '').lower() == 'notification':
- # then...
- return findaddr(subpart)
- return None
diff --git a/mailman/Bouncers/Qmail.py b/mailman/Bouncers/Qmail.py
deleted file mode 100644
index 2431da653..000000000
--- a/mailman/Bouncers/Qmail.py
+++ /dev/null
@@ -1,72 +0,0 @@
-# Copyright (C) 1998-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 .
-
-"""Parse bounce messages generated by qmail.
-
-Qmail actually has a standard, called QSBMF (qmail-send bounce message
-format), as described in
-
- http://cr.yp.to/proto/qsbmf.txt
-
-This module should be conformant.
-
-"""
-
-import re
-import email.Iterators
-
-# Other (non-standard?) intros have been observed in the wild.
-introtags = [
- 'Hi. This is the',
- "We're sorry. There's a problem",
- 'Check your send e-mail address.',
- 'This is the mail delivery agent at',
- 'Unfortunately, your mail was not delivered'
- ]
-acre = re.compile(r'<(?P[^>]*)>:')
-
-
-
-def process(msg):
- addrs = []
- # simple state machine
- # 0 = nothing seen yet
- # 1 = intro paragraph seen
- # 2 = recip paragraphs seen
- state = 0
- for line in email.Iterators.body_line_iterator(msg):
- line = line.strip()
- if state == 0:
- for introtag in introtags:
- if line.startswith(introtag):
- state = 1
- break
- elif state == 1 and not line:
- # Looking for the end of the intro paragraph
- state = 2
- elif state == 2:
- if line.startswith('-'):
- # We're looking at the break paragraph, so we're done
- break
- # At this point we know we must be looking at a recipient
- # paragraph
- mo = acre.match(line)
- if mo:
- addrs.append(mo.group('addr'))
- # Otherwise, it must be a continuation line, so just ignore it
- # Not looking at anything in particular
- return addrs
diff --git a/mailman/Bouncers/SMTP32.py b/mailman/Bouncers/SMTP32.py
deleted file mode 100644
index a7fff2ed3..000000000
--- a/mailman/Bouncers/SMTP32.py
+++ /dev/null
@@ -1,60 +0,0 @@
-# Copyright (C) 1998-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 .
-
-"""Something which claims
-X-Mailer:
-
-What the heck is this thing? Here's a recent host:
-
-% telnet 207.51.255.218 smtp
-Trying 207.51.255.218...
-Connected to 207.51.255.218.
-Escape character is '^]'.
-220 X1 NT-ESMTP Server 208.24.118.205 (IMail 6.00 45595-15)
-
-"""
-
-import re
-import email
-
-ecre = re.compile('original message follows', re.IGNORECASE)
-acre = re.compile(r'''
- ( # several different prefixes
- user\ mailbox[^:]*: # have been spotted in the
- |delivery\ failed[^:]*: # wild...
- |unknown\ user[^:]*:
- |undeliverable\ +to
- |delivery\ userid[^:]*:
- )
- \s* # space separator
- (?P[^\s]*) # and finally, the address
- ''', re.IGNORECASE | re.VERBOSE)
-
-
-
-def process(msg):
- mailer = msg.get('x-mailer', '')
- if not mailer.startswith('.
-
-"""Recognizes simple heuristically delimited bounces."""
-
-import re
-import email.Iterators
-
-
-
-def _c(pattern):
- return re.compile(pattern, re.IGNORECASE)
-
-# This is a list of tuples of the form
-#
-# (start cre, end cre, address cre)
-#
-# where `cre' means compiled regular expression, start is the line just before
-# the bouncing address block, end is the line just after the bouncing address
-# block, and address cre is the regexp that will recognize the addresses. It
-# must have a group called `addr' which will contain exactly and only the
-# address that bounced.
-PATTERNS = [
- # sdm.de
- (_c('here is your list of failed recipients'),
- _c('here is your returned mail'),
- _c(r'<(?P[^>]*)>')),
- # sz-sb.de, corridor.com, nfg.nl
- (_c('the following addresses had'),
- _c('transcript of session follows'),
- _c(r'<(?P[^>]*)>|\(expanded from: (?P[^>)]*)>?\)')),
- # robanal.demon.co.uk
- (_c('this message was created automatically by mail delivery software'),
- _c('original message follows'),
- _c('rcpt to:\s*<(?P[^>]*)>')),
- # s1.com (InterScan E-Mail VirusWall NT ???)
- (_c('message from interscan e-mail viruswall nt'),
- _c('end of message'),
- _c('rcpt to:\s*<(?P[^>]*)>')),
- # Smail
- (_c('failed addresses follow:'),
- _c('message text follows:'),
- _c(r'\s*(?P\S+@\S+)')),
- # newmail.ru
- (_c('This is the machine generated message from mail service.'),
- _c('--- Below the next line is a copy of the message.'),
- _c('<(?P[^>]*)>')),
- # turbosport.com runs something called `MDaemon 3.5.2' ???
- (_c('The following addresses did NOT receive a copy of your message:'),
- _c('--- Session Transcript ---'),
- _c('[>]\s*(?P.*)$')),
- # usa.net
- (_c('Intended recipient:\s*(?P.*)$'),
- _c('--------RETURNED MAIL FOLLOWS--------'),
- _c('Intended recipient:\s*(?P.*)$')),
- # hotpop.com
- (_c('Undeliverable Address:\s*(?P.*)$'),
- _c('Original message attached'),
- _c('Undeliverable Address:\s*(?P.*)$')),
- # Another demon.co.uk format
- (_c('This message was created automatically by mail delivery'),
- _c('^---- START OF RETURNED MESSAGE ----'),
- _c("addressed to '(?P[^']*)'")),
- # Prodigy.net full mailbox
- (_c("User's mailbox is full:"),
- _c('Unable to deliver mail.'),
- _c("User's mailbox is full:\s*<(?P[^>]*)>")),
- # Microsoft SMTPSVC
- (_c('The email below could not be delivered to the following user:'),
- _c('Old message:'),
- _c('<(?P[^>]*)>')),
- # Yahoo on behalf of other domains like sbcglobal.net
- (_c('Unable to deliver message to the following address\(es\)\.'),
- _c('--- Original message follows\.'),
- _c('<(?P[^>]*)>:')),
- # googlemail.com
- (_c('Delivery to the following recipient failed'),
- _c('----- Original message -----'),
- _c('^\s*(?P[^\s@]+@[^\s@]+)\s*$')),
- # kundenserver.de
- (_c('A message that you sent could not be delivered'),
- _c('^---'),
- _c('<(?P[^>]*)>')),
- # another kundenserver.de
- (_c('A message that you sent could not be delivered'),
- _c('^---'),
- _c('^(?P[^\s@]+@[^\s@:]+):')),
- # thehartford.com
- (_c('Delivery to the following recipients failed'),
- # this one may or may not have the original message, but there's nothing
- # unique to stop on, so stop on the first line of at least 3 characters
- # that doesn't start with 'D' (to not stop immediately) and has no '@'.
- _c('^[^D][^@]{2,}$'),
- _c('^\s*(?P[^\s@]+@[^\s@]+)\s*$')),
- # and another thehartfod.com/hartfordlife.com
- (_c('^Your message\s*$'),
- _c('^because:'),
- _c('^\s*(?P[^\s@]+@[^\s@]+)\s*$')),
- # kviv.be (InterScan NT)
- (_c('^Unable to deliver message to'),
- _c(r'\*+\s+End of message\s+\*+'),
- _c('<(?P[^>]*)>')),
- # earthlink.net supported domains
- (_c('^Sorry, unable to deliver your message to'),
- _c('^A copy of the original message'),
- _c('\s*(?P[^\s@]+@[^\s@]+)\s+')),
- # ademe.fr
- (_c('^A message could not be delivered to:'),
- _c('^Subject:'),
- _c('^\s*(?P[^\s@]+@[^\s@]+)\s*$')),
- # andrew.ac.jp
- (_c('^Invalid final delivery userid:'),
- _c('^Original message follows.'),
- _c('\s*(?P[^\s@]+@[^\s@]+)\s*$')),
- # E500_SMTP_Mail_Service@lerctr.org
- (_c('------ Failed Recipients ------'),
- _c('-------- Returned Mail --------'),
- _c('<(?P[^>]*)>')),
- # cynergycom.net
- (_c('A message that you sent could not be delivered'),
- _c('^---'),
- _c('(?P[^\s@]+@[^\s@)]+)')),
- # LSMTP for Windows
- (_c('^--> Error description:\s*$'),
- _c('^Error-End:'),
- _c('^Error-for:\s+(?P[^\s@]+@[^\s@]+)')),
- # Qmail with a tri-language intro beginning in spanish
- (_c('Your message could not be delivered'),
- _c('^-'),
- _c('<(?P[^>]*)>:')),
- # socgen.com
- (_c('Your message could not be delivered to'),
- _c('^\s*$'),
- _c('(?P[^\s@]+@[^\s@]+)')),
- # dadoservice.it
- (_c('Your message has encountered delivery problems'),
- _c('Your message reads'),
- _c('addressed to\s*(?P[^\s@]+@[^\s@)]+)')),
- # gomaps.com
- (_c('Did not reach the following recipient'),
- _c('^\s*$'),
- _c('\s(?P[^\s@]+@[^\s@]+)')),
- # EYOU MTA SYSTEM
- (_c('This is the deliver program at'),
- _c('^-'),
- _c('^(?P[^\s@]+@[^\s@<>]+)')),
- # A non-standard qmail at ieo.it
- (_c('this is the email server at'),
- _c('^-'),
- _c('\s(?P[^\s@]+@[^\s@]+)[\s,]')),
- # pla.net.py (MDaemon.PRO ?)
- (_c('- no such user here'),
- _c('There is no user'),
- _c('^(?P[^\s@]+@[^\s@]+)\s')),
- # Next one goes here...
- ]
-
-
-
-def process(msg, patterns=None):
- if patterns is None:
- patterns = PATTERNS
- # simple state machine
- # 0 = nothing seen yet
- # 1 = intro seen
- addrs = {}
- # MAS: This is a mess. The outer loop used to be over the message
- # so we only looped through the message once. Looping through the
- # message for each set of patterns is obviously way more work, but
- # if we don't do it, problems arise because scre from the wrong
- # pattern set matches first and then acre doesn't match. The
- # alternative is to split things into separate modules, but then
- # we process the message multiple times anyway.
- for scre, ecre, acre in patterns:
- state = 0
- for line in email.Iterators.body_line_iterator(msg):
- if state == 0:
- if scre.search(line):
- state = 1
- if state == 1:
- mo = acre.search(line)
- if mo:
- addr = mo.group('addr')
- if addr:
- addrs[mo.group('addr')] = 1
- elif ecre.search(line):
- break
- if addrs:
- break
- return addrs.keys()
diff --git a/mailman/Bouncers/SimpleWarning.py b/mailman/Bouncers/SimpleWarning.py
deleted file mode 100644
index ab18d2530..000000000
--- a/mailman/Bouncers/SimpleWarning.py
+++ /dev/null
@@ -1,62 +0,0 @@
-# Copyright (C) 2001-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 .
-
-"""Recognizes simple heuristically delimited warnings."""
-
-from mailman.Bouncers.BouncerAPI import Stop
-from mailman.Bouncers.SimpleMatch import _c
-from mailman.Bouncers.SimpleMatch import process as _process
-
-
-
-# This is a list of tuples of the form
-#
-# (start cre, end cre, address cre)
-#
-# where `cre' means compiled regular expression, start is the line just before
-# the bouncing address block, end is the line just after the bouncing address
-# block, and address cre is the regexp that will recognize the addresses. It
-# must have a group called `addr' which will contain exactly and only the
-# address that bounced.
-patterns = [
- # pop3.pta.lia.net
- (_c('The address to which the message has not yet been delivered is'),
- _c('No action is required on your part'),
- _c(r'\s*(?P\S+@\S+)\s*')),
- # This is from MessageSwitch. It is a kludge because the text that
- # identifies it as a warning only comes after the address. We can't
- # use ecre, because it really isn't significant, so we fake it. Once
- # we see the start, we know it's a warning, and we're going to return
- # Stop anyway, so we match anything for the address and end.
- (_c('This is just a warning, you do not need to take any action'),
- _c('.+'),
- _c('(?P.+)')),
- # Symantec_AntiVirus_for_SMTP_Gateways - see comments for MessageSwitch
- (_c('Delivery attempts will continue to be made'),
- _c('.+'),
- _c('(?P.+)')),
- # Next one goes here...
- ]
-
-
-
-def process(msg):
- if _process(msg, patterns):
- # It's a recognized warning so stop now
- return Stop
- else:
- return []
diff --git a/mailman/Bouncers/Sina.py b/mailman/Bouncers/Sina.py
deleted file mode 100644
index a6b8e0911..000000000
--- a/mailman/Bouncers/Sina.py
+++ /dev/null
@@ -1,48 +0,0 @@
-# Copyright (C) 2002-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 .
-
-"""sina.com bounces"""
-
-import re
-from email import Iterators
-
-acre = re.compile(r'<(?P[^>]*)>')
-
-
-
-def process(msg):
- if msg.get('from', '').lower() <> 'mailer-daemon@sina.com':
- print 'out 1'
- return []
- if not msg.is_multipart():
- print 'out 2'
- return []
- # The interesting bits are in the first text/plain multipart
- part = None
- try:
- part = msg.get_payload(0)
- except IndexError:
- pass
- if not part:
- print 'out 3'
- return []
- addrs = {}
- for line in Iterators.body_line_iterator(part):
- mo = acre.match(line)
- if mo:
- addrs[mo.group('addr')] = 1
- return addrs.keys()
diff --git a/mailman/Bouncers/Yahoo.py b/mailman/Bouncers/Yahoo.py
deleted file mode 100644
index b0480b818..000000000
--- a/mailman/Bouncers/Yahoo.py
+++ /dev/null
@@ -1,54 +0,0 @@
-# Copyright (C) 1998-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 .
-
-"""Yahoo! has its own weird format for bounces."""
-
-import re
-import email
-from email.Utils import parseaddr
-
-tcre = re.compile(r'message\s+from\s+yahoo\.\S+', re.IGNORECASE)
-acre = re.compile(r'<(?P[^>]*)>:')
-ecre = re.compile(r'--- Original message follows')
-
-
-
-def process(msg):
- # Yahoo! bounces seem to have a known subject value and something called
- # an x-uidl: header, the value of which seems unimportant.
- sender = parseaddr(msg.get('from', '').lower())[1] or ''
- if not sender.startswith('mailer-daemon@yahoo'):
- return None
- addrs = []
- # simple state machine
- # 0 == nothing seen
- # 1 == tag line seen
- state = 0
- for line in email.Iterators.body_line_iterator(msg):
- line = line.strip()
- if state == 0 and tcre.match(line):
- state = 1
- elif state == 1:
- mo = acre.match(line)
- if mo:
- addrs.append(mo.group('addr'))
- continue
- mo = ecre.match(line)
- if mo:
- # we're at the end of the error response
- break
- return addrs
diff --git a/mailman/Bouncers/Yale.py b/mailman/Bouncers/Yale.py
deleted file mode 100644
index 956dfb838..000000000
--- a/mailman/Bouncers/Yale.py
+++ /dev/null
@@ -1,80 +0,0 @@
-# Copyright (C) 2000-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 .
-
-"""Yale's mail server is pretty dumb.
-
-Its reports include the end user's name, but not the full domain. I think we
-can usually guess it right anyway. This is completely based on examination of
-the corpse, and is subject to failure whenever Yale even slightly changes
-their MTA. :(
-
-"""
-
-import re
-from cStringIO import StringIO
-from email.Utils import getaddresses
-
-scre = re.compile(r'Message not delivered to the following', re.IGNORECASE)
-ecre = re.compile(r'Error Detail', re.IGNORECASE)
-acre = re.compile(r'\s+(?P\S+)\s+')
-
-
-
-def process(msg):
- if msg.is_multipart():
- return None
- try:
- whofrom = getaddresses([msg.get('from', '')])[0][1]
- if not whofrom:
- return None
- username, domain = whofrom.split('@', 1)
- except (IndexError, ValueError):
- return None
- if username.lower() <> 'mailer-daemon':
- return None
- parts = domain.split('.')
- parts.reverse()
- for part1, part2 in zip(parts, ('edu', 'yale')):
- if part1 <> part2:
- return None
- # Okay, we've established that the bounce came from the mailer-daemon at
- # yale.edu. Let's look for a name, and then guess the relevant domains.
- names = {}
- body = StringIO(msg.get_payload())
- state = 0
- # simple state machine
- # 0 == init
- # 1 == intro found
- while 1:
- line = body.readline()
- if not line:
- break
- if state == 0 and scre.search(line):
- state = 1
- elif state == 1 and ecre.search(line):
- break
- elif state == 1:
- mo = acre.search(line)
- if mo:
- names[mo.group('addr')] = 1
- # Now we have a bunch of names, these are either @yale.edu or
- # @cs.yale.edu. Add them both.
- addrs = []
- for name in names.keys():
- addrs.append(name + '@yale.edu')
- addrs.append(name + '@cs.yale.edu')
- return addrs
diff --git a/mailman/Bouncers/__init__.py b/mailman/Bouncers/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/mailman/Mailbox.py b/mailman/Mailbox.py
deleted file mode 100644
index 3a2f079c4..000000000
--- a/mailman/Mailbox.py
+++ /dev/null
@@ -1,106 +0,0 @@
-# Copyright (C) 1998-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 .
-
-"""Extend mailbox.UnixMailbox.
-"""
-
-import sys
-import email
-import mailbox
-
-from email.errors import MessageParseError
-from email.generator import Generator
-
-from mailman.Message import Message
-from mailman.config import config
-
-
-
-def _safeparser(fp):
- try:
- return email.message_from_file(fp, Message)
- except MessageParseError:
- # Don't return None since that will stop a mailbox iterator
- return ''
-
-
-
-class Mailbox(mailbox.PortableUnixMailbox):
- def __init__(self, fp):
- mailbox.PortableUnixMailbox.__init__(self, fp, _safeparser)
-
- # msg should be an rfc822 message or a subclass.
- def AppendMessage(self, msg):
- # Check the last character of the file and write a newline if it isn't
- # a newline (but not at the beginning of an empty file).
- try:
- self.fp.seek(-1, 2)
- except IOError, e:
- # Assume the file is empty. We can't portably test the error code
- # returned, since it differs per platform.
- pass
- else:
- if self.fp.read(1) <> '\n':
- self.fp.write('\n')
- # Seek to the last char of the mailbox
- self.fp.seek(1, 2)
- # Create a Generator instance to write the message to the file
- g = Generator(self.fp)
- g.flatten(msg, unixfrom=True)
- # Add one more trailing newline for separation with the next message
- # to be appended to the mbox.
- print >> self.fp
-
-
-
-# This stuff is used by pipermail.py:processUnixMailbox(). It provides an
-# opportunity for the built-in archiver to scrub archived messages of nasty
-# things like attachments and such...
-def _archfactory(mailbox):
- # The factory gets a file object, but it also needs to have a MailList
- # object, so the clearest way to do this is to build a factory
- # function that has a reference to the mailbox object, which in turn holds
- # a reference to the mailing list. Nested scopes would help here, BTW,
- # but we can't rely on them being around (e.g. Python 2.0).
- def scrubber(fp, mailbox=mailbox):
- msg = _safeparser(fp)
- if msg == '':
- return msg
- return mailbox.scrub(msg)
- return scrubber
-
-
-class ArchiverMailbox(Mailbox):
- # This is a derived class which is instantiated with a reference to the
- # MailList object. It is build such that the factory calls back into its
- # scrub() method, giving the scrubber module a chance to do its thing
- # before the message is archived.
- def __init__(self, fp, mlist):
- scrubber_module = config.scrubber.archive_scrubber
- if scrubber_module:
- __import__(scrubber_module)
- self._scrubber = sys.modules[scrubber_module].process
- else:
- self._scrubber = None
- self._mlist = mlist
- mailbox.PortableUnixMailbox.__init__(self, fp, _archfactory(self))
-
- def scrub(self, msg):
- if self._scrubber:
- return self._scrubber(self._mlist, msg)
- else:
- return msg
diff --git a/mailman/Message.py b/mailman/Message.py
deleted file mode 100644
index ac41a758c..000000000
--- a/mailman/Message.py
+++ /dev/null
@@ -1,297 +0,0 @@
-# Copyright (C) 1998-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 .
-
-"""Standard Mailman message object.
-
-This is a subclass of email.message.Message but provides a slightly extended
-interface which is more convenient for use inside Mailman.
-"""
-
-import re
-import email
-import email.message
-import email.utils
-
-from email.charset import Charset
-from email.header import Header
-from lazr.config import as_boolean
-
-from mailman import Utils
-from mailman.config import config
-
-COMMASPACE = ', '
-
-mo = re.match(r'([\d.]+)', email.__version__)
-VERSION = tuple(int(s) for s in mo.group().split('.'))
-
-
-
-class Message(email.message.Message):
- def __init__(self):
- # We need a version number so that we can optimize __setstate__()
- self.__version__ = VERSION
- email.message.Message.__init__(self)
-
- def __getitem__(self, key):
- value = email.message.Message.__getitem__(self, key)
- if isinstance(value, str):
- return unicode(value, 'ascii')
- return value
-
- def get(self, name, failobj=None):
- value = email.message.Message.get(self, name, failobj)
- if isinstance(value, str):
- return unicode(value, 'ascii')
- return value
-
- def get_all(self, name, failobj=None):
- missing = object()
- all_values = email.message.Message.get_all(self, name, missing)
- if all_values is missing:
- return failobj
- return [(unicode(value, 'ascii') if isinstance(value, str) else value)
- for value in all_values]
-
- # BAW: For debugging w/ bin/dumpdb. Apparently pprint uses repr.
- def __repr__(self):
- return self.__str__()
-
- def __setstate__(self, d):
- # The base class attributes have changed over time. Which could
- # affect Mailman if messages are sitting in the queue at the time of
- # upgrading the email package. We shouldn't burden email with this,
- # so we handle schema updates here.
- self.__dict__ = d
- # We know that email 2.4.3 is up-to-date
- version = d.get('__version__', (0, 0, 0))
- d['__version__'] = VERSION
- if version >= VERSION:
- return
- # Messages grew a _charset attribute between email version 0.97 and 1.1
- if not d.has_key('_charset'):
- self._charset = None
- # Messages grew a _default_type attribute between v2.1 and v2.2
- if not d.has_key('_default_type'):
- # We really have no idea whether this message object is contained
- # inside a multipart/digest or not, so I think this is the best we
- # can do.
- self._default_type = 'text/plain'
- # Header instances used to allow both strings and Charsets in their
- # _chunks, but by email 2.4.3 now it's just Charsets.
- headers = []
- hchanged = 0
- for k, v in self._headers:
- if isinstance(v, Header):
- chunks = []
- cchanged = 0
- for s, charset in v._chunks:
- if isinstance(charset, str):
- charset = Charset(charset)
- cchanged = 1
- chunks.append((s, charset))
- if cchanged:
- v._chunks = chunks
- hchanged = 1
- headers.append((k, v))
- if hchanged:
- self._headers = headers
-
- # I think this method ought to eventually be deprecated
- def get_sender(self):
- """Return the address considered to be the author of the email.
-
- This can return either the From: header, the Sender: header or the
- envelope header (a.k.a. the unixfrom header). The first non-empty
- header value found is returned. However the search order is
- determined by the following:
-
- - If config.mailman.use_envelope_sender is true, then the search order
- is Sender:, From:, unixfrom
-
- - Otherwise, the search order is From:, Sender:, unixfrom
-
- unixfrom should never be empty. The return address is always
- lower cased.
-
- This method differs from get_senders() in that it returns one and only
- one address, and uses a different search order.
- """
- senderfirst = as_boolean(config.mailman.use_envelope_sender)
- if senderfirst:
- headers = ('sender', 'from')
- else:
- headers = ('from', 'sender')
- for h in headers:
- # Use only the first occurrance of Sender: or From:, although it's
- # not likely there will be more than one.
- fieldval = self[h]
- if not fieldval:
- continue
- addrs = email.utils.getaddresses([fieldval])
- try:
- realname, address = addrs[0]
- except IndexError:
- continue
- if address:
- break
- else:
- # We didn't find a non-empty header, so let's fall back to the
- # unixfrom address. This should never be empty, but if it ever
- # is, it's probably a Really Bad Thing. Further, we just assume
- # that if the unixfrom exists, the second field is the address.
- unixfrom = self.get_unixfrom()
- if unixfrom:
- address = unixfrom.split()[1]
- else:
- # TBD: now what?!
- address = ''
- return address.lower()
-
- def get_senders(self):
- """Return a list of addresses representing the author of the email.
-
- The list will contain the following addresses (in order)
- depending on availability:
-
- 1. From:
- 2. unixfrom (From_)
- 3. Reply-To:
- 4. Sender:
-
- The return addresses are always lower cased.
- """
- pairs = []
- for header in config.mailman.sender_headers.split():
- header = header.lower()
- if header == 'from_':
- # get_unixfrom() returns None if there's no envelope
- unix_from = self.get_unixfrom()
- fieldval = (unix_from if unix_from is not None else '')
- try:
- pairs.append(('', fieldval.split()[1]))
- except IndexError:
- # Ignore badly formatted unixfroms
- pass
- else:
- fieldvals = self.get_all(header)
- if fieldvals:
- pairs.extend(email.utils.getaddresses(fieldvals))
- authors = []
- for pair in pairs:
- address = pair[1]
- if address is not None:
- address = address.lower()
- authors.append(address)
- return authors
-
- def get_filename(self, failobj=None):
- """Some MUA have bugs in RFC2231 filename encoding and cause
- Mailman to stop delivery in Scrubber.py (called from ToDigest.py).
- """
- try:
- filename = email.message.Message.get_filename(self, failobj)
- return filename
- except (UnicodeError, LookupError, ValueError):
- return failobj
-
-
-
-class UserNotification(Message):
- """Class for internally crafted messages."""
-
- def __init__(self, recip, sender, subject=None, text=None, lang=None):
- Message.__init__(self)
- charset = 'us-ascii'
- if lang is not None:
- charset = Utils.GetCharSet(lang)
- if text is not None:
- self.set_payload(text.encode(charset), charset)
- if subject is None:
- subject = '(no subject)'
- self['Subject'] = Header(subject.encode(charset), charset,
- header_name='Subject', errors='replace')
- self['From'] = sender
- if isinstance(recip, list):
- self['To'] = COMMASPACE.join(recip)
- self.recips = recip
- else:
- self['To'] = recip
- self.recips = [recip]
-
- def send(self, mlist, **_kws):
- """Sends the message by enqueuing it to the 'virgin' queue.
-
- This is used for all internally crafted messages.
- """
- # Since we're crafting the message from whole cloth, let's make sure
- # this message has a Message-ID.
- if 'message-id' not in self:
- self['Message-ID'] = email.utils.make_msgid()
- # Ditto for Date: as required by RFC 2822.
- if 'date' not in self:
- self['Date'] = email.utils.formatdate(localtime=True)
- # UserNotifications are typically for admin messages, and for messages
- # other than list explosions. Send these out as Precedence: bulk, but
- # don't override an existing Precedence: header.
- if 'precedence' not in self:
- self['Precedence'] = 'bulk'
- self._enqueue(mlist, **_kws)
-
- def _enqueue(self, mlist, **_kws):
- # Not imported at module scope to avoid import loop
- virginq = config.switchboards['virgin']
- # The message metadata better have a 'recip' attribute.
- enqueue_kws = dict(
- recips=self.recips,
- nodecorate=True,
- reduced_list_headers=True,
- )
- if mlist is not None:
- enqueue_kws['listname'] = mlist.fqdn_listname
- enqueue_kws.update(_kws)
- virginq.enqueue(self, **enqueue_kws)
-
-
-
-class OwnerNotification(UserNotification):
- """Like user notifications, but this message goes to the list owners."""
-
- def __init__(self, mlist, subject=None, text=None, tomoderators=True):
- if tomoderators:
- roster = mlist.moderators
- else:
- roster = mlist.owners
- recips = [address.address for address in roster.addresses]
- sender = config.mailman.site_owner
- lang = mlist.preferred_language
- UserNotification.__init__(self, recips, sender, subject, text, lang)
- # Hack the To header to look like it's going to the -owner address
- del self['to']
- self['To'] = mlist.owner_address
- self._sender = sender
-
- def _enqueue(self, mlist, **_kws):
- # Not imported at module scope to avoid import loop
- virginq = config.switchboards['virgin']
- # The message metadata better have a `recip' attribute
- virginq.enqueue(self,
- listname=mlist.fqdn_listname,
- recips=self.recips,
- nodecorate=True,
- reduced_list_headers=True,
- envsender=self._sender,
- **_kws)
diff --git a/mailman/Utils.py b/mailman/Utils.py
deleted file mode 100644
index 9946273c9..000000000
--- a/mailman/Utils.py
+++ /dev/null
@@ -1,702 +0,0 @@
-# Copyright (C) 1998-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 .
-
-"""Miscellaneous essential routines.
-
-This includes actual message transmission routines, address checking and
-message and address munging, a handy-dandy routine to map a function on all
-the mailing lists, and whatever else doesn't belong elsewhere.
-"""
-
-import os
-import re
-import cgi
-import time
-import errno
-import base64
-import random
-import logging
-import htmlentitydefs
-import email.Header
-import email.Iterators
-
-from email.Errors import HeaderParseError
-from lazr.config import as_boolean
-from string import ascii_letters, digits, whitespace
-
-import mailman.templates
-
-from mailman import passwords
-from mailman.config import config
-from mailman.core import errors
-from mailman.utilities.string import expand
-
-
-AT = '@'
-CR = '\r'
-DOT = '.'
-EMPTYSTRING = ''
-IDENTCHARS = ascii_letters + digits + '_'
-NL = '\n'
-UEMPTYSTRING = u''
-TEMPLATE_DIR = os.path.dirname(mailman.templates.__file__)
-
-# Search for $(identifier)s strings, except that the trailing s is optional,
-# since that's a common mistake
-cre = re.compile(r'%\(([_a-z]\w*?)\)s?', re.IGNORECASE)
-# Search for $$, $identifier, or ${identifier}
-dre = re.compile(r'(\${2})|\$([_a-z]\w*)|\${([_a-z]\w*)}', re.IGNORECASE)
-
-log = logging.getLogger('mailman.error')
-
-
-
-# a much more naive implementation than say, Emacs's fill-paragraph!
-def wrap(text, column=70, honor_leading_ws=True):
- """Wrap and fill the text to the specified column.
-
- Wrapping is always in effect, although if it is not possible to wrap a
- line (because some word is longer than `column' characters) the line is
- broken at the next available whitespace boundary. Paragraphs are also
- always filled, unless honor_leading_ws is true and the line begins with
- whitespace. This is the algorithm that the Python FAQ wizard uses, and
- seems like a good compromise.
-
- """
- wrapped = ''
- # first split the text into paragraphs, defined as a blank line
- paras = re.split('\n\n', text)
- for para in paras:
- # fill
- lines = []
- fillprev = False
- for line in para.split(NL):
- if not line:
- lines.append(line)
- continue
- if honor_leading_ws and line[0] in whitespace:
- fillthis = False
- else:
- fillthis = True
- if fillprev and fillthis:
- # if the previous line should be filled, then just append a
- # single space, and the rest of the current line
- lines[-1] = lines[-1].rstrip() + ' ' + line
- else:
- # no fill, i.e. retain newline
- lines.append(line)
- fillprev = fillthis
- # wrap each line
- for text in lines:
- while text:
- if len(text) <= column:
- line = text
- text = ''
- else:
- bol = column
- # find the last whitespace character
- while bol > 0 and text[bol] not in whitespace:
- bol -= 1
- # now find the last non-whitespace character
- eol = bol
- while eol > 0 and text[eol] in whitespace:
- eol -= 1
- # watch out for text that's longer than the column width
- if eol == 0:
- # break on whitespace after column
- eol = column
- while eol < len(text) and text[eol] not in whitespace:
- eol += 1
- bol = eol
- while bol < len(text) and text[bol] in whitespace:
- bol += 1
- bol -= 1
- line = text[:eol+1] + '\n'
- # find the next non-whitespace character
- bol += 1
- while bol < len(text) and text[bol] in whitespace:
- bol += 1
- text = text[bol:]
- wrapped += line
- wrapped += '\n'
- # end while text
- wrapped += '\n'
- # end for text in lines
- # the last two newlines are bogus
- return wrapped[:-2]
-
-
-
-def QuotePeriods(text):
- JOINER = '\n .\n'
- SEP = '\n.\n'
- return JOINER.join(text.split(SEP))
-
-
-# This takes an email address, and returns a tuple containing (user,host)
-def ParseEmail(email):
- user = None
- domain = None
- email = email.lower()
- at_sign = email.find('@')
- if at_sign < 1:
- return email, None
- user = email[:at_sign]
- rest = email[at_sign+1:]
- domain = rest.split('.')
- return user, domain
-
-
-def LCDomain(addr):
- "returns the address with the domain part lowercased"
- atind = addr.find('@')
- if atind == -1: # no domain part
- return addr
- return addr[:atind] + '@' + addr[atind+1:].lower()
-
-
-# TBD: what other characters should be disallowed?
-_badchars = re.compile(r'[][()<>|;^,\000-\037\177-\377]')
-
-def ValidateEmail(s):
- """Verify that the an email address isn't grossly evil."""
- # Pretty minimal, cheesy check. We could do better...
- if not s or ' ' in s:
- raise errors.InvalidEmailAddress(repr(s))
- if _badchars.search(s) or s[0] == '-':
- raise errors.InvalidEmailAddress(repr(s))
- user, domain_parts = ParseEmail(s)
- # Local, unqualified addresses are not allowed.
- if not domain_parts:
- raise errors.InvalidEmailAddress(repr(s))
- if len(domain_parts) < 2:
- raise errors.InvalidEmailAddress(repr(s))
-
-
-
-# Patterns which may be used to form malicious path to inject a new
-# line in the mailman error log. (TK: advisory by Moritz Naumann)
-CRNLpat = re.compile(r'[^\x21-\x7e]')
-
-def GetPathPieces(envar='PATH_INFO'):
- path = os.environ.get(envar)
- if path:
- if CRNLpat.search(path):
- path = CRNLpat.split(path)[0]
- log.error('Warning: Possible malformed path attack.')
- return [p for p in path.split('/') if p]
- return []
-
-
-
-def ScriptURL(target):
- up = '../' * len(GetPathPieces())
- return '%s%s' % (up, target + config.CGIEXT)
-
-
-
-def GetPossibleMatchingAddrs(name):
- """returns a sorted list of addresses that could possibly match
- a given name.
-
- For Example, given scott@pobox.com, return ['scott@pobox.com'],
- given scott@blackbox.pobox.com return ['scott@blackbox.pobox.com',
- 'scott@pobox.com']"""
-
- name = name.lower()
- user, domain = ParseEmail(name)
- res = [name]
- if domain:
- domain = domain[1:]
- while len(domain) >= 2:
- res.append("%s@%s" % (user, DOT.join(domain)))
- domain = domain[1:]
- return res
-
-
-
-def List2Dict(L, foldcase=False):
- """Return a dict keyed by the entries in the list passed to it."""
- d = {}
- if foldcase:
- for i in L:
- d[i.lower()] = True
- else:
- for i in L:
- d[i] = True
- return d
-
-
-
-_vowels = ('a', 'e', 'i', 'o', 'u')
-_consonants = ('b', 'c', 'd', 'f', 'g', 'h', 'k', 'm', 'n',
- 'p', 'r', 's', 't', 'v', 'w', 'x', 'z')
-_syllables = []
-
-for v in _vowels:
- for c in _consonants:
- _syllables.append(c+v)
- _syllables.append(v+c)
-del c, v
-
-def UserFriendly_MakeRandomPassword(length):
- syls = []
- while len(syls) * 2 < length:
- syls.append(random.choice(_syllables))
- return EMPTYSTRING.join(syls)[:length]
-
-
-def Secure_MakeRandomPassword(length):
- bytesread = 0
- bytes = []
- fd = None
- try:
- while bytesread < length:
- try:
- # Python 2.4 has this on available systems.
- newbytes = os.urandom(length - bytesread)
- except (AttributeError, NotImplementedError):
- if fd is None:
- try:
- fd = os.open('/dev/urandom', os.O_RDONLY)
- except OSError, e:
- if e.errno <> errno.ENOENT:
- raise
- # We have no available source of cryptographically
- # secure random characters. Log an error and fallback
- # to the user friendly passwords.
- log.error(
- 'urandom not available, passwords not secure')
- return UserFriendly_MakeRandomPassword(length)
- newbytes = os.read(fd, length - bytesread)
- bytes.append(newbytes)
- bytesread += len(newbytes)
- s = base64.encodestring(EMPTYSTRING.join(bytes))
- # base64 will expand the string by 4/3rds
- return s.replace('\n', '')[:length]
- finally:
- if fd is not None:
- os.close(fd)
-
-
-def MakeRandomPassword(length=None):
- if length is None:
- length = int(config.passwords.member_password_length)
- if as_boolean(config.passwords.user_friendly_passwords):
- password = UserFriendly_MakeRandomPassword(length)
- else:
- password = Secure_MakeRandomPassword(length)
- return password.decode('ascii')
-
-
-def GetRandomSeed():
- chr1 = int(random.random() * 52)
- chr2 = int(random.random() * 52)
- def mkletter(c):
- if 0 <= c < 26:
- c += 65
- if 26 <= c < 52:
- #c = c - 26 + 97
- c += 71
- return c
- return "%c%c" % tuple(map(mkletter, (chr1, chr2)))
-
-
-
-def set_global_password(pw, siteadmin=True, scheme=None):
- if scheme is None:
- scheme = passwords.Schemes.ssha
- if siteadmin:
- filename = config.SITE_PW_FILE
- else:
- filename = config.LISTCREATOR_PW_FILE
- try:
- fp = open(filename, 'w')
- print >> fp, passwords.make_secret(pw, scheme)
- finally:
- fp.close()
-
-
-def get_global_password(siteadmin=True):
- if siteadmin:
- filename = config.SITE_PW_FILE
- else:
- filename = config.LISTCREATOR_PW_FILE
- try:
- fp = open(filename)
- challenge = fp.read()[:-1] # strip off trailing nl
- fp.close()
- except IOError, e:
- if e.errno <> errno.ENOENT:
- raise
- # It's okay not to have a site admin password
- return None
- return challenge
-
-
-def check_global_password(response, siteadmin=True):
- challenge = get_global_password(siteadmin)
- if challenge is None:
- return False
- return passwords.check_response(challenge, response)
-
-
-
-def websafe(s):
- return cgi.escape(s, quote=True)
-
-
-def nntpsplit(s):
- parts = s.split(':', 1)
- if len(parts) == 2:
- try:
- return parts[0], int(parts[1])
- except ValueError:
- pass
- # Use the defaults
- return s, 119
-
-
-
-# Just changing these two functions should be enough to control the way
-# that email address obscuring is handled.
-def ObscureEmail(addr, for_text=False):
- """Make email address unrecognizable to web spiders, but invertable.
-
- When for_text option is set (not default), make a sentence fragment
- instead of a token."""
- if for_text:
- return addr.replace('@', ' at ')
- else:
- return addr.replace('@', '--at--')
-
-def UnobscureEmail(addr):
- """Invert ObscureEmail() conversion."""
- # Contrived to act as an identity operation on already-unobscured
- # emails, so routines expecting obscured ones will accept both.
- return addr.replace('--at--', '@')
-
-
-
-class OuterExit(Exception):
- pass
-
-def findtext(templatefile, raw_dict=None, raw=False, lang=None, mlist=None):
- # Make some text from a template file. The order of searches depends on
- # whether mlist and lang are provided. Once the templatefile is found,
- # string substitution is performed by interpolation in `dict'. If `raw'
- # is false, the resulting text is wrapped/filled by calling wrap().
- #
- # When looking for a template in a specific language, there are 4 places
- # that are searched, in this order:
- #
- # 1. the list-specific language directory
- # lists//
- #
- # 2. the domain-specific language directory
- # templates//
- #
- # 3. the site-wide language directory
- # templates/site/
- #
- # 4. the global default language directory
- # templates/
- #
- # The first match found stops the search. In this way, you can specialize
- # templates at the desired level, or, if you use only the default
- # templates, you don't need to change anything. You should never modify
- # files in the templates/ subdirectory, since Mailman will
- # overwrite these when you upgrade. That's what the templates/site
- # language directories are for.
- #
- # A further complication is that the language to search for is determined
- # by both the `lang' and `mlist' arguments. The search order there is
- # that if lang is given, then the 4 locations above are searched,
- # substituting lang for . If no match is found, and mlist is
- # given, then the 4 locations are searched using the list's preferred
- # language. After that, the server default language is used for
- # . If that still doesn't yield a template, then the standard
- # distribution's English language template is used as an ultimate
- # fallback, and when lang is not 'en', the resulting template is passed
- # through the translation service. If this template is missing you've got
- # big problems. ;)
- #
- # A word on backwards compatibility: Mailman versions prior to 2.1 stored
- # templates in templates/*.{html,txt} and lists//*.{html,txt}.
- # Those directories are no longer searched so if you've got customizations
- # in those files, you should move them to the appropriate directory based
- # on the above description. Mailman's upgrade script cannot do this for
- # you.
- #
- # The function has been revised and renamed as it now returns both the
- # template text and the path from which it retrieved the template. The
- # original function is now a wrapper which just returns the template text
- # as before, by calling this renamed function and discarding the second
- # item returned.
- #
- # Calculate the languages to scan
- languages = set()
- if lang is not None:
- languages.add(lang)
- if mlist is not None:
- languages.add(mlist.preferred_language)
- languages.add(config.mailman.default_language)
- assert None not in languages, 'None in languages'
- # Calculate the locations to scan
- searchdirs = []
- if mlist is not None:
- searchdirs.append(mlist.data_path)
- searchdirs.append(os.path.join(TEMPLATE_DIR, mlist.host_name))
- searchdirs.append(os.path.join(TEMPLATE_DIR, 'site'))
- searchdirs.append(TEMPLATE_DIR)
- # Start scanning
- fp = None
- try:
- for lang in languages:
- for dir in searchdirs:
- filename = os.path.join(dir, lang, templatefile)
- try:
- fp = open(filename)
- raise OuterExit
- except IOError, e:
- if e.errno <> errno.ENOENT:
- raise
- # Okay, it doesn't exist, keep looping
- fp = None
- except OuterExit:
- pass
- if fp is None:
- # Try one last time with the distro English template, which, unless
- # you've got a really broken installation, must be there.
- try:
- filename = os.path.join(TEMPLATE_DIR, 'en', templatefile)
- fp = open(filename)
- except IOError, e:
- if e.errno <> errno.ENOENT:
- raise
- # We never found the template. BAD!
- raise IOError(errno.ENOENT, 'No template file found', templatefile)
- else:
- from mailman.i18n import get_translation
- # XXX BROKEN HACK
- data = fp.read()[:-1]
- template = get_translation().ugettext(data)
- fp.close()
- else:
- template = fp.read()
- fp.close()
- template = unicode(template, GetCharSet(lang), 'replace')
- text = template
- if raw_dict is not None:
- text = expand(template, raw_dict)
- if raw:
- return text, filename
- return wrap(text), filename
-
-
-def maketext(templatefile, dict=None, raw=False, lang=None, mlist=None):
- return findtext(templatefile, dict, raw, lang, mlist)[0]
-
-
-
-def GetRequestURI(fallback=None, escape=True):
- """Return the full virtual path this CGI script was invoked with.
-
- Newer web servers seems to supply this info in the REQUEST_URI
- environment variable -- which isn't part of the CGI/1.1 spec.
- Thus, if REQUEST_URI isn't available, we concatenate SCRIPT_NAME
- and PATH_INFO, both of which are part of CGI/1.1.
-
- Optional argument `fallback' (default `None') is returned if both of
- the above methods fail.
-
- The url will be cgi escaped to prevent cross-site scripting attacks,
- unless `escape' is set to 0.
- """
- url = fallback
- if 'REQUEST_URI' in os.environ:
- url = os.environ['REQUEST_URI']
- elif 'SCRIPT_NAME' in os.environ and 'PATH_INFO' in os.environ:
- url = os.environ['SCRIPT_NAME'] + os.environ['PATH_INFO']
- if escape:
- return websafe(url)
- return url
-
-
-
-# XXX Replace this with direct calls. For now, existing uses of GetCharSet()
-# are too numerous to change.
-def GetCharSet(lang):
- return config.languages.get_charset(lang)
-
-
-
-def get_request_domain():
- host = os.environ.get('HTTP_HOST', os.environ.get('SERVER_NAME'))
- port = os.environ.get('SERVER_PORT')
- # Strip off the port if there is one
- if port and host.endswith(':' + port):
- host = host[:-len(port)-1]
- return host.lower()
-
-
-def get_site_noreply():
- return '%s@%s' % (config.NO_REPLY_ADDRESS, config.DEFAULT_EMAIL_HOST)
-
-
-
-# Figure out epoch seconds of midnight at the start of today (or the given
-# 3-tuple date of (year, month, day).
-def midnight(date=None):
- if date is None:
- date = time.localtime()[:3]
- # -1 for dst flag tells the library to figure it out
- return time.mktime(date + (0,)*5 + (-1,))
-
-
-
-# The opposite of canonstr() -- sorta. I.e. it attempts to encode s in the
-# charset of the given language, which is the character set that the page will
-# be rendered in, and failing that, replaces non-ASCII characters with their
-# html references. It always returns a byte string.
-def uncanonstr(s, lang=None):
- if s is None:
- s = u''
- if lang is None:
- charset = 'us-ascii'
- else:
- charset = GetCharSet(lang)
- # See if the string contains characters only in the desired character
- # set. If so, return it unchanged, except for coercing it to a byte
- # string.
- try:
- if isinstance(s, unicode):
- return s.encode(charset)
- else:
- u = unicode(s, charset)
- return s
- except UnicodeError:
- # Nope, it contains funny characters, so html-ref it
- return uquote(s)
-
-
-def uquote(s):
- a = []
- for c in s:
- o = ord(c)
- if o > 127:
- a.append('%3d;' % o)
- else:
- a.append(c)
- # Join characters together and coerce to byte string
- return str(EMPTYSTRING.join(a))
-
-
-def oneline(s, cset='us-ascii', in_unicode=False):
- # Decode header string in one line and convert into specified charset
- try:
- h = email.Header.make_header(email.Header.decode_header(s))
- ustr = h.__unicode__()
- line = UEMPTYSTRING.join(ustr.splitlines())
- if in_unicode:
- return line
- else:
- return line.encode(cset, 'replace')
- except (LookupError, UnicodeError, ValueError, HeaderParseError):
- # possibly charset problem. return with undecoded string in one line.
- return EMPTYSTRING.join(s.splitlines())
-
-
-def strip_verbose_pattern(pattern):
- # Remove white space and comments from a verbose pattern and return a
- # non-verbose, equivalent pattern. Replace CR and NL in the result
- # with '\\r' and '\\n' respectively to avoid multi-line results.
- if not isinstance(pattern, str):
- return pattern
- newpattern = ''
- i = 0
- inclass = False
- skiptoeol = False
- copynext = False
- while i < len(pattern):
- c = pattern[i]
- if copynext:
- if c == NL:
- newpattern += '\\n'
- elif c == CR:
- newpattern += '\\r'
- else:
- newpattern += c
- copynext = False
- elif skiptoeol:
- if c == NL:
- skiptoeol = False
- elif c == '#' and not inclass:
- skiptoeol = True
- elif c == '[' and not inclass:
- inclass = True
- newpattern += c
- copynext = True
- elif c == ']' and inclass:
- inclass = False
- newpattern += c
- elif re.search('\s', c):
- if inclass:
- if c == NL:
- newpattern += '\\n'
- elif c == CR:
- newpattern += '\\r'
- else:
- newpattern += c
- elif c == '\\' and not inclass:
- newpattern += c
- copynext = True
- else:
- if c == NL:
- newpattern += '\\n'
- elif c == CR:
- newpattern += '\\r'
- else:
- newpattern += c
- i += 1
- return newpattern
-
-
-
-def get_pattern(email, pattern_list):
- """Returns matched entry in pattern_list if email matches.
- Otherwise returns None.
- """
- if not pattern_list:
- return None
- matched = None
- for pattern in pattern_list:
- if pattern.startswith('^'):
- # This is a regular expression match
- try:
- if re.search(pattern, email, re.IGNORECASE):
- matched = pattern
- break
- except re.error:
- # BAW: we should probably remove this pattern
- pass
- else:
- # Do the comparison case insensitively
- if pattern.lower() == email.lower():
- matched = pattern
- break
- return matched
diff --git a/mailman/__init__.py b/mailman/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/mailman/app/__init__.py b/mailman/app/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/mailman/app/bounces.py b/mailman/app/bounces.py
deleted file mode 100644
index 875f615a5..000000000
--- a/mailman/app/bounces.py
+++ /dev/null
@@ -1,63 +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 .
-
-"""Application level bounce handling."""
-
-from __future__ import unicode_literals
-
-__metaclass__ = type
-__all__ = [
- 'bounce_message',
- ]
-
-import logging
-
-from email.mime.message import MIMEMessage
-from email.mime.text import MIMEText
-
-from mailman import Message
-from mailman import Utils
-from mailman.i18n import _
-
-log = logging.getLogger('mailman.config')
-
-
-
-def bounce_message(mlist, msg, e=None):
- # Bounce a message back to the sender, with an error message if provided
- # in the exception argument.
- sender = msg.get_sender()
- subject = msg.get('subject', _('(no subject)'))
- subject = Utils.oneline(subject,
- Utils.GetCharSet(mlist.preferred_language))
- if e is None:
- notice = _('[No bounce details are available]')
- else:
- notice = _(e.notice)
- # Currently we always craft bounces as MIME messages.
- bmsg = Message.UserNotification(msg.get_sender(),
- mlist.owner_address,
- subject,
- lang=mlist.preferred_language)
- # BAW: Be sure you set the type before trying to attach, or you'll get
- # a MultipartConversionError.
- bmsg.set_type('multipart/mixed')
- txt = MIMEText(notice,
- _charset=Utils.GetCharSet(mlist.preferred_language))
- bmsg.attach(txt)
- bmsg.attach(MIMEMessage(msg))
- bmsg.send(mlist)
diff --git a/mailman/app/commands.py b/mailman/app/commands.py
deleted file mode 100644
index d7676af9c..000000000
--- a/mailman/app/commands.py
+++ /dev/null
@@ -1,44 +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 .
-
-"""Initialize the email commands."""
-
-from __future__ import unicode_literals
-
-__metaclass__ = type
-__all__ = [
- 'initialize',
- ]
-
-
-from mailman.config import config
-from mailman.core.plugins import get_plugins
-from mailman.interfaces.command import IEmailCommand
-
-
-
-def initialize():
- """Initialize the email commands."""
- for module in get_plugins('mailman.commands'):
- for name in module.__all__:
- command_class = getattr(module, name)
- if not IEmailCommand.implementedBy(command_class):
- continue
- assert command_class.name not in config.commands, (
- 'Duplicate email command "{0}" found in {1}'.format(
- command_class.name, module))
- config.commands[command_class.name] = command_class()
diff --git a/mailman/app/lifecycle.py b/mailman/app/lifecycle.py
deleted file mode 100644
index eec00dc86..000000000
--- a/mailman/app/lifecycle.py
+++ /dev/null
@@ -1,114 +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 .
-
-"""Application level list creation."""
-
-from __future__ import unicode_literals
-
-__metaclass__ = type
-__all__ = [
- 'create_list',
- 'remove_list',
- ]
-
-
-import os
-import sys
-import shutil
-import logging
-
-from mailman import Utils
-from mailman.Utils import ValidateEmail
-from mailman.config import config
-from mailman.core import errors
-from mailman.interfaces.member import MemberRole
-
-
-log = logging.getLogger('mailman.error')
-
-
-
-def create_list(fqdn_listname, owners=None):
- """Create the named list and apply styles."""
- if owners is None:
- owners = []
- ValidateEmail(fqdn_listname)
- listname, domain = fqdn_listname.split('@', 1)
- if domain not in config.domains:
- raise errors.BadDomainSpecificationError(domain)
- mlist = config.db.list_manager.create(fqdn_listname)
- for style in config.style_manager.lookup(mlist):
- style.apply(mlist)
- # Coordinate with the MTA, as defined in the configuration file.
- module_name, class_name = config.mta.incoming.rsplit('.', 1)
- __import__(module_name)
- getattr(sys.modules[module_name], class_name)().create(mlist)
- # Create any owners that don't yet exist, and subscribe all addresses as
- # owners of the mailing list.
- usermgr = config.db.user_manager
- for owner_address in owners:
- addr = usermgr.get_address(owner_address)
- if addr is None:
- # XXX Make this use an IRegistrar instead, but that requires
- # sussing out the IDomain stuff. For now, fake it.
- user = usermgr.create_user(owner_address)
- addr = list(user.addresses)[0]
- addr.subscribe(mlist, MemberRole.owner)
- return mlist
-
-
-
-def remove_list(fqdn_listname, mailing_list=None, archives=True):
- """Remove the list and all associated artifacts and subscriptions."""
- removeables = []
- # mailing_list will be None when only residual archives are being removed.
- if mailing_list:
- # Remove all subscriptions, regardless of role.
- for member in mailing_list.subscribers.members:
- member.unsubscribe()
- # Delete the mailing list from the database.
- config.db.list_manager.delete(mailing_list)
- # Do the MTA-specific list deletion tasks
- module_name, class_name = config.mta.incoming.rsplit('.', 1)
- __import__(module_name)
- getattr(sys.modules[module_name], class_name)().create(mailing_list)
- # Remove the list directory.
- removeables.append(os.path.join(config.LIST_DATA_DIR, fqdn_listname))
- # Remove any stale locks associated with the list.
- for filename in os.listdir(config.LOCK_DIR):
- fn_listname = filename.split('.')[0]
- if fn_listname == fqdn_listname:
- removeables.append(os.path.join(config.LOCK_DIR, filename))
- if archives:
- private_dir = config.PRIVATE_ARCHIVE_FILE_DIR
- public_dir = config.PUBLIC_ARCHIVE_FILE_DIR
- removeables.extend([
- os.path.join(private_dir, fqdn_listname),
- os.path.join(private_dir, fqdn_listname + '.mbox'),
- os.path.join(public_dir, fqdn_listname),
- os.path.join(public_dir, fqdn_listname + '.mbox'),
- ])
- # Now that we know what files and directories to delete, delete them.
- for target in removeables:
- if os.path.islink(target):
- os.unlink(target)
- elif os.path.isdir(target):
- shutil.rmtree(target)
- elif os.path.isfile(target):
- os.unlink(target)
- else:
- log.error('Could not delete list artifact: %s', target)
diff --git a/mailman/app/membership.py b/mailman/app/membership.py
deleted file mode 100644
index 4b9609469..000000000
--- a/mailman/app/membership.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 .
-
-"""Application support for membership management."""
-
-from __future__ import unicode_literals
-
-__metaclass__ = type
-__all__ = [
- 'add_member',
- 'delete_member',
- ]
-
-
-from email.utils import formataddr
-
-from mailman import Message
-from mailman import Utils
-from mailman import i18n
-from mailman.app.notifications import send_goodbye_message
-from mailman.config import config
-from mailman.core import errors
-from mailman.interfaces.member import AlreadySubscribedError, MemberRole
-
-_ = i18n._
-
-
-
-def add_member(mlist, address, realname, password, delivery_mode, language):
- """Add a member right now.
-
- The member's subscription must be approved by whatever policy the list
- enforces.
-
- :param mlist: the mailing list to add the member to
- :type mlist: IMailingList
- :param address: the address to subscribe
- :type address: string
- :param realname: the subscriber's full name
- :type realname: string
- :param password: the subscriber's password
- :type password: string
- :param delivery_mode: the delivery mode the subscriber has chosen
- :type delivery_mode: DeliveryMode
- :param language: the language that the subscriber is going to use
- :type language: string
- """
- # Let's be extra cautious.
- Utils.ValidateEmail(address)
- if mlist.members.get_member(address) is not None:
- raise AlreadySubscribedError(
- mlist.fqdn_listname, address, MemberRole.member)
- # Check for banned address here too for admin mass subscribes and
- # confirmations.
- pattern = Utils.get_pattern(address, mlist.ban_list)
- if pattern:
- raise errors.MembershipIsBanned(pattern)
- # Do the actual addition. First, see if there's already a user linked
- # with the given address.
- user = config.db.user_manager.get_user(address)
- if user is None:
- # A user linked to this address does not yet exist. Is the address
- # itself known but just not linked to a user?
- address_obj = config.db.user_manager.get_address(address)
- if address_obj is None:
- # Nope, we don't even know about this address, so create both the
- # user and address now.
- user = config.db.user_manager.create_user(address, realname)
- # Do it this way so we don't have to flush the previous change.
- address_obj = list(user.addresses)[0]
- else:
- # The address object exists, but it's not linked to a user.
- # Create the user and link it now.
- user = config.db.user_manager.create_user()
- user.real_name = (realname if realname else address_obj.real_name)
- user.link(address_obj)
- # Since created the user, then the member, and set preferences on the
- # appropriate object.
- user.password = password
- user.preferences.preferred_language = language
- member = address_obj.subscribe(mlist, MemberRole.member)
- member.preferences.delivery_mode = delivery_mode
- else:
- # The user exists and is linked to the address.
- for address_obj in user.addresses:
- if address_obj.address == address:
- break
- else:
- raise AssertionError(
- 'User should have had linked address: {0}'.format(address))
- # Create the member and set the appropriate preferences.
- member = address_obj.subscribe(mlist, MemberRole.member)
- member.preferences.preferred_language = language
- member.preferences.delivery_mode = delivery_mode
-## mlist.setMemberOption(email, config.Moderate,
-## mlist.default_member_moderation)
-
-
-
-def delete_member(mlist, address, admin_notif=None, userack=None):
- if userack is None:
- userack = mlist.send_goodbye_msg
- if admin_notif is None:
- admin_notif = mlist.admin_notify_mchanges
- # Delete a member, for which we know the approval has been made
- member = mlist.members.get_member(address)
- language = member.preferred_language
- member.unsubscribe()
- # And send an acknowledgement to the user...
- if userack:
- send_goodbye_message(mlist, address, language)
- # ...and to the administrator.
- if admin_notif:
- user = config.db.user_manager.get_user(address)
- realname = user.real_name
- subject = _('$mlist.real_name unsubscription notification')
- text = Utils.maketext(
- 'adminunsubscribeack.txt',
- {'listname': mlist.real_name,
- 'member' : formataddr((realname, address)),
- }, mlist=mlist)
- msg = Message.OwnerNotification(mlist, subject, text)
- msg.send(mlist)
diff --git a/mailman/app/moderator.py b/mailman/app/moderator.py
deleted file mode 100644
index b40a34344..000000000
--- a/mailman/app/moderator.py
+++ /dev/null
@@ -1,351 +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 .
-
-"""Application support for moderators."""
-
-from __future__ import unicode_literals
-
-__metaclass__ = type
-__all__ = [
- 'handle_message',
- 'handle_subscription',
- 'handle_unsubscription',
- 'hold_message',
- 'hold_subscription',
- 'hold_unsubscription',
- ]
-
-import logging
-
-from datetime import datetime
-from email.utils import formataddr, formatdate, getaddresses, make_msgid
-
-from mailman import Message
-from mailman import Utils
-from mailman import i18n
-from mailman.app.membership import add_member, delete_member
-from mailman.app.notifications import (
- send_admin_subscription_notice, send_welcome_message)
-from mailman.config import config
-from mailman.core import errors
-from mailman.interfaces import Action
-from mailman.interfaces.member import AlreadySubscribedError, DeliveryMode
-from mailman.interfaces.requests import RequestType
-
-_ = i18n._
-
-vlog = logging.getLogger('mailman.vette')
-slog = logging.getLogger('mailman.subscribe')
-
-
-
-def hold_message(mlist, msg, msgdata=None, reason=None):
- """Hold a message for moderator approval.
-
- The message is added to the mailing list's request database.
-
- :param mlist: The mailing list to hold the message on.
- :param msg: The message to hold.
- :param msgdata: Optional message metadata to hold. If not given, a new
- metadata dictionary is created and held with the message.
- :param reason: Optional string reason why the message is being held. If
- not given, the empty string is used.
- :return: An id used to handle the held message later.
- """
- if msgdata is None:
- msgdata = {}
- else:
- # Make a copy of msgdata so that subsequent changes won't corrupt the
- # request database. TBD: remove the `filebase' key since this will
- # not be relevant when the message is resurrected.
- msgdata = msgdata.copy()
- if reason is None:
- reason = ''
- # Add the message to the message store. It is required to have a
- # Message-ID header.
- message_id = msg.get('message-id')
- if message_id is None:
- msg['Message-ID'] = message_id = unicode(make_msgid())
- assert isinstance(message_id, unicode), (
- 'Message-ID is not a unicode: %s' % message_id)
- config.db.message_store.add(msg)
- # Prepare the message metadata with some extra information needed only by
- # the moderation interface.
- msgdata['_mod_message_id'] = message_id
- msgdata['_mod_fqdn_listname'] = mlist.fqdn_listname
- msgdata['_mod_sender'] = msg.get_sender()
- msgdata['_mod_subject'] = msg.get('subject', _('(no subject)'))
- msgdata['_mod_reason'] = reason
- msgdata['_mod_hold_date'] = datetime.now().isoformat()
- # Now hold this request. We'll use the message_id as the key.
- requestsdb = config.db.requests.get_list_requests(mlist)
- request_id = requestsdb.hold_request(
- RequestType.held_message, message_id, msgdata)
- return request_id
-
-
-
-def handle_message(mlist, id, action,
- comment=None, preserve=False, forward=None):
- requestdb = config.db.requests.get_list_requests(mlist)
- key, msgdata = requestdb.get_request(id)
- # Handle the action.
- rejection = None
- message_id = msgdata['_mod_message_id']
- sender = msgdata['_mod_sender']
- subject = msgdata['_mod_subject']
- if action is Action.defer:
- # Nothing to do, but preserve the message for later.
- preserve = True
- elif action is Action.discard:
- rejection = 'Discarded'
- elif action is Action.reject:
- rejection = 'Refused'
- member = mlist.members.get_member(sender)
- if member:
- language = member.preferred_language
- else:
- language = None
- _refuse(mlist, _('Posting of your message titled "$subject"'),
- sender, comment or _('[No reason given]'), language)
- elif action is Action.accept:
- # Start by getting the message from the message store.
- msg = config.db.message_store.get_message_by_id(message_id)
- # Delete moderation-specific entries from the message metadata.
- for key in msgdata.keys():
- if key.startswith('_mod_'):
- del msgdata[key]
- # Add some metadata to indicate this message has now been approved.
- msgdata['approved'] = True
- msgdata['moderator_approved'] = True
- # Calculate a new filebase for the approved message, otherwise
- # delivery errors will cause duplicates.
- if 'filebase' in msgdata:
- del msgdata['filebase']
- # Queue the file for delivery by qrunner. Trying to deliver the
- # message directly here can lead to a huge delay in web turnaround.
- # Log the moderation and add a header.
- msg['X-Mailman-Approved-At'] = formatdate(localtime=True)
- vlog.info('held message approved, message-id: %s',
- msg.get('message-id', 'n/a'))
- # Stick the message back in the incoming queue for further
- # processing.
- config.switchboards['in'].enqueue(msg, _metadata=msgdata)
- else:
- raise AssertionError('Unexpected action: {0}'.format(action))
- # Forward the message.
- if forward:
- # Get a copy of the original message from the message store.
- msg = config.db.message_store.get_message_by_id(message_id)
- # It's possible the forwarding address list is a comma separated list
- # of realname/address pairs.
- addresses = [addr[1] for addr in getaddresses(forward)]
- language = mlist.preferred_language
- if len(addresses) == 1:
- # If the address getting the forwarded message is a member of
- # the list, we want the headers of the outer message to be
- # encoded in their language. Otherwise it'll be the preferred
- # language of the mailing list. This is better than sending a
- # separate message per recipient.
- member = mlist.members.get_member(addresses[0])
- if member:
- language = member.preferred_language
- with i18n.using_language(language):
- fmsg = Message.UserNotification(
- addresses, mlist.bounces_address,
- _('Forward of moderated message'),
- lang=language)
- fmsg.set_type('message/rfc822')
- fmsg.attach(msg)
- fmsg.send(mlist)
- # Delete the message from the message store if it is not being preserved.
- if not preserve:
- config.db.message_store.delete_message(message_id)
- requestdb.delete_request(id)
- # Log the rejection
- if rejection:
- note = """%s: %s posting:
-\tFrom: %s
-\tSubject: %s"""
- if comment:
- note += '\n\tReason: ' + comment
- vlog.info(note, mlist.fqdn_listname, rejection, sender, subject)
-
-
-
-def hold_subscription(mlist, address, realname, password, mode, language):
- data = dict(when=datetime.now().isoformat(),
- address=address,
- realname=realname,
- password=password,
- delivery_mode=str(mode),
- language=language)
- # Now hold this request. We'll use the address as the key.
- requestsdb = config.db.requests.get_list_requests(mlist)
- request_id = requestsdb.hold_request(
- RequestType.subscription, address, data)
- vlog.info('%s: held subscription request from %s',
- mlist.fqdn_listname, address)
- # Possibly notify the administrator in default list language
- if mlist.admin_immed_notify:
- subject = _(
- 'New subscription request to list $mlist.real_name from $address')
- text = Utils.maketext(
- 'subauth.txt',
- {'username' : address,
- 'listname' : mlist.fqdn_listname,
- 'admindb_url': mlist.script_url('admindb'),
- }, mlist=mlist)
- # This message should appear to come from the -owner so as
- # to avoid any useless bounce processing.
- msg = Message.UserNotification(
- mlist.owner_address, mlist.owner_address,
- subject, text, mlist.preferred_language)
- msg.send(mlist, tomoderators=True)
- return request_id
-
-
-
-def handle_subscription(mlist, id, action, comment=None):
- requestdb = config.db.requests.get_list_requests(mlist)
- if action is Action.defer:
- # Nothing to do.
- return
- elif action is Action.discard:
- # Nothing to do except delete the request from the database.
- pass
- elif action is Action.reject:
- key, data = requestdb.get_request(id)
- _refuse(mlist, _('Subscription request'),
- data['address'],
- comment or _('[No reason given]'),
- lang=data['language'])
- elif action is Action.accept:
- key, data = requestdb.get_request(id)
- enum_value = data['delivery_mode'].split('.')[-1]
- delivery_mode = DeliveryMode(enum_value)
- address = data['address']
- realname = data['realname']
- language = data['language']
- password = data['password']
- try:
- add_member(mlist, address, realname, password,
- delivery_mode, language)
- except AlreadySubscribedError:
- # The address got subscribed in some other way after the original
- # request was made and accepted.
- pass
- else:
- if mlist.send_welcome_msg:
- send_welcome_message(mlist, address, language, delivery_mode)
- if mlist.admin_notify_mchanges:
- send_admin_subscription_notice(
- mlist, address, realname, language)
- slog.info('%s: new %s, %s %s', mlist.fqdn_listname,
- delivery_mode, formataddr((realname, address)),
- 'via admin approval')
- else:
- raise AssertionError('Unexpected action: {0}'.format(action))
- # Delete the request from the database.
- requestdb.delete_request(id)
-
-
-
-def hold_unsubscription(mlist, address):
- data = dict(address=address)
- requestsdb = config.db.requests.get_list_requests(mlist)
- request_id = requestsdb.hold_request(
- RequestType.unsubscription, address, data)
- vlog.info('%s: held unsubscription request from %s',
- mlist.fqdn_listname, address)
- # Possibly notify the administrator of the hold
- if mlist.admin_immed_notify:
- subject = _(
- 'New unsubscription request from $mlist.real_name by $address')
- text = Utils.maketext(
- 'unsubauth.txt',
- {'address' : address,
- 'listname' : mlist.fqdn_listname,
- 'admindb_url': mlist.script_url('admindb'),
- }, mlist=mlist)
- # This message should appear to come from the -owner so as
- # to avoid any useless bounce processing.
- msg = Message.UserNotification(
- mlist.owner_address, mlist.owner_address,
- subject, text, mlist.preferred_language)
- msg.send(mlist, tomoderators=True)
- return request_id
-
-
-
-def handle_unsubscription(mlist, id, action, comment=None):
- requestdb = config.db.requests.get_list_requests(mlist)
- key, data = requestdb.get_request(id)
- address = data['address']
- if action is Action.defer:
- # Nothing to do.
- return
- elif action is Action.discard:
- # Nothing to do except delete the request from the database.
- pass
- elif action is Action.reject:
- key, data = requestdb.get_request(id)
- _refuse(mlist, _('Unsubscription request'), address,
- comment or _('[No reason given]'))
- elif action is Action.accept:
- key, data = requestdb.get_request(id)
- try:
- delete_member(mlist, address)
- except errors.NotAMemberError:
- # User has already been unsubscribed.
- pass
- slog.info('%s: deleted %s', mlist.fqdn_listname, address)
- else:
- raise AssertionError('Unexpected action: {0}'.format(action))
- # Delete the request from the database.
- requestdb.delete_request(id)
-
-
-
-def _refuse(mlist, request, recip, comment, origmsg=None, lang=None):
- # As this message is going to the requester, try to set the language to
- # his/her language choice, if they are a member. Otherwise use the list's
- # preferred language.
- realname = mlist.real_name
- if lang is None:
- member = mlist.members.get_member(recip)
- if member:
- lang = member.preferred_language
- text = Utils.maketext(
- 'refuse.txt',
- {'listname' : mlist.fqdn_listname,
- 'request' : request,
- 'reason' : comment,
- 'adminaddr': mlist.owner_address,
- }, lang=lang, mlist=mlist)
- with i18n.using_language(lang):
- # add in original message, but not wrap/filled
- if origmsg:
- text = NL.join(
- [text,
- '---------- ' + _('Original Message') + ' ----------',
- str(origmsg)
- ])
- subject = _('Request to mailing list "$realname" rejected')
- msg = Message.UserNotification(recip, mlist.bounces_address,
- subject, text, lang)
- msg.send(mlist)
diff --git a/mailman/app/notifications.py b/mailman/app/notifications.py
deleted file mode 100644
index 9bef9998b..000000000
--- a/mailman/app/notifications.py
+++ /dev/null
@@ -1,136 +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 .
-
-"""Sending notifications."""
-
-from __future__ import unicode_literals
-
-__metaclass__ = type
-__all__ = [
- 'send_admin_subscription_notice',
- 'send_goodbye_message',
- 'send_welcome_message',
- ]
-
-
-from email.utils import formataddr
-from lazr.config import as_boolean
-
-from mailman import Message
-from mailman import Utils
-from mailman import i18n
-from mailman.config import config
-from mailman.interfaces.member import DeliveryMode
-
-
-_ = i18n._
-
-
-
-def send_welcome_message(mlist, address, language, delivery_mode, text=''):
- """Send a welcome message to a subscriber.
-
- Prepending to the standard welcome message template is the mailing list's
- welcome message, if there is one.
-
- :param mlist: the mailing list
- :type mlist: IMailingList
- :param address: The address to respond to
- :type address: string
- :param language: the language of the response
- :type language: string
- :param delivery_mode: the type of delivery the subscriber is getting
- :type delivery_mode: DeliveryMode
- """
- if mlist.welcome_msg:
- welcome = Utils.wrap(mlist.welcome_msg) + '\n'
- else:
- welcome = ''
- # Find the IMember object which is subscribed to the mailing list, because
- # from there, we can get the member's options url.
- member = mlist.members.get_member(address)
- options_url = member.options_url
- # Get the text from the template.
- text += Utils.maketext(
- 'subscribeack.txt', {
- 'real_name' : mlist.real_name,
- 'posting_address' : mlist.fqdn_listname,
- 'listinfo_url' : mlist.script_url('listinfo'),
- 'optionsurl' : options_url,
- 'request_address' : mlist.request_address,
- 'welcome' : welcome,
- }, lang=language, mlist=mlist)
- if delivery_mode is not DeliveryMode.regular:
- digmode = _(' (Digest mode)')
- else:
- digmode = ''
- msg = Message.UserNotification(
- address, mlist.request_address,
- _('Welcome to the "$mlist.real_name" mailing list${digmode}'),
- text, language)
- msg['X-No-Archive'] = 'yes'
- msg.send(mlist, verp=as_boolean(config.mta.verp_personalized_deliveries))
-
-
-
-def send_goodbye_message(mlist, address, language):
- """Send a goodbye message to a subscriber.
-
- Prepending to the standard goodbye message template is the mailing list's
- goodbye message, if there is one.
-
- :param mlist: the mailing list
- :type mlist: IMailingList
- :param address: The address to respond to
- :type address: string
- :param language: the language of the response
- :type language: string
- """
- if mlist.goodbye_msg:
- goodbye = Utils.wrap(mlist.goodbye_msg) + '\n'
- else:
- goodbye = ''
- msg = Message.UserNotification(
- address, mlist.bounces_address,
- _('You have been unsubscribed from the $mlist.real_name mailing list'),
- goodbye, language)
- msg.send(mlist, verp=as_boolean(config.mta.verp_personalized_deliveries))
-
-
-
-def send_admin_subscription_notice(mlist, address, full_name, language):
- """Send the list administrators a subscription notice.
-
- :param mlist: the mailing list
- :type mlist: IMailingList
- :param address: the address being subscribed
- :type address: string
- :param full_name: the name of the subscriber
- :type full_name: string
- :param language: the language of the address's realname
- :type language: string
- """
- with i18n.using_language(mlist.preferred_language):
- subject = _('$mlist.real_name subscription notification')
- full_name = full_name.encode(Utils.GetCharSet(language), 'replace')
- text = Utils.maketext(
- 'adminsubscribeack.txt',
- {'listname' : mlist.real_name,
- 'member' : formataddr((full_name, address)),
- }, mlist=mlist)
- msg = Message.OwnerNotification(mlist, subject, text)
- msg.send(mlist)
diff --git a/mailman/app/registrar.py b/mailman/app/registrar.py
deleted file mode 100644
index 6a2abeba9..000000000
--- a/mailman/app/registrar.py
+++ /dev/null
@@ -1,163 +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 .
-
-"""Implementation of the IUserRegistrar interface."""
-
-from __future__ import unicode_literals
-
-__metaclass__ = type
-__all__ = [
- 'Registrar',
- 'adapt_domain_to_registrar',
- ]
-
-
-import datetime
-
-from pkg_resources import resource_string
-from zope.interface import implements
-
-from mailman.Message import UserNotification
-from mailman.Utils import ValidateEmail
-from mailman.config import config
-from mailman.i18n import _
-from mailman.interfaces.domain import IDomain
-from mailman.interfaces.member import MemberRole
-from mailman.interfaces.pending import IPendable
-from mailman.interfaces.registrar import IRegistrar
-
-
-
-class PendableRegistration(dict):
- implements(IPendable)
- PEND_KEY = 'registration'
-
-
-
-class Registrar:
- implements(IRegistrar)
-
- def __init__(self, context):
- self._context = context
-
- def register(self, address, real_name=None, mlist=None):
- """See `IUserRegistrar`."""
- # First, do validation on the email address. If the address is
- # invalid, it will raise an exception, otherwise it just returns.
- ValidateEmail(address)
- # Create a pendable for the registration.
- pendable = PendableRegistration(
- type=PendableRegistration.PEND_KEY,
- address=address,
- real_name=real_name)
- if mlist is not None:
- pendable['list_name'] = mlist.fqdn_listname
- token = config.db.pendings.add(pendable)
- # Set up some local variables for translation interpolation.
- domain = IDomain(self._context)
- domain_name = _(domain.email_host)
- contact_address = domain.contact_address
- confirm_url = domain.confirm_url(token)
- confirm_address = domain.confirm_address(token)
- email_address = address
- # Calculate the message's Subject header. XXX Have to deal with
- # translating this subject header properly. XXX Must deal with
- # VERP_CONFIRMATIONS as well.
- subject = 'confirm ' + token
- # Send a verification email to the address.
- text = _(resource_string('mailman.templates.en', 'verify.txt'))
- msg = UserNotification(address, confirm_address, subject, text)
- msg.send(mlist=None)
- return token
-
- def confirm(self, token):
- """See `IUserRegistrar`."""
- # For convenience
- pendable = config.db.pendings.confirm(token)
- if pendable is None:
- return False
- missing = object()
- address = pendable.get('address', missing)
- real_name = pendable.get('real_name', missing)
- list_name = pendable.get('list_name', missing)
- if pendable.get('type') != PendableRegistration.PEND_KEY:
- # It seems like it would be very difficult to accurately guess
- # tokens, or brute force an attack on the SHA1 hash, so we'll just
- # throw the pendable away in that case. It's possible we'll need
- # to repend the event or adjust the API to handle this case
- # better, but for now, the simpler the better.
- return False
- # We are going to end up with an IAddress for the verified address
- # and an IUser linked to this IAddress. See if any of these objects
- # currently exist in our database.
- usermgr = config.db.user_manager
- addr = (usermgr.get_address(address)
- if address is not missing else None)
- user = (usermgr.get_user(address)
- if address is not missing else None)
- # If there is neither an address nor a user matching the confirmed
- # record, then create the user, which will in turn create the address
- # and link the two together
- if addr is None:
- assert user is None, 'How did we get a user but not an address?'
- user = usermgr.create_user(address, real_name)
- # Because the database changes haven't been flushed, we can't use
- # IUserManager.get_address() to find the IAddress just created
- # under the hood. Instead, iterate through the IUser's addresses,
- # of which really there should be only one.
- for addr in user.addresses:
- if addr.address == address:
- break
- else:
- raise AssertionError('Could not find expected IAddress')
- elif user is None:
- user = usermgr.create_user()
- user.real_name = real_name
- user.link(addr)
- else:
- # The IAddress and linked IUser already exist, so all we need to
- # do is verify the address.
- pass
- addr.verified_on = datetime.datetime.now()
- # If this registration is tied to a mailing list, subscribe the person
- # to the list right now.
- list_name = pendable.get('list_name')
- if list_name is not None:
- mlist = config.db.list_manager.get(list_name)
- if mlist:
- addr.subscribe(mlist, MemberRole.member)
- return True
-
- def discard(self, token):
- # Throw the record away.
- config.db.pendings.confirm(token)
-
-
-
-def adapt_domain_to_registrar(iface, obj):
- """Adapt `IDomain` to `IRegistrar`.
-
- :param iface: The interface to adapt to.
- :type iface: `zope.interface.Interface`
- :param obj: The object being adapted.
- :type obj: `IDomain`
- :return: An `IRegistrar` instance if adaptation succeeded or None if it
- didn't.
- """
- return (Registrar(obj)
- if IDomain.providedBy(obj) and iface is IRegistrar
- else None)
diff --git a/mailman/app/replybot.py b/mailman/app/replybot.py
deleted file mode 100644
index 0537f6645..000000000
--- a/mailman/app/replybot.py
+++ /dev/null
@@ -1,125 +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 .
-
-"""Application level auto-reply code."""
-
-# XXX This should undergo a rewrite to move this functionality off of the
-# mailing list. The reply governor should really apply site-wide per
-# recipient (I think).
-
-from __future__ import unicode_literals
-
-__metaclass__ = type
-__all__ = [
- 'autorespond_to_sender',
- 'can_acknowledge',
- ]
-
-import logging
-import datetime
-
-from mailman import Utils
-from mailman import i18n
-from mailman.config import config
-
-
-log = logging.getLogger('mailman.vette')
-_ = i18n._
-
-
-
-def autorespond_to_sender(mlist, sender, lang=None):
- """Return True if Mailman should auto-respond to this sender.
-
- This is only consulted for messages sent to the -request address, or
- for posting hold notifications, and serves only as a safety value for
- mail loops with email 'bots.
- """
- if lang is None:
- lang = mlist.preferred_language
- max_autoresponses_per_day = int(config.mta.max_autoresponses_per_day)
- if max_autoresponses_per_day == 0:
- # Unlimited.
- return True
- today = datetime.date.today()
- info = mlist.hold_and_cmd_autoresponses.get(sender)
- if info is None or info[0] <> today:
- # This is the first time we've seen a -request/post-hold for this
- # sender today.
- mlist.hold_and_cmd_autoresponses[sender] = (today, 1)
- return True
- date, count = info
- if count < 0:
- # They've already hit the limit for today, and we've already notified
- # them of this fact, so there's nothing more to do.
- log.info('-request/hold autoresponse discarded for: %s', sender)
- return False
- if count >= max_autoresponses_per_day:
- log.info('-request/hold autoresponse limit hit for: %s', sender)
- mlist.hold_and_cmd_autoresponses[sender] = (today, -1)
- # Send this notification message instead.
- text = Utils.maketext(
- 'nomoretoday.txt',
- {'sender' : sender,
- 'listname': mlist.fqdn_listname,
- 'num' : count,
- 'owneremail': mlist.owner_address,
- },
- lang=lang)
- with i18n.using_language(lang):
- msg = Message.UserNotification(
- sender, mlist.owner_address,
- _('Last autoresponse notification for today'),
- text, lang=lang)
- msg.send(mlist)
- return False
- mlist.hold_and_cmd_autoresponses[sender] = (today, count + 1)
- return True
-
-
-
-def can_acknowledge(msg):
- """A boolean specifying whether this message can be acknowledged.
-
- There are several reasons why a message should not be acknowledged, mostly
- related to competing standards or common practices. These include:
-
- * The message has a X-No-Ack header with any value
- * The message has an X-Ack header with a 'no' value
- * The message has a Precedence header
- * The message has an Auto-Submitted header and that header does not have a
- value of 'no'
- * The message has an empty Return-Path header, e.g. <>
- * The message has any RFC 2369 headers (i.e. List-* headers)
-
- :param msg: a Message object.
- :return: Boolean specifying whether the message can be acknowledged or not
- (which is different from whether it will be acknowledged).
- """
- # I wrote it this way for clarity and consistency with the docstring.
- for header in msg.keys():
- if header in ('x-no-ack', 'precedence'):
- return False
- if header.lower().startswith('list-'):
- return False
- if msg.get('x-ack', '').lower() == 'no':
- return False
- if msg.get('auto-submitted', 'no').lower() <> 'no':
- return False
- if msg.get('return-path') == '<>':
- return False
- return True
diff --git a/mailman/archiving/__init__.py b/mailman/archiving/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/mailman/archiving/mailarchive.py b/mailman/archiving/mailarchive.py
deleted file mode 100644
index a5eb27db0..000000000
--- a/mailman/archiving/mailarchive.py
+++ /dev/null
@@ -1,87 +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 .
-
-"""The Mail-Archive.com archiver."""
-
-from __future__ import absolute_import, unicode_literals
-
-__metaclass__ = type
-__all__ = [
- 'MailArchive',
- ]
-
-
-import hashlib
-
-from base64 import urlsafe_b64encode
-from urllib import quote
-from urlparse import urljoin
-from zope.interface import implements
-
-from mailman.config import config
-from mailman.interfaces.archiver import IArchiver
-
-
-
-class MailArchive:
- """Public archiver at the Mail-Archive.com.
-
- Messages get archived at http://go.mail-archive.com.
- """
-
- implements(IArchiver)
-
- name = 'mail-archive'
-
- @staticmethod
- def list_url(mlist):
- """See `IArchiver`."""
- if mlist.archive_private:
- return None
- return urljoin(config.archiver.mail_archive.base_url,
- quote(mlist.posting_address))
-
- @staticmethod
- def permalink(mlist, msg):
- """See `IArchiver`."""
- if mlist.archive_private:
- return None
- message_id = msg.get('message-id')
- # It is not the archiver's job to ensure the message has a Message-ID.
- # If no Message-ID is available, there is no permalink.
- if message_id is None:
- return None
- # The angle brackets are not part of the Message-ID. See RFC 2822.
- if message_id.startswith('<') and message_id.endswith('>'):
- message_id = message_id[1:-1]
- else:
- message_id = message_id.strip()
- sha = hashlib.sha1(message_id)
- sha.update(str(mlist.posting_address))
- message_id_hash = urlsafe_b64encode(sha.digest())
- del msg['x-message-id-hash']
- msg['X-Message-ID-Hash'] = message_id_hash
- return urljoin(config.archiver.mail_archive.base_url, message_id_hash)
-
- @staticmethod
- def archive_message(mlist, msg):
- """See `IArchiver`."""
- if not mlist.archive_private:
- config.switchboards['out'].enqueue(
- msg,
- listname=mlist.fqdn_listname,
- recips=[config.archiver.mail_archive.recipient])
diff --git a/mailman/archiving/mhonarc.py b/mailman/archiving/mhonarc.py
deleted file mode 100644
index 949a79144..000000000
--- a/mailman/archiving/mhonarc.py
+++ /dev/null
@@ -1,97 +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 .
-
-"""MHonArc archiver."""
-
-from __future__ import absolute_import, unicode_literals
-
-__metaclass__ = type
-__all__ = [
- 'MHonArc',
- ]
-
-
-import hashlib
-import logging
-import subprocess
-
-from base64 import b32encode
-from urlparse import urljoin
-from zope.interface import implements
-
-from mailman.config import config
-from mailman.interfaces.archiver import IArchiver
-from mailman.utilities.string import expand
-
-
-log = logging.getLogger('mailman.archiver')
-
-
-
-class MHonArc:
- """Local MHonArc archiver."""
-
- implements(IArchiver)
-
- name = 'mhonarc'
-
- @staticmethod
- def list_url(mlist):
- """See `IArchiver`."""
- # XXX What about private MHonArc archives?
- web_host = config.domains[mlist.host_name].url_host
- return expand(config.archiver.mhonarc.base_url,
- dict(listname=mlist.fqdn_listname,
- hostname=web_host,
- fqdn_listname=mlist.fqdn_listname,
- ))
-
- @staticmethod
- def permalink(mlist, msg):
- """See `IArchiver`."""
- # XXX What about private MHonArc archives?
- message_id = msg.get('message-id')
- # It is not the archiver's job to ensure the message has a Message-ID.
- # If no Message-ID is available, there is no permalink.
- if message_id is None:
- return None
- # The angle brackets are not part of the Message-ID. See RFC 2822.
- if message_id.startswith('<') and message_id.endswith('>'):
- message_id = message_id[1:-1]
- else:
- message_id = message_id.strip()
- sha = hashlib.sha1(message_id)
- message_id_hash = b32encode(sha.digest())
- del msg['x-message-id-hash']
- msg['X-Message-ID-Hash'] = message_id_hash
- return urljoin(MHonArc.list_url(mlist), message_id_hash)
-
- @staticmethod
- def archive_message(mlist, msg):
- """See `IArchiver`."""
- substitutions = config.__dict__.copy()
- substitutions['listname'] = mlist.fqdn_listname
- command = expand(config.archiver.mhonarc.command, substitutions)
- proc = subprocess.Popen(
- command, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
- shell=True)
- stdout, stderr = proc.communicate(msg.as_string())
- if proc.returncode <> 0:
- log.error('%s: mhonarc subprocess had non-zero exit code: %s' %
- (msg['message-id'], proc.returncode))
- log.info(stdout)
- log.error(stderr)
diff --git a/mailman/archiving/pipermail.py b/mailman/archiving/pipermail.py
deleted file mode 100644
index 377f4ab53..000000000
--- a/mailman/archiving/pipermail.py
+++ /dev/null
@@ -1,121 +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 .
-
-"""Pipermail archiver."""
-
-from __future__ import absolute_import, unicode_literals
-
-__metaclass__ = type
-__all__ = [
- 'Pipermail',
- ]
-
-
-import os
-
-from cStringIO import StringIO
-from zope.interface import implements
-from zope.interface.interface import adapter_hooks
-
-from mailman.config import config
-from mailman.interfaces.archiver import IArchiver, IPipermailMailingList
-from mailman.interfaces.mailinglist import IMailingList
-from mailman.utilities.filesystem import makedirs
-from mailman.utilities.string import expand
-
-from mailman.Archiver.HyperArch import HyperArchive
-
-
-
-class PipermailMailingListAdapter:
- """An adapter for MailingList objects to work with Pipermail."""
-
- implements(IPipermailMailingList)
-
- def __init__(self, mlist):
- self._mlist = mlist
-
- def __getattr__(self, name):
- return getattr(self._mlist, name)
-
- def archive_dir(self):
- """See `IPipermailMailingList`."""
- if self._mlist.archive_private:
- basedir = config.PRIVATE_ARCHIVE_FILE_DIR
- else:
- basedir = config.PUBLIC_ARCHIVE_FILE_DIR
- # Make sure the archive directory exists.
- archive_dir = os.path.join(basedir, self._mlist.fqdn_listname)
- makedirs(archive_dir)
- return archive_dir
-
-
-def adapt_mailing_list_for_pipermail(iface, obj):
- """Adapt `IMailingLists` to `IPipermailMailingList`.
-
- :param iface: The interface to adapt to.
- :type iface: `zope.interface.Interface`
- :param obj: The object being adapted.
- :type obj: any object
- :return: An `IPipermailMailingList` instance if adaptation succeeded or
- None if it didn't.
- """
- return (PipermailMailingListAdapter(obj)
- if IMailingList.providedBy(obj) and iface is IPipermailMailingList
- else None)
-
-adapter_hooks.append(adapt_mailing_list_for_pipermail)
-
-
-
-class Pipermail:
- """The stock Pipermail archiver."""
-
- implements(IArchiver)
-
- name = 'pipermail'
-
- @staticmethod
- def list_url(mlist):
- """See `IArchiver`."""
- if mlist.archive_private:
- url = mlist.script_url('private') + '/index.html'
- else:
- web_host = config.domains[mlist.host_name].url_host
- return expand(config.archiver.pipermail.base_url,
- dict(listname=mlist.fqdn_listname,
- hostname=web_host,
- fqdn_listname=mlist.fqdn_listname,
- ))
-
- @staticmethod
- def permalink(mlist, message):
- """See `IArchiver`."""
- # Not currently implemented.
- return None
-
- @staticmethod
- def archive_message(mlist, message):
- """See `IArchiver`."""
- text = str(message)
- fileobj = StringIO(text)
- h = HyperArchive(IPipermailMailingList(mlist))
- h.processUnixMailbox(fileobj)
- h.close()
- fileobj.close()
- # There's no good way to know the url for the archived message.
- return None
diff --git a/mailman/archiving/prototype.py b/mailman/archiving/prototype.py
deleted file mode 100644
index 81163e184..000000000
--- a/mailman/archiving/prototype.py
+++ /dev/null
@@ -1,77 +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 .
-
-"""Prototypical permalinking archiver."""
-
-from __future__ import absolute_import, unicode_literals
-
-__metaclass__ = type
-__all__ = [
- 'Prototype',
- ]
-
-
-import hashlib
-
-from base64 import b32encode
-from urlparse import urljoin
-from zope.interface import implements
-
-from mailman.config import config
-from mailman.interfaces.archiver import IArchiver
-
-
-
-class Prototype:
- """A prototype of a third party archiver.
-
- Mailman proposes a draft specification for interoperability between list
- servers and archivers: .
- """
-
- implements(IArchiver)
-
- name = 'prototype'
-
- @staticmethod
- def list_url(mlist):
- """See `IArchiver`."""
- return config.domains[mlist.host_name].base_url
-
- @staticmethod
- def permalink(mlist, msg):
- """See `IArchiver`."""
- message_id = msg.get('message-id')
- # It is not the archiver's job to ensure the message has a Message-ID.
- # If this header is missing, there is no permalink.
- if message_id is None:
- return None
- # The angle brackets are not part of the Message-ID. See RFC 2822.
- if message_id.startswith('<') and message_id.endswith('>'):
- message_id = message_id[1:-1]
- else:
- message_id = message_id.strip()
- digest = hashlib.sha1(message_id).digest()
- message_id_hash = b32encode(digest)
- del msg['x-message-id-hash']
- msg['X-Message-ID-Hash'] = message_id_hash
- return urljoin(Prototype.list_url(mlist), message_id_hash)
-
- @staticmethod
- def archive_message(mlist, message):
- """See `IArchiver`."""
- raise NotImplementedError
diff --git a/mailman/attic/Bouncer.py b/mailman/attic/Bouncer.py
deleted file mode 100644
index e2de3c915..000000000
--- a/mailman/attic/Bouncer.py
+++ /dev/null
@@ -1,250 +0,0 @@
-# Copyright (C) 1998-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 .
-
-"""Handle delivery bounces."""
-
-import sys
-import time
-import logging
-
-from email.MIMEMessage import MIMEMessage
-from email.MIMEText import MIMEText
-
-from mailman import Defaults
-from mailman import Message
-from mailman import Utils
-from mailman import i18n
-from mailman.configuration import config
-from mailman.interfaces import DeliveryStatus
-
-EMPTYSTRING = ''
-
-# This constant is supposed to represent the day containing the first midnight
-# after the epoch. We'll add (0,)*6 to this tuple to get a value appropriate
-# for time.mktime().
-ZEROHOUR_PLUSONEDAY = time.localtime(60 * 60 * 24)[:3]
-
-def _(s): return s
-
-REASONS = {
- DeliveryStatus.by_bounces : _('due to excessive bounces'),
- DeliveryStatus.by_user : _('by yourself'),
- DeliveryStatus.by_moderator : _('by the list administrator'),
- DeliveryStatus.unknown : _('for unknown reasons'),
- }
-
-_ = i18n._
-
-log = logging.getLogger('mailman.bounce')
-slog = logging.getLogger('mailman.subscribe')
-
-
-
-class _BounceInfo:
- def __init__(self, member, score, date, noticesleft):
- self.member = member
- self.cookie = None
- self.reset(score, date, noticesleft)
-
- def reset(self, score, date, noticesleft):
- self.score = score
- self.date = date
- self.noticesleft = noticesleft
- self.lastnotice = ZEROHOUR_PLUSONEDAY
-
- def __repr__(self):
- # For debugging
- return """\
-""" % self.__dict__
-
-
-
-class Bouncer:
- def registerBounce(self, member, msg, weight=1.0, day=None):
- if not self.isMember(member):
- return
- info = self.getBounceInfo(member)
- if day is None:
- # Use today's date
- day = time.localtime()[:3]
- if not isinstance(info, _BounceInfo):
- # This is the first bounce we've seen from this member
- info = _BounceInfo(member, weight, day,
- self.bounce_you_are_disabled_warnings)
- self.setBounceInfo(member, info)
- log.info('%s: %s bounce score: %s', self.internal_name(),
- member, info.score)
- # Continue to the check phase below
- elif self.getDeliveryStatus(member) <> DeliveryStatus.enabled:
- # The user is already disabled, so we can just ignore subsequent
- # bounces. These are likely due to residual messages that were
- # sent before disabling the member, but took a while to bounce.
- log.info('%s: %s residual bounce received',
- self.internal_name(), member)
- return
- elif info.date == day:
- # We've already scored any bounces for this day, so ignore it.
- log.info('%s: %s already scored a bounce for date %s',
- self.internal_name(), member,
- time.strftime('%d-%b-%Y', day + (0,0,0,0,1,0)))
- # Continue to check phase below
- else:
- # See if this member's bounce information is stale.
- now = Utils.midnight(day)
- lastbounce = Utils.midnight(info.date)
- if lastbounce + self.bounce_info_stale_after < now:
- # Information is stale, so simply reset it
- info.reset(weight, day, self.bounce_you_are_disabled_warnings)
- log.info('%s: %s has stale bounce info, resetting',
- self.internal_name(), member)
- else:
- # Nope, the information isn't stale, so add to the bounce
- # score and take any necessary action.
- info.score += weight
- info.date = day
- log.info('%s: %s current bounce score: %s',
- self.internal_name(), member, info.score)
- # Continue to the check phase below
- #
- # Now that we've adjusted the bounce score for this bounce, let's
- # check to see if the disable-by-bounce threshold has been reached.
- if info.score >= self.bounce_score_threshold:
- if config.VERP_PROBES:
- log.info('sending %s list probe to: %s (score %s >= %s)',
- self.internal_name(), member, info.score,
- self.bounce_score_threshold)
- self.sendProbe(member, msg)
- info.reset(0, info.date, info.noticesleft)
- else:
- self.disableBouncingMember(member, info, msg)
-
- def disableBouncingMember(self, member, info, msg):
- # Initialize their confirmation cookie. If we do it when we get the
- # first bounce, it'll expire by the time we get the disabling bounce.
- cookie = self.pend_new(Pending.RE_ENABLE, self.internal_name(), member)
- info.cookie = cookie
- # Disable them
- if config.VERP_PROBES:
- log.info('%s: %s disabling due to probe bounce received',
- self.internal_name(), member)
- else:
- log.info('%s: %s disabling due to bounce score %s >= %s',
- self.internal_name(), member,
- info.score, self.bounce_score_threshold)
- self.setDeliveryStatus(member, DeliveryStatus.by_bounces)
- self.sendNextNotification(member)
- if self.bounce_notify_owner_on_disable:
- self.__sendAdminBounceNotice(member, msg)
-
- def __sendAdminBounceNotice(self, member, msg):
- # BAW: This is a bit kludgey, but we're not providing as much
- # information in the new admin bounce notices as we used to (some of
- # it was of dubious value). However, we'll provide empty, strange, or
- # meaningless strings for the unused %()s fields so that the language
- # translators don't have to provide new templates.
- text = Utils.maketext(
- 'bounce.txt',
- {'listname' : self.real_name,
- 'addr' : member,
- 'negative' : '',
- 'did' : _('disabled'),
- 'but' : '',
- 'reenable' : '',
- 'owneraddr': self.no_reply_address,
- }, mlist=self)
- subject = _('Bounce action notification')
- umsg = Message.UserNotification(self.GetOwnerEmail(),
- self.no_reply_address,
- subject,
- lang=self.preferred_language)
- # BAW: Be sure you set the type before trying to attach, or you'll get
- # a MultipartConversionError.
- umsg.set_type('multipart/mixed')
- umsg.attach(
- MIMEText(text, _charset=Utils.GetCharSet(self.preferred_language)))
- if isinstance(msg, str):
- umsg.attach(MIMEText(msg))
- else:
- umsg.attach(MIMEMessage(msg))
- umsg.send(self)
-
- def sendNextNotification(self, member):
- info = self.getBounceInfo(member)
- if info is None:
- return
- reason = self.getDeliveryStatus(member)
- if info.noticesleft <= 0:
- # BAW: Remove them now, with a notification message
- self.ApprovedDeleteMember(
- member, 'disabled address',
- admin_notif=self.bounce_notify_owner_on_removal,
- userack=1)
- # Expunge the pending cookie for the user. We throw away the
- # returned data.
- self.pend_confirm(info.cookie)
- if reason == DeliveryStatus.by_bounces:
- log.info('%s: %s deleted after exhausting notices',
- self.internal_name(), member)
- slog.info('%s: %s auto-unsubscribed [reason: %s]',
- self.internal_name(), member,
- {DeliveryStatus.by_bounces: 'BYBOUNCE',
- DeliveryStatus.by_user: 'BYUSER',
- DeliveryStatus.by_moderator: 'BYADMIN',
- DeliveryStatus.unknown: 'UNKNOWN'}.get(
- reason, 'invalid value'))
- return
- # Send the next notification
- confirmurl = '%s/%s' % (self.GetScriptURL('confirm', absolute=1),
- info.cookie)
- optionsurl = self.GetOptionsURL(member, absolute=1)
- reqaddr = self.GetRequestEmail()
- lang = self.getMemberLanguage(member)
- txtreason = REASONS.get(reason)
- if txtreason is None:
- txtreason = _('for unknown reasons')
- else:
- txtreason = _(txtreason)
- # Give a little bit more detail on bounce disables
- if reason == DeliveryStatus.by_bounces:
- date = time.strftime('%d-%b-%Y',
- time.localtime(Utils.midnight(info.date)))
- extra = _(' The last bounce received from you was dated %(date)s')
- txtreason += extra
- text = Utils.maketext(
- 'disabled.txt',
- {'listname' : self.real_name,
- 'noticesleft': info.noticesleft,
- 'confirmurl' : confirmurl,
- 'optionsurl' : optionsurl,
- 'password' : self.getMemberPassword(member),
- 'owneraddr' : self.GetOwnerEmail(),
- 'reason' : txtreason,
- }, lang=lang, mlist=self)
- msg = Message.UserNotification(member, reqaddr, text=text, lang=lang)
- # BAW: See the comment in MailList.py ChangeMemberAddress() for why we
- # set the Subject this way.
- del msg['subject']
- msg['Subject'] = 'confirm ' + info.cookie
- msg.send(self)
- info.noticesleft -= 1
- info.lastnotice = time.localtime()[:3]
diff --git a/mailman/attic/Defaults.py b/mailman/attic/Defaults.py
deleted file mode 100644
index 6f72ed535..000000000
--- a/mailman/attic/Defaults.py
+++ /dev/null
@@ -1,1324 +0,0 @@
-# Copyright (C) 1998-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 .
-
-"""Distributed default settings for significant Mailman config variables."""
-
-from datetime import timedelta
-
-from mailman.interfaces.mailinglist import ReplyToMunging
-
-
-
-class CompatibleTimeDelta(timedelta):
- def __float__(self):
- # Convert to float seconds.
- return (self.days * 24 * 60 * 60 +
- self.seconds + self.microseconds / 1.0e6)
-
- def __int__(self):
- return int(float(self))
-
-
-def seconds(s):
- return CompatibleTimeDelta(seconds=s)
-
-def minutes(m):
- return CompatibleTimeDelta(minutes=m)
-
-def hours(h):
- return CompatibleTimeDelta(hours=h)
-
-def days(d):
- return CompatibleTimeDelta(days=d)
-
-
-# Some convenient constants
-Yes = yes = On = on = True
-No = no = Off = off = False
-
-
-
-#####
-# General system-wide defaults
-#####
-
-# Should image logos be used? Set this to 0 to disable image logos from "our
-# sponsors" and just use textual links instead (this will also disable the
-# shortcut "favicon"). Otherwise, this should contain the URL base path to
-# the logo images (and must contain the trailing slash).. If you want to
-# disable Mailman's logo footer altogther, hack
-# mailman/htmlformat.py:MailmanLogo(), which also contains the hardcoded links
-# and image names.
-IMAGE_LOGOS = '/icons/'
-
-# The name of the Mailman favicon
-SHORTCUT_ICON = 'mm-icon.png'
-
-# Don't change MAILMAN_URL, unless you want to point it at one of the mirrors.
-MAILMAN_URL = 'http://www.gnu.org/software/mailman/index.html'
-#MAILMAN_URL = 'http://www.list.org/'
-#MAILMAN_URL = 'http://mailman.sf.net/'
-
-DEFAULT_URL_PATTERN = 'http://%s/mailman/'
-
-# This address is used as the from address whenever a message comes from some
-# entity to which there is no natural reply recipient. Set this to a real
-# human or to /dev/null. It will be appended with the hostname of the list
-# involved or the DEFAULT_EMAIL_HOST if none is available. Address must not
-# bounce and it must not point to a Mailman process.
-NO_REPLY_ADDRESS = 'noreply'
-
-# This address is the "site owner" address. Certain messages which must be
-# delivered to a human, but which can't be delivered to a list owner (e.g. a
-# bounce from a list owner), will be sent to this address. It should point to
-# a human.
-SITE_OWNER_ADDRESS = 'changeme@example.com'
-
-# Normally when a site administrator authenticates to a web page with the site
-# password, they get a cookie which authorizes them as the list admin. It
-# makes me nervous to hand out site auth cookies because if this cookie is
-# cracked or intercepted, the intruder will have access to every list on the
-# site. OTOH, it's dang handy to not have to re-authenticate to every list on
-# the site. Set this value to Yes to allow site admin cookies.
-ALLOW_SITE_ADMIN_COOKIES = No
-
-# Command that is used to convert text/html parts into plain text. This
-# should output results to standard output. %(filename)s will contain the
-# name of the temporary file that the program should operate on.
-HTML_TO_PLAIN_TEXT_COMMAND = '/usr/bin/lynx -dump %(filename)s'
-
-# Default password hashing scheme. See 'bin/mmsitepass -P' for a list of
-# available schemes.
-PASSWORD_SCHEME = 'ssha'
-
-# Default run-time directory.
-DEFAULT_VAR_DIRECTORY = '/var/mailman'
-
-
-
-#####
-# Database options
-#####
-
-# Use this to set the SQLAlchemy database engine URL. You generally have one
-# primary database connection for all of Mailman. List data and most rosters
-# will store their data in this database, although external rosters may access
-# other databases in their own way. This string support substitutions using
-# any variable in the Configuration object.
-DEFAULT_DATABASE_URL = 'sqlite:///$DATA_DIR/mailman.db'
-
-
-
-#####
-# Spam avoidance defaults
-#####
-
-# This variable contains a list of tuple of the format:
-#
-# (header, pattern[, chain])
-#
-# which is used to match against the current message's headers. If the
-# pattern matches the given header in the current message, then the named
-# chain is jumped to. header is case-insensitive and should not include the
-# trailing colon. pattern is always matched with re.IGNORECASE. chain is
-# optional; if not given the 'hold' chain is used, but if given it may be any
-# existing chain, such as 'discard', 'reject', or 'accept'.
-#
-# Note that the more searching done, the slower the whole process gets.
-# Header matching is run against all messages coming to either the list, or
-# the -owners address, unless the message is explicitly approved.
-HEADER_MATCHES = []
-
-
-
-#####
-# Web UI defaults
-#####
-
-# Almost all the colors used in Mailman's web interface are parameterized via
-# the following variables. This lets you easily change the color schemes for
-# your preferences without having to do major surgery on the source code.
-# Note that in general, the template colors are not included here since it is
-# easy enough to override the default template colors via site-wide,
-# vdomain-wide, or list-wide specializations.
-
-WEB_BG_COLOR = 'white' # Page background
-WEB_HEADER_COLOR = '#99ccff' # Major section headers
-WEB_SUBHEADER_COLOR = '#fff0d0' # Minor section headers
-WEB_ADMINITEM_COLOR = '#dddddd' # Option field background
-WEB_ADMINPW_COLOR = '#99cccc' # Password box color
-WEB_ERROR_COLOR = 'red' # Error message foreground
-WEB_LINK_COLOR = '' # If true, forces LINK=
-WEB_ALINK_COLOR = '' # If true, forces ALINK=
-WEB_VLINK_COLOR = '' # If true, forces VLINK=
-WEB_HIGHLIGHT_COLOR = '#dddddd' # If true, alternating rows
- # in listinfo & admin display
-# CGI file extension.
-CGIEXT = ''
-
-
-
-#####
-# Archive defaults
-#####
-
-# The url template for the public archives. This will be used in several
-# places, including the List-Archive: header, links to the archive on the
-# list's listinfo page, and on the list's admin page.
-#
-# This variable supports several substitution variables
-# - $hostname -- the host on which the archive resides
-# - $listname -- the short name of the list being accessed
-# - $fqdn_listname -- the long name of the list being accessed
-PUBLIC_ARCHIVE_URL = 'http://$hostname/pipermail/$fqdn_listname'
-
-# The public Mail-Archive.com service's base url.
-MAIL_ARCHIVE_BASEURL = 'http://go.mail-archive.com/'
-# The posting address for the Mail-Archive.com service
-MAIL_ARCHIVE_RECIPIENT = 'archive@mail-archive.com'
-
-# The command for archiving to a local MHonArc instance.
-MHONARC_COMMAND = """\
-/usr/bin/mhonarc \
--add \
--dbfile $PRIVATE_ARCHIVE_FILE_DIR/${listname}.mbox/mhonarc.db \
--outdir $VAR_DIR/mhonarc/${listname} \
--stderr $LOG_DIR/mhonarc \
--stdout $LOG_DIR/mhonarc \
--spammode \
--umask 022"""
-
-# Are archives on or off by default?
-DEFAULT_ARCHIVE = On
-
-# Are archives public or private by default?
-# 0=public, 1=private
-DEFAULT_ARCHIVE_PRIVATE = 0
-
-# ARCHIVE_TO_MBOX
-#-1 - do not do any archiving
-# 0 - do not archive to mbox, use builtin mailman html archiving only
-# 1 - do not use builtin mailman html archiving, archive to mbox only
-# 2 - archive to both mbox and builtin mailman html archiving.
-# See the settings below for PUBLIC_EXTERNAL_ARCHIVER and
-# PRIVATE_EXTERNAL_ARCHIVER which can be used to replace mailman's
-# builtin html archiving with an external archiver. The flat mail
-# mbox file can be useful for searching, and is another way to
-# interface external archivers, etc.
-ARCHIVE_TO_MBOX = 2
-
-# 0 - yearly
-# 1 - monthly
-# 2 - quarterly
-# 3 - weekly
-# 4 - daily
-DEFAULT_ARCHIVE_VOLUME_FREQUENCY = 1
-DEFAULT_DIGEST_VOLUME_FREQUENCY = 1
-
-# These variables control the use of an external archiver. Normally if
-# archiving is turned on (see ARCHIVE_TO_MBOX above and the list's archive*
-# attributes) the internal Pipermail archiver is used. This is the default if
-# both of these variables are set to No. When either is set, the value should
-# be a shell command string which will get passed to os.popen(). This string
-# can contain the following substitution strings:
-#
-# $listname -- gets the internal name of the list
-# $hostname -- gets the email hostname for the list
-#
-# being archived will be substituted for this. Please note that os.popen() is
-# used.
-#
-# Note that if you set one of these variables, you should set both of them
-# (they can be the same string). This will mean your external archiver will
-# be used regardless of whether public or private archives are selected.
-PUBLIC_EXTERNAL_ARCHIVER = No
-PRIVATE_EXTERNAL_ARCHIVER = No
-
-# A filter module that converts from multipart messages to "flat" messages
-# (i.e. containing a single payload). This is required for Pipermail, and you
-# may want to set it to 0 for external archivers. You can also replace it
-# with your own module as long as it contains a process() function that takes
-# a MailList object and a Message object. It should raise
-# Errors.DiscardMessage if it wants to throw the message away. Otherwise it
-# should modify the Message object as necessary.
-ARCHIVE_SCRUBBER = 'mailman.pipeline.scrubber'
-
-# Control parameter whether mailman.Handlers.Scrubber should use message
-# attachment's filename as is indicated by the filename parameter or use
-# 'attachement-xxx' instead. The default is set True because the applications
-# on PC and Mac begin to use longer non-ascii filenames. Historically, it
-# was set False in 2.1.6 for backward compatiblity but it was reset to True
-# for safer operation in mailman-2.1.7.
-SCRUBBER_DONT_USE_ATTACHMENT_FILENAME = True
-
-# Use of attachment filename extension per se is may be dangerous because
-# virus fakes it. You can set this True if you filter the attachment by
-# filename extension
-SCRUBBER_USE_ATTACHMENT_FILENAME_EXTENSION = False
-
-# This variable defines what happens to text/html subparts. They can be
-# stripped completely, escaped, or filtered through an external program. The
-# legal values are:
-# 0 - Strip out text/html parts completely, leaving a notice of the removal in
-# the message. If the outer part is text/html, the entire message is
-# discarded.
-# 1 - Remove any embedded text/html parts, leaving them as HTML-escaped
-# attachments which can be separately viewed. Outer text/html parts are
-# simply HTML-escaped.
-# 2 - Leave it inline, but HTML-escape it
-# 3 - Remove text/html as attachments but don't HTML-escape them. Note: this
-# is very dangerous because it essentially means anybody can send an HTML
-# email to your site containing evil JavaScript or web bugs, or other
-# nasty things, and folks viewing your archives will be susceptible. You
-# should only consider this option if you do heavy moderation of your list
-# postings.
-#
-# Note: given the current archiving code, it is not possible to leave
-# text/html parts inline and un-escaped. I wouldn't think it'd be a good idea
-# to do anyway.
-#
-# The value can also be a string, in which case it is the name of a command to
-# filter the HTML page through. The resulting output is left in an attachment
-# or as the entirety of the message when the outer part is text/html. The
-# format of the string must include a "%(filename)s" which will contain the
-# name of the temporary file that the program should operate on. It should
-# write the processed message to stdout. Set this to
-# HTML_TO_PLAIN_TEXT_COMMAND to specify an HTML to plain text conversion
-# program.
-ARCHIVE_HTML_SANITIZER = 1
-
-# Set this to Yes to enable gzipping of the downloadable archive .txt file.
-# Note that this is /extremely/ inefficient, so an alternative is to just
-# collect the messages in the associated .txt file and run a cron job every
-# night to generate the txt.gz file. See cron/nightly_gzip for details.
-GZIP_ARCHIVE_TXT_FILES = No
-
-# This sets the default `clobber date' policy for the archiver. When a
-# message is to be archived either by Pipermail or an external archiver,
-# Mailman can modify the Date: header to be the date the message was received
-# instead of the Date: in the original message. This is useful if you
-# typically receive messages with outrageous dates. Set this to 0 to retain
-# the date of the original message, or to 1 to always clobber the date. Set
-# it to 2 to perform `smart overrides' on the date; when the date is outside
-# ARCHIVER_ALLOWABLE_SANE_DATE_SKEW (either too early or too late), then the
-# received date is substituted instead.
-ARCHIVER_CLOBBER_DATE_POLICY = 2
-ARCHIVER_ALLOWABLE_SANE_DATE_SKEW = days(15)
-
-# Pipermail archives contain the raw email addresses of the posting authors.
-# Some view this as a goldmine for spam harvesters. Set this to Yes to
-# moderately obscure email addresses, but note that this breaks mailto: URLs
-# in the archives too.
-ARCHIVER_OBSCURES_EMAILADDRS = Yes
-
-# Pipermail assumes that messages bodies contain US-ASCII text.
-# Change this option to define a different character set to be used as
-# the default character set for the archive. The term "character set"
-# is used in MIME to refer to a method of converting a sequence of
-# octets into a sequence of characters. If you change the default
-# charset, you might need to add it to VERBATIM_ENCODING below.
-DEFAULT_CHARSET = None
-
-# Most character set encodings require special HTML entity characters to be
-# quoted, otherwise they won't look right in the Pipermail archives. However
-# some character sets must not quote these characters so that they can be
-# rendered properly in the browsers. The primary issue is multi-byte
-# encodings where the octet 0x26 does not always represent the & character.
-# This variable contains a list of such characters sets which are not
-# HTML-quoted in the archives.
-VERBATIM_ENCODING = ['iso-2022-jp']
-
-# When the archive is public, should Mailman also make the raw Unix mbox file
-# publically available?
-PUBLIC_MBOX = No
-
-
-
-#####
-# Delivery defaults
-#####
-
-# Final delivery module for outgoing mail. This handler is used for message
-# delivery to the list via the smtpd, and to an individual user. This value
-# must be a string naming an IHandler.
-DELIVERY_MODULE = 'smtp-direct'
-
-# MTA should name a module in mailman/MTA which provides the MTA specific
-# functionality for creating and removing lists. Some MTAs like Exim can be
-# configured to automatically recognize new lists, in which case the MTA
-# variable should be set to None. Use 'Manual' to print new aliases to
-# standard out (or send an email to the site list owner) for manual twiddling
-# of an /etc/aliases style file. Use 'Postfix' if you are using the Postfix
-# MTA -- but then also see POSTFIX_STYLE_VIRTUAL_DOMAINS.
-MTA = 'Manual'
-
-# If you set MTA='Postfix', then you also want to set the following variable,
-# depending on whether you're using virtual domains in Postfix, and which
-# style of virtual domain you're using. Set this to the empty list if you're
-# not using virtual domains in Postfix, or if you're using Sendmail-style
-# virtual domains (where all addresses are visible in all domains). If you're
-# using Postfix-style virtual domains, where aliases should only show up in
-# the virtual domain, set this variable to the list of host_name values to
-# write separate virtual entries for. I.e. if you run dom1.ain, dom2.ain, and
-# dom3.ain, but only dom2 and dom3 are virtual, set this variable to the list
-# ['dom2.ain', 'dom3.ain']. Matches are done against the host_name attribute
-# of the mailing lists. See the Postfix section of the installation manual
-# for details.
-POSTFIX_STYLE_VIRTUAL_DOMAINS = []
-
-# We should use a separator in place of '@' for list-etc@dom2.ain in both
-# aliases and mailman-virtual files.
-POSTFIX_VIRTUAL_SEPARATOR = '_at_'
-
-# These variables describe the program to use for regenerating the aliases.db
-# and virtual-mailman.db files, respectively, from the associated plain text
-# files. The file being updated will be appended to this string (with a
-# separating space), so it must be appropriate for os.system().
-POSTFIX_ALIAS_CMD = '/usr/sbin/postalias'
-POSTFIX_MAP_CMD = '/usr/sbin/postmap'
-
-# Ceiling on the number of recipients that can be specified in a single SMTP
-# transaction. Set to 0 to submit the entire recipient list in one
-# transaction. Only used with the SMTPDirect DELIVERY_MODULE.
-SMTP_MAX_RCPTS = 500
-
-# Ceiling on the number of SMTP sessions to perform on a single socket
-# connection. Some MTAs have limits. Set this to 0 to do as many as we like
-# (i.e. your MTA has no limits). Set this to some number great than 0 and
-# Mailman will close the SMTP connection and re-open it after this number of
-# consecutive sessions.
-SMTP_MAX_SESSIONS_PER_CONNECTION = 0
-
-# Maximum number of simultaneous subthreads that will be used for SMTP
-# delivery. After the recipients list is chunked according to SMTP_MAX_RCPTS,
-# each chunk is handed off to the smptd by a separate such thread. If your
-# Python interpreter was not built for threads, this feature is disabled. You
-# can explicitly disable it in all cases by setting MAX_DELIVERY_THREADS to
-# 0. This feature is only supported with the SMTPDirect DELIVERY_MODULE.
-#
-# NOTE: This is an experimental feature and limited testing shows that it may
-# in fact degrade performance, possibly due to Python's global interpreter
-# lock. Use with caution.
-MAX_DELIVERY_THREADS = 0
-
-# SMTP host and port, when DELIVERY_MODULE is 'SMTPDirect'. Make sure the
-# host exists and is resolvable (i.e., if it's the default of "localhost" be
-# sure there's a localhost entry in your /etc/hosts file!)
-SMTPHOST = 'localhost'
-SMTPPORT = 0 # default from smtplib
-
-# Command for direct command pipe delivery to sendmail compatible program,
-# when DELIVERY_MODULE is 'Sendmail'.
-SENDMAIL_CMD = '/usr/lib/sendmail'
-
-# Set these variables if you need to authenticate to your NNTP server for
-# Usenet posting or reading. If no authentication is necessary, specify None
-# for both variables.
-NNTP_USERNAME = None
-NNTP_PASSWORD = None
-
-# Set this if you have an NNTP server you prefer gatewayed lists to use.
-DEFAULT_NNTP_HOST = u''
-
-# These variables controls how headers must be cleansed in order to be
-# accepted by your NNTP server. Some servers like INN reject messages
-# containing prohibited headers, or duplicate headers. The NNTP server may
-# reject the message for other reasons, but there's little that can be
-# programmatically done about that. See mailman/Queue/NewsRunner.py
-#
-# First, these headers (case ignored) are removed from the original message.
-NNTP_REMOVE_HEADERS = ['nntp-posting-host', 'nntp-posting-date', 'x-trace',
- 'x-complaints-to', 'xref', 'date-received', 'posted',
- 'posting-version', 'relay-version', 'received']
-
-# Next, these headers are left alone, unless there are duplicates in the
-# original message. Any second and subsequent headers are rewritten to the
-# second named header (case preserved).
-NNTP_REWRITE_DUPLICATE_HEADERS = [
- ('To', 'X-Original-To'),
- ('CC', 'X-Original-CC'),
- ('Content-Transfer-Encoding', 'X-Original-Content-Transfer-Encoding'),
- ('MIME-Version', 'X-MIME-Version'),
- ]
-
-# Some list posts and mail to the -owner address may contain DomainKey or
-# DomainKeys Identified Mail (DKIM) signature headers .
-# Various list transformations to the message such as adding a list header or
-# footer or scrubbing attachments or even reply-to munging can break these
-# signatures. It is generally felt that these signatures have value, even if
-# broken and even if the outgoing message is resigned. However, some sites
-# may wish to remove these headers by setting this to Yes.
-REMOVE_DKIM_HEADERS = No
-
-# This is the pipeline which messages sent to the -owner address go through
-OWNER_PIPELINE = [
- 'SpamDetect',
- 'Replybot',
- 'CleanseDKIM',
- 'OwnerRecips',
- 'ToOutgoing',
- ]
-
-
-# This defines a logging subsystem confirmation file, which overrides the
-# default log settings. This is a ConfigParser formatted file which can
-# contain sections named after the logger name (without the leading 'mailman.'
-# common prefix). Each section may contain the following options:
-#
-# - level -- Overrides the default level; this may be any of the
-# standard Python logging levels, case insensitive.
-# - format -- Overrides the default format string; see below.
-# - datefmt -- Overrides the default date format string; see below.
-# - path -- Overrides the default logger path. This may be a relative
-# path name, in which case it is relative to Mailman's LOG_DIR,
-# or it may be an absolute path name. You cannot change the
-# handler class that will be used.
-# - propagate -- Boolean specifying whether to propagate log message from this
-# logger to the root "mailman" logger. You cannot override
-# settings for the root logger.
-#
-# The file name may be absolute, or relative to Mailman's etc directory.
-LOG_CONFIG_FILE = None
-
-# This defines log format strings for the SMTPDirect delivery module (see
-# DELIVERY_MODULE above). Valid %()s string substitutions include:
-#
-# time -- the time in float seconds that it took to complete the smtp
-# hand-off of the message from Mailman to your smtpd.
-#
-# size -- the size of the entire message, in bytes
-#
-# #recips -- the number of actual recipients for this message.
-#
-# #refused -- the number of smtp refused recipients (use this only in
-# SMTP_LOG_REFUSED).
-#
-# listname -- the `internal' name of the mailing list for this posting
-#
-# msg_ -- the value of the delivered message's given header. If
-# the message had no such header, then "n/a" will be used. Note though
-# that if the message had multiple such headers, then it is undefined
-# which will be used.
-#
-# allmsg_ - Same as msg_ above, but if there are multiple
-# such headers in the message, they will all be printed, separated by
-# comma-space.
-#
-# sender -- the "sender" of the messages, which will be the From: or
-# envelope-sender as determeined by the USE_ENVELOPE_SENDER variable
-# below.
-#
-# The format of the entries is a 2-tuple with the first element naming the
-# logger (as a child of the root 'mailman' logger) to print the message to,
-# and the second being a format string appropriate for Python's %-style string
-# interpolation. The file name is arbitrary; qfiles/ will be created
-# automatically if it does not exist.
-
-# The format of the message printed for every delivered message, regardless of
-# whether the delivery was successful or not. Set to None to disable the
-# printing of this log message.
-SMTP_LOG_EVERY_MESSAGE = (
- 'smtp',
- ('${message-id} smtp to $listname for ${#recips} recips, '
- 'completed in $time seconds'))
-
-# This will only be printed if there were no immediate smtp failures.
-# Mutually exclusive with SMTP_LOG_REFUSED.
-SMTP_LOG_SUCCESS = (
- 'post',
- '${message-id} post to $listname from $sender, size=$size, success')
-
-# This will only be printed if there were any addresses which encountered an
-# immediate smtp failure. Mutually exclusive with SMTP_LOG_SUCCESS.
-SMTP_LOG_REFUSED = (
- 'post',
- ('${message-id} post to $listname from $sender, size=$size, '
- '${#refused} failures'))
-
-# This will be logged for each specific recipient failure. Additional %()s
-# keys are:
-#
-# recipient -- the failing recipient address
-# failcode -- the smtp failure code
-# failmsg -- the actual smtp message, if available
-SMTP_LOG_EACH_FAILURE = (
- 'smtp-failure',
- ('${message-id} delivery to $recipient failed with code $failcode: '
- '$failmsg'))
-
-# These variables control the format and frequency of VERP-like delivery for
-# better bounce detection. VERP is Variable Envelope Return Path, defined
-# here:
-#
-# http://cr.yp.to/proto/verp.txt
-#
-# This involves encoding the address of the recipient as we (Mailman) know it
-# into the envelope sender address (i.e. the SMTP `MAIL FROM:' address).
-# Thus, no matter what kind of forwarding the recipient has in place, should
-# it eventually bounce, we will receive an unambiguous notice of the bouncing
-# address.
-#
-# However, we're technically only "VERP-like" because we're doing the envelope
-# sender encoding in Mailman, not in the MTA. We do require cooperation from
-# the MTA, so you must be sure your MTA can be configured for extended address
-# semantics.
-#
-# The first variable describes how to encode VERP envelopes. It must contain
-# these three string interpolations:
-#
-# %(bounces)s -- the list-bounces mailbox will be set here
-# %(mailbox)s -- the recipient's mailbox will be set here
-# %(host)s -- the recipient's host name will be set here
-#
-# This example uses the default below.
-#
-# FQDN list address is: mylist@dom.ain
-# Recipient is: aperson@a.nother.dom
-#
-# The envelope sender will be mylist-bounces+aperson=a.nother.dom@dom.ain
-#
-# Note that your MTA /must/ be configured to deliver such an addressed message
-# to mylist-bounces!
-VERP_DELIMITER = '+'
-VERP_FORMAT = '%(bounces)s+%(mailbox)s=%(host)s'
-
-# The second describes a regular expression to unambiguously decode such an
-# address, which will be placed in the To: header of the bounce message by the
-# bouncing MTA. Getting this right is critical -- and tricky. Learn your
-# Python regular expressions. It must define exactly three named groups,
-# bounces, mailbox and host, with the same definition as above. It will be
-# compiled case-insensitively.
-VERP_REGEXP = r'^(?P[^+]+?)\+(?P[^=]+)=(?P[^@]+)@.*$'
-
-# VERP format and regexp for probe messages
-VERP_PROBE_FORMAT = '%(bounces)s+%(token)s'
-VERP_PROBE_REGEXP = r'^(?P[^+]+?)\+(?P[^@]+)@.*$'
-# Set this Yes to activate VERP probe for disabling by bounce
-VERP_PROBES = No
-
-# A perfect opportunity for doing VERP is the password reminders, which are
-# already addressed individually to each recipient. Set this to Yes to enable
-# VERPs on all password reminders.
-VERP_PASSWORD_REMINDERS = No
-
-# Another good opportunity is when regular delivery is personalized. Here
-# again, we're already incurring the performance hit for addressing each
-# individual recipient. Set this to Yes to enable VERPs on all personalized
-# regular deliveries (personalized digests aren't supported yet).
-VERP_PERSONALIZED_DELIVERIES = No
-
-# And finally, we can VERP normal, non-personalized deliveries. However,
-# because it can be a significant performance hit, we allow you to decide how
-# often to VERP regular deliveries. This is the interval, in number of
-# messages, to do a VERP recipient address. The same variable controls both
-# regular and digest deliveries. Set to 0 to disable occasional VERPs, set to
-# 1 to VERP every delivery, or to some number > 1 for only occasional VERPs.
-VERP_DELIVERY_INTERVAL = 0
-
-# For nicer confirmation emails, use a VERP-like format which encodes the
-# confirmation cookie in the reply address. This lets us put a more user
-# friendly Subject: on the message, but requires cooperation from the MTA.
-# Format is like VERP_FORMAT above, but with the following substitutions:
-#
-# $address -- the list-confirm address
-# $cookie -- the confirmation cookie
-VERP_CONFIRM_FORMAT = '$address+$cookie'
-
-# This is analogous to VERP_REGEXP, but for splitting apart the
-# VERP_CONFIRM_FORMAT. MUAs have been observed that mung
-# From: local_part@host
-# into
-# To: "local_part"
-# when replying, so we skip everything up to '<' if any.
-VERP_CONFIRM_REGEXP = r'^(.*<)?(?P[^+]+?)\+(?P[^@]+)@.*$'
-
-# Set this to Yes to enable VERP-like (more user friendly) confirmations
-VERP_CONFIRMATIONS = No
-
-# This is the maximum number of automatic responses sent to an address because
-# of -request messages or posting hold messages. This limit prevents response
-# loops between Mailman and misconfigured remote email robots. Mailman
-# already inhibits automatic replies to any message labeled with a header
-# "Precendence: bulk|list|junk". This is a fallback safety valve so it should
-# be set fairly high. Set to 0 for no limit (probably useful only for
-# debugging).
-MAX_AUTORESPONSES_PER_DAY = 10
-
-
-
-#####
-# Qrunner defaults
-#####
-
-# Which queues should the qrunner master watchdog spawn? add_qrunner() takes
-# one required argument, which is the name of the qrunner to start
-# (capitalized and without the 'Runner' suffix). Optional second argument
-# specifies the number of parallel processes to fork for each qrunner. If
-# more than one process is used, each will take an equal subdivision of the
-# hash space, so the number must be a power of 2.
-#
-# del_qrunners() takes one argument which is the name of the qrunner not to
-# start. This is used because by default, Mailman starts the Arch, Bounce,
-# Command, Incoming, News, Outgoing, Retry, and Virgin queues.
-#
-# Set this to Yes to use the `Maildir' delivery option. If you change this
-# you will need to re-run bin/genaliases for MTAs that don't use list
-# auto-detection.
-#
-# WARNING: If you want to use Maildir delivery, you /must/ start Mailman's
-# qrunner as root, or you will get permission problems.
-USE_MAILDIR = No
-
-# Set this to Yes to use the `LMTP' delivery option. If you change this
-# you will need to re-run bin/genaliases for MTAs that don't use list
-# auto-detection.
-#
-# You have to set following line in postfix main.cf:
-# transport_maps = hash:/data/transport
-# Also needed is following line if your list is in $mydestination:
-# alias_maps = hash:/etc/aliases, hash:/data/aliases
-USE_LMTP = No
-
-# Name of the domains which operate on LMTP Mailman only. Currently valid
-# only for Postfix alias generation.
-LMTP_ONLY_DOMAINS = []
-
-# If the list is not present in LMTP_ONLY_DOMAINS, LMTPRunner would return
-# 550 response to the master SMTP agent. This may cause 'bounce spam relay'
-# in that a spammer expects to deliver the message as bounce info to the
-# 'From:' address. You can override this behavior by setting
-# LMTP_ERR_550 = '250 Ok. But, blackholed because mailbox unavailable'.
-LMTP_ERR_550 = '550 Requested action not taken: mailbox unavailable'
-
-# WSGI Server.
-#
-# You must enable PROXY of Apache httpd server and configure to pass Mailman
-# CGI requests to this WSGI Server:
-#
-# ProxyPass /mailman/ http://localhost:2580/mailman/
-#
-# Note that local URI part should be the same.
-# XXX If you are running Apache 2.2, you will probably also want to set
-# ProxyPassReverseCookiePath
-#
-# Also you have to add following line to /etc/mailman.cfg
-# add_qrunner('HTTP')
-HTTP_HOST = 'localhost'
-HTTP_PORT = 2580
-
-# After processing every file in the qrunner's slice, how long should the
-# runner sleep for before checking the queue directory again for new files?
-# This can be a fraction of a second, or zero to check immediately
-# (essentially busy-loop as fast as possible).
-QRUNNER_SLEEP_TIME = seconds(1)
-
-# When a message that is unparsable (by the email package) is received, what
-# should we do with it? The most common cause of unparsable messages is
-# broken MIME encapsulation, and the most common cause of that is viruses like
-# Nimda. Set this variable to No to discard such messages, or to Yes to store
-# them in qfiles/bad subdirectory.
-QRUNNER_SAVE_BAD_MESSAGES = Yes
-
-# This flag causes Mailman to fsync() its data files after writing and
-# flushing its contents. While this ensures the data is written to disk,
-# avoiding data loss, it may be a performance killer. Note that this flag
-# affects both message pickles and MailList config.pck files.
-SYNC_AFTER_WRITE = No
-
-# The maximum number of times that the mailmanctl watcher will try to restart
-# a qrunner that exits uncleanly.
-MAX_RESTARTS = 10
-
-
-
-#####
-# General defaults
-#####
-
-# The default language for this server. Whenever we can't figure out the list
-# context or user context, we'll fall back to using this language. This code
-# must be in the list of available language codes.
-DEFAULT_SERVER_LANGUAGE = u'en'
-
-# When allowing only members to post to a mailing list, how is the sender of
-# the message determined? If this variable is set to Yes, then first the
-# message's envelope sender is used, with a fallback to the sender if there is
-# no envelope sender. Set this variable to No to always use the sender.
-#
-# The envelope sender is set by the SMTP delivery and is thus less easily
-# spoofed than the sender, which is typically just taken from the From: header
-# and thus easily spoofed by the end-user. However, sometimes the envelope
-# sender isn't set correctly and this will manifest itself by postings being
-# held for approval even if they appear to come from a list member. If you
-# are having this problem, set this variable to No, but understand that some
-# spoofed messages may get through.
-USE_ENVELOPE_SENDER = No
-
-# Membership tests for posting purposes are usually performed by looking at a
-# set of headers, passing the test if any of their values match a member of
-# the list. Headers are checked in the order given in this variable. The
-# value None means use the From_ (envelope sender) header. Field names are
-# case insensitive.
-SENDER_HEADERS = ('from', None, 'reply-to', 'sender')
-
-# How many members to display at a time on the admin cgi to unsubscribe them
-# or change their options?
-DEFAULT_ADMIN_MEMBER_CHUNKSIZE = 30
-
-# how many bytes of a held message post should be displayed in the admindb web
-# page? Use a negative number to indicate the entire message, regardless of
-# size (though this will slow down rendering those pages).
-ADMINDB_PAGE_TEXT_LIMIT = 4096
-
-# Set this variable to Yes to allow list owners to delete their own mailing
-# lists. You may not want to give them this power, in which case, setting
-# this variable to No instead requires list removal to be done by the site
-# administrator, via the command line script bin/rmlist.
-OWNERS_CAN_DELETE_THEIR_OWN_LISTS = No
-
-# Set this variable to Yes to allow list owners to set the "personalized"
-# flags on their mailing lists. Turning these on tells Mailman to send
-# separate email messages to each user instead of batching them together for
-# delivery to the MTA. This gives each member a more personalized message,
-# but can have a heavy impact on the performance of your system.
-OWNERS_CAN_ENABLE_PERSONALIZATION = No
-
-# Should held messages be saved on disk as Python pickles or as plain text?
-# The former is more efficient since we don't need to go through the
-# parse/generate roundtrip each time, but the latter might be preferred if you
-# want to edit the held message on disk.
-HOLD_MESSAGES_AS_PICKLES = Yes
-
-# This variable controls the order in which list-specific category options are
-# presented in the admin cgi page.
-ADMIN_CATEGORIES = [
- # First column
- 'general', 'passwords', 'language', 'members', 'nondigest', 'digest',
- # Second column
- 'privacy', 'bounce', 'archive', 'gateway', 'autoreply',
- 'contentfilter', 'topics',
- ]
-
-# See "Bitfield for user options" below; make this a sum of those options, to
-# make all new members of lists start with those options flagged. We assume
-# by default that people don't want to receive two copies of posts. Note
-# however that the member moderation flag's initial value is controlled by the
-# list's config variable default_member_moderation.
-DEFAULT_NEW_MEMBER_OPTIONS = 256
-
-# Specify the type of passwords to use, when Mailman generates the passwords
-# itself, as would be the case for membership requests where the user did not
-# fill in a password, or during list creation, when auto-generation of admin
-# passwords was selected.
-#
-# Set this value to Yes for classic Mailman user-friendly(er) passwords.
-# These generate semi-pronounceable passwords which are easier to remember.
-# Set this value to No to use more cryptographically secure, but harder to
-# remember, passwords -- if your operating system and Python version support
-# the necessary feature (specifically that /dev/urandom be available).
-USER_FRIENDLY_PASSWORDS = Yes
-# This value specifies the default lengths of member and list admin passwords
-MEMBER_PASSWORD_LENGTH = 8
-ADMIN_PASSWORD_LENGTH = 10
-
-
-
-#####
-# List defaults. NOTE: Changing these values does NOT change the
-# configuration of an existing list. It only defines the default for new
-# lists you subsequently create.
-#####
-
-# Should a list, by default be advertised? What is the default maximum number
-# of explicit recipients allowed? What is the default maximum message size
-# allowed?
-DEFAULT_LIST_ADVERTISED = Yes
-DEFAULT_MAX_NUM_RECIPIENTS = 10
-DEFAULT_MAX_MESSAGE_SIZE = 40 # KB
-
-# These format strings will be expanded w.r.t. the dictionary for the
-# mailing list instance.
-DEFAULT_SUBJECT_PREFIX = u'[$mlist.real_name] '
-# DEFAULT_SUBJECT_PREFIX = "[$mlist.real_name %%d]" # for numbering
-DEFAULT_MSG_HEADER = u''
-DEFAULT_MSG_FOOTER = u"""\
-_______________________________________________
-$real_name mailing list
-$fqdn_listname
-${listinfo_page}
-"""
-
-# Scrub regular delivery
-DEFAULT_SCRUB_NONDIGEST = False
-
-# Mail command processor will ignore mail command lines after designated max.
-EMAIL_COMMANDS_MAX_LINES = 10
-
-# Is the list owner notified of admin requests immediately by mail, as well as
-# by daily pending-request reminder?
-DEFAULT_ADMIN_IMMED_NOTIFY = Yes
-
-# Is the list owner notified of subscribes/unsubscribes?
-DEFAULT_ADMIN_NOTIFY_MCHANGES = No
-
-# Discard held messages after this days
-DEFAULT_MAX_DAYS_TO_HOLD = 0
-
-# Should list members, by default, have their posts be moderated?
-DEFAULT_DEFAULT_MEMBER_MODERATION = No
-
-# Should non-member posts which are auto-discarded also be forwarded to the
-# moderators?
-DEFAULT_FORWARD_AUTO_DISCARDS = Yes
-
-# What shold happen to non-member posts which are do not match explicit
-# non-member actions?
-# 0 = Accept
-# 1 = Hold
-# 2 = Reject
-# 3 = Discard
-DEFAULT_GENERIC_NONMEMBER_ACTION = 1
-
-# Bounce if 'To:', 'Cc:', or 'Resent-To:' fields don't explicitly name list?
-# This is an anti-spam measure
-DEFAULT_REQUIRE_EXPLICIT_DESTINATION = Yes
-
-# Alternate names acceptable as explicit destinations for this list.
-DEFAULT_ACCEPTABLE_ALIASES = """
-"""
-# For mailing lists that have only other mailing lists for members:
-DEFAULT_UMBRELLA_LIST = No
-
-# For umbrella lists, the suffix for the account part of address for
-# administrative notices (subscription confirmations, password reminders):
-DEFAULT_UMBRELLA_MEMBER_ADMIN_SUFFIX = "-owner"
-
-# This variable controls whether monthly password reminders are sent.
-DEFAULT_SEND_REMINDERS = Yes
-
-# Send welcome messages to new users?
-DEFAULT_SEND_WELCOME_MSG = Yes
-
-# Send goodbye messages to unsubscribed members?
-DEFAULT_SEND_GOODBYE_MSG = Yes
-
-# Wipe sender information, and make it look like the list-admin
-# address sends all messages
-DEFAULT_ANONYMOUS_LIST = No
-
-# {header-name: regexp} spam filtering - we include some for example sake.
-DEFAULT_BOUNCE_MATCHING_HEADERS = u"""
-# Lines that *start* with a '#' are comments.
-to: friend@public.com
-message-id: relay.comanche.denmark.eu
-from: list@listme.com
-from: .*@uplinkpro.com
-"""
-
-# Mailman can be configured to "munge" Reply-To: headers for any passing
-# messages. One the one hand, there are a lot of good reasons not to munge
-# Reply-To: but on the other, people really seem to want this feature. See
-# the help for reply_goes_to_list in the web UI for links discussing the
-# issue.
-# 0 - Reply-To: not munged
-# 1 - Reply-To: set back to the list
-# 2 - Reply-To: set to an explicit value (reply_to_address)
-DEFAULT_REPLY_GOES_TO_LIST = ReplyToMunging.no_munging
-
-# Mailman can be configured to strip any existing Reply-To: header, or simply
-# extend any existing Reply-To: with one based on the above setting.
-DEFAULT_FIRST_STRIP_REPLY_TO = No
-
-# SUBSCRIBE POLICY
-# 0 - open list (only when ALLOW_OPEN_SUBSCRIBE is set to 1) **
-# 1 - confirmation required for subscribes
-# 2 - admin approval required for subscribes
-# 3 - both confirmation and admin approval required
-#
-# ** please do not choose option 0 if you are not allowing open
-# subscribes (next variable)
-DEFAULT_SUBSCRIBE_POLICY = 1
-
-# Does this site allow completely unchecked subscriptions?
-ALLOW_OPEN_SUBSCRIBE = No
-
-# This is the default list of addresses and regular expressions (beginning
-# with ^) that are exempt from approval if SUBSCRIBE_POLICY is 2 or 3.
-DEFAULT_SUBSCRIBE_AUTO_APPROVAL = []
-
-# The default policy for unsubscriptions. 0 (unmoderated unsubscribes) is
-# highly recommended!
-# 0 - unmoderated unsubscribes
-# 1 - unsubscribes require approval
-DEFAULT_UNSUBSCRIBE_POLICY = 0
-
-# Private_roster == 0: anyone can see, 1: members only, 2: admin only.
-DEFAULT_PRIVATE_ROSTER = 1
-
-# When exposing members, make them unrecognizable as email addrs, so
-# web-spiders can't pick up addrs for spam purposes.
-DEFAULT_OBSCURE_ADDRESSES = Yes
-
-# RFC 2369 defines List-* headers which are added to every message sent
-# through to the mailing list membership. These are a very useful aid to end
-# users and should always be added. However, not all MUAs are compliant and
-# if a list's membership has many such users, they may clamor for these
-# headers to be suppressed. By setting this variable to Yes, list owners will
-# be given the option to suppress these headers. By setting it to No, list
-# owners will not be given the option to suppress these headers (although some
-# header suppression may still take place, i.e. for announce-only lists, or
-# lists with no archives).
-ALLOW_RFC2369_OVERRIDES = Yes
-
-# Defaults for content filtering on mailing lists. DEFAULT_FILTER_CONTENT is
-# a flag which if set to true, turns on content filtering.
-DEFAULT_FILTER_CONTENT = No
-
-# DEFAULT_FILTER_MIME_TYPES is a list of MIME types to be removed. This is a
-# list of strings of the format "maintype/subtype" or simply "maintype".
-# E.g. "text/html" strips all html attachments while "image" strips all image
-# types regardless of subtype (jpeg, gif, etc.).
-DEFAULT_FILTER_MIME_TYPES = []
-
-# DEFAULT_PASS_MIME_TYPES is a list of MIME types to be passed through.
-# Format is the same as DEFAULT_FILTER_MIME_TYPES
-DEFAULT_PASS_MIME_TYPES = ['multipart/mixed',
- 'multipart/alternative',
- 'text/plain']
-
-# DEFAULT_FILTER_FILENAME_EXTENSIONS is a list of filename extensions to be
-# removed. It is useful because many viruses fake their content-type as
-# harmless ones while keep their extension as executable and expect to be
-# executed when victims 'open' them.
-DEFAULT_FILTER_FILENAME_EXTENSIONS = [
- 'exe', 'bat', 'cmd', 'com', 'pif', 'scr', 'vbs', 'cpl'
- ]
-
-# DEFAULT_PASS_FILENAME_EXTENSIONS is a list of filename extensions to be
-# passed through. Format is the same as DEFAULT_FILTER_FILENAME_EXTENSIONS.
-DEFAULT_PASS_FILENAME_EXTENSIONS = []
-
-# Replace multipart/alternative with its first alternative.
-DEFAULT_COLLAPSE_ALTERNATIVES = Yes
-
-# Whether text/html should be converted to text/plain after content filtering
-# is performed. Conversion is done according to HTML_TO_PLAIN_TEXT_COMMAND
-DEFAULT_CONVERT_HTML_TO_PLAINTEXT = Yes
-
-# Default action to take on filtered messages.
-# 0 = Discard, 1 = Reject, 2 = Forward, 3 = Preserve
-DEFAULT_FILTER_ACTION = 0
-
-# Whether to allow list owners to preserve content filtered messages to a
-# special queue on the disk.
-OWNERS_CAN_PRESERVE_FILTERED_MESSAGES = Yes
-
-# Check for administrivia in messages sent to the main list?
-DEFAULT_ADMINISTRIVIA = Yes
-
-
-
-#####
-# Digestification defaults. Same caveat applies here as with list defaults.
-#####
-
-# Will list be available in non-digested form?
-DEFAULT_NONDIGESTABLE = Yes
-
-# Will list be available in digested form?
-DEFAULT_DIGESTABLE = Yes
-DEFAULT_DIGEST_HEADER = u''
-DEFAULT_DIGEST_FOOTER = DEFAULT_MSG_FOOTER
-
-DEFAULT_DIGEST_IS_DEFAULT = No
-DEFAULT_MIME_IS_DEFAULT_DIGEST = No
-DEFAULT_DIGEST_SIZE_THRESHOLD = 30 # KB
-DEFAULT_DIGEST_SEND_PERIODIC = Yes
-
-# Headers which should be kept in both RFC 1153 (plain) and MIME digests. RFC
-# 1153 also specifies these headers in this exact order, so order matters.
-MIME_DIGEST_KEEP_HEADERS = [
- 'Date', 'From', 'To', 'Cc', 'Subject', 'Message-ID', 'Keywords',
- # I believe we should also keep these headers though.
- 'In-Reply-To', 'References', 'Content-Type', 'MIME-Version',
- 'Content-Transfer-Encoding', 'Precedence', 'Reply-To',
- # Mailman 2.0 adds these headers
- 'Message',
- ]
-
-PLAIN_DIGEST_KEEP_HEADERS = [
- 'Message', 'Date', 'From',
- 'Subject', 'To', 'Cc',
- 'Message-ID', 'Keywords',
- 'Content-Type',
- ]
-
-
-
-#####
-# Bounce processing defaults. Same caveat applies here as with list defaults.
-#####
-
-# Should we do any bounced mail response at all?
-DEFAULT_BOUNCE_PROCESSING = Yes
-
-# How often should the bounce qrunner process queued detected bounces?
-REGISTER_BOUNCES_EVERY = minutes(15)
-
-# Bounce processing works like this: when a bounce from a member is received,
-# we look up the `bounce info' for this member. If there is no bounce info,
-# this is the first bounce we've received from this member. In that case, we
-# record today's date, and initialize the bounce score (see below for initial
-# value).
-#
-# If there is existing bounce info for this member, we look at the last bounce
-# receive date. If this date is farther away from today than the `bounce
-# expiration interval', we throw away all the old data and initialize the
-# bounce score as if this were the first bounce from the member.
-#
-# Otherwise, we increment the bounce score. If we can determine whether the
-# bounce was soft or hard (i.e. transient or fatal), then we use a score value
-# of 0.5 for soft bounces and 1.0 for hard bounces. Note that we only score
-# one bounce per day. If the bounce score is then greater than the `bounce
-# threshold' we disable the member's address.
-#
-# After disabling the address, we can send warning messages to the member,
-# providing a confirmation cookie/url for them to use to re-enable their
-# delivery. After a configurable period of time, we'll delete the address.
-# When we delete the address due to bouncing, we'll send one last message to
-# the member.
-
-# Bounce scores greater than this value get disabled.
-DEFAULT_BOUNCE_SCORE_THRESHOLD = 5.0
-
-# Bounce information older than this interval is considered stale, and is
-# discarded.
-DEFAULT_BOUNCE_INFO_STALE_AFTER = days(7)
-
-# The number of notifications to send to the disabled/removed member before we
-# remove them from the list. A value of 0 means we remove the address
-# immediately (with one last notification). Note that the first one is sent
-# upon change of status to disabled.
-DEFAULT_BOUNCE_YOU_ARE_DISABLED_WARNINGS = 3
-
-# The interval of time between disabled warnings.
-DEFAULT_BOUNCE_YOU_ARE_DISABLED_WARNINGS_INTERVAL = days(7)
-
-# Does the list owner get messages to the -bounces (and -admin) address that
-# failed to match by the bounce detector?
-DEFAULT_BOUNCE_UNRECOGNIZED_GOES_TO_LIST_OWNER = Yes
-
-# Notifications on bounce actions. The first specifies whether the list owner
-# should get a notification when a member is disabled due to bouncing, while
-# the second specifies whether the owner should get one when the member is
-# removed due to bouncing.
-DEFAULT_BOUNCE_NOTIFY_OWNER_ON_DISABLE = Yes
-DEFAULT_BOUNCE_NOTIFY_OWNER_ON_REMOVAL = Yes
-
-
-
-#####
-# General time limits
-#####
-
-# Default length of time a pending request is live before it is evicted from
-# the pending database.
-PENDING_REQUEST_LIFE = days(3)
-
-# How long should messages which have delivery failures continue to be
-# retried? After this period of time, a message that has failed recipients
-# will be dequeued and those recipients will never receive the message.
-DELIVERY_RETRY_PERIOD = days(5)
-
-# How long should we wait before we retry a temporary delivery failure?
-DELIVERY_RETRY_WAIT = hours(1)
-
-
-
-#####
-# Lock management defaults
-#####
-
-# These variables control certain aspects of lock acquisition and retention.
-# They should be tuned as appropriate for your environment. All variables are
-# specified in units of floating point seconds. YOU MAY NEED TO TUNE THESE
-# VARIABLES DEPENDING ON THE SIZE OF YOUR LISTS, THE PERFORMANCE OF YOUR
-# HARDWARE, NETWORK AND GENERAL MAIL HANDLING CAPABILITIES, ETC.
-
-# This variable specifies how long the lock will be retained for a specific
-# operation on a mailing list. Watch your logs/lock file and if you see a lot
-# of lock breakages, you might need to bump this up. However if you set this
-# too high, a faulty script (or incorrect use of bin/withlist) can prevent the
-# list from being used until the lifetime expires. This is probably one of
-# the most crucial tuning variables in the system.
-LIST_LOCK_LIFETIME = hours(5)
-
-# This variable specifies how long an attempt will be made to acquire a list
-# lock by the incoming qrunner process. If the lock acquisition times out,
-# the message will be re-queued for later delivery.
-LIST_LOCK_TIMEOUT = seconds(10)
-
-# Set this to On to turn on lock debugging messages for the pending requests
-# database, which will be written to logs/locks. If you think you're having
-# lock problems, or just want to tune the locks for your system, turn on lock
-# debugging.
-PENDINGDB_LOCK_DEBUGGING = Off
-
-
-
-#####
-# Nothing below here is user configurable. Most of these values are in this
-# file for internal system convenience. Don't change any of them or override
-# any of them in your mailman.cfg file!
-#####
-
-# Enumeration for Mailman cgi widget types
-Toggle = 1
-Radio = 2
-String = 3
-Text = 4
-Email = 5
-EmailList = 6
-Host = 7
-Number = 8
-FileUpload = 9
-Select = 10
-Topics = 11
-Checkbox = 12
-# An "extended email list". Contents must be an email address or a ^-prefixed
-# regular expression. Used in the sender moderation text boxes.
-EmailListEx = 13
-# Extended spam filter widget
-HeaderFilter = 14
-
-# Actions
-DEFER = 0
-APPROVE = 1
-REJECT = 2
-DISCARD = 3
-SUBSCRIBE = 4
-UNSUBSCRIBE = 5
-ACCEPT = 6
-HOLD = 7
-
-# Standard text field width
-TEXTFIELDWIDTH = 40
-
-# Bitfield for user options. See DEFAULT_NEW_MEMBER_OPTIONS above to set
-# defaults for all new lists.
-Digests = 0 # handled by other mechanism, doesn't need a flag.
-DisableDelivery = 1 # Obsolete; use set/getDeliveryStatus()
-DontReceiveOwnPosts = 2 # Non-digesters only
-AcknowledgePosts = 4
-DisableMime = 8 # Digesters only
-ConcealSubscription = 16
-SuppressPasswordReminder = 32
-ReceiveNonmatchingTopics = 64
-Moderate = 128
-DontReceiveDuplicates = 256
-
-
-# A mapping between short option tags and their flag
-OPTINFO = {'hide' : ConcealSubscription,
- 'nomail' : DisableDelivery,
- 'ack' : AcknowledgePosts,
- 'notmetoo': DontReceiveOwnPosts,
- 'digest' : 0,
- 'plain' : DisableMime,
- 'nodupes' : DontReceiveDuplicates
- }
-
-# Authentication contexts.
-#
-# Mailman defines the following roles:
-
-# - User, a normal user who has no permissions except to change their personal
-# option settings
-# - List creator, someone who can create and delete lists, but cannot
-# (necessarily) configure the list.
-# - List moderator, someone who can tend to pending requests such as
-# subscription requests, or held messages
-# - List administrator, someone who has total control over a list, can
-# configure it, modify user options for members of the list, subscribe and
-# unsubscribe members, etc.
-# - Site administrator, someone who has total control over the entire site and
-# can do any of the tasks mentioned above. This person usually also has
-# command line access.
-
-UnAuthorized = 0
-AuthUser = 1 # Joe Shmoe User
-AuthCreator = 2 # List Creator / Destroyer
-AuthListAdmin = 3 # List Administrator (total control over list)
-AuthListModerator = 4 # List Moderator (can only handle held requests)
-AuthSiteAdmin = 5 # Site Administrator (total control over everything)
-
-
-
-# Vgg: Language descriptions and charsets dictionary, any new supported
-# language must have a corresponding entry here. Key is the name of the
-# directories that hold the localized texts. Data are tuples with first
-# element being the description, as described in the catalogs, and second
-# element is the language charset. I have chosen code from /usr/share/locale
-# in my GNU/Linux. :-)
-#
-# TK: Now the site admin can select languages for the installation from those
-# in the distribution tarball. We don't touch add_language() function for
-# backward compatibility. You may have to add your own language in your
-# mailman.cfg file, if it is not included in the distribution even if you had
-# put language files in source directory and configured by `--with-languages'
-# option.
-def _(s):
- return s
-
-_DEFAULT_LANGUAGE_DATA = {
- 'ar': (_('Arabic'), 'utf-8'),
- 'ca': (_('Catalan'), 'iso-8859-1'),
- 'cs': (_('Czech'), 'iso-8859-2'),
- 'da': (_('Danish'), 'iso-8859-1'),
- 'de': (_('German'), 'iso-8859-1'),
- 'en': (_('English (USA)'), 'us-ascii'),
- 'es': (_('Spanish (Spain)'), 'iso-8859-1'),
- 'et': (_('Estonian'), 'iso-8859-15'),
- 'eu': (_('Euskara'), 'iso-8859-15'), # Basque
- 'fi': (_('Finnish'), 'iso-8859-1'),
- 'fr': (_('French'), 'iso-8859-1'),
- 'hr': (_('Croatian'), 'iso-8859-2'),
- 'hu': (_('Hungarian'), 'iso-8859-2'),
- 'ia': (_('Interlingua'), 'iso-8859-15'),
- 'it': (_('Italian'), 'iso-8859-1'),
- 'ja': (_('Japanese'), 'euc-jp'),
- 'ko': (_('Korean'), 'euc-kr'),
- 'lt': (_('Lithuanian'), 'iso-8859-13'),
- 'nl': (_('Dutch'), 'iso-8859-1'),
- 'no': (_('Norwegian'), 'iso-8859-1'),
- 'pl': (_('Polish'), 'iso-8859-2'),
- 'pt': (_('Portuguese'), 'iso-8859-1'),
- 'pt_BR': (_('Portuguese (Brazil)'), 'iso-8859-1'),
- 'ro': (_('Romanian'), 'iso-8859-2'),
- 'ru': (_('Russian'), 'koi8-r'),
- 'sr': (_('Serbian'), 'utf-8'),
- 'sl': (_('Slovenian'), 'iso-8859-2'),
- 'sv': (_('Swedish'), 'iso-8859-1'),
- 'tr': (_('Turkish'), 'iso-8859-9'),
- 'uk': (_('Ukrainian'), 'utf-8'),
- 'vi': (_('Vietnamese'), 'utf-8'),
- 'zh_CN': (_('Chinese (China)'), 'utf-8'),
- 'zh_TW': (_('Chinese (Taiwan)'), 'utf-8'),
-}
-
-
-del _
diff --git a/mailman/attic/Deliverer.py b/mailman/attic/Deliverer.py
deleted file mode 100644
index 0ba3a01bb..000000000
--- a/mailman/attic/Deliverer.py
+++ /dev/null
@@ -1,174 +0,0 @@
-# Copyright (C) 1998-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 .
-
-
-"""Mixin class with message delivery routines."""
-
-from __future__ import with_statement
-
-import logging
-
-from email.MIMEMessage import MIMEMessage
-from email.MIMEText import MIMEText
-
-from mailman import Errors
-from mailman import Message
-from mailman import Utils
-from mailman import i18n
-from mailman.configuration import config
-
-_ = i18n._
-
-log = logging.getLogger('mailman.error')
-mlog = logging.getLogger('mailman.mischief')
-
-
-
-class Deliverer:
- def MailUserPassword(self, user):
- listfullname = self.fqdn_listname
- requestaddr = self.GetRequestEmail()
- # find the lowercased version of the user's address
- adminaddr = self.GetBouncesEmail()
- assert self.isMember(user)
- if not self.getMemberPassword(user):
- # The user's password somehow got corrupted. Generate a new one
- # for him, after logging this bogosity.
- log.error('User %s had a false password for list %s',
- user, self.internal_name())
- waslocked = self.Locked()
- if not waslocked:
- self.Lock()
- try:
- self.setMemberPassword(user, Utils.MakeRandomPassword())
- self.Save()
- finally:
- if not waslocked:
- self.Unlock()
- # Now send the user his password
- cpuser = self.getMemberCPAddress(user)
- recipient = self.GetMemberAdminEmail(cpuser)
- subject = _('%(listfullname)s mailing list reminder')
- # Get user's language and charset
- lang = self.getMemberLanguage(user)
- cset = Utils.GetCharSet(lang)
- password = self.getMemberPassword(user)
- # TK: Make unprintables to ?
- # The list owner should allow users to set language options if they
- # want to use non-us-ascii characters in password and send it back.
- password = unicode(password, cset, 'replace').encode(cset, 'replace')
- # get the text from the template
- text = Utils.maketext(
- 'userpass.txt',
- {'user' : cpuser,
- 'listname' : self.real_name,
- 'fqdn_lname' : self.GetListEmail(),
- 'password' : password,
- 'options_url': self.GetOptionsURL(user, absolute=True),
- 'requestaddr': requestaddr,
- 'owneraddr' : self.GetOwnerEmail(),
- }, lang=lang, mlist=self)
- msg = Message.UserNotification(recipient, adminaddr, subject, text,
- lang)
- msg['X-No-Archive'] = 'yes'
- msg.send(self, verp=config.VERP_PERSONALIZED_DELIVERIES)
-
- def ForwardMessage(self, msg, text=None, subject=None, tomoderators=True):
- # Wrap the message as an attachment
- if text is None:
- text = _('No reason given')
- if subject is None:
- text = _('(no subject)')
- text = MIMEText(Utils.wrap(text),
- _charset=Utils.GetCharSet(self.preferred_language))
- attachment = MIMEMessage(msg)
- notice = Message.OwnerNotification(
- self, subject, tomoderators=tomoderators)
- # Make it look like the message is going to the -owner address
- notice.set_type('multipart/mixed')
- notice.attach(text)
- notice.attach(attachment)
- notice.send(self)
-
- def SendHostileSubscriptionNotice(self, listname, address):
- # Some one was invited to one list but tried to confirm to a different
- # list. We inform both list owners of the bogosity, but be careful
- # not to reveal too much information.
- selfname = self.internal_name()
- mlog.error('%s was invited to %s but confirmed to %s',
- address, listname, selfname)
- # First send a notice to the attacked list
- msg = Message.OwnerNotification(
- self,
- _('Hostile subscription attempt detected'),
- Utils.wrap(_("""%(address)s was invited to a different mailing
-list, but in a deliberate malicious attempt they tried to confirm the
-invitation to your list. We just thought you'd like to know. No further
-action by you is required.""")))
- msg.send(self)
- # Now send a notice to the invitee list
- try:
- # Avoid import loops
- from mailman.MailList import MailList
- mlist = MailList(listname, lock=False)
- except Errors.MMListError:
- # Oh well
- return
- with i18n.using_language(mlist.preferred_language):
- msg = Message.OwnerNotification(
- mlist,
- _('Hostile subscription attempt detected'),
- Utils.wrap(_("""You invited %(address)s to your list, but in a
-deliberate malicious attempt, they tried to confirm the invitation to a
-different list. We just thought you'd like to know. No further action by you
-is required.""")))
- msg.send(mlist)
-
- def sendProbe(self, member, msg):
- listname = self.real_name
- # Put together the substitution dictionary.
- d = {'listname': listname,
- 'address': member,
- 'optionsurl': self.GetOptionsURL(member, absolute=True),
- 'owneraddr': self.GetOwnerEmail(),
- }
- text = Utils.maketext('probe.txt', d,
- lang=self.getMemberLanguage(member),
- mlist=self)
- # Calculate the VERP'd sender address for bounce processing of the
- # probe message.
- token = self.pend_new(Pending.PROBE_BOUNCE, member, msg)
- probedict = {
- 'bounces': self.internal_name() + '-bounces',
- 'token': token,
- }
- probeaddr = '%s@%s' % ((config.VERP_PROBE_FORMAT % probedict),
- self.host_name)
- # Calculate the Subject header, in the member's preferred language
- ulang = self.getMemberLanguage(member)
- with i18n.using_language(ulang):
- subject = _('%(listname)s mailing list probe message')
- outer = Message.UserNotification(member, probeaddr, subject,
- lang=ulang)
- outer.set_type('multipart/mixed')
- text = MIMEText(text, _charset=Utils.GetCharSet(ulang))
- outer.attach(text)
- outer.attach(MIMEMessage(msg))
- # Turn off further VERP'ing in the final delivery step. We set
- # probe_token for the OutgoingRunner to more easily handling local
- # rejects of probe messages.
- outer.send(self, envsender=probeaddr, verp=False, probe_token=token)
diff --git a/mailman/attic/Digester.py b/mailman/attic/Digester.py
deleted file mode 100644
index a88d08abc..000000000
--- a/mailman/attic/Digester.py
+++ /dev/null
@@ -1,57 +0,0 @@
-# Copyright (C) 1998-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 .
-
-
-"""Mixin class with list-digest handling methods and settings."""
-
-import os
-import errno
-
-from mailman import Errors
-from mailman import Utils
-from mailman.Handlers import ToDigest
-from mailman.configuration import config
-from mailman.i18n import _
-
-
-
-class Digester:
- def send_digest_now(self):
- # Note: Handler.ToDigest.send_digests() handles bumping the digest
- # volume and issue number.
- digestmbox = os.path.join(self.fullpath(), 'digest.mbox')
- try:
- try:
- mboxfp = None
- # See if there's a digest pending for this mailing list
- if os.stat(digestmbox).st_size > 0:
- mboxfp = open(digestmbox)
- ToDigest.send_digests(self, mboxfp)
- os.unlink(digestmbox)
- finally:
- if mboxfp:
- mboxfp.close()
- except OSError, e:
- if e.errno <> errno.ENOENT:
- raise
- # List has no outstanding digests
- return False
- return True
-
- def bump_digest_volume(self):
- self.volume += 1
- self.next_digest_number = 1
diff --git a/mailman/attic/MailList.py b/mailman/attic/MailList.py
deleted file mode 100644
index 2d538f026..000000000
--- a/mailman/attic/MailList.py
+++ /dev/null
@@ -1,731 +0,0 @@
-# Copyright (C) 1998-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 .
-
-
-"""The class representing a Mailman mailing list.
-
-Mixes in many task-specific classes.
-"""
-
-from __future__ import with_statement
-
-import os
-import re
-import sys
-import time
-import errno
-import shutil
-import socket
-import urllib
-import cPickle
-import logging
-import marshal
-import email.Iterators
-
-from UserDict import UserDict
-from cStringIO import StringIO
-from string import Template
-from types import MethodType
-from urlparse import urlparse
-from zope.interface import implements
-
-from email.Header import Header
-from email.Utils import getaddresses, formataddr, parseaddr
-
-from Mailman import Errors
-from Mailman import Utils
-from Mailman import Version
-from Mailman import database
-from Mailman.UserDesc import UserDesc
-from Mailman.configuration import config
-from Mailman.interfaces import *
-
-# Base classes
-from Mailman.Archiver import Archiver
-from Mailman.Bouncer import Bouncer
-from Mailman.Digester import Digester
-from Mailman.SecurityManager import SecurityManager
-
-# GUI components package
-from Mailman import Gui
-
-# Other useful classes
-from Mailman import i18n
-from Mailman import MemberAdaptor
-from Mailman import Message
-
-_ = i18n._
-
-DOT = '.'
-EMPTYSTRING = ''
-OR = '|'
-
-clog = logging.getLogger('mailman.config')
-elog = logging.getLogger('mailman.error')
-vlog = logging.getLogger('mailman.vette')
-slog = logging.getLogger('mailman.subscribe')
-
-
-
-# Use mixins here just to avoid having any one chunk be too large.
-class MailList(object, Archiver, Digester, SecurityManager, Bouncer):
-
- implements(
- IMailingList,
- IMailingListAddresses,
- IMailingListIdentity,
- IMailingListRosters,
- )
-
- def __init__(self, data):
- self._data = data
- # Only one level of mixin inheritance allowed.
- for baseclass in self.__class__.__bases__:
- if hasattr(baseclass, '__init__'):
- baseclass.__init__(self)
- # Initialize the web u/i components.
- self._gui = []
- for component in dir(Gui):
- if component.startswith('_'):
- continue
- self._gui.append(getattr(Gui, component)())
- # Give the extension mechanism a chance to process this list.
- try:
- from Mailman.ext import init_mlist
- except ImportError:
- pass
- else:
- init_mlist(self)
-
- def __getattr__(self, name):
- missing = object()
- if name.startswith('_'):
- return getattr(super(MailList, self), name)
- # Delegate to the database model object if it has the attribute.
- obj = getattr(self._data, name, missing)
- if obj is not missing:
- return obj
- # Finally, delegate to one of the gui components.
- for guicomponent in self._gui:
- obj = getattr(guicomponent, name, missing)
- if obj is not missing:
- return obj
- # Nothing left to delegate to, so it's got to be an error.
- raise AttributeError(name)
-
- def __repr__(self):
- return '' % (self.fqdn_listname, id(self))
-
-
- def GetConfirmJoinSubject(self, listname, cookie):
- if config.VERP_CONFIRMATIONS and cookie:
- cset = i18n.get_translation().charset() or \
- Utils.GetCharSet(self.preferred_language)
- subj = Header(
- _('Your confirmation is required to join the %(listname)s mailing list'),
- cset, header_name='subject')
- return subj
- else:
- return 'confirm ' + cookie
-
- def GetConfirmLeaveSubject(self, listname, cookie):
- if config.VERP_CONFIRMATIONS and cookie:
- cset = i18n.get_translation().charset() or \
- Utils.GetCharSet(self.preferred_language)
- subj = Header(
- _('Your confirmation is required to leave the %(listname)s mailing list'),
- cset, header_name='subject')
- return subj
- else:
- return 'confirm ' + cookie
-
- def GetMemberAdminEmail(self, member):
- """Usually the member addr, but modified for umbrella lists.
-
- Umbrella lists have other mailing lists as members, and so admin stuff
- like confirmation requests and passwords must not be sent to the
- member addresses - the sublists - but rather to the administrators of
- the sublists. This routine picks the right address, considering
- regular member address to be their own administrative addresses.
-
- """
- if not self.umbrella_list:
- return member
- else:
- acct, host = tuple(member.split('@'))
- return "%s%s@%s" % (acct, self.umbrella_member_suffix, host)
-
- def GetScriptURL(self, target, absolute=False):
- if absolute:
- return self.web_page_url + target + '/' + self.fqdn_listname
- else:
- return Utils.ScriptURL(target) + '/' + self.fqdn_listname
-
- def GetOptionsURL(self, user, obscure=False, absolute=False):
- url = self.GetScriptURL('options', absolute)
- if obscure:
- user = Utils.ObscureEmail(user)
- return '%s/%s' % (url, urllib.quote(user.lower()))
-
-
- #
- # Web API support via administrative categories
- #
- def GetConfigCategories(self):
- class CategoryDict(UserDict):
- def __init__(self):
- UserDict.__init__(self)
- self.keysinorder = config.ADMIN_CATEGORIES[:]
- def keys(self):
- return self.keysinorder
- def items(self):
- items = []
- for k in config.ADMIN_CATEGORIES:
- items.append((k, self.data[k]))
- return items
- def values(self):
- values = []
- for k in config.ADMIN_CATEGORIES:
- values.append(self.data[k])
- return values
-
- categories = CategoryDict()
- # Only one level of mixin inheritance allowed
- for gui in self._gui:
- k, v = gui.GetConfigCategory()
- categories[k] = (v, gui)
- return categories
-
- def GetConfigSubCategories(self, category):
- for gui in self._gui:
- if hasattr(gui, 'GetConfigSubCategories'):
- # Return the first one that knows about the given subcategory
- subcat = gui.GetConfigSubCategories(category)
- if subcat is not None:
- return subcat
- return None
-
- def GetConfigInfo(self, category, subcat=None):
- for gui in self._gui:
- if hasattr(gui, 'GetConfigInfo'):
- value = gui.GetConfigInfo(self, category, subcat)
- if value:
- return value
-
-
- #
- # Membership management front-ends and assertion checks
- #
- def InviteNewMember(self, userdesc, text=''):
- """Invite a new member to the list.
-
- This is done by creating a subscription pending for the user, and then
- crafting a message to the member informing them of the invitation.
- """
- invitee = userdesc.address
- Utils.ValidateEmail(invitee)
- # check for banned address
- pattern = Utils.get_pattern(invitee, self.ban_list)
- if pattern:
- raise Errors.MembershipIsBanned(pattern)
- # Hack alert! Squirrel away a flag that only invitations have, so
- # that we can do something slightly different when an invitation
- # subscription is confirmed. In those cases, we don't need further
- # admin approval, even if the list is so configured. The flag is the
- # list name to prevent invitees from cross-subscribing.
- userdesc.invitation = self.internal_name()
- cookie = self.pend_new(Pending.SUBSCRIPTION, userdesc)
- requestaddr = self.getListAddress('request')
- confirmurl = '%s/%s' % (self.GetScriptURL('confirm', absolute=1),
- cookie)
- listname = self.real_name
- text += Utils.maketext(
- 'invite.txt',
- {'email' : invitee,
- 'listname' : listname,
- 'hostname' : self.host_name,
- 'confirmurl' : confirmurl,
- 'requestaddr': requestaddr,
- 'cookie' : cookie,
- 'listowner' : self.GetOwnerEmail(),
- }, mlist=self)
- sender = self.GetRequestEmail(cookie)
- msg = Message.UserNotification(
- invitee, sender,
- text=text, lang=self.preferred_language)
- subj = self.GetConfirmJoinSubject(listname, cookie)
- del msg['subject']
- msg['Subject'] = subj
- msg.send(self)
-
- def AddMember(self, userdesc, remote=None):
- """Front end to member subscription.
-
- This method enforces subscription policy, validates values, sends
- notifications, and any other grunt work involved in subscribing a
- user. It eventually calls ApprovedAddMember() to do the actual work
- of subscribing the user.
-
- userdesc is an instance with the following public attributes:
-
- address -- the unvalidated email address of the member
- fullname -- the member's full name (i.e. John Smith)
- digest -- a flag indicating whether the user wants digests or not
- language -- the requested default language for the user
- password -- the user's password
-
- Other attributes may be defined later. Only address is required; the
- others all have defaults (fullname='', digests=0, language=list's
- preferred language, password=generated).
-
- remote is a string which describes where this add request came from.
- """
- assert self.Locked()
- # Suck values out of userdesc, apply defaults, and reset the userdesc
- # attributes (for passing on to ApprovedAddMember()). Lowercase the
- # addr's domain part.
- email = Utils.LCDomain(userdesc.address)
- name = getattr(userdesc, 'fullname', '')
- lang = getattr(userdesc, 'language', self.preferred_language)
- digest = getattr(userdesc, 'digest', None)
- password = getattr(userdesc, 'password', Utils.MakeRandomPassword())
- if digest is None:
- if self.nondigestable:
- digest = 0
- else:
- digest = 1
- # Validate the e-mail address to some degree.
- Utils.ValidateEmail(email)
- if self.isMember(email):
- raise Errors.MMAlreadyAMember, email
- if email.lower() == self.GetListEmail().lower():
- # Trying to subscribe the list to itself!
- raise Errors.InvalidEmailAddress
- realname = self.real_name
- # Is the subscribing address banned from this list?
- pattern = Utils.get_pattern(email, self.ban_list)
- if pattern:
- vlog.error('%s banned subscription: %s (matched: %s)',
- realname, email, pattern)
- raise Errors.MembershipIsBanned, pattern
- # Sanity check the digest flag
- if digest and not self.digestable:
- raise Errors.MMCantDigestError
- elif not digest and not self.nondigestable:
- raise Errors.MMMustDigestError
-
- userdesc.address = email
- userdesc.fullname = name
- userdesc.digest = digest
- userdesc.language = lang
- userdesc.password = password
-
- # Apply the list's subscription policy. 0 means open subscriptions; 1
- # means the user must confirm; 2 means the admin must approve; 3 means
- # the user must confirm and then the admin must approve
- if self.subscribe_policy == 0:
- self.ApprovedAddMember(userdesc, whence=remote or '')
- elif self.subscribe_policy == 1 or self.subscribe_policy == 3:
- # User confirmation required. BAW: this should probably just
- # accept a userdesc instance.
- cookie = self.pend_new(Pending.SUBSCRIPTION, userdesc)
- # Send the user the confirmation mailback
- if remote is None:
- by = remote = ''
- else:
- by = ' ' + remote
- remote = _(' from %(remote)s')
-
- recipient = self.GetMemberAdminEmail(email)
- confirmurl = '%s/%s' % (self.GetScriptURL('confirm', absolute=1),
- cookie)
- text = Utils.maketext(
- 'verify.txt',
- {'email' : email,
- 'listaddr' : self.GetListEmail(),
- 'listname' : realname,
- 'cookie' : cookie,
- 'requestaddr' : self.getListAddress('request'),
- 'remote' : remote,
- 'listadmin' : self.GetOwnerEmail(),
- 'confirmurl' : confirmurl,
- }, lang=lang, mlist=self)
- msg = Message.UserNotification(
- recipient, self.GetRequestEmail(cookie),
- text=text, lang=lang)
- # BAW: See ChangeMemberAddress() for why we do it this way...
- del msg['subject']
- msg['Subject'] = self.GetConfirmJoinSubject(realname, cookie)
- msg['Reply-To'] = self.GetRequestEmail(cookie)
- msg.send(self)
- who = formataddr((name, email))
- slog.info('%s: pending %s %s', self.internal_name(), who, by)
- raise Errors.MMSubscribeNeedsConfirmation
- elif self.HasAutoApprovedSender(email):
- # no approval necessary:
- self.ApprovedAddMember(userdesc)
- else:
- # Subscription approval is required. Add this entry to the admin
- # requests database. BAW: this should probably take a userdesc
- # just like above.
- self.HoldSubscription(email, name, password, digest, lang)
- raise Errors.MMNeedApproval, _(
- 'subscriptions to %(realname)s require moderator approval')
-
- def DeleteMember(self, name, whence=None, admin_notif=None, userack=True):
- realname, email = parseaddr(name)
- if self.unsubscribe_policy == 0:
- self.ApprovedDeleteMember(name, whence, admin_notif, userack)
- else:
- self.HoldUnsubscription(email)
- raise Errors.MMNeedApproval, _(
- 'unsubscriptions require moderator approval')
-
- def ChangeMemberAddress(self, oldaddr, newaddr, globally):
- # Changing a member address consists of verifying the new address,
- # making sure the new address isn't already a member, and optionally
- # going through the confirmation process.
- #
- # Most of these checks are copied from AddMember
- newaddr = Utils.LCDomain(newaddr)
- Utils.ValidateEmail(newaddr)
- # Raise an exception if this email address is already a member of the
- # list, but only if the new address is the same case-wise as the old
- # address and we're not doing a global change.
- if not globally and newaddr == oldaddr and self.isMember(newaddr):
- raise Errors.MMAlreadyAMember
- if newaddr == self.GetListEmail().lower():
- raise Errors.InvalidEmailAddress
- realname = self.real_name
- # Don't allow changing to a banned address. MAS: maybe we should
- # unsubscribe the oldaddr too just for trying, but that's probably
- # too harsh.
- pattern = Utils.get_pattern(newaddr, self.ban_list)
- if pattern:
- vlog.error('%s banned address change: %s -> %s (matched: %s)',
- realname, oldaddr, newaddr, pattern)
- raise Errors.MembershipIsBanned, pattern
- # Pend the subscription change
- cookie = self.pend_new(Pending.CHANGE_OF_ADDRESS,
- oldaddr, newaddr, globally)
- confirmurl = '%s/%s' % (self.GetScriptURL('confirm', absolute=1),
- cookie)
- lang = self.getMemberLanguage(oldaddr)
- text = Utils.maketext(
- 'verify.txt',
- {'email' : newaddr,
- 'listaddr' : self.GetListEmail(),
- 'listname' : realname,
- 'cookie' : cookie,
- 'requestaddr': self.getListAddress('request'),
- 'remote' : '',
- 'listadmin' : self.GetOwnerEmail(),
- 'confirmurl' : confirmurl,
- }, lang=lang, mlist=self)
- # BAW: We don't pass the Subject: into the UserNotification
- # constructor because it will encode it in the charset of the language
- # being used. For non-us-ascii charsets, this means it will probably
- # quopri quote it, and thus replies will also be quopri encoded. But
- # CommandRunner doesn't yet grok such headers. So, just set the
- # Subject: in a separate step, although we have to delete the one
- # UserNotification adds.
- msg = Message.UserNotification(
- newaddr, self.GetRequestEmail(cookie),
- text=text, lang=lang)
- del msg['subject']
- msg['Subject'] = self.GetConfirmJoinSubject(realname, cookie)
- msg['Reply-To'] = self.GetRequestEmail(cookie)
- msg.send(self)
-
- def ApprovedChangeMemberAddress(self, oldaddr, newaddr, globally):
- # Check here for banned address in case address was banned after
- # confirmation was mailed. MAS: If it's global change should we just
- # skip this list and proceed to the others? For now we'll throw the
- # exception.
- pattern = Utils.get_pattern(newaddr, self.ban_list)
- if pattern:
- raise Errors.MembershipIsBanned, pattern
- # It's possible they were a member of this list, but choose to change
- # their membership globally. In that case, we simply remove the old
- # address.
- if self.getMemberCPAddress(oldaddr) == newaddr:
- self.removeMember(oldaddr)
- else:
- self.changeMemberAddress(oldaddr, newaddr)
- self.log_and_notify_admin(oldaddr, newaddr)
- # If globally is true, then we also include every list for which
- # oldaddr is a member.
- if not globally:
- return
- for listname in config.list_manager.names:
- # Don't bother with ourselves
- if listname == self.internal_name():
- continue
- mlist = MailList(listname, lock=0)
- if mlist.host_name <> self.host_name:
- continue
- if not mlist.isMember(oldaddr):
- continue
- # If new address is banned from this list, just skip it.
- if Utils.get_pattern(newaddr, mlist.ban_list):
- continue
- mlist.Lock()
- try:
- # Same logic as above, re newaddr is already a member
- if mlist.getMemberCPAddress(oldaddr) == newaddr:
- mlist.removeMember(oldaddr)
- else:
- mlist.changeMemberAddress(oldaddr, newaddr)
- mlist.log_and_notify_admin(oldaddr, newaddr)
- mlist.Save()
- finally:
- mlist.Unlock()
-
- def log_and_notify_admin(self, oldaddr, newaddr):
- """Log member address change and notify admin if requested."""
- slog.info('%s: changed member address from %s to %s',
- self.internal_name(), oldaddr, newaddr)
- if self.admin_notify_mchanges:
- with i18n.using_language(self.preferred_language):
- realname = self.real_name
- subject = _('%(realname)s address change notification')
- name = self.getMemberName(newaddr)
- if name is None:
- name = ''
- if isinstance(name, unicode):
- name = name.encode(Utils.GetCharSet(self.preferred_language),
- 'replace')
- text = Utils.maketext(
- 'adminaddrchgack.txt',
- {'name' : name,
- 'oldaddr' : oldaddr,
- 'newaddr' : newaddr,
- 'listname': self.real_name,
- }, mlist=self)
- msg = Message.OwnerNotification(self, subject, text)
- msg.send(self)
-
-
- #
- # Confirmation processing
- #
- def ProcessConfirmation(self, cookie, context=None):
- rec = self.pend_confirm(cookie)
- if rec is None:
- raise Errors.MMBadConfirmation, 'No cookie record for %s' % cookie
- try:
- op = rec[0]
- data = rec[1:]
- except ValueError:
- raise Errors.MMBadConfirmation, 'op-less data %s' % (rec,)
- if op == Pending.SUBSCRIPTION:
- whence = 'via email confirmation'
- try:
- userdesc = data[0]
- # If confirmation comes from the web, context should be a
- # UserDesc instance which contains overrides of the original
- # subscription information. If it comes from email, then
- # context is a Message and isn't relevant, so ignore it.
- if isinstance(context, UserDesc):
- userdesc += context
- whence = 'via web confirmation'
- addr = userdesc.address
- fullname = userdesc.fullname
- password = userdesc.password
- digest = userdesc.digest
- lang = userdesc.language
- except ValueError:
- raise Errors.MMBadConfirmation, 'bad subscr data %s' % (data,)
- # Hack alert! Was this a confirmation of an invitation?
- invitation = getattr(userdesc, 'invitation', False)
- # We check for both 2 (approval required) and 3 (confirm +
- # approval) because the policy could have been changed in the
- # middle of the confirmation dance.
- if invitation:
- if invitation <> self.internal_name():
- # Not cool. The invitee was trying to subscribe to a
- # different list than they were invited to. Alert both
- # list administrators.
- self.SendHostileSubscriptionNotice(invitation, addr)
- raise Errors.HostileSubscriptionError
- elif self.subscribe_policy in (2, 3) and \
- not self.HasAutoApprovedSender(addr):
- self.HoldSubscription(addr, fullname, password, digest, lang)
- name = self.real_name
- raise Errors.MMNeedApproval, _(
- 'subscriptions to %(name)s require administrator approval')
- self.ApprovedAddMember(userdesc, whence=whence)
- return op, addr, password, digest, lang
- elif op == Pending.UNSUBSCRIPTION:
- addr = data[0]
- # Log file messages don't need to be i18n'd
- if isinstance(context, Message.Message):
- whence = 'email confirmation'
- else:
- whence = 'web confirmation'
- # Can raise NotAMemberError if they unsub'd via other means
- self.ApprovedDeleteMember(addr, whence=whence)
- return op, addr
- elif op == Pending.CHANGE_OF_ADDRESS:
- oldaddr, newaddr, globally = data
- self.ApprovedChangeMemberAddress(oldaddr, newaddr, globally)
- return op, oldaddr, newaddr
- elif op == Pending.HELD_MESSAGE:
- id = data[0]
- approved = None
- # Confirmation should be coming from email, where context should
- # be the confirming message. If the message does not have an
- # Approved: header, this is a discard. If it has an Approved:
- # header that does not match the list password, then we'll notify
- # the list administrator that they used the wrong password.
- # Otherwise it's an approval.
- if isinstance(context, Message.Message):
- # See if it's got an Approved: header, either in the headers,
- # or in the first text/plain section of the response. For
- # robustness, we'll accept Approve: as well.
- approved = context.get('Approved', context.get('Approve'))
- if not approved:
- try:
- subpart = list(email.Iterators.typed_subpart_iterator(
- context, 'text', 'plain'))[0]
- except IndexError:
- subpart = None
- if subpart:
- s = StringIO(subpart.get_payload())
- while True:
- line = s.readline()
- if not line:
- break
- if not line.strip():
- continue
- i = line.find(':')
- if i > 0:
- if (line[:i].lower() == 'approve' or
- line[:i].lower() == 'approved'):
- # then
- approved = line[i+1:].strip()
- break
- # Is there an approved header?
- if approved is not None:
- # Does it match the list password? Note that we purposefully
- # do not allow the site password here.
- if self.Authenticate([config.AuthListAdmin,
- config.AuthListModerator],
- approved) <> config.UnAuthorized:
- action = config.APPROVE
- else:
- # The password didn't match. Re-pend the message and
- # inform the list moderators about the problem.
- self.pend_repend(cookie, rec)
- raise Errors.MMBadPasswordError
- else:
- action = config.DISCARD
- try:
- self.HandleRequest(id, action)
- except KeyError:
- # Most likely because the message has already been disposed of
- # via the admindb page.
- elog.error('Could not process HELD_MESSAGE: %s', id)
- return (op,)
- elif op == Pending.RE_ENABLE:
- member = data[1]
- self.setDeliveryStatus(member, MemberAdaptor.ENABLED)
- return op, member
- else:
- assert 0, 'Bad op: %s' % op
-
- def ConfirmUnsubscription(self, addr, lang=None, remote=None):
- if lang is None:
- lang = self.getMemberLanguage(addr)
- cookie = self.pend_new(Pending.UNSUBSCRIPTION, addr)
- confirmurl = '%s/%s' % (self.GetScriptURL('confirm', absolute=1),
- cookie)
- realname = self.real_name
- if remote is not None:
- by = " " + remote
- remote = _(" from %(remote)s")
- else:
- by = ""
- remote = ""
- text = Utils.maketext(
- 'unsub.txt',
- {'email' : addr,
- 'listaddr' : self.GetListEmail(),
- 'listname' : realname,
- 'cookie' : cookie,
- 'requestaddr' : self.getListAddress('request'),
- 'remote' : remote,
- 'listadmin' : self.GetOwnerEmail(),
- 'confirmurl' : confirmurl,
- }, lang=lang, mlist=self)
- msg = Message.UserNotification(
- addr, self.GetRequestEmail(cookie),
- text=text, lang=lang)
- # BAW: See ChangeMemberAddress() for why we do it this way...
- del msg['subject']
- msg['Subject'] = self.GetConfirmLeaveSubject(realname, cookie)
- msg['Reply-To'] = self.GetRequestEmail(cookie)
- msg.send(self)
-
-
- #
- # Miscellaneous stuff
- #
-
- def HasAutoApprovedSender(self, sender):
- """Returns True and logs if sender matches address or pattern
- in subscribe_auto_approval. Otherwise returns False.
- """
- auto_approve = False
- if Utils.get_pattern(sender, self.subscribe_auto_approval):
- auto_approve = True
- vlog.info('%s: auto approved subscribe from %s',
- self.internal_name(), sender)
- return auto_approve
-
-
- #
- # Multilingual (i18n) support
- #
- def set_languages(self, *language_codes):
- # XXX FIXME not to use a database entity directly.
- from Mailman.database.model import Language
- # Don't use the language_codes property because that will add the
- # default server language. The effect would be that the default
- # server language would never get added to the list's list of
- # languages.
- requested_codes = set(language_codes)
- enabled_codes = set(config.languages.enabled_codes)
- self.available_languages = [
- Language(code) for code in requested_codes & enabled_codes]
-
- def add_language(self, language_code):
- self.available_languages.append(Language(language_code))
-
- @property
- def language_codes(self):
- # Callers of this method expect a list of language codes
- available_codes = set(self.available_languages)
- enabled_codes = set(config.languages.enabled_codes)
- codes = available_codes & enabled_codes
- # If we don't add this, and the site admin has never added any
- # language support to the list, then the general admin page may have a
- # blank field where the list owner is supposed to chose the list's
- # preferred language.
- if config.DEFAULT_SERVER_LANGUAGE not in codes:
- codes.add(config.DEFAULT_SERVER_LANGUAGE)
- return list(codes)
diff --git a/mailman/attic/SecurityManager.py b/mailman/attic/SecurityManager.py
deleted file mode 100644
index 8d4a30592..000000000
--- a/mailman/attic/SecurityManager.py
+++ /dev/null
@@ -1,306 +0,0 @@
-# Copyright (C) 1998-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 .
-
-"""Handle passwords and sanitize approved messages."""
-
-# There are current 5 roles defined in Mailman, as codified in Defaults.py:
-# user, list-creator, list-moderator, list-admin, site-admin.
-#
-# Here's how we do cookie based authentication.
-#
-# Each role (see above) has an associated password, which is currently the
-# only way to authenticate a role (in the future, we'll authenticate a
-# user and assign users to roles).
-#
-# Each cookie has the following ingredients: the authorization context's
-# secret (i.e. the password, and a timestamp. We generate an SHA1 hex
-# digest of these ingredients, which we call the 'mac'. We then marshal
-# up a tuple of the timestamp and the mac, hexlify that and return that as
-# a cookie keyed off the authcontext. Note that authenticating the user
-# also requires the user's email address to be included in the cookie.
-#
-# The verification process is done in CheckCookie() below. It extracts
-# the cookie, unhexlifies and unmarshals the tuple, extracting the
-# timestamp. Using this, and the shared secret, the mac is calculated,
-# and it must match the mac passed in the cookie. If so, they're golden,
-# otherwise, access is denied.
-#
-# It is still possible for an adversary to attempt to brute force crack
-# the password if they obtain the cookie, since they can extract the
-# timestamp and create macs based on password guesses. They never get a
-# cleartext version of the password though, so security rests on the
-# difficulty and expense of retrying the cgi dialog for each attempt. It
-# also relies on the security of SHA1.
-
-import os
-import re
-import sha
-import time
-import urllib
-import Cookie
-import logging
-import marshal
-import binascii
-
-from urlparse import urlparse
-
-from Mailman import Defaults
-from Mailman import Errors
-from Mailman import Utils
-from Mailman import passwords
-from Mailman.configuration import config
-
-log = logging.getLogger('mailman.error')
-dlog = logging.getLogger('mailman.debug')
-
-SLASH = '/'
-
-
-
-class SecurityManager:
- def AuthContextInfo(self, authcontext, user=None):
- # authcontext may be one of AuthUser, AuthListModerator,
- # AuthListAdmin, AuthSiteAdmin. Not supported is the AuthCreator
- # context.
- #
- # user is ignored unless authcontext is AuthUser
- #
- # Return the authcontext's secret and cookie key. If the authcontext
- # doesn't exist, return the tuple (None, None). If authcontext is
- # AuthUser, but the user isn't a member of this mailing list, a
- # NotAMemberError will be raised. If the user's secret is None, raise
- # a MMBadUserError.
- key = urllib.quote(self.fqdn_listname) + '+'
- if authcontext == Defaults.AuthUser:
- if user is None:
- # A bad system error
- raise TypeError('No user supplied for AuthUser context')
- secret = self.getMemberPassword(user)
- userdata = urllib.quote(Utils.ObscureEmail(user), safe='')
- key += 'user+%s' % userdata
- elif authcontext == Defaults.AuthListModerator:
- secret = self.mod_password
- key += 'moderator'
- elif authcontext == Defaults.AuthListAdmin:
- secret = self.password
- key += 'admin'
- # BAW: AuthCreator
- elif authcontext == Defaults.AuthSiteAdmin:
- sitepass = Utils.get_global_password()
- if config.ALLOW_SITE_ADMIN_COOKIES and sitepass:
- secret = sitepass
- key = 'site'
- else:
- # BAW: this should probably hand out a site password based
- # cookie, but that makes me a bit nervous, so just treat site
- # admin as a list admin since there is currently no site
- # admin-only functionality.
- secret = self.password
- key += 'admin'
- else:
- return None, None
- return key, secret
-
- def Authenticate(self, authcontexts, response, user=None):
- # Given a list of authentication contexts, check to see if the
- # response matches one of the passwords. authcontexts must be a
- # sequence, and if it contains the context AuthUser, then the user
- # argument must not be None.
- #
- # Return the authcontext from the argument sequence that matches the
- # response, or UnAuthorized.
- for ac in authcontexts:
- if ac == Defaults.AuthCreator:
- ok = Utils.check_global_password(response, siteadmin=False)
- if ok:
- return Defaults.AuthCreator
- elif ac == Defaults.AuthSiteAdmin:
- ok = Utils.check_global_password(response)
- if ok:
- return Defaults.AuthSiteAdmin
- elif ac == Defaults.AuthListAdmin:
- # The password for the list admin and list moderator are not
- # kept as plain text, but instead as an sha hexdigest. The
- # response being passed in is plain text, so we need to
- # digestify it first.
- key, secret = self.AuthContextInfo(ac)
- if secret is None:
- continue
- if passwords.check_response(secret, response):
- return ac
- elif ac == Defaults.AuthListModerator:
- # The list moderator password must be sha'd
- key, secret = self.AuthContextInfo(ac)
- if secret and passwords.check_response(secret, response):
- return ac
- elif ac == Defaults.AuthUser:
- if user is not None:
- try:
- if self.authenticateMember(user, response):
- return ac
- except Errors.NotAMemberError:
- pass
- else:
- # What is this context???
- log.error('Bad authcontext: %s', ac)
- raise ValueError('Bad authcontext: %s' % ac)
- return Defaults.UnAuthorized
-
- def WebAuthenticate(self, authcontexts, response, user=None):
- # Given a list of authentication contexts, check to see if the cookie
- # contains a matching authorization, falling back to checking whether
- # the response matches one of the passwords. authcontexts must be a
- # sequence, and if it contains the context AuthUser, then the user
- # argument should not be None.
- #
- # Returns a flag indicating whether authentication succeeded or not.
- for ac in authcontexts:
- ok = self.CheckCookie(ac, user)
- if ok:
- return True
- # Check passwords
- ac = self.Authenticate(authcontexts, response, user)
- if ac:
- print self.MakeCookie(ac, user)
- return True
- return False
-
- def _cookie_path(self):
- script_name = os.environ.get('SCRIPT_NAME', '')
- return SLASH.join(script_name.split(SLASH)[:-1]) + SLASH
-
- def MakeCookie(self, authcontext, user=None):
- key, secret = self.AuthContextInfo(authcontext, user)
- if key is None or secret is None or not isinstance(secret, basestring):
- raise ValueError
- # Timestamp
- issued = int(time.time())
- # Get a digest of the secret, plus other information.
- mac = sha.new(secret + repr(issued)).hexdigest()
- # Create the cookie object.
- c = Cookie.SimpleCookie()
- c[key] = binascii.hexlify(marshal.dumps((issued, mac)))
- c[key]['path'] = self._cookie_path()
- # We use session cookies, so don't set 'expires' or 'max-age' keys.
- # Set the RFC 2109 required header.
- c[key]['version'] = 1
- return c
-
- def ZapCookie(self, authcontext, user=None):
- # We can throw away the secret.
- key, secret = self.AuthContextInfo(authcontext, user)
- # Logout of the session by zapping the cookie. For safety both set
- # max-age=0 (as per RFC2109) and set the cookie data to the empty
- # string.
- c = Cookie.SimpleCookie()
- c[key] = ''
- c[key]['path'] = self._cookie_path()
- c[key]['max-age'] = 0
- # Don't set expires=0 here otherwise it'll force a persistent cookie
- c[key]['version'] = 1
- return c
-
- def CheckCookie(self, authcontext, user=None):
- # Two results can occur: we return 1 meaning the cookie authentication
- # succeeded for the authorization context, we return 0 meaning the
- # authentication failed.
- #
- # Dig out the cookie data, which better be passed on this cgi
- # environment variable. If there's no cookie data, we reject the
- # authentication.
- cookiedata = os.environ.get('HTTP_COOKIE')
- if not cookiedata:
- return False
- # We can't use the Cookie module here because it isn't liberal in what
- # it accepts. Feed it a MM2.0 cookie along with a MM2.1 cookie and
- # you get a CookieError. :(. All we care about is accessing the
- # cookie data via getitem, so we'll use our own parser, which returns
- # a dictionary.
- c = parsecookie(cookiedata)
- # If the user was not supplied, but the authcontext is AuthUser, we
- # can try to glean the user address from the cookie key. There may be
- # more than one matching key (if the user has multiple accounts
- # subscribed to this list), but any are okay.
- if authcontext == Defaults.AuthUser:
- if user:
- usernames = [user]
- else:
- usernames = []
- prefix = urllib.quote(self.fqdn_listname) + '+user+'
- for k in c.keys():
- if k.startswith(prefix):
- usernames.append(k[len(prefix):])
- # If any check out, we're golden. Note: '@'s are no longer legal
- # values in cookie keys.
- for user in [Utils.UnobscureEmail(u) for u in usernames]:
- ok = self.__checkone(c, authcontext, user)
- if ok:
- return True
- return False
- else:
- return self.__checkone(c, authcontext, user)
-
- def __checkone(self, c, authcontext, user):
- # Do the guts of the cookie check, for one authcontext/user
- # combination.
- try:
- key, secret = self.AuthContextInfo(authcontext, user)
- except Errors.NotAMemberError:
- return False
- if key not in c or not isinstance(secret, basestring):
- return False
- # Undo the encoding we performed in MakeCookie() above. BAW: I
- # believe this is safe from exploit because marshal can't be forced to
- # load recursive data structures, and it can't be forced to execute
- # any unexpected code. The worst that can happen is that either the
- # client will have provided us bogus data, in which case we'll get one
- # of the caught exceptions, or marshal format will have changed, in
- # which case, the cookie decoding will fail. In either case, we'll
- # simply request reauthorization, resulting in a new cookie being
- # returned to the client.
- try:
- data = marshal.loads(binascii.unhexlify(c[key]))
- issued, received_mac = data
- except (EOFError, ValueError, TypeError, KeyError):
- return False
- # Make sure the issued timestamp makes sense
- now = time.time()
- if now < issued:
- return False
- # Calculate what the mac ought to be based on the cookie's timestamp
- # and the shared secret.
- mac = sha.new(secret + repr(issued)).hexdigest()
- if mac <> received_mac:
- return False
- # Authenticated!
- return True
-
-
-
-splitter = re.compile(';\s*')
-
-def parsecookie(s):
- c = {}
- for line in s.splitlines():
- for p in splitter.split(line):
- try:
- k, v = p.split('=', 1)
- except ValueError:
- pass
- else:
- c[k] = v
- return c
diff --git a/mailman/attic/bin/clone_member b/mailman/attic/bin/clone_member
deleted file mode 100755
index 1f2a03aca..000000000
--- a/mailman/attic/bin/clone_member
+++ /dev/null
@@ -1,219 +0,0 @@
-#! @PYTHON@
-#
-# Copyright (C) 1998,1999,2000,2001,2002 by the Free Software Foundation, Inc.
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-"""Clone a member address.
-
-Cloning a member address means that a new member will be added who has all the
-same options and passwords as the original member address. Note that this
-operation is fairly trusting of the user who runs it -- it does no
-verification to the new address, it does not send out a welcome message, etc.
-
-The existing member's subscription is usually not modified in any way. If you
-want to remove the old address, use the -r flag. If you also want to change
-any list admin addresses, use the -a flag.
-
-Usage:
- clone_member [options] fromoldaddr tonewaddr
-
-Where:
-
- --listname=listname
- -l listname
- Check and modify only the named mailing lists. If -l is not given,
- then all mailing lists are scanned from the address. Multiple -l
- options can be supplied.
-
- --remove
- -r
- Remove the old address from the mailing list after it's been cloned.
-
- --admin
- -a
- Scan the list admin addresses for the old address, and clone or change
- them too.
-
- --quiet
- -q
- Do the modifications quietly.
-
- --nomodify
- -n
- Print what would be done, but don't actually do it. Inhibits the
- --quiet flag.
-
- --help
- -h
- Print this help message and exit.
-
- fromoldaddr (`from old address') is the old address of the user. tonewaddr
- (`to new address') is the new address of the user.
-
-"""
-
-import sys
-import getopt
-
-import paths
-from Mailman import MailList
-from Mailman import Utils
-from Mailman import Errors
-from Mailman.i18n import _
-
-
-
-def usage(code, msg=''):
- if code:
- fd = sys.stderr
- else:
- fd = sys.stdout
- print >> fd, _(__doc__)
- if msg:
- print >> fd, msg
- sys.exit(code)
-
-
-
-def dolist(mlist, options):
- SPACE = ' '
- if not options.quiet:
- print _('processing mailing list:'), mlist.internal_name()
-
- # scan the list owners. TBD: mlist.owner keys should be lowercase?
- oldowners = mlist.owner[:]
- oldowners.sort()
- if options.admintoo:
- if not options.quiet:
- print _(' scanning list owners:'), SPACE.join(oldowners)
- newowners = {}
- foundp = 0
- for owner in mlist.owner:
- if options.lfromaddr == owner.lower():
- foundp = 1
- if options.remove:
- continue
- newowners[owner] = 1
- if foundp:
- newowners[options.toaddr] = 1
- newowners = newowners.keys()
- newowners.sort()
- if options.modify:
- mlist.owner = newowners
- if not options.quiet:
- if newowners <> oldowners:
- print
- print _(' new list owners:'), SPACE.join(newowners)
- else:
- print _('(no change)')
-
- # see if the fromaddr is a digest member or regular member
- if options.lfromaddr in mlist.getDigestMemberKeys():
- digest = 1
- elif options.lfromaddr in mlist.getRegularMemberKeys():
- digest = 0
- else:
- if not options.quiet:
- print _(' address not found:'), options.fromaddr
- return
-
- # Now change the membership address
- try:
- if options.modify:
- mlist.changeMemberAddress(options.fromaddr, options.toaddr,
- not options.remove)
- if not options.quiet:
- print _(' clone address added:'), options.toaddr
- except Errors.MMAlreadyAMember:
- if not options.quiet:
- print _(' clone address is already a member:'), options.toaddr
-
- if options.remove:
- print _(' original address removed:'), options.fromaddr
-
-
-
-def main():
- # default options
- class Options:
- listnames = None
- remove = 0
- admintoo = 0
- quiet = 0
- modify = 1
-
- # scan sysargs
- try:
- opts, args = getopt.getopt(
- sys.argv[1:], 'arl:qnh',
- ['admin', 'remove', 'listname=', 'quiet', 'nomodify', 'help'])
- except getopt.error, msg:
- usage(1, msg)
-
- options = Options()
- for opt, arg in opts:
- if opt in ('-h', '--help'):
- usage(0)
- elif opt in ('-q', '--quiet'):
- options.quiet = 1
- elif opt in ('-n', '--nomodify'):
- options.modify = 0
- elif opt in ('-a', '--admin'):
- options.admintoo = 1
- elif opt in ('-r', '--remove'):
- options.remove = 1
- elif opt in ('-l', '--listname'):
- if options.listnames is None:
- options.listnames = []
- options.listnames.append(arg.lower())
-
- # further options and argument processing
- if not options.modify:
- options.quiet = 0
-
- if len(args) <> 2:
- usage(1)
- fromaddr = args[0]
- toaddr = args[1]
-
- # validate and normalize the target address
- try:
- Utils.ValidateEmail(toaddr)
- except Errors.EmailAddressError:
- usage(1, _('Not a valid email address: %(toaddr)s'))
- lfromaddr = fromaddr.lower()
- options.toaddr = toaddr
- options.fromaddr = fromaddr
- options.lfromaddr = lfromaddr
-
- if options.listnames is None:
- options.listnames = Utils.list_names()
-
- for listname in options.listnames:
- try:
- mlist = MailList.MailList(listname)
- except Errors.MMListError, e:
- print _('Error opening list "%(listname)s", skipping.\n%(e)s')
- continue
- try:
- dolist(mlist, options)
- finally:
- mlist.Save()
- mlist.Unlock()
-
-
-if __name__ == '__main__':
- main()
diff --git a/mailman/attic/bin/discard b/mailman/attic/bin/discard
deleted file mode 100644
index c30198441..000000000
--- a/mailman/attic/bin/discard
+++ /dev/null
@@ -1,120 +0,0 @@
-#! @PYTHON@
-#
-# Copyright (C) 2003 by the Free Software Foundation, Inc.
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-"""Discard held messages.
-
-Usage:
- discard [options] file ...
-
-Options:
- --help / -h
- Print this help message and exit.
-
- --quiet / -q
- Don't print status messages.
-"""
-
-# TODO: add command line arguments for specifying other actions than DISCARD,
-# and also for specifying other __handlepost() arguments, i.e. comment,
-# preserve, forward, addr
-
-import os
-import re
-import sys
-import getopt
-
-import paths
-from Mailman import mm_cfg
-from Mailman.MailList import MailList
-from Mailman.i18n import _
-
-try:
- True, False
-except NameError:
- True = 1
- False = 0
-
-cre = re.compile(r'heldmsg-(?P.*)-(?P[0-9]+)\.(pck|txt)$')
-
-
-
-def usage(code, msg=''):
- if code:
- fd = sys.stderr
- else:
- fd = sys.stdout
- print >> fd, _(__doc__)
- if msg:
- print >> fd, msg
- sys.exit(code)
-
-
-
-def main():
- try:
- opts, args = getopt.getopt(sys.argv[1:], 'hq', ['help', 'quiet'])
- except getopt.error, msg:
- usage(1, msg)
-
- quiet = False
- for opt, arg in opts:
- if opt in ('-h', '--help'):
- usage(0)
- elif opt in ('-q', '--quiet'):
- quiet = True
-
- files = args
- if not files:
- print _('Nothing to do.')
-
- # Mapping from listnames to sequence of request ids
- discards = {}
-
- # Cruise through all the named files, collating by mailing list. We'll
- # lock the list once, process all holds for that list and move on.
- for f in files:
- basename = os.path.basename(f)
- mo = cre.match(basename)
- if not mo:
- print >> sys.stderr, _('Ignoring non-held message: %(f)s')
- continue
- listname, id = mo.group('listname', 'id')
- try:
- id = int(id)
- except (ValueError, TypeError):
- print >> sys.stderr, _('Ignoring held msg w/bad id: %(f)s')
- continue
- discards.setdefault(listname, []).append(id)
-
- # Now do the discards
- for listname, ids in discards.items():
- mlist = MailList(listname)
- try:
- for id in ids:
- # No comment, no preserve, no forward, no forwarding address
- mlist.HandleRequest(id, mm_cfg.DISCARD, '', False, False, '')
- if not quiet:
- print _('Discarded held msg #%(id)s for list %(listname)s')
- mlist.Save()
- finally:
- mlist.Unlock()
-
-
-
-if __name__ == '__main__':
- main()
diff --git a/mailman/attic/bin/fix_url.py b/mailman/attic/bin/fix_url.py
deleted file mode 100644
index 30618a1a3..000000000
--- a/mailman/attic/bin/fix_url.py
+++ /dev/null
@@ -1,93 +0,0 @@
-#! @PYTHON@
-#
-# Copyright (C) 2001-2009 by the Free Software Foundation, Inc.
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
-# USA.
-
-"""Reset a list's web_page_url attribute to the default setting.
-
-This script is intended to be run as a bin/withlist script, i.e.
-
-% bin/withlist -l -r fix_url listname [options]
-
-Options:
- -u urlhost
- --urlhost=urlhost
- Look up urlhost in the virtual host table and set the web_page_url and
- host_name attributes of the list to the values found. This
- essentially moves the list from one virtual domain to another.
-
- Without this option, the default web_page_url and host_name values are
- used.
-
- -v / --verbose
- Print what the script is doing.
-
-If run standalone, it prints this help text and exits.
-"""
-
-import sys
-import getopt
-
-import paths
-from Mailman.configuration import config
-from Mailman.i18n import _
-
-
-
-def usage(code, msg=''):
- print _(__doc__.replace('%', '%%'))
- if msg:
- print msg
- sys.exit(code)
-
-
-
-def fix_url(mlist, *args):
- try:
- opts, args = getopt.getopt(args, 'u:v', ['urlhost=', 'verbose'])
- except getopt.error, msg:
- usage(1, msg)
-
- verbose = 0
- urlhost = mailhost = None
- for opt, arg in opts:
- if opt in ('-u', '--urlhost'):
- urlhost = arg
- elif opt in ('-v', '--verbose'):
- verbose = 1
-
- if urlhost:
- web_page_url = config.DEFAULT_URL_PATTERN % urlhost
- mailhost = config.VIRTUAL_HOSTS.get(urlhost.lower(), urlhost)
- else:
- web_page_url = config.DEFAULT_URL_PATTERN % config.DEFAULT_URL_HOST
- mailhost = config.DEFAULT_EMAIL_HOST
-
- if verbose:
- print _('Setting web_page_url to: %(web_page_url)s')
- mlist.web_page_url = web_page_url
- if verbose:
- print _('Setting host_name to: %(mailhost)s')
- mlist.host_name = mailhost
- print _('Saving list')
- mlist.Save()
- mlist.Unlock()
-
-
-
-if __name__ == '__main__':
- usage(0)
diff --git a/mailman/attic/bin/list_admins b/mailman/attic/bin/list_admins
deleted file mode 100644
index c628a42dc..000000000
--- a/mailman/attic/bin/list_admins
+++ /dev/null
@@ -1,101 +0,0 @@
-#! @PYTHON@
-#
-# Copyright (C) 2001,2002 by the Free Software Foundation, Inc.
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-"""List all the owners of a mailing list.
-
-Usage: %(program)s [options] listname ...
-
-Where:
-
- --all-vhost=vhost
- -v=vhost
- List the owners of all the mailing lists for the given virtual host.
-
- --all
- -a
- List the owners of all the mailing lists on this system.
-
- --help
- -h
- Print this help message and exit.
-
-`listname' is the name of the mailing list to print the owners of. You can
-have more than one named list on the command line.
-"""
-
-import sys
-import getopt
-
-import paths
-from Mailman import MailList, Utils
-from Mailman import Errors
-from Mailman.i18n import _
-
-COMMASPACE = ', '
-
-program = sys.argv[0]
-
-
-
-def usage(code, msg=''):
- if code:
- fd = sys.stderr
- else:
- fd = sys.stdout
- print >> fd, _(__doc__)
- if msg:
- print >> fd, msg
- sys.exit(code)
-
-
-
-def main():
- try:
- opts, args = getopt.getopt(sys.argv[1:], 'hv:a',
- ['help', 'all-vhost=', 'all'])
- except getopt.error, msg:
- usage(1, msg)
-
- listnames = args
- vhost = None
- for opt, arg in opts:
- if opt in ('-h', '--help'):
- usage(0)
- elif opt in ('-a', '--all'):
- listnames = Utils.list_names()
- elif opt in ('-v', '--all-vhost'):
- listnames = Utils.list_names()
- vhost = arg
-
- for listname in listnames:
- try:
- mlist = MailList.MailList(listname, lock=0)
- except Errors.MMListError, e:
- print _('No such list: %(listname)s')
- continue
-
- if vhost and vhost <> mlist.host_name:
- continue
-
- owners = COMMASPACE.join(mlist.owner)
- print _('List: %(listname)s, \tOwners: %(owners)s')
-
-
-
-if __name__ == '__main__':
- main()
diff --git a/mailman/attic/bin/msgfmt.py b/mailman/attic/bin/msgfmt.py
deleted file mode 100644
index 8a2d4e66e..000000000
--- a/mailman/attic/bin/msgfmt.py
+++ /dev/null
@@ -1,203 +0,0 @@
-#! /usr/bin/env python
-# -*- coding: iso-8859-1 -*-
-# Written by Martin v. Löwis
-
-"""Generate binary message catalog from textual translation description.
-
-This program converts a textual Uniforum-style message catalog (.po file) into
-a binary GNU catalog (.mo file). This is essentially the same function as the
-GNU msgfmt program, however, it is a simpler implementation.
-
-Usage: msgfmt.py [OPTIONS] filename.po
-
-Options:
- -o file
- --output-file=file
- Specify the output file to write to. If omitted, output will go to a
- file named filename.mo (based off the input file name).
-
- -h
- --help
- Print this message and exit.
-
- -V
- --version
- Display version information and exit.
-"""
-
-import sys
-import os
-import getopt
-import struct
-import array
-
-__version__ = "1.1"
-
-MESSAGES = {}
-
-
-
-def usage(code, msg=''):
- print >> sys.stderr, __doc__
- if msg:
- print >> sys.stderr, msg
- sys.exit(code)
-
-
-
-def add(id, str, fuzzy):
- "Add a non-fuzzy translation to the dictionary."
- global MESSAGES
- if not fuzzy and str:
- MESSAGES[id] = str
-
-
-
-def generate():
- "Return the generated output."
- global MESSAGES
- keys = MESSAGES.keys()
- # the keys are sorted in the .mo file
- keys.sort()
- offsets = []
- ids = strs = ''
- for id in keys:
- # For each string, we need size and file offset. Each string is NUL
- # terminated; the NUL does not count into the size.
- offsets.append((len(ids), len(id), len(strs), len(MESSAGES[id])))
- ids += id + '\0'
- strs += MESSAGES[id] + '\0'
- output = ''
- # The header is 7 32-bit unsigned integers. We don't use hash tables, so
- # the keys start right after the index tables.
- # translated string.
- keystart = 7*4+16*len(keys)
- # and the values start after the keys
- valuestart = keystart + len(ids)
- koffsets = []
- voffsets = []
- # The string table first has the list of keys, then the list of values.
- # Each entry has first the size of the string, then the file offset.
- for o1, l1, o2, l2 in offsets:
- koffsets += [l1, o1+keystart]
- voffsets += [l2, o2+valuestart]
- offsets = koffsets + voffsets
- output = struct.pack("Iiiiiii",
- 0x950412deL, # Magic
- 0, # Version
- len(keys), # # of entries
- 7*4, # start of key index
- 7*4+len(keys)*8, # start of value index
- 0, 0) # size and offset of hash table
- output += array.array("i", offsets).tostring()
- output += ids
- output += strs
- return output
-
-
-
-def make(filename, outfile):
- ID = 1
- STR = 2
-
- # Compute .mo name from .po name and arguments
- if filename.endswith('.po'):
- infile = filename
- else:
- infile = filename + '.po'
- if outfile is None:
- outfile = os.path.splitext(infile)[0] + '.mo'
-
- try:
- lines = open(infile).readlines()
- except IOError, msg:
- print >> sys.stderr, msg
- sys.exit(1)
-
- section = None
- fuzzy = 0
-
- # Parse the catalog
- lno = 0
- for l in lines:
- lno += 1
- # If we get a comment line after a msgstr, this is a new entry
- if l[0] == '#' and section == STR:
- add(msgid, msgstr, fuzzy)
- section = None
- fuzzy = 0
- # Record a fuzzy mark
- if l[:2] == '#,' and l.find('fuzzy'):
- fuzzy = 1
- # Skip comments
- if l[0] == '#':
- continue
- # Now we are in a msgid section, output previous section
- if l.startswith('msgid'):
- if section == STR:
- add(msgid, msgstr, fuzzy)
- section = ID
- l = l[5:]
- msgid = msgstr = ''
- # Now we are in a msgstr section
- elif l.startswith('msgstr'):
- section = STR
- l = l[6:]
- # Skip empty lines
- l = l.strip()
- if not l:
- continue
- # XXX: Does this always follow Python escape semantics?
- l = eval(l)
- if section == ID:
- msgid += l
- elif section == STR:
- msgstr += l
- else:
- print >> sys.stderr, 'Syntax error on %s:%d' % (infile, lno), \
- 'before:'
- print >> sys.stderr, l
- sys.exit(1)
- # Add last entry
- if section == STR:
- add(msgid, msgstr, fuzzy)
-
- # Compute output
- output = generate()
-
- try:
- open(outfile,"wb").write(output)
- except IOError,msg:
- print >> sys.stderr, msg
-
-
-
-def main():
- try:
- opts, args = getopt.getopt(sys.argv[1:], 'hVo:',
- ['help', 'version', 'output-file='])
- except getopt.error, msg:
- usage(1, msg)
-
- outfile = None
- # parse options
- for opt, arg in opts:
- if opt in ('-h', '--help'):
- usage(0)
- elif opt in ('-V', '--version'):
- print >> sys.stderr, "msgfmt.py", __version__
- sys.exit(0)
- elif opt in ('-o', '--output-file'):
- outfile = arg
- # do it
- if not args:
- print >> sys.stderr, 'No input file given'
- print >> sys.stderr, "Try `msgfmt --help' for more information."
- return
-
- for filename in args:
- make(filename, outfile)
-
-
-if __name__ == '__main__':
- main()
diff --git a/mailman/attic/bin/po2templ.py b/mailman/attic/bin/po2templ.py
deleted file mode 100644
index 86eae96b9..000000000
--- a/mailman/attic/bin/po2templ.py
+++ /dev/null
@@ -1,90 +0,0 @@
-#! @PYTHON@
-#
-# Copyright (C) 2005-2009 by the Free Software Foundation, Inc.
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
-# USA.
-
-# Author: Tokio Kikuchi
-
-
-"""po2templ.py
-
-Extract templates from language po file.
-
-Usage: po2templ.py languages
-"""
-
-import re
-import sys
-
-cre = re.compile('^#:\s*templates/en/(?P.*?):1')
-
-
-
-def do_lang(lang):
- in_template = False
- in_msg = False
- msgstr = ''
- fp = file('messages/%s/LC_MESSAGES/mailman.po' % lang)
- try:
- for line in fp:
- m = cre.search(line)
- if m:
- in_template = True
- in_msg = False
- filename = m.group('filename')
- outfilename = 'templates/%s/%s' % (lang, filename)
- continue
- if in_template and line.startswith('#,'):
- if line.strip() == '#, fuzzy':
- in_template = False
- continue
- if in_template and line.startswith('msgstr'):
- line = line[7:]
- in_msg = True
- if in_msg:
- if not line.strip():
- in_template = False
- in_msg = False
- if len(msgstr) > 1 and outfilename:
- # exclude no translation ... 1 is for LF only
- outfile = file(outfilename, 'w')
- try:
- outfile.write(msgstr)
- outfile.write('\n')
- finally:
- outfile.close()
- outfilename = ''
- msgstr = ''
- continue
- msgstr += eval(line)
- finally:
- fp.close()
- if len(msgstr) > 1 and outfilename:
- # flush remaining msgstr (last template file)
- outfile = file(outfilename, 'w')
- try:
- outfile.write(msgstr)
- outfile.write('\n')
- finally:
- outfile.close()
-
-
-
-if __name__ == '__main__':
- langs = sys.argv[1:]
- for lang in langs:
- do_lang(lang)
diff --git a/mailman/attic/bin/pygettext.py b/mailman/attic/bin/pygettext.py
deleted file mode 100644
index 84421ee8c..000000000
--- a/mailman/attic/bin/pygettext.py
+++ /dev/null
@@ -1,545 +0,0 @@
-#! @PYTHON@
-# Originally written by Barry Warsaw
-#
-# Minimally patched to make it even more xgettext compatible
-# by Peter Funk
-
-"""pygettext -- Python equivalent of xgettext(1)
-
-Many systems (Solaris, Linux, Gnu) provide extensive tools that ease the
-internationalization of C programs. Most of these tools are independent of
-the programming language and can be used from within Python programs. Martin
-von Loewis' work[1] helps considerably in this regard.
-
-There's one problem though; xgettext is the program that scans source code
-looking for message strings, but it groks only C (or C++). Python introduces
-a few wrinkles, such as dual quoting characters, triple quoted strings, and
-raw strings. xgettext understands none of this.
-
-Enter pygettext, which uses Python's standard tokenize module to scan Python
-source code, generating .pot files identical to what GNU xgettext[2] generates
-for C and C++ code. From there, the standard GNU tools can be used.
-
-A word about marking Python strings as candidates for translation. GNU
-xgettext recognizes the following keywords: gettext, dgettext, dcgettext, and
-gettext_noop. But those can be a lot of text to include all over your code.
-C and C++ have a trick: they use the C preprocessor. Most internationalized C
-source includes a #define for gettext() to _() so that what has to be written
-in the source is much less. Thus these are both translatable strings:
-
- gettext("Translatable String")
- _("Translatable String")
-
-Python of course has no preprocessor so this doesn't work so well. Thus,
-pygettext searches only for _() by default, but see the -k/--keyword flag
-below for how to augment this.
-
- [1] http://www.python.org/workshops/1997-10/proceedings/loewis.html
- [2] http://www.gnu.org/software/gettext/gettext.html
-
-NOTE: pygettext attempts to be option and feature compatible with GNU xgettext
-where ever possible. However some options are still missing or are not fully
-implemented. Also, xgettext's use of command line switches with option
-arguments is broken, and in these cases, pygettext just defines additional
-switches.
-
-Usage: pygettext [options] inputfile ...
-
-Options:
-
- -a
- --extract-all
- Extract all strings.
-
- -d name
- --default-domain=name
- Rename the default output file from messages.pot to name.pot.
-
- -E
- --escape
- Replace non-ASCII characters with octal escape sequences.
-
- -D
- --docstrings
- Extract module, class, method, and function docstrings. These do not
- need to be wrapped in _() markers, and in fact cannot be for Python to
- consider them docstrings. (See also the -X option).
-
- -h
- --help
- Print this help message and exit.
-
- -k word
- --keyword=word
- Keywords to look for in addition to the default set, which are:
- %(DEFAULTKEYWORDS)s
-
- You can have multiple -k flags on the command line.
-
- -K
- --no-default-keywords
- Disable the default set of keywords (see above). Any keywords
- explicitly added with the -k/--keyword option are still recognized.
-
- --no-location
- Do not write filename/lineno location comments.
-
- -n
- --add-location
- Write filename/lineno location comments indicating where each
- extracted string is found in the source. These lines appear before
- each msgid. The style of comments is controlled by the -S/--style
- option. This is the default.
-
- -o filename
- --output=filename
- Rename the default output file from messages.pot to filename. If
- filename is `-' then the output is sent to standard out.
-
- -p dir
- --output-dir=dir
- Output files will be placed in directory dir.
-
- -S stylename
- --style stylename
- Specify which style to use for location comments. Two styles are
- supported:
-
- Solaris # File: filename, line: line-number
- GNU #: filename:line
-
- The style name is case insensitive. GNU style is the default.
-
- -v
- --verbose
- Print the names of the files being processed.
-
- -V
- --version
- Print the version of pygettext and exit.
-
- -w columns
- --width=columns
- Set width of output to columns.
-
- -x filename
- --exclude-file=filename
- Specify a file that contains a list of strings that are not be
- extracted from the input files. Each string to be excluded must
- appear on a line by itself in the file.
-
- -X filename
- --no-docstrings=filename
- Specify a file that contains a list of files (one per line) that
- should not have their docstrings extracted. This is only useful in
- conjunction with the -D option above.
-
-If `inputfile' is -, standard input is read.
-"""
-
-import os
-import sys
-import time
-import getopt
-import tokenize
-import operator
-
-# for selftesting
-try:
- import fintl
- _ = fintl.gettext
-except ImportError:
- def _(s): return s
-
-__version__ = '1.4'
-
-default_keywords = ['_']
-DEFAULTKEYWORDS = ', '.join(default_keywords)
-
-EMPTYSTRING = ''
-
-
-
-# The normal pot-file header. msgmerge and Emacs's po-mode work better if it's
-# there.
-pot_header = _('''\
-# SOME DESCRIPTIVE TITLE.
-# Copyright (C) YEAR ORGANIZATION
-# FIRST AUTHOR , YEAR.
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: PACKAGE VERSION\\n"
-"POT-Creation-Date: %(time)s\\n"
-"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n"
-"Last-Translator: FULL NAME \\n"
-"Language-Team: LANGUAGE \\n"
-"MIME-Version: 1.0\\n"
-"Content-Type: text/plain; charset=CHARSET\\n"
-"Content-Transfer-Encoding: ENCODING\\n"
-"Generated-By: pygettext.py %(version)s\\n"
-
-''')
-
-
-def usage(code, msg=''):
- if code:
- fd = sys.stderr
- else:
- fd = sys.stdout
- print >> fd, _(__doc__) % globals()
- if msg:
- print >> fd, msg
- sys.exit(code)
-
-
-
-escapes = []
-
-def make_escapes(pass_iso8859):
- global escapes
- if pass_iso8859:
- # Allow iso-8859 characters to pass through so that e.g. 'msgid
- # "H[o-umlaut]he"' would result not result in 'msgid "H\366he"'.
- # Otherwise we escape any character outside the 32..126 range.
- mod = 128
- else:
- mod = 256
- for i in range(256):
- if 32 <= (i % mod) <= 126:
- escapes.append(chr(i))
- else:
- escapes.append("\\%03o" % i)
- escapes[ord('\\')] = '\\\\'
- escapes[ord('\t')] = '\\t'
- escapes[ord('\r')] = '\\r'
- escapes[ord('\n')] = '\\n'
- escapes[ord('\"')] = '\\"'
-
-
-def escape(s):
- global escapes
- s = list(s)
- for i in range(len(s)):
- s[i] = escapes[ord(s[i])]
- return EMPTYSTRING.join(s)
-
-
-def safe_eval(s):
- # unwrap quotes, safely
- return eval(s, {'__builtins__':{}}, {})
-
-
-def normalize(s):
- # This converts the various Python string types into a format that is
- # appropriate for .po files, namely much closer to C style.
- lines = s.split('\n')
- if len(lines) == 1:
- s = '"' + escape(s) + '"'
- else:
- if not lines[-1]:
- del lines[-1]
- lines[-1] = lines[-1] + '\n'
- for i in range(len(lines)):
- lines[i] = escape(lines[i])
- lineterm = '\\n"\n"'
- s = '""\n"' + lineterm.join(lines) + '"'
- return s
-
-
-
-class TokenEater:
- def __init__(self, options):
- self.__options = options
- self.__messages = {}
- self.__state = self.__waiting
- self.__data = []
- self.__lineno = -1
- self.__freshmodule = 1
- self.__curfile = None
-
- def __call__(self, ttype, tstring, stup, etup, line):
- # dispatch
-## import token
-## print >> sys.stderr, 'ttype:', token.tok_name[ttype], \
-## 'tstring:', tstring
- self.__state(ttype, tstring, stup[0])
-
- def __waiting(self, ttype, tstring, lineno):
- opts = self.__options
- # Do docstring extractions, if enabled
- if opts.docstrings and not opts.nodocstrings.get(self.__curfile):
- # module docstring?
- if self.__freshmodule:
- if ttype == tokenize.STRING:
- self.__addentry(safe_eval(tstring), lineno, isdocstring=1)
- self.__freshmodule = 0
- elif ttype not in (tokenize.COMMENT, tokenize.NL):
- self.__freshmodule = 0
- return
- # class docstring?
- if ttype == tokenize.NAME and tstring in ('class', 'def'):
- self.__state = self.__suiteseen
- return
- if ttype == tokenize.NAME and tstring in opts.keywords:
- self.__state = self.__keywordseen
-
- def __suiteseen(self, ttype, tstring, lineno):
- # ignore anything until we see the colon
- if ttype == tokenize.OP and tstring == ':':
- self.__state = self.__suitedocstring
-
- def __suitedocstring(self, ttype, tstring, lineno):
- # ignore any intervening noise
- if ttype == tokenize.STRING:
- self.__addentry(safe_eval(tstring), lineno, isdocstring=1)
- self.__state = self.__waiting
- elif ttype not in (tokenize.NEWLINE, tokenize.INDENT,
- tokenize.COMMENT):
- # there was no class docstring
- self.__state = self.__waiting
-
- def __keywordseen(self, ttype, tstring, lineno):
- if ttype == tokenize.OP and tstring == '(':
- self.__data = []
- self.__lineno = lineno
- self.__state = self.__openseen
- else:
- self.__state = self.__waiting
-
- def __openseen(self, ttype, tstring, lineno):
- if ttype == tokenize.OP and tstring == ')':
- # We've seen the last of the translatable strings. Record the
- # line number of the first line of the strings and update the list
- # of messages seen. Reset state for the next batch. If there
- # were no strings inside _(), then just ignore this entry.
- if self.__data:
- self.__addentry(EMPTYSTRING.join(self.__data))
- self.__state = self.__waiting
- elif ttype == tokenize.STRING:
- self.__data.append(safe_eval(tstring))
- # TBD: should we warn if we seen anything else?
-
- def __addentry(self, msg, lineno=None, isdocstring=0):
- if lineno is None:
- lineno = self.__lineno
- if not msg in self.__options.toexclude:
- entry = (self.__curfile, lineno)
- self.__messages.setdefault(msg, {})[entry] = isdocstring
-
- def set_filename(self, filename):
- self.__curfile = filename
- self.__freshmodule = 1
-
- def write(self, fp):
- options = self.__options
- timestamp = time.ctime(time.time())
- # The time stamp in the header doesn't have the same format as that
- # generated by xgettext...
- print >> fp, pot_header % {'time': timestamp, 'version': __version__}
- # Sort the entries. First sort each particular entry's keys, then
- # sort all the entries by their first item.
- reverse = {}
- for k, v in self.__messages.items():
- keys = v.keys()
- keys.sort()
- reverse.setdefault(tuple(keys), []).append((k, v))
- rkeys = reverse.keys()
- rkeys.sort()
- for rkey in rkeys:
- rentries = reverse[rkey]
- rentries.sort()
- for k, v in rentries:
- isdocstring = 0
- # If the entry was gleaned out of a docstring, then add a
- # comment stating so. This is to aid translators who may wish
- # to skip translating some unimportant docstrings.
- if reduce(operator.__add__, v.values()):
- isdocstring = 1
- # k is the message string, v is a dictionary-set of (filename,
- # lineno) tuples. We want to sort the entries in v first by
- # file name and then by line number.
- v = v.keys()
- v.sort()
- if not options.writelocations:
- pass
- # location comments are different b/w Solaris and GNU:
- elif options.locationstyle == options.SOLARIS:
- for filename, lineno in v:
- d = {'filename': filename, 'lineno': lineno}
- print >>fp, _(
- '# File: %(filename)s, line: %(lineno)d') % d
- elif options.locationstyle == options.GNU:
- # fit as many locations on one line, as long as the
- # resulting line length doesn't exceeds 'options.width'
- locline = '#:'
- for filename, lineno in v:
- d = {'filename': filename, 'lineno': lineno}
- s = _(' %(filename)s:%(lineno)d') % d
- if len(locline) + len(s) <= options.width:
- locline = locline + s
- else:
- print >> fp, locline
- locline = "#:" + s
- if len(locline) > 2:
- print >> fp, locline
- if isdocstring:
- print >> fp, '#, docstring'
- print >> fp, 'msgid', normalize(k)
- print >> fp, 'msgstr ""\n'
-
-
-
-def main():
- global default_keywords
- try:
- opts, args = getopt.getopt(
- sys.argv[1:],
- 'ad:DEhk:Kno:p:S:Vvw:x:X:',
- ['extract-all', 'default-domain=', 'escape', 'help',
- 'keyword=', 'no-default-keywords',
- 'add-location', 'no-location', 'output=', 'output-dir=',
- 'style=', 'verbose', 'version', 'width=', 'exclude-file=',
- 'docstrings', 'no-docstrings',
- ])
- except getopt.error, msg:
- usage(1, msg)
-
- # for holding option values
- class Options:
- # constants
- GNU = 1
- SOLARIS = 2
- # defaults
- extractall = 0 # FIXME: currently this option has no effect at all.
- escape = 0
- keywords = []
- outpath = ''
- outfile = 'messages.pot'
- writelocations = 1
- locationstyle = GNU
- verbose = 0
- width = 78
- excludefilename = ''
- docstrings = 0
- nodocstrings = {}
-
- options = Options()
- locations = {'gnu' : options.GNU,
- 'solaris' : options.SOLARIS,
- }
-
- # parse options
- for opt, arg in opts:
- if opt in ('-h', '--help'):
- usage(0)
- elif opt in ('-a', '--extract-all'):
- options.extractall = 1
- elif opt in ('-d', '--default-domain'):
- options.outfile = arg + '.pot'
- elif opt in ('-E', '--escape'):
- options.escape = 1
- elif opt in ('-D', '--docstrings'):
- options.docstrings = 1
- elif opt in ('-k', '--keyword'):
- options.keywords.append(arg)
- elif opt in ('-K', '--no-default-keywords'):
- default_keywords = []
- elif opt in ('-n', '--add-location'):
- options.writelocations = 1
- elif opt in ('--no-location',):
- options.writelocations = 0
- elif opt in ('-S', '--style'):
- options.locationstyle = locations.get(arg.lower())
- if options.locationstyle is None:
- usage(1, _('Invalid value for --style: %s') % arg)
- elif opt in ('-o', '--output'):
- options.outfile = arg
- elif opt in ('-p', '--output-dir'):
- options.outpath = arg
- elif opt in ('-v', '--verbose'):
- options.verbose = 1
- elif opt in ('-V', '--version'):
- print _('pygettext.py (xgettext for Python) %s') % __version__
- sys.exit(0)
- elif opt in ('-w', '--width'):
- try:
- options.width = int(arg)
- except ValueError:
- usage(1, _('--width argument must be an integer: %s') % arg)
- elif opt in ('-x', '--exclude-file'):
- options.excludefilename = arg
- elif opt in ('-X', '--no-docstrings'):
- fp = open(arg)
- try:
- while 1:
- line = fp.readline()
- if not line:
- break
- options.nodocstrings[line[:-1]] = 1
- finally:
- fp.close()
-
- # calculate escapes
- make_escapes(options.escape)
-
- # calculate all keywords
- options.keywords.extend(default_keywords)
-
- # initialize list of strings to exclude
- if options.excludefilename:
- try:
- fp = open(options.excludefilename)
- options.toexclude = fp.readlines()
- fp.close()
- except IOError:
- print >> sys.stderr, _(
- "Can't read --exclude-file: %s") % options.excludefilename
- sys.exit(1)
- else:
- options.toexclude = []
-
- # slurp through all the files
- eater = TokenEater(options)
- for filename in args:
- if filename == '-':
- if options.verbose:
- print _('Reading standard input')
- fp = sys.stdin
- closep = 0
- else:
- if options.verbose:
- print _('Working on %s') % filename
- fp = open(filename)
- closep = 1
- try:
- eater.set_filename(filename)
- try:
- tokenize.tokenize(fp.readline, eater)
- except tokenize.TokenError, e:
- print >> sys.stderr, '%s: %s, line %d, column %d' % (
- e[0], filename, e[1][0], e[1][1])
- finally:
- if closep:
- fp.close()
-
- # write the output
- if options.outfile == '-':
- fp = sys.stdout
- closep = 0
- else:
- if options.outpath:
- options.outfile = os.path.join(options.outpath, options.outfile)
- fp = open(options.outfile, 'w')
- closep = 1
- try:
- eater.write(fp)
- finally:
- if closep:
- fp.close()
-
-
-if __name__ == '__main__':
- main()
- # some more test strings
- _(u'a unicode string')
diff --git a/mailman/attic/bin/remove_members b/mailman/attic/bin/remove_members
deleted file mode 100755
index a7b4ebb47..000000000
--- a/mailman/attic/bin/remove_members
+++ /dev/null
@@ -1,186 +0,0 @@
-#! @PYTHON@
-#
-# Copyright (C) 1998-2005 by the Free Software Foundation, Inc.
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
-# USA.
-
-"""Remove members from a list.
-
-Usage:
- remove_members [options] [listname] [addr1 ...]
-
-Options:
-
- --file=file
- -f file
- Remove member addresses found in the given file. If file is
- `-', read stdin.
-
- --all
- -a
- Remove all members of the mailing list.
- (mutually exclusive with --fromall)
-
- --fromall
- Removes the given addresses from all the lists on this system
- regardless of virtual domains if you have any. This option cannot be
- used -a/--all. Also, you should not specify a listname when using
- this option.
-
- --nouserack
- -n
- Don't send the user acknowledgements. If not specified, the list
- default value is used.
-
- --noadminack
- -N
- Don't send the admin acknowledgements. If not specified, the list
- default value is used.
-
- --help
- -h
- Print this help message and exit.
-
- listname is the name of the mailing list to use.
-
- addr1 ... are additional addresses to remove.
-"""
-
-import sys
-import getopt
-
-import paths
-from Mailman import MailList
-from Mailman import Utils
-from Mailman import Errors
-from Mailman.i18n import _
-
-try:
- True, False
-except NameError:
- True = 1
- False = 0
-
-
-
-def usage(code, msg=''):
- if code:
- fd = sys.stderr
- else:
- fd = sys.stdout
- print >> fd, _(__doc__)
- if msg:
- print >> fd, msg
- sys.exit(code)
-
-
-def ReadFile(filename):
- lines = []
- if filename == "-":
- fp = sys.stdin
- closep = False
- else:
- fp = open(filename)
- closep = True
- lines = filter(None, [line.strip() for line in fp.readlines()])
- if closep:
- fp.close()
- return lines
-
-
-
-def main():
- try:
- opts, args = getopt.getopt(
- sys.argv[1:], 'naf:hN',
- ['all', 'fromall', 'file=', 'help', 'nouserack', 'noadminack'])
- except getopt.error, msg:
- usage(1, msg)
-
- filename = None
- all = False
- alllists = False
- # None means use list default
- userack = None
- admin_notif = None
-
- for opt, arg in opts:
- if opt in ('-h', '--help'):
- usage(0)
- elif opt in ('-f', '--file'):
- filename = arg
- elif opt in ('-a', '--all'):
- all = True
- elif opt == '--fromall':
- alllists = True
- elif opt in ('-n', '--nouserack'):
- userack = False
- elif opt in ('-N', '--noadminack'):
- admin_notif = False
-
- if len(args) < 1 and not (filename and alllists):
- usage(1)
-
- # You probably don't want to delete all the users of all the lists -- Marc
- if all and alllists:
- usage(1)
-
- if alllists:
- addresses = args
- else:
- listname = args[0].lower().strip()
- addresses = args[1:]
-
- if alllists:
- listnames = Utils.list_names()
- else:
- listnames = [listname]
-
- if filename:
- try:
- addresses = addresses + ReadFile(filename)
- except IOError:
- print _('Could not open file for reading: %(filename)s.')
-
- for listname in listnames:
- try:
- # open locked
- mlist = MailList.MailList(listname)
- except Errors.MMListError:
- print _('Error opening list %(listname)s... skipping.')
- continue
-
- if all:
- addresses = mlist.getMembers()
-
- try:
- for addr in addresses:
- if not mlist.isMember(addr):
- if not alllists:
- print _('No such member: %(addr)s')
- continue
- mlist.ApprovedDeleteMember(addr, 'bin/remove_members',
- admin_notif, userack)
- if alllists:
- print _("User `%(addr)s' removed from list: %(listname)s.")
- mlist.Save()
- finally:
- mlist.Unlock()
-
-
-
-if __name__ == '__main__':
- main()
diff --git a/mailman/attic/bin/reset_pw.py b/mailman/attic/bin/reset_pw.py
deleted file mode 100644
index 453c8b849..000000000
--- a/mailman/attic/bin/reset_pw.py
+++ /dev/null
@@ -1,83 +0,0 @@
-#! @PYTHON@
-#
-# Copyright (C) 2004-2009 by the Free Software Foundation, Inc.
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-# Inspired by Florian Weimer.
-
-"""Reset the passwords for members of a mailing list.
-
-This script resets all the passwords of a mailing list's members. It can also
-be used to reset the lists of all members of all mailing lists, but it is your
-responsibility to let the users know that their passwords have been changed.
-
-This script is intended to be run as a bin/withlist script, i.e.
-
-% bin/withlist -l -r reset_pw listname [options]
-
-Options:
- -v / --verbose
- Print what the script is doing.
-"""
-
-import sys
-import getopt
-
-import paths
-from Mailman import Utils
-from Mailman.i18n import _
-
-
-
-def usage(code, msg=''):
- if code:
- fd = sys.stderr
- else:
- fd = sys.stdout
- print >> fd, _(__doc__.replace('%', '%%'))
- if msg:
- print >> fd, msg
- sys.exit(code)
-
-
-
-def reset_pw(mlist, *args):
- try:
- opts, args = getopt.getopt(args, 'v', ['verbose'])
- except getopt.error, msg:
- usage(1, msg)
-
- verbose = False
- for opt, args in opts:
- if opt in ('-v', '--verbose'):
- verbose = True
-
- listname = mlist.internal_name()
- if verbose:
- print _('Changing passwords for list: %(listname)s')
-
- for member in mlist.getMembers():
- randompw = Utils.MakeRandomPassword()
- mlist.setMemberPassword(member, randompw)
- if verbose:
- print _('New password for member %(member)40s: %(randompw)s')
-
- mlist.Save()
-
-
-
-if __name__ == '__main__':
- usage(0)
diff --git a/mailman/attic/bin/sync_members b/mailman/attic/bin/sync_members
deleted file mode 100755
index 4a21624c1..000000000
--- a/mailman/attic/bin/sync_members
+++ /dev/null
@@ -1,286 +0,0 @@
-#! @PYTHON@
-#
-# Copyright (C) 1998-2003 by the Free Software Foundation, Inc.
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-"""Synchronize a mailing list's membership with a flat file.
-
-This script is useful if you have a Mailman mailing list and a sendmail
-:include: style list of addresses (also as is used in Majordomo). For every
-address in the file that does not appear in the mailing list, the address is
-added. For every address in the mailing list that does not appear in the
-file, the address is removed. Other options control what happens when an
-address is added or removed.
-
-Usage: %(PROGRAM)s [options] -f file listname
-
-Where `options' are:
-
- --no-change
- -n
- Don't actually make the changes. Instead, print out what would be
- done to the list.
-
- --welcome-msg[=]
- -w[=]
- Sets whether or not to send the newly added members a welcome
- message, overriding whatever the list's `send_welcome_msg' setting
- is. With -w=yes or -w, the welcome message is sent. With -w=no, no
- message is sent.
-
- --goodbye-msg[=]
- -g[=]
- Sets whether or not to send the goodbye message to removed members,
- overriding whatever the list's `send_goodbye_msg' setting is. With
- -g=yes or -g, the goodbye message is sent. With -g=no, no message is
- sent.
-
- --digest[=]
- -d[=]
- Selects whether to make newly added members receive messages in
- digests. With -d=yes or -d, they become digest members. With -d=no
- (or if no -d option given) they are added as regular members.
-
- --notifyadmin[=]
- -a[=]
- Specifies whether the admin should be notified for each subscription
- or unsubscription. If you're adding a lot of addresses, you
- definitely want to turn this off! With -a=yes or -a, the admin is
- notified. With -a=no, the admin is not notified. With no -a option,
- the default for the list is used.
-
- --file
- -f
- This option is required. It specifies the flat file to synchronize
- against. Email addresses must appear one per line. If filename is
- `-' then stdin is used.
-
- --help
- -h
- Print this message.
-
- listname
- Required. This specifies the list to synchronize.
-"""
-
-import sys
-
-import paths
-# Import this /after/ paths so that the sys.path is properly hacked
-import email.Utils
-
-from Mailman import MailList
-from Mailman import Errors
-from Mailman import Utils
-from Mailman.UserDesc import UserDesc
-from Mailman.i18n import _
-
-
-
-PROGRAM = sys.argv[0]
-
-def usage(code, msg=''):
- if code:
- fd = sys.stderr
- else:
- fd = sys.stdout
- print >> fd, _(__doc__)
- if msg:
- print >> fd, msg
- sys.exit(code)
-
-
-
-def yesno(opt):
- i = opt.find('=')
- yesno = opt[i+1:].lower()
- if yesno in ('y', 'yes'):
- return 1
- elif yesno in ('n', 'no'):
- return 0
- else:
- usage(1, _('Bad choice: %(yesno)s'))
- # no return
-
-
-def main():
- dryrun = 0
- digest = 0
- welcome = None
- goodbye = None
- filename = None
- listname = None
- notifyadmin = None
-
- # TBD: can't use getopt with this command line syntax, which is broken and
- # should be changed to be getopt compatible.
- i = 1
- while i < len(sys.argv):
- opt = sys.argv[i]
- if opt in ('-h', '--help'):
- usage(0)
- elif opt in ('-n', '--no-change'):
- dryrun = 1
- i += 1
- print _('Dry run mode')
- elif opt in ('-d', '--digest'):
- digest = 1
- i += 1
- elif opt.startswith('-d=') or opt.startswith('--digest='):
- digest = yesno(opt)
- i += 1
- elif opt in ('-w', '--welcome-msg'):
- welcome = 1
- i += 1
- elif opt.startswith('-w=') or opt.startswith('--welcome-msg='):
- welcome = yesno(opt)
- i += 1
- elif opt in ('-g', '--goodbye-msg'):
- goodbye = 1
- i += 1
- elif opt.startswith('-g=') or opt.startswith('--goodbye-msg='):
- goodbye = yesno(opt)
- i += 1
- elif opt in ('-f', '--file'):
- if filename is not None:
- usage(1, _('Only one -f switch allowed'))
- try:
- filename = sys.argv[i+1]
- except IndexError:
- usage(1, _('No argument to -f given'))
- i += 2
- elif opt in ('-a', '--notifyadmin'):
- notifyadmin = 1
- i += 1
- elif opt.startswith('-a=') or opt.startswith('--notifyadmin='):
- notifyadmin = yesno(opt)
- i += 1
- elif opt[0] == '-':
- usage(1, _('Illegal option: %(opt)s'))
- else:
- try:
- listname = sys.argv[i].lower()
- i += 1
- except IndexError:
- usage(1, _('No listname given'))
- break
-
- if listname is None or filename is None:
- usage(1, _('Must have a listname and a filename'))
-
- # read the list of addresses to sync to from the file
- if filename == '-':
- filemembers = sys.stdin.readlines()
- else:
- try:
- fp = open(filename)
- except IOError, (code, msg):
- usage(1, _('Cannot read address file: %(filename)s: %(msg)s'))
- try:
- filemembers = fp.readlines()
- finally:
- fp.close()
-
- # strip out lines we don't care about, they are comments (# in first
- # non-whitespace) or are blank
- for i in range(len(filemembers)-1, -1, -1):
- addr = filemembers[i].strip()
- if addr == '' or addr[:1] == '#':
- del filemembers[i]
- print _('Ignore : %(addr)30s')
-
- # first filter out any invalid addresses
- filemembers = email.Utils.getaddresses(filemembers)
- invalid = 0
- for name, addr in filemembers:
- try:
- Utils.ValidateEmail(addr)
- except Errors.EmailAddressError:
- print _('Invalid : %(addr)30s')
- invalid = 1
- if invalid:
- print _('You must fix the preceding invalid addresses first.')
- sys.exit(1)
-
- # get the locked list object
- try:
- mlist = MailList.MailList(listname)
- except Errors.MMListError, e:
- print _('No such list: %(listname)s')
- sys.exit(1)
-
- try:
- # Get the list of addresses currently subscribed
- addrs = {}
- needsadding = {}
- matches = {}
- for addr in mlist.getMemberCPAddresses(mlist.getMembers()):
- addrs[addr.lower()] = addr
-
- for name, addr in filemembers:
- # Any address found in the file that is also in the list can be
- # ignored. If not found in the list, it must be added later.
- laddr = addr.lower()
- if addrs.has_key(laddr):
- del addrs[laddr]
- matches[laddr] = 1
- elif not matches.has_key(laddr):
- needsadding[laddr] = (name, addr)
-
- if not needsadding and not addrs:
- print _('Nothing to do.')
- sys.exit(0)
-
- enc = sys.getdefaultencoding()
- # addrs contains now all the addresses that need removing
- for laddr, (name, addr) in needsadding.items():
- pw = Utils.MakeRandomPassword()
- # should not already be subscribed, otherwise our test above is
- # broken. Bogosity is if the address is listed in the file more
- # than once. Second and subsequent ones trigger an
- # MMAlreadyAMember error. Just catch it and go on.
- userdesc = UserDesc(addr, name, pw, digest)
- try:
- if not dryrun:
- mlist.ApprovedAddMember(userdesc, welcome, notifyadmin)
- s = email.Utils.formataddr((name, addr)).encode(enc, 'replace')
- print _('Added : %(s)s')
- except Errors.MMAlreadyAMember:
- pass
-
- for laddr, addr in addrs.items():
- # Should be a member, otherwise our test above is broken
- name = mlist.getMemberName(laddr) or ''
- if not dryrun:
- try:
- mlist.ApprovedDeleteMember(addr, admin_notif=notifyadmin,
- userack=goodbye)
- except Errors.NotAMemberError:
- # This can happen if the address is illegal (i.e. can't be
- # parsed by email.Utils.parseaddr()) but for legacy
- # reasons is in the database. Use a lower level remove to
- # get rid of this member's entry
- mlist.removeMember(addr)
- s = email.Utils.formataddr((name, addr)).encode(enc, 'replace')
- print _('Removed: %(s)s')
-
- mlist.Save()
- finally:
- mlist.Unlock()
-
-
-if __name__ == '__main__':
- main()
diff --git a/mailman/attic/bin/templ2pot.py b/mailman/attic/bin/templ2pot.py
deleted file mode 100644
index 0253cc2cd..000000000
--- a/mailman/attic/bin/templ2pot.py
+++ /dev/null
@@ -1,120 +0,0 @@
-#! @PYTHON@
-# Code stolen from pygettext.py
-# by Tokio Kikuchi
-
-"""templ2pot.py -- convert mailman template (en) to pot format.
-
-Usage: templ2pot.py inputfile ...
-
-Options:
-
- -h, --help
-
-Inputfiles are english templates. Outputs are written to stdout.
-"""
-
-import sys
-import getopt
-
-
-
-try:
- import paths
- from Mailman.i18n import _
-except ImportError:
- def _(s): return s
-
-EMPTYSTRING = ''
-
-
-
-def usage(code, msg=''):
- if code:
- fd = sys.stderr
- else:
- fd = sys.stdout
- print >> fd, _(__doc__) % globals()
- if msg:
- print >> fd, msg
- sys.exit(code)
-
-
-
-escapes = []
-
-def make_escapes(pass_iso8859):
- global escapes
- if pass_iso8859:
- # Allow iso-8859 characters to pass through so that e.g. 'msgid
- # "H[o-umlaut]he"' would result not result in 'msgid "H\366he"'.
- # Otherwise we escape any character outside the 32..126 range.
- mod = 128
- else:
- mod = 256
- for i in range(256):
- if 32 <= (i % mod) <= 126:
- escapes.append(chr(i))
- else:
- escapes.append("\\%03o" % i)
- escapes[ord('\\')] = '\\\\'
- escapes[ord('\t')] = '\\t'
- escapes[ord('\r')] = '\\r'
- escapes[ord('\n')] = '\\n'
- escapes[ord('\"')] = '\\"'
-
-
-def escape(s):
- global escapes
- s = list(s)
- for i in range(len(s)):
- s[i] = escapes[ord(s[i])]
- return EMPTYSTRING.join(s)
-
-
-def normalize(s):
- # This converts the various Python string types into a format that is
- # appropriate for .po files, namely much closer to C style.
- lines = s.splitlines()
- if len(lines) == 1:
- s = '"' + escape(s) + '"'
- else:
- if not lines[-1]:
- del lines[-1]
- lines[-1] = lines[-1] + '\n'
- for i in range(len(lines)):
- lines[i] = escape(lines[i])
- lineterm = '\\n"\n"'
- s = '""\n"' + lineterm.join(lines) + '"'
- return s
-
-
-
-def main():
- try:
- opts, args = getopt.getopt(
- sys.argv[1:],
- 'h',
- ['help',]
- )
- except getopt.error, msg:
- usage(1, msg)
-
- # parse options
- for opt, arg in opts:
- if opt in ('-h', '--help'):
- usage(0)
-
- # calculate escapes
- make_escapes(0)
-
- for filename in args:
- print '#: %s:1' % filename
- s = file(filename).read()
- print '#, template'
- print 'msgid', normalize(s)
- print 'msgstr ""\n'
-
-
-
-if __name__ == '__main__':
- main()
diff --git a/mailman/attic/bin/transcheck b/mailman/attic/bin/transcheck
deleted file mode 100755
index 73910e771..000000000
--- a/mailman/attic/bin/transcheck
+++ /dev/null
@@ -1,412 +0,0 @@
-#! @PYTHON@
-#
-# transcheck - (c) 2002 by Simone Piunno
-#
-# This program is free software; you can redistribute it and/or modify it
-# under the terms of the version 2.0 of the GNU General Public License as
-# published by the Free Software Foundation.
-#
-# This program is distributed in the hope that it will be useful, but
-# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
-# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
-# for more details.
-#
-# You should have received a copy of the GNU General Public License along
-# with this program; if not, write to the Free Software Foundation, Inc.,
-# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-"""
-Check a given Mailman translation, making sure that variables and
-tags referenced in translation are the same variables and tags in
-the original templates and catalog.
-
-Usage:
-
-cd $MAILMAN_DIR
-%(program)s [-q]
-
-Where is your country code (e.g. 'it' for Italy) and -q is
-to ask for a brief summary.
-"""
-
-import sys
-import re
-import os
-import getopt
-
-import paths
-from Mailman.i18n import _
-
-program = sys.argv[0]
-
-
-
-def usage(code, msg=''):
- if code:
- fd = sys.stderr
- else:
- fd = sys.stdout
- print >> fd, _(__doc__)
- if msg:
- print >> fd, msg
- sys.exit(code)
-
-
-
-class TransChecker:
- "check a translation comparing with the original string"
- def __init__(self, regexp, escaped=None):
- self.dict = {}
- self.errs = []
- self.regexp = re.compile(regexp)
- self.escaped = None
- if escaped:
- self.escaped = re.compile(escaped)
-
- def checkin(self, string):
- "scan a string from the original file"
- for key in self.regexp.findall(string):
- if self.escaped and self.escaped.match(key):
- continue
- if self.dict.has_key(key):
- self.dict[key] += 1
- else:
- self.dict[key] = 1
-
- def checkout(self, string):
- "scan a translated string"
- for key in self.regexp.findall(string):
- if self.escaped and self.escaped.match(key):
- continue
- if self.dict.has_key(key):
- self.dict[key] -= 1
- else:
- self.errs.append(
- "%(key)s was not found" %
- { 'key' : key }
- )
-
- def computeErrors(self):
- "check for differences between checked in and checked out"
- for key in self.dict.keys():
- if self.dict[key] < 0:
- self.errs.append(
- "Too much %(key)s" %
- { 'key' : key }
- )
- if self.dict[key] > 0:
- self.errs.append(
- "Too few %(key)s" %
- { 'key' : key }
- )
- return self.errs
-
- def status(self):
- if self.errs:
- return "FAILED"
- else:
- return "OK"
-
- def errorsAsString(self):
- msg = ""
- for err in self.errs:
- msg += " - %(err)s" % { 'err': err }
- return msg
-
- def reset(self):
- self.dict = {}
- self.errs = []
-
-
-
-class POParser:
- "parse a .po file extracting msgids and msgstrs"
- def __init__(self, filename=""):
- self.status = 0
- self.files = []
- self.msgid = ""
- self.msgstr = ""
- self.line = 1
- self.f = None
- self.esc = { "n": "\n", "r": "\r", "t": "\t" }
- if filename:
- self.f = open(filename)
-
- def open(self, filename):
- self.f = open(filename)
-
- def close(self):
- self.f.close()
-
- def parse(self):
- """States table for the finite-states-machine parser:
- 0 idle
- 1 filename-or-comment
- 2 msgid
- 3 msgstr
- 4 end
- """
- # each time we can safely re-initialize those vars
- self.files = []
- self.msgid = ""
- self.msgstr = ""
-
-
- # can't continue if status == 4, this is a dead status
- if self.status == 4:
- return 0
-
- while 1:
- # continue scanning, char-by-char
- c = self.f.read(1)
- if not c:
- # EOF -> maybe we have a msgstr to save?
- self.status = 4
- if self.msgstr:
- return 1
- else:
- return 0
-
- # keep the line count up-to-date
- if c == "\n":
- self.line += 1
-
- # a pound was detected the previous char...
- if self.status == 1:
- if c == ":":
- # was a line of filenames
- row = self.f.readline()
- self.files += row.split()
- self.line += 1
- elif c == "\n":
- # was a single pount on the line
- pass
- else:
- # was a comment... discard
- self.f.readline()
- self.line += 1
- # in every case, we switch to idle status
- self.status = 0;
- continue
-
- # in idle status we search for a '#' or for a 'm'
- if self.status == 0:
- if c == "#":
- # this could be a comment or a filename
- self.status = 1;
- continue
- elif c == "m":
- # this should be a msgid start...
- s = self.f.read(4)
- assert s == "sgid"
- # so now we search for a '"'
- self.status = 2
- continue
- # in idle only those other chars are possibile
- assert c in [ "\n", " ", "\t" ]
-
- # searching for the msgid string
- if self.status == 2:
- if c == "\n":
- # a double LF is not possible here
- c = self.f.read(1)
- assert c != "\n"
- if c == "\"":
- # ok, this is the start of the string,
- # now search for the end
- while 1:
- c = self.f.read(1)
- if not c:
- # EOF, bailout
- self.status = 4
- return 0
- if c == "\\":
- # a quoted char...
- c = self.f.read(1)
- if self.esc.has_key(c):
- self.msgid += self.esc[c]
- else:
- self.msgid += c
- continue
- if c == "\"":
- # end of string found
- break
- # a normal char, add it
- self.msgid += c
- if c == "m":
- # this should be a msgstr identifier
- s = self.f.read(5)
- assert s == "sgstr"
- # ok, now search for the msgstr string
- self.status = 3
-
- # searching for the msgstr string
- if self.status == 3:
- if c == "\n":
- # a double LF is the end of the msgstr!
- c = self.f.read(1)
- if c == "\n":
- # ok, time to go idle and return
- self.status = 0
- self.line += 1
- return 1
- if c == "\"":
- # start of string found
- while 1:
- c = self.f.read(1)
- if not c:
- # EOF, bail out
- self.status = 4
- return 1
- if c == "\\":
- # a quoted char...
- c = self.f.read(1)
- if self.esc.has_key(c):
- self.msgid += self.esc[c]
- else:
- self.msgid += c
- continue
- if c == "\"":
- # end of string
- break
- # a normal char, add it
- self.msgstr += c
-
-
-
-
-def check_file(translatedFile, originalFile, html=0, quiet=0):
- """check a translated template against the original one
- search also tags if html is not zero"""
-
- if html:
- c = TransChecker("(%%|%\([^)]+\)[0-9]*[sd]|?MM-[^>]+>)", "^%%$")
- else:
- c = TransChecker("(%%|%\([^)]+\)[0-9]*[sd])", "^%%$")
-
- try:
- f = open(originalFile)
- except IOError:
- if not quiet:
- print " - Can'open original file " + originalFile
- return 1
-
- while 1:
- line = f.readline()
- if not line: break
- c.checkin(line)
-
- f.close()
-
- try:
- f = open(translatedFile)
- except IOError:
- if not quiet:
- print " - Can'open translated file " + translatedFile
- return 1
-
- while 1:
- line = f.readline()
- if not line: break
- c.checkout(line)
-
- f.close()
-
- n = 0
- msg = ""
- for desc in c.computeErrors():
- n +=1
- if not quiet:
- print " - %(desc)s" % { 'desc': desc }
- return n
-
-
-
-def check_po(file, quiet=0):
- "scan the po file comparing msgids with msgstrs"
- n = 0
- p = POParser(file)
- c = TransChecker("(%%|%\([^)]+\)[0-9]*[sdu]|%[0-9]*[sdu])", "^%%$")
- while p.parse():
- if p.msgstr:
- c.reset()
- c.checkin(p.msgid)
- c.checkout(p.msgstr)
- for desc in c.computeErrors():
- n += 1
- if not quiet:
- print " - near line %(line)d %(file)s: %(desc)s" % {
- 'line': p.line,
- 'file': p.files,
- 'desc': desc
- }
- p.close()
- return n
-
-
-def main():
- try:
- opts, args = getopt.getopt(sys.argv[1:], 'qh', ['quiet', 'help'])
- except getopt.error, msg:
- usage(1, msg)
-
- quiet = 0
- for opt, arg in opts:
- if opt in ('-h', '--help'):
- usage(0)
- elif opt in ('-q', '--quiet'):
- quiet = 1
-
- if len(args) <> 1:
- usage(1)
-
- lang = args[0]
-
- isHtml = re.compile("\.html$");
- isTxt = re.compile("\.txt$");
-
- numerrors = 0
- numfiles = 0
- try:
- files = os.listdir("templates/" + lang + "/")
- except:
- print "can't open templates/%s/" % lang
- for file in files:
- fileEN = "templates/en/" + file
- fileIT = "templates/" + lang + "/" + file
- errlist = []
- if isHtml.search(file):
- if not quiet:
- print "HTML checking " + fileIT + "... "
- n = check_file(fileIT, fileEN, html=1, quiet=quiet)
- if n:
- numerrors += n
- numfiles += 1
- elif isTxt.search(file):
- if not quiet:
- print "TXT checking " + fileIT + "... "
- n = check_file(fileIT, fileEN, html=0, quiet=quiet)
- if n:
- numerrors += n
- numfiles += 1
-
- else:
- continue
-
- file = "messages/" + lang + "/LC_MESSAGES/mailman.po"
- if not quiet:
- print "PO checking " + file + "... "
- n = check_po(file, quiet=quiet)
- if n:
- numerrors += n
- numfiles += 1
-
- if quiet:
- print "%(errs)u warnings in %(files)u files" % {
- 'errs': numerrors,
- 'files': numfiles
- }
-
-
-if __name__ == '__main__':
- main()
diff --git a/mailman/bin/__init__.py b/mailman/bin/__init__.py
deleted file mode 100644
index d61693c5e..000000000
--- a/mailman/bin/__init__.py
+++ /dev/null
@@ -1,61 +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 .
-
-__all__ = [
- 'add_members',
- 'arch',
- 'bounces',
- 'bumpdigests',
- 'check_perms',
- 'checkdbs',
- 'cleanarch',
- 'config_list',
- 'confirm',
- 'create_list',
- 'disabled',
- 'dumpdb',
- 'export',
- 'find_member',
- 'gate_news',
- 'genaliases',
- 'import',
- 'inject',
- 'join',
- 'leave',
- 'list_lists',
- 'list_members',
- 'list_owners',
- 'mailmanctl',
- 'make_instance',
- 'master',
- 'mmsitepass',
- 'nightly_gzip',
- 'owner',
- 'post',
- 'qrunner',
- 'remove_list',
- 'request',
- 'senddigests',
- 'set_members',
- 'show_config',
- 'show_qfiles',
- 'testall',
- 'unshunt',
- 'update',
- 'version',
- 'withlist',
- ]
diff --git a/mailman/bin/add_members.py b/mailman/bin/add_members.py
deleted file mode 100644
index 9c87f4af9..000000000
--- a/mailman/bin/add_members.py
+++ /dev/null
@@ -1,186 +0,0 @@
-# Copyright (C) 1998-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 .
-
-import os
-import sys
-import codecs
-
-from cStringIO import StringIO
-from email.utils import parseaddr
-
-from mailman import Message
-from mailman import Utils
-from mailman import i18n
-from mailman.app.membership import add_member
-from mailman.config import config
-from mailman.core import errors
-from mailman.interfaces.member import AlreadySubscribedError, DeliveryMode
-from mailman.options import SingleMailingListOptions
-
-_ = i18n._
-
-
-
-class ScriptOptions(SingleMailingListOptions):
- usage=_("""\
-%prog [options]
-
-Add members to a list. 'listname' is the name of the Mailman list you are
-adding members to; the list must already exist.
-
-You must supply at least one of -r and -d options. At most one of the
-files can be '-'.
-""")
-
- def add_options(self):
- super(ScriptOptions, self).add_options()
- self.parser.add_option(
- '-r', '--regular-members-file',
- type='string', dest='regular', help=_("""\
-A file containing addresses of the members to be added, one address per line.
-This list of people become non-digest members. If file is '-', read addresses
-from stdin."""))
- self.parser.add_option(
- '-d', '--digest-members-file',
- type='string', dest='digest', help=_("""\
-Similar to -r, but these people become digest members."""))
- self.parser.add_option(
- '-w', '--welcome-msg',
- type='yesno', metavar='', help=_("""\
-Set whether or not to send the list members a welcome message, overriding
-whatever the list's 'send_welcome_msg' setting is."""))
- self.parser.add_option(
- '-a', '--admin-notify',
- type='yesno', metavar='', help=_("""\
-Set whether or not to send the list administrators a notification on the
-success/failure of these subscriptions, overriding whatever the list's
-'admin_notify_mchanges' setting is."""))
-
- def sanity_check(self):
- if not self.options.listname:
- self.parser.error(_('Missing listname'))
- if len(self.arguments) > 0:
- self.parser.print_error(_('Unexpected arguments'))
- if self.options.regular is None and self.options.digest is None:
- parser.error(_('At least one of -r or -d is required'))
- if self.options.regular == '-' and self.options.digest == '-':
- parser.error(_("-r and -d cannot both be '-'"))
-
-
-
-def readfile(filename):
- if filename == '-':
- fp = sys.stdin
- else:
- # XXX Need to specify other encodings.
- fp = codecs.open(filename, encoding='utf-8')
- # Strip all the lines of whitespace and discard blank lines
- try:
- return set(line.strip() for line in fp if line)
- finally:
- if fp is not sys.stdin:
- fp.close()
-
-
-
-class Tee:
- def __init__(self, outfp):
- self._outfp = outfp
-
- def write(self, msg):
- sys.stdout.write(msg)
- self._outfp.write(msg)
-
-
-
-def addall(mlist, subscribers, delivery_mode, ack, admin_notify, outfp):
- tee = Tee(outfp)
- for subscriber in subscribers:
- try:
- fullname, address = parseaddr(subscriber)
- # Watch out for the empty 8-bit string.
- if not fullname:
- fullname = u''
- password = Utils.MakeRandomPassword()
- add_member(mlist, address, fullname, password, delivery_mode,
- unicode(config.mailman.default_language))
- # XXX Support ack and admin_notify
- except AlreadySubscribedError:
- print >> tee, _('Already a member: $subscriber')
- except errors.InvalidEmailAddress:
- if not address:
- print >> tee, _('Bad/Invalid email address: blank line')
- else:
- print >> tee, _('Bad/Invalid email address: $subscriber')
- else:
- print >> tee, _('Subscribing: $subscriber')
-
-
-
-def main():
- options = ScriptOptions()
- options.initialize()
-
- fqdn_listname = options.options.listname
- mlist = config.db.list_manager.get(fqdn_listname)
- if mlist is None:
- parser.error(_('No such list: $fqdn_listname'))
-
- # Set up defaults.
- send_welcome_msg = (options.options.welcome_msg
- if options.options.welcome_msg is not None
- else mlist.send_welcome_msg)
- admin_notify = (options.options.admin_notify
- if options.options.admin_notify is not None
- else mlist.admin_notify)
-
- with i18n.using_language(mlist.preferred_language):
- if options.options.digest:
- dmembers = readfile(options.options.digest)
- else:
- dmembers = set()
- if options.options.regular:
- nmembers = readfile(options.options.regular)
- else:
- nmembers = set()
-
- if not dmembers and not nmembers:
- print _('Nothing to do.')
- sys.exit(0)
-
- outfp = StringIO()
- if nmembers:
- addall(mlist, nmembers, DeliveryMode.regular,
- send_welcome_msg, admin_notify, outfp)
-
- if dmembers:
- addall(mlist, dmembers, DeliveryMode.mime_digests,
- send_welcome_msg, admin_notify, outfp)
-
- config.db.commit()
-
- if admin_notify:
- subject = _('$mlist.real_name subscription notification')
- msg = Message.UserNotification(
- mlist.owner, mlist.no_reply_address, subject,
- outfp.getvalue(), mlist.preferred_language)
- msg.send(mlist)
-
-
-
-if __name__ == '__main__':
- main()
diff --git a/mailman/bin/arch.py b/mailman/bin/arch.py
deleted file mode 100644
index a27fa8d7f..000000000
--- a/mailman/bin/arch.py
+++ /dev/null
@@ -1,151 +0,0 @@
-# Copyright (C) 1998-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 .
-
-import os
-import sys
-import errno
-import shutil
-import optparse
-
-from locknix.lockfile import Lock
-
-from mailman import i18n
-from mailman.Archiver.HyperArch import HyperArchive
-from mailman.Defaults import hours
-from mailman.configuration import config
-from mailman.initialize import initialize
-from mailman.version import MAILMAN_VERSION
-
-_ = i18n._
-
-
-
-def parseargs():
- parser = optparse.OptionParser(version=MAILMAN_VERSION,
- usage=_("""\
-%%prog [options] listname [mbox]
-
-Rebuild a list's archive.
-
-Use this command to rebuild the archives for a mailing list. You may want to
-do this if you edit some messages in an archive, or remove some messages from
-an archive.
-
-Where 'mbox' is the path to a list's complete mbox archive. Usually this will
-be some path in the archives/private directory. For example:
-
-% bin/arch mylist archives/private/mylist.mbox/mylist.mbox
-
-'mbox' is optional. If it is missing, it is calculated from the listname.
-"""))
- parser.add_option('-q', '--quiet',
- dest='verbose', default=True, action='store_false',
- help=_('Make the archiver output less verbose'))
- parser.add_option('--wipe',
- default=False, action='store_true',
- help=_("""\
-First wipe out the original archive before regenerating. You usually want to
-specify this argument unless you're generating the archive in chunks."""))
- parser.add_option('-s', '--start',
- default=None, type='int', metavar='N',
- help=_("""\
-Start indexing at article N, where article 0 is the first in the mbox.
-Defaults to 0."""))
- parser.add_option('-e', '--end',
- default=None, type='int', metavar='M',
- help=_("""\
-End indexing at article M. This script is not very efficient with respect to
-memory management, and for large archives, it may not be possible to index the
-mbox entirely. For that reason, you can specify the start and end article
-numbers."""))
- parser.add_option('-C', '--config',
- help=_('Alternative configuration file to use'))
- opts, args = parser.parse_args()
- if len(args) < 1:
- parser.print_help()
- print >> sys.stderr, _('listname is required')
- sys.exit(1)
- if len(args) > 2:
- parser.print_help()
- print >> sys.stderr, _('Unexpected arguments')
- sys.exit(1)
- return parser, opts, args
-
-
-
-def main():
- parser, opts, args = parseargs()
- initialize(opts.config)
-
- i18n.set_language(config.DEFAULT_SERVER_LANGUAGE)
-
- listname = args[0].lower().strip()
- if len(args) < 2:
- mbox = None
- else:
- mbox = args[1]
-
- # Open the mailing list object
- mlist = config.list_manager.get(listname)
- if mlist is None:
- parser.error(_('No such list: $listname'))
- if mbox is None:
- mbox = mlist.ArchiveFileName()
-
- i18n.set_language(mlist.preferred_language)
- # Lay claim to the archive's lock file. This is so no other post can
- # mess up the archive while we're processing it. Try to pick a
- # suitably long period of time for the lock lifetime even though we
- # really don't know how long it will take.
- #
- # XXX processUnixMailbox() should refresh the lock.
- lock_path = os.path.join(mlist.data_path, '.archiver.lck')
- with Lock(lock_path, lifetime=int(hours(3))):
- # Try to open mbox before wiping old archive.
- try:
- fp = open(mbox)
- except IOError, e:
- if e.errno == errno.ENOENT:
- print >> sys.stderr, _('Cannot open mbox file: $mbox')
- else:
- print >> sys.stderr, e
- sys.exit(1)
- # Maybe wipe the old archives
- if opts.wipe:
- if mlist.scrub_nondigest:
- # TK: save the attachments dir because they are not in mbox
- saved = False
- atchdir = os.path.join(mlist.archive_dir(), 'attachments')
- savedir = os.path.join(mlist.archive_dir() + '.mbox',
- 'attachments')
- try:
- os.rename(atchdir, savedir)
- saved = True
- except OSError, e:
- if e.errno <> errno.ENOENT:
- raise
- shutil.rmtree(mlist.archive_dir())
- if mlist.scrub_nondigest and saved:
- os.renames(savedir, atchdir)
-
- archiver = HyperArchive(mlist)
- archiver.VERBOSE = opts.verbose
- try:
- archiver.processUnixMailbox(fp, opts.start, opts.end)
- finally:
- archiver.close()
- fp.close()
diff --git a/mailman/bin/bumpdigests.py b/mailman/bin/bumpdigests.py
deleted file mode 100644
index b1ed37a21..000000000
--- a/mailman/bin/bumpdigests.py
+++ /dev/null
@@ -1,74 +0,0 @@
-# Copyright (C) 1998-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 .
-
-import sys
-import optparse
-
-from mailman import errors
-from mailman import MailList
-from mailman.configuration import config
-from mailman.i18n import _
-from mailman.version import MAILMAN_VERSION
-
-# Work around known problems with some RedHat cron daemons
-import signal
-signal.signal(signal.SIGCHLD, signal.SIG_DFL)
-
-
-
-def parseargs():
- parser = optparse.OptionParser(version=MAILMAN_VERSION,
- usage=_("""\
-%prog [options] [listname ...]
-
-Increment the digest volume number and reset the digest number to one. All
-the lists named on the command line are bumped. If no list names are given,
-all lists are bumped."""))
- parser.add_option('-C', '--config',
- help=_('Alternative configuration file to use'))
- opts, args = parser.parse_args()
- return opts, args, parser
-
-
-
-def main():
- opts, args, parser = parseargs()
- config.load(opts.config)
-
- listnames = set(args or config.list_manager.names)
- if not listnames:
- print _('Nothing to do.')
- sys.exit(0)
-
- for listname in listnames:
- try:
- # Be sure the list is locked
- mlist = MailList.MailList(listname)
- except errors.MMListError, e:
- parser.print_help()
- print >> sys.stderr, _('No such list: $listname')
- sys.exit(1)
- try:
- mlist.bump_digest_volume()
- finally:
- mlist.Save()
- mlist.Unlock()
-
-
-
-if __name__ == '__main__':
- main()
diff --git a/mailman/bin/check_perms.py b/mailman/bin/check_perms.py
deleted file mode 100644
index 4b75aa9f6..000000000
--- a/mailman/bin/check_perms.py
+++ /dev/null
@@ -1,408 +0,0 @@
-# Copyright (C) 1998-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 .
-
-import os
-import sys
-import pwd
-import grp
-import errno
-import optparse
-
-from stat import *
-
-from mailman.configuration import config
-from mailman.i18n import _
-from mailman.version import MAILMAN_VERSION
-
-
-# XXX Need to check the archives/private/*/database/* files
-
-
-
-class State:
- FIX = False
- VERBOSE = False
- ERRORS = 0
-
-STATE = State()
-
-DIRPERMS = S_ISGID | S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH
-QFILEPERMS = S_ISGID | S_IRWXU | S_IRWXG
-PYFILEPERMS = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH
-ARTICLEFILEPERMS = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP
-MBOXPERMS = S_IRGRP | S_IWGRP | S_IRUSR | S_IWUSR
-PRIVATEPERMS = QFILEPERMS
-
-
-
-def statmode(path):
- return os.stat(path).st_mode
-
-
-def statgidmode(path):
- stat = os.stat(path)
- return stat.st_mode, stat.st_gid
-
-
-seen = {}
-
-# libc's getgrgid re-opens /etc/group each time :(
-_gidcache = {}
-
-def getgrgid(gid):
- data = _gidcache.get(gid)
- if data is None:
- data = grp.getgrgid(gid)
- _gidcache[gid] = data
- return data
-
-
-
-def checkwalk(arg, dirname, names):
- # Short-circuit duplicates
- if seen.has_key(dirname):
- return
- seen[dirname] = True
- for name in names:
- path = os.path.join(dirname, name)
- if arg.VERBOSE:
- print _(' checking gid and mode for $path')
- try:
- mode, gid = statgidmode(path)
- except OSError, e:
- if e.errno <> errno.ENOENT: raise
- continue
- if gid <> MAILMAN_GID:
- try:
- groupname = getgrgid(gid)[0]
- except KeyError:
- groupname = '' % gid
- arg.ERRORS += 1
- print _(
- '$path bad group (has: $groupname, expected $MAILMAN_GROUP)'),
- if STATE.FIX:
- print _('(fixing)')
- os.chown(path, -1, MAILMAN_GID)
- else:
- print
- # Most directories must be at least rwxrwsr-x.
- # The private archive directory and database directory must be at
- # least rwxrws---. Their 'other' permissions are checked in
- # checkarchives() and checkarchivedbs() below. Their 'user' and
- # 'group' permissions are checked here.
- # The directories under qfiles should be rwxrws---. Their 'user' and
- # 'group' permissions are checked here. Their 'other' permissions
- # aren't checked.
- private = config.PRIVATE_ARCHIVE_FILE_DIR
- if path == private or (
- os.path.commonprefix((path, private)) == private
- and os.path.split(path)[1] == 'database'):
- # then...
- targetperms = PRIVATEPERMS
- elif (os.path.commonprefix((path, config.QUEUE_DIR))
- == config.QUEUE_DIR):
- targetperms = QFILEPERMS
- else:
- targetperms = DIRPERMS
- octperms = oct(targetperms)
- if S_ISDIR(mode) and (mode & targetperms) <> targetperms:
- arg.ERRORS += 1
- print _('directory permissions must be $octperms: $path'),
- if STATE.FIX:
- print _('(fixing)')
- os.chmod(path, mode | targetperms)
- else:
- print
- elif os.path.splitext(path)[1] in ('.py', '.pyc', '.pyo'):
- octperms = oct(PYFILEPERMS)
- if mode & PYFILEPERMS <> PYFILEPERMS:
- print _('source perms must be $octperms: $path'),
- arg.ERRORS += 1
- if STATE.FIX:
- print _('(fixing)')
- os.chmod(path, mode | PYFILEPERMS)
- else:
- print
- elif path.endswith('-article'):
- # Article files must be group writeable
- octperms = oct(ARTICLEFILEPERMS)
- if mode & ARTICLEFILEPERMS <> ARTICLEFILEPERMS:
- print _('article db files must be $octperms: $path'),
- arg.ERRORS += 1
- if STATE.FIX:
- print _('(fixing)')
- os.chmod(path, mode | ARTICLEFILEPERMS)
- else:
- print
-
-
-
-def checkall():
- # first check PREFIX
- if STATE.VERBOSE:
- prefix = config.PREFIX
- print _('checking mode for $prefix')
- dirs = {}
- for d in (config.PREFIX, config.EXEC_PREFIX, config.VAR_PREFIX,
- config.LOG_DIR):
- dirs[d] = True
- for d in dirs.keys():
- try:
- mode = statmode(d)
- except OSError, e:
- if e.errno <> errno.ENOENT: raise
- print _('WARNING: directory does not exist: $d')
- continue
- if (mode & DIRPERMS) <> DIRPERMS:
- STATE.ERRORS += 1
- print _('directory must be at least 02775: $d'),
- if STATE.FIX:
- print _('(fixing)')
- os.chmod(d, mode | DIRPERMS)
- else:
- print
- # check all subdirs
- os.path.walk(d, checkwalk, STATE)
-
-
-
-def checkarchives():
- private = config.PRIVATE_ARCHIVE_FILE_DIR
- if STATE.VERBOSE:
- print _('checking perms on $private')
- # private archives must not be other readable
- mode = statmode(private)
- if mode & S_IROTH:
- STATE.ERRORS += 1
- print _('$private must not be other-readable'),
- if STATE.FIX:
- print _('(fixing)')
- os.chmod(private, mode & ~S_IROTH)
- else:
- print
- # In addition, on a multiuser system you may want to hide the private
- # archives so other users can't read them.
- if mode & S_IXOTH:
- print _("""\
-Warning: Private archive directory is other-executable (o+x).
- This could allow other users on your system to read private archives.
- If you're on a shared multiuser system, you should consult the
- installation manual on how to fix this.""")
-
-
-
-def checkmboxfile(mboxdir):
- absdir = os.path.join(config.PRIVATE_ARCHIVE_FILE_DIR, mboxdir)
- for f in os.listdir(absdir):
- if not f.endswith('.mbox'):
- continue
- mboxfile = os.path.join(absdir, f)
- mode = statmode(mboxfile)
- if (mode & MBOXPERMS) <> MBOXPERMS:
- STATE.ERRORS = STATE.ERRORS + 1
- print _('mbox file must be at least 0660:'), mboxfile
- if STATE.FIX:
- print _('(fixing)')
- os.chmod(mboxfile, mode | MBOXPERMS)
- else:
- print
-
-
-
-def checkarchivedbs():
- # The archives/private/listname/database file must not be other readable
- # or executable otherwise those files will be accessible when the archives
- # are public. That may not be a horrible breach, but let's close this off
- # anyway.
- for dir in os.listdir(config.PRIVATE_ARCHIVE_FILE_DIR):
- if dir.endswith('.mbox'):
- checkmboxfile(dir)
- dbdir = os.path.join(config.PRIVATE_ARCHIVE_FILE_DIR, dir, 'database')
- try:
- mode = statmode(dbdir)
- except OSError, e:
- if e.errno not in (errno.ENOENT, errno.ENOTDIR): raise
- continue
- if mode & S_IRWXO:
- STATE.ERRORS += 1
- print _('$dbdir "other" perms must be 000'),
- if STATE.FIX:
- print _('(fixing)')
- os.chmod(dbdir, mode & ~S_IRWXO)
- else:
- print
-
-
-
-def checkcgi():
- cgidir = os.path.join(config.EXEC_PREFIX, 'cgi-bin')
- if STATE.VERBOSE:
- print _('checking cgi-bin permissions')
- exes = os.listdir(cgidir)
- for f in exes:
- path = os.path.join(cgidir, f)
- if STATE.VERBOSE:
- print _(' checking set-gid for $path')
- mode = statmode(path)
- if mode & S_IXGRP and not mode & S_ISGID:
- STATE.ERRORS += 1
- print _('$path must be set-gid'),
- if STATE.FIX:
- print _('(fixing)')
- os.chmod(path, mode | S_ISGID)
- else:
- print
-
-
-
-def checkmail():
- wrapper = os.path.join(config.WRAPPER_DIR, 'mailman')
- if STATE.VERBOSE:
- print _('checking set-gid for $wrapper')
- mode = statmode(wrapper)
- if not mode & S_ISGID:
- STATE.ERRORS += 1
- print _('$wrapper must be set-gid'),
- if STATE.FIX:
- print _('(fixing)')
- os.chmod(wrapper, mode | S_ISGID)
-
-
-
-def checkadminpw():
- for pwfile in (os.path.join(config.DATA_DIR, 'adm.pw'),
- os.path.join(config.DATA_DIR, 'creator.pw')):
- targetmode = S_IFREG | S_IRUSR | S_IWUSR | S_IRGRP
- if STATE.VERBOSE:
- print _('checking permissions on $pwfile')
- try:
- mode = statmode(pwfile)
- except OSError, e:
- if e.errno <> errno.ENOENT:
- raise
- return
- if mode <> targetmode:
- STATE.ERRORS += 1
- octmode = oct(mode)
- print _('$pwfile permissions must be exactly 0640 (got $octmode)'),
- if STATE.FIX:
- print _('(fixing)')
- os.chmod(pwfile, targetmode)
- else:
- print
-
-
-def checkmta():
- if config.MTA:
- modname = 'mailman.MTA.' + config.MTA
- __import__(modname)
- try:
- sys.modules[modname].checkperms(STATE)
- except AttributeError:
- pass
-
-
-
-def checkdata():
- targetmode = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP
- checkfiles = ('config.pck', 'config.pck.last',
- 'config.db', 'config.db.last',
- 'next-digest', 'next-digest-topics',
- 'digest.mbox', 'pending.pck',
- 'request.db', 'request.db.tmp')
- if STATE.VERBOSE:
- print _('checking permissions on list data')
- for dir in os.listdir(config.LIST_DATA_DIR):
- for file in checkfiles:
- path = os.path.join(config.LIST_DATA_DIR, dir, file)
- if STATE.VERBOSE:
- print _(' checking permissions on: $path')
- try:
- mode = statmode(path)
- except OSError, e:
- if e.errno <> errno.ENOENT:
- raise
- continue
- if (mode & targetmode) <> targetmode:
- STATE.ERRORS += 1
- print _('file permissions must be at least 660: $path'),
- if STATE.FIX:
- print _('(fixing)')
- os.chmod(path, mode | targetmode)
- else:
- print
-
-
-
-def parseargs():
- parser = optparse.OptionParser(version=MAILMAN_VERSION,
- usage=_("""\
-%prog [options]
-
-Check the permissions of all Mailman files. With no options, just report the
-permission and ownership problems found."""))
- parser.add_option('-f', '--fix',
- default=False, action='store_true', help=_("""\
-Fix all permission and ownership problems found. With this option, you must
-run check_perms as root."""))
- parser.add_option('-v', '--verbose',
- default=False, action='store_true',
- help=_('Produce more verbose output'))
- parser.add_option('-C', '--config',
- help=_('Alternative configuration file to use'))
- opts, args = parser.parse_args()
- if args:
- parser.print_help()
- print >> sys.stderr, _('Unexpected arguments')
- sys.exit(1)
- return parser, opts, args
-
-
-
-def main():
- global MAILMAN_USER, MAILMAN_GROUP, MAILMAN_UID, MAILMAN_GID
-
- parser, opts, args = parseargs()
- STATE.FIX = opts.fix
- STATE.VERBOSE = opts.verbose
-
- config.load(opts.config)
-
- MAILMAN_USER = config.MAILMAN_USER
- MAILMAN_GROUP = config.MAILMAN_GROUP
- # Let KeyErrors percolate
- MAILMAN_GID = grp.getgrnam(MAILMAN_GROUP).gr_gid
- MAILMAN_UID = pwd.getpwnam(MAILMAN_USER).pw_uid
-
- checkall()
- checkarchives()
- checkarchivedbs()
- checkcgi()
- checkmail()
- checkdata()
- checkadminpw()
- checkmta()
-
- if not STATE.ERRORS:
- print _('No problems found')
- else:
- print _('Problems found:'), STATE.ERRORS
- print _('Re-run as $MAILMAN_USER (or root) with -f flag to fix')
-
-
-if __name__ == '__main__':
- main()
diff --git a/mailman/bin/checkdbs.py b/mailman/bin/checkdbs.py
deleted file mode 100644
index 2ce08aab7..000000000
--- a/mailman/bin/checkdbs.py
+++ /dev/null
@@ -1,199 +0,0 @@
-# Copyright (C) 1998-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 .
-
-import sys
-import time
-import optparse
-
-from email.Charset import Charset
-
-from mailman import MailList
-from mailman import Message
-from mailman import Utils
-from mailman import i18n
-from mailman.app.requests import handle_request
-from mailman.configuration import config
-from mailman.version import MAILMAN_VERSION
-
-_ = i18n._
-
-# Work around known problems with some RedHat cron daemons
-import signal
-signal.signal(signal.SIGCHLD, signal.SIG_DFL)
-
-NL = u'\n'
-now = time.time()
-
-
-
-def parseargs():
- parser = optparse.OptionParser(version=MAILMAN_VERSION,
- usage=_("""\
-%prog [options]
-
-Check for pending admin requests and mail the list owners if necessary."""))
- parser.add_option('-C', '--config',
- help=_('Alternative configuration file to use'))
- opts, args = parser.parse_args()
- if args:
- parser.print_help()
- print >> sys.stderr, _('Unexpected arguments')
- sys.exit(1)
- return opts, args, parser
-
-
-
-def pending_requests(mlist):
- # Must return a byte string
- lcset = Utils.GetCharSet(mlist.preferred_language)
- pending = []
- first = True
- requestsdb = config.db.get_list_requests(mlist)
- for request in requestsdb.of_type(RequestType.subscription):
- if first:
- pending.append(_('Pending subscriptions:'))
- first = False
- key, data = requestsdb.get_request(request.id)
- when = data['when']
- addr = data['addr']
- fullname = data['fullname']
- passwd = data['passwd']
- digest = data['digest']
- lang = data['lang']
- if fullname:
- if isinstance(fullname, unicode):
- fullname = fullname.encode(lcset, 'replace')
- fullname = ' (%s)' % fullname
- pending.append(' %s%s %s' % (addr, fullname, time.ctime(when)))
- first = True
- for request in requestsdb.of_type(RequestType.held_message):
- if first:
- pending.append(_('\nPending posts:'))
- first = False
- key, data = requestsdb.get_request(request.id)
- when = data['when']
- sender = data['sender']
- subject = data['subject']
- reason = data['reason']
- text = data['text']
- msgdata = data['msgdata']
- subject = Utils.oneline(subject, lcset)
- date = time.ctime(when)
- reason = _(reason)
- pending.append(_("""\
-From: $sender on $date
-Subject: $subject
-Cause: $reason"""))
- pending.append('')
- # Coerce all items in pending to a Unicode so we can join them
- upending = []
- charset = Utils.GetCharSet(mlist.preferred_language)
- for s in pending:
- if isinstance(s, unicode):
- upending.append(s)
- else:
- upending.append(unicode(s, charset, 'replace'))
- # Make sure that the text we return from here can be encoded to a byte
- # string in the charset of the list's language. This could fail if for
- # example, the request was pended while the list's language was French,
- # but then it was changed to English before checkdbs ran.
- text = NL.join(upending)
- charset = Charset(Utils.GetCharSet(mlist.preferred_language))
- incodec = charset.input_codec or 'ascii'
- outcodec = charset.output_codec or 'ascii'
- if isinstance(text, unicode):
- return text.encode(outcodec, 'replace')
- # Be sure this is a byte string encodeable in the list's charset
- utext = unicode(text, incodec, 'replace')
- return utext.encode(outcodec, 'replace')
-
-
-
-def auto_discard(mlist):
- # Discard old held messages
- discard_count = 0
- expire = config.days(mlist.max_days_to_hold)
- requestsdb = config.db.get_list_requests(mlist)
- heldmsgs = list(requestsdb.of_type(RequestType.held_message))
- if expire and heldmsgs:
- for request in heldmsgs:
- key, data = requestsdb.get_request(request.id)
- if now - data['date'] > expire:
- handle_request(mlist, request.id, config.DISCARD)
- discard_count += 1
- mlist.Save()
- return discard_count
-
-
-
-def main():
- opts, args, parser = parseargs()
- config.load(opts.config)
-
- i18n.set_language(config.DEFAULT_SERVER_LANGUAGE)
-
- for name in config.list_manager.names:
- # The list must be locked in order to open the requests database
- mlist = MailList.MailList(name)
- try:
- count = config.db.requests.get_list_requests(mlist).count
- # While we're at it, let's evict yesterday's autoresponse data
- midnight_today = Utils.midnight()
- evictions = []
- for sender in mlist.hold_and_cmd_autoresponses.keys():
- date, respcount = mlist.hold_and_cmd_autoresponses[sender]
- if Utils.midnight(date) < midnight_today:
- evictions.append(sender)
- if evictions:
- for sender in evictions:
- del mlist.hold_and_cmd_autoresponses[sender]
- # This is the only place we've changed the list's database
- mlist.Save()
- if count:
- i18n.set_language(mlist.preferred_language)
- realname = mlist.real_name
- discarded = auto_discard(mlist)
- if discarded:
- count = count - discarded
- text = _(
- 'Notice: $discarded old request(s) automatically expired.\n\n')
- else:
- text = ''
- if count:
- text += Utils.maketext(
- 'checkdbs.txt',
- {'count' : count,
- 'host_name': mlist.host_name,
- 'adminDB' : mlist.GetScriptURL('admindb', absolute=1),
- 'real_name': realname,
- }, mlist=mlist)
- text += '\n' + pending_requests(mlist)
- subject = _('$count $realname moderator request(s) waiting')
- else:
- subject = _('$realname moderator request check result')
- msg = Message.UserNotification(mlist.GetOwnerEmail(),
- mlist.GetBouncesEmail(),
- subject, text,
- mlist.preferred_language)
- msg.send(mlist, **{'tomoderators': True})
- finally:
- mlist.Unlock()
-
-
-
-if __name__ == '__main__':
- main()
diff --git a/mailman/bin/cleanarch.py b/mailman/bin/cleanarch.py
deleted file mode 100644
index 325fad91a..000000000
--- a/mailman/bin/cleanarch.py
+++ /dev/null
@@ -1,133 +0,0 @@
-# Copyright (C) 2001-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 .
-
-"""Clean up an .mbox archive file."""
-
-import re
-import sys
-import mailbox
-import optparse
-
-from mailman.i18n import _
-from mailman.version import MAILMAN_VERSION
-
-
-cre = re.compile(mailbox.UnixMailbox._fromlinepattern)
-# From RFC 2822, a header field name must contain only characters from 33-126
-# inclusive, excluding colon. I.e. from oct 41 to oct 176 less oct 072. Must
-# use re.match() so that it's anchored at the beginning of the line.
-fre = re.compile(r'[\041-\071\073-\176]+')
-
-
-
-def parseargs():
- parser = optparse.OptionParser(version=MAILMAN_VERSION,
- usage=_("""\
-%prog [options] < inputfile > outputfile
-
-The archiver looks for Unix-From lines separating messages in an mbox archive
-file. For compatibility, it specifically looks for lines that start with
-'From ' -- i.e. the letters capital-F, lowercase-r, o, m, space, ignoring
-everything else on the line.
-
-Normally, any lines that start 'From ' in the body of a message should be
-escaped such that a > character is actually the first on a line. It is
-possible though that body lines are not actually escaped. This script
-attempts to fix these by doing a stricter test of the Unix-From lines. Any
-lines that start From ' but do not pass this stricter test are escaped with a
-'>' character."""))
- parser.add_option('-q', '--quiet',
- default=False, action='store_true', help=_("""\
-Don't print changed line information to standard error."""))
- parser.add_option('-s', '--status',
- default=-1, type='int', help=_("""\
-Print a '#' character for every n lines processed. With a number less than or
-equal to zero, suppress the '#' characters."""))
- parser.add_option('-n', '--dry-run',
- default=False, action='store_true', help=_("""\
-Don't actually output anything."""))
- opts, args = parser.parser_args()
- if args:
- parser.print_error(_('Unexpected arguments'))
- return parser, opts, args
-
-
-
-def escape_line(line, lineno, quiet, output):
- if output:
- sys.stdout.write('>' + line)
- if not quiet:
- print >> sys.stderr, _('Unix-From line changed: $lineno')
- print >> sys.stderr, line[:-1]
-
-
-
-def main():
- parser, opts, args = parseargs()
-
- lineno = 0
- statuscnt = 0
- messages = 0
- prevline = None
- while True:
- lineno += 1
- line = sys.stdin.readline()
- if not line:
- break
- if line.startswith('From '):
- if cre.match(line):
- # This is a real Unix-From line. But it could be a message
- # /about/ Unix-From lines, so as a second order test, make
- # sure there's at least one RFC 2822 header following
- nextline = sys.stdin.readline()
- lineno += 1
- if not nextline:
- # It was the last line of the mbox, so it couldn't have
- # been a Unix-From
- escape_line(line, lineno, quiet, output)
- break
- fieldname = nextline.split(':', 1)
- if len(fieldname) < 2 or not fre.match(nextline):
- # The following line was not a header, so this wasn't a
- # valid Unix-From
- escape_line(line, lineno, quiet, output)
- if output:
- sys.stdout.write(nextline)
- else:
- # It's a valid Unix-From line
- messages += 1
- if output:
- # Before we spit out the From_ line, make sure the
- # previous line was blank.
- if prevline is not None and prevline <> '\n':
- sys.stdout.write('\n')
- sys.stdout.write(line)
- sys.stdout.write(nextline)
- else:
- # This is a bogus Unix-From line
- escape_line(line, lineno, quiet, output)
- elif output:
- # Any old line
- sys.stdout.write(line)
- if status > 0 and (lineno % status) == 0:
- sys.stderr.write('#')
- statuscnt += 1
- if statuscnt > 50:
- print >> sys.stderr
- statuscnt = 0
- prevline = line
- print >> sys.stderr, _('%(messages)d messages found')
diff --git a/mailman/bin/config_list.py b/mailman/bin/config_list.py
deleted file mode 100644
index a5cec9480..000000000
--- a/mailman/bin/config_list.py
+++ /dev/null
@@ -1,332 +0,0 @@
-# Copyright (C) 1998-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 .
-
-import re
-import sys
-import time
-import optparse
-
-from mailman import errors
-from mailman import MailList
-from mailman import Utils
-from mailman import i18n
-from mailman.configuration import config
-from mailman.version import MAILMAN_VERSION
-
-_ = i18n._
-
-NL = '\n'
-nonasciipat = re.compile(r'[\x80-\xff]')
-
-
-
-def parseargs():
- parser = optparse.OptionParser(version=MAILMAN_VERSION,
- usage=_("""\
-%prog [options] listname
-
-Configure a list from a text file description, or dump a list's configuration
-settings."""))
- parser.add_option('-i', '--inputfile',
- metavar='FILENAME', default=None, type='string',
- help=_("""\
-Configure the list by assigning each module-global variable in the file to an
-attribute on the mailing list object, then save the list. The named file is
-loaded with execfile() and must be legal Python code. Any variable that isn't
-already an attribute of the list object is ignored (a warning message is
-printed). See also the -c option.
-
-A special variable named 'mlist' is put into the globals during the execfile,
-which is bound to the actual MailList object. This lets you do all manner of
-bizarre thing to the list object, but BEWARE! Using this can severely (and
-possibly irreparably) damage your mailing list!
-
-The may not be used with the -o option."""))
- parser.add_option('-o', '--outputfile',
- metavar='FILENAME', default=None, type='string',
- help=_("""\
-Instead of configuring the list, print out a mailing list's configuration
-variables in a format suitable for input using this script. In this way, you
-can easily capture the configuration settings for a particular list and
-imprint those settings on another list. FILENAME is the file to output the
-settings to. If FILENAME is `-', standard out is used.
-
-This may not be used with the -i option."""))
- parser.add_option('-c', '--checkonly',
- default=False, action='store_true', help=_("""\
-With this option, the modified list is not actually changed. This is only
-useful with the -i option."""))
- parser.add_option('-v', '--verbose',
- default=False, action='store_true', help=_("""\
-Print the name of each attribute as it is being changed. This is only useful
-with the -i option."""))
- parser.add_option('-C', '--config',
- help=_('Alternative configuration file to use'))
- opts, args = parser.parse_args()
- if len(args) > 1:
- parser.print_help()
- parser.error(_('Unexpected arguments'))
- if not args:
- parser.error(_('List name is required'))
- return parser, opts, args
-
-
-
-def do_output(listname, outfile, parser):
- closep = False
- try:
- if outfile == '-':
- outfp = sys.stdout
- else:
- outfp = open(outfile, 'w')
- closep = True
- # Open the specified list unlocked, since we're only reading it.
- try:
- mlist = MailList.MailList(listname, lock=False)
- except errors.MMListError:
- parser.error(_('No such list: $listname'))
- # Preamble for the config info. PEP 263 charset and capture time.
- language = mlist.preferred_language
- charset = Utils.GetCharSet(language)
- i18n.set_language(language)
- if not charset:
- charset = 'us-ascii'
- when = time.ctime(time.time())
- print >> outfp, _('''\
-# -*- python -*-
-# -*- coding: $charset -*-
-## "$listname" mailing list configuration settings
-## captured on $when
-''')
- # Get all the list config info. All this stuff is accessible via the
- # web interface.
- for k in config.ADMIN_CATEGORIES:
- subcats = mlist.GetConfigSubCategories(k)
- if subcats is None:
- do_list_categories(mlist, k, None, outfp)
- else:
- for subcat in [t[0] for t in subcats]:
- do_list_categories(mlist, k, subcat, outfp)
- finally:
- if closep:
- outfp.close()
-
-
-
-def do_list_categories(mlist, k, subcat, outfp):
- info = mlist.GetConfigInfo(k, subcat)
- label, gui = mlist.GetConfigCategories()[k]
- if info is None:
- return
- charset = Utils.GetCharSet(mlist.preferred_language)
- print >> outfp, '##', k.capitalize(), _('options')
- print >> outfp, '#'
- # First, massage the descripton text, which could have obnoxious
- # leading whitespace on second and subsequent lines due to
- # triple-quoted string nonsense in the source code.
- desc = NL.join([s.lstrip() for s in info[0].splitlines()])
- # Print out the category description
- desc = Utils.wrap(desc)
- for line in desc.splitlines():
- print >> outfp, '#', line
- print >> outfp
- for data in info[1:]:
- if not isinstance(data, tuple):
- continue
- varname = data[0]
- # Variable could be volatile
- if varname[0] == '_':
- continue
- vtype = data[1]
- # First, massage the descripton text, which could have
- # obnoxious leading whitespace on second and subsequent lines
- # due to triple-quoted string nonsense in the source code.
- desc = NL.join([s.lstrip() for s in data[-1].splitlines()])
- # Now strip out all HTML tags
- desc = re.sub('<.*?>', '', desc)
- # And convert </> to <>
- desc = re.sub('<', '<', desc)
- desc = re.sub('>', '>', desc)
- # Print out the variable description.
- desc = Utils.wrap(desc)
- for line in desc.split('\n'):
- print >> outfp, '#', line
- # munge the value based on its type
- value = None
- if hasattr(gui, 'getValue'):
- value = gui.getValue(mlist, vtype, varname, data[2])
- if value is None and not varname.startswith('_'):
- value = getattr(mlist, varname)
- if vtype in (config.String, config.Text, config.FileUpload):
- print >> outfp, varname, '=',
- lines = value.splitlines()
- if not lines:
- print >> outfp, "''"
- elif len(lines) == 1:
- if charset <> 'us-ascii' and nonasciipat.search(lines[0]):
- # This is more readable for non-english list.
- print >> outfp, '"' + lines[0].replace('"', '\\"') + '"'
- else:
- print >> outfp, repr(lines[0])
- else:
- if charset == 'us-ascii' and nonasciipat.search(value):
- # Normally, an english list should not have non-ascii char.
- print >> outfp, repr(NL.join(lines))
- else:
- outfp.write(' """')
- outfp.write(NL.join(lines).replace('"', '\\"'))
- outfp.write('"""\n')
- elif vtype in (config.Radio, config.Toggle):
- print >> outfp, '#'
- print >> outfp, '#', _('legal values are:')
- # TBD: This is disgusting, but it's special cased
- # everywhere else anyway...
- if varname == 'subscribe_policy' and \
- not config.ALLOW_OPEN_SUBSCRIBE:
- i = 1
- else:
- i = 0
- for choice in data[2]:
- print >> outfp, '# ', i, '= "%s"' % choice
- i += 1
- print >> outfp, varname, '=', repr(value)
- else:
- print >> outfp, varname, '=', repr(value)
- print >> outfp
-
-
-
-def getPropertyMap(mlist):
- guibyprop = {}
- categories = mlist.GetConfigCategories()
- for category, (label, gui) in categories.items():
- if not hasattr(gui, 'GetConfigInfo'):
- continue
- subcats = mlist.GetConfigSubCategories(category)
- if subcats is None:
- subcats = [(None, None)]
- for subcat, sclabel in subcats:
- for element in gui.GetConfigInfo(mlist, category, subcat):
- if not isinstance(element, tuple):
- continue
- propname = element[0]
- wtype = element[1]
- guibyprop[propname] = (gui, wtype)
- return guibyprop
-
-
-class FakeDoc:
- # Fake the error reporting API for the htmlformat.Document class
- def addError(self, s, tag=None, *args):
- if tag:
- print >> sys.stderr, tag
- print >> sys.stderr, s % args
-
- def set_language(self, val):
- pass
-
-
-
-def do_input(listname, infile, checkonly, verbose, parser):
- fakedoc = FakeDoc()
- # Open the specified list locked, unless checkonly is set
- try:
- mlist = MailList.MailList(listname, lock=not checkonly)
- except errors.MMListError, e:
- parser.error(_('No such list "$listname"\n$e'))
- savelist = False
- guibyprop = getPropertyMap(mlist)
- try:
- globals = {'mlist': mlist}
- # Any exception that occurs in execfile() will cause the list to not
- # be saved, but any other problems are not save-fatal.
- execfile(infile, globals)
- savelist = True
- for k, v in globals.items():
- if k in ('mlist', '__builtins__'):
- continue
- if not hasattr(mlist, k):
- print >> sys.stderr, _('attribute "$k" ignored')
- continue
- if verbose:
- print >> sys.stderr, _('attribute "$k" changed')
- missing = []
- gui, wtype = guibyprop.get(k, (missing, missing))
- if gui is missing:
- # This isn't an official property of the list, but that's
- # okay, we'll just restore it the old fashioned way
- print >> sys.stderr, _('Non-standard property restored: $k')
- setattr(mlist, k, v)
- else:
- # BAW: This uses non-public methods. This logic taken from
- # the guts of GUIBase.handleForm().
- try:
- validval = gui._getValidValue(mlist, k, wtype, v)
- except ValueError:
- print >> sys.stderr, _('Invalid value for property: $k')
- except errors.EmailAddressError:
- print >> sys.stderr, _(
- 'Bad email address for option $k: $v')
- else:
- # BAW: Horrible hack, but then this is special cased
- # everywhere anyway. :( Privacy._setValue() knows that
- # when ALLOW_OPEN_SUBSCRIBE is false, the web values are
- # 0, 1, 2 but these really should be 1, 2, 3, so it adds
- # one. But we really do provide [0..3] so we need to undo
- # the hack that _setValue adds. :( :(
- if k == 'subscribe_policy' and \
- not config.ALLOW_OPEN_SUBSCRIBE:
- validval -= 1
- # BAW: Another horrible hack. This one is just too hard
- # to fix in a principled way in Mailman 2.1
- elif k == 'new_member_options':
- # Because this is a Checkbox, _getValidValue()
- # transforms the value into a list of one item.
- validval = validval[0]
- validval = [bitfield for bitfield, bitval
- in config.OPTINFO.items()
- if validval & bitval]
- gui._setValue(mlist, k, validval, fakedoc)
- # BAW: when to do gui._postValidate()???
- finally:
- if savelist and not checkonly:
- mlist.Save()
- mlist.Unlock()
-
-
-
-def main():
- parser, opts, args = parseargs()
- config.load(opts.config)
- listname = args[0]
-
- # Sanity check
- if opts.inputfile and opts.outputfile:
- parser.error(_('Only one of -i or -o is allowed'))
- if not opts.inputfile and not opts.outputfile:
- parser.error(_('One of -i or -o is required'))
-
- if opts.outputfile:
- do_output(listname, opts.outputfile, parser)
- else:
- do_input(listname, opts.inputfile, opts.checkonly,
- opts.verbose, parser)
-
-
-
-if __name__ == '__main__':
- main()
diff --git a/mailman/bin/create_list.py b/mailman/bin/create_list.py
deleted file mode 100644
index 8058a7d67..000000000
--- a/mailman/bin/create_list.py
+++ /dev/null
@@ -1,129 +0,0 @@
-# Copyright (C) 1998-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 .
-
-import sys
-
-from mailman import Message
-from mailman import Utils
-from mailman import i18n
-from mailman.app.lifecycle import create_list
-from mailman.config import config
-from mailman.core import errors
-from mailman.interfaces.listmanager import ListAlreadyExistsError
-from mailman.options import SingleMailingListOptions
-
-
-_ = i18n._
-
-
-
-class ScriptOptions(SingleMailingListOptions):
- usage = _("""\
-%prog [options]
-
-Create a new mailing list.
-
-fqdn_listname is the 'fully qualified list name', basically the posting
-address of the list. It must be a valid email address and the domain must be
-registered with Mailman.
-
-Note that listnames are forced to lowercase.""")
-
- def add_options(self):
- super(ScriptOptions, self).add_options()
- self.parser.add_option(
- '--language',
- type='unicode', action='store',
- help=_("""\
-Make the list's preferred language LANGUAGE, which must be a two letter
-language code."""))
- self.parser.add_option(
- '-o', '--owner',
- type='unicode', action='append', default=[],
- dest='owners', help=_("""\
-Specific a listowner email address. If the address is not currently
-registered with Mailman, the address is registered and linked to a user.
-Mailman will send a confirmation message to the address, but it will also send
-a list creation notice to the address. More than one owner can be
-specified."""))
- self.parser.add_option(
- '-q', '--quiet',
- default=False, action='store_true',
- help=_("""\
-Normally the administrator is notified by email (after a prompt) that their
-list has been created. This option suppresses the prompt and
-notification."""))
- self.parser.add_option(
- '-a', '--automate',
- default=False, action='store_true',
- help=_("""\
-This option suppresses the prompt prior to administrator notification but
-still sends the notification. It can be used to make newlist totally
-non-interactive but still send the notification, assuming at least one list
-owner is specified with the -o option.."""))
-
- def sanity_check(self):
- """Set up some defaults we couldn't set up earlier."""
- if self.options.language is None:
- self.options.language = unicode(config.mailman.default_language)
- # Is the language known?
- if self.options.language not in config.languages.enabled_codes:
- self.parser.error(_('Unknown language: $opts.language'))
- # Handle variable number of positional arguments
- if len(self.arguments) > 0:
- parser.error(_('Unexpected arguments'))
-
-
-
-def main():
- options = ScriptOptions()
- options.initialize()
-
- # Create the mailing list, applying styles as appropriate.
- fqdn_listname = options.options.listname
- if fqdn_listname is None:
- options.parser.error(_('--listname is required'))
- try:
- mlist = create_list(fqdn_listname, options.options.owners)
- mlist.preferred_language = options.options.language
- except errors.InvalidEmailAddress:
- options.parser.error(_('Illegal list name: $fqdn_listname'))
- except ListAlreadyExistsError:
- options.parser.error(_('List already exists: $fqdn_listname'))
- except errors.BadDomainSpecificationError, domain:
- options.parser.error(_('Undefined domain: $domain'))
-
- config.db.commit()
-
- if not options.options.quiet:
- d = dict(
- listname = mlist.fqdn_listname,
- admin_url = mlist.script_url('admin'),
- listinfo_url = mlist.script_url('listinfo'),
- requestaddr = mlist.request_address,
- siteowner = mlist.no_reply_address,
- )
- text = Utils.maketext('newlist.txt', d, mlist=mlist)
- # Set the I18N language to the list's preferred language so the header
- # will match the template language. Stashing and restoring the old
- # translation context is just (healthy? :) paranoia.
- with i18n.using_language(mlist.preferred_language):
- msg = Message.UserNotification(
- owner_mail, mlist.no_reply_address,
- _('Your new mailing list: $fqdn_listname'),
- text, mlist.preferred_language)
- msg.send(mlist)
diff --git a/mailman/bin/disabled.py b/mailman/bin/disabled.py
deleted file mode 100644
index cc8eb2c69..000000000
--- a/mailman/bin/disabled.py
+++ /dev/null
@@ -1,201 +0,0 @@
-# Copyright (C) 2001-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 .
-
-import time
-import logging
-import optparse
-
-from mailman import errors
-from mailman import MailList
-from mailman import MemberAdaptor
-from mailman import Pending
-from mailman import loginit
-from mailman.Bouncer import _BounceInfo
-from mailman.configuration import config
-from mailman.i18n import _
-from mailman.version import MAILMAN_VERSION
-
-
-# Work around known problems with some RedHat cron daemons
-import signal
-signal.signal(signal.SIGCHLD, signal.SIG_DFL)
-
-ALL = (MemberAdaptor.BYBOUNCE,
- MemberAdaptor.BYADMIN,
- MemberAdaptor.BYUSER,
- MemberAdaptor.UNKNOWN,
- )
-
-
-
-def who_callback(option, opt, value, parser):
- dest = getattr(parser.values, option.dest)
- if opt in ('-o', '--byadmin'):
- dest.add(MemberAdaptor.BYADMIN)
- elif opt in ('-m', '--byuser'):
- dest.add(MemberAdaptor.BYUSER)
- elif opt in ('-u', '--unknown'):
- dest.add(MemberAdaptor.UNKNOWN)
- elif opt in ('-b', '--notbybounce'):
- dest.discard(MemberAdaptor.BYBOUNCE)
- elif opt in ('-a', '--all'):
- dest.update(ALL)
-
-
-def parseargs():
- parser = optparse.OptionParser(version=MAILMAN_VERSION,
- usage=_("""\
-%prog [options]
-
-Process disabled members, recommended once per day.
-
-This script iterates through every mailing list looking for members whose
-delivery is disabled. If they have been disabled due to bounces, they will
-receive another notification, or they may be removed if they've received the
-maximum number of notifications.
-
-Use the --byadmin, --byuser, and --unknown flags to also send notifications to
-members whose accounts have been disabled for those reasons. Use --all to
-send the notification to all disabled members."""))
- # This is the set of working flags for who to send notifications to. By
- # default, we notify anybody who has been disable due to bounces.
- parser.set_defaults(who=set([MemberAdaptor.BYBOUNCE]))
- parser.add_option('-o', '--byadmin',
- callback=who_callback, action='callback', dest='who',
- help=_("""\
-Also send notifications to any member disabled by the list
-owner/administrator."""))
- parser.add_option('-m', '--byuser',
- callback=who_callback, action='callback', dest='who',
- help=_("""\
-Also send notifications to any member who has disabled themself."""))
- parser.add_option('-u', '--unknown',
- callback=who_callback, action='callback', dest='who',
- help=_("""\
-Also send notifications to any member disabled for unknown reasons
-(usually a legacy disabled address)."""))
- parser.add_option('-b', '--notbybounce',
- callback=who_callback, action='callback', dest='who',
- help=_("""\
-Don't send notifications to members disabled because of bounces (the
-default is to notify bounce disabled members)."""))
- parser.add_option('-a', '--all',
- callback=who_callback, action='callback', dest='who',
- help=_('Send notifications to all disabled members'))
- parser.add_option('-f', '--force',
- default=False, action='store_true',
- help=_("""\
-Send notifications to disabled members even if they're not due a new
-notification yet."""))
- parser.add_option('-l', '--listname',
- dest='listnames', action='append', default=[],
- type='string', help=_("""\
-Process only the given list, otherwise do all lists."""))
- parser.add_option('-C', '--config',
- help=_('Alternative configuration file to use'))
- opts, args = parser.parse_args()
- return opts, args, parser
-
-
-
-def main():
- opts, args, parser = parseargs()
- config.load(opts.config)
-
- loginit.initialize(propagate=True)
- elog = logging.getLogger('mailman.error')
- blog = logging.getLogger('mailman.bounce')
-
- listnames = set(opts.listnames or config.list_manager.names)
- who = tuple(opts.who)
-
- msg = _('[disabled by periodic sweep and cull, no message available]')
- today = time.mktime(time.localtime()[:3] + (0,) * 6)
- for listname in listnames:
- # List of members to notify
- notify = []
- mlist = MailList.MailList(listname)
- try:
- interval = mlist.bounce_you_are_disabled_warnings_interval
- # Find all the members who are currently bouncing and see if
- # they've reached the disable threshold but haven't yet been
- # disabled. This is a sweep through the membership catching
- # situations where they've bounced a bunch, then the list admin
- # lowered the threshold, but we haven't (yet) seen more bounces
- # from the member. Note: we won't worry about stale information
- # or anything else since the normal bounce processing code will
- # handle that.
- disables = []
- for member in mlist.getBouncingMembers():
- if mlist.getDeliveryStatus(member) <> MemberAdaptor.ENABLED:
- continue
- info = mlist.getBounceInfo(member)
- if info.score >= mlist.bounce_score_threshold:
- disables.append((member, info))
- if disables:
- for member, info in disables:
- mlist.disableBouncingMember(member, info, msg)
- # Go through all the members who have delivery disabled, and find
- # those that are due to have another notification. If they are
- # disabled for another reason than bouncing, and we're processing
- # them (because of the command line switch) then they won't have a
- # bounce info record. We can piggyback on that for all disable
- # purposes.
- members = mlist.getDeliveryStatusMembers(who)
- for member in members:
- info = mlist.getBounceInfo(member)
- if not info:
- # See if they are bounce disabled, or disabled for some
- # other reason.
- status = mlist.getDeliveryStatus(member)
- if status == MemberAdaptor.BYBOUNCE:
- elog.error(
- '%s disabled BYBOUNCE lacks bounce info, list: %s',
- member, mlist.internal_name())
- continue
- info = _BounceInfo(
- member, 0, today,
- mlist.bounce_you_are_disabled_warnings,
- mlist.pend_new(Pending.RE_ENABLE,
- mlist.internal_name(),
- member))
- mlist.setBounceInfo(member, info)
- lastnotice = time.mktime(info.lastnotice + (0,) * 6)
- if opts.force or today >= lastnotice + interval:
- notify.append(member)
- # Now, send notifications to anyone who is due
- for member in notify:
- blog.info('Notifying disabled member %s for list: %s',
- member, mlist.internal_name())
- try:
- mlist.sendNextNotification(member)
- except errors.NotAMemberError:
- # There must have been some problem with the data we have
- # on this member. Most likely it's that they don't have a
- # password assigned. Log this and delete the member.
- blog.info(
- 'NotAMemberError when sending disabled notice: %s',
- member)
- mlist.ApprovedDeleteMember(member, 'cron/disabled')
- mlist.Save()
- finally:
- mlist.Unlock()
-
-
-
-if __name__ == '__main__':
- main()
diff --git a/mailman/bin/docs/master.txt b/mailman/bin/docs/master.txt
deleted file mode 100644
index 0d3cade77..000000000
--- a/mailman/bin/docs/master.txt
+++ /dev/null
@@ -1,49 +0,0 @@
-Mailman queue runner control
-============================
-
-Mailman has a number of queue runners which process messages in its queue file
-directories. In normal operation, a command line script called 'mailmanctl'
-is used to start, stop and manage the queue runners. mailmanctl actually is
-just a wrapper around the real queue runner watcher script called master.py.
-
- >>> from mailman.testing.helpers import TestableMaster
-
-Start the master in a subthread.
-
- >>> master = TestableMaster()
- >>> master.start()
-
-There should be a process id for every qrunner that claims to be startable.
-
- >>> from lazr.config import as_boolean
- >>> startable_qrunners = [qconf for qconf in config.qrunner_configs
- ... if as_boolean(qconf.start)]
- >>> len(list(master.qrunner_pids)) == len(startable_qrunners)
- True
-
-Now verify that all the qrunners are running.
-
- >>> import os
-
- # This should produce no output.
- >>> for pid in master.qrunner_pids:
- ... os.kill(pid, 0)
-
-Stop the master process, which should also kill (and not restart) the child
-queue runner processes.
-
- >>> master.stop()
-
-None of the children are running now.
-
- >>> import errno
- >>> for pid in master.qrunner_pids:
- ... try:
- ... os.kill(pid, 0)
- ... print 'Process did not exit:', pid
- ... except OSError, error:
- ... if error.errno == errno.ESRCH:
- ... # The child process exited.
- ... pass
- ... else:
- ... raise
diff --git a/mailman/bin/dumpdb.py b/mailman/bin/dumpdb.py
deleted file mode 100644
index 6657602e4..000000000
--- a/mailman/bin/dumpdb.py
+++ /dev/null
@@ -1,88 +0,0 @@
-# Copyright (C) 1998-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 .
-
-import pprint
-import cPickle
-
-from mailman.config import config
-from mailman.i18n import _
-from mailman.interact import interact
-from mailman.options import Options
-
-
-COMMASPACE = ', '
-m = []
-
-
-
-class ScriptOptions(Options):
- usage=_("""\
-%prog [options] filename
-
-Dump the contents of any Mailman queue file. The queue file is a data file
-with multiple Python pickles in it.""")
-
- def add_options(self):
- super(ScriptOptions, self).add_options()
- self.parser.add_option(
- '-n', '--noprint',
- dest='doprint', default=True, action='store_false',
- help=_("""\
-Don't attempt to pretty print the object. This is useful if there is some
-problem with the object and you just want to get an unpickled representation.
-Useful with 'bin/dumpdb -i '. In that case, the list of
-unpickled objects will be left in a variable called 'm'."""))
- self.parser.add_option(
- '-i', '--interact',
- default=False, action='store_true',
- help=_("""\
-Start an interactive Python session, with a variable called 'm' containing the
-list of unpickled objects."""))
-
- def sanity_check(self):
- if len(self.arguments) < 1:
- self.parser.error(_('No filename given.'))
- elif len(self.arguments) > 1:
- self.parser.error(_('Unexpected arguments'))
- else:
- self.filename = self.arguments[0]
-
-
-
-def main():
- options = ScriptOptions()
- options.initialize()
-
- pp = pprint.PrettyPrinter(indent=4)
- with open(options.filename) as fp:
- while True:
- try:
- m.append(cPickle.load(fp))
- except EOFError:
- break
- if options.options.doprint:
- print _('[----- start pickle -----]')
- for i, obj in enumerate(m):
- count = i + 1
- print _('<----- start object $count ----->')
- if isinstance(obj, basestring):
- print obj
- else:
- pp.pprint(obj)
- print _('[----- end pickle -----]')
- if options.options.interact:
- interact()
diff --git a/mailman/bin/export.py b/mailman/bin/export.py
deleted file mode 100644
index d1992b4b4..000000000
--- a/mailman/bin/export.py
+++ /dev/null
@@ -1,310 +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 .
-
-"""Export an XML representation of a mailing list."""
-
-import sys
-import codecs
-import datetime
-import optparse
-
-from xml.sax.saxutils import escape
-
-from mailman import Defaults
-from mailman import errors
-from mailman import MemberAdaptor
-from mailman.MailList import MailList
-from mailman.configuration import config
-from mailman.i18n import _
-from mailman.initialize import initialize
-from mailman.version import MAILMAN_VERSION
-
-
-SPACE = ' '
-
-TYPES = {
- Defaults.Toggle : 'bool',
- Defaults.Radio : 'radio',
- Defaults.String : 'string',
- Defaults.Text : 'text',
- Defaults.Email : 'email',
- Defaults.EmailList : 'email_list',
- Defaults.Host : 'host',
- Defaults.Number : 'number',
- Defaults.FileUpload : 'upload',
- Defaults.Select : 'select',
- Defaults.Topics : 'topics',
- Defaults.Checkbox : 'checkbox',
- Defaults.EmailListEx : 'email_list_ex',
- Defaults.HeaderFilter : 'header_filter',
- }
-
-
-
-class Indenter:
- def __init__(self, fp, indentwidth=4):
- self._fp = fp
- self._indent = 0
- self._width = indentwidth
-
- def indent(self):
- self._indent += 1
-
- def dedent(self):
- self._indent -= 1
- assert self._indent >= 0
-
- def write(self, s):
- if s <> '\n':
- self._fp.write(self._indent * self._width * ' ')
- self._fp.write(s)
-
-
-
-class XMLDumper(object):
- def __init__(self, fp):
- self._fp = Indenter(fp)
- self._tagbuffer = None
- self._stack = []
-
- def _makeattrs(self, tagattrs):
- # The attribute values might contain angle brackets. They might also
- # be None.
- attrs = []
- for k, v in tagattrs.items():
- if v is None:
- v = ''
- else:
- v = escape(str(v))
- attrs.append('%s="%s"' % (k, v))
- return SPACE.join(attrs)
-
- def _flush(self, more=True):
- if not self._tagbuffer:
- return
- name, attributes = self._tagbuffer
- self._tagbuffer = None
- if attributes:
- attrstr = ' ' + self._makeattrs(attributes)
- else:
- attrstr = ''
- if more:
- print >> self._fp, '<%s%s>' % (name, attrstr)
- self._fp.indent()
- self._stack.append(name)
- else:
- print >> self._fp, '<%s%s/>' % (name, attrstr)
-
- # Use this method when you know you have sub-elements.
- def _push_element(self, _name, **_tagattrs):
- self._flush()
- self._tagbuffer = (_name, _tagattrs)
-
- def _pop_element(self, _name):
- buffered = bool(self._tagbuffer)
- self._flush(more=False)
- if not buffered:
- name = self._stack.pop()
- assert name == _name, 'got: %s, expected: %s' % (_name, name)
- self._fp.dedent()
- print >> self._fp, '%s>' % name
-
- # Use this method when you do not have sub-elements
- def _element(self, _name, _value=None, **_attributes):
- self._flush()
- if _attributes:
- attrs = ' ' + self._makeattrs(_attributes)
- else:
- attrs = ''
- if _value is None:
- print >> self._fp, '<%s%s/>' % (_name, attrs)
- else:
- # The value might contain angle brackets.
- value = escape(unicode(_value))
- print >> self._fp, '<%s%s>%s%s>' % (_name, attrs, value, _name)
-
- def _do_list_categories(self, mlist, k, subcat=None):
- info = mlist.GetConfigInfo(k, subcat)
- label, gui = mlist.GetConfigCategories()[k]
- if info is None:
- return
- for data in info[1:]:
- if not isinstance(data, tuple):
- continue
- varname = data[0]
- # Variable could be volatile
- if varname.startswith('_'):
- continue
- vtype = data[1]
- # Munge the value based on its type
- value = None
- if hasattr(gui, 'getValue'):
- value = gui.getValue(mlist, vtype, varname, data[2])
- if value is None:
- value = getattr(mlist, varname)
- widget_type = TYPES[vtype]
- if isinstance(value, list):
- self._push_element('option', name=varname, type=widget_type)
- for v in value:
- self._element('value', v)
- self._pop_element('option')
- else:
- self._element('option', value, name=varname, type=widget_type)
-
- def _dump_list(self, mlist):
- # Write list configuration values
- self._push_element('list', name=mlist.fqdn_listname)
- self._push_element('configuration')
- self._element('option',
- mlist.preferred_language,
- name='preferred_language')
- for k in config.ADMIN_CATEGORIES:
- subcats = mlist.GetConfigSubCategories(k)
- if subcats is None:
- self._do_list_categories(mlist, k)
- else:
- for subcat in [t[0] for t in subcats]:
- self._do_list_categories(mlist, k, subcat)
- self._pop_element('configuration')
- # Write membership
- self._push_element('roster')
- digesters = set(mlist.getDigestMemberKeys())
- for member in sorted(mlist.getMembers()):
- attrs = dict(id=member)
- cased = mlist.getMemberCPAddress(member)
- if cased <> member:
- attrs['original'] = cased
- self._push_element('member', **attrs)
- self._element('realname', mlist.getMemberName(member))
- self._element('password', mlist.getMemberPassword(member))
- self._element('language', mlist.getMemberLanguage(member))
- # Delivery status, combined with the type of delivery
- attrs = {}
- status = mlist.getDeliveryStatus(member)
- if status == MemberAdaptor.ENABLED:
- attrs['status'] = 'enabled'
- else:
- attrs['status'] = 'disabled'
- attrs['reason'] = {MemberAdaptor.BYUSER : 'byuser',
- MemberAdaptor.BYADMIN : 'byadmin',
- MemberAdaptor.BYBOUNCE : 'bybounce',
- }.get(mlist.getDeliveryStatus(member),
- 'unknown')
- if member in digesters:
- if mlist.getMemberOption(member, Defaults.DisableMime):
- attrs['delivery'] = 'plain'
- else:
- attrs['delivery'] = 'mime'
- else:
- attrs['delivery'] = 'regular'
- changed = mlist.getDeliveryStatusChangeTime(member)
- if changed:
- when = datetime.datetime.fromtimestamp(changed)
- attrs['changed'] = when.isoformat()
- self._element('delivery', **attrs)
- for option, flag in Defaults.OPTINFO.items():
- # Digest/Regular delivery flag must be handled separately
- if option in ('digest', 'plain'):
- continue
- value = mlist.getMemberOption(member, flag)
- self._element(option, value)
- topics = mlist.getMemberTopics(member)
- if not topics:
- self._element('topics')
- else:
- self._push_element('topics')
- for topic in topics:
- self._element('topic', topic)
- self._pop_element('topics')
- self._pop_element('member')
- self._pop_element('roster')
- self._pop_element('list')
-
- def dump(self, listnames):
- print >> self._fp, ''
- self._push_element('mailman', **{
- 'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
- 'xsi:noNamespaceSchemaLocation': 'ssi-1.0.xsd',
- })
- for listname in sorted(listnames):
- try:
- mlist = MailList(listname, lock=False)
- except errors.MMUnknownListError:
- print >> sys.stderr, _('No such list: $listname')
- continue
- self._dump_list(mlist)
- self._pop_element('mailman')
-
- def close(self):
- while self._stack:
- self._pop_element()
-
-
-
-def parseargs():
- parser = optparse.OptionParser(version=MAILMAN_VERSION,
- usage=_("""\
-%prog [options]
-
-Export the configuration and members of a mailing list in XML format."""))
- parser.add_option('-o', '--outputfile',
- metavar='FILENAME', default=None, type='string',
- help=_("""\
-Output XML to FILENAME. If not given, or if FILENAME is '-', standard out is
-used."""))
- parser.add_option('-l', '--listname',
- default=[], action='append', type='string',
- metavar='LISTNAME', dest='listnames', help=_("""\
-The list to include in the output. If not given, then all mailing lists are
-included in the XML output. Multiple -l flags may be given."""))
- parser.add_option('-C', '--config',
- help=_('Alternative configuration file to use'))
- opts, args = parser.parse_args()
- if args:
- parser.print_help()
- parser.error(_('Unexpected arguments'))
- return parser, opts, args
-
-
-
-def main():
- parser, opts, args = parseargs()
- initialize(opts.config)
-
- close = False
- if opts.outputfile in (None, '-'):
- writer = codecs.getwriter('utf-8')
- fp = writer(sys.stdout)
- else:
- fp = codecs.open(opts.outputfile, 'w', 'utf-8')
- close = True
-
- try:
- dumper = XMLDumper(fp)
- if opts.listnames:
- listnames = []
- for listname in opts.listnames:
- if '@' not in listname:
- listname = '%s@%s' % (listname, config.DEFAULT_EMAIL_HOST)
- listnames.append(listname)
- else:
- listnames = config.list_manager.names
- dumper.dump(listnames)
- dumper.close()
- finally:
- if close:
- fp.close()
diff --git a/mailman/bin/find_member.py b/mailman/bin/find_member.py
deleted file mode 100644
index 0982724a0..000000000
--- a/mailman/bin/find_member.py
+++ /dev/null
@@ -1,135 +0,0 @@
-# Copyright (C) 1998-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 .
-
-import re
-import sys
-import optparse
-
-from mailman import errors
-from mailman import MailList
-from mailman.configuration import config
-from mailman.i18n import _
-from mailman.version import MAILMAN_VERSION
-
-
-AS_MEMBER = 0x01
-AS_OWNER = 0x02
-
-
-
-def parseargs():
- parser = optparse.OptionParser(version=MAILMAN_VERSION,
- usage=_("""\
-%prog [options] regex [regex ...]
-
-Find all lists that a member's address is on.
-
-The interaction between -l and -x (see below) is as follows. If any -l option
-is given then only the named list will be included in the search. If any -x
-option is given but no -l option is given, then all lists will be search
-except those specifically excluded.
-
-Regular expression syntax uses the Python 're' module. Complete
-specifications are at:
-
-http://www.python.org/doc/current/lib/module-re.html
-
-Address matches are case-insensitive, but case-preserved addresses are
-displayed."""))
- parser.add_option('-l', '--listname',
- type='string', default=[], action='append',
- dest='listnames',
- help=_('Include only the named list in the search'))
- parser.add_option('-x', '--exclude',
- type='string', default=[], action='append',
- dest='excludes',
- help=_('Exclude the named list from the search'))
- parser.add_option('-w', '--owners',
- default=False, action='store_true',
- help=_('Search list owners as well as members'))
- parser.add_option('-C', '--config',
- help=_('Alternative configuration file to use'))
- opts, args = parser.parse_args()
- if not args:
- parser.print_help()
- print >> sys.stderr, _('Search regular expression required')
- sys.exit(1)
- return parser, opts, args
-
-
-
-def main():
- parser, opts, args = parseargs()
- config.load(opts.config)
-
- listnames = opts.listnames or config.list_manager.names
- includes = set(listname.lower() for listname in listnames)
- excludes = set(listname.lower() for listname in opts.excludes)
- listnames = includes - excludes
-
- if not listnames:
- print _('No lists to search')
- return
-
- cres = []
- for r in args:
- cres.append(re.compile(r, re.IGNORECASE))
- # dictionary of {address, (listname, ownerp)}
- matches = {}
- for listname in listnames:
- try:
- mlist = MailList.MailList(listname, lock=False)
- except errors.MMListError:
- print _('No such list: $listname')
- continue
- if opts.owners:
- owners = mlist.owner
- else:
- owners = []
- for cre in cres:
- for member in mlist.getMembers():
- if cre.search(member):
- addr = mlist.getMemberCPAddress(member)
- entries = matches.get(addr, {})
- aswhat = entries.get(listname, 0)
- aswhat |= AS_MEMBER
- entries[listname] = aswhat
- matches[addr] = entries
- for owner in owners:
- if cre.search(owner):
- entries = matches.get(owner, {})
- aswhat = entries.get(listname, 0)
- aswhat |= AS_OWNER
- entries[listname] = aswhat
- matches[owner] = entries
- addrs = matches.keys()
- addrs.sort()
- for k in addrs:
- hits = matches[k]
- lists = hits.keys()
- print k, _('found in:')
- for name in lists:
- aswhat = hits[name]
- if aswhat & AS_MEMBER:
- print ' ', name
- if aswhat & AS_OWNER:
- print ' ', name, _('(as owner)')
-
-
-
-if __name__ == '__main__':
- main()
diff --git a/mailman/bin/gate_news.py b/mailman/bin/gate_news.py
deleted file mode 100644
index eac30422d..000000000
--- a/mailman/bin/gate_news.py
+++ /dev/null
@@ -1,243 +0,0 @@
-# Copyright (C) 1998-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 .
-
-import os
-import sys
-import time
-import socket
-import logging
-import nntplib
-import optparse
-import email.Errors
-
-from email.Parser import Parser
-from locknix import lockfile
-
-from mailman import MailList
-from mailman import Message
-from mailman import Utils
-from mailman import loginit
-from mailman.configuration import config
-from mailman.i18n import _
-from mailman.queue import Switchboard
-from mailman.version import MAILMAN_VERSION
-
-# Work around known problems with some RedHat cron daemons
-import signal
-signal.signal(signal.SIGCHLD, signal.SIG_DFL)
-
-NL = '\n'
-
-log = None
-
-class _ContinueLoop(Exception):
- pass
-
-
-
-def parseargs():
- parser = optparse.OptionParser(version=MAILMAN_VERSION,
- usage=_("""\
-%prog [options]
-
-Poll the NNTP servers for messages to be gatewayed to mailing lists."""))
- parser.add_option('-C', '--config',
- help=_('Alternative configuration file to use'))
- opts, args = parser.parse_args()
- if args:
- parser.print_help()
- print >> sys.stderr, _('Unexpected arguments')
- sys.exit(1)
- return opts, args, parser
-
-
-
-_hostcache = {}
-
-def open_newsgroup(mlist):
- # Split host:port if given
- nntp_host, nntp_port = Utils.nntpsplit(mlist.nntp_host)
- # Open up a "mode reader" connection to nntp server. This will be shared
- # for all the gated lists having the same nntp_host.
- conn = _hostcache.get(mlist.nntp_host)
- if conn is None:
- try:
- conn = nntplib.NNTP(nntp_host, nntp_port,
- readermode=True,
- user=config.NNTP_USERNAME,
- password=config.NNTP_PASSWORD)
- except (socket.error, nntplib.NNTPError, IOError), e:
- log.error('error opening connection to nntp_host: %s\n%s',
- mlist.nntp_host, e)
- raise
- _hostcache[mlist.nntp_host] = conn
- # Get the GROUP information for the list, but we're only really interested
- # in the first article number and the last article number
- r, c, f, l, n = conn.group(mlist.linked_newsgroup)
- return conn, int(f), int(l)
-
-
-def clearcache():
- for conn in set(_hostcache.values()):
- conn.quit()
- _hostcache.clear()
-
-
-
-# This function requires the list to be locked.
-def poll_newsgroup(mlist, conn, first, last, glock):
- listname = mlist.internal_name()
- # NEWNEWS is not portable and has synchronization issues.
- for num in range(first, last):
- glock.refresh()
- try:
- headers = conn.head(repr(num))[3]
- found_to = False
- beenthere = False
- for header in headers:
- i = header.find(':')
- value = header[:i].lower()
- if i > 0 and value == 'to':
- found_to = True
- if value <> 'x-beenthere':
- continue
- if header[i:] == ': %s' % mlist.posting_address:
- beenthere = True
- break
- if not beenthere:
- body = conn.body(repr(num))[3]
- # Usenet originated messages will not have a Unix envelope
- # (i.e. "From " header). This breaks Pipermail archiving, so
- # we will synthesize one. Be sure to use the format searched
- # for by mailbox.UnixMailbox._isrealfromline(). BAW: We use
- # the -bounces address here in case any downstream clients use
- # the envelope sender for bounces; I'm not sure about this,
- # but it's the closest to the old semantics.
- lines = ['From %s %s' % (mlist.GetBouncesEmail(),
- time.ctime(time.time()))]
- lines.extend(headers)
- lines.append('')
- lines.extend(body)
- lines.append('')
- p = Parser(Message.Message)
- try:
- msg = p.parsestr(NL.join(lines))
- except email.Errors.MessageError, e:
- log.error('email package exception for %s:%d\n%s',
- mlist.linked_newsgroup, num, e)
- raise _ContinueLoop
- if found_to:
- del msg['X-Originally-To']
- msg['X-Originally-To'] = msg['To']
- del msg['To']
- msg['To'] = mlist.posting_address
- # Post the message to the locked list
- inq = Switchboard(config.INQUEUE_DIR)
- inq.enqueue(msg,
- listname=mlist.internal_name(),
- fromusenet=True)
- log.info('posted to list %s: %7d', listname, num)
- except nntplib.NNTPError, e:
- log.exception('NNTP error for list %s: %7d', listname, num)
- except _ContinueLoop:
- continue
- # Even if we don't post the message because it was seen on the
- # list already, update the watermark
- mlist.usenet_watermark = num
-
-
-
-def process_lists(glock):
- for listname in config.list_manager.names:
- glock.refresh()
- # Open the list unlocked just to check to see if it is gating news to
- # mail. If not, we're done with the list. Otherwise, lock the list
- # and gate the group.
- mlist = MailList.MailList(listname, lock=False)
- if not mlist.gateway_to_mail:
- continue
- # Get the list's watermark, i.e. the last article number that we gated
- # from news to mail. None means that this list has never polled its
- # newsgroup and that we should do a catch up.
- watermark = getattr(mlist, 'usenet_watermark', None)
- # Open the newsgroup, but let most exceptions percolate up.
- try:
- conn, first, last = open_newsgroup(mlist)
- except (socket.error, nntplib.NNTPError):
- break
- log.info('%s: [%d..%d]', listname, first, last)
- try:
- try:
- if watermark is None:
- mlist.Lock(timeout=config.LIST_LOCK_TIMEOUT)
- # This is the first time we've tried to gate this
- # newsgroup. We essentially do a mass catch-up, otherwise
- # we'd flood the mailing list.
- mlist.usenet_watermark = last
- log.info('%s caught up to article %d', listname, last)
- else:
- # The list has been polled previously, so now we simply
- # grab all the messages on the newsgroup that have not
- # been seen by the mailing list. The first such article
- # is the maximum of the lowest article available in the
- # newsgroup and the watermark. It's possible that some
- # articles have been expired since the last time gate_news
- # has run. Not much we can do about that.
- start = max(watermark + 1, first)
- if start > last:
- log.info('nothing new for list %s', listname)
- else:
- mlist.Lock(timeout=config.LIST_LOCK_TIMEOUT)
- log.info('gating %s articles [%d..%d]',
- listname, start, last)
- # Use last+1 because poll_newsgroup() employes a for
- # loop over range, and this will not include the last
- # element in the list.
- poll_newsgroup(mlist, conn, start, last + 1, glock)
- except lockfile.TimeOutError:
- log.error('Could not acquire list lock: %s', listname)
- finally:
- if mlist.Locked():
- mlist.Save()
- mlist.Unlock()
- log.info('%s watermark: %d', listname, mlist.usenet_watermark)
-
-
-
-def main():
- opts, args, parser = parseargs()
- config.load(opts.config)
-
- GATENEWS_LOCK_FILE = os.path.join(config.LOCK_DIR, 'gate_news.lock')
- LOCK_LIFETIME = config.hours(2)
-
- loginit.initialize(propagate=True)
- log = logging.getLogger('mailman.fromusenet')
-
- try:
- with lockfile.Lock(GATENEWS_LOCK_FILE,
- # It's okay to hijack this
- lifetime=LOCK_LIFETIME) as lock:
- process_lists(lock)
- clearcache()
- except lockfile.TimeOutError:
- log.error('Could not acquire gate_news lock')
-
-
-
-if __name__ == '__main__':
- main()
diff --git a/mailman/bin/genaliases.py b/mailman/bin/genaliases.py
deleted file mode 100644
index e8916d030..000000000
--- a/mailman/bin/genaliases.py
+++ /dev/null
@@ -1,64 +0,0 @@
-# Copyright (C) 2001-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 .
-
-__metaclass__ = type
-__all__ = [
- 'main',
- ]
-
-
-import sys
-
-from mailman.config import config
-from mailman.i18n import _
-from mailman.options import Options
-
-
-
-class ScriptOptions(Options):
- """Options for the genaliases script."""
-
- usage = _("""\
-%prog [options]
-
-Regenerate the Mailman specific MTA aliases from scratch. The actual output
-depends on the value of the 'MTA' variable in your etc/mailman.cfg file.""")
-
- def add_options(self):
- super(ScriptOptions, self).add_options()
- self.parser.add_option(
- '-q', '--quiet',
- default=False, action='store_true', help=_("""\
-Some MTA output can include more verbose help text. Use this to tone down the
-verbosity."""))
-
-
-
-
-def main():
- options = ScriptOptions()
- options.initialize()
-
- # Get the MTA-specific module.
- module_path, class_path = config.mta.incoming.rsplit('.', 1)
- __import__(module_path)
- getattr(sys.modules[module_path], class_path)().regenerate()
-
-
-
-if __name__ == '__main__':
- main()
diff --git a/mailman/bin/import.py b/mailman/bin/import.py
deleted file mode 100644
index d2361e808..000000000
--- a/mailman/bin/import.py
+++ /dev/null
@@ -1,315 +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 .
-
-"""Import the XML representation of a mailing list."""
-
-import sys
-import codecs
-import optparse
-import traceback
-
-from xml.dom import minidom
-from xml.parsers.expat import ExpatError
-
-from mailman import Defaults
-from mailman import errors
-from mailman import MemberAdaptor
-from mailman import Utils
-from mailman import passwords
-from mailman.MailList import MailList
-from mailman.i18n import _
-from mailman.initialize import initialize
-from mailman.version import MAILMAN_VERSION
-
-
-OPTS = None
-
-
-
-def nodetext(node):
- # Expect only one TEXT_NODE in the list of children
- for child in node.childNodes:
- if child.nodeType == node.TEXT_NODE:
- return child.data
- return u''
-
-
-def nodegen(node, *elements):
- for child in node.childNodes:
- if child.nodeType <> minidom.Node.ELEMENT_NODE:
- continue
- if elements and child.tagName not in elements:
- print _('Ignoring unexpected element: $node.tagName')
- else:
- yield child
-
-
-
-def parse_config(node):
- config = dict()
- for child in nodegen(node, 'option'):
- name = child.getAttribute('name')
- if not name:
- print _('Skipping unnamed option')
- continue
- vtype = child.getAttribute('type') or 'string'
- if vtype in ('email_list', 'email_list_ex', 'checkbox'):
- value = []
- for subnode in nodegen(child):
- value.append(nodetext(subnode))
- elif vtype == 'bool':
- value = nodetext(child)
- try:
- value = bool(int(value))
- except ValueError:
- value = {'true' : True,
- 'false': False,
- }.get(value.lower())
- if value is None:
- print _('Skipping bad boolean value: $value')
- continue
- elif vtype == 'radio':
- value = nodetext(child).lower()
- boolval = {'true' : True,
- 'false': False,
- }.get(value)
- if boolval is None:
- value = int(value)
- else:
- value = boolval
- elif vtype == 'number':
- value = nodetext(child)
- # First try int then float
- try:
- value = int(value)
- except ValueError:
- value = float(value)
- elif vtype in ('header_filter', 'topics'):
- value = []
- fakebltins = dict(__builtins__ = dict(True=True, False=False))
- for subnode in nodegen(child):
- reprstr = nodetext(subnode)
- # Turn the reprs back into tuples, in a safe way
- tupleval = eval(reprstr, fakebltins)
- value.append(tupleval)
- else:
- value = nodetext(child)
- # And now some special casing :(
- if name == 'new_member_options':
- value = int(nodetext(child))
- config[name] = value
- return config
-
-
-
-
-def parse_roster(node):
- members = []
- for child in nodegen(node, 'member'):
- member = dict()
- member['id'] = mid = child.getAttribute('id')
- if not mid:
- print _('Skipping member with no id')
- continue
- if OPTS.verbose:
- print _('* Processing member: $mid')
- for subnode in nodegen(child):
- attr = subnode.tagName
- if attr == 'delivery':
- value = (subnode.getAttribute('status'),
- subnode.getAttribute('delivery'))
- elif attr in ('hide', 'ack', 'notmetoo', 'nodupes', 'nomail'):
- value = {'true' : True,
- 'false': False,
- }.get(nodetext(subnode).lower(), False)
- elif attr == 'topics':
- value = []
- for subsubnode in nodegen(subnode):
- value.append(nodetext(subsubnode))
- elif attr == 'password':
- value = nodetext(subnode)
- if OPTS.reset_passwords or value == '{NONE}' or not value:
- value = passwords.make_secret(Utils.MakeRandomPassword())
- else:
- value = nodetext(subnode)
- member[attr] = value
- members.append(member)
- return members
-
-
-
-def load(fp):
- try:
- doc = minidom.parse(fp)
- except ExpatError:
- print _('Expat error in file: $fp.name')
- traceback.print_exc()
- sys.exit(1)
- doc.normalize()
- # Make sure there's only one top-level node
- gen = nodegen(doc, 'mailman')
- top = gen.next()
- try:
- gen.next()
- except StopIteration:
- pass
- else:
- print _('Malformed XML; duplicate nodes')
- sys.exit(1)
- all_listdata = []
- for listnode in nodegen(top, 'list'):
- listdata = dict()
- name = listnode.getAttribute('name')
- if OPTS.verbose:
- print _('Processing list: $name')
- if not name:
- print _('Ignoring malformed node')
- continue
- for child in nodegen(listnode, 'configuration', 'roster'):
- if child.tagName == 'configuration':
- list_config = parse_config(child)
- else:
- assert(child.tagName == 'roster')
- list_roster = parse_roster(child)
- all_listdata.append((name, list_config, list_roster))
- return all_listdata
-
-
-
-def create(all_listdata):
- for name, list_config, list_roster in all_listdata:
- fqdn_listname = '%s@%s' % (name, list_config['host_name'])
- if Utils.list_exists(fqdn_listname):
- print _('Skipping already existing list: $fqdn_listname')
- continue
- mlist = MailList()
- try:
- if OPTS.verbose:
- print _('Creating mailing list: $fqdn_listname')
- mlist.Create(fqdn_listname, list_config['owner'][0],
- list_config['password'])
- except errors.BadDomainSpecificationError:
- print _('List is not in a supported domain: $fqdn_listname')
- continue
- # Save the list creation, then unlock and relock the list. This is so
- # that we use normal SQLAlchemy transactions to manage all the
- # attribute and membership updates. Without this, no transaction will
- # get committed in the second Save() below and we'll lose all our
- # updates.
- mlist.Save()
- mlist.Unlock()
- mlist.Lock()
- try:
- for option, value in list_config.items():
- # XXX Here's what sucks. Some properties need to have
- # _setValue() called on the gui component, because those
- # methods do some pre-processing on the values before they're
- # applied to the MailList instance. But we don't have a good
- # way to find a category and sub-category that a particular
- # property belongs to. Plus this will probably change. So
- # for now, we'll just hard code the extra post-processing
- # here. The good news is that not all _setValue() munging
- # needs to be done -- for example, we've already converted
- # everything to dollar strings.
- if option in ('filter_mime_types', 'pass_mime_types',
- 'filter_filename_extensions',
- 'pass_filename_extensions'):
- value = value.splitlines()
- if option == 'available_languages':
- mlist.set_languages(*value)
- else:
- setattr(mlist, option, value)
- for member in list_roster:
- mid = member['id']
- if OPTS.verbose:
- print _('* Adding member: $mid')
- status, delivery = member['delivery']
- kws = {'password' : member['password'],
- 'language' : member['language'],
- 'realname' : member['realname'],
- 'digest' : delivery <> 'regular',
- }
- mlist.addNewMember(mid, **kws)
- status = {'enabled' : MemberAdaptor.ENABLED,
- 'byuser' : MemberAdaptor.BYUSER,
- 'byadmin' : MemberAdaptor.BYADMIN,
- 'bybounce' : MemberAdaptor.BYBOUNCE,
- }.get(status, MemberAdaptor.UNKNOWN)
- mlist.setDeliveryStatus(mid, status)
- for opt in ('hide', 'ack', 'notmetoo', 'nodupes', 'nomail'):
- mlist.setMemberOption(mid,
- Defaults.OPTINFO[opt],
- member[opt])
- topics = member.get('topics')
- if topics:
- mlist.setMemberTopics(mid, topics)
- mlist.Save()
- finally:
- mlist.Unlock()
-
-
-
-def parseargs():
- parser = optparse.OptionParser(version=MAILMAN_VERSION,
- usage=_("""\
-%prog [options]
-
-Import the configuration and/or members of a mailing list in XML format. The
-imported mailing list must not already exist. All mailing lists named in the
-XML file are imported, but those that already exist are skipped unless --error
-is given."""))
- parser.add_option('-i', '--inputfile',
- metavar='FILENAME', default=None, type='string',
- help=_("""\
-Input XML from FILENAME. If not given, or if FILENAME is '-', standard input
-is used."""))
- parser.add_option('-p', '--reset-passwords',
- default=False, action='store_true', help=_("""\
-With this option, user passwords in the XML are ignored and are reset to a
-random password. If the generated passwords were not included in the input
-XML, they will always be randomly generated."""))
- parser.add_option('-v', '--verbose',
- default=False, action='store_true',
- help=_('Produce more verbose output'))
- parser.add_option('-C', '--config',
- help=_('Alternative configuration file to use'))
- opts, args = parser.parse_args()
- if args:
- parser.print_help()
- parser.error(_('Unexpected arguments'))
- return parser, opts, args
-
-
-
-def main():
- global OPTS
-
- parser, opts, args = parseargs()
- initialize(opts.config)
- OPTS = opts
-
- if opts.inputfile in (None, '-'):
- fp = sys.stdin
- else:
- fp = open(opts.inputfile, 'r')
-
- try:
- listbags = load(fp)
- create(listbags)
- finally:
- if fp is not sys.stdin:
- fp.close()
diff --git a/mailman/bin/inject.py b/mailman/bin/inject.py
deleted file mode 100644
index 2bc8a49e3..000000000
--- a/mailman/bin/inject.py
+++ /dev/null
@@ -1,89 +0,0 @@
-# Copyright (C) 2002-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 .
-
-import os
-import sys
-
-from email import message_from_string
-
-from mailman import Utils
-from mailman.Message import Message
-from mailman.configuration import config
-from mailman.i18n import _
-from mailman.inject import inject_text
-from mailman.options import SingleMailingListOptions
-
-
-
-class ScriptOptions(SingleMailingListOptions):
- usage=_("""\
-%prog [options] [filename]
-
-Inject a message from a file into Mailman's incoming queue. 'filename' is the
-name of the plaintext message file to inject. If omitted, or the string '-',
-standard input is used.
-""")
-
- def add_options(self):
- super(ScriptOptions, self).add_options()
- self.parser.add_option(
- '-q', '--queue',
- type='string', help=_("""\
-The name of the queue to inject the message to. The queuename must be one of
-the directories inside the qfiles directory. If omitted, the incoming queue
-is used."""))
-
- def sanity_check(self):
- if not self.options.listname:
- self.parser.error(_('Missing listname'))
- if len(self.arguments) == 0:
- self.filename = '-'
- elif len(self.arguments) > 1:
- self.parser.print_error(_('Unexpected arguments'))
- else:
- self.filename = self.arguments[0]
-
-
-
-def main():
- options = ScriptOptions()
- options.initialize()
-
- if options.options.queue is None:
- qdir = config.INQUEUE_DIR
- else:
- qdir = os.path.join(config.QUEUE_DIR, options.options.queue)
- if not os.path.isdir(qdir):
- options.parser.error(_('Bad queue directory: $qdir'))
-
- fqdn_listname = options.options.listname
- mlist = config.db.list_manager.get(fqdn_listname)
- if mlist is None:
- options.parser.error(_('No such list: $fqdn_listname'))
-
- if options.filename == '-':
- message_text = sys.stdin.read()
- else:
- with open(options.filename) as fp:
- message_text = fp.read()
-
- inject_text(mlist, message_text, qdir=qdir)
-
-
-
-if __name__ == '__main__':
- main()
diff --git a/mailman/bin/list_lists.py b/mailman/bin/list_lists.py
deleted file mode 100644
index ea1640910..000000000
--- a/mailman/bin/list_lists.py
+++ /dev/null
@@ -1,104 +0,0 @@
-# Copyright (C) 1998-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 .
-
-from mailman.config import config
-from mailman.i18n import _
-from mailman.options import Options
-
-
-
-class ScriptOptions(Options):
- usage = _("""\
-%prog [options]
-
-List all mailing lists.""")
-
- def add_options(self):
- super(ScriptOptions, self).add_options()
- self.parser.add_option(
- '-a', '--advertised',
- default=False, action='store_true',
- help=_("""\
-List only those mailing lists that are publicly advertised"""))
- self.parser.add_option(
- '-b', '--bare',
- default=False, action='store_true',
- help=_("""\
-Displays only the list name, with no description."""))
- self.parser.add_option(
- '-d', '--domain',
- default=[], type='string', action='append',
- dest='domains', help=_("""\
-List only those mailing lists that match the given virtual domain, which may
-be either the email host or the url host name. Multiple -d options may be
-given."""))
- self.parser.add_option(
- '-f', '--full',
- default=False, action='store_true',
- help=_("""\
-Print the full list name, including the posting address."""))
-
- def sanity_check(self):
- if len(self.arguments) > 0:
- self.parser.error(_('Unexpected arguments'))
-
-
-
-def main():
- options = ScriptOptions()
- options.initialize()
-
- mlists = []
- longest = 0
-
- listmgr = config.db.list_manager
- for fqdn_name in sorted(listmgr.names):
- mlist = listmgr.get(fqdn_name)
- if options.options.advertised and not mlist.advertised:
- continue
- if options.options.domains:
- for domain in options.options.domains:
- if domain in mlist.web_page_url or domain == mlist.host_name:
- mlists.append(mlist)
- break
- else:
- mlists.append(mlist)
- if options.options.full:
- name = mlist.fqdn_listname
- else:
- name = mlist.real_name
- longest = max(len(name), longest)
-
- if not mlists and not options.options.bare:
- print _('No matching mailing lists found')
- return
-
- if not options.options.bare:
- num_mlists = len(mlists)
- print _('$num_mlists matching mailing lists found:')
-
- format = '%%%ds - %%.%ds' % (longest, 77 - longest)
- for mlist in mlists:
- if options.options.full:
- name = mlist.fqdn_listname
- else:
- name = mlist.real_name
- if options.options.bare:
- print name
- else:
- description = mlist.description or _('[no description available]')
- print ' ', format % (name, description)
diff --git a/mailman/bin/list_members.py b/mailman/bin/list_members.py
deleted file mode 100644
index 443f764d6..000000000
--- a/mailman/bin/list_members.py
+++ /dev/null
@@ -1,201 +0,0 @@
-# Copyright (C) 1998-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 .
-
-import sys
-
-from email.Utils import formataddr
-
-from mailman import Utils
-from mailman.config import config
-from mailman.core import errors
-from mailman.i18n import _
-from mailman.interfaces import DeliveryStatus
-from mailman.options import SingleMailingListOptions
-
-
-COMMASPACE = ', '
-
-WHYCHOICES = {
- 'enabled' : DeliveryStatus.enabled,
- 'byuser' : DeliveryStatus.by_user,
- 'byadmin' : DeliveryStatus.by_moderator,
- 'bybounce': DeliveryStatus.by_bounces,
- }
-
-KINDCHOICES = set(('mime', 'plain', 'any'))
-
-
-
-class ScriptOptions(SingleMailingListOptions):
- usage = _("""\
-%prog [options]
-
-List all the members of a mailing list. Note that with the options below, if
-neither -r or -d is supplied, regular members are printed first, followed by
-digest members, but no indication is given as to address status.
-
-listname is the name of the mailing list to use.""")
-
- def add_options(self):
- super(ScriptOptions, self).add_options()
- self.parser.add_option(
- '-o', '--output',
- type='string', help=_("""\
-Write output to specified file instead of standard out."""))
- self.parser.add_option(
- '-r', '--regular',
- default=None, action='store_true',
- help=_('Print just the regular (non-digest) members.'))
- self.parser.add_option(
- '-d', '--digest',
- default=None, type='string', metavar='KIND',
- help=_("""\
-Print just the digest members. KIND can be 'mime', 'plain', or
-'any'. 'mime' prints just the members receiving MIME digests, while 'plain'
-prints just the members receiving plain text digests. 'any' prints all
-members receiving any kind of digest."""))
- self.parser.add_option(
- '-n', '--nomail',
- type='string', metavar='WHY', help=_("""\
-Print the members that have delivery disabled. WHY selects just the subset of
-members with delivery disabled for a particular reason, where 'any' prints all
-disabled members. 'byadmin', 'byuser', 'bybounce', and 'unknown' prints just
-the users who are disabled for that particular reason. WHY can also be
-'enabled' which prints just those members for whom delivery is enabled."""))
- self.parser.add_option(
- '-f', '--fullnames',
- default=False, action='store_true',
- help=_('Include the full names in the output'))
- self.parser.add_option(
- '-i', '--invalid',
- default=False, action='store_true', help=_("""\
-Print only the addresses in the membership list that are invalid. Ignores -r,
--d, -n."""))
-
- def sanity_check(self):
- if not self.options.listname:
- self.parser.error(_('Missing listname'))
- if len(self.arguments) > 0:
- self.parser.print_error(_('Unexpected arguments'))
- if self.options.digest is not None:
- self.options.kind = self.options.digest.lower()
- if self.options.kind not in KINDCHOICES:
- self.parser.error(
- _('Invalid value for -d: $self.options.digest'))
- if self.options.nomail is not None:
- why = self.options.nomail.lower()
- if why == 'any':
- self.options.why = 'any'
- elif why not in WHYCHOICES:
- self.parser.error(
- _('Invalid value for -n: $self.options.nomail'))
- self.options.why = why
- if self.options.regular is None and self.options.digest is None:
- self.options.regular = self.options.digest = True
- self.options.kind = 'any'
-
-
-
-def safe(string):
- if not string:
- return ''
- return string.encode(sys.getdefaultencoding(), 'replace')
-
-
-def isinvalid(addr):
- try:
- Utils.ValidateEmail(addr)
- return False
- except errors.EmailAddressError:
- return True
-
-
-
-def whymatches(mlist, addr, why):
- # Return true if the `why' matches the reason the address is enabled, or
- # in the case of why is None, that they are disabled for any reason
- # (i.e. not enabled).
- status = mlist.getDeliveryStatus(addr)
- if why in (None, 'any'):
- return status <> DeliveryStatus.enabled
- return status == WHYCHOICES[why]
-
-
-
-def main():
- options = ScriptOptions()
- options.initialize()
-
- fqdn_listname = options.options.listname
- if options.options.output:
- try:
- fp = open(options.output, 'w')
- except IOError:
- options.parser.error(
- _('Could not open file for writing: $options.options.output'))
- else:
- fp = sys.stdout
-
- mlist = config.db.list_manager.get(fqdn_listname)
- if mlist is None:
- options.parser.error(_('No such list: $fqdn_listname'))
-
- # The regular delivery and digest members.
- rmembers = set(mlist.regular_members.members)
- dmembers = set(mlist.digest_members.members)
-
- fullnames = options.options.fullnames
- if options.options.invalid:
- all = sorted(member.address.address for member in rmembers + dmembers)
- for address in all:
- user = config.db.user_manager.get_user(address)
- name = (user.real_name if fullnames and user else u'')
- if options.options.invalid and isinvalid(address):
- print >> fp, formataddr((safe(name), address))
- return
- if options.options.regular:
- for address in sorted(member.address.address for member in rmembers):
- user = config.db.user_manager.get_user(address)
- name = (user.real_name if fullnames and user else u'')
- # Filter out nomails
- if (options.options.nomail and
- not whymatches(mlist, address, options.options.why)):
- continue
- print >> fp, formataddr((safe(name), address))
- if options.options.digest:
- for address in sorted(member.address.address for member in dmembers):
- user = config.db.user_manager.get_user(address)
- name = (user.real_name if fullnames and user else u'')
- # Filter out nomails
- if (options.options.nomail and
- not whymatches(mlist, address, options.options.why)):
- continue
- # Filter out digest kinds
-## if mlist.getMemberOption(addr, config.DisableMime):
-## # They're getting plain text digests
-## if opts.kind == 'mime':
-## continue
-## else:
-## # They're getting MIME digests
-## if opts.kind == 'plain':
-## continue
- print >> fp, formataddr((safe(name), address))
-
-
-
-if __name__ == '__main__':
- main()
diff --git a/mailman/bin/list_owners.py b/mailman/bin/list_owners.py
deleted file mode 100644
index 953fb8941..000000000
--- a/mailman/bin/list_owners.py
+++ /dev/null
@@ -1,88 +0,0 @@
-# Copyright (C) 2002-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 .
-
-import sys
-import optparse
-
-from mailman.MailList import MailList
-from mailman.configuration import config
-from mailman.i18n import _
-from mailman.initialize import initialize
-from mailman.version import MAILMAN_VERSION
-
-
-
-def parseargs():
- parser = optparse.OptionParser(version=MAILMAN_VERSION,
- usage=_("""\
-%prog [options] [listname ...]
-
-List the owners of a mailing list, or all mailing lists if no list names are
-given."""))
- parser.add_option('-w', '--with-listnames',
- default=False, action='store_true',
- help=_("""\
-Group the owners by list names and include the list names in the output.
-Otherwise, the owners will be sorted and uniquified based on the email
-address."""))
- parser.add_option('-m', '--moderators',
- default=False, action='store_true',
- help=_('Include the list moderators in the output.'))
- parser.add_option('-C', '--config',
- help=_('Alternative configuration file to use'))
- opts, args = parser.parse_args()
- return parser, opts, args
-
-
-
-def main():
- parser, opts, args = parseargs()
- initialize(opts.config)
-
- listmgr = config.db.list_manager
- listnames = set(args or listmgr.names)
- bylist = {}
-
- for listname in listnames:
- mlist = listmgr.get(listname)
- addrs = [addr.address for addr in mlist.owners.addresses]
- if opts.moderators:
- addrs.extend([addr.address for addr in mlist.moderators.addresses])
- bylist[listname] = addrs
-
- if opts.with_listnames:
- for listname in listnames:
- unique = set()
- for addr in bylist[listname]:
- unique.add(addr)
- keys = list(unique)
- keys.sort()
- print listname
- for k in keys:
- print '\t', k
- else:
- unique = set()
- for listname in listnames:
- for addr in bylist[listname]:
- unique.add(addr)
- for k in sorted(unique):
- print k
-
-
-
-if __name__ == '__main__':
- main()
diff --git a/mailman/bin/mailmanctl.py b/mailman/bin/mailmanctl.py
deleted file mode 100644
index 667a46a70..000000000
--- a/mailman/bin/mailmanctl.py
+++ /dev/null
@@ -1,232 +0,0 @@
-# Copyright (C) 2001-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 .
-
-"""Mailman start/stop script."""
-
-import os
-import grp
-import pwd
-import sys
-import errno
-import signal
-import logging
-
-from optparse import OptionParser
-
-from mailman.config import config
-from mailman.core.initialize import initialize
-from mailman.i18n import _
-from mailman.version import MAILMAN_VERSION
-
-
-COMMASPACE = ', '
-
-log = None
-parser = None
-
-
-
-def parseargs():
- parser = OptionParser(version=MAILMAN_VERSION,
- usage=_("""\
-Primary start-up and shutdown script for Mailman's qrunner daemon.
-
-This script starts, stops, and restarts the main Mailman queue runners, making
-sure that the various long-running qrunners are still alive and kicking. It
-does this by forking and exec'ing the qrunners and waiting on their pids.
-When it detects a subprocess has exited, it may restart it.
-
-The qrunners respond to SIGINT, SIGTERM, SIGUSR1 and SIGHUP. SIGINT, SIGTERM
-and SIGUSR1 all cause the qrunners to exit cleanly, but the master will only
-restart qrunners that have exited due to a SIGUSR1. SIGHUP causes the master
-and the qrunners to close their log files, and reopen then upon the next
-printed message.
-
-The master also responds to SIGINT, SIGTERM, SIGUSR1 and SIGHUP, which it
-simply passes on to the qrunners (note that the master will close and reopen
-its own log files on receipt of a SIGHUP). The master also leaves its own
-process id in the file data/master-qrunner.pid but you normally don't need to
-use this pid directly. The `start', `stop', `restart', and `reopen' commands
-handle everything for you.
-
-Commands:
-
- start - Start the master daemon and all qrunners. Prints a message and
- exits if the master daemon is already running.
-
- stop - Stops the master daemon and all qrunners. After stopping, no
- more messages will be processed.
-
- restart - Restarts the qrunners, but not the master process. Use this
- whenever you upgrade or update Mailman so that the qrunners will
- use the newly installed code.
-
- reopen - This will close all log files, causing them to be re-opened the
- next time a message is written to them
-
-Usage: %prog [options] [ start | stop | restart | reopen ]"""))
- parser.add_option('-u', '--run-as-user',
- default=True, action='store_false',
- help=_("""\
-Normally, this script will refuse to run if the user id and group id are not
-set to the `mailman' user and group (as defined when you configured Mailman).
-If run as root, this script will change to this user and group before the
-check is made.
-
-This can be inconvenient for testing and debugging purposes, so the -u flag
-means that the step that sets and checks the uid/gid is skipped, and the
-program is run as the current user and group. This flag is not recommended
-for normal production environments.
-
-Note though, that if you run with -u and are not in the mailman group, you may
-have permission problems, such as begin unable to delete a list's archives
-through the web. Tough luck!"""))
- parser.add_option('-f', '--force',
- default=False, action='store_true',
- help=_("""\
-If the master watcher finds an existing master lock, it will normally exit
-with an error message. With this option,the master will perform an extra
-level of checking. If a process matching the host/pid described in the lock
-file is running, the master will still exit, requiring you to manually clean
-up the lock. But if no matching process is found, the master will remove the
-apparently stale lock and make another attempt to claim the master lock."""))
- parser.add_option('-q', '--quiet',
- default=False, action='store_true',
- help=_("""\
-Don't print status messages. Error messages are still printed to standard
-error."""))
- parser.add_option('-C', '--config',
- help=_('Alternative configuration file to use'))
- options, arguments = parser.parse_args()
- if not arguments:
- parser.error(_('No command given.'))
- if len(arguments) > 1:
- commands = COMMASPACE.join(arguments)
- parser.error(_('Bad command: $commands'))
- parser.options = options
- parser.arguments = arguments
- return parser
-
-
-
-def kill_watcher(sig):
- try:
- with open(config.PIDFILE) as f:
- pid = int(f.read().strip())
- except (IOError, ValueError), e:
- # For i18n convenience
- print >> sys.stderr, _('PID unreadable in: $config.PIDFILE')
- print >> sys.stderr, e
- print >> sys.stderr, _('Is qrunner even running?')
- return
- try:
- os.kill(pid, sig)
- except OSError, error:
- if e.errno <> errno.ESRCH:
- raise
- print >> sys.stderr, _('No child with pid: $pid')
- print >> sys.stderr, e
- print >> sys.stderr, _('Stale pid file removed.')
- os.unlink(config.PIDFILE)
-
-
-
-def check_privileges():
- # If we're running as root (uid == 0), coerce the uid and gid to that
- # which Mailman was configured for, and refuse to run if we didn't coerce
- # the uid/gid.
- gid = grp.getgrnam(config.MAILMAN_GROUP).gr_gid
- uid = pwd.getpwnam(config.MAILMAN_USER).pw_uid
- myuid = os.getuid()
- if myuid == 0:
- # Set the process's supplimental groups.
- groups = [group.gr_gid for group in grp.getgrall()
- if config.MAILMAN_USER in group.gr_mem]
- groups.append(gid)
- os.setgroups(groups)
- os.setgid(gid)
- os.setuid(uid)
- elif myuid <> uid:
- name = config.MAILMAN_USER
- parser.error(
- _('Run this program as root or as the $name user, or use -u.'))
-
-
-
-def main():
- global log, parser
-
- parser = parseargs()
- initialize(parser.options.config)
-
- log = logging.getLogger('mailman.qrunner')
-
- if not parser.options.run_as_user:
- check_privileges()
- else:
- if not parser.options.quiet:
- print _('Warning! You may encounter permission problems.')
-
- # Handle the commands
- command = parser.arguments[0].lower()
- if command == 'stop':
- if not parser.options.quiet:
- print _("Shutting down Mailman's master qrunner")
- kill_watcher(signal.SIGTERM)
- elif command == 'restart':
- if not parser.options.quiet:
- print _("Restarting Mailman's master qrunner")
- kill_watcher(signal.SIGUSR1)
- elif command == 'reopen':
- if not parser.options.quiet:
- print _('Re-opening all log files')
- kill_watcher(signal.SIGHUP)
- elif command == 'start':
- # Start the master qrunner watcher process.
- #
- # Daemon process startup according to Stevens, Advanced Programming in
- # the UNIX Environment, Chapter 13.
- pid = os.fork()
- if pid:
- # parent
- if not parser.options.quiet:
- print _("Starting Mailman's master qrunner.")
- return
- # child
- #
- # Create a new session and become the session leader, but since we
- # won't be opening any terminal devices, don't do the ultra-paranoid
- # suggestion of doing a second fork after the setsid() call.
- os.setsid()
- # Instead of cd'ing to root, cd to the Mailman runtime directory.
- os.chdir(config.VAR_DIR)
- # Exec the master watcher.
- args = [sys.executable, sys.executable,
- os.path.join(config.BIN_DIR, 'master')]
- if parser.options.force:
- args.append('--force')
- if parser.options.config:
- args.extend(['-C', parser.options.config])
- log.debug('starting: %s', args)
- os.execl(*args)
- # We should never get here.
- raise RuntimeError('os.execl() failed')
-
-
-
-if __name__ == '__main__':
- main()
diff --git a/mailman/bin/master.py b/mailman/bin/master.py
deleted file mode 100644
index d954bc865..000000000
--- a/mailman/bin/master.py
+++ /dev/null
@@ -1,452 +0,0 @@
-# Copyright (C) 2001-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 .
-
-"""Master sub-process watcher."""
-
-__metaclass__ = type
-__all__ = [
- 'Loop',
- 'get_lock_data',
- ]
-
-
-import os
-import sys
-import errno
-import signal
-import socket
-import logging
-
-from datetime import timedelta
-from lazr.config import as_boolean
-from locknix import lockfile
-from munepy import Enum
-
-from mailman.config import config
-from mailman.core.logging import reopen
-from mailman.i18n import _
-from mailman.options import Options
-
-
-DOT = '.'
-LOCK_LIFETIME = timedelta(days=1, hours=6)
-SECONDS_IN_A_DAY = 86400
-
-
-
-class ScriptOptions(Options):
- """Options for the master watcher."""
-
- usage = _("""\
-Master sub-process watcher.
-
-Start and watch the configured queue runners and ensure that they stay alive
-and kicking. Each are fork and exec'd in turn, with the master waiting on
-their process ids. When it detects a child queue runner has exited, it may
-restart it.
-
-The queue runners respond to SIGINT, SIGTERM, SIGUSR1 and SIGHUP. SIGINT,
-SIGTERM and SIGUSR1 all cause the qrunners to exit cleanly. The master will
-restart qrunners that have exited due to a SIGUSR1 or some kind of other exit
-condition (say because of an exception). SIGHUP causes the master and the
-qrunners to close their log files, and reopen then upon the next printed
-message.
-
-The master also responds to SIGINT, SIGTERM, SIGUSR1 and SIGHUP, which it
-simply passes on to the qrunners. Note that the master will close and reopen
-its own log files on receipt of a SIGHUP. The master also leaves its own
-process id in the file `data/master-qrunner.pid` but you normally don't need
-to use this pid directly.
-
-Usage: %prog [options]""")
-
- def add_options(self):
- self.parser.add_option(
- '-n', '--no-restart',
- dest='restartable', default=True, action='store_false',
- help=_("""\
-Don't restart the qrunners when they exit because of an error or a SIGUSR1.
-Use this only for debugging."""))
- self.parser.add_option(
- '-f', '--force',
- default=False, action='store_true',
- help=_("""\
-If the master watcher finds an existing master lock, it will normally exit
-with an error message. With this option,the master will perform an extra
-level of checking. If a process matching the host/pid described in the lock
-file is running, the master will still exit, requiring you to manually clean
-up the lock. But if no matching process is found, the master will remove the
-apparently stale lock and make another attempt to claim the master lock."""))
- self.parser.add_option(
- '-r', '--runner',
- dest='runners', action='append', default=[],
- help=_("""\
-Override the default set of queue runners that the master watch will invoke
-instead of the default set. Multiple -r options may be given. The values for
--r are passed straight through to bin/qrunner."""))
-
- def sanity_check(self):
- if len(self.arguments) > 0:
- self.parser.error(_('Too many arguments'))
-
-
-
-def get_lock_data():
- """Get information from the master lock file.
-
- :return: A 3-tuple of the hostname, integer process id, and file name of
- the lock file.
- """
- with open(config.LOCK_FILE) as fp:
- filename = os.path.split(fp.read().strip())[1]
- parts = filename.split('.')
- hostname = DOT.join(parts[1:-2])
- pid = int(parts[-2])
- return hostname, int(pid), filename
-
-
-class WatcherState(Enum):
- # Another master watcher is running.
- conflict = 1
- # No conflicting process exists.
- stale_lock = 2
- # Hostname from lock file doesn't match.
- host_mismatch = 3
-
-
-def master_state():
- """Get the state of the master watcher.
-
- :return: WatcherState describing the state of the lock file.
- """
-
- # 1 if proc exists on host (but is it qrunner? ;)
- # 0 if host matches but no proc
- # hostname if hostname doesn't match
- hostname, pid, tempfile = get_lock_data()
- if hostname <> socket.gethostname():
- return WatcherState.host_mismatch
- # Find out if the process exists by calling kill with a signal 0.
- try:
- os.kill(pid, 0)
- return WatcherState.conflict
- except OSError, e:
- if e.errno == errno.ESRCH:
- # No matching process id.
- return WatcherState.stale_lock
- # Some other error occurred.
- raise
-
-
-def acquire_lock_1(force):
- """Try to acquire the master queue runner lock.
-
- :param force: Flag that controls whether to force acquisition of the lock.
- :return: The master queue runner lock.
- :raises: `TimeOutError` if the lock could not be acquired.
- """
- lock = lockfile.Lock(config.LOCK_FILE, LOCK_LIFETIME)
- try:
- lock.lock(timedelta(seconds=0.1))
- return lock
- except lockfile.TimeOutError:
- if not force:
- raise
- # Force removal of lock first.
- lock.disown()
- hostname, pid, tempfile = get_lock_data()
- os.unlink(config.LOCK_FILE)
- os.unlink(os.path.join(config.LOCK_DIR, tempfile))
- return acquire_lock_1(force=False)
-
-
-def acquire_lock(force):
- """Acquire the master queue runner lock.
-
- :return: The master queue runner lock or None if the lock couldn't be
- acquired. In that case, an error messages is also printed to standard
- error.
- """
- try:
- lock = acquire_lock_1(force)
- return lock
- except lockfile.TimeOutError:
- status = master_state()
- if status == WatcherState.conflict:
- # Hostname matches and process exists.
- message = _("""\
-The master qrunner lock could not be acquired because it appears
-as though another master qrunner is already running.
-""")
- elif status == WatcherState.stale_lock:
- # Hostname matches but the process does not exist.
- message = _("""\
-The master qrunner lock could not be acquired. It appears as though there is
-a stale master qrunner lock. Try re-running mailmanctl with the -s flag.
-""")
- else:
- assert status == WatcherState.host_mismatch, (
- 'Invalid enum value: %s' % status)
- # Hostname doesn't even match.
- hostname, pid, tempfile = get_lock_data()
- message = _("""\
-The master qrunner lock could not be acquired, because it appears as if some
-process on some other host may have acquired it. We can't test for stale
-locks across host boundaries, so you'll have to clean this up manually.
-
-Lock file: $config.LOCK_FILE
-Lock host: $hostname
-
-Exiting.""")
- config.options.parser.error(message)
-
-
-
-class Loop:
- """Main control loop class."""
-
- def __init__(self, lock=None, restartable=None, config_file=None):
- self._lock = lock
- self._restartable = restartable
- self._config_file = config_file
- self._kids = {}
-
- def install_signal_handlers(self):
- """Install various signals handlers for control from mailmanctl."""
- log = logging.getLogger('mailman.qrunner')
- # Set up our signal handlers. Also set up a SIGALRM handler to
- # refresh the lock once per day. The lock lifetime is 1 day + 6 hours
- # so this should be plenty.
- def sigalrm_handler(signum, frame):
- self._lock.refresh()
- signal.alarm(SECONDS_IN_A_DAY)
- signal.signal(signal.SIGALRM, sigalrm_handler)
- signal.alarm(SECONDS_IN_A_DAY)
- # SIGHUP tells the qrunners to close and reopen their log files.
- def sighup_handler(signum, frame):
- reopen()
- for pid in self._kids:
- os.kill(pid, signal.SIGHUP)
- log.info('Master watcher caught SIGHUP. Re-opening log files.')
- signal.signal(signal.SIGHUP, sighup_handler)
- # SIGUSR1 is used by 'mailman restart'.
- def sigusr1_handler(signum, frame):
- for pid in self._kids:
- os.kill(pid, signal.SIGUSR1)
- log.info('Master watcher caught SIGUSR1. Exiting.')
- signal.signal(signal.SIGUSR1, sigusr1_handler)
- # SIGTERM is what init will kill this process with when changing run
- # levels. It's also the signal 'mailmanctl stop' uses.
- def sigterm_handler(signum, frame):
- for pid in self._kids:
- os.kill(pid, signal.SIGTERM)
- log.info('Master watcher caught SIGTERM. Exiting.')
- signal.signal(signal.SIGTERM, sigterm_handler)
- # SIGINT is what control-C gives.
- def sigint_handler(signum, frame):
- for pid in self._kids:
- os.kill(pid, signal.SIGINT)
- log.info('Master watcher caught SIGINT. Restarting.')
- signal.signal(signal.SIGINT, sigint_handler)
-
- def _start_runner(self, spec):
- """Start a queue runner.
-
- All arguments are passed to the qrunner process.
-
- :param spec: A queue runner spec, in a format acceptable to
- bin/qrunner's --runner argument, e.g. name:slice:count
- :type spec: string
- :return: The process id of the child queue runner.
- :rtype: int
- """
- pid = os.fork()
- if pid:
- # Parent.
- return pid
- # Child.
- #
- # Craft the command line arguments for the exec() call.
- rswitch = '--runner=' + spec
- # Wherever mailmanctl lives, so too must live the qrunner script.
- exe = os.path.join(config.BIN_DIR, 'qrunner')
- # config.PYTHON, which is the absolute path to the Python interpreter,
- # must be given as argv[0] due to Python's library search algorithm.
- args = [sys.executable, sys.executable, exe, rswitch, '-s']
- if self._config_file is not None:
- args.extend(['-C', self._config_file])
- log = logging.getLogger('mailman.qrunner')
- log.debug('starting: %s', args)
- os.execl(*args)
- # We should never get here.
- raise RuntimeError('os.execl() failed')
-
- def start_qrunners(self, qrunner_names=None):
- """Start all the configured qrunners.
-
- :param qrunners: If given, a sequence of queue runner names to start.
- If not given, this sequence is taken from the configuration file.
- :type qrunners: a sequence of strings
- """
- if not qrunner_names:
- qrunner_names = []
- for qrunner_config in config.qrunner_configs:
- # Strip off the 'qrunner.' prefix.
- assert qrunner_config.name.startswith('qrunner.'), (
- 'Unexpected qrunner configuration section name: %s',
- qrunner_config.name)
- qrunner_names.append(qrunner_config.name[8:])
- # For each qrunner we want to start, find their config section, which
- # will tell us the name of the class to instantiate, along with the
- # number of hash space slices to manage.
- for name in qrunner_names:
- section_name = 'qrunner.' + name
- # Let AttributeError propagate.
- qrunner_config = getattr(config, section_name)
- if not as_boolean(qrunner_config.start):
- continue
- package, class_name = qrunner_config['class'].rsplit(DOT, 1)
- __import__(package)
- # Let AttributeError propagate.
- class_ = getattr(sys.modules[package], class_name)
- # Find out how many qrunners to instantiate. This must be a power
- # of 2.
- count = int(qrunner_config.instances)
- assert (count & (count - 1)) == 0, (
- 'Queue runner "%s", not a power of 2: %s', name, count)
- for slice_number in range(count):
- # qrunner name, slice #, # of slices, restart count
- info = (name, slice_number, count, 0)
- spec = '%s:%d:%d' % (name, slice_number, count)
- pid = self._start_runner(spec)
- log = logging.getLogger('mailman.qrunner')
- log.debug('[%d] %s', pid, spec)
- self._kids[pid] = info
-
- def loop(self):
- """Main loop.
-
- Wait until all the qrunners have exited, restarting them if necessary
- and configured to do so.
- """
- log = logging.getLogger('mailman.qrunner')
- while True:
- try:
- pid, status = os.wait()
- except OSError, error:
- # No children? We're done.
- if error.errno == errno.ECHILD:
- break
- # If the system call got interrupted, just restart it.
- elif error.errno == errno.EINTR:
- continue
- else:
- raise
- # Find out why the subprocess exited by getting the signal
- # received or exit status.
- if os.WIFSIGNALED(status):
- why = os.WTERMSIG(status)
- elif os.WIFEXITED(status):
- why = os.WEXITSTATUS(status)
- else:
- why = None
- # We'll restart the subprocess if it exited with a SIGUSR1 or
- # because of a failure (i.e. no exit signal), and the no-restart
- # command line switch was not given. This lets us better handle
- # runaway restarts (e.g. if the subprocess had a syntax error!)
- qrname, slice_number, count, restarts = self._kids.pop(pid)
- config_name = 'qrunner.' + qrname
- restart = False
- if why == signal.SIGUSR1 and self._restartable:
- restart = True
- # Have we hit the maximum number of restarts?
- restarts += 1
- max_restarts = int(getattr(config, config_name).max_restarts)
- if restarts > max_restarts:
- restart = False
- # Are we permanently non-restartable?
- log.debug("""\
-Master detected subprocess exit
-(pid: %d, why: %s, class: %s, slice: %d/%d) %s""",
- pid, why, qrname, slice_number + 1, count,
- ('[restarting]' if restart else ''))
- # See if we've reached the maximum number of allowable restarts
- if restarts > max_restarts:
- log.info("""\
-qrunner %s reached maximum restart limit of %d, not restarting.""",
- qrname, max_restarts)
- # Now perhaps restart the process unless it exited with a
- # SIGTERM or we aren't restarting.
- if restart:
- spec = '%s:%d:%d' % (qrname, slice_number, count)
- newpid = self._start_runner(spec)
- self._kids[newpid] = (qrname, slice_number, count, restarts)
-
- def cleanup(self):
- """Ensure that all children have exited."""
- log = logging.getLogger('mailman.qrunner')
- # Send SIGTERMs to all the child processes and wait for them all to
- # exit.
- for pid in self._kids:
- try:
- os.kill(pid, signal.SIGTERM)
- except OSError, error:
- if error.errno == errno.ESRCH:
- # The child has already exited.
- log.info('ESRCH on pid: %d', pid)
- # Wait for all the children to go away.
- while self._kids:
- try:
- pid, status = os.wait()
- del self._kids[pid]
- except OSError, e:
- if e.errno == errno.ECHILD:
- break
- elif e.errno == errno.EINTR:
- continue
- raise
-
-
-
-def main():
- """Main process."""
-
- options = ScriptOptions()
- options.initialize()
-
- # Acquire the master lock, exiting if we can't acquire it. We'll let the
- # caller handle any clean up or lock breaking. No with statement here
- # because Lock's constructor doesn't support a timeout.
- lock = acquire_lock(options.options.force)
- try:
- with open(config.PIDFILE, 'w') as fp:
- print >> fp, os.getpid()
- loop = Loop(lock, options.options.restartable, options.options.config)
- loop.install_signal_handlers()
- try:
- loop.start_qrunners(options.options.runners)
- loop.loop()
- finally:
- loop.cleanup()
- os.remove(config.PIDFILE)
- finally:
- lock.unlock()
-
-
-
-if __name__ == '__main__':
- main()
diff --git a/mailman/bin/mmsitepass.py b/mailman/bin/mmsitepass.py
deleted file mode 100644
index 132803fc9..000000000
--- a/mailman/bin/mmsitepass.py
+++ /dev/null
@@ -1,113 +0,0 @@
-# Copyright (C) 1998-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 .
-
-import sys
-import getpass
-import optparse
-
-from mailman import Utils
-from mailman import passwords
-from mailman.configuration import config
-from mailman.i18n import _
-from mailman.initialize import initialize
-from mailman.version import MAILMAN_VERSION
-
-
-
-def parseargs():
- parser = optparse.OptionParser(version=MAILMAN_VERSION,
- usage=_("""\
-%prog [options] [password]
-
-Set the site or list creator password.
-
-The site password can be used in most if not all places that the list
-administrator's password can be used, which in turn can be used in most places
-that a list user's password can be used. The list creator password is a
-separate password that can be given to non-site administrators to delegate the
-ability to create new mailing lists.
-
-If password is not given on the command line, it will be prompted for.
-"""))
- parser.add_option('-c', '--listcreator',
- default=False, action='store_true',
- help=_("""\
-Set the list creator password instead of the site password. The list
-creator is authorized to create and remove lists, but does not have
-the total power of the site administrator."""))
- parser.add_option('-p', '--password-scheme',
- default='', type='string',
- help=_("""\
-Specify the RFC 2307 style hashing scheme for passwords included in the
-output. Use -P to get a list of supported schemes, which are
-case-insensitive."""))
- parser.add_option('-P', '--list-hash-schemes',
- default=False, action='store_true', help=_("""\
-List the supported password hashing schemes and exit. The scheme labels are
-case-insensitive."""))
- parser.add_option('-C', '--config',
- help=_('Alternative configuration file to use'))
- opts, args = parser.parse_args()
- if len(args) > 1:
- parser.error(_('Unexpected arguments'))
- if opts.list_hash_schemes:
- for label in passwords.Schemes:
- print str(label).upper()
- sys.exit(0)
- return parser, opts, args
-
-
-def check_password_scheme(parser, password_scheme):
- # shoule be checked after config is loaded.
- if password_scheme == '':
- password_scheme = config.PASSWORD_SCHEME
- scheme = passwords.lookup_scheme(password_scheme.lower())
- if not scheme:
- parser.error(_('Invalid password scheme'))
- return scheme
-
-
-
-def main():
- parser, opts, args = parseargs()
- initialize(opts.config)
- opts.password_scheme = check_password_scheme(parser, opts.password_scheme)
- if args:
- password = args[0]
- else:
- # Prompt for the password
- if opts.listcreator:
- prompt_1 = _('New list creator password: ')
- else:
- prompt_1 = _('New site administrator password: ')
- pw1 = getpass.getpass(prompt_1)
- pw2 = getpass.getpass(_('Enter password again to confirm: '))
- if pw1 <> pw2:
- print _('Passwords do not match; no changes made.')
- sys.exit(1)
- password = pw1
- Utils.set_global_password(password,
- not opts.listcreator, opts.password_scheme)
- if Utils.check_global_password(password, not opts.listcreator):
- print _('Password changed.')
- else:
- print _('Password change failed.')
-
-
-
-if __name__ == '__main__':
- main()
diff --git a/mailman/bin/nightly_gzip.py b/mailman/bin/nightly_gzip.py
deleted file mode 100644
index f886e5801..000000000
--- a/mailman/bin/nightly_gzip.py
+++ /dev/null
@@ -1,117 +0,0 @@
-# Copyright (C) 1998-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 .
-
-import os
-import sys
-import optparse
-
-try:
- import gzip
-except ImportError:
- sys.exit(0)
-
-from mailman import MailList
-from mailman.configuration import config
-from mailman.i18n import _
-from mailman.initialize import initialize
-from mailman.version import MAILMAN_VERSION
-
-
-
-def parseargs():
- parser = optparse.OptionParser(version=MAILMAN_VERSION,
- usage=_("""\
-%prog [options] [listname ...]
-
-Re-generate the Pipermail gzip'd archive flat files."""))
- parser.add_option('-v', '--verbose',
- default=False, action='store_true',
- help=_("Print each file as it's being gzip'd"))
- parser.add_option('-z', '--level',
- default=6, type='int',
- help=_('Specifies the compression level'))
- parser.add_option('-C', '--config',
- help=_('Alternative configuration file to use'))
- opts, args = parser.parse_args()
- if opts.level < 1 or opts.level > 9:
- parser.print_help()
- print >> sys.stderr, _('Illegal compression level: $opts.level')
- sys.exit(1)
- return opts, args, parser
-
-
-
-def compress(txtfile, opts):
- if opts.verbose:
- print _("gzip'ing: $txtfile")
- infp = outfp = None
- try:
- infp = open(txtfile)
- outfp = gzip.open(txtfile + '.gz', 'wb', opts.level)
- outfp.write(infp.read())
- finally:
- if outfp:
- outfp.close()
- if infp:
- infp.close()
-
-
-
-def main():
- opts, args, parser = parseargs()
- initialize(opts.config)
-
- if config.ARCHIVE_TO_MBOX not in (1, 2) or config.GZIP_ARCHIVE_TXT_FILES:
- # We're only going to run the nightly archiver if messages are
- # archived to the mbox, and the gzip file is not created on demand
- # (i.e. for every individual post). This is the normal mode of
- # operation.
- return
-
- # Process all the specified lists
- for listname in set(args or config.list_manager.names):
- mlist = MailList.MailList(listname, lock=False)
- if not mlist.archive:
- continue
- dir = mlist.archive_dir()
- try:
- allfiles = os.listdir(dir)
- except OSError:
- # Has the list received any messages? If not, last_post_time will
- # be zero, so it's not really a bogus archive dir.
- if mlist.last_post_time > 0:
- print _('List $listname has a bogus archive_directory: $dir')
- continue
- if opts.verbose:
- print _('Processing list: $listname')
- files = []
- for f in allfiles:
- if os.path.splitext(f)[1] <> '.txt':
- continue
- # stat both the .txt and .txt.gz files and append them only if
- # the former is newer than the latter.
- txtfile = os.path.join(dir, f)
- gzpfile = txtfile + '.gz'
- txt_mtime = os.path.getmtime(txtfile)
- try:
- gzp_mtime = os.path.getmtime(gzpfile)
- except OSError:
- gzp_mtime = -1
- if txt_mtime > gzp_mtime:
- files.append(txtfile)
- for f in files:
- compress(f, opts)
diff --git a/mailman/bin/qrunner.py b/mailman/bin/qrunner.py
deleted file mode 100644
index 62e943aad..000000000
--- a/mailman/bin/qrunner.py
+++ /dev/null
@@ -1,269 +0,0 @@
-# Copyright (C) 2001-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 .
-
-import sys
-import signal
-import logging
-
-from mailman.config import config
-from mailman.core.logging import reopen
-from mailman.i18n import _
-from mailman.options import Options
-
-
-COMMASPACE = ', '
-log = None
-
-
-
-def r_callback(option, opt, value, parser):
- dest = getattr(parser.values, option.dest)
- parts = value.split(':')
- if len(parts) == 1:
- runner = parts[0]
- rslice = rrange = 1
- elif len(parts) == 3:
- runner = parts[0]
- try:
- rslice = int(parts[1])
- rrange = int(parts[2])
- except ValueError:
- parser.print_help()
- print >> sys.stderr, _('Bad runner specification: $value')
- sys.exit(1)
- else:
- parser.print_help()
- print >> sys.stderr, _('Bad runner specification: $value')
- sys.exit(1)
- dest.append((runner, rslice, rrange))
-
-
-
-class ScriptOptions(Options):
-
- usage = _("""\
-Run one or more qrunners, once or repeatedly.
-
-Each named runner class is run in round-robin fashion. In other words, the
-first named runner is run to consume all the files currently in its
-directory. When that qrunner is done, the next one is run to consume all the
-files in /its/ directory, and so on. The number of total iterations can be
-given on the command line.
-
-Usage: %prog [options]
-
--r is required unless -l or -h is given, and its argument must be one of the
-names displayed by the -l switch.
-
-Normally, this script should be started from mailmanctl. Running it
-separately or with -o is generally useful only for debugging.
-""")
-
- def add_options(self):
- self.parser.add_option(
- '-r', '--runner',
- metavar='runner[:slice:range]', dest='runners',
- type='string', default=[],
- action='callback', callback=r_callback,
- help=_("""\
-Run the named qrunner, which must be one of the strings returned by the -l
-option. Optional slice:range if given, is used to assign multiple qrunner
-processes to a queue. range is the total number of qrunners for this queue
-while slice is the number of this qrunner from [0..range).
-
-When using the slice:range form, you must ensure that each qrunner for the
-queue is given the same range value. If slice:runner is not given, then 1:1
-is used.
-
-Multiple -r options may be given, in which case each qrunner will run once in
-round-robin fashion. The special runner `All' is shorthand for a qrunner for
-each listed by the -l option."""))
- self.parser.add_option(
- '-o', '--once',
- default=False, action='store_true', help=_("""\
-Run each named qrunner exactly once through its main loop. Otherwise, each
-qrunner runs indefinitely, until the process receives signal."""))
- self.parser.add_option(
- '-l', '--list',
- default=False, action='store_true',
- help=_('List the available qrunner names and exit.'))
- self.parser.add_option(
- '-v', '--verbose',
- default=0, action='count', help=_("""\
-Display more debugging information to the logs/qrunner log file."""))
- self.parser.add_option(
- '-s', '--subproc',
- default=False, action='store_true', help=_("""\
-This should only be used when running qrunner as a subprocess of the
-mailmanctl startup script. It changes some of the exit-on-error behavior to
-work better with that framework."""))
-
- def sanity_check(self):
- if self.arguments:
- self.parser.error(_('Unexpected arguments'))
- if not self.options.runners and not self.options.list:
- self.parser.error(_('No runner name given.'))
-
-
-
-def make_qrunner(name, slice, range, once=False):
- # Several conventions for specifying the runner name are supported. It
- # could be one of the shortcut names. If the name is a full module path,
- # use it explicitly. If the name starts with a dot, it's a class name
- # relative to the Mailman.queue package.
- qrunner_config = getattr(config, 'qrunner.' + name, None)
- if qrunner_config is not None:
- # It was a shortcut name.
- class_path = qrunner_config['class']
- elif name.startswith('.'):
- class_path = 'mailman.queue' + name
- else:
- class_path = name
- module_name, class_name = class_path.rsplit('.', 1)
- try:
- __import__(module_name)
- except ImportError, e:
- if config.options.options.subproc:
- # Exit with SIGTERM exit code so the master watcher won't try to
- # restart us.
- print >> sys.stderr, _('Cannot import runner module: $module_name')
- print >> sys.stderr, e
- sys.exit(signal.SIGTERM)
- else:
- raise
- qrclass = getattr(sys.modules[module_name], class_name)
- if once:
- # Subclass to hack in the setting of the stop flag in _do_periodic()
- class Once(qrclass):
- def _do_periodic(self):
- self.stop()
- qrunner = Once(name, slice)
- else:
- qrunner = qrclass(name, slice)
- return qrunner
-
-
-
-def set_signals(loop):
- """Set up the signal handlers.
-
- Signals caught are: SIGTERM, SIGINT, SIGUSR1 and SIGHUP. The latter is
- used to re-open the log files. SIGTERM and SIGINT are treated exactly the
- same -- they cause qrunner to exit with no restart from the master.
- SIGUSR1 also causes qrunner to exit, but the master watcher will restart
- it in that case.
-
- :param loop: A loop queue runner instance.
- """
- def sigterm_handler(signum, frame):
- # Exit the qrunner cleanly
- loop.stop()
- loop.status = signal.SIGTERM
- log.info('%s qrunner caught SIGTERM. Stopping.', loop.name())
- signal.signal(signal.SIGTERM, sigterm_handler)
- def sigint_handler(signum, frame):
- # Exit the qrunner cleanly
- loop.stop()
- loop.status = signal.SIGINT
- log.info('%s qrunner caught SIGINT. Stopping.', loop.name())
- signal.signal(signal.SIGINT, sigint_handler)
- def sigusr1_handler(signum, frame):
- # Exit the qrunner cleanly
- loop.stop()
- loop.status = signal.SIGUSR1
- log.info('%s qrunner caught SIGUSR1. Stopping.', loop.name())
- signal.signal(signal.SIGUSR1, sigusr1_handler)
- # SIGHUP just tells us to rotate our log files.
- def sighup_handler(signum, frame):
- reopen()
- log.info('%s qrunner caught SIGHUP. Reopening logs.', loop.name())
- signal.signal(signal.SIGHUP, sighup_handler)
-
-
-
-def main():
- global log
-
- options = ScriptOptions()
- options.initialize()
-
- if options.options.list:
- prefixlen = max(len(shortname)
- for shortname in config.qrunner_shortcuts)
- for shortname in sorted(config.qrunner_shortcuts):
- runnername = config.qrunner_shortcuts[shortname]
- shortname = (' ' * (prefixlen - len(shortname))) + shortname
- print _('$shortname runs $runnername')
- sys.exit(0)
-
- # Fast track for one infinite runner
- if len(options.options.runners) == 1 and not options.options.once:
- qrunner = make_qrunner(*options.options.runners[0])
- class Loop:
- status = 0
- def __init__(self, qrunner):
- self._qrunner = qrunner
- def name(self):
- return self._qrunner.__class__.__name__
- def stop(self):
- self._qrunner.stop()
- loop = Loop(qrunner)
- set_signals(loop)
- # Now start up the main loop
- log = logging.getLogger('mailman.qrunner')
- log.info('%s qrunner started.', loop.name())
- qrunner.run()
- log.info('%s qrunner exiting.', loop.name())
- else:
- # Anything else we have to handle a bit more specially
- qrunners = []
- for runner, rslice, rrange in options.options.runners:
- qrunner = make_qrunner(runner, rslice, rrange, once=True)
- qrunners.append(qrunner)
- # This class is used to manage the main loop
- class Loop:
- status = 0
- def __init__(self):
- self._isdone = False
- def name(self):
- return 'Main loop'
- def stop(self):
- self._isdone = True
- def isdone(self):
- return self._isdone
- loop = Loop()
- set_signals(loop)
- log.info('Main qrunner loop started.')
- while not loop.isdone():
- for qrunner in qrunners:
- # In case the SIGTERM came in the middle of this iteration
- if loop.isdone():
- break
- if options.options.verbose:
- log.info('Now doing a %s qrunner iteration',
- qrunner.__class__.__bases__[0].__name__)
- qrunner.run()
- if options.options.once:
- break
- log.info('Main qrunner loop exiting.')
- # All done
- sys.exit(loop.status)
-
-
-
-if __name__ == '__main__':
- main()
diff --git a/mailman/bin/remove_list.py b/mailman/bin/remove_list.py
deleted file mode 100644
index 05211b200..000000000
--- a/mailman/bin/remove_list.py
+++ /dev/null
@@ -1,83 +0,0 @@
-# Copyright (C) 1998-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 .
-
-import sys
-
-from mailman.app.lifecycle import remove_list
-from mailman.config import config
-from mailman.i18n import _
-from mailman.options import MultipleMailingListOptions
-
-
-
-class ScriptOptions(MultipleMailingListOptions):
- usage = _("""\
-%prog [options]
-
-Remove the components of a mailing list with impunity - beware!
-
-This removes (almost) all traces of a mailing list. By default, the lists
-archives are not removed, which is very handy for retiring old lists.
-""")
-
- def add_options(self):
- super(ScriptOptions, self).add_options()
- self.parser.add_option(
- '-a', '--archives',
- default=False, action='store_true',
- help=_("""\
-Remove the list's archives too, or if the list has already been deleted,
-remove any residual archives."""))
- self.parser.add_option(
- '-q', '--quiet',
- default=False, action='store_true',
- help=_('Suppress status messages'))
-
- def sanity_check(self):
- if len(self.options.listnames) == 0:
- self.parser.error(_('Nothing to do'))
- if len(self.arguments) > 0:
- self.parser.error(_('Unexpected arguments'))
-
-
-
-def main():
- options = ScriptOptions()
- options.initialize()
-
- for fqdn_listname in options.options.listnames:
- if not options.options.quiet:
- print _('Removing list: $fqdn_listname')
- mlist = config.db.list_manager.get(fqdn_listname)
- if mlist is None:
- if options.options.archives:
- print _("""\
-No such list: ${fqdn_listname}. Removing its residual archives.""")
- else:
- print >> sys.stderr, _(
- 'No such list (or list already deleted): $fqdn_listname')
-
- if not options.options.archives:
- print _('Not removing archives. Reinvoke with -a to remove them.')
-
- remove_list(fqdn_listname, mlist, options.options.archives)
- config.db.commit()
-
-
-
-if __name__ == '__main__':
- main()
diff --git a/mailman/bin/senddigests.py b/mailman/bin/senddigests.py
deleted file mode 100644
index fb057d6b9..000000000
--- a/mailman/bin/senddigests.py
+++ /dev/null
@@ -1,83 +0,0 @@
-# Copyright (C) 1998-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 .
-
-import os
-import sys
-import optparse
-
-from mailman import MailList
-from mailman.i18n import _
-from mailman.initialize import initialize
-from mailman.version import MAILMAN_VERSION
-
-# Work around known problems with some RedHat cron daemons
-import signal
-signal.signal(signal.SIGCHLD, signal.SIG_DFL)
-
-
-
-def parseargs():
- parser = optparse.OptionParser(version=MAILMAN_VERSION,
- usage=_("""\
-%prog [options]
-
-Dispatch digests for lists w/pending messages and digest_send_periodic
-set."""))
- parser.add_option('-l', '--listname',
- type='string', default=[], action='append',
- dest='listnames', help=_("""\
-Send the digest for the given list only, otherwise the digests for all
-lists are sent out. Multiple -l options may be given."""))
- parser.add_option('-C', '--config',
- help=_('Alternative configuration file to use'))
- opts, args = parser.parse_args()
- if args:
- parser.print_help()
- print >> sys.stderr, _('Unexpected arguments')
- sys.exit(1)
- return opts, args, parser
-
-
-
-def main():
- opts, args, parser = parseargs()
- initialize(opts.config)
-
- for listname in set(opts.listnames or config.list_manager.names):
- mlist = MailList.MailList(listname, lock=False)
- if mlist.digest_send_periodic:
- mlist.Lock()
- try:
- try:
- mlist.send_digest_now()
- mlist.Save()
- # We are unable to predict what exception may occur in digest
- # processing and we don't want to lose the other digests, so
- # we catch everything.
- except Exception, errmsg:
- print >> sys.stderr, \
- 'List: %s: problem processing %s:\n%s' % \
- (listname,
- os.path.join(mlist.data_path, 'digest.mbox'),
- errmsg)
- finally:
- mlist.Unlock()
-
-
-
-if __name__ == '__main__':
- main()
diff --git a/mailman/bin/set_members.py b/mailman/bin/set_members.py
deleted file mode 100644
index cdd11c56f..000000000
--- a/mailman/bin/set_members.py
+++ /dev/null
@@ -1,189 +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 .
-
-import csv
-import optparse
-
-from mailman import Message
-from mailman import Utils
-from mailman import i18n
-from mailman import passwords
-from mailman.app.membership import add_member
-from mailman.app.notifications import (
- send_admin_subscription_notice, send_welcome_message)
-from mailman.configuration import config
-from mailman.initialize import initialize
-from mailman.interfaces import DeliveryMode
-from mailman.version import MAILMAN_VERSION
-
-
-_ = i18n._
-
-DELIVERY_MODES = {
- 'regular': DeliveryMode.regular,
- 'plain': DeliveryMode.plaintext_digests,
- 'mime': DeliveryMode.mime_digests,
- }
-
-
-
-def parseargs():
- parser = optparse.OptionParser(version=MAILMAN_VERSION,
- usage=_("""\
-%prog [options] csv-file
-
-Set the membership of a mailing list to that described in a CSV file. Each
-row of the CSV file has the following format. Only the address column is
-required.
-
- - email address
- - full name (default: the empty string)
- - delivery mode (default: regular delivery) [1]
-
-[1] The delivery mode is a case insensitive string of the following values:
-
- regular - regular, i.e. immediate delivery
- mime - MIME digest delivery
- plain - plain text (RFC 1153) digest delivery
-
-Any address not included in the CSV file is removed from the list membership.
-"""))
- parser.add_option('-l', '--listname',
- type='string', help=_("""\
-Mailng list to set the membership for."""))
- parser.add_option('-w', '--welcome-msg',
- type='string', metavar='', help=_("""\
-Set whether or not to send the list members a welcome message, overriding
-whatever the list's 'send_welcome_msg' setting is."""))
- parser.add_option('-a', '--admin-notify',
- type='string', metavar='', help=_("""\
-Set whether or not to send the list administrators a notification on the
-success/failure of these subscriptions, overriding whatever the list's
-'admin_notify_mchanges' setting is."""))
- parser.add_option('-v', '--verbose', action='store_true',
- help=_('Increase verbosity'))
- parser.add_option('-C', '--config',
- help=_('Alternative configuration file to use'))
- opts, args = parser.parse_args()
- if opts.welcome_msg is not None:
- ch = opts.welcome_msg[0].lower()
- if ch == 'y':
- opts.welcome_msg = True
- elif ch == 'n':
- opts.welcome_msg = False
- else:
- parser.error(_('Illegal value for -w: $opts.welcome_msg'))
- if opts.admin_notify is not None:
- ch = opts.admin_notify[0].lower()
- if ch == 'y':
- opts.admin_notify = True
- elif ch == 'n':
- opts.admin_notify = False
- else:
- parser.error(_('Illegal value for -a: $opts.admin_notify'))
- return parser, opts, args
-
-
-
-def parse_file(filename):
- members = {}
- with open(filename) as fp:
- for row in csv.reader(fp):
- if len(row) == 0:
- continue
- elif len(row) == 1:
- address = row[0]
- real_name = None
- delivery_mode = DeliveryMode.regular
- elif len(row) == 2:
- address, real_name = row
- delivery_mode = DeliveryMode.regular
- else:
- # Ignore extra columns
- address, real_name = row[0:2]
- delivery_mode = DELIVERY_MODES.get(row[2].lower())
- if delivery_mode is None:
- delivery_mode = DeliveryMode.regular
- members[address] = real_name, delivery_mode
- return members
-
-
-
-def main():
- parser, opts, args = parseargs()
- initialize(opts.config)
-
- mlist = config.db.list_manager.get(opts.listname)
- if mlist is None:
- parser.error(_('No such list: $opts.listname'))
-
- # Set up defaults.
- if opts.welcome_msg is None:
- send_welcome_msg = mlist.send_welcome_msg
- else:
- send_welcome_msg = opts.welcome_msg
- if opts.admin_notify is None:
- admin_notify = mlist.admin_notify_mchanges
- else:
- admin_notify = opts.admin_notify
-
- # Parse the csv files.
- member_data = {}
- for filename in args:
- member_data.update(parse_file(filename))
-
- future_members = set(member_data)
- current_members = set(obj.address for obj in mlist.members.addresses)
- add_members = future_members - current_members
- delete_members = current_members - future_members
- change_members = current_members & future_members
-
- with i18n.using_language(mlist.preferred_language):
- # Start by removing all the delete members.
- for address in delete_members:
- print _('deleting address: $address')
- member = mlist.members.get_member(address)
- member.unsubscribe()
- # For all members that are in both lists, update their full name and
- # delivery mode.
- for address in change_members:
- print _('updating address: $address')
- real_name, delivery_mode = member_data[address]
- member = mlist.members.get_member(address)
- member.preferences.delivery_mode = delivery_mode
- user = config.db.user_manager.get_user(address)
- user.real_name = real_name
- for address in add_members:
- print _('adding address: $address')
- real_name, delivery_mode = member_data[address]
- password = passwords.make_secret(
- Utils.MakeRandomPassword(),
- passwords.lookup_scheme(config.PASSWORD_SCHEME))
- add_member(mlist, address, real_name, password, delivery_mode,
- mlist.preferred_language, send_welcome_msg,
- admin_notify)
- if send_welcome_msg:
- send_welcome_message(mlist, address, language, delivery_mode)
- if admin_notify:
- send_admin_subscription_notice(mlist, address, real_name)
-
- config.db.flush()
-
-
-
-if __name__ == '__main__':
- main()
diff --git a/mailman/bin/show_config.py b/mailman/bin/show_config.py
deleted file mode 100644
index 8d26c5c97..000000000
--- a/mailman/bin/show_config.py
+++ /dev/null
@@ -1,97 +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 .
-
-import re
-import sys
-import pprint
-import optparse
-
-from mailman.configuration import config
-from mailman.i18n import _
-from mailman.version import MAILMAN_VERSION
-
-
-# List of names never to show even if --verbose
-NEVER_SHOW = ['__builtins__', '__doc__']
-
-
-
-def parseargs():
- parser = optparse.OptionParser(version=MAILMAN_VERSION,
- usage=_("""\
-%%prog [options] [pattern ...]
-
-Show the values of various Defaults.py/mailman.cfg variables.
-If one or more patterns are given, show only those variables
-whose names match a pattern"""))
- parser.add_option('-v', '--verbose',
- default=False, action='store_true',
- help=_(
-"Show all configuration names, not just 'settings'."))
- parser.add_option('-i', '--ignorecase',
- default=False, action='store_true',
- help=_("Match patterns case-insensitively."))
- parser.add_option('-C', '--config',
- help=_('Alternative configuration file to use'))
- opts, args = parser.parse_args()
- return parser, opts, args
-
-
-
-def main():
- parser, opts, args = parseargs()
-
- patterns = []
- if opts.ignorecase:
- flag = re.IGNORECASE
- else:
- flag = 0
- for pattern in args:
- patterns.append(re.compile(pattern, flag))
-
- pp = pprint.PrettyPrinter(indent=4)
- config.load(opts.config)
- names = config.__dict__.keys()
- names.sort()
- for name in names:
- if name in NEVER_SHOW:
- continue
- if not opts.verbose:
- if name.startswith('_') or re.search('[a-z]', name):
- continue
- if patterns:
- hit = False
- for pattern in patterns:
- if pattern.search(name):
- hit = True
- break
- if not hit:
- continue
- value = config.__dict__[name]
- if isinstance(value, str):
- if re.search('\n', value):
- print '%s = """%s"""' %(name, value)
- else:
- print "%s = '%s'" % (name, value)
- else:
- print '%s = ' % name,
- pp.pprint(value)
-
-
-
-if __name__ == '__main__':
- main()
diff --git a/mailman/bin/show_qfiles.py b/mailman/bin/show_qfiles.py
deleted file mode 100644
index e4b64e0cd..000000000
--- a/mailman/bin/show_qfiles.py
+++ /dev/null
@@ -1,91 +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 .
-
-import os
-import sys
-
-from cPickle import load
-
-from mailman.config import config
-from mailman.i18n import _
-from mailman.options import Options
-
-
-
-class ScriptOptions(Options):
- usage = _("""
-%%prog [options] qfiles ...
-
-Show the contents of one or more Mailman queue files.""")
-
- def add_options(self):
- super(ScriptOptions, self).add_options()
- self.parser.add_option(
- '-q', '--quiet',
- default=False, action='store_true',
- help=_("Don't print 'helpful' message delimiters."))
- self.parser.add_option(
- '-s', '--summary',
- default=False, action='store_true',
- help=_('Show a summary of queue files.'))
-
-
-
-def main():
- options = ScriptOptions()
- options.initialize()
-
- if options.options.summary:
- queue_totals = {}
- files_by_queue = {}
- for switchboard in config.switchboards.values():
- total = 0
- file_mappings = {}
- for filename in os.listdir(switchboard.queue_directory):
- base, ext = os.path.splitext(filename)
- file_mappings[ext] = file_mappings.get(ext, 0) + 1
- total += 1
- files_by_queue[switchboard.queue_directory] = file_mappings
- queue_totals[switchboard.queue_directory] = total
- # Sort by queue name.
- for queue_directory in sorted(files_by_queue):
- total = queue_totals[queue_directory]
- print queue_directory
- print _('\tfile count: $total')
- file_mappings = files_by_queue[queue_directory]
- for ext in sorted(file_mappings):
- print '\t{0}: {1}'.format(ext, file_mappings[ext])
- return
- # No summary.
- for filename in options.arguments:
- if not options.options.quiet:
- print '====================>', filename
- with open(filename) as fp:
- if filename.endswith('.pck'):
- msg = load(fp)
- data = load(fp)
- if data.get('_parsemsg'):
- sys.stdout.write(msg)
- else:
- sys.stdout.write(msg.as_string())
- else:
- sys.stdout.write(fp.read())
-
-
-
-if __name__ == '__main__':
- main()
diff --git a/mailman/bin/unshunt.py b/mailman/bin/unshunt.py
deleted file mode 100644
index fc889377c..000000000
--- a/mailman/bin/unshunt.py
+++ /dev/null
@@ -1,51 +0,0 @@
-# Copyright (C) 2002-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 .
-
-__metaclass__ = type
-__all__ = [
- 'main',
- ]
-
-
-import sys
-
-from mailman.config import config
-from mailman.i18n import _
-from mailman.options import Options
-
-
-
-def main():
- options = Options()
- options.initialize()
-
- switchboard = config.switchboards['shunt']
- switchboard.recover_backup_files()
-
- for filebase in switchboard.files:
- try:
- msg, msgdata = switchboard.dequeue(filebase)
- whichq = msgdata.get('whichq', 'in')
- config.switchboards[whichq].enqueue(msg, msgdata)
- except Exception, e:
- # If there are any unshunting errors, log them and continue trying
- # other shunted messages.
- print >> sys.stderr, _(
- 'Cannot unshunt message $filebase, skipping:\n$e')
- else:
- # Unlink the .bak file left by dequeue()
- switchboard.finish(filebase)
diff --git a/mailman/bin/update.py b/mailman/bin/update.py
deleted file mode 100644
index 34ea6cda3..000000000
--- a/mailman/bin/update.py
+++ /dev/null
@@ -1,660 +0,0 @@
-# Copyright (C) 1998-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 .
-
-import os
-import md5
-import sys
-import time
-import email
-import errno
-import shutil
-import cPickle
-import marshal
-import optparse
-
-from locknix.lockfile import TimeOutError
-
-from mailman import MailList
-from mailman import Message
-from mailman import Pending
-from mailman import Utils
-from mailman import version
-from mailman.MemberAdaptor import BYBOUNCE, ENABLED
-from mailman.OldStyleMemberships import OldStyleMemberships
-from mailman.Queue.Switchboard import Switchboard
-from mailman.configuration import config
-from mailman.i18n import _
-from mailman.initialize import initialize
-from mailman.utilities.filesystem import makedirs
-
-
-FRESH = 0
-NOTFRESH = -1
-
-
-
-def parseargs():
- parser = optparse.OptionParser(version=version.MAILMAN_VERSION,
- usage=_("""\
-Perform all necessary upgrades.
-
-%prog [options]"""))
- parser.add_option('-f', '--force',
- default=False, action='store_true', help=_("""\
-Force running the upgrade procedures. Normally, if the version number of the
-installed Mailman matches the current version number (or a 'downgrade' is
-detected), nothing will be done."""))
- parser.add_option('-C', '--config',
- help=_('Alternative configuration file to use'))
- opts, args = parser.parse_args()
- if args:
- parser.print_help()
- print >> sys.stderr, _('Unexpected arguments')
- sys.exit(1)
- return parser, opts, args
-
-
-
-def calcversions():
- # Returns a tuple of (lastversion, thisversion). If the last version
- # could not be determined, lastversion will be FRESH or NOTFRESH,
- # depending on whether this installation appears to be fresh or not. The
- # determining factor is whether there are files in the $var_prefix/logs
- # subdir or not. The version numbers are HEX_VERSIONs.
- #
- # See if we stored the last updated version
- lastversion = None
- thisversion = version.HEX_VERSION
- try:
- fp = open(os.path.join(config.DATA_DIR, 'last_mailman_version'))
- data = fp.read()
- fp.close()
- lastversion = int(data, 16)
- except (IOError, ValueError):
- pass
- #
- # try to figure out if this is a fresh install
- if lastversion is None:
- lastversion = FRESH
- try:
- if os.listdir(config.LOG_DIR):
- lastversion = NOTFRESH
- except OSError:
- pass
- return (lastversion, thisversion)
-
-
-
-def makeabs(relpath):
- return os.path.join(config.PREFIX, relpath)
-
-
-def make_varabs(relpath):
- return os.path.join(config.VAR_PREFIX, relpath)
-
-
-
-def move_language_templates(mlist):
- listname = mlist.internal_name()
- print _('Fixing language templates: $listname')
- # Mailman 2.1 has a new cascading search for its templates, defined and
- # described in Utils.py:maketext(). Putting templates in the top level
- # templates/ subdir or the lists/ subdir is deprecated and no
- # longer searched..
- #
- # What this means is that most templates can live in the global templates/
- # subdirectory, and only needs to be copied into the list-, vhost-, or
- # site-specific language directories when needed.
- #
- # Also, by default all standard (i.e. English) templates must now live in
- # the templates/en directory. This update cleans up all the templates,
- # deleting more-specific duplicates (as calculated by md5 checksums) in
- # favor of more-global locations.
- #
- # First, get rid of any lists/ template or lists//en template
- # that is identical to the global templates/* default.
- for gtemplate in os.listdir(os.path.join(config.TEMPLATE_DIR, 'en')):
- # BAW: get rid of old templates, e.g. admlogin.txt and
- # handle_opts.html
- try:
- fp = open(os.path.join(config.TEMPLATE_DIR, gtemplate))
- except IOError, e:
- if e.errno <> errno.ENOENT:
- raise
- # No global template
- continue
- gcksum = md5.new(fp.read()).digest()
- fp.close()
- # Match against the lists//* template
- try:
- fp = open(os.path.join(mlist.fullpath(), gtemplate))
- except IOError, e:
- if e.errno <> errno.ENOENT:
- raise
- else:
- tcksum = md5.new(fp.read()).digest()
- fp.close()
- if gcksum == tcksum:
- os.unlink(os.path.join(mlist.fullpath(), gtemplate))
- # Match against the lists//*.prev template
- try:
- fp = open(os.path.join(mlist.fullpath(), gtemplate + '.prev'))
- except IOError, e:
- if e.errno <> errno.ENOENT:
- raise
- else:
- tcksum = md5.new(fp.read()).digest()
- fp.close()
- if gcksum == tcksum:
- os.unlink(os.path.join(mlist.fullpath(), gtemplate + '.prev'))
- # Match against the lists//en/* templates
- try:
- fp = open(os.path.join(mlist.fullpath(), 'en', gtemplate))
- except IOError, e:
- if e.errno <> errno.ENOENT:
- raise
- else:
- tcksum = md5.new(fp.read()).digest()
- fp.close()
- if gcksum == tcksum:
- os.unlink(os.path.join(mlist.fullpath(), 'en', gtemplate))
- # Match against the templates/* template
- try:
- fp = open(os.path.join(config.TEMPLATE_DIR, gtemplate))
- except IOError, e:
- if e.errno <> errno.ENOENT:
- raise
- else:
- tcksum = md5.new(fp.read()).digest()
- fp.close()
- if gcksum == tcksum:
- os.unlink(os.path.join(config.TEMPLATE_DIR, gtemplate))
- # Match against the templates/*.prev template
- try:
- fp = open(os.path.join(config.TEMPLATE_DIR, gtemplate + '.prev'))
- except IOError, e:
- if e.errno <> errno.ENOENT:
- raise
- else:
- tcksum = md5.new(fp.read()).digest()
- fp.close()
- if gcksum == tcksum:
- os.unlink(os.path.join(config.TEMPLATE_DIR,
- gtemplate + '.prev'))
-
-
-
-def situate_list(listname):
- # This turns the directory called 'listname' into a directory called
- # 'listname@domain'. Start by finding out what the domain should be.
- # A list's domain is its email host.
- mlist = MailList.MailList(listname, lock=False, check_version=False)
- fullname = mlist.fqdn_listname
- oldpath = os.path.join(config.VAR_PREFIX, 'lists', listname)
- newpath = os.path.join(config.VAR_PREFIX, 'lists', fullname)
- if os.path.exists(newpath):
- print >> sys.stderr, _('WARNING: could not situate list: $listname')
- else:
- os.rename(oldpath, newpath)
- print _('situated list $listname to $fullname')
- return fullname
-
-
-
-def dolist(listname):
- mlist = MailList.MailList(listname, lock=False)
- try:
- mlist.Lock(0.5)
- except TimeOutError:
- print >> sys.stderr, _(
- 'WARNING: could not acquire lock for list: $listname')
- return 1
- # Sanity check the invariant that every BYBOUNCE disabled member must have
- # bounce information. Some earlier betas broke this. BAW: we're
- # submerging below the MemberAdaptor interface, so skip this if we're not
- # using OldStyleMemberships.
- if isinstance(mlist._memberadaptor, OldStyleMemberships):
- noinfo = {}
- for addr, (reason, when) in mlist.delivery_status.items():
- if reason == BYBOUNCE and not mlist.bounce_info.has_key(addr):
- noinfo[addr] = reason, when
- # What to do about these folks with a BYBOUNCE delivery status and no
- # bounce info? This number should be very small, and I think it's
- # fine to simple re-enable them and let the bounce machinery
- # re-disable them if necessary.
- n = len(noinfo)
- if n > 0:
- print _(
- 'Resetting $n BYBOUNCEs disabled addrs with no bounce info')
- for addr in noinfo.keys():
- mlist.setDeliveryStatus(addr, ENABLED)
-
- mbox_dir = make_varabs('archives/private/%s.mbox' % (listname))
- mbox_file = make_varabs('archives/private/%s.mbox/%s' % (listname,
- listname))
- o_pub_mbox_file = make_varabs('archives/public/%s' % (listname))
- o_pri_mbox_file = make_varabs('archives/private/%s' % (listname))
- html_dir = o_pri_mbox_file
- o_html_dir = makeabs('public_html/archives/%s' % (listname))
- # Make the mbox directory if it's not there.
- if not os.path.exists(mbox_dir):
- makedirs(mbox_dir)
- else:
- # This shouldn't happen, but hey, just in case
- if not os.path.isdir(mbox_dir):
- print _("""\
-For some reason, $mbox_dir exists as a file. This won't work with b6, so I'm
-renaming it to ${mbox_dir}.tmp and proceeding.""")
- os.rename(mbox_dir, "%s.tmp" % (mbox_dir))
- makedirs(mbox_dir)
- # Move any existing mboxes around, but watch out for both a public and a
- # private one existing
- if os.path.isfile(o_pri_mbox_file) and os.path.isfile(o_pub_mbox_file):
- if mlist.archive_private:
- print _("""\
-
-$listname has both public and private mbox archives. Since this list
-currently uses private archiving, I'm installing the private mbox archive --
-$o_pri_mbox_file -- as the active archive, and renaming
- $o_pub_mbox_file
-to
- ${o_pub_mbox_file}.preb6
-
-You can integrate that into the archives if you want by using the 'arch'
-script.
-""") % (mlist._internal_name, o_pri_mbox_file, o_pub_mbox_file,
- o_pub_mbox_file)
- os.rename(o_pub_mbox_file, "%s.preb6" % (o_pub_mbox_file))
- else:
- print _("""\
-$mlist._internal_name has both public and private mbox archives. Since this
-list currently uses public archiving, I'm installing the public mbox file
-archive file ($o_pub_mbox_file) as the active one, and renaming
-$o_pri_mbox_file to ${o_pri_mbox_file}.preb6
-
-You can integrate that into the archives if you want by using the 'arch'
-script.
-""")
- os.rename(o_pri_mbox_file, "%s.preb6" % (o_pri_mbox_file))
- # Move private archive mbox there if it's around
- # and take into account all sorts of absurdities
- print _('- updating old private mbox file')
- if os.path.exists(o_pri_mbox_file):
- if os.path.isfile(o_pri_mbox_file):
- os.rename(o_pri_mbox_file, mbox_file)
- elif not os.path.isdir(o_pri_mbox_file):
- newname = "%s.mm_install-dunno_what_this_was_but_its_in_the_way" \
- % o_pri_mbox_file
- os.rename(o_pri_mbox_file, newname)
- print _("""\
- unknown file in the way, moving
- $o_pri_mbox_file
- to
- $newname""")
- else:
- # directory
- print _("""\
- looks like you have a really recent development installation...
- you're either one brave soul, or you already ran me""")
- # Move public archive mbox there if it's around
- # and take into account all sorts of absurdities.
- print _('- updating old public mbox file')
- if os.path.exists(o_pub_mbox_file):
- if os.path.isfile(o_pub_mbox_file):
- os.rename(o_pub_mbox_file, mbox_file)
- elif not os.path.isdir(o_pub_mbox_file):
- newname = "%s.mm_install-dunno_what_this_was_but_its_in_the_way" \
- % o_pub_mbox_file
- os.rename(o_pub_mbox_file, newname)
- print _("""\
- unknown file in the way, moving
- $o_pub_mbox_file
- to
- $newname""")
- else: # directory
- print _("""\
- looks like you have a really recent development installation...
- you're either one brave soul, or you already ran me""")
- # Move the html archives there
- if os.path.isdir(o_html_dir):
- os.rename(o_html_dir, html_dir)
- # chmod the html archives
- os.chmod(html_dir, 02775)
- # BAW: Is this still necessary?!
- mlist.Save()
- # Check to see if pre-b4 list-specific templates are around
- # and move them to the new place if there's not already
- # a new one there
- tmpl_dir = os.path.join(config.PREFIX, "templates")
- list_dir = os.path.join(config.PREFIX, "lists")
- b4_tmpl_dir = os.path.join(tmpl_dir, mlist._internal_name)
- new_tmpl_dir = os.path.join(list_dir, mlist._internal_name)
- if os.path.exists(b4_tmpl_dir):
- print _("""\
-- This list looks like it might have <= b4 list templates around""")
- for f in os.listdir(b4_tmpl_dir):
- o_tmpl = os.path.join(b4_tmpl_dir, f)
- n_tmpl = os.path.join(new_tmpl_dir, f)
- if os.path.exists(o_tmpl):
- if not os.path.exists(n_tmpl):
- os.rename(o_tmpl, n_tmpl)
- print _('- moved $o_tmpl to $n_tmpl')
- else:
- print _("""\
-- both $o_tmpl and $n_tmpl exist, leaving untouched""")
- else:
- print _("""\
-- $o_tmpl doesn't exist, leaving untouched""")
- # Move all the templates to the en language subdirectory as required for
- # Mailman 2.1
- move_language_templates(mlist)
- # Avoid eating filehandles with the list lockfiles
- mlist.Unlock()
- return 0
-
-
-
-def archive_path_fixer(unused_arg, dir, files):
- # Passed to os.path.walk to fix the perms on old html archives.
- for f in files:
- abs = os.path.join(dir, f)
- if os.path.isdir(abs):
- if f == "database":
- os.chmod(abs, 02770)
- else:
- os.chmod(abs, 02775)
- elif os.path.isfile(abs):
- os.chmod(abs, 0664)
-
-
-def remove_old_sources(module):
- # Also removes old directories.
- src = '%s/%s' % (config.PREFIX, module)
- pyc = src + "c"
- if os.path.isdir(src):
- print _('removing directory $src and everything underneath')
- shutil.rmtree(src)
- elif os.path.exists(src):
- print _('removing $src')
- try:
- os.unlink(src)
- except os.error, rest:
- print _("Warning: couldn't remove $src -- $rest")
- if module.endswith('.py') and os.path.exists(pyc):
- try:
- os.unlink(pyc)
- except OSError, rest:
- print _("couldn't remove old file $pyc -- $rest")
-
-
-
-def update_qfiles():
- print _('updating old qfiles')
- prefix = `time.time()` + '+'
- # Be sure the qfiles/in directory exists (we don't really need the
- # switchboard object, but it's convenient for creating the directory).
- sb = Switchboard(config.INQUEUE_DIR)
- for filename in os.listdir(config.QUEUE_DIR):
- # Updating means just moving the .db and .msg files to qfiles/in where
- # it should be dequeued, converted, and processed normally.
- if os.path.splitext(filename) == '.msg':
- oldmsgfile = os.path.join(config.QUEUE_DIR, filename)
- newmsgfile = os.path.join(config.INQUEUE_DIR, prefix + filename)
- os.rename(oldmsgfile, newmsgfile)
- elif os.path.splitext(filename) == '.db':
- olddbfile = os.path.join(config.QUEUE_DIR, filename)
- newdbfile = os.path.join(config.INQUEUE_DIR, prefix + filename)
- os.rename(olddbfile, newdbfile)
- # Now update for the Mailman 2.1.5 qfile format. For every filebase in
- # the qfiles/* directories that has both a .pck and a .db file, pull the
- # data out and re-queue them.
- for dirname in os.listdir(config.QUEUE_DIR):
- dirpath = os.path.join(config.QUEUE_DIR, dirname)
- if dirpath == config.BADQUEUE_DIR:
- # The files in qfiles/bad can't possibly be pickles
- continue
- sb = Switchboard(dirpath)
- try:
- for filename in os.listdir(dirpath):
- filepath = os.path.join(dirpath, filename)
- filebase, ext = os.path.splitext(filepath)
- # Handle the .db metadata files as part of the handling of the
- # .pck or .msg message files.
- if ext not in ('.pck', '.msg'):
- continue
- msg, data = dequeue(filebase)
- if msg is not None and data is not None:
- sb.enqueue(msg, data)
- except EnvironmentError, e:
- if e.errno <> errno.ENOTDIR:
- raise
- print _('Warning! Not a directory: $dirpath')
-
-
-
-# Implementations taken from the pre-2.1.5 Switchboard
-def ext_read(filename):
- fp = open(filename)
- d = marshal.load(fp)
- # Update from version 2 files
- if d.get('version', 0) == 2:
- del d['filebase']
- # Do the reverse conversion (repr -> float)
- for attr in ['received_time']:
- try:
- sval = d[attr]
- except KeyError:
- pass
- else:
- # Do a safe eval by setting up a restricted execution
- # environment. This may not be strictly necessary since we
- # know they are floats, but it can't hurt.
- d[attr] = eval(sval, {'__builtins__': {}})
- fp.close()
- return d
-
-
-def dequeue(filebase):
- # Calculate the .db and .msg filenames from the given filebase.
- msgfile = os.path.join(filebase + '.msg')
- pckfile = os.path.join(filebase + '.pck')
- dbfile = os.path.join(filebase + '.db')
- # Now we are going to read the message and metadata for the given
- # filebase. We want to read things in this order: first, the metadata
- # file to find out whether the message is stored as a pickle or as
- # plain text. Second, the actual message file. However, we want to
- # first unlink the message file and then the .db file, because the
- # qrunner only cues off of the .db file
- msg = None
- try:
- data = ext_read(dbfile)
- os.unlink(dbfile)
- except EnvironmentError, e:
- if e.errno <> errno.ENOENT:
- raise
- data = {}
- # Between 2.1b4 and 2.1b5, the `rejection-notice' key in the metadata
- # was renamed to `rejection_notice', since dashes in the keys are not
- # supported in METAFMT_ASCII.
- if data.has_key('rejection-notice'):
- data['rejection_notice'] = data['rejection-notice']
- del data['rejection-notice']
- msgfp = None
- try:
- try:
- msgfp = open(pckfile)
- msg = cPickle.load(msgfp)
- os.unlink(pckfile)
- except EnvironmentError, e:
- if e.errno <> errno.ENOENT: raise
- msgfp = None
- try:
- msgfp = open(msgfile)
- msg = email.message_from_file(msgfp, Message.Message)
- os.unlink(msgfile)
- except EnvironmentError, e:
- if e.errno <> errno.ENOENT: raise
- except (email.Errors.MessageParseError, ValueError), e:
- # This message was unparsable, most likely because its
- # MIME encapsulation was broken. For now, there's not
- # much we can do about it.
- print _('message is unparsable: $filebase')
- msgfp.close()
- msgfp = None
- if config.QRUNNER_SAVE_BAD_MESSAGES:
- # Cheapo way to ensure the directory exists w/ the
- # proper permissions.
- sb = Switchboard(config.BADQUEUE_DIR)
- os.rename(msgfile, os.path.join(
- config.BADQUEUE_DIR, filebase + '.txt'))
- else:
- os.unlink(msgfile)
- msg = data = None
- except EOFError:
- # For some reason the pckfile was empty. Just delete it.
- print _('Warning! Deleting empty .pck file: $pckfile')
- os.unlink(pckfile)
- finally:
- if msgfp:
- msgfp.close()
- return msg, data
-
-
-
-def main():
- parser, opts, args = parseargs()
- initialize(opts.config)
-
- # calculate the versions
- lastversion, thisversion = calcversions()
- hexlversion = hex(lastversion)
- hextversion = hex(thisversion)
- if lastversion == thisversion and not opts.force:
- # nothing to do
- print _('No updates are necessary.')
- sys.exit(0)
- if lastversion > thisversion and not opts.force:
- print _("""\
-Downgrade detected, from version $hexlversion to version $hextversion
-This is probably not safe.
-Exiting.""")
- sys.exit(1)
- print _('Upgrading from version $hexlversion to $hextversion')
- errors = 0
- # get rid of old stuff
- print _('getting rid of old source files')
- for mod in ('mailman/Archiver.py', 'mailman/HyperArch.py',
- 'mailman/HyperDatabase.py', 'mailman/pipermail.py',
- 'mailman/smtplib.py', 'mailman/Cookie.py',
- 'bin/update_to_10b6', 'scripts/mailcmd',
- 'scripts/mailowner', 'mail/wrapper', 'mailman/pythonlib',
- 'cgi-bin/archives', 'mailman/MailCommandHandler'):
- remove_old_sources(mod)
- if not config.list_manager.names:
- print _('no lists == nothing to do, exiting')
- return
- # For people with web archiving, make sure the directories
- # in the archiving are set with proper perms for b6.
- if os.path.isdir("%s/public_html/archives" % config.PREFIX):
- print _("""\
-fixing all the perms on your old html archives to work with b6
-If your archives are big, this could take a minute or two...""")
- os.path.walk("%s/public_html/archives" % config.PREFIX,
- archive_path_fixer, "")
- print _('done')
- for listname in config.list_manager.names:
- # With 2.2.0a0, all list names grew an @domain suffix. If you find a
- # list without that, move it now.
- if not '@' in listname:
- listname = situate_list(listname)
- print _('Updating mailing list: $listname')
- errors += dolist(listname)
- print
- print _('Updating Usenet watermarks')
- wmfile = os.path.join(config.DATA_DIR, 'gate_watermarks')
- try:
- fp = open(wmfile)
- except IOError:
- print _('- nothing to update here')
- else:
- d = marshal.load(fp)
- fp.close()
- for listname in d.keys():
- if listname not in listnames:
- # this list no longer exists
- continue
- mlist = MailList.MailList(listname, lock=0)
- try:
- mlist.Lock(0.5)
- except TimeOutError:
- print >> sys.stderr, _(
- 'WARNING: could not acquire lock for list: $listname')
- errors = errors + 1
- else:
- # Pre 1.0b7 stored 0 in the gate_watermarks file to indicate
- # that no gating had been done yet. Without coercing this to
- # None, the list could now suddenly get flooded.
- mlist.usenet_watermark = d[listname] or None
- mlist.Save()
- mlist.Unlock()
- os.unlink(wmfile)
- print _('- usenet watermarks updated and gate_watermarks removed')
- # In Mailman 2.1, the qfiles directory has a different structure and a
- # different content. Also, in Mailman 2.1.5 we collapsed the message
- # files from separate .msg (pickled Message objects) and .db (marshalled
- # dictionaries) to a shared .pck file containing two pickles.
- update_qfiles()
- # This warning was necessary for the upgrade from 1.0b9 to 1.0b10.
- # There's no good way of figuring this out for releases prior to 2.0beta2
- # :(
- if lastversion == NOTFRESH:
- print _("""
-
-NOTE NOTE NOTE NOTE NOTE
-
- You are upgrading an existing Mailman installation, but I can't tell what
- version you were previously running.
-
- If you are upgrading from Mailman 1.0b9 or earlier you will need to
- manually update your mailing lists. For each mailing list you need to
- copy the file templates/options.html lists//options.html.
-
- However, if you have edited this file via the Web interface, you will have
- to merge your changes into this file, otherwise you will lose your
- changes.
-
-NOTE NOTE NOTE NOTE NOTE
-
-""")
- if not errors:
- # Record the version we just upgraded to
- fp = open(os.path.join(config.DATA_DIR, 'last_mailman_version'), 'w')
- fp.write(hex(config.HEX_VERSION) + '\n')
- fp.close()
- else:
- lockdir = config.LOCK_DIR
- print _('''\
-
-ERROR:
-
-The locks for some lists could not be acquired. This means that either
-Mailman was still active when you upgraded, or there were stale locks in the
-$lockdir directory.
-
-You must put Mailman into a quiescent state and remove all stale locks, then
-re-run "make update" manually. See the INSTALL and UPGRADE files for details.
-''')
diff --git a/mailman/bin/version.py b/mailman/bin/version.py
deleted file mode 100644
index 0fb2c5a5b..000000000
--- a/mailman/bin/version.py
+++ /dev/null
@@ -1,46 +0,0 @@
-# Copyright (C) 1998-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 .
-
-import optparse
-
-from mailman import version
-from mailman.i18n import _
-
-
-
-def parseargs():
- parser = optparse.OptionParser(version=version.MAILMAN_VERSION,
- usage=_("""\
-%prog
-
-Print the Mailman version and exit."""))
- opts, args = parser.parse_args()
- if args:
- parser.error(_('Unexpected arguments'))
- return parser, opts, args
-
-
-
-def main():
- parser, opts, args = parseargs()
- # Yes, this is kind of silly
- print _('Using $version.MAILMAN_VERSION ($version.CODENAME)')
-
-
-
-if __name__ == '__main__':
- main()
diff --git a/mailman/bin/withlist.py b/mailman/bin/withlist.py
deleted file mode 100644
index 8f2d8a2b5..000000000
--- a/mailman/bin/withlist.py
+++ /dev/null
@@ -1,220 +0,0 @@
-# Copyright (C) 1998-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 .
-
-import os
-import sys
-import optparse
-
-from mailman import interact
-from mailman.config import config
-from mailman.core.initialize import initialize
-from mailman.i18n import _
-from mailman.version import MAILMAN_VERSION
-
-
-LAST_MLIST = None
-VERBOSE = True
-
-
-
-def do_list(listname, args, func):
- global LAST_MLIST
-
- if '@' not in listname:
- listname += '@' + config.DEFAULT_EMAIL_HOST
-
- # XXX FIXME Remove this when this script is converted to
- # MultipleMailingListOptions.
- listname = listname.decode(sys.getdefaultencoding())
- mlist = config.db.list_manager.get(listname)
- if mlist is None:
- print >> sys.stderr, _('Unknown list: $listname')
- else:
- if VERBOSE:
- print >> sys.stderr, _('Loaded list: $listname')
- LAST_MLIST = mlist
- # Try to import the module and run the callable.
- if func:
- return func(mlist, *args)
- return None
-
-
-
-def parseargs():
- parser = optparse.OptionParser(version=MAILMAN_VERSION,
- usage=_("""\
-%prog [options] listname [args ...]
-
-General framework for interacting with a mailing list object.
-
-There are two ways to use this script: interactively or programmatically.
-Using it interactively allows you to play with, examine and modify a
-IMailinglist object from Python's interactive interpreter. When running
-interactively, a IMailingList object called 'm' will be available in the
-global namespace.
-
-Programmatically, you can write a function to operate on a IMailingList
-object, and this script will take care of the housekeeping (see below for
-examples). In that case, the general usage syntax is:
-
- % bin/withlist [options] listname [args ...]
-
-Here's an example of how to use the -r option. Say you have a file in the
-Mailman installation directory called 'listaddr.py', with the following
-two functions:
-
- def listaddr(mlist):
- print mlist.posting_address
-
- def requestaddr(mlist):
- print mlist.request_address
-
-Now, from the command line you can print the list's posting address by running
-the following from the command line:
-
- % bin/withlist -r listaddr mylist
- Loading list: mylist
- Importing listaddr ...
- Running listaddr.listaddr() ...
- mylist@myhost.com
-
-And you can print the list's request address by running:
-
- % bin/withlist -r listaddr.requestaddr mylist
- Loading list: mylist
- Importing listaddr ...
- Running listaddr.requestaddr() ...
- mylist-request@myhost.com
-
-As another example, say you wanted to change the password for a particular
-user on a particular list. You could put the following function in a file
-called 'changepw.py':
-
- from mailman.errors import NotAMemberError
-
- def changepw(mlist, addr, newpasswd):
- try:
- mlist.setMemberPassword(addr, newpasswd)
- mlist.Save()
- except NotAMemberError:
- print 'No address matched:', addr
-
-and run this from the command line:
-
- % bin/withlist -l -r changepw mylist somebody@somewhere.org foobar"""))
- parser.add_option('-i', '--interactive',
- default=None, action='store_true', help=_("""\
-Leaves you at an interactive prompt after all other processing is complete.
-This is the default unless the -r option is given."""))
- parser.add_option('-r', '--run',
- type='string', help=_("""\
-This can be used to run a script with the opened IMailingList object. This
-works by attempting to import'module' (which must be in the directory
-containing withlist, or already be accessible on your sys.path), and then
-calling 'callable' from the module. callable can be a class or function; it
-is called with the IMailingList object as the first argument. If additional
-args are given on the command line, they are passed as subsequent positional
-args to the callable.
-
-Note that 'module.' is optional; if it is omitted then a module with the name
-'callable' will be imported.
-
-The global variable 'r' will be set to the results of this call."""))
- parser.add_option('-a', '--all',
- default=False, action='store_true', help=_("""\
-This option only works with the -r option. Use this if you want to execute
-the script on all mailing lists. When you use -a you should not include a
-listname argument on the command line. The variable 'r' will be a list of all
-the results."""))
- parser.add_option('-q', '--quiet',
- default=False, action='store_true',
- help=_('Suppress all status messages.'))
- parser.add_option('-C', '--config',
- help=_('Alternative configuration file to use'))
- opts, args = parser.parse_args()
- return parser, opts, args
-
-
-
-def main():
- global VERBOSE
-
- parser, opts, args = parseargs()
- config_file = (os.getenv('MAILMAN_CONFIG_FILE')
- if opts.config is None
- else opts.config)
- initialize(config_file, not opts.quiet)
-
- VERBOSE = not opts.quiet
- # The default for interact is true unless -r was given
- if opts.interactive is None:
- if not opts.run:
- opts.interactive = True
- else:
- opts.interactive = False
-
- dolist = True
- if len(args) < 1 and not opts.all:
- warning = _('No list name supplied.')
- if opts.interactive:
- # Let them keep going
- print >> sys.stderr, warning
- dolist = False
- else:
- parser.error(warning)
-
- if opts.all and not opts.run:
- parser.error(_('--all requires --run'))
-
- # Try to import the module for the callable
- func = None
- if opts.run:
- i = opts.run.rfind('.')
- if i < 0:
- module = opts.run
- callable = opts.run
- else:
- module = opts.run[:i]
- callable = opts.run[i+1:]
- if VERBOSE:
- print >> sys.stderr, _('Importing $module ...')
- __import__(module)
- mod = sys.modules[module]
- if VERBOSE:
- print >> sys.stderr, _('Running ${module}.${callable}() ...')
- func = getattr(mod, callable)
-
- r = None
- if opts.all:
- r = [do_list(listname, args, func)
- for listname in config.list_manager.names]
- elif dolist:
- listname = args.pop(0).lower().strip()
- r = do_list(listname, args, func)
-
- # Now go to interactive mode, perhaps
- if opts.interactive:
- if dolist:
- banner = _(
- "The variable 'm' is the $listname mailing list")
- else:
- banner = interact.DEFAULT_BANNER
- overrides = dict(m=LAST_MLIST, r=r,
- commit=config.db.commit,
- abort=config.db.abort,
- config=config)
- interact.interact(upframe=False, banner=banner, overrides=overrides)
diff --git a/mailman/chains/__init__.py b/mailman/chains/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/mailman/chains/accept.py b/mailman/chains/accept.py
deleted file mode 100644
index bd47f42c8..000000000
--- a/mailman/chains/accept.py
+++ /dev/null
@@ -1,58 +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 .
-
-"""The terminal 'accept' chain."""
-
-from __future__ import absolute_import, unicode_literals
-
-__metaclass__ = type
-__all__ = [
- 'AcceptChain',
- ]
-
-import logging
-
-from mailman.chains.base import TerminalChainBase
-from mailman.config import config
-from mailman.i18n import _
-
-
-log = logging.getLogger('mailman.vette')
-SEMISPACE = '; '
-
-
-
-class AcceptChain(TerminalChainBase):
- """Accept the message for posting."""
-
- name = 'accept'
- description = _('Accept a message.')
-
- def _process(self, mlist, msg, msgdata):
- """See `TerminalChainBase`."""
- # Start by decorating the message with a header that contains a list
- # of all the rules that matched. These metadata could be None or an
- # empty list.
- rule_hits = msgdata.get('rule_hits')
- if rule_hits:
- msg['X-Mailman-Rule-Hits'] = SEMISPACE.join(rule_hits)
- rule_misses = msgdata.get('rule_misses')
- if rule_misses:
- msg['X-Mailman-Rule-Misses'] = SEMISPACE.join(rule_misses)
- accept_queue = config.switchboards['pipeline']
- accept_queue.enqueue(msg, msgdata)
- log.info('ACCEPT: %s', msg.get('message-id', 'n/a'))
diff --git a/mailman/chains/base.py b/mailman/chains/base.py
deleted file mode 100644
index bcd946b40..000000000
--- a/mailman/chains/base.py
+++ /dev/null
@@ -1,122 +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 .
-
-"""Base class for terminal chains."""
-
-from __future__ import absolute_import, unicode_literals
-
-__metaclass__ = type
-__all__ = [
- 'Chain',
- 'Link',
- 'TerminalChainBase',
- ]
-
-
-from zope.interface import implements
-
-from mailman.config import config
-from mailman.interfaces.chain import (
- IChain, IChainIterator, IChainLink, IMutableChain, LinkAction)
-
-
-
-class Link:
- """A chain link."""
- implements(IChainLink)
-
- def __init__(self, rule, action=None, chain=None, function=None):
- self.rule = rule
- self.action = (LinkAction.defer if action is None else action)
- self.chain = chain
- self.function = function
-
-
-
-class TerminalChainBase:
- """A base chain that always matches and executes a method.
-
- The method is called 'process' and must be provided by the subclass.
- """
- implements(IChain, IChainIterator)
-
- def _process(self, mlist, msg, msgdata):
- """Process the message for the given mailing list.
-
- This must be overridden by subclasses.
- """
- raise NotImplementedError
-
- def get_links(self, mlist, msg, msgdata):
- """See `IChain`."""
- return iter(self)
-
- def __iter__(self):
- """See `IChainIterator`."""
- truth = config.rules['truth']
- # First, yield a link that always runs the process method.
- yield Link(truth, LinkAction.run, function=self._process)
- # Now yield a rule that stops all processing.
- yield Link(truth, LinkAction.stop)
-
-
-
-class Chain:
- """Generic chain base class."""
- implements(IMutableChain)
-
- def __init__(self, name, description):
- assert name not in config.chains, (
- 'Duplicate chain name: {0}'.format(name))
- self.name = name
- self.description = description
- self._links = []
- # Register the chain.
- config.chains[name] = self
-
- def append_link(self, link):
- """See `IMutableChain`."""
- self._links.append(link)
-
- def flush(self):
- """See `IMutableChain`."""
- self._links = []
-
- def get_links(self, mlist, msg, msgdata):
- """See `IChain`."""
- return iter(ChainIterator(self))
-
- def get_iterator(self):
- """Return an iterator over the links."""
- # We do it this way in order to preserve a separation of interfaces,
- # and allows .get_links() to be overridden.
- for link in self._links:
- yield link
-
-
-
-class ChainIterator:
- """Generic chain iterator."""
-
- implements(IChainIterator)
-
- def __init__(self, chain):
- self._chain = chain
-
- def __iter__(self):
- """See `IChainIterator`."""
- return self._chain.get_iterator()
diff --git a/mailman/chains/builtin.py b/mailman/chains/builtin.py
deleted file mode 100644
index 05912a2f2..000000000
--- a/mailman/chains/builtin.py
+++ /dev/null
@@ -1,86 +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 .
-
-"""The default built-in starting chain."""
-
-from __future__ import absolute_import, unicode_literals
-
-__metaclass__ = type
-__all__ = [
- 'BuiltInChain',
- ]
-
-
-import logging
-
-from zope.interface import implements
-
-from mailman.chains.base import Link
-from mailman.config import config
-from mailman.i18n import _
-from mailman.interfaces.chain import IChain, LinkAction
-
-
-log = logging.getLogger('mailman.vette')
-
-
-
-class BuiltInChain:
- """Default built-in chain."""
-
- implements(IChain)
-
- name = 'built-in'
- description = _('The built-in moderation chain.')
-
- _link_descriptions = (
- ('approved', LinkAction.jump, 'accept'),
- ('emergency', LinkAction.jump, 'hold'),
- ('loop', LinkAction.jump, 'discard'),
- # Do all of the following before deciding whether to hold the message
- # for moderation.
- ('administrivia', LinkAction.defer, None),
- ('implicit-dest', LinkAction.defer, None),
- ('max-recipients', LinkAction.defer, None),
- ('max-size', LinkAction.defer, None),
- ('news-moderation', LinkAction.defer, None),
- ('no-subject', LinkAction.defer, None),
- ('suspicious-header', LinkAction.defer, None),
- # Now if any of the above hit, jump to the hold chain.
- ('any', LinkAction.jump, 'hold'),
- # Take a detour through the self header matching chain, which we'll
- # create later.
- ('truth', LinkAction.detour, 'header-match'),
- # Finally, the builtin chain selfs to acceptance.
- ('truth', LinkAction.jump, 'accept'),
- )
-
- def __init__(self):
- self._cached_links = None
-
- def get_links(self, mlist, msg, msgdata):
- """See `IChain`."""
- if self._cached_links is None:
- self._cached_links = links = []
- for rule_name, action, chain_name in self._link_descriptions:
- # Get the named rule.
- rule = config.rules[rule_name]
- # Get the chain, if one is defined.
- chain = (None if chain_name is None
- else config.chains[chain_name])
- links.append(Link(rule, action, chain))
- return iter(self._cached_links)
diff --git a/mailman/chains/discard.py b/mailman/chains/discard.py
deleted file mode 100644
index 1899e0340..000000000
--- a/mailman/chains/discard.py
+++ /dev/null
@@ -1,47 +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 .
-
-"""The terminal 'discard' chain."""
-
-from __future__ import absolute_import, unicode_literals
-
-__metaclass__ = type
-__all__ = [
- 'DiscardChain',
- ]
-
-
-import logging
-
-from mailman.chains.base import TerminalChainBase
-from mailman.i18n import _
-
-
-log = logging.getLogger('mailman.vette')
-
-
-
-class DiscardChain(TerminalChainBase):
- """Discard a message."""
-
- name = 'discard'
- description = _('Discard a message and stop processing.')
-
- def _process(self, mlist, msg, msgdata):
- """See `TerminalChainBase`."""
- log.info('DISCARD: %s', msg.get('message-id', 'n/a'))
- # Nothing more needs to happen.
diff --git a/mailman/chains/headers.py b/mailman/chains/headers.py
deleted file mode 100644
index 2f85d78d0..000000000
--- a/mailman/chains/headers.py
+++ /dev/null
@@ -1,156 +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 .
-
-"""The header-matching chain."""
-
-from __future__ import absolute_import, unicode_literals
-
-__metaclass__ = type
-__all__ = [
- 'HeaderMatchChain',
- ]
-
-
-import re
-import logging
-import itertools
-
-from zope.interface import implements
-
-from mailman.chains.base import Chain, Link
-from mailman.config import config
-from mailman.i18n import _
-from mailman.interfaces.chain import IChainIterator, LinkAction
-from mailman.interfaces.rules import IRule
-
-
-log = logging.getLogger('mailman.vette')
-
-
-
-def make_link(entry):
- """Create a Link object.
-
- :param entry: a 2- or 3-tuple describing a link. If a 2-tuple, it is a
- header and a pattern, and a default chain of 'hold' will be used. If
- a 3-tuple, the third item is the chain name to use.
- :return: an ILink.
- """
- if len(entry) == 2:
- header, pattern = entry
- chain_name = 'hold'
- elif len(entry) == 3:
- header, pattern, chain_name = entry
- # We don't assert that the chain exists here because the jump
- # chain may not yet have been created.
- else:
- raise AssertionError('Bad link description: {0}'.format(entry))
- rule = HeaderMatchRule(header, pattern)
- chain = config.chains[chain_name]
- return Link(rule, LinkAction.jump, chain)
-
-
-
-class HeaderMatchRule:
- """Header matching rule used by header-match chain."""
- implements(IRule)
-
- # Sequential rule counter.
- _count = 1
-
- def __init__(self, header, pattern):
- self._header = header
- self._pattern = pattern
- self.name = 'header-match-{0:02}'.format(HeaderMatchRule._count)
- HeaderMatchRule._count += 1
- self.description = '{0}: {1}'.format(header, pattern)
- # XXX I think we should do better here, somehow recording that a
- # particular header matched a particular pattern, but that gets ugly
- # with RFC 2822 headers. It also doesn't match well with the rule
- # name concept. For now, we just record the rather useless numeric
- # rule name. I suppose we could do the better hit recording in the
- # check() method, and set self.record = False.
- self.record = True
-
- def check(self, mlist, msg, msgdata):
- """See `IRule`."""
- for value in msg.get_all(self._header, []):
- if re.search(self._pattern, value, re.IGNORECASE):
- return True
- return False
-
-
-
-class HeaderMatchChain(Chain):
- """Default header matching chain.
-
- This could be extended by header match rules in the database.
- """
-
- def __init__(self):
- super(HeaderMatchChain, self).__init__(
- 'header-match', _('The built-in header matching chain'))
- # The header match rules are not global, so don't register them.
- # These are the only rules that the header match chain can execute.
- self._links = []
- # Initialize header check rules with those from the global
- # HEADER_MATCHES variable.
- for entry in config.header_matches:
- self._links.append(make_link(entry))
- # Keep track of how many global header matching rules we've seen.
- # This is so the flush() method will only delete those that were added
- # via extend() or append_link().
- self._permanent_link_count = len(self._links)
-
- def extend(self, header, pattern, chain_name='hold'):
- """Extend the existing header matches.
-
- :param header: The case-insensitive header field name.
- :param pattern: The pattern to match the header's value again. The
- match is not anchored and is done case-insensitively.
- :param chain: Option chain to jump to if the pattern matches any of
- the named header values. If not given, the 'hold' chain is used.
- """
- self._links.append(make_link((header, pattern, chain_name)))
-
- def flush(self):
- """See `IMutableChain`."""
- del self._links[self._permanent_link_count:]
-
- def get_links(self, mlist, msg, msgdata):
- """See `IChain`."""
- list_iterator = HeaderMatchIterator(mlist)
- return itertools.chain(iter(self._links), iter(list_iterator))
-
- def __iter__(self):
- for link in self._links:
- yield link
-
-
-
-class HeaderMatchIterator:
- """An iterator of both the global and list-specific chain links."""
-
- implements(IChainIterator)
-
- def __init__(self, mlist):
- self._mlist = mlist
-
- def __iter__(self):
- """See `IChainIterator`."""
- for entry in self._mlist.header_matches:
- yield make_link(entry)
diff --git a/mailman/chains/hold.py b/mailman/chains/hold.py
deleted file mode 100644
index 16238a541..000000000
--- a/mailman/chains/hold.py
+++ /dev/null
@@ -1,178 +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 .
-
-"""The terminal 'hold' chain."""
-
-from __future__ import absolute_import, unicode_literals
-
-__metaclass__ = type
-__all__ = [
- 'HoldChain',
- ]
-
-
-import logging
-
-from email.mime.message import MIMEMessage
-from email.mime.text import MIMEText
-from email.utils import formatdate, make_msgid
-from zope.interface import implements
-
-from mailman import i18n
-from mailman.Message import UserNotification
-from mailman.Utils import maketext, oneline, wrap, GetCharSet
-from mailman.app.moderator import hold_message
-from mailman.app.replybot import autorespond_to_sender, can_acknowledge
-from mailman.chains.base import TerminalChainBase
-from mailman.config import config
-from mailman.interfaces.pending import IPendable
-
-
-log = logging.getLogger('mailman.vette')
-SEMISPACE = '; '
-_ = i18n._
-
-
-
-class HeldMessagePendable(dict):
- implements(IPendable)
- PEND_KEY = 'held message'
-
-
-
-class HoldChain(TerminalChainBase):
- """Hold a message."""
-
- name = 'hold'
- description = _('Hold a message and stop processing.')
-
- def _process(self, mlist, msg, msgdata):
- """See `TerminalChainBase`."""
- # Start by decorating the message with a header that contains a list
- # of all the rules that matched. These metadata could be None or an
- # empty list.
- rule_hits = msgdata.get('rule_hits')
- if rule_hits:
- msg['X-Mailman-Rule-Hits'] = SEMISPACE.join(rule_hits)
- rule_misses = msgdata.get('rule_misses')
- if rule_misses:
- msg['X-Mailman-Rule-Misses'] = SEMISPACE.join(rule_misses)
- # Hold the message by adding it to the list's request database.
- # XXX How to calculate the reason?
- request_id = hold_message(mlist, msg, msgdata, None)
- # Calculate a confirmation token to send to the author of the
- # message.
- pendable = HeldMessagePendable(type=HeldMessagePendable.PEND_KEY,
- id=request_id)
- token = config.db.pendings.add(pendable)
- # Get the language to send the response in. If the sender is a
- # member, then send it in the member's language, otherwise send it in
- # the mailing list's preferred language.
- sender = msg.get_sender()
- member = mlist.members.get_member(sender)
- language = (member.preferred_language
- if member else mlist.preferred_language)
- # A substitution dictionary for the email templates.
- charset = GetCharSet(mlist.preferred_language)
- original_subject = msg.get('subject')
- if original_subject is None:
- original_subject = _('(no subject)')
- else:
- original_subject = oneline(original_subject, charset)
- substitutions = dict(
- listname = mlist.fqdn_listname,
- subject = original_subject,
- sender = sender,
- reason = 'XXX', #reason,
- confirmurl = '{0}/{1}'.format(mlist.script_url('confirm'), token),
- admindb_url = mlist.script_url('admindb'),
- )
- # At this point the message is held, but now we have to craft at least
- # two responses. The first will go to the original author of the
- # message and it will contain the token allowing them to approve or
- # discard the message. The second one will go to the moderators of
- # the mailing list, if the list is so configured.
- #
- # Start by possibly sending a response to the message author. There
- # are several reasons why we might not go through with this. If the
- # message was gated from NNTP, the author may not even know about this
- # list, so don't spam them. If the author specifically requested that
- # acknowledgments not be sent, or if the message was bulk email, then
- # we do not send the response. It's also possible that either the
- # mailing list, or the author (if they are a member) have been
- # configured to not send such responses.
- if (not msgdata.get('fromusenet') and
- can_acknowledge(msg) and
- mlist.respond_to_post_requests and
- autorespond_to_sender(mlist, sender, language)):
- # We can respond to the sender with a message indicating their
- # posting was held.
- subject = _(
- 'Your message to $mlist.fqdn_listname awaits moderator approval')
- send_language = msgdata.get('lang', language)
- text = maketext('postheld.txt', substitutions,
- lang=send_language, mlist=mlist)
- adminaddr = mlist.bounces_address
- nmsg = UserNotification(sender, adminaddr, subject, text,
- send_language)
- nmsg.send(mlist)
- # Now the message for the list moderators. This one should appear to
- # come from -owner since we really don't need to do bounce
- # processing on it.
- if mlist.admin_immed_notify:
- # Now let's temporarily set the language context to that which the
- # administrators are expecting.
- with i18n.using_language(mlist.preferred_language):
- language = mlist.preferred_language
- charset = GetCharSet(language)
- # We need to regenerate or re-translate a few values in the
- # substitution dictionary.
- #d['reason'] = _(reason) # XXX reason
- substitutions['subject'] = original_subject
- # craft the admin notification message and deliver it
- subject = _(
- '$mlist.fqdn_listname post from $sender requires approval')
- nmsg = UserNotification(mlist.owner_address,
- mlist.owner_address,
- subject, lang=language)
- nmsg.set_type('multipart/mixed')
- text = MIMEText(
- maketext('postauth.txt', substitutions,
- raw=True, mlist=mlist),
- _charset=charset)
- dmsg = MIMEText(wrap(_("""\
-If you reply to this message, keeping the Subject: header intact, Mailman will
-discard the held message. Do this if the message is spam. If you reply to
-this message and include an Approved: header with the list password in it, the
-message will be approved for posting to the list. The Approved: header can
-also appear in the first line of the body of the reply.""")),
- _charset=GetCharSet(language))
- dmsg['Subject'] = 'confirm ' + token
- dmsg['Sender'] = mlist.request_address
- dmsg['From'] = mlist.request_address
- dmsg['Date'] = formatdate(localtime=True)
- dmsg['Message-ID'] = make_msgid()
- nmsg.attach(text)
- nmsg.attach(MIMEMessage(msg))
- nmsg.attach(MIMEMessage(dmsg))
- nmsg.send(mlist, **dict(tomoderators=True))
- # Log the held message
- # XXX reason
- reason = 'n/a'
- log.info('HOLD: %s post from %s held, message-id=%s: %s',
- mlist.fqdn_listname, sender,
- msg.get('message-id', 'n/a'), reason)
diff --git a/mailman/chains/reject.py b/mailman/chains/reject.py
deleted file mode 100644
index 3faf563da..000000000
--- a/mailman/chains/reject.py
+++ /dev/null
@@ -1,59 +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 .
-
-"""The terminal 'reject' chain."""
-
-from __future__ import absolute_import, unicode_literals
-
-__metaclass__ = type
-__all__ = [
- 'RejectChain',
- ]
-
-
-import logging
-
-from mailman.app.bounces import bounce_message
-from mailman.chains.base import TerminalChainBase
-from mailman.i18n import _
-
-
-log = logging.getLogger('mailman.vette')
-SEMISPACE = '; '
-
-
-
-class RejectChain(TerminalChainBase):
- """Reject/bounce a message."""
-
- name = 'reject'
- description = _('Reject/bounce a message and stop processing.')
-
- def _process(self, mlist, msg, msgdata):
- """See `TerminalChainBase`."""
- # Start by decorating the message with a header that contains a list
- # of all the rules that matched. These metadata could be None or an
- # empty list.
- rule_hits = msgdata.get('rule_hits')
- if rule_hits:
- msg['X-Mailman-Rule-Hits'] = SEMISPACE.join(rule_hits)
- rule_misses = msgdata.get('rule_misses')
- if rule_misses:
- msg['X-Mailman-Rule-Misses'] = SEMISPACE.join(rule_misses)
- # XXX Exception/reason
- bounce_message(mlist, msg)
- log.info('REJECT: %s', msg.get('message-id', 'n/a'))
diff --git a/mailman/commands/__init__.py b/mailman/commands/__init__.py
deleted file mode 100644
index 6e89bc6da..000000000
--- a/mailman/commands/__init__.py
+++ /dev/null
@@ -1,22 +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 .
-
-__all__ = [
- 'echo',
- 'end',
- 'join',
- ]
diff --git a/mailman/commands/cmd_confirm.py b/mailman/commands/cmd_confirm.py
deleted file mode 100644
index b5e4182bd..000000000
--- a/mailman/commands/cmd_confirm.py
+++ /dev/null
@@ -1,98 +0,0 @@
-# Copyright (C) 2002-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 .
-
-"""
- confirm
- Confirm an action. The confirmation-string is required and should be
- supplied by a mailback confirmation notice.
-"""
-
-from mailman import Errors
-from mailman import Pending
-from mailman.config import config
-from mailman.i18n import _
-
-STOP = 1
-
-
-
-def gethelp(mlist):
- return _(__doc__)
-
-
-
-def process(res, args):
- mlist = res.mlist
- if len(args) <> 1:
- res.results.append(_('Usage:'))
- res.results.append(gethelp(mlist))
- return STOP
- cookie = args[0]
- try:
- results = mlist.ProcessConfirmation(cookie, res.msg)
- except Errors.MMBadConfirmation, e:
- # Express in approximate days
- days = int(config.PENDING_REQUEST_LIFE / config.days(1) + 0.5)
- res.results.append(_("""\
-Invalid confirmation string. Note that confirmation strings expire
-approximately %(days)s days after the initial subscription request. If your
-confirmation has expired, please try to re-submit your original request or
-message."""))
- except Errors.MMNeedApproval:
- res.results.append(_("""\
-Your request has been forwarded to the list moderator for approval."""))
- except Errors.MMAlreadyAMember:
- # Some other subscription request for this address has
- # already succeeded.
- res.results.append(_('You are already subscribed.'))
- except Errors.NotAMemberError:
- # They've already been unsubscribed
- res.results.append(_("""\
-You are not currently a member. Have you already unsubscribed or changed
-your email address?"""))
- except Errors.MembershipIsBanned:
- owneraddr = mlist.GetOwnerEmail()
- res.results.append(_("""\
-You are currently banned from subscribing to this list. If you think this
-restriction is erroneous, please contact the list owners at
-%(owneraddr)s."""))
- except Errors.HostileSubscriptionError:
- res.results.append(_("""\
-You were not invited to this mailing list. The invitation has been discarded,
-and both list administrators have been alerted."""))
- except Errors.MMBadPasswordError:
- res.results.append(_("""\
-Bad approval password given. Held message is still being held."""))
- else:
- if ((results[0] == Pending.SUBSCRIPTION and mlist.send_welcome_msg)
- or
- (results[0] == Pending.UNSUBSCRIPTION and mlist.send_goodbye_msg)):
- # We don't also need to send a confirmation succeeded message
- res.respond = 0
- else:
- res.results.append(_('Confirmation succeeded'))
- # Consume any other confirmation strings with the same cookie so
- # the user doesn't get a misleading "unprocessed" message.
- match = 'confirm ' + cookie
- unprocessed = []
- for line in res.commands:
- if line.lstrip() == match:
- continue
- unprocessed.append(line)
- res.commands = unprocessed
- # Process just one confirmation string per message
- return STOP
diff --git a/mailman/commands/cmd_help.py b/mailman/commands/cmd_help.py
deleted file mode 100644
index eeee33ca7..000000000
--- a/mailman/commands/cmd_help.py
+++ /dev/null
@@ -1,93 +0,0 @@
-# Copyright (C) 2002-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 .
-
-"""
- help
- Print this help message.
-"""
-
-import os
-import sys
-
-from mailman import Utils
-from mailman.config import config
-from mailman.i18n import _
-
-EMPTYSTRING = ''
-
-
-
-def gethelp(mlist):
- return _(__doc__)
-
-
-
-def process(res, args):
- # Get the help text introduction
- mlist = res.mlist
- # Since this message is personalized, add some useful information if the
- # address requesting help is a member of the list.
- msg = res.msg
- for sender in msg.get_senders():
- if mlist.isMember(sender):
- memberurl = mlist.GetOptionsURL(sender, absolute=1)
- urlhelp = _(
- 'You can access your personal options via the following url:')
- res.results.append(urlhelp)
- res.results.append(memberurl)
- # Get a blank line in the output.
- res.results.append('')
- break
- # build the specific command helps from the module docstrings
- modhelps = {}
- import mailman.Commands
- path = os.path.dirname(os.path.abspath(mailman.Commands.__file__))
- for file in os.listdir(path):
- if not file.startswith('cmd_') or not file.endswith('.py'):
- continue
- module = os.path.splitext(file)[0]
- modname = 'mailman.Commands.' + module
- try:
- __import__(modname)
- except ImportError:
- continue
- cmdname = module[4:]
- help = None
- if hasattr(sys.modules[modname], 'gethelp'):
- help = sys.modules[modname].gethelp(mlist)
- if help:
- modhelps[cmdname] = help
- # Now sort the command helps
- helptext = []
- keys = modhelps.keys()
- keys.sort()
- for cmd in keys:
- helptext.append(modhelps[cmd])
- commands = EMPTYSTRING.join(helptext)
- # Now craft the response
- helptext = Utils.maketext(
- 'help.txt',
- {'listname' : mlist.real_name,
- 'version' : config.VERSION,
- 'listinfo_url': mlist.GetScriptURL('listinfo', absolute=1),
- 'requestaddr' : mlist.GetRequestEmail(),
- 'adminaddr' : mlist.GetOwnerEmail(),
- 'commands' : commands,
- }, mlist=mlist, lang=res.msgdata['lang'], raw=1)
- # Now add to the response
- res.results.append('help')
- res.results.append(helptext)
diff --git a/mailman/commands/cmd_info.py b/mailman/commands/cmd_info.py
deleted file mode 100644
index 3bdea178f..000000000
--- a/mailman/commands/cmd_info.py
+++ /dev/null
@@ -1,50 +0,0 @@
-# Copyright (C) 2002-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 .
-
-"""
- info
- Get information about this mailing list.
-"""
-
-from mailman.i18n import _
-
-STOP = 1
-
-
-
-def gethelp(mlist):
- return _(__doc__)
-
-
-
-def process(res, args):
- mlist = res.mlist
- if args:
- res.results.append(gethelp(mlist))
- return STOP
- listname = mlist.real_name
- description = mlist.description or _('n/a')
- postaddr = mlist.posting_address
- requestaddr = mlist.request_address
- owneraddr = mlist.owner_address
- listurl = mlist.script_url('listinfo')
- res.results.append(_('List name: %(listname)s'))
- res.results.append(_('Description: %(description)s'))
- res.results.append(_('Postings to: %(postaddr)s'))
- res.results.append(_('List Helpbot: %(requestaddr)s'))
- res.results.append(_('List Owners: %(owneraddr)s'))
- res.results.append(_('More information: %(listurl)s'))
diff --git a/mailman/commands/cmd_leave.py b/mailman/commands/cmd_leave.py
deleted file mode 100644
index 5844824f7..000000000
--- a/mailman/commands/cmd_leave.py
+++ /dev/null
@@ -1,21 +0,0 @@
-# Copyright (C) 2002-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 .
-
-"""The `leave' command is synonymous with `unsubscribe'.
-"""
-
-from mailman.Commands.cmd_unsubscribe import process
diff --git a/mailman/commands/cmd_lists.py b/mailman/commands/cmd_lists.py
deleted file mode 100644
index 234ef46fc..000000000
--- a/mailman/commands/cmd_lists.py
+++ /dev/null
@@ -1,65 +0,0 @@
-# Copyright (C) 2002-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 .
-
-"""
- lists
- See a list of the public mailing lists on this GNU Mailman server.
-"""
-
-from mailman.MailList import MailList
-from mailman.config import config
-from mailman.i18n import _
-
-
-STOP = 1
-
-
-
-def gethelp(mlist):
- return _(__doc__)
-
-
-
-def process(res, args):
- mlist = res.mlist
- if args:
- res.results.append(_('Usage:'))
- res.results.append(gethelp(mlist))
- return STOP
- hostname = mlist.host_name
- res.results.append(_('Public mailing lists at %(hostname)s:'))
- i = 1
- for listname in sorted(config.list_manager.names):
- if listname == mlist.internal_name():
- xlist = mlist
- else:
- xlist = MailList(listname, lock=0)
- # We can mention this list if you already know about it
- if not xlist.advertised and xlist is not mlist:
- continue
- # Skip the list if it isn't in the same virtual domain.
- if xlist.host_name <> mlist.host_name:
- continue
- realname = xlist.real_name
- description = xlist.description or _('n/a')
- requestaddr = xlist.GetRequestEmail()
- if i > 1:
- res.results.append('')
- res.results.append(_('%(i)3d. List name: %(realname)s'))
- res.results.append(_(' Description: %(description)s'))
- res.results.append(_(' Requests to: %(requestaddr)s'))
- i += 1
diff --git a/mailman/commands/cmd_password.py b/mailman/commands/cmd_password.py
deleted file mode 100644
index 545da0cb5..000000000
--- a/mailman/commands/cmd_password.py
+++ /dev/null
@@ -1,123 +0,0 @@
-# Copyright (C) 2002-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 .
-
-"""
- password [] [address=]
- Retrieve or change your password. With no arguments, this returns
- your current password. With arguments and
- you can change your password.
-
- If you're posting from an address other than your membership address,
- specify your membership address with `address=' (no brackets
- around the email address, and no quotes!). Note that in this case the
- response is always sent to the subscribed address.
-"""
-
-from email.Utils import parseaddr
-
-from mailman.config import config
-from mailman.i18n import _
-
-STOP = 1
-
-
-
-def gethelp(mlist):
- return _(__doc__)
-
-
-
-def process(res, args):
- mlist = res.mlist
- address = None
- if not args:
- # They just want to get their existing password
- realname, address = parseaddr(res.msg['from'])
- if mlist.isMember(address):
- password = mlist.getMemberPassword(address)
- res.results.append(_('Your password is: %(password)s'))
- # Prohibit multiple password retrievals.
- return STOP
- else:
- listname = mlist.real_name
- res.results.append(
- _('You are not a member of the %(listname)s mailing list'))
- return STOP
- elif len(args) == 1 and args[0].startswith('address='):
- # They want their password, but they're posting from a different
- # address. We /must/ return the password to the subscribed address.
- address = args[0][8:]
- res.returnaddr = address
- if mlist.isMember(address):
- password = mlist.getMemberPassword(address)
- res.results.append(_('Your password is: %(password)s'))
- # Prohibit multiple password retrievals.
- return STOP
- else:
- listname = mlist.real_name
- res.results.append(
- _('You are not a member of the %(listname)s mailing list'))
- return STOP
- elif len(args) == 2:
- # They are changing their password
- oldpasswd = args[0]
- newpasswd = args[1]
- realname, address = parseaddr(res.msg['from'])
- if mlist.isMember(address):
- if mlist.Authenticate((config.AuthUser, config.AuthListAdmin),
- oldpasswd, address):
- mlist.setMemberPassword(address, newpasswd)
- res.results.append(_('Password successfully changed.'))
- else:
- res.results.append(_("""\
-You did not give the correct old password, so your password has not been
-changed. Use the no argument version of the password command to retrieve your
-current password, then try again."""))
- res.results.append(_('\nUsage:'))
- res.results.append(gethelp(mlist))
- return STOP
- else:
- listname = mlist.real_name
- res.results.append(
- _('You are not a member of the %(listname)s mailing list'))
- return STOP
- elif len(args) == 3 and args[2].startswith('address='):
- # They want to change their password, and they're sending this from a
- # different address than what they're subscribed with. Be sure the
- # response goes to the subscribed address.
- oldpasswd = args[0]
- newpasswd = args[1]
- address = args[2][8:]
- res.returnaddr = address
- if mlist.isMember(address):
- if mlist.Authenticate((config.AuthUser, config.AuthListAdmin),
- oldpasswd, address):
- mlist.setMemberPassword(address, newpasswd)
- res.results.append(_('Password successfully changed.'))
- else:
- res.results.append(_("""\
-You did not give the correct old password, so your password has not been
-changed. Use the no argument version of the password command to retrieve your
-current password, then try again."""))
- res.results.append(_('\nUsage:'))
- res.results.append(gethelp(mlist))
- return STOP
- else:
- listname = mlist.real_name
- res.results.append(
- _('You are not a member of the %(listname)s mailing list'))
- return STOP
diff --git a/mailman/commands/cmd_remove.py b/mailman/commands/cmd_remove.py
deleted file mode 100644
index 8f3ce9669..000000000
--- a/mailman/commands/cmd_remove.py
+++ /dev/null
@@ -1,21 +0,0 @@
-# Copyright (C) 2002-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 .
-
-"""The `remove' command is synonymous with `unsubscribe'.
-"""
-
-from mailman.Commands.cmd_unsubscribe import process
diff --git a/mailman/commands/cmd_set.py b/mailman/commands/cmd_set.py
deleted file mode 100644
index 020bc3636..000000000
--- a/mailman/commands/cmd_set.py
+++ /dev/null
@@ -1,360 +0,0 @@
-# Copyright (C) 2002-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 .
-
-from email.Utils import parseaddr, formatdate
-
-from mailman import Errors
-from mailman import MemberAdaptor
-from mailman import i18n
-from mailman.config import config
-
-def _(s): return s
-
-OVERVIEW = _("""
- set ...
- Set or view your membership options.
-
- Use `set help' (without the quotes) to get a more detailed list of the
- options you can change.
-
- Use `set show' (without the quotes) to view your current option
- settings.
-""")
-
-DETAILS = _("""
- set help
- Show this detailed help.
-
- set show [address=]
- View your current option settings. If you're posting from an address
- other than your membership address, specify your membership address
- with `address=' (no brackets around the email address, and no
- quotes!).
-
- set authenticate [address=]
- To set any of your options, you must include this command first, along
- with your membership password. If you're posting from an address
- other than your membership address, specify your membership address
- with `address=' (no brackets around the email address, and no
- quotes!).
-
- set ack on
- set ack off
- When the `ack' option is turned on, you will receive an
- acknowledgement message whenever you post a message to the list.
-
- set digest plain
- set digest mime
- set digest off
- When the `digest' option is turned off, you will receive postings
- immediately when they are posted. Use `set digest plain' if instead
- you want to receive postings bundled into a plain text digest
- (i.e. RFC 1153 digest). Use `set digest mime' if instead you want to
- receive postings bundled together into a MIME digest.
-
- set delivery on
- set delivery off
- Turn delivery on or off. This does not unsubscribe you, but instead
- tells Mailman not to deliver messages to you for now. This is useful
- if you're going on vacation. Be sure to use `set delivery on' when
- you return from vacation!
-
- set myposts on
- set myposts off
- Use `set myposts off' to not receive copies of messages you post to
- the list. This has no effect if you're receiving digests.
-
- set hide on
- set hide off
- Use `set hide on' to conceal your email address when people request
- the membership list.
-
- set duplicates on
- set duplicates off
- Use `set duplicates off' if you want Mailman to not send you messages
- if your address is explicitly mentioned in the To: or Cc: fields of
- the message. This can reduce the number of duplicate postings you
- will receive.
-
- set reminders on
- set reminders off
- Use `set reminders off' if you want to disable the monthly password
- reminder for this mailing list.
-""")
-
-_ = i18n._
-
-STOP = 1
-
-
-
-def gethelp(mlist):
- return _(OVERVIEW)
-
-
-
-class SetCommands:
- def __init__(self):
- self.__address = None
- self.__authok = 0
-
- def process(self, res, args):
- if not args:
- res.results.append(_(DETAILS))
- return STOP
- subcmd = args.pop(0)
- methname = 'set_' + subcmd
- method = getattr(self, methname, None)
- if method is None:
- res.results.append(_('Bad set command: %(subcmd)s'))
- res.results.append(_(DETAILS))
- return STOP
- return method(res, args)
-
- def set_help(self, res, args=1):
- res.results.append(_(DETAILS))
- if args:
- return STOP
-
- def _usage(self, res):
- res.results.append(_('Usage:'))
- return self.set_help(res)
-
- def set_show(self, res, args):
- mlist = res.mlist
- if not args:
- realname, address = parseaddr(res.msg['from'])
- elif len(args) == 1 and args[0].startswith('address='):
- # Send the results to the address, not the From: dude
- address = args[0][8:]
- res.returnaddr = address
- else:
- return self._usage(res)
- if not mlist.isMember(address):
- listname = mlist.real_name
- res.results.append(
- _('You are not a member of the %(listname)s mailing list'))
- return STOP
- res.results.append(_('Your current option settings:'))
- opt = mlist.getMemberOption(address, config.AcknowledgePosts)
- onoff = opt and _('on') or _('off')
- res.results.append(_(' ack %(onoff)s'))
- # Digests are a special ternary value
- digestsp = mlist.getMemberOption(address, config.Digests)
- if digestsp:
- plainp = mlist.getMemberOption(address, config.DisableMime)
- if plainp:
- res.results.append(_(' digest plain'))
- else:
- res.results.append(_(' digest mime'))
- else:
- res.results.append(_(' digest off'))
- # If their membership is disabled, let them know why
- status = mlist.getDeliveryStatus(address)
- how = None
- if status == MemberAdaptor.ENABLED:
- status = _('delivery on')
- elif status == MemberAdaptor.BYUSER:
- status = _('delivery off')
- how = _('by you')
- elif status == MemberAdaptor.BYADMIN:
- status = _('delivery off')
- how = _('by the admin')
- elif status == MemberAdaptor.BYBOUNCE:
- status = _('delivery off')
- how = _('due to bounces')
- else:
- assert status == MemberAdaptor.UNKNOWN
- status = _('delivery off')
- how = _('for unknown reasons')
- changetime = mlist.getDeliveryStatusChangeTime(address)
- if how and changetime > 0:
- date = formatdate(changetime)
- res.results.append(_(' %(status)s (%(how)s on %(date)s)'))
- else:
- res.results.append(' ' + status)
- opt = mlist.getMemberOption(address, config.DontReceiveOwnPosts)
- # sense is reversed
- onoff = (not opt) and _('on') or _('off')
- res.results.append(_(' myposts %(onoff)s'))
- opt = mlist.getMemberOption(address, config.ConcealSubscription)
- onoff = opt and _('on') or _('off')
- res.results.append(_(' hide %(onoff)s'))
- opt = mlist.getMemberOption(address, config.DontReceiveDuplicates)
- # sense is reversed
- onoff = (not opt) and _('on') or _('off')
- res.results.append(_(' duplicates %(onoff)s'))
- opt = mlist.getMemberOption(address, config.SuppressPasswordReminder)
- # sense is reversed
- onoff = (not opt) and _('on') or _('off')
- res.results.append(_(' reminders %(onoff)s'))
-
- def set_authenticate(self, res, args):
- mlist = res.mlist
- if len(args) == 1:
- realname, address = parseaddr(res.msg['from'])
- password = args[0]
- elif len(args) == 2 and args[1].startswith('address='):
- password = args[0]
- address = args[1][8:]
- else:
- return self._usage(res)
- # See if the password matches
- if not mlist.isMember(address):
- listname = mlist.real_name
- res.results.append(
- _('You are not a member of the %(listname)s mailing list'))
- return STOP
- if not mlist.Authenticate((config.AuthUser,
- config.AuthListAdmin),
- password, address):
- res.results.append(_('You did not give the correct password'))
- return STOP
- self.__authok = 1
- self.__address = address
-
- def _status(self, res, arg):
- status = arg.lower()
- if status == 'on':
- flag = 1
- elif status == 'off':
- flag = 0
- else:
- res.results.append(_('Bad argument: %(arg)s'))
- self._usage(res)
- return -1
- # See if we're authenticated
- if not self.__authok:
- res.results.append(_('Not authenticated'))
- self._usage(res)
- return -1
- return flag
-
- def set_ack(self, res, args):
- mlist = res.mlist
- if len(args) <> 1:
- return self._usage(res)
- status = self._status(res, args[0])
- if status < 0:
- return STOP
- mlist.setMemberOption(self.__address, config.AcknowledgePosts, status)
- res.results.append(_('ack option set'))
-
- def set_digest(self, res, args):
- mlist = res.mlist
- if len(args) <> 1:
- return self._usage(res)
- if not self.__authok:
- res.results.append(_('Not authenticated'))
- self._usage(res)
- return STOP
- arg = args[0].lower()
- if arg == 'off':
- try:
- mlist.setMemberOption(self.__address, config.Digests, 0)
- except Errors.AlreadyReceivingRegularDeliveries:
- pass
- elif arg == 'plain':
- try:
- mlist.setMemberOption(self.__address, config.Digests, 1)
- except Errors.AlreadyReceivingDigests:
- pass
- mlist.setMemberOption(self.__address, config.DisableMime, 1)
- elif arg == 'mime':
- try:
- mlist.setMemberOption(self.__address, config.Digests, 1)
- except Errors.AlreadyReceivingDigests:
- pass
- mlist.setMemberOption(self.__address, config.DisableMime, 0)
- else:
- res.results.append(_('Bad argument: %(arg)s'))
- self._usage(res)
- return STOP
- res.results.append(_('digest option set'))
-
- def set_delivery(self, res, args):
- mlist = res.mlist
- if len(args) <> 1:
- return self._usage(res)
- status = self._status(res, args[0])
- if status < 0:
- return STOP
- # Delivery status is handled differently than other options. If
- # status is true (set delivery on), then we enable delivery.
- # Otherwise, we have to use the setDeliveryStatus() interface to
- # specify that delivery was disabled by the user.
- if status:
- mlist.setDeliveryStatus(self.__address, MemberAdaptor.ENABLED)
- res.results.append(_('delivery enabled'))
- else:
- mlist.setDeliveryStatus(self.__address, MemberAdaptor.BYUSER)
- res.results.append(_('delivery disabled by user'))
-
- def set_myposts(self, res, args):
- mlist = res.mlist
- if len(args) <> 1:
- return self._usage(res)
- status = self._status(res, args[0])
- if status < 0:
- return STOP
- # sense is reversed
- mlist.setMemberOption(self.__address, config.DontReceiveOwnPosts,
- not status)
- res.results.append(_('myposts option set'))
-
- def set_hide(self, res, args):
- mlist = res.mlist
- if len(args) <> 1:
- return self._usage(res)
- status = self._status(res, args[0])
- if status < 0:
- return STOP
- mlist.setMemberOption(self.__address, config.ConcealSubscription,
- status)
- res.results.append(_('hide option set'))
-
- def set_duplicates(self, res, args):
- mlist = res.mlist
- if len(args) <> 1:
- return self._usage(res)
- status = self._status(res, args[0])
- if status < 0:
- return STOP
- # sense is reversed
- mlist.setMemberOption(self.__address, config.DontReceiveDuplicates,
- not status)
- res.results.append(_('duplicates option set'))
-
- def set_reminders(self, res, args):
- mlist = res.mlist
- if len(args) <> 1:
- return self._usage(res)
- status = self._status(res, args[0])
- if status < 0:
- return STOP
- # sense is reversed
- mlist.setMemberOption(self.__address, config.SuppressPasswordReminder,
- not status)
- res.results.append(_('reminder option set'))
-
-
-
-def process(res, args):
- # We need to keep some state between set commands
- if not getattr(res, 'setstate', None):
- res.setstate = SetCommands()
- res.setstate.process(res, args)
diff --git a/mailman/commands/cmd_unsubscribe.py b/mailman/commands/cmd_unsubscribe.py
deleted file mode 100644
index 456b8089d..000000000
--- a/mailman/commands/cmd_unsubscribe.py
+++ /dev/null
@@ -1,88 +0,0 @@
-# Copyright (C) 2002-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 .
-
-"""
- unsubscribe [password] [address=]
- Unsubscribe from the mailing list. If given, your password must match
- your current password. If omitted, a confirmation email will be sent
- to the unsubscribing address. If you wish to unsubscribe an address
- other than the address you sent this request from, you may specify
- `address=' (no brackets around the email address, and no
- quotes!)
-"""
-
-from email.Utils import parseaddr
-
-from mailman import Errors
-from mailman.i18n import _
-
-STOP = 1
-
-
-
-def gethelp(mlist):
- return _(__doc__)
-
-
-
-def process(res, args):
- mlist = res.mlist
- password = None
- address = None
- argnum = 0
- for arg in args:
- if arg.startswith('address='):
- address = arg[8:]
- elif argnum == 0:
- password = arg
- else:
- res.results.append(_('Usage:'))
- res.results.append(gethelp(mlist))
- return STOP
- argnum += 1
- # Fill in empty defaults
- if address is None:
- realname, address = parseaddr(res.msg['from'])
- if not mlist.isMember(address):
- listname = mlist.real_name
- res.results.append(
- _('%(address)s is not a member of the %(listname)s mailing list'))
- return STOP
- # If we're doing admin-approved unsubs, don't worry about the password
- if mlist.unsubscribe_policy:
- try:
- mlist.DeleteMember(address, 'mailcmd')
- except Errors.MMNeedApproval:
- res.results.append(_("""\
-Your unsubscription request has been forwarded to the list administrator for
-approval."""))
- elif password is None:
- # No password was given, so we need to do a mailback confirmation
- # instead of unsubscribing them here.
- cpaddr = mlist.getMemberCPAddress(address)
- mlist.ConfirmUnsubscription(cpaddr)
- # We don't also need to send a confirmation to this command
- res.respond = 0
- else:
- # No admin approval is necessary, so we can just delete them if the
- # passwords match.
- oldpw = mlist.getMemberPassword(address)
- if oldpw <> password:
- res.results.append(_('You gave the wrong password'))
- return STOP
- mlist.ApprovedDeleteMember(address, 'mailcmd')
- res.results.append(_('Unsubscription request succeeded.'))
diff --git a/mailman/commands/cmd_who.py b/mailman/commands/cmd_who.py
deleted file mode 100644
index 6c66610b3..000000000
--- a/mailman/commands/cmd_who.py
+++ /dev/null
@@ -1,152 +0,0 @@
-# Copyright (C) 2002-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 .
-
-from email.Utils import parseaddr
-
-from mailman import i18n
-from mailman.config import config
-
-STOP = 1
-
-def _(s): return s
-
-PUBLICHELP = _("""
- who
- See the non-hidden members of this mailing list.
- who password
- See everyone who is on this mailing list. The password is the
- list's admin or moderator password.
-""")
-
-MEMBERSONLYHELP = _("""
- who password [address=]
- See the non-hidden members of this mailing list. The roster is
- limited to list members only, and you must supply your membership
- password to retrieve it. If you're posting from an address other
- than your membership address, specify your membership address with
- `address=' (no brackets around the email address, and no
- quotes!). If you provide the list's admin or moderator password,
- hidden members will be included.
-""")
-
-ADMINONLYHELP = _("""
- who password
- See everyone who is on this mailing list. The roster is limited to
- list administrators and moderators only; you must supply the list
- admin or moderator password to retrieve the roster.
-""")
-
-_ = i18n._
-
-
-
-def gethelp(mlist):
- if mlist.private_roster == 0:
- return _(PUBLICHELP)
- elif mlist.private_roster == 1:
- return _(MEMBERSONLYHELP)
- elif mlist.private_roster == 2:
- return _(ADMINONLYHELP)
-
-
-def usage(res):
- res.results.append(_('Usage:'))
- res.results.append(gethelp(res.mlist))
-
-
-
-def process(res, args):
- mlist = res.mlist
- address = None
- password = None
- ok = False
- full = False
- if mlist.private_roster == 0:
- # Public rosters
- if args:
- if len(args) == 1:
- if mlist.Authenticate((config.AuthListModerator,
- config.AuthListAdmin),
- args[0]):
- full = True
- else:
- usage(res)
- return STOP
- else:
- usage(res)
- return STOP
- ok = True
- elif mlist.private_roster == 1:
- # List members only
- if len(args) == 1:
- password = args[0]
- realname, address = parseaddr(res.msg['from'])
- elif len(args) == 2 and args[1].startswith('address='):
- password = args[0]
- address = args[1][8:]
- else:
- usage(res)
- return STOP
- if mlist.isMember(address) and mlist.Authenticate(
- (config.AuthUser,
- config.AuthListModerator,
- config.AuthListAdmin),
- password, address):
- # Then
- ok = True
- if mlist.Authenticate(
- (config.AuthListModerator,
- config.AuthListAdmin),
- password):
- # Then
- ok = full = True
- else:
- # Admin only
- if len(args) <> 1:
- usage(res)
- return STOP
- if mlist.Authenticate((config.AuthListModerator,
- config.AuthListAdmin),
- args[0]):
- ok = full = True
- if not ok:
- res.results.append(
- _('You are not allowed to retrieve the list membership.'))
- return STOP
- # It's okay for this person to see the list membership
- dmembers = mlist.getDigestMemberKeys()
- rmembers = mlist.getRegularMemberKeys()
- if not dmembers and not rmembers:
- res.results.append(_('This list has no members.'))
- return
- # Convenience function
- def addmembers(members):
- for member in members:
- if not full and mlist.getMemberOption(member,
- config.ConcealSubscription):
- continue
- realname = mlist.getMemberName(member)
- if realname:
- res.results.append(' %s (%s)' % (member, realname))
- else:
- res.results.append(' %s' % member)
- if rmembers:
- res.results.append(_('Non-digest (regular) members:'))
- addmembers(rmembers)
- if dmembers:
- res.results.append(_('Digest members:'))
- addmembers(dmembers)
diff --git a/mailman/commands/docs/echo.txt b/mailman/commands/docs/echo.txt
deleted file mode 100644
index 181cc58c8..000000000
--- a/mailman/commands/docs/echo.txt
+++ /dev/null
@@ -1,30 +0,0 @@
-The 'echo' command
-==================
-
-The mail command 'echo' simply replies with the original command and arguments
-to the sender.
-
- >>> command = config.commands['echo']
- >>> command.name
- 'echo'
- >>> command.argument_description
- '[args]'
- >>> command.description
- u'Echo an acknowledgement. Arguments are return unchanged.'
-
-The original message is ignored, but the results receive the echoed command.
-
- >>> from mailman.app.lifecycle import create_list
- >>> mlist = create_list(u'test@example.com')
-
- >>> from mailman.queue.command import Results
- >>> results = Results()
-
- >>> from mailman.Message import Message
- >>> print command.process(mlist, Message(), {}, ('foo', 'bar'), results)
- ContinueProcessing.yes
- >>> print unicode(results)
- The results of your email command are provided below.
-
- echo foo bar
-
diff --git a/mailman/commands/docs/end.txt b/mailman/commands/docs/end.txt
deleted file mode 100644
index 4f6af26cb..000000000
--- a/mailman/commands/docs/end.txt
+++ /dev/null
@@ -1,37 +0,0 @@
-The 'end' command
-=================
-
-The mail command processor recognized an 'end' command which tells it to stop
-processing email messages.
-
- >>> command = config.commands['end']
- >>> command.name
- 'end'
- >>> command.description
- u'Stop processing commands.'
-
-The 'end' command takes no arguments.
-
- >>> command.argument_description
- ''
-
-The command itself is fairly simple; it just stops command processing, and the
-message isn't even looked at.
-
- >>> from mailman.app.lifecycle import create_list
- >>> mlist = create_list(u'test@example.com')
- >>> from mailman.Message import Message
- >>> print command.process(mlist, Message(), {}, (), None)
- ContinueProcessing.no
-
-The 'stop' command is a synonym for 'end'.
-
- >>> command = config.commands['stop']
- >>> command.name
- 'stop'
- >>> command.description
- u'Stop processing commands.'
- >>> command.argument_description
- ''
- >>> print command.process(mlist, Message(), {}, (), None)
- ContinueProcessing.no
diff --git a/mailman/commands/docs/join.txt b/mailman/commands/docs/join.txt
deleted file mode 100644
index 9b85e816c..000000000
--- a/mailman/commands/docs/join.txt
+++ /dev/null
@@ -1,170 +0,0 @@
-The 'join' command
-==================
-
-The mail command 'join' subscribes an email address to the mailing list.
-'subscribe' is an alias for 'join'.
-
- >>> command = config.commands['join']
- >>> print command.name
- join
- >>> print command.description
- Join this mailing list. You will be asked to confirm your subscription
- request and you may be issued a provisional password.
-
- By using the 'digest' option, you can specify whether you want digest
- delivery or not. If not specified, the mailing list's default will be
- used. You can also subscribe an alternative address by using the
- 'address' option. For example:
-
- join address=myotheraddress@example.com
-
- >>> print command.argument_description
- [digest=] [address=]
-
-
-No address to join
-------------------
-
- >>> from mailman.Message import Message
- >>> from mailman.app.lifecycle import create_list
- >>> from mailman.queue.command import Results
- >>> mlist = create_list(u'alpha@example.com')
-
-When no address argument is given, the message's From address will be used.
-If that's missing though, then an error is returned.
-
- >>> results = Results()
- >>> print command.process(mlist, Message(), {}, (), results)
- ContinueProcessing.no
- >>> print unicode(results)
- The results of your email command are provided below.
-
- join: No valid address found to subscribe
-
-
-The 'subscribe' command is an alias.
-
- >>> subscribe = config.commands['subscribe']
- >>> print subscribe.name
- subscribe
- >>> results = Results()
- >>> print subscribe.process(mlist, Message(), {}, (), results)
- ContinueProcessing.no
- >>> print unicode(results)
- The results of your email command are provided below.
-
- subscribe: No valid address found to subscribe
-
-
-
-Joining the sender
-------------------
-
-When the message has a From field, that address will be subscribed.
-
- >>> msg = message_from_string("""\
- ... From: Anne Person
- ...
- ... """)
- >>> results = Results()
- >>> print command.process(mlist, msg, {}, (), results)
- ContinueProcessing.yes
- >>> print unicode(results)
- The results of your email command are provided below.
-
- Confirmation email sent to Anne Person
-
-
-Anne is not yet a member because she must confirm her subscription request
-first.
-
- >>> print config.db.user_manager.get_user(u'anne@example.com')
- None
-
-Mailman has sent her the confirmation message.
-
- >>> virginq = config.switchboards['virgin']
- >>> qmsg, qdata = virginq.dequeue(virginq.files[0])
- >>> print qmsg.as_string()
- MIME-Version: 1.0
- ...
- Subject: confirm ...
- From: confirm-...@example.com
- To: anne@example.com
- ...
-
- Email Address Registration Confirmation
-
- Hello, this is the GNU Mailman server at example.com.
-
- We have received a registration request for the email address
-
- anne@example.com
-
- Before you can start using GNU Mailman at this site, you must first
- confirm that this is your email address. You can do this by replying to
- this message, keeping the Subject header intact. Or you can visit this
- web page
-
- http://lists.example.com/confirm/...
-
- If you do not wish to register this email address simply disregard this
- message. If you think you are being maliciously subscribed to the list, or
- have any other questions, you may contact
-
- postmaster@example.com
-
-
-Once Anne confirms her registration, she will be made a member of the mailing
-list.
-
- >>> token = str(qmsg['subject']).split()[1].strip()
- >>> from mailman.interfaces.registrar import IRegistrar
- >>> registrar = IRegistrar(config.domains['example.com'])
- >>> registrar.confirm(token)
- True
-
- >>> user = config.db.user_manager.get_user(u'anne@example.com')
- >>> print user.real_name
- Anne Person
- >>> list(user.addresses)
- [ [verified] at ...>]
-
-Anne is also now a member of the mailing list.
-
- >>> mlist.members.get_member(u'anne@example.com')
-
- on alpha@example.com as MemberRole.member>
-
-
-Joining a second list
----------------------
-
- >>> mlist_2 = create_list(u'baker@example.com')
- >>> msg = message_from_string("""\
- ... From: Anne Person
- ...
- ... """)
- >>> print command.process(mlist_2, msg, {}, (), Results())
- ContinueProcessing.yes
-
-Anne of course, is still registered.
-
- >>> print config.db.user_manager.get_user(u'anne@example.com')
-
-
-But she is not a member of the mailing list.
-
- >>> print mlist_2.members.get_member(u'anne@example.com')
- None
-
-One Anne confirms this subscription, she becomes a member of the mailing list.
-
- >>> qmsg, qdata = virginq.dequeue(virginq.files[0])
- >>> token = str(qmsg['subject']).split()[1].strip()
- >>> registrar.confirm(token)
- True
-
- >>> print mlist_2.members.get_member(u'anne@example.com')
-
- on baker@example.com as MemberRole.member>
diff --git a/mailman/commands/echo.py b/mailman/commands/echo.py
deleted file mode 100644
index 30590acf8..000000000
--- a/mailman/commands/echo.py
+++ /dev/null
@@ -1,48 +0,0 @@
-# Copyright (C) 2002-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 .
-
-"""The email command 'echo'."""
-
-__metaclass__ = type
-__all__ = [
- 'Echo',
- ]
-
-
-from zope.interface import implements
-
-from mailman.i18n import _
-from mailman.interfaces.command import ContinueProcessing, IEmailCommand
-
-
-SPACE = ' '
-
-
-
-class Echo:
- """The email 'echo' command."""
- implements(IEmailCommand)
-
- name = 'echo'
- argument_description = '[args]'
- description = _(
- 'Echo an acknowledgement. Arguments are return unchanged.')
-
- def process(self, mlist, msg, msgdata, arguments, results):
- """See `IEmailCommand`."""
- print >> results, 'echo', SPACE.join(arguments)
- return ContinueProcessing.yes
diff --git a/mailman/commands/end.py b/mailman/commands/end.py
deleted file mode 100644
index a9298bc92..000000000
--- a/mailman/commands/end.py
+++ /dev/null
@@ -1,51 +0,0 @@
-# Copyright (C) 2002-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 .
-
-"""The email commands 'end' and 'stop'."""
-
-__metaclass__ = type
-__all__ = [
- 'End',
- 'Stop',
- ]
-
-
-from zope.interface import implements
-
-from mailman.i18n import _
-from mailman.interfaces.command import ContinueProcessing, IEmailCommand
-
-
-
-class End:
- """The email 'end' command."""
- implements(IEmailCommand)
-
- name = 'end'
- argument_description = ''
- description = _('Stop processing commands.')
-
- def process(self, mlist, msg, msgdata, arguments, results):
- """See `IEmailCommand`."""
- # Ignore all arguments.
- return ContinueProcessing.no
-
-
-class Stop(End):
- """The email 'stop' command (an alias for 'end')."""
-
- name = 'stop'
diff --git a/mailman/commands/join.py b/mailman/commands/join.py
deleted file mode 100644
index c14f3142b..000000000
--- a/mailman/commands/join.py
+++ /dev/null
@@ -1,126 +0,0 @@
-# Copyright (C) 2002-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 .
-
-"""The email commands 'join' and 'subscribe'."""
-
-__metaclass__ = type
-__all__ = [
- 'Join',
- 'Subscribe',
- ]
-
-
-from email.utils import formataddr, parseaddr
-from zope.interface import implements
-
-from mailman.config import config
-from mailman.i18n import _
-from mailman.interfaces.command import ContinueProcessing, IEmailCommand
-from mailman.interfaces.member import DeliveryMode
-from mailman.interfaces.registrar import IRegistrar
-
-
-
-class Join:
- """The email 'join' command."""
- implements(IEmailCommand)
-
- name = 'join'
- argument_description = '[digest=] [address=]'
- description = _("""\
-Join this mailing list. You will be asked to confirm your subscription
-request and you may be issued a provisional password.
-
-By using the 'digest' option, you can specify whether you want digest delivery
-or not. If not specified, the mailing list's default will be used. You can
-also subscribe an alternative address by using the 'address' option. For
-example:
-
- join address=myotheraddress@example.com
-""")
-
- def process(self, mlist, msg, msgdata, arguments, results):
- """See `IEmailCommand`."""
- # Parse the arguments.
- address, delivery_mode = self._parse_arguments(arguments)
- if address is None:
- real_name, address = parseaddr(msg['from'])
- # Address could be None or the empty string.
- if not address:
- address = msg.get_sender()
- if not address:
- print >> results, _(
- '$self.name: No valid address found to subscribe')
- return ContinueProcessing.no
- domain = config.domains[mlist.host_name]
- registrar = IRegistrar(domain)
- registrar.register(address, real_name, mlist)
- person = formataddr((real_name, address))
- print >> results, _('Confirmation email sent to $person')
- return ContinueProcessing.yes
-
- def _parse_arguments(self, arguments):
- """Parse command arguments.
-
- :param arguments: The sequences of arguments as given to the
- `process()` method.
- :return: address, delivery_mode
- """
- address = None
- delivery_mode = None
- for argument in arguments:
- parts = argument.split('=', 1)
- if parts[0].lower() == 'digest':
- if digest is not None:
- print >> results, self.name, \
- _('duplicate argument: $argument')
- return ContinueProcessing.no
- if len(parts) == 0:
- # We treat just plain 'digest' as 'digest=yes'. We don't
- # yet support the other types of digest delivery.
- delivery_mode = DeliveryMode.mime_digests
- else:
- if parts[1].lower() == 'yes':
- delivery_mode = DeliveryMode.mime_digests
- elif parts[1].lower() == 'no':
- delivery_mode = DeliveryMode.regular
- else:
- print >> results, self.name, \
- _('bad argument: $argument')
- return ContinueProcessing.no
- elif parts[0].lower() == 'address':
- if address is not None:
- print >> results, self.name, \
- _('duplicate argument $argument')
- return ContinueProcessing.no
- if len(parts) == 0:
- print >> results, self.name, \
- _('missing argument value: $argument')
- return ContinueProcessing.no
- if len(parts) > 1:
- print >> results, self.name, \
- _('too many argument values: $argument')
- return ContinueProcessing.no
- address = parts[1]
- return address, delivery_mode
-
-
-
-class Subscribe(Join):
- """The email 'subscribe' command (an alias for 'join')."""
-
- name = 'subscribe'
diff --git a/mailman/config/__init__.py b/mailman/config/__init__.py
deleted file mode 100644
index 11f4f0c80..000000000
--- a/mailman/config/__init__.py
+++ /dev/null
@@ -1,30 +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 .
-
-"""Mailman configuration package."""
-
-from __future__ import absolute_import, unicode_literals
-
-__metaclass__ = type
-__all__ = [
- 'config',
- ]
-
-
-from mailman.config.config import Configuration
-
-config = Configuration()
diff --git a/mailman/config/config.py b/mailman/config/config.py
deleted file mode 100644
index fa359a6f5..000000000
--- a/mailman/config/config.py
+++ /dev/null
@@ -1,206 +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 .
-
-"""Configuration file loading and management."""
-
-from __future__ import absolute_import, unicode_literals
-
-__metaclass__ = type
-__all__ = [
- 'Configuration',
- ]
-
-
-import os
-import sys
-import errno
-import logging
-
-from StringIO import StringIO
-from lazr.config import ConfigSchema, as_boolean
-from pkg_resources import resource_string
-
-from mailman import version
-from mailman.core import errors
-from mailman.domain import Domain
-from mailman.languages import LanguageManager
-from mailman.styles.manager import StyleManager
-from mailman.utilities.filesystem import makedirs
-
-
-SPACE = ' '
-
-
-
-class Configuration(object):
- """The core global configuration object."""
-
- def __init__(self):
- self.domains = {} # email host -> IDomain
- self.switchboards = {}
- self.languages = LanguageManager()
- self.style_manager = StyleManager()
- self.QFILE_SCHEMA_VERSION = version.QFILE_SCHEMA_VERSION
- self._config = None
- self.filename = None
- # Create various registries.
- self.chains = {}
- self.rules = {}
- self.handlers = {}
- self.pipelines = {}
- self.commands = {}
-
- def _clear(self):
- """Clear the cached configuration variables."""
- self.domains.clear()
- self.switchboards.clear()
- self.languages = LanguageManager()
-
- def __getattr__(self, name):
- """Delegate to the configuration object."""
- return getattr(self._config, name)
-
- def load(self, filename=None):
- """Load the configuration from the schema and config files."""
- schema_string = resource_string('mailman.config', 'schema.cfg')
- schema = ConfigSchema('schema.cfg', StringIO(schema_string))
- # If a configuration file was given, load it now too. First, load the
- # absolute minimum default configuration, then if a configuration
- # filename was given by the user, push it.
- config_string = resource_string('mailman.config', 'mailman.cfg')
- self._config = schema.loadFile(StringIO(config_string), 'mailman.cfg')
- if filename is not None:
- self.filename = filename
- with open(filename) as user_config:
- self._config.push(filename, user_config.read())
- self._post_process()
-
- def push(self, config_name, config_string):
- """Push a new configuration onto the stack."""
- self._clear()
- self._config.push(config_name, config_string)
- self._post_process()
-
- def pop(self, config_name):
- """Pop a configuration from the stack."""
- self._clear()
- self._config.pop(config_name)
- self._post_process()
-
- def _post_process(self):
- """Perform post-processing after loading the configuration files."""
- # Set up the domains.
- domains = self._config.getByCategory('domain', [])
- for section in domains:
- domain = Domain(section.email_host, section.base_url,
- section.description, section.contact_address)
- if domain.email_host in self.domains:
- raise errors.BadDomainSpecificationError(
- 'Duplicate email host: %s' % domain.email_host)
- # Make sure there's only one mapping for the url_host
- if domain.url_host in self.domains.values():
- raise errors.BadDomainSpecificationError(
- 'Duplicate url host: %s' % domain.url_host)
- # We'll do the reverse mappings on-demand. There shouldn't be too
- # many virtual hosts that it will really matter that much.
- self.domains[domain.email_host] = domain
- # Set up directories.
- self.BIN_DIR = os.path.abspath(os.path.dirname(sys.argv[0]))
- self.VAR_DIR = var_dir = self._config.mailman.var_dir
- # Now that we've loaded all the configuration files we're going to
- # load, set up some useful directories.
- join = os.path.join
- self.LIST_DATA_DIR = join(var_dir, 'lists')
- self.LOG_DIR = join(var_dir, 'logs')
- self.LOCK_DIR = lockdir = join(var_dir, 'locks')
- self.DATA_DIR = datadir = join(var_dir, 'data')
- self.ETC_DIR = etcdir = join(var_dir, 'etc')
- self.SPAM_DIR = join(var_dir, 'spam')
- self.EXT_DIR = join(var_dir, 'ext')
- self.QUEUE_DIR = join(var_dir, 'qfiles')
- self.MESSAGES_DIR = join(var_dir, 'messages')
- self.PUBLIC_ARCHIVE_FILE_DIR = join(var_dir, 'archives', 'public')
- self.PRIVATE_ARCHIVE_FILE_DIR = join(var_dir, 'archives', 'private')
- # Other useful files
- self.PIDFILE = join(datadir, 'master-qrunner.pid')
- self.SITE_PW_FILE = join(datadir, 'adm.pw')
- self.LISTCREATOR_PW_FILE = join(datadir, 'creator.pw')
- self.CONFIG_FILE = join(etcdir, 'mailman.cfg')
- self.LOCK_FILE = join(lockdir, 'master-qrunner')
- # Set up the switchboards.
- from mailman.queue import Switchboard
- Switchboard.initialize()
- # Set up all the languages.
- languages = self._config.getByCategory('language', [])
- for language in languages:
- code = language.name.split('.')[1]
- self.languages.add_language(code, language.description,
- language.charset, language.enabled)
- # Always enable the server default language, which must be defined.
- self.languages.enable_language(self._config.mailman.default_language)
- self.ensure_directories_exist()
- self.style_manager.populate()
-
- @property
- def logger_configs(self):
- """Return all log config sections."""
- return self._config.getByCategory('logging', [])
-
- @property
- def paths(self):
- """Return a substitution dictionary of all path variables."""
- return dict((k, self.__dict__[k])
- for k in self.__dict__
- if k.endswith('_DIR'))
-
- def ensure_directories_exist(self):
- """Create all path directories if they do not exist."""
- for variable, directory in self.paths.items():
- makedirs(directory)
-
- @property
- def qrunner_configs(self):
- """Iterate over all the qrunner configuration sections."""
- for section in self._config.getByCategory('qrunner', []):
- yield section
-
- @property
- def archivers(self):
- """Iterate over all the enabled archivers."""
- for section in self._config.getByCategory('archiver', []):
- if not as_boolean(section.enable):
- continue
- class_path = section['class']
- module_name, class_name = class_path.rsplit('.', 1)
- __import__(module_name)
- yield getattr(sys.modules[module_name], class_name)()
-
- @property
- def style_configs(self):
- """Iterate over all the style configuration sections."""
- for section in self._config.getByCategory('style', []):
- yield section
-
- @property
- def header_matches(self):
- """Iterate over all spam matching headers.
-
- Values are 3-tuples of (header, pattern, chain)
- """
- matches = self._config.getByCategory('spam.headers', [])
- for match in matches:
- yield (matches.header, matches.pattern, matches.chain)
diff --git a/mailman/config/mailman.cfg b/mailman/config/mailman.cfg
deleted file mode 100644
index 2bf528bea..000000000
--- a/mailman/config/mailman.cfg
+++ /dev/null
@@ -1,69 +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 .
-
-# This is the absolute bare minimum base configuration file. User supplied
-# configurations are pushed onto this.
-
-[language.en]
-
-[qrunner.archive]
-class: mailman.queue.archive.ArchiveRunner
-
-[qrunner.bad]
-class: mailman.queue.fake.BadRunner
-# The shunt runner is just a placeholder for its switchboard.
-start: no
-
-[qrunner.bounces]
-class: mailman.queue.bounce.BounceRunner
-
-[qrunner.command]
-class: mailman.queue.command.CommandRunner
-
-[qrunner.in]
-class: mailman.queue.incoming.IncomingRunner
-
-[qrunner.lmtp]
-class: mailman.queue.lmtp.LMTPRunner
-
-[qrunner.maildir]
-class: mailman.queue.maildir.MaildirRunner
-# This is still experimental.
-start: no
-
-[qrunner.news]
-class: mailman.queue.news.NewsRunner
-
-[qrunner.out]
-class: mailman.queue.outgoing.OutgoingRunner
-
-[qrunner.pipeline]
-class: mailman.queue.pipeline.PipelineRunner
-
-[qrunner.retry]
-class: mailman.queue.retry.RetryRunner
-sleep_time: 15m
-
-[qrunner.shunt]
-class: mailman.queue.fake.ShuntRunner
-# The shunt runner is just a placeholder for its switchboard.
-start: no
-
-[qrunner.virgin]
-class: mailman.queue.virgin.VirginRunner
-
-[style.default]
diff --git a/mailman/config/schema.cfg b/mailman/config/schema.cfg
deleted file mode 100644
index df20a7370..000000000
--- a/mailman/config/schema.cfg
+++ /dev/null
@@ -1,589 +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 .
-
-# This is the GNU Mailman configuration schema. It defines the default
-# configuration options for the core system and plugins. It uses ini-style
-# formats under the lazr.config regime to define all system configuration
-# options. See for details.
-
-[mailman]
-# This address is the "site owner" address. Certain messages which must be
-# delivered to a human, but which can't be delivered to a list owner (e.g. a
-# bounce from a list owner), will be sent to this address. It should point to
-# a human.
-site_owner: changeme@example.com
-
-# This address is used as the from address whenever a message comes from some
-# entity to which there is no natural reply recipient. Set this to a real
-# human or to /dev/null. It will be appended with the host name of the list
-# involved. This address must not bounce and it must not point to a Mailman
-# process.
-noreply_address: noreply
-
-# Where all the runtime data will be kept. This directory must exist.
-var_dir: /tmp/mailman
-
-# The default language for this server.
-default_language: en
-
-# When allowing only members to post to a mailing list, how is the sender of
-# the message determined? If this variable is set to Yes, then first the
-# message's envelope sender is used, with a fallback to the sender if there is
-# no envelope sender. Set this variable to No to always use the sender.
-#
-# The envelope sender is set by the SMTP delivery and is thus less easily
-# spoofed than the sender, which is typically just taken from the From: header
-# and thus easily spoofed by the end-user. However, sometimes the envelope
-# sender isn't set correctly and this will manifest itself by postings being
-# held for approval even if they appear to come from a list member. If you
-# are having this problem, set this variable to No, but understand that some
-# spoofed messages may get through.
-use_envelope_sender: no
-
-# Membership tests for posting purposes are usually performed by looking at a
-# set of headers, passing the test if any of their values match a member of
-# the list. Headers are checked in the order given in this variable. The
-# value From_ means to use the envelope sender. Field names are case
-# insensitive. This is a space separate list of headers.
-sender_headers: from from_ reply-to sender
-
-# Mail command processor will ignore mail command lines after designated max.
-email_commands_max_lines: 10
-
-# Default length of time a pending request is live before it is evicted from
-# the pending database.
-pending_request_life: 3d
-
-
-[passwords]
-# When Mailman generates them, this is the default length of member passwords.
-member_password_length: 8
-
-# Specify the type of passwords to use, when Mailman generates the passwords
-# itself, as would be the case for membership requests where the user did not
-# fill in a password, or during list creation, when auto-generation of admin
-# passwords was selected.
-#
-# Set this value to 'yes' for classic Mailman user-friendly(er) passwords.
-# These generate semi-pronounceable passwords which are easier to remember.
-# Set this value to 'no' to use more cryptographically secure, but harder to
-# remember, passwords -- if your operating system and Python version support
-# the necessary feature (specifically that /dev/urandom be available).
-user_friendly_passwords: yes
-
-
-[qrunner.master]
-# Define which process queue runners, and how many of them, to start.
-
-# The full import path to the class for this queue runner.
-class: mailman.queue.runner.Runner
-
-# The directory path that this queue runner scans.
-path: $VAR_DIR/qfiles/$name
-
-# The number of parallel queue runners. This must be a power of 2.
-instances: 1
-
-# Whether to start this queue runner or not.
-start: yes
-
-# The maximum number of restarts for this queue runner. When the runner exits
-# because of an error or other unexpected problem, it is automatically
-# restarted, until the maximum number of restarts has been reached.
-max_restarts: 10
-
-# The sleep interval for the queue runner. It wakes up once every interval to
-# process the files in its slice of the queue directory.
-sleep_time: 1s
-
-[database]
-# Use this to set the Storm database engine URL. You generally have one
-# primary database connection for all of Mailman. List data and most rosters
-# will store their data in this database, although external rosters may access
-# other databases in their own way. This string supports standard
-# 'configuration' substitutions.
-url: sqlite:///$DATA_DIR/mailman.db
-debug: no
-
-[logging.template]
-# This defines various log settings. The options available are:
-#
-# - level -- Overrides the default level; this may be any of the
-# standard Python logging levels, case insensitive.
-# - format -- Overrides the default format string
-# - datefmt -- Overrides the default date format string
-# - path -- Overrides the default logger path. This may be a relative
-# path name, in which case it is relative to Mailman's LOG_DIR,
-# or it may be an absolute path name. You cannot change the
-# handler class that will be used.
-# - propagate -- Boolean specifying whether to propagate log message from this
-# logger to the root "mailman" logger. You cannot override
-# settings for the root logger.
-#
-# In this section, you can define defaults for all loggers, which will be
-# prefixed by 'mailman.'. Use subsections to override settings for specific
-# loggers. The names of the available loggers are:
-#
-# - archiver -- All archiver output
-# - bounce -- All bounce processing logs go here
-# - config -- Configuration issues
-# - debug -- Only used for development
-# - error -- All exceptions go to this log
-# - fromusenet -- Information related to the Usenet to Mailman gateway
-# - http -- Internal wsgi-based web interface
-# - locks -- Lock state changes
-# - mischief -- Various types of hostile activity
-# - post -- Information about messages posted to mailing lists
-# - qrunner -- qrunner start/stops
-# - smtp -- Successful SMTP activity
-# - smtp-failure -- Unsuccessful SMTP activity
-# - subscribe -- Information about leaves/joins
-# - vette -- Information related to admindb activity
-format: %(asctime)s (%(process)d) %(message)s
-datefmt: %b %d %H:%M:%S %Y
-propagate: no
-level: info
-path: mailman
-
-[logging.root]
-
-[logging.archiver]
-
-[logging.bounce]
-path: bounce
-
-[logging.config]
-
-[logging.debug]
-path: debug
-level: debug
-
-[logging.error]
-
-[logging.fromusenet]
-
-[logging.http]
-
-[logging.locks]
-
-[logging.mischief]
-
-[logging.qrunner]
-
-[logging.smtp]
-path: smtp
-
-# The smtp logger defines additional options for handling the logging of each
-# attempted delivery. These format strings specify what information is logged
-# for every message, every successful delivery, every refused delivery and
-# every recipient failure. To disable a status message, set the value to 'no'
-# (without the quotes).
-#
-# These template strings accept the following set of substitution
-# placeholders, if available.
-#
-# msgid -- the Message-ID of the message in question
-# listname -- the fully-qualified list name
-# sender -- the sender if available
-# recip -- the recipient address if available, or the number of
-# recipients being delivered to
-# size -- the approximate size of the message in bytes
-# seconds -- the number of seconds the operation took
-# refused -- the number of refused recipients
-# smtpcode -- the SMTP success or failure code
-# smtpmsg -- the SMTP success or failure message
-
-every: $msgid smtp to $listname for $recip recips, completed in $time seconds
-success: $msgid post to $listname from $sender, $size bytes
-refused: $msgid post to $listname from $sender, $size bytes, $refused failures
-failure: $msgid delivery to $recip failed with code $smtpcode, $smtpmsg
-
-
-[logging.subscribe]
-
-[logging.vette]
-
-
-[domain.master]
-# Site-wide domain defaults. To configure an individual
-# domain, add a [domain.example_com] section with the overrides.
-
-# This is the host name for the email interface.
-email_host: example.com
-# This is the base url for the domain's web interface. It must include the
-# url scheme.
-base_url: http://example.com
-# The contact address for this domain. This is advertised as the human to
-# contact when users have problems with the lists in this domain.
-contact_address: postmaster@example.com
-# A short description of this domain.
-description: An example domain.
-
-
-[language.master]
-# Template for language definitions. The section name must be [language.xx]
-# where xx is the 2-character ISO code for the language.
-
-# The English name for the language.
-description: English (USA)
-# And the default character set for the language.
-charset: us-ascii
-# Whether the language is enabled or not.
-enabled: yes
-
-
-[spam.headers.template]
-# This section defines basic header matching actions. Each spam.header
-# section names a header to match (case-insensitively), a pattern to match
-# against the header's value, and the chain to jump to when the match
-# succeeds.
-#
-# The header value should not include the trailing colon.
-header: X-Spam
-# The pattern is always matched with re.IGNORECASE.
-pattern: xyz
-# The chain to jump to if the pattern matches. Maybe be any existing chain
-# such as 'discard', 'reject', 'hold', or 'accept'.
-chain: hold
-
-
-[mta]
-# The class defining the interface to the incoming mail transport agent.
-incoming: mailman.mta.postfix.LMTP
-
-# The class defining the interface to the outgoing mail transport agent.
-outgoing: mailman.mta.smtp_direct.process
-
-# How to connect to the outgoing MTA.
-smtp_host: localhost
-smtp_port: 25
-
-# Where the LMTP server listens for connections.
-lmtp_host: localhost
-lmtp_port: 8025
-
-# Ceiling on the number of recipients that can be specified in a single SMTP
-# transaction. Set to 0 to submit the entire recipient list in one
-# transaction.
-max_recipients: 500
-
-# Ceiling on the number of SMTP sessions to perform on a single socket
-# connection. Some MTAs have limits. Set this to 0 to do as many as we like
-# (i.e. your MTA has no limits). Set this to some number great than 0 and
-# Mailman will close the SMTP connection and re-open it after this number of
-# consecutive sessions.
-max_sessions_per_connection: 0
-
-# Maximum number of simultaneous subthreads that will be used for SMTP
-# delivery. After the recipients list is chunked according to max_recipients,
-# each chunk is handed off to the SMTP server by a separate such thread. If
-# your Python interpreter was not built for threads, this feature is disabled.
-# You can explicitly disable it in all cases by setting max_delivery_threads
-# to 0.
-max_delivery_threads: 0
-
-# How long should messages which have delivery failures continue to be
-# retried? After this period of time, a message that has failed recipients
-# will be dequeued and those recipients will never receive the message.
-delivery_retry_period: 5d
-
-# These variables control the format and frequency of VERP-like delivery for
-# better bounce detection. VERP is Variable Envelope Return Path, defined
-# here:
-#
-# http://cr.yp.to/proto/verp.txt
-#
-# This involves encoding the address of the recipient as we (Mailman) know it
-# into the envelope sender address (i.e. the SMTP `MAIL FROM:' address).
-# Thus, no matter what kind of forwarding the recipient has in place, should
-# it eventually bounce, we will receive an unambiguous notice of the bouncing
-# address.
-#
-# However, we're technically only "VERP-like" because we're doing the envelope
-# sender encoding in Mailman, not in the MTA. We do require cooperation from
-# the MTA, so you must be sure your MTA can be configured for extended address
-# semantics.
-#
-# The first variable describes how to encode VERP envelopes. It must contain
-# these three string interpolations:
-#
-# $bounces -- the list-bounces mailbox will be set here
-# $mailbox -- the recipient's mailbox will be set here
-# $host -- the recipient's host name will be set here
-#
-# This example uses the default below.
-#
-# FQDN list address is: mylist@dom.ain
-# Recipient is: aperson@a.nother.dom
-#
-# The envelope sender will be mylist-bounces+aperson=a.nother.dom@dom.ain
-#
-# Note that your MTA /must/ be configured to deliver such an addressed message
-# to mylist-bounces!
-verp_delimiter: +
-verp_format: ${bounces}+${mailbox}=${host}
-
-# For nicer confirmation emails, use a VERP-like format which encodes the
-# confirmation cookie in the reply address. This lets us put a more user
-# friendly Subject: on the message, but requires cooperation from the MTA.
-# Format is like verp_format, but with the following substitutions:
-#
-# $address -- the list-confirm address
-# $cookie -- the confirmation cookie
-verp_confirm_format: $address+$cookie
-
-# This is analogous to verp_regexp, but for splitting apart the
-# verp_confirm_format. MUAs have been observed that mung
-#
-# From: local_part@host
-#
-# into
-#
-# To: "local_part"
-#
-# when replying, so we skip everything up to '<' if any.
-verp_confirm_regexp: ^(.*<)?(?P[^+]+?)\+(?P[^@]+)@.*$
-
-# Set this to 'yes' to enable VERP-like (more user friendly) confirmations.
-verp_confirmations: no
-
-# Another good opportunity is when regular delivery is personalized. Here
-# again, we're already incurring the performance hit for addressing each
-# individual recipient. Set this to 'yes' to enable VERPs on all personalized
-# regular deliveries (personalized digests aren't supported yet).
-verp_personalized_deliveries: no
-
-# And finally, we can VERP normal, non-personalized deliveries. However,
-# because it can be a significant performance hit, we allow you to decide how
-# often to VERP regular deliveries. This is the interval, in number of
-# messages, to do a VERP recipient address. The same variable controls both
-# regular and digest deliveries. Set to 0 to disable occasional VERPs, set to
-# 1 to VERP every delivery, or to some number > 1 for only occasional VERPs.
-verp_delivery_interval: 0
-
-# VERP format and regexp for probe messages.
-verp_probe_format: %(bounces)s+%(token)s
-verp_probe_regexp: ^(?P[^+]+?)\+(?P[^@]+)@.*$
-# Set this 'yes' to activate VERP probe for disabling by bounce.
-verp_probes: no
-
-# This is the maximum number of automatic responses sent to an address because
-# of -request messages or posting hold messages. This limit prevents response
-# loops between Mailman and misconfigured remote email robots. Mailman
-# already inhibits automatic replies to any message labeled with a header
-# "Precendence: bulk|list|junk". This is a fallback safety valve so it should
-# be set fairly high. Set to 0 for no limit (probably useful only for
-# debugging).
-max_autoresponses_per_day: 10
-
-# Some list posts and mail to the -owner address may contain DomainKey or
-# DomainKeys Identified Mail (DKIM) signature headers .
-# Various list transformations to the message such as adding a list header or
-# footer or scrubbing attachments or even reply-to munging can break these
-# signatures. It is generally felt that these signatures have value, even if
-# broken and even if the outgoing message is resigned. However, some sites
-# may wish to remove these headers by setting this to 'yes'.
-remove_dkim_headers: no
-
-# This variable describe the program to use for regenerating the transport map
-# db file, from the associated plain text files. The file being updated will
-# be appended to this string (with a separating space), so it must be
-# appropriate for os.system().
-postfix_map_cmd: /usr/sbin/postmap
-
-
-[bounces]
-# How often should the bounce qrunner process queued detected bounces?
-register_bounces_every: 15m
-
-
-[archiver.master]
-# To add new archivers, define a new section based on this one, overriding the
-# following values.
-
-# The class implementing the IArchiver interface.
-class: mailman.archiving.prototype.Prototype
-
-# Set this to 'yes' to enable the archiver.
-enable: no
-
-# The base url for the archiver. This is used to to calculate links to
-# individual messages in the archive.
-base_url: http://archive.example.com/
-
-# If the archiver works by getting a copy of the message, this is the address
-# to send the copy to.
-recipient: archive@archive.example.com
-
-# If the archiver works by calling a command on the local machine, this is the
-# command to call.
-command: /bin/echo
-
-
-[archiver.mhonarc]
-# This is the stock MHonArc archiver.
-class: mailman.archiving.mhonarc.MHonArc
-
-base_url: http://$hostname/archives/$fqdn_listname
-
-
-[archiver.mail_archive]
-# This is the stock mail-archive.com archiver.
-class: mailman.archiving.mailarchive.MailArchive
-
-[archiver.pipermail]
-# This is the stock Pipermail archiver.
-class: mailman.archiving.pipermail.Pipermail
-
-# This sets the default `clobber date' policy for the archiver. When a
-# message is to be archived either by Pipermail or an external archiver,
-# Mailman can modify the Date: header to be the date the message was received
-# instead of the Date: in the original message. This is useful if you
-# typically receive messages with outrageous dates. Set this to 0 to retain
-# the date of the original message, or to 1 to always clobber the date. Set
-# it to 2 to perform `smart overrides' on the date; when the date is outside
-# allowable_sane_date_skew (either too early or too late), then the received
-# date is substituted instead.
-clobber_date_policy: 2
-allowable_sane_date_skew: 15d
-
-# Pipermail archives contain the raw email addresses of the posting authors.
-# Some view this as a goldmine for spam harvesters. Set this to 'yes' to
-# moderately obscure email addresses, but note that this breaks mailto: URLs
-# in the archives too.
-obscure_email_addresses: yes
-
-# When the archive is public, should Pipermail also make the raw Unix mbox
-# file publically available?
-public_mbox: no
-
-
-[archiver.prototype]
-# This is a prototypical sample archiver.
-class: mailman.archiving.prototype.Prototype
-
-
-[style.master]
-# The style's priority, with 0 being the lowest priority.
-priority: 0
-
-# The class implementing the IStyle interface, which applies the style.
-class: mailman.styles.default.DefaultStyle
-
-
-[scrubber]
-# A filter module that converts from multipart messages to "flat" messages
-# (i.e. containing a single payload). This is required for Pipermail, and you
-# may want to set it to 0 for external archivers. You can also replace it
-# with your own module as long as it contains a process() function that takes
-# a MailList object and a Message object. It should raise
-# Errors.DiscardMessage if it wants to throw the message away. Otherwise it
-# should modify the Message object as necessary.
-archive_scrubber: mailman.pipeline.scrubber
-
-# This variable defines what happens to text/html subparts. They can be
-# stripped completely, escaped, or filtered through an external program. The
-# legal values are:
-# 0 - Strip out text/html parts completely, leaving a notice of the removal in
-# the message. If the outer part is text/html, the entire message is
-# discarded.
-# 1 - Remove any embedded text/html parts, leaving them as HTML-escaped
-# attachments which can be separately viewed. Outer text/html parts are
-# simply HTML-escaped.
-# 2 - Leave it inline, but HTML-escape it
-# 3 - Remove text/html as attachments but don't HTML-escape them. Note: this
-# is very dangerous because it essentially means anybody can send an HTML
-# email to your site containing evil JavaScript or web bugs, or other
-# nasty things, and folks viewing your archives will be susceptible. You
-# should only consider this option if you do heavy moderation of your list
-# postings.
-#
-# Note: given the current archiving code, it is not possible to leave
-# text/html parts inline and un-escaped. I wouldn't think it'd be a good idea
-# to do anyway.
-#
-# The value can also be a string, in which case it is the name of a command to
-# filter the HTML page through. The resulting output is left in an attachment
-# or as the entirety of the message when the outer part is text/html. The
-# format of the string must include a $filename substitution variable which
-# will contain the name of the temporary file that the program should operate
-# on. It should write the processed message to stdout. Set this to
-# HTML_TO_PLAIN_TEXT_COMMAND to specify an HTML to plain text conversion
-# program.
-archive_html_sanitizer: 1
-
-# Control parameter whether the scrubber should use the message attachment's
-# filename as is indicated by the filename parameter or use 'attachement-xxx'
-# instead. The default is set 'no' because the applications on PC and Mac
-# begin to use longer non-ascii filenames.
-use_attachment_filename: no
-
-# Use of attachment filename extension per se is may be dangerous because
-# viruses fakes it. You can set this 'yes' if you filter the attachment by
-# filename extension.
-use_attachment_filename_extension: no
-
-
-[digests]
-# Headers which should be kept in both RFC 1153 (plain) and MIME digests. RFC
-# 1153 also specifies these headers in this exact order, so order matters.
-# These are space separated and case insensitive.
-mime_digest_keep_headers:
- Date From To Cc Subject Message-ID Keywords
- In-Reply-To References Content-Type MIME-Version
- Content-Transfer-Encoding Precedence Reply-To
- Message
-
-plain_digest_keep_headers:
- Message Date From
- Subject To Cc
- Message-ID Keywords
- Content-Type
-
-
-[nntp]
-# Set these variables if you need to authenticate to your NNTP server for
-# Usenet posting or reading. If no authentication is necessary, specify None
-# for both variables.
-username:
-password:
-
-# Set this if you have an NNTP server you prefer gatewayed lists to use.
-host:
-
-# This controls how headers must be cleansed in order to be accepted by your
-# NNTP server. Some servers like INN reject messages containing prohibited
-# headers, or duplicate headers. The NNTP server may reject the message for
-# other reasons, but there's little that can be programmatically done about
-# that.
-#
-# These headers (case ignored) are removed from the original message. This is
-# a whitespace separate list of headers.
-remove_headers:
- nntp-posting-host nntp-posting-date x-trace
- x-complaints-to xref date-received posted
- posting-version relay-version received
-
-# These headers are left alone, unless there are duplicates in the original
-# message. Any second and subsequent headers are rewritten to the second
-# named header (case preserved). This is a list of header pairs, one pair per
-# line.
-rewrite_duplicate_headers:
- To X-Original-To
- CC X-Original-CC
- Content-Transfer-Encoding X-Original-Content-Transfer-Encoding
- MIME-Version X-MIME-Version
diff --git a/mailman/constants.py b/mailman/constants.py
deleted file mode 100644
index 39c4547f8..000000000
--- a/mailman/constants.py
+++ /dev/null
@@ -1,44 +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 .
-
-"""Various constants and enumerations."""
-
-from __future__ import unicode_literals
-
-__metaclass__ = type
-__all__ = [
- 'SystemDefaultPreferences',
- ]
-
-
-from zope.interface import implements
-
-from mailman.interfaces.member import DeliveryMode, DeliveryStatus
-from mailman.interfaces.preferences import IPreferences
-
-
-
-class SystemDefaultPreferences:
- implements(IPreferences)
-
- acknowledge_posts = False
- hide_address = True
- preferred_language = 'en'
- receive_list_copy = True
- receive_own_postings = True
- delivery_mode = DeliveryMode.regular
- delivery_status = DeliveryStatus.enabled
diff --git a/mailman/core/__init__.py b/mailman/core/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/mailman/core/chains.py b/mailman/core/chains.py
deleted file mode 100644
index 40b8c779f..000000000
--- a/mailman/core/chains.py
+++ /dev/null
@@ -1,118 +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 .
-
-"""Application support for chain processing."""
-
-from __future__ import absolute_import, unicode_literals
-
-__metaclass__ = type
-__all__ = [
- 'initialize',
- 'process',
- ]
-
-
-from mailman.chains.accept import AcceptChain
-from mailman.chains.builtin import BuiltInChain
-from mailman.chains.discard import DiscardChain
-from mailman.chains.headers import HeaderMatchChain
-from mailman.chains.hold import HoldChain
-from mailman.chains.reject import RejectChain
-from mailman.config import config
-from mailman.interfaces.chain import LinkAction
-
-
-
-def process(mlist, msg, msgdata, start_chain='built-in'):
- """Process the message through a chain.
-
- :param mlist: the IMailingList for this message.
- :param msg: The Message object.
- :param msgdata: The message metadata dictionary.
- :param start_chain: The name of the chain to start the processing with.
- """
- # Set up some bookkeeping.
- chain_stack = []
- msgdata['rule_hits'] = hits = []
- msgdata['rule_misses'] = misses = []
- # Find the starting chain and begin iterating through its links.
- chain = config.chains[start_chain]
- chain_iter = chain.get_links(mlist, msg, msgdata)
- # Loop until we've reached the end of all processing chains.
- while chain:
- # Iterate over all links in the chain. Do this outside a for-loop so
- # we can capture a chain's link iterator in mid-flight. This supports
- # the 'detour' link action
- try:
- link = next(chain_iter)
- except StopIteration:
- # This chain is exhausted. Pop the last chain on the stack and
- # continue iterating through it. If there's nothing left on the
- # chain stack then we're completely finished processing.
- if len(chain_stack) == 0:
- return
- chain, chain_iter = chain_stack.pop()
- continue
- # Process this link.
- if link.rule.check(mlist, msg, msgdata):
- if link.rule.record:
- hits.append(link.rule.name)
- # The rule matched so run its action.
- if link.action is LinkAction.jump:
- chain = link.chain
- chain_iter = chain.get_links(mlist, msg, msgdata)
- continue
- elif link.action is LinkAction.detour:
- # Push the current chain so that we can return to it when
- # the next chain is finished.
- chain_stack.append((chain, chain_iter))
- chain = link.chain
- chain_iter = chain.get_links(mlist, msg, msgdata)
- continue
- elif link.action is LinkAction.stop:
- # Stop all processing.
- return
- elif link.action is LinkAction.defer:
- # Just process the next link in the chain.
- pass
- elif link.action is LinkAction.run:
- link.function(mlist, msg, msgdata)
- else:
- raise AssertionError(
- 'Bad link action: {0}'.format(link.action))
- else:
- # The rule did not match; keep going.
- if link.rule.record:
- misses.append(link.rule.name)
-
-
-
-def initialize():
- """Set up chains, both built-in and from the database."""
- for chain_class in (DiscardChain, HoldChain, RejectChain, AcceptChain):
- chain = chain_class()
- assert chain.name not in config.chains, (
- 'Duplicate chain name: {0}'.format(chain.name))
- config.chains[chain.name] = chain
- # Set up a couple of other default chains.
- chain = BuiltInChain()
- config.chains[chain.name] = chain
- # Create and initialize the header matching chain.
- chain = HeaderMatchChain()
- config.chains[chain.name] = chain
- # XXX Read chains from the database and initialize them.
- pass
diff --git a/mailman/core/errors.py b/mailman/core/errors.py
deleted file mode 100644
index 39401127e..000000000
--- a/mailman/core/errors.py
+++ /dev/null
@@ -1,172 +0,0 @@
-# Copyright (C) 1998-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 .
-
-"""Mailman errors."""
-
-from __future__ import absolute_import, unicode_literals
-
-__metaclass__ = type
-__all__ = [
- 'AlreadyReceivingDigests',
- 'AlreadyReceivingRegularDeliveries',
- 'BadDomainSpecificationError',
- 'BadPasswordSchemeError',
- 'CantDigestError',
- 'DiscardMessage',
- 'EmailAddressError',
- 'HandlerError',
- 'HoldMessage',
- 'HostileSubscriptionError',
- 'InvalidEmailAddress',
- 'LostHeldMessage',
- 'MailmanError',
- 'MailmanException',
- 'MemberError',
- 'MembershipIsBanned',
- 'MustDigestError',
- 'NotAMemberError',
- 'PasswordError',
- 'RejectMessage',
- 'SomeRecipientsFailed',
- 'SubscriptionError',
- ]
-
-
-
-# Base class for all exceptions raised in Mailman (XXX except legacy string
-# exceptions).
-class MailmanException(Exception):
- pass
-
-
-
-# "New" style membership exceptions (new w/ MM2.1)
-class MemberError(MailmanException): pass
-class NotAMemberError(MemberError): pass
-class AlreadyReceivingDigests(MemberError): pass
-class AlreadyReceivingRegularDeliveries(MemberError): pass
-class CantDigestError(MemberError): pass
-class MustDigestError(MemberError): pass
-class MembershipIsBanned(MemberError): pass
-
-
-
-# New style class based exceptions. All the above errors should eventually be
-# converted.
-
-class MailmanError(MailmanException):
- """Base class for all Mailman errors."""
- pass
-
-class BadDomainSpecificationError(MailmanError):
- """The specification of a virtual domain is invalid or duplicated."""
-
-
-
-# Exception hierarchy for bad email address errors that can be raised from
-# Utils.ValidateEmail()
-class EmailAddressError(MailmanError):
- """Base class for email address validation errors."""
-
-
-class InvalidEmailAddress(EmailAddressError):
- """Email address is invalid."""
-
-
-
-# Exceptions for admin request database
-class LostHeldMessage(MailmanError):
- """Held message was lost."""
- pass
-
-
-
-def _(s):
- return s
-
-# Exceptions for the Handler subsystem
-class HandlerError(MailmanError):
- """Base class for all handler errors."""
-
-class HoldMessage(HandlerError):
- """Base class for all message-being-held short circuits."""
-
- # funky spelling is necessary to break import loops
- reason = _('For some unknown reason')
-
- def reason_notice(self):
- return self.reason
-
- # funky spelling is necessary to break import loops
- rejection = _('Your message was rejected')
-
- def rejection_notice(self, mlist):
- return self.rejection
-
-class DiscardMessage(HandlerError):
- """The message can be discarded with no further action"""
-
-class SomeRecipientsFailed(HandlerError):
- """Delivery to some or all recipients failed"""
- def __init__(self, tempfailures, permfailures):
- HandlerError.__init__(self)
- self.tempfailures = tempfailures
- self.permfailures = permfailures
-
-class RejectMessage(HandlerError):
- """The message will be bounced back to the sender"""
- def __init__(self, notice=None):
- super(RejectMessage, self).__init__()
- if notice is None:
- notice = _('Your message was rejected')
- if notice.endswith('\n\n'):
- pass
- elif notice.endswith('\n'):
- notice += '\n'
- else:
- notice += '\n\n'
- self.notice = notice
-
-
-
-# Subscription exceptions
-class SubscriptionError(MailmanError):
- """Subscription errors base class."""
-
-
-class HostileSubscriptionError(SubscriptionError):
- """A cross-subscription attempt was made.
-
- This exception gets raised when an invitee attempts to use the
- invitation to cross-subscribe to some other mailing list.
- """
-
-
-
-class PasswordError(MailmanError):
- """A password related error."""
-
-
-class BadPasswordSchemeError(PasswordError):
- """A bad password scheme was given."""
-
- def __init__(self, scheme_name='unknown'):
- super(BadPasswordSchemeError, self).__init__()
- self.scheme_name = scheme_name
-
- def __str__(self):
- return 'A bad password scheme was given: %s' % self.scheme_name
diff --git a/mailman/core/initialize.py b/mailman/core/initialize.py
deleted file mode 100644
index bb16f0036..000000000
--- a/mailman/core/initialize.py
+++ /dev/null
@@ -1,125 +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 .
-
-"""Initialize all global state.
-
-Every entrance into the Mailman system, be it by command line, mail program,
-or cgi, must call the initialize function here in order for the system's
-global state to be set up properly. Typically this is called after command
-line argument parsing, since some of the initialization behavior is controlled
-by the command line arguments.
-"""
-
-from __future__ import absolute_import, unicode_literals
-
-__metaclass__ = type
-__all__ = [
- 'initialize',
- 'initialize_1',
- 'initialize_2',
- 'initialize_3',
- ]
-
-
-import os
-
-from zope.interface.interface import adapter_hooks
-from zope.interface.verify import verifyObject
-
-import mailman.config.config
-import mailman.core.logging
-
-from mailman.core.plugins import get_plugin
-from mailman.interfaces.database import IDatabase
-
-
-
-# These initialization calls are separated for the testing framework, which
-# needs to do some internal calculations after config file loading and log
-# initialization, but before database initialization. Generally all other
-# code will just call initialize().
-
-def initialize_1(config_path=None, propagate_logs=None):
- """First initialization step.
-
- * The configuration system
- * Run-time directories
- * The logging subsystem
-
- :param config_path: The path to the configuration file.
- :type config_path: string
- :param propagate_logs: Should the log output propagate to stderr?
- :type propagate_logs: boolean or None
- """
- # By default, set the umask so that only owner and group can read and
- # write our files. Specifically we must have g+rw and we probably want
- # o-rwx although I think in most cases it doesn't hurt if other can read
- # or write the files. Note that the Pipermail archive has more
- # restrictive permissions in order to handle private archives, but it
- # handles that correctly.
- os.umask(007)
- mailman.config.config.load(config_path)
- # Create the queue and log directories if they don't already exist.
- mailman.config.config.ensure_directories_exist()
- mailman.core.logging.initialize(propagate_logs)
-
-
-def initialize_2(debug=False):
- """Second initialization step.
-
- * Rules
- * Chains
- * Pipelines
- * Commands
-
- :param debug: Should the database layer be put in debug mode?
- :type debug: boolean
- """
- database_plugin = get_plugin('mailman.database')
- # Instantiate the database plugin, ensure that it's of the right type, and
- # initialize it. Then stash the object on our configuration object.
- database = database_plugin()
- verifyObject(IDatabase, database)
- database.initialize(debug)
- mailman.config.config.db = database
- # Initialize the rules and chains. Do the imports here so as to avoid
- # circular imports.
- from mailman.app.commands import initialize as initialize_commands
- from mailman.core.chains import initialize as initialize_chains
- from mailman.core.pipelines import initialize as initialize_pipelines
- from mailman.core.rules import initialize as initialize_rules
- # Order here is somewhat important.
- initialize_rules()
- initialize_chains()
- initialize_pipelines()
- initialize_commands()
-
-
-def initialize_3():
- """Third initialization step.
-
- * Adapters
- """
- from mailman.app.registrar import adapt_domain_to_registrar
- adapter_hooks.append(adapt_domain_to_registrar)
-
-
-
-def initialize(config_path=None, propagate_logs=None):
- initialize_1(config_path, propagate_logs)
- initialize_2()
- initialize_3()
diff --git a/mailman/core/logging.py b/mailman/core/logging.py
deleted file mode 100644
index a18065965..000000000
--- a/mailman/core/logging.py
+++ /dev/null
@@ -1,163 +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 .
-
-"""Logging initialization, using Python's standard logging package."""
-
-from __future__ import absolute_import, unicode_literals
-
-__metaclass__ = type
-__all__ = [
- 'initialize',
- 'reopen',
- ]
-
-
-import os
-import sys
-import codecs
-import logging
-
-from lazr.config import as_boolean, as_log_level
-
-from mailman.config import config
-
-
-_handlers = {}
-
-
-
-# XXX I would love to simplify things and use Python 2.6's WatchedFileHandler,
-# but there are two problems. First, it's more difficult to handle the test
-# suite's need to reopen the file handler to a different path. Does
-# zope.testing's logger support fix this?
-#
-# The other problem is that WatchedFileHandler doesn't really easily support
-# HUPing the process to reopen the log file. Now, maybe that's not a big deal
-# because the standard logging module would already handle things correctly if
-# the file is moved, but still that's not an interface I'm ready to give up on
-# yet. For now, keep our hack.
-
-class ReopenableFileHandler(logging.Handler):
- """A file handler that supports reopening."""
-
- def __init__(self, name, filename):
- self.name = name
- self._filename = filename
- self._stream = self._open()
- logging.Handler.__init__(self)
-
- def _open(self):
- return codecs.open(self._filename, 'a', 'utf-8')
-
- def flush(self):
- if self._stream:
- self._stream.flush()
-
- def emit(self, record):
- # It's possible for the stream to have been closed by the time we get
- # here, due to the shut down semantics. This mostly happens in the
- # test suite, but be defensive anyway.
- stream = (self._stream if self._stream else sys.stderr)
- try:
- msg = self.format(record)
- try:
- stream.write('{0}'.format(msg))
- except UnicodeError:
- stream.write('{0}'.format(msg.encode('string-escape')))
- self.flush()
- except:
- self.handleError(record)
-
- def close(self):
- self.flush()
- self._stream.close()
- self._stream = None
- logging.Handler.close(self)
-
- def reopen(self, filename=None):
- """Reopen the output stream.
-
- :param filename: If given, this reopens the output stream to a new
- file. This is used in the test suite.
- :type filename: string
- """
- if filename is not None:
- self._filename = filename
- self._stream.close()
- self._stream = self._open()
-
-
-
-def initialize(propagate=None):
- """Initialize all logs.
-
- :param propagate: Flag specifying whether logs should propagate their
- messages to the root logger. If omitted, propagation is determined
- from the configuration files.
- :type propagate: bool or None
- """
- # First, find the root logger and configure the logging subsystem.
- # Initialize the root logger, then create a formatter for all the
- # sublogs. The root logger should log to stderr.
- logging.basicConfig(format=config.logging.root.format,
- datefmt=config.logging.root.datefmt,
- level=as_log_level(config.logging.root.level),
- stream=sys.stderr)
- # Create the subloggers.
- for logger_config in config.logger_configs:
- sub_name = logger_config.name.split('.')[-1]
- if sub_name == 'root':
- continue
- logger_name = 'mailman.' + sub_name
- log = logging.getLogger(logger_name)
- # Get settings from log configuration file (or defaults).
- log_format = logger_config.format
- log_datefmt = logger_config.datefmt
- # Propagation to the root logger is how we handle logging to stderr
- # when the qrunners are not run as a subprocess of mailmanctl.
- log.propagate = (as_boolean(logger_config.propagate)
- if propagate is None else propagate)
- # Set the logger's level.
- log.setLevel(as_log_level(logger_config.level))
- # Create a formatter for this logger, then a handler, and link the
- # formatter to the handler.
- formatter = logging.Formatter(fmt=log_format, datefmt=log_datefmt)
- path_str = logger_config.path
- path_abs = os.path.normpath(os.path.join(config.LOG_DIR, path_str))
- handler = ReopenableFileHandler(sub_name, path_abs)
- _handlers[sub_name] = handler
- handler.setFormatter(formatter)
- log.addHandler(handler)
-
-
-
-def reopen():
- """Re-open all log files."""
- for handler in _handlers.values():
- handler.reopen()
-
-
-
-def get_handler(sub_name):
- """Return the handler associated with a named logger.
-
- :param sub_name: The logger name, sans the 'mailman.' prefix.
- :type sub_name: string
- :return: The file handler associated with the named logger.
- :rtype: `ReopenableFileHandler`
- """
- return _handlers[sub_name]
diff --git a/mailman/core/pipelines.py b/mailman/core/pipelines.py
deleted file mode 100644
index 8aae5cc25..000000000
--- a/mailman/core/pipelines.py
+++ /dev/null
@@ -1,125 +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 .
-
-"""Pipeline processor."""
-
-from __future__ import absolute_import, unicode_literals
-
-__metaclass__ = type
-__all__ = [
- 'initialize',
- 'process',
- ]
-
-
-from zope.interface import implements
-from zope.interface.verify import verifyObject
-
-from mailman.config import config
-from mailman.core.plugins import get_plugins
-from mailman.i18n import _
-from mailman.interfaces.handler import IHandler
-from mailman.interfaces.pipeline import IPipeline
-
-
-
-def process(mlist, msg, msgdata, pipeline_name='built-in'):
- """Process the message through the given pipeline.
-
- :param mlist: the IMailingList for this message.
- :param msg: The Message object.
- :param msgdata: The message metadata dictionary.
- :param pipeline_name: The name of the pipeline to process through.
- """
- pipeline = config.pipelines[pipeline_name]
- for handler in pipeline:
- handler.process(mlist, msg, msgdata)
-
-
-
-class BasePipeline:
- """Base pipeline implementation."""
-
- implements(IPipeline)
-
- _default_handlers = ()
-
- def __init__(self):
- self._handlers = []
- for handler_name in self._default_handlers:
- self._handlers.append(config.handlers[handler_name])
-
- def __iter__(self):
- """See `IPipeline`."""
- for handler in self._handlers:
- yield handler
-
-
-class BuiltInPipeline(BasePipeline):
- """The built-in pipeline."""
-
- name = 'built-in'
- description = _('The built-in pipeline.')
-
- _default_handlers = (
- 'mime-delete',
- 'scrubber',
- 'tagger',
- 'calculate-recipients',
- 'avoid-duplicates',
- 'cleanse',
- 'cleanse-dkim',
- 'cook-headers',
- 'to-digest',
- 'to-archive',
- 'to-usenet',
- 'after-delivery',
- 'acknowledge',
- 'to-outgoing',
- )
-
-
-class VirginPipeline(BasePipeline):
- """The processing pipeline for virgin messages.
-
- Virgin messages are those that are crafted internally by Mailman.
- """
- name = 'virgin'
- description = _('The virgin queue pipeline.')
-
- _default_handlers = (
- 'cook-headers',
- 'to-outgoing',
- )
-
-
-
-def initialize():
- """Initialize the pipelines."""
- # Find all handlers in the registered plugins.
- for handler_finder in get_plugins('mailman.handlers'):
- for handler_class in handler_finder():
- handler = handler_class()
- verifyObject(IHandler, handler)
- assert handler.name not in config.handlers, (
- 'Duplicate handler "{0}" found in {1}'.format(
- handler.name, handler_finder))
- config.handlers[handler.name] = handler
- # Set up some pipelines.
- for pipeline_class in (BuiltInPipeline, VirginPipeline):
- pipeline = pipeline_class()
- config.pipelines[pipeline.name] = pipeline
diff --git a/mailman/core/plugins.py b/mailman/core/plugins.py
deleted file mode 100644
index e9ba26571..000000000
--- a/mailman/core/plugins.py
+++ /dev/null
@@ -1,74 +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 .
-
-"""Get a requested plugin."""
-
-from __future__ import absolute_import, unicode_literals
-
-__metaclass__ = type
-__all__ = [
- ]
-
-
-import pkg_resources
-
-
-
-def get_plugin(group):
- """Get the named plugin.
-
- In general, this returns exactly one plugin. If no plugins have been
- added to the named group, the 'stock' plugin will be used. If more than
- one plugin -- other than the stock one -- exists, an exception will be
- raised.
-
- :param group: The plugin group name.
- :return: The loaded plugin.
- :raises RuntimeError: If more than one plugin overrides the stock plugin
- for the named group.
- """
- entry_points = list(pkg_resources.iter_entry_points(group))
- if len(entry_points) == 0:
- raise RuntimeError(
- 'No entry points found for group: {0}'.format(group))
- elif len(entry_points) == 1:
- # Okay, this is the one to use.
- return entry_points[0].load()
- elif len(entry_points) == 2:
- # Find the one /not/ named 'stock'.
- entry_points = [ep for ep in entry_points if ep.name <> 'stock']
- if len(entry_points) == 0:
- raise RuntimeError(
- 'No stock plugin found for group: {0}'.format(group))
- elif len(entry_points) == 2:
- raise RuntimeError('Too many stock plugins defined')
- else:
- raise AssertionError('Insanity')
- return entry_points[0].load()
- else:
- raise RuntimeError('Too many plugins for group: {0}'.format(group))
-
-
-
-def get_plugins(group):
- """Get and return all plugins in the named group.
-
- :param group: Plugin group name.
- :return: The loaded plugin.
- """
- for entry_point in pkg_resources.iter_entry_points(group):
- yield entry_point.load()
diff --git a/mailman/core/rules.py b/mailman/core/rules.py
deleted file mode 100644
index 83e24dfa2..000000000
--- a/mailman/core/rules.py
+++ /dev/null
@@ -1,46 +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 .
-
-"""Various rule helpers"""
-
-from __future__ import absolute_import, unicode_literals
-
-__metaclass__ = type
-__all__ = [
- 'initialize',
- ]
-
-
-from zope.interface.verify import verifyObject
-
-from mailman.config import config
-from mailman.core.plugins import get_plugins
-from mailman.interfaces.rules import IRule
-
-
-
-def initialize():
- """Find and register all rules in all plugins."""
- # Find rules in plugins.
- for rule_finder in get_plugins('mailman.rules'):
- for rule_class in rule_finder():
- rule = rule_class()
- verifyObject(IRule, rule)
- assert rule.name not in config.rules, (
- 'Duplicate rule "{0}" found in {1}'.format(
- rule.name, rule_finder))
- config.rules[rule.name] = rule
diff --git a/mailman/database/__init__.py b/mailman/database/__init__.py
deleted file mode 100644
index 8b7f584c2..000000000
--- a/mailman/database/__init__.py
+++ /dev/null
@@ -1,153 +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 .
-
-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.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.listmanager import ListManager
-from mailman.database.messagestore import MessageStore
-from mailman.database.pending import Pendings
-from mailman.database.requests import Requests
-from mailman.database.usermanager import UserManager
-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.list_manager = None
- self.user_manager = None
- self.message_store = None
- self.pendings = None
- self.requests = 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)
- self.list_manager = ListManager()
- self.user_manager = UserManager()
- self.message_store = MessageStore()
- self.pendings = Pendings()
- self.requests = Requests()
-
- 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...
- touch(url)
- database = create_database(url)
- store = Store(database)
- 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=u'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/mailman/database/address.py b/mailman/database/address.py
deleted file mode 100644
index 528d3af51..000000000
--- a/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 .
-
-"""Model for addresses."""
-
-from __future__ import absolute_import, unicode_literals
-
-__metaclass__ = type
-__all__ = [
- 'Address',
- ]
-
-
-from email.utils import formataddr
-from storm.locals import *
-from zope.interface import implements
-
-from mailman.config import config
-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 ''.format(
- address_str, verified, id(self))
- else:
- return ''.format(
- address_str, verified, self.address, id(self))
-
- def subscribe(self, mailing_list, role):
- # This member has no preferences by default.
- member = config.db.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()
- config.db.store.add(member)
- return member
-
- @property
- def original_address(self):
- return (self.address if self._original is None else self._original)
diff --git a/mailman/database/language.py b/mailman/database/language.py
deleted file mode 100644
index 8adc5c4a5..000000000
--- a/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 .
-
-"""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/mailman/database/listmanager.py b/mailman/database/listmanager.py
deleted file mode 100644
index 790a2509a..000000000
--- a/mailman/database/listmanager.py
+++ /dev/null
@@ -1,82 +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 .
-
-"""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.database.mailinglist import MailingList
-from mailman.interfaces.listmanager import IListManager, ListAlreadyExistsError
-
-
-
-class ListManager(object):
- """An implementation of the `IListManager` interface."""
-
- implements(IListManager)
-
- def create(self, fqdn_listname):
- """See `IListManager`."""
- listname, hostname = fqdn_listname.split('@', 1)
- 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, hostname = fqdn_listname.split('@', 1)
- 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)
diff --git a/mailman/database/mailinglist.py b/mailman/database/mailinglist.py
deleted file mode 100644
index 8803a5fa4..000000000
--- a/mailman/database/mailinglist.py
+++ /dev/null
@@ -1,272 +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 .
-
-"""Model for mailing lists."""
-
-from __future__ import absolute_import, unicode_literals
-
-__metaclass__ = type
-__all__ = [
- 'MailingList',
- ]
-
-
-import os
-import string
-
-from storm.locals import *
-from urlparse import urljoin
-from zope.interface import implements
-
-from mailman.config import config
-from mailman.database import roster
-from mailman.database.model import Model
-from mailman.database.types import Enum
-from mailman.interfaces.mailinglist import IMailingList, Personalization
-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()
- # Attributes not directly modifiable via the web u/i
- created_at = DateTime()
- admin_member_chunksize = Int()
- hold_and_cmd_autoresponses = Pickle()
- # 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()
- admin_responses = Pickle()
- postings_responses = Pickle()
- request_responses = Pickle()
- digest_last_sent_at = Float()
- one_last_digest = Pickle()
- volume = Int()
- last_post_time = DateTime()
- # 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()
- acceptable_aliases = 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()
- autorespond_admin = Bool()
- autorespond_postings = Bool()
- autorespond_requests = Int()
- autoresponse_admin_text = Unicode()
- autoresponse_graceperiod = TimeDelta()
- autoresponse_postings_text = Unicode()
- autoresponse_request_text = Unicode()
- 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()
- collapse_alternatives = Bool()
- convert_html_to_plaintext = Bool()
- default_member_moderation = Bool()
- description = Unicode()
- digest_footer = Unicode()
- digest_header = Unicode()
- digest_is_default = Bool()
- digest_send_periodic = Bool()
- digest_size_threshold = Int()
- digest_volume_frequency = Int()
- digestable = Bool()
- discard_these_nonmembers = Pickle()
- emergency = Bool()
- encode_ascii_prefixes = Bool()
- filter_action = Int()
- filter_content = Bool()
- filter_filename_extensions = Pickle()
- filter_mime_types = Pickle()
- 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()
- include_list_post_header = Bool()
- include_rfc2369_headers = Bool()
- 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()
- pass_filename_extensions = Pickle()
- pass_mime_types = Pickle()
- personalize = Enum()
- pipeline = Unicode()
- post_id = Int()
- preferred_language = Unicode()
- 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, hostname = fqdn_listname.split('@', 1)
- self.list_name = listname
- self.host_name = hostname
- # For the pending database
- self.next_request_id = 1
- self._restore()
- # Max autoresponses per day. A mapping between addresses and a
- # 2-tuple of the date of the last autoresponse and the number of
- # autoresponses sent on that date.
- self.hold_and_cmd_autoresponses = {}
- 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)
-
- @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 config.domains[self.host_name]
-
- def script_url(self, target, context=None):
- """See `IMailingList`."""
- # Find the domain for this mailing list.
- domain = config.domains[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):
- return self.fqdn_listname
-
- @property
- def no_reply_address(self):
- return '{0}@{1}'.format(config.mailman.noreply_address, self.host_name)
-
- @property
- def owner_address(self):
- return '{0}-owner@{1}'.format(self.list_name, self.host_name)
-
- @property
- def request_address(self):
- return '{0}-request@{1}'.format(self.list_name, self.host_name)
-
- @property
- def bounces_address(self):
- return '{0}-bounces@{1}'.format(self.list_name, self.host_name)
-
- @property
- def join_address(self):
- return '{0}-join@{1}'.format(self.list_name, self.host_name)
-
- @property
- def leave_address(self):
- return '{0}-leave@{1}'.format(self.list_name, self.host_name)
-
- @property
- def subscribe_address(self):
- return '{0}-subscribe@{1}'.format(self.list_name, self.host_name)
-
- @property
- def unsubscribe_address(self):
- return '{0}-unsubscribe@{1}'.format(self.list_name, self.host_name)
-
- def confirm_address(self, cookie):
- 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)
-
- def __repr__(self):
- return ''.format(
- self.fqdn_listname, id(self))
diff --git a/mailman/database/mailman.sql b/mailman/database/mailman.sql
deleted file mode 100644
index b098ed13b..000000000
--- a/mailman/database/mailman.sql
+++ /dev/null
@@ -1,208 +0,0 @@
-CREATE TABLE _request (
- id INTEGER NOT NULL,
- "key" TEXT,
- request_type TEXT,
- data_hash TEXT,
- mailing_list_id INTEGER,
- PRIMARY KEY (id),
- CONSTRAINT _request_mailing_list_id_fk FOREIGN KEY(mailing_list_id) REFERENCES mailinglist (id)
-);
-CREATE TABLE address (
- id INTEGER NOT NULL,
- address TEXT,
- _original TEXT,
- real_name TEXT,
- verified_on TIMESTAMP,
- registered_on TIMESTAMP,
- user_id INTEGER,
- preferences_id INTEGER,
- PRIMARY KEY (id),
- CONSTRAINT address_user_id_fk FOREIGN KEY(user_id) REFERENCES user (id),
- CONSTRAINT address_preferences_id_fk FOREIGN KEY(preferences_id) REFERENCES preferences (id)
-);
-CREATE TABLE language (
- id INTEGER NOT NULL,
- code TEXT,
- PRIMARY KEY (id)
-);
-CREATE TABLE mailinglist (
- id INTEGER NOT NULL,
- list_name TEXT,
- host_name TEXT,
- created_at TIMESTAMP,
- admin_member_chunksize INTEGER,
- hold_and_cmd_autoresponses BLOB,
- next_request_id INTEGER,
- next_digest_number INTEGER,
- admin_responses BLOB,
- postings_responses BLOB,
- request_responses BLOB,
- digest_last_sent_at NUMERIC(10, 2),
- one_last_digest BLOB,
- volume INTEGER,
- last_post_time TIMESTAMP,
- accept_these_nonmembers BLOB,
- acceptable_aliases BLOB,
- admin_immed_notify BOOLEAN,
- admin_notify_mchanges BOOLEAN,
- administrivia BOOLEAN,
- advertised BOOLEAN,
- anonymous_list BOOLEAN,
- archive BOOLEAN,
- archive_private BOOLEAN,
- archive_volume_frequency INTEGER,
- autorespond_admin BOOLEAN,
- autorespond_postings BOOLEAN,
- autorespond_requests INTEGER,
- autoresponse_admin_text TEXT,
- autoresponse_graceperiod TEXT,
- autoresponse_postings_text TEXT,
- autoresponse_request_text TEXT,
- ban_list BLOB,
- bounce_info_stale_after TEXT,
- bounce_matching_headers TEXT,
- bounce_notify_owner_on_disable BOOLEAN,
- bounce_notify_owner_on_removal BOOLEAN,
- bounce_processing BOOLEAN,
- bounce_score_threshold INTEGER,
- bounce_unrecognized_goes_to_list_owner BOOLEAN,
- bounce_you_are_disabled_warnings INTEGER,
- bounce_you_are_disabled_warnings_interval TEXT,
- collapse_alternatives BOOLEAN,
- convert_html_to_plaintext BOOLEAN,
- default_member_moderation BOOLEAN,
- description TEXT,
- digest_footer TEXT,
- digest_header TEXT,
- digest_is_default BOOLEAN,
- digest_send_periodic BOOLEAN,
- digest_size_threshold INTEGER,
- digest_volume_frequency INTEGER,
- digestable BOOLEAN,
- discard_these_nonmembers BLOB,
- emergency BOOLEAN,
- encode_ascii_prefixes BOOLEAN,
- filter_action INTEGER,
- filter_content BOOLEAN,
- filter_filename_extensions BLOB,
- filter_mime_types BLOB,
- first_strip_reply_to BOOLEAN,
- forward_auto_discards BOOLEAN,
- gateway_to_mail BOOLEAN,
- gateway_to_news BOOLEAN,
- generic_nonmember_action INTEGER,
- goodbye_msg TEXT,
- header_matches BLOB,
- hold_these_nonmembers BLOB,
- include_list_post_header BOOLEAN,
- include_rfc2369_headers BOOLEAN,
- info TEXT,
- linked_newsgroup TEXT,
- max_days_to_hold INTEGER,
- max_message_size INTEGER,
- max_num_recipients INTEGER,
- member_moderation_action BOOLEAN,
- member_moderation_notice TEXT,
- mime_is_default_digest BOOLEAN,
- moderator_password TEXT,
- msg_footer TEXT,
- msg_header TEXT,
- new_member_options INTEGER,
- news_moderation TEXT,
- news_prefix_subject_too BOOLEAN,
- nntp_host TEXT,
- nondigestable BOOLEAN,
- nonmember_rejection_notice TEXT,
- obscure_addresses BOOLEAN,
- pass_filename_extensions BLOB,
- pass_mime_types BLOB,
- personalize TEXT,
- pipeline TEXT,
- post_id INTEGER,
- preferred_language TEXT,
- private_roster BOOLEAN,
- real_name TEXT,
- reject_these_nonmembers BLOB,
- reply_goes_to_list TEXT,
- reply_to_address TEXT,
- require_explicit_destination BOOLEAN,
- respond_to_post_requests BOOLEAN,
- scrub_nondigest BOOLEAN,
- send_goodbye_msg BOOLEAN,
- send_reminders BOOLEAN,
- send_welcome_msg BOOLEAN,
- start_chain TEXT,
- subject_prefix TEXT,
- subscribe_auto_approval BLOB,
- subscribe_policy INTEGER,
- topics BLOB,
- topics_bodylines_limit INTEGER,
- topics_enabled BOOLEAN,
- unsubscribe_policy INTEGER,
- welcome_msg TEXT,
- PRIMARY KEY (id)
-);
-CREATE TABLE member (
- id INTEGER NOT NULL,
- role TEXT,
- mailing_list TEXT,
- is_moderated BOOLEAN,
- address_id INTEGER,
- preferences_id INTEGER,
- PRIMARY KEY (id),
- CONSTRAINT member_address_id_fk FOREIGN KEY(address_id) REFERENCES address (id),
- CONSTRAINT member_preferences_id_fk FOREIGN KEY(preferences_id) REFERENCES preferences (id)
-);
-CREATE TABLE message (
- id INTEGER NOT NULL,
- message_id_hash TEXT,
- path TEXT,
- message_id TEXT,
- PRIMARY KEY (id)
-);
-CREATE TABLE pended (
- id INTEGER NOT NULL,
- token TEXT,
- expiration_date TIMESTAMP,
- PRIMARY KEY (id)
-);
-CREATE TABLE pendedkeyvalue (
- id INTEGER NOT NULL,
- "key" TEXT,
- value TEXT,
- pended_id INTEGER,
- PRIMARY KEY (id),
- CONSTRAINT pendedkeyvalue_pended_id_fk FOREIGN KEY(pended_id) REFERENCES pended (id)
-);
-CREATE TABLE preferences (
- id INTEGER NOT NULL,
- acknowledge_posts BOOLEAN,
- hide_address BOOLEAN,
- preferred_language TEXT,
- receive_list_copy BOOLEAN,
- receive_own_postings BOOLEAN,
- delivery_mode TEXT,
- delivery_status TEXT,
- PRIMARY KEY (id)
-);
-CREATE TABLE user (
- id INTEGER NOT NULL,
- real_name TEXT,
- password TEXT,
- preferences_id INTEGER,
- PRIMARY KEY (id),
- CONSTRAINT user_preferences_id_fk FOREIGN KEY(preferences_id) REFERENCES preferences (id)
-);
-CREATE TABLE version (
- id INTEGER NOT NULL,
- component TEXT,
- version INTEGER,
- PRIMARY KEY (id)
-);
-CREATE INDEX ix__request_mailing_list_id ON _request (mailing_list_id);
-CREATE INDEX ix_address_preferences_id ON address (preferences_id);
-CREATE INDEX ix_address_user_id ON address (user_id);
-CREATE INDEX ix_member_address_id ON member (address_id);
-CREATE INDEX ix_member_preferences_id ON member (preferences_id);
-CREATE INDEX ix_pendedkeyvalue_pended_id ON pendedkeyvalue (pended_id);
-CREATE INDEX ix_user_preferences_id ON user (preferences_id);
diff --git a/mailman/database/member.py b/mailman/database/member.py
deleted file mode 100644
index 22bf042f6..000000000
--- a/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 .
-
-"""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 SystemDefaultPreferences
-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 ''.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(SystemDefaultPreferences, 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/mailman/database/message.py b/mailman/database/message.py
deleted file mode 100644
index e77e11429..000000000
--- a/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 .
-
-"""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/mailman/database/messagestore.py b/mailman/database/messagestore.py
deleted file mode 100644
index a129f47ec..000000000
--- a/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 .
-
-"""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/mailman/database/model.py b/mailman/database/model.py
deleted file mode 100644
index 85fa033c7..000000000
--- a/mailman/database/model.py
+++ /dev/null
@@ -1,56 +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 .
-
-"""Base class for all database classes."""
-
-from __future__ import absolute_import, unicode_literals
-
-__metaclass__ = type
-__all__ = [
- 'Model',
- ]
-
-from storm.properties import PropertyPublisherMeta
-
-
-
-class ModelMeta(PropertyPublisherMeta):
- """Do more magic on table classes."""
-
- _class_registry = set()
-
- def __init__(self, name, bases, dict):
- # Before we let the base class do it's thing, force an __storm_table__
- # property to enforce our table naming convention.
- self.__storm_table__ = name.lower()
- super(ModelMeta, self).__init__(name, bases, dict)
- # Register the model class so that it can be more easily cleared.
- # This is required by the test framework.
- if name == 'Model':
- return
- ModelMeta._class_registry.add(self)
-
- @staticmethod
- def _reset(store):
- for model_class in ModelMeta._class_registry:
- store.find(model_class).remove()
-
-
-
-class Model:
- """Like Storm's `Storm` subclass, but with a bit extra."""
- __metaclass__ = ModelMeta
diff --git a/mailman/database/pending.py b/mailman/database/pending.py
deleted file mode 100644
index f4c2057e0..000000000
--- a/mailman/database/pending.py
+++ /dev/null
@@ -1,177 +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 .
-
-"""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)
-
-
-
-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:
- typename, value = keyvalue.value.split('\1', 1)
- package, classname = typename.rsplit('.', 1)
- __import__(package)
- module = sys.modules[package]
- pendable[keyvalue.key] = getattr(module, classname)(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/mailman/database/preferences.py b/mailman/database/preferences.py
deleted file mode 100644
index f3ee55673..000000000
--- a/mailman/database/preferences.py
+++ /dev/null
@@ -1,50 +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 .
-
-"""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.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()
- receive_list_copy = Bool()
- receive_own_postings = Bool()
- delivery_mode = Enum()
- delivery_status = Enum()
-
- def __repr__(self):
- return ''.format(id(self))
diff --git a/mailman/database/requests.py b/mailman/database/requests.py
deleted file mode 100644
index 249feb6b6..000000000
--- a/mailman/database/requests.py
+++ /dev/null
@@ -1,138 +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 .
-
-"""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.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
-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 = config.db.pendings.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 = config.db.pendings.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.
- config.db.pendings.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/mailman/database/roster.py b/mailman/database/roster.py
deleted file mode 100644
index fc0a24c7d..000000000
--- a/mailman/database/roster.py
+++ /dev/null
@@ -1,270 +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 .
-
-"""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.locals import *
-from zope.interface import implements
-
-from mailman.config import config
-from mailman.database.address import Address
-from mailman.database.member import Member
-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 RegularMemberRoster(AbstractRoster):
- """Return all the regular delivery members of a list."""
-
- name = 'regular_members'
-
- @property
- def members(self):
- # Query for all the Members which have a role of MemberRole.member and
- # are subscribed to this mailing list. Then return only those members
- # that have a regular delivery mode.
- for member in config.db.store.find(
- Member,
- mailing_list=self._mlist.fqdn_listname,
- role=MemberRole.member):
- if member.delivery_mode == DeliveryMode.regular:
- yield member
-
-
-
-_digest_modes = (
- DeliveryMode.mime_digests,
- DeliveryMode.plaintext_digests,
- DeliveryMode.summary_digests,
- )
-
-
-
-class DigestMemberRoster(AbstractRoster):
- """Return all the regular delivery members of a list."""
-
- name = 'digest_members'
-
- @property
- def members(self):
- # Query for all the Members which have a role of MemberRole.member and
- # are subscribed to this mailing list. Then return only those members
- # that have one of the digest delivery modes.
- for member in config.db.store.find(
- Member,
- mailing_list=self._mlist.fqdn_listname,
- role=MemberRole.member):
- if member.delivery_mode in _digest_modes:
- 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/mailman/database/transaction.py b/mailman/database/transaction.py
deleted file mode 100644
index d42562389..000000000
--- a/mailman/database/transaction.py
+++ /dev/null
@@ -1,53 +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 .
-
-"""Transactional support."""
-
-from __future__ import absolute_import, unicode_literals
-
-__metaclass__ = type
-__all__ = [
- 'txn',
- ]
-
-
-from mailman.config import config
-
-
-
-class txn(object):
- """Decorator for transactional support.
-
- When the function this decorator wraps exits cleanly, the current
- transaction is committed. When it exits uncleanly (i.e. because of an
- exception, the transaction is aborted.
-
- Either way, the current transaction is completed.
- """
- def __init__(self, function):
- self._function = function
-
- def __get__(self, obj, type=None):
- def wrapper(*args, **kws):
- try:
- rtn = self._function(obj, *args, **kws)
- config.db.commit()
- return rtn
- except:
- config.db.abort()
- raise
- return wrapper
diff --git a/mailman/database/types.py b/mailman/database/types.py
deleted file mode 100644
index 2f901fe49..000000000
--- a/mailman/database/types.py
+++ /dev/null
@@ -1,64 +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 .
-
-"""Storm type conversions."""
-
-
-from __future__ import absolute_import, unicode_literals
-
-__metaclass__ = type
-__all__ = [
- 'Enum',
- ]
-
-
-import sys
-
-from storm.properties import SimpleProperty
-from storm.variables import Variable
-
-
-
-class _EnumVariable(Variable):
- """Storm variable."""
-
- def parse_set(self, value, from_db):
- if value is None:
- return None
- if not from_db:
- return value
- path, intvalue = value.rsplit(':', 1)
- modulename, classname = path.rsplit('.', 1)
- __import__(modulename)
- cls = getattr(sys.modules[modulename], classname)
- return cls[int(intvalue)]
-
- def parse_get(self, value, to_db):
- if value is None:
- return None
- if not to_db:
- return value
- return '{0}.{1}:{2}'.format(
- value.enumclass.__module__,
- value.enumclass.__name__,
- int(value))
-
-
-class Enum(SimpleProperty):
- """Custom munepy.Enum type for Storm."""
-
- variable_class = _EnumVariable
diff --git a/mailman/database/user.py b/mailman/database/user.py
deleted file mode 100644
index 23701686b..000000000
--- a/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 .
-
-"""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 ''.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/mailman/database/usermanager.py b/mailman/database/usermanager.py
deleted file mode 100644
index 3b0c8b534..000000000
--- a/mailman/database/usermanager.py
+++ /dev/null
@@ -1,103 +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 .
-
-"""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/mailman/database/version.py b/mailman/database/version.py
deleted file mode 100644
index d15065395..000000000
--- a/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 .
-
-"""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
diff --git a/mailman/docs/__init__.py b/mailman/docs/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/mailman/docs/addresses.txt b/mailman/docs/addresses.txt
deleted file mode 100644
index 9eccb2673..000000000
--- a/mailman/docs/addresses.txt
+++ /dev/null
@@ -1,231 +0,0 @@
-Email addresses
-===============
-
-Addresses represent a text email address, along with some meta data about
-those addresses, such as their registration date, and whether and when they've
-been validated. Addresses may be linked to the users that Mailman knows
-about. Addresses are subscribed to mailing lists though members.
-
- >>> usermgr = config.db.user_manager
-
-
-Creating addresses
-------------------
-
-Addresses are created directly through the user manager, which starts out with
-no addresses.
-
- >>> sorted(address.address for address in usermgr.addresses)
- []
-
-Creating an unlinked email address is straightforward.
-
- >>> address_1 = usermgr.create_address(u'aperson@example.com')
- >>> sorted(address.address for address in usermgr.addresses)
- [u'aperson@example.com']
-
-However, such addresses have no real name.
-
- >>> address_1.real_name
- u''
-
-You can also create an email address object with a real name.
-
- >>> address_2 = usermgr.create_address(
- ... u'bperson@example.com', u'Ben Person')
- >>> sorted(address.address for address in usermgr.addresses)
- [u'aperson@example.com', u'bperson@example.com']
- >>> sorted(address.real_name for address in usermgr.addresses)
- [u'', u'Ben Person']
-
-The str() of the address is the RFC 2822 preferred originator format, while
-the repr() carries more information.
-
- >>> str(address_2)
- 'Ben Person '
- >>> repr(address_2)
- ' [not verified] at 0x...>'
-
-You can assign real names to existing addresses.
-
- >>> address_1.real_name = u'Anne Person'
- >>> sorted(address.real_name for address in usermgr.addresses)
- [u'Anne Person', u'Ben Person']
-
-These addresses are not linked to users, and can be seen by searching the user
-manager for an associated user.
-
- >>> print usermgr.get_user(u'aperson@example.com')
- None
- >>> print usermgr.get_user(u'bperson@example.com')
- None
-
-You can create email addresses that are linked to users by using a different
-interface.
-
- >>> user_1 = usermgr.create_user(u'cperson@example.com', u'Claire Person')
- >>> sorted(address.address for address in user_1.addresses)
- [u'cperson@example.com']
- >>> sorted(address.address for address in usermgr.addresses)
- [u'aperson@example.com', u'bperson@example.com', u'cperson@example.com']
- >>> sorted(address.real_name for address in usermgr.addresses)
- [u'Anne Person', u'Ben Person', u'Claire Person']
-
-And now you can find the associated user.
-
- >>> print usermgr.get_user(u'aperson@example.com')
- None
- >>> print usermgr.get_user(u'bperson@example.com')
- None
- >>> usermgr.get_user(u'cperson@example.com')
-
-
-
-Deleting addresses
-------------------
-
-You can remove an unlinked address from the user manager.
-
- >>> usermgr.delete_address(address_1)
- >>> sorted(address.address for address in usermgr.addresses)
- [u'bperson@example.com', u'cperson@example.com']
- >>> sorted(address.real_name for address in usermgr.addresses)
- [u'Ben Person', u'Claire Person']
-
-Deleting a linked address does not delete the user, but it does unlink the
-address from the user.
-
- >>> sorted(address.address for address in user_1.addresses)
- [u'cperson@example.com']
- >>> user_1.controls(u'cperson@example.com')
- True
- >>> address_3 = list(user_1.addresses)[0]
- >>> usermgr.delete_address(address_3)
- >>> sorted(address.address for address in user_1.addresses)
- []
- >>> user_1.controls(u'cperson@example.com')
- False
- >>> sorted(address.address for address in usermgr.addresses)
- [u'bperson@example.com']
-
-
-Registration and validation
----------------------------
-
-Addresses have two dates, the date the address was registered on and the date
-the address was validated on. Neither date is set by default.
-
- >>> address_4 = usermgr.create_address(
- ... u'dperson@example.com', u'Dan Person')
- >>> print address_4.registered_on
- None
- >>> print address_4.verified_on
- None
-
-The registered date takes a Python datetime object.
-
- >>> from datetime import datetime
- >>> address_4.registered_on = datetime(2007, 5, 8, 22, 54, 1)
- >>> print address_4.registered_on
- 2007-05-08 22:54:01
- >>> print address_4.verified_on
- None
-
-And of course, you can also set the validation date.
-
- >>> address_4.verified_on = datetime(2007, 5, 13, 22, 54, 1)
- >>> print address_4.registered_on
- 2007-05-08 22:54:01
- >>> print address_4.verified_on
- 2007-05-13 22:54:01
-
-
-Subscriptions
--------------
-
-Addresses get subscribed to mailing lists, not users. When the address is
-subscribed, a role is specified.
-
- >>> address_5 = usermgr.create_address(
- ... u'eperson@example.com', u'Elly Person')
- >>> mlist = config.db.list_manager.create(u'_xtext@example.com')
- >>> from mailman.interfaces.member import MemberRole
- >>> address_5.subscribe(mlist, MemberRole.owner)
- on
- _xtext@example.com as MemberRole.owner>
- >>> address_5.subscribe(mlist, MemberRole.member)
- on
- _xtext@example.com as MemberRole.member>
-
-Now Elly is both an owner and a member of the mailing list.
-
- >>> sorted(mlist.owners.members)
- [ on
- _xtext@example.com as MemberRole.owner>]
- >>> sorted(mlist.moderators.members)
- []
- >>> sorted(mlist.administrators.members)
- [ on
- _xtext@example.com as MemberRole.owner>]
- >>> sorted(mlist.members.members)
- [ on
- _xtext@example.com as MemberRole.member>]
- >>> sorted(mlist.regular_members.members)
- [ on
- _xtext@example.com as MemberRole.member>]
- >>> sorted(mlist.digest_members.members)
- []
-
-
-Case-preserved addresses
-------------------------
-
-Technically speaking, email addresses are case sensitive in the local part.
-Mailman preserves the case of addresses and uses the case preserved version
-when sending the user a message, but it treats addresses that are different in
-case equivalently in all other situations.
-
- >>> address_6 = usermgr.create_address(
- ... u'FPERSON@example.com', u'Frank Person')
-
-The str() of such an address prints the RFC 2822 preferred originator format
-with the original case-preserved address. The repr() contains all the gory
-details.
-
- >>> str(address_6)
- 'Frank Person '
- >>> repr(address_6)
- ' [not verified]
- key: fperson@example.com at 0x...>'
-
-Both the case-insensitive version of the address and the original
-case-preserved version are available on attributes of the IAddress object.
-
- >>> address_6.address
- u'fperson@example.com'
- >>> address_6.original_address
- u'FPERSON@example.com'
-
-Because addresses are case-insensitive for all other purposes, you cannot
-create an address that differs only in case.
-
- >>> usermgr.create_address(u'fperson@example.com')
- Traceback (most recent call last):
- ...
- ExistingAddressError: FPERSON@example.com
- >>> usermgr.create_address(u'fperson@EXAMPLE.COM')
- Traceback (most recent call last):
- ...
- ExistingAddressError: FPERSON@example.com
- >>> usermgr.create_address(u'FPERSON@example.com')
- Traceback (most recent call last):
- ...
- ExistingAddressError: FPERSON@example.com
-
-You can get the address using either the lower cased version or case-preserved
-version. In fact, searching for an address is case insensitive.
-
- >>> usermgr.get_address(u'fperson@example.com').address
- u'fperson@example.com'
- >>> usermgr.get_address(u'FPERSON@example.com').address
- u'fperson@example.com'
diff --git a/mailman/docs/archivers.txt b/mailman/docs/archivers.txt
deleted file mode 100644
index ef36a25ac..000000000
--- a/mailman/docs/archivers.txt
+++ /dev/null
@@ -1,184 +0,0 @@
-Archivers
-=========
-
-Mailman supports pluggable archivers, and it comes with several default
-archivers.
-
- >>> from mailman.app.lifecycle import create_list
- >>> mlist = create_list(u'test@example.com')
- >>> msg = message_from_string("""\
- ... From: aperson@example.org
- ... To: test@example.com
- ... Subject: An archived message
- ... Message-ID: <12345>
- ...
- ... Here is an archived message.
- ... """)
-
-Archivers support an interface which provides the RFC 2369 List-Archive
-header, and one that provides a 'permalink' to the specific message object in
-the archive. This latter is appropriate for the message footer or for the RFC
-5064 Archived-At header.
-
-Pipermail does not support a permalink, so that interface returns None.
-Mailman defines a draft spec for how list servers and archivers can
-interoperate.
-
- >>> archivers = {}
- >>> from operator import attrgetter
- >>> for archiver in sorted(config.archivers, key=attrgetter('name')):
- ... print archiver.name
- ... print ' ', archiver.list_url(mlist)
- ... print ' ', archiver.permalink(mlist, msg)
- ... archivers[archiver.name] = archiver
- mail-archive
- http://go.mail-archive.dev/test%40example.com
- http://go.mail-archive.dev/ZaXPPxRMM9_hFZL4vTRlQlBx8pc=
- mhonarc
- http://lists.example.com/.../test@example.com
- http://lists.example.com/.../RSZCG7IGPHFIRW3EMTVMMDNJMNCVCOLE
- pipermail
- http://www.example.com/pipermail/test@example.com
- None
- prototype
- http://lists.example.com
- http://lists.example.com/RSZCG7IGPHFIRW3EMTVMMDNJMNCVCOLE
-
-
-Sending the message to the archiver
------------------------------------
-
-The archiver is also able to archive the message.
-
- >>> archivers['pipermail'].archive_message(mlist, msg)
-
- >>> import os
- >>> from mailman.interfaces.archiver import IPipermailMailingList
- >>> pckpath = os.path.join(
- ... IPipermailMailingList(mlist).archive_dir(),
- ... 'pipermail.pck')
- >>> os.path.exists(pckpath)
- True
-
-Note however that the prototype archiver can't archive messages.
-
- >>> archivers['prototype'].archive_message(mlist, msg)
- Traceback (most recent call last):
- ...
- NotImplementedError
-
-
-The Mail-Archive.com
---------------------
-
-The Mail-Archive is a public archiver that can
-be used to archive message for free. Mailman comes with a plugin for this
-archiver; by enabling it messages to public lists will get sent there
-automatically.
-
- >>> archiver = archivers['mail-archive']
- >>> print archiver.list_url(mlist)
- http://go.mail-archive.dev/test%40example.com
- >>> print archiver.permalink(mlist, msg)
- http://go.mail-archive.dev/ZaXPPxRMM9_hFZL4vTRlQlBx8pc=
-
-To archive the message, the archiver actually mails the message to a special
-address at the Mail-Archive.
-
- >>> archiver.archive_message(mlist, msg)
-
- >>> from mailman.queue.outgoing import OutgoingRunner
- >>> from mailman.testing.helpers import make_testable_runner
- >>> outgoing = make_testable_runner(OutgoingRunner, 'out')
- >>> outgoing.run()
-
- >>> from operator import itemgetter
- >>> messages = list(smtpd.messages)
- >>> len(messages)
- 1
-
- >>> print messages[0].as_string()
- From: aperson@example.org
- To: test@example.com
- Subject: An archived message
- Message-ID: <12345>
- X-Message-ID-Hash: ZaXPPxRMM9_hFZL4vTRlQlBx8pc=
- MIME-Version: 1.0
- Content-Type: text/plain; charset="us-ascii"
- Content-Transfer-Encoding: 7bit
- Sender: test-bounces@example.com
- Errors-To: test-bounces@example.com
- X-Peer: 127.0.0.1:...
- X-MailFrom: test-bounces@example.com
- X-RcptTo: archive@mail-archive.dev
-
- Here is an archived message.
- _______________________________________________
- Test mailing list
- test@example.com
- http://lists.example.com/listinfo/test@example.com
-
- >>> smtpd.clear()
-
-However, if the mailing list is not public, the message will never be archived
-at this service.
-
- >>> mlist.archive_private = True
- >>> print archiver.list_url(mlist)
- None
- >>> print archiver.permalink(mlist, msg)
- None
- >>> archiver.archive_message(mlist, msg)
- >>> list(smtpd.messages)
- []
-
-Additionally, this archiver can handle malformed Message-IDs.
-
- >>> mlist.archive_private = False
- >>> del msg['message-id']
- >>> msg['Message-ID'] = '12345>'
- >>> print archiver.permalink(mlist, msg)
- http://go.mail-archive.dev/bXvG32YzcDEIVDaDLaUSVQekfo8=
-
- >>> del msg['message-id']
- >>> msg['Message-ID'] = '<12345'
- >>> print archiver.permalink(mlist, msg)
- http://go.mail-archive.dev/9rockPrT1Mm-jOsLWS6_hseR_OY=
-
- >>> del msg['message-id']
- >>> msg['Message-ID'] = '12345'
- >>> print archiver.permalink(mlist, msg)
- http://go.mail-archive.dev/ZaXPPxRMM9_hFZL4vTRlQlBx8pc=
-
- >>> del msg['message-id']
- >>> msg['Message-ID'] = ' 12345 '
- >>> print archiver.permalink(mlist, msg)
- http://go.mail-archive.dev/ZaXPPxRMM9_hFZL4vTRlQlBx8pc=
-
-
-MHonArc
--------
-
-The MHonArc archiver is also available.
-
- >>> archiver = archivers['mhonarc']
- >>> print archiver.name
- mhonarc
-
-Messages sent to a local MHonArc instance are added to its archive via a
-subprocess call.
-
- >>> archiver.archive_message(mlist, msg)
- >>> archive_log = open(os.path.join(config.LOG_DIR, 'archiver'))
- >>> try:
- ... contents = archive_log.read()
- ... finally:
- ... archive_log.close()
- >>> print 'LOG:', contents
- LOG: ... /usr/bin/mhonarc -add
- -dbfile /.../private/test@example.com.mbox/mhonarc.db
- -outdir /.../mhonarc/test@example.com
- -stderr /.../logs/mhonarc
- -stdout /.../logs/mhonarc
- -spammode -umask 022
- ...
diff --git a/mailman/docs/bounces.txt b/mailman/docs/bounces.txt
deleted file mode 100644
index 9e8bcd23b..000000000
--- a/mailman/docs/bounces.txt
+++ /dev/null
@@ -1,107 +0,0 @@
-Bounces
-=======
-
-An important feature of Mailman is automatic bounce process.
-
-XXX Many more converted tests go here.
-
-
-Bounces, or message rejection
------------------------------
-
-Mailman can also bounce messages back to the original sender. This is
-essentially equivalent to rejecting the message with notification. Mailing
-lists can bounce a message with an optional error message.
-
- >>> mlist = config.db.list_manager.create(u'_xtest@example.com')
- >>> mlist.preferred_language = u'en'
-
-Any message can be bounced.
-
- >>> msg = message_from_string("""\
- ... To: _xtest@example.com
- ... From: aperson@example.com
- ... Subject: Something important
- ...
- ... I sometimes say something important.
- ... """)
-
-Bounce a message by passing in the original message, and an optional error
-message. The bounced message ends up in the virgin queue, awaiting sending
-to the original messageauthor.
-
- >>> switchboard = config.switchboards['virgin']
- >>> from mailman.app.bounces import bounce_message
- >>> bounce_message(mlist, msg)
- >>> len(switchboard.files)
- 1
- >>> filebase = switchboard.files[0]
- >>> qmsg, qmsgdata = switchboard.dequeue(filebase)
- >>> switchboard.finish(filebase)
- >>> print qmsg.as_string()
- Subject: Something important
- From: _xtest-owner@example.com
- To: aperson@example.com
- MIME-Version: 1.0
- Content-Type: multipart/mixed; boundary="..."
- Message-ID: ...
- Date: ...
- Precedence: bulk
-
- --...
- Content-Type: text/plain; charset="us-ascii"
- MIME-Version: 1.0
- Content-Transfer-Encoding: 7bit
-
- [No bounce details are available]
- --...
- Content-Type: message/rfc822
- MIME-Version: 1.0
-
- To: _xtest@example.com
- From: aperson@example.com
- Subject: Something important
-
- I sometimes say something important.
-
- --...--
-
-An error message can be given when the message is bounced, and this will be
-included in the payload of the text/plain part. The error message must be
-passed in as an instance of a RejectMessage exception.
-
- >>> from mailman.core.errors import RejectMessage
- >>> error = RejectMessage("This wasn't very important after all.")
- >>> bounce_message(mlist, msg, error)
- >>> len(switchboard.files)
- 1
- >>> filebase = switchboard.files[0]
- >>> qmsg, qmsgdata = switchboard.dequeue(filebase)
- >>> switchboard.finish(filebase)
- >>> print qmsg.as_string()
- Subject: Something important
- From: _xtest-owner@example.com
- To: aperson@example.com
- MIME-Version: 1.0
- Content-Type: multipart/mixed; boundary="..."
- Message-ID: ...
- Date: ...
- Precedence: bulk
-
- --...
- Content-Type: text/plain; charset="us-ascii"
- MIME-Version: 1.0
- Content-Transfer-Encoding: 7bit
-
- This wasn't very important after all.
- --...
- Content-Type: message/rfc822
- MIME-Version: 1.0
-
- To: _xtest@example.com
- From: aperson@example.com
- Subject: Something important
-
- I sometimes say something important.
-
- --...--
diff --git a/mailman/docs/chains.txt b/mailman/docs/chains.txt
deleted file mode 100644
index b6e75e6e1..000000000
--- a/mailman/docs/chains.txt
+++ /dev/null
@@ -1,345 +0,0 @@
-Chains
-======
-
-When a new message comes into the system, Mailman uses a set of rule chains to
-decide whether the message gets posted to the list, rejected, discarded, or
-held for moderator approval.
-
-There are a number of built-in chains available that act as end-points in the
-processing of messages.
-
-
-The Discard chain
------------------
-
-The Discard chain simply throws the message away.
-
- >>> from zope.interface.verify import verifyObject
- >>> from mailman.interfaces.chain import IChain
- >>> chain = config.chains['discard']
- >>> verifyObject(IChain, chain)
- True
- >>> print chain.name
- discard
- >>> print chain.description
- Discard a message and stop processing.
-
- >>> from mailman.app.lifecycle import create_list
- >>> mlist = create_list(u'_xtest@example.com')
- >>> msg = message_from_string("""\
- ... From: aperson@example.com
- ... To: _xtest@example.com
- ... Subject: My first post
- ... Message-ID:
- ...
- ... An important message.
- ... """)
-
- >>> from mailman.core.chains import process
-
- # XXX This checks the vette log file because there is no other evidence
- # that this chain has done anything.
- >>> import os
- >>> fp = open(os.path.join(config.LOG_DIR, 'vette'))
- >>> file_pos = fp.tell()
- >>> process(mlist, msg, {}, 'discard')
- >>> fp.seek(file_pos)
- >>> print 'LOG:', fp.read()
- LOG: ... DISCARD:
-
-
-
-The Reject chain
-----------------
-
-The Reject chain bounces the message back to the original sender, and logs
-this action.
-
- >>> chain = config.chains['reject']
- >>> verifyObject(IChain, chain)
- True
- >>> print chain.name
- reject
- >>> print chain.description
- Reject/bounce a message and stop processing.
- >>> file_pos = fp.tell()
- >>> process(mlist, msg, {}, 'reject')
- >>> fp.seek(file_pos)
- >>> print 'LOG:', fp.read()
- LOG: ... REJECT:
-
-The bounce message is now sitting in the Virgin queue.
-
- >>> virginq = config.switchboards['virgin']
- >>> len(virginq.files)
- 1
- >>> qmsg, qdata = virginq.dequeue(virginq.files[0])
- >>> print qmsg.as_string()
- Subject: My first post
- From: _xtest-owner@example.com
- To: aperson@example.com
- ...
- [No bounce details are available]
- ...
- Content-Type: message/rfc822
- MIME-Version: 1.0
-
- From: aperson@example.com
- To: _xtest@example.com
- Subject: My first post
- Message-ID:
-
- An important message.
-
- ...
-
-
-The Hold Chain
---------------
-
-The Hold chain places the message into the admin request database and
-depending on the list's settings, sends a notification to both the original
-sender and the list moderators.
-
- >>> chain = config.chains['hold']
- >>> verifyObject(IChain, chain)
- True
- >>> print chain.name
- hold
- >>> print chain.description
- Hold a message and stop processing.
-
- >>> file_pos = fp.tell()
- >>> process(mlist, msg, {}, 'hold')
- >>> fp.seek(file_pos)
- >>> print 'LOG:', fp.read()
- LOG: ... HOLD: _xtest@example.com post from aperson@example.com held,
- message-id=: n/a
-
-
-There are now two messages in the Virgin queue, one to the list moderators and
-one to the original author.
-
- >>> len(virginq.files)
- 2
- >>> qfiles = []
- >>> for filebase in virginq.files:
- ... qmsg, qdata = virginq.dequeue(filebase)
- ... virginq.finish(filebase)
- ... qfiles.append(qmsg)
- >>> from operator import itemgetter
- >>> qfiles.sort(key=itemgetter('to'))
-
-This message is addressed to the mailing list moderators.
-
- >>> print qfiles[0].as_string()
- Subject: _xtest@example.com post from aperson@example.com requires approval
- From: _xtest-owner@example.com
- To: _xtest-owner@example.com
- MIME-Version: 1.0
- ...
- As list administrator, your authorization is requested for the
- following mailing list posting:
-
- List: _xtest@example.com
- From: aperson@example.com
- Subject: My first post
- Reason: XXX
-
- At your convenience, visit:
-
- http://lists.example.com/admindb/_xtest@example.com
-
- to approve or deny the request.
-
- ...
- Content-Type: message/rfc822
- MIME-Version: 1.0
-
- From: aperson@example.com
- To: _xtest@example.com
- Subject: My first post
- Message-ID:
- X-Message-ID-Hash: RXJU4JL6N2OUN3OYMXXPPSCR7P7JE2BW
-
- An important message.
-
- ...
- Content-Type: message/rfc822
- MIME-Version: 1.0
-
- Content-Type: text/plain; charset="us-ascii"
- MIME-Version: 1.0
- Content-Transfer-Encoding: 7bit
- Subject: confirm ...
- Sender: _xtest-request@example.com
- From: _xtest-request@example.com
- ...
-
- If you reply to this message, keeping the Subject: header intact,
- Mailman will discard the held message. Do this if the message is
- spam. If you reply to this message and include an Approved: header
- with the list password in it, the message will be approved for posting
- to the list. The Approved: header can also appear in the first line
- of the body of the reply.
- ...
-
-This message is addressed to the sender of the message.
-
- >>> print qfiles[1].as_string()
- MIME-Version: 1.0
- Content-Type: text/plain; charset="us-ascii"
- Content-Transfer-Encoding: 7bit
- Subject: Your message to _xtest@example.com awaits moderator approval
- From: _xtest-bounces@example.com
- To: aperson@example.com
- ...
- Your mail to '_xtest@example.com' with the subject
-
- My first post
-
- Is being held until the list moderator can review it for approval.
-
- The reason it is being held:
-
- XXX
-
- Either the message will get posted to the list, or you will receive
- notification of the moderator's decision. If you would like to cancel
- this posting, please visit the following URL:
-
- http://lists.example.com/confirm/_xtest@example.com/...
-
-
-
-In addition, the pending database is holding the original messages, waiting
-for them to be disposed of by the original author or the list moderators. The
-database is essentially a dictionary, with the keys being the randomly
-selected tokens included in the urls and the values being a 2-tuple where the
-first item is a type code and the second item is a message id.
-
- >>> import re
- >>> cookie = None
- >>> for line in qfiles[1].get_payload().splitlines():
- ... mo = re.search('confirm/[^/]+/(?P.*)$', line)
- ... if mo:
- ... cookie = mo.group('cookie')
- ... break
- >>> assert cookie is not None, 'No confirmation token found'
- >>> data = config.db.pendings.confirm(cookie)
- >>> sorted(data.items())
- [(u'id', ...), (u'type', u'held message')]
-
-The message itself is held in the message store.
-
- >>> rkey, rdata = config.db.requests.get_list_requests(mlist).get_request(
- ... data['id'])
- >>> msg = config.db.message_store.get_message_by_id(
- ... rdata['_mod_message_id'])
- >>> print msg.as_string()
- From: aperson@example.com
- To: _xtest@example.com
- Subject: My first post
- Message-ID:
- X-Message-ID-Hash: RXJU4JL6N2OUN3OYMXXPPSCR7P7JE2BW
-
- An important message.
-
-
-
-The Accept chain
-----------------
-
-The Accept chain sends the message on the 'prep' queue, where it will be
-processed and sent on to the list membership.
-
- >>> chain = config.chains['accept']
- >>> verifyObject(IChain, chain)
- True
- >>> print chain.name
- accept
- >>> print chain.description
- Accept a message.
- >>> file_pos = fp.tell()
- >>> process(mlist, msg, {}, 'accept')
- >>> fp.seek(file_pos)
- >>> print 'LOG:', fp.read()
- LOG: ... ACCEPT:
-
- >>> pipelineq = config.switchboards['pipeline']
- >>> len(pipelineq.files)
- 1
- >>> qmsg, qdata = pipelineq.dequeue(pipelineq.files[0])
- >>> print qmsg.as_string()
- From: aperson@example.com
- To: _xtest@example.com
- Subject: My first post
- Message-ID:
- X-Message-ID-Hash: RXJU4JL6N2OUN3OYMXXPPSCR7P7JE2BW
-
- An important message.
-