1#! @PYTHON@
2#
3# Copyright (C) 2001-2018 by the Free Software Foundation, Inc.
4#
5# This program is free software; you can redistribute it and/or
6# modify it under the terms of the GNU General Public License
7# as published by the Free Software Foundation; either version 2
8# of the License, or (at your option) any later version.
9#
10# This program is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with this program; if not, write to the Free Software
17# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
18# USA.
19
20"""Process disabled members, recommended once per day.
21
22This script cruises through every mailing list looking for members whose
23delivery is disabled.  If they have been disabled due to bounces, they will
24receive another notification, or they may be removed if they've received the
25maximum number of notifications.
26
27Use the --byadmin, --byuser, and --unknown flags to also send notifications to
28members whose accounts have been disabled for those reasons.  Use --all to
29send the notification to all disabled members.
30
31Usage: %(PROGRAM)s [options]
32
33Options:
34    -h / --help
35        Print this message and exit.
36
37    -o / --byadmin
38        Also send notifications to any member disabled by the list
39        owner/administrator.
40
41    -m / --byuser
42        Also send notifications to any member disabled by themselves.
43
44    -u / --unknown
45        Also send notifications to any member disabled for unknown reasons
46        (usually a legacy disabled address).
47
48    -b / --notbybounce
49        Don't send notifications to members disabled because of bounces (the
50        default is to notify bounce disabled members).
51
52    -a / --all
53        Send notifications to all disabled members.
54
55    -f / --force
56        Send notifications to disabled members even if they're not due a new
57        notification yet.
58
59    -l listname
60    --listname=listname
61        Process only the given list, otherwise do all lists.
62"""
63
64import sys
65import time
66import getopt
67
68import paths
69# mm_cfg must be imported before the other modules, due to the side-effect of
70# it hacking sys.paths to include site-packages.  Without this, running this
71# script from cron with python -S will fail.
72from Mailman import mm_cfg
73from Mailman import Utils
74from Mailman import MailList
75from Mailman import Pending
76from Mailman import MemberAdaptor
77from Mailman import Errors
78from Mailman.Bouncer import _BounceInfo
79from Mailman.Logging.Syslog import syslog
80from Mailman.i18n import _
81
82# Work around known problems with some RedHat cron daemons
83import signal
84signal.signal(signal.SIGCHLD, signal.SIG_DFL)
85
86PROGRAM = sys.argv[0]
87
88
89
90def usage(code, msg=''):
91    if code:
92        fd = sys.stderr
93    else:
94        fd = sys.stdout
95    print >> fd, _(__doc__)
96    if msg:
97        print >> fd, msg
98    sys.exit(code)
99
100
101
102def main():
103    try:
104        opts, args = getopt.getopt(
105            sys.argv[1:], 'hl:omubaf',
106            ['byadmin', 'byuser', 'unknown', 'notbybounce', 'all',
107             'listname=', 'help', 'force'])
108    except getopt.error, msg:
109        usage(1, msg)
110
111    if args:
112        usage(1)
113
114    force = 0
115    listnames = []
116    who = [MemberAdaptor.BYBOUNCE]
117    for opt, arg in opts:
118        if opt in ('-h', '--help'):
119            usage(0)
120        elif opt in ('-l', '--list'):
121            listnames.append(arg)
122        elif opt in ('-o', '--byadmin'):
123            who.append(MemberAdaptor.BYADMIN)
124        elif opt in ('-m', '--byuser'):
125            who.append(MemberAdaptor.BYUSER)
126        elif opt in ('-u', '--unknown'):
127            who.append(MemberAdaptor.UNKNOWN)
128        elif opt in ('-b', '--notbybounce'):
129            try:
130                who.remove(MemberAdaptor.BYBOUNCE)
131            except ValueError:
132                # Already removed
133                pass
134        elif opt in ('-a', '--all'):
135            who = [MemberAdaptor.BYBOUNCE, MemberAdaptor.BYADMIN,
136                   MemberAdaptor.BYUSER, MemberAdaptor.UNKNOWN]
137        elif opt in ('-f', '--force'):
138            force = 1
139
140    who = tuple(who)
141
142    if not listnames:
143        listnames = Utils.list_names()
144
145    msg = _('[disabled by periodic sweep and cull, no message available]')
146    today = time.mktime(time.localtime()[:3] + (0,) * 6)
147    for listname in listnames:
148        # List of members to notify
149        notify = []
150        mlist = MailList.MailList(listname)
151        try:
152            interval = mlist.bounce_you_are_disabled_warnings_interval
153            # Find all the members who are currently bouncing and see if
154            # they've reached the disable threshold but haven't yet been
155            # disabled.  This is a sweep through the membership catching
156            # situations where they've bounced a bunch, then the list admin
157            # lowered the threshold, but we haven't (yet) seen more bounces
158            # from the member.
159            disables = []
160            for member in mlist.getBouncingMembers():
161                if mlist.getDeliveryStatus(member) <> MemberAdaptor.ENABLED:
162                    continue
163                info = mlist.getBounceInfo(member)
164                if (Utils.midnight(info.date) + mlist.bounce_info_stale_after
165                        < Utils.midnight()):
166                    # Bounce info is stale; reset it.
167                    mlist.setBounceInfo(member, None)
168                    continue
169                if info.score >= mlist.bounce_score_threshold:
170                    disables.append((member, info))
171            if disables:
172                for member, info in disables:
173                    mlist.disableBouncingMember(member, info, msg)
174            # Go through all the members who have delivery disabled, and find
175            # those that are due to have another notification.  If they are
176            # disabled for another reason than bouncing, and we're processing
177            # them (because of the command line switch) then they won't have a
178            # bounce info record.  We can piggyback on that for all disable
179            # purposes.
180            members = mlist.getDeliveryStatusMembers(who)
181            for member in members:
182                info = mlist.getBounceInfo(member)
183                if not info:
184                    # See if they are bounce disabled, or disabled for some
185                    # other reason.
186                    status = mlist.getDeliveryStatus(member)
187                    if status == MemberAdaptor.BYBOUNCE:
188                        # Bouncing member with no bounce info.  Just log it and continue.
189                        syslog(
190                            'error',
191                            '%s disabled BYBOUNCE lacks bounce info, list: %s',
192                            member, mlist.internal_name())
193                        continue
194                    # Disabled other than by bounce.  Create bounce info (why?)
195                    info = _BounceInfo(
196                        member, 0, today,
197                        mlist.bounce_you_are_disabled_warnings)
198                lastnotice = time.mktime(info.lastnotice + (0,) * 6)
199                if force or today >= lastnotice + interval:
200                    notify.append(member)
201                # Get a fresh re-enable cookie and set it.
202                info.cookie = mlist.pend_new(Pending.RE_ENABLE,
203                                       mlist.internal_name(),
204                                       member)
205                mlist.setBounceInfo(member, info)
206            # Now, send notifications to anyone who is due
207            for member in notify:
208                syslog('bounce', 'Notifying disabled member %s for list: %s',
209                       member, mlist.internal_name())
210                try:
211                    mlist.sendNextNotification(member)
212                except Errors.NotAMemberError:
213                    # There must have been some problem with the data we have
214                    # on this member.  Most likely it's that they don't have a
215                    # password assigned.  Log this and delete the member.
216                    syslog('bounce',
217                           'NotAMemberError when sending disabled notice: %s',
218                           member)
219                    mlist.ApprovedDeleteMember(member, 'cron/disabled')
220            mlist.Save()
221        finally:
222            mlist.Unlock()
223
224
225
226if __name__ == '__main__':
227    main()
228