aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'lib-python/3/smtpd.py')
-rwxr-xr-xlib-python/3/smtpd.py297
1 files changed, 225 insertions, 72 deletions
diff --git a/lib-python/3/smtpd.py b/lib-python/3/smtpd.py
index 8cd405c5f6..778d6d6280 100755
--- a/lib-python/3/smtpd.py
+++ b/lib-python/3/smtpd.py
@@ -1,5 +1,5 @@
#! /usr/bin/env python3
-"""An RFC 2821 smtp proxy.
+"""An RFC 5321 smtp proxy.
Usage: %(program)s [options] [localhost:localport [remotehost:remoteport]]
@@ -20,6 +20,11 @@ Options:
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 via the RFC 1870 SIZE extension. Defaults to 33554432 bytes.
+
--debug
-d
Turn on debugging prints.
@@ -35,10 +40,9 @@ 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
+# 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:
#
@@ -66,7 +70,7 @@ and if remoteport is not given, then 25 is used.
#
# - support mailbox delivery
# - alias files
-# - ESMTP
+# - Handle more ESMTP extensions
# - handle error codes from the backend smtpd
import sys
@@ -77,12 +81,14 @@ import time
import socket
import asyncore
import asynchat
+import collections
from warnings import warn
+from email._header_value_parser import get_addr_spec, get_angle_addr
__all__ = ["SMTPServer","DebuggingServer","PureProxy","MailmanProxy"]
program = sys.argv[0]
-__version__ = 'Python SMTP proxy version 0.2'
+__version__ = 'Python SMTP proxy version 0.3'
class Devnull:
@@ -94,9 +100,9 @@ DEBUGSTREAM = Devnull()
NEWLINE = '\n'
EMPTYSTRING = ''
COMMASPACE = ', '
+DATA_SIZE_DEFAULT = 33554432
-
def usage(code, msg=''):
print(__doc__ % globals(), file=sys.stderr)
if msg:
@@ -104,19 +110,23 @@ def usage(code, msg=''):
sys.exit(code)
-
class SMTPChannel(asynchat.async_chat):
COMMAND = 0
DATA = 1
- data_size_limit = 33554432
command_size_limit = 512
+ command_size_limits = collections.defaultdict(lambda x=command_size_limit: x)
+ command_size_limits.update({
+ 'MAIL': command_size_limit + 26,
+ })
+ max_command_size_limit = max(command_size_limits.values())
- def __init__(self, server, conn, addr):
+ def __init__(self, server, conn, addr, data_size_limit=DATA_SIZE_DEFAULT):
asynchat.async_chat.__init__(self, conn)
self.smtp_server = server
self.conn = conn
self.addr = addr
+ self.data_size_limit = data_size_limit
self.received_lines = []
self.smtp_state = self.COMMAND
self.seen_greeting = ''
@@ -137,127 +147,128 @@ class SMTPChannel(asynchat.async_chat):
print('Peer:', repr(self.peer), file=DEBUGSTREAM)
self.push('220 %s %s' % (self.fqdn, __version__))
self.set_terminator(b'\r\n')
+ self.extended_smtp = False
# properties for backwards-compatibility
@property
def __server(self):
warn("Access to __server attribute on SMTPChannel is deprecated, "
- "use 'smtp_server' instead", PendingDeprecationWarning, 2)
+ "use 'smtp_server' instead", DeprecationWarning, 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)
+ "set 'smtp_server' instead", DeprecationWarning, 2)
self.smtp_server = value
@property
def __line(self):
warn("Access to __line attribute on SMTPChannel is deprecated, "
- "use 'received_lines' instead", PendingDeprecationWarning, 2)
+ "use 'received_lines' instead", DeprecationWarning, 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)
+ "set 'received_lines' instead", DeprecationWarning, 2)
self.received_lines = value
@property
def __state(self):
warn("Access to __state attribute on SMTPChannel is deprecated, "
- "use 'smtp_state' instead", PendingDeprecationWarning, 2)
+ "use 'smtp_state' instead", DeprecationWarning, 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)
+ "set 'smtp_state' instead", DeprecationWarning, 2)
self.smtp_state = value
@property
def __greeting(self):
warn("Access to __greeting attribute on SMTPChannel is deprecated, "
- "use 'seen_greeting' instead", PendingDeprecationWarning, 2)
+ "use 'seen_greeting' instead", DeprecationWarning, 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)
+ "set 'seen_greeting' instead", DeprecationWarning, 2)
self.seen_greeting = value
@property
def __mailfrom(self):
warn("Access to __mailfrom attribute on SMTPChannel is deprecated, "
- "use 'mailfrom' instead", PendingDeprecationWarning, 2)
+ "use 'mailfrom' instead", DeprecationWarning, 2)
return self.mailfrom
@__mailfrom.setter
def __mailfrom(self, value):
warn("Setting __mailfrom attribute on SMTPChannel is deprecated, "
- "set 'mailfrom' instead", PendingDeprecationWarning, 2)
+ "set 'mailfrom' instead", DeprecationWarning, 2)
self.mailfrom = value
@property
def __rcpttos(self):
warn("Access to __rcpttos attribute on SMTPChannel is deprecated, "
- "use 'rcpttos' instead", PendingDeprecationWarning, 2)
+ "use 'rcpttos' instead", DeprecationWarning, 2)
return self.rcpttos
@__rcpttos.setter
def __rcpttos(self, value):
warn("Setting __rcpttos attribute on SMTPChannel is deprecated, "
- "set 'rcpttos' instead", PendingDeprecationWarning, 2)
+ "set 'rcpttos' instead", DeprecationWarning, 2)
self.rcpttos = value
@property
def __data(self):
warn("Access to __data attribute on SMTPChannel is deprecated, "
- "use 'received_data' instead", PendingDeprecationWarning, 2)
+ "use 'received_data' instead", DeprecationWarning, 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)
+ "set 'received_data' instead", DeprecationWarning, 2)
self.received_data = value
@property
def __fqdn(self):
warn("Access to __fqdn attribute on SMTPChannel is deprecated, "
- "use 'fqdn' instead", PendingDeprecationWarning, 2)
+ "use 'fqdn' instead", DeprecationWarning, 2)
return self.fqdn
@__fqdn.setter
def __fqdn(self, value):
warn("Setting __fqdn attribute on SMTPChannel is deprecated, "
- "set 'fqdn' instead", PendingDeprecationWarning, 2)
+ "set 'fqdn' instead", DeprecationWarning, 2)
self.fqdn = value
@property
def __peer(self):
warn("Access to __peer attribute on SMTPChannel is deprecated, "
- "use 'peer' instead", PendingDeprecationWarning, 2)
+ "use 'peer' instead", DeprecationWarning, 2)
return self.peer
@__peer.setter
def __peer(self, value):
warn("Setting __peer attribute on SMTPChannel is deprecated, "
- "set 'peer' instead", PendingDeprecationWarning, 2)
+ "set 'peer' instead", DeprecationWarning, 2)
self.peer = value
@property
def __conn(self):
warn("Access to __conn attribute on SMTPChannel is deprecated, "
- "use 'conn' instead", PendingDeprecationWarning, 2)
+ "use 'conn' instead", DeprecationWarning, 2)
return self.conn
@__conn.setter
def __conn(self, value):
warn("Setting __conn attribute on SMTPChannel is deprecated, "
- "set 'conn' instead", PendingDeprecationWarning, 2)
+ "set 'conn' instead", DeprecationWarning, 2)
self.conn = value
@property
def __addr(self):
warn("Access to __addr attribute on SMTPChannel is deprecated, "
- "use 'addr' instead", PendingDeprecationWarning, 2)
+ "use 'addr' instead", DeprecationWarning, 2)
return self.addr
@__addr.setter
def __addr(self, value):
warn("Setting __addr attribute on SMTPChannel is deprecated, "
- "set 'addr' instead", PendingDeprecationWarning, 2)
+ "set 'addr' instead", DeprecationWarning, 2)
self.addr = value
# Overrides base class for convenience
@@ -268,14 +279,14 @@ class SMTPChannel(asynchat.async_chat):
def collect_incoming_data(self, data):
limit = None
if self.smtp_state == self.COMMAND:
- limit = self.command_size_limit
+ limit = self.max_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"))
+ self.received_lines.append(str(data, "utf-8"))
# Implementation of base class abstract method
def found_terminator(self):
@@ -283,11 +294,7 @@ class SMTPChannel(asynchat.async_chat):
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
+ sz, self.num_bytes = self.num_bytes, 0
if not line:
self.push('500 Error: bad syntax')
return
@@ -299,9 +306,14 @@ class SMTPChannel(asynchat.async_chat):
else:
command = line[:i].upper()
arg = line[i+1:].strip()
+ max_sz = (self.command_size_limits[command]
+ if self.extended_smtp else self.command_size_limit)
+ if sz > max_sz:
+ self.push('500 Error: line too long')
+ return
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
@@ -310,12 +322,12 @@ class SMTPChannel(asynchat.async_chat):
self.push('451 Internal confusion')
self.num_bytes = 0
return
- if self.num_bytes > self.data_size_limit:
+ if self.data_size_limit and 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.
+ # to RFC 5321, Section 4.5.2.
data = []
for text in line.split('\r\n'):
if text and text[0] == '.':
@@ -333,7 +345,7 @@ class SMTPChannel(asynchat.async_chat):
self.num_bytes = 0
self.set_terminator(b'\r\n')
if not status:
- self.push('250 Ok')
+ self.push('250 OK')
else:
self.push(status)
@@ -346,58 +358,188 @@ class SMTPChannel(asynchat.async_chat):
self.push('503 Duplicate HELO/EHLO')
else:
self.seen_greeting = arg
+ self.extended_smtp = False
self.push('250 %s' % self.fqdn)
+ def smtp_EHLO(self, arg):
+ if not arg:
+ self.push('501 Syntax: EHLO hostname')
+ return
+ if self.seen_greeting:
+ self.push('503 Duplicate HELO/EHLO')
+ else:
+ self.seen_greeting = arg
+ self.extended_smtp = True
+ self.push('250-%s' % self.fqdn)
+ if self.data_size_limit:
+ self.push('250-SIZE %s' % self.data_size_limit)
+ 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
+ def _strip_command_keyword(self, keyword, arg):
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 <person@dom.com> but watch out
- # for null address, e.g. <>
- address = address[1:-1]
- return address
+ return arg[keylen:].strip()
+ return ''
+
+ def _getaddr(self, arg):
+ if not arg:
+ return '', ''
+ if arg.lstrip().startswith('<'):
+ address, rest = get_angle_addr(arg)
+ else:
+ address, rest = get_addr_spec(arg)
+ if not address:
+ return address, rest
+ return address.addr_spec, rest
+
+ def _getparams(self, params):
+ # Return any parameters that appear to be syntactically valid according
+ # to RFC 1869, ignore all others. (Postel rule: accept what we can.)
+ params = [param.split('=', 1) for param in params.split()
+ if '=' in param]
+ return {k: v for k, v in params if k.isalnum()}
+
+ def smtp_HELP(self, arg):
+ if arg:
+ extended = ' [SP <mail parameters]'
+ lc_arg = arg.upper()
+ if lc_arg == 'EHLO':
+ self.push('250 Syntax: EHLO hostname')
+ elif lc_arg == 'HELO':
+ self.push('250 Syntax: HELO hostname')
+ elif lc_arg == 'MAIL':
+ msg = '250 Syntax: MAIL FROM: <address>'
+ if self.extended_smtp:
+ msg += extended
+ self.push(msg)
+ elif lc_arg == 'RCPT':
+ msg = '250 Syntax: RCPT TO: <address>'
+ if self.extended_smtp:
+ msg += extended
+ self.push(msg)
+ elif lc_arg == 'DATA':
+ self.push('250 Syntax: DATA')
+ elif lc_arg == 'RSET':
+ self.push('250 Syntax: RSET')
+ elif lc_arg == 'NOOP':
+ self.push('250 Syntax: NOOP')
+ elif lc_arg == 'QUIT':
+ self.push('250 Syntax: QUIT')
+ elif lc_arg == 'VRFY':
+ self.push('250 Syntax: VRFY <address>')
+ else:
+ self.push('501 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):
+ if arg:
+ address, params = self._getaddr(arg)
+ if address:
+ self.push('252 Cannot VRFY user, but will accept message '
+ 'and attempt delivery')
+ else:
+ self.push('502 Could not VRFY %s' % arg)
+ else:
+ self.push('501 Syntax: VRFY <address>')
def smtp_MAIL(self, arg):
+ if not self.seen_greeting:
+ self.push('503 Error: send HELO first');
+ return
print('===> MAIL', arg, file=DEBUGSTREAM)
- address = self.__getaddr('FROM:', arg) if arg else None
+ syntaxerr = '501 Syntax: MAIL FROM: <address>'
+ if self.extended_smtp:
+ syntaxerr += ' [SP <mail-parameters>]'
+ if arg is None:
+ self.push(syntaxerr)
+ return
+ arg = self._strip_command_keyword('FROM:', arg)
+ address, params = self._getaddr(arg)
+ if not address:
+ self.push(syntaxerr)
+ return
+ if not self.extended_smtp and params:
+ self.push(syntaxerr)
+ return
if not address:
- self.push('501 Syntax: MAIL FROM:<address>')
+ self.push(syntaxerr)
return
if self.mailfrom:
self.push('503 Error: nested MAIL command')
return
+ params = self._getparams(params.upper())
+ if params is None:
+ self.push(syntaxerr)
+ return
+ size = params.pop('SIZE', None)
+ if size:
+ if not size.isdigit():
+ self.push(syntaxerr)
+ return
+ elif self.data_size_limit and int(size) > self.data_size_limit:
+ self.push('552 Error: message size exceeds fixed maximum message size')
+ return
+ if len(params.keys()) > 0:
+ self.push('555 MAIL FROM parameters not recognized or not implemented')
+ return
self.mailfrom = address
print('sender:', self.mailfrom, file=DEBUGSTREAM)
- self.push('250 Ok')
+ self.push('250 OK')
def smtp_RCPT(self, arg):
+ if not self.seen_greeting:
+ self.push('503 Error: send HELO first');
+ return
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
+ syntaxerr = '501 Syntax: RCPT TO: <address>'
+ if self.extended_smtp:
+ syntaxerr += ' [SP <mail-parameters>]'
+ if arg is None:
+ self.push(syntaxerr)
+ return
+ arg = self._strip_command_keyword('TO:', arg)
+ address, params = self._getaddr(arg)
+ if not address:
+ self.push(syntaxerr)
+ return
+ if params:
+ if self.extended_smtp:
+ params = self._getparams(params.upper())
+ if params is None:
+ self.push(syntaxerr)
+ return
+ else:
+ self.push(syntaxerr)
+ return
+ if not address:
+ self.push(syntaxerr)
+ return
+ if params and len(params.keys()) > 0:
+ self.push('555 RCPT TO parameters not recognized or not implemented')
+ return
if not address:
self.push('501 Syntax: RCPT TO: <address>')
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:
@@ -408,9 +550,12 @@ class SMTPChannel(asynchat.async_chat):
self.rcpttos = []
self.received_data = ''
self.smtp_state = self.COMMAND
- self.push('250 Ok')
+ self.push('250 OK')
def smtp_DATA(self, arg):
+ if not self.seen_greeting:
+ self.push('503 Error: send HELO first');
+ return
if not self.rcpttos:
self.push('503 Error: need RCPT command')
return
@@ -421,15 +566,20 @@ class SMTPChannel(asynchat.async_chat):
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):
# SMTPChannel class to use for managing client connections
channel_class = SMTPChannel
- def __init__(self, localaddr, remoteaddr):
+ def __init__(self, localaddr, remoteaddr,
+ data_size_limit=DATA_SIZE_DEFAULT):
self._localaddr = localaddr
self._remoteaddr = remoteaddr
+ self.data_size_limit = data_size_limit
asyncore.dispatcher.__init__(self)
try:
self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
@@ -447,7 +597,7 @@ class SMTPServer(asyncore.dispatcher):
def handle_accepted(self, conn, addr):
print('Incoming connection from %s' % repr(addr), file=DEBUGSTREAM)
- channel = self.channel_class(self, conn, addr)
+ channel = self.channel_class(self, conn, addr, self.data_size_limit)
# API for "doing something useful with the message"
def process_message(self, peer, mailfrom, rcpttos, data):
@@ -475,7 +625,6 @@ class SMTPServer(asyncore.dispatcher):
raise NotImplementedError
-
class DebuggingServer(SMTPServer):
# Do something with the gathered message
def process_message(self, peer, mailfrom, rcpttos, data):
@@ -491,7 +640,6 @@ class DebuggingServer(SMTPServer):
print('------------ END MESSAGE ------------')
-
class PureProxy(SMTPServer):
def process_message(self, peer, mailfrom, rcpttos, data):
lines = data.split('\n')
@@ -532,7 +680,6 @@ class PureProxy(SMTPServer):
return refused
-
class MailmanProxy(PureProxy):
def process_message(self, peer, mailfrom, rcpttos, data):
from io import StringIO
@@ -611,19 +758,18 @@ class MailmanProxy(PureProxy):
msg.Enqueue(mlist, torequest=1)
-
class Options:
setuid = 1
classname = 'PureProxy'
+ size_limit = None
-
def parseargs():
global DEBUGSTREAM
try:
opts, args = getopt.getopt(
- sys.argv[1:], 'nVhc:d',
- ['class=', 'nosetuid', 'version', 'help', 'debug'])
+ sys.argv[1:], 'nVhc:s:d',
+ ['class=', 'nosetuid', 'version', 'help', 'size=', 'debug'])
except getopt.error as e:
usage(1, e)
@@ -640,6 +786,13 @@ def parseargs():
options.classname = arg
elif opt in ('-d', '--debug'):
DEBUGSTREAM = sys.stderr
+ elif opt in ('-s', '--size'):
+ try:
+ int_size = int(arg)
+ options.size_limit = int_size
+ except:
+ print('Invalid size: ' + arg, file=sys.stderr)
+ sys.exit(1)
# parse the rest of the arguments
if len(args) < 1:
@@ -674,7 +827,6 @@ def parseargs():
return options
-
if __name__ == '__main__':
options = parseargs()
# Become nobody
@@ -687,7 +839,8 @@ if __name__ == '__main__':
import __main__ as mod
class_ = getattr(mod, classname)
proxy = class_((options.localhost, options.localport),
- (options.remotehost, options.remoteport))
+ (options.remotehost, options.remoteport),
+ options.size_limit)
if options.setuid:
try:
import pwd