import logging
from CommandStream import *
from StoreConnection import StoreConnection
from cStringIO import StringIO
import bongo
import re
import time
import random
import md5
import socket

class DocTypes:
    Unknown = 0x0001
    Mail = 0x0002
    Event = 0x0003
    Addressbook = 0x0004
    Conversation = 0x0005
    Calendar = 0x0006
    Config = 0x0007

    Folder = 0x1000
    Shared = 0x2000
    SharedFolder = Folder | Shared
    Purged = 0x4000

class DocFlags:
    Purged = (1 << 0)
    Seen = (1 << 1)
    Answered = (1 << 2)
    Flagged = (1 << 3)
    Deleted = (1 << 4)
    Draft = (1 << 5)
    Recent = (1 << 6)
    Junk = (1 << 7)
    Sent = (1 << 8)

    Starred = (3 << 16)

    names = {
        Purged : "purged",
        Seen : "read",
        Answered : "answered",
        Flagged : "flagged",
        Deleted : "deleted",
        Draft : "draft",
        Recent : "recent",
        Junk : "junk",
        Sent : "sent",
        Starred : "starred"
        }

    # reverse the names hash so we can look up flags by name
    revNames = {}
    for name, flag in names.items():
        revNames[flag] = name

    # some helpful functions
    def GetName(flag):
        return DocFlags.names[flag]
    GetName = staticmethod(GetName)

    def ByName(name):
        return DocFlags.revNames[name]
    ByName = staticmethod(ByName)

    def AllFlags():
        return DocFlags.names.keys()
    AllFlags = staticmethod(AllFlags)

    def GetMask(hash):
        flags = mask = 0

        for flag, status in hash.items():
            if status:
                flags = flags | flag
            mask = mask | flag

        return flags, mask
    GetMask = staticmethod(GetMask)

class FlagMode:
    Show = 0
    Add = 1
    Remove = 2
    Replace = 3

class ACL :
    def __init__(self, acl = None) :
        self.entries = []

        if acl :
            split = acl.split(';')
            for entry in split :
                if entry == '' :
                    continue
                
                tmp = re.split('\s+', entry)
                self.entries.append((tmp[0], tmp[1], tmp[2]))
        self.changed = False

    def __str__(self) :
        acl = ''
        for entry in self.entries :
            acl = acl + "%s %s %s;" % entry
        return acl

    def Clear(self) :
        if (self.entries != []) :
            self.entries = []
            self.changed = True

    def Grant(self, principal, priv) :
        newEntry = ("grant", principal, priv)
        for entry in self.entries :
            if entry == newEntry :
                return

        self.changed = True
        self.entries.append(newEntry)

    def AddEntries(self, entries) :
        self.changed = True
        self.entries.extend(entries)

    def Revoke(self, principal, priv) :
        self.RevokeEntry(("grant", principal, priv))

    def RevokeEntry(self, revokeEntry) :
        newEntries = []
        for entry in self.entries :
            if entry != revokeEntry :
                newEntries.append(entry)
            else :
                self.changed = True

        self.entries = newEntries

    def RevokeEntries(self, entries) :
        for entry in entries :
            self.RevokeEntry(entry)

