#!/usr/bin/python
#
# Successful login will store user and save password base64 encoded
# User defaults from database or defaults from config file
#
#

import getopt
import sys
import os
import stat
import logging
import ConfigParser
import fileinput
import getpass
import string
import imaplib
import base64
from Crypto.Cipher import PKCS1_v1_5
from Crypto.PublicKey import RSA

versionstring='benno_imapauth version 2.2.3'


class User:
    def __init__(self,imapuser,imaphost=''):
        if imaphost:
            self.id=imapuser+'@'+imaphost
        else:
            self.id=''
        self.imapuser=imapuser
        self.imaphost=imaphost
        self.name=''
        self.password=''
        self.archive=''
        self.role=''
        self.addresses=[]
        self.status=0


    def load(self,config):
        """Load user data from database"""
        if self.id:
            self.loadUserData(config,self.id)
            self.loadImapuserData(config,self.id)
            logging.debug("Load userdata by id: %s" % self.id)
        elif self.imaphost:
            self.loadImapuserImaphost(config.self.imapuser,self.imaphost)
            self.loadUserData(config,self.id)
            logging.debug("Load userdata by imaphost: %s => %s" % (self.imapuser, self.imaphost))
        else:
            self.loadImapuser(config,self.imapuser)
            self.loadUserData(config,self.id)
            logging.debug("Load userdata by imapuser: %s => %s" % (self.imapuser, self.imaphost))
        self.loadAddress(config)
        # set attributes not set in database to defaults
        self.load_defaults(config)


    def load_defaults(self,config):
        """Load default settings from config file"""
        logging.debug("Set default attributes for user from configfile")
        if self.name=='':
            self.name=self.id
        if self.role=='':
            self.role=config.get('DEFAULT','role')
        if self.archive=='':
            self.archive=config.get('DEFAULT','archive')
        if self.imaphost=='':
            self.imaphost=config.get('DEFAULT','host')


    def loadAddress(self,config):
        """Load adresses for user from adress table"""
        logging.debug("Load user aliases for %s from adress table" % self.id)
        conn=DBaccess(config).conn
        c=conn.cursor()
        try:
            sql="SELECT address FROM address WHERE id='%s'" % self.id
            c.execute(sql)
            for row in c:
                self.addresses.append(row[0])
            c.close()
            conn.close()
        except Exception, e:
            sys.exit("Cannot read data from address table: %s" % e)
        # set adresslist to userid ...
        if not self.addresses:
            logging.debug("No adresses configured. Set to login name""")
            if string.count(self.id,'@') == 0:
                # with domain
                try:
                    domain=config.get('DEFAULT','domain')
                    logging.info("Add default domain %s",domain)
                    self.addresses.append(self.id+'@'+domain)
                except Exception, e:
                    logging.info("Domain for %s set" % self.id)
            else:
                self.addresses.append(self.id)



    def loadUserData(self,config,id):
        """Load name, archive, role from table 'user'"""
        sql="SELECT name,archive,role FROM user WHERE id ='%s'" % id
        try:
            conn=DBaccess(config).conn
            c=conn.cursor()
            rowcount=c.execute(sql)
            rows=c.fetchall()
            rowcount=len(rows)
            if rowcount == 0:
                logging.debug('SQL: %s' % sql)
                raise ValueError('ERROR ERR_NOUSER')
            if rowcount == 1:
                for row in rows:
                    self.name=row[0]
                    self.archive=row[1]
                    self.role=row[2]
                c.close()
                conn.close()
            else:
                raise ValueError('ERROR ERR_NOUSER')
        except Exception, e:
            logging.debug("Cannot read data from user table: %s" % e)
            print e
            sys.exit("User does not exist at database backend.")



    def loadImapuserData(self,config,id):
        """Load imapuser, password, imaphost, status from table 'imapuser'"""
        sql="SELECT imapuser,password,imaphost,status FROM imapuser WHERE id ='%s'" % id
        try:
            conn=DBaccess(config).conn
            c=conn.cursor()
            rowcount=c.execute(sql)
            rows=c.fetchall()
            rowcount=len(rows)
            if rowcount == 0:
                raise ValueError('ERROR ERR_NOUSER')
            if rowcount == 1:
                for row in c:
                    self.imapuser=row[0]
                    # strip leading '$rsa$_' marker and decode base64
                    try:
                        self.password=base64.b64decode(row[1].split('_')[1])
                    except IndexError:
                        self.password=row[1]
                    self.imaphost=row[2]
                    self.status=row[3]
                c.close()
                conn.close()
            else:
                raise ValueError('ERROR ERR_NOUSER')
        except Exception, e:
            sys.exit("Cannot read data from imapuser table: %s" % e)



    def loadImapuserImaphost(self,config,imapuser,imaphost):
        """Load id,status from table 'imapuser'"""
        sql="SELECT id,status FROM imapuser WHERE imapuser ='%s' AND imaphost = '%s'" % (imapuser,imaphost)
        try:
            conn=DBaccess(config).conn
            c=conn.cursor()
            c.execute(sql)
            rows=c.fetchall()
            rowcount=len(rows)
            if rowcount == 0:
                raise ValueError('ERROR ERR_NOUSER')
            if rowcount == 1:
                for row in rows:
                    self.id=row[0]
                    self.status=row[1]
                c.close()
                conn.close()
            else:
                raise ValueError('ERROR ERR_NOUSER')
        except Exception, e:
            sys.exit("Cannot read data from imapuser table: %s" % e)



    def loadImapuser(self,config,imapuser):
        """Load id, imaphost, status from table 'imapuser'"""
        sql="SELECT id, imaphost, status FROM imapuser WHERE imapuser = '%s'" % imapuser
        try:
            conn=DBaccess(config).conn
            c=conn.cursor()
            c.execute(sql)
            rows=c.fetchall()
            rowcount=len(rows)
            if rowcount == 0:
                raise ValueError('ERROR ERR_NOUSER')
            if rowcount > 1:
                raise ValueError('ERROR ERR_NOUNIQEUSER')
            if rowcount == 1:
                for row in rows:
                    self.id=row[0]
                    self.imaphost=row[1]
                    self.status=row[2]
                c.close()
                conn.close()
            else:
                raise ValueError('ERROR ERR_NOIMAPHOST')
        except Exception, e:
            logging.debug('%s' % sql)
            logging.warn("Cannot read data from imapuser table: %s" % e)
            sys.exit("%s" % e)



    def save(self,config):
        """Save user data or update user password"""

        if config.getboolean('DEFAULT','store password'):
            keyfile=config.get('DEFAULT','keyfile')
            logging.debug("Encrypt password with key: %s" % keyfile)
            key=RSA.importKey(open(keyfile).read())
            cipher=PKCS1_v1_5.new(key)
            if not self.password=='':
                encpass=cipher.encrypt(self.password)
                pwstring='$rsa$_'+base64.b64encode(str(encpass))
            else:
                pwstring='!'
        else:
            pwstring='*'

        if config.getboolean('DEFAULT','store password'):
            conn=DBaccess(config).conn
            conn.text_factory = str
            c=conn.cursor()
            logging.debug("Update user password in database")
            try:
                sql="UPDATE imapuser SET password='%s' WHERE id='%s'" % (pwstring,self.id)
                c.execute(sql)
            except Exception, e:
                logging.error("Cannot update user password: %s",e)
            c.close()
            conn.commit()
            conn.close()


