~mailman-coders/mailman/2.1

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