class CalendarACL :
    class Rights :
        Read = (1 << 0)
        Write = (1 << 1)
        ReadProps = (1 << 2)

    def __init__(self, acl) :
        self.acl = acl
        self.summary = None

    def __str__(self) :
        return str(self.acl)

    def GetACL(self) :
        return self.acl

    def _GetUid (self, *args):
        t = long(time.time() * 1000)
        r = long(random.random() * 100000000000000000L)
        try:
            a = socket.gethostbyname(socket.gethostname())
        except:
            # if we can't get a network address, just imagine one
            a = random.random() * 100000000000000000L
        data = str(t) + ' ' + str (r) + ' ' + str(a) + ' ' + str(args)
        data = md5.md5(data).hexdigest()
        return data

    
    def _GetRights(self, name) :
        rights = 0
        if name == "read" :
            rights = rights | CalendarACL.Rights.Read
        elif name == "write" :
            rights = rights | CalendarACL.Rights.Write
        elif name == "read-props" :
            rights = rights | CalendarACL.Rights.ReadProps
        return rights

    def RightsToString(self, rights) :
        names = []
        if rights == 0 :
            return "none"
        if rights & CalendarACL.Rights.Read :
            names.append("read")
        if rights & CalendarACL.Rights.Write :
            names.append("write")
        if rights & CalendarACL.Rights.ReadProps :
            names.append("read-props")
        return ", ".join(names)


    def SetPublic(self, rights) :
        self.summary = None
        entries = self._Find("all")
        if (entries) :
            self.acl.RevokeEntries(entries)
        if (rights & CalendarACL.Rights.Read) :
            self.acl.Grant("all", "read")
        if (rights & CalendarACL.Rights.Write) :
            self.acl.Grant("all", "write")
        if (rights & CalendarACL.Rights.ReadProps) :
            self.acl.Grant("all", "read-props")

    def GetPublic(self) :
        entries = self._Find("all")
        rights = 0
        for entry in entries :
            rights = rights | self._GetRights(entry[2])
        return rights

    def AddPassword(self, user, password, rights) :
        self.summary = None
        if (rights & CalendarACL.Rights.Read) :
            self.acl.Grant("token:pw:" + user + ":" + password, "read")
        if (rights & CalendarACL.Rights.Write) :
            self.acl.Grant("token:pw:" + user + ":" + password, "write")

    def CheckPassword(self, user, password) :
        entries = self._Find("token:pw:" + user + ":" + password)
        if entries :
            return True
        else :
            return False

    def HasPassword(self, user = None) :
        entries = self._FindPasswords()
        if entries :
            return True
        else :
            return False

    def RemovePassword(self, user, password) :
        self.summary = None
        self.acl.RemoveEntries(self._Find("token:pw:" + user + ":" + password))

    def RemovePasswords(self) :
        self.summary = None
        self.acl.RemoveEntries(self._FindPasswords())

    def GetPasswords(self) :
        entries = self._FindPasswords()
        ret = {}
        for entry in entries :
            split = entry[1].split(":")
            ret[split[2]] = split[3]

        return ret

    def Share(self, address, rights, password = None) :
        self.summary = None
        entries = self._FindAddress(address)

        if (entries) :
            if not password :
                token = entries[0][1]
            else :
                token = "token:email:%s:%s" % (address, password)
                
            self.acl.RevokeEntries(entries)
        else :
            if not password :
                password = self._GetUid()
            token = "token:email:%s:%s" % (address, password)

        if (rights & CalendarACL.Rights.Read) :
            self.acl.Grant(token, "read")
        if (rights & CalendarACL.Rights.Write) :
            self.acl.Grant(token, "write")
        if (rights & CalendarACL.Rights.ReadProps) :
            self.acl.Grant(token, "read-props")

        return token[len("token:"):]

    def Unshare(self, address) :
        entries = self._FindAddress(address)
        if (entries) :
            self.acl.RevokeEntries(entries)

    def GetAddressToken(self, address) :
        entries = self._FindAddress(address)
        token = entries[0][1]
        return token[len("token:"):]
        
    def UnshareAll(self) :
        rights = self.GetPublic()
        self.acl.Clear()
        self.SetPublic(rights)

    def Clear(self) :
        self.acl.Clear()

    def Summarize(self) :
        if self.summary :
            return self.summary

        public = 0
        addresses = {}
        passwords = {}

        for entry in self.acl.entries :
            principal = entry[1]
            rights = self._GetRights(entry[2])

            if principal == "all" :
                public = public | rights
            elif principal.startswith("token:email:") :
                remaining = principal[len("token:email:"):]
                split = remaining.split(':', 2)
                address = split[0]
                password = split[1]

                if addresses.has_key(address) :
                    entry = addresses[address]
                else :
                    entry = { "password": password, "rights": 0 }
                    addresses[address] = entry
                entry["token"] = principal[len("token:"):]
                entry["rights"] = entry["rights"] | rights
            elif principal.startswith("token:") :
                password = principal[len("token:"):]
                if (passwords.has_key(password)) :
                    passwords[password] = passwords[password] | rights
                else :
                    passwords[password] = rights

        ret = {}
        ret["passwords"] = passwords
        ret["addresses"] = addresses
        ret["public"] = public
        self.summary = ret
        return ret
    
    def _FindAddress(self, address) :
        """Find acls with a given email token prefix"""
        ret = []
        for entry in self.acl.entries :
            if entry[1].startswith ("token:email:" + address + ":") :
                ret.append(entry)
        return ret

    def _Find(self, principal) :
        """Find acls with a given principal"""
        ret = []
        for entry in self.acl.entries :
            if entry[1] == principal :
                ret.append(entry)
        return ret

    def _FindPasswords(self) :
        """Find all password acls"""
        ret = []
        for entry in self.acl.entries :
            if entry[1].startswith("token:pw:") :
                ret.append(entry)
        return ret

