#! /usr/bin/env python # # Copyright (C) 2015 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. import os import re import sys import argparse import subprocess import paths from Mailman import Utils from Mailman import mm_cfg from Mailman.MailList import MailList MEMBER_ATTRIBUTES = ['admin_responses', 'bounce_info', 'delivery_status', 'digest_members', 'hold_and_cmd_autoresponses', 'language', 'members', 'one_last_digest', 'passwords', 'postings_responses', 'request_responses', 'topics_userinterest', 'user_options', 'usernames', ] width = int(os.environ.get('COLUMNS', 80)) - 2 def parseargs(): parser = argparse.ArgumentParser( description=Utils.wrap("""Clone an existing list. I.e., create a new list with settings that match as closely as possible those of an existing list.""", column=width), epilog=Utils.wrap("""This script must be put in Mailman's bin/ directory. Other than the list's name, real_name and any options given as above, all attributes of the new list except for membership will be the same as those of the old list. The new list will have no members unless -m/--members is specified and no archives unless -a/--archives is specified. Note that there will be issues with the new archives. Both list information links and links to scrubbed attachments will still point to the old list's archives. These can be fixed by running bin/arch with the --wipe option on the new list, but only if the list's scrub_nondigest setting is No. Running bin/arch with the --wipe option on a list with scrub_nondigest = Yes WILL CAUSE THE SCRUBBED ATTACHMENTS TO BE LOST. If scrub_nondigest = Yes, you'll have to fix the listname in links in the archives some other way.""", column=width), formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument('old_list', type=str, help="""\ The name of the list to clone.""") parser.add_argument('new_list', type=str, help="""\ The name of the list to create.""") parser.add_argument('-r', '--real_name', default=None, type=str, help="""\ The real_name attribute of the new list. This must differ only in case from the new_list argument. The default is the name of the new list with the initial character uppercased.""") parser.add_argument('-o', '--owner', default=None, type=str, help="""\ Specify the new list owner's email address. The default is the old list's owner(s).""") parser.add_argument('-p', '--password', default=None, type=str, help="""\ Specify a new list admin password. The default is the old list's password.""") parser.add_argument('-s', '--subject_prefix', default=None, type=str, help="""\ Specify a new value for subject_prefix. The default is the old list's subject_prefix.""") parser.add_argument('-m', '--members', dest='clone_members', action='store_true', help="""\ Clone the membership list and member options from the old list. The default is the new list has no members.""") parser.add_argument('-a', '--archives', dest='archives', action='store_true', help="""\ Clone the archives of the old list. The default is an empty archive.""") parser.add_argument('-e', '--extra_files', dest='extra', action='store_true', help="""\ Copy pending requests, digest messages and list specific templates from the old list's lists/ directory to the new list. Requires -m/--members.""") parser.add_argument('-v', '--verbose', dest='verbose', action='store_true', help="""\ Print a few progress messages.""") return parser.parse_args() def abort(msg): print >> sys.stderr, '%s: error: %s' % ( os.path.basename(sys.argv[0]), msg) sys.exit(2) def copy_files(from_path, to_path): cp = subprocess.Popen(['cp', '-a', from_path, to_path ], stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) so, se = cp.communicate() return cp.returncode, so, se def copy_archives(old_list, new_list): archdir = mm_cfg.PRIVATE_ARCHIVE_FILE_DIR for x in os.listdir(os.path.join(archdir, old_list)): ret, so, se = copy_files(os.path.join(archdir, old_list, x), os.path.join(archdir, new_list) ) if ret: abort('unable to copy %s archives\n%s' % (old_list, se)) old_mbox = '%s.mbox' % old_list new_mbox = '%s.mbox' % new_list ret, so, se = copy_files(os.path.join(archdir, old_mbox, old_mbox), os.path.join(archdir, new_mbox, new_mbox) ) if ret: abort('unable to copy %s archives .mbox\n%s' % (old_list, se)) def copy_extra(old_list, new_list): listdir = mm_cfg.LIST_DATA_DIR for x in os.listdir(os.path.join(listdir, old_list)): if x.startswith('config.'): continue ret, so, se = copy_files(os.path.join(listdir, old_list, x), os.path.join(listdir, new_list) ) if ret: abort('unable to copy %s from %s\n%s' % (x, old_list, se)) def main(): ns = parseargs() new_list = ns.new_list.lower() old_list = ns.old_list.lower() if ns.verbose: print 'Validating options and arguments...' if Utils.list_exists(new_list): abort('list %s already exists.' % ns.new_list) if not Utils.list_exists(old_list): abort("list %s doesn't exist." % ns.old_list) if ns.real_name and ns.real_name.lower() != new_list: abort("%s and %s don't match case insensitively." % (ns.real_name, ns.new_list)) if ns.owner: try: Utils.ValidateEmail(ns.owner) except: abort("%s doesn't appear to be a valid email address" % ns.owner) if ns.extra and not ns.clone_members: abort('-e/--extra_files requires -m/--members.') if ns.verbose: print 'Getting %s list...' % ns.old_list ol = MailList(old_list, lock=False) l = ol.preferred_language e = ol.host_name u = re.sub('^.*://([^/]*).*', r'\1', ol.web_page_url) if ns.owner: o = ns.owner else: o = ol.owner[0] if ns.password: p = ns.password else: p = 'z' if ns.verbose: print 'Creating %s list...' % ns.new_list newlist = os.path.join(os.path.dirname(sys.argv[0]), 'newlist') if not os.path.isfile(newlist): abort("""%s doesn't exist. Am I installed in Mailman's bin/ directory?""" % newlist) nlst = subprocess.Popen([newlist, '-l', l, '-e', e, '-u', u, '-q', new_list, o, p ], stdout=subprocess.PIPE, stderr=subprocess.PIPE ) so, se = nlst.communicate() if nlst.returncode: abort('unable to create %s\n%s' % (ns.new_list, se)) # If there was stdout output, print it. It is probably aliases. if so: print so # New list is created, now update it. if ns.verbose: print 'Configuring %s list...' % ns.new_list nl = MailList(new_list, lock=True) for k, v in ol.__dict__.items(): if k.startswith('_'): continue if not ns.clone_members: if k in MEMBER_ATTRIBUTES: continue if k == 'password': if not ns.password: nl.password = v elif k == 'owner': if not ns.owner: nl.owner = v elif k == 'subject_prefix': if ns.subject_prefix: nl.subject_prefix = ns.subject_prefix else: nl.subject_prefix = v elif k == 'real_name': if ns.real_name: nl.real_name = ns.real_name else: setattr(nl, k, v) nl.Save() nl.Unlock() if ns.extra: if ns.verbose: print ('Copying extra files from %s list to %s list...' % (ns.old_list, ns.new_list)) copy_extra(old_list, new_list) if ns.archives: if ns.verbose: print ('Copying archives from %s list to %s list...' % (ns.old_list, ns.new_list)) copy_archives(old_list, new_list) if __name__ == '__main__': main()