Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 82c6b45

Browse files
committed
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.
2 parents 6168362 + f1a40b4 commit 82c6b45

3 files changed

Lines changed: 80 additions & 11 deletions

File tree

Lib/smtplib.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -751,7 +751,10 @@ def sendmail(self, from_addr, to_addrs, msg, mail_options=[],
751751
esmtp_opts.append(option)
752752
(code, resp) = self.mail(from_addr, esmtp_opts)
753753
if code != 250:
754-
self.rset()
754+
if code == 421:
755+
self.close()
756+
else:
757+
self.rset()
755758
raise SMTPSenderRefused(code, resp, from_addr)
756759
senderrs = {}
757760
if isinstance(to_addrs, str):
@@ -760,13 +763,19 @@ def sendmail(self, from_addr, to_addrs, msg, mail_options=[],
760763
(code, resp) = self.rcpt(each, rcpt_options)
761764
if (code != 250) and (code != 251):
762765
senderrs[each] = (code, resp)
766+
if code == 421:
767+
self.close()
768+
raise SMTPRecipientsRefused(senderrs)
763769
if len(senderrs) == len(to_addrs):
764770
# the server refused all our recipients
765771
self.rset()
766772
raise SMTPRecipientsRefused(senderrs)
767773
(code, resp) = self.data(msg)
768774
if code != 250:
769-
self.rset()
775+
if code == 421:
776+
self.close()
777+
else:
778+
self.rset()
770779
raise SMTPDataError(code, resp)
771780
#if we got here then somebody got our mail
772781
return senderrs

Lib/test/test_smtplib.py

Lines changed: 65 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -586,8 +586,12 @@ def testFailingHELO(self):
586586
# Simulated SMTP channel & server
587587
class SimSMTPChannel(smtpd.SMTPChannel):
588588

589-
# For testing failures in QUIT when using the context manager API.
590589
quit_response = None
590+
mail_response = None
591+
rcpt_response = None
592+
data_response = None
593+
rcpt_count = 0
594+
rset_count = 0
591595

592596
def __init__(self, extra_features, *args, **kw):
593597
self._extrafeatures = ''.join(
@@ -602,6 +606,8 @@ def smtp_EHLO(self, arg):
602606
'250-DELIVERBY\r\n')
603607
resp = resp + self._extrafeatures + '250 HELP'
604608
self.push(resp)
609+
self.seen_greeting = arg
610+
self.extended_smtp = True
605611

606612
def smtp_VRFY(self, arg):
607613
# For max compatibility smtplib should be sending the raw address.
@@ -640,30 +646,50 @@ def smtp_AUTH(self, arg):
640646
self.push('550 No access for you!')
641647

642648
def smtp_QUIT(self, arg):
643-
# args is ignored
644649
if self.quit_response is None:
645650
super(SimSMTPChannel, self).smtp_QUIT(arg)
646651
else:
647652
self.push(self.quit_response)
648653
self.close_when_done()
649654

655+
def smtp_MAIL(self, arg):
656+
if self.mail_response is None:
657+
super().smtp_MAIL(arg)
658+
else:
659+
self.push(self.mail_response)
660+
661+
def smtp_RCPT(self, arg):
662+
if self.rcpt_response is None:
663+
super().smtp_RCPT(arg)
664+
return
665+
self.push(self.rcpt_response[self.rcpt_count])
666+
self.rcpt_count += 1
667+
668+
def smtp_RSET(self, arg):
669+
super().smtp_RSET(arg)
670+
self.rset_count += 1
671+
672+
def smtp_DATA(self, arg):
673+
if self.data_response is None:
674+
super().smtp_DATA(arg)
675+
else:
676+
self.push(self.data_response)
677+
650678
def handle_error(self):
651679
raise
652680

653681

654682
class SimSMTPServer(smtpd.SMTPServer):
655683

656-
# For testing failures in QUIT when using the context manager API.
657-
quit_response = None
684+
channel_class = SimSMTPChannel
658685

659686
def __init__(self, *args, **kw):
660687
self._extra_features = []
661688
smtpd.SMTPServer.__init__(self, *args, **kw)
662689

663690
def handle_accepted(self, conn, addr):
664-
self._SMTPchannel = SimSMTPChannel(
691+
self._SMTPchannel = self.channel_class(
665692
self._extra_features, self, conn, addr)
666-
self._SMTPchannel.quit_response = self.quit_response
667693

668694
def process_message(self, peer, mailfrom, rcpttos, data):
669695
pass
@@ -803,18 +829,48 @@ def test_with_statement(self):
803829
self.assertRaises(smtplib.SMTPServerDisconnected, smtp.send, b'foo')
804830

805831
def test_with_statement_QUIT_failure(self):
806-
self.serv.quit_response = '421 QUIT FAILED'
807832
with self.assertRaises(smtplib.SMTPResponseException) as error:
808833
with smtplib.SMTP(HOST, self.port) as smtp:
834+
self.serv._SMTPchannel.quit_response = '421 QUIT FAILED'
809835
smtp.noop()
810836
self.assertEqual(error.exception.smtp_code, 421)
811837
self.assertEqual(error.exception.smtp_error, b'QUIT FAILED')
812-
# We don't need to clean up self.serv.quit_response because a new
813-
# server is always instantiated in the setUp().
814838

815839
#TODO: add tests for correct AUTH method fallback now that the
816840
#test infrastructure can support it.
817841

842+
# Issue 5713: make sure close, not rset, is called if we get a 421 error
843+
def test_421_from_mail_cmd(self):
844+
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=15)
845+
self.serv._SMTPchannel.mail_response = '421 closing connection'
846+
with self.assertRaises(smtplib.SMTPSenderRefused):
847+
smtp.sendmail('John', 'Sally', 'test message')
848+
self.assertIsNone(smtp.sock)
849+
self.assertEqual(self.serv._SMTPchannel.rcpt_count, 0)
850+
851+
def test_421_from_rcpt_cmd(self):
852+
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=15)
853+
self.serv._SMTPchannel.rcpt_response = ['250 accepted', '421 closing']
854+
with self.assertRaises(smtplib.SMTPRecipientsRefused) as r:
855+
smtp.sendmail('John', ['Sally', 'Frank', 'George'], 'test message')
856+
self.assertIsNone(smtp.sock)
857+
self.assertEqual(self.serv._SMTPchannel.rset_count, 0)
858+
self.assertDictEqual(r.exception.args[0], {'Frank': (421, b'closing')})
859+
860+
def test_421_from_data_cmd(self):
861+
class MySimSMTPChannel(SimSMTPChannel):
862+
def found_terminator(self):
863+
if self.smtp_state == self.DATA:
864+
self.push('421 closing')
865+
else:
866+
super().found_terminator()
867+
self.serv.channel_class = MySimSMTPChannel
868+
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=15)
869+
with self.assertRaises(smtplib.SMTPDataError):
870+
smtp.sendmail('[email protected]', ['[email protected]'], 'test message')
871+
self.assertIsNone(smtp.sock)
872+
self.assertEqual(self.serv._SMTPchannel.rcpt_count, 0)
873+
818874

819875
@support.reap_threads
820876
def test_main(verbose=None):

Misc/NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,10 @@ Core and Builtins
292292
Library
293293
-------
294294

295+
- Issue #5713: smtplib now handles 421 (closing connection) error codes when
296+
sending mail by closing the socket and reporting the 421 error code via the
297+
exception appropriate to the command that received the error response.
298+
295299
- Issue #16997: unittest.TestCase now provides a subTest() context manager
296300
to procedurally generate, in an easy way, small test instances.
297301

0 commit comments

Comments
 (0)