]> code.delx.au - pymsnt/blobdiff - src/tlib/msn/msn.py
Handle MSN error conditions
[pymsnt] / src / tlib / msn / msn.py
index aaa18f5119a1c8cb47135f6db43210d1fb4ab98a..fcab164707075d7226403463c5bf87c342695b07 100644 (file)
@@ -78,13 +78,8 @@ 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: U{Sam Jordan<mailto:sam@twistedmatrix.com>}
+@author: U{James Bunton<mailto:james@delx.cjb.net>}
 """
 
 from __future__ import nested_scopes
@@ -100,24 +95,26 @@ except ImportError:
         print "Couldn't find a HTTPClient. If you're using Twisted 2.0 make sure you've installed twisted.web"
         raise
 import msnp11chl
-from msnp2p import random_guid
-import msnp2p
 
 # Twisted imports
 from twisted.internet import reactor, task
 from twisted.internet.defer import Deferred
-from twisted.internet.protocol import ClientFactory
-from twisted.internet.ssl import ClientContextFactory
+from twisted.internet.protocol import ReconnectingClientFactory, ClientFactory
+try:
+    from twisted.internet.ssl import ClientContextFactory
+except ImportError:
+    print "You must install pycrypto and pyopenssl."
+    raise
 from twisted.python import failure, log
-from twisted.xish.domish import unescapeFromXml
 
 # Compat stuff
 from tlib import xmlw
 
 # System imports
-import types, operator, os, sys, base64, random
+import types, operator, os, sys, base64, random, struct, random, sha, base64, StringIO, array, codecs, binascii
 from urllib import quote, unquote
 
+
 MSN_PROTOCOL_VERSION = "MSNP11 CVR0"      # protocol version
 MSN_PORT             = 1863               # default dispatch server port
 MSN_MAX_MESSAGE      = 1664               # max message length
@@ -155,12 +152,26 @@ STATUS_BRB     = 'BRB'
 STATUS_PHONE   = 'PHN'
 STATUS_LUNCH   = 'LUN'
 
-CR = "\r"
-LF = "\n"
 PINGSPEED = 50.0
 
-LINEDEBUG = True
-MESSAGEDEBUG = True
+DEBUGALL = True
+LINEDEBUG = False
+MESSAGEDEBUG = False
+MSNP2PDEBUG = False
+
+if DEBUGALL:
+    LINEDEBUG = True
+    MESSAGEDEBUG = True
+    MSNP2PDEBUG = True
+
+
+P2PSEQ = [-3, -2, 0, -1, 1, 2, 3, 4, 5, 6, 7, 8]
+def p2pseq(n):
+    if n > 5:
+        return n - 3
+    else:
+        return P2PSEQ[n]
+
 
 def getVal(inp):
     return inp.split('=')[1]
@@ -190,6 +201,41 @@ def getVals(params):
 
     return userHandle, screenName, userGuid, lists, groups
 
+def ljust(s, n, c):
+    """ Needed for Python 2.3 compatibility """
+    return s + (n-len(s))*c
+
+if sys.byteorder == "little":
+    def utf16net(s):
+        """ Encodes to utf-16 and ensures network byte order. Strips the BOM """
+        a = array.array("h", s.encode("utf-16")[2:])
+        a.byteswap()
+        return a.tostring()
+else:
+    def utf16net(s):
+        """ Encodes to utf-16 and ensures network byte order. Strips the BOM """
+        return s.encode("utf-16")[2:]
+
+def b64enc(s):
+    return base64.encodestring(s).replace("\n", "")
+
+def b64dec(s):
+    for pad in ["", "=", "==", "A", "A=", "A=="]: # Stupid MSN client!
+        try:
+            return base64.decodestring(s + pad)
+        except:
+            pass
+    raise ValueError("Got some very bad base64!")
+
+def random_guid():
+    format = "{%4X%4X-%4X-%4X-%4X-%4X%4X%4X}"
+    data = []
+    for x in xrange(8):
+        data.append(random.random() * 0xAAFF + 0x1111)
+    data = tuple(data)
+
+    return format % data
+
 def checkParamLen(num, expected, cmd, error=None):
     if error == None: error = "Invalid Number of Parameters for %s" % cmd
     if num != expected: raise MSNProtocolError, error
@@ -300,7 +346,7 @@ class PassportLogin(HTTPClient):
     def connectionMade(self):
         self.sendCommand('GET', self.path)
         self.sendHeader('Authorization', 'Passport1.4 OrgVerb=GET,OrgURL=http://messenger.msn.com,' +
-                                         'sign-in=%s,pwd=%s,%s' % (quote(self.userHandle), self.passwd,self.authData))
+                                         'sign-in=%s,pwd=%s,%s' % (quote(self.userHandle), quote(self.passwd), self.authData))
         self.sendHeader('Host', self.host)
         self.endHeaders()
         self.headers = {}
@@ -387,7 +433,7 @@ class MSNMessage:
         self.screenName = screenName
         self.specialMessage = specialMessage
         self.message = message
-        self.headers = {'MIME-Version' : '1.0', 'Content-Type' : 'text/plain'}
+        self.headers = {'MIME-Version' : '1.0', 'Content-Type' : 'text/plain; charset=UTF-8'}
         self.length = length
         self.readPos = 0
 
@@ -398,6 +444,11 @@ class MSNMessage:
         """
         return reduce(operator.add, [len(x[0]) + len(x[1]) + 4  for x in self.headers.items()]) + len(self.message) + 2
 
+    def delHeader(self, header):
+        """ delete the desired header """
+        if self.headers.has_key(header):
+            del self.headers[header]
+
     def setHeader(self, header, value):
         """ set the desired header """
         self.headers[header] = value
@@ -421,6 +472,76 @@ class MSNMessage:
         """ set the message text """
         self.message = message
 
