1# Copyright (C) 2011-2020 by the Free Software Foundation, Inc. 2# 3# This file is part of GNU Mailman. 4# 5# GNU Mailman is free software: you can redistribute it and/or modify it under 6# the terms of the GNU General Public License as published by the Free 7# Software Foundation, either version 3 of the License, or (at your option) 8# any later version. 9# 10# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT 11# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 12# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 13# more details. 14# 15# You should have received a copy of the GNU General Public License along with 16# GNU Mailman. If not, see <https://www.gnu.org/licenses/>. 17 18"""Test bounce model objects.""" 19 20import unittest 21 22from datetime import datetime, timedelta 23from mailman.app.lifecycle import create_list 24from mailman.database.transaction import transaction 25from mailman.interfaces.bounce import ( 26 BounceContext, IBounceProcessor, InvalidBounceEvent) 27from mailman.interfaces.member import DeliveryStatus 28from mailman.interfaces.usermanager import IUserManager 29from mailman.testing.helpers import ( 30 LogFileMark, configuration, get_queue_messages, 31 specialized_message_from_string as message_from_string) 32from mailman.testing.layers import ConfigLayer 33from mailman.utilities.datetime import now 34from zope.component import getUtility 35 36 37class TestBounceEvents(unittest.TestCase): 38 layer = ConfigLayer 39 40 def setUp(self): 41 self._processor = getUtility(IBounceProcessor) 42 with transaction(): 43 self._mlist = create_list('test@example.com') 44 self._msg = message_from_string("""\ 45From: mail-daemon@example.com 46To: test-bounces@example.com 47Message-Id: <first> 48 49""") 50 51 def _subscribe_and_add_bounce_event( 52 self, addr, subscribe=True, create=True, context=None): 53 user_mgr = getUtility(IUserManager) 54 with transaction(): 55 if create: 56 anne = user_mgr.create_address(addr) 57 else: 58 anne = user_mgr.get_address(addr) 59 if subscribe: 60 self._mlist.subscribe(anne) 61 self._processor.register( 62 self._mlist, addr, self._msg, where=context) 63 return self._mlist.members.get_member(addr) 64 65 def _process_pending_events(self): 66 events = list(self._processor.unprocessed) 67 for event in events: 68 self._processor.process_event(event) 69 return events 70 71 def test_events_iterator(self): 72 self._subscribe_and_add_bounce_event('anne@example.com') 73 events = list(self._processor.events) 74 self.assertEqual(len(events), 1) 75 event = events[0] 76 self.assertEqual(event.list_id, 'test.example.com') 77 self.assertEqual(event.email, 'anne@example.com') 78 self.assertEqual(event.timestamp, datetime(2005, 8, 1, 7, 49, 23)) 79 self.assertEqual(event.message_id, '<first>') 80 self.assertEqual(event.context, BounceContext.normal) 81 self.assertEqual(event.processed, False) 82 # The unprocessed list will be exactly the same right now. 83 unprocessed = list(self._processor.unprocessed) 84 self.assertEqual(len(unprocessed), 1) 85 event = unprocessed[0] 86 self.assertEqual(event.list_id, 'test.example.com') 87 self.assertEqual(event.email, 'anne@example.com') 88 self.assertEqual(event.timestamp, datetime(2005, 8, 1, 7, 49, 23)) 89 self.assertEqual(event.message_id, '<first>') 90 self.assertEqual(event.context, BounceContext.normal) 91 self.assertFalse(event.processed) 92 93 def test_unprocessed_events_iterator(self): 94 self._subscribe_and_add_bounce_event('anne@example.com') 95 self._subscribe_and_add_bounce_event('bartanne@example.com') 96 97 events = list(self._processor.events) 98 self.assertEqual(len(events), 2) 99 unprocessed = list(self._processor.unprocessed) 100 # The unprocessed list will be exactly the same right now. 101 self.assertEqual(len(unprocessed), 2) 102 # Process one of the events. 103 with transaction(): 104 events[0].processed = True 105 # Now there will be only one unprocessed event. 106 unprocessed = list(self._processor.unprocessed) 107 self.assertEqual(len(unprocessed), 1) 108 # Process the other event. 109 with transaction(): 110 events[1].processed = True 111 # Now there will be no unprocessed events. 112 unprocessed = list(self._processor.unprocessed) 113 self.assertEqual(len(unprocessed), 0) 114 115 def test_process_bounce_event(self): 116 # Test that we are able to process bounce events. 117 self._subscribe_and_add_bounce_event( 118 'anne@example.com', subscribe=False) 119 events = list(self._processor.unprocessed) 120 self.assertEqual(len(events), 1) 121 # If the associated email with the event is not a member of the 122 # MailingList, an InvalidBounceEvent exception is raised. 123 with self.assertRaises(InvalidBounceEvent): 124 self._processor.process_event(events[0]) 125 # Now, we will subscribe the user and see if we can process the event 126 # further and add another bounce event for anne. 127 self._subscribe_and_add_bounce_event('anne@example.com', create=False) 128 events = list(self._processor.unprocessed) 129 self.assertEqual(len(events), 1) 130 131 member = self._mlist.members.get_member('anne@example.com') 132 self.assertTrue(member is not None) 133 134 self._processor.process_event(events[0]) 135 # Now, we should be able to check the bounce score of anne. 136 self.assertEqual(member.bounce_score, 1) 137 self.assertIsNotNone(member.last_bounce_received) 138 # Also, the delivery should be unset, the default. 139 self.assertIsNone(member.preferences.delivery_status) 140 141 def test_bounce_score_increases_once_everyday(self): 142 # Test only the bounce events more than a day apart can increase the 143 # bounce score of a member. 144 # Add two events, for the same day. 145 self._subscribe_and_add_bounce_event('anne@example.com') 146 member = self._subscribe_and_add_bounce_event( 147 'anne@example.com', create=False, subscribe=False) 148 events = list(self._processor.unprocessed) 149 self.assertEqual(len(events), 2) 150 for event in events: 151 self._processor.process_event(event) 152 self.assertEqual(member.bounce_score, 1) 153 154 def test_stale_bounce_score_is_reset(self): 155 # Test that the bounce score is reset after 156 # mlist.bounce_info_stale_after number of days. 157 member = self._subscribe_and_add_bounce_event('anne@example.com') 158 member.bounce_score = 10 159 # Set the last bouce received to be 2 days before the threshold. 160 member.last_bounce_received = ( 161 now() - self._mlist.bounce_info_stale_after - timedelta(days=2)) 162 events = list(self._processor.unprocessed) 163 self.assertEqual(len(events), 1) 164 self._processor.process_event(events[0]) 165 self.assertEqual(member.bounce_score, 1) 166 167 def test_bounce_score_over_threshold_disables_delivery( 168 self, expected_count=1): 169 # Test that the bounce score higher than thereshold disbales delivery 170 # for the member. 171 self._mlist.bounce_score_threshold = 1 172 # Disable welcome message so we can assert admin notice later. 173 self._mlist.send_welcome_message = False 174 175 self._subscribe_and_add_bounce_event('anne@example.com') 176 member = self._subscribe_and_add_bounce_event( 177 'anne@example.com', create=False, subscribe=False) 178 179 # We need to make sure that events are not on same date to have them 180 # increase the bounce score. 181 events = list(self._processor.unprocessed) 182 events[0].timestamp = events[0].timestamp - timedelta(days=2) 183 184 # Now, process the events and check that user is disabled. 185 for event in events: 186 self._processor.process_event(event) 187 # The first event scores 1 and disables delivery. The second is 188 # not processed because delivery is already disabled. 189 self.assertEqual(member.bounce_score, 1) 190 self.assertEqual( 191 member.preferences.delivery_status, DeliveryStatus.by_bounces) 192 193 # There should be an admin notice about the disabled subscription. 194 messages = get_queue_messages('virgin', expected_count=expected_count) 195 if expected_count > 0: 196 msg = messages[0].msg 197 self.assertEqual( 198 str(msg['Subject']), 199 'anne@example.com\'s subscription disabled on Test') 200 201 def test_bounce_disable_skips_admin_notice(self): 202 # Test that when a subscription is disabled, the admin is notified if 203 # the mailing list is configured to send notices. 204 self._mlist.bounce_notify_owner_on_disable = False 205 self.test_bounce_score_over_threshold_disables_delivery( 206 expected_count=0) 207 208 @configuration('mta', verp_probes='yes') 209 def test_bounce_score_over_threshold_sends_probe(self): 210 # Test that bounce score over threshold does not disables delivery if 211 # the MailingList is configured to send probes first. 212 # Sending probe also resets bounce_score. 213 # Disable welcome message so we can assert admin notice later. 214 self._mlist.send_welcome_message = False 215 self._mlist.bounce_score_threshold = 0 216 member = self._subscribe_and_add_bounce_event('anne@example.com') 217 member.bounce_score = 1 218 # Process events. 219 self._process_pending_events() 220 self.assertEqual(member.bounce_score, 0) 221 self.assertIsNone(member.preferences.delivery_status) 222 messages = get_queue_messages('virgin', expected_count=1) 223 msg = messages[0].msg 224 self.assertEqual(str(msg['subject']), 225 'Test mailing list probe message') 226 227 def test_bounce_event_probe_disables_delivery(self): 228 # That that bounce probe disables delivery immidiately. 229 member = self._subscribe_and_add_bounce_event( 230 'anne@example.com', context=BounceContext.probe) 231 self._process_pending_events() 232 self.assertEqual( 233 member.preferences.delivery_status, DeliveryStatus.by_bounces) 234 235 def test_disable_delivery_already_disabled(self): 236 # Attempting to disable delivery for an already disabled member does 237 # nothing. 238 self._mlist.send_welcome_message = False 239 member = self._subscribe_and_add_bounce_event('anne@example.com') 240 events = list(self._processor.events) 241 self.assertEqual(len(events), 1) 242 member.total_warnings_sent = 3 243 member.last_warning_sent = now() - timedelta(days=2) 244 member.preferences.delivery_status = DeliveryStatus.by_bounces 245 mark = LogFileMark('mailman.bounce') 246 self._processor._disable_delivery(self._mlist, member, events[0]) 247 self.assertEqual(mark.read(), '') 248 self.assertEqual(member.total_warnings_sent, 3) 249 self.assertEqual(member.last_warning_sent, now() - timedelta(days=2)) 250 get_queue_messages('virgin', expected_count=0) 251 252 def test_residual_bounce_marked_processed(self): 253 # A bounce received after delivery is disabled should be marked as 254 # processed. 255 member = self._subscribe_and_add_bounce_event('anne@example.com') 256 events = list(self._processor.unprocessed) 257 self.assertEqual(len(events), 1) 258 member.preferences.delivery_status = DeliveryStatus.by_bounces 259 self._processor.process_event(events[0]) 260 events = list(self._processor.unprocessed) 261 self.assertEqual(len(events), 0) 262 263 def test_send_warnings_after_disable(self): 264 # Test that required number of warnings are sent after the delivery is 265 # disabled. 266 self._mlist.bounce_notify_owner_on_disable = False 267 self._mlist.bounce_you_are_disabled_warnings = 1 268 self._mlist.bounce_you_are_disabled_warnings_interval = timedelta( 269 days=1) 270 self._mlist.bounce_score_threshold = 3 271 self._mlist.send_welcome_message = False 272 273 member = self._subscribe_and_add_bounce_event('anne@example.com') 274 member.bounce_score = 3 275 member.last_bounce_received = now() - timedelta(days=2) 276 # We will process all events now. 277 self._process_pending_events() 278 self.assertEqual(member.preferences.delivery_status, 279 DeliveryStatus.by_bounces) 280 self.assertEqual(member.bounce_score, 4) 281 282 self._processor._send_warnings() 283 self.assertEqual(member.last_warning_sent.day, now().day) 284 self.assertEqual(member.total_warnings_sent, 1) 285 msgs = get_queue_messages('virgin', expected_count=1) 286 msg = msgs[0].msg 287 self.assertEqual(str(msg['Subject']), 288 'Your subscription for Test mailing list has' 289 ' been disabled') 290 291 def test_send_warnings_and_remove_membership(self): 292 # Test that required number of warnings are send and then the the 293 # membership is removed. 294 self._mlist.bounce_notify_owner_on_disable = False 295 self._mlist.bounce_notify_owner_on_removal = True 296 self._mlist.bounce_you_are_disabled_warnings = 1 297 self._mlist.bounce_you_are_disabled_warnings_interval = timedelta( 298 days=1) 299 self._mlist.bounce_score_threshold = 3 300 self._mlist.send_welcome_message = False 301 302 member = self._subscribe_and_add_bounce_event('anne@example.com') 303 member.bounce_score = 4 304 member.last_bounce_received = now() - timedelta(days=2) 305 member.total_warnings_sent = 1 306 member.last_warning_sent = now() - timedelta(days=2) 307 member.preferences.delivery_status = DeliveryStatus.by_bounces 308 309 # Now make sure that we send the warnings. 310 with transaction(): 311 self._processor.send_warnings_and_remove() 312 313 member = self._mlist.members.get_member('anne@example.com') 314 self.assertIsNone(member) 315 # There should be only 2 messages in the queue. One notifying the user 316 # of their removal and other notifying the admin about the removal. 317 msgs = get_queue_messages('virgin', expected_count=2) 318 if msgs[0].msg['to'] == self._mlist.owner_address: 319 owner_notif, user_notice = msgs 320 else: 321 user_notice, owner_notif = msgs 322 self.assertEqual( 323 user_notice.msg['subject'], 324 'You have been unsubscribed from the Test mailing list') 325 self.assertEqual( 326 owner_notif.msg['subject'], 327 'anne@example.com unsubscribed from Test mailing list due ' 328 'to bounces') 329