#!/usr/bin/env python
#
# Copyright (C) 2001,2002 Jason R. Mastaler <jason@mastaler.com>
#
# This file is part of TMDA.
#
# TMDA is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.  A copy of this license should
# be included in the file COPYING.
#
# TMDA is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
# for more details.
#
# You should have received a copy of the GNU General Public License
# along with TMDA; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

# Based on code from Python's (undocumented) smtpd module
# Copyright (C) 2001,2002 Python Software Foundation.

"""An authenticated ofmip proxy for TMDA.  Tag your outgoing mail through SMTP.

See <URL:http://tmda.net/tmda-ofmipd.html> for complete setup and
usage information.

Usage: %(program)s [OPTIONS]

OPTIONS:
    -h
    --help
        Print this message and exit.

    -V
    --version
        Print TMDA version information and exit.

    -d
    --daemon
        Daemon mode.

    -D
    --debug
        Turn on debugging prints.

    -u <username>
    --username <username>
        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.

    -H <host:port>
    --hostport <host:port>
        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).

    -R proto[://host[:port]]
    --remoteauth proto[://host[:port]][/dn]
        Host to connect to to check username and password.
        - Allowed protocols are:
            imap, imaps, apop, pop3, ldap
        - Host defaults to localhost
        - Port defaults to default port for the protocol
        - dn is manditory for ldap and must contain a '%%(user)s'
          identifying the username
        Examples: -r imap
                  -r imaps://myimapserver.net
                  -r pop3://mypopserver.net:2110
                  -r ldap://host.com/cn=%%(user)s,dc=host,dc=com

    -A <program>
    --authprog <program>
        Specify checkpassword-style authentication
        - Must conform exactly to the checkpassword stardard:
            http://cr.yp.to/checkpwd/interface.html
        - Any program that returns '0' (true) is acceptable as the command
          run by the checkpassword program upon successful authentication.
        - If "program" requires commandline switches, you must supply the
          command to be run upon successful authentication.
          If "program" does not, the default program (/usr)/bin/true is
          automatically appended if not specified.
        Examples: -p "/usr/sbin/checkpassword-pam -s id -- /bin/true"
                  -p /usr/local/vpopmair/vchkpw
                     (/usr/bin/true or /bin/true is automatically used) 

    --authfile <file>
       Specify a different authentication file than the default.
       - This file is expected to have a single username:password pair on each
         line.  The password may be unix crypt encrypted.
       - The file mode must be 400 or 600 if it contains cleartext passwords.
       - The default is "/.tmda/tmdauth or /etc/tmdauth, whichever is found
         first.

    -l <path>
    --virtual-lookup <path>
       Specify a path to virtual user lookup script.
       - The lookup script should take user login as a parameter and return
         <homedir>\\n<UID>\\n<GID>
"""

import getopt
import os
import socket
import sys

try:
    import paths
except ImportError:
    # Prepend /usr/lib/python2.x/site-packages/TMDA/pythonlib
    sitedir = os.path.join(sys.prefix, 'lib', 'python'+sys.version[:3],
                           'site-packages', 'TMDA', 'pythonlib')
    sys.path.insert(0, sitedir)

from TMDA import Util
from TMDA import Auth
from TMDA import Errors
from TMDA import Version

# Some defaults
FQDN = socket.getfqdn()
defaultport = 8765
hostport = '%s:%s' % (FQDN, defaultport)
program = sys.argv[0]
daemon_mode = None
quiet = 0
debug = 0
VLookup = None
authobj = Auth.Auth()

def usage(code, msg=''):
    print __doc__ % globals()
    if msg:
        print msg
    sys.exit(code)

try:
    opts, args = getopt.getopt(sys.argv[1:], 'H:u:R:A:a:l:dDqVh',
                                            ['hostport=',
                                             'username=',
                                             'authfile=',
                                             'remoteauth=',
                                             'authprog=',
                                             'virtual-lookup=',
                                             'daemon',
                                             'debug',
                                             'quiet',
                                             'version',
                                             'help',
                                            ])
except getopt.error, msg:
    usage(1, msg)

