]> granicus.if.org Git - python/commitdiff
#5713: Handle 421 error codes during sendmail by closing the socket.
authorR David Murray <rdmurray@bitdance.com>
Thu, 21 Mar 2013 00:36:14 +0000 (20:36 -0400)
committerR David Murray <rdmurray@bitdance.com>
Thu, 21 Mar 2013 00:36:14 +0000 (20:36 -0400)
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.

Lib/smtplib.py
Lib/test/test_smtplib.py
Misc/NEWS

index e06a9be7413abf9b9441c1f723141297628a9401..679e4782950ccfc486a9e46b43ed35e206a2283a 100644 (file)
@@ -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
index 2cb0d1ab52b46fc2e3818353e26820036cb75a20..92f986b72ee0dc3b084eff2ccd2c3c3341964d5c 100644 (file)
@@ -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):
index 856eae4cf4aa2df30f0e5db793e42094128e1415..20ffcc9bd6be6705d1db798a639ec8c875bb18d2 100644 (file)
--- 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