Merge: #5713: Handle 421 error codes during sendmail by closing the socket.

This is a partial fix to the issue of servers disconnecting unexpectedly; in
this case the 421 says they are disconnecting, so we close the socket and
return the 421 in the appropriate error context.

Original patch by Mark Sapiro, updated by Kushal Das, with additional
tests by me.
This commit is contained in:
R David Murray 2013-03-20 21:13:56 -04:00
commit 82c6b45c14
3 changed files with 80 additions and 11 deletions

View File

@ -751,7 +751,10 @@ class SMTP:
esmtp_opts.append(option) esmtp_opts.append(option)
(code, resp) = self.mail(from_addr, esmtp_opts) (code, resp) = self.mail(from_addr, esmtp_opts)
if code != 250: if code != 250:
self.rset() if code == 421:
self.close()
else:
self.rset()
raise SMTPSenderRefused(code, resp, from_addr) raise SMTPSenderRefused(code, resp, from_addr)
senderrs = {} senderrs = {}
if isinstance(to_addrs, str): if isinstance(to_addrs, str):
@ -760,13 +763,19 @@ class SMTP:
(code, resp) = self.rcpt(each, rcpt_options) (code, resp) = self.rcpt(each, rcpt_options)
if (code != 250) and (code != 251): if (code != 250) and (code != 251):
senderrs[each] = (code, resp) senderrs[each] = (code, resp)
if code == 421:
self.close()
raise SMTPRecipientsRefused(senderrs)
if len(senderrs) == len(to_addrs): if len(senderrs) == len(to_addrs):
# the server refused all our recipients # the server refused all our recipients
self.rset() self.rset()
raise SMTPRecipientsRefused(senderrs) raise SMTPRecipientsRefused(senderrs)
(code, resp) = self.data(msg) (code, resp) = self.data(msg)
if code != 250: if code != 250:
self.rset() if code == 421:
self.close()
else:
self.rset()
raise SMTPDataError(code, resp) raise SMTPDataError(code, resp)
#if we got here then somebody got our mail #if we got here then somebody got our mail
return senderrs return senderrs

View File

