1#!/usr/local/bin/python3.11
2"""An RFC 5321 smtp proxy with optional RFC 1870 and RFC 6531 extensions.
3
4Usage: %(program)s [options] [localhost:localport [remotehost:remoteport]]
5
6Options:
7
8    --nosetuid
9    -n
10        This program generally tries to setuid `nobody', unless this flag is
11        set.  The setuid call will fail if this program is not run as root (in
12        which case, use this flag).
13
14    --version
15    -V
16        Print the version number and exit.
17
18    --class classname
19    -c classname
20        Use `classname' as the concrete SMTP proxy class.  Uses `PureProxy' by
21        default.
22
23    --size limit
24    -s limit
25        Restrict the total size of the incoming message to "limit" number of
26        bytes via the RFC 1870 SIZE extension.  Defaults to 33554432 bytes.
27
28    --smtputf8
29    -u
30        Enable the SMTPUTF8 extension and behave as an RFC 6531 smtp proxy.
31
32    --debug
33    -d
34        Turn on debugging prints.
35
36    --help
37    -h
38        Print this message and exit.
39
40Version: %(__version__)s
41
42If localhost is not given then `localhost' is used, and if localport is not
43given then 8025 is used.  If remotehost is not given then `localhost' is used,
44and if remoteport is not given, then 25 is used.
45"""
46
47# Overview:
48#
49# This file implements the minimal SMTP protocol as defined in RFC 5321.  It
50# has a hierarchy of classes which implement the backend functionality for the
51# smtpd.  A number of classes are provided:
52#
53#   SMTPServer - the base class for the backend.  Raises NotImplementedError
54#   if you try to use it.
55#
56#   DebuggingServer - simply prints each message it receives on stdout.
57#
58#   PureProxy - Proxies all messages to a real smtpd which does final
59#   delivery.  One known problem with this class is that it doesn't handle
60#   SMTP errors from the backend server at all.  This should be fixed
61#   (contributions are welcome!).
62#
63#
64# Author: Barry Warsaw <barry@python.org>
65#
66# TODO:
67#
68# - support mailbox delivery
69# - alias files
70# - Handle more ESMTP extensions
71# - handle error codes from the backend smtpd
72
73import sys
74import os
75import errno
76import getopt
77import time
78import socket
79import collections
80from warnings import warn
81from email._header_value_parser import get_addr_spec, get_angle_addr
82
83__all__ = [
84    "SMTPChannel", "SMTPServer", "DebuggingServer", "PureProxy",
85]
86
87warn(
88    'The smtpd module is deprecated and unmaintained.  Please see aiosmtpd '
89    '(https://aiosmtpd.readthedocs.io/) for the recommended replacement.',
90    DeprecationWarning,
91    stacklevel=2)
92
93
94# These are imported after the above warning so that users get the correct
95# deprecation warning.
96import asyncore
97import asynchat
98
99
100program = sys.argv[0]
101__version__ = 'Python SMTP proxy version 0.3'
102
103
104class Devnull:
105    def write(self, msg): pass
106    def flush(self): pass
107
108
109DEBUGSTREAM = Devnull()
110NEWLINE = '\n'
111COMMASPACE = ', '
112DATA_SIZE_DEFAULT = 33554432
113
114
115def usage(code, msg=''):
116    print(__doc__ % globals(), file=sys.stderr)
117    if msg:
118        print(msg, file=sys.stderr)
119    sys.exit(code)
120
121
122class SMTPChannel(asynchat.async_chat):
123    COMMAND = 0
124    DATA = 1
125
126    command_size_limit = 512
127    command_size_limits = collections.defaultdict(lambda x=command_size_limit: x)
128
129    @property
130    def max_command_size_limit(self):
131        try:
132            return max(self.command_size_limits.values())
133        except ValueError:
134            return self.command_size_limit
135
136    def __init__(self, server, conn, addr, data_size_limit=DATA_SIZE_DEFAULT,
137                 map=None, enable_SMTPUTF8=False, decode_data=False):
138        asynchat.async_chat.__init__(self, conn, map=map)
139        self.smtp_server = server
140        self.conn = conn
141        self.addr = addr
142        self.data_size_limit = data_size_limit
143        self.enable_SMTPUTF8 = enable_SMTPUTF8
144        self._decode_data = decode_data
145        if enable_SMTPUTF8 and decode_data:
146            raise ValueError("decode_data and enable_SMTPUTF8 cannot"
147                             " be set to True at the same time")
148        if decode_data:
149            self._emptystring = ''
150            self._linesep = '\r\n'
151            self._dotsep = '.'
152            self._newline = NEWLINE
153        else:
154            self._emptystring = b''
155            self._linesep = b'\r\n'
156            self._dotsep = ord(b'.')
157            self._newline = b'\n'
158        self._set_rset_state()
159        self.seen_greeting = ''
160        self.extended_smtp = False
161        self.command_size_limits.clear()
162        self.fqdn = socket.getfqdn()
163        try:
164            self.peer = conn.getpeername()
165        except OSError as err:
166            # a race condition  may occur if the other end is closing
167            # before we can get the peername
168            self.close()
169            if err.errno != errno.ENOTCONN:
170                raise
171            return
172        print('Peer:', repr(self.peer), file=DEBUGSTREAM)
173        self.push('220 %s %s' % (self.fqdn, __version__))
174
175    def _set_post_data_state(self):
176        """Reset state variables to their post-DATA state."""
177        self.smtp_state = self.COMMAND
178        self.mailfrom = None
179        self.rcpttos = []
180        self.require_SMTPUTF8 = False
181        self.num_bytes = 0
182        self.set_terminator(b'\r\n')
183
184    def _set_rset_state(self):
185        """Reset all state variables except the greeting."""
186        self._set_post_data_state()
187        self.received_data = ''
188        self.received_lines = []
189
190
191    # properties for backwards-compatibility
192    @property
193    def __server(self):
194        warn("Access to __server attribute on SMTPChannel is deprecated, "
195            "use 'smtp_server' instead", DeprecationWarning, 2)
196        return self.smtp_server
197    @__server.setter
198    def __server(self, value):
199        warn("Setting __server attribute on SMTPChannel is deprecated, "
200            "set 'smtp_server' instead", DeprecationWarning, 2)
201        self.smtp_server = value
202
203    @property
204    def __line(self):
205        warn("Access to __line attribute on SMTPChannel is deprecated, "
206            "use 'received_lines' instead", DeprecationWarning, 2)
207        return self.received_lines
208    @__line.setter
209    def __line(self, value):
210        warn("Setting __line attribute on SMTPChannel is deprecated, "
211            "set 'received_lines' instead", DeprecationWarning, 2)
212        self.received_lines = value
213
214    @property
215    def __state(self):
216        warn("Access to __state attribute on SMTPChannel is deprecated, "
217            "use 'smtp_state' instead", DeprecationWarning, 2)
218        return self.smtp_state
219    @__state.setter
220    def __state(self, value):
221        warn("Setting __state attribute on SMTPChannel is deprecated, "
222            "set 'smtp_state' instead", DeprecationWarning, 2)
223        self.smtp_state = value
224
225    @property
226    def __greeting(self):
227        warn("Access to __greeting attribute on SMTPChannel is deprecated, "
228            "use 'seen_greeting' instead", DeprecationWarning, 2)
229        return self.seen_greeting
230    @__greeting.setter
231    def __greeting(self, value):
232        warn("Setting __greeting attribute on SMTPChannel is deprecated, "
233            "set 'seen_greeting' instead", DeprecationWarning, 2)
234        self.seen_greeting = value
235
236    @property
237    def __mailfrom(self):
238        warn("Access to __mailfrom attribute on SMTPChannel is deprecated, "
239            "use 'mailfrom' instead", DeprecationWarning, 2)
240        return self.mailfrom
241    @__mailfrom.setter
242    def __mailfrom(self, value):
243        warn("Setting __mailfrom attribute on SMTPChannel is deprecated, "
244            "set 'mailfrom' instead", DeprecationWarning, 2)
245        self.mailfrom = value
246
247    @property
248    def __rcpttos(self):
249        warn("Access to __rcpttos attribute on SMTPChannel is deprecated, "
250            "use 'rcpttos' instead", DeprecationWarning, 2)
251        return self.rcpttos
252    @__rcpttos.setter
253    def __rcpttos(self, value):
254        warn("Setting __rcpttos attribute on SMTPChannel is deprecated, "
255            "set 'rcpttos' instead", DeprecationWarning, 2)
256        self.rcpttos = value
257
258    @property
259    def __data(self):
260        warn("Access to __data attribute on SMTPChannel is deprecated, "
261            "use 'received_data' instead", DeprecationWarning, 2)
262        return self.received_data
263    @__data.setter
264    def __data(self, value):
265        warn("Setting __data attribute on SMTPChannel is deprecated, "
266            "set 'received_data' instead", DeprecationWarning, 2)
267        self.received_data = value
268
269    @property
270    def __fqdn(self):
271        warn("Access to __fqdn attribute on SMTPChannel is deprecated, "
272            "use 'fqdn' instead", DeprecationWarning, 2)
273        return self.fqdn
274    @__fqdn.setter
275    def __fqdn(self, value):
276        warn("Setting __fqdn attribute on SMTPChannel is deprecated, "
277            "set 'fqdn' instead", DeprecationWarning, 2)
278        self.fqdn = value
279
280    @property
281    def __peer(self):
282        warn("Access to __peer attribute on SMTPChannel is deprecated, "
283            "use 'peer' instead", DeprecationWarning, 2)
284        return self.peer
285    @__peer.setter
286    def __peer(self, value):
287        warn("Setting __peer attribute on SMTPChannel is deprecated, "
288            "set 'peer' instead", DeprecationWarning, 2)
289        self.peer = value
290
291    @property
292    def __conn(self):
293        warn("Access to __conn attribute on SMTPChannel is deprecated, "
294            "use 'conn' instead", DeprecationWarning, 2)
295        return self.conn
296    @__conn.setter
297    def __conn(self, value):
298        warn("Setting __conn attribute on SMTPChannel is deprecated, "
299            "set 'conn' instead", DeprecationWarning, 2)
300        self.conn = value
301
302    @property
303    def __addr(self):
304        warn("Access to __addr attribute on SMTPChannel is deprecated, "
305            "use 'addr' instead", DeprecationWarning, 2)
306        return self.addr
307    @__addr.setter
308    def __addr(self, value):
309        warn("Setting __addr attribute on SMTPChannel is deprecated, "
310            "set 'addr' instead", DeprecationWarning, 2)
311        self.addr = value
312
313    # Overrides base class for convenience.
314    def push(self, msg):
315        asynchat.async_chat.push(self, bytes(
316            msg + '\r\n', 'utf-8' if self.require_SMTPUTF8 else 'ascii'))
317
318    # Implementation of base class abstract method
319    def collect_incoming_data(self, data):
320        limit = None
321        if self.smtp_state == self.COMMAND:
322            limit = self.max_command_size_limit
323        elif self.smtp_state == self.DATA:
324            limit = self.data_size_limit
325        if limit and self.num_bytes > limit:
326            return
327        elif limit:
328            self.num_bytes += len(data)
329        if self._decode_data:
330            self.received_lines.append(str(data, 'utf-8'))
331        else:
332            self.received_lines.append(data)
333
334    # Implementation of base class abstract method
335    def found_terminator(self):
336        line = self._emptystring.join(self.received_lines)
337        print('Data:', repr(line), file=DEBUGSTREAM)
338        self.received_lines = []
339        if self.smtp_state == self.COMMAND:
340            sz, self.num_bytes = self.num_bytes, 0
341            if not line:
342                self.push('500 Error: bad syntax')
343                return
344            if not self._decode_data:
345                line = str(line, 'utf-8')
346            i = line.find(' ')
347            if i < 0:
348                command = line.upper()
349                arg = None
350            else:
351                command = line[:i].upper()
352                arg = line[i+1:].strip()
353            max_sz = (self.command_size_limits[command]
354                        if self.extended_smtp else self.command_size_limit)
355            if sz > max_sz:
356                self.push('500 Error: line too long')
357                return
358            method = getattr(self, 'smtp_' + command, None)
359            if not method:
360                self.push('500 Error: command "%s" not recognized' % command)
361                return
362            method(arg)
363            return
364        else:
365            if self.smtp_state != self.DATA:
366                self.push('451 Internal confusion')
367                self.num_bytes = 0
368                return
369            if self.data_size_limit and self.num_bytes > self.data_size_limit:
370                self.push('552 Error: Too much mail data')
371                self.num_bytes = 0
372                return
373            # Remove extraneous carriage returns and de-transparency according
374            # to RFC 5321, Section 4.5.2.
375            data = []
376            for text in line.split(self._linesep):
377                if text and text[0] == self._dotsep:
378                    data.append(text[1:])
379                else:
380                    data.append(text)
381            self.received_data = self._newline.join(data)
382            args = (self.peer, self.mailfrom, self.rcpttos, self.received_data)
383            kwargs = {}
384            if not self._decode_data:
385                kwargs = {
386                    'mail_options': self.mail_options,
387                    'rcpt_options': self.rcpt_options,
388                }
389            status = self.smtp_server.process_message(*args, **kwargs)
390            self._set_post_data_state()
391            if not status:
392                self.push('250 OK')
393            else:
394                self.push(status)
395
396    # SMTP and ESMTP commands
397    def smtp_HELO(self, arg):
398        if not arg:
399            self.push('501 Syntax: HELO hostname')
400            return
401        # See issue #21783 for a discussion of this behavior.
402        if self.seen_greeting:
403            self.push('503 Duplicate HELO/EHLO')
404            return
405        self._set_rset_state()
406        self.seen_greeting = arg
407        self.push('250 %s' % self.fqdn)
408
409    def smtp_EHLO(self, arg):
410        if not arg:
411            self.push('501 Syntax: EHLO hostname')
412            return
413        # See issue #21783 for a discussion of this behavior.
414        if self.seen_greeting:
415            self.push('503 Duplicate HELO/EHLO')
416            return
417        self._set_rset_state()
418        self.seen_greeting = arg
419        self.extended_smtp = True
420        self.push('250-%s' % self.fqdn)
421        if self.data_size_limit:
422            self.push('250-SIZE %s' % self.data_size_limit)
423            self.command_size_limits['MAIL'] += 26
424        if not self._decode_data:
425            self.push('250-8BITMIME')
426        if self.enable_SMTPUTF8:
427            self.push('250-SMTPUTF8')
428            self.command_size_limits['MAIL'] += 10
429        self.push('250 HELP')
430
431    def smtp_NOOP(self, arg):
432        if arg:
433            self.push('501 Syntax: NOOP')
434        else:
435            self.push('250 OK')
436
437    def smtp_QUIT(self, arg):
438        # args is ignored
439        self.push('221 Bye')
440        self.close_when_done()
441
442    def _strip_command_keyword(self, keyword, arg):
443        keylen = len(keyword)
444        if arg[:keylen].upper() == keyword:
445            return arg[keylen:].strip()
446        return ''
447
448    def _getaddr(self, arg):
449        if not arg:
450            return '', ''
451        if arg.lstrip().startswith('<'):
452            address, rest = get_angle_addr(arg)
453        else:
454            address, rest = get_addr_spec(arg)
455        if not address:
456            return address, rest
457        return address.addr_spec, rest
458
459    def _getparams(self, params):
460        # Return params as dictionary. Return None if not all parameters
461        # appear to be syntactically valid according to RFC 1869.
462        result = {}
463        for param in params:
464            param, eq, value = param.partition('=')
465            if not param.isalnum() or eq and not value:
466                return None
467            result[param] = value if eq else True
468        return result
469
470    def smtp_HELP(self, arg):
471        if arg:
472            extended = ' [SP <mail-parameters>]'
473            lc_arg = arg.upper()
474            if lc_arg == 'EHLO':
475                self.push('250 Syntax: EHLO hostname')
476            elif lc_arg == 'HELO':
477                self.push('250 Syntax: HELO hostname')
478            elif lc_arg == 'MAIL':
479                msg = '250 Syntax: MAIL FROM: <address>'
480                if self.extended_smtp:
481                    msg += extended
482                self.push(msg)
483            elif lc_arg == 'RCPT':
484                msg = '250 Syntax: RCPT TO: <address>'
485                if self.extended_smtp:
486                    msg += extended
487                self.push(msg)
488            elif lc_arg == 'DATA':
489                self.push('250 Syntax: DATA')
490            elif lc_arg == 'RSET':
491                self.push('250 Syntax: RSET')
492            elif lc_arg == 'NOOP':
493                self.push('250 Syntax: NOOP')
494            elif lc_arg == 'QUIT':
495                self.push('250 Syntax: QUIT')
496            elif lc_arg == 'VRFY':
497                self.push('250 Syntax: VRFY <address>')
498            else:
499                self.push('501 Supported commands: EHLO HELO MAIL RCPT '
500                          'DATA RSET NOOP QUIT VRFY')
501        else:
502            self.push('250 Supported commands: EHLO HELO MAIL RCPT DATA '
503                      'RSET NOOP QUIT VRFY')
504
505    def smtp_VRFY(self, arg):
506        if arg:
507            address, params = self._getaddr(arg)
508            if address:
509                self.push('252 Cannot VRFY user, but will accept message '
510                          'and attempt delivery')
511            else:
512                self.push('502 Could not VRFY %s' % arg)
513        else:
514            self.push('501 Syntax: VRFY <address>')
515
516    def smtp_MAIL(self, arg):
517        if not self.seen_greeting:
518            self.push('503 Error: send HELO first')
519            return
520        print('===> MAIL', arg, file=DEBUGSTREAM)
521        syntaxerr = '501 Syntax: MAIL FROM: <address>'
522        if self.extended_smtp:
523            syntaxerr += ' [SP <mail-parameters>]'
524        if arg is None:
525            self.push(syntaxerr)
526            return
527        arg = self._strip_command_keyword('FROM:', arg)
528        address, params = self._getaddr(arg)
529        if not address:
530            self.push(syntaxerr)
531            return
532        if not self.extended_smtp and params:
533            self.push(syntaxerr)
534            return
535        if self.mailfrom:
536            self.push('503 Error: nested MAIL command')
537            return
538        self.mail_options = params.upper().split()
539        params = self._getparams(self.mail_options)
540        if params is None:
541            self.push(syntaxerr)
542            return
543        if not self._decode_data:
544            body = params.pop('BODY', '7BIT')
545            if body not in ['7BIT', '8BITMIME']:
546                self.push('501 Error: BODY can only be one of 7BIT, 8BITMIME')
547                return
548        if self.enable_SMTPUTF8:
549            smtputf8 = params.pop('SMTPUTF8', False)
550            if smtputf8 is True:
551                self.require_SMTPUTF8 = True
552            elif smtputf8 is not False:
553                self.push('501 Error: SMTPUTF8 takes no arguments')
554                return
555        size = params.pop('SIZE', None)
556        if size:
557            if not size.isdigit():
558                self.push(syntaxerr)
559                return
560            elif self.data_size_limit and int(size) > self.data_size_limit:
561                self.push('552 Error: message size exceeds fixed maximum message size')
562                return
563        if len(params.keys()) > 0:
564            self.push('555 MAIL FROM parameters not recognized or not implemented')
565            return
566        self.mailfrom = address
567        print('sender:', self.mailfrom, file=DEBUGSTREAM)
568        self.push('250 OK')
569
570    def smtp_RCPT(self, arg):
571        if not self.seen_greeting:
572            self.push('503 Error: send HELO first');
573            return
574        print('===> RCPT', arg, file=DEBUGSTREAM)
575        if not self.mailfrom:
576            self.push('503 Error: need MAIL command')
577            return
578        syntaxerr = '501 Syntax: RCPT TO: <address>'
579        if self.extended_smtp:
580            syntaxerr += ' [SP <mail-parameters>]'
581        if arg is None:
582            self.push(syntaxerr)
583            return
584        arg = self._strip_command_keyword('TO:', arg)
585        address, params = self._getaddr(arg)
586        if not address:
587            self.push(syntaxerr)
588            return
589        if not self.extended_smtp and params:
590            self.push(syntaxerr)
591            return
592        self.rcpt_options = params.upper().split()
593        params = self._getparams(self.rcpt_options)
594        if params is None:
595            self.push(syntaxerr)
596            return
597        # XXX currently there are no options we recognize.
598        if len(params.keys()) > 0:
599            self.push('555 RCPT TO parameters not recognized or not implemented')
600            return
601        self.rcpttos.append(address)
602        print('recips:', self.rcpttos, file=DEBUGSTREAM)
603        self.push('250 OK')
604
605    def smtp_RSET(self, arg):
606        if arg:
607            self.push('501 Syntax: RSET')
608            return
609        self._set_rset_state()
610        self.push('250 OK')
611
612    def smtp_DATA(self, arg):
613        if not self.seen_greeting:
614            self.push('503 Error: send HELO first');
615            return
616        if not self.rcpttos:
617            self.push('503 Error: need RCPT command')
618            return
619        if arg:
620            self.push('501 Syntax: DATA')
621            return
622        self.smtp_state = self.DATA
623        self.set_terminator(b'\r\n.\r\n')
624        self.push('354 End data with <CR><LF>.<CR><LF>')
625
626    # Commands that have not been implemented
627    def smtp_EXPN(self, arg):
628        self.push('502 EXPN not implemented')
629
630
631class SMTPServer(asyncore.dispatcher):
632    # SMTPChannel class to use for managing client connections
633    channel_class = SMTPChannel
634
635    def __init__(self, localaddr, remoteaddr,
636                 data_size_limit=DATA_SIZE_DEFAULT, map=None,
637                 enable_SMTPUTF8=False, decode_data=False):
638        self._localaddr = localaddr
639        self._remoteaddr = remoteaddr
640        self.data_size_limit = data_size_limit
641        self.enable_SMTPUTF8 = enable_SMTPUTF8
642        self._decode_data = decode_data
643        if enable_SMTPUTF8 and decode_data:
644            raise ValueError("decode_data and enable_SMTPUTF8 cannot"
645                             " be set to True at the same time")
646        asyncore.dispatcher.__init__(self, map=map)
647        try:
648            gai_results = socket.getaddrinfo(*localaddr,
649                                             type=socket.SOCK_STREAM)
650            self.create_socket(gai_results[0][0], gai_results[0][1])
651            # try to re-use a server port if possible
652            self.set_reuse_addr()
653            self.bind(localaddr)
654            self.listen(5)
655        except:
656            self.close()
657            raise
658        else:
659            print('%s started at %s\n\tLocal addr: %s\n\tRemote addr:%s' % (
660                self.__class__.__name__, time.ctime(time.time()),
661                localaddr, remoteaddr), file=DEBUGSTREAM)
662
663    def handle_accepted(self, conn, addr):
664        print('Incoming connection from %s' % repr(addr), file=DEBUGSTREAM)
665        channel = self.channel_class(self,
666                                     conn,
667                                     addr,
668                                     self.data_size_limit,
669                                     self._map,
670                                     self.enable_SMTPUTF8,
671                                     self._decode_data)
672
673    # API for "doing something useful with the message"
674    def process_message(self, peer, mailfrom, rcpttos, data, **kwargs):
675        """Override this abstract method to handle messages from the client.
676
677        peer is a tuple containing (ipaddr, port) of the client that made the
678        socket connection to our smtp port.
679
680        mailfrom is the raw address the client claims the message is coming
681        from.
682
683        rcpttos is a list of raw addresses the client wishes to deliver the
684        message to.
685
686        data is a string containing the entire full text of the message,
687        headers (if supplied) and all.  It has been `de-transparencied'
688        according to RFC 821, Section 4.5.2.  In other words, a line
689        containing a `.' followed by other text has had the leading dot
690        removed.
691
692        kwargs is a dictionary containing additional information.  It is
693        empty if decode_data=True was given as init parameter, otherwise
694        it will contain the following keys:
695            'mail_options': list of parameters to the mail command.  All
696                            elements are uppercase strings.  Example:
697                            ['BODY=8BITMIME', 'SMTPUTF8'].
698            'rcpt_options': same, for the rcpt command.
699
700        This function should return None for a normal `250 Ok' response;
701        otherwise, it should return the desired response string in RFC 821
702        format.
703
704        """
705        raise NotImplementedError
706
707
708class DebuggingServer(SMTPServer):
709
710    def _print_message_content(self, peer, data):
711        inheaders = 1
712        lines = data.splitlines()
713        for line in lines:
714            # headers first
715            if inheaders and not line:
716                peerheader = 'X-Peer: ' + peer[0]
717                if not isinstance(data, str):
718                    # decoded_data=false; make header match other binary output
719                    peerheader = repr(peerheader.encode('utf-8'))
720                print(peerheader)
721                inheaders = 0
722            if not isinstance(data, str):
723                # Avoid spurious 'str on bytes instance' warning.
724                line = repr(line)
725            print(line)
726
727    def process_message(self, peer, mailfrom, rcpttos, data, **kwargs):
728        print('---------- MESSAGE FOLLOWS ----------')
729        if kwargs:
730            if kwargs.get('mail_options'):
731                print('mail options: %s' % kwargs['mail_options'])
732            if kwargs.get('rcpt_options'):
733                print('rcpt options: %s\n' % kwargs['rcpt_options'])
734        self._print_message_content(peer, data)
735        print('------------ END MESSAGE ------------')
736
737
738class PureProxy(SMTPServer):
739    def __init__(self, *args, **kwargs):
740        if 'enable_SMTPUTF8' in kwargs and kwargs['enable_SMTPUTF8']:
741            raise ValueError("PureProxy does not support SMTPUTF8.")
742        super(PureProxy, self).__init__(*args, **kwargs)
743
744    def process_message(self, peer, mailfrom, rcpttos, data):
745        lines = data.split('\n')
746        # Look for the last header
747        i = 0
748        for line in lines:
749            if not line:
750                break
751            i += 1
752        lines.insert(i, 'X-Peer: %s' % peer[0])
753        data = NEWLINE.join(lines)
754        refused = self._deliver(mailfrom, rcpttos, data)
755        # TBD: what to do with refused addresses?
756        print('we got some refusals:', refused, file=DEBUGSTREAM)
757
758    def _deliver(self, mailfrom, rcpttos, data):
759        import smtplib
760        refused = {}
761        try:
762            s = smtplib.SMTP()
763            s.connect(self._remoteaddr[0], self._remoteaddr[1])
764            try:
765                refused = s.sendmail(mailfrom, rcpttos, data)
766            finally:
767                s.quit()
768        except smtplib.SMTPRecipientsRefused as e:
769            print('got SMTPRecipientsRefused', file=DEBUGSTREAM)
770            refused = e.recipients
771        except (OSError, smtplib.SMTPException) as e:
772            print('got', e.__class__, file=DEBUGSTREAM)
773            # All recipients were refused.  If the exception had an associated
774            # error code, use it.  Otherwise,fake it with a non-triggering
775            # exception code.
776            errcode = getattr(e, 'smtp_code', -1)
777            errmsg = getattr(e, 'smtp_error', 'ignore')
778            for r in rcpttos:
779                refused[r] = (errcode, errmsg)
780        return refused
781
782
783class Options:
784    setuid = True
785    classname = 'PureProxy'
786    size_limit = None
787    enable_SMTPUTF8 = False
788
789
790def parseargs():
791    global DEBUGSTREAM
792    try:
793        opts, args = getopt.getopt(
794            sys.argv[1:], 'nVhc:s:du',
795            ['class=', 'nosetuid', 'version', 'help', 'size=', 'debug',
796             'smtputf8'])
797    except getopt.error as e:
798        usage(1, e)
799
800    options = Options()
801    for opt, arg in opts:
802        if opt in ('-h', '--help'):
803            usage(0)
804        elif opt in ('-V', '--version'):
805            print(__version__)
806            sys.exit(0)
807        elif opt in ('-n', '--nosetuid'):
808            options.setuid = False
809        elif opt in ('-c', '--class'):
810            options.classname = arg
811        elif opt in ('-d', '--debug'):
812            DEBUGSTREAM = sys.stderr
813        elif opt in ('-u', '--smtputf8'):
814            options.enable_SMTPUTF8 = True
815        elif opt in ('-s', '--size'):
816            try:
817                int_size = int(arg)
818                options.size_limit = int_size
819            except:
820                print('Invalid size: ' + arg, file=sys.stderr)
821                sys.exit(1)
822
823    # parse the rest of the arguments
824    if len(args) < 1:
825        localspec = 'localhost:8025'
826        remotespec = 'localhost:25'
827    elif len(args) < 2:
828        localspec = args[0]
829        remotespec = 'localhost:25'
830    elif len(args) < 3:
831        localspec = args[0]
832        remotespec = args[1]
833    else:
834        usage(1, 'Invalid arguments: %s' % COMMASPACE.join(args))
835
836    # split into host/port pairs
837    i = localspec.find(':')
838    if i < 0:
839        usage(1, 'Bad local spec: %s' % localspec)
840    options.localhost = localspec[:i]
841    try:
842        options.localport = int(localspec[i+1:])
843    except ValueError:
844        usage(1, 'Bad local port: %s' % localspec)
845    i = remotespec.find(':')
846    if i < 0:
847        usage(1, 'Bad remote spec: %s' % remotespec)
848    options.remotehost = remotespec[:i]
849    try:
850        options.remoteport = int(remotespec[i+1:])
851    except ValueError:
852        usage(1, 'Bad remote port: %s' % remotespec)
853    return options
854
855
856if __name__ == '__main__':
857    options = parseargs()
858    # Become nobody
859    classname = options.classname
860    if "." in classname:
861        lastdot = classname.rfind(".")
862        mod = __import__(classname[:lastdot], globals(), locals(), [""])
863        classname = classname[lastdot+1:]
864    else:
865        import __main__ as mod
866    class_ = getattr(mod, classname)
867    proxy = class_((options.localhost, options.localport),
868                   (options.remotehost, options.remoteport),
869                   options.size_limit, enable_SMTPUTF8=options.enable_SMTPUTF8)
870    if options.setuid:
871        try:
872            import pwd
873        except ImportError:
874            print('Cannot import module "pwd"; try running with -n option.', file=sys.stderr)
875            sys.exit(1)
876        nobody = pwd.getpwnam('nobody')[2]
877        try:
878            os.setuid(nobody)
879        except PermissionError:
880            print('Cannot setuid "nobody"; try running with -n option.', file=sys.stderr)
881            sys.exit(1)
882    try:
883        asyncore.loop()
884    except KeyboardInterrupt:
885        pass
886