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