Skip to content

Commit ecd73a4

Browse files
authored
Merge pull request #26 from 153957/bandit-email-list
Allow BANDIT_EMAIL to be a list
2 parents 1121c89 + 74a647d commit ecd73a4

5 files changed

Lines changed: 90 additions & 39 deletions

File tree

README.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ and set the email which will receive all of the emails::
4848

4949
BANDIT_EMAIL = 'bandit@example.com'
5050

51+
or even multiple addresses:
52+
53+
BANDIT_EMAIL = ['bandit@example.com', 'accomplice@example.com']
54+
5155
It's also possible to whitelist certain email addresses and domains::
5256

5357
BANDIT_WHITELIST = [

bandit/backends/base.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
from email.utils import parseaddr
66
from functools import reduce
7-
from operator import and_
87

98
from django.conf import settings
109
from django.template.loader import render_to_string
@@ -27,8 +26,10 @@ def send_messages(self, email_messages):
2726
admins = getattr(settings, 'ADMINS', ())
2827
server_email = getattr(settings, 'SERVER_EMAIL', 'root@localhost')
2928
bandit_email = getattr(settings, 'BANDIT_EMAIL', server_email)
29+
if not isinstance(bandit_email, list):
30+
bandit_email = [bandit_email]
3031
whitelist_emails = set(getattr(settings, 'BANDIT_WHITELIST', ()))
31-
approved_emails = set([server_email, bandit_email, ] + list(whitelist_emails) +
32+
approved_emails = set([server_email] + bandit_email + list(whitelist_emails) +
3233
[email for name, email in admins])
3334

3435
def is_approved(email):
@@ -40,7 +41,7 @@ def is_approved(email):
4041
logged_count = 0
4142
for message in email_messages:
4243
recipients = message.to + message.cc + message.bcc
43-
all_approved = reduce(and_, map(is_approved, recipients))
44+
all_approved = all(map(is_approved, recipients))
4445
if all_approved:
4546
to_send.append(message)
4647
else:
@@ -54,7 +55,7 @@ def is_approved(email):
5455
if not self.log_only:
5556
header = render_to_string("bandit/hijacked-email-header.txt", context)
5657
message.body = header + message.body
57-
message.to = [bandit_email]
58+
message.to = bandit_email
5859
# clear cc/bcc
5960
message.cc = []
6061
message.bcc = []

bandit/backends/smtp.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ class HijackSMTPBackend(HijackBackendMixin, SMTPBackend):
1616
class LogOnlySMTPBackend(LogOnlyBackendMixin, SMTPBackend):
1717
"""
1818
This backend intercepts outgoing messages and logs them, allowing
19-
only messages destined for ADMINS to be sent via SMTP.
19+
only messages destined for ADMINS, BANDIT_EMAIL, SERVER_EMAIL, or
20+
BANDIT_WHITELIST to be sent via SMTP.
2021
"""
2122
pass

bandit/tests.py

Lines changed: 75 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
from django.conf import settings
1212
from django.core.mail import get_connection, EmailMessage
13-
from django.test import TestCase
13+
from django.test import TestCase, override_settings
1414

1515

1616
class FakeSMTPServer(smtpd.SMTPServer, threading.Thread):
@@ -73,6 +73,8 @@ def stop(self):
7373
self.join()
7474

7575

76+
@override_settings(BANDIT_EMAIL='bandit@example.com')
77+
@override_settings(ADMINS=(('Admin', 'admin@example.com'),))
7678
class BaseBackendTestCase(TestCase):
7779
"""
7880
Test email interception in the HijackBackend.
@@ -83,17 +85,11 @@ def setUpClass(cls):
8385
cls.server = FakeSMTPServer(('127.0.0.1', 0), None)
8486
settings.EMAIL_HOST = "127.0.0.1"
8587
settings.EMAIL_PORT = cls.server.socket.getsockname()[1]
86-
cls._original_admins = settings.ADMINS
87-
cls._original_bandit = getattr(settings, 'BANDIT_EMAIL', '')
88-
settings.BANDIT_EMAIL = 'bandit@example.com'
89-
settings.ADMINS = (('Admin', 'admin@example.com'), )
9088
cls.server.start()
9189

9290
@classmethod
9391
def tearDownClass(cls):
9492
cls.server.stop()
95-
settings.BANDIT_EMAIL = cls._original_bandit
96-
settings.ADMINS = cls._original_admins
9793

9894
def setUp(self):
9995
super(BaseBackendTestCase, self).setUp()
@@ -118,35 +114,51 @@ class HijackBackendTestCase(BaseBackendTestCase):
118114
def get_connection(self):
119115
return get_connection('bandit.backends.smtp.HijackSMTPBackend')
120116

121-
def test_basic_hijack(self):
122-
"""Emails should be redirected to send to BANDIT_EMAIL."""
123-
emails = [EmailMessage('Subject', 'Content', 'from@example.com', ['to@example.com'])]
117+
def assert_emails_are_hijacked(self, emails):
124118
num_sent = self.get_connection().send_messages(emails)
125119
self.assertEqual(len(emails), num_sent)
126120
messages = self.get_mailbox_content()
127121
self.assertEqual(len(messages), num_sent)
128-
message = messages[0]
129-
self.assertEqual(message.get_all('to'), ['bandit@example.com', ])
122+
if isinstance(settings.BANDIT_EMAIL, list):
123+
self.assertEqual(messages[0].get_all('to')[0].replace('\n', ''), ', '.join(settings.BANDIT_EMAIL))
124+
else:
125+
self.assertEqual(messages[0].get_all('to'), ['bandit@example.com', ])
130126

131-
def test_send_to_admins(self):
132-
"""Admin emails should not be hijacked."""
133-
emails = [EmailMessage('Subject', 'Content', 'from@example.com', ['admin@example.com'])]
134-
num_sent = self.get_connection().send_messages(emails)
135-
self.assertEqual(len(emails), num_sent)
136-
messages = self.get_mailbox_content()
137-
self.assertEqual(len(messages), num_sent)
138-
message = messages[0]
139-
self.assertEqual(message.get_all('to'), ['admin@example.com', ])
127+
def test_basic_hijack(self):
128+
"""Emails should be redirected to send to BANDIT_EMAIL."""
129+
emails = [EmailMessage('Subject', 'Content', 'from@example.com', ['to@example.com'])]
130+
self.assert_emails_are_hijacked(emails)
131+
132+
@override_settings(BANDIT_EMAIL=['bandit@example.com', 'accomplice@example.com', 'Hijacker <hijacker@example.com>'])
133+
def test_send_to_multiple_bandits(self):
134+
"""Emails should be redirected to all bandit emails."""
135+
emails = [EmailMessage('Subject', 'Content', 'from@example.com', ['to@example.com'])]
136+
self.assert_emails_are_hijacked(emails)
137+
138+
def test_hijack_cc(self):
139+
"""Emails with unapproved recipient in CC should be redirected to send to BANDIT_EMAIL."""
140+
emails = [EmailMessage('Subject', 'Content', 'from@example.com', to=['admin@example.com'], cc=['to@example.com'])]
141+
self.assert_emails_are_hijacked(emails)
142+
143+
def test_hijack_bcc(self):
144+
"""Emails with unapproved recipient in BCC should be redirected to send to BANDIT_EMAIL."""
145+
emails = [EmailMessage('Subject', 'Content', 'from@example.com', to=['admin@example.com'], bcc=['to@example.com'])]
146+
self.assert_emails_are_hijacked(emails)
140147

141148
def test_send_to_mixed(self):
142149
"""Emails with mixed recipients will be hijacked."""
143150
emails = [EmailMessage('Subject', 'Content', 'from@example.com', ['to@example.com', 'admin@example.com'])]
151+
self.assert_emails_are_hijacked(emails)
152+
153+
def test_send_to_admins(self):
154+
"""Admin emails should not be hijacked."""
155+
emails = [EmailMessage('Subject', 'Content', 'from@example.com', ['admin@example.com'])]
144156
num_sent = self.get_connection().send_messages(emails)
145157
self.assertEqual(len(emails), num_sent)
146158
messages = self.get_mailbox_content()
147159
self.assertEqual(len(messages), num_sent)
148160
message = messages[0]
149-
self.assertEqual(message.get_all('to'), ['bandit@example.com', ])
161+
self.assertEqual(message.get_all('to'), ['admin@example.com', ])
150162

151163
def test_send_multiple(self):
152164
"""Emails with mixed recipients will be hijacked."""
@@ -162,10 +174,11 @@ def test_send_multiple(self):
162174
self.assertEqual(message.get_all('to'), ['admin@example.com', ])
163175

164176
def test_whitelist_domain(self):
177+
"""Emails send to whitelisted domains should not be hijacked"""
165178
addresses = ['foo@whitelisted.test.com',
166179
'<bar@whitelisted.test.com>',
167180
'Foo Bar <baz@whitelisted.test.com>']
168-
emails = [EmailMessage( 'Subject', 'Content', 'from@example.com', addresses)]
181+
emails = [EmailMessage('Subject', 'Content', 'from@example.com', addresses)]
169182
num_sent = self.get_connection().send_messages(emails)
170183
self.assertEqual(len(emails), num_sent)
171184
messages = self.get_mailbox_content()
@@ -177,14 +190,38 @@ class LogOnlyBackendTestCase(BaseBackendTestCase):
177190
def get_connection(self):
178191
return get_connection('bandit.backends.smtp.LogOnlySMTPBackend')
179192

180-
def test_basic_hijack(self):
181-
"""Emails should only be logged."""
182-
emails = [EmailMessage('Subject', 'Content', 'from@example.com', ['to@example.com'])]
193+
def assert_emails_are_only_logged(self, emails):
183194
num_sent = self.get_connection().send_messages(emails)
184195
self.assertEqual(len(emails), num_sent)
185196
messages = self.get_mailbox_content()
186197
self.assertEqual(len(messages), 0)
187198

199+
def test_basic_hijack(self):
200+
"""Emails should only be logged."""
201+
emails = [EmailMessage('Subject', 'Content', 'from@example.com', ['to@example.com'])]
202+
self.assert_emails_are_only_logged(emails)
203+
204+
@override_settings(BANDIT_EMAIL=['bandit@example.com', 'accomplice@example.com', 'Hijacker <hijacker@example.com>'])
205+
def test_send_to_multiple_bandits(self):
206+
"""Even with multiple bandit emails the email are only logged."""
207+
emails = [EmailMessage('Subject', 'Content', 'from@example.com', ['to@example.com'])]
208+
self.assert_emails_are_only_logged(emails)
209+
210+
def test_hijack_cc(self):
211+
"""Emails with unapproved recipient in CC should only be logged."""
212+
emails = [EmailMessage('Subject', 'Content', 'from@example.com', to=['admin@example.com'], cc=['to@example.com'])]
213+
self.assert_emails_are_only_logged(emails)
214+
215+
def test_hijack_bcc(self):
216+
"""Emails with unapproved recipient in BCC should only be logged."""
217+
emails = [EmailMessage('Subject', 'Content', 'from@example.com', to=['admin@example.com'], bcc=['to@example.com'])]
218+
self.assert_emails_are_only_logged(emails)
219+
220+
def test_send_to_mixed(self):
221+
"""Emails with mixed recipients will only be logged."""
222+
emails = [EmailMessage('Subject', 'Content', 'from@example.com', ['to@example.com', 'admin@example.com'])]
223+
self.assert_emails_are_only_logged(emails)
224+
188225
def test_send_to_admins(self):
189226
"""Admin emails should still be sent."""
190227
emails = [EmailMessage('Subject', 'Content', 'from@example.com', ['admin@example.com'])]
@@ -195,14 +232,6 @@ def test_send_to_admins(self):
195232
message = messages[0]
196233
self.assertEqual(message.get_all('to'), ['admin@example.com', ])
197234

198-
def test_send_to_mixed(self):
199-
"""Emails with mixed recipients will only be logged."""
200-
emails = [EmailMessage('Subject', 'Content', 'from@example.com', ['to@example.com', 'admin@example.com'])]
201-
num_sent = self.get_connection().send_messages(emails)
202-
self.assertEqual(len(emails), num_sent)
203-
messages = self.get_mailbox_content()
204-
self.assertEqual(len(messages), 0)
205-
206235
def test_send_multiple(self):
207236
"""Only the email to the admin should be sent (the other should be logged)."""
208237
emails = [EmailMessage('Subject', 'Content', 'from@example.com', ['to@example.com']),
@@ -213,3 +242,15 @@ def test_send_multiple(self):
213242
self.assertEqual(len(messages), 1)
214243
message = messages[0]
215244
self.assertEqual(message.get_all('to'), ['admin@example.com', ])
245+
246+
def test_whitelist_domain(self):
247+
"""Emails send to whitelisted domains are still sent"""
248+
addresses = ['foo@whitelisted.test.com',
249+
'<bar@whitelisted.test.com>',
250+
'Foo Bar <baz@whitelisted.test.com>']
251+
emails = [EmailMessage('Subject', 'Content', 'from@example.com', addresses)]
252+
num_sent = self.get_connection().send_messages(emails)
253+
self.assertEqual(len(emails), num_sent)
254+
messages = self.get_mailbox_content()
255+
self.assertEqual(len(messages), num_sent)
256+
self.assertEqual(messages[0].get_all('to')[0].replace('\n', ''), ', '.join(addresses))

docs/settings.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ This defaults to the ``SERVER_EMAIL`` setting if not set::
1212

1313
BANDIT_EMAIL = 'bandit@example.com'
1414

15+
This option can also be a list to send the hijacked email to multiple addresses::
16+
17+
BANDIT_EMAIL = ['bandit@example.com', 'accomplice@example.com']
18+
1519

1620
``BANDIT_WHITELIST``
1721
----------------------------------------

0 commit comments

Comments
 (0)