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