# -*- test-case-name: twisted.words.test -*-
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.

"""
MSNP8 Protocol (client only) - semi-experimental

This module provides support for clients using the MSN Protocol (MSNP8).
There are basically 3 servers involved in any MSN session:

I{Dispatch server}

The DispatchClient class handles connections to the
dispatch server, which basically delegates users to a
suitable notification server.

You will want to subclass this and handle the gotNotificationReferral
method appropriately.

I{Notification Server}

The NotificationClient class handles connections to the
notification server, which acts as a session server
(state updates, message negotiation etc...)

I{Switcboard Server}

The SwitchboardClient handles connections to switchboard
servers which are used to conduct conversations with other users.

There are also two classes (FileSend and FileReceive) used
for file transfers.

Clients handle events in two ways.

  - each client request requiring a response will return a Deferred,
    the callback for same will be fired when the server sends the
    required response
  - Events which are not in response to any client request have
    respective methods which should be overridden and handled in
    an adequate manner

Most client request callbacks require more than one argument,
and since Deferreds can only pass the callback one result,
most of the time the callback argument will be a tuple of
values (documented in the respective request method).
To make reading/writing code easier, callbacks can be defined in
a number of ways to handle this 'cleanly'. One way would be to
define methods like: def callBack(self, (arg1, arg2, arg)): ...
another way would be to do something like:
d.addCallback(lambda result: myCallback(*result)).

If the server sends an error response to a client request,
the errback of the corresponding Deferred will be called,
the argument being the corresponding error code.

B{NOTE}:
Due to the lack of an official spec for MSNP8, extra checking
than may be deemed necessary often takes place considering the
server is never 'wrong'. Thus, if gotBadLine (in any of the 3
main clients) is called, or an MSNProtocolError is raised, it's
probably a good idea to submit a bug report. ;)
Use of this module requires that PyOpenSSL is installed.

TODO
====
- check message hooks with invalid x-msgsinvite messages.
- font handling
- switchboard factory

@author: Sam Jordan
"""

import types, operator, os
from random import randint
from urllib import quote, unquote
from hashlib import md5

from twisted.python import failure, log
from twisted.internet import reactor
from twisted.internet.defer import Deferred, execute
from twisted.internet.protocol import ClientFactory
try:
    from twisted.internet.ssl import ClientContextFactory
except ImportError:
    ClientContextFactory = None
from twisted.protocols.basic import LineReceiver
from twisted.web.http import HTTPClient


MSN_PROTOCOL_VERSION = "MSNP8 CVR0"       # protocol version
MSN_PORT             = 1863               # default dispatch server port
MSN_MAX_MESSAGE      = 1664               # max message length
MSN_CHALLENGE_STR    = "Q1P7W2E4J9R8U3S5" # used for server challenges
MSN_CVR_STR          = "0x0409 win 4.10 i386 MSNMSGR 5.0.0544 MSMSGS" # :(

# auth constants
LOGIN_SUCCESS  = 1
LOGIN_FAILURE  = 2
LOGIN_REDIRECT = 3

# list constants
FORWARD_LIST = 1
ALLOW_LIST   = 2
BLOCK_LIST   = 4
REVERSE_LIST = 8

# phone constants
HOME_PHONE   = "PHH"
WORK_PHONE   = "PHW"
MOBILE_PHONE = "PHM"
HAS_PAGER    = "MOB"

# status constants
STATUS_ONLINE  = 'NLN'
STATUS_OFFLINE = 'FLN'
STATUS_HIDDEN  = 'HDN'
STATUS_IDLE    = 'IDL'
STATUS_AWAY    = 'AWY'
STATUS_BUSY    = 'BSY'
STATUS_BRB     = 'BRB'
STATUS_PHONE   = 'PHN'
STATUS_LUNCH   = 'LUN'

CR = "\r"
LF = "\n"