for opt, arg in opts:
    if opt in ('-h', '--help'):
        usage(0)
    if opt == '-V':
        print Version.ALL
        sys.exit()
    if opt == '--version':
        print Version.TMDA
        sys.exit()
    elif opt in ('-D', '--debug'):
        debug = 1
        authobj.DEBUGSTREAM = sys.stderr
    elif opt in ('-q', '--quiet'):
        quiet = 1
    elif opt in ('-d', '--daemon'):
        daemon_mode = 1
    elif opt in ('-H', '--hostport'):
        hostport = arg
    elif opt in ('-u', '--username'):
        username = arg
    elif opt in ('-R', '--remoteauth'):
        try:
            authobj.init_remote( arg )
        except ValueError, err:
            print >> sys.stderr, "Fatal: %s" % err
            sys.exit(1)
    elif opt in ('-A', '--authprog'):
        try:
            authobj.init_checkpw( arg )
        except ValueError, err:
            print >> sys.stderr, "Fatal: %s" % err
            sys.exit(1)
    elif opt in ('-a', '--authfile'):
        try:
            authobj.init_file( arg )
        except ValueError, err:
            print >> sys.stderr, "Fatal: %s" % err
            sys.exit(1)
    elif opt in ('-l', '--virtual-lookup'):
        VLookup = arg

authobj.security_disclaimer()

if daemon_mode and quiet:
    print >> sys.stderr, "Warning: cannot be quiet in daemon mode."
    quiet = 0

import asynchat
import asyncore
import base64
import hmac
import md5
import popen2
import random
import time


# This can be removed eventually.
warning = 'NOTE: This is early alpha software, and not yet meant for general ' + \
          'use. If you decide to use it anyway, please send your comments to ' + \
          'the tmda-workers@tmda.net mailing list, and not tmda-users.'
print Util.wraptext(warning), '\n'

__version__ = Version.TMDA


import readline
from cmd import Cmd

TMDAShellExit = ''

class TMDAShell(Cmd):
    ProtoVer = '0.2'
    def __init__(self, prompt = '+',
                       stdin = sys.stdin,
                       stdout = sys.stdout,
                       stderr = sys.stderr):
        sys.stdin = stdin
        sys.stdout = stdout
        sys.stderr = stderr
        if quiet:
            self.prompt = ''
        else:
            self.prompt = prompt
        self.authenticated = not authobj.running_as_root
        if debug:
            # patch ourself
            self.precmd = self._precmd
        Cmd.__init__(self)

    def outputData(self, data):
        """Output data."""
        if type(data) == str:
            data = data.split('\n')
        d = []
        for line in data:
            for subline in line.split('\n'):
                d.append(subline)
        if quiet:
            sys.stdout.write('\n'.join(d) + '\n')
        else:
            sys.stdout.write(' ' + '\n '.join(d) + '\n')
        if debug:
            self.log('<' + '\n<'.join(d) + '\n')

    def parseArgs(self, args, shortopts=[], longopts=[]):
        """Parse command line arguments."""
        pargs = []
        import getopt
        for arg in args.split(' '):
            if not arg:
                continue
            pargs.append(arg)
        try:
            opts, args = getopt.getopt(pargs,
                                        shortopts,
                                        longopts)
        except getopt.error, msg:
            self.error(0, "ARG", str(msg))
            ## FIXME: raise an exception here, and
            ##        catch it in the callers
            return ([], [])
        return (opts, args)

        return pargs

    def mainLoop(self):
        greetings = ''
        if not quiet:
            greetings = 'TMDA Manager v%s\nProtocol v%s' % \
                (Version.TMDA, self.ProtoVer)
        try:
            self.cmdloop(greetings)
        except KeyboardInterrupt, msg:
            raise KeyboardInterrupt, msg

    def _precmd(self, line):
        """Log commands in debug mode."""
        self.log('>'+line)
        return line

    def log(self, *strings):
        """Log strings to stderr."""
        for s in strings:
            sys.stderr.write(str(s))
        sys.stderr.write('\n')

    def error(self, ret, code, str):
        """Output an error message."""
        line = "-%s: %s" % (code, str.replace('\n', ' '))
        print line
        if debug:
            self.log('!' + line)
        return ret

    ## Cmd functions
    def do_help(self, args):
        self.outputData("Write me!")
        if args:
            Cmd.do_help(self, args)

    def do_nop(self, args):
        """No-OPeration, do nothing but echo args."""
        self.outputData(args)

    def help_auth(self):
        print "Write me!"

    def do_auth(self, args):
        """Authenticate."""
        if self.authenticated:
            return self.error(0, "AUTH", "Already authenticated.")
        (opts, args) = self.parseArgs(args)
        try:
            self.userid = args[0]
            self.password = args[1]
        except IndexError:
            self.userid = None
            self.password = None
            return self.error(0, "ARG", "Missing argument.")
        try:
            if authobj.authenticate_plain(self.userid, self.password):
                self.authenticated = 1
                try:
                    home, uid, gid = Util.getuserparams(self.userid, VLookup)
                    os.environ['HOME'] = home
                    os.seteuid(uid)
                    os.setegid(gid)
                except KeyError:
                    self.authenticated = 0
                    return self.error(0, "AUTH", "Auth succeeded, but no user %s found in system." % self.userid )
            else:
                self.authenticated = 0
                return self.error(0, "AUTH", "Invalid credentials.")
        except Errors.AuthError, ex:
            self.error(0, "AUTH", ex.__repr__() )
