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