Add Debian directory
[mspang/vmailman.git] / debian / patches / 81_backport_export.dpatch
1 #! /bin/sh /usr/share/dpatch/dpatch-run
2 ## 81_backport_export.dpatch by  <lmamane@debian.org>
3 ##
4 ## All lines beginning with `## DP:' are a description of the patch.
5 ## DP: Backport bin/export from upstream SVN so that we can
6 ## DP: have automatic upgrades to lenny.
7
8 @DPATCH@
9 diff -urNad mailman-2.1.9~/bin/Makefile.in mailman-2.1.9/bin/Makefile.in
10 --- mailman-2.1.9~/bin/Makefile.in      2005-08-27 03:40:17.000000000 +0200
11 +++ mailman-2.1.9/bin/Makefile.in       2007-01-20 04:56:07.585102043 +0100
12 @@ -1,4 +1,4 @@
13 -# Copyright (C) 1998-2004 by the Free Software Foundation, Inc.
14 +# Copyright (C) 1998-2006 by the Free Software Foundation, Inc.
15  #
16  # This program is free software; you can redistribute it and/or
17  # modify it under the terms of the GNU General Public License
18 @@ -49,7 +49,7 @@
19                 list_admins genaliases change_pw mailmanctl qrunner inject \
20                 unshunt fix_url.py convert.py transcheck b4b5-archfix \
21                 list_owners msgfmt.py show_qfiles discard rb-archfix \
22 -               reset_pw.py
23 +               reset_pw.py export.py
24  
25  BUILDDIR=      ../build/bin
26  
27 diff -urNad mailman-2.1.9~/bin/export.py mailman-2.1.9/bin/export.py
28 --- mailman-2.1.9~/bin/export.py        1970-01-01 01:00:00.000000000 +0100
29 +++ mailman-2.1.9/bin/export.py 2007-01-20 04:56:07.585102043 +0100
30 @@ -0,0 +1,382 @@
31 +#! @PYTHON@
32 +#
33 +# Copyright (C) 2006-2007 by the Free Software Foundation, Inc.
34 +#
35 +# This program is free software; you can redistribute it and/or
36 +# modify it under the terms of the GNU General Public License
37 +# as published by the Free Software Foundation; either version 2
38 +# of the License, or (at your option) any later version.
39 +#
40 +# This program is distributed in the hope that it will be useful,
41 +# but WITHOUT ANY WARRANTY; without even the implied warranty of
42 +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
43 +# GNU General Public License for more details.
44 +#
45 +# You should have received a copy of the GNU General Public License
46 +# along with this program; if not, write to the Free Software
47 +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
48 +# USA.
49 +
50 +"""Export an XML representation of a mailing list."""
51 +
52 +import os
53 +import sys
54 +import sha
55 +import base64
56 +import codecs
57 +import datetime
58 +import optparse
59 +
60 +from xml.sax.saxutils import escape
61 +
62 +import paths
63 +from Mailman import Defaults
64 +from Mailman import Errors
65 +from Mailman import MemberAdaptor
66 +from Mailman import Utils
67 +from Mailman import mm_cfg
68 +from Mailman.MailList import MailList
69 +from Mailman.i18n import _
70 +
71 +__i18n_templates__ = True
72 +
73 +SPACE           = ' '
74 +DOLLAR_STRINGS  = ('msg_header', 'msg_footer',
75 +                   'digest_header', 'digest_footer',
76 +                   'autoresponse_postings_text',
77 +                   'autoresponse_admin_text',
78 +                   'autoresponse_request_text')
79 +SALT_LENGTH     = 4 # bytes
80 +
81 +TYPES = {
82 +    mm_cfg.Toggle         : 'bool',
83 +    mm_cfg.Radio          : 'radio',
84 +    mm_cfg.String         : 'string',
85 +    mm_cfg.Text           : 'text',
86 +    mm_cfg.Email          : 'email',
87 +    mm_cfg.EmailList      : 'email_list',
88 +    mm_cfg.Host           : 'host',
89 +    mm_cfg.Number         : 'number',
90 +    mm_cfg.FileUpload     : 'upload',
91 +    mm_cfg.Select         : 'select',
92 +    mm_cfg.Topics         : 'topics',
93 +    mm_cfg.Checkbox       : 'checkbox',
94 +    mm_cfg.EmailListEx    : 'email_list_ex',
95 +    mm_cfg.HeaderFilter   : 'header_filter',
96 +    }
97 +
98 +
99 +
100 +\f
101 +class Indenter:
102 +    def __init__(self, fp, indentwidth=4):
103 +        self._fp     = fp
104 +        self._indent = 0
105 +        self._width  = indentwidth
106 +
107 +    def indent(self):
108 +        self._indent += 1
109 +
110 +    def dedent(self):
111 +        self._indent -= 1
112 +        assert self._indent >= 0
113 +
114 +    def write(self, s):
115 +        if s <> '\n':
116 +            self._fp.write(self._indent * self._width * ' ')
117 +        self._fp.write(s)
118 +
119 +
120 +\f
121 +class XMLDumper(object):
122 +    def __init__(self, fp):
123 +        self._fp        = Indenter(fp)
124 +        self._tagbuffer = None
125 +        self._stack     = []
126 +
127 +    def _makeattrs(self, tagattrs):
128 +        # The attribute values might contain angle brackets.  They might also
129 +        # be None.
130 +        attrs = []
131 +        for k, v in tagattrs.items():
132 +            if v is None:
133 +                v = ''
134 +            else:
135 +                v = escape(str(v))
136 +            attrs.append('%s="%s"' % (k, v))
137 +        return SPACE.join(attrs)
138 +
139 +    def _flush(self, more=True):
140 +        if not self._tagbuffer:
141 +            return
142 +        name, attributes = self._tagbuffer
143 +        self._tagbuffer = None
144 +        if attributes:
145 +            attrstr = ' ' + self._makeattrs(attributes)
146 +        else:
147 +            attrstr = ''
148 +        if more:
149 +            print >> self._fp, '<%s%s>' % (name, attrstr)
150 +            self._fp.indent()
151 +            self._stack.append(name)
152 +        else:
153 +            print >> self._fp, '<%s%s/>' % (name, attrstr)
154 +
155 +    # Use this method when you know you have sub-elements.
156 +    def _push_element(self, _name, **_tagattrs):
157 +        self._flush()
158 +        self._tagbuffer = (_name, _tagattrs)
159 +
160 +    def _pop_element(self, _name):
161 +        buffered = bool(self._tagbuffer)
162 +        self._flush(more=False)
163 +        if not buffered:
164 +            name = self._stack.pop()
165 +            assert name == _name, 'got: %s, expected: %s' % (_name, name)
166 +            self._fp.dedent()
167 +            print >> self._fp, '</%s>' % name
168 +
169 +    # Use this method when you do not have sub-elements
170 +    def _element(self, _name, _value=None, **_attributes):
171 +        self._flush()
172 +        if _attributes:
173 +            attrs = ' ' + self._makeattrs(_attributes)
174 +        else:
175 +            attrs = ''
176 +        if _value is None:
177 +            print >> self._fp, '<%s%s/>' % (_name, attrs)
178 +        else:
179 +            value = escape(unicode(_value))
180 +            print >> self._fp, '<%s%s>%s</%s>' % (_name, attrs, value, _name)
181 +
182 +    def _do_list_categories(self, mlist, k, subcat=None):
183 +        is_converted = bool(getattr(mlist, 'use_dollar_strings', False))
184 +        info = mlist.GetConfigInfo(k, subcat)
185 +        label, gui = mlist.GetConfigCategories()[k]
186 +        if info is None:
187 +            return
188 +        for data in info[1:]:
189 +            if not isinstance(data, tuple):
190 +                continue
191 +            varname = data[0]
192 +            # Variable could be volatile
193 +            if varname.startswith('_'):
194 +                continue
195 +            vtype = data[1]
196 +            # Munge the value based on its type
197 +            value = None
198 +            if hasattr(gui, 'getValue'):
199 +                value = gui.getValue(mlist, vtype, varname, data[2])
200 +            if value is None:
201 +                value = getattr(mlist, varname)
202 +            # Do %-string to $-string conversions if the list hasn't already
203 +            # been converted.
204 +            if varname == 'use_dollar_strings':
205 +                continue
206 +            if not is_converted and varname in DOLLAR_STRINGS:
207 +                value = Utils.to_dollar(value)
208 +            widget_type = TYPES[vtype]
209 +            if isinstance(value, list):
210 +                self._push_element('option', name=varname, type=widget_type)
211 +                for v in value:
212 +                    self._element('value', v)
213 +                self._pop_element('option')
214 +            else:
215 +                self._element('option', value, name=varname, type=widget_type)
216 +
217 +    def _dump_list(self, mlist, password_scheme):
218 +        # Write list configuration values
219 +        self._push_element('list', name=mlist._internal_name)
220 +        self._push_element('configuration')
221 +        self._element('option',
222 +                      mlist.preferred_language,
223 +                      name='preferred_language',
224 +                      type='string')
225 +        self._element('option',
226 +                      mlist.password,
227 +                      name='password',
228 +                      type='string')
229 +        for k in mm_cfg.ADMIN_CATEGORIES:
230 +            subcats = mlist.GetConfigSubCategories(k)
231 +            if subcats is None:
232 +                self._do_list_categories(mlist, k)
233 +            else:
234 +                for subcat in [t[0] for t in subcats]:
235 +                    self._do_list_categories(mlist, k, subcat)
236 +        self._pop_element('configuration')
237 +        # Write membership
238 +        self._push_element('roster')
239 +        digesters = set(mlist.getDigestMemberKeys())
240 +        for member in sorted(mlist.getMembers()):
241 +            attrs = dict(id=member)
242 +            cased = mlist.getMemberCPAddress(member)
243 +            if cased <> member:
244 +                attrs['original'] = cased
245 +            self._push_element('member', **attrs)
246 +            self._element('realname', mlist.getMemberName(member))
247 +            self._element('password',
248 +                          password_scheme(mlist.getMemberPassword(member)))
249 +            self._element('language', mlist.getMemberLanguage(member))
250 +            # Delivery status, combined with the type of delivery
251 +            attrs = {}
252 +            status = mlist.getDeliveryStatus(member)
253 +            if status == MemberAdaptor.ENABLED:
254 +                attrs['status'] = 'enabled'
255 +            else:
256 +                attrs['status'] = 'disabled'
257 +                attrs['reason'] = {MemberAdaptor.BYUSER    : 'byuser',
258 +                                   MemberAdaptor.BYADMIN   : 'byadmin',
259 +                                   MemberAdaptor.BYBOUNCE  : 'bybounce',
260 +                                   }.get(mlist.getDeliveryStatus(member),
261 +                                         'unknown')
262 +            if member in digesters:
263 +                if mlist.getMemberOption(member, mm_cfg.DisableMime):
264 +                    attrs['delivery'] = 'plain'
265 +                else:
266 +                    attrs['delivery'] = 'mime'
267 +            else:
268 +                attrs['delivery'] = 'regular'
269 +            changed = mlist.getDeliveryStatusChangeTime(member)
270 +            if changed:
271 +                when = datetime.datetime.fromtimestamp(changed)
272 +                attrs['changed'] = when.isoformat()
273 +            self._element('delivery', **attrs)
274 +            for option, flag in Defaults.OPTINFO.items():
275 +                # Digest/Regular delivery flag must be handled separately
276 +                if option in ('digest', 'plain'):
277 +                    continue
278 +                value = mlist.getMemberOption(member, flag)
279 +                self._element(option, value)
280 +            topics = mlist.getMemberTopics(member)
281 +            if not topics:
282 +                self._element('topics')
283 +            else:
284 +                self._push_element('topics')
285 +                for topic in topics:
286 +                    self._element('topic', topic)
287 +                self._pop_element('topics')
288 +            self._pop_element('member')
289 +        self._pop_element('roster')
290 +        self._pop_element('list')
291 +
292 +    def dump(self, listnames, password_scheme):
293 +        print >> self._fp, '<?xml version="1.0" encoding="UTF-8"?>'
294 +        self._push_element('mailman', **{
295 +            'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
296 +            'xsi:noNamespaceSchemaLocation': 'ssi-1.0.xsd',
297 +            })
298 +        for listname in sorted(listnames):
299 +            try:
300 +                mlist = MailList(listname, lock=False)
301 +            except Errors.MMUnknownListError:
302 +                print >> sys.stderr, _('No such list: %(listname)s')
303 +                continue
304 +            self._dump_list(mlist, password_scheme)
305 +        self._pop_element('mailman')
306 +
307 +    def close(self):
308 +        while self._stack:
309 +            self._pop_element()
310 +
311 +
312 +\f
313 +def no_password(password):
314 +    return '{NONE}'
315 +
316 +
317 +def plaintext_password(password):
318 +    return '{PLAIN}' + password
319 +
320 +
321 +def sha_password(password):
322 +    h = sha.new(password)
323 +    return '{SHA}' + base64.b64encode(h.digest())
324 +
325 +
326 +def ssha_password(password):
327 +    salt = os.urandom(SALT_LENGTH)
328 +    h = sha.new(password)
329 +    h.update(salt)
330 +    return '{SSHA}' + base64.b64encode(h.digest() + salt)
331 +
332 +
333 +SCHEMES = {
334 +    'none'  : no_password,
335 +    'plain' : plaintext_password,
336 +    'sha'   : sha_password,
337 +    }
338 +
339 +try:
340 +    os.urandom(1)
341 +except NotImplementedError:
342 +    pass
343 +else:
344 +    SCHEMES['ssha'] = ssha_password
345 +
346 +
347 +\f
348 +def parseargs():
349 +    parser = optparse.OptionParser(version=mm_cfg.VERSION,
350 +                                   usage=_("""\
351 +%%prog [options]
352 +
353 +Export the configuration and members of a mailing list in XML format."""))
354 +    parser.add_option('-o', '--outputfile',
355 +                      metavar='FILENAME', default=None, type='string',
356 +                      help=_("""\
357 +Output XML to FILENAME.  If not given, or if FILENAME is '-', standard out is
358 +used."""))
359 +    parser.add_option('-p', '--password-scheme',
360 +                      default='none', type='string', help=_("""\
361 +Specify the RFC 2307 style hashing scheme for passwords included in the
362 +output.  Use -P to get a list of supported schemes, which are
363 +case-insensitive."""))
364 +    parser.add_option('-P', '--list-hash-schemes',
365 +                      default=False, action='store_true', help=_("""\
366 +List the supported password hashing schemes and exit.  The scheme labels are
367 +case-insensitive."""))
368 +    parser.add_option('-l', '--listname',
369 +                      default=[], action='append', type='string',
370 +                      metavar='LISTNAME', dest='listnames', help=_("""\
371 +The list to include in the output.  If not given, then all mailing lists are
372 +included in the XML output.  Multiple -l flags may be given."""))
373 +    opts, args = parser.parse_args()
374 +    if args:
375 +        parser.print_help()
376 +        parser.error(_('Unexpected arguments'))
377 +    if opts.list_hash_schemes:
378 +        for label in SCHEMES:
379 +            print label.upper()
380 +        sys.exit(0)
381 +    if opts.password_scheme.lower() not in SCHEMES:
382 +        parser.error(_('Invalid password scheme'))
383 +    return parser, opts, args
384 +
385 +
386 +\f
387 +def main():
388 +    parser, opts, args = parseargs()
389 +
390 +    if opts.outputfile in (None, '-'):
391 +        # This will fail if there are characters in the output incompatible
392 +        # with stdout.
393 +        fp = sys.stdout
394 +    else:
395 +        fp = codecs.open(opts.outputfile, 'w', 'utf-8')
396 +
397 +    try:
398 +        dumper = XMLDumper(fp)
399 +        if opts.listnames:
400 +            listnames = opts.listnames
401 +        else:
402 +            listnames = Utils.list_names()
403 +        dumper.dump(listnames, SCHEMES[opts.password_scheme])
404 +        dumper.close()
405 +    finally:
406 +        if fp is not sys.stdout:
407 +            fp.close()
408 +
409 +
410 +\f
411 +if __name__ == '__main__':
412 +    main()
413 diff -urNad mailman-2.1.9~/configure.in mailman-2.1.9/configure.in
414 --- mailman-2.1.9~/configure.in 2005-08-27 03:40:17.000000000 +0200
415 +++ mailman-2.1.9/configure.in  2007-01-20 04:56:07.585102043 +0100
416 @@ -1,4 +1,4 @@
417 -# Copyright (C) 1998-2004 by the Free Software Foundation, Inc.
418 +# Copyright (C) 1998-2006 by the Free Software Foundation, Inc.
419  #
420  # This program is free software; you can redistribute it and/or
421  # modify it under the terms of the GNU General Public License
422 @@ -15,7 +15,7 @@
423  # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
424  
425  dnl Process this file with autoconf to produce a configure script.
426 -AC_REVISION($Revision: 7462 $)
427 +AC_REVISION($Revision: 8122 $)
428  AC_PREREQ(2.0)
429  AC_INIT(src/common.h)
430  
431 @@ -553,6 +553,7 @@
432  bin/convert.py \
433  bin/discard \
434  bin/dumpdb \
435 +bin/export.py \
436  bin/find_member \
437  bin/fix_url.py \
438  bin/genaliases \