#        What is/was this for???
#        if Auth.auth_fork(self.userid):
#            sys.exit(0)

        return 0

    def help_checkaddress(self):
        print "Write me!"

    def do_checkaddress(self, args):
        """Check a tagged address."""
        if not self.authenticated:
            return self.error(0, "AUTH", "Please authenticate first.")
        (opts, args) = self.parseArgs(args, 'l', [ 'localtime' ] )

        from TMDA import Address

        try:
            opts[0][0]
            localtime = 1
        except IndexError:
            localtime = None
        try:
            address = args[0]
        except IndexError:
            return self.error(0, "ARG", "Missing address to check.")

        try:
            sender = args[1]
        except IndexError:
            sender = None

        try:
            addr = Address.Factory(address)
            if type(addr) == Address.Address:
                raise
            addr.verify(sender)
            self.outputData("STATUS: VALID")
            try:
                self.outputData("EXPIRES: %s" % addr.timestamp(1, localtime))
            except AttributeError:
                # this is not a dated address
                pass
        except Address.AddressError, msg:
            self.outputData("STATUS:" + str(msg))
        except:
            self.outputData("STATUS: couldn't check the address")


    def help_address(self):
        print "Write me!"

    def do_address(self, args):
        """Generate a tagged address."""
        if not self.authenticated:
            return self.error(0, "AUTH", "Please authenticate first.")
        (opts, args)  = self.parseArgs(args, 'd:k:s:b',
                                            [ 'dated=',
                                              'keyword=',
                                              'sender=',
                                              'base',
                                            ] )
        from TMDA import Address
        tag = 'dated'
        date = '5d'
        param = None
        for (opt, arg) in opts:
            if opt in ('-b', '--base'):
                self.outputData(Address.Address().base())
                return 0
            elif opt in ('-d', '--dated'):
                tag = 'dated'
                param = arg
            elif opt in ('-k', '--keyword'):
                tag = 'keyword'
                param = arg
            elif opt in ('-s', '--sender'):
                tag = 'sender'
                param = arg
            elif opt in ('-a', '--address'):
                address = arg

        try:
            address = args[0]
        except IndexError:
            address = None

        if param is None:
            if tag != 'dated':
                return self.error(0, "ARG", "Missing or malformed parameter.")
            else:
                param = date
        try:
            tagged_address = Address.Factory(tag = tag).create(address, param).address
        except ValueError, msg:
            return self.error(0, "UNK", "Unknown error (Address.Factory).")
        if not tagged_address:
            return self.error(0, "UNK", "Unknown error (Address.Factory).")
        self.outputData(tagged_address)
        return 0

    def help_pending(self):
        self.outputData([
            "pending --queue",
            "pending [--all|--confirmed|--released|--delivered|--only]"+\
                " [--descending] [--range N[:M]]",
            "pending [--all|--confirmed|--released|--delivered|--only]"+\
                " --number",
            ])

    def do_pending(self, args):
        """List pending message ids."""
        if not self.authenticated:
            return self.error(0, "AUTH", "Please authenticate first.")
        (opts, args) = self.parseArgs(args, 'qacrdopDns:u:R:',
                                            [ 'queue',
                                              'all', 
                                              'any', 
                                              'confirmed',
                                              'released',
                                              'delivered',
                                              'only',
                                              'pending',
                                              'descending',
                                              'number',
                                              'since=',
                                              'until=',
                                              'range=',
                                            ] )
        descending = None
        descending = 1
        interactive = 0
        plist = 'only'
        number = 0
        since = None
        until = None
        range = None
        for (opt, arg) in opts:
            if opt in ('-D', '--descending'):
                descending = 1 
            elif opt in ('-q', '--queue'):
                interactive = 1
            elif opt in ('-a', '--all', '--any'):
                plist = 'all'
            elif opt in ('-c', '--confirmed'):
                plist = 'confirmed'
            elif opt in ('-r', '--released'):
                plist = 'released'
            elif opt in ('-d', '--delivered'):
                plist = 'delivered'
            elif opt in ('-o', '--only', '-p', '--pending'):
                plist = 'only'
            elif opt in ('-n', '--number'):
                number = 1
            elif opt in ('-s', '--since'):
                since = arg
            elif opt in ('-u', '--until'):
                until = arg
            elif opt in ('-R', '--range'):
                try:
                    min, max = arg.split(':', 1)
                    try:
                        min = int(min)
                    except ValueError:
                        min = 0
                    try:
                        max = int(max)
                    except ValueError:
                        max = 0
                    range = (min, max)
                except ValueError:
                    # only one value given
                    try:
                        min = int(arg)
                        range = (min, min+1)
                    except ValueError:
                        # ignore bogus --range flag
                        pass
                    
        from TMDA import Pending
        if interactive:
            Pending.InteractiveQueue().initQueue().mainLoop()
            return 0
        ids = ''
        if plist == 'all':
            ids = Pending.Queue(descending=descending).initQueue().listIds()
        elif plist == 'confirmed':
            ids = Pending.Queue(descending=descending).initQueue().listConfirmedIds()
        elif plist == 'released':
            ids = Pending.Queue(descending=descending).initQueue().listReleasedIds()
        elif plist == 'delivered':
            ids = Pending.Queue(descending=descending).initQueue().listDeliveredIds()
        elif plist == 'only':
            ids = Pending.Queue(descending=descending).initQueue().listPendingIds()

        if since:
            ids = [ id for id in ids if id > since ]

        if until:
            ids = [ id for id in ids if id <= until ]

        if number:
            self.outputData(str(len(ids)))
