SF.net SVN: tmda: [2176] trunk/tmda/bin/tmda-ofmipd

[email protected] Tue, 13 Mar 2007 21:29:11 -0700
Newsgroups gmane.mail.spam.tmda.cvs
Message-ID <[email protected]>
Revision: 2176
          http://svn.sourceforge.net/tmda/?rev=2176&view=rev
Author:   srwarren
Date:     2007-03-13 21:29:10 -0700 (Tue, 13 Mar 2007)

Log Message:
-----------
tmda-ofmipd: Purely a code-reorganization.

This is what looks like a very large diff, but there are zero functional changes.
I've simply re-ordered all the imports, function definitions, classes, and main
code so that those groups are all nestled together. This makes it a little
easier to follow the flow of the main code, without interspersed variable,
function, or class definitions.

I hope nobody minds. Can you tell I'm anal yet;-)

Modified Paths:
--------------
    trunk/tmda/bin/tmda-ofmipd

Modified: trunk/tmda/bin/tmda-ofmipd
===================================================================
--- trunk/tmda/bin/tmda-ofmipd	2007-03-14 04:04:38 UTC (rev 2175)
+++ trunk/tmda/bin/tmda-ofmipd	2007-03-14 04:29:10 UTC (rev 2176)
@@ -29,6 +29,16 @@
 import signal
 import socket
 import sys
+import asynchat
+import asyncore
+import base64
+import hmac
+import imaplib
+import md5
+import popen2
+import poplib
+import random
+import time
 
 try:
     import paths
@@ -42,628 +52,13 @@
 from TMDA import Util
 from TMDA import Version
 
+# Classes
 
 class Devnull:
     def write(self, msg): pass
     def flush(self): pass
 
-program = sys.argv[0]
 
