diff options
author | <> | 2003-01-02 05:25:50 +0000 |
---|---|---|
committer | <> | 2003-01-02 05:25:50 +0000 |
commit | b132a73f15e432eaf43310fce9196ca0c0651465 (patch) | |
tree | c15f816ba7c4de99fef510e3bd75af0890d47441 /Mailman/versions.py | |
download | mailman2-b132a73f15e432eaf43310fce9196ca0c0651465.tar.gz mailman2-b132a73f15e432eaf43310fce9196ca0c0651465.tar.xz mailman2-b132a73f15e432eaf43310fce9196ca0c0651465.zip |
This commit was manufactured by cvs2svn to create branch
'Release_2_1-maint'.
Diffstat (limited to '')
-rw-r--r-- | Mailman/versions.py | 495 |
1 files changed, 495 insertions, 0 deletions
diff --git a/Mailman/versions.py b/Mailman/versions.py new file mode 100644 index 00000000..7aa9d9e8 --- /dev/null +++ b/Mailman/versions.py @@ -0,0 +1,495 @@ +# Copyright (C) 1998,1999,2000,2001,2002 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + + +"""Routines which rectify an old mailing list with current structure. + +The MailList.CheckVersion() method looks for an old .data_version setting in +the loaded structure, and if found calls the Update() routine from this +module, supplying the list and the state last loaded from storage. The state +is necessary to distinguish from default assignments done in the .InitVars() +methods, before .CheckVersion() is called. + +For new versions you should add sections to the UpdateOldVars() and the +UpdateOldUsers() sections, to preserve the sense of settings across structural +changes. Note that the routines have only one pass - when .CheckVersions() +finds a version change it runs this routine and then updates the data_version +number of the list, and then does a .Save(), so the transformations won't be +run again until another version change is detected. + +""" + + +from types import ListType, StringType + +from Mailman import mm_cfg +from Mailman import Utils +from Mailman import Message +from Mailman.MemberAdaptor import UNKNOWN +from Mailman.Logging.Syslog import syslog + + + +def Update(l, stored_state): + "Dispose of old vars and user options, mapping to new ones when suitable." + ZapOldVars(l) + UpdateOldUsers(l) + NewVars(l) + UpdateOldVars(l, stored_state) + CanonicalizeUserOptions(l) + NewRequestsDatabase(l) + + + +def ZapOldVars(mlist): + for name in ('num_spawns', 'filter_prog', 'clobber_date', + 'public_archive_file_dir', 'private_archive_file_dir', + 'archive_directory', + # Pre-2.1a4 bounce data + 'minimum_removal_date', + 'minimum_post_count_before_bounce_action', + 'automatic_bounce_action', + 'max_posts_between_bounces', + ): + if hasattr(mlist, name): + delattr(mlist, name) + + + +uniqueval = [] +def UpdateOldVars(l, stored_state): + """Transform old variable values into new ones, deleting old ones. + stored_state is last snapshot from file, as opposed to from InitVars().""" + + def PreferStored(oldname, newname, newdefault=uniqueval, + l=l, state=stored_state): + """Use specified old value if new value is not in stored state. + + If the old attr does not exist, and no newdefault is specified, the + new attr is *not* created - so either specify a default or be positive + that the old attr exists - or don't depend on the new attr. + + """ + if hasattr(l, oldname): + if not state.has_key(newname): + setattr(l, newname, getattr(l, oldname)) + delattr(l, oldname) + if not hasattr(l, newname) and newdefault is not uniqueval: + setattr(l, newname, newdefault) + + # Migrate to 2.1b3, baw 17-Aug-2001 + if hasattr(l, 'dont_respond_to_post_requests'): + oldval = getattr(l, 'dont_respond_to_post_requests') + if not hasattr(l, 'respond_to_post_requests'): + l.respond_to_post_requests = not oldval + del l.dont_respond_to_post_requests + + # Migrate to 2.1b3, baw 13-Oct-2001 + # Basic defaults for new variables + if not hasattr(l, 'default_member_moderation'): + l.default_member_moderation = mm_cfg.DEFAULT_DEFAULT_MEMBER_MODERATION + if not hasattr(l, 'accept_these_nonmembers'): + l.accept_these_nonmembers = [] + if not hasattr(l, 'hold_these_nonmembers'): + l.hold_these_nonmembers = [] + if not hasattr(l, 'reject_these_nonmembers'): + l.reject_these_nonmembers = [] + if not hasattr(l, 'discard_these_nonmembers'): + l.discard_these_nonmembers = [] + if not hasattr(l, 'forward_auto_discards'): + l.forward_auto_discards = mm_cfg.DEFAULT_FORWARD_AUTO_DISCARDS + if not hasattr(l, 'generic_nonmember_action'): + l.generic_nonmember_action = mm_cfg.DEFAULT_GENERIC_NONMEMBER_ACTION + # Now convert what we can... Note that the interaction between the + # MM2.0.x attributes `moderated', `member_posting_only', and `posters' is + # so confusing, it makes my brain really ache. Which is why they go away + # in MM2.1. I think the best we can do semantically is the following: + # + # - If moderated == yes, then any sender who's address is not on the + # posters attribute would get held for approval. If the sender was on + # the posters list, then we'd defer judgement to a later step + # - If member_posting_only == yes, then members could post without holds, + # and if there were any addresses added to posters, they could also post + # without holds. + # - If member_posting_only == no, then what happens depends on the value + # of the posters attribute: + # o If posters was empty, then anybody can post without their + # message being held for approval + # o If posters was non-empty, then /only/ those addresses could post + # without approval, i.e. members not on posters would have their + # messages held for approval. + # + # How to translate this mess to MM2.1 values? I'm sure I got this wrong + # before, but here's how we're going to do it, as of MM2.1b3. + # + # - We'll control member moderation through their Moderate flag, and + # non-member moderation through the generic_nonmember_action, + # hold_these_nonmembers, and accept_these_nonmembers. + # - If moderated == yes then we need to troll through the addresses on + # posters, and any non-members would get added to + # accept_these_nonmembers. /Then/ we need to troll through the + # membership and any member on posters would get their Moderate flag + # unset, while members not on posters would get their Moderate flag set. + # Then generic_nonmember_action gets set to 1 (hold) so nonmembers get + # moderated, and default_member_moderation will be set to 1 (hold) so + # new members will also get held for moderation. We'll stop here. + # - We only get to here if moderated == no. + # - If member_posting_only == yes, then we'll turn off the Moderate flag + # for members. We troll through the posters attribute and add all those + # addresses to accept_these_nonmembers. We'll also set + # generic_nonmember_action to 1 and default_member_moderation to 0. + # We'll stop here. + # - We only get to here if member_posting_only == no + # - If posters is empty, then anybody could post without being held for + # approval, so we'll set generic_nonmember_action to 0 (accept), and + # we'll turn off the Moderate flag for all members. We'll also turn off + # default_member_moderation so new members can post without approval. + # We'll stop here. + # - We only get here if posters is non-empty. + # - This means that /only/ the addresses on posters got to post without + # being held for approval. So first, we troll through posters and add + # all non-members to accept_these_nonmembers. Then we troll through the + # membership and if their address is on posters, we'll clear their + # Moderate flag, otherwise we'll set it. We'll turn on + # default_member_moderation so new members get moderated. We'll set + # generic_nonmember_action to 1 (hold) so all other non-members will get + # moderated. And I think we're finally done. + # + # SIGH. + if hasattr(l, 'moderated'): + # We'll assume we're converting all these attributes at once + if l.moderated: + #syslog('debug', 'Case 1') + for addr in l.posters: + if not l.isMember(addr): + l.accept_these_nonmembers.append(addr) + for member in l.getMembers(): + l.setMemberOption(member, mm_cfg.Moderate, + # reset for explicitly named members + member not in l.posters) + l.generic_nonmember_action = 1 + l.default_member_moderation = 1 + elif l.member_posting_only: + #syslog('debug', 'Case 2') + for addr in l.posters: + if not l.isMember(addr): + l.accept_these_nonmembers.append(addr) + for member in l.getMembers(): + l.setMemberOption(member, mm_cfg.Moderate, 0) + l.generic_nonmember_action = 1 + l.default_member_moderation = 0 + elif not l.posters: + #syslog('debug', 'Case 3') + for member in l.getMembers(): + l.setMemberOption(member, mm_cfg.Moderate, 0) + l.generic_nonmember_action = 0 + l.default_member_moderation = 0 + else: + #syslog('debug', 'Case 4') + for addr in l.posters: + if not l.isMember(addr): + l.accept_these_nonmembers.append(addr) + for member in l.getMembers(): + l.setMemberOption(member, mm_cfg.Moderate, + # reset for explicitly named members + member not in l.posters) + l.generic_nonmember_action = 1 + l.default_member_moderation = 1 + # Now get rid of the old attributes + del l.moderated + del l.posters + del l.member_posting_only + if hasattr(l, 'forbidden_posters'): + # For each of the posters on this list, if they are members, toggle on + # their moderation flag. If they are not members, then add them to + # hold_these_nonmembers. + forbiddens = l.forbidden_posters + for addr in forbiddens: + if l.isMember(addr): + l.setMemberOption(addr, mm_cfg.Moderate, 1) + else: + l.hold_these_nonmembers.append(addr) + del l.forbidden_posters + + # Migrate to 1.0b6, klm 10/22/1998: + PreferStored('reminders_to_admins', 'umbrella_list', + mm_cfg.DEFAULT_UMBRELLA_LIST) + + # Migrate up to 1.0b5: + PreferStored('auto_subscribe', 'open_subscribe') + PreferStored('closed', 'private_roster') + PreferStored('mimimum_post_count_before_removal', + 'mimimum_post_count_before_bounce_action') + PreferStored('bad_posters', 'forbidden_posters') + PreferStored('automatically_remove', 'automatic_bounce_action') + if hasattr(l, "open_subscribe"): + if l.open_subscribe: + if mm_cfg.ALLOW_OPEN_SUBSCRIBE: + l.subscribe_policy = 0 + else: + l.subscribe_policy = 1 + else: + l.subscribe_policy = 2 # admin approval + delattr(l, "open_subscribe") + if not hasattr(l, "administrivia"): + setattr(l, "administrivia", mm_cfg.DEFAULT_ADMINISTRIVIA) + if not hasattr(l, "admin_member_chunksize"): + setattr(l, "admin_member_chunksize", + mm_cfg.DEFAULT_ADMIN_MEMBER_CHUNKSIZE) + # + # this attribute was added then deleted, so there are a number of + # cases to take care of + # + if hasattr(l, "posters_includes_members"): + if l.posters_includes_members: + if l.posters: + l.member_posting_only = 1 + else: + if l.posters: + l.member_posting_only = 0 + delattr(l, "posters_includes_members") + elif l.data_version <= 10 and l.posters: + # make sure everyone gets the behavior the list used to have, but only + # for really old versions of Mailman (1.0b5 or before). Any newer + # version of Mailman should not get this attribute whacked. + l.member_posting_only = 0 + # + # transfer the list data type for holding members and digest members + # to the dict data type starting file format version 11 + # + if type(l.members) is ListType: + members = {} + for m in l.members: + members[m] = 1 + l.members = members + if type(l.digest_members) is ListType: + dmembers = {} + for dm in l.digest_members: + dmembers[dm] = 1 + l.digest_members = dmembers + # + # set admin_notify_mchanges + # + if not hasattr(l, "admin_notify_mchanges"): + setattr(l, "admin_notify_mchanges", + mm_cfg.DEFAULT_ADMIN_NOTIFY_MCHANGES) + # + # Convert the members and digest_members addresses so that the keys of + # both these are always lowercased, but if there is a case difference, the + # value contains the case preserved value + # + for k in l.members.keys(): + if k.lower() <> k: + l.members[k.lower()] = Utils.LCDomain(k) + del l.members[k] + elif type(l.members[k]) == StringType and k == l.members[k].lower(): + # already converted + pass + else: + l.members[k] = 0 + for k in l.digest_members.keys(): + if k.lower() <> k: + l.digest_members[k.lower()] = Utils.LCDomain(k) + del l.digest_members[k] + elif type(l.digest_members[k]) == StringType and \ + k == l.digest_members[k].lower(): + # already converted + pass + else: + l.digest_members[k] = 0 + + + +def NewVars(l): + """Add defaults for these new variables if they don't exist.""" + def add_only_if_missing(attr, initval, l=l): + if not hasattr(l, attr): + setattr(l, attr, initval) + # 1.2 beta 1, baw 18-Feb-2000 + # Autoresponder mixin class attributes + add_only_if_missing('autorespond_postings', 0) + add_only_if_missing('autorespond_admin', 0) + add_only_if_missing('autorespond_requests', 0) + add_only_if_missing('autoresponse_postings_text', '') + add_only_if_missing('autoresponse_admin_text', '') + add_only_if_missing('autoresponse_request_text', '') + add_only_if_missing('autoresponse_graceperiod', 90) + add_only_if_missing('postings_responses', {}) + add_only_if_missing('admin_responses', {}) + add_only_if_missing('reply_goes_to_list', '') + add_only_if_missing('preferred_language', mm_cfg.DEFAULT_SERVER_LANGUAGE) + add_only_if_missing('available_languages', []) + add_only_if_missing('digest_volume_frequency', + mm_cfg.DEFAULT_DIGEST_VOLUME_FREQUENCY) + add_only_if_missing('digest_last_sent_at', 0) + add_only_if_missing('mod_password', None) + add_only_if_missing('moderator', []) + add_only_if_missing('topics', []) + add_only_if_missing('topics_enabled', 0) + add_only_if_missing('topics_bodylines_limit', 5) + add_only_if_missing('one_last_digest', {}) + add_only_if_missing('usernames', {}) + add_only_if_missing('personalize', 0) + add_only_if_missing('first_strip_reply_to', + mm_cfg.DEFAULT_FIRST_STRIP_REPLY_TO) + add_only_if_missing('unsubscribe_policy', + mm_cfg.DEFAULT_UNSUBSCRIBE_POLICY) + add_only_if_missing('send_goodbye_msg', mm_cfg.DEFAULT_SEND_GOODBYE_MSG) + add_only_if_missing('include_rfc2369_headers', 1) + add_only_if_missing('include_list_post_header', 1) + add_only_if_missing('bounce_score_threshold', + mm_cfg.DEFAULT_BOUNCE_SCORE_THRESHOLD) + add_only_if_missing('bounce_info_stale_after', + mm_cfg.DEFAULT_BOUNCE_INFO_STALE_AFTER) + add_only_if_missing('bounce_you_are_disabled_warnings', + mm_cfg.DEFAULT_BOUNCE_YOU_ARE_DISABLED_WARNINGS) + add_only_if_missing( + 'bounce_you_are_disabled_warnings_interval', + mm_cfg.DEFAULT_BOUNCE_YOU_ARE_DISABLED_WARNINGS_INTERVAL) + add_only_if_missing( + 'bounce_unrecognized_goes_to_list_owner', + mm_cfg.DEFAULT_BOUNCE_UNRECOGNIZED_GOES_TO_LIST_OWNER) + add_only_if_missing( + 'bounce_notify_owner_on_disable', + mm_cfg.DEFAULT_BOUNCE_NOTIFY_OWNER_ON_DISABLE) + add_only_if_missing( + 'bounce_notify_owner_on_removal', + mm_cfg.DEFAULT_BOUNCE_NOTIFY_OWNER_ON_REMOVAL) + add_only_if_missing('ban_list', []) + add_only_if_missing('filter_mime_types', mm_cfg.DEFAULT_FILTER_MIME_TYPES) + add_only_if_missing('pass_mime_types', mm_cfg.DEFAULT_PASS_MIME_TYPES) + add_only_if_missing('filter_content', mm_cfg.DEFAULT_FILTER_CONTENT) + add_only_if_missing('convert_html_to_plaintext', + mm_cfg.DEFAULT_CONVERT_HTML_TO_PLAINTEXT) + add_only_if_missing('filter_action', mm_cfg.DEFAULT_FILTER_ACTION) + add_only_if_missing('delivery_status', {}) + # This really ought to default to mm_cfg.HOLD, but that doesn't work with + # the current GUI description model. So, 0==Hold, 1==Reject, 2==Discard + add_only_if_missing('member_moderation_action', 0) + add_only_if_missing('member_moderation_notice', '') + add_only_if_missing('new_member_options', + mm_cfg.DEFAULT_NEW_MEMBER_OPTIONS) + # Emergency moderation flag + add_only_if_missing('emergency', 0) + add_only_if_missing('hold_and_cmd_autoresponses', {}) + add_only_if_missing('news_prefix_subject_too', 1) + # Should prefixes be encoded? + if Utils.GetCharSet(l.preferred_language) == 'us-ascii': + encode = 0 + else: + encode = 2 + add_only_if_missing('encode_ascii_prefixes', encode) + add_only_if_missing('news_moderation', 0) + + + +def UpdateOldUsers(mlist): + """Transform sense of changed user options.""" + # pre-1.0b11 to 1.0b11. Force all keys in l.passwords to be lowercase + passwords = {} + for k, v in mlist.passwords.items(): + passwords[k.lower()] = v + mlist.passwords = passwords + # Go through all the keys in bounce_info. If the key is not a member, or + # if the data is not a _BounceInfo instance, chuck the bounce info. We're + # doing things differently now. + from Mailman.Bouncer import _BounceInfo + for m in mlist.bounce_info.keys(): + if not mlist.isMember(m) or not isinstance(mlist.getBounceInfo(m), + _BounceInfo): + del mlist.bounce_info[m] + + + +def CanonicalizeUserOptions(l): + """Fix up the user options.""" + # I want to put a flag in the list database which tells this routine to + # never try to canonicalize the user options again. + if getattr(l, 'useropts_version', 0) > 0: + return + # pre 1.0rc2 to 1.0rc3. For all keys in l.user_options to be lowercase, + # but merge options for both cases + options = {} + for k, v in l.user_options.items(): + if k is None: + continue + lcuser = k.lower() + flags = 0 + if options.has_key(lcuser): + flags = options[lcuser] + flags |= v + options[lcuser] = flags + l.user_options = options + # 2.1alpha3 -> 2.1alpha4. The DisableDelivery flag is now moved into + # get/setDeilveryStatus(). This must be done after the addresses are + # canonicalized. + for k, v in l.user_options.items(): + if not l.isMember(k): + # There's a key in user_options that isn't associated with a real + # member address. This is likely caused by an earlier bug. + del l.user_options[k] + continue + if l.getMemberOption(k, mm_cfg.DisableDelivery): + # Convert this flag into a legacy disable + l.setDeliveryStatus(k, UNKNOWN) + l.setMemberOption(k, mm_cfg.DisableDelivery, 0) + l.useropts_version = 1 + + + +def NewRequestsDatabase(l): + """With version 1.2, we use a new pending request database schema.""" + r = getattr(l, 'requests', {}) + if not r: + # no old-style requests + return + for k, v in r.items(): + if k == 'post': + # This is a list of tuples with the following format + # + # a sequential request id integer + # a timestamp float + # a message tuple: (author-email-str, message-text-str) + # a reason string + # the subject string + # + # We'll re-submit this as a new HoldMessage request, but we'll + # blow away the original timestamp and request id. This means the + # request will live a little longer than it possibly should have, + # but that's no big deal. + for p in v: + author, text = p[2] + reason = p[3] + msg = Message.OutgoingMessage(text) + l.HoldMessage(msg, reason) + del r[k] + elif k == 'add_member': + # This is a list of tuples with the following format + # + # a sequential request id integer + # a timestamp float + # a digest flag (0 == nodigest, 1 == digest) + # author-email-str + # password + # + # See the note above; the same holds true. + for ign, ign, digest, addr, password in v: + l.HoldSubscription(addr, password, digest) + del r[k] + else: + syslog('error', """\ +VERY BAD NEWS. Unknown pending request type `%s' found for list: %s""", + k, l.internal_name()) |