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)
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
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."""
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
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',
56 def __init__(self
, fp
, indentwidth
=4):
59 self
._width
= indentwidth
66 assert self
._indent
>= 0
70 self
._fp
.write(self
._indent
* self
._width
* ' ')
75 class XMLDumper(object):
76 def __init__(self
, fp
):
77 self
._fp
= Indenter(fp
)
78 self
._tagbuffer
= None
81 def _makeattrs(self
, tagattrs
):
82 # The attribute values might contain angle brackets. They might also
85 for k
, v
in tagattrs
.items():
90 attrs
.append('%s="%s"' % (k
, v
))
91 return SPACE
.join(attrs
)
93 def _flush(self
, more
=True):
94 if not self
._tagbuffer
:
96 name
, attributes
= self
._tagbuffer
97 self
._tagbuffer
= None
99 attrstr
= ' ' + self
._makeattrs
(attributes
)
103 print >> self
._fp
, '<%s%s>' % (name
, attrstr
)
105 self
._stack
.append(name
)
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
):
112 self
._tagbuffer
= (_name
, _tagattrs
)
114 def _pop_element(self
, _name
):
115 buffered
= bool(self
._tagbuffer
)
116 self
._flush
(more
=False)
118 name
= self
._stack
.pop()
119 assert name
== _name
, 'got: %s, expected: %s' % (_name
, name
)
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
):
127 attrs
= ' ' + self
._makeattrs
(_attributes
)
131 print >> self
._fp
, '<%s%s/>' % (_name
, attrs
)
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
]
142 for data
in info
[1:]:
143 if not isinstance(data
, tuple):
146 # Variable could be volatile
147 if varname
.startswith('_'):
150 # Munge the value based on its type
152 if hasattr(gui
, 'getValue'):
153 value
= gui
.getValue(mlist
, vtype
, varname
, data
[2])
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
)
160 self
._element
('value', v
)
161 self
._pop
_element
('option')
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
)
175 self
._do
_list
_categories
(mlist
, k
)
177 for subcat
in [t
[0] for t
in subcats
]:
178 self
._do
_list
_categories
(mlist
, k
, subcat
)
179 self
._pop
_element
('configuration')
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
)
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
194 status
= mlist
.getDeliveryStatus(member
)
195 if status
== MemberAdaptor
.ENABLED
:
196 attrs
['status'] = 'enabled'
198 attrs
['status'] = 'disabled'
199 attrs
['reason'] = {MemberAdaptor
.BYUSER
: 'byuser',
200 MemberAdaptor
.BYADMIN
: 'byadmin',
201 MemberAdaptor
.BYBOUNCE
: 'bybounce',
202 }.get(mlist
.getDeliveryStatus(member
),
204 if member
in digesters
:
205 if mlist
.getMemberOption(member
, Defaults
.DisableMime
):
206 attrs
['delivery'] = 'plain'
208 attrs
['delivery'] = 'mime'
210 attrs
['delivery'] = 'regular'
211 changed
= mlist
.getDeliveryStatusChangeTime(member
)
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'):
220 value
= mlist
.getMemberOption(member
, flag
)
221 self
._element
(option
, value
)
222 topics
= mlist
.getMemberTopics(member
)
224 self
._element
('topics')
226 self
._push
_element
('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
):
242 mlist
= MailList(listname
, lock
=False)
243 except errors
.MMUnknownListError
:
244 print >> sys
.stderr
, _('No such list: $listname')
246 self
._dump
_list
(mlist
)
247 self
._pop
_element
('mailman')
256 parser
= optparse
.OptionParser(version
=MAILMAN_VERSION
,
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',
264 Output XML to FILENAME. If not given, or if FILENAME is '-', standard out is
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()
276 parser
.error(_('Unexpected arguments'))
277 return parser
, opts
, args
282 parser
, opts
, args
= parseargs()
283 initialize(opts
.config
)
286 if opts
.outputfile
in (None, '-'):
287 writer
= codecs
.getwriter('utf-8')
288 fp
= writer(sys
.stdout
)
290 fp
= codecs
.open(opts
.outputfile
, 'w', 'utf-8')
294 dumper
= XMLDumper(fp
)
297 for listname
in opts
.listnames
:
298 if '@' not in listname
:
299 listname
= '%s@%s' % (listname
, config
.DEFAULT_EMAIL_HOST
)
300 listnames
.append(listname
)
302 listnames
= config
.list_manager
.names
303 dumper
.dump(listnames
)