-FQDN = socket.getfqdn()
-if FQDN == 'localhost':
-    FQDN = socket.gethostname()
-
-if os.getuid() == 0:
-    running_as_root = True
-else:
-    running_as_root = False
-
-remoteauth = { 'proto': None,
-               'host': 'localhost',
-               'port': None,
-               'dn': '',
-               'enable': 0,
-               }
-defaultauthports = { 'imap': 143,
-                     'imaps': 993,
-                     'apop': 110,
-                     'pop3': 110,
-                     'ldap': 389,
-                     # 'pop3s': 995,
-                     }
-
-# option parsing
-
-opt_desc = \
-"""An authenticated ofmip proxy for TMDA that allows you to 'tag' your
-mail client's outgoing mail through SMTP.  For more information,
-including setup and usage instructions, see
-http://wiki.tmda.net/TmdaOfmipdHowto""" 
-
-parser = OptionParser(description=opt_desc, version=Version.TMDA)
-
-parser.add_option("-V", 
-                  action="store_true", default=False, dest="full_version",
-                  help="show full TMDA version information and exit.")
-
-# option groups
-gengroup = OptionGroup(parser, "General")
-congroup = OptionGroup(parser, "Connection")
-authgroup = OptionGroup(parser, "Authentication")
-virtgroup = OptionGroup(parser, "Virtual Domains")
-    
-# general
-gengroup.add_option("-d", "--debug",
-                    action="store_true", default=False, dest="debug",
-                    help="Turn on debugging prints.")
-
-gengroup.add_option("-L", "--log",
-                    action="store_true", default=False, dest="log",
-                    help= \
-"""Turn on logging prints.
-This option logs everything that -d logs, except for the raw SMTP protocol
-data. Hence, it is useful if you want to leave logging enabled permanently,
-but don't want your logs bloated with AUTH data and/or the content of large
-attachments.""")
-
-gengroup.add_option("-b", "--background",
-                    action="store_false", dest="foreground",
-                    help="Detach and run in the background (default).")
-
-gengroup.add_option("-f", "--foreground",
-                    action="store_true", default=False, dest="foreground",
-                    help="Don't detach; run in the foreground.")
-
-gengroup.add_option("-u", "--username",
-                    dest="username",
-                    help= \
-"""The username that this program should run under.  The default is to
-run as the user who starts the program unless that is root, in which
-case an attempt to seteuid user 'tofmipd' will be made.  Use this
-option to override these defaults.""")
-
-gengroup.add_option("-c", "--configdir",
-                    metavar="DIR", dest="configdir",
-                    help= \
-"""DIR is the base directory to search for the authenticated user's TMDA
-configuration file in.  This might be useful if you wish to maintain
-TMDA files outside the user's home directory.
-'username/config' will be appended to form the path; e.g, `-c
-/var/tmda' will have tmda-ofmipd search for `/var/tmda/bobby/config'.
-If this option is not used, `~user/.tmda/config' will be assumed, but
-see the --vhome-script option for qmail virtual domain users.""")
-
-# connection
-congroup.add_option("-p", "--proxyport",
-                    default="%s:%s" % (FQDN, 8025), metavar="HOST:PORT", 
-                    dest="proxyport", help= \
-"""The HOST:PORT to listen for incoming connections on.  The default is
-FQDN:8025 (i.e, port 8025 on the fully qualified domain name for the
-local host).  Use '0.0.0.0:PORT' to listen on all available
-interfaces.""")
-
-congroup.add_option("-C", "--connections",
-                    type="int", default="20", metavar="NUM", dest="connections",
-                    help= \
-"""Do not handle more than NUM simultaneous connections. If there are NUM
-active connections, defer acceptance of new connections until one
-finishes. NUM must be a positive integer. Default: 20""")
-
-congroup.add_option("-1", "--one-session",
-                    action="store_true", default=False, dest="one_session",
-                    help= \
-"""Don't bind to a port and accept new connections; Process a single SMTP
-session on stdin (used both for input & output).  This is useful when
-started from tcpserver or stunnel.""")
-
-congroup.add_option("-P", "--pure-proxy",
-                    action="store_true", default=False, dest="pure_proxy",
-                    help= \
-"""Proxy the message straight through to the mail transport system
-unaltered if the user's TMDA config file is missing.  The
-/usr/sbin/sendmail program on the system is used to inject the
-message.  You can override this by setting $TMDA_SENDMAIL_PROGRAM in
-the environment.  This option might be useful when serving a mixed
-environment of TMDA and non-TMDA users.""")
-
-congroup.add_option("-t", "--throttle-script",
-                    metavar="/PATH/TO/SCRIPT", dest="throttlescript",
-                    help= \
-"""Full pathname of a script which can meter how much mail any user
-sends.  The script is passed a login name whenever a user tries to
-send mail.  If the script returns a 0, the message is allowed.  For
-any other value, the message is rejected.""")
-
-congroup.add_option("", "--ssl",
-                    action="store_true", default=False, dest="ssl",
-                    help= \
-"""Enable SSL encryption. This mode immediately initiates the SSL/TLS
-protocol as soon as a connection is made. This mode is not support
-for the STARTTLS command. This configuration is typically run on
-port 465 (smtps).""")
-
-congroup.add_option("", "--tls",
-                    type="choice", default=None, dest="tls",
-                    choices=['optional', 'on'],
-                    help= \
-"""Enable TLS mode. Valid options are optional and on. With this option
-enabled, the STARTTLS SMTP command may be used to upgrade the plain-text
-connection to SSL/TLS. In 'optional' mode, AUTH is allowed either before
-or after STARTTLS. In 'on' mode, clients are forced to STARTTLS prior to
-AUTH, to ensure that plain-text AUTH commands are protected. This
-configuration is typically run on port 587 (submission).""")
-
-congroup.add_option("", "--ssl-cert",
-                    metavar="/PATH/TO/FILE", default=None, dest="ssl_cert",
-                    help= \
-"""Location of the SSL/TLS certificate key file.""")
-
-congroup.add_option("", "--ssl-key",
-                    metavar="/PATH/TO/FILE", default=None, dest="ssl_key",
-                    help= \
-"""Location of the SSL/TLS private key file.""")
-
-# authentication
-authgroup.add_option("-R", "--remoteauth",
-                     metavar="PROTO://HOST[:PORT][/DN]", dest="remoteauth",
-                     help= \
-"""Protocol and host to check username and password. PROTO can be one of
-the following: 'imap' (IMAP4 server), 'imaps' (IMAP4 server over SSL),
-'pop3' (POP3 server), 'apop' (POP3 server with APOP authentication),
-'ldap' (LDAP server).  Optional :PORT defaults to the standard port for
-the specified protocol (143 for imap, 993 for imaps, 110 for
-pop3/apop, and 389 for ldap).  /DN is mandatory for ldap and should
-contain a '%%s' identifying the username. Examples: '-R
-imaps://myimapserver.net', '-R pop3://mypopserver.net:2110', '-R
-ldap://example.com/cn=%%s,dc=host,dc=com'""")
-
-authgroup.add_option("-A", "--authprog",
-                     metavar="PROGRAM", dest="authprog",
-                     help= \
-"""A checkpassword compatible command used to check username/password.
-Examples: '-A "/usr/sbin/checkpassword-pam -s id -- /bin/true"',
-'-A "/usr/local/vpopmail/bin/vchkpw /usr/bin/true"'.
-The program must be able to receive the username/password pair on
-descriptor 3 and in the following format: `username\\0password\\0'
-Any program claiming to be checkpassword-compatible should be able to
-do this.  If you can tell the program to accept input on another
-descriptor, such as stdin, don't.  It won't work, because TMDA follows
-the standard (http://cr.yp.to/checkpwd/interface.html) exactly.
-Also, checkpassword-type programs expect to find the name of another
-program to run on their command line.  For tmda-ofmipd's purpose,
-/bin/true is perfectly fine.
-Note the position of the quotes in the Examples, which cause the the
-whole string following the -A to be passed as a single argument.""")
-
-authgroup.add_option("-a", "--authfile",
-                     metavar="FILE", dest="authfile",
-                     help= \
-"""Path to the file holding authentication information for this proxy.
-Default location is /etc/tofmipd if running as root/tofmipd, otherwise
-~user/.tmda/tofmipd.  Use this option to override these defaults.""")
-
-authgroup.add_option("-F", "--fallback",
-                     action="store_true", default=False, dest="fallback",
-                     help= \
-"""When used with -R or -A, fallback to authenticate against the authfile
-if remote authentication fails.  Note: this flag has no effect on -R
-to -A fallback. If you specify both -R and -A methods, then authprog
-will be tried after remoteauth has failed.""")
-
-# virtual domains
-virtgroup.add_option("-S", "--vhome-script",
-                     metavar="/PATH/TO/SCRIPT", dest="vhomescript",
-                     help= \
-"""Full pathname of a script that prints a virtual email user's home
-directory on standard output.  tmda-ofmipd will read that and use it
-to build the path to the user's config file instead of '~user/.tmda'.
-The script must take two arguments, the user name and the domain, on
-its command line.  This option is for use only with the VPopMail and
-VMailMgr add-ons to qmail.  See the contrib directory for sample
-scripts.""")
-
-virtgroup.add_option("-v", "--vdomains-path",
-                     default="/var/qmail/control/virtualdomains", 
-                     metavar="/PATH/TO/FILE", dest="vdomainspath",
-                     help= \
-"""Full pathname to qmail's virtualdomains file.  The default is
-/var/qmail/control/virtualdomains.  This is also tmda-ofmipd's
-default, so you normally won't need to set this parameter.  If you
-have installed qmail somewhere other than /var/qmail, you will need to
-set this so tmda-ofmipd can find the virtualdomains file.  NOTE: This
-is only used when you have a qmail installation with virtual domains
-using the VMailMgr add-on.  It implies that you will also set the
-'--vhome-script' option above.""")
-
-for g in (gengroup, congroup, authgroup, virtgroup):
-    parser.add_option_group(g)
-
-(opts, args) = parser.parse_args()
-
-if opts.full_version:
-    print Version.ALL
-    sys.exit()
-if opts.vhomescript and opts.configdir:
-    parser.error("options '--vhome-script' and '--configdir' are incompatible!")
-if opts.debug or opts.log:
-    DEBUGSTREAM = sys.stderr
-else:
-    DEBUGSTREAM = Devnull()
-
-if opts.remoteauth:
-    # arg is like: imap://host:port
-    autharg = opts.remoteauth
-    try:
-        authproto, autharg = autharg.split('://', 1)
-    except ValueError:
-        authproto, autharg = autharg, None
-    if authproto not in defaultauthports.keys():
-        raise ValueError, 'Protocol not supported: ' + authproto + \
-            '\nPlease pick one of ' + repr(defaultauthports.keys())
-    remoteauth['proto'] = authproto
-    remoteauth['port'] = defaultauthports[authproto]
-    if autharg:
-        try:
-            autharg, dn = autharg.split('/', 1)
-            remoteauth['dn'] = dn
-        except ValueError:
-            dn = ''
-        try:
-            authhost, authport = autharg.split(':', 1)
-        except ValueError:
-            authhost = autharg
-            authport = defaultauthports[authproto]
-        if authhost:
-            remoteauth['host'] = authhost
-        if authport:
-            remoteauth['port'] = authport
-    print >> DEBUGSTREAM, "auth method: %s://%s:%s/%s" % \
-        (remoteauth['proto'], remoteauth['host'],
-         remoteauth['port'], remoteauth['dn'])
-    remoteauth['enable'] = 1
-
-if running_as_root:
-    if not opts.username:
-        opts.username = 'tofmipd'
-    if not opts.authfile:
-        opts.authfile = '/etc/tofmipd'
-    ipauthmapfile = '/etc/ipauthmap'
-else:
-    tmda_path = os.path.join(os.path.expanduser('~'), '.tmda')
-    ipauthmapfile = os.path.join(tmda_path, 'ipauthmap')
-    if not opts.authfile:
-        opts.authfile = os.path.join(tmda_path, 'tofmipd')
-
-def warning(msg='', exit=1):
-    delimiter = '*' * 70
-    if msg:
-        msg = Util.wraptext(msg)
-        print >> sys.stderr, '\n', delimiter, '\n', msg, '\n', delimiter, '\n'
-    if exit:
-        sys.exit()
-
-
-# provide disclaimer if running as root
-if running_as_root:
-    msg = 'WARNING: The security implications and risks of running ' + \
-          program + ' in "seteuid" mode have not been fully evaluated.  ' + \
-          'If you are uncomfortable with this, quit now and instead run ' + \
-          program + ' under your non-privileged TMDA user account.'
-    warning(msg, exit=0)
-
-
-import asynchat
-import asyncore
-import base64
-import hmac
-import imaplib
-import md5
-import popen2
-import poplib
-import random
-import time
-
-
-__version__ = Version.TMDA
-NEWLINE = '\n'
-EMPTYSTRING = ''
-COMMASPACE = ', '
-
-
-if remoteauth['proto'] == 'ldap':
-    try:
-        import ldap
-    except ImportError:
-        raise ImportError, \
-              'python-ldap (http://python-ldap.sf.net/) required.'
-    if remoteauth['dn'] == '':
-        print >> DEBUGSTREAM, "Error: Missing ldap dn\n"
-        raise ValueError
-    try:
-        remoteauth['dn'].index('%s')
-    except:
-        print >> DEBUGSTREAM, "Error: Invalid ldap dn\n"
-        raise ValueError
-
-if opts.ssl or opts.tls:
-    if opts.ssl and opts.tls:
-        raise ValueError, 'Can\'t do SSL and TLS at the same time'
-
-    try:
-        from tlslite.api import *
-        from tlslite.TLSConnection import TLSConnection
-        from tlslite.integration.AsyncStateMachine import AsyncStateMachine
-    except ImportError:
-        raise ImportError, \
-              'tlslite (http://trevp.net/tlslite/) required.'
-
-    if (not opts.ssl_cert) or (not opts.ssl_key):
-        raise ValueError, \
-            '--ssl-cert and --ssl-key are required when using --ssl or --tls'
-
-    fhc = file(os.path.expanduser(opts.ssl_cert), 'r')
-    datac = fhc.read()
-    fhc.close()
-    x509 = X509()
-    x509.parse(datac)
-    opts.ssl_cert_value = X509CertChain([x509])
-
-    fhk = file(os.path.expanduser(opts.ssl_key), 'r')
-    datak = fhk.read()
-    fhk.close()
-    opts.ssl_key_value = parsePEMKey(datak, private=True)
-
-    class TMDATLSAsyncDispatcherMixIn(AsyncStateMachine):
-        """A custom version of tlslite's TLSAsyncDispatcherMixIn.
-        This version:
-        * Requires siblingClass to be specified explicitly, to ease use with
-          inheritance.
-        * Allows the mixin to remain dormant until activated, which allows
-          implementation of deferred SSL startup, as required for the STARTTLS
-          SMTP command.
-        """
-
-        def __init__(self, sock, siblingClass):
-            AsyncStateMachine.__init__(self)
-
-            self.siblingClass = siblingClass
-            self._active = False
-
-            self.tlsConnection = TLSConnection(sock)
-
-        def tlsMixinSetActive(self):
-            self._active = True
-
-        def readable(self):
-            if self._active:
-                result = self.wantsReadEvent()
-                if result != None:
-                    return result
-                return self.siblingClass.readable(self)
-            else:
-                return self.siblingClass.readable(self)
-
-        def writable(self):
-            if self._active:
-                result = self.wantsWriteEvent()
-                if result != None:
-                    return result
-                return self.siblingClass.readable(self)
-            else:
-                return self.siblingClass.writable(self)
-
-        def handle_read(self):
-            if self._active:
-                self.inReadEvent()
-            else:
-                self.siblingClass.handle_read(self)
-
-        def handle_write(self):
-            if self._active:
-                self.inWriteEvent()
-            else:
-                self.siblingClass.handle_write(self)
-
-        def outConnectEvent(self):
-            if not self._active:
-                raise "Internal state confusion"
-            self.siblingClass.handle_connect(self)
-
-        def outCloseEvent(self):
-            if not self._active:
-                raise "Internal state confusion"
-            asyncore.dispatcher.close(self)
-
-        def outReadEvent(self, readBuffer):
-            if not self._active:
-                raise "Internal state confusion"
-            if readBuffer == '':
-                self.outCloseEvent()
-            else:
-                self.readBuffer = readBuffer
-                self.siblingClass.handle_read(self)
-
-        def outWriteEvent(self):
-            if not self._active:
-                raise "Internal state confusion"
-            self.siblingClass.handle_write(self)
-
-        def recv(self, bufferSize=16384):
-            if self._active:
-                if bufferSize < 16384 or self.readBuffer == None:
-                    raise AssertionError()
-                returnValue = self.readBuffer
-                self.readBuffer = None
-                return returnValue
-            else:
-                return self.siblingClass.recv(self, bufferSize)
-
-        def send(self, writeBuffer):
-            if self._active:
-                self.setWriteOp(writeBuffer)
-                return len(writeBuffer)
-            else:
-                return self.siblingClass.send(self, writeBuffer)
-
-        def close(self):
-            if self._active:
-                if hasattr(self, "tlsConnection"):
-                    self.setCloseOp()
-                else:
-                    asyncore.dispatcher.close(self)
-            else:
-                return self.siblingClass.close(self)
-
-
-# Utility functions
-
-def pipecmd(command, *strings):
-    popen2._cleanup()
-    cmd = popen2.Popen3(command, 1, bufsize=-1)
-    cmdout, cmdin, cmderr = cmd.fromchild, cmd.tochild, cmd.childerr
-    if strings:
-        # Write to the tochild file object.
-        for s in strings:
-            cmdin.write(s)
-        cmdin.flush()
-        cmdin.close()
-    # Read from the childerr object; command will block until exit.
-    err = cmderr.read().strip()
-    cmderr.close()
-    # Read from the fromchild object.
-    out = cmdout.read().strip()
-    cmdout.close()
-    # Get exit status from the wait() member function.
-    return cmd.wait()
-
-
-def run_authprog(username, password):
-    """authprog should return 0 for auth ok, and a positive integer in
-    case of a problem."""
-    print >> DEBUGSTREAM, "Trying authprog method"
-    cmd = "/bin/sh -c 'exec %s 3<&0'" % (opts.authprog,)
-    return pipecmd(cmd, '%s\0%s\0' % (username, password))
-
-
-def run_remoteauth(username, password, localip):
-    """Authenticate username/password combination against a remote
-    resource.  Return 1 upon successful authentication, and 0
-    otherwise."""
-    authhost = remoteauth['host']
-    authport = remoteauth['port']
-    if authhost == '0.0.0.0':
-        ipauthmap = ipauthmap2dict(ipauthmapfile)
-        if len(ipauthmap) == 0:
-            authhost = localip
-        else:
-            authdata = ipauthmap.get(localip, '127.0.0.1').split(':')
-            authhost = authdata[0]
-            if len(authdata) > 1:
-                authport = authdata[1]
-            else:
-                authport = remoteauth['port']
-    print >> DEBUGSTREAM, "trying %s authentication for %s@%s:%s" % \
-          (remoteauth['proto'], username, authhost, authport)
-    if remoteauth['proto'] == 'imap':
-        M = imaplib.IMAP4(authhost, int(authport))
-        try:
-            M.login(username, password)
-            M.logout()
-            return 1
-        except:
-            print >> DEBUGSTREAM, "imap authentication for %s@%s failed" % \
-                  (username, authhost)
-            return 0
-    elif remoteauth['proto'] == 'imaps':
-        M = imaplib.IMAP4_SSL(authhost, int(authport))
-        try:
-            M.login(username, password)
-            M.logout()
-            return 1
-        except:
-            print >> DEBUGSTREAM, "imaps authentication for %s@%s failed" % \
-                  (username, authhost)
-            return 0
-    elif remoteauth['proto'] in ('pop3', 'apop'):
-        M = poplib.POP3(authhost, int(authport))
-        try:
-            if remoteauth['proto'] == 'pop3':
-                M.user(username)
-                M.pass_(password)
-                M.quit()
-                return 1
-            else:
-                M.apop(username, password)
-                M.quit()
-                return 1
-        except:
-            print >> DEBUGSTREAM, "%s authentication for %s@%s failed" % \
-                  (remoteauth['proto'], username, authhost)
-            return 0
-    elif remoteauth['proto'] == 'ldap':
-        import ldap
-        try:
-            M = ldap.initialize("ldap://%s:%s" % (authhost, authport))
-            M.simple_bind_s(remoteauth['dn'] % username, password)
-            M.unbind_s()
-            return 1
-        except:
-            print >> DEBUGSTREAM, "ldap authentication for %s@%s failed" % \
-                  (username, authhost)
-            return 0
-    # proto not implemented
-    print >> DEBUGSTREAM, "Error: protocol %s not implemented" % \
-            remoteauth['proto']
-    return 0
-
-
-def authfile2dict(authfile):
-    """Iterate over a tmda-ofmipd authentication file, and return a
-    dictionary containing username:password pairs.  Username is
-    returned in lowercase."""
-    authdict = {}
-    fp = file(authfile, 'r')
-    for line in fp:
-        line = line.strip()
-        if line == '':
-            continue
-        else:
-            fields = line.split(':', 1)
-            authdict[fields[0].lower().strip()] = fields[1].strip()
-    fp.close()
-    return authdict
-
-
-def ipauthmap2dict(ipauthmapfile):
-    """Iterate 'ipauthmapfile' (IP1:IP2:port) and return a dictionary
-    containing IP1 -> IP2:port hashes."""
-    ipauthmap = {}
-    try:
-        fp = file(ipauthmapfile, 'r')
-        for line in fp:
-            line = line.strip()
-            if line == '':
-                continue
-            ipdata = line.split(':', 1)
-            ipauthmap[ipdata[0].strip()] = ipdata[1].strip()
-        fp.close()
-    except IOError:
-        pass
-    return ipauthmap
-
-
-def b64_encode(s):
-    """base64 encoding without the trailing newline."""
-    return base64.encodestring(s)[:-1]
-
-
-def b64_decode(s):
-    """base64 decoding."""
-    return base64.decodestring(s)
-
-
-# Classes
-
 # Integration helper class for tlslite.
 #
 # tlslite's SSL handshake function is not synchronous; it may return prior
