@@ -27,6 +27,11 @@ Options:
2727 of the installed Mailman matches the current version number (or a
2828 `downgrade' is detected), nothing will be done.
2929
30+ -n/--dry-run
31+ Show what would be done without actually making changes. When checking
32+ for password upgrades, this will show which lists need upgrades but
33+ will not send email notifications to list administrators.
34+
3035 -h/--help
3136 Print this text and exit.
3237
@@ -55,6 +60,7 @@ from Mailman import Message
5560from Mailman import Pending
5661from Mailman.LockFile import TimeOutError
5762from Mailman.i18n import C_
63+ import Mailman.i18n as i18n
5864from Mailman.Queue.Switchboard import Switchboard
5965from Mailman.OldStyleMemberships import OldStyleMemberships
6066from Mailman.MemberAdaptor import BYBOUNCE, ENABLED
@@ -663,7 +669,158 @@ def update_pending():
663669
664670
665671
666- def main():
672+ def check_and_notify_password_upgrades(dry_run=False):
673+ """Check for old password formats and notify list admins to login and upgrade.
674+
675+ This function identifies lists with old SHA1 password hashes and sends
676+ email notifications to list administrators asking them to login, which
677+ will trigger automatic password upgrades to the new PBKDF2 format.
678+
679+ Args:
680+ dry_run: If True, show what would be done but don't send emails
681+ """
682+ from Mailman.Utils import is_old_password_format
683+
684+ print('Checking for lists with old password formats...')
685+ lists_needing_upgrade = []
686+
687+ # Check all lists for old password formats
688+ listnames = Utils.list_names()
689+ for listname in listnames:
690+ try:
691+ mlist = MailList.MailList(listname, lock=0)
692+ except Exception as e:
693+ print(C_('Warning: Could not check list %(listname)s: %(e)s') % {
694+ 'listname': listname, 'e': e})
695+ continue
696+
697+ needs_upgrade = False
698+ # Check list admin password
699+ if mlist.password and is_old_password_format(mlist.password):
700+ needs_upgrade = True
701+ # Check moderator password
702+ if mlist.mod_password and is_old_password_format(mlist.mod_password):
703+ needs_upgrade = True
704+ # Check poster password
705+ if mlist.post_password and is_old_password_format(mlist.post_password):
706+ needs_upgrade = True
707+
708+ if needs_upgrade:
709+ lists_needing_upgrade.append(mlist)
710+
711+ # Check global passwords
712+ global_passwords_old = []
713+ site_pw = Utils.get_global_password(siteadmin=True)
714+ if site_pw and is_old_password_format(site_pw):
715+ global_passwords_old.append(('site admin', True))
716+ creator_pw = Utils.get_global_password(siteadmin=False)
717+ if creator_pw and is_old_password_format(creator_pw):
718+ global_passwords_old.append(('list creator', False))
719+
720+ # Send notifications if needed
721+ if lists_needing_upgrade or global_passwords_old:
722+ print(C_('Found %(count)d lists with old password formats that need upgrading.') % {
723+ 'count': len(lists_needing_upgrade)})
724+ if global_passwords_old:
725+ print(C_('Also found %(count)d global password(s) with old format.') % {
726+ 'count': len(global_passwords_old)})
727+
728+ # Send emails to list owners (unless dry-run)
729+ if dry_run:
730+ print(C_('DRY-RUN: Would send password upgrade notifications to %(count)d list(s).') % {
731+ 'count': len(lists_needing_upgrade)})
732+ for mlist in lists_needing_upgrade:
733+ print(C_(' - Would notify owners of %(listname)s (%(listaddr)s)') % {
734+ 'listname': mlist.internal_name(),
735+ 'listaddr': mlist.GetListEmail()})
736+ else:
737+ for mlist in lists_needing_upgrade:
738+ send_password_upgrade_notification(mlist)
739+
740+ # Note about global passwords
741+ if global_passwords_old:
742+ print(C_('Note: Global passwords will be automatically upgraded when used.'))
743+ for pw_type, siteadmin in global_passwords_old:
744+ print(C_(' - %(pw_type)s password needs upgrade (will upgrade on next use)') % {
745+ 'pw_type': pw_type})
746+ else:
747+ print('All passwords are already in the new format.')
748+
749+
750+ def send_password_upgrade_notification(mlist):
751+ """Send an email to list administrators asking them to login to upgrade passwords.
752+
753+ Args:
754+ mlist: The MailList object for the list needing password upgrade
755+ """
756+ # Get list owner addresses
757+ if not mlist.owner:
758+ print(C_('Warning: List %(listname)s has no owners, skipping notification.') % {
759+ 'listname': mlist.internal_name()})
760+ return
761+
762+ # Set up i18n for the list's language
763+ otrans = i18n.get_translation()
764+ i18n.set_language(mlist.preferred_language)
765+
766+ try:
767+ # Create the notification message
768+ admin_url = mlist.GetScriptURL('admin', absolute=1)
769+ listname = mlist.real_name
770+ listaddr = mlist.GetListEmail()
771+ siteowner = Utils.get_site_email(mlist.host_name, 'owner')
772+
773+ text = _("""\
774+ This is an automated message from your Mailman installation.
775+
776+ Your mailing list "%(listname)s" (%(listaddr)s) is using an older password
777+ hashing format. For security reasons, we have upgraded Mailman to use a more
778+ secure password hashing method (PBKDF2-SHA256 instead of SHA1).
779+
780+ To complete the upgrade, please log in to your list administration page:
781+
782+ %(admin_url)s
783+
784+ Simply logging in with your current list administrator password will
785+ automatically upgrade your password to the new secure format. You don't
786+ need to change your password - just log in once and the upgrade will happen
787+ automatically.
788+
789+ This is a one-time upgrade. After you log in, your password will be
790+ automatically converted to the new format and you won't need to do anything
791+ else.
792+
793+ If you have any questions or concerns, please contact the site administrator
794+ at %(siteowner)s.
795+
796+ Thank you for your attention to this important security upgrade.
797+ """) % {
798+ 'listname': listname,
799+ 'listaddr': listaddr,
800+ 'admin_url': admin_url,
801+ 'siteowner': siteowner,
802+ }
803+
804+ subject = _('Action Required: Password Upgrade for %(listname)s') % {
805+ 'listname': listname
806+ }
807+
808+ # Send to all list owners
809+ msg = Message.OwnerNotification(
810+ mlist, subject, text, tomoderators=0)
811+ msg.send(mlist)
812+
813+ print(C_('Sent password upgrade notification to owners of %(listname)s') % {
814+ 'listname': mlist.internal_name()})
815+ except Exception as e:
816+ print(C_('Error sending notification for %(listname)s: %(e)s') % {
817+ 'listname': mlist.internal_name(), 'e': e})
818+ finally:
819+ i18n.set_translation(otrans)
820+
821+
822+
823+ def main(dry_run=False):
667824 errors = 0
668825 # get rid of old stuff
669826 print('getting rid of old source files')
@@ -731,6 +888,8 @@ If your archives are big, this could take a minute or two..."""))
731888 # files from separate .msg (pickled Message objects) and .db (marshalled
732889 # dictionaries) to a shared .pck file containing two pickles.
733890 update_qfiles()
891+ # Check for old password formats and notify list admins
892+ check_and_notify_password_upgrades(dry_run=dry_run)
734893 # This warning was necessary for the upgrade from 1.0b9 to 1.0b10.
735894 # There's no good way of figuring this out for releases prior to 2.0beta2
736895 # :(
@@ -771,20 +930,23 @@ def usage(code, msg=''):
771930
772931if __name__ == '__main__':
773932 try:
774- opts, args = getopt.getopt(sys.argv[1:], 'hf ',
775- ['help', 'force'])
933+ opts, args = getopt.getopt(sys.argv[1:], 'hfn ',
934+ ['help', 'force', 'dry-run' ])
776935 except getopt.error as msg:
777936 usage(1, msg)
778937
779938 if args:
780939 usage(1, 'Unexpected arguments: %s' % args)
781940
782941 force = 0
942+ dry_run = 0
783943 for opt, arg in opts:
784944 if opt in ('-h', '--help'):
785945 usage(0)
786946 elif opt in ('-f', '--force'):
787947 force = 1
948+ elif opt in ('-n', '--dry-run'):
949+ dry_run = 1
788950
789951 # calculate the versions
790952 lastversion, thisversion = calcversions()
@@ -801,7 +963,9 @@ This is probably not safe.
801963Exiting."""))
802964 sys.exit(1)
803965 print(C_('Upgrading from version %(hexlversion)s to %(hextversion)s'))
804- errors = main()
966+ if dry_run:
967+ print(C_('DRY-RUN mode: No changes will be made, emails will not be sent.'))
968+ errors = main(dry_run=dry_run)
805969 if not errors:
806970 # Record the version we just upgraded to
807971 fp = open(LMVFILE, 'w')
0 commit comments