+
+class MSNObject:
+    """
+    Used to represent a MSNObject. This can be currently only be an avatar.
+
+    @ivar creator: The userHandle of the creator of this picture.
+    @ivar imageData: The PNG image data (only for our own avatar)
+    @ivar type: Always set to 3, for avatar.
+    @ivar size: The size of the image.
+    @ivar location: The filename of the image.
+    @ivar friendly: Unknown.
+    @ivar text: The textual representation of this MSNObject.
+    """
+    def __init__(self, s=""):
+        """ Pass a XML MSNObject string to parse it, or pass no arguments for a null MSNObject to be created. """
+        if s:
+            self.parse(s)
+        else:
+            self.setNull()
+    
+    def setData(self, creator, imageData):
+        """ Set the creator and imageData for this object """
+        self.creator = creator
+        self.imageData = imageData
+        self.size = len(imageData)
+        self.type = 3
+        self.location = "TMP" + str(random.randint(1000,9999))
+        self.friendly = "AAA="
+        self.sha1d = b64enc(sha.sha(imageData).digest())
+        self.makeText()
+    
+    def setNull(self):
+        self.creator = ""
+        self.imageData = ""
+        self.size = 0
+        self.type = 0
+        self.location = ""
+        self.friendly = ""
+        self.sha1d = ""
+        self.text = ""
+    
+    def makeText(self):
+        """ Makes a textual representation of this MSNObject. Stores it in self.text """
+        h = []
+        h.append("Creator")
+        h.append(self.creator)
+        h.append("Size")
+        h.append(str(self.size))
+        h.append("Type")
+        h.append(str(self.type))
+        h.append("Location")
+        h.append(self.location)
+        h.append("Friendly")
+        h.append(self.friendly)
+        h.append("SHA1D")
+        h.append(self.sha1d)
+        sha1c = b64enc(sha.sha("".join(h)).digest())
+        self.text = '<msnobj Creator="%s" Size="%s" Type="%s" Location="%s" Friendly="%s" SHA1D="%s" SHA1C="%s"/>' % (self.creator, str(self.size), str(self.type), self.location, self.friendly, self.sha1d, sha1c)
+    
+    def parse(self, s):
+        e = xmlw.parseText(s, True)
+        self.creator = e.getAttribute("Creator")
+        self.size = int(e.getAttribute("Size"))
+        self.type = int(e.getAttribute("Type"))
+        self.location = e.getAttribute("Location")
+        self.friendly = e.getAttribute("Friendly")
+        self.sha1d = e.getAttribute("SHA1D")
+        self.text = s
+
+
 class MSNContact:
     
     """
@@ -434,7 +555,7 @@ class MSNContact:
     @ivar lists: An integer representing the sum of all lists
                  that this contact belongs to.
     @ivar caps: int, The capabilities of this client
-    @ivar msnobj: msnp2p.MSNOBJ, The MSNObject representing the contact's avatar
+    @ivar msnobj: The MSNObject representing the contact's avatar
     @ivar status: The contact's status code.
     @type status: str if contact's status is known, None otherwise.
     @ivar personal: The contact's personal message .
@@ -665,13 +786,14 @@ class MSNEventBase(LineReceiver):
         raise NotImplementedError
 
     def sendLine(self, line):
-        if LINEDEBUG: log.msg(">> " + line)
+        if LINEDEBUG: log.msg("<< " + line)
         LineReceiver.sendLine(self, line)
 
     def lineReceived(self, line):
-        if LINEDEBUG: log.msg("<< " + line)
+        if LINEDEBUG: log.msg(">> " + line)
+        if not self.connected: return
         if self.currentMessage:
-            self.currentMessage.readPos += len(line+CR+LF)
+            self.currentMessage.readPos += len(line+"\r\n")
             try:
                 header, value = line.split(':')
                 self.currentMessage.setHeader(header, unquote(value).lstrip())
@@ -690,8 +812,10 @@ class MSNEventBase(LineReceiver):
 
         if len(cmd) != 3: raise MSNProtocolError, "Invalid Command, %s" % repr(cmd)
         if cmd.isdigit():
-            if self.ids.has_key(params.split(' ')[0]):
-                self.ids[id].errback(int(cmd))
+            id = params.split(' ')[0]
+            if id.isdigit() and self.ids.has_key(int(id)):
+                id = int(id)
+                self.ids[id][0].errback(int(cmd))
                 del self.ids[id]
                 return
             else:       # we received an error which doesn't map to a sent command
@@ -706,6 +830,7 @@ class MSNEventBase(LineReceiver):
             self.handle_UNKNOWN(cmd, params.split(' '))
 
     def rawDataReceived(self, data):
+        if not self.connected: return
         extra = ""
         self.currentMessage.readPos += len(data)
         diff = self.currentMessage.readPos - self.currentMessage.length
@@ -721,9 +846,13 @@ class MSNEventBase(LineReceiver):
         m = self.currentMessage
         self.currentMessage = None
         if MESSAGEDEBUG: log.msg(m.message)
-        if not self.checkMessage(m):
+        try:
+            if not self.checkMessage(m):
+                self.setLineMode(extra)
+                return
+        except Exception, e:
             self.setLineMode(extra)
-            return
+            raise
         self.gotMessage(m)
         self.setLineMode(extra)
 
@@ -753,16 +882,13 @@ class MSNEventBase(LineReceiver):
         """
         log.msg('Error %s' % (errorCodes[errorCode]))
 
+
 class DispatchClient(MSNEventBase):
     """
     This class provides support for clients connecting to the dispatch server
     @ivar userHandle: your user handle (passport) needed before connecting.
     """
 
-    # eventually this may become an attribute of the
-    # factory.
-    userHandle = ""
-
     def connectionMade(self):
         MSNEventBase.connectionMade(self)
         self.sendLine('VER %s %s' % (self._nextTransactionID(), MSN_PROTOCOL_VERSION))
@@ -775,10 +901,10 @@ class DispatchClient(MSNEventBase):
             self.transport.loseConnection()
             raise MSNProtocolError, "Invalid version response"
         id = self._nextTransactionID()
-        self.sendLine("CVR %s %s %s" % (id, MSN_CVR_STR, self.userHandle))
+        self.sendLine("CVR %s %s %s" % (id, MSN_CVR_STR, self.factory.userHandle))
 
     def handle_CVR(self, params):
-        self.sendLine("USR %s TWN I %s" % (self._nextTransactionID(), self.userHandle))
+        self.sendLine("USR %s TWN I %s" % (self._nextTransactionID(), self.factory.userHandle))
 
     def handle_XFR(self, params):
         if len(params) < 4: raise MSNProtocolError, "Invalid number of parameters for XFR"
@@ -804,6 +930,18 @@ class DispatchClient(MSNEventBase):
         pass
 
 
