mailpasswds   [plain text]


#! @PYTHON@
#
# Copyright (C) 1998-2003 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'
            # 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()