smtpd.py 23.7 KB
Newer Older
1
#! /usr/bin/env python3
2
"""An RFC 2821 smtp proxy.
3

4
Usage: %(program)s [options] [localhost:localport [remotehost:remoteport]]
5 6 7 8 9 10 11 12 13 14 15 16 17 18 19

Options:

    --nosetuid
    -n
        This program generally tries to setuid `nobody', unless this flag is
        set.  The setuid call will fail if this program is not run as root (in
        which case, use this flag).

    --version
    -V
        Print the version number and exit.

    --class classname
    -c classname
20
        Use `classname' as the concrete SMTP proxy class.  Uses `PureProxy' by
21 22 23 24 25 26 27 28 29 30 31 32
        default.

    --debug
    -d
        Turn on debugging prints.

    --help
    -h
        Print this message and exit.

Version: %(__version__)s

33 34 35
If localhost is not given then `localhost' is used, and if localport is not
given then 8025 is used.  If remotehost is not given then `localhost' is used,
and if remoteport is not given, then 25 is used.
36 37
"""

38

39 40 41 42 43 44
# Overview:
#
# This file implements the minimal SMTP protocol as defined in RFC 821.  It
# has a hierarchy of classes which implement the backend functionality for the
# smtpd.  A number of classes are provided:
#
45
#   SMTPServer - the base class for the backend.  Raises NotImplementedError
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
#   if you try to use it.
#
#   DebuggingServer - simply prints each message it receives on stdout.
#
#   PureProxy - Proxies all messages to a real smtpd which does final
#   delivery.  One known problem with this class is that it doesn't handle
#   SMTP errors from the backend server at all.  This should be fixed
#   (contributions are welcome!).
#
#   MailmanProxy - An experimental hack to work with GNU Mailman
#   <www.list.org>.  Using this server as your real incoming smtpd, your
#   mailhost will automatically recognize and accept mail destined to Mailman
#   lists when those lists are created.  Every message not destined for a list
#   gets forwarded to a real backend smtpd, as with PureProxy.  Again, errors
#   are not handled correctly yet.
#
#
63
# Author: Barry Warsaw <barry@python.org>
64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
#
# TODO:
#
# - support mailbox delivery
# - alias files
# - ESMTP
# - handle error codes from the backend smtpd

import sys
import os
import errno
import getopt
import time
import socket
import asyncore
import asynchat
80
from warnings import warn
81

82
__all__ = ["SMTPServer","DebuggingServer","PureProxy","MailmanProxy"]
83 84 85 86 87 88 89 90 91 92 93 94 95

program = sys.argv[0]
__version__ = 'Python SMTP proxy version 0.2'


class Devnull:
    def write(self, msg): pass
    def flush(self): pass


DEBUGSTREAM = Devnull()
NEWLINE = '\n'
EMPTYSTRING = ''
96
COMMASPACE = ', '
97 98


99

100
def usage(code, msg=''):
101
    print(__doc__ % globals(), file=sys.stderr)
102
    if msg:
103
        print(msg, file=sys.stderr)
104 105 106
    sys.exit(code)


107

108 109 110 111
class SMTPChannel(asynchat.async_chat):
    COMMAND = 0
    DATA = 1

112 113 114
    data_size_limit = 33554432
    command_size_limit = 512

115 116
    def __init__(self, server, conn, addr):
        asynchat.async_chat.__init__(self, conn)
117 118 119 120 121 122 123 124 125 126
        self.smtp_server = server
        self.conn = conn
        self.addr = addr
        self.received_lines = []
        self.smtp_state = self.COMMAND
        self.seen_greeting = ''
        self.mailfrom = None
        self.rcpttos = []
        self.received_data = ''
        self.fqdn = socket.getfqdn()
127
        self.num_bytes = 0
128 129 130 131 132 133 134 135 136
        try:
            self.peer = conn.getpeername()
        except socket.error as err:
            # a race condition  may occur if the other end is closing
            # before we can get the peername
            self.close()
            if err.args[0] != errno.ENOTCONN:
                raise
            return
137 138
        print('Peer:', repr(self.peer), file=DEBUGSTREAM)
        self.push('220 %s %s' % (self.fqdn, __version__))
