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