Commit 669b755c authored by R David Murray's avatar R David Murray

#14269: smtpd now conforms to the RFC and requires HELO before MAIL.

This is a backward incompatible change, but since it is an RFC conformance bug
and all real mail servers that I know of do conform to the RFC in this regard,
I believe it is an acceptable change for a feature release.

Patch by Jason Killen.
parent b4dcb105
...@@ -374,6 +374,10 @@ class SMTPChannel(asynchat.async_chat): ...@@ -374,6 +374,10 @@ class SMTPChannel(asynchat.async_chat):
return address return address
def smtp_MAIL(self, arg): def smtp_MAIL(self, arg):
if not self.seen_greeting:
self.push('503 Error: send HELO first');
return
print('===> MAIL', arg, file=DEBUGSTREAM) print('===> MAIL', arg, file=DEBUGSTREAM)
address = self.__getaddr('FROM:', arg) if arg else None address = self.__getaddr('FROM:', arg) if arg else None
if not address: if not address:
...@@ -387,6 +391,10 @@ class SMTPChannel(asynchat.async_chat): ...@@ -387,6 +391,10 @@ class SMTPChannel(asynchat.async_chat):
self.push('250 Ok') self.push('250 Ok')
def smtp_RCPT(self, arg): def smtp_RCPT(self, arg):
if not self.seen_greeting:
self.push('503 Error: send HELO first');
return
print('===> RCPT', arg, file=DEBUGSTREAM) print('===> RCPT', arg, file=DEBUGSTREAM)
if not self.mailfrom: if not self.mailfrom:
self.push('503 Error: need MAIL command') self.push('503 Error: need MAIL command')
...@@ -411,6 +419,10 @@ class SMTPChannel(asynchat.async_chat): ...@@ -411,6 +419,10 @@ class SMTPChannel(asynchat.async_chat):
self.push('250 Ok') self.push('250 Ok')
def smtp_DATA(self, arg): def smtp_DATA(self, arg):
if not self.seen_greeting:
self.push('503 Error: send HELO first');
return
if not self.rcpttos: if not self.rcpttos:
self.push('503 Error: need RCPT command') self.push('503 Error: need RCPT command')
return return
......
...@@ -39,6 +39,7 @@ class SMTPDServerTest(TestCase): ...@@ -39,6 +39,7 @@ class SMTPDServerTest(TestCase):
channel.socket.queue_recv(line) channel.socket.queue_recv(line)
channel.handle_read() channel.handle_read()
write_line(b'HELO test.example')
write_line(b'MAIL From:eggs@example') write_line(b'MAIL From:eggs@example')
write_line(b'RCPT To:spam@example') write_line(b'RCPT To:spam@example')
write_line(b'DATA') write_line(b'DATA')
...@@ -104,6 +105,11 @@ class SMTPDChannelTest(TestCase): ...@@ -104,6 +105,11 @@ class SMTPDChannelTest(TestCase):
self.write_line(b'NOOP') self.write_line(b'NOOP')
self.assertEqual(self.channel.socket.last, b'250 Ok\r\n') self.assertEqual(self.channel.socket.last, b'250 Ok\r\n')
def test_HELO_NOOP(self):
self.write_line(b'HELO example')
self.write_line(b'NOOP')
self.assertEqual(self.channel.socket.last, b'250 Ok\r\n')
def test_NOOP_bad_syntax(self): def test_NOOP_bad_syntax(self):
self.write_line(b'NOOP hi') self.write_line(b'NOOP hi')
self.assertEqual(self.channel.socket.last, self.assertEqual(self.channel.socket.last,
...@@ -113,17 +119,23 @@ class SMTPDChannelTest(TestCase): ...@@ -113,17 +119,23 @@ class SMTPDChannelTest(TestCase):
self.write_line(b'QUIT') self.write_line(b'QUIT')
self.assertEqual(self.channel.socket.last, b'221 Bye\r\n') self.assertEqual(self.channel.socket.last, b'221 Bye\r\n')
def test_HELO_QUIT(self):
self.write_line(b'HELO example')
self.write_line(b'QUIT')
self.assertEqual(self.channel.socket.last, b'221 Bye\r\n')
def test_QUIT_arg_ignored(self): def test_QUIT_arg_ignored(self):
self.write_line(b'QUIT bye bye') self.write_line(b'QUIT bye bye')
self.assertEqual(self.channel.socket.last, b'221 Bye\r\n') self.assertEqual(self.channel.socket.last, b'221 Bye\r\n')
def test_bad_state(self): def test_bad_state(self):
self.channel.smtp_state = 'BAD STATE' self.channel.smtp_state = 'BAD STATE'
self.write_line(b'HELO') self.write_line(b'HELO example')
self.assertEqual(self.channel.socket.last, self.assertEqual(self.channel.socket.last,
b'451 Internal confusion\r\n') b'451 Internal confusion\r\n')
def test_command_too_long(self): def test_command_too_long(self):
self.write_line(b'HELO example')
self.write_line(b'MAIL from ' + self.write_line(b'MAIL from ' +
b'a' * self.channel.command_size_limit + b'a' * self.channel.command_size_limit +
b'@example') b'@example')
...@@ -133,6 +145,7 @@ class SMTPDChannelTest(TestCase): ...@@ -133,6 +145,7 @@ class SMTPDChannelTest(TestCase):
def test_data_too_long(self): def test_data_too_long(self):
# Small hack. Setting limit to 2K octets here will save us some time. # Small hack. Setting limit to 2K octets here will save us some time.
self.channel.data_size_limit = 2048 self.channel.data_size_limit = 2048
self.write_line(b'HELO example')
self.write_line(b'MAIL From:eggs@example') self.write_line(b'MAIL From:eggs@example')
self.write_line(b'RCPT To:spam@example') self.write_line(b'RCPT To:spam@example')
self.write_line(b'DATA') self.write_line(b'DATA')
...@@ -142,43 +155,61 @@ class SMTPDChannelTest(TestCase): ...@@ -142,43 +155,61 @@ class SMTPDChannelTest(TestCase):
b'552 Error: Too much mail data\r\n') b'552 Error: Too much mail data\r\n')
def test_need_MAIL(self): def test_need_MAIL(self):
self.write_line(b'HELO example')
self.write_line(b'RCPT to:spam@example') self.write_line(b'RCPT to:spam@example')
self.assertEqual(self.channel.socket.last, self.assertEqual(self.channel.socket.last,
b'503 Error: need MAIL command\r\n') b'503 Error: need MAIL command\r\n')
def test_MAIL_syntax(self): def test_MAIL_syntax(self):
self.write_line(b'HELO example')
self.write_line(b'MAIL from eggs@example') self.write_line(b'MAIL from eggs@example')
self.assertEqual(self.channel.socket.last, self.assertEqual(self.channel.socket.last,
b'501 Syntax: MAIL FROM:<address>\r\n') b'501 Syntax: MAIL FROM:<address>\r\n')
def test_MAIL_missing_from(self): def test_MAIL_missing_from(self):
self.write_line(b'HELO example')
self.write_line(b'MAIL from:') self.write_line(b'MAIL from:')
self.assertEqual(self.channel.socket.last, self.assertEqual(self.channel.socket.last,
b'501 Syntax: MAIL FROM:<address>\r\n') b'501 Syntax: MAIL FROM:<address>\r\n')
def test_MAIL_chevrons(self): def test_MAIL_chevrons(self):
self.write_line(b'HELO example')
self.write_line(b'MAIL from:<eggs@example>') self.write_line(b'MAIL from:<eggs@example>')
self.assertEqual(self.channel.socket.last, b'250 Ok\r\n') self.assertEqual(self.channel.socket.last, b'250 Ok\r\n')
def test_nested_MAIL(self): def test_nested_MAIL(self):
self.write_line(b'HELO example')
self.write_line(b'MAIL from:eggs@example') self.write_line(b'MAIL from:eggs@example')
self.write_line(b'MAIL from:spam@example') self.write_line(b'MAIL from:spam@example')
self.assertEqual(self.channel.socket.last, self.assertEqual(self.channel.socket.last,
b'503 Error: nested MAIL command\r\n') b'503 Error: nested MAIL command\r\n')
def test_no_HELO_MAIL(self):
self.write_line(b'MAIL from:<foo@example.com>')
self.assertEqual(self.channel.socket.last,
b'503 Error: send HELO first\r\n')
def test_need_RCPT(self): def test_need_RCPT(self):
self.write_line(b'HELO example')
self.write_line(b'MAIL From:eggs@example') self.write_line(b'MAIL From:eggs@example')
self.write_line(b'DATA') self.write_line(b'DATA')
self.assertEqual(self.channel.socket.last, self.assertEqual(self.channel.socket.last,
b'503 Error: need RCPT command\r\n') b'503 Error: need RCPT command\r\n')
def test_RCPT_syntax(self): def test_RCPT_syntax(self):
self.write_line(b'HELO example')
self.write_line(b'MAIL From:eggs@example') self.write_line(b'MAIL From:eggs@example')
self.write_line(b'RCPT to eggs@example') self.write_line(b'RCPT to eggs@example')
self.assertEqual(self.channel.socket.last, self.assertEqual(self.channel.socket.last,
b'501 Syntax: RCPT TO: <address>\r\n') b'501 Syntax: RCPT TO: <address>\r\n')
def test_no_HELO_RCPT(self):
self.write_line(b'RCPT to eggs@example')
self.assertEqual(self.channel.socket.last,
b'503 Error: send HELO first\r\n')
def test_data_dialog(self): def test_data_dialog(self):
self.write_line(b'HELO example')
self.write_line(b'MAIL From:eggs@example') self.write_line(b'MAIL From:eggs@example')
self.assertEqual(self.channel.socket.last, b'250 Ok\r\n') self.assertEqual(self.channel.socket.last, b'250 Ok\r\n')
self.write_line(b'RCPT To:spam@example') self.write_line(b'RCPT To:spam@example')
...@@ -193,12 +224,19 @@ class SMTPDChannelTest(TestCase): ...@@ -193,12 +224,19 @@ class SMTPDChannelTest(TestCase):
[('peer', 'eggs@example', ['spam@example'], 'data\nmore')]) [('peer', 'eggs@example', ['spam@example'], 'data\nmore')])
def test_DATA_syntax(self): def test_DATA_syntax(self):
self.write_line(b'HELO example')
self.write_line(b'MAIL From:eggs@example') self.write_line(b'MAIL From:eggs@example')
self.write_line(b'RCPT To:spam@example') self.write_line(b'RCPT To:spam@example')
self.write_line(b'DATA spam') self.write_line(b'DATA spam')
self.assertEqual(self.channel.socket.last, b'501 Syntax: DATA\r\n') self.assertEqual(self.channel.socket.last, b'501 Syntax: DATA\r\n')
def test_no_HELO_DATA(self):
self.write_line(b'DATA spam')
self.assertEqual(self.channel.socket.last,
b'503 Error: send HELO first\r\n')
def test_data_transparency_section_4_5_2(self): def test_data_transparency_section_4_5_2(self):
self.write_line(b'HELO example')
self.write_line(b'MAIL From:eggs@example') self.write_line(b'MAIL From:eggs@example')
self.write_line(b'RCPT To:spam@example') self.write_line(b'RCPT To:spam@example')
self.write_line(b'DATA') self.write_line(b'DATA')
...@@ -206,6 +244,7 @@ class SMTPDChannelTest(TestCase): ...@@ -206,6 +244,7 @@ class SMTPDChannelTest(TestCase):
self.assertEqual(self.channel.received_data, '.') self.assertEqual(self.channel.received_data, '.')
def test_multiple_RCPT(self): def test_multiple_RCPT(self):
self.write_line(b'HELO example')
self.write_line(b'MAIL From:eggs@example') self.write_line(b'MAIL From:eggs@example')
self.write_line(b'RCPT To:spam@example') self.write_line(b'RCPT To:spam@example')
self.write_line(b'RCPT To:ham@example') self.write_line(b'RCPT To:ham@example')
...@@ -216,6 +255,7 @@ class SMTPDChannelTest(TestCase): ...@@ -216,6 +255,7 @@ class SMTPDChannelTest(TestCase):
def test_manual_status(self): def test_manual_status(self):
# checks that the Channel is able to return a custom status message # checks that the Channel is able to return a custom status message
self.write_line(b'HELO example')
self.write_line(b'MAIL From:eggs@example') self.write_line(b'MAIL From:eggs@example')
self.write_line(b'RCPT To:spam@example') self.write_line(b'RCPT To:spam@example')
self.write_line(b'DATA') self.write_line(b'DATA')
...@@ -223,6 +263,7 @@ class SMTPDChannelTest(TestCase): ...@@ -223,6 +263,7 @@ class SMTPDChannelTest(TestCase):
self.assertEqual(self.channel.socket.last, b'250 Okish\r\n') self.assertEqual(self.channel.socket.last, b'250 Okish\r\n')
def test_RSET(self): def test_RSET(self):
self.write_line(b'HELO example')
self.write_line(b'MAIL From:eggs@example') self.write_line(b'MAIL From:eggs@example')
self.write_line(b'RCPT To:spam@example') self.write_line(b'RCPT To:spam@example')
self.write_line(b'RSET') self.write_line(b'RSET')
...@@ -234,6 +275,11 @@ class SMTPDChannelTest(TestCase): ...@@ -234,6 +275,11 @@ class SMTPDChannelTest(TestCase):
self.assertEqual(self.server.messages, self.assertEqual(self.server.messages,
[('peer', 'foo@example', ['eggs@example'], 'data')]) [('peer', 'foo@example', ['eggs@example'], 'data')])
def test_HELO_RSET(self):
self.write_line(b'HELO example')
self.write_line(b'RSET')
self.assertEqual(self.channel.socket.last, b'250 Ok\r\n')
def test_RSET_syntax(self): def test_RSET_syntax(self):
self.write_line(b'RSET hi') self.write_line(b'RSET hi')
self.assertEqual(self.channel.socket.last, b'501 Syntax: RSET\r\n') self.assertEqual(self.channel.socket.last, b'501 Syntax: RSET\r\n')
......
...@@ -531,6 +531,7 @@ Magnus Kessler ...@@ -531,6 +531,7 @@ Magnus Kessler
Lawrence Kesteloot Lawrence Kesteloot
Vivek Khera Vivek Khera
Mads Kiilerich Mads Kiilerich
Jason Killen
Taek Joo Kim Taek Joo Kim
W. Trevor King W. Trevor King
Paul Kippes Paul Kippes
......
...@@ -30,6 +30,9 @@ Core and Builtins ...@@ -30,6 +30,9 @@ Core and Builtins
Library Library
------- -------
- Issue #14269: SMTPD now conforms to the RFC and requires a HELO command
before MAIL, RCPT, or DATA.
- Issue #13694: asynchronous connect in asyncore.dispatcher does not set addr - Issue #13694: asynchronous connect in asyncore.dispatcher does not set addr
attribute. attribute.
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment