#17498: Defer SMTPServerDisconnected errors until the next command.

Normally an SMTP server will return an error, and smtplib will then issue an
RSET to return the connection to the known starting state.  Some servers,
however, disconnect after issuing certain errors.  When we issue the RSET,
this would result in raising an SMTPServerDisconnected error, *instead* of
returning the error code the user of the library was expecting.  This fix
makes the internal RSET calls ignore the disconnection so that the error code
is returned.  The user of the library will then get the SMTPServerDisconnected
error the next time they try to talk to the server.

Patch by Kushal Das.
This commit is contained in:
R David Murray 2014-04-14 18:21:38 -04:00
parent 4a24d09d62
commit afb151a5cc
3 changed files with 33 additions and 3 deletions

View File

@ -478,6 +478,18 @@ class SMTP:
"""SMTP 'rset' command -- resets session.""" """SMTP 'rset' command -- resets session."""
return self.docmd("rset") return self.docmd("rset")
def _rset(self):
"""Internal 'rset' command which ignores any SMTPServerDisconnected error.
Used internally in the library, since the server disconnected error
should appear to the application when the *next* command is issued, if
we are doing an internal "safety" reset.
"""
try:
self.rset()
except SMTPServerDisconnected:
pass
def noop(self): def noop(self):
"""SMTP 'noop' command -- doesn't do anything :>""" """SMTP 'noop' command -- doesn't do anything :>"""
return self.docmd("noop") return self.docmd("noop")
@ -762,7 +774,7 @@ class SMTP:
if code == 421: if code == 421:
self.close() self.close()
else: else:
self.rset() 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):
@ -776,14 +788,14 @@ class SMTP:
raise SMTPRecipientsRefused(senderrs) 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:
if code == 421: if code == 421:
self.close() self.close()
else: else:
self.rset() 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

@ -619,6 +619,7 @@ class SimSMTPChannel(smtpd.SMTPChannel):
data_response = None data_response = None
rcpt_count = 0 rcpt_count = 0
rset_count = 0 rset_count = 0
disconnect = 0
def __init__(self, extra_features, *args, **kw): def __init__(self, extra_features, *args, **kw):
self._extrafeatures = ''.join( self._extrafeatures = ''.join(
@ -684,6 +685,8 @@ class SimSMTPChannel(smtpd.SMTPChannel):
super().smtp_MAIL(arg) super().smtp_MAIL(arg)
else: else:
self.push(self.mail_response) self.push(self.mail_response)
if self.disconnect:
self.close_when_done()
def smtp_RCPT(self, arg): def smtp_RCPT(self, arg):
if self.rcpt_response is None: if self.rcpt_response is None:
@ -875,6 +878,16 @@ class SMTPSimTests(unittest.TestCase):
#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 17498: make sure _rset does not raise SMTPServerDisconnected exception
def test__rest_from_mail_cmd(self):
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=15)
smtp.noop()
self.serv._SMTPchannel.mail_response = '451 Requested action aborted'
self.serv._SMTPchannel.disconnect = True
with self.assertRaises(smtplib.SMTPSenderRefused):
smtp.sendmail('John', 'Sally', 'test message')
self.assertIsNone(smtp.sock)
# Issue 5713: make sure close, not rset, is called if we get a 421 error # Issue 5713: make sure close, not rset, is called if we get a 421 error
def test_421_from_mail_cmd(self): def test_421_from_mail_cmd(self):
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=15) smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=15)

View File

@ -37,6 +37,11 @@ Core and Builtins
Library Library
------- -------
- Issue #17498: Some SMTP servers disconnect after certain errors, violating
strict RFC conformance. Instead of losing the error code when we issue the
subsequent RSET, smtplib now returns the error code and defers raising the
SMTPServerDisconnected error until the next command is issued.
- Issue #17826: setting an iterable side_effect on a mock function created by - Issue #17826: setting an iterable side_effect on a mock function created by
create_autospec now works. Patch by Kushal Das. create_autospec now works. Patch by Kushal Das.