class DBaccess:
    def __init__(self,config):
        self.conn=''
        dbtype=config.get('DEFAULT','dbtype')
        if dbtype=='sqlite3':
            import sqlite3
            dbfile=config.get('DEFAULT','userdb')
            try:
                self.conn=sqlite3.connect(dbfile)
            except Exception, e:
                sys.exit('Cannot connect database "%s": %s' % (dbfile,e))
        elif dbtype=='mysql':
            import MySQLdb
            dbhost=config.get('DEFAULT','dbhost')
            dbuser=config.get('DEFAULT','dbuser')
            dbpass=config.get('DEFAULT','dbpass')
            dbname=config.get('DEFAULT','dbname')
            try:
                self.conn=MySQLdb.connect(host=dbhost,user=dbuser,passwd=dbpass,db=dbname)
            except Exception, e:
                sys.exit('Cannot connect database "%s": %s' % (dbname,e[1]))


################################################################################
def print_usage(errormsg=''):
    if errormsg != '':
        print errormsg
        print ""
    print "Usage: echo 'USERNAME PASSWORD' | "+sys.argv[0]+" [-l <LOGLEVEL>"
    print "  -c <configfile>            default: /etc/benno-imap/imapauth.conf"
    print "                             (or environment variable: CONFIGFILE)"
    print ""
    print "    -l <WARNING|INFO|DEBUG>    loglevel (default ERROR)"


