# Copyright (C) 2001-2007 by the Free Software Foundation, Inc. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, # USA. """Creation/deletion hooks for the Postfix MTA.""" import os import grp import pwd import time import errno import logging from stat import * from Mailman import LockFile 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 makelock(): return LockFile.LockFile(LOCKFILE) 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: 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 lock = makelock() lock.lock() try: 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() finally: lock.unlock(unconditionally=True) 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