@@ -1216,10 +611,190 @@
         self.do_ssl_handshake()
 
 
-if opts.ssl or opts.tls:
-    SMTPSession.__bases__ = (TMDATLSAsyncDispatcherMixIn,) + SMTPSession.__bases__
+class SMTPServer(asyncore.dispatcher):
+    """Run an SMTP server daemon - accept new socket connections and
+    process SMTP sessions on each connection."""
+    def __init__(self, localaddr, process_msg_func):
+        self._localaddr = localaddr
+        self._process_msg_func = process_msg_func
+        asyncore.dispatcher.__init__(self)
+        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)
+        print >> DEBUGSTREAM, \
+              'tmda-ofmipd started at %s\n\tListening on %s:%d' % \
+              (Util.make_date(), localaddr[0], localaddr[1])
 
+    def readable(self):
+        if len(asyncore.socket_map) > opts.connections:
+            # too many simultaneous connections
+            return 0
+        else:
+            return 1
 
+    def handle_accept(self):
+        conn = self.accept()[0]
+        SMTPSession(conn, self._process_msg_func)
+
+
+# Utility functions
+
+def warning(msg='', exit=1):
+    delimiter = '*' * 70
+    if msg:
+        msg = Util.wraptext(msg)
+        print >> sys.stderr, '\n', delimiter, '\n', msg, '\n', delimiter, '\n'
+    if exit:
+        sys.exit()
+
+
+def pipecmd(command, *strings):
+    popen2._cleanup()
+    cmd = popen2.Popen3(command, 1, bufsize=-1)
+    cmdout, cmdin, cmderr = cmd.fromchild, cmd.tochild, cmd.childerr
+    if strings:
+        # Write to the tochild file object.
+        for s in strings:
+            cmdin.write(s)
+        cmdin.flush()
+        cmdin.close()
+    # Read from the childerr object; command will block until exit.
+    err = cmderr.read().strip()
+    cmderr.close()
+    # Read from the fromchild object.
+    out = cmdout.read().strip()
+    cmdout.close()
+    # Get exit status from the wait() member function.
+    return cmd.wait()
+
+
+def run_authprog(username, password):
+    """authprog should return 0 for auth ok, and a positive integer in
+    case of a problem."""
+    print >> DEBUGSTREAM, "Trying authprog method"
+    cmd = "/bin/sh -c 'exec %s 3<&0'" % (opts.authprog,)
+    return pipecmd(cmd, '%s\0%s\0' % (username, password))
+
+
+def run_remoteauth(username, password, localip):
+    """Authenticate username/password combination against a remote
+    resource.  Return 1 upon successful authentication, and 0
+    otherwise."""
+    authhost = remoteauth['host']
+    authport = remoteauth['port']
+    if authhost == '0.0.0.0':
+        ipauthmap = ipauthmap2dict(ipauthmapfile)
+        if len(ipauthmap) == 0:
+            authhost = localip
+        else:
+            authdata = ipauthmap.get(localip, '127.0.0.1').split(':')
+            authhost = authdata[0]
+            if len(authdata) > 1:
+                authport = authdata[1]
+            else:
+                authport = remoteauth['port']
+    print >> DEBUGSTREAM, "trying %s authentication for %s@%s:%s" % \
+          (remoteauth['proto'], username, authhost, authport)
+    if remoteauth['proto'] == 'imap':
+        M = imaplib.IMAP4(authhost, int(authport))
+        try:
+            M.login(username, password)
+            M.logout()
+            return 1
+        except:
+            print >> DEBUGSTREAM, "imap authentication for %s@%s failed" % \
+                  (username, authhost)
+            return 0
+    elif remoteauth['proto'] == 'imaps':
+        M = imaplib.IMAP4_SSL(authhost, int(authport))
+        try:
+            M.login(username, password)
+            M.logout()
+            return 1
+        except:
+            print >> DEBUGSTREAM, "imaps authentication for %s@%s failed" % \
+                  (username, authhost)
+            return 0
+    elif remoteauth['proto'] in ('pop3', 'apop'):
+        M = poplib.POP3(authhost, int(authport))
+        try:
+            if remoteauth['proto'] == 'pop3':
+                M.user(username)
+                M.pass_(password)
+                M.quit()
+                return 1
+            else:
+                M.apop(username, password)
+                M.quit()
+                return 1
+        except:
+            print >> DEBUGSTREAM, "%s authentication for %s@%s failed" % \
+                  (remoteauth['proto'], username, authhost)
+            return 0
+    elif remoteauth['proto'] == 'ldap':
+        import ldap
+        try:
+            M = ldap.initialize("ldap://%s:%s" % (authhost, authport))
+            M.simple_bind_s(remoteauth['dn'] % username, password)
+            M.unbind_s()
+            return 1
+        except:
+            print >> DEBUGSTREAM, "ldap authentication for %s@%s failed" % \
+                  (username, authhost)
+            return 0
+    # proto not implemented
+    print >> DEBUGSTREAM, "Error: protocol %s not implemented" % \
+            remoteauth['proto']
+    return 0
+
+
+def authfile2dict(authfile):
+    """Iterate over a tmda-ofmipd authentication file, and return a
+    dictionary containing username:password pairs.  Username is
+    returned in lowercase."""
+    authdict = {}
+    fp = file(authfile, 'r')
+    for line in fp:
+        line = line.strip()
+        if line == '':
+            continue
+        else:
+            fields = line.split(':', 1)
+            authdict[fields[0].lower().strip()] = fields[1].strip()
+    fp.close()
+    return authdict
+
+
+def ipauthmap2dict(ipauthmapfile):
+    """Iterate 'ipauthmapfile' (IP1:IP2:port) and return a dictionary
+    containing IP1 -> IP2:port hashes."""
+    ipauthmap = {}
+    try:
+        fp = file(ipauthmapfile, 'r')
+        for line in fp:
+            line = line.strip()
+            if line == '':
+                continue
+            ipdata = line.split(':', 1)
+            ipauthmap[ipdata[0].strip()] = ipdata[1].strip()
+        fp.close()
+    except IOError:
+        pass
+    return ipauthmap
+
+
+def b64_encode(s):
+    """base64 encoding without the trailing newline."""
+    return base64.encodestring(s)[:-1]
+
+
+def b64_decode(s):
+    """base64 decoding."""
+    return base64.decodestring(s)
+
+
 def process_message_fail(peer, mailfrom, rcpttos, data, auth_username):
     """Debug class which prevents the mail from actually being accepted."""
     raise "Test Exception"
