summaryrefslogtreecommitdiff
path: root/Mailman/bin/import.py
blob: e9171f73a33187608263897f53ef3f313a28b3d4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
# Copyright (C) 2006-2007 by the Free Software Foundation, Inc.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
# USA.

"""Import the XML representation of a mailing list."""

import sys
import codecs
import optparse
import traceback

from xml.dom import minidom
from xml.parsers.expat import ExpatError

from Mailman import Defaults
from Mailman import Errors
from Mailman import MemberAdaptor
from Mailman import Utils
from Mailman import Version
from Mailman.MailList import MailList
from Mailman.i18n import _
from Mailman.initialize import initialize

__i18n_templates__ = True



def nodetext(node):
    # Expect only one TEXT_NODE in the list of children
    for child in node.childNodes:
        if child.nodeType == node.TEXT_NODE:
            return child.data
    return u''


def nodegen(node, *elements):
    for child in node.childNodes:
        if child.nodeType <> minidom.Node.ELEMENT_NODE:
            continue
        if elements and child.tagName not in elements:
            print _('Ignoring unexpected element: $node.tagName')
        else:
            yield child



def parse_config(node):
    config = dict()
    for child in nodegen(node, 'option'):
        name  = child.getAttribute('name')
        if not name:
            print _('Skipping unnamed option')
            continue
        vtype = child.getAttribute('type') or 'string'
        if vtype in ('email_list', 'email_list_ex', 'checkbox'):
            value = []
            for subnode in nodegen(child):
                value.append(nodetext(subnode))
        elif vtype == 'bool':
            value = nodetext(child)
            try:
                value = bool(int(value))
            except ValueError:
                value = {'true' : True,
                         'false': False,
                         }.get(value.lower())
                if value is None:
                    print _('Skipping bad boolean value: $value')
                    continue
        elif vtype == 'radio':
            value = nodetext(child).lower()
            boolval = {'true' : True,
                       'false': False,
                       }.get(value)
            if boolval is None:
                value = int(value)
            else:
                value = boolval
        elif vtype == 'number':
            value = nodetext(child)
            # First try int then float
            try:
                value = int(value)
            except ValueError:
                value = float(value)
        elif vtype in ('header_filter', 'topics'):
            value = []
            fakebltins = dict(__builtins__ = dict(True=True, False=False))
            for subnode in nodegen(child):
                reprstr  = nodetext(subnode)
                # Turn the reprs back into tuples, in a safe way
                tupleval = eval(reprstr, fakebltins)
                value.append(tupleval)
        else:
            value = nodetext(child)
        # And now some special casing :(
        if name == 'new_member_options':
            value = int(nodetext(child))
        config[name] = value
    return config




def parse_roster(node):
    members = []
    for child in nodegen(node, 'member'):
        member = dict()
        member['id'] = mid = child.getAttribute('id')
        if not mid:
            print _('Skipping member with no id')
            continue
        if VERBOSE:
            print _('* Processing member: $mid')
        for subnode in nodegen(child):
            attr = subnode.tagName
            if attr == 'delivery':
                value = (subnode.getAttribute('status'),
                         subnode.getAttribute('delivery'))
            elif attr in ('hide', 'ack', 'notmetoo', 'nodupes', 'nomail'):
                value = {'true' : True,
                         'false': False,
                         }.get(nodetext(subnode).lower(), False)
            elif attr == 'topics':
                value = []
                for subsubnode in nodegen(subnode):
                    value.append(nodetext(subsubnode))
            else:
                value = nodetext(subnode)
            member[attr] = value
        members.append(member)
    return members



def load(fp):
    try:
        doc = minidom.parse(fp)
    except ExpatError:
        print _('Expat error in file: $fp.name')
        traceback.print_exc()
        sys.exit(1)
    doc.normalize()
    # Make sure there's only one top-level <mailman> node
    gen = nodegen(doc, 'mailman')
    top = gen.next()
    try:
        gen.next()
    except StopIteration:
        pass
    else:
        print _('Malformed XML; duplicate <mailman> nodes')
        sys.exit(1)
    all_listdata = []
    for listnode in nodegen(top, 'list'):
        listdata = dict()
        name = listnode.getAttribute('name')
        if VERBOSE:
            print _('Processing list: $name')
        if not name:
            print _('Ignoring malformed <list> node')
            continue
        for child in nodegen(listnode, 'configuration', 'roster'):
            if child.tagName == 'configuration':
                list_config = parse_config(child)
            else:
                assert(child.tagName == 'roster')
                list_roster = parse_roster(child)
        all_listdata.append((name, list_config, list_roster))
    return all_listdata



