Skip to content

Commit fe5d53f

Browse files
committed
Merged in [7591] from rjsparks@nostrum.com:
Adds a real (if simple) SMTP server to the test framework and tests handling of exceptions and rejected addresses. Fixes ticket ietf-tools#1314. - Legacy-Id: 7613 Note: SVN reference [7591] has been migrated to Git commit 54919f0
1 parent fc0c605 commit fe5d53f

4 files changed

Lines changed: 123 additions & 8 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: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
from django.core.management import call_command
4242

4343
import ietf.utils.mail
44+
from ietf.utils.test_smtpserver import SMTPTestServerDriver
4445

4546
loaded_templates = set()
4647
visited_urls = set()
@@ -192,6 +193,8 @@ def run_tests(self, test_labels, extra_tests=None, **kwargs):
192193
if socket.gethostname().split('.')[0] in ['core3', 'ietfa', 'ietfb', 'ietfc', ]:
193194
raise EnvironmentError("Refusing to run tests on production server")
194195
ietf.utils.mail.test_mode = True
196+
ietf.utils.mail.SMTP_ADDR['ip4'] = '127.0.0.1'
197+
ietf.utils.mail.SMTP_ADDR['port'] = 2025
195198

196199
global old_destroy, old_create, test_database_name
197200
from django.db import connection
@@ -219,7 +222,13 @@ def run_tests(self, test_labels, extra_tests=None, **kwargs):
219222

220223
assert not settings.IDTRACKER_BASE_URL.endswith('/')
221224

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

224233
if check_coverage and not failures:
225234
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 SMTPTestChannel(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 SMTPTestServer(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 = SMTPTestChannel(self, conn, addr)
56+
SMTPTestChannel(self, conn, addr)
57+
58+
def process_message(self, peer, mailfrom, rcpttos, data):
59+
self.inbox.append(data)
60+
61+
62+
class SMTPTestServerDriver(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 = SMTPTestServer(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+

ietf/utils/tests.py

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1-
from __future__ import print_function
2-
31
import os.path
2+
from smtplib import SMTPRecipientsRefused
43

54
from django.conf import settings
5+
from django.test import TestCase
66

77
from ietf.utils.management.commands import pyflakes
8-
from ietf.utils.test_utils import TestCase
9-
8+
from ietf.utils.mail import send_mail_text, outbox, SMTPSomeRefusedRecipients, smtp_error_logging
109

1110
class PyFlakesTestCase(TestCase):
1211

@@ -15,3 +14,29 @@ def test_pyflakes(self):
1514
warnings = []
1615
warnings = pyflakes.checkPaths([path], verbosity=0)
1716
self.assertEqual([str(w) for w in warnings], [])
17+
18+
class TestSMTPServer(TestCase):
19+
20+
def test_address_rejected(self):
21+
22+
def send_mail(to):
23+
send_mail_text(None, to=to, frm=None, subject="Test for rejection", txt="dummy body")
24+
25+
with self.assertRaises(SMTPSomeRefusedRecipients):
26+
send_mail('good@example.com,poison@example.com')
27+
28+
with self.assertRaises(SMTPRecipientsRefused):
29+
send_mail('poison@example.com')
30+
31+
len_before = len(outbox)
32+
with smtp_error_logging(send_mail) as send:
33+
send('good@example.com,poison@example.com')
34+
self.assertEqual(len(outbox),len_before+2)
35+
self.assertTrue('Some recipients were refused' in outbox[-1]['Subject'])
36+
37+
len_before = len(outbox)
38+
with smtp_error_logging(send_mail) as send:
39+
send('poison@example.com')
40+
self.assertEqual(len(outbox),len_before+2)
41+
self.assertTrue('error while sending email' in outbox[-1]['Subject'])
42+

0 commit comments

Comments
 (0)