summaryrefslogtreecommitdiff
path: root/mailman/mta/Postfix.py
diff options
context:
space:
mode:
Diffstat (limited to 'mailman/mta/Postfix.py')
-rw-r--r--mailman/mta/Postfix.py411
1 files changed, 411 insertions, 0 deletions
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 <http://www.gnu.org/licenses/>.
+
+"""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