+class DispatchFactory(ClientFactory):
+    """
+    This class keeps the state for the DispatchClient.
+
+    @ivar userHandle: the userHandle to request a notification
+                      server for.
+    """
+    protocol = DispatchClient
+    userHandle = ""
+
+
+
 class NotificationClient(MSNEventBase):
     """
     This class provides support for clients connecting
@@ -818,7 +956,7 @@ class NotificationClient(MSNEventBase):
         self._state = ['DISCONNECTED', {}]
         self.pingCounter = 0
         self.pingCheckTask = None
-        self.msnobj = msnp2p.MSNOBJ()
+        self.msnobj = MSNObject()
 
     def _setState(self, state):
         self._state[0] = state
@@ -839,6 +977,7 @@ class NotificationClient(MSNEventBase):
         MSNEventBase.connectionMade(self)
         self._setState('CONNECTED')
         self.sendLine("VER %s %s" % (self._nextTransactionID(), MSN_PROTOCOL_VERSION))
+        self.factory.resetDelay()
 
     def connectionLost(self, reason):
         self._setState('DISCONNECTED')
@@ -918,16 +1057,17 @@ class NotificationClient(MSNEventBase):
         self.gotMSNAlert(bodytext, actionurl, subscrurl)
 
     def _gotUBX(self, message):
+        msnContact = self.factory.contacts.getContact(message.userHandle)
+        if not msnContact: return
         lm = message.message.lower()
         p1 = lm.find("<psm>") + 5
         p2 = lm.find("</psm>")
         if p1 >= 0 and p2 >= 0:
-            personal = unescapeFromXml(message.message[p1:p2])
-            msnContact = self.factory.contacts.getContact(message.userHandle)
-            if not msnContact: return
+            personal = xmlw.unescapeFromXml(message.message[p1:p2])
             msnContact.personal = personal
             self.contactPersonalChanged(message.userHandle, personal)
         else:
+            msnContact.personal = ''
             self.contactPersonalChanged(message.userHandle, '')
 
     def checkMessage(self, message):
@@ -1007,7 +1147,7 @@ class NotificationClient(MSNEventBase):
             self.handleAvatarHelper(msnContact, params[5])
         else:
             self.handleAvatarGoneHelper(msnContact)
-        self.gotContactStatus(params[1], params[2], unquote(params[3]))
+        self.gotContactStatus(params[2], params[1], unquote(params[3]))
 
     def handleAvatarGoneHelper(self, msnContact):
         if msnContact.msnobj:
@@ -1016,12 +1156,12 @@ class NotificationClient(MSNEventBase):
             self.contactAvatarChanged(msnContact.userHandle, "")
 
     def handleAvatarHelper(self, msnContact, msnobjStr):
-        msnobj = msnp2p.MSNOBJ(unquote(msnobjStr))
+        msnobj = MSNObject(unquote(msnobjStr))
         if not msnContact.msnobj or msnobj.sha1d != msnContact.msnobj.sha1d:
-            if msnp2p.MSNP2P_DEBUG: log.msg("Updated MSNOBJ received!" + msnobjStr)
+            if MSNP2PDEBUG: log.msg("Updated MSNObject received!" + msnobjStr)
             msnContact.msnobj = msnobj
             msnContact.msnobjGot = False
-            self.contactAvatarChanged(msnContact.userHandle, msnContact.msnobj.sha1d)
+            self.contactAvatarChanged(msnContact.userHandle, binascii.hexlify(b64dec(msnContact.msnobj.sha1d)))
 
     def handle_CHL(self, params):
         checkParamLen(len(params), 2, 'CHL')
@@ -1043,7 +1183,7 @@ class NotificationClient(MSNEventBase):
             self.handleAvatarHelper(msnContact, params[4])
         else:
             self.handleAvatarGoneHelper(msnContact)
-        self.contactStatusChanged(params[0], params[1], unquote(params[2]))
+        self.contactStatusChanged(params[1], params[0], unquote(params[2]))
 
     def handle_FLN(self, params):
         checkParamLen(len(params), 1, 'FLN')
@@ -1123,18 +1263,18 @@ class NotificationClient(MSNEventBase):
 
     def handle_PRP(self, params):
         if params[1] == "MFN":
-            self._fireCallback(int(params[0]), unquote(params[2]))
+            self._fireCallback(int(params[0]))
         elif self._getState() == 'SYNC':
             self._getStateData('phone').append((params[0], unquote(params[1])))
         else:
             self._fireCallback(int(params[0]), int(params[1]), unquote(params[3]))
 
     def handle_BPR(self, params):
-        if not self.factory.contacts: raise MSNProtocolError, "handle_BPR called with no contact list" # debug
         numParams = len(params)
         if numParams == 2: # part of a syn
             self._getStateData('last_contact').setPhone(params[0], unquote(params[1]))
         elif numParams == 4:
+            if not self.factory.contacts: raise MSNProtocolError, "handle_BPR called with no contact list" # debug
             self.factory.contacts.version = int(params[0])
             userHandle, phoneType, number = params[1], params[2], unquote(params[3])
             self.factory.contacts.getContact(userHandle).setPhone(phoneType, number)
@@ -1162,7 +1302,7 @@ class NotificationClient(MSNEventBase):
     def handle_ADC(self, params):
         if not self.factory.contacts: raise MSNProtocolError, "handle_ADC called with no contact list"
         numParams = len(params)
-        if numParams < 3 or params[1].upper() not in ('AL','BL','RL','FL', 'PL'):
+        if numParams < 3 or params[1].upper() not in ('AL','BL','RL','FL','PL'):
             raise MSNProtocolError, "Invalid Paramaters for ADC" # debug
         id = int(params[0])
         listType = params[1].lower()
@@ -1182,7 +1322,7 @@ class NotificationClient(MSNEventBase):
     def handle_REM(self, params):
         if not self.factory.contacts: raise MSNProtocolError, "handle_REM called with no contact list available!"
         numParams = len(params)
-        if numParams < 3 or params[1].upper() not in ('AL','BL','FL','RL'):
+        if numParams < 3 or params[1].upper() not in ('AL','BL','FL','RL','PL'):
             raise MSNProtocolError, "Invalid Paramaters for REM" # debug
         id = int(params[0])
         listType = params[1].lower()
@@ -1241,7 +1381,7 @@ class NotificationClient(MSNEventBase):
             self.currentMessage = MSNMessage(length=messageLen, userHandle=params[0], screenName="UBX", specialMessage=True)
             self.setRawMode()
         else:
-            self.contactPersonalChanged(params[0], '')
+            self._gotUBX(MSNMessage(userHandle=params[0]))
 
     def handle_UUX(self, params):
         checkParamLen(len(params), 2, 'UUX')
@@ -1251,6 +1391,7 @@ class NotificationClient(MSNEventBase):
 
     def handle_OUT(self, params):
         checkParamLen(len(params), 1, 'OUT')
+        self.factory.stopTrying()
         if params[0] == "OTH": self.multipleLogin()
         elif params[0] == "SSD": self.serverGoingDown()
         else: raise MSNProtocolError, "Invalid Parameters received for OUT" # debug
@@ -1323,7 +1464,7 @@ class NotificationClient(MSNEventBase):
         contact.
 
         @param userHandle: contact who's msnobj has been changed
-        @param hash: sha1 hash of their avatar
+        @param hash: sha1 hash of their avatar as hex string
         """
 
     def statusChanged(self, statusCode):
@@ -1335,22 +1476,22 @@ class NotificationClient(MSNEventBase):
         """
         pass
 
-    def gotContactStatus(self, statusCode, userHandle, screenName):
+    def gotContactStatus(self, userHandle, statusCode, screenName):
         """
         Called when we receive a list of statuses upon login.
 
-        @param statusCode: 3-letter status code
         @param userHandle: the contact's user handle (passport)
+        @param statusCode: 3-letter status code
         @param screenName: the contact's screen name
         """
         pass
 
-    def contactStatusChanged(self, statusCode, userHandle, screenName):
+    def contactStatusChanged(self, userHandle, statusCode, screenName):
         """
         Called when we're notified that a contact's status has changed.
 
-        @param statusCode: 3-letter status code
         @param userHandle: the contact's user handle (passport)
+        @param statusCode: 3-letter status code
         @param screenName: the contact's screen name
         """
         pass
@@ -1709,6 +1850,7 @@ class NotificationClient(MSNEventBase):
             c = self.factory.contacts.getContact(r[2])
             if not c:
                 c = MSNContact(userGuid=r[1], userHandle=r[2], screenName=r[3])
+                self.factory.contacts.addContact(c)
             #if r[3]: c.groups.append(r[3])
             c.addToList(r[0])
             return r
@@ -1775,14 +1917,13 @@ class NotificationClient(MSNEventBase):
 
         @return: A Deferred, the callback for which will be called
                  when the server acknowledges the change.
-                 The callback argument will be a tuple of 1 element,
-                 the new screen name.
+                 The callback argument will be an empty tuple.
         """
 
         id, d = self._createIDMapping()
         self.sendLine("PRP %s MFN %s" % (id, quote(newName)))
         def _cb(r):
-            self.factory.screenName = r[0]
+            self.factory.screenName = newName
             return r
         return d.addCallback(_cb)
 
@@ -1856,9 +1997,11 @@ class NotificationClient(MSNEventBase):
         if self.pingCheckTask:
             self.pingCheckTask.stop()
             self.pingCheckTask = None
+        self.factory.stopTrying()
         self.sendLine("OUT")
+        self.transport.loseConnection()
 
-class NotificationFactory(ClientFactory):
+class NotificationFactory(ReconnectingClientFactory):
     """
     Factory for the NotificationClient protocol.
     This is basically responsible for keeping
@@ -1881,6 +2024,8 @@ class NotificationFactory(ClientFactory):
                           (the whole URL is required)
     @ivar status: The status of the client -- this is generally kept
                   up to date by the default command handlers
+    @ivar maxRetries: The number of times the factory will reconnect
+                      if the connection dies because of a network error.
     """
 
     contacts = None
@@ -1888,15 +2033,11 @@ class NotificationFactory(ClientFactory):
     screenName = ''
     password = ''
     passportServer = 'https://nexus.passport.com/rdr/pprdr.asp'
-    status = 'FLN'
+    status = 'NLN'
     protocol = NotificationClient
+    maxRetries = 5
 
 
-# XXX: A lot of the state currently kept in
-# instances of SwitchboardClient is likely to
-# be moved into a factory at some stage in the
-# future
-
 class SwitchboardClient(MSNEventBase):
     """
     This class provides support for clients connecting to a switchboard server.
@@ -1917,21 +2058,23 @@ class SwitchboardClient(MSNEventBase):
                      to a switchboard invitation
     @ivar reply: set this to 1 in connectionMade or before to signifiy
                  that you are replying to a switchboard invitation.
+    @ivar msnobj: the MSNObject for the user's avatar. So that the
+                  switchboard can distribute it to anyone who asks.
     """
 
     key = 0
     userHandle = ""
     sessionID = ""
     reply = 0
+    msnobj = None
 
     _iCookie = 0
 
