aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/Lib/smtpd.py
diff options
context:
space:
mode:
Diffstat (limited to 'Lib/smtpd.py')
-rwxr-xr-xLib/smtpd.py324
1 files changed, 238 insertions, 86 deletions
diff --git a/Lib/smtpd.py b/Lib/smtpd.py
index b4d208b2eeb..8cd405c5f6c 100755
--- a/Lib/smtpd.py
+++ b/Lib/smtpd.py
@@ -1,4 +1,4 @@
-#! /usr/bin/env python
+#! /usr/bin/env python3
"""An RFC 2821 smtp proxy.
Usage: %(program)s [options] [localhost:localport [remotehost:remoteport]]
@@ -35,6 +35,7 @@ given then 8025 is used. If remotehost is not given then `localhost' is used,
and if remoteport is not given, then 25 is used.
"""
+
# Overview:
#
# This file implements the minimal SMTP protocol as defined in RFC 821. It
@@ -58,7 +59,6 @@ and if remoteport is not given, then 25 is used.
# 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
#
# Author: Barry Warsaw <barry@python.org>
#
@@ -77,6 +77,7 @@ import time
import socket
import asyncore
import asynchat
+from warnings import warn
__all__ = ["SMTPServer","DebuggingServer","PureProxy","MailmanProxy"]
@@ -95,56 +96,198 @@ EMPTYSTRING = ''
COMMASPACE = ', '
+
def usage(code, msg=''):
- print >> sys.stderr, __doc__ % globals()
+ print(__doc__ % globals(), file=sys.stderr)
if msg:
- print >> sys.stderr, msg
+ print(msg, file=sys.stderr)
sys.exit(code)
+
class SMTPChannel(asynchat.async_chat):
COMMAND = 0
DATA = 1
+ data_size_limit = 33554432
+ command_size_limit = 512
+
def __init__(self, server, conn, addr):
asynchat.async_chat.__init__(self, conn)
- self.__server = server
- self.__conn = conn
- self.__addr = addr
- self.__line = []
- self.__state = self.COMMAND
- self.__greeting = 0
- self.__mailfrom = None
- self.__rcpttos = []
- self.__data = ''
- self.__fqdn = socket.getfqdn()
+ self.smtp_server = server
+ self.conn = conn
+ self.addr = addr
+ self.received_lines = []
+ self.smtp_state = self.COMMAND
+ self.seen_greeting = ''
+ self.mailfrom = None
+ self.rcpttos = []
+ self.received_data = ''
+ self.fqdn = socket.getfqdn()
+ self.num_bytes = 0
try:
- self.__peer = conn.getpeername()
- except socket.error, err:
+ self.peer = conn.getpeername()
+ except socket.error as err:
# a race condition may occur if the other end is closing
# before we can get the peername
self.close()
- if err[0] != errno.ENOTCONN:
+ if err.args[0] != errno.ENOTCONN:
raise
return
- print >> DEBUGSTREAM, 'Peer:', repr(self.__peer)
- self.push('220 %s %s' % (self.__fqdn, __version__))
- self.set_terminator('\r\n')
+ print('Peer:', repr(self.peer), file=DEBUGSTREAM)
+ self.push('220 %s %s' % (self.fqdn, __version__))
+ self.set_terminator(b'\r\n')
+
+ # properties for backwards-compatibility
+ @property
+ def __server(self):
+ warn("Access to __server attribute on SMTPChannel is deprecated, "
+ "use 'smtp_server' instead", PendingDeprecationWarning, 2)
+ return self.smtp_server
+ @__server.setter
+ def __server(self, value):
+ warn("Setting __server attribute on SMTPChannel is deprecated, "
+ "set 'smtp_server' instead", PendingDeprecationWarning, 2)
+ self.smtp_server = value
+
+ @property
+ def __line(self):
+ warn("Access to __line attribute on SMTPChannel is deprecated, "
+ "use 'received_lines' instead", PendingDeprecationWarning, 2)
+ return self.received_lines
+ @__line.setter
+ def __line(self, value):
+ warn("Setting __line attribute on SMTPChannel is deprecated, "
+ "set 'received_lines' instead", PendingDeprecationWarning, 2)
+ self.received_lines = value
+
+ @property
+ def __state(self):
+ warn("Access to __state attribute on SMTPChannel is deprecated, "
+ "use 'smtp_state' instead", PendingDeprecationWarning, 2)
+ return self.smtp_state
+ @__state.setter
+ def __state(self, value):
+ warn("Setting __state attribute on SMTPChannel is deprecated, "
+ "set 'smtp_state' instead", PendingDeprecationWarning, 2)
+ self.smtp_state = value
+
+ @property
+ def __greeting(self):
+ warn("Access to __greeting attribute on SMTPChannel is deprecated, "
+ "use 'seen_greeting' instead", PendingDeprecationWarning, 2)
+ return self.seen_greeting
+ @__greeting.setter
+ def __greeting(self, value):
+ warn("Setting __greeting attribute on SMTPChannel is deprecated, "
+ "set 'seen_greeting' instead", PendingDeprecationWarning, 2)
+ self.seen_greeting = value
+
+ @property
+ def __mailfrom(self):
+ warn("Access to __mailfrom attribute on SMTPChannel is deprecated, "
+ "use 'mailfrom' instead", PendingDeprecationWarning, 2)
+ return self.mailfrom
+ @__mailfrom.setter
+ def __mailfrom(self, value):
+ warn("Setting __mailfrom attribute on SMTPChannel is deprecated, "
+ "set 'mailfrom' instead", PendingDeprecationWarning, 2)
+ self.mailfrom = value
+
+ @property
+ def __rcpttos(self):
+ warn("Access to __rcpttos attribute on SMTPChannel is deprecated, "
+ "use 'rcpttos' instead", PendingDeprecationWarning, 2)
+ return self.rcpttos
+ @__rcpttos.setter
+ def __rcpttos(self, value):
+ warn("Setting __rcpttos attribute on SMTPChannel is deprecated, "
+ "set 'rcpttos' instead", PendingDeprecationWarning, 2)
+ self.rcpttos = value
+
+ @property
+ def __data(self):
+ warn("Access to __data attribute on SMTPChannel is deprecated, "
+ "use 'received_data' instead", PendingDeprecationWarning, 2)
+ return self.received_data
+ @__data.setter
+ def __data(self, value):
+ warn("Setting __data attribute on SMTPChannel is deprecated, "
+ "set 'received_data' instead", PendingDeprecationWarning, 2)
+ self.received_data = value
+
+ @property
+ def __fqdn(self):
+ warn("Access to __fqdn attribute on SMTPChannel is deprecated, "
+ "use 'fqdn' instead", PendingDeprecationWarning, 2)
+ return self.fqdn
+ @__fqdn.setter
+ def __fqdn(self, value):
+ warn("Setting __fqdn attribute on SMTPChannel is deprecated, "
+ "set 'fqdn' instead", PendingDeprecationWarning, 2)
+ self.fqdn = value
+
+ @property
+ def __peer(self):
+ warn("Access to __peer attribute on SMTPChannel is deprecated, "
+ "use 'peer' instead", PendingDeprecationWarning, 2)
+ return self.peer
+ @__peer.setter
+ def __peer(self, value):
+ warn("Setting __peer attribute on SMTPChannel is deprecated, "
+ "set 'peer' instead", PendingDeprecationWarning, 2)
+ self.peer = value
+
+ @property
+ def __conn(self):
+ warn("Access to __conn attribute on SMTPChannel is deprecated, "
+ "use 'conn' instead", PendingDeprecationWarning, 2)
+ return self.conn
+ @__conn.setter
+ def __conn(self, value):
+ warn("Setting __conn attribute on SMTPChannel is deprecated, "
+ "set 'conn' instead", PendingDeprecationWarning, 2)
+ self.conn = value
+
+ @property
+ def __addr(self):
+ warn("Access to __addr attribute on SMTPChannel is deprecated, "
+ "use 'addr' instead", PendingDeprecationWarning, 2)
+ return self.addr
+ @__addr.setter
+ def __addr(self, value):
+ warn("Setting __addr attribute on SMTPChannel is deprecated, "
+ "set 'addr' instead", PendingDeprecationWarning, 2)
+ self.addr = value
# Overrides base class for convenience
def push(self, msg):
- asynchat.async_chat.push(self, msg + '\r\n')
+ asynchat.async_chat.push(self, bytes(msg + '\r\n', 'ascii'))
# Implementation of base class abstract method
def collect_incoming_data(self, data):
- self.__line.append(data)
+ limit = None
+ if self.smtp_state == self.COMMAND:
+ limit = self.command_size_limit
+ elif self.smtp_state == self.DATA:
+ limit = self.data_size_limit
+ if limit and self.num_bytes > limit:
+ return
+ elif limit:
+ self.num_bytes += len(data)
+ self.received_lines.append(str(data, "utf8"))
# Implementation of base class abstract method
def found_terminator(self):
- line = EMPTYSTRING.join(self.__line)
- print >> DEBUGSTREAM, 'Data:', repr(line)
- self.__line = []
- if self.__state == self.COMMAND:
+ line = EMPTYSTRING.join(self.received_lines)
+ print('Data:', repr(line), file=DEBUGSTREAM)
+ self.received_lines = []
+ if self.smtp_state == self.COMMAND:
+ if self.num_bytes > self.command_size_limit:
+ self.push('500 Error: line too long')
+ self.num_bytes = 0
+ return
+ self.num_bytes = 0
if not line:
self.push('500 Error: bad syntax')
return
@@ -163,8 +306,13 @@ class SMTPChannel(asynchat.async_chat):
method(arg)
return
else:
- if self.__state != self.DATA:
+ if self.smtp_state != self.DATA:
self.push('451 Internal confusion')
+ self.num_bytes = 0
+ return
+ if self.num_bytes > self.data_size_limit:
+ self.push('552 Error: Too much mail data')
+ self.num_bytes = 0
return
# Remove extraneous carriage returns and de-transparency according
# to RFC 821, Section 4.5.2.
@@ -174,15 +322,16 @@ class SMTPChannel(asynchat.async_chat):
data.append(text[1:])
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('\r\n')
+ self.received_data = NEWLINE.join(data)
+ status = self.smtp_server.process_message(self.peer,
+ self.mailfrom,
+ self.rcpttos,
+ self.received_data)
+ self.rcpttos = []
+ self.mailfrom = None
+ self.smtp_state = self.COMMAND
+ self.num_bytes = 0
+ self.set_terminator(b'\r\n')
if not status:
self.push('250 Ok')
else:
@@ -193,11 +342,11 @@ class SMTPChannel(asynchat.async_chat):
if not arg:
self.push('501 Syntax: HELO hostname')
return
- if self.__greeting:
+ if self.seen_greeting:
self.push('503 Duplicate HELO/EHLO')
else:
- self.__greeting = arg
- self.push('250 %s' % self.__fqdn)
+ self.seen_greeting = arg
+ self.push('250 %s' % self.fqdn)
def smtp_NOOP(self, arg):
if arg:
@@ -225,29 +374,29 @@ class SMTPChannel(asynchat.async_chat):
return address
def smtp_MAIL(self, arg):
- print >> DEBUGSTREAM, '===> MAIL', arg
+ print('===> MAIL', arg, file=DEBUGSTREAM)
address = self.__getaddr('FROM:', arg) if arg else None
if not address:
self.push('501 Syntax: MAIL FROM:<address>')
return
- if self.__mailfrom:
+ if self.mailfrom:
self.push('503 Error: nested MAIL command')
return
- self.__mailfrom = address
- print >> DEBUGSTREAM, 'sender:', self.__mailfrom
+ self.mailfrom = address
+ print('sender:', self.mailfrom, file=DEBUGSTREAM)
self.push('250 Ok')
def smtp_RCPT(self, arg):
- print >> DEBUGSTREAM, '===> RCPT', arg
- if not self.__mailfrom:
+ print('===> RCPT', arg, file=DEBUGSTREAM)
+ if not self.mailfrom:
self.push('503 Error: need MAIL command')
return
address = self.__getaddr('TO:', arg) if arg else None
if not address:
self.push('501 Syntax: RCPT TO: <address>')
return
- self.__rcpttos.append(address)
- print >> DEBUGSTREAM, 'recips:', self.__rcpttos
+ self.rcpttos.append(address)
+ print('recips:', self.rcpttos, file=DEBUGSTREAM)
self.push('250 Ok')
def smtp_RSET(self, arg):
@@ -255,25 +404,29 @@ class SMTPChannel(asynchat.async_chat):
self.push('501 Syntax: RSET')
return
# Resets the sender, recipients, and data, but not the greeting
- self.__mailfrom = None
- self.__rcpttos = []
- self.__data = ''
- self.__state = self.COMMAND
+ self.mailfrom = None
+ self.rcpttos = []
+ self.received_data = ''
+ self.smtp_state = self.COMMAND
self.push('250 Ok')
def smtp_DATA(self, arg):
- if not self.__rcpttos:
+ if not self.rcpttos:
self.push('503 Error: need RCPT command')
return
if arg:
self.push('501 Syntax: DATA')
return
- self.__state = self.DATA
- self.set_terminator('\r\n.\r\n')
+ self.smtp_state = self.DATA
+ self.set_terminator(b'\r\n.\r\n')
self.push('354 End data with <CR><LF>.<CR><LF>')
+
class SMTPServer(asyncore.dispatcher):
+ # SMTPChannel class to use for managing client connections
+ channel_class = SMTPChannel
+
def __init__(self, localaddr, remoteaddr):
self._localaddr = localaddr
self._remoteaddr = remoteaddr
@@ -285,21 +438,16 @@ class SMTPServer(asyncore.dispatcher):
self.bind(localaddr)
self.listen(5)
except:
- # cleanup asyncore.socket_map before raising
self.close()
raise
else:
- print >> DEBUGSTREAM, \
- '%s started at %s\n\tLocal addr: %s\n\tRemote addr:%s' % (
+ print('%s started at %s\n\tLocal addr: %s\n\tRemote addr:%s' % (
self.__class__.__name__, time.ctime(time.time()),
- localaddr, remoteaddr)
+ localaddr, remoteaddr), file=DEBUGSTREAM)
- def handle_accept(self):
- pair = self.accept()
- if pair is not None:
- conn, addr = pair
- print >> DEBUGSTREAM, 'Incoming connection from %s' % repr(addr)
- channel = SMTPChannel(self, conn, addr)
+ def handle_accepted(self, conn, addr):
+ print('Incoming connection from %s' % repr(addr), file=DEBUGSTREAM)
+ channel = self.channel_class(self, conn, addr)
# API for "doing something useful with the message"
def process_message(self, peer, mailfrom, rcpttos, data):
@@ -327,21 +475,23 @@ class SMTPServer(asyncore.dispatcher):
raise NotImplementedError
+
class DebuggingServer(SMTPServer):
# Do something with the gathered message
def process_message(self, peer, mailfrom, rcpttos, data):
inheaders = 1
lines = data.split('\n')
- print '---------- MESSAGE FOLLOWS ----------'
+ print('---------- MESSAGE FOLLOWS ----------')
for line in lines:
# headers first
if inheaders and not line:
- print 'X-Peer:', peer[0]
+ print('X-Peer:', peer[0])
inheaders = 0
- print line
- print '------------ END MESSAGE ------------'
+ print(line)
+ print('------------ END MESSAGE ------------')
+
class PureProxy(SMTPServer):
def process_message(self, peer, mailfrom, rcpttos, data):
lines = data.split('\n')
@@ -355,7 +505,7 @@ class PureProxy(SMTPServer):
data = NEWLINE.join(lines)
refused = self._deliver(mailfrom, rcpttos, data)
# TBD: what to do with refused addresses?
- print >> DEBUGSTREAM, 'we got some refusals:', refused
+ print('we got some refusals:', refused, file=DEBUGSTREAM)
def _deliver(self, mailfrom, rcpttos, data):
import smtplib
@@ -367,11 +517,11 @@ class PureProxy(SMTPServer):
refused = s.sendmail(mailfrom, rcpttos, data)
finally:
s.quit()
- except smtplib.SMTPRecipientsRefused, e:
- print >> DEBUGSTREAM, 'got SMTPRecipientsRefused'
+ except smtplib.SMTPRecipientsRefused as e:
+ print('got SMTPRecipientsRefused', file=DEBUGSTREAM)
refused = e.recipients
- except (socket.error, smtplib.SMTPException), e:
- print >> DEBUGSTREAM, 'got', e.__class__
+ except (socket.error, smtplib.SMTPException) as e:
+ print('got', e.__class__, file=DEBUGSTREAM)
# All recipients were refused. If the exception had an associated
# error code, use it. Otherwise,fake it with a non-triggering
# exception code.
@@ -382,9 +532,10 @@ class PureProxy(SMTPServer):
return refused
+
class MailmanProxy(PureProxy):
def process_message(self, peer, mailfrom, rcpttos, data):
- from cStringIO import StringIO
+ from io import StringIO
from Mailman import Utils
from Mailman import Message
from Mailman import MailList
@@ -419,11 +570,11 @@ class MailmanProxy(PureProxy):
for rcpt, listname, command in listnames:
rcpttos.remove(rcpt)
# If there's any non-list destined recipients left,
- print >> DEBUGSTREAM, 'forwarding recips:', ' '.join(rcpttos)
+ print('forwarding recips:', ' '.join(rcpttos), file=DEBUGSTREAM)
if rcpttos:
refused = self._deliver(mailfrom, rcpttos, data)
# TBD: what to do with refused addresses?
- print >> DEBUGSTREAM, 'we got refusals:', refused
+ print('we got refusals:', refused, file=DEBUGSTREAM)
# Now deliver directly to the list commands
mlists = {}
s = StringIO(data)
@@ -431,12 +582,12 @@ class MailmanProxy(PureProxy):
# These headers are required for the proper execution of Mailman. All
# MTAs in existence seem to add these if the original message doesn't
# have them.
- if not msg.getheader('from'):
+ if not msg.get('from'):
msg['From'] = mailfrom
- if not msg.getheader('date'):
+ if not msg.get('date'):
msg['Date'] = time.ctime(time.time())
for rcpt, listname, command in listnames:
- print >> DEBUGSTREAM, 'sending message to', rcpt
+ print('sending message to', rcpt, file=DEBUGSTREAM)
mlist = mlists.get(listname)
if not mlist:
mlist = MailList.MailList(listname, lock=0)
@@ -460,18 +611,20 @@ class MailmanProxy(PureProxy):
msg.Enqueue(mlist, torequest=1)
+
class Options:
setuid = 1
classname = 'PureProxy'
+
def parseargs():
global DEBUGSTREAM
try:
opts, args = getopt.getopt(
sys.argv[1:], 'nVhc:d',
['class=', 'nosetuid', 'version', 'help', 'debug'])
- except getopt.error, e:
+ except getopt.error as e:
usage(1, e)
options = Options()
@@ -479,7 +632,7 @@ def parseargs():
if opt in ('-h', '--help'):
usage(0)
elif opt in ('-V', '--version'):
- print >> sys.stderr, __version__
+ print(__version__, file=sys.stderr)
sys.exit(0)
elif opt in ('-n', '--nosetuid'):
options.setuid = 0
@@ -521,6 +674,7 @@ def parseargs():
return options
+
if __name__ == '__main__':
options = parseargs()
# Become nobody
@@ -538,16 +692,14 @@ if __name__ == '__main__':
try:
import pwd
except ImportError:
- print >> sys.stderr, \
- 'Cannot import module "pwd"; try running with -n option.'
+ 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, e:
+ except OSError as e:
if e.errno != errno.EPERM: raise
- print >> sys.stderr, \
- 'Cannot setuid "nobody"; try running with -n option.'
+ print('Cannot setuid "nobody"; try running with -n option.', file=sys.stderr)
sys.exit(1)
try:
asyncore.loop()