Alberto Trevino <albe...@byu.edu> added the comment:
On Monday, July 05, 2010 10:41:28 am you wrote:
> Yes, the fact that there are no unit tests for the new functionality.
Sorry to take so long to reply. I have attached the latest version of the
patch which does everything in rev. 2 of the patch, patches the setuid
problem discussed in issue 9168, updates to the unit test to account for the
changes and to test the new functionality, plus some little changes here and
there.
Please review and advise.
----------
Added file:
http://bugs.python.org/file18332/smtpd.py-0.2-rfc5321-enhancements-4.diff
_______________________________________
Python tracker <rep...@bugs.python.org>
<http://bugs.python.org/issue8739>
_______________________________________
diff -aur Python-3.1.2.orig/Lib/smtpd.py Python-3.1.2/Lib/smtpd.py
--- Python-3.1.2.orig/Lib/smtpd.py 2009-02-21 13:59:32.000000000 -0700
+++ Python-3.1.2/Lib/smtpd.py 2010-08-02 08:23:04.424066197 -0600
@@ -1,5 +1,5 @@
#! /usr/bin/env python
-"""An RFC 2821 smtp proxy.
+"""An RFC 5321 smtp proxy.
Usage: %(program)s [options] [localhost:localport [remotehost:remoteport]]
@@ -20,6 +20,11 @@
Use `classname' as the concrete SMTP proxy class. Uses `PureProxy' by
default.
+ --size limit
+ -s limit
+ Restrict the total size of the incoming message to "limit" number of
+ bytes. Defaults to 0 (no limit).
+
--debug
-d
Turn on debugging prints.
@@ -35,10 +40,9 @@
and if remoteport is not given, then 25 is used.
"""
-
# Overview:
#
-# This file implements the minimal SMTP protocol as defined in RFC 821. It
+# This file implements the minimal SMTP protocol as defined in RFC 5321. It
# has a hierarchy of classes which implement the backend functionality for the
# smtpd. A number of classes are provided:
#
@@ -59,15 +63,18 @@
# gets forwarded to a real backend smtpd, as with PureProxy. Again, errors
# are not handled correctly yet.
#
-# Please note that this script requires Python 2.0
+# Please note that this script requires Python 3.0
#
# Author: Barry Warsaw <ba...@python.org>
#
+# Contributors:
+# Alberto Trevino <albe...@byu.edu>
+#
# TODO:
#
# - support mailbox delivery
# - alias files
-# - ESMTP
+# - Handle more ESMTP extensions
# - handle error codes from the backend smtpd
import sys
@@ -82,7 +89,7 @@
__all__ = ["SMTPServer","DebuggingServer","PureProxy","MailmanProxy"]
program = sys.argv[0]
-__version__ = 'Python SMTP proxy version 0.2'
+__version__ = 'Python SMTP proxy version 0.21'
class Devnull:
@@ -96,7 +103,6 @@
COMMASPACE = ', '
-
def usage(code, msg=''):
print(__doc__ % globals(), file=sys.stderr)
if msg:
@@ -104,16 +110,16 @@
sys.exit(code)
-
class SMTPChannel(asynchat.async_chat):
COMMAND = 0
DATA = 1
- def __init__(self, server, conn, addr):
+ def __init__(self, server, conn, addr, size):
asynchat.async_chat.__init__(self, conn)
self.__server = server
self.__conn = conn
self.__addr = addr
+ self.__size = size
self.__line = []
self.__state = self.COMMAND
self.__greeting = 0
@@ -122,6 +128,7 @@
self.__data = ''
self.__fqdn = socket.getfqdn()
self.__peer = conn.getpeername()
+ self.__8bitmime = False
print('Peer:', repr(self.__peer), file=DEBUGSTREAM)
self.push('220 %s %s' % (self.__fqdn, __version__))
self.set_terminator(b'\r\n')
@@ -153,7 +160,7 @@
arg = line[i+1:].strip()
method = getattr(self, 'smtp_' + command, None)
if not method:
- self.push('502 Error: command "%s" not implemented' % command)
+ self.push('500 Error: command "%s" not recognized' % command)
return
method(arg)
return
@@ -162,7 +169,7 @@
self.push('451 Internal confusion')
return
# Remove extraneous carriage returns and de-transparency according
- # to RFC 821, Section 4.5.2.
+ # to RFC 5321, Section 4.5.2.
data = []
for text in line.split('\r\n'):
if text and text[0] == '.':
@@ -170,18 +177,39 @@
else:
data.append(text)
self.__data = NEWLINE.join(data)
- status = self.__server.process_message(self.__peer,
- self.__mailfrom,
- self.__rcpttos,
- self.__data)
- self.__rcpttos = []
- self.__mailfrom = None
- self.__state = self.COMMAND
- self.set_terminator(b'\r\n')
- if not status:
- self.push('250 Ok')
+
+ # Enforce data size limit
+ if self.__size == 0 or len(self.__data) <= self.__size:
+ status = self.__server.process_message(self.__peer,
+ self.__mailfrom,
+ self.__rcpttos,
+ self.__data)
+ self.__rcpttos = []
+ self.__mailfrom = None
+ self.__state = self.COMMAND
+ self.set_terminator(b'\r\n')
+ if not status:
+ self.push('250 OK')
+ else:
+ self.push(status)
else:
- self.push(status)
+ self.__state = self.COMMAND
+ self.set_terminator(b'\r\n')
+ self.push('552 Too much mail data')
+
+ # factored
+ def __getaddr(self, keyword, arg):
+ address = None
+ keylen = len(keyword)
+ if arg[:keylen].upper() == keyword:
+ address = arg[keylen:].strip()
+ if not address:
+ pass
+ elif address[0] == '<' and address[-1] == '>' and address != '<>':
+ # Addresses can be in the form <per...@dom.com> but watch out
+ # for null address, e.g. <>
+ address = address[1:-1]
+ return address
# SMTP and ESMTP commands
def smtp_HELO(self, arg):
@@ -194,30 +222,73 @@
self.__greeting = arg
self.push('250 %s' % self.__fqdn)
+ def smtp_EHLO(self, arg):
+ if not arg:
+ self.push('501 Syntax: EHLO hostname')
+ return
+ if self.__greeting:
+ self.push('503 Duplicate HELO/EHLO')
+ else:
+ self.__greeting = arg
+ self.push('250-%s' % self.__fqdn)
+ self.push('250-8BITMIME')
+ if self.__size > 0:
+ self.push('250-SIZE %s' % self.__size)
+ self.push('250 HELP')
+
def smtp_NOOP(self, arg):
if arg:
self.push('501 Syntax: NOOP')
else:
- self.push('250 Ok')
+ self.push('250 OK')
def smtp_QUIT(self, arg):
# args is ignored
self.push('221 Bye')
self.close_when_done()
- # factored
- def __getaddr(self, keyword, arg):
- address = None
- keylen = len(keyword)
- if arg[:keylen].upper() == keyword:
- address = arg[keylen:].strip()
- if not address:
- pass
- elif address[0] == '<' and address[-1] == '>' and address != '<>':
- # Addresses can be in the form <per...@dom.com> but watch out
- # for null address, e.g. <>
- address = address[1:-1]
- return address
+ def smtp_8BITMIME(self, arg):
+ # There is nothing in this code that forces 7 bits, so it seems OK
+ # to simply accept this command; its value is saved for future use;
+ # args is ignored
+ self.__8bitmime = True
+ self.push('250 OK')
+
+ def smtp_HELP(self, arg):
+ if arg:
+ lc_arg = arg.upper()
+ if lc_arg == 'EHLO':
+ self.push('250 EHLO your_fqdn')
+ elif lc_arg == 'HELO':
+ self.push('250 HELO your_fqdn')
+ elif lc_arg == 'MAIL':
+ self.push('250 MAIL FROM:<sen...@your.domain>')
+ elif lc_arg == 'RCPT':
+ self.push('250 RCPT TO:<recipi...@this.domain')
+ elif lc_arg == 'DATA':
+ self.push('250 DATA')
+ elif lc_arg == 'RSET':
+ self.push('250 RSET')
+ elif lc_arg == 'NOOP':
+ self.push('250 NOOP')
+ elif lc_arg == 'QUIT':
+ self.push('250 QUIT')
+ elif lc_arg == 'VRFY':
+ self.push('250 VRFY user | u...@this.domain')
+ else:
+ self.push('250 SUPPORTED COMMANDS: EHLO HELO MAIL RCPT ' + \
+ 'DATA RSET NOOP QUIT VRFY')
+ else:
+ self.push('250 SUPPORTED COMMANDS: EHLO HELO MAIL RCPT DATA ' + \
+ 'RSET NOOP QUIT VRFY')
+
+ def smtp_VRFY(self, arg):
+ address = self.__getaddr('', arg) if arg else None
+ if address:
+ self.push('252 Cannot VRFY user, but will accept message and ' + \
+ 'attempt delivery')
+ else:
+ self.push('502 Could not VRFY %s' % arg)
def smtp_MAIL(self, arg):
print('===> MAIL', arg, file=DEBUGSTREAM)
@@ -230,7 +301,7 @@
return
self.__mailfrom = address
print('sender:', self.__mailfrom, file=DEBUGSTREAM)
- self.push('250 Ok')
+ self.push('250 OK')
def smtp_RCPT(self, arg):
print('===> RCPT', arg, file=DEBUGSTREAM)
@@ -243,7 +314,7 @@
return
self.__rcpttos.append(address)
print('recips:', self.__rcpttos, file=DEBUGSTREAM)
- self.push('250 Ok')
+ self.push('250 OK')
def smtp_RSET(self, arg):
if arg:
@@ -254,7 +325,7 @@
self.__rcpttos = []
self.__data = ''
self.__state = self.COMMAND
- self.push('250 Ok')
+ self.push('250 OK')
def smtp_DATA(self, arg):
if not self.__rcpttos:
@@ -267,12 +338,16 @@
self.set_terminator(b'\r\n.\r\n')
self.push('354 End data with <CR><LF>.<CR><LF>')
+ # Commands that have not been implemented
+ def smtp_EXPN(self, arg):
+ self.push('502 EXPN not implemented')
+
-
class SMTPServer(asyncore.dispatcher):
- def __init__(self, localaddr, remoteaddr):
+ def __init__(self, localaddr, remoteaddr, size = 0):
self._localaddr = localaddr
self._remoteaddr = remoteaddr
+ self._size = size
asyncore.dispatcher.__init__(self)
self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
# try to re-use a server port if possible
@@ -286,7 +361,7 @@
def handle_accept(self):
conn, addr = self.accept()
print('Incoming connection from %s' % repr(addr), file=DEBUGSTREAM)
- channel = SMTPChannel(self, conn, addr)
+ channel = SMTPChannel(self, conn, addr, self._size)
# API for "doing something useful with the message"
def process_message(self, peer, mailfrom, rcpttos, data):
@@ -307,14 +382,13 @@
containing a `.' followed by other text has had the leading dot
removed.
- This function should return None, for a normal `250 Ok' response;
+ This function should return None, for a normal `250 OK' response;
otherwise it returns the desired response string in RFC 821 format.
"""
raise NotImplementedError
-
class DebuggingServer(SMTPServer):
# Do something with the gathered message
def process_message(self, peer, mailfrom, rcpttos, data):
@@ -330,7 +404,6 @@
print('------------ END MESSAGE ------------')
-
class PureProxy(SMTPServer):
def process_message(self, peer, mailfrom, rcpttos, data):
lines = data.split('\n')
@@ -371,7 +444,6 @@
return refused
-
class MailmanProxy(PureProxy):
def process_message(self, peer, mailfrom, rcpttos, data):
from io import StringIO
@@ -450,19 +522,18 @@
msg.Enqueue(mlist, torequest=1)
-
class Options:
setuid = 1
classname = 'PureProxy'
+ size = 0
-
def parseargs():
global DEBUGSTREAM
try:
opts, args = getopt.getopt(
- sys.argv[1:], 'nVhc:d',
- ['class=', 'nosetuid', 'version', 'help', 'debug'])
+ sys.argv[1:], 'nVhc:ds:',
+ ['class=', 'nosetuid', 'version', 'help', 'debug', 'size='])
except getopt.error as e:
usage(1, e)
@@ -479,6 +550,13 @@
options.classname = arg
elif opt in ('-d', '--debug'):
DEBUGSTREAM = sys.stderr
+ elif opt in ('-s', '--size'):
+ try:
+ int_size = int(arg)
+ options.size = int_size
+ except:
+ print('Invalid size: ' + arg, file=sys.stderr)
+ sys.exit(1)
# parse the rest of the arguments
if len(args) < 1:
@@ -513,33 +591,45 @@
return options
-
if __name__ == '__main__':
options = parseargs()
+ classname = options.classname
+ if "." in classname:
+ lastdot = classname.rfind(".")
+ mod = __import__(classname[:lastdot], globals(), locals(), [""])
+ classname = classname[lastdot+1:]
+ else:
+ import __main__ as mod
+ class_ = getattr(mod, classname)
+ proxy = class_((options.localhost, options.localport),
+ (options.remotehost, options.remoteport))
+ classname = options.classname
+ if "." in classname:
+ lastdot = classname.rfind(".")
+ mod = __import__(classname[:lastdot], globals(), locals(), [""])
+ classname = classname[lastdot+1:]
+ else:
+ import __main__ as mod
+ class_ = getattr(mod, classname)
+ proxy = class_((options.localhost, options.localport),
+ (options.remotehost, options.remoteport),
+ options.size)
# Become nobody
if options.setuid:
try:
import pwd
except ImportError:
- print('Cannot import module "pwd"; try running with -n option.',
file=sys.stderr)
+ print('Cannot import module "pwd"; try running with -n option.', \
+ file=sys.stderr)
sys.exit(1)
nobody = pwd.getpwnam('nobody')[2]
try:
os.setuid(nobody)
except OSError as e:
if e.errno != errno.EPERM: raise
- print('Cannot setuid "nobody"; try running with -n option.',
file=sys.stderr)
+ print('Cannot setuid "nobody"; try running with -n option.', \
+ file=sys.stderr)
sys.exit(1)
- classname = options.classname
- if "." in classname:
- lastdot = classname.rfind(".")
- mod = __import__(classname[:lastdot], globals(), locals(), [""])
- classname = classname[lastdot+1:]
- else:
- import __main__ as mod
- class_ = getattr(mod, classname)
- proxy = class_((options.localhost, options.localport),
- (options.remotehost, options.remoteport))
try:
asyncore.loop()
except KeyboardInterrupt:
diff -aur Python-3.1.2.orig/Lib/test/test_smtplib.py
Python-3.1.2/Lib/test/test_smtplib.py
--- Python-3.1.2.orig/Lib/test/test_smtplib.py 2009-05-29 12:03:16.000000000
-0600
+++ Python-3.1.2/Lib/test/test_smtplib.py 2010-08-02 08:25:52.587378508
-0600
@@ -153,7 +153,9 @@
self.serv_evt = threading.Event()
self.client_evt = threading.Event()
self.port = support.find_unused_port()
- self.serv = smtpd.DebuggingServer((HOST, self.port), ('nowhere', -1))
+ # Set a small size to test DATA size limits; it won't affect other
tests
+ self.size = 20
+ self.serv = smtpd.DebuggingServer((HOST, self.port), ('nowhere', -1),
self.size)
serv_args = (self.serv, self.serv_evt, self.client_evt)
threading.Thread(target=debugging_server, args=serv_args).start()
@@ -176,27 +178,26 @@
def testNOOP(self):
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost',
timeout=3)
- expected = (250, b'Ok')
+ expected = (250, b'OK')
self.assertEqual(smtp.noop(), expected)
smtp.quit()
def testRSET(self):
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost',
timeout=3)
- expected = (250, b'Ok')
+ expected = (250, b'OK')
self.assertEqual(smtp.rset(), expected)
smtp.quit()
def testNotImplemented(self):
- # EHLO isn't implemented in DebuggingServer
+ # EXPN isn't implemented in DebuggingServer
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost',
timeout=3)
- expected = (502, b'Error: command "EHLO" not implemented')
- self.assertEqual(smtp.ehlo(), expected)
+ expected = (502, b'EXPN not implemented')
+ self.assertEqual(smtp.expn('nob...@nowhere.com'), expected)
smtp.quit()
def testVRFY(self):
- # VRFY isn't implemented in DebuggingServer
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost',
timeout=3)
- expected = (502, b'Error: command "VRFY" not implemented')
+ expected = (252, b'Cannot VRFY user, but will accept message and
attempt delivery')
self.assertEqual(smtp.vrfy('nob...@nowhere.com'), expected)
self.assertEqual(smtp.verify('nob...@nowhere.com'), expected)
smtp.quit()
@@ -212,7 +213,35 @@
def testHELP(self):
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost',
timeout=3)
- self.assertEqual(smtp.help(), b'Error: command "HELP" not implemented')
+ self.assertEqual(smtp.help(), b'SUPPORTED COMMANDS: EHLO HELO MAIL
RCPT DATA RSET NOOP QUIT VRFY')
+ smtp.quit()
+
+ def testHELPARG(self):
+ # Test arguments to HELP
+ smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost',
timeout=3)
+ self.assertEqual(smtp.help('EHLO'), b'EHLO your_fqdn')
+ smtp.quit()
+
+ def test8BITMIME(self):
+ # Tests the 8BITMIME command; should simply return 250 OK
+ smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost',
timeout=3)
+ smtp.ehlo()
+ smtp.putcmd('8BITMIME')
+ self.assertEqual(smtp.getreply(), (250, b'OK'))
+ smtp.quit()
+
+ def testSIZELIMIT(self):
+ # Send a test message longer than self.size to trigger error
+ m = 'A test message that is too long'
+ smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost',
timeout=3)
+ try:
+ smtp.sendmail('John', 'Sally', m)
+ raise 'Expected error code not received'
+ except smtplib.SMTPDataError as err:
+ self.assertEqual(str(err), "(552, b'Too much mail data')")
+
+ # See below for time out reason
+ time.sleep(0.01)
smtp.quit()
def testSend(self):
@@ -305,7 +334,7 @@
def __init__(self, extra_features, *args, **kw):
self._extrafeatures = ''.join(
[ "250-{0}\r\n".format(x) for x in extra_features ])
- super(SimSMTPChannel, self).__init__(*args, **kw)
+ super(SimSMTPChannel, self).__init__(*args, size = 0, **kw)
def smtp_EHLO(self, arg):
resp = ('250-testhost\r\n'
_______________________________________________
Python-bugs-list mailing list
Unsubscribe:
http://mail.python.org/mailman/options/python-bugs-list/archive%40mail-archive.com