-    def __init__(self, msnobj=None):
+    def __init__(self):
         MSNEventBase.__init__(self)
         self.pendingUsers = {}
         self.cookies = {'iCookies' : {}} # will maybe be moved to a factory in the future
         self.slpLinks = {}
-        self.msnobj = msnobj
 
     def connectionMade(self):
         MSNEventBase.connectionMade(self)
@@ -1960,7 +2103,7 @@ class SwitchboardClient(MSNEventBase):
     def _checkTyping(self, message, cTypes):
         """ helper method for checkMessage """
         if 'text/x-msmsgscontrol' in cTypes and message.hasHeader('TypingUser'):
-            self.userTyping(message)
+            self.gotContactTyping(message)
             return 1
 
     def _checkFileInvitation(self, message, info):
@@ -1970,93 +2113,66 @@ class SwitchboardClient(MSNEventBase):
             cookie = info['Invitation-Cookie']
             filename = info['Application-File']
             filesize = int(info['Application-FileSize'])
-            connectivity = (info.get('Connectivity').lower() == 'y')
+            connectivity = (info.get('Connectivity', 'n').lower() == 'y')
         except KeyError:
             log.msg('Received munged file transfer request ... ignoring.')
             return 0
-        import msnft
+        raise NotImplementedError
         self.gotSendRequest(msnft.MSNFTP_Receive(filename, filesize, message.userHandle, cookie, connectivity, self))
         return 1
 
-    def _checkP2PMessage(self, message, cTypes):
+    def _handleP2PMessage(self, message):
         """ helper method for msnslp messages (file transfer & avatars) """
+        if not message.getHeader("P2P-Dest") == self.userHandle: return
         packet = message.message
-        binaryFields = msnp2p.BinaryFields(packet=packet)
-        if binaryFields[0] != 0:
-            slpLink = self.slpLinks[binaryFields[0]]
+        binaryFields = BinaryFields(packet=packet)
+        if binaryFields[5] == BinaryFields.BYEGOT:
+            pass # Ignore the ACKs to SLP messages
+        elif binaryFields[0] != 0:
+            slpLink = self.slpLinks.get(binaryFields[0])
+            if not slpLink:
+                # Link has been killed. Ignore
+                return
             if slpLink.remoteUser == message.userHandle:
                 slpLink.handlePacket(packet)
-        elif binaryFields[5] == BinaryFields.ACK or binaryFields[5] == BinaryFields.BYEGOT:
-            pass # Ignore the ACKs
+        elif binaryFields[5] == BinaryFields.ACK:
+            pass # Ignore the ACKs to SLP messages
         else:
-            slpMessage = msnp2p.MSNSLPMessage(packet)
+            slpMessage = MSNSLPMessage(packet)
             slpLink = None
-            if slpMessage.method == "INVITE":
+            # Always try and give a slpMessage to a slpLink first.
+            # If none can be found, and it was INVITE, then create
+            # one to handle the session.
+            for slpLink in self.slpLinks.values():
+                if slpLink.sessionGuid == slpMessage.sessionGuid:
+                    slpLink.handleSLPMessage(slpMessage)
+                    break
+            else:
+                slpLink = None # Was not handled
+
+            if not slpLink and slpMessage.method == "INVITE":
                 if slpMessage.euf_guid == MSN_MSNFTP_GUID:
-                    slpLink = msnp2p.SLPLink_Receive(slpMessage.fro, slpMessage.sessionID, slpMessage.sessionGuid)
-                    context = msnp2p.FileContext(slpMessage.context)
-                    fileReceive = msnft.MSNP2P_Receive(context.filename, context.filesize, slpMessage.fro, self)
+                    context = FileContext(slpMessage.context)
+                    slpLink = SLPLink_FileReceive(remoteUser=slpMessage.fro, switchboard=self, filename=context.filename, filesize=context.filesize, sessionID=slpMessage.sessionID, sessionGuid=slpMessage.sessionGuid, branch=slpMessage.branch)
+                    self.slpLinks[slpMessage.sessionID] = slpLink
+                    self.gotFileReceive(slpLink)
                 elif slpMessage.euf_guid == MSN_AVATAR_GUID:
-                    slpLink = msnp2p.SLPLink_Send(slpMessage.fro, slpMessage.sessionID, slpMessage.sessionGuid)
-                    self._sendMSNSLPResponse(slpLink, "200 OK")
+                    # Check that we have an avatar to send
+                    if self.msnobj:
+                        slpLink = SLPLink_AvatarSend(remoteUser=slpMessage.fro, switchboard=self, filesize=self.msnobj.size, sessionID=slpMessage.sessionID, sessionGuid=slpMessage.sessionGuid)
+                        slpLink.write(self.msnobj.imageData)
+                        slpLink.close()
+                    else:
+                        # They shouldn't have sent a request if we have
+                        # no avatar. So we'll just ignore them.
+                        # FIXME We should really send an error
+                        pass
                 if slpLink:
                     self.slpLinks[slpMessage.sessionID] = slpLink
-            else:
-                if slpMessage.status != "200":
-                    for slpLink in self.slpLinks:
-                        if slpLink.sessionGuid == slpMessage.sessionGuid:
-                            del self.slpLinks[slpLink.sessionID]
-                    if slpMessage.method != "BYE":
-                        # Must be an error. If its a file transfer we need to signal that it failed
-                        slpLink.transferError()
-                else:
-                    slpLink = self.slpLinks[slpMessage.sessionID]
-                    slpLink.transferReady()
             if slpLink:
                 # Always need to ACK these packets if we can
-                self._sendP2PACK(self, slpLink, binaryHeaders)
-            
-        return 1
-
-    def _sendP2PACK(self, slpLink, ackHeaders):
-        binaryFields = msnp2p.BinaryFields()
-        binaryFields[1] = slpLink.nextBaseID()
-        binaryFields[3] = ackHeaders[3]
-        binaryFields[5] = BinaryFields.ACK
-        binaryFields[6] = ackHeaders[1]
-        binaryFields[7] = ackHeaders[6]
-        binaryFields[8] = ackHeaders[3]
-        self._sendP2PMessage(binaryFields, "")
-
-    def _sendMSNSLPInvite(self, slpLink, guid, context):
-        msg = msnp2p.MSNSLP_Message()
-        msg.create(method="INVITE", to=slpLink.remoteUser, fro=self.userHandle, cseq=0, sessionGuid=slpLink.sessionGuid)
-        msg.setData(sessionID=slpLink.sessionID, appID="1", guid=guid, context=msnp2p.b64enc(context))
-        self._sendMSNSLPMessage(slpLink, msg)
-
-    def _sendMSNSLPResponse(self, slpLink, response):
-        msg = msnp2p.MSNSLPMessage()
-        msg.create(status=response, to=slpLink.remoteUser, fro=self.userHandle, cseq=1, sessionGuid=slpLink.sessionGuid)
-        msg.setData(sessionID=slpLink.sessionID)
-        self._sendMSNSLPMessage(slpLink, msg)
-
-    def _sendMSNSLPMessage(self, slpLink, msnSlpMessage):
-        msgStr = str(msg)
-        binaryFields = msnp2p.BinaryFields()
-        binaryFields[1] = slpLink.nextBaseID()
-        binaryFields[3] = len(msgStr)
-        binaryFields[4] = binaryFields[3]
-        binaryFields[6] = random.randint(0, sys.maxint)
-        self._sendP2PMessage(binaryFields, msgStr)
+                slpLink.sendP2PACK(binaryFields)
 
-    def _sendP2PMessage(self, binaryFields, msgStr):
-        packet = binaryFields.packHeaders() + msgStr + binaryFields.packFooter()
-
-        message = MSNMessage(message=packet)
-        message.setHeader("Content-Type", "application/x-msnmsgrp2p")
-        message.setHeader("P2P-Dest", handler.to)
-        message.ack = MSNMessage.MESSAGE_ACK_FAT
-        self.sendMessage(message)
 
     def checkMessage(self, message):
         """
@@ -2065,17 +2181,18 @@ class SwitchboardClient(MSNEventBase):
         """
         cTypes = [s.lstrip() for s in message.getHeader('Content-Type').split(';')]
         if self._checkTyping(message, cTypes): return 0