#            self.outputData((str(len(ids)),))

        if ids and not number:
            if range:
                if range[1]:
                    self.outputData(ids[range[0]:range[1]])
                else:
                    self.outputData(ids[range[0]:])
            else:
                self.outputData(ids)
        return 0

    def help_message(self):
        print "Write me!"

    def do_message(self, args):
        """Do action on message."""
        if not self.authenticated:
            return self.error(0, "AUTH", "Please authenticate first.")
        (opts, args)  = self.parseArgs(args, 'tsSrdwbD:',
                                             [ 'terse',
                                               'summary',
                                               'show',
                                               'release',
                                               'delete',
                                               'whitelist',
                                               'blacklist',
                                               'date=',
                                             ] )
        (opt, arg) = ('-t', '')
        date = 0
        for (o, a) in opts:
            if o == '-D' or o == '--date':
                if a:
                    date = 1
                break
            else:
                ## FIXME: one action allowed for now
                (opt, arg) = (o, a)
        from TMDA import Pending
        from TMDA import Errors
        if not len(args):
            return self.error(0, 'ARG', 'Missing message id.')
        try:
            if opt == '-t' or opt == '--terse':
                for l in Pending.Message(args[0]).initMessage().terse(tsv=0, date=date):
                    self.outputData(l.replace('\n', r'\n'))
            elif opt == '-s' or opt == '--summary':
                self.outputData(Pending.Message(args[0]).initMessage().summary())
            elif opt == '-S' or opt == '--show':
                self.outputData(Pending.Message(args[0]).initMessage().show())
            elif opt == '-r' or opt == '--release':
                Pending.Message(args[0]).initMessage().release()
            elif opt == '-d' or opt == '--delete':
                Pending.Message(args[0]).initMessage().delete()
            elif opt == '-w' or opt == '--whitelist':
                Pending.Message(args[0]).initMessage().whitelist()
            elif opt == '-b' or opt == '--blacklist':
                Pending.Message(args[0]).initMessage().blacklist()
        except Errors.MessageError:
            return self.error(0, 'ARG', 'Unknown message id %s.' %
                                        args[0])

        return 0

    def help_get(self):
        print "Write me!"
        
    def do_get(self, args):
        """Get a Default module variable."""
        if not self.authenticated:
            return self.error(0, "AUTH", "Please authenticate first.")
        (opts, args)  = self.parseArgs(args)
        
        from TMDA import Defaults
        res = []
        if '*' in args:
            args = vars(Defaults).keys()
            args.sort()
        for a in args:
            try:
                val = getattr(Defaults, a)
                if type(val) in (int, str, list) and \
                    a[0] >= 'A' and a[0] <= 'Z' and \
                    a != 'CRYPT_KEY':
                    res.append('%s=%s' % (a, repr(val)))
            except AttributeError, msg:
                pass
        self.outputData(res)
        return 0
            
    def help_getfile(self):
        print "Write me!"
        
    def do_getfile(self, args):
        """Get a TMDA file (except CRYPT_KEY_FILE)."""
        if not self.authenticated:
            return self.error(0, "AUTH", "Please authenticate first.")
        (opts, args)  = self.parseArgs(args)
        
        import os.path
        from TMDA import Defaults
        path = os.path.expanduser(args[0])

        # Check if the client wants to peep into our crypt_key file
        if os.path.expanduser(Defaults.CRYPT_KEY_FILE) == path:
            return self.error(0, 'ACC', "You can't do that.")

        # Check if the client wants to fetch non-TMDA files
        if os.path.commonprefix((Defaults.DATADIR,
                                path)) != Defaults.DATADIR:
            return self.error(0, 'ACC', "You can't do that.")

        # All seems OK, let's serve the request
        try:
            file = open(path)
            self.outputData([ line[:-1] for line in file.readlines() ])
            file.close()
        except IOError:
            return self.error(0, 'RES', "Resource not found.")

        return 0



    def do_exit(self, args):
        """Exit the shell."""
        print '.bye.'
        return 1

    # aliases on exit
    do_quit = do_exit
    do_EOF = do_exit