139
        self.set_terminator(b'\r\n')
140

141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262
    # properties for backwards-compatibility
    @property
    def __server(self):
        warn("Access to __server attribute on SMTPChannel is deprecated, "
            "use 'smtp_server' instead", PendingDeprecationWarning, 2)
        return self.smtp_server
    @__server.setter
    def __server(self, value):
        warn("Setting __server attribute on SMTPChannel is deprecated, "
            "set 'smtp_server' instead", PendingDeprecationWarning, 2)
        self.smtp_server = value

    @property
    def __line(self):
        warn("Access to __line attribute on SMTPChannel is deprecated, "
            "use 'received_lines' instead", PendingDeprecationWarning, 2)
        return self.received_lines
    @__line.setter
    def __line(self, value):
        warn("Setting __line attribute on SMTPChannel is deprecated, "
            "set 'received_lines' instead", PendingDeprecationWarning, 2)
        self.received_lines = value

    @property
    def __state(self):
        warn("Access to __state attribute on SMTPChannel is deprecated, "
            "use 'smtp_state' instead", PendingDeprecationWarning, 2)
        return self.smtp_state
    @__state.setter
    def __state(self, value):
        warn("Setting __state attribute on SMTPChannel is deprecated, "
            "set 'smtp_state' instead", PendingDeprecationWarning, 2)
        self.smtp_state = value

    @property
    def __greeting(self):
        warn("Access to __greeting attribute on SMTPChannel is deprecated, "
            "use 'seen_greeting' instead", PendingDeprecationWarning, 2)
        return self.seen_greeting
    @__greeting.setter
    def __greeting(self, value):
        warn("Setting __greeting attribute on SMTPChannel is deprecated, "
            "set 'seen_greeting' instead", PendingDeprecationWarning, 2)
        self.seen_greeting = value

    @property
    def __mailfrom(self):
        warn("Access to __mailfrom attribute on SMTPChannel is deprecated, "
            "use 'mailfrom' instead", PendingDeprecationWarning, 2)
        return self.mailfrom
    @__mailfrom.setter
    def __mailfrom(self, value):
        warn("Setting __mailfrom attribute on SMTPChannel is deprecated, "
            "set 'mailfrom' instead", PendingDeprecationWarning, 2)
        self.mailfrom = value

    @property
    def __rcpttos(self):
        warn("Access to __rcpttos attribute on SMTPChannel is deprecated, "
            "use 'rcpttos' instead", PendingDeprecationWarning, 2)
        return self.rcpttos
    @__rcpttos.setter
    def __rcpttos(self, value):
        warn("Setting __rcpttos attribute on SMTPChannel is deprecated, "
            "set 'rcpttos' instead", PendingDeprecationWarning, 2)
        self.rcpttos = value

    @property
    def __data(self):
        warn("Access to __data attribute on SMTPChannel is deprecated, "
            "use 'received_data' instead", PendingDeprecationWarning, 2)
        return self.received_data
    @__data.setter
    def __data(self, value):
        warn("Setting __data attribute on SMTPChannel is deprecated, "
            "set 'received_data' instead", PendingDeprecationWarning, 2)
        self.received_data = value

    @property
    def __fqdn(self):
        warn("Access to __fqdn attribute on SMTPChannel is deprecated, "
            "use 'fqdn' instead", PendingDeprecationWarning, 2)
        return self.fqdn
    @__fqdn.setter
    def __fqdn(self, value):
        warn("Setting __fqdn attribute on SMTPChannel is deprecated, "
            "set 'fqdn' instead", PendingDeprecationWarning, 2)
        self.fqdn = value

    @property
    def __peer(self):
        warn("Access to __peer attribute on SMTPChannel is deprecated, "
            "use 'peer' instead", PendingDeprecationWarning, 2)
        return self.peer
    @__peer.setter
    def __peer(self, value):
        warn("Setting __peer attribute on SMTPChannel is deprecated, "
            "set 'peer' instead", PendingDeprecationWarning, 2)
        self.peer = value

    @property
    def __conn(self):
        warn("Access to __conn attribute on SMTPChannel is deprecated, "
            "use 'conn' instead", PendingDeprecationWarning, 2)
        return self.conn
    @__conn.setter
    def __conn(self, value):
        warn("Setting __conn attribute on SMTPChannel is deprecated, "
            "set 'conn' instead", PendingDeprecationWarning, 2)
        self.conn = value

    @property
    def __addr(self):
        warn("Access to __addr attribute on SMTPChannel is deprecated, "
            "use 'addr' instead", PendingDeprecationWarning, 2)
        return self.addr
    @__addr.setter
    def __addr(self, value):
        warn("Setting __addr attribute on SMTPChannel is deprecated, "
            "set 'addr' instead", PendingDeprecationWarning, 2)
        self.addr = value