@ -586,8 +586,12 @@ sim_lists = {'list-1':['Mr.A@somewhere.com','Mrs.C@somewhereesle.com'],
# Simulated SMTP channel & server # Simulated SMTP channel & server
class SimSMTPChannel(smtpd.SMTPChannel): class SimSMTPChannel(smtpd.SMTPChannel):
# For testing failures in QUIT when using the context manager API.
quit_response = None quit_response = None
mail_response = None
rcpt_response = None
data_response = None
rcpt_count = 0
rset_count = 0
def __init__(self, extra_features, *args, **kw): def __init__(self, extra_features, *args, **kw):
self._extrafeatures = ''.join( self._extrafeatures = ''.join(
@ -602,6 +606,8 @@ class SimSMTPChannel(smtpd.SMTPChannel):
'250-DELIVERBY\r\n') '250-DELIVERBY\r\n')
resp = resp + self._extrafeatures + '250 HELP' resp = resp + self._extrafeatures + '250 HELP'
self.push(resp) self.push(resp)
self.seen_greeting = arg
self.extended_smtp = True
def smtp_VRFY(self, arg): def smtp_VRFY(self, arg):
# For max compatibility smtplib should be sending the raw address. # For max compatibility smtplib should be sending the raw address.
@ -640,30 +646,50 @@ class SimSMTPChannel(smtpd.SMTPChannel):
self.push('550 No access for you!') self.push('550 No access for you!')
def smtp_QUIT(self, arg): def smtp_QUIT(self, arg):
# args is ignored
if self.quit_response is None: if self.quit_response is None:
super(SimSMTPChannel, self).smtp_QUIT(arg) super(SimSMTPChannel, self).smtp_QUIT(arg)
else: else:
self.push(self.quit_response) self.push(self.quit_response)
self.close_when_done() self.close_when_done()
def smtp_MAIL(self, arg):
if self.mail_response is None:
super().smtp_MAIL(arg)
else:
self.push(self.mail_response)
def smtp_RCPT(self, arg):
if self.rcpt_response is None:
super().smtp_RCPT(arg)
return
self.push(self.rcpt_response[self.rcpt_count])
self.rcpt_count += 1
def smtp_RSET(self, arg):
super().smtp_RSET(arg)
self.rset_count += 1
def smtp_DATA(self, arg):
if self.data_response is None:
super().smtp_DATA(arg)
else:
self.push(self.data_response)
def handle_error(self): def handle_error(self):
raise raise
class SimSMTPServer(smtpd.SMTPServer): class SimSMTPServer(smtpd.SMTPServer):
# For testing failures in QUIT when using the context manager API. channel_class = SimSMTPChannel
quit_response = None
def __init__(self, *args, **kw): def __init__(self, *args, **kw):
self._extra_features = [] self._extra_features = []
smtpd.SMTPServer.__init__(self, *args, **kw) smtpd.SMTPServer.__init__(self, *args, **kw)
def handle_accepted(self, conn, addr): def handle_accepted(self, conn, addr):
self._SMTPchannel = SimSMTPChannel( self._SMTPchannel = self.channel_class(
self._extra_features, self, conn, addr) self._extra_features, self, conn, addr)
self._SMTPchannel.quit_response = self.quit_response
def process_message(self, peer, mailfrom, rcpttos, data): def process_message(self, peer, mailfrom, rcpttos, data):
pass pass
@ -803,18 +829,48 @@ class SMTPSimTests(unittest.TestCase):
self.assertRaises(smtplib.SMTPServerDisconnected, smtp.send, b'foo') self.assertRaises(smtplib.SMTPServerDisconnected, smtp.send, b'foo')
def test_with_statement_QUIT_failure(self): def test_with_statement_QUIT_failure(self):
self.serv.quit_response = '421 QUIT FAILED'
with self.assertRaises(smtplib.SMTPResponseException) as error: with self.assertRaises(smtplib.SMTPResponseException) as error:
with smtplib.SMTP(HOST, self.port) as smtp: with smtplib.SMTP(HOST, self.port) as smtp:
self.serv._SMTPchannel.quit_response = '421 QUIT FAILED'
smtp.noop() smtp.noop()
self.assertEqual(error.exception.smtp_code, 421) self.assertEqual(error.exception.smtp_code, 421)
self.assertEqual(error.exception.smtp_error, b'QUIT FAILED') self.assertEqual(error.exception.smtp_error, b'QUIT FAILED')
# We don't need to clean up self.serv.quit_response because a new
# server is always instantiated in the setUp().
#TODO: add tests for correct AUTH method fallback now that the #TODO: add tests for correct AUTH method fallback now that the
#test infrastructure can support it. #test infrastructure can support it.
# Issue 5713: make sure close, not rset, is called if we get a 421 error
def test_421_from_mail_cmd(self):
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=15)
self.serv._SMTPchannel.mail_response = '421 closing connection'
with self.assertRaises(smtplib.SMTPSenderRefused):
smtp.sendmail('John', 'Sally', 'test message')
self.assertIsNone(smtp.sock)
self.assertEqual(self.serv._SMTPchannel.rcpt_count, 0)
def test_421_from_rcpt_cmd(self):
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=15)
self.serv._SMTPchannel.rcpt_response = ['250 accepted', '421 closing']
with self.assertRaises(smtplib.SMTPRecipientsRefused) as r:
smtp.sendmail('John', ['Sally', 'Frank', 'George'], 'test message')
self.assertIsNone(smtp.sock)
self.assertEqual(self.serv._SMTPchannel.rset_count, 0)
self.assertDictEqual(r.exception.args[0], {'Frank': (421, b'closing')})
def test_421_from_data_cmd(self):
class MySimSMTPChannel(SimSMTPChannel):
def found_terminator(self):
if self.smtp_state == self.DATA:
self.push('421 closing')
else:
super().found_terminator()
self.serv.channel_class = MySimSMTPChannel
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=15)
with self.assertRaises(smtplib.SMTPDataError):
smtp.sendmail('John@foo.org', ['Sally@foo.org'], 'test message')
self.assertIsNone(smtp.sock)
self.assertEqual(self.serv._SMTPchannel.rcpt_count, 0)
@support.reap_threads @support.reap_threads
def test_main(verbose=None): def test_main(verbose=None):

View File

@ -292,6 +292,10 @@ Core and Builtins
Library Library
------- -------
- Issue #5713: smtplib now handles 421 (closing connection) error codes when
sending mail by closing the socket and reporting the 421 error code via the
exception appropriate to the command that received the error response.
- Issue #16997: unittest.TestCase now provides a subTest() context manager - Issue #16997: unittest.TestCase now provides a subTest() context manager
to procedurally generate, in an easy way, small test instances. to procedurally generate, in an easy way, small test instances.