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