263 264
    # Overrides base class for convenience
    def push(self, msg):
265
        asynchat.async_chat.push(self, bytes(msg + '\r\n', 'ascii'))
266 267 268

    # Implementation of base class abstract method
    def collect_incoming_data(self, data):
269 270 271 272 273 274 275 276 277
        limit = None
        if self.smtp_state == self.COMMAND:
            limit = self.command_size_limit
        elif self.smtp_state == self.DATA:
            limit = self.data_size_limit
        if limit and self.num_bytes > limit:
            return
        elif limit:
            self.num_bytes += len(data)
278
        self.received_lines.append(str(data, "utf8"))
279 280 281

    # Implementation of base class abstract method
    def found_terminator(self):
282
        line = EMPTYSTRING.join(self.received_lines)
283
        print('Data:', repr(line), file=DEBUGSTREAM)
284 285
        self.received_lines = []
        if self.smtp_state == self.COMMAND:
286 287 288 289 290
            if self.num_bytes > self.command_size_limit:
                self.push('500 Error: line too long')
                self.num_bytes = 0
                return
            self.num_bytes = 0
291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308
            if not line:
                self.push('500 Error: bad syntax')
                return
            method = None
            i = line.find(' ')
            if i < 0:
                command = line.upper()
                arg = None
            else:
                command = line[:i].upper()
                arg = line[i+1:].strip()
            method = getattr(self, 'smtp_' + command, None)
            if not method:
                self.push('502 Error: command "%s" not implemented' % command)
                return
            method(arg)
            return
        else:
309
            if self.smtp_state != self.DATA:
310
                self.push('451 Internal confusion')
311 312 313 314 315
                self.num_bytes = 0
                return
            if self.num_bytes > self.data_size_limit:
                self.push('552 Error: Too much mail data')
                self.num_bytes = 0
316 317 318 319 320 321 322 323 324
                return
            # Remove extraneous carriage returns and de-transparency according
            # to RFC 821, Section 4.5.2.
            data = []
            for text in line.split('\r\n'):
                if text and text[0] == '.':
                    data.append(text[1:])
                else:
                    data.append(text)
325
            self.received_data = NEWLINE.join(data)
326 327 328 329
            status = self.smtp_server.process_message(self.peer,
                                                      self.mailfrom,
                                                      self.rcpttos,
                                                      self.received_data)
330 331 332
            self.rcpttos = []
            self.mailfrom = None
            self.smtp_state = self.COMMAND
333
            self.num_bytes = 0
334
            self.set_terminator(b'\r\n')
335 336 337 338 339 340 341 342 343 344
            if not status:
                self.push('250 Ok')
            else:
                self.push(status)

    # SMTP and ESMTP commands
    def smtp_HELO(self, arg):
        if not arg:
            self.push('501 Syntax: HELO hostname')
            return
345
        if self.seen_greeting:
346 347
            self.push('503 Duplicate HELO/EHLO')
        else:
348 349
            self.seen_greeting = arg
            self.push('250 %s' % self.fqdn)
350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367

    def smtp_NOOP(self, arg):
        if arg:
            self.push('501 Syntax: NOOP')
        else:
            self.push('250 Ok')

    def smtp_QUIT(self, arg):
        # args is ignored
        self.push('221 Bye')
        self.close_when_done()

    # factored
    def __getaddr(self, keyword, arg):
        address = None
        keylen = len(keyword)
        if arg[:keylen].upper() == keyword:
            address = arg[keylen:].strip()