class StoreClient:
    def __init__(self, user, owner, authToken=None, authCookie=None,
                 authPassword=None):
        from bongo.libs import msgapi
        self.owner = self.user = None
        
        addr = msgapi.FindUserNmap(owner)
        if addr is None:
            raise bongo.BongoError ("Could not look up nmap host for user %s" % owner)
        (host, port) = addr

        if authCookie:
            self.connection = StoreConnection(host, port, systemAuth=False)
            if not self.connection.CookieAuth(user, authCookie):
                raise bongo.BongoError("Failed cookie authentication with store")
        elif authPassword:
            self.connection = StoreConnection(host, port, systemAuth=False)
            if not self.connection.UserAuth(user, authPassword):
                raise bongo.BongoError("Failed user authentication with store")
        else:
            self.connection = StoreConnection(host, port)

        self.stream = self.connection.stream

        # if we authed with a cookie or password, we don't need to USER
        if not authCookie and not authPassword and user is not None:
            self.User(user)
        self.Store(owner)

        if authToken is not None:
            self.Token(authToken)

    def _escape_arg(self, arg):
        return "\"" + self._escape_quotes(arg) + "\""

    def _escape_quotes(self, s):
        return s.replace('"', '\\"')

    def _escape_query(self, s):
        return self._escape_quotes(s)

    def CookieDelete(self, token):
        command = "COOKIE DELETE %s" % token

        stream.log.debug("SEND: COOKIE DELETE ****************")
        self.stream.Write(command, nolog=1)

        r = self.stream.GetResponse()

        if r.code != 1000:
            raise CommandError(r)

    def CookieNew(self, timeout):
        command = "COOKIE NEW %d" % (time.time() + timeout)
        self.stream.Write(command)

        r = self.stream.GetResponse(nolog=True)

        if r.code != 1000:
            self.stream.log.debug("READ: %s %s", r.code, r.message)
            raise CommandError(r)

        self.stream.log.debug("READ: 1000 ****************")
        return r.message

    def Create(self, path, existingOk=False):
        command = "CREATE %s" % path
        self.stream.Write(command)

        r = self.stream.GetResponse()

        if r.code != 1000:
            if r.code != 4226 or not existingOk:
                raise CommandError(r)

        return r

    def Conversation(self, uid, props=[]):
        command = "CONVERSATION %s" % uid

        if len(props) > 0:
            command = command + " P" + ",".join(props)

        self.stream.Write(command)
        return ItemIterator(self.stream, len(props))

    def Conversations(self, source=None, props=[], query=None, start=-1, end=-1,
                      center=None, showTotal=False, flags=None, mask=None):
        command = "CONVERSATIONS"

        if source is not None:
            command = command + " S" + source

        if start == -1 and end == -1:
            command = command + " Rall"
        else:
            command = command + " R%d-%d" % (start, end)

        if len(props) > 0:
            command = command + " P" + ",".join(props)

        if query is not None:
            command = command + " \"Q" + self._escape_query(query) + "\""

        if center is not None:
            command = command + " C" + center

        if flags is not None:
            command = command + " F" + str(flags)

        if mask is not None:
            command = command + " M" + str(mask)

        if showTotal:
            command = command + " T"

        self.stream.Write(command)
        return ItemIterator(self.stream, len(props))

    def Delete(self, doc):
        self.stream.Write("DELETE %s" % (doc))
        r = self.stream.GetResponse()

        if r.code != 1000:
            raise CommandError(r)

    def Events(self, calendar=None, props=[], query=None, start=None, end=None):
        command = "EVENTS"

        if calendar is not None:
            command = command + " C" + calendar

        if len(props) > 0:
            command = command + " P" + ",".join(props)

        if query is not None:
            command = command + " \"Q" + self._escape_query(query) + "\""

        if start != None and end != None:
            command = command + " \"D%s-%s\"" % (start.strftime('%Y%m%dT%H%M%S'), end.strftime('%Y%m%dT%H%M%S'))

        self.stream.Write(command)
        return ItemIterator(self.stream, len(props))

    def Flag(self, guid, flags=None, mode=None):
        command = "FLAG %s" % guid

        if flags is None:
            mode = FlagMode.Show
        elif mode is None:
            mode = FlagMode.Replace
            command = "%s %d" % (command, flags)
        elif mode == FlagMode.Add:
            command = "%s +%d" % (command, flags)
        elif mode == FlagMode.Remove:
            command = "%s -%d" % (command, flags)

        self.stream.Write(command)
        r = self.stream.GetResponse()
        if r.code != 1000:
            raise CommandError(r)

        (old, new) = r.message.split(" ", 1)
        return (old, new)

    def GetACL(self, doc) :
        command = "PROPGET \"%s\" nmap.access-control" % doc
        self.stream.Write(command)
        r = self.stream.GetResponse()
        if r.code != 2001 and r.code != 3245:
            raise CommandError(r)

        acl = ""
        if r.code == 2001 :
            (key, length) = r.message.split(" ", 2)
            acl = self.stream.Read(int(length))
            # eat the \r\n afterward
            self.stream.Read(2)

        return ACL(acl)

    def Info(self, path, props=[]):
        command = "INFO %s" % self._escape_arg(path)

        if len(props) > 0:
            command = command + " P" + ",".join(props)

        self.stream.Write(command)
        r = self.stream.GetResponse()

        if r.code == 2001:
            item = Item(r)

            for i in range(len(props)):
                r = self.stream.GetResponse()
                if r.code == 2001:
                    (key, length) = r.message.split(" ", 2)
                    item.props[key] = self.stream.Read(int(length))
                    # eat the \r\n afterward
                    self.stream.Read(2)

            # eat the response line
            self.stream.GetResponse()
            return item
        elif r.code == 1000:
            return None

        raise CommandError(r)

    def ISearch(self, query, props = [], start=-1, end=-1):
        command = "ISEARCH "

        if start == -1 and end == -1:
            command = command + " Rall"
        else:
            command = command + " R%d-%d" % (start, end)

        command = "%s \"%s\"" % (command, self._escape_query(query))

        if len(props) > 0:
            command = command + " P" + ",".join(props)

        self.stream.Write(command)
        return ItemIterator(self.stream, len(props))

    def Link(self, calendar, doc) :
        self.stream.Write("LINK \"%s\" %s" % (calendar, doc))
        r = self.stream.GetResponse()
        if r.code != 1000 :
            raise CommandError(r)

    def List(self, path, props=[], start=-1, end=-1, flags=None, mask=None):
        command = "LIST %s" % path

        if start == -1 and end == -1:
            command = command + " Rall"
        else:
            command = command + " R%d-%d" % (start, end)

        if len(props) > 0:
            command = command + " P" + ",".join(props)

        if flags is not None:
            command = command + " F" + str(flags)

        if mask is not None:
            command = command + " M" + str(mask)

        self.stream.Write(command)
        return ItemIterator(self.stream, len(props))

    def MailingLists(self, source):
        command = "MAILINGLISTS %s" % source
        self.stream.Write(command)
        return ResponseIterator(self.stream)

    def Mime(self, uid):
        command = "MIME %s" % uid

        self.stream.Write(command)

        parts = []

        r = self.stream.GetResponse()
        while r.code in (2002, 2003, 2004):
            parts.append(MimeItem(r))
            r = self.stream.GetResponse()

        if len(parts) == 0:
            return None

        topPart = parts[0]

        if topPart.multipart:
            parent = topPart

            for child in parts[1:]:
                child.parent = parent

                if child.end:
                    parent = parent.parent
                    if parent is None:
                        break
                else:
                    parent.children.append(child)
                    if child.multipart:
                        parent = child

        if r.code != 1000:
            raise CommandError(r)

        return topPart

    def Move(self, uid, path, filename=None):
        command = "MOVE %s \"%s\"" % (uid, path)

        if filename is not None:
            command = "%s \"%s\"" % (command, filename)
        self.stream.Write(command)
        r = self.stream.GetResponse()
        if r.code != 1000:
            raise CommandError(r)

    def PropGet(self, doc, name=None):
        command = "PROPGET %s" % doc

        if name is not None:
            self.stream.Write("%s %s" % (command, name))
            r = self.stream.GetResponse()
            if r.code != 2001:
                raise CommandError(r)
            (key, length) = r.message.split(" ", 2)
            data = unicode(self.stream.Read(int(length)), "utf-8")
            # eat the \r\n afterward
            self.stream.Read(2)
            return data
        
        self.stream.Write(command)
        props = {}
        r = self.stream.GetResponse()
        while r.code == 2001 or r.code == 3245:
            if r.code == 3245:
                # set empty properties to None
                props[key] = None
                r = self.stream.GetResponse()
                continue

            (key, length) = r.message.split(" ", 2)
            props[key] = unicode(self.stream.Read(int(length)), "utf-8")
            # eat the \r\n afterward
            self.stream.Read(2)
            r = self.stream.GetResponse()

        return props

    def PropSet(self, doc, name, value=None):
        if value is None:
            value = ""

        self.stream.Write("PROPSET %s %s %d" % (doc, name, len(value)))

        r = self.stream.GetResponse()
        if r.code != 2002:
            raise CommandError(r)

        self.stream.WriteRaw(value)
        r = self.stream.GetResponse()
        if r.code != 1000:
            raise CommandError(r)

    def Quit(self):
        self.stream.Write("QUIT")
        self.connection.Close()

    def Read(self, doc):
        self.stream.Write("READ %s" % (doc))
        r = self.stream.GetResponse()
        if r.code != 2001:
            raise CommandError(r)
        length = r.message.split(" ")[-1]
        s = self.stream.Read(int(length))
        # eat the \r\n afterward
        self.stream.Read(2)
        return s

    def ReadMimePart(self, doc, part):
        self.stream.Write("READ %s %d %d" % (doc, part.start, part.length))
        r = self.stream.GetResponse()
        if r.code != 2001:
            raise CommandError(r)
        length = r.message.split(" ")[-1]
        s = self.stream.Read(int(length))
        # eat the \r\n afterward
        self.stream.Read(2)
        return StringIO(s)

    def Replace(self, doc, data):
        self.stream.Write("REPLACE %s %d" % (doc, len(data)))

        r = self.stream.GetResponse()
        if r.code != 2002:
            raise CommandError(r)

        self.stream.WriteRaw(data)

        r = self.stream.GetResponse()
        (guid, junk) = r.message.split(" ", 1)
        return guid

    def Reset(self):
        self.stream.Write("RESET")
        r = self.stream.GetResponse()
        if r.code != 1000:
            raise CommandError(r)

    def SetACL(self, doc, acl, force=False) :
        if force or acl.changed :
            self.PropSet(doc, "nmap.access-control", str(acl))
        
    def Store(self, name):
        self.stream.Write("STORE %s" % (name))
        r = self.stream.GetResponse()
        if r.code != 1000:
            raise CommandError(r)
        self.owner = name
        return r

    def Token(self, token):
        self.stream.Write("TOKEN %s" % (token))
        r = self.stream.GetResponse()
        if r.code != 1000:
            raise CommandError(r)
        return r

    def Unlink(self, calendar, event):
        self.stream.Write("UNLINK %s %s" % (calendar, event))
        r = self.stream.GetResponse()
        if r.code != 1000:
            raise CommandError(r)
        return r

    def User(self, name):
        self.stream.Write("USER %s" % (name))
        r = self.stream.GetResponse()
        if r.code != 1000:
            raise CommandError(r)
        self.user = name
        return r

    def Write(self, collection, type, data, filename=None, index=None, guid=None, timeCreated=None, flags=None, link=None):
        if data is None:
            data = ""

        command = "WRITE \"%s\" %d %d" % (collection, type, len(data))

        if index is not None:
            command = command + " I%s" % index

        if filename is not None:
            command = command + " \"F%s\"" % filename

        if guid is not None:
            command = command + " G%s" % guid

        if timeCreated is not None:
            command += " T" + timeCreated

        if flags is not None:
            command = command + " Z%d" % flags

        if link is not None:
            command = command + " \"L%s\"" % link

        self.stream.Write(command)
        

        r = self.stream.GetResponse()
        if r.code != 2002:
            raise CommandError(r)

        self.stream.WriteRaw(data)

        r = self.stream.GetResponse()
        if r.code != 1000:
            raise CommandError(r)

        (guid, junk) = r.message.split(" ", 1)
        return guid

    def Reset(self) :
        self.stream.Write("RESET\r\n")
        self.stream.GetResponse()
        

