summaryrefslogtreecommitdiff
path: root/src/mailman/bin/import.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/mailman/bin/import.py')
-rw-r--r--src/mailman/bin/import.py315
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()