def connection(hostport):
    stdin = sys.stdin
    stdout = sys.stdout
    import socket
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    try:
        (host, port) = hostport.split(':',2)
    except ValueError:
        (host, port) = (hostport, defaultport)
    if host == 'any':
        host = '0.0.0.0'
    try:
        port = int(port)
    except ValueError:
        raise IOError, "%s is not a valid port" % port
    sock.bind((host, int(port)))
    print 'Listening on %s:%s' % (host, port)
    sock.listen(5)
    try:
        while 1:
            conn, addr = sock.accept()
            pid = os.fork()
            if not pid:
                sin = conn.makefile('r', 0)
                sout = conn.makefile('w', 0)
                print 'accepted connection from %s:%s' % addr
                try:
                    TMDAShell('+', sin, sout, stdout).mainLoop()
                    sys.stdout = stdout
                except IOError, msg:
                    sys.stdout = stdout
                    print msg
                sys.stdin = stdin
                print 'closing connection from %s:%s' % addr
                sin.close()
                sout.close()
                break
            conn.close()
    except KeyboardInterrupt:
        print "Keyboard interrupt: exiting..."
        sock.close()


def main():
    if daemon_mode:
        connection(hostport)
    else:
        TMDAShell().mainLoop()

# This is the end my friend.
if __name__ == '__main__':
    main()