################################################################################
def init_config(configfile):
    config=ConfigParser.SafeConfigParser({
            'adminuser':'benno',
            'archive':'BennoContainer',
            'host':'localhost',
            'port':'0',
            'role':'USER',
            'ssl':'True',
            'store password':'False',
            'dbtype':'sqlite3',
            'userdb':'/var/lib/benno-web/bennoweb.sqlite',
            'dbhost':'localhost',
            'dbuser':'benno',
            'dbpass':'secret',
            'dbname':'benno',
            'keyfile':'/etc/benno-imap/benno-imap.pub'
        })
    try:
        confighandle=open(configfile,'r')
    except Exception, e:
        sys.stderr.write('Cannot open %s: %s\n\n' % (configfile, e[1]))
        print_usage()
        sys.exit()
    try:
        config.readfp(confighandle)
    except ConfigParser.MissingSectionHeaderError:
        sys.exit("Config file format incorrect")
    except Exception, e:
        sys.exit('Cannot read config file %s', configfile)
    return config


################################################################################
def isTTY(message):
    mode = os.fstat(sys.stdin.fileno()).st_mode
    if stat.S_ISFIFO(mode):
        pass
    elif stat.S_ISREG(mode):
        pass
    else:
        if message=='':
            return True
        sys.stderr.write('%s ' % (message))
    return False



### MAIN #################
if __name__ == "__main__":
    logarg=os.getenv('LOGLEVEL','ERROR')
    configfile=os.getenv('CONFIGFILE','/etc/benno-imap/imapauth.conf')
    opts, args = getopt.getopt(sys.argv[1:], "hvc:L:", ["help", "version", "config", "loglevel"])
    username=''
    password=''
    imaphost=''
    deleteuser=''
    for o, v in opts:
        if o in ('-c', '--config'):
            configfile=v
        if o in ('-L', '--loglevel'):
            logarg=v
            deleteuser=v
        if o in ('-h', '--help'):
            print_usage()
            sys.exit(0)
        if o in ('-v', '--version'):
            print "%s" % versionstring
            sys.exit(0)

    loglevel=getattr(logging, logarg.upper(), None)
    if not isinstance(loglevel, int):
        sys.exit('Invalid log level: %s' % loglevel)
    logging.basicConfig(format='%(levelname)s: %(message)s', level=loglevel)

    config=init_config(configfile)

    logging.info("Read file \"%s\"", configfile)

    # read username and password from stdin
    isTTY('Username:')
    line1=sys.stdin.readline()
    if isTTY(''):
        line2=getpass.getpass('Password: ')
        username=line1.rstrip()
        password=line2.rstrip()
        isTTY('IMAP hostname:')
        line3=sys.stdin.readline()
        if line3:
            imaphost=line3.rstrip()
    else:
        line2=sys.stdin.readline()
        if line2:
            username=line1.rstrip()
            password=line2.rstrip()
        else:
            try:
                # whitespace separated
                token=line1.split()
                username=token[0]
                password=token[1]
                if len(token) > 2:
                    imaphost=token[2]
            except Exception, e:
                logging.debug(e)
    if not password:
        print_usage('No username or password given.')
        sys.exit(1)
    # load data from userdb
    try:
        user=User(username,imaphost)
        user.load(config)
    except Exception, e:
        print e
        sys.exit(1)
    if (user.status & 2) < 2:
        logging.info("Login of user %s/%s disabled" % (user.id,user.imaphost))
        sys.exit('ERROR ERR_DISABLED')
    imapssl=config.getboolean('DEFAULT','ssl')
    adminuser=config.get('DEFAULT','adminuser')
    try:
        imaphost,imapport=user.imaphost.split(':')
    except ValueError:
        imaphost=user.imaphost
        imapport=config.getint('DEFAULT','port')
        try:
            if imapssl == True:
                imapport=993
                logging.debug("Connect %s at port %s",imaphost,imapport)
                imap=imaplib.IMAP4_SSL(imaphost,imapport)
            else:
                if imapport == 0:
                    imapport=143
                logging.debug("Connect %s at port %s",imaphost,imapport)
                imap=imaplib.IMAP4(imaphost,imapport)
            imap.login(username,password)
            imap.logout()
            logging.info("User %s successful authenticated", username)
        except Exception, e:
            logging.error("Cannot connect to \"%s:%s\": %s", imaphost,imapport,e)
            print 'ERROR ERR_AUTH'
            sys.exit(1)
    # authentication successful
    user.password=password
    user.imaphost=imaphost
    # store password
    try:
        user.save(config)
    except Exception, e:
        logging.error("Cannot store password for %s: %s", user.id,e)
        sys.exit(1)

    if username == config.get('DEFAULT','adminuser'):
        logging.info("%s is admin user",username)
        bennorole=admin
        mailfilter='*@*'
    print "ARCHIVE %s" % user.archive
    print "ROLE %s" % user.role
    print "DISPLAYNAME %s" % user.name
    for mailfilter in user.addresses:
        print "MAIL %s" % mailfilter
    print "AUTH OK"
