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