diff options
| author | bwarsaw | 1998-06-19 19:32:48 +0000 |
|---|---|---|
| committer | bwarsaw | 1998-06-19 19:32:48 +0000 |
| commit | 99f721f65906e4f2d1036da3a886426aa0ec5aea (patch) | |
| tree | 93ffed3285a375b2f9766715fe61b03cd3aafd78 /modules | |
| parent | 664f1baa491de8a96d859f28b73aca877ce23f14 (diff) | |
| download | mailman-99f721f65906e4f2d1036da3a886426aa0ec5aea.tar.gz mailman-99f721f65906e4f2d1036da3a886426aa0ec5aea.tar.zst mailman-99f721f65906e4f2d1036da3a886426aa0ec5aea.zip | |
All these files have been moved to the Mailman directory (and some renamed)
Diffstat (limited to 'modules')
| -rw-r--r-- | modules/.cvsignore | 4 | ||||
| -rw-r--r-- | modules/Makefile.in | 89 | ||||
| -rw-r--r-- | modules/__init__.py | 0 | ||||
| -rw-r--r-- | modules/aliases.py | 57 | ||||
| -rw-r--r-- | modules/debug.py | 48 | ||||
| -rw-r--r-- | modules/flock.py | 101 | ||||
| -rw-r--r-- | modules/htmlformat.py | 468 | ||||
| -rw-r--r-- | modules/maillist.py | 922 | ||||
| -rw-r--r-- | modules/mm_admin.py | 234 | ||||
| -rw-r--r-- | modules/mm_archive.py | 161 | ||||
| -rw-r--r-- | modules/mm_bouncer.py | 416 | ||||
| -rw-r--r-- | modules/mm_cfg.py.in | 59 | ||||
| -rw-r--r-- | modules/mm_crypt.py | 8 | ||||
| -rw-r--r-- | modules/mm_defaults.py.in | 224 | ||||
| -rw-r--r-- | modules/mm_deliver.py | 249 | ||||
| -rw-r--r-- | modules/mm_digest.py | 408 | ||||
| -rw-r--r-- | modules/mm_err.py | 70 | ||||
| -rw-r--r-- | modules/mm_gateway.py | 142 | ||||
| -rw-r--r-- | modules/mm_html.py | 336 | ||||
| -rw-r--r-- | modules/mm_mailcmd.py | 607 | ||||
| -rw-r--r-- | modules/mm_mbox.py | 38 | ||||
| -rw-r--r-- | modules/mm_message.py | 268 | ||||
| -rw-r--r-- | modules/mm_pending.py | 71 | ||||
| -rw-r--r-- | modules/mm_security.py | 96 | ||||
| -rw-r--r-- | modules/mm_utils.py | 506 | ||||
| -rw-r--r-- | modules/pipermail.py | 560 | ||||
| -rw-r--r-- | modules/runcgi.py | 23 | ||||
| -rw-r--r-- | modules/smtplib.py | 126 | ||||
| -rw-r--r-- | modules/versions.py | 133 |
29 files changed, 0 insertions, 6424 deletions
diff --git a/modules/.cvsignore b/modules/.cvsignore deleted file mode 100644 index a157208f8..000000000 --- a/modules/.cvsignore +++ /dev/null @@ -1,4 +0,0 @@ -.cvsignore -mm_cfg.py -Makefile -mm_defaults.py diff --git a/modules/Makefile.in b/modules/Makefile.in deleted file mode 100644 index 73690b63c..000000000 --- a/modules/Makefile.in +++ /dev/null @@ -1,89 +0,0 @@ -# Copyright (C) 1998 by the Free Software Foundation, Inc. -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - -# NOTE: Makefile.in is converted into Makefile by the configure script -# in the parent directory. Once configure has run, you can recreate -# the Makefile by running just config.status. - -# Variables set by configure - -VERSION= @VERSION@ - -VPATH= @srcdir@ -srcdir= @srcdir@ -bindir= @bindir@ -prefix= @prefix@ -exec_prefix= @exec_prefix@ - -CC= @CC@ -CHMOD= @CHMOD@ -INSTALL= @INSTALL@ - -DEFS= @DEFS@ - -# Customizable but not set by configure - -OPT= @OPT@ -CFLAGS= $(OPT) $(DEFS) -PACKAGEDIR= $(prefix)/Mailman -CGIDIR= $(PACKAGEDIR)/Cgi -SHELL= /bin/sh - -MODULES= __init__.py aliases.py htmlformat.py maillist.py \ -mm_admin.py mm_archive.py mm_bouncer.py mm_defaults.py \ -mm_deliver.py mm_digest.py mm_err.py mm_html.py mm_mailcmd.py \ -mm_mbox.py mm_message.py mm_security.py mm_utils.py \ -mm_pending.py mm_crypt.py mm_gateway.py \ -pipermail.py smtplib.py versions.py - -CGI_MODULES= __init__.py admin.py admindb.py archives.py \ -edithtml.py handle_opts.py listinfo.py options.py private.py \ -roster.py subscribe.py - - -# Modes for directories and executables created by the install -# process. Default to group-writable directories but -# user-only-writable for executables. -DIRMODE= 775 -EXEMODE= 755 -FILEMODE= 644 -INSTALL_PROGRAM=$(INSTALL) -m $(EXEMODE) - - -# Rules - -all: - -install: - for f in $(MODULES); \ - do \ - $(INSTALL) -m $(FILEMODE) $$f $(PACKAGEDIR); \ - done - $(INSTALL) -d $(PACKAGEDIR)/Cgi - for f in $(CGI_MODULES); \ - do \ - $(INSTALL) -m $(FILEMODE) Cgi/$$f $(PACKAGEDIR)/Cgi; \ - done - $(INSTALL) -m $(FILEMODE) mm_cfg.py $(PACKAGEDIR)/mm_cfg.py.dist - if test ! -f $(PACKAGEDIR)/mm_cfg.py; \ - then \ - $(INSTALL) -m $(FILEMODE) mm_cfg.py $(PACKAGEDIR); \ - fi - -clean: - -distclean: - -rm Makefile mm_defaults.py diff --git a/modules/__init__.py b/modules/__init__.py deleted file mode 100644 index e69de29bb..000000000 --- a/modules/__init__.py +++ /dev/null diff --git a/modules/aliases.py b/modules/aliases.py deleted file mode 100644 index 525e0cfe2..000000000 --- a/modules/aliases.py +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright (C) 1998 by the Free Software Foundation, Inc. -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - -# This is mailman's interface to the alias database. - -# TODO: - -# Write a wrapper program w/ root uid that allows the mailman user -# only to update the alias database. - -import string -_file = open('/etc/aliases', 'r') -_lines = _file.readlines() -aliases = {} -_cur_line = None - -def _AddAlias(line): - line = string.strip(line) - if not line: - return - colon_index = string.find(line, ":") - if colon_index < 1: - raise "SyntaxError", "Malformed /etc/aliases file" - alias = string.lower(string.strip(line[:colon_index])) - rest = string.split(line[colon_index+1:], ",") - rest = map(string.strip, rest) - aliases[alias] = rest - -for _line in _lines: - if _line[0] == '#': - continue - if _line[0] == ' ' or _line[0] == '\t': - _cur_line = _cur_line + _line - continue - if _cur_line: - _AddAlias(_cur_line) - _cur_line = _line - -def GetAlias(str): - str = string.lower(str) - if not aliases.has_key(str): - raise KeyError, "No such alias" - return aliases[str] - diff --git a/modules/debug.py b/modules/debug.py deleted file mode 100644 index 5bd96a8c1..000000000 --- a/modules/debug.py +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright (C) 1998 by the Free Software Foundation, Inc. -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. -# -# debug.py: Utility functions for debugging. -# Michael McLay <mclay@nist.gov> wrote print_trace(). -# John Viega reconstructed print_environ since it wasn't provided... - -import sys - -def print_trace(): - print "Content-type: text/html\n" - print "<p><h3>We're sorry, we hit a bug!</h3>\n" - print "If you would like to help us identify the problem, please " - print "email a copy of this page to the webmaster for this site" - print 'with a description of what happened. Thanks!' - print "\n<PRE>" - print sys.argv - try: - import traceback - sys.stderr = sys.stdout - traceback.print_exc() - except: - print "[failed to get traceback]" - print "\n\n</PRE>" - -def print_environ(): - import os - print "<p><hr><h4>Environment variables:</h4>" - print "<table>" - print "<tr><td><strong><font size=+1>Variable</font></strong></td>" - print "<td><strong><font size=+1>Value</font></strong></td></tr>" - for (x,y) in os.environ.items(): - print "<tr><td>", x, "</td><td>", y, "</td></tr>" - print "</table>" - diff --git a/modules/flock.py b/modules/flock.py deleted file mode 100644 index b77bb44d7..000000000 --- a/modules/flock.py +++ /dev/null @@ -1,101 +0,0 @@ -# Copyright (C) 1998 by the Free Software Foundation, Inc. -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. -# -# flock.py: Portable file locking. John Viega, Jun 13, 1998 - - -"""Portable (?) file locking with timeouts. -This code should work with all versions of NFS. -The algorithm was suggested by the GNU/Linux open() man page. Make -sure no malicious people have access to link() to the lock file. -""" - -# Potential change: let the locker insert a field saying when he promises -# to be done with the lock, so if he needs more time than the other -# processes think he needs, he can say so. - -import socket, os, time - -DEFAULT_HUNG_TIMEOUT = 15 -DEFAULT_SLEEP_INTERVAL = .25 - -AlreadyCalledLockError = "AlreadyCalledLockError" -NotLockedError = "NotLockedError" -TimeOutError = "TimeOutError" - -class FileLock: - def __init__(self, lockfile, hung_timeout = DEFAULT_HUNG_TIMEOUT, - sleep_interval = DEFAULT_SLEEP_INTERVAL): - self.lockfile = lockfile - self.hung_timeout = hung_timeout - self.sleep_interval = sleep_interval - self.tmpfname = "%s.%s.%d" % (lockfile, socket.gethostname(), os.getpid()) - self.is_locked = 0 - if not os.path.exists(self.lockfile): - try: - file = open(self.lockfile, "w+") - except IOError: - pass - - # Note that no one new can grab the lock once we've opened our - # tmpfile until we close it, even if we don't have the lock. So - # checking the PID and stealing the lock are garunteed to be atomic. - def lock(self, timeout = 0): - """Blocks until the lock can be obtained. Raises a TimeOutError - exception if a positive timeout value is given and that time - elapses before the lock is obtained. - """ - if timeout > 0: - timeout_time = time.time() + timeout - last_pid = -1 - if self.locked(): - raise AlreadyCalledLockError - while 1: - os.link(self.lockfile, self.tmpfname) - if os.stat(self.tmpfname)[3] == 2: - file = open(self.tmpfname, 'w+') - file.write(`os.getpid(),self.tmpfname`) - file.close() - self.is_locked = 1 - break - if timeout and timeout_time < time.time(): - raise TimeOutError - file = open(self.tmpfname, 'r') - try: - pid,winner = eval(file.read()) - except SyntaxError: # no info in file... *can* happen - file.close() - continue - file.close() - if pid <> last_pid: - last_pid = pid - stime = time.time() - if (stime + self.hung_timeout < time.time()) and self.hung_timeout > 0: - file = open(self.tmpfname, 'w+') - file.write(`os.getpid(),self.tmpfname`) - os.unlink(winner) - self.is_locked = 1 - break - os.unlink(self.tmpfname) - time.sleep(self.sleep_interval) - # This could error if the lock is stolen. You must catch it. - def unlock(self): - if not self.locked(): - raise NotLockedError - self.is_locked = 0 - os.unlink(self.tmpfname) - def locked(self): - return os.path.exists(self.tmpfname) and self.is_locked
\ No newline at end of file diff --git a/modules/htmlformat.py b/modules/htmlformat.py deleted file mode 100644 index ddb6ac229..000000000 --- a/modules/htmlformat.py +++ /dev/null @@ -1,468 +0,0 @@ -# Copyright (C) 1998 by the Free Software Foundation, Inc. -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - - -"""Library for program-based construction of an HTML documents. - -Encapsulate HTML formatting directives in classes that act as containers -for python and, recursively, for nested HTML formatting objects.""" - -__version__ = "$Revision: 635 $" - -# Eventually could abstract down to HtmlItem, which outputs an arbitrary html -# object given start / end tags, valid options, and a value. -# Ug, objects shouldn't be adding their own newlines. The next object should. - - -import string, types - - -# Format an arbitrary object. -def HTMLFormatObject(item, indent): - "Return a presentation of an object, invoking their Format method if any." - if type(item) == type(''): - return item - elif not hasattr(item, "Format"): - return `item` - else: - return item.Format(indent) - -def CaseInsensitiveKeyedDict(d): - result = {} - for (k,v) in d.items(): - result[string.lower(k)] = v - return result - -# Given references to two dictionaries, copy the second dictionary into the -# first one. -def DictMerge(destination, fresh_dict): - for (key, value) in fresh_dict.items(): - destination[key] = value - -class Table: - def __init__(self, **table_opts): - self.cells = [] - self.cell_info = {} - self.row_info = {} - self.opts = table_opts - - def AddOptions(self, opts): - DictMerge(self.opts, opts) - - # Sets all of the cells. It writes over whatever cells you had there - # previously. - - def SetAllCells(self, cells): - self.cells = cells - - # Add a new blank row at the end - def NewRow(self): - self.cells.append([]) - - # Add a new blank cell at the end - def NewCell(self): - self.cells[-1].append('') - - def AddRow(self, row): - self.cells.append(row) - - def AddCell(self, cell): - self.cells[-1].append(cell) - - def AddCellInfo(self, row, col, **kws): - kws = CaseInsensitiveKeyedDict(kws) - if not self.cell_info.has_key(row): - self.cell_info[row] = { col : kws } - elif self.cell_info[row].has_key(col): - DictMerge(self.cell_info[row], kws) - else: - self.cell_info[row][col] = kws - - def AddRowInfo(self, row, **kws): - kws = CaseInsensitiveKeyedDict(kws) - if not self.row_info.has_key(row): - self.row_info[row] = kws - else: - DictMerge(self.row_info[row], kws) - - # What's the index for the row we just put in? - def GetCurrentRowIndex(self): - return len(self.cells)-1 - - # What's the index for the col we just put in? - def GetCurrentCellIndex(self): - return len(self.cells[-1])-1 - - def ExtractCellInfo(self, info): - valid_mods = ['align', 'valign', 'nowrap', 'rowspan', 'colspan', - 'bgcolor'] - output = '' - - for (key, val) in info.items(): - if not key in valid_mods: - continue - if key == 'nowrap': - output = output + ' NOWRAP' - continue - else: - output = output + ' %s=%s' %(string.upper(key), val) - - return output - - def ExtractRowInfo(self, info): - valid_mods = ['align', 'valign', 'bgcolor'] - output = '' - - for (key, val) in info.items(): - if not key in valid_mods: - continue - output = output + ' %s=%s' %(string.upper(key), val) - - return output - - def ExtractTableInfo(self, info): - valid_mods = ['align', 'width', 'border', 'cellspacing', 'cellpadding', - 'bgcolor'] - - output = '' - - for (key, val) in info.items(): - if not key in valid_mods: - continue - if key == 'border' and val == None: - output = output + ' BORDER' - continue - else: - output = output + ' %s=%s' %(string.upper(key), val) - - return output - - def FormatCell(self, row, col, indent): - try: - my_info = self.cell_info[row][col] - except: - my_info = None - - output = '\n' + ' '*indent + '<td' - if my_info: - output = output + self.ExtractCellInfo(my_info) - item = self.cells[row][col] - item_format = HTMLFormatObject(item, indent+4) - output = '%s>%s</td>' % (output, item_format) - return output - - def FormatRow(self, row, indent): - try: - my_info = self.row_info[row] - except: - my_info = None - - output = '\n' + ' '*indent + '<tr' - if my_info: - output = output + self.ExtractRowInfo(my_info) - output = output + '>' - - for i in range(len(self.cells[row])): - output = output + self.FormatCell(row, i, indent + 2) - - output = output + '\n' + ' '*indent + '</tr>' - - return output - - def Format(self, indent=0): - output = '\n' + ' '*indent + '<table' - output = output + self.ExtractTableInfo(self.opts) - output = output + '>' - - for i in range(len(self.cells)): - output = output + self.FormatRow(i, indent + 2) - - output = output + '\n' + ' '*indent + '</table>\n' - - return output - - -class Link: - def __init__(self, href, text, target=None): - self.href = href - self.text = text - self.target = target - - def Format(self, indent=0): - texpr = "" - if self.target != None: - texpr = ' target="%s"' % self.target - return '<a href="%s"%s>%s</a>' % (HTMLFormatObject(self.href, indent), - texpr, - HTMLFormatObject(self.text, indent)) - -class FontSize: - """FontSize is being deprecated - use FontAttr(..., size="...") instead.""" - def __init__(self, size, *items): - self.items = list(items) - self.size = size - - def Format(self, indent=0): - output = '<font size="%s">' % self.size - for item in self.items: - output = output + HTMLFormatObject(item, indent) - output = output + '</font>' - return output - -class FontAttr: - """Present arbitrary font attributes.""" - def __init__(self, *items, **kw): - self.items = list(items) - self.attrs = kw - - def Format(self, indent=0): - seq = [] - for k, v in self.attrs.items(): - seq.append('%s="%s"' % (k, v)) - output = '<font %s>' % string.join(seq, ' ') - for item in self.items: - output = output + HTMLFormatObject(item, indent) - output = output + '</font>' - return output - - -class Container: - def __init__(self, *items): - if not items: - self.items = [] - else: - self.items = items - - def AddItem(self, obj): - self.items.append(obj) - - def Format(self, indent=0): - output = '' - for item in self.items: - output = output + HTMLFormatObject(item, indent) - return output - -# My own standard document template. YMMV. -# something more abstract would be more work to use... - -class Document(Container): - title = None - - def SetTitle(self, title): - self.title = title - - def Format(self, indent=0, **kw): - output = 'Content-type: text/html\n\n' - spaces = ' ' * indent - output = output + spaces - output = output + '<html>\n<head>\n' - if self.title: - output = '%s%s<TITLE>%s</TITLE>\n' % (output, spaces, - self.title) - output = '%s%s</head>\n%s<body' % (output, spaces, spaces) - quals = [] - for k, v in kw.items(): - quals.append('%s="%s"' % (k, v)) - if quals: - output = output + ' %s>\n' % string.join(quals, " ") - else: - output = output + '>\n' - output = output + Container.Format(self, indent) - output = output + '%s</html>\n' % spaces - return output - -class HeadlessDocument(Document): - """Document without head section, for templates that provide their own.""" - def Format(self, indent=0, **kw): - output = 'Content-type: text/html\n\n' - spaces = ' ' * indent - output = output + spaces - output = output + Container.Format(self, indent) - return output - -class StdContainer(Container): - def Format(self, indent=0): - # If I don't start a new I ignore indent - output = '<%s>' % self.tag - output = output + Container.Format(self, indent) - output = '%s</%s>' % (output, self.tag) - return output - - -class Header(StdContainer): - def __init__(self, num, *items): - self.items = items - self.tag = 'h%d' % num - -class Address(StdContainer): - tag = 'address' - -class Underline(StdContainer): - tag = 'u' - -class Bold(StdContainer): - tag = 'strong' - -class Italic(StdContainer): - tag = 'em' - -class Preformatted(StdContainer): - tag = 'pre' - -class Subscript(StdContainer): - tag = 'sub' - -class Superscript(StdContainer): - tag = 'sup' - -class Strikeout(StdContainer): - tag = 'strike' - -class Center(StdContainer): - tag = 'center' - -class Form(Container): - def __init__(self, action='', method='POST', *items): - apply(Container.__init__, (self,) + items) - self.action = action - self.method = method - - def Format(self, indent=0): - spaces = ' ' * indent - output = '\n%s<FORM action="%s" method="%s">\n' % (spaces, self.action, - self.method) - output = output + Container.Format(self, indent+2) - output = '%s\n%s</FORM>\n' % (output, spaces) - return output - - -class InputObj: - def __init__(self, name, ty, value, checked, **kws): - self.name = name - self.type = ty - self.value = `value` - self.checked = checked - self.kws = kws - - def Format(self, indent=0): - output = '<INPUT name=%s type=%s value=%s' % (self.name, self.type, - self.value) - - for (key, val) in self.kws.items(): - output = '%s %s=%s' % (output, key, val) - - if self.checked: - output = output + ' CHECKED' - - output = output + '>' - return output - -class SubmitButton(InputObj): - def __init__(self, name, button_text): - InputObj.__init__(self, name, "SUBMIT", button_text, checked=0) - -class PasswordBox(InputObj): - def __init__(self, name): - InputObj.__init__(self, name, "PASSWORD", '', checked=0) - -class TextBox(InputObj): - def __init__(self, name, value='', size=10): - InputObj.__init__(self, name, "TEXT", value, checked=0, size=size) - -class TextArea: - def __init__(self, name, text='', rows=None, cols=None, wrap='soft'): - self.name = name - self.text = text - self.rows = rows - self.cols = cols - self.wrap = wrap - - def Format(self, indent=0): - output = '<TEXTAREA NAME=%s' % self.name - if self.rows: - output = output + ' ROWS=%s' % self.rows - if self.cols: - output = output + ' COLS=%s' % self.cols - if self.wrap: - output = output + ' WRAP=%s' % self.wrap - output = output + '>%s</TEXTAREA>' % self.text - return output - - -class RadioButton(InputObj): - def __init__(self, name, value, checked=0, **kws): - apply(InputObj.__init__, (self, name, 'RADIO', value, checked), kws) - -class CheckBox(InputObj): - def __init__(self, name, value, checked=0, **kws): - apply(InputObj.__init__, (self, name, "CHECKBOX", value, checked), kws) - - - -class VerticalSpacer: - def __init__(self, size=10): - self.size = size - def Format(self, indent=0): - output = '<spacer type="vertical" height="%d">' % self.size - return output - -class RadioButtonArray: - def __init__(self, name, button_names, checked = None, horizontal=1): - self.button_names = button_names - self.horizontal = horizontal - self.name = name - self.checked = checked - self.horizontal = horizontal - - def Format(self, indent=0): - t = Table(cellspacing=5) - items = [] - l = len(self.button_names) - for i in range(l): - if self.checked == i: - items.append(RadioButton(self.name, i, 1)) - items.append(self.button_names[i]) - else: - items.append(RadioButton(self.name, i)) - items.append(self.button_names[i]) - if self.horizontal: - t.AddRow(items) - else: - for item in items: - t.AddRow([item]) - return t.Format(indent) - -class UnorderedList(Container): - def Format(self, indent=0): - spaces = ' ' * indent - output = '\n%s<ul>\n' % spaces - for item in self.items: - output = output + '%s<li>%s\n' % (spaces, - HTMLFormatObject(item, - indent + 2)) - output = output + '%s</ul>\n' % spaces - return output - -class OrderedList(Container): - def Format(self, indent=0): - spaces = ' ' * indent - output = '\n%s<ol>\n' % spaces - for item in self.items: - output = output + '%s<li>%s\n' % (spaces, - HTMLFormatObject(item, indent + 2)) - output = output + '%s</ol>\n' % spaces - return output - diff --git a/modules/maillist.py b/modules/maillist.py deleted file mode 100644 index 92e84868e..000000000 --- a/modules/maillist.py +++ /dev/null @@ -1,922 +0,0 @@ -# Copyright (C) 1998 by the Free Software Foundation, Inc. -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - - -"""The class representing a Mailman mailing list. - -Mixes in many feature classes. -""" - -try: - import mm_cfg -except ImportError: - raise RuntimeError, ('missing mm_cfg - has mm_cfg_dist been configured ' - 'for the site?') - -import sys, os, marshal, string, posixfile, time -import re -import mm_utils, mm_err, flock - -from mm_admin import ListAdmin -from mm_deliver import Deliverer -from mm_mailcmd import MailCommandHandler -from mm_html import HTMLFormatter -from mm_archive import Archiver -from mm_digest import Digester -from mm_security import SecurityManager -from mm_bouncer import Bouncer -from mm_gateway import GatewayManager - -# Note: -# an _ in front of a member variable for the MailList class indicates -# a variable that does not save when we marshal our state. - -# Use mixins here just to avoid having any one chunk be too large. - -class MailList(MailCommandHandler, HTMLFormatter, Deliverer, ListAdmin, - Archiver, Digester, SecurityManager, Bouncer, GatewayManager): - def __del__(self): - self.Unlock() - def __init__(self, name=None, lock=1): - MailCommandHandler.__init__(self) - self.InitTempVars(name, lock) - if name: - self.Load() - - def InitTempVars(self, name, lock): - if name: - if name not in mm_utils.list_names(): - raise mm_err.MMUnknownListError, 'list not found: %s' % name - self._full_path = os.path.join(mm_cfg.LIST_DATA_DIR, name) - self._tmp_lock = lock - self._lock_file = None - self._internal_name = name - self._ready = 0 - self._log_files = {} # 'class': log_file_obj - lock_file_name = os.path.join(mm_cfg.LOCK_DIR, '%s.lock' % - self._internal_name) - self._lock_file = flock.FileLock(lock_file_name) - HTMLFormatter.InitTempVars(self) - self._mime_separator = '__--__--' - - - - def __del__(self): - for f in self._log_files.values(): - f.close() - - def GetAdminEmail(self): - return '%s-admin@%s' % (self._internal_name, self.host_name) - - def GetRequestEmail(self): - return '%s-request@%s' % (self._internal_name, self.host_name) - - def GetListEmail(self): - return '%s@%s' % (self._internal_name, self.host_name) - - def GetRelativeScriptURL(self, script_name): - prefix = '../'*mm_utils.GetNestingLevel() - return '%s%s/%s' % (prefix,script_name, self._internal_name) - def GetAbsoluteScriptURL(self, script_name): - if self.web_page_url: - prefix = self.web_page_url - else: - prefix = mm_cfg.DEFAULT_URL - return os.path.join(prefix, '%s/%s' % (script_name, - self._internal_name)) - - def GetAbsoluteOptionsURL(self, addr, obscured=0,): - options = self.GetAbsoluteScriptURL('options') - if obscured: - treated = mm_utils.ObscureEmail(addr, for_text=0) - else: - treated = addr - return os.path.join(options, treated) - - def GetUserOption(self, user, option): - if option == mm_cfg.Digests: - return user in self.digest_members - if not self.user_options.has_key(user): - return 0 - return not not self.user_options[user] & option - - def SetUserOption(self, user, option, value): - if not self.user_options.has_key(user): - self.user_options[user] = 0 - if value: - self.user_options[user] = self.user_options[user] | option - else: - self.user_options[user] = self.user_options[user] & ~(option) - if not self.user_options[user]: - del self.user_options[user] - self.Save() - - def FindUser(self, email): - matches = mm_utils.FindMatchingAddresses(email, - (self.members - + self.digest_members)) - if not matches or not len(matches): - return None - return matches[0] - - def InitVars(self, name=None, admin='', crypted_password=''): - """Assign default values - some will be overriden by stored state.""" - # Must save this state, even though it isn't configurable - self.volume = 1 - self.members = [] # self.digest_members is initted in mm_digest - self.data_version = mm_cfg.VERSION - self.last_post_time = 0 - - self.post_id = 1. # A float so it never has a chance to overflow. - self.user_options = {} - - # This stuff is configurable - self.filter_prog = mm_cfg.DEFAULT_FILTER_PROG - self.dont_respond_to_post_requests = 0 - self.num_spawns = mm_cfg.DEFAULT_NUM_SPAWNS - self.advertised = mm_cfg.DEFAULT_LIST_ADVERTISED - self.max_num_recipients = mm_cfg.DEFAULT_MAX_NUM_RECIPIENTS - self.max_message_size = mm_cfg.DEFAULT_MAX_MESSAGE_SIZE - self.web_page_url = mm_cfg.DEFAULT_URL - self.owner = [admin] - self.reply_goes_to_list = mm_cfg.DEFAULT_REPLY_GOES_TO_LIST - self.posters = [] - self.forbidden_posters = [] - self.admin_immed_notify = mm_cfg.DEFAULT_ADMIN_IMMED_NOTIFY - self.moderated = mm_cfg.DEFAULT_MODERATED - self.require_explicit_destination = \ - mm_cfg.DEFAULT_REQUIRE_EXPLICIT_DESTINATION - self.acceptable_aliases = mm_cfg.DEFAULT_ACCEPTABLE_ALIASES - self.reminders_to_admins = mm_cfg.DEFAULT_REMINDERS_TO_ADMINS - self.send_reminders = mm_cfg.DEFAULT_SEND_REMINDERS - self.send_welcome_msg = mm_cfg.DEFAULT_SEND_WELCOME_MSG - self.bounce_matching_headers = \ - mm_cfg.DEFAULT_BOUNCE_MATCHING_HEADERS - self.anonymous_list = mm_cfg.DEFAULT_ANONYMOUS_LIST - self.real_name = '%s%s' % (string.upper(self._internal_name[0]), - self._internal_name[1:]) - self.description = '' - self.info = '' - self.welcome_msg = '' - self.goodbye_msg = '' - self.open_subscribe = mm_cfg.DEFAULT_OPEN_SUBSCRIBE - self.private_roster = mm_cfg.DEFAULT_PRIVATE_ROSTER - self.obscure_addresses = mm_cfg.DEFAULT_OBSCURE_ADDRESSES - self.member_posting_only = mm_cfg.DEFAULT_MEMBER_POSTING_ONLY - self.web_subscribe_requires_confirmation = \ - mm_cfg.DEFAULT_WEB_SUBSCRIBE_REQUIRES_CONFIRMATION - self.host_name = mm_cfg.DEFAULT_HOST_NAME - - # Analogs to these are initted in Digester.InitVars - self.nondigestable = mm_cfg.DEFAULT_NONDIGESTABLE - - Digester.InitVars(self) # has configurable stuff - SecurityManager.InitVars(self, crypted_password) - HTMLFormatter.InitVars(self) - Archiver.InitVars(self) # has configurable stuff - ListAdmin.InitVars(self) - Bouncer.InitVars(self) - GatewayManager.InitVars(self) - - # These need to come near the bottom because they're dependent on - # other settings. - self.subject_prefix = mm_cfg.DEFAULT_SUBJECT_PREFIX % self.__dict__ - self.msg_header = mm_cfg.DEFAULT_MSG_HEADER - self.msg_footer = mm_cfg.DEFAULT_MSG_FOOTER - - def GetConfigInfo(self): - config_info = {} - config_info['digest'] = Digester.GetConfigInfo(self) - config_info['archive'] = Archiver.GetConfigInfo(self) - config_info['gateway'] = GatewayManager.GetConfigInfo(self) - - config_info['general'] = [ - "Fundamental list characteristics, including descriptive" - " info and basic behaviors.", - ('real_name', mm_cfg.String, 50, 0, - 'The public name of this list (make case-changes only).', - - "The capitalization of this name can be changed to make it" - " presentable in polite company as a proper noun, or to make an" - " acronym part all upper case, etc. However, the name" - " will be advertised as the email address (e.g., in subscribe" - " confirmation notices), so it should <em>not</em> be otherwise" - " altered. (Email addresses are not case sensitive, but" - " they are sensitive to almost everything else:-)"), - - ('owner', mm_cfg.EmailList, (3,30), 0, - "The list admin's email address - having multiple" - " admins/addresses (on separate lines) is ok."), - - ('description', mm_cfg.String, 50, 0, - 'A terse phrase identifying this list.', - - "This description is used when the maillist is listed with" - " other maillists, or in headers, and so forth. It should" - " be as succinct as you can get it, while still identifying" - " what the list is."), - - ('info', mm_cfg.Text, (7, 50), 0, - ' An introductory description - a few paragraphs - about the' - ' list. It will be included, as html, at the top of the' - ' listinfo page. Carriage returns will end a paragraph - see' - ' the details for more info.', - - "The text will be treated as html <em>except</em> that newlines" - " newlines will be translated to <br> - so you can use" - " links, preformatted text, etc, but don't put in carriage" - " returns except where you mean to separate paragraphs. And" - " review your changes - bad html (like some unterminated HTML" - " constructs) can prevent display of the entire listinfo page."), - - ('subject_prefix', mm_cfg.String, 10, 0, - 'Prefix for subject line of list postings.', - - "This text will be prepended to subject lines of messages" - " posted to the list, to distinguish maillist messages in" - " in mailbox summaries. Brevity is premium here, it's ok" - " to shorten long maillist names to something more concise," - " as long as it still identifies the maillist."), - - ('welcome_msg', mm_cfg.Text, (4, 50), 0, - 'List-specific text prepended to new-subscriber welcome message', - - "This value, if any, will be added to the front of the" - " new-subscriber welcome message. The rest of the" - " welcome message already describes the important addresses" - " and URLs for the maillist, so you don't need to include" - " any of that kind of stuff here. This should just contain" - " mission-specific kinds of things, like etiquette policies" - " or team orientation, or that kind of thing."), - - ('goodbye_msg', mm_cfg.Text, (4, 50), 0, - 'Text sent to people leaving the list. If empty, no special' - ' text will be added to the unsubscribe message.'), - - ('reply_goes_to_list', mm_cfg.Radio, ('Poster', 'List'), 0, - 'Are replies to a post directed to the original poster' - ' or to the list? <tt>Poster</tt> is <em>strongly</em>' - ' recommended.', - - "There are many reasons not to introduce headers like reply-to" - " into other peoples messages - one is that some posters depend" - " on their own reply-to setting to convey their valid email" - " addr. See" - ' <a href="http://www.unicom.com/pw/reply-to-harmful.html">' - '"Reply-To" Munging Considered Harmful</a> for a general.' - " discussion of this issue."), - - ('reminders_to_admins', mm_cfg.Radio, ('No', 'Yes'), 0, - 'Send password reminders to "-admin" address instead of' - ' directly to user.', - - "Set this to yes when this list is intended only to cascade to" - " other maillists. When set, the password reminders will be" - " directed to an address derived from the member's address" - ' - it will have "-admin" appended to the member\'s account' - " name."), - - ('send_reminders', mm_cfg.Radio, ('No', 'Yes'), 0, - 'Send monthly password reminders or no? Overrides the previous ' - 'option.'), - - ('send_welcome_msg', mm_cfg.Radio, ('No', 'Yes'), 0, - 'Send welcome message when people subscribe?', - "Turn this on only if you plan on subscribing people manually " - "and don't want them to know that you did so. This option " - "is most useful for transparently migrating lists from " - "some other mailing list manager to Mailman."), - - - ('admin_immed_notify', mm_cfg.Radio, ('No', 'Yes'), 0, - 'Should administrator get immediate notice of new requests, ' - 'as well as daily notices about collected ones?', - - "List admins are sent daily reminders of pending admin approval" - " requests, like subscriptions to a moderated list or postings" - " that are being held for one reason or another. Setting this" - " option causes notices to be sent immediately on the arrival" - " of new requests, as well."), - - ('dont_respond_to_post_requests', mm_cfg.Radio, ('Yes', 'No'), 0, - 'Send mail to poster when their posting is held for approval?', - - "Approval notices are sent when mail triggers certain of the" - " limits <em>except</em> routine list moderation and spam" - " filters, for which notices are <em>not</em> sent. This" - " option overrides ever sending the notice."), - - # XXX UNSAFE! Perhaps more selective capability could be - # offered, with some kind of super-admin option, but for now - # let's not even expose this. (Apparently was never - # implemented, anyway.) -## ('filter_prog', mm_cfg.String, 40, 0, -## 'Program for pre-processing text, if any? ' -## '(Useful, eg, for signature auto-stripping, etc...)'), - - ('max_message_size', mm_cfg.Number, 3, 0, - 'Maximum length in Kb of a message body. Use 0 for no limit.'), - - ('num_spawns', mm_cfg.Number, 3, 0, - 'Number of outgoing connections to open at once ' - '(expert users only).', - - "This determines the maximum number of batches into which" - " a mass posting will be divided."), - - ('host_name', mm_cfg.Host, 50, 0, 'Host name this list prefers.', - - "The host_name is the preferred name for email to mailman-related" - " addresses on this host, and generally should be the mail" - " host's exchanger address, if any. This setting can be useful" - " for selecting among alternative names of a host that has" - " multiple addresses."), - - ('web_page_url', mm_cfg.String, 50, 0, - 'Base URL for Mailman web interface', - - "This is the common root for all mailman URLs concerning this" - " list. It can be useful for selecting a particular URL" - " of a host that has multiple addresses."), - ] - - config_info['privacy'] = [ - "List access policies, including anti-spam measures," - " covering members and outsiders." - ' (See also the <a href="%s">Archival Options section</a> for' - ' separate archive-privacy settings.)' - % os.path.join(self.GetRelativeScriptURL('admin'), 'archive'), - - "Subscribing", - - ('advertised', mm_cfg.Radio, ('No', 'Yes'), 0, - 'Advertise this list when people ask what lists are on ' - 'this machine?'), - - ('open_subscribe', mm_cfg.Radio, ('No', 'Yes'), 0, - 'Are subscribes done without admins approval (ie, is this' - ' an <em>open</em> list)?', - - "Disabling this option makes the list <em>closed</em>, where" - " members are admitted only at the discretion of the list" - " administrator."), - - ('web_subscribe_requires_confirmation', mm_cfg.Radio, - ('None', 'Requestor confirms via email', 'Admin approves'), 0, - 'What confirmation is required for on-the-web subscribes?', - - "This option determines whether web-initiated subscribes" - " require further confirmation, either from the subscribed" - " address or from the list administrator. Absence of" - " <em>any</em> confirmation makes web-based subscription a" - " tempting opportunity for mischievous subscriptions by third" - " parties."), - - "Membership exposure", - - ('private_roster', mm_cfg.Radio, - ('Anyone', 'List members', 'List admin only'), 0, - 'Who can view subscription list?', - - "When set, the list of subscribers is protected by" - " member or admin password authentication."), - - ('obscure_addresses', mm_cfg.Radio, ('No', 'Yes'), 0, - "Show member addrs so they're not directly recognizable" - ' as email addrs?', - - "Setting this option causes member email addresses to be" - " transformed when they are presented on list web pages (both" - " in text and as links), so they're not trivially" - " recognizable as email addresses. The intention is to" - " to prevent the addresses from being snarfed up by" - " automated web scanners for use by spammers."), - - "General posting filters", - - ('moderated', mm_cfg.Radio, ('No', 'Yes'), 0, - 'Must posts be approved by a moderator?', - - "If the 'posters' option has any entries then it supercedes" - " this setting."), - - ('member_posting_only', mm_cfg.Radio, ('No', 'Yes'), 0, - 'Restrict posting privilege to only list members?'), - - ('posters', mm_cfg.EmailList, (5, 30), 1, - 'Addresses blessed for posting to this list. (Adding' - ' anyone to this list implies moderation of everyone else.)', - - "Adding any entries to this list supercedes the setting of" - " the list-moderation option."), - - "Spam-specific posting filters", - - ('require_explicit_destination', mm_cfg.Radio, ('No', 'Yes'), 0, - 'Must posts have list named in destination (to, cc) field' - ' (or be among the acceptable alias names, specified below)?', - - "Many (in fact, most) spams do not explicitly name their myriad" - " destinations in the explicit destination addresses - in fact," - " often the to field has a totally bogus address for" - " obfuscation. The constraint applies only to the stuff in" - " the address before the '@' sign, but still catches all such" - " spams." - "<p>The cost is that the list will not accept unhindered any" - " postings relayed from other addresses, unless <ol>" - " <li>The relaying address has the same name, or" - " <li>The relaying address name is included on the options that" - " specifies acceptable aliases for the list. </ol>"), - - ('acceptable_aliases', mm_cfg.Text, ('4', '30'), 0, - 'Alias names (regexps) which qualify as explicit to or cc' - ' destination names for this list.', - - "Alternate list names (the stuff before the '@') that are to be" - " accepted when the explicit-destination constraint (a prior" - " option) is active. This enables things like cascading" - " maillists and relays while the constraint is still" - " preventing random spams."), - - ('max_num_recipients', mm_cfg.Number, 3, 0, - 'Ceiling on acceptable number of recipients for a posting.', - - "If a posting has this number, or more, of recipients, it is" - " held for admin approval. Use 0 for no ceiling."), - - ('forbidden_posters', mm_cfg.EmailList, (5, 30), 1, - 'Addresses whose postings are always held for approval.', - - "Email addresses whose posts should always be held for" - " approval, no matter what other options you have set." - " See also the subsequent option which applies to arbitrary" - " content of arbitrary headers."), - - ('bounce_matching_headers', mm_cfg.Text, ('6', '50'), 0, - 'Hold posts with header value matching a specified regexp.', - - "Use this option to prohibit posts according to specific header" - " values. The target value is a regular-expression for" - " matching against the specified header. The match is done" - " disregarding letter case. Lines beginning with '#' are" - " ignored as comments." - "<p>For example:<pre>to: .*@public.com </pre> says" - " to hold all postings with a <em>to</em> mail header" - " containing '@public.com' anywhere among the addresses." - "<p>Note that leading whitespace is trimmed from the" - " regexp. This can be circumvented in a number of ways, eg" - " by escaping or bracketing it." - "<p> See also the <em>forbidden_posters</em> option for" - " a related mechanism."), - ('anonymous_list', mm_cfg.Radio, ('No', 'Yes'), 0, - 'Hide the sender of a message, replacing it with the list ' - 'address (Removes From, Sender and Reply-To fields)'), - - ] - - config_info['nondigest'] = [ - "Policies concerning immediately delivered list traffic.", - - ('nondigestable', mm_cfg.Toggle, ('No', 'Yes'), 1, - 'Can subscribers choose to receive mail immediately,' - ' rather than in batched digests?'), - - ('msg_header', mm_cfg.Text, (4, 55), 0, - 'Header added to mail sent to regular list members', - - "Text prepended to the top of every immediately-delivery" - " message. <p>" + mm_err.MESSAGE_DECORATION_NOTE), - - ('msg_footer', mm_cfg.Text, (4, 55), 0, - 'Footer added to mail sent to regular list members', - - "Text appended to the bottom of every immediately-delivery" - " message. <p>" + mm_err.MESSAGE_DECORATION_NOTE), - ] - - config_info['bounce'] = Bouncer.GetConfigInfo(self) - return config_info - - def Create(self, name, admin, crypted_password): - if name in mm_utils.list_names(): - raise ValueError, 'List %s already exists.' % name - else: - mm_utils.MakeDirTree(os.path.join(mm_cfg.LIST_DATA_DIR, name)) - self._full_path = os.path.join(mm_cfg.LIST_DATA_DIR, name) - self._internal_name = name - self.Lock() - self.InitVars(name, admin, crypted_password) - self._ready = 1 - self.InitTemplates() - self.Save() - self.CreateFiles() - - def CreateFiles(self): - # Touch these files so they have the right dir perms no matter what. - # A "just-in-case" thing. This shouldn't have to be here. - ou = os.umask(002) - try: - import mm_archive -## open(os.path.join(self._full_path, -## mm_archive.ARCHIVE_PENDING), "a+").close() -## open(os.path.join(self._full_path, -## mm_archive.ARCHIVE_RETAIN), "a+").close() - open(os.path.join(mm_cfg.LOCK_DIR, '%s.lock' % - self._internal_name), 'a+').close() - open(os.path.join(self._full_path, "next-digest"), "a+").close() - open(os.path.join(self._full_path, "next-digest-topics"), - "a+").close() - finally: - os.umask(ou) - - def Save(self): - # If more than one client is manipulating the database at once, we're - # pretty hosed. That's a good reason to make this a daemon not a - # program. - self.IsListInitialized() - ou = os.umask(002) - try: - fname = os.path.join(self._full_path, 'config.db') - fname_last = fname + ".last" - if os.path.exists(fname_last): - os.unlink(fname_last) - if os.path.exists(fname): - os.link(fname, fname_last) - os.unlink(fname) - file = open(fname, 'w') - finally: - os.umask(ou) - dict = {} - for (key, value) in self.__dict__.items(): - if key[0] <> '_': - dict[key] = value - marshal.dump(dict, file) - file.close() - - def Load(self, check_version = 1): - if self._tmp_lock: - self.Lock() - try: - file = open(os.path.join(self._full_path, 'config.db'), 'r') - except IOError: - raise mm_cfg.MMBadListError, 'Failed to access config info' - try: - dict = marshal.load(file) - except (EOFError, ValueError, TypeError): - raise mm_cfg.MMBadListError, 'Failed to unmarshal config info' - for (key, value) in dict.items(): - setattr(self, key, value) - file.close() - self._ready = 1 - if check_version: - self.CheckValues() - self.CheckVersion(dict) - - def LogMsg(self, kind, msg, *args): - """Append a message to the log file for messages of specified kind.""" - # For want of a better fallback, we use sys.stderr if we can't get - # a log file. We need a better way to warn of failed log access... - if self._log_files.has_key(kind): - logf = self._log_files[kind] - else: - logf = self._log_files[kind] = mm_utils.StampedLogger(kind) - logf.write("%s\n" % (msg % args)) - logf.flush() - - def CheckVersion(self, stored_state): - """Migrate prior version's state to new structure, if changed.""" - if (self.data_version >= mm_cfg.DATA_FILE_VERSION and - type(self.data_version) == type(mm_cfg.DATA_FILE_VERSION)): - return - else: - self.InitVars() # Init any new variables, - self.Load(check_version = 0) # then reload the file - from versions import Update - Update(self, stored_state) - self.data_version = mm_cfg.DATA_FILE_VERSION - self.Save() - - def CheckValues(self): - """Normalize selected values to known formats.""" - if self.web_page_url and self.web_page_url[-1] != '/': - self.web_page_url = self.web_page_url + '/' - - def IsListInitialized(self): - if not self._ready: - raise mm_err.MMListNotReady - - def AddMember(self, name, password, digest=0, web_subscribe=0): - self.IsListInitialized() - # Remove spaces... it's a common thing for people to add... - name = string.join(string.split(string.lower(name)), '') - - # Validate the e-mail address to some degree. - if not mm_utils.ValidEmail(name): - raise mm_err.MMBadEmailError - if self.IsMember(name): - raise mm_err.MMAlreadyAMember - - if digest and not self.digestable: - raise mm_err.MMCantDigestError - elif not digest and not self.nondigestable: - raise mm_err.MMMustDigestError - - if self.open_subscribe: - if (web_subscribe and self.web_subscribe_requires_confirmation): - if self.web_subscribe_requires_confirmation == 1: - # Requester confirmation required. - raise mm_err.MMWebSubscribeRequiresConfirmation - else: - # Admin approval required. - self.AddRequest('add_member', digest, name, password) - else: - # No approval required. - self.ApprovedAddMember(name, password, digest) - else: - # Blanket admin approval requred... - self.AddRequest('add_member', digest, name, password) - - def ApprovedAddMember(self, name, password, digest, noack=0): - # XXX klm: It *might* be nice to leave the case of the name alone, - # but provide a common interface that always returns the - # lower case version for computations. - name = string.lower(name) - if self.IsMember(name): - raise mm_err.MMAlreadyAMember - if digest: - self.digest_members.append(name) - kind = " (D)" - else: - self.members.append(name) - kind = "" - self.LogMsg("subscribe", "%s: new%s %s", - self._internal_name, kind, name) - self.passwords[name] = password - self.Save() - if not noack: - self.SendSubscribeAck(name, password, digest) - - def DeleteMember(self, name, whence=None): - self.IsListInitialized() -# FindMatchingAddresses *should* never return more than 1 address. -# However, should log this, just to make sure. - aliases = mm_utils.FindMatchingAddresses(name, self.members + - self.digest_members) - if not len(aliases): - raise mm_err.MMNoSuchUserError - - def DoActualRemoval(alias, me=self): - kind = "(unfound)" - try: - del me.passwords[alias] - except KeyError: - pass - if me.user_options.has_key(alias): - del me.user_options[alias] - try: - me.members.remove(alias) - kind = "regular" - except ValueError: - pass - try: - me.digest_members.remove(alias) - kind = "digest" - except ValueError: - pass - - map(DoActualRemoval, aliases) - if self.goodbye_msg and len(self.goodbye_msg): - self.SendUnsubscribeAck(name) - self.ClearBounceInfo(name) - self.Save() - if whence: whence = "; %s" % whence - else: whence = "" - self.LogMsg("subscribe", "%s: deleted %s%s", - self._internal_name, name, whence) - - def IsMember(self, address): - return len(mm_utils.FindMatchingAddresses(address, self.members + - self.digest_members)) - - def HasExplicitDest(self, msg): - """True if list name or any acceptable_alias is included among the - to or cc addrs.""" - # Note that qualified host can be different! This allows, eg, for - # relaying from remote lists that have the same name. Still - # stringent, but offers a way to provide for remote exploders. - lowname = string.lower(self.real_name) - recips = [] - # First check all dests against simple name: - for recip in msg.getaddrlist('to') + msg.getaddrlist('cc'): - curr = string.lower(string.split(recip[1], '@')[0]) - if lowname == curr: - return 1 - recips.append(curr) - # ... and only then try the regexp acceptable aliases. - for recip in recips: - for alias in string.split(self.acceptable_aliases, '\n'): - stripped = string.strip(alias) - if stripped and re.match(stripped, recip): - return 1 - return 0 - - def parse_matching_header_opt(self): - """Return a list of triples [(field name, regex, line), ...].""" - # - Blank lines and lines with '#' as first char are skipped. - # - Leading whitespace in the matchexp is trimmed - you can defeat - # that by, eg, containing it in gratuitous square brackets. - all = [] - for line in string.split(self.bounce_matching_headers, '\n'): - stripped = string.strip(line) - if not stripped or (stripped[0] == "#"): - # Skip blank lines and lines *starting* with a '#'. - continue - else: - try: - h, e = re.split(":[ ]*", stripped) - all.append((h, e, stripped)) - except ValueError: - # Whoops - some bad data got by: - self.LogMsg("config", "%s - " - "bad bounce_matching_header line %s" - % (self.real_name, `stripped`)) - return all - - - def HasMatchingHeader(self, msg): - """True if named header field (case-insensitive matches regexp. - - Case insensitive. - - Returns constraint line which matches or empty string for no - matches.""" - - pairs = self.parse_matching_header_opt() - - for field, matchexp, line in pairs: - fragments = msg.getallmatchingheaders(field) - subjs = [] - l = len(field) - for f in fragments: - # Consolidate header lines, stripping header name & whitespace. - if (len(f) > l - and f[l] == ":" - and string.lower(field) == string.lower(f[0:l])): - # Non-continuation line - trim header name: - subjs.append(f[l+2:]) - elif not subjs: - # Whoops - non-continuation that matches? - subjs.append(f) - else: - # Continuation line. - subjs[-1] = subjs[-1] + f - for s in subjs: - if re.search(matchexp, s, re.I): - return line - return 0 - -#msg should be an IncomingMessage object. - def Post(self, msg, approved=0): - self.IsListInitialized() - # Be sure to ExtractApproval, whether or not flag is already set! - msgapproved = self.ExtractApproval(msg) - if not approved: - approved = msgapproved - sender = msg.GetSender() - # If it's the admin, which we know by the approved variable, - # we can skip a large number of checks. - if not approved: - from_lists = msg.getallmatchingheaders('x-beenthere') - if self.GetListEmail() in from_lists: - self.AddRequest('post', mm_utils.SnarfMessage(msg), - mm_err.LOOPING_POST, - msg.getheader('subject')) - if len(self.forbidden_posters): - addrs = mm_utils.FindMatchingAddresses(sender, - self.forbidden_posters) - if len(addrs): - self.AddRequest('post', mm_utils.SnarfMessage(msg), - mm_err.FORBIDDEN_SENDER_MSG, - msg.getheader('subject')) - if len(self.posters): - addrs = mm_utils.FindMatchingAddresses(sender, self.posters) - if not len(addrs): - self.AddRequest('post', mm_utils.SnarfMessage(msg), - 'Only approved posters may post without ' - 'moderator approval.', - msg.getheader('subject')) - elif self.moderated: - self.AddRequest('post', mm_utils.SnarfMessage(msg), - mm_err.MODERATED_LIST_MSG, - msg.getheader('subject')) - if self.member_posting_only and not self.IsMember(sender): - self.AddRequest('post', mm_utils.SnarfMessage(msg), - 'Postings from member addresses only.', - msg.getheader('subject')) - if self.max_num_recipients > 0: - recips = [] - toheader = msg.getheader('to') - if toheader: - recips = recips + string.split(toheader, ',') - ccheader = msg.getheader('cc') - if ccheader: - recips = recips + string.split(ccheader, ',') - if len(recips) > self.max_num_recipients: - self.AddRequest('post', mm_utils.SnarfMessage(msg), - 'Too many recipients.', - msg.getheader('subject')) - if (self.require_explicit_destination and - not self.HasExplicitDest(msg)): - self.AddRequest('post', mm_utils.SnarfMessage(msg), - mm_err.IMPLICIT_DEST_MSG, - msg.getheader('subject')) - if self.bounce_matching_headers: - triggered = self.HasMatchingHeader(msg) - if triggered: - # Darn - can't include the matching line for the admin - # message because the info would also go to the sender. - self.AddRequest('post', mm_utils.SnarfMessage(msg), - mm_err.SUSPICIOUS_HEADER_MSG, - msg.getheader('subject')) - if self.max_message_size > 0: - if len(msg.body)/1024. > self.max_message_size: - self.AddRequest('post', mm_utils.SnarfMessage(msg), - 'Message body too long (>%dk)' % - self.max_message_size, - msg.getheader('subject')) - # Prepend the subject_prefix to the subject line. - subj = msg.getheader('subject') - prefix = self.subject_prefix - if not subj: - msg.SetHeader('Subject', '%s(no subject)' % prefix) - elif not re.match("(re:? *)?" + re.escape(self.subject_prefix), - subj, re.I): - msg.SetHeader('Subject', '%s%s' % (prefix, subj)) - if self.anonymous_list: - del msg['reply-to'] - del msg['sender'] - msg.SetHeader('From', self.GetAdminEmail()) - if self.digestable: - self.SaveForDigest(msg) - if self.archive: - self.ArchiveMail(msg) - if self.gateway_to_news: - self.SendMailToNewsGroup(msg) - - dont_send_to_sender = 0 - ack_post = 0 - # Try to get the address the list thinks this sender is - sender = self.FindUser(msg.GetSender()) - if sender: - if self.GetUserOption(sender, mm_cfg.DontReceiveOwnPosts): - dont_send_to_sender = 1 - if self.GetUserOption(sender, mm_cfg.AcknowlegePosts): - ack_post = 1 - # Deliver the mail. - recipients = self.members[:] - if dont_send_to_sender: - recipients.remove(sender) - def DeliveryEnabled(x, s=self, v=mm_cfg.DisableDelivery): - return not s.GetUserOption(x, v) - recipients = filter(DeliveryEnabled, recipients) - self.DeliverToList(msg, recipients, - header = self.msg_header % self.__dict__, - footer = self.msg_footer % self.__dict__) - if ack_post: - self.SendPostAck(msg, sender) - self.last_post_time = time.time() - self.post_id = self.post_id + 1 - self.Save() - - def Locked(self): - if not self._lock_file: - return 0 - return self._lock_file.locked() - - def Lock(self): - if self._lock_file.locked(): - return - self._lock_file.lock() - - def Unlock(self): - self._lock_file.unlock() - - def __repr__(self): - if self.Locked(): status = " (locked)" - else: status = "" - return ("<%s.%s %s%s at %s>" - % (self.__module__, self.__class__.__name__, - `self._internal_name`, status, hex(id(self))[2:])) diff --git a/modules/mm_admin.py b/modules/mm_admin.py deleted file mode 100644 index a1d775900..000000000 --- a/modules/mm_admin.py +++ /dev/null @@ -1,234 +0,0 @@ -# Copyright (C) 1998 by the Free Software Foundation, Inc. -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - - -"""Mixin class which handles of administrative requests.""" - - -# When an operation can't be completed, and is sent to the list admin for -# Handling, we consider that an error condition, and raise MMNeedApproval - -import mm_err, mm_cfg, mm_message -import os, marshal, time, string - -SUBSCRIPTION_AUTH_TEXT = """ -Your authorization is required for a maillist subscription request approval: - - For: %s - List: %s@%s - -At your convenience, visit: - - %s - -to process the request.""" - -POSTING_AUTH_TEXT = """ -Your authorization is required for a maillist posting request approval: - - List: %s@%s - Reason held: %s - From: %s - Subject: %s - -At your convenience, visit: - - %s - -to approve or deny the request.""" - -class ListAdmin: - def InitVars(self): - # Non-configurable data: - self.requests = {} - self.next_request_id = 1 - - def AddRequest(self, request, *args): - now = time.time() - request_id = self.GetRequestId() - if not self.requests.has_key(request): - self.requests[request] = [(request_id, now) + args] - else: - self.requests[request].append( (request_id, now) + args ) - self.Save() - if request == 'add_member': - who = args[1] - self.LogMsg("vette", ("%s: Subscribe request: %s" - % (self.real_name, who))) - if self.admin_immed_notify: - subj = 'New %s subscription request: %s' % (self.real_name, - who) - self.SendTextToUser(subject = subj, - recipient = self.GetAdminEmail(), - text = (SUBSCRIPTION_AUTH_TEXT - % (who, - self.real_name, - self.host_name, - self.GetAbsoluteScriptURL('admindb')))) - raise mm_err.MMNeedApproval, "Admin approval required to subscribe" - - elif request == 'post': - sender = args[0][0] - reason = args[1] - subject = args[2] - self.LogMsg("vette", ("%s: %s post hold\n\t%s" - % (self.real_name, sender, `reason`))) - if self.admin_immed_notify: - subj = '%s post approval required for %s' % (self.real_name, - sender) - self.SendTextToUser(subject = subj, - recipient = self.GetAdminEmail(), - text = (POSTING_AUTH_TEXT - % (self.real_name, - self.host_name, - reason, - sender, - subject, - self.GetAbsoluteScriptURL('admindb')))) - raise mm_err.MMNeedApproval, args[1] - - def CleanRequests(self): - for (key, val) in self.requests.items(): - if not len(val): - del self.requests[key] - - def GetRequest(self, id): - for (key, val) in self.requests.items(): - for i in range(len(val)): - if val[i][0] == id: - return (key, i) - raise mm_err.MMBadRequestId - - def RemoveRequest(self, id): - for (key, val) in self.requests.items(): - for item in val: - if item[0] == id: - val.remove(item) - return - raise mm_err.MMBadRequestId - - def RequestsPending(self): - self.CleanRequests() - total = 0 - for (k,v) in self.requests.items(): - total = total + len(v) - return total - - def HandleRequest(self, request_info, value, comment=None): - request = request_info[0] - index = request_info[1] - request_data = self.requests[request][index] - if request == 'add_member': - self.HandleAddMemberRequest(request_data[2:], value, comment) - elif request == 'post': - self.HandlePostRequest(request_data[2:], value, comment) - self.RemoveRequest(request_data[0]) - - def HandlePostRequest(self, data, value, comment): - destination_email = data[0][0] - msg = mm_message.IncomingMessage(data[0][1]) - rejection = None - if not value: - # Accept. - self.Post(msg, 1) - return - elif value == 1: - # Reject. - rejection = "Refused" - subj = msg.getheader('subject') - if subj == None: - request = 'Posting of your untitled message' - else: - request = ('Posting of your message entitled:\n\t\t %s' - % subj) - if not comment: - comment = data[1] - if not self.dont_respond_to_post_requests: - self.RefuseRequest(request, destination_email, - comment, msg) - else: - # Discard. - rejection = "Discarded" - if rejection: - note = "%s: %s posting:" % (self._internal_name, rejection) - note = note + "\n\tFrom: %s" % msg.GetSender() - note = note + ("\n\tSubject: %s" - % (msg.getheader('subject') or '<none>')) - if data[1]: - note = note + "\n\tHeld: %s" % data[1] - if comment: - note = note + "\n\tDiscarded: %s" % comment - self.LogMsg("vette", note) - - def HandleAddMemberRequest(self, data, value, comment): - digest = data[0] - destination_email = data[1] - pw = data[2] - if value == 0: - if digest: - digest_text = 'digest' - else: - digest_text = 'nodigest' - self.RefuseRequest('subscribe %s %s' % (pw, digest_text), - destination_email, comment) - else: - try: - self.ApprovedAddMember(destination_email, pw, digest) - except mm_err.MMAlreadyAMember: - pass - - - -# Don't call any methods below this point from outside this mixin. - - def GetRequestId(self): - id = self.next_request_id - self.next_request_id = self.next_request_id + 1 - # No need to save, we know it's about to be done. - return id - - def RefuseRequest(self, request, destination_email, comment, msg=None): - text = '''Your request to the '%s' mailing-list: - - %s - -Has been rejected by the list moderator. -''' % (self.real_name, request) - if comment: - text = text + ''' -The moderator gave the following reason for rejecting your request: - - %s - -''' % comment - text = text + 'Any questions or comments should be directed to %s.\n' \ - % self.GetAdminEmail() - if msg: - text = text + ''' -Your original message follows: - -%s - -%s -''' % (string.join(msg.headers, ''), msg.body) - - self.SendTextToUser(subject = '%s request rejected' % self.real_name, - recipient = destination_email, - text = text, - # XXX: some of this text should probably be - # wrapped by calling mm_utils.wrap() separately - raw = 1) - diff --git a/modules/mm_archive.py b/modules/mm_archive.py deleted file mode 100644 index 76f8c3130..000000000 --- a/modules/mm_archive.py +++ /dev/null @@ -1,161 +0,0 @@ -# Copyright (C) 1998 by the Free Software Foundation, Inc. -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - - -"""Mixin class for putting new messages in the right place for archival. - -Public archives are separated from private ones. An external archival -mechanism (eg, pipermail) should be pointed to the right places, to do the -archival.""" - - -import sys, os, string -import mm_utils, mm_mbox, mm_cfg, mm_message - -## ARCHIVE_PENDING = "to-archive.mail" -## # ARCHIVE_RETAIN will be ignored, below, in our hook up with andrew's new -## # pipermail. -## ARCHIVE_RETAIN = "retained.mail" - -class Archiver: - def InitVars(self): - # Configurable - self.archive = 1 - # 0=public, 1=private: - self.archive_private = mm_cfg.DEFAULT_ARCHIVE_PRIVATE -## self.archive_update_frequency = \ -## mm_cfg.DEFAULT_ARCHIVE_UPDATE_FREQUENCY -## self.archive_volume_frequency = \ -## mm_cfg.DEFAULT_ARCHIVE_VOLUME_FREQUENCY -## self.archive_retain_text_copy = \ -## mm_cfg.DEFAULT_ARCHIVE_RETAIN_TEXT_COPY - - # Not configurable - self.clobber_date = 0 - # Though the archive file dirs are list-specific, they are not - # settable from the web interface. If you REALLY want to redirect - # something to a different dir, you can set the member vars by - # hand, from the python interpreter! - self.public_archive_file_dir = mm_cfg.PUBLIC_ARCHIVE_FILE_DIR - self.private_archive_file_dir = mm_cfg.PRIVATE_ARCHIVE_FILE_DIR - self.archive_directory = os.path.join(mm_cfg.HTML_DIR, - 'archives', - self._internal_name) - - def GetBaseArchiveURL(self): - if self.archive_private: - return os.path.join(mm_cfg.PRIVATE_ARCHIVE_URL, - self._internal_name + ".html") - else: - return os.path.join(mm_cfg.PUBLIC_ARCHIVE_URL, - self._internal_name + ".html") - - def GetConfigInfo(self): - return [ - "List traffic archival policies.", - - ('archive', mm_cfg.Toggle, ('No', 'Yes'), 0, - 'Archive messages?'), - - ('archive_private', mm_cfg.Radio, ('public', 'private'), 0, - 'Is archive file source for public or private archival?'), - - ('clobber_date', mm_cfg.Radio, ('When sent', 'When resent'), 0, - 'Set date in archive to when the mail is claimed to have been ' - 'sent, or to the time we resend it?'), - -## ('archive_update_frequency', mm_cfg.Number, 3, 0, -## "How often should new messages be incorporated? " -## "0 for no archival, 1 for daily, 2 for hourly"), - -## ('archive_volume_frequency', mm_cfg.Radio, ('Yearly', 'Monthly'), -## 0, -## 'How often should a new archive volume be started?'), - -## ('archive_retain_text_copy', mm_cfg.Toggle, ('No', 'Yes'), -## 0, -## 'Retain plain text copy of archive?'), - ] - - def UpdateArchive(self): - # This method is not being used, in favor of external archiver! - if not self.archive: - return - archive_file_name = os.path.join(self._full_path, ARCHIVE_PENDING) - archive_dir = os.path.join(self.archive_directory, 'volume_%d' - % self.volume) - - # Test to make sure there are posts to archive - archive_file = open(archive_file_name, 'r') - text = string.strip(archive_file.read()) - archive_file.close() - if not text: - return - mm_utils.MakeDirTree(archive_dir, 0755) - # Pipermail 0.0.2 always looks at sys.argv, and I wasn't into hacking - # it more than I had to, so here's a small hack to get around that, - # calling pipermail w/ the correct options. - real_argv = sys.argv - sys.argv = ['pipermail', '-d%s' % archive_dir, '-l%s' % - self._internal_name, '-m%s' % archive_file_name, - '-s%s' % os.path.join(archive_dir, "INDEX")] - - import pipermail - sys.argv = real_argv - f = open(archive_file_name, 'w+') - f.truncate(0) - f.close() - -# Internal function, don't call this. - def ArchiveMail(self, post): - """Retain a text copy of the message in an mbox file.""" - if self.clobber_date: - import time - olddate = post.getheader('date') - post.SetHeader('Date', time.ctime(time.time())) - try: - afn = self.ArchiveFileName() - mbox = self.ArchiveFile(afn) - mbox.AppendMessage(post) - mbox.fp.close() - except IOError, msg: - self.LogMsg("error", ("Archive file access failure:\n" - "\t%s %s" - % (afn, `msg[1]`))) - if self.clobber_date: - # Resurrect original date setting. - post.SetHeader('Date', olddate) - self.Save () - - def ArchiveFileName(self): - """The mbox name where messages are left for archive construction.""" - if self.archive_private: - return os.path.join(self.private_archive_file_dir, - self._internal_name) - else: - return os.path.join(self.public_archive_file_dir, - self._internal_name) - def ArchiveFile(self, afn): - """Open (creating, if necessary) the named archive file.""" - ou = os.umask(002) - try: - try: - return mm_mbox.Mailbox(open(afn, "a+")) - except IOError, msg: - raise IOError, msg - finally: - os.umask(ou) - diff --git a/modules/mm_bouncer.py b/modules/mm_bouncer.py deleted file mode 100644 index 8845861d0..000000000 --- a/modules/mm_bouncer.py +++ /dev/null @@ -1,416 +0,0 @@ -# Copyright (C) 1998 by the Free Software Foundation, Inc. -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - - -"Handle delivery bounce messages, doing filtering when list is set for it." - -__version__ = "$Revision: 693 $" - -# It's possible to get the mail-list senders address (list-admin) in the -# bounce list. You probably don't want to have list mail sent to that -# address anyway. - -import sys -import time -import regsub, string, regex, re -import mm_utils, mm_cfg, mm_err - -class Bouncer: - def InitVars(self): - # Not configurable... - self.bounce_info = {} - - # Configurable... - self.bounce_processing = mm_cfg.DEFAULT_BOUNCE_PROCESSING - self.minimum_removal_date = mm_cfg.DEFAULT_MINIMUM_REMOVAL_DATE - self.minimum_post_count_before_bounce_action = \ - mm_cfg.DEFAULT_MINIMUM_POST_COUNT_BEFORE_BOUNCE_ACTION - self.automatic_bounce_action = mm_cfg.DEFAULT_AUTOMATIC_BOUNCE_ACTION - self.max_posts_between_bounces = \ - mm_cfg.DEFAULT_MAX_POSTS_BETWEEN_BOUNCES - - def GetConfigInfo(self): - return [ - "Policies regarding systematic processing of bounce messages," - " to help automate recognition and handling of defunct" - " addresses.", - ('bounce_processing', mm_cfg.Toggle, ('No', 'Yes'), 0, - 'Try to figure out error messages automatically? '), - ('minimum_removal_date', mm_cfg.Number, 3, 0, - 'Minimum number of days an address has been non-fatally ' - 'bad before we take action'), - ('minimum_post_count_before_bounce_action', mm_cfg.Number, 3, 0, - 'Minimum number of posts to the list since members first ' - 'bounce before we consider removing them from the list'), - ('max_posts_between_bounces', mm_cfg.Number, 3, 0, - "Maximum number of messages your list gets in an hour. " - "(Yes, bounce detection finds this info useful)"), - ('automatic_bounce_action', mm_cfg.Radio, - ("Do nothing", - "Disable and notify me", - "Disable and DON'T notify me", - "Remove and notify me"), - 0, "Action when critical or excessive bounces are detected.") - ] - def ClearBounceInfo(self, email): - email = string.lower(email) - if self.bounce_info.has_key(email): - del self.bounce_info[email] - - def RegisterBounce(self, email, msg): - report = "%s: %s - " % (self.real_name, email) - bouncees = self.bounce_info.keys() - this_dude = mm_utils.FindMatchingAddresses(email, bouncees) - now = time.time() - if not len(this_dude): - # Time address went bad, post where address went bad, - # What the last post ID was that we saw a bounce. - self.bounce_info[string.lower(email)] = [now, self.post_id, - self.post_id] - self.LogMsg("bounce", report + "first") - self.Save() - return - - addr = string.lower(this_dude[0]) - inf = self.bounce_info[addr] - difference = now - inf[0] - if len(mm_utils.FindMatchingAddresses(addr, self.members)): - if self.post_id - inf[2] > self.max_posts_between_bounces: - # Stale entry that's now being restarted... - # Should maybe keep track in see if people become stale entries - # often... - self.LogMsg("bounce", - report + "first fresh") - self.bounce_info[addr] = [now, self.post_id, self.post_id] - return - self.bounce_info[addr][2] = self.post_id - if ((self.post_id - inf[1] > - self.minimum_post_count_before_bounce_action) - and difference > self.minimum_removal_date * 24 * 60 * 60): - self.LogMsg("bounce", report + "exceeded limits") - self.HandleBouncingAddress(addr, msg) - return - else: - post_count = (self.minimum_post_count_before_bounce_action - - (self.post_id - inf[1])) - if post_count < 0: - post_count = 0 - remain = self.minimum_removal_date * 24 * 60 * 60 - difference - self.LogMsg("bounce", - report + ("%d more allowed over %d secs" - % (post_count, remain))) - self.Save() - return - - elif len(mm_utils.FindMatchingAddresses(addr, self.digest_members)): - if self.volume > inf[1]: - self.LogMsg("bounce", - "%s: first fresh (D)", self._internal_name) - self.bounce_info[addr] = [now, self.volume, self.volume] - return - if difference > self.minimum_removal_date * 24 * 60 * 60: - self.LogMsg("bounce", report + "exceeded limits (D)") - self.HandleBouncingAddress(addr, msg) - return - self.LogMsg("bounce", report + "digester lucked out") - else: - self.LogMsg("bounce", - "%s: address %s not a member.", - self._internal_name, - addr) - - def HandleBouncingAddress(self, addr, msg): - """Disable or remove addr according to bounce_action setting.""" - if self.automatic_bounce_action == 0: - return - elif self.automatic_bounce_action == 1: - # Only send if call works ok. - (succeeded, send) = self.DisableBouncingAddress(addr) - did = "disabled" - elif self.automatic_bounce_action == 2: - (succeeded, send) = self.DisableBouncingAddress(addr) - did = "disabled" - # Never send. - send = 0 - elif self.automatic_bounce_action == 3: - (succeeded, send) = self.RemoveBouncingAddress(addr) - # Always send. - send = 1 - did = "removed" - if send: - if succeeded != 1: - negative="not " - else: - negative="" - recipient = self.GetAdminEmail() - if addr in self.owner + [recipient]: - # Whoops! This is a bounce of a bounce notice - do not - # perpetuate the bounce loop! Log it prominently and be - # satisfied with that. - self.LogMsg("error", - "%s: Bounce recipient loop" - " encountered!\n\t%s\n\tBad admin recipient: %s", - self._internal_name, - "(Ie, bounce notification addr, itself, bounces.)", - addr) - return - import mimetools - boundary = mimetools.choose_boundary() - text = [""] - text.append("(This MIME message should be" - " readable as plain text.)") - text.append("") - text.append("--" + boundary) - text.append("Content-type: text/plain; charset=us-ascii") - text.append("") - text.append("This is a mailman mailing list bounce action notice:") - text.append("") - text.append("\tMaillist:\t%s" % self.real_name) - text.append("\tMember:\t\t%s" % addr) - text.append("\tAction:\t\tSubscription %s%s." % (negative, did)) - text.append("\tReason:\t\tExcessive or fatal bounces.") - if succeeded != 1: - text.append("\tBUT:\t\t%s\n" % succeeded) - text.append("") - if did == "disabled" and succeeded == 1: - text.append("You can reenable their subscription by visiting " - "their options page") - text.append("(via %s) and using your" - % self.GetAbsoluteScriptURL('listinfo')) - text.append( - "list admin password to authorize the option change.") - text.append("") - text.append("The triggering bounce notice is attached below.") - text.append("") - text.append("Questions? Contact the mailman site admin,") - text.append("\t" + mm_cfg.MAILMAN_OWNER) - - text.append("") - text.append("--" + boundary) - text.append("Content-type: text/plain; charset=us-ascii") - text.append("") - text.append(string.join(msg.headers, '')) - text.append("") - text.append(mm_utils.QuotePeriods(msg.body)) - text.append("") - text.append("--" + boundary + "--") - - if negative: - negative = string.upper(negative) - self.SendTextToUser(subject = ("%s member %s %s%s due to bounces" - % (self.real_name, addr, - negative, did)), - recipient = recipient, - sender = mm_cfg.MAILMAN_OWNER, - add_headers = [ - "Errors-To: %s" % mm_cfg.MAILMAN_OWNER, - "MIME-version: 1.0", - "Content-type: multipart/mixed;" - ' boundary="%s"' % boundary], - text = string.join(text, '\n')) - def DisableBouncingAddress(self, addr): - """Disable delivery for bouncing user address. - - Returning success and notification status.""" - if not self.IsMember(addr): - reason = "User not found." - self.LogMsg("bounce", "%s: NOT disabled %s: %s", - self.real_name, addr, reason) - return reason, 1 - try: - if self.GetUserOption(addr, mm_cfg.DisableDelivery): - # No need to send out notification if they're already disabled. - self.LogMsg("bounce", - "%s: already disabled %s", self.real_name, addr) - return 1, 0 - else: - self.SetUserOption(addr, mm_cfg.DisableDelivery, 1) - self.LogMsg("bounce", - "%s: disabled %s", self.real_name, addr) - self.Save() - return 1, 1 - except mm_err.MMNoSuchUserError: - self.LogMsg("bounce", "%s: NOT disabled %s: %s", - self.real_name, addr, mm_err.MMNoSuchUserError) - self.ClearBounceInfo(addr) - self.Save() - return mm_err.MMNoSuchUserError, 1 - - def RemoveBouncingAddress(self, addr): - """Unsubscribe user with bouncing address. - - Returning success and notification status.""" - if not self.IsMember(addr): - reason = "User not found." - self.LogMsg("bounce", "%s: NOT removed %s: %s", - self.real_name, addr, reason) - return reason, 1 - try: - self.DeleteMember(addr, "bouncing addr") - self.LogMsg("bounce", "%s: removed %s", self.real_name, addr) - self.Save() - return 1, 1 - except mm_err.MMNoSuchUserError: - self.LogMsg("bounce", "%s: NOT removed %s: %s", - self.real_name, addr, mm_err.MMNoSuchUserError) - self.ClearBounceInfo(addr) - self.Save() - return mm_err.MMNoSuchUserError, 1 - - # Return 0 if we couldn't make any sense of it, 1 if we handled it. - def ScanMessage(self, msg): -## realname, who_from = msg.getaddr('from') -## who_info = string.lower(who_from) - candidates = [] - who_info = string.lower(msg.GetSender()) - at_index = string.find(who_info, '@') - if at_index != -1: - who_from = who_info[:at_index] - remote_host = who_info[at_index+1:] - else: - who_from = who_info - remote_host = self.host_name - if not who_from in ['mailer-daemon', 'postmaster', 'orphanage', - 'postoffice', 'ucx_smtp', 'a2']: - return 0 - mime_info = msg.getheader('content-type') - boundry = None - if mime_info: - mime_info_parts = regsub.splitx( - mime_info, '[Bb][Oo][Uu][Nn][Dd][Aa][Rr][Yy]="[^"]+"') - if len(mime_info_parts) > 1: - boundry = regsub.splitx(mime_info_parts[1], - '"[^"]+"')[1][1:-1] - - if boundry: - relevant_text = string.split(msg.body, '--%s' % boundry)[1] - else: - # This looks strange, but at least 2 are going to be no-ops. - relevant_text = regsub.split(msg.body, - '^.*Message header follows.*$')[0] - relevant_text = regsub.split(relevant_text, - '^The text you sent follows:.*$')[0] - relevant_text = regsub.split( - relevant_text, '^Additional Message Information:.*$')[0] - relevant_text = regsub.split(relevant_text, - '^-+Your original message-+.*$')[0] - - BOUNCE = 1 - REMOVE = 2 - - # Bounce patterns where it's simple to figure out the email addr. - email_regexp = '<?\([^ \t@s|<>]+@[^ \t@<>]+\.[^ \t<>.]+\)>?' - simple_bounce_pats = ( - (regex.compile('.*451 %s.*' % email_regexp), BOUNCE), - (regex.compile('.*554 %s.*' % email_regexp), BOUNCE), - (regex.compile('.*552 %s.*' % email_regexp), BOUNCE), - (regex.compile('.*501 %s.*' % email_regexp), BOUNCE), - (regex.compile('.*553 %s.*' % email_regexp), BOUNCE), - (regex.compile('.*550 %s.*' % email_regexp), REMOVE), - (regex.compile('%s .bounced.*' % email_regexp), BOUNCE), - (regex.compile('.*%s\.\.\. Deferred.*' % email_regexp), BOUNCE), - (regex.compile('.*User %s not known.*' % email_regexp), REMOVE), - (regex.compile('.*%s: User unknown.*' % email_regexp), REMOVE)) - # patterns we can't directly extract the email (special case these) - messy_pattern_1 = regex.compile('^Recipient .*$') - messy_pattern_2 = regex.compile('^Addressee: .*$') - messy_pattern_3 = regex.compile('^User .* not listed.*$') - messy_pattern_4 = regex.compile('^550 [^ ]+\.\.\. User unknown.*$') - messy_pattern_5 = regex.compile('^User [^ ]+ is not defined.*$') - messy_pattern_6 = regex.compile('^[ \t]*[^ ]+: User unknown.*$') - messy_pattern_7 = regex.compile('^[^ ]+ - User currently disabled.*$') - - # Patterns for cases where email addr is separate from error cue. - separate_cue_1 = re.compile( - '^554 [^ ]+\.\.\. unknown mailer error.*$', re.I) - separate_addr_1 = regex.compile('expanded from: %s' % email_regexp) - - message_grokked = 0 - use_prospects = 0 - prospects = [] # If bad but no candidates found. - - for line in string.split(relevant_text, '\n'): - for pattern, action in simple_bounce_pats: - if pattern.match(line) <> -1: - email = self.ExtractBouncingAddr(line) - candidates.append((string.split(email,',')[0], action)) - message_grokked = 1 - - # Now for the special case messages that are harder to parse... - if (messy_pattern_1.match(line) <> -1 - or messy_pattern_2.match(line) <> -1): - username = string.split(line)[1] - candidates.append(('%s@%s' % (username, remote_host), - BOUNCE)) - message_grokked = 1 - continue - if (messy_pattern_3.match(line) <> -1 - or messy_pattern_4.match(line) <> -1 - or messy_pattern_5.match(line) <> -1): - username = string.split(line)[1] - candidates.append(('%s@%s' % (username, remote_host), - REMOVE)) - message_grokked = 1 - continue - if messy_pattern_6.match(line) <> -1: - username = string.split(string.strip(line))[0][:-1] - candidates.append(('%s@%s' % (username, remote_host), - REMOVE)) - message_grokked = 1 - continue - if messy_pattern_7.match(line) <> -1: - username = string.split(string.strip(line))[0] - candidates.append(('%s@%s' % (username, remote_host), - REMOVE)) - message_grokked = 1 - continue - - if separate_cue_1.match(line): - # Here's an error message that doesn't contain the addr. - # Set a flag to use prospects found on separate lines. - use_prospects = 1 - if separate_addr_1.search(line) != -1: - # Found an addr that *might* be part of an error message. - # Register it on prospects, where it will only be used if a - # separate check identifies this message as an error message. - prospects.append((separate_addr_1.group(1), BOUNCE)) - - if use_prospects and prospects: - candidates = candidates + prospects - - did = [] - for who, action in candidates: - # First clean up some cruft around the addrs. - el = string.find(who, "...") - if el != -1: - who = who[:el] - if len(who) > 1 and who[0] == '<': - # Use stuff after open angle and before (optional) close: - who = regsub.splitx(who[1:], ">")[0] - if who not in did: - if action == REMOVE: - self.HandleBouncingAddress(who, msg) - else: - self.RegisterBounce(who, msg) - did.append(who) - return message_grokked - - def ExtractBouncingAddr(self, line): - email = regsub.splitx(line, '[^ \t@<>]+@[^ \t@<>]+\.[^ \t<>.]+')[1] - if email[0] == '<': - return regsub.splitx(email[1:], ">")[0] - else: - return email diff --git a/modules/mm_cfg.py.in b/modules/mm_cfg.py.in deleted file mode 100644 index c8f3a4ec6..000000000 --- a/modules/mm_cfg.py.in +++ /dev/null @@ -1,59 +0,0 @@ -# -*- python -*- - -# Copyright (C) 1998 by the Free Software Foundation, Inc. -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - - -"""This is the module which takes your site-specific settings. - -From a raw distribution it should be copied to mm_cfg.py. If you -already have an mm_cfg.py, be careful to add in only the new settings -you want. The complete set of distributed defaults, with annotation, -are in ./mm_defaults. In mm_cfg, override only those you want to -change, after the - - from mm_defaults import * - -line (see below). - -Note that these are just default settings - many can be overridden via the -admin and user interfaces on a per-list or per-user basis. - -Note also that some of the settings are resolved against the active list -setting by using the value as a format string against the -list-instance-object's dictionary - see the distributed value of -DEFAULT_MSG_FOOTER for an example.""" - - -####################################################### -# Here's where we get the distributed defaults. # - -from mm_defaults import * - -############################################################## -# Put YOUR site-specific configuration below, in mm_cfg.py . # -# See mm_defaults.py for explanations of the values. # - -DEFAULT_HOST_NAME = '@FQDN@' -DEFAULT_URL = 'http://@URL@/mailman' - -MAILMAN_OWNER = 'mailman-owner@%s' % DEFAULT_HOST_NAME - -PUBLIC_ARCHIVE_URL = '/pipermail' -PRIVATE_ARCHIVE_URL = '/mailman/private' - -# (Note - if you're looking for something that is imported from -# mm_cfg, but you didn't find it above, it's probably in mm_defaults.py.) diff --git a/modules/mm_crypt.py b/modules/mm_crypt.py deleted file mode 100644 index 6f568464c..000000000 --- a/modules/mm_crypt.py +++ /dev/null @@ -1,8 +0,0 @@ -import mm_cfg - -if mm_cfg.USE_CRYPT: - from crypt import * -else: - def crypt(string, seed): - import md5 - return md5.new(string).digest() diff --git a/modules/mm_defaults.py.in b/modules/mm_defaults.py.in deleted file mode 100644 index 5be909032..000000000 --- a/modules/mm_defaults.py.in +++ /dev/null @@ -1,224 +0,0 @@ -# -*- python -*- - -# Copyright (C) 1998 by the Free Software Foundation, Inc. -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - -"""Distributed default settings for significant mailman config variables. - -You should NOT edit the values here unless you're changing settings for -distribution. For site-specific settings, put your definitions in -mm_cfg.py after the point at which it includes (via 'from ... import *') -this file, to override the distributed defaults with site-specific ones. -""" - -import os - -# The URL for Mailman sources, etc. - you probably don't want to change this. -MAILMAN_URL = 'http://www.list.org/' - - # Many site-specific settings # - -DEFAULT_HOST_NAME = 'OVERRIDE.WITH.YOUR.MX.OR.HOST.NAME' -SMTPHOST = 'localhost' -DEFAULT_URL = 'http://www.OVERRIDE.WITH.YOUR.HOST/mailman/' -PUBLIC_ARCHIVE_URL = 'http://www.OVERRIDE.WITH.YOUR.PUBLIC.ARCHIVE.URL/' -PRIVATE_ARCHIVE_URL = 'http://www.OVERRIDE.WITH.YOUR.PRIVATE.ARCHIVE.URL/' - -DEFAULT_ARCHIVE_PRIVATE = 0 # 0=public, 1=private -HOME_PAGE = 'index.html' -MAILMAN_OWNER = 'mailman-owner@%s' % DEFAULT_HOST_NAME - -# System ceiling on number of batches into which deliveries are divided: -MAX_SPAWNS = 40 - -# 1 to use crypt for passwords instead of md5. -# Crypt may not work on all python installs. -# Don't change this value once you have lists running... -# In fact, you should just let configure set this one and leave it alone. -USE_CRYPT = 1 - - # General Defaults # - -DEFAULT_FILTER_PROG = '' -# Default number of batches in which to divide large deliveries: -DEFAULT_NUM_SPAWNS = 5 -DEFAULT_LIST_ADVERTISED = 1 -DEFAULT_MAX_NUM_RECIPIENTS = 10 -DEFAULT_MAX_MESSAGE_SIZE = 40 # KB - -# These format strings will be expanded w.r.t. the dictionary for the -# maillist instance. -DEFAULT_SUBJECT_PREFIX = "[%(real_name)s] " -DEFAULT_MSG_HEADER = "" -DEFAULT_MSG_FOOTER = """_______________________________________________ -%(real_name)s maillist - %(real_name)s@%(host_name)s -%(web_page_url)slistinfo/%(_internal_name)s -""" - - # List Accessibility Defaults # - -# Is admin notified of admin requests immediately by mail, as well as by -# daily pending-request reminder? -DEFAULT_ADMIN_IMMED_NOTIFY = 1 -DEFAULT_MODERATED = 0 -# Bounce if 'to' or 'cc' fields don't explicitly name list (anti-spam)? -DEFAULT_REQUIRE_EXPLICIT_DESTINATION = 1 -# Alternate names acceptable as explicit destinations for this list. -DEFAULT_ACCEPTABLE_ALIASES =""" -""" -# This provisional measure is for maillists that have only other maillists -# for members. Ultimately we will probably use surrogate administrative -# message delivery addresses, instead. -DEFAULT_REMINDERS_TO_ADMINS = 0 -# This variable controlls whether monthly password reminders are sent. -DEFAULT_SEND_REMINDERS = 1 -# Send welcome messages to new users? Probably should keep this set to 1. -DEFAULT_SEND_WELCOME_MSG = 1 -# Wipe sender information, and make it look like the list-admin -# address sends all messages -DEFAULT_ANONYMOUS_LIST = 0 -# {header-name: regexp} spam filtering - we include some for example sake. -DEFAULT_BOUNCE_MATCHING_HEADERS = """ -# Lines that *start* with a '#' are comments. -to: friend@public.com -message-id: relay.comanche.denmark.eu -from: list@listme.com -from: .*@uplinkpro.com -""" -# Replies to posts inherently directed to list or original sender? -DEFAULT_REPLY_GOES_TO_LIST = 0 -# Admin approval unnecessary for subscribes? -DEFAULT_OPEN_SUBSCRIBE = 1 -# Private_roster == 0: anyone can see, 1: members only, 2: admin only. -DEFAULT_PRIVATE_ROSTER = 0 -# When exposing members, make them unrecognizable as email addrs. To -# web-spiders from picking up addrs for spamming. -DEFAULT_OBSCURE_ADDRESSES = 1 -# Make it 1 when it works. -DEFAULT_MEMBER_POSTING_ONLY = 0 -# 1 for email subscription verification, 2 for admin confirmation: -DEFAULT_WEB_SUBSCRIBE_REQUIRES_CONFIRMATION = 1 - - # Digestification Defaults # - -# Will list be available in non-digested form? -DEFAULT_NONDIGESTABLE = 1 -# Will list be available in digested form? -DEFAULT_DIGESTABLE = 1 -DEFAULT_DIGEST_HEADER = "" -DEFAULT_DIGEST_FOOTER = DEFAULT_MSG_FOOTER - -DEFAULT_DIGEST_IS_DEFAULT = 0 -DEFAULT_MIME_IS_DEFAULT_DIGEST = 0 -DEFAULT_DIGEST_SIZE_THRESHHOLD = 30 # KB -DEFAULT_DIGEST_SEND_PERIODIC = 1 -# We're only retaining the text file, an external pipermail (andrew's -# newest version) is pointed at the retained text copies. -## # 0 = never, 1 = daily, 2 = hourly: -## DEFAULT_ARCHIVE_UPDATE_FREQUENCY = 2 -## # 0 = yearly, 1 = monthly -## DEFAULT_ARCHIVE_VOLUME_FREQUENCY = 0 -## # Retain a flat text mailbox of postings as well as the fancy archives? -## DEFAULT_ARCHIVE_RETAIN_TEXT_COPY = 1 - - # Bounce Processing Defaults # - -# Should we do any bounced mail checking at all? -DEFAULT_BOUNCE_PROCESSING = 1 -# Minimum number of days that address has been undeliverable before -# we consider nuking it.. -DEFAULT_MINIMUM_REMOVAL_DATE = 5 -# Minimum number of bounced posts to the list before we consider nuking it. -DEFAULT_MINIMUM_POST_COUNT_BEFORE_BOUNCE_ACTION = 3 -# 0 means do nothing -# 1 means disable and send admin a report, -# 2 means nuke'em (remove) and send admin a report, -# 3 means nuke 'em and don't report (whee:) -DEFAULT_AUTOMATIC_BOUNCE_ACTION = 1 -# Maximum number of posts that can go by w/o a bounce before we figure your -# problem must have gotten resolved... usually this could be 1, but we -# need to account for lag time in getting the error messages. I'd set this -# to the maximum number of messages you'd expect your list to reasonably -# get in 1 hour. -DEFAULT_MAX_POSTS_BETWEEN_BOUNCES = 5 - -# -# how long the cookie authorizing administrative -# changes via the admin cgi lasts -# -ADMIN_COOKIE_LIFE = 60 * 20 # 20 minutes - -# how many members to display at a time -# on the admin cgi to unsubscribe them or change their options -# -ADMIN_MEMBER_CHUNKSIZE = 10 - -# These directories are used to find various important files in the Mailman -# installation. PREFIX and EXEC_PREFIX are set by configure and should point -# to the installation directory of the Mailman package. -# -# Do not override these in mm_cfg.py! - -PYTHON = '@PYTHON@' -PREFIX = '@prefix@' -EXEC_PREFIX = '@exec_prefix@' - -# Work around a bogus autoconf 2.12 bug -if EXEC_PREFIX == '${prefix}': - EXEC_PREFIX = PREFIX - -# Don't change anything from here down unless you know what you're doing... - - -# Enumeration for types of configurable variables in Mailman. -Toggle = 1 -Radio = 2 -String = 3 -Text = 4 -Email = 5 -EmailList = 6 -Host = 7 -Number = 8 - -# could add Directory and URL - - -# Bitfield for user options -Digests = 0 # handled by other mechanism, doesn't need a flag. -DisableDelivery = 1 -DontReceiveOwnPosts = 2 # Non-digesters only -AcknowlegePosts = 4 -DisableMime = 8 # Digesters only -ConcealSubscription = 16 - - -LIST_DATA_DIR = os.path.join(PREFIX, 'lists') -HTML_DIR = os.path.join(PREFIX, 'public_html') -CGI_DIR = os.path.join(EXEC_PREFIX, 'cgi-bin') -LOG_DIR = os.path.join(PREFIX, 'logs') -LOCK_DIR = os.path.join(PREFIX, 'locks') -DATA_DIR = os.path.join(PREFIX, 'data') -WRAPPER_DIR = os.path.join(EXEC_PREFIX, 'mail') -SCRIPTS_DIR = os.path.join(PREFIX, 'scripts') -TEMPLATE_DIR = os.path.join(PREFIX, 'templates') -PUBLIC_ARCHIVE_FILE_DIR = os.path.join(PREFIX, 'archives/public') -PRIVATE_ARCHIVE_FILE_DIR = os.path.join(PREFIX, 'archives/private') - -# The Mailman version, also set by configure -VERSION = '@VERSION@' - -# Data file version number -DATA_FILE_VERSION = 3 diff --git a/modules/mm_deliver.py b/modules/mm_deliver.py deleted file mode 100644 index 205fdfd71..000000000 --- a/modules/mm_deliver.py +++ /dev/null @@ -1,249 +0,0 @@ -# Copyright (C) 1998 by the Free Software Foundation, Inc. -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - - -"""Mixin class with message delivery routines.""" - - -import string, os, sys, tempfile -import mm_cfg, mm_message, mm_err, mm_utils - -# Text for various messages: - -POSTACKTEXT = ''' -Your message entitled: - - %s - -was successfully received by the %s mailing list. - -List info page: %s -''' - -SUBSCRIBEACKTEXT = """Welcome to the %(real_name)s@%(host_name)s mailing list! -%(welcome)s -To post to this list, send your email to: - - %(emailaddr)s - -General information about the mailing list is at: - - %(generalurl)s - -If you ever want to unsubscribe or change your options (eg, switch to or -from digest mode, change your password, etc.), visit your subscription -page at: - - %(optionsurl)s - -You can also make such adjustments via email by sending a message to: - - %(real_name)s-request@%(host_name)s - -with the word `help' in the subject or body, and you will get back a -message with instructions. - -You must know your password to change your options (including changing -the password, itself) or to unsubscribe. It is: - - %(password)s - -If you forget your password, don't worry, you will receive a monthly -reminder telling you what all your %(host_name)s mailing list passwords -are, and how to unsubscribe or change your options. There is also a -button on your options page that will email your current password to -you. - -You may also have your password mailed to you automatically off of the -web page noted above. - -""" - -USERPASSWORDTEXT = ''' -This is a reminder of how to unsubscribe or change your configuration -for the mailing list "%s". You need to have your password for -these things. YOUR %s PASSWORD IS: - - %s - -To make changes to your subscription, use the password on your options web -page: - - %s - -You can also make such changes via email - send a message to: - - %s - -with the text "help" in the subject or body, and you will be emailed -instructions. - -Questions or comments? Please send them to %s. -''' - -# We could abstract these two better... -class Deliverer: - # This method assumes the sender is list-admin if you don't give one. - def SendTextToUser(self, subject, text, recipient, sender=None, - add_headers=[], raw=0): - # repr(recipient) necessary for addresses containing "'" quotes! - if not sender: - sender = self.GetAdminEmail() - mm_utils.SendTextToUser(subject, text, recipient, sender, - add_headers=add_headers, raw=raw) - - def DeliverToUser(self, msg, recipient): - # This method assumes the sender is the one given by the message. - mm_utils.DeliverToUser(msg, recipient, - add_headers=['Errors-To: %s\n' - % Self.GetAdminEmail()]) - - def QuotePeriods(self, text): - return string.join(string.split(text, '\n.\n'), '\n .\n') - def DeliverToList(self, msg, recipients, - header="", footer="", remove_to=0, tmpfile_prefix = ""): - if not(len(recipients)): - return - # repr(recipient) necessary for addresses containing "'" quotes! - recipients = map(repr, recipients) - to_list = string.join(recipients) - tempfile.tempdir = '/tmp' - -## If this is a digest, or we ask to remove them, -## Remove old To: headers. We're going to stick our own in there. -## Also skip: Sender, return-receipt-to, errors-to, return-path, reply-to, -## (precedence, and received). - - if remove_to: - # Writing to a file is better than waiting for sendmail to exit - tempfile.template = tmpfile_prefix +'mailman-digest.' - del msg['to'] - del msg['x-to'] - msg.headers.append('To: %s\n' % self.GetListEmail()) - else: - tempfile.template = tmpfile_prefix + 'mailman.' - if self.reply_goes_to_list: - del msg['reply-to'] - msg.headers.append('Reply-To: %s\n' % self.GetListEmail()) - msg.headers.append('Sender: %s\n' % self.GetAdminEmail()) - msg.headers.append('Errors-To: %s\n' % self.GetAdminEmail()) - msg.headers.append('X-BeenThere: %s\n' % self.GetListEmail()) - - tmp_file_name = tempfile.mktemp() - tmp_file = open(tmp_file_name, 'w+') - tmp_file.write(string.join(msg.headers,'') + '\n') - - if header: # The *body* header: - tmp_file.write(header + '\n') - tmp_file.write(self.QuotePeriods(msg.body)) - if footer: - tmp_file.write(footer) - tmp_file.close() - cmd = "%s %s %s %s %s %s" % ( - mm_cfg.PYTHON, - os.path.join(mm_cfg.SCRIPTS_DIR, "deliver"), - tmp_file_name, self.GetAdminEmail(), - self.num_spawns, to_list) - file = os.popen(cmd) - status = file.close() - if status: - sys.stderr.write('Non-zero exit status: %d' - '\nCmd: %s' % ((status >> 8), cmd)) - def SendPostAck(self, msg, sender): - subject = msg.getheader('subject') - if not subject: - subject = '[none]' - else: - sp = self.subject_prefix - if (len(subject) > len(sp) - and subject[0:len(sp)] == sp): - # Trim off subject prefix - subject = subject[len(sp) + 1:] - body = POSTACKTEXT % (subject, self.real_name, - self.GetAbsoluteScriptURL('listinfo')) - self.SendTextToUser('%s post acknowlegement' % self.real_name, - body, sender) - - def CreateSubscribeAck(self, name, password): - if self.welcome_msg: - welcome = self.welcome_msg + '\n' - else: - welcome = '' - - body = SUBSCRIBEACKTEXT % {'real_name' : self.real_name, - 'host_name' : self.host_name, - 'welcome' : welcome, - 'emailaddr' : self.GetListEmail(), - 'generalurl': self.GetAbsoluteScriptURL('listinfo'), - 'optionsurl': self.GetAbsoluteOptionsURL(name), - 'password' : password, - } - return body - - def SendSubscribeAck(self, name, password, digest): - if not self.send_welcome_msg: - return - if digest: - digest_mode = '(Digest mode)' - else: - digest_mode = '' - - if self.reminders_to_admins: - recipient = "%s-admin@%s" % tuple(string.split(name, '@')) - else: - recipient = name - - self.SendTextToUser(subject = 'Welcome To "%s"! %s' % (self.real_name, - digest_mode), - recipient = recipient, - text = self.CreateSubscribeAck(name, password)) - - def SendUnsubscribeAck(self, name): - self.SendTextToUser(subject = 'Unsubscribed from "%s"\n' % - self.real_name, - recipient = name, - text = self.goodbye_msg) - def MailUserPassword(self, user): - subjpref = '%s@%s' % (self.real_name, self.host_name) - ok = 1 - if self.passwords.has_key(user): - if self.reminders_to_admins: - recipient = "%s-admin@%s" % tuple(string.split(user, '@')) - else: - recipient = user - subj = '%s maillist reminder\n' % subjpref - text = USERPASSWORDTEXT % (user, - self.real_name, - self.passwords[user], - self.GetAbsoluteOptionsURL(user), - self.GetRequestEmail(), - self.GetAdminEmail()) - else: - ok = 0 - recipient = self.GetAdminEmail() - subj = '%s user %s missing password!\n' % (subjpref, user) - text = ("Mailman noticed (in .MailUserPassword()) that:\n\n" - "\tUser: %s\n\tList: %s\n\nlacks a password - please" - " notify the mailman system manager!" - % (`user`, self._internal_name)) - self.SendTextToUser(subject = subj, - recipient = recipient, - text = text, - add_headers=["Errors-To: %s" - % self.GetAdminEmail(), - "X-No-Archive: yes"]) - if not ok: - raise mm_err.MMBadUserError diff --git a/modules/mm_digest.py b/modules/mm_digest.py deleted file mode 100644 index 42b14325c..000000000 --- a/modules/mm_digest.py +++ /dev/null @@ -1,408 +0,0 @@ -# Copyright (C) 1998 by the Free Software Foundation, Inc. -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - - -"""Mixin class with list-digest handling methods and settings.""" - -__version__ = "$Revision: 719 $" - -import mm_utils, mm_err, mm_message, mm_cfg -import time, os, string, re - -DIGEST_MASTHEAD = """ -Send %(real_name)s maillist submissions to - %(got_list_email)s - -To subscribe or unsubscribe via the web, visit - %(got_listinfo_url)s -or, via email, send a message with subject or body 'help' to - %(got_request_email)s -You can reach the person managing the list at - %(got_owner_email)s - -(When replying, please edit your Subject line so it is more specific than -"Re: Contents of %(real_name)s digest...") -""" - - -class Digester: - def InitVars(self): - # Configurable - self.digestable = mm_cfg.DEFAULT_DIGESTABLE - self.digest_is_default = mm_cfg.DEFAULT_DIGEST_IS_DEFAULT - self.mime_is_default_digest = mm_cfg.DEFAULT_MIME_IS_DEFAULT_DIGEST - self.digest_size_threshhold = mm_cfg.DEFAULT_DIGEST_SIZE_THRESHHOLD - self.digest_send_periodic = mm_cfg.DEFAULT_DIGEST_SEND_PERIODIC - self.next_post_number = 1 - self.digest_header = mm_cfg.DEFAULT_DIGEST_HEADER - self.digest_footer = mm_cfg.DEFAULT_DIGEST_FOOTER - - # Non-configurable. - self.digest_members = [] - self.next_digest_number = 1 - - def GetConfigInfo(self): - return [ - "Batched-delivery digest characteristics.", - - ('digestable', mm_cfg.Toggle, ('No', 'Yes'), 1, - 'Can list members choose to receive list traffic ' - 'bunched in digests?'), - - ('digest_is_default', mm_cfg.Radio, - ('Regular', 'Digest'), 0, - 'Which delivery mode is the default for new users?'), - - ('mime_is_default_digest', mm_cfg.Radio, - ('Plain', 'Mime'), 0, - 'When receiving digests, which format is default?'), - - ('digest_size_threshhold', mm_cfg.Number, 3, 0, - 'How big in Kb should a digest be before it gets sent out?'), - # Should offer a 'set to 0' for no size threshhold. - -# ('digest_send_periodic', mm_cfg.Number, 3, 0, - ('digest_send_periodic', mm_cfg.Radio, ('No', 'Yes'), 1, - 'Should a digest be dispatched daily when the size threshold ' - "isn't reached?"), - - ('digest_header', mm_cfg.Text, (4, 55), 0, - 'Header added to every digest', - "Text attached (as an initial message, before the table" - " of contents) to the top of digests.<p>" - + mm_err.MESSAGE_DECORATION_NOTE), - - ('digest_footer', mm_cfg.Text, (4, 55), 0, - 'Footer added to every digest', - "Text attached (as a final message) to the bottom of digests.<p>" - + mm_err.MESSAGE_DECORATION_NOTE), - ] - - def SetUserDigest(self, sender, value): - self.IsListInitialized() - addr = self.FindUser(sender) - if not addr: - raise mm_err.MMNotAMemberError - if addr in self.members: - if value == 0: - raise mm_err.MMAlreadyUndigested - else: - if not self.digestable: - raise mm_err.MMCantDigestError - self.members.remove(addr) - self.digest_members.append(addr) - else: - if value == 1: - raise mm_err.MMAlreadyDigested - else: - if not self.nondigestable: - raise mm_err.MMMustDigestError - self.digest_members.remove(addr) - self.members.append(addr) - self.Save() - -# Internal function, don't call this. - def SaveForDigest(self, post): - """Add message to index, and to the digest. If the digest is large - enough when we're done writing, send it out.""" - ou = os.umask(002) - try: - digest_file = open(os.path.join(self._full_path, "next-digest"), - "a+") - topics_file = open(os.path.join(self._full_path, - "next-digest-topics"), - "a+") - finally: - os.umask(ou) - sender = self.QuoteMime(post.GetSenderName()) - fromline = self.QuoteMime(post.getheader("from")) - date = self.QuoteMime(post.getheader("date")) - body = self.QuoteMime(post.body) - subject = self.QuoteMime(post.getheader("subject")) - # Don't include the redundant subject prefix in the toc entries: - matched = re.match("(re:? *)?(%s)" % re.escape(self.subject_prefix), - subject, re.I) - if matched: - subject = subject[:matched.start(2)] + subject[matched.end(2):] - topics_file.write(" %d. %s (%s)\n" % (self.next_post_number, - subject, sender)) - # We exclude specified headers *and* all "X-*" headers. - exclude_headers = ['received', 'errors-to'] - kept_headers = [] - keeping = 0 - have_content_type = 0 - have_content_description = 0 - lower, split = string.lower, string.split - for h in post.headers: - if (lower(h[:2]) == "x-" - or lower(split(h, ':')[0]) in exclude_headers): - keeping = 0 - elif (h and h[0] in [" ", "\t"]): - if (keeping and kept_headers): - # Continuation of something we're keeping. - kept_headers[-1] = kept_headers[-1] + h - else: - keeping = 1 - if lower(h[:7]) == "content-": - kept_headers.append(h) - if lower(h[:12]) == "content-type": - have_content_type = 1 - if lower(h[:19]) == "content-description": - have_content_description = 1 - else: - kept_headers.append(self.QuoteMime(h)) - if (have_content_type and not have_content_description): - kept_headers.append("Content-Description: %s\n" % subject) - if self.reply_goes_to_list: - # Munge the reply-to - sigh. - kept_headers.append('Reply-To: %s\n' - % self.QuoteMime(self.GetListEmail())) - - # Do the save. - digest_file.write("--%s\n\nMessage: %d\n%s\n%s" - % (self._mime_separator, self.next_post_number, - string.join(kept_headers, ""), - body)) - self.next_post_number = self.next_post_number + 1 - topics_file.close() - digest_file.close() - self.SendDigestOnSize(self.digest_size_threshhold) - - def SendDigestIfAny(self): - """Send the digest if there are any messages pending.""" - self.SendDigestOnSize(0) - - def SendDigestOnSize(self, threshhold): - """Call SendDigest if accumulated digest exceeds threshhold. - - (There must be some content, even if threshhold is 0.)""" - try: - ndf = os.path.join(self._full_path, "next-digest") - size = os.stat(ndf)[6] - if size == 0: - return - elif (size/1024.) >= threshhold: - self.SendDigest() - except os.error, err: - if err[0] == 2: - # No such file or directory - self.LogMsg("error", "mm_digest lost digest file %s, %s", - ndf, err) - -# If the mime separator appears in the text anywhere, throw a space on -# both sides of it, so it doesn't get interpreted as a real mime separator. - def QuoteMime(self, text): - if not text: - return text - return string.join(string.split(text, self._mime_separator), ' %s ' % - self._mime_separator) - - def FakeDigest(self): - def DeliveryEnabled(x, s=self, v=mm_cfg.DisableDelivery): - return not s.GetUserOption(x, v) - - def LikesMime(x, s=self, v=mm_cfg.DisableMime): - return not s.GetUserOption(x, v) - - def HatesMime(x, s=self, v=mm_cfg.DisableMime): - return s.GetUserOption(x, v) - - recipients = filter(DeliveryEnabled, self.digest_members) - mime_recipients = filter(LikesMime, recipients) - text_recipients = filter(HatesMime, recipients) - self.LogMsg("digest", - 'Fake %s digest %d log--', - self.real_name, self.next_digest_number) - self.LogMsg("digest", - ('Fake %d digesters, %d disabled. ' - 'Active: %d MIMEers, %d non.'), - len(self.digest_members), - len(self.digest_members) - len(recipients), - len(mime_recipients), len(text_recipients)) - - def SendDigest(self): - topics_file = open(os.path.join(self._full_path, 'next-digest-topics'), - 'r+') - topics_text = topics_file.read() - topics_number = string.count(topics_text, '\n') - topics_plural = ((topics_number != 1) and "s") or "" - digest_file = open(os.path.join(self._full_path, 'next-digest'), 'r+') - - def DeliveryEnabled(x, s=self, v=mm_cfg.DisableDelivery): - return not s.GetUserOption(x, v) - def LikesMime(x, s=self, v=mm_cfg.DisableMime): - return not s.GetUserOption(x, v) - def HatesMime(x, s=self, v=mm_cfg.DisableMime): - return s.GetUserOption(x, v) - recipients = filter(DeliveryEnabled, self.digest_members) - mime_recipients = filter(LikesMime, recipients) - text_recipients = filter(HatesMime, recipients) - - self.LogMsg("digest", - ('%s v %d - ' - '%d msgs %d dgstrs: %d m %d non %d dis'), - self.real_name, - self.next_digest_number, - topics_number, - len(self.digest_members), - len(mime_recipients), - len(text_recipients), - len(self.digest_members) - len(recipients)) - - if mime_recipients or text_recipients: - d = Digest(self, topics_text, digest_file.read()) - else: - d = None - - # Zero the digest files only just before the messages go out. - topics_file.truncate(0) - topics_file.close() - digest_file.truncate(0) - digest_file.close() - self.next_digest_number = self.next_digest_number + 1 - self.next_post_number = 1 - self.Save() - - if text_recipients: - self.DeliverToList(d.Present(mime=0), - text_recipients, remove_to=1) - if mime_recipients: - self.DeliverToList(d.Present(mime=1), - mime_recipients, - remove_to=1, tmpfile_prefix = "mime.") - -class Digest: - "Represent a maillist digest, presentable in either plain or mime format." - def __init__(self, list, toc, body): - self.list = list - self.toc = toc - self.body = body - self.baseheaders = [] - self.volinfo = "Vol %d #%d" % (list.volume, list.next_digest_number) - numtopics = string.count(self.toc, '\n') - plural = ((numtopics != 1) and "s") or "" - self.numinfo = "%d msg%s" % (numtopics, plural) - - def ComposeBaseHeaders(self, msg): - """Populate the message with the presentation-independent headers.""" - lst = self.list - msg.SetSender(lst.GetAdminEmail()) - msg.SetHeader('Subject', - ('%s digest, %s - %s' % - (lst.real_name, self.volinfo, self.numinfo))) - msg.SetHeader('Reply-to', lst.GetListEmail()) - msg.SetHeader('X-Mailer', "Mailman v%s" % mm_cfg.VERSION) - msg.SetHeader('MIME-version', '1.0') - - def SatisfyRefs(self, text): - """Resolve references in a format string against list settings. - - The resolution is done against a copy of the lists attribute - dictionary, with the addition of some of settings for computed - items - got_listinfo_url, got_request_email, got_list_email, and - got_owner_email.""" - # Collect the substitutions: - if hasattr(self, 'substitutions'): - substs = self.substitutions - else: - lst = self.list - substs = {} - substs.update(lst.__dict__) - substs.update({'got_listinfo_url': - lst.GetAbsoluteScriptURL('listinfo'), - 'got_request_email': lst.GetRequestEmail(), - 'got_list_email': lst.GetListEmail(), - 'got_owner_email': lst.GetAdminEmail(), - }) - return text % substs - - def Present(self, mime=0): - """Produce a rendering of the digest, as an OutgoingMessage.""" - msg = mm_message.OutgoingMessage() - self.ComposeBaseHeaders(msg) - digestboundary = self.list._mime_separator - if mime: - import mimetools - envboundary = mimetools.choose_boundary() - msg.SetHeader('Content-type', - 'multipart/mixed; boundary="%s"' % envboundary) - else: - envboundary = self.list._mime_separator - msg.SetHeader('Content-type', 'text/plain') - dashbound = "--" + envboundary - - lines = [] - - # Masthead: - if mime: - lines.append(dashbound) - lines.append("Content-type: text/plain; charset=us-ascii") - lines.append("Content-description: Masthead (%s digest, %s)" - % (self.list.real_name, self.volinfo)) - lines.append(self.SatisfyRefs(DIGEST_MASTHEAD)) - - # List-specific header: - if self.list.digest_header: - lines.append("") - if mime: - lines.append(dashbound) - lines.append("Content-type: text/plain; charset=us-ascii") - lines.append("Content-description: Digest Header") - lines.append("") - lines.append(self.SatisfyRefs(self.list.digest_header)) - - # Table of contents: - lines.append("") - if mime: - lines.append(dashbound) - lines.append("Content-type: text/plain; charset=us-ascii") - lines.append("Content-description: Today's Topics (%s)" % - self.numinfo) - lines.append("") - lines.append("Today's Topics:") - lines.append("") - lines.append(self.toc) - - # Digest text: - if mime: - lines.append(dashbound) - lines.append('Content-type: multipart/digest; boundary="%s"' - % digestboundary) - lines.append("") - lines.append(self.body) - - # List-specific footer: - if self.list.digest_footer: - lines.append("") - lines.append(dashbound) - if mime: - lines.append("Content-type: text/plain; charset=us-ascii") - lines.append("Content-description: Digest Footer") - lines.append("") - lines.append(self.SatisfyRefs(self.list.digest_footer)) - - # Close: - lines.append("") - lines.append("--" + digestboundary + "--") - if mime: - # Close encompassing mime envelope. - lines.append("") - lines.append(dashbound + "--") - lines.append("") - lines.append("End of %s Digest" % self.list.real_name) - - msg.SetBody(string.join(lines, "\n")) - return msg diff --git a/modules/mm_err.py b/modules/mm_err.py deleted file mode 100644 index 33bd99148..000000000 --- a/modules/mm_err.py +++ /dev/null @@ -1,70 +0,0 @@ -# Copyright (C) 1998 by the Free Software Foundation, Inc. -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - - -"""Shared mailman errors and messages.""" - -__version__ = "$Revision: 547 $" - - -MMUnknownListError = "MMUnknownListError" -MMBadListError = "MMBadListError" -MMBadUserError = "MMBadUserError" -MMBadConfigError = "MMBadConfigError" - -MMBadEmailError = "MMBadEmailError" -MMMustDigestError = "MMMustDigestError" -MMCantDigestError = "MMCantDigestError" -MMNotAMemberError = "MMNotAMemberError" -MMListNotReady = "MMListNotReady" -MMNoSuchUserError = "MMNoSuchUserError" -MMBadPasswordError = "MMBadPasswordError" -MMNeedApproval = "MMNeedApproval" -MMHostileAddress = "MMHostileAddress" -MMAlreadyAMember = "MMAlreadyAMember" -MMPasswordsMustMatch = "MMPasswordsMustMatch" -MMAlreadyDigested = "MMAlreadyDigested" -MMAlreadyUndigested = "MMAlreadyUndigested" -MMBadRequestId = "MMBadRequestId" -MMWebSubscribeRequiresConfirmation = "MMWebSubscribeRequiresConfirmation" - -MODERATED_LIST_MSG = "Moderated list" -IMPLICIT_DEST_MSG = "Implicit destination" -SUSPICIOUS_HEADER_MSG = "Suspicious header" -FORBIDDEN_SENDER_MSG = "Forbidden sender" -LOOPING_POST = "Post already went through this list!" - -MESSAGE_DECORATION_NOTE = """This text can include <b>%(field)s</b> format -strings which are resolved against the list's attribute dictionary (__dict__). -Some useful fields are: - -<dl> - <dt>real_name - <dd>The "pretty" name of the list, with capitalization. - <dt>_internal_name - <dd>The name by which the list is identified in URLs, where case - is germane. - <dt>host_name - <dd>The domain-qualified host name where the list server runs. - <dt>web_page_url - <dd>The mailman root URL to which, eg, 'listinfo/%(_internal_name)s - can be appended to yield the listinfo page for the list. - <dt>description - <dd>The brief description of the list. - <dt>info - <dd>The less brief list description. -</dl> -""" diff --git a/modules/mm_gateway.py b/modules/mm_gateway.py deleted file mode 100644 index f26811fcf..000000000 --- a/modules/mm_gateway.py +++ /dev/null @@ -1,142 +0,0 @@ -# Copyright (C) 1998 by the Free Software Foundation, Inc. -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - -'''Mixin class for gatewaying mail to news, and news to mail.''' - -ImproperNNTPConfigError = "ImproperNNTPConfigError" -class GatewayManager: - def InitVars(self): - # Configurable - self.nntp_host = '' - self.linked_newsgroup = '' - self.gateway_to_news = 0 - self.gateway_to_mail = 0 - - def GetConfigInfo(self): - import mm_cfg - return [ - 'Mail to news and news to mail gateway services.', - ('nntp_host', mm_cfg.String, 50, 0, - 'The internet address of the machine your news server is running on.', - - 'The news server is not part of Mailman proper. You have to already' - ' have access to a nntp server, and that nntp server has to recognize' - ' the machine this mailing list runs on as a machine capable of' - ' reading and posting news.'), - ('linked_newsgroup', mm_cfg.String, 50, 0, - 'The name of the usenet group to gateway to and/or from.'), - ('gateway_to_news', mm_cfg.Toggle, ('No', 'Yes'), 0, - 'Should posts to the mailing list be resent to the newsgroup?'), - ('gateway_to_mail', mm_cfg.Toggle, ('No', 'Yes'), 0, - 'Should newsgroup posts not sent from the list be resent to the' - ' list?') - ] - - # Watermarks are kept externally to avoid locking problems. - def PollNewsGroup(self, watermark): - if (not self.gateway_to_mail or not self.nntp_host or - not self.linked_newsgroup): - return 0 - import nntplib, os, string, mm_cfg - con = nntplib.NNTP(self.nntp_host) - r,c,f,l,n = con.group(self.linked_newsgroup) - # NEWNEWS is not portable and has synchronization issues... - # Use a watermark system instead. - if watermark == 0: - return eval(l) - for num in range(max(watermark+1, eval(f)), eval(l)+1): - try: - headers = con.head(`num`)[3] - found_to = 0 - for header in headers: - i = string.find(header, ':') - if i > 0 and string.lower(header[:i]) == 'to': - found_to = 1 - if header[:i] <> 'X-BeenThere': - continue - if header[i:] == ': %s' % self.GetListEmail(): - raise "QuickEscape" - body = con.body(`num`)[3] - file = os.popen("%s %s nonews" % - (os.path.join(mm_cfg.SCRIPTS_DIR, - "post"), self._internal_name), "w") - file.write(string.join(headers,'\n')) - # If there wasn't already a TO: header, add one. - if not found_to: - file.write("\nTo: %s" % self.GetListEmail()) - file.write('\n\n') - file.write(string.join(body,'\n')) - file.write('\n') - file.close() - except nntplib.error_temp: - pass # Probably canceled, etc... - except "QuickEscape": - pass # We gated this TO news, don't repost it! - return eval(l) - - def SendMailToNewsGroup(self, mail_msg): - import mm_message - import os - #if self.gateway_to_news == 0: - # return - if self.linked_newsgroup == '' or self.nntp_host == '': - raise ImproperNNTPConfigError - try: - if self.tmp_prevent_gate: - return - except AttributeError: - pass # Wasn't remailed by the news gater then. Let it through. - # Fork in case the nntp connection hangs. - x = os.fork() - if not x: - # Now make the news message... - msg = mm_message.NewsMessage(mail_msg) - - import nntplib,string - - # Ok, munge headers, etc. - subj = msg.getheader('subject') - if not subj: - msg.SetHeader('Subject', '%s(no subject)' % prefix) - if self.reply_goes_to_list: - del msg['reply-to'] - msg.headers.append('Reply-To: %s\n' % self.GetListEmail()) - msg.headers.append('Sender: %s\n' % self.GetAdminEmail()) - msg.headers.append('Errors-To: %s\n' % self.GetAdminEmail()) - msg.headers.append('X-BeenThere: %s\n' % self.GetListEmail()) - msg.headers.append('Newsgroups: %s\n' % self.linked_newsgroup) - # Note: Need to be sure 2 messages aren't ever sent to the same - # list in the same process, since message ID's need to be unique. - # could make the ID be mm.listname.postnum instead if that happens - if msg.getheader('Message-ID') == None: - import time - msg.headers.append('Message-ID: <mm.%s.%s@%s>\n' % - (time.time(), os.getpid(), self.host_name)) - if msg.getheader('Lines') == None: - msg.headers.append('Lines: %s\n' % - len(string.split(msg.body,"\n"))) - del msg['received'] - - # NNTP is strict about spaces after the colon in headers. - for n in range(len(msg.headers)): - line = msg.headers[n] - i = string.find(line,":") - if i <> -1 and line[i+1] <> ' ': - msg.headers[n] = line[:i+1] + ' ' + line[i+1:] - con = nntplib.NNTP(self.nntp_host) - con.post(msg) - con.quit() - os._exit(0) diff --git a/modules/mm_html.py b/modules/mm_html.py deleted file mode 100644 index 9b060bfdb..000000000 --- a/modules/mm_html.py +++ /dev/null @@ -1,336 +0,0 @@ -# Copyright (C) 1998 by the Free Software Foundation, Inc. -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - - -"""Routines for presentation of list-specific HTML text.""" - -__version__ = "$Revision: 730 $" - - -import os -import regsub -import string -import mm_cfg, mm_utils -from htmlformat import * - -class HTMLFormatter: - def InitTempVars(self): - self._template_dir = os.path.join(mm_cfg.TEMPLATE_DIR, - self._internal_name) - - def GetMailmanFooter(self): - owners_html = Container() - for i in range(len(self.owner)): - owner = self.owner[i] - owners_html.AddItem(Link('mailto:%s' % owner, owner)) - if i + 1 <> len(self.owner): - owners_html.AddItem(', ') - - # Remove the .Format() when htmlformat conversion is done. - return Container( - '<hr>', - Address( - Container( - 'List run by ', - owners_html, - '<p>', - 'HTML generated by ', - Link( - mm_cfg.MAILMAN_URL, - "Mailman v %s" % mm_cfg.VERSION)))).Format() - - def SnarfHTMLTemplate(self, file): - filename = os.path.join(self._template_dir, file) - f = open(filename,'r') - str = f.read() - f.close() - return str - - def FormatUsers(self, digest): - def NotHidden(x, s=self, v=mm_cfg.ConcealSubscription): - return not s.GetUserOption(x, v) - - if digest: - people = filter(NotHidden, self.digest_members) - num_concealed = len(self.digest_members) - len(people) - else: - people = filter(NotHidden, self.members) - num_concealed = len(self.members) - len(people) - people.sort() - if (num_concealed > 0): - plurality = (((num_concealed > 1) and "s") or "") - concealed = ("<em>(%d private member%s not shown)</em>" - % (num_concealed, plurality)) - else: - concealed = "" - - def FormatOneUser(person, me=self, - # Make some local refs for efficiency: - disdel=mm_cfg.DisableDelivery, - Link=Link, os=os, - ObscureEmail=mm_utils.ObscureEmail): - id = ObscureEmail(person) - if me.obscure_addresses: - showing = ObscureEmail(person, for_text=1) - else: - showing = person - got = Link(os.path.join(me.GetRelativeScriptURL('options'), - id), showing) - if me.GetUserOption(person, disdel): - got = Italic("(", got, ")") - return got - items = map(FormatOneUser, people) - # Just return the .Format() so this works until I finish - # converting everything to htmlformat... - return (concealed + - apply(UnorderedList, tuple(items)).Format()) - - - def FormatOptionButton(self, type, value, user): - users_val = self.GetUserOption(user, type) - if users_val == value: - checked = ' CHECKED' - else: - checked = '' - name = { mm_cfg.DontReceiveOwnPosts : "dontreceive", - mm_cfg.DisableDelivery : "disablemail", - mm_cfg.DisableMime : "mime", - mm_cfg.AcknowlegePosts : "ackposts", - mm_cfg.Digests : "digest", - mm_cfg.ConcealSubscription : "conceal" - }[type] - import sys - return ('<input type=radio name="%s" value="%d"%s>' - % (name, value, checked)) - - def FormatDigestButton(self): - if self.digest_is_default: - checked = ' CHECKED' - else: - checked = '' - return '<input type=radio name="digest" value="1"%s>' % checked - - def FormatDisabledNotice(self, user): - if self.GetUserOption(user, mm_cfg.DisableDelivery): - text = Center(Header(3, - "Note - your list delivery is currently" - " disabled.")).Format() - text = text + "\n" - text = text + ("You may have set non-delivery deliberately, or" - " it may have been triggered by bounces from your" - " delivery address. In either case, to reenable " - " delivery, change the ") - text = text + Link('#disable', - "Disable mail delivery").Format() - text = text + " option. Contact " - text = text + Link('mailto:' + self.GetAdminEmail(), - 'your list administrator').Format() - text = text + " if you have questions." - return text - else: - return "" - - def FormatSubscriptionMsg(self): - "Tailor to approval, roster privacy, and web vetting requirements." - msg = "" - also = "" - if self.web_subscribe_requires_confirmation: - msg = msg + ("You will be sent email requesting confirmation, " - "to prevent others from gratuitously subscribing " - "you. ") - if not self.open_subscribe: - msg = msg + ("This is a closed list, which means your " - "subscription will be held for approval. You will " - "be notified of the administrators decision by " - "email. ") - also = "also " - if self.private_roster: - msg = msg + ("This is %sa private list, which means that " - "the members list is not available to non-" - "members. " % also) - else: - msg = msg + ("This is %sa public list, which means that the " - "members list is openly available" % also) - if self.obscure_addresses: - msg = msg + (" (but we obscure the addresses so they are " - "not easily recognizable by spammers). ") - else: - msg = msg + ". " - - return msg - - def FormatUndigestButton(self): - if self.digest_is_default: - checked = '' - else: - checked = ' CHECKED' - return '<input type=radio name="digest" value="0"%s>' % checked - - def FormatMimeDigestsButton(self): - if self.mime_is_default_digest: - checked = ' CHECKED' - else: - checked = '' - return '<input type=radio name="mime" value="1"%s>' % checked - def FormatPlainDigestsButton(self): - if self.mime_is_default_digest: - checked = '' - else: - checked = ' CHECKED' - return '<input type=radio name="plain" value="1"%s>' % checked - - def FormatEditingOption(self): - "Present editing options, according to list privacy." - - text = ('To change your subscription (set options like digest' - ' and delivery modes, get a reminder of your password,' - ' or unsubscribe from ' - + self.real_name - + '), %senter your subscription email address:<p><center> ') - - if self.private_roster == 0: - text = text % "<b><i>either</i></b> " - else: - text = text % "" - text = (text - + TextBox('info', size=30).Format() - + " " - + SubmitButton('UserOptions', 'Edit Options').Format() - + "</center>") - if self.private_roster == 0: - text = text + ("<p>... <b><i>or</i></b> select your entry from the" - " subscribers list (see above).") - return text - - def RestrictedListMessage(self, which, restriction): - if not restriction: - return "" - elif restriction == 1: - return ("<i>The %s is only available to the list members.</i>" - % which) - else: - return ("<i>The %s is only available to the list" - " administrator.</i>" % which) - def FormatRosterOptionForUser(self): - return self.RosterOption().Format() - def RosterOption(self): - "Provide avenue to subscribers roster, contingent to .private_roster." - container = Container() - if not self.private_roster: - container.AddItem("Click here for the list of " - + self.real_name - + " subscribers: ") - container.AddItem(SubmitButton('SubscriberRoster', - 'Visit Subscriber list')) - else: - if self.private_roster == 1: - only = 'members' - whom = 'Address:' - else: - only = 'the list administrator' - whom = 'Admin address:' - # Solicit the user and password. - container.AddItem(self.RestrictedListMessage('subscribers list', - self.private_roster) - + " <p>Enter your " - + string.lower(whom[:-1]) - + " address and password to visit" - " the subscribers list: <p><center> " - + whom - + " ") - container.AddItem(self.FormatBox('roster-email')) - container.AddItem(" Password: " - + self.FormatSecureBox('roster-pw') - + " ") - container.AddItem(SubmitButton('SubscriberRoster', - 'Visit Subscriber List')) - container.AddItem("</center>") - return container - - def FormatFormStart(self, name, extra=''): - base_url = self.GetRelativeScriptURL(name) - full_url = os.path.join(base_url, extra) - return ('<FORM Method=POST ACTION="%s">' % full_url) - - def FormatArchiveAnchor(self): - return '<a href="%s">' % self.GetBaseArchiveURL() - - def FormatFormEnd(self): - return '</FORM>' - - def FormatBox(self, name, size=20): - return '<INPUT type="Text" name="%s" size="%d">' % (name, size) - - def FormatSecureBox(self, name): - return '<INPUT type="Password" name="%s" size="15">' % name - - def FormatButton(self, name, text='Submit'): - return '<INPUT type="Submit" name="%s" value="%s">' % (name, text) - - def ParseTags(self, template, replacements): - text = self.SnarfHTMLTemplate(template) - parts = regsub.splitx(text, '</?[Mm][Mm]-[^>]*>') - i = 1 - while i < len(parts): - tag = string.lower(parts[i]) - if replacements.has_key(tag): - parts[i] = replacements[tag] - else: - parts[i] = '' - i = i + 2 - return string.join(parts, '') - - # This needs to wait until after the list is inited, so let's build it - # when it's needed only. - def GetStandardReplacements(self): - return { - '<mm-mailman-footer>' : self.GetMailmanFooter(), - '<mm-list-name>' : self.real_name, - '<mm-email-user>' : self._internal_name, - '<mm-list-description>' : self.description, - '<mm-list-info>' : string.join(string.split(self.info, '\n'), - '<br>'), - '<mm-form-end>' : self.FormatFormEnd(), - '<mm-archive>' : self.FormatArchiveAnchor(), - '</mm-archive>' : '</a>', - '<mm-regular-users>' : self.FormatUsers(0), - '<mm-list-subscription-msg>' : self.FormatSubscriptionMsg(), - '<mm-restricted-list-message>' : \ - self.RestrictedListMessage('current archive', - self.archive_private), - '<mm-digest-users>' : self.FormatUsers(1), - '<mm-num-reg-users>' : `len(self.members)`, - '<mm-num-digesters>' : `len(self.digest_members)`, - '<mm-num-members>' : (`len(self.members)` - + `len(self.digest_members)`), - '<mm-posting-addr>' : '%s' % self.GetListEmail(), - '<mm-request-addr>' : '%s' % self.GetRequestEmail(), - '<mm-owner>' : self.GetAdminEmail() - } - - def InitTemplates(self): - def ExtensionFilter(item): - return item[-5:] == '.html' - - files = filter(ExtensionFilter, os.listdir(mm_cfg.TEMPLATE_DIR)) - mm_utils.MakeDirTree(self._template_dir) - for filename in files: - file1 = open(os.path.join(mm_cfg.TEMPLATE_DIR, filename), 'r') - text = file1.read() - file1.close() - file2 = open(os.path.join(self._template_dir, filename), 'w+') - file2.write(text) - file2.close() diff --git a/modules/mm_mailcmd.py b/modules/mm_mailcmd.py deleted file mode 100644 index 745327394..000000000 --- a/modules/mm_mailcmd.py +++ /dev/null @@ -1,607 +0,0 @@ -# Copyright (C) 1998 by the Free Software Foundation, Inc. -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - - -"""Process maillist user commands arriving via email.""" - -__version__ = "$Revision: 693 $" - -# Try to stay close to majordomo commands, but accept common mistakes. -# Not implemented: get / index / which. - -import string, os, sys, re -import mm_message, mm_err, mm_cfg, mm_utils, mm_pending - -option_descs = { 'digest' : - 'receive mail from the list bundled together instead of ' - 'one post at a time', - 'nomail' : - 'Stop delivering mail. Useful if you plan on taking a ' - 'short vacation.', - 'norcv' : - 'Turn this on to NOT receive posts you send to the list. ' - 'does not work if digest is set', - 'ack' : - 'Turn this on to receive acknowlegement mail when you ' - 'send mail to the list', - 'plain' : - 'Get plain, not MIME-compliant, ' - 'digests (only if digest is set)', - 'hide' : - 'Conceals your email from the list of subscribers' - } -option_info = { 'digest' : 0, - 'nomail' : mm_cfg.DisableDelivery, - 'norcv' : mm_cfg.DontReceiveOwnPosts, - 'ack' : mm_cfg.AcknowlegePosts, - 'plain' : mm_cfg.DisableMime, - 'hide' : mm_cfg.ConcealSubscription - } - -class MailCommandHandler: - def __init__(self): - self._response_buffer = '' - self._cmd_dispatch = { - 'subscribe' : self.ProcessSubscribeCmd, - 'confirm': self.ProcessConfirmCmd, - 'unsubscribe' : self.ProcessUnsubscribeCmd, - 'who' : self.ProcessWhoCmd, - 'info' : self.ProcessInfoCmd, - 'lists' : self.ProcessListsCmd, - 'help' : self.ProcessHelpCmd, - 'set' : self.ProcessSetCmd, - 'options' : self.ProcessOptionsCmd, - 'password' : self.ProcessPasswordCmd, - } - self.__NoMailCmdResponse = 0 - - def AddToResponse(self, text): - self._response_buffer = self._response_buffer + text + "\n" - - def AddError(self, text): - self._response_buffer = self._response_buffer + "**** " + text + "\n" - - def ParseMailCommands(self): - mail = mm_message.IncomingMessage() - subject = mail.getheader("subject") - sender = string.lower(mail.GetSender()) - if sender in ['daemon', 'nobody', 'mailer-daemon', 'postmaster', - 'orphanage', 'postoffice']: - # This is for what are probably delivery-failure notices of - # subscription confirmations that are, of necessity, bounced - # back to the -request address. - self.LogMsg("bounce", - ("%s: Mailcmd rejected" - "\n\tReason: Probable bounced subscribe-confirmation" - "\n\tFrom: %s" - "\n\tSubject: %s" - ), - self._internal_name, - mail.getheader('from'), - subject) - return - if subject: - subject = string.strip(subject) - if (subject and self._cmd_dispatch.has_key(string.split(subject)[0])): - lines = [subject] + string.split(mail.body, '\n') - else: - lines = string.split(mail.body, '\n') - if subject: - # - # check to see if confirmation request -- special handling - # - conf_pat = r"%s -- confirmation of subscription -- request (\d\d\d\d\d\d)" % \ - self.real_name - match = re.search(conf_pat, subject) - if not match: - match = re.search(conf_pat, mail.body) - if match: - lines = ["confirm %s" % (match.group(1))] - else: - self.AddError("Subject line ignored: %s" % subject) - for line in lines: - line = string.strip(line) - if not line: - continue - self.AddToResponse("\n>>>> %s" % line) - line = string.strip(line) - if not line: - continue - args = string.split(line) - cmd = string.lower(args[0]) - args = args[1:] - if cmd == 'end': - self.AddError("End of commands.") - break - if not self._cmd_dispatch.has_key(cmd): - self.AddError("%s: Command UNKNOWN." % cmd) - else: - self._cmd_dispatch[cmd](args, line, mail) - if not self.__NoMailCmdResponse: - self.SendMailCmdResponse(mail) - - def SendMailCmdResponse(self, mail): - self.SendTextToUser(subject = 'Mailman results for %s' % - self.real_name, - recipient = mail.GetSender(), - sender = self.GetRequestEmail(), - text = self._response_buffer) - self._response_buffer = '' - - def ProcessPasswordCmd(self, args, cmd, mail): - if len(args) <> 2: - self.AddError("Usage: password <oldpw> <newpw>") - return - try: - self.ChangeUserPassword(mail.GetSender(), - args[0], args[1], args[1]) - self.AddToResponse('Succeeded.') - except mm_err.MMListNotReady: - self.AddError("List is not functional.") - except mm_err.MMNotAMemberError: - self.AddError("%s isn't subscribed to this list." % - mail.GetSender()) - except mm_err.MMBadPasswordError: - self.AddError("You gave the wrong password.") - except: - self.AddError("An unknown Mailman error occured.") - self.AddError("Please forward on your request to %s" % - self.GetAdminEmail()) - self.AddError("%s" % sys.exc_type) - - def ProcessOptionsCmd(self, args, cmd, mail): - sender = self.FindUser(mail.GetSender()) - if not sender: - self.AddError("%s is not a member of the list." % mail.GetSender()) - return - options = option_info.keys() - options.sort() - value = '' - for option in options: - if self.GetUserOption(sender, option_info[option]): - value = 'on' - else: - value = 'off' - self.AddToResponse("%s: %s" % (option, value)) - self.AddToResponse("") - self.AddToResponse("To change an option, do: " - "set <option> <on|off> <password>") - self.AddToResponse("") - self.AddToResponse("Option explanations:") - self.AddToResponse("--------------------") - for option in options: - self.AddToResponse("%s:" % option) - self.AddToResponse(option_descs[option]) - self.AddToResponse("") - - def ProcessSetCmd(self, args, cmd, mail): - def ShowSetUsage(s=self, od = option_descs): - options = od.keys() - options.sort() - s.AddError("Usage: set <option> <on|off> <password>") - s.AddError("Valid options are:") - for option in options: - s.AddError("%s: %s" % (option, od[option])) - if len(args) <> 3: - ShowSetUsage() - return - if args[1] == 'on': - value = 1 - elif args[1] == 'off': - value = 0 - else: - ShowSetUsage() - return - if option_info.has_key(args[0]): - try: - sender = self.FindUser(mail.GetSender()) - if not sender: - self.AddError("You aren't subscribed.") - return - self.ConfirmUserPassword(sender, args[2]) - self.SetUserOption(sender, option_info[args[0]], value) - self.AddToResponse("Succeeded.") - except mm_err.MMBadPasswordError: - self.AddError("You gave the wrong password.") - except: - self.AddError("An unknown Mailman error occured.") - self.AddError("Please forward on your request to %s" % - self.GetAdminEmail()) - self.AddError("%s" % sys.exc_type) - elif args[0] == 'digest': - try: - self.SetUserDigest(mail.GetSender(), args[2], value) - self.AddToResponse("Succeeded.") - except mm_err.MMAlreadyDigested: - self.AddError("You are already receiving digests.") - except mm_err.MMAlreadyUndigested: - self.AddError("You already have digests off.") - except mm_err.MMBadEmailError: - self.AddError("Email address '%s' not accepted by Mailman." % - mail.GetSender()) - except mm_err.MMMustDigestError: - self.AddError("List only accepts digest members.") - except mm_err.MMCantDigestError: - self.AddError("List doesn't accept digest members.") - except mm_err.MMNotAMemberError: - self.AddError("%s isn't subscribed to this list." - % mail.GetSender()) - except mm_err.MMListNotReady: - self.AddError("List is not functional.") - except mm_err.MMNoSuchUserError: - self.AddError("%s is not subscribed to this list." - % mail.GetSender()) - except mm_err.MMBadPasswordError: - self.AddError("You gave the wrong password.") - except mm_err.MMNeedApproval: - self.AddApprovalMsg(cmd) - except: - # TODO: Should log the error we got if we got here. - self.AddError("An unknown Mailman error occured.") - self.AddError("Please forward on your request to %s" % - self.GetAdminEmail()) - self.AddError("%s" % sys.exc_type) - else: - ShowSetUsage() - return - - def ProcessListsCmd(self, args, cmd, mail): - if len(args) != 0: - self.AddError("Usage: lists") - return - lists = os.listdir(mm_cfg.LIST_DATA_DIR) - lists.sort() - self.AddToResponse("** Public mailing lists run by Mailman@%s:" - % self.host_name) - for list in lists: - if list == self._internal_name: - listob = self - else: - try: - import maillist - listob = maillist.MailList(list) - except: - continue - # We can mention this list if you already know about it. - if not listob.advertised and listob <> self: - continue - self.AddToResponse("%s (requests to %s):\n\t%s" % - (listob.real_name, listob.GetRequestEmail(), - listob.description)) - - def ProcessInfoCmd(self, args, cmd, mail): - if len(args) != 0: - self.AddError("Usage: info\n" - "To get info for a particular list, " - "send your request to\n" - "the '-request' address for that list, or " - "use the 'lists' command\n" - "to get info for all the lists.") - return - - if self.private_roster and not self.IsMember(mail.GetSender()): - self.AddError("Private list: only members may see info.") - return - - self.AddToResponse("\nFor more complete info about %s, including" - " background" % self.real_name) - self.AddToResponse("and instructions for subscribing to and" - " using it, visit:\n\n\t%s\n" - % self.GetAbsoluteScriptURL('listinfo')) - - if not self.info: - self.AddToResponse("No other details about %s are available." % - self.real_name) - else: - self.AddToResponse("Here is the specific description of %s:\n" - % self.real_name) - # Put a blank line between the paragraphs, as indicated by CRs. - self.AddToResponse(string.join(string.split(self.info, "\n"), - "\n\n")) - - def ProcessWhoCmd(self, args, cmd, mail): - if len(args) != 0: - self.AddError("To get subscribership for a particular list, " - "send your request\n" - "to the '-request' address for that list.") - return - def AddTab(str): - return '\t' + str - - if self.private_roster == 2: - self.AddError("Private list: No one may see subscription list.") - return - if self.private_roster and not self.IsMember(mail.GetSender()): - self.AddError("Private list: only members may see list " - "of subscribers.") - return - if not len(self.digest_members) and not len(self.members): - self.AddToResponse("NO MEMBERS.") - return - - def NotHidden(x, s=self, v=mm_cfg.ConcealSubscription): - return not s.GetUserOption(x, v) - - if len(self.digest_members): - self.AddToResponse("") - self.AddToResponse("Digest Members:") - digestmembers = self.digest_members[:] - digestmembers.sort() - self.AddToResponse(string.join(map(AddTab, - filter(NotHidden, - digestmembers)), - "\n")) - if len(self.members): - self.AddToResponse("Non-Digest Members:") - members = self.members[:] - members.sort() - self.AddToResponse(string.join(map(AddTab, - filter(NotHidden, members)), - "\n")) - - def ProcessUnsubscribeCmd(self, args, cmd, mail): - if not len(args): - self.AddError("Must supply a password.") - return - if len(args) > 2: - self.AddError("To get unsubscribe from a particular list, " - "send your request\nto the '-request' address" - "for that list.") - return - - if len(args) == 2: - addr = args[1] - else: - addr = mail.GetSender() - try: - self.ConfirmUserPassword(addr, args[0]) - self.DeleteMember(addr, "mailcmd") - self.AddToResponse("Succeeded.") - except mm_err.MMListNotReady: - self.AddError("List is not functional.") - except mm_err.MMNoSuchUserError: - self.AddError("%s is not subscribed to this list." - % mail.GetSender()) - except mm_err.MMBadPasswordError: - self.AddError("You gave the wrong password.") - except: - # TODO: Should log the error we got if we got here. - self.AddError("An unknown Mailman error occured.") - self.AddError("Please forward on your request to %s" - % self.GetAdminEmail()) - self.AddError("%s %s" % (sys.exc_type, sys.exc_value)) - self.AddError("%s" % sys.exc_traceback) - - def ProcessSubscribeCmd(self, args, cmd, mail): - digest = self.digest_is_default - password = "" - address = "" - done_digest = 0 - if not len(args): - password = "%s%s" % (mm_utils.GetRandomSeed(), - mm_utils.GetRandomSeed()) - elif len(args) > 3: - self.AddError("Usage: subscribe [password] [digest|nodigest] [address=<email-address>]") - return - else: - for arg in args: - if string.lower(arg) == 'digest' and not done_digest: - digest = 1 - done_digest = 1 - elif string.lower(arg) == 'nodigest' and not done_digest: - digest = 0 - done_digest = 1 - elif string.lower(arg)[:8] == 'address=' and not address: - address = string.lower(arg)[8:] - elif not password: - password = arg - else: - self.AddError("Usage: subscribe [password] " - "[digest|nodigest] [address=<email-address>]") - return - if not password: - password = "%s%s" % (mm_utils.GetRandomSeed(), - mm_utils.GetRandomSeed()) - if not address: - pending_addr = mail.GetSender() - else: - pending_addr = address - cookie = mm_pending.gencookie() - mm_pending.add2pending(pending_addr, password, digest, cookie) - self.SendTextToUser(subject = "%s -- confirmation of subscription -- request %d" % \ - (self.real_name, cookie), - recipient = pending_addr, - sender = self.GetRequestEmail(), - text = mm_pending.VERIFY_FMT % ({"email": pending_addr, - "listaddress": self.GetListEmail(), - "listname": self.real_name, - "cookie": cookie, - "requestor": mail.GetSender(), - "request_addr": self.GetRequestEmail()})) - self.__NoMailCmdResponse = 1 - return - - - def FinishSubscribe(self, addr, password, digest): - try: - self.AddMember(addr, password, digest) - self.AddToResponse("Succeeded.") - except mm_err.MMBadEmailError: - self.AddError("Email address '%s' not accepted by Mailman." % - addr) - except mm_err.MMMustDigestError: - self.AddError("List only accepts digest members.") - except mm_err.MMCantDigestError: - self.AddError("List doesn't accept digest members.") - except mm_err.MMListNotReady: - self.AddError("List is not functional.") - except mm_err.MMNeedApproval: - self.AddApprovalMsg(cmd) - except mm_err.MMHostileAddress: - self.AddError("Email address '%s' not accepted by Mailman " - "(insecure address)" % addr) - except mm_err.MMAlreadyAMember: - self.AddError("%s is already a list member." % addr) - except: - # TODO: Should log the error we got if we got here. - self.AddError("An unknown Mailman error occured.") - self.AddError("Please forward on your request to %s" % - self.GetAdminEmail()) - self.AddError("%s" % sys.exc_type) - - - - def ProcessConfirmCmd(self, args, cmd, mail): - if len(args) != 1: - self.AddError("Usage: confirm <confirmation number>\n") - return - try: - cookie = string.atoi(args[0]) - except: - self.AddError("Usage: confirm <confirmation number>\n") - return - pending = mm_pending.get_pending() - if not pending.has_key(cookie): - self.AddError("Invalid confirmation number!\n" - "Please recheck the confirmation number and try again.") - return - (email_addr, password, digest, ts) = pending[cookie] - if self.open_subscribe: - self.ApprovedAddMember(email_addr, password, digest) - self.AddToResponse("Succeeded") - else: - self.AddRequest('add_member', digest, email_addr, password) - del pending[cookie] - mm_pending.set_pending(pending) - - - - def AddApprovalMsg(self, cmd): - self.AddError('''Your request to %s: - - %s - -has been forwarded to the person running the list. - -This is probably because you are trying to subscribe to a 'closed' list. - -You will receive email notification of the list owner's decision about -your subscription request. - -Any questions about the list owner's policy should be directed to: - - %s - -''' % (self.GetRequestEmail(), cmd, self.GetAdminEmail())) - - - def ProcessHelpCmd(self, args, cmd, mail): - self.AddToResponse("**** Help for %s maillist:" % self.real_name) - self.AddToResponse(""" -This is email command help for version %s of the "Mailman" list manager. -The following describes commands you can send to get information about and -control your subscription to mailman lists at this site. A command can -be the subject line or in the body of the message. - -(Note that much of the following can also be accomplished via the web, at: - - %s - -In particular, you can use the web site to have your password sent to your -delivery address.) - -List specific commands (subscribe, who, etc) should be sent to the -*-request address for the particular list, e.g. for the 'mailman' list, -use 'mailman-request@...'. - -About the descriptions - words in "<>"s signify REQUIRED items and words in -"[]" denote OPTIONAL items. Do not include the "<>"s or "[]"s when you use -the commands. - -The following commands are valid: - - subscribe [password] [digest-option] [address=<address>] - Subscribe to the mailing list. Your password must be given to - unsubscribe or change your options. When you subscribe to the - list, you'll be reminded of your password periodically. - 'digest-option' may be either: 'nodigest' or 'digest' (no quotes!) - If you wish to subscribe an address other than the address you send - this request from, you may specify "address=<email address>" (no brackets - around the email address, no quotes!) - - unsubscribe <password> [address] - Unsubscribe from the mailing list. Your password must match - the one you gave when you subscribed. If you are trying to - unsubscribe from a different address than the one you subscribed - from, you may specify it in the 'address' field. - - who - See who is on this mailing list. - - info - View the introductory information for this list. - - lists - See what mailing lists are run by this Mailman server. - - help - This message. - - set <option> <on|off> <password> - Turn on or off list options. Valid options are: - - ack: - Turn this on to receive acknowlegement mail when you send mail to - the list - - digest: - receive mail from the list bundled together instead of one post at - a time - - plain: - Get plain-text, not MIME-compliant, digests (only if digest is set) - - nomail: - Stop delivering mail. Useful if you plan on taking a short vacation. - - norcv: - Turn this on to NOT receive posts you send to the list. - Does not work if digest is set - - hide: - Conceals your address when people look at who is on this list. - - - options - Show the current values of your list options. - - password <oldpassword> <newpassword> - Change your list password. - - end - Stop processing commands (good to do if your mailer automatically - adds a signature file - it'll save you from a lot of cruft). - - -Commands should be sent to %s - -Questions and concerns for the attention of a person should be sent to -%s -""" % (mm_cfg.VERSION, - self.GetAbsoluteScriptURL('listinfo'), - self.GetRequestEmail(), - self.GetAdminEmail())) - diff --git a/modules/mm_mbox.py b/modules/mm_mbox.py deleted file mode 100644 index 8eb50c78c..000000000 --- a/modules/mm_mbox.py +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright (C) 1998 by the Free Software Foundation, Inc. -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - - -"Extend mailbox.UnixMailbox." - -__version__ = "$Revision: 547 $" - - -import mailbox - -class Mailbox(mailbox.UnixMailbox): - # msg should be an rfc822 message or a subclass. - def AppendMessage(self, msg): - # seek to the last char of the mailbox - self.fp.seek(1,2) - if self.fp.read(1) <> '\n': - self.fp.write('\n') - self.fp.write(msg.unixfrom) - for line in msg.headers: - self.fp.write(line) - if msg.body[0] <> '\n': - self.fp.write('\n') - self.fp.write(msg.body) - diff --git a/modules/mm_message.py b/modules/mm_message.py deleted file mode 100644 index 82b2cb554..000000000 --- a/modules/mm_message.py +++ /dev/null @@ -1,268 +0,0 @@ -# Copyright (C) 1998 by the Free Software Foundation, Inc. -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - - -"""Embody incoming and outgoing messages as objects.""" - -__version__ = "$Revision: 670 $" - - -import sys -import rfc822, string, time - - -# Utility functions 2 of these classes use: -def AddBackNewline(str): - return str + '\n' - -def RemoveNewline(str): - return str[:-1] - - -# XXX klm - use the standard lib StringIO module instead of FakeFile. -# If we're trying to create a message object from text, we need to pass -# a file object to rfc822.Message to get it to do its magic. Well, -# to avoid writing text out to a file, and having it read back in, -# here we define a class that will fool rfc822 into thinking it's a -# non-seekable message. -# The only method rfc822.Message ever calls on a non-seekable file is -# readline. It doesn't use the optional arg to readline, either. -# In my subclasses, I use the read() method, and might just use readlines() -# someday. -# -# It might be useful to expand this into a full blown fully functional class. - -class FakeFile: - def __init__(self, text): - self.lines = map(AddBackNewline, string.split(text, '\n')) - self.curline = 0 - self.lastline = len(self.lines) - 1 - def readline(self): - if self.curline > self.lastline: - return '' - self.curline = self.curline + 1 - return self.lines[self.curline - 1] - def read(self): - startline = self.curline - self.curline = self.lastline + 1 - return string.join(self.lines[startline:], '') - def readlines(self): - startline = self.curline - self.curline = self.lastline + 1 - return self.lines[startline:] - def seek(self, pos): - if pos <> 0: - raise ValueError, "FakeFiles can only seek to the beginning." - self.curline = 0 - - - -# We know the message is gonna come in on stdin or from text for our purposes. -class IncomingMessage(rfc822.Message): - def __init__(self, text=None): - if not text: - rfc822.Message.__init__(self, sys.stdin, 0) - self.body = self.fp.read() - else: - rfc822.Message.__init__(self, FakeFile(text), 0) - self.body = self.fp.read() - self.file_count = None - - def readlines(self): - if self.file_count <> None: - x = self.file_count - self.file_count = len(self.file_data) - return self.file_data[x:] - return map(RemoveNewline, self.headers) + [''] + \ - string.split(self.body,'\n') - - def readline(self): - if self.file_count == None: - self.file_count = 0 - self.file_data = map(RemoveNewline, self.headers) + [''] + \ - string.split(self.body,'\n') - if self.file_count >= len(self.file_data): - return '' - self.file_count = self.file_count + 1 - return self.file_data[self.file_count-1] + '\n' - - def GetSender(self): - # Look for a Sender field. - sender = self.getheader('sender') - if sender: - realname, mail_address = self.getaddr('sender') - else: - try: - realname, mail_address = self.getaddr('from') - except: - # The unix from line is all we have left... - if self.unixfrom: - return string.lower(string.split(self.unixfrom)[1]) - - return string.lower(mail_address) - - def GetSenderName(self): - real_name, mail_addr = self.getaddr('from') - if not real_name: - return self.GetSender() - return real_name - - def SetHeader(self, name, value, crush_duplicates=1): - # Well, we crush dups in the dict no matter what... - name = "%s%s" % (name[0], name[1:]) - self.dict[string.lower(name)] = value - if value[-1] <> '\n': - value = value + '\n' - - if not crush_duplicates: - self.headers.append('%s: %s' % (name, value)) - return - for i in range(len(self.headers)): - if (string.lower(self.headers[i][:len(name)+1]) == - string.lower(name) + ':'): - self.headers[i] = '%s: %s' % (name, value) - - # XXX Eventually (1.5.1?) Python rfc822.Message() will have its own - # __delitem__. - def __delitem__(self, name): - """Delete all occurrences of a specific header, if it is present.""" - name = string.lower(name) - if not self.dict.has_key(name): - return - del self.dict[name] - name = name + ':' - n = len(name) - list = [] - hit = 0 - for i in range(len(self.headers)): - line = self.headers[i] - if string.lower(line[:n]) == name: - hit = 1 - elif line[:1] not in string.whitespace: - hit = 0 - if hit: - list.append(i) - list.reverse() - for i in list: - del self.headers[i] - -# This is a simplistic class. It could do multi-line headers etc... -# But it doesn't because I don't need that for this app. -class OutgoingMessage: - def __init__(self, headers=None, body='', sender=None): - self.cached_headers = {} - if headers: - self.SetHeaders(headers) - else: - self.headers = [] - self.body = body - self.sender = sender - - def readlines(self): - if self.file_count <> None: - x = self.file_count - self.file_count = len(self.file_data) - return self.file_data[x:] - return map(RemoveNewline, self.headers) + [''] + \ - string.split(self.body,'\n') - - def readline(self): - if self.file_count == None: - self.file_count = 0 - self.file_data = map(RemoveNewline, self.headers) + [''] + \ - string.split(self.body,'\n') - if self.file_count >= len(self.file_data): - return '' - self.file_count = self.file_count + 1 - return self.file_data[self.file_count-1] + '\n' - - def SetHeaders(self, headers): - self.headers = map(AddBackNewline, string.split(headers, '\n')) - self.CacheHeaders() - - def CacheHeaders(self): - for header in self.headers: - i = string.find(header, ':') - self.cached_headers[string.lower(string.strip(header[:i])) - ] = header[i+2:] - - def SetHeader(self, header, value, crush_duplicates=1): - if value[-1] <> '\n': - value = value + '\n' - if crush_duplicates: - # Run through the list and make sure header isn't already there. - remove_these = [] - for item in self.headers: - f = string.find(item, ':') - if string.lower(item[:f]) == string.lower(header): - remove_these.append(item) - for item in remove_these: - self.headers.remove(item) - del remove_these - self.headers.append('%s%s: %s' % (string.upper(header[0]), - string.lower(header[1:]), - value)) - self.cached_headers[string.lower(header)] = value - - def SetBody(self, body): - self.body = body - - def AppendToBody(self, text): - self.body = self.body + text - - def SetSender(self, sender, set_from=1): - self.sender = sender - if not self.getheader('from') and set_from: - self.SetHeader('from', sender) - - def SetDate(self, date=time.ctime(time.time())): - self.SetHeader('date', date) - - def GetSender(self): - return self.sender - -# Lower case the name to give it the same UI as IncomingMessage -# inherits from rfc822 - def getheader(self, str): - str = string.lower(str) - if not self.cached_headers.has_key(str): - return None - return self.cached_headers[str] - - def __delitem__(self, name): - if not self.getheader(name): - return None - newheaders = [] - name = string.lower(name) - nlen = len(name) - for h in self.headers: - if (len(h) > (nlen+1) - and h[nlen] == ":" - and string.lower(h[:nlen]) == name): - continue - newheaders.append(h) - self.headers = newheaders - self.CacheHeaders() - - - -class NewsMessage(IncomingMessage): - def __init__(self, mail_msg): - self.fp = mail_msg.fp - self.fp.seek(0) - rfc822.Message.__init__(self, self.fp, 0) - self.body = self.fp.read() - self.file_count = None diff --git a/modules/mm_pending.py b/modules/mm_pending.py deleted file mode 100644 index 2789a851e..000000000 --- a/modules/mm_pending.py +++ /dev/null @@ -1,71 +0,0 @@ -""" -module for handling pending subscriptions -""" - -import os -import sys -import posixfile -import marshal -import time -import whrandom -import mm_cfg -import flock - -DB_PATH = os.path.join(mm_cfg.DATA_DIR,"pending_subscriptions.db") -LOCK_PATH = os.path.join(mm_cfg.LOCK_DIR, "pending_subscriptions.lock") - - -VERIFY_FMT = """\ - %(listname)s -- confirmation of subscription -- request %(cookie)s - -You or someone (%(requestor)s) has requested that your email -address (%(email)s) be subscribed to the %(listname)s mailling -list at %(listaddress)s. If you wish to fulfill this request, -please simply reply to this message, or mail %(request_addr)s -with the following line, and only the following line in the -message body: - -confirm %(cookie)s - -If you do not wish to subscribe to this list, please simply ignore -or delete this message. -""" - -# ' icky emacs font lock thing - - -def get_pending(): - " returns a dict containing pending information" - try: - fp = open(DB_PATH,"r" ) - except IOError: - return {} - dict = marshal.load(fp) - return dict - - -def gencookie(p=None): - if p is None: - p = get_pending() - while 1: - newcookie = int(whrandom.random() * 1000000) - if p.has_key(newcookie) or newcookie < 100000: - continue - return newcookie - -def set_pending(p): - lock_file = flock.FileLock(LOCK_PATH) - lock_file.lock() - fp = open(DB_PATH, "w") - marshal.dump(p, fp) - fp.close() - lock_file.unlock() - -def add2pending(email_addr, password, digest, cookie): - ts = int(time.time()) - processed = 0 - p = get_pending() - p[cookie] = (email_addr, password, digest, ts) - set_pending(p) - - diff --git a/modules/mm_security.py b/modules/mm_security.py deleted file mode 100644 index 51cb6a309..000000000 --- a/modules/mm_security.py +++ /dev/null @@ -1,96 +0,0 @@ -# Copyright (C) 1998 by the Free Software Foundation, Inc. -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - - -"""Handle passwords and sanitize approved messages.""" - - -import mm_crypt, types, string, os -import mm_err, mm_utils, mm_cfg - -# TBD: is this the best location for the site password? -SITE_PW_FILE = os.path.join(mm_cfg.DATA_DIR, 'adm.pw') - - -class SecurityManager: - def SetSiteAdminPassword(self, pw): - old = os.umask(0022) - f = open(SITE_PW_FILE, "w+") - f.write(mm_crypt.crypt(pw, mm_utils.GetRandomSeed())) - f.close() - os.umask(old) - - def CheckSiteAdminPassword(self, str): - try: - f = open(SITE_PW_FILE, "r") - pw = f.read() - f.close() - return mm_crypt.crypt(str, pw) == pw - # There probably is no site admin password if there was an exception - except: - return 0 - - def InitVars(self, crypted_password): - # Configurable, however, we don't pass this back in GetConfigInfo - # because it's a special case as it requires confirmation to change. - self.password = crypted_password - - # Non configurable - self.passwords = {} - - def ValidAdminPassword(self, pw): - if self.CheckSiteAdminPassword(pw): - return 1 - return ((type(pw) == types.StringType) and - (mm_crypt.crypt(pw, self.password) == self.password)) - - def ConfirmAdminPassword(self, pw): - if(not self.ValidAdminPassword(pw)): - raise mm_err.MMBadPasswordError - return 1 - - def ConfirmUserPassword(self, user, pw): - if self.ValidAdminPassword(pw): - return 1 - if not user in self.members and not user in self.digest_members: - user = self.FindUser(user) - try: - if string.lower(pw) <> string.lower(self.passwords[user]): - raise mm_err.MMBadPasswordError - except KeyError: - raise mm_err.MMBadUserError - return 1 - - def ChangeUserPassword(self, user, newpw, confirm): - self.IsListInitialized() - addr = self.FindUser(user) - if not addr: - raise mm_err.MMNotAMemberError - if newpw <> confirm: - raise mm_err.MMPasswordsMustMatch - self.passwords[addr] = newpw - self.Save() - - def ExtractApproval(self, msg): - """True if message has valid administrator approval. - - Approval line is always stripped from message as a side effect.""" - - p = msg.getheader('approved') - if p == None: - return 0 - del msg['approved'] # Mustn't deliver this line!! - return self.ValidAdminPassword(p) diff --git a/modules/mm_utils.py b/modules/mm_utils.py deleted file mode 100644 index e76ac1920..000000000 --- a/modules/mm_utils.py +++ /dev/null @@ -1,506 +0,0 @@ -# Copyright (C) 1998 by the Free Software Foundation, Inc. -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - - -"""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 maillists, the Logging routines, and whatever else doesn't belong -elsewhere.""" - - -import sys, string, fcntl, os, random, regsub, re -import mm_cfg - -# Valid toplevel domains for when we check the validity of an email address. - -valid_toplevels = ["com", "edu", "gov", "int", "mil", "net", "org", -"inc", "af", "al", "dz", "as", "ad", "ao", "ai", "aq", "ag", "ar", -"am", "aw", "au", "at", "az", "bs", "bh", "bd", "bb", "by", "be", -"bz", "bj", "bm", "bt", "bo", "ba", "bw", "bv", "br", "io", "bn", -"bg", "bf", "bi", "kh", "cm", "ca", "cv", "ky", "cf", "td", "cl", -"cn", "cx", "cc", "co", "km", "cg", "ck", "cr", "ci", "hr", "cu", -"cy", "cz", "dk", "dj", "dm", "do", "tp", "ec", "eg", "sv", "gq", -"ee", "et", "fk", "fo", "fj", "fi", "fr", "gf", "pf", "tf", "ga", -"gm", "ge", "de", "gh", "gi", "gb", "uk", "gr", "gl", "gd", "gp", -"gu", "gt", "gn", "gw", "gy", "ht", "hm", "hn", "hk", "hu", "is", -"in", "id", "ir", "iq", "ie", "il", "it", "jm", "jp", "jo", "kz", -"ke", "ki", "kp", "kr", "kw", "kg", "la", "lv", "lb", "ls", "lr", -"ly", "li", "lt", "lu", "mo", "mg", "mw", "my", "mv", "ml", "mt", -"mh", "mq", "mr", "mu", "mx", "fm", "md", "mc", "mn", "ms", "ma", -"mz", "mm", "na", "nr", "np", "an", "nl", "nt", "nc", "nz", "ni", -"ne", "ng", "nu", "nf", "mp", "no", "om", "pk", "pw", "pa", "pg", -"py", "pe", "ph", "pn", "pl", "pt", "pr", "qa", "re", "ro", "ru", -"rw", "kn", "lc", "vc", "sm", "st", "sa", "sn", "sc", "sl", "sg", -"sk", "si", "sb", "so", "za", "es", "lk", "sh", "pm", "sd", "sr", -"sj", "sz", "se", "ch", "sy", "tw", "tj", "tz", "th", "tg", "tk", -"to", "tt", "tn", "tr", "tm", "tc", "tv", "ug", "ua", "um", "us", -"uy", "uz", "vu", "va", "ve", "vn", "vg", "vi", "wf", "eh", "ws", -"ye", "yu", "zr", "zm", "zw", "su"] - -def list_names(): - """Return the names of all lists in default list directory.""" - got = [] - for fn in os.listdir(mm_cfg.LIST_DATA_DIR): - if not ( - os.path.exists( - os.path.join(os.path.join(mm_cfg.LIST_DATA_DIR, fn), - 'config.db'))): - continue - got.append(fn) - return got - -# a much more naive implementation than say, Emacs's fill-paragraph! -def wrap(text, column=70): - """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 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 = 0 - for line in string.split(para, '\n'): - if not line: - lines.append(line) - continue - if line[0] in string.whitespace: - fillthis = 0 - else: - fillthis = 1 - 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] = string.rstrip(lines[-1]) + ' ' + 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 string.whitespace: - bol = bol - 1 - # now find the last non-whitespace character - eol = bol - while eol > 0 and text[eol] in string.whitespace: - eol = 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 string.whitespace: - eol = eol + 1 - bol = eol - while bol < len(text) and \ - text[bol] in string.whitespace: - bol = bol + 1 - bol = bol - 1 - line = text[:eol+1] + '\n' - text = text[bol+1:] - wrapped = wrapped + line - wrapped = wrapped + '\n' - wrapped = wrapped + '\n' - return wrapped - - -def SendTextToUser(subject, text, recipient, sender, add_headers=[], raw=0): - import mm_message - msg = mm_message.OutgoingMessage() - msg.SetSender(sender) - msg.SetHeader('Subject', subject, 1) - if not raw: - text = wrap(text) - msg.SetBody(QuotePeriods(text)) - DeliverToUser(msg, recipient, add_headers=add_headers) - -def DeliverToUser(msg, recipient, add_headers=[]): - """Use smtplib to deliver message. - - Optional argument add_headers should be a list of headers to be added - to the message, e.g. for Errors-To and X-No-Archive.""" - # We fork to ensure no deadlock. Otherwise, even if sendmail is - # invoked in forking mode, if it eg detects a bad address before - # forking, then it will try deliver to the errorsto addr *in the - # foreground*. If the errorsto happens to be the list owner for a list - # that is doing the send - and holding a lock - then the delivery will - # hang pending release of the lock - deadlock. - if os.fork(): - return - import smtplib - sender = msg.GetSender() - - try: - try: - msg.headers.remove('\n') - except ValueError: - pass - if not msg.getheader('to'): - msg.headers.append('To: %s\n' % recipient) - for i in add_headers: - if i and i[-1] != '\n': - i = i + '\n' - msg.headers.append(i) - - text = string.join(msg.headers, '')+ '\n'+ QuotePeriods(msg.body) - con = smtplib.SmtpConnection(mm_cfg.SMTPHOST) - con.helo(mm_cfg.DEFAULT_HOST_NAME) - con.send(to=recipient,frm=sender,text=text) - con.quit() - finally: - os._exit(0) - -def QuotePeriods(text): - return string.join(string.split(text, '\n.\n'), '\n .\n') - -def ValidEmail(str): - """Verify that the an email address isn't grossly invalid.""" - # Pretty minimal, cheesy check. We could do better... - if ((string.find(str, '|') <> -1) or (string.find(str, ';') <> -1) - or str[0] == '-'): - raise mm_err.MMHostileAddress - if string.find(str, '/') <> -1: - if os.path.isdir(os.path.split(str)[0]): - raise mm_err.MMHostileAddress - user, domain_parts = ParseEmail(str) - if not domain_parts: - if string.find(str, '@') < 1: - return 0 - else: - return 1 - if len(domain_parts) < 2: - return 0 -## if domain_parts[-1] not in valid_toplevels: -## if len(domain_parts) <> 4: -## return 0 -## try: -## domain_parts = map(eval, domain_parts) -## except: -## return 0 -## for i in domain_parts: -## if i < 0 or i > 255: -## return 0 - return 1 - - -# -def GetPathPieces(path): - l = string.split(path, '/') - try: - while 1: - l.remove('') - except ValueError: - pass - return l - -nesting_level = None -def GetNestingLevel(): - global nesting_level - if nesting_level == None: - try: - path = os.environ['PATH_INFO'] - if path[0] <> '/': - path= '/' + path - nesting_level = len(string.split(path, '/')) - 1 - except KeyError: - nesting_level = 0 - return nesting_level - -def MakeDirTree(path, perms=0775, verbose=0): - made_part = '/' - path_parts = GetPathPieces(path) - for item in path_parts: - made_part = os.path.join(made_part, item) - if os.path.exists(made_part): - if not os.path.isdir(made_part): - raise "RuntimeError", ("Couldn't make dir tree for %s. (%s" - " already exists)" % (path, made_part)) - else: - ou = os.umask(0) - try: - os.mkdir(made_part, perms) - finally: - os.umask(ou) - if verbose: - print 'made directory: ', madepart - -# This takes an email address, and returns a tuple containing (user,host) -def ParseEmail(email): - user = None - domain = None - email = string.lower(email) - at_sign = string.find(email, '@') - if at_sign < 1: - return (email, None) - user = email[:at_sign] - rest = email[at_sign+1:] - domain = string.split(rest, '.') - return (user, domain) - -# Return 1 if the 2 addresses match. 0 otherwise. -# Might also want to match if there's any common domain name... -# There's password protection anyway. - -def AddressesMatch(addr1, addr2): - "True when username matches and host addr of one addr contains other's." - user1, domain1 = ParseEmail(addr1) - user2, domain2 = ParseEmail(addr2) - if user1 != user2: - return 0 - if domain1 == domain2: - return 1 - elif not domain1 or not domain2: - return 0 - for i in range(-1 * min(len(domain1), len(domain2)), 0): - # By going from most specific component of host part we're likely - # to hit a difference sooner. - if domain1[i] != domain2[i]: - return 0 - return 1 - - -def FindMatchingAddresses(name, array): - """Given an email address, and a list of email addresses, returns the - subset of the list that matches the given address. Should sort based - on exactness of match, just in case.""" - - def CallAddressesMatch (x, y=name): - return AddressesMatch(x,y) - - matches = filter(CallAddressesMatch, array) - return matches - -def GetRandomSeed(): - chr1 = int(random.random() * 57) + 65 - chr2 = int(random.random() * 57) + 65 - return "%c%c" % (chr1, chr2) - - -def SnarfMessage(msg): - if msg.unixfrom: - text = msg.unixfrom + string.join(msg.headers, '') + '\n' + msg.body - else: - text = string.join(msg.headers, '') + '\r\n' + msg.body - return (msg.GetSender(), text) - - -def QuoteHyperChars(str): - arr = regsub.splitx(str, '[<>"&]') - i = 1 - while i < len(arr): - if arr[i] == '<': - arr[i] = '<' - elif arr[i] == '>': - arr[i] = '>' - elif arr[i] == '"': - arr[i] = '"' - else: #if arr[i] == '&': - arr[i] = '&' - i = i + 2 - return string.join(arr, '') - -# Just changing these two functions should be enough to control the way -# that email address obscuring is handled. - -def ObscureEmail(addr, for_text=0): - """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 re.sub("@", " at ", addr) - else: - return re.sub("@", "__at__", addr) - -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 re.sub("__at__", "@", addr) - -def map_maillists(func, names=None, unlock=None, verbose=0): - """Apply function (of one argument) to all list objs in turn. - - Returns a list of the results. - - Optional arg 'names' specifies which lists, default all. - Optional arg unlock says to unlock immediately after instantiation. - Optional arg verbose says to print list name as it's about to be - instantiated, CR when instantiation is complete, and result of - application as it shows.""" - from maillist import MailList - if names == None: names = list_names() - got = [] - for i in names: - if verbose: print i, - l = MailList(i) - if verbose: print - if unlock and l.Locked(): - l.Unlock() - got.append(apply(func, (l,))) - if verbose: print got[-1] - if not unlock: - l.Unlock() - del l - return got - -class Logger: - """File-based logger, writes to named category files in mm_cfg.LOG_DIR.""" - def __init__(self, category, nofail=1): - """Nofail (by default) says to fallback to sys.stderr if write - fails to category file. A message is emitted, but the IOError is - caught. Set nofail=0 if you want to handle the error in your code, - instead.""" - - self.__category=category - self.__f = None - self.__nofail = nofail - def __get_f(self): - if self.__f: - return self.__f - else: - fname = os.path.join(mm_cfg.LOG_DIR, self.__category) - try: - ou = os.umask(002) - try: - f = self.__f = open(fname, 'a+') - finally: - os.umask(ou) - except IOError, msg: - if not self.__nofail: - raise IOError, msg, sys.exc_info()[2] - else: - f = self.__f = sys.stderr - f.write("logger open %s failed %s, using stderr\n" - % (fname, msg)) - return f - def flush(self): - f = self.__get_f() - if hasattr(f, 'flush'): - f.flush() - def write(self, msg): - f = self.__get_f() - try: - f.write(msg) - except IOError, msg: - f = self.__f = sys.stderr - f.write("logger write %s failed %s, using stderr\n" - % (fname, msg)) - def writelines(self, lines): - for l in lines: - self.write(l) - def close(self): - if not self.__f: - return - self.__get_f().close() - def __del__(self): - try: - if self.__f and self.__f != sys.stderr: - self.close() - except: - pass - -class StampedLogger(Logger): - """Record messages in log files, including date stamp and optional label. - - If manual_reprime is on (off by default), then timestamp prefix will - included only on first .write() and on any write immediately following - a call to the .reprime() method. This is useful for when StampedLogger - is substituting for sys.stderr, where you'd like to see the grouping of - multiple writes under a single timestamp (and there is often is one - group, for uncaught exceptions where a script is bombing). - - In any case, the identifying prefix will only follow writes that start - on a new line. - - Nofail (by default) says to fallback to sys.stderr if write fails to - category file. A message is emitted, but the IOError is caught. - Initialize with nofail=0 if you want to handle the error in your code, - instead.""" - - def __init__(self, category, label=None, manual_reprime=0, nofail=1): - "If specified, optional label is included after timestamp." - self.label = label - self.manual_reprime = manual_reprime - self.primed = 1 - self.bol = 1 - Logger.__init__(self, category, nofail=nofail) - def reprime(self): - """Reset so timestamp will be included with next write.""" - self.primed = 1 - def write(self, msg): - import time - if not self.bol: - prefix = "" - else: - if not self.manual_reprime or self.primed: - stamp = time.strftime("%b %d %H:%M:%S %Y ", - time.localtime(time.time())) - self.primed = 0 - else: - stamp = "" - if self.label == None: - label = "" - else: - label = "%s:" % self.label - prefix = stamp + label - Logger.write(self, "%s %s" % (prefix, msg)) - if msg and msg[-1] == '\n': - self.bol = 1 - else: - self.bol = 0 - def writelines(self, lines): - first = 1 - for l in lines: - if first: - self.write(l) - first = 0 - else: - if l and l[0] not in [' ', '\t', '\n']: - Logger.write(self, ' ' + l) - else: - Logger.write(self, l) - -def chunkify(members, chunksize=mm_cfg.ADMIN_MEMBER_CHUNKSIZE): - """ - return a list of lists of members - """ - members.sort() - res = [] - while 1: - if not members: - break - chunk = members[:chunksize] - res.append(chunk) - members = members[chunksize:] - return res diff --git a/modules/pipermail.py b/modules/pipermail.py deleted file mode 100644 index 185133364..000000000 --- a/modules/pipermail.py +++ /dev/null @@ -1,560 +0,0 @@ -#!/usr/local/bin/python -# Hey Emacs, this is -*-Python-*- code! -# -# Pipermail 0.0.2-mm -# -# **NOTE** -# -# This internal version of pipermail has been deprecated in favor of use of -# an external version of pipermail, available from: -# http://starship.skyport.net/crew/amk/maintained/pipermail.html -# The external version should be pointed at the created archive files. -# -# -# Some minor mods have been made for use with the Mailman mailing list manager. -# All changes will have JV by them. -# -# (C) Copyright 1996, A.M. Kuchling (amk@magnet.com) -# Home page at http://amarok.magnet.com/python/pipermail.html -# -# HTML code for frames courtesy of Scott Hassan (hassan@cs.stanford.edu) -# -# TODO: -# * Prev. article, next. article pointers in each article -# * I suspect there may be problems with rfc822.py's getdate() method; -# take a look at the threads "Greenaway and the net (fwd)" or -# "Pillow Book pictures". To be looked into... -# * Anything else Hypermail can do that we can't? -# * General code cleanups -# * Profiling & optimization -# * Should there be an option to enable/disable frames? -# * Like any truly useful program, Pipermail should have an ILU interface. -# * There's now an option to keep from preserving line breaks, -# so paragraphs in messages would be reflowed by the browser. -# Unfortunately, this mangles .sigs horribly, and pipermail doesn't yet -# put in paragraph breaks. Putting in the breaks will only require a -# half hour or so; I have no clue as to how to preserve .sigs. -# * Outside URLs shouldn't appear in the display frame. How to fix? -# - -VERSION = "0.0.2.mm" - -import posixpath, time, os, string, sys, rfc822 - -# JV -- to get HOME_PAGE -import mm_cfg - -class ListDict: - def __init__(self): self.dict={} - def keys(self): return self.dict.keys() - def __setitem__(self, key, value): - "Add the value to a list for the key, creating the list if needed." - if not self.dict.has_key(key): self.dict[key]=[value] - else: self.dict[key].append(value) - def __getitem__(self, key): - "Return the list matching a key" - return self.dict[key] - -def PrintUsage(): - print """Pipermail %s -usage: pipermail [options] -options: -a URL : URL to other archives - -b URL : URL to archive information - -c file : name of configuration file (default: ~/.pmrc) - -d dir : directory where the output files will be placed - (default: archive/) - -l name : name of the output archive - -m file : name of input file - -s file : name where the archive state is stored - (default: <input file>+'.pipermail' - -u : Select 'update' mode - -v : verbose mode of operation - """ % (VERSION,) - sys.exit(0) - -# Compile various important regexp patterns -import regex, regsub -# Starting <html> directive -htmlpat=regex.compile('^[ \t]*<html>[ \t]*$') -# Ending </html> directive -nohtmlpat=regex.compile('^[ \t]*</html>[ \t]*$') -# Match quoted text -quotedpat=regex.compile('^[>|:]+') -# Parenthesized human name -paren_name_pat=regex.compile('.*\([(].*[)]\).*') -# Subject lines preceded with 'Re:' -REpat=regex.compile('[ \t]*[Rr][Ee][ \t]*:[ \t]*') -# Lines in the configuration file: set pm_XXX = <something> -cfg_line_pat=regex.compile('^[ \t]*[sS][eE][tT][ \t]*[Pp][Mm]_\([a-zA-Z0-9]*\)' - '[ \t]*=[ \t]*\(.*\)[ \t\n]*$') -# E-mail addresses and URLs in text -emailpat=regex.compile('\([-+,.a-zA-Z0-9]*@[-+.a-zA-Z0-9]*\)') -urlpat=regex.compile('\([a-zA-Z0-9]+://[^ \t\n]+\)') # URLs in text -# Blank lines -blankpat=regex.compile('^[ \t\n]*$') - -def ReadCfgFile(prefs): - import posixpath - try: - f=open(posixpath.expanduser(prefs['CONFIGFILE']), 'r') - except IOError, (num, msg): - if num==2: return - else: raise IOError, (num, msg) - line=0 - while(1): - L=f.readline() ; line=line+1 - if L=="": break - if string.strip(L)=="": continue # Skip blank lines - match=cfg_line_pat.match(L) - if match==-1: - print "Syntax error in line %i of %s" %(line, prefs['CONFIGFILE']) - print L - else: - varname, value=cfg_line_pat.group(1,2) - varname=string.upper(varname) - if not prefs.has_key(varname): - print ("Unknown variable name %s in line %i of %s" - %(varname, line, prefs['CONFIGFILE'])) - print L - else: - prefs[varname]=eval(value) - f.close() - -def ReadEnvironment(prefs): - import sys, os - for key in prefs.keys(): - envvar=string.upper('PM_'+key) - if os.environ.has_key(envvar): - if type(prefs[key])==type(''): prefs[key]=os.environ[envvar] - else: prefs[key]=string.atoi(os.environ[envvar]) - -def UpdateMsgHeaders(prefs, filename, L): - """Update the next/previous message information in a message header. -The message is scanned for <!--next--> and <!--endnext--> comments, and -new pointers are written. Otherwise, the text is simply copied without any processing.""" - pass - -def ProcessMsgBody(prefs, msg, filename, articles): - """Transform one mail message from plain text to HTML. -This involves writing an HTML header, scanning through the text looking -for <html></html> directives, e-mail addresses, and URLs, and -finishing off with a footer.""" - import cgi, posixpath - outputname=posixpath.join(prefs['DIR'], filename) - output=open(outputname, 'w') - os.chmod(outputname, prefs['FILEMODE']) - subject, email, poster, date, datestr, parent, id = articles[filename] - # JV - if not email: - email = '' - if not subject: - subject = '<No subject>' - if not poster: - poster = '*Unknown*' - if not datestr: - datestr = '' - output.write('<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 3.0//EN">' - "<html><head><title>%s Mailing List: %s</title></head>" - "<body><h1>%s</h1>" - "%s (<i>%s</i>)<br><i>%s</i><p>" % - (prefs['LABEL'], cgi.escape(subject),cgi.escape(subject), - cgi.escape(poster),cgi.escape(email), - cgi.escape(datestr))) - output.write('<ul><li> <b>Messages sorted by:</b>' - '<a target="toc" href="date.html#1">[ date ]</a>' - '<a target="toc" href="thread.html#1">[ thread ]</a>' - '<a target="toc" href="subject.html#1">[ subject ]</a>' - '<a target="toc" href="author.html#1">[ author ]</a></ul>\n') - - html_mode=0 - if prefs['SHOWHR']: output.write('<hr>') - output.write('<p>') - if not prefs['SHOWHTML']: output.write('<pre>\n') - msg.rewindbody() # Seek to start of message body - quoted=-1 - while (1): - L=msg.fp.readline() - if L=="": break - if html_mode: - # If in HTML mode, check for ending tag; otherwise, we - # copy the line without any changes. - if nohtmlpat.match(L)==-1: - output.write(L) ; continue - else: - html_mode=0 - if not prefs['SHOWHTML']: output.write('<pre>\n') - continue - # Check for opening <html> tag - elif htmlpat.match(L)!=-1: - html_mode=1 - if not prefs['SHOWHTML']: output.write('</pre>\n') - continue - if prefs['SHOWHTML'] and prefs['IQUOTES']: - # Check for a line of quoted text and italicise it - # (We have to do this before escaping HTML special - # characters because '>' is commonly used.) - quoted=quotedpat.match(L) - if quoted!=-1: - L=cgi.escape(L[:quoted]) + '<i>' + cgi.escape(L[quoted:]) + '</i>' - # If we're flowing the message text together, quoted lines - # need explicit breaks, no matter what mode we're in. - if prefs['SHOWHTML']: L=L+'<br>' - else: L=cgi.escape(L) - else: L=cgi.escape(L) - - # Check for an e-mail address - L2="" ; i=emailpat.search(L) - while i!=-1: - length=len(emailpat.group(1)) - mailcmd=prefs['MAILCOMMAND'] % {'TO':L[i:i+length]} - L2=L2+'%s<A HREF="%s">%s</A>' % (L[:i], - mailcmd, L[i:i+length]) - L=L[i+length:] - i=emailpat.search(L) - L=L2+L ; L2=""; i=urlpat.search(L) - while i!=-1: - length=len(urlpat.group(1)) - L2=L2+'%s<A HREF="%s">%s</A>' % (L[:i], - L[i:i+length], L[i:i+length]) - L=L[i+length:] - i=urlpat.search(L) - L=L2+L - if prefs['SHOWHTML']: - while (L!="" and L[-1] in '\015\012'): L=L[:-1] - if prefs['SHOWBR']: - # We don't want to flow quoted passages - if quoted==-1: L=L+'<br>' - else: - # If we're not adding <br> to each line, we'll need to - # insert <p> markup on blank lines to separate paragraphs. - if blankpat.match(L)!=-1: L=L+'<p>' - L=L+'\n' - output.write(L) - - if not prefs['SHOWHTML'] and not html_mode: output.write('</pre>') - if prefs['SHOWHR']: output.write('<hr>') - output.write('<!--next-->\n<!--endnext-->\n</body></html>') - output.close() - -def WriteHTMLIndex(prefs, fp, L, articles, indexname): - """Process a list L into an HTML index, written to fp. -L is processed from left to right, and contains a list of 2-tuples; -an integer of 1 or more giving the depth of indentation, and -a list of strings which are used to reference the 'articles' -dictionary. Most of the time the lists contain only 1 element.""" - fp.write('<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 3.0//EN">\n' - "<html><head><base target=display>" - "<title>%s Mailing List Archive by %s</title></head><body>\n" - % (prefs['LABEL'], indexname)) - fp.write('<H1><A name="start">%s Mailing List Archive by %s</A></H1>' - '<ul><li> <b><a target="toc" href="#end">Most recent messages</a></b>' - '<li> <b>Messages sorted by:</b>' - % (prefs['LABEL'], indexname)) - if indexname!='Date': - fp.write('<a target="toc" href="date.html#start">[ date ]</a>') - if indexname!='Subject': - fp.write('<a target="toc" href="subject.html#start">[ subject ]</a>') - if indexname!='Author': - fp.write('<a target="toc" href="author.html#start">[ author ]</a>') - if indexname!='Thread': - fp.write('<a target="toc" href="thread.html#start">[ thread ]</a>') - if prefs['ARCHIVES']!='NONE': - fp.write('<li> <b><a href="%s">Other mail archives</a></b>' % - (prefs['ARCHIVES'],)) -# This doesn't look professional. -- JV -# mailcmd=prefs['MAILCOMMAND'] % {'TO':'amk@magnet.com'} -# fp.write('</ul><p>Please inform <A HREF="%s">amk@magnet.com</a> if any of the messages are formatted incorrectly.' % (mailcmd,) ) - - fp.write("<p><b>Starting:</b> <i>%s</i><br>" - "<b>Ending:</b> <i>%s</i><br><b>Messages:</b> %i<p>" - % (prefs['firstDate'], prefs['endDate'], len(L)) ) - - # Write the index - level=1 - fp.write('<ul>\n') - for indent, keys in L: - if indent>level and indent<=prefs['THRDLEVELS']: - fp.write((indent-level)*'<ul>'+'\n') - if indent<level: fp.write((level-indent)*'</ul>'+'\n') - level=indent - for j in keys: - subj, email, poster, date, datestr, parent, id=articles[j] - # XXX Should we put a mailto URL in here? - fp.write('<li> <A HREF="%s"><b>%s</b></a> <i>%s</i>\n' % - (j, subj, poster) ) - for i in range(0, indent): fp.write('</ul>') - fp.write('<p>') - - # Write the footer - import time - now=time.asctime(time.localtime(time.time())) - -# JV -- Fixed a bug here. - if prefs['ARCHIVES'] <> 'NONE': - otherstr=('<li> <b><a href="%s">Other mail archives</a></b>' % - (prefs['ARCHIVES'],) ) - else: otherstr="" - fp.write('<a name="end"><b>Last message date:</b></a> <i>%s</i><br>' - '<b>Archived on:</b> <i>%s</i><p><ul>' - '<li> <b>Messages sorted by:</b>' - '<a target="toc" href="date.html#start">[ date ]</a>' - '<a target="toc" href="subject.html#start">[ subject ]</a>' - '<a target="toc" href="author.html#start">[ author ]</a>' - '<a target="toc" href="thread.html#start">[ thread ]</a>' - '%s</ul><p>' % (prefs['endDate'], now, otherstr)) - - fp.write('<p><hr><i>This archive was generated by ' -# JV Updated the URL. - '<A HREF="http://www.magnet.com/~amk/python/pipermail.html">' - 'Pipermail %s</A>.</i></body></html>' % (VERSION,)) - -# Set the hard-wired preferences first -# JV Changed the SHOWHTML pref default to 0 because 1 looks bad. -prefs={'CONFIGFILE':'~/.pmrc', 'MBOX':'mbox', - 'ARCHIVES': 'NONE', 'ABOUT':'NONE', 'REVERSE':0, - 'SHOWHEADERS':0, 'SHOWHTML':0, 'LABEL':"", - 'DIR':'archive', 'DIRMODE':0755, - 'FILEMODE':0644, 'OVERWRITE':0, 'VERBOSE':0, - 'THRDLEVELS':3, 'SHOWBR':0, 'IQUOTES':1, - 'SHOWHR':1, 'MAILCOMMAND':'mailto:%(TO)s', - 'INDEXFILE':'NONE' -} - -# Read the ~/.pmrc file -ReadCfgFile(prefs) -# Read environment variables -ReadEnvironment(prefs) - -# Parse command-line options -import getopt -options, params=getopt.getopt(sys.argv[1:], 'a:b:c:d:l:m:s:uipvxzh?') -for option, value in options: - if option=='-a': prefs['ARCHIVES']=value - if option=='-b': prefs['ABOUT']=value - if option=='-c': prefs['CONFIGFILE']=value - if option=='-d': prefs['DIR']=value -# if option=='-f': prefs.frames=1 - if option=='-i': prefs['MBOX']='-' - if option=='-l': prefs['LABEL']=value - if option=='-m': prefs['MBOX']=value - if option=='-s': prefs['INDEXFILE']=value - if option=='-p' or option=='-v': prefs['VERBOSE']=1 - if option=='-x': prefs['OVERWRITE']=1 - if option=='-z' or option=='-h' or option=='-?': PrintUsage() - -# Set up various variables -articles={} ; sequence=0 -for key in ['INDEXFILE', 'MBOX', 'CONFIGFILE', 'DIR']: - prefs[key]=posixpath.expanduser(prefs[key]) - -if prefs['INDEXFILE']=='NONE': - if prefs['MBOX']!='-': - prefs['INDEXFILE']=prefs['MBOX']+'.pipermail' - else: prefs['INDEXFILE']='mbox.pipermail' - -# Read an index file, if one can be found -if not prefs['OVERWRITE']: - # Look for a file contained pickled state - import pickle - try: - if prefs['VERBOSE']: - print 'Attempting to read index file', prefs['INDEXFILE'] - f=open(prefs['INDEXFILE'], 'r') - articles, sequence =pickle.load(f) - f.close() - except IOError: - if prefs['VERBOSE']: print 'No index file found.' - pass # Ignore errors - -# Open the input file -if prefs['MBOX']=='-': prefs['MBOX']=sys.stdin -else: - if prefs['VERBOSE']: print 'Opening input file', prefs['MBOX'] - prefs['MBOX']=open(prefs['MBOX'], 'r') - -# Create the destination directory; if it already exists, we don't care -try: - os.mkdir(prefs['DIR'], prefs['DIRMODE']) - if prefs['VERBOSE']: print 'Directory %s created'%(prefs['DIR'],) -except os.error, (errno, errmsg): pass - -# Create various data structures: -# msgids maps Message-IDs to filenames. -# roots maps Subject lines to (date, filename) tuples, and is used to -# identify the oldest article with a given subject line for threading. - -msgids={} ; roots={} -for i in articles.keys(): - subject, email, poster, date, datestr, parent, id =articles[i] - if id: msgids[id]=i - if not roots.has_key(subject) or roots[subject]<date: - roots[subject]=(date, i) - -# Start processing the index -import mailbox -mbox=mailbox.UnixMailbox(prefs['MBOX']) -while (1): - m=mbox.next() - if not m: break - - filename='%04i.html' % (sequence,) - if prefs['VERBOSE']: sys.stdout.write("Processing "+filename+"\n") - # The apparently redundant str() actually catches the case where - # m.getheader() returns None. - subj=str(m.getheader('Subject')) - # Remove any number of 'Re:' prefixes from the subject line - while (1): - i=REpat.match(subj) - if i!=-1: subj=subj[i:] - else: break - # Locate an e-mail address - L=m.getheader('From') - # JV: sometimes there is no From header, so use the one from unixfrom. - if not L: - try: - L = string.split(m.unixfrom)[1] - except: - L = "***Unknown***" - email=None - i=emailpat.search(L) - if i!=-1: - length=emailpat.match(L[i:]) - email=L[i:i+length] - # Remove e-mail addresses inside angle brackets - poster=str(regsub.gsub('<.*>', '', L)) - # Check if there's a name in parentheses - i=paren_name_pat.match(poster) - if i!=-1: poster=paren_name_pat.group(1)[1:-1] - datestr=m.getheader('Date') - # JV -- Hacks to make the getdate work. - # These hacks might skew the post time a bit. - # Crude, but so far, effective. - words = string.split(datestr) - if ((len(words[-1]) == 4) and (len(words) == 5) - and (words[-1][:-1] == '199')): - try: - date = time.mktime(rfc822.parsedate('%s, %s %s %s %s' % - (words[0], words[2], words[1], - words[4], words[3]))) - except: - date = time.mktime(m.getdate('Date')) # Odd - elif len(words) > 4 and words[4][-1] == ',': - try: - date = time.mktime(rfc822.parsedate('%s, %s %s %s %s' % - (words[0], words[1], words[2], - words[3], words[4][:-1]))) - except: - date = time.mktime(m.getdate('Date')) # Hmm - else: - try: - date=time.mktime(m.getdate('Date')) - except: - print 'Error getting date!' - print 'Subject = ', m.getheader('subject') - print 'Date = ', m.getheader('date') - - id=m.getheader('Message-Id') - if id: id=id[1:-1] ; msgids[id]=filename - parent=None - in_reply_to=m.getheader('In-Reply-To') - if in_reply_to: - in_reply_to=in_reply_to[1:-1] - if msgids.has_key(in_reply_to): parent=msgids[in_reply_to] - elif roots.has_key(subj) and roots[subj][0]<date: - parent=roots[subj][1] - else: roots[subj]=(date,filename) - - articles[filename]=(subj, email, poster, date, datestr, parent, id) - ProcessMsgBody(prefs, m, filename, articles) - sequence=sequence+1 -prefs['MBOX'].close() - -if prefs['VERBOSE']: sys.stdout.write("Writing date index\n") -import time -indexname=posixpath.join(prefs['DIR'], 'date.html') -f=open(indexname, 'w') ; os.chmod(indexname, prefs['FILEMODE']) -dates=ListDict() -for i in articles.keys(): - subject, email, poster, date, datestr, parent, id=articles[i] - dates[date]=i -L=dates.keys() ; L.sort() -if prefs['REVERSE']: L.reverse() -prefs['firstDate']=time.asctime(time.localtime(L[0])) -prefs['endDate']=time.asctime(time.localtime(L[-1])) -L=map(lambda key, s=dates: (1,s[key]), L) -WriteHTMLIndex(prefs, f, L, articles, 'Date') -f.close() ; del dates, L - -if prefs['VERBOSE']: sys.stdout.write("Writing thread index\n") -indexname=posixpath.join(prefs['DIR'], 'thread.html') -f=open(indexname, 'w') ; os.chmod(indexname, prefs['FILEMODE']) -def DFS(p, N=None, depth=0, prefs=prefs): - set=filter(lambda x, N=N, p=p: p[x][1]==N, p.keys()) - set=map(lambda x, a=articles: (articles[x][3],x), set) - set.sort() - if prefs['REVERSE']: set.reverse() - set=map(lambda x: x[1], set) - if len(set)==0: return [(depth, [N])] - else: - L=[] - for i in set: - L=L+DFS(p, i, depth+1) - return [(depth,[N])]+L -parents={} -for i in articles.keys(): - subject, email, poster, date, datestr, parent, id=articles[i] - parents[i]=(date, parent) -L=DFS(parents)[1:] -WriteHTMLIndex(prefs, f, L, articles, 'Thread') -f.close() ; del L, parents - -if prefs['VERBOSE']: sys.stdout.write("Writing subject index\n") -indexname=posixpath.join(prefs['DIR'], 'subject.html') -f=open(indexname, 'w') ; os.chmod(indexname, prefs['FILEMODE']) -subjects=ListDict() -for i in articles.keys(): - subject, email, poster, date, datestr, parent, id=articles[i] - subjects[(subject, date)]=i -L=subjects.keys() ; L.sort() ; L=map(lambda key, s=subjects: (1, s[key]), L) -WriteHTMLIndex(prefs, f, L, articles, 'Subject') -f.close() ; del subjects, L - -if prefs['VERBOSE']: sys.stdout.write("Writing author index\n") -indexname=posixpath.join(prefs['DIR'], 'author.html') -f=open(indexname, 'w') ; os.chmod(indexname, prefs['FILEMODE']) -authors=ListDict() -for i in articles.keys(): - v=articles[i] - authors[(v[2],v[3])]=i -L=authors.keys() ; L.sort() ; L=map(lambda key, s=authors: (1,s[key]), L) -WriteHTMLIndex(prefs, f, L, articles, 'Author') -f.close() ; del authors, L - -if prefs['VERBOSE']: sys.stdout.write("Writing framed index\n") -f=open(posixpath.join(prefs['DIR'], 'blank.html'), 'w') -f.write("<html></html>") ; f.close() -# JV changed... -f=open(posixpath.join(prefs['DIR'], mm_cfg.HOME_PAGE), 'w') -f.write("""<html><head><title>%s Pipermail Archive</title> -<frameset cols="*, 60%%"> -<FRAME SRC="thread.html" NAME=toc> -<FRAME SRC="blank.html" NAME=display> -</frameset></head> -<body><noframes> -To access the %s Pipermail Archive, choose one of the following links: -<p> -Messages sorted by <a target="toc" href="date.html#start">[ date ] </a> -<a target="toc" href="subject.html#start">[ subject ]</a> -<a target="toc" href="author.html#start">[ author ]</a> -<a target="toc" href="thread.html#start">[ thread ]</a> -</ol> -</noframes> -</body></html> -""" % (prefs['LABEL'],prefs['LABEL']) ) - - -import pickle -if prefs['VERBOSE']: print 'Writing index file', prefs['INDEXFILE'] -f=open(prefs['INDEXFILE'], 'w') -pickle.dump( (articles, sequence), f ) -f.close() diff --git a/modules/runcgi.py b/modules/runcgi.py deleted file mode 100644 index d35c70ae4..000000000 --- a/modules/runcgi.py +++ /dev/null @@ -1,23 +0,0 @@ -from Mailman.debug import * - -def wrap_func(func, debug=1, print_env=1): - if not debug: - try: - sys.stderr = mm_utils.StampedLogger("error", label = 'admin', - manual_reprime=1, nofail=0) - except: - # Error opening log, show thru anyway! - wrap_func(func, print_env=print_env, debug=1) - return - func() - return - else: - try: - func() - except SystemExit: - pass - except: - print_trace() - if print_env: - print_environ() - diff --git a/modules/smtplib.py b/modules/smtplib.py deleted file mode 100644 index ad877f689..000000000 --- a/modules/smtplib.py +++ /dev/null @@ -1,126 +0,0 @@ -# Copyright (C) 1998 by the Free Software Foundation, Inc. -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - - -# A quick hack. Talk to the SMTP port. -# Right now this isn't very functional. -# A lot of functionality was borrowed directly from ftplib... -# John Viega (viega@list.org) - -# >>> from smtplib import * -# >>> s = SmtpConnection('list.org') -# >>> s.helo('adder.cs.virginia.edu') -# >>> s.send(to='viega@list.org', frm='jtv2j@cs.virginia.edu', text='hello, world!') -# >>> s.quit() - -from socket import * -import string, types - -SMTP_PORT = 25 - -CRLF = '\r\n' - -# Exception raised when an error or invalid response is received -error_reply = 'smtplib.error_reply' # unexpected [123]xx reply -error_temp = 'smtplib.error_temp' # 4xx errors -error_perm = 'smtplib.error_perm' # 5xx errors -error_proto = 'smtplib.error_proto' # response does not begin with [1-5] - -class SmtpConnection: - def __init__(self, host=''): - self.host = host - self._file = None - self.connect() - - def connect(self): - self._sock = socket(AF_INET, SOCK_STREAM) - self._sock.connect(self.host, SMTP_PORT) - self._file = self._sock.makefile('r') - self.getresp() - - def helo(self, host): - self._sock.send('HELO %s\r\n' % host) - self.getresp() - - def quit(self): - self._sock.send('QUIT\r\n') - self.getresp() - - # text should be \n at eol, we'll add the \r. - def send(self, to, frm, text, headers=None): - if headers: - hlines = string.split(headers, '\n') - lines = string.split(text, '\n') - self._sock.send('MAIL FROM: <%s>\r\n' % frm) - self.getresp() - if type(to) == types.StringType: - self._sock.send('RCPT TO: <%s>\r\n' % to) - self.getresp() - else: - for item in to: - self._sock.send('RCPT TO: <%s>\r\n' % item) - self.getresp() - self._sock.send('DATA\r\n') - self.getresp() - if headers: - for line in hlines: - self._sock.send(line + '\r\n') - self._sock.send('\r\n') - for line in lines: - if line == '.': line = '..' - self._sock.send(line + '\r\n') - self._sock.send('.\r\n') - self.getresp() - -# Private crap from here down. - def getline(self): - line = self._file.readline() - if not line: raise EOFError - if line[-2:] == CRLF: line = line[:-2] - elif line[-1:] in CRLF: line = line[:-1] - return line - - # Internal: get a response from the server, which may possibly - # consist of multiple lines. Return a single string with no - # trailing CRLF. If the response consists of multiple lines, - # these are separated by '\n' characters in the string - def getmultiline(self): - line = self.getline() - if line[3:4] == '-': - code = line[:3] - while 1: - nextline = self.getline() - line = line + ('\n' + nextline) - if nextline[:3] == code and \ - nextline[3:4] <> '-': - break - return line - - def getresp(self): - resp = self.getmultiline() - self.lastresp = resp[:3] - c = resp[:1] - if c == '4': - raise error_temp, resp - if c == '5': - raise error_perm, resp - if c not in '123': - raise error_proto, resp - return resp - - - - diff --git a/modules/versions.py b/modules/versions.py deleted file mode 100644 index 6b41725a2..000000000 --- a/modules/versions.py +++ /dev/null @@ -1,133 +0,0 @@ -# Copyright (C) 1998 by the Free Software Foundation, Inc. -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - - -"""Routines which rectify an old maillist with current maillist structure. - -The maillist .CheckVersion() method looks for an old .data_version -setting in the loaded maillist structure, and if found calls the -Update() routine from this module, supplying the list and the state -last loaded from storage. (Th state is necessary to distinguish from -default assignments done in the .InitVars() methods, before -.CheckVersion() is called.) - -For new versions you should add sections to the UpdateOldVars() and the -UpdateOldUsers() sections, to preserve the sense of settings across -structural changes. Note that the routines have only one pass - when -.CheckVersions() finds a version change it runs this routine and then -updates the data_version number of the list, and then does a .Save(), so -the transformations won't be run again until another version change is -detected.""" - -__version__ = "$Revision: 721 $" - -import re, string, types -import mm_cfg - -def Update(l, stored_state): - "Dispose of old vars and user options, mapping to new ones when suitable." - # No worry about entirely new vars because InitVars() takes care of them. - UpdateOldVars(l, stored_state) - UpdateOldUsers(l) - -def UpdateOldVars(l, stored_state): - """Transform old variable values into new ones, deleting old ones. - stored_state is last snapshot from file, as opposed to from InitVars().""" - - def PreferStored(oldname, newname, l=l, state=stored_state): - "Use specified value if new value does not come from stored state." - if hasattr(l, oldname): - if not state.has_key(newname): - setattr(l, newname, getattr(l, oldname)) - delattr(l, oldname) - - # Pre 1.0b1.2, klm 04/11/1998. - # - migrated vars: - PreferStored('auto_subscribe', 'open_subscribe') - PreferStored('closed', 'private_roster') - PreferStored('mimimum_post_count_before_removal', - 'mimimum_post_count_before_bounce_action') - PreferStored('bad_posters', 'forbidden_posters') - PreferStored('automatically_remove', 'automatic_bounce_action') - # - dropped vars: - for a in ['archive_retain_text_copy', - 'archive_update_frequency', - 'archive_volume_frequency']: - if hasattr(l, a): delattr(l, a) - -def UpdateOldUsers(l): - """Transform sense of changed user options.""" - if older(l.data_version, "1.0b1.2"): - # Mime-digest bitfield changed from Enable to Disable after 1.0b1.1. - for m in l.members + l.digest_members: - was = l.GetUserOption(m, mm_cfg.DisableMime) - l.SetUserOption(m, mm_cfg.DisableMime, not was) - -def older(version, reference): - """True if version is older than current. - - Different numbering systems imply version is older.""" - if type(version) != type(reference): - return 1 - if version >= reference: - return 0 - else: - return 1 - # Iterate over the repective contiguous sections of letters and digits - # until a section from the reference is found to be different than the - # corresponding version section, and return the sense of the - # difference. If no differences are found, then 0 is returned. - #for v, r in map(None, section(version), section(reference)): - # if r == None: - # Reference is a full release and version is an interim - eg, - # alpha or beta - which precede full, are older: - # return 1 - # if type(v) != type(r): - # # Numbering system changed. - # return 1 - # if v < r: - # return 1 - # if v > r: - # return 0 - # return 0 - -#def section(s): -# """Split string into contiguous sequences of letters and digits.""" -# section = "" -# got = [] -# wasat = "" -# for c in s: -# if c in string.letters: -# at = string.letters; add = c -# elif c in string.digits: -# at = string.digits; add = c -# else: -# at = ""; add = "" -# -# if at == wasat: # In continuous sequence. -# section = section + add -# else: # Switching. -# if section: -# if wasat == string.digits: -# section = int(section) -# got.append(section) -# section = add -# wasat = at -# if section: # Get trailing stuff. -# if wasat == string.digits: -# section = int(section) -# got.append(section) -# return got |
