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

Skip to content

Commit d312c74

Browse files
committed
#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.
1 parent 958f7ae commit d312c74

3 files changed

Lines changed: 79 additions & 3 deletions

File tree

Lib/smtplib.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -742,7 +742,10 @@ def sendmail(self, from_addr, to_addrs, msg, mail_options=[],
742742
esmtp_opts.append(option)
743743
(code, resp) = self.mail(from_addr, esmtp_opts)
744744
if code != 250:
745-
self.rset()
745+
if code == 421:
746+
self.close()
747+
else:
748+
self.rset()
746749
raise SMTPSenderRefused(code, resp, from_addr)
747750
senderrs = {}
748751
if isinstance(to_addrs, str):
@@ -751,13 +754,19 @@ def sendmail(self, from_addr, to_addrs, msg, mail_options=[],
751754
(code, resp) = self.rcpt(each, rcpt_options)
752755
if (code != 250) and (code != 251):
753756
senderrs[each] = (code, resp)
757+
if code == 421:
758+
self.close()
759+
raise SMTPRecipientsRefused(senderrs)
754760
if len(senderrs) == len(to_addrs):
755761
# the server refused all our recipients
756762
self.rset()
757763
raise SMTPRecipientsRefused(senderrs)
758764
(code, resp) = self.data(msg)
759765
if code != 250:
760-
self.rset()
766+
if code == 421:
767+
self.close()
768+
else:
769+
self.rset()
761770
raise SMTPDataError(code, resp)
762771
#if we got here then somebody got our mail
763772
return senderrs

Lib/test/test_smtplib.py

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,12 @@ def testFailingHELO(self):
560560
# Simulated SMTP channel & server
561561
class SimSMTPChannel(smtpd.SMTPChannel):
562562

563+
mail_response = None
564+
rcpt_response = None
565+
data_response = None
566+
rcpt_count = 0
567+
rset_count = 0
568+
563569
def __init__(self, extra_features, *args, **kw):
564570
self._extrafeatures = ''.join(
565571
[ "250-{0}\r\n".format(x) for x in extra_features ])
@@ -610,18 +616,43 @@ def smtp_AUTH(self, arg):
610616
else:
611617
self.push('550 No access for you!')
612618

619+
def smtp_MAIL(self, arg):
620+
if self.mail_response is None:
621+
super().smtp_MAIL(arg)
622+
else:
623+
self.push(self.mail_response)
624+
625+
def smtp_RCPT(self, arg):
626+
if self.rcpt_response is None:
627+
super().smtp_RCPT(arg)
628+
return
629+
self.push(self.rcpt_response[self.rcpt_count])
630+
self.rcpt_count += 1
631+
632+
def smtp_RSET(self, arg):
633+
super().smtp_RSET(arg)
634+
self.rset_count += 1
635+
636+
def smtp_DATA(self, arg):
637+
if self.data_response is None:
638+
super().smtp_DATA(arg)
639+
else:
640+
self.push(self.data_response)
641+
613642
def handle_error(self):
614643
raise
615644

616645

617646
class SimSMTPServer(smtpd.SMTPServer):
618647

648+
channel_class = SimSMTPChannel
649+
619650
def __init__(self, *args, **kw):
620651
self._extra_features = []
621652
smtpd.SMTPServer.__init__(self, *args, **kw)
622653

623654
def handle_accepted(self, conn, addr):
624-
self._SMTPchannel = SimSMTPChannel(self._extra_features,
655+
self._SMTPchannel = self.channel_class(self._extra_features,
625656
self, conn, addr)
626657

627658
def process_message(self, peer, mailfrom, rcpttos, data):
@@ -755,6 +786,38 @@ def testAUTH_CRAM_MD5(self):
755786
#TODO: add tests for correct AUTH method fallback now that the
756787
#test infrastructure can support it.
757788

789+
# Issue 5713: make sure close, not rset, is called if we get a 421 error
790+
def test_421_from_mail_cmd(self):
791+
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=15)
792+
self.serv._SMTPchannel.mail_response = '421 closing connection'
793+
with self.assertRaises(smtplib.SMTPSenderRefused):
794+
smtp.sendmail('John', 'Sally', 'test message')
795+
self.assertIsNone(smtp.sock)
796+
self.assertEqual(self.serv._SMTPchannel.rcpt_count, 0)
797+
798+
def test_421_from_rcpt_cmd(self):
799+
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=15)
800+
self.serv._SMTPchannel.rcpt_response = ['250 accepted', '421 closing']
801+
with self.assertRaises(smtplib.SMTPRecipientsRefused) as r:
802+
smtp.sendmail('John', ['Sally', 'Frank', 'George'], 'test message')
803+
self.assertIsNone(smtp.sock)
804+
self.assertEqual(self.serv._SMTPchannel.rset_count, 0)
805+
self.assertDictEqual(r.exception.args[0], {'Frank': (421, b'closing')})
806+
807+
def test_421_from_data_cmd(self):
808+
class MySimSMTPChannel(SimSMTPChannel):
809+
def found_terminator(self):
810+
if self.smtp_state == self.DATA:
811+
self.push('421 closing')
812+
else:
813+
super().found_terminator()
814+
self.serv.channel_class = MySimSMTPChannel
815+
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=15)
816+
with self.assertRaises(smtplib.SMTPDataError):
817+
smtp.sendmail('[email protected]', ['[email protected]'], 'test message')
818+
self.assertIsNone(smtp.sock)
819+
self.assertEqual(self.serv._SMTPchannel.rcpt_count, 0)
820+
758821

759822
@support.reap_threads
760823
def test_main(verbose=None):

Misc/NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,10 @@ Core and Builtins
233233
Library
234234
-------
235235

236+
- Issue #5713: smtplib now handles 421 (closing connection) error codes when
237+
sending mail by closing the socket and reporting the 421 error code via the
238+
exception appropriate to the command that received the error response.
239+
236240
- Issue #8862: Fixed curses cleanup when getkey is interrputed by a signal.
237241

238242
- Issue #17443: impalib.IMAP4_stream was using the default unbuffered IO

0 commit comments

Comments
 (0)