@@ -1336,41 +911,470 @@
     else:
         # no need to fork
         Util.pipecmd(inject_cmd, data)
-            
 
-class SMTPServer(asyncore.dispatcher):
-    """Run an SMTP server daemon - accept new socket connections and
-    process SMTP sessions on each connection."""
-    def __init__(self, localaddr, process_msg_func):
-        self._localaddr = localaddr
-        self._process_msg_func = process_msg_func
-        asyncore.dispatcher.__init__(self)
-        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)
-        print >> DEBUGSTREAM, \
-              'tmda-ofmipd started at %s\n\tListening on %s:%d' % \
-              (Util.make_date(), localaddr[0], localaddr[1])
 
-    def readable(self):
-        if len(asyncore.socket_map) > opts.connections:
-            # too many simultaneous connections
-            return 0
-        else:
-            return 1
-
-    def handle_accept(self):
-        conn = self.accept()[0]
-        SMTPSession(conn, self._process_msg_func)
-
-
 def create_smtp_session_from_stdin(process_msg_func):
     conn = socket.fromfd(0, socket.AF_INET, socket.SOCK_STREAM)
     SMTPSession(conn, process_msg_func)
- 
 
+
+# Main code begins
+
+
+# Constants
+
+remoteauth = { 'proto': None,
+               'host': 'localhost',
+               'port': None,
+               'dn': '',
+               'enable': 0,
+               }
+defaultauthports = { 'imap': 143,
+                     'imaps': 993,
+                     'apop': 110,
+                     'pop3': 110,
+                     'ldap': 389,
+                     # 'pop3s': 995,
+                     }
+
+NEWLINE = '\n'
+EMPTYSTRING = ''
+
+
+# Runtime global variables
+
+program = sys.argv[0]
+
+__version__ = Version.TMDA
+
+FQDN = socket.getfqdn()
+if FQDN == 'localhost':
+    FQDN = socket.gethostname()
+
+if os.getuid() == 0:
+    running_as_root = True
+else:
+    running_as_root = False
+
+
+# Option parsing
+
+opt_desc = \
+"""An authenticated ofmip proxy for TMDA that allows you to 'tag' your
+mail client's outgoing mail through SMTP.  For more information,
+including setup and usage instructions, see
+http://wiki.tmda.net/TmdaOfmipdHowto""" 
+
+parser = OptionParser(description=opt_desc, version=Version.TMDA)
+
+parser.add_option("-V", 
+                  action="store_true", default=False, dest="full_version",
+                  help="show full TMDA version information and exit.")
+
+# option groups
+gengroup = OptionGroup(parser, "General")
+congroup = OptionGroup(parser, "Connection")
+authgroup = OptionGroup(parser, "Authentication")
+virtgroup = OptionGroup(parser, "Virtual Domains")
+    
+# general
+gengroup.add_option("-d", "--debug",
+                    action="store_true", default=False, dest="debug",
+                    help="Turn on debugging prints.")
+
+gengroup.add_option("-L", "--log",
+                    action="store_true", default=False, dest="log",
+                    help= \
+"""Turn on logging prints.
+This option logs everything that -d logs, except for the raw SMTP protocol
+data. Hence, it is useful if you want to leave logging enabled permanently,
+but don't want your logs bloated with AUTH data and/or the content of large
+attachments.""")
+
+gengroup.add_option("-b", "--background",
+                    action="store_false", dest="foreground",
+                    help="Detach and run in the background (default).")
+
+gengroup.add_option("-f", "--foreground",
+                    action="store_true", default=False, dest="foreground",
+                    help="Don't detach; run in the foreground.")
+
+gengroup.add_option("-u", "--username",
+                    dest="username",
+                    help= \
+"""The username that this program should run under.  The default is to
+run as the user who starts the program unless that is root, in which
+case an attempt to seteuid user 'tofmipd' will be made.  Use this
+option to override these defaults.""")
+
+gengroup.add_option("-c", "--configdir",
+                    metavar="DIR", dest="configdir",
+                    help= \
+"""DIR is the base directory to search for the authenticated user's TMDA
+configuration file in.  This might be useful if you wish to maintain
+TMDA files outside the user's home directory.
+'username/config' will be appended to form the path; e.g, `-c
+/var/tmda' will have tmda-ofmipd search for `/var/tmda/bobby/config'.
+If this option is not used, `~user/.tmda/config' will be assumed, but
+see the --vhome-script option for qmail virtual domain users.""")
+
+# connection
+congroup.add_option("-p", "--proxyport",
+                    default="%s:%s" % (FQDN, 8025), metavar="HOST:PORT", 
+                    dest="proxyport", help= \
+"""The HOST:PORT to listen for incoming connections on.  The default is
+FQDN:8025 (i.e, port 8025 on the fully qualified domain name for the
+local host).  Use '0.0.0.0:PORT' to listen on all available
+interfaces.""")
+
+congroup.add_option("-C", "--connections",
+                    type="int", default="20", metavar="NUM", dest="connections",
+                    help= \
+"""Do not handle more than NUM simultaneous connections. If there are NUM
+active connections, defer acceptance of new connections until one
+finishes. NUM must be a positive integer. Default: 20""")
+
+congroup.add_option("-1", "--one-session",
+                    action="store_true", default=False, dest="one_session",
+                    help= \
+"""Don't bind to a port and accept new connections; Process a single SMTP
+session on stdin (used both for input & output).  This is useful when
+started from tcpserver or stunnel.""")
+
+congroup.add_option("-P", "--pure-proxy",
+                    action="store_true", default=False, dest="pure_proxy",
+                    help= \
+"""Proxy the message straight through to the mail transport system
+unaltered if the user's TMDA config file is missing.  The
+/usr/sbin/sendmail program on the system is used to inject the
+message.  You can override this by setting $TMDA_SENDMAIL_PROGRAM in
+the environment.  This option might be useful when serving a mixed
+environment of TMDA and non-TMDA users.""")
+
+congroup.add_option("-t", "--throttle-script",
+                    metavar="/PATH/TO/SCRIPT", dest="throttlescript",
+                    help= \
+"""Full pathname of a script which can meter how much mail any user
+sends.  The script is passed a login name whenever a user tries to
+send mail.  If the script returns a 0, the message is allowed.  For
+any other value, the message is rejected.""")
+
+congroup.add_option("", "--ssl",
+                    action="store_true", default=False, dest="ssl",
+                    help= \
+"""Enable SSL encryption. This mode immediately initiates the SSL/TLS
+protocol as soon as a connection is made. This mode is not support
+for the STARTTLS command. This configuration is typically run on
+port 465 (smtps).""")
+
+congroup.add_option("", "--tls",
+                    type="choice", default=None, dest="tls",
+                    choices=['optional', 'on'],
+                    help= \
+"""Enable TLS mode. Valid options are optional and on. With this option
+enabled, the STARTTLS SMTP command may be used to upgrade the plain-text
+connection to SSL/TLS. In 'optional' mode, AUTH is allowed either before
+or after STARTTLS. In 'on' mode, clients are forced to STARTTLS prior to
+AUTH, to ensure that plain-text AUTH commands are protected. This
+configuration is typically run on port 587 (submission).""")
+
+congroup.add_option("", "--ssl-cert",
+                    metavar="/PATH/TO/FILE", default=None, dest="ssl_cert",
+                    help= \
+"""Location of the SSL/TLS certificate key file.""")
+
+congroup.add_option("", "--ssl-key",
+                    metavar="/PATH/TO/FILE", default=None, dest="ssl_key",
+                    help= \
+"""Location of the SSL/TLS private key file.""")
+
+# authentication
+authgroup.add_option("-R", "--remoteauth",
+                     metavar="PROTO://HOST[:PORT][/DN]", dest="remoteauth",
+                     help= \
+"""Protocol and host to check username and password. PROTO can be one of
+the following: 'imap' (IMAP4 server), 'imaps' (IMAP4 server over SSL),
+'pop3' (POP3 server), 'apop' (POP3 server with APOP authentication),
+'ldap' (LDAP server).  Optional :PORT defaults to the standard port for
+the specified protocol (143 for imap, 993 for imaps, 110 for
+pop3/apop, and 389 for ldap).  /DN is mandatory for ldap and should
+contain a '%%s' identifying the username. Examples: '-R
+imaps://myimapserver.net', '-R pop3://mypopserver.net:2110', '-R
+ldap://example.com/cn=%%s,dc=host,dc=com'""")
+
+authgroup.add_option("-A", "--authprog",
+                     metavar="PROGRAM", dest="authprog",
+                     help= \
+"""A checkpassword compatible command used to check username/password.
+Examples: '-A "/usr/sbin/checkpassword-pam -s id -- /bin/true"',
+'-A "/usr/local/vpopmail/bin/vchkpw /usr/bin/true"'.
+The program must be able to receive the username/password pair on
+descriptor 3 and in the following format: `username\\0password\\0'
+Any program claiming to be checkpassword-compatible should be able to
+do this.  If you can tell the program to accept input on another
+descriptor, such as stdin, don't.  It won't work, because TMDA follows
+the standard (http://cr.yp.to/checkpwd/interface.html) exactly.
+Also, checkpassword-type programs expect to find the name of another
+program to run on their command line.  For tmda-ofmipd's purpose,
+/bin/true is perfectly fine.
+Note the position of the quotes in the Examples, which cause the the
+whole string following the -A to be passed as a single argument.""")
+
+authgroup.add_option("-a", "--authfile",
+                     metavar="FILE", dest="authfile",
+                     help= \
+"""Path to the file holding authentication information for this proxy.
+Default location is /etc/tofmipd if running as root/tofmipd, otherwise
+~user/.tmda/tofmipd.  Use this option to override these defaults.""")
+
+authgroup.add_option("-F", "--fallback",
+                     action="store_true", default=False, dest="fallback",
+                     help= \
+"""When used with -R or -A, fallback to authenticate against the authfile
+if remote authentication fails.  Note: this flag has no effect on -R
+to -A fallback. If you specify both -R and -A methods, then authprog
+will be tried after remoteauth has failed.""")
+
+# virtual domains
+virtgroup.add_option("-S", "--vhome-script",
+                     metavar="/PATH/TO/SCRIPT", dest="vhomescript",
+                     help= \
+"""Full pathname of a script that prints a virtual email user's home
+directory on standard output.  tmda-ofmipd will read that and use it
+to build the path to the user's config file instead of '~user/.tmda'.
+The script must take two arguments, the user name and the domain, on
+its command line.  This option is for use only with the VPopMail and
+VMailMgr add-ons to qmail.  See the contrib directory for sample
+scripts.""")
+
+virtgroup.add_option("-v", "--vdomains-path",
+                     default="/var/qmail/control/virtualdomains", 
+                     metavar="/PATH/TO/FILE", dest="vdomainspath",
+                     help= \
+"""Full pathname to qmail's virtualdomains file.  The default is
+/var/qmail/control/virtualdomains.  This is also tmda-ofmipd's
+default, so you normally won't need to set this parameter.  If you
+have installed qmail somewhere other than /var/qmail, you will need to
+set this so tmda-ofmipd can find the virtualdomains file.  NOTE: This
+is only used when you have a qmail installation with virtual domains
+using the VMailMgr add-on.  It implies that you will also set the
+'--vhome-script' option above.""")
+
+for g in (gengroup, congroup, authgroup, virtgroup):
+    parser.add_option_group(g)
+
+(opts, args) = parser.parse_args()
+
+if opts.full_version:
+    print Version.ALL
+    sys.exit()
+if opts.vhomescript and opts.configdir:
+    parser.error("options '--vhome-script' and '--configdir' are incompatible!")
+if opts.debug or opts.log:
+    DEBUGSTREAM = sys.stderr
+else:
+    DEBUGSTREAM = Devnull()
+
+if opts.remoteauth:
+    # arg is like: imap://host:port
+    autharg = opts.remoteauth
+    try:
+        authproto, autharg = autharg.split('://', 1)
+    except ValueError:
+        authproto, autharg = autharg, None
+    if authproto not in defaultauthports.keys():
+        raise ValueError, 'Protocol not supported: ' + authproto + \
+            '\nPlease pick one of ' + repr(defaultauthports.keys())
+    remoteauth['proto'] = authproto
+    remoteauth['port'] = defaultauthports[authproto]
+    if autharg:
+        try:
+            autharg, dn = autharg.split('/', 1)
+            remoteauth['dn'] = dn
+        except ValueError:
+            dn = ''
+        try:
+            authhost, authport = autharg.split(':', 1)
+        except ValueError:
+            authhost = autharg
+            authport = defaultauthports[authproto]
+        if authhost:
+            remoteauth['host'] = authhost
+        if authport:
+            remoteauth['port'] = authport
+    print >> DEBUGSTREAM, "auth method: %s://%s:%s/%s" % \
+        (remoteauth['proto'], remoteauth['host'],
+         remoteauth['port'], remoteauth['dn'])
+    remoteauth['enable'] = 1
+
+if running_as_root:
+    if not opts.username:
+        opts.username = 'tofmipd'
+    if not opts.authfile:
+        opts.authfile = '/etc/tofmipd'
+    ipauthmapfile = '/etc/ipauthmap'
+else:
+    tmda_path = os.path.join(os.path.expanduser('~'), '.tmda')
+    ipauthmapfile = os.path.join(tmda_path, 'ipauthmap')
+    if not opts.authfile:
+        opts.authfile = os.path.join(tmda_path, 'tofmipd')
+
+# provide disclaimer if running as root
+if running_as_root:
+    msg = 'WARNING: The security implications and risks of running ' + \
+          program + ' in "seteuid" mode have not been fully evaluated.  ' + \
+          'If you are uncomfortable with this, quit now and instead run ' + \
+          program + ' under your non-privileged TMDA user account.'
+    warning(msg, exit=0)
+
+if remoteauth['proto'] == 'ldap':
+    try:
+        import ldap
+    except ImportError:
+        raise ImportError, \
+              'python-ldap (http://python-ldap.sf.net/) required.'
+    if remoteauth['dn'] == '':
+        print >> DEBUGSTREAM, "Error: Missing ldap dn\n"
+        raise ValueError
+    try:
+        remoteauth['dn'].index('%s')
+    except:
+        print >> DEBUGSTREAM, "Error: Invalid ldap dn\n"
+        raise ValueError
+
+if opts.ssl or opts.tls:
+    if opts.ssl and opts.tls:
+        raise ValueError, 'Can\'t do SSL and TLS at the same time'
+
+    try:
+        from tlslite.api import *
+        from tlslite.TLSConnection import TLSConnection
+        from tlslite.integration.AsyncStateMachine import AsyncStateMachine
+    except ImportError:
+        raise ImportError, \
+              'tlslite (http://trevp.net/tlslite/) required.'
+
+    if (not opts.ssl_cert) or (not opts.ssl_key):
+        raise ValueError, \
+            '--ssl-cert and --ssl-key are required when using --ssl or --tls'
+
+    fhc = file(os.path.expanduser(opts.ssl_cert), 'r')
+    datac = fhc.read()
+    fhc.close()
+    x509 = X509()
+    x509.parse(datac)
+    opts.ssl_cert_value = X509CertChain([x509])
+
+    fhk = file(os.path.expanduser(opts.ssl_key), 'r')
+    datak = fhk.read()
+    fhk.close()
+    opts.ssl_key_value = parsePEMKey(datak, private=True)
+
+    class TMDATLSAsyncDispatcherMixIn(AsyncStateMachine):
+        """A custom version of tlslite's TLSAsyncDispatcherMixIn.
+        This version:
+        * Requires siblingClass to be specified explicitly, to ease use with
+          inheritance.
+        * Allows the mixin to remain dormant until activated, which allows
+          implementation of deferred SSL startup, as required for the STARTTLS
+          SMTP command.
+        """
+
+        def __init__(self, sock, siblingClass):
+            AsyncStateMachine.__init__(self)
+
+            self.siblingClass = siblingClass
+            self._active = False
+
+            self.tlsConnection = TLSConnection(sock)
+
+        def tlsMixinSetActive(self):
+            self._active = True
+
+        def readable(self):
+            if self._active:
+                result = self.wantsReadEvent()
+                if result != None:
+                    return result
+                return self.siblingClass.readable(self)
+            else:
+                return self.siblingClass.readable(self)
+
+        def writable(self):
+            if self._active:
+                result = self.wantsWriteEvent()
+                if result != None:
+                    return result
+                return self.siblingClass.readable(self)
+            else:
+                return self.siblingClass.writable(self)
+
+        def handle_read(self):
+            if self._active:
+                self.inReadEvent()
+            else:
+                self.siblingClass.handle_read(self)
+
+        def handle_write(self):
+            if self._active:
+                self.inWriteEvent()
+            else:
+                self.siblingClass.handle_write(self)
+
+        def outConnectEvent(self):
+            if not self._active:
+                raise "Internal state confusion"
+            self.siblingClass.handle_connect(self)
+
+        def outCloseEvent(self):
+            if not self._active:
+                raise "Internal state confusion"
+            asyncore.dispatcher.close(self)
+
+        def outReadEvent(self, readBuffer):
+            if not self._active:
+                raise "Internal state confusion"
+            if readBuffer == '':
+                self.outCloseEvent()
+            else:
+                self.readBuffer = readBuffer
+                self.siblingClass.handle_read(self)
+
+        def outWriteEvent(self):
+            if not self._active:
+                raise "Internal state confusion"
+            self.siblingClass.handle_write(self)
+
+        def recv(self, bufferSize=16384):
+            if self._active:
+                if bufferSize < 16384 or self.readBuffer == None:
+                    raise AssertionError()
+                returnValue = self.readBuffer
+                self.readBuffer = None
+                return returnValue
+            else:
+                return self.siblingClass.recv(self, bufferSize)
+
+        def send(self, writeBuffer):
+            if self._active:
+                self.setWriteOp(writeBuffer)
+                return len(writeBuffer)
+            else:
+                return self.siblingClass.send(self, writeBuffer)
+
+        def close(self):
+            if self._active:
+                if hasattr(self, "tlsConnection"):
+                    self.setCloseOp()
+                else:
+                    asyncore.dispatcher.close(self)
+            else:
+                return self.siblingClass.close(self)
+
+
+    SMTPSession.__bases__ = (TMDATLSAsyncDispatcherMixIn,) + SMTPSession.__bases__
+
+
 def main():
     # check permissions of authfile if using only remote
     # authentication.


This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site.