def create(all_listdata):
    for name, list_config, list_roster in all_listdata:
        fqdn_listname = '%s@%s' % (name, list_config['host_name'])
        if Utils.list_exists(fqdn_listname):
            print _('Skipping already existing list: $fqdn_listname')
            continue
        mlist = MailList()
        try:
            if VERBOSE:
                print _('Creating mailing list: $fqdn_listname')
            mlist.Create(fqdn_listname, list_config['owner'][0],
                         list_config['password'])
        except Errors.BadDomainSpecificationError:
            print _('List is not in a supported domain: $fqdn_listname')
            continue
        # Save the list creation, then unlock and relock the list.  This is so
        # that we use normal SQLAlchemy transactions to manage all the
        # attribute and membership updates.  Without this, no transaction will
        # get committed in the second Save() below and we'll lose all our
        # updates.
        mlist.Save()
        mlist.Unlock()
        mlist.Lock()
        try:
            for option, value in list_config.items():
                # XXX Here's what sucks.  Some properties need to have
                # _setValue() called on the gui component, because those
                # methods do some pre-processing on the values before they're
                # applied to the MailList instance.  But we don't have a good
                # way to find a category and sub-category that a particular
                # property belongs to.  Plus this will probably change.  So
                # for now, we'll just hard code the extra post-processing
                # here.  The good news is that not all _setValue() munging
                # needs to be done -- for example, we've already converted
                # everything to dollar strings.
                if option in ('filter_mime_types', 'pass_mime_types',
                              'filter_filename_extensions',
                              'pass_filename_extensions'):
                    value = value.splitlines()
                setattr(mlist, option, value)
            for member in list_roster:
                mid = member['id']
                if VERBOSE:
                    print _('* Adding member: $mid')
                status, delivery = member['delivery']
                kws = {'password'   : member['password'],
                       'language'   : member['language'],
                       'realname'   : member['realname'],
                       'digest'     : delivery <> 'regular',
                       }
                mlist.addNewMember(mid, **kws)
                status = {'enabled'     : MemberAdaptor.ENABLED,
                          'byuser'      : MemberAdaptor.BYUSER,
                          'byadmin'     : MemberAdaptor.BYADMIN,
                          'bybounce'    : MemberAdaptor.BYBOUNCE,
                          }.get(status, MemberAdaptor.UNKNOWN)
                mlist.setDeliveryStatus(mid, status)
                for opt in ('hide', 'ack', 'notmetoo', 'nodupes', 'nomail'):
                    mlist.setMemberOption(mid,
                                          Defaults.OPTINFO[opt],
                                          member[opt])
                topics = member.get('topics')
                if topics:
                    mlist.setMemberTopics(mid, topics)
            mlist.Save()
        finally:
            mlist.Unlock()



def parseargs():
    parser = optparse.OptionParser(version=Version.MAILMAN_VERSION,
                                   usage=_("""\
%prog [options]

Import the configuration and/or members of a mailing list in XML format.  The
imported mailing list must not already exist.  All mailing lists named in the
XML file are imported, but those that already exist are skipped unless --error
is given."""))
    parser.add_option('-i', '--inputfile',
                      metavar='FILENAME', default=None, type='string',
                      help=_("""\
Input XML from FILENAME.  If not given, or if FILENAME is '-', standard input
is used."""))
    parser.add_option('-p', '--reset-passwords',
                      default=False, action='store_true', help=_("""\
With this option, user passwords in the XML are ignored and are reset to a
random password.  If the generated passwords were not included in the input
XML, they will always be randomly generated."""))
    parser.add_option('-v', '--verbose',
                      default=False, action='store_true',
                      help=_('Produce more verbose output'))
    parser.add_option('-C', '--config',
                      help=_('Alternative configuration file to use'))
    opts, args = parser.parse_args()
    if args:
        parser.print_help()
        parser.error(_('Unexpected arguments'))
    return parser, opts, args



def main():
    global VERBOSE

    parser, opts, args = parseargs()
    initialize(opts.config)
    VERBOSE = opts.verbose

    if opts.inputfile in (None, '-'):
        fp = sys.stdin
    else:
        fp = open(opts.inputfile, 'r')

    try:
        listbags = load(fp)
        create(listbags)
    finally:
        if fp is not sys.stdin:
            fp.close()