368 369 370
            if not address:
                pass
            elif address[0] == '<' and address[-1] == '>' and address != '<>':
371 372 373 374 375 376
                # Addresses can be in the form <person@dom.com> but watch out
                # for null address, e.g. <>
                address = address[1:-1]
        return address

    def smtp_MAIL(self, arg):
377
        print('===> MAIL', arg, file=DEBUGSTREAM)
378
        address = self.__getaddr('FROM:', arg) if arg else None
379 380 381
        if not address:
            self.push('501 Syntax: MAIL FROM:<address>')
            return
382
        if self.mailfrom:
383 384
            self.push('503 Error: nested MAIL command')
            return
385 386
        self.mailfrom = address
        print('sender:', self.mailfrom, file=DEBUGSTREAM)
387 388 389
        self.push('250 Ok')

    def smtp_RCPT(self, arg):
390
        print('===> RCPT', arg, file=DEBUGSTREAM)
391
        if not self.mailfrom:
392 393
            self.push('503 Error: need MAIL command')
            return
394
        address = self.__getaddr('TO:', arg) if arg else None
395 396 397
        if not address:
            self.push('501 Syntax: RCPT TO: <address>')
            return
398 399
        self.rcpttos.append(address)
        print('recips:', self.rcpttos, file=DEBUGSTREAM)
400 401 402 403 404 405 406
        self.push('250 Ok')

    def smtp_RSET(self, arg):
        if arg:
            self.push('501 Syntax: RSET')
            return
        # Resets the sender, recipients, and data, but not the greeting
407 408 409 410
        self.mailfrom = None
        self.rcpttos = []
        self.received_data = ''
        self.smtp_state = self.COMMAND
411 412 413
        self.push('250 Ok')

    def smtp_DATA(self, arg):
414
        if not self.rcpttos:
415 416 417 418 419
            self.push('503 Error: need RCPT command')
            return
        if arg:
            self.push('501 Syntax: DATA')
            return
420
        self.smtp_state = self.DATA
421
        self.set_terminator(b'\r\n.\r\n')
422 423 424
        self.push('354 End data with <CR><LF>.<CR><LF>')


425

426
class SMTPServer(asyncore.dispatcher):
427 428 429
    # SMTPChannel class to use for managing client connections
    channel_class = SMTPChannel

430 431 432 433
    def __init__(self, localaddr, remoteaddr):
        self._localaddr = localaddr
        self._remoteaddr = remoteaddr
        asyncore.dispatcher.__init__(self)
434 435 436 437 438 439 440 441 442 443 444 445 446
        try:
            self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
            # try to re-use a server port if possible
            self.set_reuse_addr()
            self.bind(localaddr)
            self.listen(5)
        except:
            self.close()
            raise
        else:
            print('%s started at %s\n\tLocal addr: %s\n\tRemote addr:%s' % (
                self.__class__.__name__, time.ctime(time.time()),
                localaddr, remoteaddr), file=DEBUGSTREAM)
447

448
    def handle_accepted(self, conn, addr):
449
        print('Incoming connection from %s' % repr(addr), file=DEBUGSTREAM)
450
        channel = self.channel_class(self, conn, addr)
451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474

    # API for "doing something useful with the message"
    def process_message(self, peer, mailfrom, rcpttos, data):
        """Override this abstract method to handle messages from the client.

        peer is a tuple containing (ipaddr, port) of the client that made the
        socket connection to our smtp port.

        mailfrom is the raw address the client claims the message is coming
        from.

        rcpttos is a list of raw addresses the client wishes to deliver the
        message to.

        data is a string containing the entire full text of the message,
        headers (if supplied) and all.  It has been `de-transparencied'
        according to RFC 821, Section 4.5.2.  In other words, a line
        containing a `.' followed by other text has had the leading dot
        removed.

        This function should return None, for a normal `250 Ok' response;
        otherwise it returns the desired response string in RFC 821 format.

        """
475
        raise NotImplementedError
476

Tim Peters's avatar
Tim Peters committed
477

478

479 480 481 482 483
class DebuggingServer(SMTPServer):
    # Do something with the gathered message
    def process_message(self, peer, mailfrom, rcpttos, data):
        inheaders = 1
        lines = data.split('\n')