-        if 'text/x-msmsgsinvite' in cTypes:
+#        if 'text/x-msmsgsinvite' in cTypes:
             # header like info is sent as part of the message body.
-            info = {}
-            for line in message.message.split('\r\n'):
-                try:
-                    key, val = line.split(':')
-                    info[key] = val.lstrip()
-                except ValueError: continue
-            if self._checkFileInvitation(message, info): return 0
-        if 'application/x-msnmsgrp2p' in cTypes:
-            if self._checkP2PMessage(message, cTypes): return 0
+#            info = {}
+#            for line in message.message.split('\r\n'):
+#                try:
+#                    key, val = line.split(':')
+#                    info[key] = val.lstrip()
+#                except ValueError: continue
+#            if self._checkFileInvitation(message, info): return 0
+        elif 'application/x-msnmsgrp2p' in cTypes:
+            self._handleP2PMessage(message)
+            return 0
         return 1
 
     # negotiation
@@ -2165,14 +2282,15 @@ class SwitchboardClient(MSNEventBase):
         """
         pass
 
-    def gotAvatarImage(self, userHandle, image):
+    def gotFileReceive(self, fileReceive):
         """
-        called when we receive an avatar from a contact
+        called when we receive a file send request from a contact.
+        Default action is to reject the file.
 
-        @param userHandle: the person who's avatar we have got
-        @param image: the avatar image
+        @param fileReceive: msnft.MSNFTReceive_Base instance
         """
-        pass
+        fileReceive.reject()
+
 
     def gotSendRequest(self, fileReceive):
         """
@@ -2182,10 +2300,10 @@ class SwitchboardClient(MSNEventBase):
         """
         pass
 
-    def userTyping(self, message):
+    def gotContactTyping(self, message):
         """
         called when we receive the special type of message notifying
-        us that a user is typing a message.
+        us that a contact is typing a message.
 
         @param message: the associated MSNMessage object
         """
@@ -2229,27 +2347,71 @@ class SwitchboardClient(MSNEventBase):
         else: id, d = self._createIDMapping()
         if message.length == 0: message.length = message._calcMessageLen()
         self.sendLine("MSG %s %s %s" % (id, message.ack, message.length))
-        # apparently order matters with at least MIME-Version and Content-Type
-        self.sendLine('MIME-Version: %s' % message.getHeader('MIME-Version'))
-        self.sendLine('Content-Type: %s' % message.getHeader('Content-Type'))
+        # Apparently order matters with these
+        orderMatters = ("MIME-Version", "Content-Type", "Message-ID")
+        for header in orderMatters:
+            if message.hasHeader(header):
+                self.sendLine("%s: %s" % (header, message.getHeader(header)))
         # send the rest of the headers
-        for header in [h for h in message.headers.items() if h[0].lower() not in ('mime-version','content-type')]:
+        for header in [h for h in message.headers.items() if h[0] not in orderMatters]:
             self.sendLine("%s: %s" % (header[0], header[1]))
-        self.transport.write(CR+LF)
+        self.transport.write("\r\n")
         self.transport.write(message.message)
         if MESSAGEDEBUG: log.msg(message.message)
         return d
 
-    def sendAvatarRequest(self, userHandle, msnobj):
-        pass
+    def sendAvatarRequest(self, msnContact):
+        """
+        used to request an avatar from a user in this switchboard
+        session.
+
+        @param msnContact: the msnContact object to request an avatar for
+
+        @return: A Deferred, the callback for which will be called
+                 when the avatar transfer succeeds.
+                 The callback argument will be a tuple with one element,
+                 the PNG avatar data.
+        """
+        if not msnContact.msnobj: return
+        d = Deferred()
+        def bufferClosed(data):
+            d.callback((data,))
+        buffer = StringBuffer(bufferClosed)
+        buffer.error = lambda: None
+        slpLink = SLPLink_AvatarReceive(remoteUser=msnContact.userHandle, switchboard=self, consumer=buffer, context=msnContact.msnobj.text)
+        self.slpLinks[slpLink.sessionID] = slpLink
+        return d
+
+    def sendFile(self, msnContact, filename, filesize):
+        """
+        used to send a file to a contact.
+
+        @param msnContact: the MSNContact object to send a file to.
+        @param filename: the name of the file to send.
+        @param filesize: the size of the file to send.
+        
+        @return: (fileSend, d) A FileSend object and a Deferred.
+                 The Deferred will pass one argument in a tuple,
+                 whether or not the transfer is accepted. If you
+                 receive a True, then you can call write() on the
+                 fileSend object to send your file. Call close()
+                 when the file is done.
+                 NOTE: You MUST write() exactly as much as you
+                 declare in filesize.
+        """
+        if not msnContact.userHandle: return
+        # FIXME, check msnContact.caps to see if we should use old-style
+        fileSend = SLPLink_FileSend(remoteUser=msnContact.userHandle, switchboard=self, filename=filename, filesize=filesize)
+        self.slpLinks[fileSend.sessionID] = fileSend
+        return fileSend, fileSend.acceptDeferred
 
     def sendTypingNotification(self):
         """
-        used to send a typing notification. Upon receiving this
+        Used to send a typing notification. Upon receiving this
         message the official client will display a 'user is typing'
         message to all other users in the chat session for 10 seconds.
