From 881791e4018022c4dd6b0d6ac7449c39c8299666 Mon Sep 17 00:00:00 2001 From: bwarsaw <> Date: Fri, 29 Dec 2006 21:56:04 +0000 Subject: Port the Mailman trunk's export.py script to Mailman 2.1. Anyone wanting to upgrade from Mailman 2.1 to the trunk -- after the merge of the SQLAlchemy code -- will need this. Note that I don't intend to implement import in MM2.1. This script is a little diffferent than what's on the trunk, but functionally (and schema-wise) equivalent. --- bin/Makefile.in | 4 +- bin/export.py | 305 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 307 insertions(+), 2 deletions(-) create mode 100644 bin/export.py (limited to 'bin') diff --git a/bin/Makefile.in b/bin/Makefile.in index dc4eee83..22c24b04 100644 --- a/bin/Makefile.in +++ b/bin/Makefile.in @@ -1,4 +1,4 @@ -# Copyright (C) 1998-2004 by the Free Software Foundation, Inc. +# Copyright (C) 1998-2006 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 @@ -49,7 +49,7 @@ SCRIPTS= mmsitepass newlist rmlist add_members \ list_admins genaliases change_pw mailmanctl qrunner inject \ unshunt fix_url.py convert.py transcheck b4b5-archfix \ list_owners msgfmt.py show_qfiles discard rb-archfix \ - reset_pw.py + reset_pw.py export.py BUILDDIR= ../build/bin diff --git a/bin/export.py b/bin/export.py new file mode 100644 index 00000000..c2592aa9 --- /dev/null +++ b/bin/export.py @@ -0,0 +1,305 @@ +#! @PYTHON@ +# +# Copyright (C) 2006 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. + +"""Export an XML representation of a mailing list.""" + +import sys +import datetime +import optparse + +from xml.sax.saxutils import escape + +import paths +from Mailman import Defaults +from Mailman import Errors +from Mailman import MemberAdaptor +from Mailman import Utils +from Mailman import mm_cfg +from Mailman.MailList import MailList +from Mailman.i18n import _ + +__i18n_templates__ = True + +SPACE = ' ' +DOLLAR_STRINGS = ('msg_header', 'msg_footer', + 'digest_header', 'digest_footer', + 'autoresponse_postings_text', + 'autoresponse_admin_text', + 'autoresponse_request_text') + + + +class Indenter: + def __init__(self, fp, indentwidth=4): + self._fp = fp + self._indent = 0 + self._width = indentwidth + + def indent(self): + self._indent += 1 + + def dedent(self): + self._indent -= 1 + assert self._indent >= 0 + + def write(self, s): + self._fp.write(self._indent * self._width * ' ') + self._fp.write(s) + + + +class XMLDumper(object): + def __init__(self, fp): + self._fp = Indenter(fp) + self._tagbuffer = None + self._stack = [] + + def _makeattrs(self, tagattrs): + # The attribute values might contain angle brackets. They might also + # be None. + attrs = [] + for k, v in tagattrs.items(): + if v is None: + v = '' + else: + v = escape(str(v)) + attrs.append('%s="%s"' % (k, v)) + return SPACE.join(attrs) + + def _flush(self, more=True): + if not self._tagbuffer: + return + name, attributes = self._tagbuffer + self._tagbuffer = None + if attributes: + attrstr = ' ' + self._makeattrs(attributes) + else: + attrstr = '' + if more: + print >> self._fp, '<%s%s>' % (name, attrstr) + self._fp.indent() + self._stack.append(name) + else: + print >> self._fp, '<%s%s/>' % (name, attrstr) + + # Use this method when you know you have sub-elements. + def _push_element(self, _name, **_tagattrs): + self._flush() + self._tagbuffer = (_name, _tagattrs) + + def _pop_element(self, _name): + buffered = bool(self._tagbuffer) + self._flush(more=False) + if not buffered: + name = self._stack.pop() + assert name == _name, 'got: %s, expected: %s' % (_name, name) + self._fp.dedent() + print >> self._fp, '' % name + + # Use this method when you do not have sub-elements + def _element(self, _name, _value=None, **_attributes): + self._flush() + if _attributes: + attrs = ' ' + self._makeattrs(_attributes) + else: + attrs = '' + if _value is None: + print >> self._fp, '<%s%s/>' % (_name, attrs) + else: + # The value might contain angle brackets. + value = escape(str(_value)) + print >> self._fp, '<%s%s>%s' % (_name, attrs, value, _name) + + def _do_list_categories(self, mlist, k, subcat=None): + is_converted = bool(getattr(mlist, 'use_dollar_strings', False)) + info = mlist.GetConfigInfo(k, subcat) + label, gui = mlist.GetConfigCategories()[k] + if info is None: + return + for data in info[1:]: + if not isinstance(data, tuple): + continue + varname = data[0] + # Variable could be volatile + if varname.startswith('_'): + continue + vtype = data[1] + # Munge the value based on its type + value = None + if hasattr(gui, 'getValue'): + value = gui.getValue(mlist, vtype, varname, data[2]) + if value is None: + value = getattr(mlist, varname) + # Do %-string to $-string conversions if the list hasn't already + # been converted. + if varname == 'use_dollar_strings': + continue + if not is_converted and varname in DOLLAR_STRINGS: + value = Utils.to_dollar(value) + if isinstance(value, list): + self._push_element('option', name=varname) + for v in value: + self._element('value', v) + self._pop_element('option') + else: + self._element('option', value, name=varname) + + def _dump_list(self, mlist, with_passwords): + # Write list configuration values + self._push_element('list', name=mlist._internal_name) + self._push_element('configuration') + self._element('option', + mlist.preferred_language, + name='preferred_language') + for k in mm_cfg.ADMIN_CATEGORIES: + subcats = mlist.GetConfigSubCategories(k) + if subcats is None: + self._do_list_categories(mlist, k) + else: + for subcat in [t[0] for t in subcats]: + self._do_list_categories(mlist, k, subcat) + self._pop_element('configuration') + # Write membership + self._push_element('roster') + digesters = set(mlist.getDigestMemberKeys()) + for member in sorted(mlist.getMembers()): + attrs = dict(id=member) + cased = mlist.getMemberCPAddress(member) + if cased <> member: + attrs['original'] = cased + self._push_element('member', **attrs) + self._element('realname', mlist.getMemberName(member)) + if with_passwords: + self._element('password', mlist.getMemberPassword(member)) + self._element('language', mlist.getMemberLanguage(member)) + # Delivery status, combined with the type of delivery + attrs = {} + status = mlist.getDeliveryStatus(member) + if status == MemberAdaptor.ENABLED: + attrs['status'] = 'enabled' + else: + attrs['status'] = 'disabled' + attrs['reason'] = {MemberAdaptor.BYUSER : 'byuser', + MemberAdaptor.BYADMIN : 'byadmin', + MemberAdaptor.BYBOUNCE : 'bybounce', + }.get(mlist.getDeliveryStatus(member), + 'unknown') + if member in digesters: + if mlist.getMemberOption('plain'): + attrs['delivery'] = 'plain' + else: + attrs['delivery'] = 'mime' + else: + attrs['delivery'] = 'regular' + changed = mlist.getDeliveryStatusChangeTime(member) + if changed: + when = datetime.datetime.fromtimestamp(changed) + attrs['changed'] = when.isoformat() + self._element('delivery', **attrs) + for option, flag in Defaults.OPTINFO.items(): + # Digest/Regular delivery flag must be handled separately + if option in ('digest', 'plain'): + continue + value = mlist.getMemberOption(member, flag) + self._element(option, value) + topics = mlist.getMemberTopics(member) + if not topics: + self._element('topics') + else: + self._push_element('topics') + for topic in topics: + self._element('topic', topic) + self._pop_element('topics') + self._pop_element('member') + self._pop_element('roster') + self._pop_element('list') + + def dump(self, listnames, with_passwords=False): + print >> self._fp, '' + self._push_element('mailman', **{ + 'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', + 'xsi:noNamespaceSchemaLocation': 'ssi-1.0.xsd', + }) + for listname in sorted(listnames): + try: + mlist = MailList(listname, lock=False) + except Errors.MMUnknownListError: + print >> sys.stderr, _('No such list: %(listname)s') + continue + self._dump_list(mlist, with_passwords) + self._pop_element('mailman') + + def close(self): + while self._stack: + self._pop_element() + + + +def parseargs(): + parser = optparse.OptionParser(version=mm_cfg.VERSION, + usage=_("""\ +%%prog [options] + +Export the configuration and members of a mailing list in XML format.""")) + parser.add_option('-o', '--outputfile', + metavar='FILENAME', default=None, type='string', + help=_("""\ +Output XML to FILENAME. If not given, or if FILENAME is '-', standard out is +used.""")) + parser.add_option('-p', '--with-passwords', + default=False, action='store_true', help=_("""\ +With this option, user passwords are included in cleartext. For this reason, +the default is to not include passwords.""")) + parser.add_option('-l', '--listname', + default=[], action='append', type='string', + metavar='LISTNAME', dest='listnames', help=_("""\ +The list to include in the output. If not given, then all mailing lists are +included in the XML output. Multiple -l flags may be given.""")) + opts, args = parser.parse_args() + if args: + parser.print_help() + parser.error(_('Unexpected arguments')) + return parser, opts, args + + + +def main(): + parser, opts, args = parseargs() + + if opts.outputfile in (None, '-'): + fp = sys.stdout + else: + fp = open(opts.outputfile, 'w') + + try: + dumper = XMLDumper(fp) + if opts.listnames: + listnames = opts.listnames + else: + listnames = Utils.list_names() + dumper.dump(listnames, opts.with_passwords) + dumper.close() + finally: + if fp is not sys.stdout: + fp.close() + + + +if __name__ == '__main__': + main() -- cgit v1.2.3