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() |