484
        print('---------- MESSAGE FOLLOWS ----------')
485 486 487
        for line in lines:
            # headers first
            if inheaders and not line:
488
                print('X-Peer:', peer[0])
489
                inheaders = 0
490 491
            print(line)
        print('------------ END MESSAGE ------------')
492 493


494

495 496 497 498 499 500 501 502 503 504 505 506 507
class PureProxy(SMTPServer):
    def process_message(self, peer, mailfrom, rcpttos, data):
        lines = data.split('\n')
        # Look for the last header
        i = 0
        for line in lines:
            if not line:
                break
            i += 1
        lines.insert(i, 'X-Peer: %s' % peer[0])
        data = NEWLINE.join(lines)
        refused = self._deliver(mailfrom, rcpttos, data)
        # TBD: what to do with refused addresses?
508
        print('we got some refusals:', refused, file=DEBUGSTREAM)
509 510 511 512 513 514 515 516 517 518 519

    def _deliver(self, mailfrom, rcpttos, data):
        import smtplib
        refused = {}
        try:
            s = smtplib.SMTP()
            s.connect(self._remoteaddr[0], self._remoteaddr[1])
            try:
                refused = s.sendmail(mailfrom, rcpttos, data)
            finally:
                s.quit()
520
        except smtplib.SMTPRecipientsRefused as e:
521
            print('got SMTPRecipientsRefused', file=DEBUGSTREAM)
522
            refused = e.recipients
523
        except (socket.error, smtplib.SMTPException) as e:
524
            print('got', e.__class__, file=DEBUGSTREAM)
525 526 527 528 529 530 531 532 533 534
            # All recipients were refused.  If the exception had an associated
            # error code, use it.  Otherwise,fake it with a non-triggering
            # exception code.
            errcode = getattr(e, 'smtp_code', -1)
            errmsg = getattr(e, 'smtp_error', 'ignore')
            for r in rcpttos:
                refused[r] = (errcode, errmsg)
        return refused


535

536 537
class MailmanProxy(PureProxy):
    def process_message(self, peer, mailfrom, rcpttos, data):
538
        from io import StringIO
539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571
        from Mailman import Utils
        from Mailman import Message
        from Mailman import MailList
        # If the message is to a Mailman mailing list, then we'll invoke the
        # Mailman script directly, without going through the real smtpd.
        # Otherwise we'll forward it to the local proxy for disposition.
        listnames = []
        for rcpt in rcpttos:
            local = rcpt.lower().split('@')[0]
            # We allow the following variations on the theme
            #   listname
            #   listname-admin
            #   listname-owner
            #   listname-request
            #   listname-join
            #   listname-leave
            parts = local.split('-')
            if len(parts) > 2:
                continue
            listname = parts[0]
            if len(parts) == 2:
                command = parts[1]
            else:
                command = ''
            if not Utils.list_exists(listname) or command not in (
                    '', 'admin', 'owner', 'request', 'join', 'leave'):
                continue
            listnames.append((rcpt, listname, command))
        # Remove all list recipients from rcpttos and forward what we're not
        # going to take care of ourselves.  Linear removal should be fine
        # since we don't expect a large number of recipients.
        for rcpt, listname, command in listnames:
            rcpttos.remove(rcpt)
Tim Peters's avatar
Tim Peters committed
572
        # If there's any non-list destined recipients left,
573
        print('forwarding recips:', ' '.join(rcpttos), file=DEBUGSTREAM)
574 575 576
        if rcpttos:
            refused = self._deliver(mailfrom, rcpttos, data)
            # TBD: what to do with refused addresses?
577
            print('we got refusals:', refused, file=DEBUGSTREAM)
578 579 580 581 582
        # Now deliver directly to the list commands
        mlists = {}
        s = StringIO(data)
        msg = Message.Message(s)
        # These headers are required for the proper execution of Mailman.  All
583
        # MTAs in existence seem to add these if the original message doesn't
584
        # have them.
585
        if not msg.get('from'):
586
            msg['From'] = mailfrom
587
        if not msg.get('date'):
588 589
            msg['Date'] = time.ctime(time.time())
        for rcpt, listname, command in listnames:
590
            print('sending message to', rcpt, file=DEBUGSTREAM)
591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613
            mlist = mlists.get(listname)
            if not mlist:
                mlist = MailList.MailList(listname, lock=0)
                mlists[listname] = mlist
            # dispatch on the type of command
            if command == '':
                # post
                msg.Enqueue(mlist, tolist=1)
            elif command == 'admin':
                msg.Enqueue(mlist, toadmin=1)
            elif command == 'owner':
                msg.Enqueue(mlist, toowner=1)
            elif command == 'request':
                msg.Enqueue(mlist, torequest=1)
            elif command in ('join', 'leave'):
                # TBD: this is a hack!
                if command == 'join':
                    msg['Subject'] = 'subscribe'
                else:
                    msg['Subject'] = 'unsubscribe'
                msg.Enqueue(mlist, torequest=1)


614

615 616 617 618 619
class Options:
    setuid = 1
    classname = 'PureProxy'


620

621 622 623 624 625 626
def parseargs():
    global DEBUGSTREAM
    try:
        opts, args = getopt.getopt(
            sys.argv[1:], 'nVhc:d',
            ['class=', 'nosetuid', 'version', 'help', 'debug'])
627
    except getopt.error as e:
628 629 630 631 632 633 634
        usage(1, e)

    options = Options()
    for opt, arg in opts:
        if opt in ('-h', '--help'):
            usage(0)
        elif opt in ('-V', '--version'):
635
            print(__version__, file=sys.stderr)
636 637 638 639 640 641 642 643 644
            sys.exit(0)
        elif opt in ('-n', '--nosetuid'):
            options.setuid = 0
        elif opt in ('-c', '--class'):
            options.classname = arg
        elif opt in ('-d', '--debug'):
            DEBUGSTREAM = sys.stderr

    # parse the rest of the arguments
645 646 647 648
    if len(args) < 1:
        localspec = 'localhost:8025'
        remotespec = 'localhost:25'
    elif len(args) < 2:
649
        localspec = args[0]
650
        remotespec = 'localhost:25'
651 652 653
    elif len(args) < 3:
        localspec = args[0]
        remotespec = args[1]
654 655 656
    else:
        usage(1, 'Invalid arguments: %s' % COMMASPACE.join(args))

657 658 659
    # split into host/port pairs
    i = localspec.find(':')
    if i < 0:
660
        usage(1, 'Bad local spec: %s' % localspec)
661 662 663 664
    options.localhost = localspec[:i]
    try:
        options.localport = int(localspec[i+1:])
    except ValueError:
665
        usage(1, 'Bad local port: %s' % localspec)
666 667
    i = remotespec.find(':')
    if i < 0:
668
        usage(1, 'Bad remote spec: %s' % remotespec)
669 670 671 672
    options.remotehost = remotespec[:i]
    try:
        options.remoteport = int(remotespec[i+1:])
    except ValueError:
673
        usage(1, 'Bad remote port: %s' % remotespec)
674 675 676
    return options


677

678 679 680
if __name__ == '__main__':
    options = parseargs()
    # Become nobody
681 682 683 684 685 686 687 688 689 690
    classname = options.classname
    if "." in classname:
        lastdot = classname.rfind(".")
        mod = __import__(classname[:lastdot], globals(), locals(), [""])
        classname = classname[lastdot+1:]
    else:
        import __main__ as mod
    class_ = getattr(mod, classname)
    proxy = class_((options.localhost, options.localport),
                   (options.remotehost, options.remoteport))
691 692 693 694
    if options.setuid:
        try:
            import pwd
        except ImportError:
695
            print('Cannot import module "pwd"; try running with -n option.', file=sys.stderr)
696 697 698 699
            sys.exit(1)
        nobody = pwd.getpwnam('nobody')[2]
        try:
            os.setuid(nobody)
700
        except OSError as e:
701
            if e.errno != errno.EPERM: raise
702
            print('Cannot setuid "nobody"; try running with -n option.', file=sys.stderr)
703 704 705 706 707
            sys.exit(1)
    try:
        asyncore.loop()
    except KeyboardInterrupt:
        pass