#! /usr/libexec/platform-python
"""An RFC 5321 smtp proxy with optional RFC 1870 and RFC 6531 extensions.
Usage: %(program)s [options] [localhost:localport [remotehost:remoteport]]
This program generally tries to setuid `nobody', unless this flag is
set. The setuid call will fail if this program is not run as root (in
which case, use this flag).
Print the version number and exit.
Use `classname' as the concrete SMTP proxy class. Uses `PureProxy' by
Restrict the total size of the incoming message to "limit" number of
bytes via the RFC 1870 SIZE extension. Defaults to 33554432 bytes.
Enable the SMTPUTF8 extension and behave as an RFC 6531 smtp proxy.
Turn on debugging prints.
Print this message and exit.
If localhost is not given then `localhost' is used, and if localport is not
given then 8025 is used. If remotehost is not given then `localhost' is used,
and if remoteport is not given, then 25 is used.
# 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:
# SMTPServer - the base class for the backend. Raises NotImplementedError
# DebuggingServer - simply prints each message it receives on stdout.
# PureProxy - Proxies all messages to a real smtpd which does final
# delivery. One known problem with this class is that it doesn't handle
# SMTP errors from the backend server at all. This should be fixed
# (contributions are welcome!).
# MailmanProxy - An experimental hack to work with GNU Mailman
# <www.list.org>. Using this server as your real incoming smtpd, your
# mailhost will automatically recognize and accept mail destined to Mailman
# lists when those lists are created. Every message not destined for a list
# gets forwarded to a real backend smtpd, as with PureProxy. Again, errors
# are not handled correctly yet.
# Author: Barry Warsaw <barry@python.org>
# - support mailbox delivery
# - Handle more ESMTP extensions
# - handle error codes from the backend smtpd
from warnings import warn
from email._header_value_parser import get_addr_spec, get_angle_addr
"SMTPChannel", "SMTPServer", "DebuggingServer", "PureProxy",
__version__ = 'Python SMTP proxy version 0.3'
def write(self, msg): pass
DATA_SIZE_DEFAULT = 33554432
print(__doc__ % globals(), file=sys.stderr)
print(msg, file=sys.stderr)
class SMTPChannel(asynchat.async_chat):
command_size_limits = collections.defaultdict(lambda x=command_size_limit: x)
def max_command_size_limit(self):
return max(self.command_size_limits.values())
return self.command_size_limit
def __init__(self, server, conn, addr, data_size_limit=DATA_SIZE_DEFAULT,
map=None, enable_SMTPUTF8=False, decode_data=False):
asynchat.async_chat.__init__(self, conn, map=map)
self.smtp_server = server
self.data_size_limit = data_size_limit
self.enable_SMTPUTF8 = enable_SMTPUTF8
self._decode_data = decode_data
if enable_SMTPUTF8 and decode_data:
raise ValueError("decode_data and enable_SMTPUTF8 cannot"
" be set to True at the same time")
self.extended_smtp = False
self.command_size_limits.clear()
self.fqdn = socket.getfqdn()
self.peer = conn.getpeername()
# a race condition may occur if the other end is closing
# before we can get the peername
if err.args[0] != errno.ENOTCONN:
print('Peer:', repr(self.peer), file=DEBUGSTREAM)
self.push('220 %s %s' % (self.fqdn, __version__))
def _set_post_data_state(self):
"""Reset state variables to their post-DATA state."""
self.smtp_state = self.COMMAND
self.require_SMTPUTF8 = False
self.set_terminator(b'\r\n')
def _set_rset_state(self):
"""Reset all state variables except the greeting."""
self._set_post_data_state()
# properties for backwards-compatibility
warn("Access to __server attribute on SMTPChannel is deprecated, "
"use 'smtp_server' instead", DeprecationWarning, 2)
def __server(self, value):
warn("Setting __server attribute on SMTPChannel is deprecated, "
"set 'smtp_server' instead", DeprecationWarning, 2)
warn("Access to __line attribute on SMTPChannel is deprecated, "
"use 'received_lines' instead", DeprecationWarning, 2)
return self.received_lines
warn("Setting __line attribute on SMTPChannel is deprecated, "
"set 'received_lines' instead", DeprecationWarning, 2)
self.received_lines = value
warn("Access to __state attribute on SMTPChannel is deprecated, "
"use 'smtp_state' instead", DeprecationWarning, 2)
def __state(self, value):
warn("Setting __state attribute on SMTPChannel is deprecated, "
"set 'smtp_state' instead", DeprecationWarning, 2)
warn("Access to __greeting attribute on SMTPChannel is deprecated, "
"use 'seen_greeting' instead", DeprecationWarning, 2)
return self.seen_greeting
def __greeting(self, value):
warn("Setting __greeting attribute on SMTPChannel is deprecated, "
"set 'seen_greeting' instead", DeprecationWarning, 2)
self.seen_greeting = value
warn("Access to __mailfrom attribute on SMTPChannel is deprecated, "
"use 'mailfrom' instead", DeprecationWarning, 2)
def __mailfrom(self, value):
warn("Setting __mailfrom attribute on SMTPChannel is deprecated, "
"set 'mailfrom' instead", DeprecationWarning, 2)
warn("Access to __rcpttos attribute on SMTPChannel is deprecated, "
"use 'rcpttos' instead", DeprecationWarning, 2)
def __rcpttos(self, value):
warn("Setting __rcpttos attribute on SMTPChannel is deprecated, "
"set 'rcpttos' instead", DeprecationWarning, 2)
warn("Access to __data attribute on SMTPChannel is deprecated, "
"use 'received_data' instead", DeprecationWarning, 2)
return self.received_data
warn("Setting __data attribute on SMTPChannel is deprecated, "
"set 'received_data' instead", DeprecationWarning, 2)
self.received_data = value
warn("Access to __fqdn attribute on SMTPChannel is deprecated, "
"use 'fqdn' instead", DeprecationWarning, 2)
warn("Setting __fqdn attribute on SMTPChannel is deprecated, "
"set 'fqdn' instead", DeprecationWarning, 2)
warn("Access to __peer attribute on SMTPChannel is deprecated, "
"use 'peer' instead", DeprecationWarning, 2)
warn("Setting __peer attribute on SMTPChannel is deprecated, "
"set 'peer' instead", DeprecationWarning, 2)
warn("Access to __conn attribute on SMTPChannel is deprecated, "
"use 'conn' instead", DeprecationWarning, 2)
warn("Setting __conn attribute on SMTPChannel is deprecated, "
"set 'conn' instead", DeprecationWarning, 2)
warn("Access to __addr attribute on SMTPChannel is deprecated, "
"use 'addr' instead", DeprecationWarning, 2)
warn("Setting __addr attribute on SMTPChannel is deprecated, "
"set 'addr' instead", DeprecationWarning, 2)
# Overrides base class for convenience.
asynchat.async_chat.push(self, bytes(
msg + '\r\n', 'utf-8' if self.require_SMTPUTF8 else 'ascii'))
# Implementation of base class abstract method
def collect_incoming_data(self, data):
if self.smtp_state == self.COMMAND:
limit = self.max_command_size_limit
elif self.smtp_state == self.DATA:
limit = self.data_size_limit
if limit and self.num_bytes > limit:
self.num_bytes += len(data)
self.received_lines.append(str(data, 'utf-8'))
self.received_lines.append(data)
# Implementation of base class abstract method
def found_terminator(self):
line = self._emptystring.join(self.received_lines)
print('Data:', repr(line), file=DEBUGSTREAM)
if self.smtp_state == self.COMMAND:
sz, self.num_bytes = self.num_bytes, 0
self.push('500 Error: bad syntax')
if not self._decode_data:
line = str(line, 'utf-8')
command = line[:i].upper()
max_sz = (self.command_size_limits[command]
if self.extended_smtp else self.command_size_limit)
self.push('500 Error: line too long')
method = getattr(self, 'smtp_' + command, None)
self.push('500 Error: command "%s" not recognized' % command)
if self.smtp_state != self.DATA:
self.push('451 Internal confusion')
if self.data_size_limit and self.num_bytes > self.data_size_limit:
self.push('552 Error: Too much mail data')
# Remove extraneous carriage returns and de-transparency according
# to RFC 5321, Section 4.5.2.
for text in line.split(self._linesep):
if text and text[0] == self._dotsep:
self.received_data = self._newline.join(data)
args = (self.peer, self.mailfrom, self.rcpttos, self.received_data)
if not self._decode_data:
'mail_options': self.mail_options,
'rcpt_options': self.rcpt_options,
status = self.smtp_server.process_message(*args, **kwargs)
self._set_post_data_state()
# SMTP and ESMTP commands
def smtp_HELO(self, arg):
self.push('501 Syntax: HELO hostname')
# See issue #21783 for a discussion of this behavior.
self.push('503 Duplicate HELO/EHLO')
self.push('250 %s' % self.fqdn)
def smtp_EHLO(self, arg):
self.push('501 Syntax: EHLO hostname')
# See issue #21783 for a discussion of this behavior.
self.push('503 Duplicate HELO/EHLO')
self.extended_smtp = True
self.push('250-%s' % self.fqdn)
self.push('250-SIZE %s' % self.data_size_limit)
self.command_size_limits['MAIL'] += 26
if not self._decode_data:
self.push('250-8BITMIME')
self.push('250-SMTPUTF8')
self.command_size_limits['MAIL'] += 10
def smtp_NOOP(self, arg):
self.push('501 Syntax: NOOP')
def smtp_QUIT(self, arg):
def _strip_command_keyword(self, keyword, arg):
if arg[:keylen].upper() == keyword:
return arg[keylen:].strip()
if arg.lstrip().startswith('<'):
address, rest = get_angle_addr(arg)
address, rest = get_addr_spec(arg)
return address.addr_spec, rest
def _getparams(self, params):
# Return params as dictionary. Return None if not all parameters
# appear to be syntactically valid according to RFC 1869.
param, eq, value = param.partition('=')
if not param.isalnum() or eq and not value:
result[param] = value if eq else True
def smtp_HELP(self, arg):
extended = ' [SP <mail-parameters>]'
self.push('250 Syntax: EHLO hostname')
self.push('250 Syntax: HELO hostname')
msg = '250 Syntax: MAIL FROM: <address>'
msg = '250 Syntax: RCPT TO: <address>'
self.push('250 Syntax: DATA')
self.push('250 Syntax: RSET')
self.push('250 Syntax: NOOP')
self.push('250 Syntax: QUIT')
self.push('250 Syntax: VRFY <address>')
self.push('501 Supported commands: EHLO HELO MAIL RCPT '
'DATA RSET NOOP QUIT VRFY')
self.push('250 Supported commands: EHLO HELO MAIL RCPT DATA '