-        The official client sends one of these every 5 seconds (I think)
-        as long as you continue to type.
+        You should send one of these every 5 seconds as long as the
+        user is typing.
         """
         m = MSNMessage()
         m.ack = m.MESSAGE_ACK_NONE
@@ -2316,107 +2478,564 @@ class SwitchboardClient(MSNEventBase):
         m.ack = m.MESSAGE_NACK
         self.sendMessage(m)
 
-class FileSend(LineReceiver):
-    """
-    This class provides support for sending files to other contacts.
-
-    @ivar bytesSent: the number of bytes that have currently been sent.
-    @ivar completed: true if the send has completed.
-    @ivar connected: true if a connection has been established.
-    @ivar targetUser: the target user (contact).
-    @ivar segmentSize: the segment (block) size.
-    @ivar auth: the auth cookie (number) to use when sending the
-                transfer invitation
-    """
+
+class FileReceive:
+    def __init__(self, filename, filesize, userHandle):
+        self.consumer = None
+        self.finished = False
+        self.error = False
+        self.buffer = []
+        self.filename, self.filesize, self.userHandle = filename, filesize, userHandle
+
+    def reject(self):
+        raise NotImplementedError
+
+    def accept(self, consumer):
+        if self.consumer: raise "AlreadyAccepted"
+        self.consumer = consumer
+        for data in self.buffer:
+            self.consumer.write(data)
+        self.buffer = None
+        if self.finished:
+            self.consumer.close()
+        if self.error:
+            self.consumer.error()
+
+    def write(self, data):
+        if self.error or self.finished:
+            raise IOError, "Attempt to write in an invalid state"
+        if self.consumer:
+            self.consumer.write(data)
+        else:
+            self.buffer.append(data)
+
+    def close(self):
+        self.finished = True
+        if self.consumer:
+            self.consumer.close()
+
+class FileContext:
+    """ Represents the Context field for P2P file transfers """
+    def __init__(self, data=""):
+        if data:
+            self.parse(data)
+        else:
+            self.filename = ""
+            self.filesize = 0
+
+    def pack(self):
+        if MSNP2PDEBUG: log.msg("FileContext packing:", self.filename, self.filesize)
+        data = struct.pack("<LLQL", 638, 0x03, self.filesize, 0x01)
+        data = data[:-1] # Uck, weird, but it works
+        data += utf16net(self.filename)
+        data = ljust(data, 570, '\0')
+        data += struct.pack("<L", 0xFFFFFFFFL)
+        data = ljust(data, 638, '\0')
+        return data
+
+    def parse(self, packet):
+        self.filesize = struct.unpack("<Q", packet[8:16])[0]
+        chunk = packet[19:540]
+        chunk = chunk[:chunk.find('\x00\x00')]
+        self.filename = unicode((codecs.BOM_UTF16_BE + chunk).decode("utf-16"))
+        if MSNP2PDEBUG: log.msg("FileContext parsed:", self.filesize, self.filename)
+
+
+class BinaryFields:
+    """ Utility class for the binary header & footer in p2p messages """
+    ACK = 0x02
+    WAIT = 0x04
+    ERR = 0x08
+    DATA = 0x20
+    BYEGOT = 0x40
+    BYESENT = 0x80
+    DATAFT = 0x1000030
+
+    def __init__(self, fields=None, packet=None):
+        if fields:
+            self.fields = fields
+        else:
+            self.fields = [0] * 10
+            if packet:
+                self.unpackFields(packet)
     
-    def __init__(self, file):
-        """
-        @param file: A string or file object represnting the file to send.
-        """
+    def __getitem__(self, key):
+        return self.fields[key]
+    
+    def __setitem__(self, key, value):
+        self.fields[key] = value
+    
+    def unpackFields(self, packet):
+        self.fields = struct.unpack("<LLQQLLLLQ", packet[0:48])
+        self.fields += struct.unpack(">L", packet[len(packet)-4:])
+        if MSNP2PDEBUG:
+            out = "Unpacked fields: "
+            for i in self.fields:
+                out += hex(i) + ' '
+            log.msg(out)
+    
+    def packHeaders(self):
+        f = tuple(self.fields)
+        if MSNP2PDEBUG:
+            out = "Packed fields: "
+            for i in self.fields:
+                out += hex(i) + ' '
+            log.msg(out)
+        return struct.pack("<LLQQLLLLQ", f[0], f[1], f[2], f[3], f[4], f[5], f[6], f[7], f[8])
+    
+    def packFooter(self):
+        return struct.pack(">L", self.fields[9])
+    
+
+class MSNSLPMessage:
+    """ Representation of a single MSNSLP message """
+    def __init__(self, packet=None):
+        self.method = ""
+        self.status = ""
+        self.to = ""
+        self.fro = ""
+        self.branch = ""
+        self.cseq = 0
+        self.sessionGuid = ""
+        self.sessionID = None
+        self.euf_guid = ""
+        self.data = "\r\n" + chr(0)
+        if packet:
+            self.parse(packet)
+
+    def create(self, method=None, status=None, to=None, fro=None, branch=None, cseq=0, sessionGuid=None, data=None):
+        self.method = method
+        self.status = status
+        self.to = to
+        self.fro = fro
+        self.branch = branch
+        self.cseq = cseq
+        self.sessionGuid = sessionGuid
+        if data: self.data = data
+    
+    def setData(self, ctype, data):
+        self.ctype = ctype
+        s = []
+        order = ["EUF-GUID", "SessionID", "AppID", "Context", "Bridge", "Listening","Bridges", "NetID", "Conn-Type", "UPnPNat", "ICF", "Hashed-Nonce"]
+        for key in order:
+            if key == "Context" and data.has_key(key):
+                s.append("Context: %s\r\n" % b64enc(data[key]))
+            elif data.has_key(key):
+                s.append("%s: %s\r\n" % (key, str(data[key])))
+        s.append("\r\n"+chr(0))
+        
+        self.data = "".join(s)
+    
+    def parse(self, s):
+        s = s[48:len(s)-4:]
+        if s.find("MSNSLP/1.0") < 0: return
 
-        if isinstance(file, types.StringType):
-            self.file = open(file, 'rb')
+        lines = s.split("\r\n")
+        
+        # Get the MSNSLP method or status
+        msnslp = lines[0].split(" ")
+        if MSNP2PDEBUG: log.msg("Parsing MSNSLPMessage %s %s" % (len(s), s))
+        if msnslp[0] in ("INVITE", "BYE"):
+            self.method = msnslp[0].strip()
         else:
-            self.file = file
+            self.status = msnslp[1].strip()
 
-        self.fileSize = 0
-        self.bytesSent = 0
-        self.completed = 0
-        self.connected = 0
-        self.targetUser = None
-        self.segmentSize = 2045
-        self.auth = random.randint(0, 2**30)
-        self._pendingSend = None # :(
+        lines.remove(lines[0])
+        
+        for line in lines:
+            line = line.split(":")
+            if len(line) < 1: continue
+            try:
+                if len(line) > 2 and line[0] == "To":
+                    self.to = line[2][:line[2].find('>')]
+                elif len(line) > 2 and line[0] == "From":
+                    self.fro = line[2][:line[2].find('>')]
+                elif line[0] == "Call-ID":
+                    self.sessionGuid = line[1].strip()
+                elif line[0] == "CSeq":
+                    self.cseq = int(line[1].strip())
+                elif line[0] == "SessionID":
+                    self.sessionID = int(line[1].strip())
+                elif line[0] == "EUF-GUID":
+                    self.euf_guid = line[1].strip()
+                elif line[0] == "Content-Type":
+                    self.ctype = line[1].strip()
+                elif line[0] == "Context":
+                    self.context = b64dec(line[1])
+                elif line[0] == "Via":
+                    self.branch = line[1].split(";")[1].split("=")[1].strip()
+            except:
+                if MSNP2PDEBUG:
+                    log.msg("Error parsing MSNSLP message.")
+                    raise
+
+    def __str__(self):
+        s = []
+        if self.method:
+           s.append("%s MSNMSGR:%s MSNSLP/1.0\r\n" % (self.method, self.to))
+        else:
+            if   self.status == "200": status = "200 OK"
+            elif self.status == "603": status = "603 Decline"
+            s.append("MSNSLP/1.0 %s\r\n" % status)
+        s.append("To: <msnmsgr:%s>\r\n" % self.to)
+        s.append("From: <msnmsgr:%s>\r\n" % self.fro)
+        s.append("Via: MSNSLP/1.0/TLP ;branch=%s\r\n" % self.branch)
+        s.append("CSeq: %s \r\n" % str(self.cseq))
+        s.append("Call-ID: %s\r\n" % self.sessionGuid)
+        s.append("Max-Forwards: 0\r\n")
+        s.append("Content-Type: %s\r\n" % self.ctype)
+        s.append("Content-Length: %s\r\n\r\n" % len(self.data))
+        s.append(self.data)
+        return "".join(s)
+
+class SeqID:
+    """ Utility for handling the weird sequence IDs in p2p messages """
+    def __init__(self, baseID=None):
+        if baseID:
+            self.baseID = baseID
+        else:
+            self.baseID = random.randint(1000, sys.maxint)
+        self.pos = -1
 
-    def connectionMade(self):
-        self.connected = 1
+    def get(self):
+        return p2pseq(self.pos) + self.baseID
+    
+    def next(self):
+        self.pos += 1
+        return self.get()
+    
 
-    def connectionLost(self, reason):
-        if self._pendingSend:
-            self._pendingSend.cancel()
-            self._pendingSend = None
-        self.connected = 0
-        self.file.close()
+class StringBuffer(StringIO.StringIO):
+    def __init__(self, notifyFunc=None):
+        self.notifyFunc = notifyFunc
+        StringIO.StringIO.__init__(self)
+
+    def close(self):
+        if self.notifyFunc:
+            self.notifyFunc(self.getvalue())
+            self.notifyFunc = None
+        StringIO.StringIO.close(self)
+
+
+class SLPLink:
+    def __init__(self, remoteUser, switchboard, sessionID, sessionGuid):
+        self.dataFlag = 0
+        if not sessionID:
+            sessionID = random.randint(1000, sys.maxint)
+        if not sessionGuid:
+            sessionGuid = random_guid()
+        self.remoteUser = remoteUser
+        self.switchboard = switchboard
+        self.sessionID = sessionID
+        self.sessionGuid = sessionGuid
+        self.seqID = SeqID()
+
+    def killLink(self):
+        if MSNP2PDEBUG: log.msg("killLink")
+        def kill():
+            if MSNP2PDEBUG: log.msg("killLink - kill()")
+            if not self.switchboard: return
+            del self.switchboard.slpLinks[self.sessionID]
+            self.switchboard = None
+        # This is so that handleP2PMessage can still use the SLPLink
+        # one last time, for ACKing BYEs and 601s.
+        reactor.callLater(0, kill)
+
+    def warn(self, text):
+        log.msg("Warning in transfer: %s %s" % (self, text))
+
+    def sendP2PACK(self, ackHeaders):
+        binaryFields = BinaryFields()
+        binaryFields[0] = ackHeaders[0]
+        binaryFields[1] = self.seqID.next()
+        binaryFields[3] = ackHeaders[3]
+        binaryFields[5] = BinaryFields.ACK
+        binaryFields[6] = ackHeaders[1]
+        binaryFields[7] = ackHeaders[6]
+        binaryFields[8] = ackHeaders[3]
+        self.sendP2PMessage(binaryFields, "")
 
-    def lineReceived(self, line):
-        temp = line.split(' ')
-        if len(temp) == 1: params = []
-        else: params = temp[1:]
-        cmd = temp[0]
-        handler = getattr(self, "handle_%s" % cmd.upper(), None)
-        if handler: handler(params)
-        else: self.handle_UNKNOWN(cmd, params)
+    def sendSLPMessage(self, cmd, ctype, data, branch=None):
+        msg = MSNSLPMessage()
+        if cmd.isdigit():
+            msg.create(status=cmd, to=self.remoteUser, fro=self.switchboard.userHandle, branch=branch, cseq=1, sessionGuid=self.sessionGuid)
+        else:
+            msg.create(method=cmd, to=self.remoteUser, fro=self.switchboard.userHandle, branch=random_guid(), cseq=0, sessionGuid=self.sessionGuid)
+        msg.setData(ctype, data)
+        msgStr = str(msg)
+        binaryFields = BinaryFields()
+        binaryFields[1] = self.seqID.next()
+        binaryFields[3] = len(msgStr)
+        binaryFields[4] = binaryFields[3]
+        binaryFields[6] = random.randint(1000, sys.maxint)
+        self.sendP2PMessage(binaryFields, msgStr)
 
-    def handle_VER(self, params):
-        checkParamLen(len(params), 1, 'VER')
-        if params[0].upper() == "MSNFTP":
-            self.sendLine("VER MSNFTP")
-        else: # they sent some weird version during negotiation, i'm quitting.
-            self.transport.loseConnection()
+    def sendP2PMessage(self, binaryFields, msgStr):
+        packet = binaryFields.packHeaders() + msgStr + binaryFields.packFooter()
 
-    def handle_USR(self, params):
-        checkParamLen(len(params), 2, 'USR')
-        self.targetUser = params[0]
-        if self.auth == int(params[1]):
-            self.sendLine("FIL %s" % (self.fileSize))
-        else: # they failed the auth test, disconnecting.
-            self.transport.loseConnection()
+        message = MSNMessage(message=packet)
+        message.setHeader("Content-Type", "application/x-msnmsgrp2p")
+        message.setHeader("P2P-Dest", self.remoteUser)
+        message.ack = MSNMessage.MESSAGE_ACK_FAT
+        self.switchboard.sendMessage(message)
+
+    def handleSLPMessage(self, slpMessage):
+        raise NotImplementedError
 
-    def handle_TFR(self, params):
-        checkParamLen(len(params), 0, 'TFR')
-        # they are ready for me to start sending
-        self.sendPart()
 
-    def handle_BYE(self, params):
-        self.completed = (self.bytesSent == self.fileSize)
-        self.transport.loseConnection()
 
-    def handle_CCL(self, params):
-        self.completed = (self.bytesSent == self.fileSize)
-        self.transport.loseConnection()
+    
+
+class SLPLink_Send(SLPLink):
+    def __init__(self, remoteUser, switchboard, filesize, sessionID=None, sessionGuid=None):
+        SLPLink.__init__(self, remoteUser, switchboard, sessionID, sessionGuid)
+        self.handlePacket = None
+        self.offset = 0
+        self.filesize = filesize
+        self.data = ""
+    
+    def send_dataprep(self):
+        if MSNP2PDEBUG: log.msg("send_dataprep")
+        binaryFields = BinaryFields()
+        binaryFields[0] = self.sessionID
+        binaryFields[1] = self.seqID.next()
+        binaryFields[3] = 4
+        binaryFields[4] = 4
+        binaryFields[6] = random.randint(1000, sys.maxint)
+        binaryFields[9] = 1
+        self.sendP2PMessage(binaryFields, chr(0) * 4)
+
+    def write(self, data):
+        if MSNP2PDEBUG: log.msg("write")
+        i = 0
+        data = self.data + data
+        self.data = ""
+        length = len(data)
+        while i < length:
+            if i + 1202 < length:
+                self._writeChunk(data[i:i+1202])
+                i += 1202
+            else:
+                self.data = data[i:]
+                return
+
+    def _writeChunk(self, chunk):
+        if MSNP2PDEBUG: log.msg("writing chunk")
+        binaryFields = BinaryFields()
+        binaryFields[0] = self.sessionID
+        if self.offset == 0:
+            binaryFields[1] = self.seqID.next()
+        else:
+            binaryFields[1] = self.seqID.get()
+        binaryFields[2] = self.offset
+        binaryFields[3] = self.filesize
+        binaryFields[4] = len(chunk)
+        binaryFields[5] = self.dataFlag
+        binaryFields[6] = random.randint(1000, sys.maxint)
+        binaryFields[9] = 1
+        self.offset += len(chunk)
+        self.sendP2PMessage(binaryFields, chunk)
+    
+    def close(self):
+        if self.data:
+            self._writeChunk(self.data)
+        #self.killLink()
+    
+    def error(self):
+        pass
+        # FIXME, should send 601 or something
+
+class SLPLink_FileSend(SLPLink_Send):
+    def __init__(self, remoteUser, switchboard, filename, filesize):
+        SLPLink_Send.__init__(self, remoteUser=remoteUser, switchboard=switchboard, filesize=filesize)
+        self.dataFlag = BinaryFields.DATAFT
+        # Send invite & wait for 200OK before sending dataprep
+        context = FileContext()
+        context.filename = filename
+        context.filesize = filesize
+        data = {"EUF-GUID" : MSN_MSNFTP_GUID,\
+                "SessionID": self.sessionID,\
+                "AppID"    : 2,\
+                "Context"  : context.pack() }
+        self.sendSLPMessage("INVITE", "application/x-msnmsgr-sessionreqbody", data)
+        self.acceptDeferred = Deferred()
+
+    def handleSLPMessage(self, slpMessage):
+        if slpMessage.status == "200":
+            if slpMessage.ctype == "application/x-msnmsgr-sessionreqbody":
+                data = {"Bridges"     : "TRUDPv1 TCPv1",\
+                        "NetID"       : "0",\
+                        "Conn-Type"   : "Firewall",\
+                        "UPnPNat"     : "false",\
+                        "ICF"         : "true",}
+                        #"Hashed-Nonce": random_guid()}
+                self.sendSLPMessage("INVITE", "application/x-msnmsgr-transreqbody", data)
+            elif slpMessage.ctype == "application/x-msnmsgr-transrespbody":
+                self.acceptDeferred.callback((True,))
+                self.handlePacket = self.wait_data_ack
+        else:
+            if slpMessage.status == "603":
+                self.acceptDeferred.callback((False,))
+            if MSNP2PDEBUG: log.msg("SLPLink is over due to decline, error or BYE")
+            self.killLink()
+
+    def wait_data_ack(self, packet):
+        if MSNP2PDEBUG: log.msg("wait_data_ack")
+        binaryFields = BinaryFields()
+        binaryFields.unpackFields(packet)
+
+        if binaryFields[5] != BinaryFields.ACK:
+            self.warn("field5," + str(binaryFields[5]))
+            return
 
-    def handle_UNKNOWN(self, cmd, params): log.msg('received unknown command (%s), params: %s' % (cmd, params))
+        self.sendSLPMessage("BYE", "application/x-msnmsgr-sessionclosebody", {})
+        self.handlePacket = None
 
-    def makeHeader(self, size):
-        """ make the appropriate header given a specific segment size. """
-        quotient, remainder = divmod(size, 256)
-        return chr(0) + chr(remainder) + chr(quotient)
+    def close(self):
+        self.handlePacket = self.wait_data_ack
+        SLPLink_Send.close(self)
 
-    def sendPart(self):
-        """ send a segment of data """
-        if not self.connected:
-            self._pendingSend = None
-            return # may be buggy (if handle_CCL/BYE is called but self.connected is still 1)
-        data = self.file.read(self.segmentSize)
-        if data:
-            dataSize = len(data)
-            header = self.makeHeader(dataSize)
-            self.transport.write(header + data)
-            self.bytesSent += dataSize
-            self._pendingSend = reactor.callLater(0, self.sendPart)
+
+class SLPLink_AvatarSend(SLPLink_Send):
+    def __init__(self, remoteUser, switchboard, filesize, sessionID=None, sessionGuid=None):
+        SLPLink_Send.__init__(self, remoteUser=remoteUser, switchboard=switchboard, filesize=filesize, sessionID=sessionID, sessionGuid=sessionGuid)
+        self.dataFlag = BinaryFields.DATA
+        self.sendSLPMessage("200", "application/x-msnmsgr-sessionreqbody", {"SessionID":self.sessionID})
+        self.send_dataprep()
+        self.handlePacket = lambda packet: None
+
+    def handleSLPMessage(self, slpMessage):
+        if MSNP2PDEBUG: log.msg("BYE or error")
+        self.killLink()
+
+    def close(self):
+        SLPLink_Send.close(self)
+        # Keep the link open to wait for a BYE
+
+class SLPLink_Receive(SLPLink):
+    def __init__(self, remoteUser, switchboard, consumer, context=None, sessionID=None, sessionGuid=None):
+        SLPLink.__init__(self, remoteUser, switchboard, sessionID, sessionGuid)
+        self.handlePacket = None
+        self.consumer = consumer
+        self.pos = 0
+
+    def wait_dataprep(self, packet):
+        if MSNP2PDEBUG: log.msg("wait_dataprep")
+        binaryFields = BinaryFields()
+        binaryFields.unpackFields(packet)
+
+        if binaryFields[3] != 4:
+            self.warn("field3," + str(binaryFields[3]))
+            return
+        if binaryFields[4] != 4:
+            self.warn("field4," + str(binaryFields[4]))
+            return
+        # Just ignore the footer
+        #if binaryFields[9] != 1:
+        #    self.warn("field9," + str(binaryFields[9]))
+        #   return
+
+        self.sendP2PACK(binaryFields)
+        self.handlePacket = self.wait_data
+
+    def wait_data(self, packet):
+        if MSNP2PDEBUG: log.msg("wait_data")
+        binaryFields = BinaryFields()
+        binaryFields.unpackFields(packet)
+
+        if binaryFields[5] != self.dataFlag:
+            self.warn("field5," + str(binaryFields[5]))
+            return
+        # Just ignore the footer
+        #if binaryFields[9] != 1:
+        #    self.warn("field9," + str(binaryFields[9]))
+        #   return
+        offset = binaryFields[2]
+        total = binaryFields[3]
+        length = binaryFields[4]
+
+        data = packet[48:-4]
+        if offset != self.pos:
+            self.warn("Received packet out of order")
+            self.consumer.error()
+            return
+        if len(data) != length:
+            self.warn("Received bad length of slp")
+            self.consumer.error()
+            return
+        
+        self.pos += length
+
+        self.consumer.write(str(data))
+
+        if self.pos == total:
+            self.sendP2PACK(binaryFields)
+            self.consumer.close()
+            self.handlePacket = None
+            self.doFinished()
+
+    def doFinished(self):
+        raise NotImplementedError
+
+
+class SLPLink_FileReceive(SLPLink_Receive, FileReceive):
+    def __init__(self, remoteUser, switchboard, filename, filesize, sessionID, sessionGuid, branch):
+        SLPLink_Receive.__init__(self, remoteUser=remoteUser, switchboard=switchboard, consumer=self, sessionID=sessionID, sessionGuid=sessionGuid)
+        self.dataFlag = BinaryFields.DATAFT
+        self.initialBranch = branch
+        FileReceive.__init__(self, filename, filesize, remoteUser)
+
+    def reject(self):
+        # Send a 603 decline
+        if not self.switchboard: return
+        self.sendSLPMessage("603", "application/x-msnmsgr-sessionreqbody", {"SessionID":self.sessionID}, branch=self.initialBranch)
+        self.killLink()
+
+    def accept(self, consumer):
+        FileReceive.accept(self, consumer)
+        if not self.switchboard: return
+        self.sendSLPMessage("200", "application/x-msnmsgr-sessionreqbody", {"SessionID":self.sessionID}, branch=self.initialBranch)
+        self.handlePacket = self.wait_data # Moved here because sometimes the second INVITE seems to be skipped
+
+    def handleSLPMessage(self, slpMessage):
+        if slpMessage.method == "INVITE": # The second invite
+            data = {"Bridge"      : "TCPv1",\
+                    "Listening"   : "false",\
+                    "Hashed-Nonce": "{00000000-0000-0000-0000-000000000000}"}
+            self.sendSLPMessage("200", "application/x-msnmsgr-transrespbody", data, branch=slpMessage.branch)
+#            self.handlePacket = self.wait_data # Moved up
+        else:
+            if MSNP2PDEBUG: log.msg("It's either a BYE or an error")
+            self.killLink()
+            # FIXME, do some error handling if it was an error
+
+    def doFinished(self):
+        #self.sendSLPMessage("BYE", "application/x-msnmsgr-sessionclosebody", {})
+        #self.killLink()
+        # Wait for BYE? #FIXME
+        pass
+
+class SLPLink_AvatarReceive(SLPLink_Receive):
+    def __init__(self, remoteUser, switchboard, consumer, context):
+        SLPLink_Receive.__init__(self, remoteUser=remoteUser, switchboard=switchboard, consumer=consumer, context=context)
+        self.dataFlag = BinaryFields.DATA
+        data = {"EUF-GUID" : MSN_AVATAR_GUID,\
+                "SessionID": self.sessionID,\
+                "AppID"    : 1,\
+                "Context"  : context}
+        self.sendSLPMessage("INVITE", "application/x-msnmsgr-sessionreqbody", data)
+        self.handlePacket = self.wait_dataprep
+
+    def handleSLPMessage(self, slpMessage):
+        if slpMessage.status == "200":
+            pass
+            #self.handlePacket = self.wait_dataprep # Moved upwards
         else:
-            self._pendingSend = None
-            self.completed = 1
+            if MSNP2PDEBUG: log.msg("SLPLink is over due to error or BYE")
+            self.killLink()
+
+    def doFinished(self):
+        self.sendSLPMessage("BYE", "application/x-msnmsgr-sessionclosebody", {})
 
 # mapping of error codes to error messages
 errorCodes = {
@@ -2447,6 +3066,7 @@ errorCodes = {
     301 : "Too many FND responses",
     302 : "Not logged in",
 
+    400 : "Message not allowed",
     402 : "Error accessing contact list",
     403 : "Error accessing contact list",