class SSLRequired(Exception):
    """
    This exception is raised when it is necessary to talk to a passport server
    using SSL, but the necessary SSL dependencies are unavailable.

    @since: 11.0
    """



def checkParamLen(num, expected, cmd, error=None):
    if error == None:
        error = "Invalid Number of Parameters for %s" % cmd
    if num != expected:
        raise MSNProtocolError, error

def _parseHeader(h, v):
    """
    Split a certin number of known
    header values with the format:
    field1=val,field2=val,field3=val into
    a dict mapping fields to values.
    @param h: the header's key
    @param v: the header's value as a string
    """

    if h in ('passporturls','authentication-info','www-authenticate'):
        v = v.replace('Passport1.4','').lstrip()
        fields = {}
        for fieldPair in v.split(','):
            try:
                field,value = fieldPair.split('=',1)
                fields[field.lower()] = value
            except ValueError:
                fields[field.lower()] = ''
        return fields
    else:
        return v

def _parsePrimitiveHost(host):
    # Ho Ho Ho
    h,p = host.replace('https://','').split('/',1)
    p = '/' + p
    return h,p


def _login(userHandle, passwd, nexusServer, cached=0, authData=''):
    """
    This functio
n is used internally and should not ever be called
    directly.

    @raise SSLRequired: If there is no SSL support availa
ble.
    """
    if ClientContextFactory is None:
        raise SSLRequired(
            'Connecting to the Passport server requires SSL, but SSL is '
            'unavailable.')

    cb = Deferred()
    def _cb(server, auth):
        loginFac = ClientFactory()
        loginFac.protocol = lambda : PassportLogin(cb, userHandle, passwd, server, auth)
        reactor.connectSSL(_parsePrimitiveHost(server)[0], 443, loginFac, ClientContextFactory())

    if cached:
        _cb(nexusServer, authData)
    else:
        fac = ClientFactory()
        d = Deferred()
        d.addCallbacks(_cb, callbackArgs=(authData,))
        d.addErrback(lambda f: cb.errback(f))
        fac.protocol = lambda : PassportNexus(d, nexusServer)
        reactor.connectSSL(_parsePrimitiveHost(nexusServer)[0], 443, fac, ClientContextFactory())
    return cb


class PassportNexus(HTTPClient):

    """
    Used to obtain the URL of a valid passport
    login HTTPS server.

    This class is used internally and should
    not be instantiated directly -- that is,
    The passport logging in process is handled
    transparantly by NotificationClient.
    """

    def __init__(self, deferred, host):
        self.deferred = deferred
        self.host, self.path = _parsePrimitiveHost(host)

    def connectionMade(self):
        HTTPClient.connectionMade(self)
        self.sendCommand('GET', self.path)
        self.sendHeader('Host', self.host)
        self.endHeaders()
        self.headers = {}

    def handleHeader(self, header, value):
        h = header.lower()
        self.headers[h] = _parseHeader(h, value)

    def handleEndHeaders(self):
        if self.connected:
            self.transport.loseConnection()
        if 'passporturls' not in self.headers or 'dalogin' not in self.headers['passporturls']:
            self.deferred.errback(failure.Failure(failure.DefaultException("Invalid Nexus Reply")))
        self.deferred.callback('https://' + self.headers['passporturls']['dalogin'])

    def handleResponse(self, r):
        pass

class PassportLogin(HTTPClient):
    """
    This class is used internally to obtain
    a login ticket from a passport HTTPS
    server -- it should not be used directly.
    """

    _finished = 0

    def __init__(self, deferred, userHandle, passwd, host, authData):
        self.deferred = deferred
        self.userHandle = userHandle
        self.passwd = passwd
        self.authData = authData
        self.host, self.path = _parsePrimitiveHost(host)

    def connectionMade(self):
        self.sendCommand('GET', self.path)
        self.sendHeader('Authorization', 'Passport1.4 OrgVerb=GET,OrgU