diff --git a/Lib/smtplib.py b/Lib/smtplib.py index e06a9be7413..679e4782950 100644 --- a/Lib/smtplib.py +++ b/Lib/smtplib.py @@ -742,7 +742,10 @@ class SMTP: esmtp_opts.append(option) (code, resp) = self.mail(from_addr, esmtp_opts) if code != 250: - self.rset() + if code == 421: + self.close() + else: + self.rset() raise SMTPSenderRefused(code, resp, from_addr) senderrs = {} if isinstance(to_addrs, str): @@ -751,13 +754,19 @@ class SMTP: (code, resp) = self.rcpt(each, rcpt_options) if (code != 250) and (code != 251): senderrs[each] = (code, resp) + if code == 421: + self.close() + raise SMTPRecipientsRefused(senderrs) if len(senderrs) == len(to_addrs): # the server refused all our recipients self.rset() raise SMTPRecipientsRefused(senderrs) (code, resp) = self.data(msg) if code != 250: - self.rset() + if code == 421: + self.close() + else: + self.rset() raise SMTPDataError(code, resp) #if we got here then somebody got our mail return senderrs diff --git a/Lib/test/test_smtplib.py b/Lib/test/test_smtplib.py index 2cb0d1ab52b..92f986b72ee 100644 --- a/Lib/test/test_smtplib.py +++ b/Lib/test/test_smtplib.py @@ -560,6 +560,12 @@ sim_lists = {'list-1':['Mr.A@somewhere.com','Mrs.C@somewhereesle.com'], # Simulated SMTP channel & server class SimSMTPChannel(smtpd.SMTPChannel): + mail_response = None + rcpt_response = None + data_response = None + rcpt_count = 0 + rset_count = 0 + def __init__(self, extra_features, *args, **kw): self._extrafeatures = ''.join( [ "250-{0}\r\n".format(x) for x in extra_features ]) @@ -610,18 +616,43 @@ class SimSMTPChannel(smtpd.SMTPChannel): else: self.push('550 No access for you!') + 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): raise class SimSMTPServer(smtpd.SMTPServer): + channel_class = SimSMTPChannel + def __init__(self, *args, **kw): self._extra_features = [] smtpd.SMTPServer.__init__(self, *args, **kw) def handle_accepted(self, conn, addr): - self._SMTPchannel = SimSMTPChannel(self._extra_features, + self._SMTPchannel = self.channel_class(self._extra_features, self, conn, addr) def process_message(self, peer, mailfrom, rcpttos, data): @@ -755,6 +786,38 @@ class SMTPSimTests(unittest.TestCase): #TODO: add tests for correct AUTH method fallback now that the #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 def test_main(verbose=None): diff --git a/Misc/NEWS b/Misc/NEWS index 856eae4cf4a..20ffcc9bd6b 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -233,6 +233,10 @@ Core and Builtins 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 #8862: Fixed curses cleanup when getkey is interrputed by a signal. - Issue #17443: impalib.IMAP4_stream was using the default unbuffered IO