#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.
This commit is contained in:
parent
958f7ae865
commit
d312c740f1
|
@ -742,6 +742,9 @@ class SMTP:
|
||||||
esmtp_opts.append(option)
|
esmtp_opts.append(option)
|
||||||
(code, resp) = self.mail(from_addr, esmtp_opts)
|
(code, resp) = self.mail(from_addr, esmtp_opts)
|
||||||
if code != 250:
|
if code != 250:
|
||||||
|
if code == 421:
|
||||||
|
self.close()
|
||||||
|
else:
|
||||||
self.rset()
|
self.rset()
|
||||||
raise SMTPSenderRefused(code, resp, from_addr)
|
raise SMTPSenderRefused(code, resp, from_addr)
|
||||||
senderrs = {}
|
senderrs = {}
|
||||||
|
@ -751,12 +754,18 @@ class SMTP:
|
||||||
(code, resp) = self.rcpt(each, rcpt_options)
|
(code, resp) = self.rcpt(each, rcpt_options)
|
||||||
if (code != 250) and (code != 251):
|
if (code != 250) and (code != 251):
|
||||||
senderrs[each] = (code, resp)
|
senderrs[each] = (code, resp)
|
||||||
|
if code == 421:
|
||||||
|
self.close()
|
||||||
|
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:
|
||||||
|
self.close()
|
||||||
|
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
|
||||||
|
|
|
@ -560,6 +560,12 @@ sim_lists = {'list-1':['Mr.A@somewhere.com','Mrs.C@somewhereesle.com'],
|
||||||
# Simulated SMTP channel & server
|
# Simulated SMTP channel & server
|
||||||
class SimSMTPChannel(smtpd.SMTPChannel):
|
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):
|
def __init__(self, extra_features, *args, **kw):
|
||||||
self._extrafeatures = ''.join(
|
self._extrafeatures = ''.join(
|
||||||
[ "250-{0}\r\n".format(x) for x in extra_features ])
|
[ "250-{0}\r\n".format(x) for x in extra_features ])
|
||||||
|
@ -610,18 +616,43 @@ class SimSMTPChannel(smtpd.SMTPChannel):
|
||||||
else:
|
else:
|
||||||
self.push('550 No access for you!')
|
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):
|
def handle_error(self):
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
class SimSMTPServer(smtpd.SMTPServer):
|
class SimSMTPServer(smtpd.SMTPServer):
|
||||||
|
|
||||||
|
channel_class = SimSMTPChannel
|
||||||
|
|
||||||
def __init__(self, *args, **kw):
|
def __init__(self, *args, **kw):
|
||||||
self._extra_features = []
|
self._extra_features = []
|
||||||
smtpd.SMTPServer.__init__(self, *args, **kw)
|
smtpd.SMTPServer.__init__(self, *args, **kw)
|
||||||
|
|
||||||
def handle_accepted(self, conn, addr):
|
def handle_accepted(self, conn, addr):
|
||||||
self._SMTPchannel = SimSMTPChannel(self._extra_features,
|
self._SMTPchannel = self.channel_class(self._extra_features,
|
||||||
self, conn, addr)
|
self, conn, addr)
|
||||||
|
|
||||||
def process_message(self, peer, mailfrom, rcpttos, data):
|
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
|
#TODO: add tests for correct AUTH method fallback now that the
|
||||||
#test infrastructure can support it.
|
#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
|
@support.reap_threads
|
||||||
def test_main(verbose=None):
|
def test_main(verbose=None):
|
||||||
|
|
|
@ -233,6 +233,10 @@ Core and Builtins
|
||||||
Library
|
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 #8862: Fixed curses cleanup when getkey is interrputed by a signal.
|
||||||
|
|
||||||
- Issue #17443: impalib.IMAP4_stream was using the default unbuffered IO
|
- Issue #17443: impalib.IMAP4_stream was using the default unbuffered IO
|
||||||
|
|
Loading…
Reference in New Issue