123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967 |
- #! /usr/bin/env python3
- """An RFC 5321 smtp proxy with optional RFC 1870 and RFC 6531 extensions.
- Usage: %(program)s [options] [localhost:localport [remotehost:remoteport]]
- Options:
- --nosetuid
- -n
- 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).
- --version
- -V
- Print the version number and exit.
- --class classname
- -c classname
- 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.
- --smtputf8
- -u
- Enable the SMTPUTF8 extension and behave as an RFC 6531 smtp proxy.
- --debug
- -d
- Turn on debugging prints.
- --help
- -h
- Print this message and exit.
- Version: %(__version__)s
- 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.
- """
- # Overview:
- #
- # 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
- # if you try to use it.
- #
- # 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>
- #
- # TODO:
- #
- # - support mailbox delivery
- # - alias files
- # - Handle more ESMTP extensions
- # - handle error codes from the backend smtpd
- import sys
- import os
- import errno
- import getopt
- 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__ = [
- "SMTPChannel", "SMTPServer", "DebuggingServer", "PureProxy",
- "MailmanProxy",
- ]
- program = sys.argv[0]
- __version__ = 'Python SMTP proxy version 0.3'
- class Devnull:
- def write(self, msg): pass
- def flush(self): pass
- DEBUGSTREAM = Devnull()
- NEWLINE = '\n'
- COMMASPACE = ', '
- DATA_SIZE_DEFAULT = 33554432
- def usage(code, msg=''):
- print(__doc__ % globals(), file=sys.stderr)
- if msg:
- print(msg, file=sys.stderr)
- sys.exit(code)
- class SMTPChannel(asynchat.async_chat):
- COMMAND = 0
- DATA = 1
- command_size_limit = 512
- command_size_limits = collections.defaultdict(lambda x=command_size_limit: x)
- @property
- def max_command_size_limit(self):
- try:
- return max(self.command_size_limits.values())
- except ValueError:
- 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.conn = conn
- self.addr = addr
- 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")
- if decode_data:
- self._emptystring = ''
- self._linesep = '\r\n'
- self._dotsep = '.'
- self._newline = NEWLINE
- else:
- self._emptystring = b''
- self._linesep = b'\r\n'
- self._dotsep = ord(b'.')
- self._newline = b'\n'
- self._set_rset_state()
- self.seen_greeting = ''
- self.extended_smtp = False
- self.command_size_limits.clear()
- self.fqdn = socket.getfqdn()
- try:
- self.peer = conn.getpeername()
- except OSError as err:
- # a race condition may occur if the other end is closing
- # before we can get the peername
- self.close()
- if err.args[0] != errno.ENOTCONN:
- raise
- return
- 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.mailfrom = None
- self.rcpttos = []
- self.require_SMTPUTF8 = False
- self.num_bytes = 0
- self.set_terminator(b'\r\n')
- def _set_rset_state(self):
- """Reset all state variables except the greeting."""
- self._set_post_data_state()
- self.received_data = ''
- self.received_lines = []
- # properties for backwards-compatibility
- @property
- def __server(self):
- warn("Access to __server attribute on SMTPChannel is deprecated, "
- "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", DeprecationWarning, 2)
- self.smtp_server = value
- @property
- def __line(self):
- warn("Access to __line attribute on SMTPChannel is deprecated, "
- "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", DeprecationWarning, 2)
- self.received_lines = value
- @property
- def __state(self):
- warn("Access to __state attribute on SMTPChannel is deprecated, "
- "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", DeprecationWarning, 2)
- self.smtp_state = value
- @property
- def __greeting(self):
- warn("Access to __greeting attribute on SMTPChannel is deprecated, "
- "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", DeprecationWarning, 2)
- self.seen_greeting = value
- @property
- def __mailfrom(self):
- warn("Access to __mailfrom attribute on SMTPChannel is deprecated, "
- "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", DeprecationWarning, 2)
- self.mailfrom = value
- @property
- def __rcpttos(self):
- warn("Access to __rcpttos attribute on SMTPChannel is deprecated, "
- "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", DeprecationWarning, 2)
- self.rcpttos = value
- @property
- def __data(self):
- warn("Access to __data attribute on SMTPChannel is deprecated, "
- "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", DeprecationWarning, 2)
- self.received_data = value
- @property
- def __fqdn(self):
- warn("Access to __fqdn attribute on SMTPChannel is deprecated, "
- "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", DeprecationWarning, 2)
- self.fqdn = value
- @property
- def __peer(self):
- warn("Access to __peer attribute on SMTPChannel is deprecated, "
- "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", DeprecationWarning, 2)
- self.peer = value
- @property
- def __conn(self):
- warn("Access to __conn attribute on SMTPChannel is deprecated, "
- "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", DeprecationWarning, 2)
- self.conn = value
- @property
- def __addr(self):
- warn("Access to __addr attribute on SMTPChannel is deprecated, "
- "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", DeprecationWarning, 2)
- self.addr = value
- # Overrides base class for convenience.
- def push(self, msg):
- 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):
- limit = None
- 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:
- return
- elif limit:
- self.num_bytes += len(data)
- if self._decode_data:
- self.received_lines.append(str(data, 'utf-8'))
- else:
- 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)
- self.received_lines = []
- if self.smtp_state == self.COMMAND:
- sz, self.num_bytes = self.num_bytes, 0
- if not line:
- self.push('500 Error: bad syntax')
- return
- if not self._decode_data:
- line = str(line, 'utf-8')
- i = line.find(' ')
- if i < 0:
- command = line.upper()
- arg = None
- 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('500 Error: command "%s" not recognized' % command)
- return
- method(arg)
- return
- else:
- if self.smtp_state != self.DATA:
- self.push('451 Internal confusion')
- self.num_bytes = 0
- return
- 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 5321, Section 4.5.2.
- data = []
- for text in line.split(self._linesep):
- if text and text[0] == self._dotsep:
- data.append(text[1:])
- else:
- data.append(text)
- self.received_data = self._newline.join(data)
- args = (self.peer, self.mailfrom, self.rcpttos, self.received_data)
- kwargs = {}
- if not self._decode_data:
- kwargs = {
- 'mail_options': self.mail_options,
- 'rcpt_options': self.rcpt_options,
- }
- status = self.smtp_server.process_message(*args, **kwargs)
- self._set_post_data_state()
- if not status:
- self.push('250 OK')
- else:
- self.push(status)
- # SMTP and ESMTP commands
- def smtp_HELO(self, arg):
- if not arg:
- self.push('501 Syntax: HELO hostname')
- return
- # See issue #21783 for a discussion of this behavior.
- if self.seen_greeting:
- self.push('503 Duplicate HELO/EHLO')
- return
- self._set_rset_state()
- self.seen_greeting = arg
- self.push('250 %s' % self.fqdn)
- def smtp_EHLO(self, arg):
- if not arg:
- self.push('501 Syntax: EHLO hostname')
- return
- # See issue #21783 for a discussion of this behavior.
- if self.seen_greeting:
- self.push('503 Duplicate HELO/EHLO')
- return
- self._set_rset_state()
- 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.command_size_limits['MAIL'] += 26
- if not self._decode_data:
- self.push('250-8BITMIME')
- if self.enable_SMTPUTF8:
- self.push('250-SMTPUTF8')
- self.command_size_limits['MAIL'] += 10
- self.push('250 HELP')
- def smtp_NOOP(self, arg):
- if arg:
- self.push('501 Syntax: NOOP')
- else:
- self.push('250 OK')
- def smtp_QUIT(self, arg):
- # args is ignored
- self.push('221 Bye')
- self.close_when_done()
- def _strip_command_keyword(self, keyword, arg):
- keylen = len(keyword)
- if arg[:keylen].upper() == keyword:
- 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 params as dictionary. Return None if not all parameters
- # appear to be syntactically valid according to RFC 1869.
- result = {}
- for param in params:
- param, eq, value = param.partition('=')
- if not param.isalnum() or eq and not value:
- return None
- result[param] = value if eq else True
- return result
- 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)
- 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 self.mailfrom:
- self.push('503 Error: nested MAIL command')
- return
- self.mail_options = params.upper().split()
- params = self._getparams(self.mail_options)
- if params is None:
- self.push(syntaxerr)
- return
- if not self._decode_data:
- body = params.pop('BODY', '7BIT')
- if body not in ['7BIT', '8BITMIME']:
- self.push('501 Error: BODY can only be one of 7BIT, 8BITMIME')
- return
- if self.enable_SMTPUTF8:
- smtputf8 = params.pop('SMTPUTF8', False)
- if smtputf8 is True:
- self.require_SMTPUTF8 = True
- elif smtputf8 is not False:
- self.push('501 Error: SMTPUTF8 takes no arguments')
- 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')
- 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
- 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 not self.extended_smtp and params:
- self.push(syntaxerr)
- return
- self.rcpt_options = params.upper().split()
- params = self._getparams(self.rcpt_options)
- if params is None:
- self.push(syntaxerr)
- return
- # XXX currently there are no options we recognize.
- if len(params.keys()) > 0:
- self.push('555 RCPT TO parameters not recognized or not implemented')
- return
- self.rcpttos.append(address)
- print('recips:', self.rcpttos, file=DEBUGSTREAM)
- self.push('250 OK')
- def smtp_RSET(self, arg):
- if arg:
- self.push('501 Syntax: RSET')
- return
- self._set_rset_state()
- 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
- if arg:
- self.push('501 Syntax: DATA')
- return
- self.smtp_state = self.DATA
- 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,
- data_size_limit=DATA_SIZE_DEFAULT, map=None,
- enable_SMTPUTF8=False, decode_data=False):
- self._localaddr = localaddr
- self._remoteaddr = remoteaddr
- 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")
- asyncore.dispatcher.__init__(self, map=map)
- try:
- gai_results = socket.getaddrinfo(*localaddr,
- type=socket.SOCK_STREAM)
- self.create_socket(gai_results[0][0], gai_results[0][1])
- # try to re-use a server port if possible
- self.set_reuse_addr()
- self.bind(localaddr)
- self.listen(5)
- except:
- self.close()
- raise
- else:
- print('%s started at %s\n\tLocal addr: %s\n\tRemote addr:%s' % (
- self.__class__.__name__, time.ctime(time.time()),
- localaddr, remoteaddr), file=DEBUGSTREAM)
- def handle_accepted(self, conn, addr):
- print('Incoming connection from %s' % repr(addr), file=DEBUGSTREAM)
- channel = self.channel_class(self,
- conn,
- addr,
- self.data_size_limit,
- self._map,
- self.enable_SMTPUTF8,
- self._decode_data)
- # API for "doing something useful with the message"
- def process_message(self, peer, mailfrom, rcpttos, data, **kwargs):
- """Override this abstract method to handle messages from the client.
- peer is a tuple containing (ipaddr, port) of the client that made the
- socket connection to our smtp port.
- mailfrom is the raw address the client claims the message is coming
- from.
- rcpttos is a list of raw addresses the client wishes to deliver the
- message to.
- data is a string containing the entire full text of the message,
- headers (if supplied) and all. It has been `de-transparencied'
- according to RFC 821, Section 4.5.2. In other words, a line
- containing a `.' followed by other text has had the leading dot
- removed.
- kwargs is a dictionary containing additional information. It is
- empty if decode_data=True was given as init parameter, otherwise
- it will contain the following keys:
- 'mail_options': list of parameters to the mail command. All
- elements are uppercase strings. Example:
- ['BODY=8BITMIME', 'SMTPUTF8'].
- 'rcpt_options': same, for the rcpt command.
- This function should return None for a normal `250 Ok' response;
- otherwise, it should return the desired response string in RFC 821
- format.
- """
- raise NotImplementedError
- class DebuggingServer(SMTPServer):
- def _print_message_content(self, peer, data):
- inheaders = 1
- lines = data.splitlines()
- for line in lines:
- # headers first
- if inheaders and not line:
- peerheader = 'X-Peer: ' + peer[0]
- if not isinstance(data, str):
- # decoded_data=false; make header match other binary output
- peerheader = repr(peerheader.encode('utf-8'))
- print(peerheader)
- inheaders = 0
- if not isinstance(data, str):
- # Avoid spurious 'str on bytes instance' warning.
- line = repr(line)
- print(line)
- def process_message(self, peer, mailfrom, rcpttos, data, **kwargs):
- print('---------- MESSAGE FOLLOWS ----------')
- if kwargs:
- if kwargs.get('mail_options'):
- print('mail options: %s' % kwargs['mail_options'])
- if kwargs.get('rcpt_options'):
- print('rcpt options: %s\n' % kwargs['rcpt_options'])
- self._print_message_content(peer, data)
- print('------------ END MESSAGE ------------')
- class PureProxy(SMTPServer):
- def __init__(self, *args, **kwargs):
- if 'enable_SMTPUTF8' in kwargs and kwargs['enable_SMTPUTF8']:
- raise ValueError("PureProxy does not support SMTPUTF8.")
- super(PureProxy, self).__init__(*args, **kwargs)
- def process_message(self, peer, mailfrom, rcpttos, data):
- lines = data.split('\n')
- # Look for the last header
- i = 0
- for line in lines:
- if not line:
- break
- i += 1
- lines.insert(i, 'X-Peer: %s' % peer[0])
- data = NEWLINE.join(lines)
- refused = self._deliver(mailfrom, rcpttos, data)
- # TBD: what to do with refused addresses?
- print('we got some refusals:', refused, file=DEBUGSTREAM)
- def _deliver(self, mailfrom, rcpttos, data):
- import smtplib
- refused = {}
- try:
- s = smtplib.SMTP()
- s.connect(self._remoteaddr[0], self._remoteaddr[1])
- try:
- refused = s.sendmail(mailfrom, rcpttos, data)
- finally:
- s.quit()
- except smtplib.SMTPRecipientsRefused as e:
- print('got SMTPRecipientsRefused', file=DEBUGSTREAM)
- refused = e.recipients
- except (OSError, 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.
- errcode = getattr(e, 'smtp_code', -1)
- errmsg = getattr(e, 'smtp_error', 'ignore')
- for r in rcpttos:
- refused[r] = (errcode, errmsg)
- return refused
- class MailmanProxy(PureProxy):
- def __init__(self, *args, **kwargs):
- warn('MailmanProxy is deprecated and will be removed '
- 'in future', DeprecationWarning, 2)
- if 'enable_SMTPUTF8' in kwargs and kwargs['enable_SMTPUTF8']:
- raise ValueError("MailmanProxy does not support SMTPUTF8.")
- super(PureProxy, self).__init__(*args, **kwargs)
- def process_message(self, peer, mailfrom, rcpttos, data):
- from io import StringIO
- from Mailman import Utils
- from Mailman import Message
- from Mailman import MailList
- # If the message is to a Mailman mailing list, then we'll invoke the
- # Mailman script directly, without going through the real smtpd.
- # Otherwise we'll forward it to the local proxy for disposition.
- listnames = []
- for rcpt in rcpttos:
- local = rcpt.lower().split('@')[0]
- # We allow the following variations on the theme
- # listname
- # listname-admin
- # listname-owner
- # listname-request
- # listname-join
- # listname-leave
- parts = local.split('-')
- if len(parts) > 2:
- continue
- listname = parts[0]
- if len(parts) == 2:
- command = parts[1]
- else:
- command = ''
- if not Utils.list_exists(listname) or command not in (
- '', 'admin', 'owner', 'request', 'join', 'leave'):
- continue
- listnames.append((rcpt, listname, command))
- # Remove all list recipients from rcpttos and forward what we're not
- # going to take care of ourselves. Linear removal should be fine
- # since we don't expect a large number of recipients.
- for rcpt, listname, command in listnames:
- rcpttos.remove(rcpt)
- # If there's any non-list destined recipients left,
- print('forwarding recips:', ' '.join(rcpttos), file=DEBUGSTREAM)
- if rcpttos:
- refused = self._deliver(mailfrom, rcpttos, data)
- # TBD: what to do with refused addresses?
- print('we got refusals:', refused, file=DEBUGSTREAM)
- # Now deliver directly to the list commands
- mlists = {}
- s = StringIO(data)
- msg = Message.Message(s)
- # 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.get('from'):
- msg['From'] = mailfrom
- if not msg.get('date'):
- msg['Date'] = time.ctime(time.time())
- for rcpt, listname, command in listnames:
- print('sending message to', rcpt, file=DEBUGSTREAM)
- mlist = mlists.get(listname)
- if not mlist:
- mlist = MailList.MailList(listname, lock=0)
- mlists[listname] = mlist
- # dispatch on the type of command
- if command == '':
- # post
- msg.Enqueue(mlist, tolist=1)
- elif command == 'admin':
- msg.Enqueue(mlist, toadmin=1)
- elif command == 'owner':
- msg.Enqueue(mlist, toowner=1)
- elif command == 'request':
- msg.Enqueue(mlist, torequest=1)
- elif command in ('join', 'leave'):
- # TBD: this is a hack!
- if command == 'join':
- msg['Subject'] = 'subscribe'
- else:
- msg['Subject'] = 'unsubscribe'
- msg.Enqueue(mlist, torequest=1)
- class Options:
- setuid = True
- classname = 'PureProxy'
- size_limit = None
- enable_SMTPUTF8 = False
- def parseargs():
- global DEBUGSTREAM
- try:
- opts, args = getopt.getopt(
- sys.argv[1:], 'nVhc:s:du',
- ['class=', 'nosetuid', 'version', 'help', 'size=', 'debug',
- 'smtputf8'])
- except getopt.error as e:
- usage(1, e)
- options = Options()
- for opt, arg in opts:
- if opt in ('-h', '--help'):
- usage(0)
- elif opt in ('-V', '--version'):
- print(__version__)
- sys.exit(0)
- elif opt in ('-n', '--nosetuid'):
- options.setuid = False
- elif opt in ('-c', '--class'):
- options.classname = arg
- elif opt in ('-d', '--debug'):
- DEBUGSTREAM = sys.stderr
- elif opt in ('-u', '--smtputf8'):
- options.enable_SMTPUTF8 = True
- 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:
- localspec = 'localhost:8025'
- remotespec = 'localhost:25'
- elif len(args) < 2:
- localspec = args[0]
- remotespec = 'localhost:25'
- elif len(args) < 3:
- localspec = args[0]
- remotespec = args[1]
- else:
- usage(1, 'Invalid arguments: %s' % COMMASPACE.join(args))
- # split into host/port pairs
- i = localspec.find(':')
- if i < 0:
- usage(1, 'Bad local spec: %s' % localspec)
- options.localhost = localspec[:i]
- try:
- options.localport = int(localspec[i+1:])
- except ValueError:
- usage(1, 'Bad local port: %s' % localspec)
- i = remotespec.find(':')
- if i < 0:
- usage(1, 'Bad remote spec: %s' % remotespec)
- options.remotehost = remotespec[:i]
- try:
- options.remoteport = int(remotespec[i+1:])
- except ValueError:
- usage(1, 'Bad remote port: %s' % remotespec)
- return options
- if __name__ == '__main__':
- options = parseargs()
- # Become nobody
- 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_limit, enable_SMTPUTF8=options.enable_SMTPUTF8)
- if options.setuid:
- try:
- import pwd
- except ImportError:
- 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 PermissionError:
- print('Cannot setuid "nobody"; try running with -n option.', file=sys.stderr)
- sys.exit(1)
- try:
- asyncore.loop()
- except KeyboardInterrupt:
- pass
|