From c0522afd1754c7a18c40c9ebaa6c2ef406929170 Mon Sep 17 00:00:00 2001
From: Barry Warsaw
Date: Thu, 1 Jan 2009 21:04:08 -0500
Subject: move directory
---
mailman/MTA/Manual.py | 139 ----------------
mailman/MTA/Postfix.py | 411 ------------------------------------------------
mailman/MTA/Utils.py | 87 ----------
mailman/MTA/__init__.py | 0
mailman/mta/Manual.py | 139 ++++++++++++++++
mailman/mta/Postfix.py | 411 ++++++++++++++++++++++++++++++++++++++++++++++++
mailman/mta/Utils.py | 87 ++++++++++
mailman/mta/__init__.py | 0
8 files changed, 637 insertions(+), 637 deletions(-)
delete mode 100644 mailman/MTA/Manual.py
delete mode 100644 mailman/MTA/Postfix.py
delete mode 100644 mailman/MTA/Utils.py
delete mode 100644 mailman/MTA/__init__.py
create mode 100644 mailman/mta/Manual.py
create mode 100644 mailman/mta/Postfix.py
create mode 100644 mailman/mta/Utils.py
create mode 100644 mailman/mta/__init__.py
diff --git a/mailman/MTA/Manual.py b/mailman/MTA/Manual.py
deleted file mode 100644
index d0e7c359a..000000000
--- a/mailman/MTA/Manual.py
+++ /dev/null
@@ -1,139 +0,0 @@
-# Copyright (C) 2001-2009 by the Free Software Foundation, Inc.
-#
-# This file is part of GNU Mailman.
-#
-# GNU Mailman is free software: you can redistribute it and/or modify it under
-# the terms of the GNU General Public License as published by the Free
-# Software Foundation, either version 3 of the License, or (at your option)
-# any later version.
-#
-# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-# more details.
-#
-# You should have received a copy of the GNU General Public License along with
-# GNU Mailman. If not, see .
-
-"""Creation/deletion hooks for manual /etc/aliases files."""
-
-import sys
-import email.Utils
-
-from cStringIO import StringIO
-
-from mailman import Message
-from mailman import Utils
-from mailman.MTA.Utils import makealiases
-from mailman.configuration import config
-from mailman.i18n import _
-from mailman.queue import Switchboard
-
-
-
-# no-ops for interface compliance
-def makelock():
- class Dummy:
- def lock(self):
- pass
- def unlock(self, unconditionally=False):
- pass
- return Dummy()
-
-
-def clear():
- pass
-
-
-
-# nolock argument is ignored, but exists for interface compliance
-def create(mlist, cgi=False, nolock=False, quiet=False):
- if mlist is None:
- return
- listname = mlist.internal_name()
- fieldsz = len(listname) + len('-unsubscribe')
- if cgi:
- # If a list is being created via the CGI, the best we can do is send
- # an email message to mailman-owner requesting that the proper aliases
- # be installed.
- sfp = StringIO()
- if not quiet:
- print >> sfp, _("""\
-The mailing list '$listname' has been created via the through-the-web
-interface. In order to complete the activation of this mailing list, the
-proper /etc/aliases (or equivalent) file must be updated. The program
-'newaliases' may also have to be run.
-
-Here are the entries for the /etc/aliases file:
-""")
- outfp = sfp
- else:
- if not quiet:
- print _("""\
-To finish creating your mailing list, you must edit your /etc/aliases (or
-equivalent) file by adding the following lines, and possibly running the
-'newaliases' program:
-""")
- print _("""\
-## $listname mailing list""")
- outfp = sys.stdout
- # Common path
- for k, v in makealiases(mlist):
- print >> outfp, k + ':', ((fieldsz - len(k)) * ' '), v
- # If we're using the command line interface, we're done. For ttw, we need
- # to actually send the message to mailman-owner now.
- if not cgi:
- print >> outfp
- return
- siteowner = Utils.get_site_noreply()
- # Should this be sent in the site list's preferred language?
- msg = Message.UserNotification(
- siteowner, siteowner,
- _('Mailing list creation request for list $listname'),
- sfp.getvalue(), config.DEFAULT_SERVER_LANGUAGE)
- msg.send(mlist)
-
-
-
-def remove(mlist, cgi=False):
- listname = mlist.fqdn_listname
- fieldsz = len(listname) + len('-unsubscribe')
- if cgi:
- # If a list is being removed via the CGI, the best we can do is send
- # an email message to mailman-owner requesting that the appropriate
- # aliases be deleted.
- sfp = StringIO()
- print >> sfp, _("""\
-The mailing list '$listname' has been removed via the through-the-web
-interface. In order to complete the de-activation of this mailing list, the
-appropriate /etc/aliases (or equivalent) file must be updated. The program
-'newaliases' may also have to be run.
-
-Here are the entries in the /etc/aliases file that should be removed:
-""")
- outfp = sfp
- else:
- print _("""
-To finish removing your mailing list, you must edit your /etc/aliases (or
-equivalent) file by removing the following lines, and possibly running the
-'newaliases' program:
-
-## $listname mailing list""")
- outfp = sys.stdout
- # Common path
- for k, v in makealiases(mlist):
- print >> outfp, k + ':', ((fieldsz - len(k)) * ' '), v
- # If we're using the command line interface, we're done. For ttw, we need
- # to actually send the message to mailman-owner now.
- if not cgi:
- print >> outfp
- return
- siteowner = Utils.get_site_noreply()
- # Should this be sent in the site list's preferred language?
- msg = Message.UserNotification(
- siteowner, siteowner,
- _('Mailing list removal request for list $listname'),
- sfp.getvalue(), config.DEFAULT_SERVER_LANGUAGE)
- msg['Date'] = email.Utils.formatdate(localtime=True)
- outq = Switchboard(config.OUTQUEUE_DIR)
- outq.enqueue(msg, recips=[siteowner], nodecorate=True)
diff --git a/mailman/MTA/Postfix.py b/mailman/MTA/Postfix.py
deleted file mode 100644
index 901c21089..000000000
--- a/mailman/MTA/Postfix.py
+++ /dev/null
@@ -1,411 +0,0 @@
-# Copyright (C) 2001-2009 by the Free Software Foundation, Inc.
-#
-# This file is part of GNU Mailman.
-#
-# GNU Mailman is free software: you can redistribute it and/or modify it under
-# the terms of the GNU General Public License as published by the Free
-# Software Foundation, either version 3 of the License, or (at your option)
-# any later version.
-#
-# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-# more details.
-#
-# You should have received a copy of the GNU General Public License along with
-# GNU Mailman. If not, see .
-
-"""Creation/deletion hooks for the Postfix MTA."""
-
-import os
-import grp
-import pwd
-import time
-import errno
-import logging
-
-from locknix.lockfile import Lock
-from stat import *
-
-from mailman import Utils
-from mailman.MTA.Utils import makealiases
-from mailman.configuration import config
-from mailman.i18n import _
-
-LOCKFILE = os.path.join(config.LOCK_DIR, 'creator')
-ALIASFILE = os.path.join(config.DATA_DIR, 'aliases')
-VIRTFILE = os.path.join(config.DATA_DIR, 'virtual-mailman')
-TRPTFILE = os.path.join(config.DATA_DIR, 'transport')
-
-log = logging.getLogger('mailman.error')
-
-
-
-def _update_maps():
- msg = 'command failed: %s (status: %s, %s)'
- if config.USE_LMTP:
- tcmd = config.POSTFIX_MAP_CMD + ' ' + TRPTFILE
- status = (os.system(tcmd) >> 8) & 0xff
- if status:
- errstr = os.strerror(status)
- log.error(msg, tcmd, status, errstr)
- raise RuntimeError(msg % (tcmd, status, errstr))
- acmd = config.POSTFIX_ALIAS_CMD + ' ' + ALIASFILE
- status = (os.system(acmd) >> 8) & 0xff
- if status:
- errstr = os.strerror(status)
- log.error(msg, acmd, status, errstr)
- raise RuntimeError(msg % (acmd, status, errstr))
- if os.path.exists(VIRTFILE):
- vcmd = config.POSTFIX_MAP_CMD + ' ' + VIRTFILE
- status = (os.system(vcmd) >> 8) & 0xff
- if status:
- errstr = os.strerror(status)
- log.error(msg, vcmd, status, errstr)
- raise RuntimeError(msg % (vcmd, status, errstr))
-
-
-
-def _zapfile(filename):
- # Truncate the file w/o messing with the file permissions, but only if it
- # already exists.
- if os.path.exists(filename):
- fp = open(filename, 'w')
- fp.close()
-
-
-def clear():
- _zapfile(ALIASFILE)
- _zapfile(VIRTFILE)
- _zapfile(TRPTFILE)
-
-
-
-def _addlist(mlist, fp):
- # Set up the mailman-loop address
- loopaddr = Utils.ParseEmail(Utils.get_site_noreply())[0]
- loopmbox = os.path.join(config.DATA_DIR, 'owner-bounces.mbox')
- # Seek to the end of the text file, but if it's empty write the standard
- # disclaimer, and the loop catch address.
- fp.seek(0, 2)
- if not fp.tell():
- print >> fp, """\
-# This file is generated by Mailman, and is kept in sync with the
-# binary hash file aliases.db. YOU SHOULD NOT MANUALLY EDIT THIS FILE
-# unless you know what you're doing, and can keep the two files properly
-# in sync. If you screw it up, you're on your own.
-"""
- print >> fp, '# The ultimate loop stopper address'
- print >> fp, '%s: %s' % (loopaddr, loopmbox)
- print >> fp
- # Bootstrapping. bin/genaliases must be run before any lists are created,
- # but if no lists exist yet then mlist is None. The whole point of the
- # exercise is to get the minimal aliases.db file into existance.
- if mlist is None:
- return
- listname = mlist.internal_name()
- hostname = mlist.host_name
- fieldsz = len(listname) + len('-unsubscribe')
- # The text file entries get a little extra info
- print >> fp, '# STANZA START: %s@%s' % (listname, hostname)
- print >> fp, '# CREATED:', time.ctime(time.time())
- # Now add all the standard alias entries
- for k, v in makealiases(mlist):
- l = len(k)
- if hostname in config.POSTFIX_STYLE_VIRTUAL_DOMAINS:
- k += config.POSTFIX_VIRTUAL_SEPARATOR + hostname
- # Format the text file nicely
- print >> fp, k + ':', ((fieldsz - l) * ' ') + v
- # Finish the text file stanza
- print >> fp, '# STANZA END: %s@%s' % (listname, hostname)
- print >> fp
-
-
-
-def _addvirtual(mlist, fp):
- listname = mlist.internal_name()
- fieldsz = len(listname) + len('-unsubscribe')
- hostname = mlist.host_name
- # Set up the mailman-loop address
- loopaddr = mlist.no_reply_address
- loopdest = Utils.ParseEmail(loopaddr)[0]
- # Seek to the end of the text file, but if it's empty write the standard
- # disclaimer, and the loop catch address.
- fp.seek(0, 2)
- if not fp.tell():
- print >> fp, """\
-# This file is generated by Mailman, and is kept in sync with the binary hash
-# file virtual-mailman.db. YOU SHOULD NOT MANUALLY EDIT THIS FILE unless you
-# know what you're doing, and can keep the two files properly in sync. If you
-# screw it up, you're on your own.
-#
-# Note that you should already have this virtual domain set up properly in
-# your Postfix installation. See README.POSTFIX for details.
-
-# LOOP ADDRESSES START
-%s\t%s
-# LOOP ADDRESSES END
-""" % (loopaddr, loopdest)
- # The text file entries get a little extra info
- print >> fp, '# STANZA START: %s@%s' % (listname, hostname)
- print >> fp, '# CREATED:', time.ctime(time.time())
- # Now add all the standard alias entries
- for k, v in makealiases(mlist):
- fqdnaddr = '%s@%s' % (k, hostname)
- l = len(k)
- # Format the text file nicely
- if hostname in config.POSTFIX_STYLE_VIRTUAL_DOMAINS:
- k += config.POSTFIX_VIRTUAL_SEPARATOR + hostname
- print >> fp, fqdnaddr, ((fieldsz - l) * ' '), k
- # Finish the text file stanza
- print >> fp, '# STANZA END: %s@%s' % (listname, hostname)
- print >> fp
-
-
-
-# Blech.
-def _check_for_virtual_loopaddr(mlist, filename, func):
- loopaddr = mlist.no_reply_address
- loopdest = Utils.ParseEmail(loopaddr)[0]
- if func is _addtransport:
- loopdest = 'local:' + loopdest
- infp = open(filename)
- outfp = open(filename + '.tmp', 'w')
- try:
- # Find the start of the loop address block
- while True:
- line = infp.readline()
- if not line:
- break
- outfp.write(line)
- if line.startswith('# LOOP ADDRESSES START'):
- break
- # Now see if our domain has already been written
- while True:
- line = infp.readline()
- if not line:
- break
- if line.startswith('# LOOP ADDRESSES END'):
- # It hasn't
- print >> outfp, '%s\t%s' % (loopaddr, loopdest)
- outfp.write(line)
- break
- elif line.startswith(loopaddr):
- # We just found it
- outfp.write(line)
- break
- else:
- # This isn't our loop address, so spit it out and continue
- outfp.write(line)
- outfp.writelines(infp.readlines())
- finally:
- infp.close()
- outfp.close()
- os.rename(filename + '.tmp', filename)
-
-
-
-def _addtransport(mlist, fp):
- # Set up the mailman-loop address
- loopaddr = mlist.no_reply_address
- loopdest = Utils.ParseEmail(loopaddr)[0]
- # create/add postfix transport file for mailman
- fp.seek(0, 2)
- if not fp.tell():
- print >> fp, """\
-# This file is generated by Mailman, and is kept in sync with the
-# binary hash file transport.db. YOU SHOULD NOT MANUALLY EDIT THIS FILE
-# unless you know what you're doing, and can keep the two files properly
-# in sync. If you screw it up, you're on your own.
-
-# LOOP ADDRESSES START
-%s\tlocal:%s
-# LOOP ADDRESSES END
-""" % (loopaddr, loopdest)
- # List LMTP_ONLY_DOMAINS
- if config.LMTP_ONLY_DOMAINS:
- print >> fp, '# LMTP ONLY DOMAINS START'
- for dom in config.LMTP_ONLY_DOMAINS:
- print >> fp, '%s\tlmtp:%s:%s' % (dom,
- config.LMTP_HOST,
- config.LMTP_PORT)
- print >> fp, '# LMTP ONLY DOMAINS END\n'
- listname = mlist.internal_name()
- hostname = mlist.host_name
- # No need of individual local part if the domain is LMTP only
- if hostname in config.LMTP_ONLY_DOMAINS:
- return
- fieldsz = len(listname) + len(hostname) + len('-unsubscribe') + 1
- # The text file entries get a little extra info
- print >> fp, '# STANZA START: %s@%s' % (listname, hostname)
- print >> fp, '# CREATED:', time.ctime(time.time())
- # Now add transport entries
- for k, v in makealiases(mlist):
- l = len(k + hostname) + 1
- print >> fp, '%s@%s' % (k, hostname), ((fieldsz - l) * ' ')\
- + 'lmtp:%s:%s' % (config.LMTP_HOST, config.LMTP_PORT)
- #
- print >> fp, '# STANZA END: %s@%s' % (listname, hostname)
- print >> fp
-
-
-
-def _do_create(mlist, textfile, func):
- # Crack open the plain text file
- try:
- fp = open(textfile, 'r+')
- except IOError, e:
- if e.errno <> errno.ENOENT:
- raise
- fp = open(textfile, 'w+')
- try:
- func(mlist, fp)
- finally:
- fp.close()
- # Now double check the virtual plain text file
- if func in (_addvirtual, _addtransport):
- _check_for_virtual_loopaddr(mlist, textfile, func)
-
-
-def create(mlist, cgi=False, nolock=False, quiet=False):
- # Acquire the global list database lock. quiet flag is ignored.
- lock = None
- if not nolock:
- # XXX FIXME
- lock = makelock()
- lock.lock()
- # Do the aliases file, which always needs to be done
- try:
- if config.USE_LMTP:
- _do_create(mlist, TRPTFILE, _addtransport)
- _do_create(None, ALIASFILE, _addlist)
- else:
- _do_create(mlist, ALIASFILE, _addlist)
- if mlist.host_name in config.POSTFIX_STYLE_VIRTUAL_DOMAINS:
- _do_create(mlist, VIRTFILE, _addvirtual)
- _update_maps()
- finally:
- if lock:
- lock.unlock(unconditionally=True)
-
-
-
-def _do_remove(mlist, textfile):
- listname = mlist.internal_name()
- hostname = mlist.host_name
- # Now do our best to filter out the proper stanza from the text file.
- # The text file better exist!
- outfp = None
- try:
- infp = open(textfile)
- except IOError, e:
- if e.errno <> errno.ENOENT:
- raise
- # Otherwise, there's no text file to filter so we're done.
- return
- try:
- outfp = open(textfile + '.tmp', 'w')
- filteroutp = False
- start = '# STANZA START: %s@%s' % (listname, hostname)
- end = '# STANZA END: %s@%s' % (listname, hostname)
- while 1:
- line = infp.readline()
- if not line:
- break
- # If we're filtering out a stanza, just look for the end marker and
- # filter out everything in between. If we're not in the middle of
- # filtering out a stanza, we're just looking for the proper begin
- # marker.
- if filteroutp:
- if line.strip() == end:
- filteroutp = False
- # Discard the trailing blank line, but don't worry if
- # we're at the end of the file.
- infp.readline()
- # Otherwise, ignore the line
- else:
- if line.strip() == start:
- # Filter out this stanza
- filteroutp = True
- else:
- outfp.write(line)
- # Close up shop, and rotate the files
- finally:
- infp.close()
- outfp.close()
- os.rename(textfile+'.tmp', textfile)
-
-
-def remove(mlist, cgi=False):
- # Acquire the global list database lock
- with Lock(LOCKFILE):
- if config.USE_LMTP:
- _do_remove(mlist, TRPTFILE)
- else:
- _do_remove(mlist, ALIASFILE)
- if mlist.host_name in config.POSTFIX_STYLE_VIRTUAL_DOMAINS:
- _do_remove(mlist, VIRTFILE)
- # Regenerate the alias and map files
- _update_maps()
- config.db.commit()
-
-
-
-def checkperms(state):
- targetmode = S_IFREG | S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP
- for file in ALIASFILE, VIRTFILE, TRPTFILE:
- if state.VERBOSE:
- print _('checking permissions on %(file)s')
- stat = None
- try:
- stat = os.stat(file)
- except OSError, e:
- if e.errno <> errno.ENOENT:
- raise
- if stat and (stat[ST_MODE] & targetmode) <> targetmode:
- state.ERRORS += 1
- octmode = oct(stat[ST_MODE])
- print _('%(file)s permissions must be 066x (got %(octmode)s)'),
- if state.FIX:
- print _('(fixing)')
- os.chmod(file, stat[ST_MODE] | targetmode)
- else:
- print
- # Make sure the corresponding .db files are owned by the Mailman user.
- # We don't need to check the group ownership of the file, since
- # check_perms checks this itself.
- dbfile = file + '.db'
- stat = None
- try:
- stat = os.stat(dbfile)
- except OSError, e:
- if e.errno <> errno.ENOENT:
- raise
- continue
- if state.VERBOSE:
- print _('checking ownership of %(dbfile)s')
- user = config.MAILMAN_USER
- ownerok = stat[ST_UID] == pwd.getpwnam(user)[2]
- if not ownerok:
- try:
- owner = pwd.getpwuid(stat[ST_UID])[0]
- except KeyError:
- owner = 'uid %d' % stat[ST_UID]
- print _('%(dbfile)s owned by %(owner)s (must be owned by %(user)s'),
- state.ERRORS += 1
- if state.FIX:
- print _('(fixing)')
- uid = pwd.getpwnam(user)[2]
- gid = grp.getgrnam(config.MAILMAN_GROUP)[2]
- os.chown(dbfile, uid, gid)
- else:
- print
- if stat and (stat[ST_MODE] & targetmode) <> targetmode:
- state.ERRORS += 1
- octmode = oct(stat[ST_MODE])
- print _('%(dbfile)s permissions must be 066x (got %(octmode)s)'),
- if state.FIX:
- print _('(fixing)')
- os.chmod(dbfile, stat[ST_MODE] | targetmode)
- else:
- print
diff --git a/mailman/MTA/Utils.py b/mailman/MTA/Utils.py
deleted file mode 100644
index bebbc69b7..000000000
--- a/mailman/MTA/Utils.py
+++ /dev/null
@@ -1,87 +0,0 @@
-# Copyright (C) 2001-2009 by the Free Software Foundation, Inc.
-#
-# This file is part of GNU Mailman.
-#
-# GNU Mailman is free software: you can redistribute it and/or modify it under
-# the terms of the GNU General Public License as published by the Free
-# Software Foundation, either version 3 of the License, or (at your option)
-# any later version.
-#
-# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-# more details.
-#
-# You should have received a copy of the GNU General Public License along with
-# GNU Mailman. If not, see .
-
-"""Utilities for list creation/deletion hooks."""
-
-import os
-import pwd
-
-from mailman.configuration import config
-
-
-
-def getusername():
- username = os.environ.get('USER') or os.environ.get('LOGNAME')
- if not username:
- import pwd
- username = pwd.getpwuid(os.getuid())[0]
- if not username:
- username = ''
- return username
-
-
-
-def _makealiases_mailprog(mlist):
- wrapper = os.path.join(config.WRAPPER_DIR, 'mailman')
- # Most of the list alias extensions are quite regular. I.e. if the
- # message is delivered to listname-foobar, it will be filtered to a
- # program called foobar. There are two exceptions:
- #
- # 1) Messages to listname (no extension) go to the post script.
- # 2) Messages to listname-admin go to the bounces script. This is for
- # backwards compatibility and may eventually go away (we really have no
- # need for the -admin address anymore).
- #
- # Seed this with the special cases.
- listname = mlist.internal_name()
- fqdn_listname = mlist.fqdn_listname
- aliases = [
- (listname, '"|%s post %s"' % (wrapper, fqdn_listname)),
- ]
- for ext in ('admin', 'bounces', 'confirm', 'join', 'leave', 'owner',
- 'request', 'subscribe', 'unsubscribe'):
- aliases.append(('%s-%s' % (listname, ext),
- '"|%s %s %s"' % (wrapper, ext, fqdn_listname)))
- return aliases
-
-
-
-def _makealiases_maildir(mlist):
- maildir = config.MAILDIR_DIR
- listname = mlist.internal_name()
- fqdn_listname = mlist.fqdn_listname
- if not maildir.endswith('/'):
- maildir += '/'
- # Deliver everything using maildir style. This way there's no mail
- # program, no forking and no wrapper necessary!
- #
- # Note, don't use this unless your MTA leaves the envelope recipient in
- # Delivered-To:, Envelope-To:, or Apparently-To:
- aliases = [(listname, maildir)]
- for ext in ('admin', 'bounces', 'confirm', 'join', 'leave', 'owner',
- 'request', 'subscribe', 'unsubscribe'):
- aliases.append(('%s-%s' % (listname, ext), maildir))
- return aliases
-
-
-
-# XXX This won't work if Mailman.MTA.Utils is imported before the
-# configuration is loaded.
-if config.USE_MAILDIR:
- makealiases = _makealiases_maildir
-else:
- makealiases = _makealiases_mailprog
diff --git a/mailman/MTA/__init__.py b/mailman/MTA/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/mailman/mta/Manual.py b/mailman/mta/Manual.py
new file mode 100644
index 000000000..d0e7c359a
--- /dev/null
+++ b/mailman/mta/Manual.py
@@ -0,0 +1,139 @@
+# Copyright (C) 2001-2009 by the Free Software Foundation, Inc.
+#
+# This file is part of GNU Mailman.
+#
+# GNU Mailman is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option)
+# any later version.
+#
+# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+# more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# GNU Mailman. If not, see .
+
+"""Creation/deletion hooks for manual /etc/aliases files."""
+
+import sys
+import email.Utils
+
+from cStringIO import StringIO
+
+from mailman import Message
+from mailman import Utils
+from mailman.MTA.Utils import makealiases
+from mailman.configuration import config
+from mailman.i18n import _
+from mailman.queue import Switchboard
+
+
+
+# no-ops for interface compliance
+def makelock():
+ class Dummy:
+ def lock(self):
+ pass
+ def unlock(self, unconditionally=False):
+ pass
+ return Dummy()
+
+
+def clear():
+ pass
+
+
+
+# nolock argument is ignored, but exists for interface compliance
+def create(mlist, cgi=False, nolock=False, quiet=False):
+ if mlist is None:
+ return
+ listname = mlist.internal_name()
+ fieldsz = len(listname) + len('-unsubscribe')
+ if cgi:
+ # If a list is being created via the CGI, the best we can do is send
+ # an email message to mailman-owner requesting that the proper aliases
+ # be installed.
+ sfp = StringIO()
+ if not quiet:
+ print >> sfp, _("""\
+The mailing list '$listname' has been created via the through-the-web
+interface. In order to complete the activation of this mailing list, the
+proper /etc/aliases (or equivalent) file must be updated. The program
+'newaliases' may also have to be run.
+
+Here are the entries for the /etc/aliases file:
+""")
+ outfp = sfp
+ else:
+ if not quiet:
+ print _("""\
+To finish creating your mailing list, you must edit your /etc/aliases (or
+equivalent) file by adding the following lines, and possibly running the
+'newaliases' program:
+""")
+ print _("""\
+## $listname mailing list""")
+ outfp = sys.stdout
+ # Common path
+ for k, v in makealiases(mlist):
+ print >> outfp, k + ':', ((fieldsz - len(k)) * ' '), v
+ # If we're using the command line interface, we're done. For ttw, we need
+ # to actually send the message to mailman-owner now.
+ if not cgi:
+ print >> outfp
+ return
+ siteowner = Utils.get_site_noreply()
+ # Should this be sent in the site list's preferred language?
+ msg = Message.UserNotification(
+ siteowner, siteowner,
+ _('Mailing list creation request for list $listname'),
+ sfp.getvalue(), config.DEFAULT_SERVER_LANGUAGE)
+ msg.send(mlist)
+
+
+
+def remove(mlist, cgi=False):
+ listname = mlist.fqdn_listname
+ fieldsz = len(listname) + len('-unsubscribe')
+ if cgi:
+ # If a list is being removed via the CGI, the best we can do is send
+ # an email message to mailman-owner requesting that the appropriate
+ # aliases be deleted.
+ sfp = StringIO()
+ print >> sfp, _("""\
+The mailing list '$listname' has been removed via the through-the-web
+interface. In order to complete the de-activation of this mailing list, the
+appropriate /etc/aliases (or equivalent) file must be updated. The program
+'newaliases' may also have to be run.
+
+Here are the entries in the /etc/aliases file that should be removed:
+""")
+ outfp = sfp
+ else:
+ print _("""
+To finish removing your mailing list, you must edit your /etc/aliases (or
+equivalent) file by removing the following lines, and possibly running the
+'newaliases' program:
+
+## $listname mailing list""")
+ outfp = sys.stdout
+ # Common path
+ for k, v in makealiases(mlist):
+ print >> outfp, k + ':', ((fieldsz - len(k)) * ' '), v
+ # If we're using the command line interface, we're done. For ttw, we need
+ # to actually send the message to mailman-owner now.
+ if not cgi:
+ print >> outfp
+ return
+ siteowner = Utils.get_site_noreply()
+ # Should this be sent in the site list's preferred language?
+ msg = Message.UserNotification(
+ siteowner, siteowner,
+ _('Mailing list removal request for list $listname'),
+ sfp.getvalue(), config.DEFAULT_SERVER_LANGUAGE)
+ msg['Date'] = email.Utils.formatdate(localtime=True)
+ outq = Switchboard(config.OUTQUEUE_DIR)
+ outq.enqueue(msg, recips=[siteowner], nodecorate=True)
diff --git a/mailman/mta/Postfix.py b/mailman/mta/Postfix.py
new file mode 100644
index 000000000..901c21089
--- /dev/null
+++ b/mailman/mta/Postfix.py
@@ -0,0 +1,411 @@
+# Copyright (C) 2001-2009 by the Free Software Foundation, Inc.
+#
+# This file is part of GNU Mailman.
+#
+# GNU Mailman is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option)
+# any later version.
+#
+# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+# more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# GNU Mailman. If not, see .
+
+"""Creation/deletion hooks for the Postfix MTA."""
+
+import os
+import grp
+import pwd
+import time
+import errno
+import logging
+
+from locknix.lockfile import Lock
+from stat import *
+
+from mailman import Utils
+from mailman.MTA.Utils import makealiases
+from mailman.configuration import config
+from mailman.i18n import _
+
+LOCKFILE = os.path.join(config.LOCK_DIR, 'creator')
+ALIASFILE = os.path.join(config.DATA_DIR, 'aliases')
+VIRTFILE = os.path.join(config.DATA_DIR, 'virtual-mailman')
+TRPTFILE = os.path.join(config.DATA_DIR, 'transport')
+
+log = logging.getLogger('mailman.error')
+
+
+
+def _update_maps():
+ msg = 'command failed: %s (status: %s, %s)'
+ if config.USE_LMTP:
+ tcmd = config.POSTFIX_MAP_CMD + ' ' + TRPTFILE
+ status = (os.system(tcmd) >> 8) & 0xff
+ if status:
+ errstr = os.strerror(status)
+ log.error(msg, tcmd, status, errstr)
+ raise RuntimeError(msg % (tcmd, status, errstr))
+ acmd = config.POSTFIX_ALIAS_CMD + ' ' + ALIASFILE
+ status = (os.system(acmd) >> 8) & 0xff
+ if status:
+ errstr = os.strerror(status)
+ log.error(msg, acmd, status, errstr)
+ raise RuntimeError(msg % (acmd, status, errstr))
+ if os.path.exists(VIRTFILE):
+ vcmd = config.POSTFIX_MAP_CMD + ' ' + VIRTFILE
+ status = (os.system(vcmd) >> 8) & 0xff
+ if status:
+ errstr = os.strerror(status)
+ log.error(msg, vcmd, status, errstr)
+ raise RuntimeError(msg % (vcmd, status, errstr))
+
+
+
+def _zapfile(filename):
+ # Truncate the file w/o messing with the file permissions, but only if it
+ # already exists.
+ if os.path.exists(filename):
+ fp = open(filename, 'w')
+ fp.close()
+
+
+def clear():
+ _zapfile(ALIASFILE)
+ _zapfile(VIRTFILE)
+ _zapfile(TRPTFILE)
+
+
+
+def _addlist(mlist, fp):
+ # Set up the mailman-loop address
+ loopaddr = Utils.ParseEmail(Utils.get_site_noreply())[0]
+ loopmbox = os.path.join(config.DATA_DIR, 'owner-bounces.mbox')
+ # Seek to the end of the text file, but if it's empty write the standard
+ # disclaimer, and the loop catch address.
+ fp.seek(0, 2)
+ if not fp.tell():
+ print >> fp, """\
+# This file is generated by Mailman, and is kept in sync with the
+# binary hash file aliases.db. YOU SHOULD NOT MANUALLY EDIT THIS FILE
+# unless you know what you're doing, and can keep the two files properly
+# in sync. If you screw it up, you're on your own.
+"""
+ print >> fp, '# The ultimate loop stopper address'
+ print >> fp, '%s: %s' % (loopaddr, loopmbox)
+ print >> fp
+ # Bootstrapping. bin/genaliases must be run before any lists are created,
+ # but if no lists exist yet then mlist is None. The whole point of the
+ # exercise is to get the minimal aliases.db file into existance.
+ if mlist is None:
+ return
+ listname = mlist.internal_name()
+ hostname = mlist.host_name
+ fieldsz = len(listname) + len('-unsubscribe')
+ # The text file entries get a little extra info
+ print >> fp, '# STANZA START: %s@%s' % (listname, hostname)
+ print >> fp, '# CREATED:', time.ctime(time.time())
+ # Now add all the standard alias entries
+ for k, v in makealiases(mlist):
+ l = len(k)
+ if hostname in config.POSTFIX_STYLE_VIRTUAL_DOMAINS:
+ k += config.POSTFIX_VIRTUAL_SEPARATOR + hostname
+ # Format the text file nicely
+ print >> fp, k + ':', ((fieldsz - l) * ' ') + v
+ # Finish the text file stanza
+ print >> fp, '# STANZA END: %s@%s' % (listname, hostname)
+ print >> fp
+
+
+
+def _addvirtual(mlist, fp):
+ listname = mlist.internal_name()
+ fieldsz = len(listname) + len('-unsubscribe')
+ hostname = mlist.host_name
+ # Set up the mailman-loop address
+ loopaddr = mlist.no_reply_address
+ loopdest = Utils.ParseEmail(loopaddr)[0]
+ # Seek to the end of the text file, but if it's empty write the standard
+ # disclaimer, and the loop catch address.
+ fp.seek(0, 2)
+ if not fp.tell():
+ print >> fp, """\
+# This file is generated by Mailman, and is kept in sync with the binary hash
+# file virtual-mailman.db. YOU SHOULD NOT MANUALLY EDIT THIS FILE unless you
+# know what you're doing, and can keep the two files properly in sync. If you
+# screw it up, you're on your own.
+#
+# Note that you should already have this virtual domain set up properly in
+# your Postfix installation. See README.POSTFIX for details.
+
+# LOOP ADDRESSES START
+%s\t%s
+# LOOP ADDRESSES END
+""" % (loopaddr, loopdest)
+ # The text file entries get a little extra info
+ print >> fp, '# STANZA START: %s@%s' % (listname, hostname)
+ print >> fp, '# CREATED:', time.ctime(time.time())
+ # Now add all the standard alias entries
+ for k, v in makealiases(mlist):
+ fqdnaddr = '%s@%s' % (k, hostname)
+ l = len(k)
+ # Format the text file nicely
+ if hostname in config.POSTFIX_STYLE_VIRTUAL_DOMAINS:
+ k += config.POSTFIX_VIRTUAL_SEPARATOR + hostname
+ print >> fp, fqdnaddr, ((fieldsz - l) * ' '), k
+ # Finish the text file stanza
+ print >> fp, '# STANZA END: %s@%s' % (listname, hostname)
+ print >> fp
+
+
+
+# Blech.
+def _check_for_virtual_loopaddr(mlist, filename, func):
+ loopaddr = mlist.no_reply_address
+ loopdest = Utils.ParseEmail(loopaddr)[0]
+ if func is _addtransport:
+ loopdest = 'local:' + loopdest
+ infp = open(filename)
+ outfp = open(filename + '.tmp', 'w')
+ try:
+ # Find the start of the loop address block
+ while True:
+ line = infp.readline()
+ if not line:
+ break
+ outfp.write(line)
+ if line.startswith('# LOOP ADDRESSES START'):
+ break
+ # Now see if our domain has already been written
+ while True:
+ line = infp.readline()
+ if not line:
+ break
+ if line.startswith('# LOOP ADDRESSES END'):
+ # It hasn't
+ print >> outfp, '%s\t%s' % (loopaddr, loopdest)
+ outfp.write(line)
+ break
+ elif line.startswith(loopaddr):
+ # We just found it
+ outfp.write(line)
+ break
+ else:
+ # This isn't our loop address, so spit it out and continue
+ outfp.write(line)
+ outfp.writelines(infp.readlines())
+ finally:
+ infp.close()
+ outfp.close()
+ os.rename(filename + '.tmp', filename)
+
+
+
+def _addtransport(mlist, fp):
+ # Set up the mailman-loop address
+ loopaddr = mlist.no_reply_address
+ loopdest = Utils.ParseEmail(loopaddr)[0]
+ # create/add postfix transport file for mailman
+ fp.seek(0, 2)
+ if not fp.tell():
+ print >> fp, """\
+# This file is generated by Mailman, and is kept in sync with the
+# binary hash file transport.db. YOU SHOULD NOT MANUALLY EDIT THIS FILE
+# unless you know what you're doing, and can keep the two files properly
+# in sync. If you screw it up, you're on your own.
+
+# LOOP ADDRESSES START
+%s\tlocal:%s
+# LOOP ADDRESSES END
+""" % (loopaddr, loopdest)
+ # List LMTP_ONLY_DOMAINS
+ if config.LMTP_ONLY_DOMAINS:
+ print >> fp, '# LMTP ONLY DOMAINS START'
+ for dom in config.LMTP_ONLY_DOMAINS:
+ print >> fp, '%s\tlmtp:%s:%s' % (dom,
+ config.LMTP_HOST,
+ config.LMTP_PORT)
+ print >> fp, '# LMTP ONLY DOMAINS END\n'
+ listname = mlist.internal_name()
+ hostname = mlist.host_name
+ # No need of individual local part if the domain is LMTP only
+ if hostname in config.LMTP_ONLY_DOMAINS:
+ return
+ fieldsz = len(listname) + len(hostname) + len('-unsubscribe') + 1
+ # The text file entries get a little extra info
+ print >> fp, '# STANZA START: %s@%s' % (listname, hostname)
+ print >> fp, '# CREATED:', time.ctime(time.time())
+ # Now add transport entries
+ for k, v in makealiases(mlist):
+ l = len(k + hostname) + 1
+ print >> fp, '%s@%s' % (k, hostname), ((fieldsz - l) * ' ')\
+ + 'lmtp:%s:%s' % (config.LMTP_HOST, config.LMTP_PORT)
+ #
+ print >> fp, '# STANZA END: %s@%s' % (listname, hostname)
+ print >> fp
+
+
+
+def _do_create(mlist, textfile, func):
+ # Crack open the plain text file
+ try:
+ fp = open(textfile, 'r+')
+ except IOError, e:
+ if e.errno <> errno.ENOENT:
+ raise
+ fp = open(textfile, 'w+')
+ try:
+ func(mlist, fp)
+ finally:
+ fp.close()
+ # Now double check the virtual plain text file
+ if func in (_addvirtual, _addtransport):
+ _check_for_virtual_loopaddr(mlist, textfile, func)
+
+
+def create(mlist, cgi=False, nolock=False, quiet=False):
+ # Acquire the global list database lock. quiet flag is ignored.
+ lock = None
+ if not nolock:
+ # XXX FIXME
+ lock = makelock()
+ lock.lock()
+ # Do the aliases file, which always needs to be done
+ try:
+ if config.USE_LMTP:
+ _do_create(mlist, TRPTFILE, _addtransport)
+ _do_create(None, ALIASFILE, _addlist)
+ else:
+ _do_create(mlist, ALIASFILE, _addlist)
+ if mlist.host_name in config.POSTFIX_STYLE_VIRTUAL_DOMAINS:
+ _do_create(mlist, VIRTFILE, _addvirtual)
+ _update_maps()
+ finally:
+ if lock:
+ lock.unlock(unconditionally=True)
+
+
+
+def _do_remove(mlist, textfile):
+ listname = mlist.internal_name()
+ hostname = mlist.host_name
+ # Now do our best to filter out the proper stanza from the text file.
+ # The text file better exist!
+ outfp = None
+ try:
+ infp = open(textfile)
+ except IOError, e:
+ if e.errno <> errno.ENOENT:
+ raise
+ # Otherwise, there's no text file to filter so we're done.
+ return
+ try:
+ outfp = open(textfile + '.tmp', 'w')
+ filteroutp = False
+ start = '# STANZA START: %s@%s' % (listname, hostname)
+ end = '# STANZA END: %s@%s' % (listname, hostname)
+ while 1:
+ line = infp.readline()
+ if not line:
+ break
+ # If we're filtering out a stanza, just look for the end marker and
+ # filter out everything in between. If we're not in the middle of
+ # filtering out a stanza, we're just looking for the proper begin
+ # marker.
+ if filteroutp:
+ if line.strip() == end:
+ filteroutp = False
+ # Discard the trailing blank line, but don't worry if
+ # we're at the end of the file.
+ infp.readline()
+ # Otherwise, ignore the line
+ else:
+ if line.strip() == start:
+ # Filter out this stanza
+ filteroutp = True
+ else:
+ outfp.write(line)
+ # Close up shop, and rotate the files
+ finally:
+ infp.close()
+ outfp.close()
+ os.rename(textfile+'.tmp', textfile)
+
+
+def remove(mlist, cgi=False):
+ # Acquire the global list database lock
+ with Lock(LOCKFILE):
+ if config.USE_LMTP:
+ _do_remove(mlist, TRPTFILE)
+ else:
+ _do_remove(mlist, ALIASFILE)
+ if mlist.host_name in config.POSTFIX_STYLE_VIRTUAL_DOMAINS:
+ _do_remove(mlist, VIRTFILE)
+ # Regenerate the alias and map files
+ _update_maps()
+ config.db.commit()
+
+
+
+def checkperms(state):
+ targetmode = S_IFREG | S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP
+ for file in ALIASFILE, VIRTFILE, TRPTFILE:
+ if state.VERBOSE:
+ print _('checking permissions on %(file)s')
+ stat = None
+ try:
+ stat = os.stat(file)
+ except OSError, e:
+ if e.errno <> errno.ENOENT:
+ raise
+ if stat and (stat[ST_MODE] & targetmode) <> targetmode:
+ state.ERRORS += 1
+ octmode = oct(stat[ST_MODE])
+ print _('%(file)s permissions must be 066x (got %(octmode)s)'),
+ if state.FIX:
+ print _('(fixing)')
+ os.chmod(file, stat[ST_MODE] | targetmode)
+ else:
+ print
+ # Make sure the corresponding .db files are owned by the Mailman user.
+ # We don't need to check the group ownership of the file, since
+ # check_perms checks this itself.
+ dbfile = file + '.db'
+ stat = None
+ try:
+ stat = os.stat(dbfile)
+ except OSError, e:
+ if e.errno <> errno.ENOENT:
+ raise
+ continue
+ if state.VERBOSE:
+ print _('checking ownership of %(dbfile)s')
+ user = config.MAILMAN_USER
+ ownerok = stat[ST_UID] == pwd.getpwnam(user)[2]
+ if not ownerok:
+ try:
+ owner = pwd.getpwuid(stat[ST_UID])[0]
+ except KeyError:
+ owner = 'uid %d' % stat[ST_UID]
+ print _('%(dbfile)s owned by %(owner)s (must be owned by %(user)s'),
+ state.ERRORS += 1
+ if state.FIX:
+ print _('(fixing)')
+ uid = pwd.getpwnam(user)[2]
+ gid = grp.getgrnam(config.MAILMAN_GROUP)[2]
+ os.chown(dbfile, uid, gid)
+ else:
+ print
+ if stat and (stat[ST_MODE] & targetmode) <> targetmode:
+ state.ERRORS += 1
+ octmode = oct(stat[ST_MODE])
+ print _('%(dbfile)s permissions must be 066x (got %(octmode)s)'),
+ if state.FIX:
+ print _('(fixing)')
+ os.chmod(dbfile, stat[ST_MODE] | targetmode)
+ else:
+ print
diff --git a/mailman/mta/Utils.py b/mailman/mta/Utils.py
new file mode 100644
index 000000000..bebbc69b7
--- /dev/null
+++ b/mailman/mta/Utils.py
@@ -0,0 +1,87 @@
+# Copyright (C) 2001-2009 by the Free Software Foundation, Inc.
+#
+# This file is part of GNU Mailman.
+#
+# GNU Mailman is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option)
+# any later version.
+#
+# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+# more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# GNU Mailman. If not, see .
+
+"""Utilities for list creation/deletion hooks."""
+
+import os
+import pwd
+
+from mailman.configuration import config
+
+
+
+def getusername():
+ username = os.environ.get('USER') or os.environ.get('LOGNAME')
+ if not username:
+ import pwd
+ username = pwd.getpwuid(os.getuid())[0]
+ if not username:
+ username = ''
+ return username
+
+
+
+def _makealiases_mailprog(mlist):
+ wrapper = os.path.join(config.WRAPPER_DIR, 'mailman')
+ # Most of the list alias extensions are quite regular. I.e. if the
+ # message is delivered to listname-foobar, it will be filtered to a
+ # program called foobar. There are two exceptions:
+ #
+ # 1) Messages to listname (no extension) go to the post script.
+ # 2) Messages to listname-admin go to the bounces script. This is for
+ # backwards compatibility and may eventually go away (we really have no
+ # need for the -admin address anymore).
+ #
+ # Seed this with the special cases.
+ listname = mlist.internal_name()
+ fqdn_listname = mlist.fqdn_listname
+ aliases = [
+ (listname, '"|%s post %s"' % (wrapper, fqdn_listname)),
+ ]
+ for ext in ('admin', 'bounces', 'confirm', 'join', 'leave', 'owner',
+ 'request', 'subscribe', 'unsubscribe'):
+ aliases.append(('%s-%s' % (listname, ext),
+ '"|%s %s %s"' % (wrapper, ext, fqdn_listname)))
+ return aliases
+
+
+
+def _makealiases_maildir(mlist):
+ maildir = config.MAILDIR_DIR
+ listname = mlist.internal_name()
+ fqdn_listname = mlist.fqdn_listname
+ if not maildir.endswith('/'):
+ maildir += '/'
+ # Deliver everything using maildir style. This way there's no mail
+ # program, no forking and no wrapper necessary!
+ #
+ # Note, don't use this unless your MTA leaves the envelope recipient in
+ # Delivered-To:, Envelope-To:, or Apparently-To:
+ aliases = [(listname, maildir)]
+ for ext in ('admin', 'bounces', 'confirm', 'join', 'leave', 'owner',
+ 'request', 'subscribe', 'unsubscribe'):
+ aliases.append(('%s-%s' % (listname, ext), maildir))
+ return aliases
+
+
+
+# XXX This won't work if Mailman.MTA.Utils is imported before the
+# configuration is loaded.
+if config.USE_MAILDIR:
+ makealiases = _makealiases_maildir
+else:
+ makealiases = _makealiases_mailprog
diff --git a/mailman/mta/__init__.py b/mailman/mta/__init__.py
new file mode 100644
index 000000000..e69de29bb
--
cgit v1.2.3-70-g09d2