#! @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()