1# Copyright (C) 1998-2018 by the Free Software Foundation, Inc.
2#
3# This program is free software; you can redistribute it and/or
4# modify it under the terms of the GNU General Public License
5# as published by the Free Software Foundation; either version 2
6# of the License, or (at your option) any later version.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program; if not, write to the Free Software
15# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
16# USA.
17
18
19"""Routines which rectify an old mailing list with current structure.
20
21The MailList.CheckVersion() method looks for an old .data_version setting in
22the loaded structure, and if found calls the Update() routine from this
23module, supplying the list and the state last loaded from storage.  The state
24is necessary to distinguish from default assignments done in the .InitVars()
25methods, before .CheckVersion() is called.
26
27For new versions you should add sections to the UpdateOldVars() and the
28UpdateOldUsers() sections, to preserve the sense of settings across structural
29changes.  Note that the routines have only one pass - when .CheckVersions()
30finds a version change it runs this routine and then updates the data_version
31number of the list, and then does a .Save(), so the transformations won't be
32run again until another version change is detected.
33"""
34
35
36import email
37
38from types import ListType, StringType
39
40from Mailman import mm_cfg
41from Mailman import Utils
42from Mailman import Message
43from Mailman.Bouncer import _BounceInfo
44from Mailman.MemberAdaptor import UNKNOWN
45from Mailman.Logging.Syslog import syslog
46
47
48
49def Update(l, stored_state):
50    "Dispose of old vars and user options, mapping to new ones when suitable."
51    ZapOldVars(l)
52    UpdateOldUsers(l)
53    NewVars(l)
54    UpdateOldVars(l, stored_state)
55    CanonicalizeUserOptions(l)
56    NewRequestsDatabase(l)
57
58
59
60def ZapOldVars(mlist):
61    for name in ('num_spawns', 'filter_prog', 'clobber_date',
62                 'public_archive_file_dir', 'private_archive_file_dir',
63                 'archive_directory',
64                 # Pre-2.1a4 bounce data
65                 'minimum_removal_date',
66                 'minimum_post_count_before_bounce_action',
67                 'automatic_bounce_action',
68                 'max_posts_between_bounces',
69                 ):
70        if hasattr(mlist, name):
71            delattr(mlist, name)
72
73
74
75uniqueval = []
76def UpdateOldVars(l, stored_state):
77    """Transform old variable values into new ones, deleting old ones.
78    stored_state is last snapshot from file, as opposed to from InitVars()."""
79
80    def PreferStored(oldname, newname, newdefault=uniqueval,
81                     l=l, state=stored_state):
82        """Use specified old value if new value is not in stored state.
83
84        If the old attr does not exist, and no newdefault is specified, the
85        new attr is *not* created - so either specify a default or be positive
86        that the old attr exists - or don't depend on the new attr.
87
88        """
89        if hasattr(l, oldname):
90            if not state.has_key(newname):
91                setattr(l, newname, getattr(l, oldname))
92            delattr(l, oldname)
93        if not hasattr(l, newname) and newdefault is not uniqueval:
94                setattr(l, newname, newdefault)
95
96    def recode(mlist, f, t):
97        """If the character set for a list's preferred_language has changed,
98        attempt to recode old string values into the new character set.
99
100        mlist is the list, f is the old charset and t is the new charset.
101        """
102        for x in dir(mlist):
103            if x.startswith('_'):
104                continue
105            nv = doitem(getattr(mlist, x), f, t)
106            if nv:
107                setattr(mlist, x, nv)
108
109    def doitem(v, f, t):
110        """Recursively process lists, tuples and dictionary values and
111        convert strings as needed. Return either the updated item or None
112        if no change."""
113        changed = False
114        if isinstance(v, str):
115            return convert(v, f, t)
116        elif isinstance(v, list):
117            for i in range(len(v)):
118                nv = doitem(v[i], f, t)
119                if nv:
120                    changed = True
121                    v[i] = nv
122            if changed:
123                return v
124            else:
125                return None
126        elif isinstance(v, tuple):
127            nt = ()
128            for i in range(len(v)):
129                nv = doitem(v[i], f, t)
130                if nv:
131                    changed = True
132                    nt += (nv,)
133                else:
134                    nt += (v[i],)
135            if changed:
136                return nt
137            else:
138                return None
139        elif isinstance(v, dict):
140            for k, ov in v.items():
141                nv = doitem(ov, f, t)
142                if nv:
143                    changed = True
144                    v[k] = nv
145            if changed:
146                return v
147            else:
148                return None
149        else:
150            return None
151
152    def convert(s, f, t):
153        """This does the actual character set conversion of the string s
154        from charset f to charset t."""
155
156        try:
157            u = unicode(s, f)
158            is_f = True
159        except ValueError:
160            is_f = False
161        try:
162            unicode(s, t)
163            is_t = True
164        except ValueError:
165            is_t = False
166        if is_f and not is_t:
167            return u.encode(t, 'replace')
168        else:
169            return None
170
171    # Migrate to 2.1b3, baw 17-Aug-2001
172    if hasattr(l, 'dont_respond_to_post_requests'):
173        oldval = getattr(l, 'dont_respond_to_post_requests')
174        if not hasattr(l, 'respond_to_post_requests'):
175            l.respond_to_post_requests = not oldval
176        del l.dont_respond_to_post_requests
177
178    # Migrate to 2.1b3, baw 13-Oct-2001
179    # Basic defaults for new variables
180    if not hasattr(l, 'default_member_moderation'):
181        l.default_member_moderation = mm_cfg.DEFAULT_DEFAULT_MEMBER_MODERATION
182    if not hasattr(l, 'accept_these_nonmembers'):
183        l.accept_these_nonmembers = []
184    if not hasattr(l, 'hold_these_nonmembers'):
185        l.hold_these_nonmembers = []
186    if not hasattr(l, 'reject_these_nonmembers'):
187        l.reject_these_nonmembers = []
188    if not hasattr(l, 'discard_these_nonmembers'):
189        l.discard_these_nonmembers = []
190    if not hasattr(l, 'forward_auto_discards'):
191        l.forward_auto_discards = mm_cfg.DEFAULT_FORWARD_AUTO_DISCARDS
192    if not hasattr(l, 'generic_nonmember_action'):
193        l.generic_nonmember_action = mm_cfg.DEFAULT_GENERIC_NONMEMBER_ACTION
194    # Now convert what we can...  Note that the interaction between the
195    # MM2.0.x attributes `moderated', `member_posting_only', and `posters' is
196    # so confusing, it makes my brain really ache.  Which is why they go away
197    # in MM2.1.  I think the best we can do semantically is the following:
198    #
199    # - If moderated == yes, then any sender who's address is not on the
200    #   posters attribute would get held for approval.  If the sender was on
201    #   the posters list, then we'd defer judgement to a later step
202    # - If member_posting_only == yes, then members could post without holds,
203    #   and if there were any addresses added to posters, they could also post
204    #   without holds.
205    # - If member_posting_only == no, then what happens depends on the value
206    #   of the posters attribute:
207    #       o If posters was empty, then anybody can post without their
208    #         message being held for approval
209    #       o If posters was non-empty, then /only/ those addresses could post
210    #         without approval, i.e. members not on posters would have their
211    #         messages held for approval.
212    #
213    # How to translate this mess to MM2.1 values?  I'm sure I got this wrong
214    # before, but here's how we're going to do it, as of MM2.1b3.
215    #
216    # - We'll control member moderation through their Moderate flag, and
217    #   non-member moderation through the generic_nonmember_action,
218    #   hold_these_nonmembers, and accept_these_nonmembers.
219    # - If moderated == yes then we need to troll through the addresses on
220    #   posters, and any non-members would get added to
221    #   accept_these_nonmembers.  /Then/ we need to troll through the
222    #   membership and any member on posters would get their Moderate flag
223    #   unset, while members not on posters would get their Moderate flag set.
224    #   Then generic_nonmember_action gets set to 1 (hold) so nonmembers get
225    #   moderated, and default_member_moderation will be set to 1 (hold) so
226    #   new members will also get held for moderation.  We'll stop here.
227    # - We only get to here if moderated == no.
228    # - If member_posting_only == yes, then we'll turn off the Moderate flag
229    #   for members.  We troll through the posters attribute and add all those
230    #   addresses to accept_these_nonmembers.  We'll also set
231    #   generic_nonmember_action to 1 and default_member_moderation to 0.
232    #   We'll stop here.
233    # - We only get to here if member_posting_only == no
234    # - If posters is empty, then anybody could post without being held for
235    #   approval, so we'll set generic_nonmember_action to 0 (accept), and
236    #   we'll turn off the Moderate flag for all members.  We'll also turn off
237    #   default_member_moderation so new members can post without approval.
238    #   We'll stop here.
239    # - We only get here if posters is non-empty.
240    # - This means that /only/ the addresses on posters got to post without
241    #   being held for approval.  So first, we troll through posters and add
242    #   all non-members to accept_these_nonmembers.  Then we troll through the
243    #   membership and if their address is on posters, we'll clear their
244    #   Moderate flag, otherwise we'll set it.  We'll turn on
245    #   default_member_moderation so new members get moderated.  We'll set
246    #   generic_nonmember_action to 1 (hold) so all other non-members will get
247    #   moderated.  And I think we're finally done.
248    #
249    # SIGH.
250    if hasattr(l, 'moderated'):
251        # We'll assume we're converting all these attributes at once
252        if l.moderated:
253            #syslog('debug', 'Case 1')
254            for addr in l.posters:
255                if not l.isMember(addr):
256                    l.accept_these_nonmembers.append(addr)
257            for member in l.getMembers():
258                l.setMemberOption(member, mm_cfg.Moderate,
259                                  # reset for explicitly named members
260                                  member not in l.posters)
261            l.generic_nonmember_action = 1
262            l.default_member_moderation = 1
263        elif l.member_posting_only:
264            #syslog('debug', 'Case 2')
265            for addr in l.posters:
266                if not l.isMember(addr):
267                    l.accept_these_nonmembers.append(addr)
268            for member in l.getMembers():
269                l.setMemberOption(member, mm_cfg.Moderate, 0)
270            l.generic_nonmember_action = 1
271            l.default_member_moderation = 0
272        elif not l.posters:
273            #syslog('debug', 'Case 3')
274            for member in l.getMembers():
275                l.setMemberOption(member, mm_cfg.Moderate, 0)
276            l.generic_nonmember_action = 0
277            l.default_member_moderation = 0
278        else:
279            #syslog('debug', 'Case 4')
280            for addr in l.posters:
281                if not l.isMember(addr):
282                    l.accept_these_nonmembers.append(addr)
283            for member in l.getMembers():
284                l.setMemberOption(member, mm_cfg.Moderate,
285                                  # reset for explicitly named members
286                                  member not in l.posters)
287            l.generic_nonmember_action = 1
288            l.default_member_moderation = 1
289        # Now get rid of the old attributes
290        del l.moderated
291        del l.posters
292        del l.member_posting_only
293    if hasattr(l, 'forbidden_posters'):
294        # For each of the posters on this list, if they are members, toggle on
295        # their moderation flag.  If they are not members, then add them to
296        # hold_these_nonmembers.
297        forbiddens = l.forbidden_posters
298        for addr in forbiddens:
299            if l.isMember(addr):
300                l.setMemberOption(addr, mm_cfg.Moderate, 1)
301            else:
302                l.hold_these_nonmembers.append(addr)
303        del l.forbidden_posters
304
305    # Migrate to 1.0b6, klm 10/22/1998:
306    PreferStored('reminders_to_admins', 'umbrella_list',
307                 mm_cfg.DEFAULT_UMBRELLA_LIST)
308
309    # Migrate up to 1.0b5:
310    PreferStored('auto_subscribe', 'open_subscribe')
311    PreferStored('closed', 'private_roster')
312    PreferStored('mimimum_post_count_before_removal',
313                 'mimimum_post_count_before_bounce_action')
314    PreferStored('bad_posters', 'forbidden_posters')
315    PreferStored('automatically_remove', 'automatic_bounce_action')
316    if hasattr(l, "open_subscribe"):
317        if l.open_subscribe:
318            if mm_cfg.ALLOW_OPEN_SUBSCRIBE:
319                l.subscribe_policy = 0
320            else:
321                l.subscribe_policy = 1
322        else:
323            l.subscribe_policy = 2      # admin approval
324        delattr(l, "open_subscribe")
325    if not hasattr(l, "administrivia"):
326        setattr(l, "administrivia", mm_cfg.DEFAULT_ADMINISTRIVIA)
327    if not hasattr(l, "admin_member_chunksize"):
328        setattr(l, "admin_member_chunksize",
329                mm_cfg.DEFAULT_ADMIN_MEMBER_CHUNKSIZE)
330    #
331    # this attribute was added then deleted, so there are a number of
332    # cases to take care of
333    #
334    if hasattr(l, "posters_includes_members"):
335        if l.posters_includes_members:
336            if l.posters:
337                l.member_posting_only = 1
338        else:
339            if l.posters:
340                l.member_posting_only = 0
341        delattr(l, "posters_includes_members")
342    elif l.data_version <= 10 and l.posters:
343        # make sure everyone gets the behavior the list used to have, but only
344        # for really old versions of Mailman (1.0b5 or before).  Any newer
345        # version of Mailman should not get this attribute whacked.
346        l.member_posting_only = 0
347    #
348    # transfer the list data type for holding members and digest members
349    # to the dict data type starting file format version 11
350    #
351    if type(l.members) is ListType:
352        members = {}
353        for m in l.members:
354            members[m] = 1
355        l.members = members
356    if type(l.digest_members) is ListType:
357        dmembers = {}
358        for dm in l.digest_members:
359            dmembers[dm] = 1
360        l.digest_members = dmembers
361    #
362    # set admin_notify_mchanges
363    #
364    if not hasattr(l, "admin_notify_mchanges"):
365        setattr(l, "admin_notify_mchanges",
366                mm_cfg.DEFAULT_ADMIN_NOTIFY_MCHANGES)
367    #
368    # Convert the members and digest_members addresses so that the keys of
369    # both these are always lowercased, but if there is a case difference, the
370    # value contains the case preserved value
371    #
372    for k in l.members.keys():
373        if k.lower() <> k:
374            l.members[k.lower()] = Utils.LCDomain(k)
375            del l.members[k]
376        elif type(l.members[k]) == StringType and k == l.members[k].lower():
377            # already converted
378            pass
379        else:
380            l.members[k] = 0
381    for k in l.digest_members.keys():
382        if k.lower() <> k:
383            l.digest_members[k.lower()] = Utils.LCDomain(k)
384            del l.digest_members[k]
385        elif type(l.digest_members[k]) == StringType and \
386                 k == l.digest_members[k].lower():
387            # already converted
388            pass
389        else:
390            l.digest_members[k] = 0
391    #
392    # Convert pre 2.2 topics regexps which were compiled in verbose mode
393    # to a non-verbose equivalent.
394    #
395    if stored_state['data_version'] < 106 and stored_state.has_key('topics'):
396        l.topics = []
397        for name, pattern, description, emptyflag in stored_state['topics']:
398            pattern = Utils.strip_verbose_pattern(pattern)
399            l.topics.append((name, pattern, description, emptyflag))
400    #
401    # Romanian and Russian had their character sets changed in 2.1.19
402    # to utf-8. If there are any strings in the old encoding, try to recode
403    # them.
404    #
405    if stored_state['data_version'] < 108:
406        if l.preferred_language == 'ro':
407            if Utils.GetCharSet('ro') == 'utf-8':
408                recode(l, 'iso-8859-2', 'utf-8')
409        if l.preferred_language == 'ru':
410            if Utils.GetCharSet('ru') == 'utf-8':
411                recode(l, 'koi8-r', 'utf-8')
412    #
413    # from_is_list was called author_is_list in 2.1.16rc2 (only).
414    PreferStored('author_is_list', 'from_is_list',
415                 mm_cfg.DEFAULT_FROM_IS_LIST)
416
417
418
419def NewVars(l):
420    """Add defaults for these new variables if they don't exist."""
421    def add_only_if_missing(attr, initval, l=l):
422        if not hasattr(l, attr):
423            setattr(l, attr, initval)
424    # 1.2 beta 1, baw 18-Feb-2000
425    # Autoresponder mixin class attributes
426    add_only_if_missing('autorespond_postings', 0)
427    add_only_if_missing('autorespond_admin', 0)
428    add_only_if_missing('autorespond_requests', 0)
429    add_only_if_missing('autoresponse_postings_text', '')
430    add_only_if_missing('autoresponse_admin_text', '')
431    add_only_if_missing('autoresponse_request_text', '')
432    add_only_if_missing('autoresponse_graceperiod', 90)
433    add_only_if_missing('postings_responses', {})
434    add_only_if_missing('admin_responses', {})
435    add_only_if_missing('reply_goes_to_list', '')
436    add_only_if_missing('preferred_language', mm_cfg.DEFAULT_SERVER_LANGUAGE)
437    add_only_if_missing('available_languages', [])
438    add_only_if_missing('digest_volume_frequency',
439                        mm_cfg.DEFAULT_DIGEST_VOLUME_FREQUENCY)
440    add_only_if_missing('digest_last_sent_at', 0)
441    add_only_if_missing('mod_password', None)
442    add_only_if_missing('post_password', None)
443    add_only_if_missing('moderator', [])
444    add_only_if_missing('topics', [])
445    add_only_if_missing('topics_enabled', 0)
446    add_only_if_missing('topics_bodylines_limit', 5)
447    add_only_if_missing('one_last_digest', {})
448    add_only_if_missing('usernames', {})
449    add_only_if_missing('personalize', 0)
450    add_only_if_missing('first_strip_reply_to',
451                        mm_cfg.DEFAULT_FIRST_STRIP_REPLY_TO)
452    add_only_if_missing('subscribe_auto_approval',
453                        mm_cfg.DEFAULT_SUBSCRIBE_AUTO_APPROVAL)
454    add_only_if_missing('unsubscribe_policy',
455                        mm_cfg.DEFAULT_UNSUBSCRIBE_POLICY)
456    add_only_if_missing('send_goodbye_msg', mm_cfg.DEFAULT_SEND_GOODBYE_MSG)
457    add_only_if_missing('include_rfc2369_headers', 1)
458    add_only_if_missing('include_list_post_header', 1)
459    add_only_if_missing('include_sender_header', 1)
460    add_only_if_missing('bounce_score_threshold',
461                        mm_cfg.DEFAULT_BOUNCE_SCORE_THRESHOLD)
462    add_only_if_missing('bounce_info_stale_after',
463                        mm_cfg.DEFAULT_BOUNCE_INFO_STALE_AFTER)
464    add_only_if_missing('bounce_you_are_disabled_warnings',
465                        mm_cfg.DEFAULT_BOUNCE_YOU_ARE_DISABLED_WARNINGS)
466    add_only_if_missing(
467        'bounce_you_are_disabled_warnings_interval',
468        mm_cfg.DEFAULT_BOUNCE_YOU_ARE_DISABLED_WARNINGS_INTERVAL)
469    add_only_if_missing(
470        'bounce_unrecognized_goes_to_list_owner',
471        mm_cfg.DEFAULT_BOUNCE_UNRECOGNIZED_GOES_TO_LIST_OWNER)
472    add_only_if_missing(
473        'bounce_notify_owner_on_bounce_increment',
474        mm_cfg.DEFAULT_BOUNCE_NOTIFY_OWNER_ON_BOUNCE_INCREMENT)
475    add_only_if_missing(
476        'bounce_notify_owner_on_disable',
477        mm_cfg.DEFAULT_BOUNCE_NOTIFY_OWNER_ON_DISABLE)
478    add_only_if_missing(
479        'bounce_notify_owner_on_removal',
480        mm_cfg.DEFAULT_BOUNCE_NOTIFY_OWNER_ON_REMOVAL)
481    add_only_if_missing('ban_list', [])
482    add_only_if_missing('filter_mime_types', mm_cfg.DEFAULT_FILTER_MIME_TYPES)
483    add_only_if_missing('pass_mime_types', mm_cfg.DEFAULT_PASS_MIME_TYPES)
484    add_only_if_missing('filter_content', mm_cfg.DEFAULT_FILTER_CONTENT)
485    add_only_if_missing('convert_html_to_plaintext',
486                        mm_cfg.DEFAULT_CONVERT_HTML_TO_PLAINTEXT)
487    add_only_if_missing('filter_action', mm_cfg.DEFAULT_FILTER_ACTION)
488    add_only_if_missing('delivery_status', {})
489    # This really ought to default to mm_cfg.HOLD, but that doesn't work with
490    # the current GUI description model.  So, 0==Hold, 1==Reject, 2==Discard
491    add_only_if_missing('member_moderation_action', 0)
492    add_only_if_missing('member_moderation_notice', '')
493    add_only_if_missing('dmarc_moderation_action',
494                       mm_cfg.DEFAULT_DMARC_MODERATION_ACTION)
495    add_only_if_missing('dmarc_quarantine_moderation_action',
496                       mm_cfg.DEFAULT_DMARC_QUARANTINE_MODERATION_ACTION)
497    add_only_if_missing('dmarc_none_moderation_action',
498                       mm_cfg.DEFAULT_DMARC_NONE_MODERATION_ACTION)
499    add_only_if_missing('dmarc_moderation_notice', '')
500    add_only_if_missing('dmarc_moderation_addresses', [])
501    add_only_if_missing('dmarc_wrapped_message_text',
502                       mm_cfg.DEFAULT_DMARC_WRAPPED_MESSAGE_TEXT)
503    add_only_if_missing('member_verbosity_threshold',
504                       mm_cfg.DEFAULT_MEMBER_VERBOSITY_THRESHOLD)
505    add_only_if_missing('member_verbosity_interval',
506                       mm_cfg.DEFAULT_MEMBER_VERBOSITY_INTERVAL)
507    add_only_if_missing('equivalent_domains',
508                       mm_cfg.DEFAULT_EQUIVALENT_DOMAINS)
509    add_only_if_missing('new_member_options',
510                        mm_cfg.DEFAULT_NEW_MEMBER_OPTIONS)
511    add_only_if_missing('drop_cc', mm_cfg.DEFAULT_DROP_CC)
512    # Emergency moderation flag
513    add_only_if_missing('emergency', 0)
514    add_only_if_missing('hold_and_cmd_autoresponses', {})
515    add_only_if_missing('news_prefix_subject_too', 1)
516    # Should prefixes be encoded?
517    if Utils.GetCharSet(l.preferred_language) == 'us-ascii':
518        encode = 0
519    else:
520        encode = 2
521    add_only_if_missing('encode_ascii_prefixes', encode)
522    add_only_if_missing('news_moderation', 0)
523    add_only_if_missing('header_filter_rules', [])
524    # Scrubber in regular delivery
525    add_only_if_missing('scrub_nondigest', 0)
526    # ContentFilter by file extensions
527    add_only_if_missing('filter_filename_extensions',
528                        mm_cfg.DEFAULT_FILTER_FILENAME_EXTENSIONS)
529    add_only_if_missing('pass_filename_extensions', [])
530    # automatic discard
531    add_only_if_missing('max_days_to_hold', 0)
532    add_only_if_missing('nonmember_rejection_notice', '')
533    # multipart/alternative collapse
534    add_only_if_missing('collapse_alternatives',
535                        mm_cfg.DEFAULT_COLLAPSE_ALTERNATIVES)
536    # exclude/include lists
537    add_only_if_missing('regular_exclude_lists',
538                        mm_cfg.DEFAULT_REGULAR_EXCLUDE_LISTS)
539    add_only_if_missing('regular_include_lists',
540                        mm_cfg.DEFAULT_REGULAR_INCLUDE_LISTS)
541    add_only_if_missing('regular_exclude_ignore',
542                        mm_cfg.DEFAULT_REGULAR_EXCLUDE_IGNORE)
543
544
545
546def UpdateOldUsers(mlist):
547    """Transform sense of changed user options."""
548    # pre-1.0b11 to 1.0b11.  Force all keys in l.passwords to be lowercase
549    passwords = {}
550    for k, v in mlist.passwords.items():
551        passwords[k.lower()] = v
552    mlist.passwords = passwords
553    # Go through all the keys in bounce_info.  If the key is not a member, or
554    # if the data is not a _BounceInfo instance, chuck the bounce info.  We're
555    # doing things differently now.
556    for m in mlist.bounce_info.keys():
557        if not mlist.isMember(m) or not isinstance(mlist.getBounceInfo(m),
558                                                   _BounceInfo):
559            del mlist.bounce_info[m]
560
561
562
563def CanonicalizeUserOptions(l):
564    """Fix up the user options."""
565    # I want to put a flag in the list database which tells this routine to
566    # never try to canonicalize the user options again.
567    if getattr(l, 'useropts_version', 0) > 0:
568        return
569    # pre 1.0rc2 to 1.0rc3.  For all keys in l.user_options to be lowercase,
570    # but merge options for both cases
571    options = {}
572    for k, v in l.user_options.items():
573        if k is None:
574            continue
575        lcuser = k.lower()
576        flags = 0
577        if options.has_key(lcuser):
578            flags = options[lcuser]
579        flags |= v
580        options[lcuser] = flags
581    l.user_options = options
582    # 2.1alpha3 -> 2.1alpha4.  The DisableDelivery flag is now moved into
583    # get/setDeilveryStatus().  This must be done after the addresses are
584    # canonicalized.
585    for k, v in l.user_options.items():
586        if not l.isMember(k):
587            # There's a key in user_options that isn't associated with a real
588            # member address.  This is likely caused by an earlier bug.
589            del l.user_options[k]
590            continue
591        if l.getMemberOption(k, mm_cfg.DisableDelivery):
592            # Convert this flag into a legacy disable
593            l.setDeliveryStatus(k, UNKNOWN)
594            l.setMemberOption(k, mm_cfg.DisableDelivery, 0)
595    l.useropts_version = 1
596
597
598
599def NewRequestsDatabase(l):
600    """With version 1.2, we use a new pending request database schema."""
601    r = getattr(l, 'requests', {})
602    if not r:
603        # no old-style requests
604        return
605    for k, v in r.items():
606        if k == 'post':
607            # This is a list of tuples with the following format
608            #
609            # a sequential request id integer
610            # a timestamp float
611            # a message tuple: (author-email-str, message-text-str)
612            # a reason string
613            # the subject string
614            #
615            # We'll re-submit this as a new HoldMessage request, but we'll
616            # blow away the original timestamp and request id.  This means the
617            # request will live a little longer than it possibly should have,
618            # but that's no big deal.
619            for p in v:
620                author, text = p[2]
621                reason = p[3]
622                msg = email.message_from_string(text, Message.Message)
623                l.HoldMessage(msg, reason)
624            del r[k]
625        elif k == 'add_member':
626            # This is a list of tuples with the following format
627            #
628            # a sequential request id integer
629            # a timestamp float
630            # a digest flag (0 == nodigest, 1 == digest)
631            # author-email-str
632            # password
633            #
634            # See the note above; the same holds true.
635            for ign, ign, digest, addr, password in v:
636                l.HoldSubscription(addr, '', password, digest,
637                                   mm_cfg.DEFAULT_SERVER_LANGUAGE)
638            del r[k]
639        else:
640            syslog('error', """\
641VERY BAD NEWS.  Unknown pending request type `%s' found for list: %s""",
642                   k, l.internal_name())
643