Merge branch 'bug-rest-lists-find-wo-role' into 'master'
[mailman.git] / port_me / export.py
blob168fd897793fdca201ca1821f5ae6c75cca4a29b
1 # Copyright (C) 2006-2023 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 <https://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 mailman import Defaults, errors, MemberAdaptor
26 from mailman.configuration import config
27 from mailman.core.i18n import _
28 from mailman.initialize import initialize
29 from mailman.MailList import MailList
30 from mailman.version import MAILMAN_VERSION
31 from xml.sax.saxutils import escape
34 SPACE = ' '
36 TYPES = {
37 Defaults.Toggle : 'bool',
38 Defaults.Radio : 'radio',
39 Defaults.String : 'string',
40 Defaults.Text : 'text',
41 Defaults.Email : 'email',
42 Defaults.EmailList : 'email_list',
43 Defaults.Host : 'host',
44 Defaults.Number : 'number',
45 Defaults.FileUpload : 'upload',
46 Defaults.Select : 'select',
47 Defaults.Topics : 'topics',
48 Defaults.Checkbox : 'checkbox',
49 Defaults.EmailListEx : 'email_list_ex',
50 Defaults.HeaderFilter : 'header_filter',
55 class Indenter:
56 def __init__(self, fp, indentwidth=4):
57 self._fp = fp
58 self._indent = 0
59 self._width = indentwidth
61 def indent(self):
62 self._indent += 1
64 def dedent(self):
65 self._indent -= 1
66 assert self._indent >= 0
68 def write(self, s):
69 if s <> '\n':
70 self._fp.write(self._indent * self._width * ' ')
71 self._fp.write(s)
75 class XMLDumper(object):
76 def __init__(self, fp):
77 self._fp = Indenter(fp)
78 self._tagbuffer = None
79 self._stack = []
81 def _makeattrs(self, tagattrs):
82 # The attribute values might contain angle brackets. They might also
83 # be None.
84 attrs = []
85 for k, v in tagattrs.items():
86 if v is None:
87 v = ''
88 else:
89 v = escape(str(v))
90 attrs.append('%s="%s"' % (k, v))
91 return SPACE.join(attrs)
93 def _flush(self, more=True):
94 if not self._tagbuffer:
95 return
96 name, attributes = self._tagbuffer
97 self._tagbuffer = None
98 if attributes:
99 attrstr = ' ' + self._makeattrs(attributes)
100 else:
101 attrstr = ''
102 if more:
103 print >> self._fp, '<%s%s>' % (name, attrstr)
104 self._fp.indent()
105 self._stack.append(name)
106 else:
107 print >> self._fp, '<%s%s/>' % (name, attrstr)
109 # Use this method when you know you have sub-elements.
110 def _push_element(self, _name, **_tagattrs):
111 self._flush()
112 self._tagbuffer = (_name, _tagattrs)
114 def _pop_element(self, _name):
115 buffered = bool(self._tagbuffer)
116 self._flush(more=False)
117 if not buffered:
118 name = self._stack.pop()
119 assert name == _name, 'got: %s, expected: %s' % (_name, name)
120 self._fp.dedent()
121 print >> self._fp, '</%s>' % name
123 # Use this method when you do not have sub-elements
124 def _element(self, _name, _value=None, **_attributes):
125 self._flush()
126 if _attributes:
127 attrs = ' ' + self._makeattrs(_attributes)
128 else:
129 attrs = ''
130 if _value is None:
131 print >> self._fp, '<%s%s/>' % (_name, attrs)
132 else:
133 # The value might contain angle brackets.
134 value = escape(_value.decode('utf-8'))
135 print >> self._fp, '<%s%s>%s</%s>' % (_name, attrs, value, _name)
137 def _do_list_categories(self, mlist, k, subcat=None):
138 info = mlist.GetConfigInfo(k, subcat)
139 label, gui = mlist.GetConfigCategories()[k]
140 if info is None:
141 return
142 for data in info[1:]:
143 if not isinstance(data, tuple):
144 continue
145 varname = data[0]
146 # Variable could be volatile
147 if varname.startswith('_'):
148 continue
149 vtype = data[1]
150 # Munge the value based on its type
151 value = None
152 if hasattr(gui, 'getValue'):
153 value = gui.getValue(mlist, vtype, varname, data[2])
154 if value is None:
155 value = getattr(mlist, varname)
156 widget_type = TYPES[vtype]
157 if isinstance(value, list):
158 self._push_element('option', name=varname, type=widget_type)
159 for v in value:
160 self._element('value', v)
161 self._pop_element('option')
162 else:
163 self._element('option', value, name=varname, type=widget_type)
165 def _dump_list(self, mlist):
166 # Write list configuration values
167 self._push_element('list', name=mlist.fqdn_listname)
168 self._push_element('configuration')
169 self._element('option',
170 mlist.preferred_language,
171 name='preferred_language')
172 for k in config.ADMIN_CATEGORIES:
173 subcats = mlist.GetConfigSubCategories(k)
174 if subcats is None:
175 self._do_list_categories(mlist, k)
176 else:
177 for subcat in [t[0] for t in subcats]:
178 self._do_list_categories(mlist, k, subcat)
179 self._pop_element('configuration')
180 # Write membership
181 self._push_element('roster')
182 digesters = set(mlist.getDigestMemberKeys())
183 for member in sorted(mlist.getMembers()):
184 attrs = dict(id=member)
185 cased = mlist.getMemberCPAddress(member)
186 if cased <> member:
187 attrs['original'] = cased
188 self._push_element('member', **attrs)
189 self._element('realname', mlist.getMemberName(member))
190 self._element('password', mlist.getMemberPassword(member))
191 self._element('language', mlist.getMemberLanguage(member))
192 # Delivery status, combined with the type of delivery
193 attrs = {}
194 status = mlist.getDeliveryStatus(member)
195 if status == MemberAdaptor.ENABLED:
196 attrs['status'] = 'enabled'
197 else:
198 attrs['status'] = 'disabled'
199 attrs['reason'] = {MemberAdaptor.BYUSER : 'byuser',
200 MemberAdaptor.BYADMIN : 'byadmin',
201 MemberAdaptor.BYBOUNCE : 'bybounce',
202 }.get(mlist.getDeliveryStatus(member),
203 'unknown')
204 if member in digesters:
205 if mlist.getMemberOption(member, Defaults.DisableMime):
206 attrs['delivery'] = 'plain'
207 else:
208 attrs['delivery'] = 'mime'
209 else:
210 attrs['delivery'] = 'regular'
211 changed = mlist.getDeliveryStatusChangeTime(member)
212 if changed:
213 when = datetime.datetime.fromtimestamp(changed)
214 attrs['changed'] = when.isoformat()
215 self._element('delivery', **attrs)
216 for option, flag in Defaults.OPTINFO.items():
217 # Digest/Regular delivery flag must be handled separately
218 if option in ('digest', 'plain'):
219 continue
220 value = mlist.getMemberOption(member, flag)
221 self._element(option, value)
222 topics = mlist.getMemberTopics(member)
223 if not topics:
224 self._element('topics')
225 else:
226 self._push_element('topics')
227 for topic in topics:
228 self._element('topic', topic)
229 self._pop_element('topics')
230 self._pop_element('member')
231 self._pop_element('roster')
232 self._pop_element('list')
234 def dump(self, listnames):
235 print >> self._fp, '<?xml version="1.0" encoding="UTF-8"?>'
236 self._push_element('mailman', **{
237 'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
238 'xsi:noNamespaceSchemaLocation': 'ssi-1.0.xsd',
240 for listname in sorted(listnames):
241 try:
242 mlist = MailList(listname, lock=False)
243 except errors.MMUnknownListError:
244 print >> sys.stderr, _('No such list: $listname')
245 continue
246 self._dump_list(mlist)
247 self._pop_element('mailman')
249 def close(self):
250 while self._stack:
251 self._pop_element()
255 def parseargs():
256 parser = optparse.OptionParser(version=MAILMAN_VERSION,
257 usage=_("""\
258 %prog [options]
260 Export the configuration and members of a mailing list in XML format."""))
261 parser.add_option('-o', '--outputfile',
262 metavar='FILENAME', default=None, type='string',
263 help=_("""\
264 Output XML to FILENAME. If not given, or if FILENAME is '-', standard out is
265 used."""))
266 parser.add_option('-l', '--listname',
267 default=[], action='append', type='string',
268 metavar='LISTNAME', dest='listnames', help=_("""\
269 The list to include in the output. If not given, then all mailing lists are
270 included in the XML output. Multiple -l flags may be given."""))
271 parser.add_option('-C', '--config',
272 help=_('Alternative configuration file to use'))
273 opts, args = parser.parse_args()
274 if args:
275 parser.print_help()
276 parser.error(_('Unexpected arguments'))
277 return parser, opts, args
281 def main():
282 parser, opts, args = parseargs()
283 initialize(opts.config)
285 close = False
286 if opts.outputfile in (None, '-'):
287 writer = codecs.getwriter('utf-8')
288 fp = writer(sys.stdout)
289 else:
290 fp = codecs.open(opts.outputfile, 'w', 'utf-8')
291 close = True
293 try:
294 dumper = XMLDumper(fp)
295 if opts.listnames:
296 listnames = []
297 for listname in opts.listnames:
298 if '@' not in listname:
299 listname = '%s@%s' % (listname, config.DEFAULT_EMAIL_HOST)
300 listnames.append(listname)
301 else:
302 listnames = config.list_manager.names
303 dumper.dump(listnames)
304 dumper.close()
305 finally:
306 if close:
307 fp.close()