summaryrefslogtreecommitdiff
path: root/modules
diff options
context:
space:
mode:
authorbwarsaw1998-06-19 19:32:48 +0000
committerbwarsaw1998-06-19 19:32:48 +0000
commit99f721f65906e4f2d1036da3a886426aa0ec5aea (patch)
tree93ffed3285a375b2f9766715fe61b03cd3aafd78 /modules
parent664f1baa491de8a96d859f28b73aca877ce23f14 (diff)
downloadmailman-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/.cvsignore4
-rw-r--r--modules/Makefile.in89
-rw-r--r--modules/__init__.py0
-rw-r--r--modules/aliases.py57
-rw-r--r--modules/debug.py48
-rw-r--r--modules/flock.py101
-rw-r--r--modules/htmlformat.py468
-rw-r--r--modules/maillist.py922
-rw-r--r--modules/mm_admin.py234
-rw-r--r--modules/mm_archive.py161
-rw-r--r--modules/mm_bouncer.py416
-rw-r--r--modules/mm_cfg.py.in59
-rw-r--r--modules/mm_crypt.py8
-rw-r--r--modules/mm_defaults.py.in224
-rw-r--r--modules/mm_deliver.py249
-rw-r--r--modules/mm_digest.py408
-rw-r--r--modules/mm_err.py70
-rw-r--r--modules/mm_gateway.py142
-rw-r--r--modules/mm_html.py336
-rw-r--r--modules/mm_mailcmd.py607
-rw-r--r--modules/mm_mbox.py38
-rw-r--r--modules/mm_message.py268
-rw-r--r--modules/mm_pending.py71
-rw-r--r--modules/mm_security.py96
-rw-r--r--modules/mm_utils.py506
-rw-r--r--modules/pipermail.py560
-rw-r--r--modules/runcgi.py23
-rw-r--r--modules/smtplib.py126
-rw-r--r--modules/versions.py133
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 &lt;br&gt; - 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')
- + "&nbsp;&nbsp;")
- 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] = '&lt;'
- elif arr[i] == '>':
- arr[i] = '&gt;'
- elif arr[i] == '"':
- arr[i] = '&quot;'
- else: #if arr[i] == '&':
- arr[i] = '&amp;'
- 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