Fix #137
[mailman.git] / port_me / export.py
blob279abc36feb207cfa5a729d8154dc0342b5346a7
1 # Copyright (C) 2006-2015 by the Free Software Foundation, Inc.
3 # This file is part of GNU Mailman.
5 # GNU Mailman is free software: you can redistribute it and/or modify it under
6 # the terms of the GNU General Public License as published by the Free
7 # Software Foundation, either version 3 of the License, or (at your option)
8 # any later version.
10 # GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
11 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12 # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
13 # more details.
15 # You should have received a copy of the GNU General Public License along with
16 # GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
18 """Export an XML representation of a mailing list."""
20 import sys
21 import codecs
22 import datetime
23 import optparse
25 from xml.sax.saxutils import escape
27 from mailman import Defaults
28 from mailman import errors
29 from mailman import MemberAdaptor
30 from mailman.MailList import MailList
31 from mailman.configuration import config
32 from mailman.core.i18n import _
33 from mailman.initialize import initialize
34 from mailman.version import MAILMAN_VERSION
37 SPACE = ' '
39 TYPES = {
40 Defaults.Toggle : 'bool',
41 Defaults.Radio : 'radio',
42 Defaults.String : 'string',
43 Defaults.Text : 'text',
44 Defaults.Email : 'email',
45 Defaults.EmailList : 'email_list',
46 Defaults.Host : 'host',
47 Defaults.Number : 'number',
48 Defaults.FileUpload : 'upload',
49 Defaults.Select : 'select',
50 Defaults.Topics : 'topics',
51 Defaults.Checkbox : 'checkbox',
52 Defaults.EmailListEx : 'email_list_ex',
53 Defaults.HeaderFilter : 'header_filter',
58 class Indenter:
59 def __init__(self, fp, indentwidth=4):
60 self._fp = fp
61 self._indent = 0
62 self._width = indentwidth
64 def indent(self):
65 self._indent += 1
67 def dedent(self):
68 self._indent -= 1
69 assert self._indent >= 0
71 def write(self, s):
72 if s <> '\n':
73 self._fp.write(self._indent * self._width * ' ')
74 self._fp.write(s)
78 class XMLDumper(object):
79 def __init__(self, fp):
80 self._fp = Indenter(fp)
81 self._tagbuffer = None
82 self._stack = []
84 def _makeattrs(self, tagattrs):
85 # The attribute values might contain angle brackets. They might also
86 # be None.
87 attrs = []
88 for k, v in tagattrs.items():
89 if v is None:
90 v = ''
91 else:
92 v = escape(str(v))
93 attrs.append('%s="%s"' % (k, v))
94 return SPACE.join(attrs)
96 def _flush(self, more=True):
97 if not self._tagbuffer:
98 return
99 name, attributes = self._tagbuffer
100 self._tagbuffer = None
101 if attributes:
102 attrstr = ' ' + self._makeattrs(attributes)
103 else:
104 attrstr = ''
105 if more:
106 print >> self._fp, '<%s%s>' % (name, attrstr)
107 self._fp.indent()
108 self._stack.append(name)
109 else:
110 print >> self._fp, '<%s%s/>' % (name, attrstr)
112 # Use this method when you know you have sub-elements.
113 def _push_element(self, _name, **_tagattrs):
114 self._flush()
115 self._tagbuffer = (_name, _tagattrs)
117 def _pop_element(self, _name):
118 buffered = bool(self._tagbuffer)
119 self._flush(more=False)
120 if not buffered:
121 name = self._stack.pop()
122 assert name == _name, 'got: %s, expected: %s' % (_name, name)
123 self._fp.dedent()
124 print >> self._fp, '</%s>' % name
126 # Use this method when you do not have sub-elements
127 def _element(self, _name, _value=None, **_attributes):
128 self._flush()
129 if _attributes:
130 attrs = ' ' + self._makeattrs(_attributes)
131 else:
132 attrs = ''
133 if _value is None:
134 print >> self._fp, '<%s%s/>' % (_name, attrs)
135 else:
136 # The value might contain angle brackets.
137 value = escape(_value.decode('utf-8'))
138 print >> self._fp, '<%s%s>%s</%s>' % (_name, attrs, value, _name)
140 def _do_list_categories(self, mlist, k, subcat=None):
141 info = mlist.GetConfigInfo(k, subcat)
142 label, gui = mlist.GetConfigCategories()[k]
143 if info is None:
144 return
145 for data in info[1:]:
146 if not isinstance(data, tuple):
147 continue
148 varname = data[0]
149 # Variable could be volatile
150 if varname.startswith('_'):
151 continue
152 vtype = data[1]
153 # Munge the value based on its type
154 value = None
155 if hasattr(gui, 'getValue'):
156 value = gui.getValue(mlist, vtype, varname, data[2])
157 if value is None:
158 value = getattr(mlist, varname)
159 widget_type = TYPES[vtype]
160 if isinstance(value, list):
161 self._push_element('option', name=varname, type=widget_type)
162 for v in value:
163 self._element('value', v)
164 self._pop_element('option')
165 else:
166 self._element('option', value, name=varname, type=widget_type)
168 def _dump_list(self, mlist):
169 # Write list configuration values
170 self._push_element('list', name=mlist.fqdn_listname)
171 self._push_element('configuration')
172 self._element('option',
173 mlist.preferred_language,
174 name='preferred_language')
175 for k in config.ADMIN_CATEGORIES:
176 subcats = mlist.GetConfigSubCategories(k)
177 if subcats is None:
178 self._do_list_categories(mlist, k)
179 else:
180 for subcat in [t[0] for t in subcats]:
181 self._do_list_categories(mlist, k, subcat)
182 self._pop_element('configuration')
183 # Write membership
184 self._push_element('roster')
185 digesters = set(mlist.getDigestMemberKeys())
186 for member in sorted(mlist.getMembers()):
187 attrs = dict(id=member)
188 cased = mlist.getMemberCPAddress(member)
189 if cased <> member:
190 attrs['original'] = cased
191 self._push_element('member', **attrs)
192 self._element('realname', mlist.getMemberName(member))
193 self._element('password', mlist.getMemberPassword(member))
194 self._element('language', mlist.getMemberLanguage(member))
195 # Delivery status, combined with the type of delivery
196 attrs = {}
197 status = mlist.getDeliveryStatus(member)
198 if status == MemberAdaptor.ENABLED:
199 attrs['status'] = 'enabled'
200 else:
201 attrs['status'] = 'disabled'
202 attrs['reason'] = {MemberAdaptor.BYUSER : 'byuser',
203 MemberAdaptor.BYADMIN : 'byadmin',
204 MemberAdaptor.BYBOUNCE : 'bybounce',
205 }.get(mlist.getDeliveryStatus(member),
206 'unknown')
207 if member in digesters:
208 if mlist.getMemberOption(member, Defaults.DisableMime):
209 attrs['delivery'] = 'plain'
210 else:
211 attrs['delivery'] = 'mime'
212 else:
213 attrs['delivery'] = 'regular'
214 changed = mlist.getDeliveryStatusChangeTime(member)
215 if changed:
216 when = datetime.datetime.fromtimestamp(changed)
217 attrs['changed'] = when.isoformat()
218 self._element('delivery', **attrs)
219 for option, flag in Defaults.OPTINFO.items():
220 # Digest/Regular delivery flag must be handled separately
221 if option in ('digest', 'plain'):
222 continue
223 value = mlist.getMemberOption(member, flag)
224 self._element(option, value)
225 topics = mlist.getMemberTopics(member)
226 if not topics:
227 self._element('topics')
228 else:
229 self._push_element('topics')
230 for topic in topics:
231 self._element('topic', topic)
232 self._pop_element('topics')
233 self._pop_element('member')
234 self._pop_element('roster')
235 self._pop_element('list')
237 def dump(self, listnames):
238 print >> self._fp, '<?xml version="1.0" encoding="UTF-8"?>'
239 self._push_element('mailman', **{
240 'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
241 'xsi:noNamespaceSchemaLocation': 'ssi-1.0.xsd',
243 for listname in sorted(listnames):
244 try:
245 mlist = MailList(listname, lock=False)
246 except errors.MMUnknownListError:
247 print >> sys.stderr, _('No such list: $listname')
248 continue
249 self._dump_list(mlist)
250 self._pop_element('mailman')
252 def close(self):
253 while self._stack:
254 self._pop_element()
258 def parseargs():
259 parser = optparse.OptionParser(version=MAILMAN_VERSION,
260 usage=_("""\
261 %prog [options]
263 Export the configuration and members of a mailing list in XML format."""))
264 parser.add_option('-o', '--outputfile',
265 metavar='FILENAME', default=None, type='string',
266 help=_("""\
267 Output XML to FILENAME. If not given, or if FILENAME is '-', standard out is
268 used."""))
269 parser.add_option('-l', '--listname',
270 default=[], action='append', type='string',
271 metavar='LISTNAME', dest='listnames', help=_("""\
272 The list to include in the output. If not given, then all mailing lists are
273 included in the XML output. Multiple -l flags may be given."""))
274 parser.add_option('-C', '--config',
275 help=_('Alternative configuration file to use'))
276 opts, args = parser.parse_args()
277 if args:
278 parser.print_help()
279 parser.error(_('Unexpected arguments'))
280 return parser, opts, args
284 def main():
285 parser, opts, args = parseargs()
286 initialize(opts.config)
288 close = False
289 if opts.outputfile in (None, '-'):
290 writer = codecs.getwriter('utf-8')
291 fp = writer(sys.stdout)
292 else:
293 fp = codecs.open(opts.outputfile, 'w', 'utf-8')
294 close = True
296 try:
297 dumper = XMLDumper(fp)
298 if opts.listnames:
299 listnames = []
300 for listname in opts.listnames:
301 if '@' not in listname:
302 listname = '%s@%s' % (listname, config.DEFAULT_EMAIL_HOST)
303 listnames.append(listname)
304 else:
305 listnames = config.list_manager.names
306 dumper.dump(listnames)
307 dumper.close()
308 finally:
309 if close:
310 fp.close()