#! @PYTHON@ # # Copyright (C) 1998-2018 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. """Send password reminders for all lists to all users. This program scans all mailing lists and collects users and their passwords, grouped by the list's host_name if mm_cfg.VIRTUAL_HOST_OVERVIEW is true. Then one email message is sent to each unique user (per-virtual host) containing the list passwords and options url for the user. The password reminder comes from the mm_cfg.MAILMAN_SITE_LIST, which must exist. Usage: %(PROGRAM)s [options] Options: -l listname --listname=listname Send password reminders for the named list only. If omitted, reminders are sent for all lists. Multiple -l/--listname options are allowed. -h/--help Print this message and exit. """ # This puppy should probably do lots of logging. import sys import os import errno import getopt from types import UnicodeType import paths # mm_cfg must be imported before the other modules, due to the side-effect of # it hacking sys.paths to include site-packages. Without this, running this # script from cron with python -S will fail. from Mailman import mm_cfg from Mailman import MailList from Mailman import Errors from Mailman import Utils from Mailman import Message from Mailman import i18n from Mailman.Logging.Syslog import syslog # Work around known problems with some RedHat cron daemons import signal signal.signal(signal.SIGCHLD, signal.SIG_DFL) NL = '\n' PROGRAM = sys.argv[0] _ = i18n._ def usage(code, msg=''): if code: fd = sys.stderr else: fd = sys.stdout print >> fd, _(__doc__) if msg: print >> fd, msg sys.exit(code) def tounicode(s, enc): if isinstance(s, UnicodeType): return s return unicode(s, enc, 'replace') def main(): try: opts, args = getopt.getopt(sys.argv[1:], 'l:h', ['listname=', 'help']) except getopt.error, msg: usage(1, msg) if args: usage(1) listnames = None for opt, arg in opts: if opt in ('-h', '--help'): usage(0) if opt in ('-l', '--listname'): if listnames is None: listnames = [arg] else: listnames.append(arg) if listnames is None: listnames = Utils.list_names() # This is the list that all the reminders will look like they come from, # but with the host name coerced to the virtual host we're processing. try: sitelist = MailList.MailList(mm_cfg.MAILMAN_SITE_LIST, lock=0) except Errors.MMUnknownListError: # Do it this way for I18n's _() sitelistname = mm_cfg.MAILMAN_SITE_LIST print >> sys.stderr, _('Site list is missing: %(sitelistname)s') syslog('error', 'Site list is missing: %s', mm_cfg.MAILMAN_SITE_LIST) sys.exit(1) # Group lists by host_name if VIRTUAL_HOST_OVERVIEW is true, otherwise # there's only one key in this dictionary: mm_cfg.DEFAULT_EMAIL_HOST. The # values are lists of the unlocked MailList instances. byhost = {} for listname in listnames: mlist = MailList.MailList(listname, lock=0) if not mlist.send_reminders: continue if mm_cfg.VIRTUAL_HOST_OVERVIEW: host = mlist.host_name else: # See the note in Defaults.py concerning DEFAULT_HOST_NAME # vs. DEFAULT_EMAIL_HOST. host = mm_cfg.DEFAULT_HOST_NAME or mm_cfg.DEFAULT_EMAIL_HOST byhost.setdefault(host, []).append(mlist) # Now for each virtual host, collate the user information. Each user # entry has the form (listaddr, password, optionsurl) for host in byhost.keys(): # Site owner is `mailman@dom.ain' userinfo = {} for mlist in byhost[host]: listaddr = mlist.GetListEmail() for member in mlist.getMembers(): # The user may have disabled reminders for this list if mlist.getMemberOption(member, mm_cfg.SuppressPasswordReminder): continue # Group by the lower-cased address, since Mailman always # treates person@dom.ain the same as PERSON@dom.ain. try: password = mlist.getMemberPassword(member) except Errors.NotAMemberError: # Here's a member with no passwords, which I think was # possible in older versions of Mailman. Log this and # move on. syslog('error', 'password-less member %s for list %s', member, mlist.internal_name()) continue optionsurl = mlist.GetOptionsURL(member) lang = mlist.getMemberLanguage(member) info = (listaddr, password, optionsurl, lang) userinfo.setdefault(member, []).append(info) # Now that we've collected user information for this host, send each # user the password reminder. for addr in userinfo.keys(): # If the person is on more than one list, it is possible that they # have different preferred languages, and there's no good way to # know which one they want their password reminder in. Pick the # most popular, and break the tie randomly. # # Also, we need an example -request address for cronpass.txt and # again, there's no clear winner. Just take the first one in this # case. table = [] langs = {} for listaddr, password, optionsurl, lang in userinfo[addr]: langs[lang] = langs.get(lang, 0) + 1 # If the list address is really long, break it across two # lines. if len(listaddr) > 39: fmt = '%s\n %-10s\n%s\n' else: fmt = '%-40s %-10s\n%s\n' table.append(fmt % (listaddr, password, optionsurl)) # Figure out which language to use langcnt = 0 poplang = None for lang, cnt in langs.items(): if cnt > langcnt: poplang = lang langcnt = cnt enc = Utils.GetCharSet(poplang) # Now we're finally ready to send the email! siteowner = Utils.get_site_email(host, 'owner') sitereq = Utils.get_site_email(host, 'request') sitebounce = Utils.get_site_email(host, 'bounces') text = Utils.maketext( 'cronpass.txt', {'hostname': host, 'useraddr': addr, 'exreq' : sitereq, 'owner' : siteowner, }, lang=poplang) # Coerce everything to Unicode text = tounicode(text, enc) table = [tounicode(_t, enc) for _t in table] # Translate the message and headers to user's suggested lang otrans = i18n.get_translation() try: i18n.set_language(poplang) # Craft table header after language was set header = '%-40s %-10s\n%-40s %-10s' % ( _('List'), _('Password // URL'), '----', '--------') header = tounicode(header, enc) # Add the table to the end so it doesn't get wrapped/filled text += (header + '\n' + NL.join(table)) msg = Message.UserNotification( addr, siteowner, _('%(host)s mailing list memberships reminder'), text.encode(enc, 'replace'), poplang) # Note that text must be encoded into 'enc' because unicode # cause error within email module in some language (Japanese). finally: i18n.set_translation(otrans) msg['X-No-Archive'] = 'yes' del msg['auto-submitted'] msg['Auto-Submitted'] = 'auto-generated' # We want to make this look like it's coming from the siteowner's # list, but we also want to be sure that the apparent host name is # the current virtual host. Look in CookHeaders.py for why this # trick works. Blarg. msg.send(sitelist, **{'errorsto': sitebounce, '_nolist' : 1, 'verp' : mm_cfg.VERP_PASSWORD_REMINDERS, }) if __name__ == '__main__': main()