~mailman-coders/mailman/2.1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
#! @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()