Skip to content

Commit 54919f0

Browse files
committed
Adds a real (if simple) SMTP server to the test framework and tests handling of exceptions and rejected addresses. Fixes ticket ietf-tools#1314. Commit ready for merge.
- Legacy-Id: 7591
1 parent 85458ab commit 54919f0

4 files changed

Lines changed: 125 additions & 4 deletions

File tree

ietf/utils/mail.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
test_mode = False
2929
outbox = []
3030

31+
SMTP_ADDR = { 'ip4':settings.EMAIL_HOST, 'port':settings.EMAIL_PORT}
32+
3133
def empty_outbox():
3234
outbox[:] = []
3335

@@ -63,7 +65,7 @@ def send_smtp(msg, bcc=None):
6365
Message. The From address will be used if present or will default
6466
to the django setting DEFAULT_FROM_EMAIL
6567
66-
If someone has set test_mode=True, then just append the msg to
68+
If someone has set test_mode=True, then append the msg to
6769
the outbox.
6870
'''
6971
add_headers(msg)
@@ -77,14 +79,13 @@ def send_smtp(msg, bcc=None):
7779
else:
7880
if test_mode:
7981
outbox.append(msg)
80-
return
8182
server = None
8283
try:
8384
server = smtplib.SMTP()
8485
#log("SMTP server: %s" % repr(server))
8586
#if settings.DEBUG:
8687
# server.set_debuglevel(1)
87-
conn_code, conn_msg = server.connect(settings.EMAIL_HOST, settings.EMAIL_PORT)
88+
conn_code, conn_msg = server.connect(SMTP_ADDR['ip4'], SMTP_ADDR['port'])
8889
#log("SMTP connect: code: %s; msg: %s" % (conn_code, conn_msg))
8990
if settings.EMAIL_HOST_USER and settings.EMAIL_HOST_PASSWORD:
9091
server.ehlo()

ietf/utils/test_runner.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@
4141
from django.core.management import call_command
4242

4343
import ietf.utils.mail
44+
from ietf.utils.test_smtpserver import TestSMTPServerDriver
45+
from ietf.utils.mail import outbox
4446

4547
loaded_templates = set()
4648
visited_urls = set()
@@ -192,6 +194,8 @@ def run_tests(self, test_labels, extra_tests=None, **kwargs):
192194
if socket.gethostname().split('.')[0] in ['core3', 'ietfa', 'ietfb', 'ietfc', ]:
193195
raise EnvironmentError("Refusing to run tests on production server")
194196
ietf.utils.mail.test_mode = True
197+
ietf.utils.mail.SMTP_ADDR['ip4'] = '127.0.0.1'
198+
ietf.utils.mail.SMTP_ADDR['port'] = 2025
195199

196200
global old_destroy, old_create, test_database_name
197201
from django.db import connection
@@ -219,7 +223,13 @@ def run_tests(self, test_labels, extra_tests=None, **kwargs):
219223

220224
assert not settings.IDTRACKER_BASE_URL.endswith('/')
221225

222-
failures = super(IetfTestRunner, self).run_tests(test_labels, extra_tests=extra_tests, **kwargs)
226+
smtpd_driver = TestSMTPServerDriver((ietf.utils.mail.SMTP_ADDR['ip4'],ietf.utils.mail.SMTP_ADDR['port']),None)
227+
smtpd_driver.start()
228+
229+
try:
230+
failures = super(IetfTestRunner, self).run_tests(test_labels, extra_tests=extra_tests, **kwargs)
231+
finally:
232+
smtpd_driver.stop()
223233

224234
if check_coverage and not failures:
225235
check_template_coverage(self.verbosity)

ietf/utils/test_smtpserver.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import smtpd
2+
import threading
3+
import asyncore
4+
5+
class AsyncCoreLoopThread(object):
6+
7+
def wrap_loop(self, exit_condition, timeout=1.0, use_poll=False, map=None):
8+
if map is None:
9+
map = asyncore.socket_map
10+
while map and not exit_condition:
11+
asyncore.loop(timeout=1.0, use_poll=False, map=map, count=1)
12+
13+
def start(self):
14+
"""Start the listening service"""
15+
self.exit_condition = []
16+
kwargs={'exit_condition':self.exit_condition,'timeout':1.0}
17+
self.thread = threading.Thread(target=self.wrap_loop,kwargs=kwargs )
18+
self.thread.start()
19+
20+
def stop(self):
21+
"""Stop the listening service"""
22+
self.exit_condition.append(True)
23+
self.thread.join()
24+
25+
26+
class TestSMTPChannel(smtpd.SMTPChannel):
27+
28+
def smtp_RCPT(self, arg):
29+
if not self._SMTPChannel__mailfrom:
30+
self.push('503 Error: need MAIL command')
31+
return
32+
address = self._SMTPChannel__getaddr('TO:', arg) if arg else None
33+
if not address:
34+
self.push('501 Syntax: RCPT TO: <address>')
35+
return
36+
if "poison" in address:
37+
self.push('550 Error: Not touching that')
38+
return
39+
self._SMTPChannel__rcpttos.append(address)
40+
self.push('250 Ok')
41+
42+
class TestSMTPServer(smtpd.SMTPServer):
43+
44+
def __init__(self,localaddr,remoteaddr,inbox):
45+
if inbox is not None:
46+
self.inbox=inbox
47+
else:
48+
self.inbox = []
49+
smtpd.SMTPServer.__init__(self,localaddr,remoteaddr)
50+
51+
def handle_accept(self):
52+
pair = self.accept()
53+
if pair is not None:
54+
conn, addr = pair
55+
#channel = TestSMTPChannel(self, conn, addr)
56+
TestSMTPChannel(self, conn, addr)
57+
58+
def process_message(self, peer, mailfrom, rcpttos, data):
59+
self.inbox.append(data)
60+
61+
62+
class TestSMTPServerDriver(object):
63+
def __init__(self, localaddr, remoteaddr, inbox=None):
64+
self.localaddr=localaddr
65+
self.remoteaddr=remoteaddr
66+
if inbox is not None:
67+
self.inbox = inbox
68+
else:
69+
self.inbox = []
70+
self.thread_driver = None
71+
72+
def start(self):
73+
self.smtpserver = TestSMTPServer(self.localaddr,self.remoteaddr,self.inbox)
74+
self.thread_driver = AsyncCoreLoopThread()
75+
self.thread_driver.start()
76+
77+
def stop(self):
78+
if self.thread_driver:
79+
self.thread_driver.stop()
80+
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from django.test import TestCase
2+
3+
from smtplib import SMTPRecipientsRefused
4+
5+
from ietf.utils.mail import send_mail_text, outbox, SMTPSomeRefusedRecipients, smtp_error_logging
6+
7+
class TestSMTPServer(TestCase):
8+
9+
def test_address_rejected(self):
10+
11+
def send_mail(to):
12+
send_mail_text(None, to=to, frm=None, subject="Test for rejection", txt="dummy body")
13+
14+
with self.assertRaises(SMTPSomeRefusedRecipients):
15+
send_mail('good@example.com,poison@example.com')
16+
17+
with self.assertRaises(SMTPRecipientsRefused):
18+
send_mail('poison@example.com')
19+
20+
len_before = len(outbox)
21+
with smtp_error_logging(send_mail) as send:
22+
send('good@example.com,poison@example.com')
23+
self.assertEqual(len(outbox),len_before+2)
24+
self.assertTrue('Some recipients were refused' in outbox[-1]['Subject'])
25+
26+
len_before = len(outbox)
27+
with smtp_error_logging(send_mail) as send:
28+
send('poison@example.com')
29+
self.assertEqual(len(outbox),len_before+2)
30+
self.assertTrue('error while sending email' in outbox[-1]['Subject'])

0 commit comments

Comments
 (0)