diff options
Diffstat (limited to 'src/mailman/bin/import.py')
| -rw-r--r-- | src/mailman/bin/import.py | 315 |
1 files changed, 315 insertions, 0 deletions
diff --git a/src/mailman/bin/import.py b/src/mailman/bin/import.py new file mode 100644 index 000000000..d2361e808 --- /dev/null +++ b/src/mailman/bin/import.py @@ -0,0 +1,315 @@ +# Copyright (C) 2006-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/>. + +"""Import the XML representation of a mailing list.""" + +import sys +import codecs +import optparse +import traceback + +from xml.dom import minidom +from xml.parsers.expat import ExpatError + +from mailman import Defaults +from mailman import errors +from mailman import MemberAdaptor +from mailman import Utils +from mailman import passwords +from mailman.MailList import MailList +from mailman.i18n import _ +from mailman.initialize import initialize +from mailman.version import MAILMAN_VERSION + + +OPTS = None + + + +def nodetext(node): + # Expect only one TEXT_NODE in the list of children + for child in node.childNodes: + if child.nodeType == node.TEXT_NODE: + return child.data + return u'' + + +def nodegen(node, *elements): + for child in node.childNodes: + if child.nodeType <> minidom.Node.ELEMENT_NODE: + continue + if elements and child.tagName not in elements: + print _('Ignoring unexpected element: $node.tagName') + else: + yield child + + + +def parse_config(node): + config = dict() + for child in nodegen(node, 'option'): + name = child.getAttribute('name') + if not name: + print _('Skipping unnamed option') + continue + vtype = child.getAttribute('type') or 'string' + if vtype in ('email_list', 'email_list_ex', 'checkbox'): + value = [] + for subnode in nodegen(child): + value.append(nodetext(subnode)) + elif vtype == 'bool': + value = nodetext(child) + try: + value = bool(int(value)) + except ValueError: + value = {'true' : True, + 'false': False, + }.get(value.lower()) + if value is None: + print _('Skipping bad boolean value: $value') + continue + elif vtype == 'radio': + value = nodetext(child).lower() + boolval = {'true' : True, + 'false': False, + }.get(value) + if boolval is None: + value = int(value) + else: + value = boolval + elif vtype == 'number': + value = nodetext(child) + # First try int then float + try: + value = int(value) + except ValueError: + value = float(value) + elif vtype in ('header_filter', 'topics'): + value = [] + fakebltins = dict(__builtins__ = dict(True=True, False=False)) + for subnode in nodegen(child): + reprstr = nodetext(subnode) + # Turn the reprs back into tuples, in a safe way + tupleval = eval(reprstr, fakebltins) + value.append(tupleval) + else: + value = nodetext(child) + # And now some special casing :( + if name == 'new_member_options': + value = int(nodetext(child)) + config[name] = value + return config + + + + +def parse_roster(node): + members = [] + for child in nodegen(node, 'member'): + member = dict() + member['id'] = mid = child.getAttribute('id') + if not mid: + print _('Skipping member with no id') + continue + if OPTS.verbose: + print _('* Processing member: $mid') + for subnode in nodegen(child): + attr = subnode.tagName + if attr == 'delivery': + value = (subnode.getAttribute('status'), + subnode.getAttribute('delivery')) + elif attr in ('hide', 'ack', 'notmetoo', 'nodupes', 'nomail'): + value = {'true' : True, + 'false': False, + }.get(nodetext(subnode).lower(), False) + elif attr == 'topics': + value = [] + for subsubnode in nodegen(subnode): + value.append(nodetext(subsubnode)) + elif attr == 'password': + value = nodetext(subnode) + if OPTS.reset_passwords or value == '{NONE}' or not value: + value = passwords.make_secret(Utils.MakeRandomPassword()) + else: + value = nodetext(subnode) + member[attr] = value + members.append(member) + return members + + + +def load(fp): + try: + doc = minidom.parse(fp) + except ExpatError: + print _('Expat error in file: $fp.name') + traceback.print_exc() + sys.exit(1) + doc.normalize() + # Make sure there's only one top-level <mailman> node + gen = nodegen(doc, 'mailman') + top = gen.next() + try: + gen.next() + except StopIteration: + pass + else: + print _('Malformed XML; duplicate <mailman> nodes') + sys.exit(1) + all_listdata = [] + for listnode in nodegen(top, 'list'): + listdata = dict() + name = listnode.getAttribute('name') + if OPTS.verbose: + print _('Processing list: $name') + if not name: + print _('Ignoring malformed <list> node') + continue + for child in nodegen(listnode, 'configuration', 'roster'): + if child.tagName == 'configuration': + list_config = parse_config(child) + else: + assert(child.tagName == 'roster') + list_roster = parse_roster(child) + all_listdata.append((name, list_config, list_roster)) + return all_listdata + + + +def create(all_listdata): + for name, list_config, list_roster in all_listdata: + fqdn_listname = '%s@%s' % (name, list_config['host_name']) + if Utils.list_exists(fqdn_listname): + print _('Skipping already existing list: $fqdn_listname') + continue + mlist = MailList() + try: + if OPTS.verbose: + print _('Creating mailing list: $fqdn_listname') + mlist.Create(fqdn_listname, list_config['owner'][0], + list_config['password']) + except errors.BadDomainSpecificationError: + print _('List is not in a supported domain: $fqdn_listname') + continue + # Save the list creation, then unlock and relock the list. This is so + # that we use normal SQLAlchemy transactions to manage all the + # attribute and membership updates. Without this, no transaction will + # get committed in the second Save() below and we'll lose all our + # updates. + mlist.Save() + mlist.Unlock() + mlist.Lock() + try: + for option, value in list_config.items(): + # XXX Here's what sucks. Some properties need to have + # _setValue() called on the gui component, because those + # methods do some pre-processing on the values before they're + # applied to the MailList instance. But we don't have a good + # way to find a category and sub-category that a particular + # property belongs to. Plus this will probably change. So + # for now, we'll just hard code the extra post-processing + # here. The good news is that not all _setValue() munging + # needs to be done -- for example, we've already converted + # everything to dollar strings. + if option in ('filter_mime_types', 'pass_mime_types', + 'filter_filename_extensions', + 'pass_filename_extensions'): + value = value.splitlines() + if option == 'available_languages': + mlist.set_languages(*value) + else: + setattr(mlist, option, value) + for member in list_roster: + mid = member['id'] + if OPTS.verbose: + print _('* Adding member: $mid') + status, delivery = member['delivery'] + kws = {'password' : member['password'], + 'language' : member['language'], + 'realname' : member['realname'], + 'digest' : delivery <> 'regular', + } + mlist.addNewMember(mid, **kws) + status = {'enabled' : MemberAdaptor.ENABLED, + 'byuser' : MemberAdaptor.BYUSER, + 'byadmin' : MemberAdaptor.BYADMIN, + 'bybounce' : MemberAdaptor.BYBOUNCE, + }.get(status, MemberAdaptor.UNKNOWN) + mlist.setDeliveryStatus(mid, status) + for opt in ('hide', 'ack', 'notmetoo', 'nodupes', 'nomail'): + mlist.setMemberOption(mid, + Defaults.OPTINFO[opt], + member[opt]) + topics = member.get('topics') + if topics: + mlist.setMemberTopics(mid, topics) + mlist.Save() + finally: + mlist.Unlock() + + + +def parseargs(): + parser = optparse.OptionParser(version=MAILMAN_VERSION, + usage=_("""\ +%prog [options] + +Import the configuration and/or members of a mailing list in XML format. The +imported mailing list must not already exist. All mailing lists named in the +XML file are imported, but those that already exist are skipped unless --error +is given.""")) + parser.add_option('-i', '--inputfile', + metavar='FILENAME', default=None, type='string', + help=_("""\ +Input XML from FILENAME. If not given, or if FILENAME is '-', standard input +is used.""")) + parser.add_option('-p', '--reset-passwords', + default=False, action='store_true', help=_("""\ +With this option, user passwords in the XML are ignored and are reset to a +random password. If the generated passwords were not included in the input +XML, they will always be randomly generated.""")) + parser.add_option('-v', '--verbose', + default=False, action='store_true', + help=_('Produce more verbose output')) + parser.add_option('-C', '--config', + help=_('Alternative configuration file to use')) + opts, args = parser.parse_args() + if args: + parser.print_help() + parser.error(_('Unexpected arguments')) + return parser, opts, args + + + +def main(): + global OPTS + + parser, opts, args = parseargs() + initialize(opts.config) + OPTS = opts + + if opts.inputfile in (None, '-'): + fp = sys.stdin + else: + fp = open(opts.inputfile, 'r') + + try: + listbags = load(fp) + create(listbags) + finally: + if fp is not sys.stdin: + fp.close() |
