[meego-commits] 9357: Changes to Trunk/papyon

Peter Zhu no_reply at build.meego.com
Wed Nov 10 08:36:41 UTC 2010


Hi,
I have made the following changes to papyon in project Trunk. Please review and accept ASAP.

Thank You,
Peter Zhu

[This message was auto-generated]

---

Request #9357:

  submit:   Trunk:Testing/papyon(r4) -> Trunk/papyon


Message:
    Move to Trunk

State:   new          2010-11-10T00:36:40 peter
Comment: None



changes files:
--------------
--- papyon.changes
+++ papyon.changes
@@ -0,0 +1,3 @@
+* Mon Nov 08 2010 Rob Bradford <rob at linux.intel.com> - 0.5.2
+- Update to latest release to support MSN protocol changes (BMC#8988)
+

old:
----
  papyon-0.4.9.tar.gz

new:
----
  papyon-0.5.2.tar.gz

spec files:
-----------
--- papyon.spec
+++ papyon.spec
@@ -1,6 +1,6 @@
 # 
-# Do not Edit! Generated by:
-# spectacle version 0.18
+# Do NOT Edit the Auto-generated Part!
+# Generated by: spectacle version 0.18
 # 
 # >> macros
 # << macros
@@ -8,7 +8,7 @@
 %{!?python_sitelib: %define python_sitelib %(%{__python} -c "from distutils.sysconfig import get_python_lib; print get_python_lib()")}
 Name:       papyon
 Summary:    Python libraries for MSN Messenger network
-Version:    0.4.9
+Version:    0.5.2
 Release:    1
 Group:      System/Libraries
 License:    GPLv2+

other changes:
--------------

++++++ papyon-0.4.9.tar.gz -> papyon-0.5.2.tar.gz
--- NEWS
+++ NEWS
@@ -1,3 +1,39 @@
+papyon-0.5.2 (2010-10-21)
+=========================
+
+Fixes:
+  * Use the right policy reference when requesting security token (fdo #31004)
+  * Set the peer end-points before requesting his display picture (fdo #30411)
+  * Don't queue all P2P chunks right away when using SB transport (fdo #29512)
+  * Don't send invalid command when contact is in Allow and Block lists
+  * Don't fail when current media that is not music (fdo #30625)
+
+papyon-0.5.1 (2010-09-10)
+=========================
+
+Fixes:
+  * Multiple fixes to video-conference and proxy support
+  * Only update MSN Object once we received extended presence (fdo #29967)
+  * Fallback on public picture when default one fails (roaming) (fdo #29965)
+  * Support MSNP18-style offline messaging
+  * Better handling of switchboard sessions
+  * Minor fixes for bugs fdo #29742, #29763 and #29764
+
+papyon-0.5.0 (2010-08-12)
+=========================
+
+Fixes:
+  * Multiple fixes to file-transfer support
+  * Crop password to first 16 characters (fdo #27613)
+  * Decode name and subject in new email notifications (fdo #27987)
+  * Use timeout_add_seconds instead of timeout_add where possible
+
+Enhancements:
+  * Added MPOP support (Multiple Points of Presence)
+  * Added back video-conference support through tunneled SIP
+  * Added P2Pv2 support
+  * Added SOCKS5 proxy implementation and improved proxy support globally.
+
 papyon-0.4.9 (2010-07-09)
 =========================
 
--- PKG-INFO
+++ PKG-INFO
@@ -1,6 +1,6 @@
 Metadata-Version: 1.0
 Name: papyon
-Version: 0.4.9
+Version: 0.5.2
 Summary: Python msn client library
 Home-page: http://telepathy.freedesktop.org/wiki/Papyon
 Author: Youness Alaoui
--- README
+++ README
@@ -1,7 +1,7 @@
-papyon - Python msn client library
+papyon - Python MSN client library
 =================================
 
-Homepage: http://telepathy.freedesktop.org/wiki/Papyon
+Homepage: http://www.freedesktop.org/wiki/Software/papyon
 
 papyon is an MSN client library, that tries to abstract the MSN protocol
 gory details. papyon is a fork of the unmaintained library pymsn
@@ -10,5 +10,8 @@
 Dependencies
 ============
 python (>= 2.5)
-python-gobject (>=2.10)
-pyOpenSSL (>=0.6)
+python-gobject (>= 2.10)
+pyOpenSSL (>= 0.6)
+gst-python (>= 0.10)
+python-farsight
+pycrypto
--- papyon/__init__.py
+++ papyon/__init__.py
@@ -26,7 +26,7 @@
     @group Network Layer: gnet
 """
 
-version = (0, 4, 9)
+version = (0, 5, 2)
 
 __version__ = ".".join(str(x) for x in version)
 __author__ = "Youness Alaoui <kakaroto at users.sourceforge.net>"
@@ -40,5 +40,5 @@
 import sip
 
 import gnet.proxy
-Proxy = gnet.proxy.ProxyFactory
+Proxy = gnet.proxy.ProxyInfos.from_string
 ProxyInfos = gnet.proxy.ProxyInfos
--- papyon/client.py
+++ papyon/client.py
@@ -87,13 +87,16 @@
 import papyon.service.AddressBook as AB
 import papyon.service.OfflineIM as OIM
 import papyon.service.Spaces as Spaces
+import papyon.service.ContentRoaming as CR
 
 from papyon.util.decorator import rw_property
 from papyon.transport import *
 from papyon.switchboard_manager import SwitchboardManager
+from papyon.media import RTCActivityManager
 from papyon.msnp2p import P2PSessionManager
+from papyon.msnp2p.transport.switchboard import SwitchboardP2PTransport
 from papyon.p2p import MSNObjectStore, FileTransferManager, WebcamHandler
-from papyon.sip import SIPConnectionManager
+from papyon.sip import SIPCallManager
 from papyon.conversation import SwitchboardConversation, \
     ExternalNetworkConversation
 from papyon.event import ClientState, ClientErrorType, \
@@ -121,6 +124,7 @@
 
             @param proxies: proxies that we can use to connect
             @type proxies: {type: string => L{gnet.proxy.ProxyInfos}}
+            @note the key should be 'http', 'https' or 'direct'
 
             @param transport_class: the transport class to use for the network
                     connection
@@ -145,7 +149,8 @@
         self._webcam_handler = WebcamHandler(self)
         self._p2p_session_manager.register_handler(self._webcam_handler)
 
-        self._call_manager = SIPConnectionManager(self, self._protocol)
+        self._call_manager = SIPCallManager(self)
+        self._rtc_activity_manager = RTCActivityManager(self, self._protocol)
 
         self._msn_object_store = MSNObjectStore(self)
         self._p2p_session_manager.register_handler(self._msn_object_store)
@@ -160,6 +165,7 @@
         self._address_book = None
         self._oim_box = None
         self._mailbox = None
+        self._roaming = None
 
         self.__die = False
         self.__connect_transport_signals()
@@ -194,17 +200,27 @@
 
     @property
     def call_manager(self):
-        """The SIP connection manager
-            @type: L{SIPConnectionManager<papyon.sip.SIPConnectionManager>}"""
+        """The SIP call manager
+            @type: L{SIPCallManager<papyon.sip.SIPCallManager>}"""
         return self._call_manager
 
     @property
+    def content_roaming(self):
+        return self._roaming
+
+    @property
     def ft_manager(self):
         """The files transfer manager
             @type: L{FileTransferManager<papyon.p2p.FileTransferManager>}"""
         return self._ft_manager
 
     @property
+    def rtc_activity_manager(self):
+        """The RTC activity manager
+            @type: L{RTCActivityManager<papyon.media.RTCActivityManager>}"""
+        return self._rtc_activity_manager
+
+    @property
     def oim_box(self):
         """The offline IM for the current user
             @rtype: L{OfflineIM<papyon.service.OfflineIM>}"""
@@ -305,6 +321,12 @@
     ### private:
     def __connect_profile_signals(self):
         """Connect profile signals"""
+        def event(contact, *args):
+            event_name = args[-1]
+            event_args = args[:-1]
+            method_name = "on_profile_%s" % event_name.replace("-", "_")
+            self._dispatch(method_name, *event_args)
+
         def property_changed(profile, pspec):
             method_name = "on_profile_%s_changed" % pspec.name.replace("-", "_")
             self._dispatch(method_name)
@@ -314,6 +336,12 @@
         self.profile.connect("notify::personal-message", property_changed)
         self.profile.connect("notify::current-media", property_changed)
         self.profile.connect("notify::msn-object", property_changed)
+        self.profile.connect("notify::end-points", property_changed)
+
+        def connect_signal(name):
+            self.profile.connect(name, event, name)
+        connect_signal("end-point-added")
+        connect_signal("end-point-removed")
 
     def __connect_mailbox_signals(self):
         """Connect mailbox signals"""
@@ -346,6 +374,7 @@
         contact.connect("notify::current-media", property_changed)
         contact.connect("notify::msn-object", property_changed)
         contact.connect("notify::client-capabilities", property_changed)
+        contact.connect("notify::end-points", property_changed)
 
         def connect_signal(name):
             contact.connect(name, event, name)
@@ -362,6 +391,7 @@
             self._oim_box = OIM.OfflineMessagesBox(self._sso, self, self._proxies)
             self.__connect_oim_box_signals()
             self._spaces = Spaces.Spaces(self._sso, self._proxies)
+            self._roaming = CR.ContentRoaming(self._sso, self._address_book, self._proxies)
 
             self._state = ClientState.CONNECTED
 
@@ -438,6 +468,8 @@
             if handler_class is SwitchboardConversation:
                 if self._dispatch("on_invite_conversation", handler) == 0:
                     logger.warning("No event handler attached for conversations")
+            elif handler_class in [SwitchboardP2PTransport]:
+                pass
             else:
                 logger.warning("Unknown Switchboard Handler class %s" % handler_class)
 
@@ -465,6 +497,7 @@
             self.address_book.connect(name, event, name)
 
         connect_signal("contact-added")
+        connect_signal("contact-pending")
         connect_signal("contact-deleted")
         connect_signal("contact-blocked")
         connect_signal("contact-unblocked")
@@ -506,7 +539,7 @@
         def invite_received(call_manager, call):
             self._dispatch("on_invite_conference", call)
 
-        self._call_manager.connect("invite-received", invite_received)
+        self._call_manager.connect("incoming-call", invite_received)
 
     def __connect_ft_manager_signals(self):
         """Connect File Transfer Manager signals"""
--- papyon/conversation.py
+++ papyon/conversation.py
@@ -26,7 +26,7 @@
 
 import msnp
 import p2p
-from switchboard_manager import SwitchboardClient
+from switchboard_manager import SwitchboardHandler
 from papyon.event import EventsDispatcher
 from papyon.profile import NetworkID
 
@@ -59,7 +59,7 @@
     external_contacts = set(contacts) - msn_contacts
 
     if len(external_contacts) == 0:
-        return SwitchboardConversation(client, contacts)
+        return SwitchboardConversation(client, None, contacts)
     elif len(msn_contacts) != 0:
         raise NotImplementedError("The protocol doesn't allow mixing " \
                 "contacts from different networks in a single conversation")
@@ -290,7 +290,7 @@
 
         self.__last_received_msn_objects = {}
 
-    def send_text_message(self, message):
+    def send_text_message(self, message, callback=None, errback=None):
         if len(message.msn_objects) > 0:
             body = []
             for alias, msn_object in message.msn_objects.iteritems():
@@ -308,7 +308,7 @@
         if message.formatting is not None:
             headers["X-MMS-IM-Format"] = str(message.formatting)
 
-        self._send_message(content_type, body, headers, ack)
+        self._send_message(content_type, body, headers, ack, callback, errback)
 
     def send_nudge(self):
         content_type = "text/x-msnmsgr-datacast"
@@ -330,7 +330,7 @@
         raise NotImplementedError
 
     def _send_message(self, content_type, body, headers={},
-            ack=msnp.MessageAcknowledgement.HALF):
+            ack=msnp.MessageAcknowledgement.HALF, callback=None, errback=None):
         raise NotImplementedError
 
     def _on_contact_joined(self, contact):
@@ -404,7 +404,7 @@
         self._client._unregister_external_conversation(self)
 
     def _send_message(self, content_type, body, headers={},
-            ack=msnp.MessageAcknowledgement.HALF):
+            ack=msnp.MessageAcknowledgement.HALF, callback=None, errback=None):
         if content_type[0]  in ['text/x-mms-emoticon',
                                 'text/x-mms-animemoticon']:
             return
@@ -418,9 +418,9 @@
                     send_unmanaged_message(contact, message)
 
 
-class SwitchboardConversation(AbstractConversation, SwitchboardClient):
-    def __init__(self, client, contacts):
-        SwitchboardClient.__init__(self, client, contacts, priority=0)
+class SwitchboardConversation(AbstractConversation, SwitchboardHandler):
+    def __init__(self, client, switchboard, contacts):
+        SwitchboardHandler.__init__(self, client, switchboard, contacts, priority=0)
         AbstractConversation.__init__(self, client)
 
     @staticmethod
@@ -433,17 +433,32 @@
                 'text/x-msnmsgr-datacast', 'text/x-mms-emoticon',
                 'text/x-mms-animemoticon')
 
+    @staticmethod
+    def handle_message(client, switchboard, message):
+        return SwitchboardConversation(client, switchboard, ())
+
     def invite_user(self, contact):
         """Request a contact to join in the conversation.
 
             @param contact: the contact to invite.
             @type contact: L{profile.Contact}"""
-        SwitchboardClient._invite_user(self, contact)
+        SwitchboardHandler._invite_user(self, contact)
 
     def leave(self):
         """Leave the conversation."""
-        SwitchboardClient._leave(self)
+        SwitchboardHandler._leave(self)
 
     def _send_message(self, content_type, body, headers={},
-            ack=msnp.MessageAcknowledgement.HALF):
-        SwitchboardClient._send_message(self, content_type, body, headers, ack)
+            ack=msnp.MessageAcknowledgement.HALF, callback=None, errback=None):
+        SwitchboardHandler._send_message(self, content_type, body, headers,
+                ack, callback=callback, errback=errback)
+
+    def _on_closed(self):
+        self._dispatch("on_conversation_closed")
+
+    def _on_switchboard_closed(self):
+        pass
+
+    def __repr__(self):
+        participants = ",".join(map(lambda p: p.account, self.total_participants))
+        return '<SwitchboardConversation participants="%s">' % participants
--- papyon/event/address_book.py
+++ papyon/event/address_book.py
@@ -31,6 +31,9 @@
     def on_addressbook_contact_added(self, contact):
         pass
 
+    def on_addressbook_contact_pending(self, contact):
+        pass
+
     def on_addressbook_contact_deleted(self, contact):
         pass
 
--- papyon/event/contact.py
+++ papyon/event/contact.py
@@ -82,10 +82,15 @@
 
     def on_contact_msn_object_changed(self, contact):
         """Called when the MSNObject of a contact changes.
-            @param contact: the contact whose presence changed
+            @param contact: the contact whose msn object changed
             @type contact: L{Contact<papyon.profile.Contact>}
 
             @see: L{MSNObjectStore<papyon.p2p.MSNObjectStore>},
                 L{MSNObject<papyon.p2p.MSNObject>}"""
         pass
 
+    def on_contact_end_points_changed(self, contact):
+        """Called when the end points of a contact change.
+            @param contact: the contact whose end points changed
+            @type contact: L{Contact<papyon.profile.Contact>}"""
+        pass
--- papyon/event/conversation.py
+++ papyon/event/conversation.py
@@ -118,3 +118,6 @@
             @type sender: L{Contact<papyon.profile.Contact>}"""
         pass
 
+    def on_conversation_closed(self):
+        """Called when the conversation is closed."""
+        pass
--- papyon/event/profile.py
+++ papyon/event/profile.py
@@ -55,3 +55,15 @@
     def on_profile_msn_object_changed(self):
         """Called when the MSNObject changes."""
         pass
+
+    def on_profile_end_points_changed(self):
+        """Called when end points change."""
+        pass
+
+    def on_profile_end_point_added(self, end_point):
+        """Called when a new end point connects."""
+        pass
+
+    def on_profile_end_point_removed(self, end_point):
+        """Called when an end point disconnects."""
+        pass
--- papyon/gnet/io/iochannel.py
+++ papyon/gnet/io/iochannel.py
@@ -23,6 +23,7 @@
 from abstract import AbstractClient
 
 import gobject
+import socket
 from errno import *
 
 __all__ = ['GIOChannelClient']
@@ -160,6 +161,13 @@
         self._transport.close()
         self._status = IoStatus.CLOSED
 
+    def disable(self):
+        # Disable the channel without actually closing the socket
+        # Rationnale: some other client might have the ownership of the socket
+        if self._status in (IoStatus.CLOSING, IoStatus.CLOSED):
+            return
+        self._watch_remove()
+
     def send(self, buffer, callback=None, *args):
         assert(self._status == IoStatus.OPEN), self._status
         self._outgoing_queue.append(OutgoingPacket(buffer, len(buffer),
--- papyon/gnet/io/ssl_socket.py
+++ papyon/gnet/io/ssl_socket.py
@@ -64,7 +64,7 @@
             return False
         if self._status == IoStatus.OPENING:
             try:
-                self._transport.do_handshake()
+                self._transport.set_connect_state()
             except (OpenSSL.WantX509LookupError,
                     OpenSSL.WantReadError, OpenSSL.WantWriteError):
                 return True
--- papyon/gnet/parser.py
+++ papyon/gnet/parser.py
@@ -43,11 +43,25 @@
             @type transport: an object derived from
                 L{io.AbstractClient}"""
         gobject.GObject.__init__(self)
-        if connect_signals:
-            transport.connect("received", self._on_received)
-            transport.connect("notify::status", self._on_status_change)
         self._transport = transport
         self._reset_state()
+        self._handles = []
+        if connect_signals:
+            self.enable()
+
+    def enable(self):
+        if self._handles:
+            self.disable()
+        self._handles.append(self._transport.connect("received",
+            self._on_received))
+        self._handles.append(self._transport.connect("notify::status",
+            self._on_status_change))
+
+    def disable(self):
+        for handle in self._handles:
+            self._transport.disconnect(handle)
+        self._handles = []
+        self._reset_state()
 
     def _reset_state(self):
         """Needs to be overriden in order to implement the default
@@ -140,12 +154,23 @@
         transport.connect("notify::status", self._on_status_change)
         AbstractParser.__init__(self, transport, connect_signals=False)
 
+    def enable(self):
+        AbstractParser.enable(self)
+        self._parser.enable()
+
+    def disable(self):
+        AbstractParser.disable(self)
+        self._parser.disable()
+
     def _reset_state(self):
         self._next_chunk = self.CHUNK_START_LINE
         self._receive_buffer = ""
-        self._content_length = None
+        self._content_length = 0
         self._parser.delimiter = "\r\n"
 
+    def _on_received(self, transport, buf, length):
+        pass
+
     def _on_status_change(self, transport, param):
         status = transport.get_property("status")
         if status == IoStatus.OPEN:
--- papyon/gnet/protocol/HTTP.py
+++ papyon/gnet/protocol/HTTP.py
@@ -22,13 +22,17 @@
 from papyon.gnet.message.HTTP import HTTPRequest
 from papyon.gnet.io import TCPClient
 from papyon.gnet.parser import HTTPParser
+from papyon.gnet.proxy.factory import ProxyFactory
 
 import gobject
 import base64
+import logging
 import platform
 
 __all__ = ['HTTP']
 
+logger = logging.getLogger('papyon.gnet.HTTP')
+
 
 class HTTP(gobject.GObject):
     """HTTP protocol client class."""
@@ -47,7 +51,7 @@
                 (object,)), # HTTPRequest
             }
 
-    def __init__(self, host, port=80, proxy=None):
+    def __init__(self, host, port=80, proxies={}):
         """Connection initialization
         
             @param host: the host to connect to.
@@ -56,18 +60,22 @@
             @param port: the port number to connect to
             @type port: integer
 
-            @param proxy: proxy that we can use to connect
-            @type proxy: L{gnet.proxy.ProxyInfos}"""
+            @param proxies: proxies that we can use to connect
+            @type proxies: L{gnet.proxy.ProxyInfos}"""
         gobject.GObject.__init__(self)
-        assert(proxy is None or proxy.type == 'http') # TODO: add support for other proxies (socks4 and 5)
         self._host = host
         self._port = port
-        self.__proxy = proxy
+        self._proxies = proxies
+        self._http_proxy = None
         self._transport = None
         self._http_parser = None
         self._outgoing_queue = []
         self._waiting_response = False
 
+        if self._proxies and self._proxies.get('http', None):
+            if self._proxies['http'].type == 'http':
+                self._http_proxy = self._proxies['http']
+
         self._errored = False
         self.connect("error", self._on_self_error)
 
@@ -76,19 +84,26 @@
 
     def _setup_transport(self):
         if self._transport is None:
-            if self.__proxy is not None:
-                self._transport = TCPClient(self.__proxy.host, self.__proxy.port)
+            if self._http_proxy:
+                self._transport = TCPClient(self._http_proxy.host,
+                        self._http_proxy.port)
             else:
                 self._transport = TCPClient(self._host, self._port)
-            self._http_parser = HTTPParser(self._transport)
-            self._http_parser.connect("received", self._on_response_received)
-            self._transport.connect("notify::status", self._on_status_change)
-            self._transport.connect("error", self._on_error)
-            self._transport.connect("sent", self._on_request_sent)
+                if self._proxies:
+                    self._transport = ProxyFactory(self._transport,
+                            self._proxies, 'http')
+            self._setup_parser()
         
         if self._transport.get_property("status") != IoStatus.OPEN:
             self._transport.open()
 
+    def _setup_parser(self):
+        self._http_parser = HTTPParser(self._transport)
+        self._http_parser.connect("received", self._on_response_received)
+        self._transport.connect("notify::status", self._on_status_change)
+        self._transport.connect("error", self._on_error)
+        self._transport.connect("sent", self._on_request_sent)
+
     def _on_status_change(self, transport, param):
         if transport.get_property("status") == IoStatus.OPEN:
             self._process_queue()
@@ -122,7 +137,12 @@
         #        self._setup_transport()
         #        return
         self._outgoing_queue.pop(0) # pop the request from the queue
-        self.emit("response-received", response)
+        if response.status >= 400:
+            logger.error("Received error code %i (%s) from %s:%i" %
+                (response.status, response.reason, self._host, self._port))
+            self.emit("error", response.status)
+        else:
+            self.emit("response-received", response)
         self._waiting_response = False
         self._process_queue() # next request ?
 
@@ -148,10 +168,10 @@
             user_agent = GNet.NAME, GNet.VERSION, platform.system(), platform.machine()
             headers['User-Agent'] = "%s/%s (%s %s)" % user_agent
 
-        if self.__proxy is not None:
+        if self._http_proxy is not None:
             url = 'http://%s:%d%s' % (self._host, self._port, resource)
-            if self.__proxy.user:
-                auth = self.__proxy.user + ':' + self.__proxy.password
+            if self._http_proxy.user:
+                auth = self._http_proxy.user + ':' + self._http_proxy.password
                 credentials = base64.encodestring(auth).strip()
                 headers['Proxy-Authorization'] = 'Basic ' + credentials
         else:
@@ -159,3 +179,7 @@
         request  = HTTPRequest(headers, data, method, url)
         self._outgoing_queue.append(request)
         self._process_queue()
+
+    def close(self):
+        if self._transport:
+            self._transport.close()
--- papyon/gnet/protocol/HTTPS.py
+++ papyon/gnet/protocol/HTTPS.py
@@ -19,7 +19,7 @@
 
 from papyon.gnet.constants import *
 from papyon.gnet.io import SSLTCPClient
-from papyon.gnet.proxy.HTTPConnect import HTTPConnectProxy
+from papyon.gnet.proxy.factory import ProxyFactory
 from papyon.gnet.parser import HTTPParser
 from HTTP import HTTP
 
@@ -27,7 +27,7 @@
 
 class HTTPS(HTTP):
     """HTTP protocol client class."""
-    def __init__(self, host, port=443, proxy=None):
+    def __init__(self, host, port=443, proxies={}):
         """Connection initialization
         
             @param host: the host to connect to.
@@ -36,25 +36,18 @@
             @param port: the port number to connect to
             @type port: integer
 
-            @param proxy: proxy that we can use to connect
-            @type proxy: L{gnet.proxy.ProxyInfos}"""
-        HTTP.__init__(self, host, port)
-        assert(proxy is None or proxy.type == 'https')
-        self.__proxy = proxy
+            @param proxies: proxies that we can use to connect
+            @type proxies: L{gnet.proxy.ProxyInfos}"""
+        HTTP.__init__(self, host, port, proxies)
+        self._http_proxy = None
 
     def _setup_transport(self):
         if self._transport is None:
-            transport = SSLTCPClient(self._host, self._port)
-            if self.__proxy is not None:
-                print 'Using proxy : ', repr(self.__proxy)
-                self._transport = HTTPConnectProxy(transport, self.__proxy)
-            else:
-                self._transport = transport
-            self._http_parser = HTTPParser(self._transport)
-            self._http_parser.connect("received", self._on_response_received)
-            self._transport.connect("notify::status", self._on_status_change)
-            self._transport.connect("error", self._on_error)
-            self._transport.connect("sent", self._on_request_sent)
+            self._transport = SSLTCPClient(self._host, self._port)
+            if self._proxies:
+                self._transport = ProxyFactory(self._transport, self._proxies,
+                        preferred='https')
+            self._setup_parser()
         
         if self._transport.get_property("status") != IoStatus.OPEN:
             self._transport.open()
--- papyon/gnet/protocol/__init__.py
+++ papyon/gnet/protocol/__init__.py
@@ -22,14 +22,14 @@
 from HTTP import *
 from HTTPS import *
 
-def ProtocolFactory(protocol, host, port=None, proxy=None):
+def ProtocolFactory(protocol, host, port=None, proxies={}):
     if protocol == "http":
         klass = HTTP
     elif protocol == "https":
         klass = HTTPS
     
     if port is None:
-        return klass(host, proxy=proxy)
+        return klass(host, proxies=proxies)
     else:
-        return klass(host, port, proxy=proxy)
+        return klass(host, port, proxies=proxies)
 
--- papyon/gnet/proxy/HTTPConnect.py
+++ papyon/gnet/proxy/HTTPConnect.py
@@ -47,9 +47,7 @@
 
     def _post_open(self):
         AbstractProxy._post_open(self)
-        host = self._client.get_property("host")
-        port = self._client.get_property("port")
-        proxy_protocol  = 'CONNECT %s:%s HTTP/1.1\r\n' % (host, port)
+        proxy_protocol  = 'CONNECT %s:%s HTTP/1.1\r\n' % (self.host, self.port)
         proxy_protocol += 'Proxy-Connection: Keep-Alive\r\n'
         proxy_protocol += 'Pragma: no-cache\r\n'
         proxy_protocol += 'User-Agent: %s/%s\r\n' % (GNet.NAME, GNet.VERSION)
@@ -57,6 +55,8 @@
             auth = base64.encodestring(self._proxy.user + ':' + self._proxy.password).strip()
             proxy_protocol += 'Proxy-authorization: Basic ' + auth + '\r\n'
         proxy_protocol += '\r\n'
+
+        self._http_parser.enable()
         self._transport.send(proxy_protocol)
 
     # public API
@@ -72,7 +72,9 @@
     
     def close(self):
         """Close the connection."""
+        self._http_parser.disable()
         self._client._proxy_closed()
+        self._transport.close()
 
     def send(self, buffer, callback=None, *args):
         self._client.send(buffer, callback, *args)
@@ -96,8 +98,8 @@
     def _on_proxy_response(self, parser, response):
         if self.status == IoStatus.OPENING:
             if response.status == 200:
-                del self._http_parser
-                self._transport._watch_remove() # HACK: ok this is ugly !
+                self._http_parser.disable()
+                self._transport.disable()
                 self._client._proxy_open()
             elif response.status == 100:
                 return True
--- papyon/gnet/proxy/SOCKS4.py
+++ papyon/gnet/proxy/SOCKS4.py
@@ -59,19 +59,18 @@
 
     def _post_open(self):
         AbstractProxy._post_open(self)
-        host = self._client.get_property("host")
-        port = self._client.get_property("port")
         user = self._proxy.user
 
         proxy_protocol  = struct.pack('!BBH', SOCKS4Proxy.PROTOCOL_VERSION,
-                SOCKS4Proxy.CONNECT_COMMAND, port)
+                SOCKS4Proxy.CONNECT_COMMAND, self.port)
 
-        for part in host.split('.'):
+        for part in self.host.split('.'):
            proxy_protocol += struct.pack('B', int(part))
 
         proxy_protocol += user
         proxy_protocol += struct.pack('B', 0)
 
+        self._delimiter_parser.enable()
         self._transport.send(proxy_protocol)
         
     # Public API
@@ -87,7 +86,9 @@
 
     def close(self):
         """Close the connection."""
+        self._delimiter_parser.disable()
         self._client._proxy_closed()
+        self._transport.close()
 
     def send(self, buffer, callback=None, *args):
         self._client.send(buffer, callback, *args)
@@ -113,8 +114,8 @@
         assert(version == 0)
         if self.status == IoStatus.OPENING:
             if response_code == 90:
-                del self._delimiter_parser
-                self._transport._watch_remove() # HACK: ok this is ugly !
+                self._delimiter_parser.disable()
+                self._transport.disable()
                 self._client._proxy_open()
             elif response_code == 91:
                 self.close()
--- papyon/gnet/proxy/SOCKS5.py
+++ papyon/gnet/proxy/SOCKS5.py
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2010 Collabora Ltd.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+#
+
+from abstract import AbstractProxy
+from papyon.gnet.io import TCPClient
+from papyon.gnet.constants import *
+from papyon.gnet.parser import DelimiterParser
+
+import gobject
+import logging
+import socket
+import struct
+
+__all__ = ['SOCKS5Proxy']
+
+logger = logging.getLogger('papyon.proxy.SOCKS5')
+
+class SOCKS5Proxy(AbstractProxy):
+
+    VERSION = 5
+    CMD_CONNECT = 0x01
+    AUTH_VERSION = 1
+    MAX_LEN = 255
+    RESERVED = 0x00
+
+    AUTH_NONE = 0x00
+    AUTH_GSSAPI = 0x01
+    AUTH_USR_PASS = 0x02
+    AUTH_NO_ACCEPT = 0xff
+
+    ATYP_IPV4 = 0x01
+    ATYP_DOMAINNAME = 0x03
+    ATYP_IPV6 = 0x04
+
+    STATE_NEGO = 0x01
+    STATE_AUTH = 0x02
+    STATE_CONN = 0x03
+    STATE_RECV_IPV4_ADDR = 0x04
+    STATE_RECV_ADDR_LEN = 0x05
+    STATE_RECV_DOMAINNAME = 0x06
+
+    CODE_SUCCEEDED = 0x00
+    CODE_SRV_FAILURE = 0x01
+    CODE_NOT_ALLOWED = 0x02
+    CODE_NET_UNREACH = 0x03
+    CODE_HOST_UNREACH = 0x04
+    CODE_REFUSED = 0x05
+    CODE_TTL_EXPIRED = 0x06
+    CODE_CMD_NOT_SUP = 0x07
+    CODE_ATYPE_NOT_SUP = 0x08
+
+    NEGO_REPLY_LEN = 2
+    AUTH_REPLY_LEN = 2
+    CONN_REPLY_LEN = 4
+    IPV4_ADDR_LEN = 4
+
+    """Proxy class used to communicate with SOCKS5 proxies."""
+    def __init__(self, client, proxy):
+        assert(proxy.type in ('socks', 'socks5')), \
+                "SOCKS5Proxy expects a socks5 proxy description"
+        assert(client.domain == AF_INET), \
+                "SOCKS5 CONNECT only handles INET address family"
+        assert(client.type == SOCK_STREAM), \
+                "SOCKS5 CONNECT only handles SOCK_STREAM"
+        assert(client.status == IoStatus.CLOSED), \
+                "SOCKS5Proxy expects a closed client"
+        AbstractProxy.__init__(self, client, proxy)
+
+        self._transport = TCPClient(self._proxy.host, self._proxy.port)
+        self._transport.connect("notify::status", self._on_transport_status)
+        self._transport.connect("error", self._on_transport_error)
+
+        self._delimiter_parser = DelimiterParser(self._transport)
+        self._delimiter_parser.connect("received", self._on_proxy_response)
+
+        self._state = None
+        self._must_auth = False
+
+    # Opening state methods
+    def _pre_open(self, io_object=None):
+        AbstractProxy._pre_open(self)
+
+    def _post_open(self):
+        AbstractProxy._post_open(self)
+        self._delimiter_parser.enable()
+        self._send_nego_msg()
+        
+    # Public API
+    def open(self):
+        """Open the connection."""
+        if not self._configure():
+            return
+        self._pre_open()
+        try:
+            self._transport.open()
+        except:
+            pass
+
+    def close(self):
+        """Close the connection."""
+        self._delimiter_parser.disable()
+        self._client._proxy_closed()
+        self._transport.close()
+
+    def send(self, buffer, callback=None, *args):
+        self._client.send(buffer, callback, *args)
+
+    # Handshake
+    def _send_nego_msg(self):
+        user = self._proxy.user
+        password = self._proxy.password
+
+        methods = [self.AUTH_NONE]
+        if user or password:
+            methods.append(self.AUTH_USR_PASS)
+
+        msg = struct.pack('!BB', SOCKS5Proxy.VERSION,
+                len(methods))
+        for method in methods:
+            msg += struct.pack('B', method)
+
+        logger.info("Sending negotiation request (%i methods)" % len(methods))
+        self._state = SOCKS5Proxy.STATE_NEGO
+        self._delimiter_parser.delimiter = SOCKS5Proxy.NEGO_REPLY_LEN
+        self._transport.send(msg)
+        return True
+
+    def _parse_nego_reply(self, response):
+        version, method = struct.unpack('!BB', response[0:2])
+        if version != SOCKS5Proxy.VERSION:
+            raise Exception("Server is not SOCKS5 compatible")
+        if method == SOCKS5Proxy.AUTH_NO_ACCEPT:
+            raise Exception("Server doesn't support any of the proposed \
+                    authentication methods")
+
+        logger.info("Server chose authentication method %i" % method)
+        self._must_auth = (method == SOCKS5Proxy.AUTH_USR_PASS)
+        return True
+
+    def _send_auth_msg(self):
+        user = self._proxy.user
+        password = self._proxy.password
+
+        if len(user) > self.MAX_LEN or len(password) > self.MAX_LEN:
+            raise Exception("User and password need to be less than %i \
+                    characters long" % self.MAX_LEN)
+
+        msg = struct.pack('B', SOCKS5Proxy.AUTH_VERSION)
+        msg += struct.pack('B', len(user))
+        if user:
+            msg += user
+        msg += struct.pack('B', len(password))
+        if password:
+            msg += password
+
+        logger.info("Sending authentication request")
+        self._state = SOCKS5Proxy.STATE_AUTH
+        self._delimiter_parser.delimiter = SOCKS5Proxy.AUTH_REPLY_LEN
+        self._transport.send(msg)
+
+    def _check_auth_status(self, response):
+        version, code = struct.unpack('!BB', response[0:2])
+        if (version != SOCKS5Proxy.VERSION or
+            code != SOCKS5Proxy.CODE_SUCCEEDED):
+            raise Exception("Authentication didn't succeed")
+        logger.info("Authentication succeeded")
+        return True
+
+    def _send_connect_msg(self):
+        msg = struct.pack('!BBB', SOCKS5Proxy.VERSION,
+                SOCKS5Proxy.CMD_CONNECT, SOCKS5Proxy.RESERVED)
+        try:
+            addr = socket.inet_aton(self.host)
+            msg += struct.pack('!BI', SOCKS5Proxy.ATYP_IPV4, addr)
+        except:
+            if len(self.host) > SOCKS5Proxy.MAX_LEN:
+                raise Exception
+            msg += struct.pack('!BB', SOCKS5Proxy.ATYP_DOMAINNAME, len(self.host))
+            msg += self.host
+
+        msg += struct.pack('!H', self.port)
+
+        logger.info("Connection request to %s:%u" % (self.host, self.port))
+        self._state = SOCKS5Proxy.STATE_CONN
+        self._delimiter_parser.delimiter = SOCKS5Proxy.CONN_REPLY_LEN
+        self._transport.send(msg)
+
+    def _parse_connect_reply(self, response):
+        version, code, reserved, atyp = struct.unpack('!BBBB', response[0:4])
+        if version != SOCKS5Proxy.VERSION or reserved != SOCKS5Proxy.RESERVED:
+            raise Exception("Connection reply isn't SOCKS5 compatible")
+        if code == SOCKS5Proxy.CODE_SUCCEEDED:
+            logger.info("Connection request has been accepted")
+        else:
+            raise Exception("Connection request has been declined (%i)" % code)
+
+        if atyp == SOCKS5Proxy.ATYP_IPV4:
+            self._state = SOCKS5Proxy.STATE_RECV_IPV4_ADDR
+            self._delimiter_parser.delimiter = SOCKS5Proxy.IPV4_ADDR_LEN
+        elif atyp == SOCKS5Proxy.ATYP_DOMAINNAME:
+            self._state = SOCKS5Proxy.STATE_RECV_ADDR_LEN
+            self._delimiter_parser.delimiter = 1
+        else:
+            raise Exception('Unsupported address type: %i' % atyp)
+
+    def _parse_domain_name_length(self, response):
+        length, = struct.unpack('!B', response)
+        self._state = SOCKS5Proxy.STATE_RECV_DOMAINNAME
+        self._delimiter_parser.delimiter = length
+
+    # Callbacks
+    def _on_transport_status(self, transport, param):
+        if transport.status == IoStatus.OPEN:
+            self._post_open()
+        elif transport.status == IoStatus.OPENING:
+            self._client._proxy_opening(self._transport._transport)
+            self._status = transport.status
+        else:
+            self._status = transport.status
+
+    def _on_transport_error(self, transport, error_code):
+        if error_code == IoError.CONNECTION_FAILED:
+            error_code = IoError.PROXY_CONNECTION_FAILED
+        self.close()
+        self.emit("error", error_code)
+
+    def _on_proxy_response(self, parser, response):
+        try:
+            if self._state == SOCKS5Proxy.STATE_NEGO:
+                self._parse_nego_reply(response)
+                if self._must_auth:
+                    self._send_auth_msg()
+                else:
+                    self._send_connect_msg()
+            elif self._state == SOCKS5Proxy.STATE_AUTH:
+                self._check_auth_status(response)
+                self._send_connect_msg()
+            elif self._state == SOCKS5Proxy.STATE_CONN:
+                self._parse_connect_reply(response)
+            elif self._state == SOCKS5Proxy.STATE_RECV_ADDR_LEN:
+                self._parse_domain_name_length(response)
+            elif (self._state == SOCKS5Proxy.STATE_RECV_IPV4_ADDR or
+                  self._state == SOCKS5Proxy.STATE_RECV_DOMAINNAME):
+                self._delimiter_parser.disable()
+                self._transport.disable()
+                self._client._proxy_open()
+        except Exception, err:
+            logger.error("Handshake failed")
+            logger.exception(err)
+            self.close()
+            self.emit("error", IoError.PROXY_CONNECTION_FAILED)
+        return False
+
+gobject.type_register(SOCKS5Proxy)
--- papyon/gnet/proxy/abstract.py
+++ papyon/gnet/proxy/abstract.py
@@ -31,7 +31,7 @@
         self._client.connect("sent", self._on_client_sent)
         self._client.connect("received", self._on_client_received)
         self._client.connect("notify::status", self._on_client_status)
-        AbstractClient.__init__(self, self._proxy.host, self._proxy.port)
+        AbstractClient.__init__(self, client.host, client.port)
 
     def _on_client_status(self, client, param):
         status = client.get_property("status")
--- papyon/gnet/proxy/factory.py
+++ papyon/gnet/proxy/factory.py
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2010 Collabora Ltd.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+#
+
+from HTTPConnect import *
+from SOCKS4 import *
+from SOCKS5 import *
+
+def ProxyFactory(client, proxies, preferred='direct'):
+    if not proxies or preferred not in proxies or not proxies[preferred]:
+        return client
+
+    proxy = proxies[preferred]
+    if proxy.type == 'http':
+        return HTTPConnectProxy(client, proxy)
+    elif proxy.type == 'https':
+        return HTTPConnectProxy(client, proxy)
+    elif proxy.type in ('socks', 'socks5'):
+        # FIXME we assume "socks://" is a SOCKS5 proxy
+        return SOCKS5Proxy(client, proxy)
+    elif proxy.type == 'socks4':
+        return SOCKS4Proxy(client, proxies['socks4'])
+    else:
+        return client
--- papyon/gnet/proxy/proxy_infos.py
+++ papyon/gnet/proxy/proxy_infos.py
@@ -20,7 +20,7 @@
 import base64
 import urlparse
 
-__all__ = ['ProxyInfos', 'ProxyFactory']
+__all__ = ['ProxyInfos']
 
 class ProxyInfos(object):
     """Contain informations needed to make use of a proxy.
@@ -98,7 +98,7 @@
     def __get_type(self):
         return self._type
     def __set_type(self, type):
-        assert(type in ('http', 'https', 'socks4', 'socks5'))
+        assert(type in ('http', 'https', 'socks', 'socks4', 'socks5'))
         self._type = type
     type = property(__get_type, __set_type, doc="Proxy type.")
 
@@ -115,6 +115,3 @@
             auth = '%s:%s' % (self.user, "*" * len(self.password))
             host = auth + '@' + host
         return self.type + '://' + host + '/'
-
-ProxyFactory = ProxyInfos.from_string
-
--- papyon/gnet/resolver.py
+++ papyon/gnet/resolver.py
@@ -72,7 +72,7 @@
             addresses = ((socket.AF_INET, result[0][4][0]),)
         self._emit_response(callback, (status, cname, expires, addresses))
 
-    @async
+    #@async
     def _emit_response(self, callback, response):
         callback[0](HostnameResponse(response), *callback[1:])
         return False
--- papyon/media/__init__.py
+++ papyon/media/__init__.py
@@ -24,5 +24,6 @@
 from constants import *
 from message import *
 from relay import *
+from rtc import *
 from session import *
 from stream import *
--- papyon/media/codec.py
+++ papyon/media/codec.py
@@ -20,6 +20,8 @@
 
 __all__ = ['MediaCodec']
 
+from papyon.media.constants import CODEC_DETAILS
+
 class MediaCodec(object):
     """Class representing a media codec."""
 
@@ -30,21 +32,8 @@
         self.params = params or dict()
 
         if encoding is None or clockrate is None or clockrate == 0:
-            if payload == 0:
-                self.encoding = "PCMU"
-                self.clockrate = 8000
-            elif payload == 8:
-                self.encoding = "PCMA"
-                self.clockrate = 8000
-            elif payload == 13:
-                self.encoding = "CN"
-                self.clockrate = 8000
-            elif payload == 31:
-                self.encoding = "H261"
-                self.clockrate = 90000
-            elif payload == 34:
-                self.encoding = "H263"
-                self.clockrate = 90000
+            if payload in CODEC_DETAILS:
+                self.encoding, self.clockrate = CODEC_DETAILS[payload]
 
     def __eq__(self, other):
         return (self.payload == other.payload and
--- papyon/media/conference.py
+++ papyon/media/conference.py
@@ -97,9 +97,10 @@
         bus = self._pipeline.get_bus()
         bus.add_signal_watch()
         bus.connect("message", self.on_bus_message)
-        if self._session.type is MediaSessionType.WEBCAM_RECV or\
-           self._session.type is MediaSessionType.WEBCAM_SEND:
-            name = "fsmsnconference"
+        if self._session.type is MediaSessionType.WEBCAM_RECV:
+            name = "fsmsncamrecvconference"
+        elif self._session.type is MediaSessionType.WEBCAM_SEND:
+            name = "fsmsncamsendconference"
         else:
             name = "fsrtpconference"
         self._conference = gst.element_factory_make(name)
@@ -320,10 +321,6 @@
 
 def make_video_sink(async=False):
     "Make a bin with a video sink in it, that will be displayed on xid."
-    sink = gst.element_factory_make("filesink", "filesink")
-    sink.set_property("location", "/tmp/videosink.log")
-    return sink
-
     bin = gst.Bin("videosink")
     sink = gst.element_factory_make("ximagesink", "imagesink")
     sink.set_property("sync", async)
@@ -337,5 +334,5 @@
     colorspace.link(sink)
     bin.add_pad(gst.GhostPad("sink", videoscale.get_pad("sink")))
     #sink.set_data("xid", xid) #Future work - proper gui place for imagesink ?
-    return bin.get_pad("sink")
+    return bin
 
--- papyon/media/constants.py
+++ papyon/media/constants.py
@@ -47,6 +47,14 @@
     "video" : ["x-rtvc1", "h263"]
 }
 
+CODEC_DETAILS = {
+    0 : ("PCMU", 8000),
+    8 : ("PCMA", 8000),
+    13: ("CN", 8000),
+    31: ("H261", 90000),
+    34: ("H264", 90000)
+}
+
 EXTRA_PARAMS = {
     34: {"x-modea-only": "1"}
 }
--- papyon/media/rtc.py
+++ papyon/media/rtc.py
+# -*- coding: utf-8 -*-
+#
+# papyon - a python client library for Msn
+#
+# Copyright (C) 2010 Collabora Ltd.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+from papyon.msnp.constants import *
+
+import gobject
+import logging
+
+logger = logging.getLogger('papyon.media.rtc')
+
+__all__ = ['RTCActivity', 'RTCActivityManager']
+
+class MessageTypes:
+    REQUESTOR_BYE = 1
+    SERVER_BYE = 2
+    ACCEPT = 3
+    DECLINE = 4
+    CAPABILITIES = 5
+
+class RTCActivityManager(object):
+    """Manage the different RTC activities. Forward the messages received from
+       the transport to the right activity."""
+
+    def __init__(self, client, protocol):
+        self._activities = []
+        self._transport = RTCActivityTunneledTransport(protocol)
+        self._transport.connect("message-received", self._on_message_received)
+
+    def register(self, activity):
+        self._activities.append(activity)
+
+    def unregister(self, activity):
+        self._activities.remove(activity)
+
+    def get_transport(self):
+        return self._transport
+
+    def _get_activity(self, id):
+        for activity in self._activities:
+            if activity.id == id:
+                return activity
+        logger.warning("There is no registered activity with id %s." % id)
+        return None
+
+    def _on_message_received(self, transport, peer, peer_guid, type, arguments):
+        if type == MessageTypes.ACCEPT:
+            id = arguments[1]
+            logger.info("Activity %s has been accepted elsewhere." % id)
+            activity = self._get_activity(id)
+            if activity:
+                activity.on_activity_accepted()
+        elif type == MessageTypes.DECLINE:
+            id = arguments[1]
+            logger.info("Activity %s has been declined elsewhere." % id)
+            activity = self._get_activity(id)
+            if activity:
+                activity.on_activity_declined()
+
+        #FIXME handle BYE on timeout
+
+
+class RTCActivity(object):
+    """RTC activity such as a video or audio call (not sure if there is any
+       other type of activity). Used to communicate between end points about
+       the status of an activity (accepted, declined..) in a MPOP context."""
+
+    def __init__(self, client):
+        logger.info("New RTC activity.")
+        self._client = client
+        self._manager = client.rtc_activity_manager
+        self._manager.register(self)
+        self._transport = self._manager.get_transport()
+
+    @property
+    def id(self):
+        raise NotImplementedError
+
+    @property
+    def peer(self):
+        raise NotImplementedError
+
+    def on_activity_accepted(self):
+        raise NotImplementedError
+
+    def on_activity_declined(self):
+        raise NotImplementedError
+
+    def _accept_activity(self):
+        logger.info("Activity %s has been accepted." % self.id)
+        if self._transport is None:
+            return
+        self._transport.send(self._client.profile.account, None,
+                MessageTypes.ACCEPT, ('1', self.id, self.peer.account, '3'))
+
+    def _decline_activity(self):
+        logger.info("Activity %s has been declined." % self.id)
+        if self._transport is None:
+            return
+        self._transport.send(self._client.profile.account, None,
+                MessageTypes.DECLINE, ('1', self.id, self.peer.account, '3'))
+
+    def _dispose_activity(self):
+        self._manager.unregister(self)
+
+
+class RTCActivityTunneledTransport(gobject.GObject):
+    """Default (and only?) RTC activity transport. The messages are sent to
+       the notification server using UBN commands."""
+
+    __gsignals__ = {
+        "message-received": (gobject.SIGNAL_RUN_FIRST,
+            gobject.TYPE_NONE,
+            (object, object, object, object))
+    }
+
+    def __init__(self, protocol):
+        gobject.GObject.__init__(self)
+        self._protocol = protocol
+        self._protocol.connect("buddy-notification-received",
+                self._on_notification_received)
+
+    def send(self, peer, peer_guid, type, params):
+        message = str(type) + ' ' + ' '.join(params)
+        self._protocol.send_user_notification(message, peer, peer_guid,
+                UserNotificationTypes.RTC_ACTIVITY)
+
+    def _on_notification_received(self, protocol, peer, peer_guid, type, message):
+        if type is not UserNotificationTypes.RTC_ACTIVITY:
+            return
+        logger.debug("<<< " + message)
+        params = message.split(' ')
+        self.emit("message-received", peer, peer_guid, int(params[0]), params[1:])
--- papyon/media/session.py
+++ papyon/media/session.py
@@ -207,6 +207,8 @@
             if self.ready:
                 self.emit("ready")
 
+        return True
+
     def on_stream_prepared(self, stream):
         if self.prepared:
             logger.debug("All media streams are prepared")
--- papyon/media/stream.py
+++ papyon/media/stream.py
@@ -248,6 +248,7 @@
 
         if self._local_candidates_prepared:
             return
+        logger.debug("Media stream %s is prepared" % self.name)
         self._local_candidates_prepared = True
         if self.prepared:
             self.emit("prepared")
--- papyon/msnp/base.py
+++ papyon/msnp/base.py
@@ -35,6 +35,7 @@
     SYNCHRONIZING = 4
     SYNCHRONIZED = 5
     OPEN = 6
+    CLOSING = 7
 
 
 class BaseProtocol(object):
--- papyon/msnp/constants.py
+++ papyon/msnp/constants.py
@@ -33,8 +33,10 @@
     XBOX = 4
 
 class UserNotificationTypes(object):
+    XML_DATA = 1
     SIP_INVITE = 2
     P2P_DATA = 3
+    CLOSED_CONVERSATION = 5
     RESYNCHRONIZE = 6
-    SIP_PROGESS = 11
+    RTC_ACTIVITY = 11
     TUNNELED_SIP = 12
--- papyon/msnp/message.py
+++ papyon/msnp/message.py
@@ -70,26 +70,12 @@
         message = ''
         for header_name, header_value in self.headers.iteritems():
             message += '\t%s: %s\\r\\n\n' % (header_name, repr(header_value))
-        message += '\t\\r\\n\n'
         if self.headers['Content-Type'] != "application/x-msnmsgrp2p":
+            message += '\t\\r\\n\n'
             message += '\t' + debug.escape_string(self.body).\
                     replace("\r\n", "\\r\\n\n\t")
         else:
-            tlp_header = self.body[:48]
-            tlp_footer = self.body[-4:]
-            tlp_session_id = struct.unpack("<L", self.body[0:4])[0]
-            tlp_flags = struct.unpack("<L", self.body[28:32])[0]
-            body = self.body[48:-4]
-
-            message += "\t" + debug.hexify_string(tlp_header).replace("\r\n", "\n\t")
-
-            if tlp_flags == 0 or tlp_session_id == 0:
-                message += "\n\t" + debug.escape_string(body).\
-                        replace("\r\n", "\\r\\n\n\t")
-            elif len(body) > 0:
-                message += "\n\t" + "[%d bytes of data]" % len(body)
-            message += "\n\t" + debug.hexify_string(tlp_footer)
-
+            message += "\t[P2P message (%d bytes)]" % (len(self.body) - 4)
         return message.rstrip("\n\t")
 
     def __get_content_type(self):
--- papyon/msnp/notification.py
+++ papyon/msnp/notification.py
@@ -32,6 +32,7 @@
 from papyon.gnet.message.HTTP import HTTPMessage
 from papyon.util.queue import PriorityQueue, LastElementQueue
 from papyon.util.decorator import throttled
+from papyon.util.encoding import decode_rfc2047_string
 import papyon.util.element_tree as ElementTree
 import papyon.profile as profile
 import papyon.service.SingleSignOn as SSO
@@ -74,7 +75,7 @@
 
             "buddy-notification-received" : (gobject.SIGNAL_RUN_FIRST,
                 gobject.TYPE_NONE,
-                (object, object,)),
+                (object, object, object, object)),
 
             "mail-received" : (gobject.SIGNAL_RUN_FIRST,
                 gobject.TYPE_NONE,
@@ -135,7 +136,7 @@
         raise AttributeError, "unknown property %s" % pspec.name
 
     # Public API -------------------------------------------------------------
-    @throttled(2000, LastElementQueue())
+    @throttled(2, LastElementQueue())
     def set_presence(self, presence, client_id=0, msn_object=None):
         """Publish the new user presence.
 
@@ -152,7 +153,7 @@
             self._send_command('CHG',
                     (presence, str(client_id), urllib.quote(str(msn_object))))
 
-    @throttled(2000, LastElementQueue())
+    @throttled(2, LastElementQueue())
     def set_display_name(self, display_name):
         """Sets the new display name
 
@@ -161,7 +162,7 @@
         self._send_command('PRP',
                 ('MFN', urllib.quote(display_name)))
 
-    @throttled(2000, LastElementQueue())
+    @throttled(2, LastElementQueue())
     def set_personal_message(self, personal_message='', current_media=None,
             signature_sound=None):
         """Sets the new personal message
@@ -191,7 +192,7 @@
             self._client.profile._server_property_changed("current-media",
                 current_media)
 
-    @throttled(2000, LastElementQueue())
+    @throttled(2, LastElementQueue())
     def set_end_point_name(self, name="Papyon", idle=False):
         ep = '<EndpointData>'\
                 '<Capabilities>%s</Capabilities>'\
@@ -208,12 +209,16 @@
         self._send_command('UUX', payload=ep)
         self._send_command('UUX', payload=pep)
 
+    @throttled(2, LastElementQueue())
+    def set_privacy(self, privacy=profile.Privacy.BLOCK):
+        self._send_command("BLP", (privacy,))
+
     def signoff(self):
         """Logout from the server"""
         self._send_command('OUT')
         self._transport.lose_connection()
 
-    @throttled(7600, list())
+    @throttled(7, list())
     def request_switchboard(self, priority, callback, *callback_args):
         self.__switchboard_callbacks.add((callback, callback_args), priority)
         self._send_command('XFR', ('SB',))
@@ -270,8 +275,13 @@
                     (domain, user, membership, network_id)
             self._send_command("RML", payload=payload)
 
-    def send_user_notification(self, message, contact, type):
-        self._send_command("UUN", (contact.account, type), message)
+    def send_user_notification(self, message, contact, contact_guid, type,
+            callback=None, *cb_args):
+        account = contact
+        if contact_guid:
+            account += ';{' + contact_guid + '}'
+        arguments = (account, type)
+        self._send_command("UUN", arguments, message, True, callback, *cb_args)
 
     def send_unmanaged_message(self, contact, message):
         content_type = message.content_type[0]
@@ -289,7 +299,22 @@
         tr_id = self._send_command('URL', url_command_args)
         self._url_callbacks[tr_id] = callback
 
-    def _parse_account(self, command, idx=0):
+    # Helpers ----------------------------------------------------------------
+    def __search_account(self, account, network_id=profile.NetworkID.MSN):
+        if account == self._client.profile.account:
+            return [self._client.profile]
+
+        contacts = self._client.address_book.contacts.\
+                search_by_network_id(network_id).\
+                search_by_account(account)
+
+        if len(contacts) == 0:
+            logger.warning("Contact (network_id=%d) %s not found" % \
+                    (network_id, account))
+
+        return contacts
+
+    def __parse_network_and_account(self, command, idx=0):
         if self._protocol_version >= 18:
             temp = command.arguments[idx].split(":")
             network_id = int(temp[0])
@@ -301,6 +326,20 @@
         idx += 1
         return idx, network_id, account
 
+    def __parse_account_and_guid(self, command, idx=0):
+        account = command.arguments[idx]
+        guid = None
+        if ';' in account:
+            account, guid = account.split(';', 1)
+            guid = guid [1:-1]
+        return idx + 1, account, guid
+
+    def __find_node(self, parent, name, default):
+        node = parent.find(name)
+        if node is not None and node.text is not None:
+            return node.text.encode("utf-8")
+        else:
+            return default
 
     # Handlers ---------------------------------------------------------------
     # --------- Connection ---------------------------------------------------
@@ -410,35 +449,19 @@
         else:
             self._client.profile._server_property_changed("msn_object", None)
 
-    def _handle_ILN(self,command):
+    def _handle_ILN(self, command):
         self._handle_NLN(command)
 
-    def _handle_FLN(self,command):
-        idx, network_id, account = self._parse_account(command)
-
-        contacts = self._client.address_book.contacts.\
-                search_by_network_id(network_id).\
-                search_by_account(account)
-
-        if len(contacts) == 0:
-            logger.warning("Contact (network_id=%d) %s not found" % \
-                    (network_id, account))
-
+    def _handle_FLN(self, command):
+        idx, network_id, account = self.__parse_network_and_account(command)
+        contacts = self.__search_account(account, network_id)
         for contact in contacts:
+            contact._remove_flag(profile.ContactFlag.EXTENDED_PRESENCE_KNOWN)
             contact._server_property_changed("presence",
                     profile.Presence.OFFLINE)
 
-    def _handle_NLN(self,command):
-        idx, network_id, account = self._parse_account(command, 1)
-
-        contacts = self._client.address_book.contacts.\
-                search_by_network_id(network_id).\
-                search_by_account(account)
-
-        if len(contacts) == 0:
-            logger.warning("Contact (network_id=%d) %s not found" % \
-                    (network_id, account))
-
+    def _handle_NLN(self, command):
+        idx, network_id, account = self.__parse_network_and_account(command, 1)
         presence = command.arguments[0]
         display_name = urllib.unquote(command.arguments[idx])
         idx += 1
@@ -447,19 +470,27 @@
 
         msn_object = None
         icon_url = None
+
         if len(command.arguments) > idx:
             if command.arguments[idx] not in ('0', '1'):
                 msn_object = papyon.p2p.MSNObject.parse(self._client,
                                urllib.unquote(command.arguments[idx]))
+
         idx += 1
         if len(command.arguments) > idx:
             icon_url = command.arguments[idx]
 
+        contacts = self.__search_account(account, network_id)
         for contact in contacts:
-            contact._server_property_changed("presence", presence)
+            # don't change local presence and capabilities
+            if contact is not self._client.profile:
+                contact._server_property_changed("presence", presence)
+                contact._server_property_changed("client-capabilities", capabilities)
             contact._server_property_changed("display-name", display_name)
-            contact._server_property_changed("client-capabilities", capabilities)
-            contact._server_property_changed("msn-object", msn_object)
+            # only change MSNObject if the extended presence is known (MSNP18)
+            if self._protocol_version < 18 or \
+               contact.has_flag(profile.ContactFlag.EXTENDED_PRESENCE_KNOWN):
+                contact._server_property_changed("msn_object", msn_object)
             if icon_url is not None:
                 contact._server_attribute_changed('icon_url', icon_url)
 
@@ -475,17 +506,20 @@
     def _handle_UUX(self, command):
         pass
 
-    def _handle_UBN(self,command): # contact infos
+    def _handle_UBN(self, command): # contact infos
         if not command.payload:
             return
+        idx, account, guid = self.__parse_account_and_guid(command)
+        contact = self.__search_account(account)[0]
         type = int(command.arguments[1])
-        self.emit("buddy-notification-received", type, command)
+        payload = command.payload
+        self.emit("buddy-notification-received", contact, guid, type, payload)
 
-    def _handle_UBX(self,command): # contact infos
+    def _handle_UBX(self, command): # contact infos
         if not command.payload:
             return
-
-        idx, network_id, account = self._parse_account(command)
+        print command.payload
+        idx, network_id, account = self.__parse_network_and_account(command)
 
         try:
             tree = ElementTree.fromstring(command.payload)
@@ -493,42 +527,47 @@
             logger.error("Invalid XML data in received UBX command")
             return
 
-        cm = tree.find("./CurrentMedia")
-        if cm is not None and cm.text is not None:
-            parts = cm.text.split('\\0')
-            if parts[1] == 'Music' and parts[2] == '1':
-                cm = (parts[4].encode("utf-8"), parts[5].encode("utf-8"))
-            elif parts[2] == '0':
-                cm = None
-        else:
-            cm = None
-
-        pm = tree.find("./PSM")
-        if pm is not None and pm.text is not None:
-            pm = pm.text.encode("utf-8")
-        else:
-            pm = ""
-
-        ss = tree.find("./SignatureSound")
-        if ss is not None and ss.text is not None:
-            ss = ss.text.encode("utf-8")
-        else:
-            ss = None
+        utl = self.__find_node(tree, "./UserTileLocation", "")
+        cm_parts = self.__find_node(tree, "./CurrentMedia", "").split('\\0')
+        pm = self.__find_node(tree, "./PSM", "")
+        rmu = self.__find_node(tree, "./RMU", "")
+        ss = self.__find_node(tree, "./SignatureSound", None)
+        mg = self.__find_node(tree, "./MachineGuid", "{}").lower()[1:-1]
 
-        contacts = self._client.address_book.contacts.\
-                search_by_network_id(network_id).\
-                search_by_account(account)
+        msn_object = None
+        if utl != "":
+            msn_object = papyon.p2p.MSNObject.parse(self._client, utl)
 
-        if len(contacts) == 0:
-            logger.warning("Contact (network_id=%d) %s not found" % \
-                    (network_id, account))
+        cm = None
+        if len(cm_parts) >= 6 and cm_parts[1] == 'Music' and cm_parts[2] == '1':
+            cm = (cm_parts[4], cm_parts[5])
+
+        eps = tree.findall("./EndpointData")
+        end_points = {}
+        for ep in eps:
+            guid = ep.get("id").encode("utf-8")[1:-1]
+            caps = self.__find_node(ep, "Capabilities", "0:0")
+            end_points[guid] = profile.EndPoint(guid, caps)
+        peps = tree.findall("./PrivateEndpointData")
+        for pep in peps:
+            guid = pep.get("id").encode("utf-8")[1:-1]
+            if guid not in end_points: continue
+            end_point = end_points[guid]
+            end_point.name = self.__find_node(pep, "EpName", "")
+            end_point.idle = bool(self.__find_node(pep, "Idle", "").lower() == "true")
+            end_point.client_type = int(self.__find_node(pep, "ClientType", 0))
+            end_point.state = self.__find_node(pep, "State", "")
 
+        contacts = self.__search_account(account, network_id)
         for contact in contacts:
+            contact._add_flag(profile.ContactFlag.EXTENDED_PRESENCE_KNOWN)
+            contact._server_property_changed("end-points", end_points)
+            contact._server_property_changed("msn-object", msn_object)
             contact._server_property_changed("current-media", cm)
             contact._server_property_changed("personal-message", pm)
             contact._server_property_changed("signature-sound", ss)
 
-    def _handle_UUN(self,command): # UBN acknowledgment
+    def _handle_UUN(self, command): # UBN acknowledgment
         pass
 
     # --------- Contact List -------------------------------------------------
@@ -559,8 +598,7 @@
                     profile[name] = value.strip()
             self._client.profile._server_property_changed("profile", profile)
 
-            self._send_command("BLP",
-                    (self._client.profile.privacy,))
+            self.set_privacy(self._client.profile.privacy)
             self._state = ProtocolState.SYNCHRONIZING
             self._client.address_book.sync()
         elif content_type[0] in \
@@ -590,9 +628,9 @@
             #New mail
             m = HTTPMessage()
             m.parse(message.body)
-            name = m.get_header('From')
+            name = decode_rfc2047_string(m.get_header('From'))
             address = m.get_header('From-Addr')
-            subject = m.get_header('Subject')
+            subject = decode_rfc2047_string(m.get_header('Subject'))
             message_url = m.get_header('Message-URL')
             post_url = m.get_header('Post-URL')
             post_id = m.get_header('id')
@@ -616,16 +654,9 @@
                 self._client.mailbox._unread_mail_increased(delta)
 
     def _handle_UBM(self, command):
-        idx, network_id, account = self._parse_account(command)
-
-        contacts = self._client.address_book.contacts.\
-                search_by_network_id(network_id).\
-                search_by_account(account)
-
-        if len(contacts) == 0:
-            logger.warning("Contact (network_id=%d) %s not found" % \
-                    (network_id, account))
-        else:
+        idx, network_id, account = self.__parse_network_and_account(command)
+        contacts = self.__search_account(account, network_id)
+        if len(contacts) > 0:
             contact = contacts[0]
             message = Message(contact, command.payload)
             self.emit("unmanaged-message-received", contact, message)
@@ -671,7 +702,7 @@
             callback(post_url, form_dict)
 
     # --------- Invitation ---------------------------------------------------
-    def _handle_RNG(self,command):
+    def _handle_RNG(self, command):
         session_id = command.arguments[0]
         host, port = command.arguments[1].split(':',1)
         port = int(port)
@@ -684,17 +715,21 @@
         self.emit("switchboard-invitation-received", session, inviter)
 
     # --------- Challenge ----------------------------------------------------
-    def _handle_QNG(self,command):
+    def _handle_QNG(self, command):
         pass
 
-    def _handle_QRY(self,command):
+    def _handle_QRY(self, command):
         pass
 
-    def _handle_CHL(self,command):
+    def _handle_CHL(self, command):
         response = _msn_challenge(command.arguments[0])
         self._send_command('QRY',
                 (ProtocolConstant.PRODUCT_ID,), payload=response)
 
+    # --------- Notification -------------------------------------------------
+    def _handle_NOT(self, command):
+        pass
+
     # callbacks --------------------------------------------------------------
     def _connect_cb(self, transport):
         self.__switchboard_callbacks = PriorityQueue()
@@ -733,9 +768,16 @@
                 address_book.profile.display_name)
 
         contacts = address_book.contacts.group_by_domain()
+        mask = ~(profile.Membership.REVERSE | profile.Membership.PENDING)
+
+        for contact in address_book.contacts:
+            if (contact.memberships & mask & ~profile.Membership.FORWARD) == \
+                    (profile.Membership.ALLOW | profile.Membership.BLOCK):
+                logger.warning("Contact is on both Allow and Block list; " \
+                               "removing from Allow list (%s)" % contact.account)
+                contact._remove_membership(profile.Membership.ALLOW)
 
         payloads = ['<ml l="1">']
-        mask = ~(profile.Membership.REVERSE | profile.Membership.PENDING)
         for domain, contacts in contacts.iteritems():
             payloads[-1] += '<d n="%s">' % domain
             for contact in contacts:
--- papyon/msnp/switchboard.py
+++ papyon/msnp/switchboard.py
@@ -108,6 +108,7 @@
         BaseProtocol.__init__(self, client, transport, proxies)
         gobject.GObject.__init__(self)
         self.participants = {}
+        self.end_points = {}
         self.__session_id = session_id
         self.__key = key
         self.__state = ProtocolState.CLOSED
@@ -115,7 +116,14 @@
 
         self.__invitations = {}
 
+        logger.info("New switchboard session %s" % session_id)
+        client.profile.connect("end-point-added", self._on_end_point_added)
+
     # Properties ------------------------------------------------------------
+    @property
+    def session_id(self):
+        return self.__session_id
+
     def __get_state(self):
         return self.__state
     def __set_state(self, state):
@@ -155,18 +163,25 @@
         self._inviting = True
         self._send_command('CAL', (contact.account,) )
 
-    def send_message(self, message, ack, callback=None, cb_args=()):
+    def send_message(self, message, ack, callback=None):
         """Send a message to all contacts in this switchboard
 
             @param message: the message to send
             @type message: L{message.Message}"""
         assert(self.state == ProtocolState.OPEN)
+
+        cb = None
+        cb_args = ()
+        if callback:
+            cb = callback[0]
+            cb_args = callback[1:]
+
         return self._send_command('MSG',
                 (ack,),
                 message,
                 True,
                 self.__on_message_sent,
-                message, callback, cb_args)
+                message, cb, cb_args)
 
     def __on_message_sent(self, message, user_callback, user_cb_args):
         self.emit("message-sent", message)
@@ -175,8 +190,12 @@
 
     def leave(self):
         """Leave the conversation"""
-        assert(self.state == ProtocolState.OPEN)
+        if self.state != ProtocolState.OPEN:
+            return
+        logger.info("Leaving switchboard %s" % self.__session_id)
         self._send_command('OUT')
+        self._state = ProtocolState.CLOSING
+
     # Handlers ---------------------------------------------------------------
     # --------- Authentication -----------------------------------------------
     def _handle_ANS(self, command):
@@ -192,70 +211,96 @@
         self._state = ProtocolState.SYNCHRONIZING
         self._state = ProtocolState.SYNCHRONIZED
         self._state = ProtocolState.OPEN
+        if self._client.protocol_version >= 16:
+            self.invite_user(self._client.profile)
 
     def _handle_OUT(self, command):
         pass
     # --------- Invitation ---------------------------------------------------
-    def __participant_join(self, account, display_name, client_id):
-        if self._client.protocol_version >= 16:
-            if account.split(";")[0] == self._client.profile.account:
-                return # ignore our own user
+    def __parse_account(self, account):
+        """Parse account string and extract end-point info if available."""
+        if ';' in account:
+            account, guid = account.split(';', 1)
+            return account, guid[1:-1]
+        else:
+            return account, None
+
+    def __search_account(self, account, display_name):
+        """Search account in address book and make sure it's not ourself."""
+        if account == self._client.profile.account:
+            return self._client.profile
+
         contacts = self._client.address_book.contacts.\
                 search_by_account(account)
         if len(contacts) == 0:
-            contact = papyon.profile.Contact(id=0,
+            return papyon.profile.Contact(id=0,
                     network_id=papyon.profile.NetworkID.MSN,
                     account=account,
                     display_name=display_name)
         else:
-            contact = contacts[0]
-        if contact in self.participants:
+            return contacts[0]
+
+    def __discard_invitation(self, account):
+        for trid, contact in self.__invitations.items():
+            if contact.account == account:
+                del self.__invitations[trid]
+                return
+            
+    def __participant_join(self, account, guid, display_name, client_id):
+        if guid is not None:
+            places = self.end_points.setdefault(account, [])
+            places.append(guid)
+            return # wait for the command without GUID
+        if account == self._client.profile.account:
+            return # ignore our own user
+        if account in self.participants:
             return # ignore duplicate users
+        contact = self.__search_account(account, display_name)
         contact._server_property_changed("client-capabilities", client_id)
         self.participants[account] = contact
         self.emit("user-joined", contact)
 
+    def __participant_left(self, account, guid):
+        if guid is not None:
+            places = self.end_points.setdefault(account, [])
+            places.remove(guid)
+            return # wait for the command without GUID to remove from participants
+        if account == self._client.profile.account:
+            return # ignore our own user
+        self.emit("user-left", self.participants[account])
+        del self.participants[account]
+
     def _handle_IRO(self, command):
-        account = command.arguments[2]
+        account, guid = self.__parse_account(command.arguments[2])
         display_name = urllib.unquote(command.arguments[3])
         client_id = command.arguments[4]
-        self.__participant_join(account, display_name, client_id)
+        self.__participant_join(account, guid, display_name, client_id)
+
+    def _handle_CAL(self, command):
+        pass
 
     def _handle_JOI(self, command):
-        account = command.arguments[0]
+        account, guid = self.__parse_account(command.arguments[0])
         display_name = urllib.unquote(command.arguments[1])
         client_id = command.arguments[2]
-        self.__participant_join(account, display_name, client_id)
-        if len(self.__invitations) == 0:
-            self._inviting = False
-
-    def _handle_CAL(self, command):
-        # this should be followed by a JOI, so we only change
-        # the self._inviting state until we get the actual JOI
-        del self.__invitations[command.transaction_id]
+        self.__participant_join(account, guid, display_name, client_id)
+        if guid is None:
+            self.__discard_invitation(account)
+            if len(self.__invitations) == 0:
+                self._inviting = False
 
     def _handle_BYE(self, command):
-        if len(command.arguments) == 1:
-            account = command.arguments[0]
-            self.emit("user-left", self.participants[account])
-            del self.participants[account]
-        else:
-            self._state = ProtocolState.CLOSED
-            self.participants = {}
+        account, guid = self.__parse_account(command.arguments[0])
+        self.__participant_left(account, guid)
+        end_points = self.end_points.get(self._client.profile.account, [])
+        if len(self.participants) == 0 and len(end_points) == 0:
+            self.leave()
 
     # --------- Messenging ---------------------------------------------------
     def _handle_MSG(self, command):
         account = command.arguments[0]
         display_name = urllib.unquote(command.arguments[1])
-        contacts = self._client.address_book.contacts.\
-                search_by_account(account)
-        if len(contacts) == 0:
-            contact = papyon.profile.Contact(id=0,
-                    network_id=papyon.profile.NetworkID.MSN,
-                    account=account,
-                    display_name=display_name)
-        else:
-            contact = contacts[0]
+        contact = self.__search_account(account, display_name)
         message = Message(contact, command.payload)
         self.emit("message-received", message)
 
@@ -297,6 +342,11 @@
         self._state = ProtocolState.AUTHENTICATING
 
     def _disconnect_cb(self, transport, reason):
-        logger.info("Disconnected")
+        logger.info("Disconnected (%s)" % self.__session_id)
         self._state = ProtocolState.CLOSED
 
+    def _on_end_point_added(self, profile, end_point):
+        if self.state != ProtocolState.OPEN:
+            return
+        logger.info("New end point connected, re-invite local user")
+        self.invite_user(profile)
--- papyon/msnp2p/SLP.py
+++ papyon/msnp2p/SLP.py
@@ -21,7 +21,7 @@
 
 from papyon.gnet.message.HTTP import HTTPMessage
 from papyon.msnp2p.exceptions import ParseError
-from papyon.msnp2p.constants import SLPContentType
+from papyon.msnp2p.constants import SLPContentType, SLPStatus
 
 import base64
 import uuid
@@ -35,7 +35,8 @@
 class SLPMessage(HTTPMessage):
     STD_HEADERS = ["To", "From", "Via", "CSeq", "Call-ID", "Max-Forwards"]
 
-    def __init__(self, to="", frm="", branch="", cseq=0, call_id="", max_forwards=0):
+    def __init__(self, to="", frm="", branch="", cseq=0, call_id="",
+            max_forwards=0, on_behalf=""):
         HTTPMessage.__init__(self)
         self.add_header("To", "<msnmsgr:%s>" % to)
         self.add_header("From", "<msnmsgr:%s>" % frm)
@@ -45,6 +46,8 @@
         if call_id:
             self.add_header("Call-ID", call_id)
         self.add_header("Max-Forwards", str(max_forwards))
+        if on_behalf:
+            self.add_header("On-Behalf", on_behalf)
 
         # Make the body a SLP Message wih "null" content type
         self.body = SLPNullBody()
@@ -374,13 +377,18 @@
 SLPMessageBody.register_content(SLPContentType.TRANSFER_RESPONSE, SLPTransferResponseBody)
 
 class SLPSessionCloseBody(SLPMessageBody):
-    def __init__(self, context=None, session_id=None, s_channel_state=None,
-            capabilities_flags=None):
+    def __init__(self, context=None, session_id=None, reason=None,
+            s_channel_state=None, capabilities_flags=None):
         SLPMessageBody.__init__(self, SLPContentType.SESSION_CLOSE,
                 session_id, s_channel_state, capabilities_flags)
 
         if context is not None:
             self.add_header("Context",  base64.b64encode(context));
+        if reason is not None:
+            if reason[0] == SLPStatus.ACCEPTED:
+                self.add_header("AcceptedBy", "{%s}" % reason[1].upper())
+            elif reason[0] == SLPStatus.DECLINED:
+                self.add_header("DeclinedBy", "{%s}" % reason[1].upper())
 
     @property
     def context(self):
--- papyon/msnp2p/constants.py
+++ papyon/msnp2p/constants.py
@@ -55,3 +55,14 @@
     INVITE = 'INVITE'
     BYE = 'BYE'
     ACK = 'ACK'
+
+class SLPStatus(object):
+    ACCEPTED = 200
+    ERROR = 500
+    DECLINED = 603
+
+class PeerInfo(object):
+    PROTOCOL_VERSION = 512
+    IMPLEMENTATION_ID = 0
+    VERSION = 3584
+    CAPABILITIES = 271
--- papyon/msnp2p/filetransfer.py
+++ papyon/msnp2p/filetransfer.py
@@ -18,18 +18,25 @@
 # along with this program; if not, write to the Free Software
 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
-from papyon.msnp2p.constants import EufGuid
+from papyon.msnp2p.constants import ApplicationID, EufGuid
 from papyon.msnp2p.session import P2PSession
 
+import gobject
 import struct
 
 __all__ = ['FileTransferSession']
 
 class FileTransferSession(P2PSession):
 
-    def __init__(self, session_manager, peer, application_id, message=None):
-        P2PSession.__init__(self, session_manager, peer,
-                EufGuid.FILE_TRANSFER, application_id, message)
+    __gsignals__ = {
+            "canceled" : (gobject.SIGNAL_RUN_FIRST,
+                gobject.TYPE_NONE,
+                ())
+    }
+
+    def __init__(self, session_manager, peer, guid, message=None):
+        P2PSession.__init__(self, session_manager, peer, guid,
+                EufGuid.FILE_TRANSFER, ApplicationID.FILE_TRANSFER, message)
         self._filename = ""
         self._size = 0
         self._has_preview = False
@@ -38,7 +45,7 @@
         self._data = None
 
         if message is not None:
-            self.parse_context(message.body.context)
+            self._parse_context(message.body.context)
 
     @property
     def filename(self):
@@ -59,7 +66,7 @@
     def invite(self, filename, size):
         self._filename = filename
         self._size = size
-        context = self.build_context()
+        context = self._build_context()
         self._invite(context)
 
     def accept(self):
@@ -73,18 +80,23 @@
 
     def send(self, data):
         self._data = data
-        self._send_p2p_data("\x00" * 4)
-        self._send_p2p_data(self._data, True)
+        self._send_data("\x00" * 4)
+        self._send_data(self._data)
 
-    def parse_context(self, context):
+    def _parse_context(self, context):
         info = struct.unpack("<5I", context[0:20])
         self._size = info[2]
         self._has_preview = not bool(info[4])
         self._filename = unicode(context[20:570], "utf-16-le").rstrip("\x00")
 
-    def build_context(self):
-        filename = self._filename.decode('ascii').encode('utf-16_le')
+    def _build_context(self):
+        filename = self._filename.encode('utf-16_le')
         context = struct.pack("<5I", 574, 2, self._size, 0, int(self._has_preview))
         context += struct.pack("550s", filename)
         context += "\xFF" * 4
         return context
+
+    def _on_bye_received(self, message):
+        if not self.completed:
+            self.emit("canceled")
+        self._dispose()
--- papyon/msnp2p/msnobject.py
+++ papyon/msnp2p/msnobject.py
@@ -33,25 +33,36 @@
 __all__ = ['MSNObjectSession']
 
 class MSNObjectSession(P2PSession):
-    def __init__(self, session_manager, peer, application_id, message=None):
-        P2PSession.__init__(self, session_manager, peer,
+    def __init__(self, session_manager, peer, peer_guid, application_id,
+            message=None, context=None):
+        P2PSession.__init__(self, session_manager, peer, peer_guid,
                 EufGuid.MSN_OBJECT, application_id, message)
 
+        self._context = None
         if message is not None:
             self._application_id = message.body.application_id
             try:
                 self._context = message.body.context.strip('\x00')
             except AttributeError:
                 raise SLPError("Incoming INVITE without context")
+        if context is not None:
+            self._context = context
+
+    @property
+    def context(self):
+        return self._context
 
     def accept(self, data_file):
         self._respond(200)
-        self._send_p2p_data("\x00" * 4)
-        self._send_p2p_data(data_file)
+        self._send_data("\x00" * 4)
+        self._send_data(data_file)
 
     def reject(self):
         self._respond(603)
 
-    def invite(self, context):
-        self._invite(context)
+    def invite(self):
+        self._invite(self._context)
         return False
+
+    def cancel(self):
+        self._close()
--- papyon/msnp2p/session.py
+++ papyon/msnp2p/session.py
@@ -30,6 +30,7 @@
 import logging
 import random
 import uuid
+import os
 
 __all__ = ['P2PSession']
 
@@ -45,22 +46,37 @@
             "accepted" : (gobject.SIGNAL_RUN_FIRST,
                 gobject.TYPE_NONE,
                 ()),
+            "rejected" : (gobject.SIGNAL_RUN_FIRST,
+                gobject.TYPE_NONE,
+                ()),
             "completed" : (gobject.SIGNAL_RUN_FIRST,
                 gobject.TYPE_NONE,
                 (object,)),
             "progressed" : (gobject.SIGNAL_RUN_FIRST,
                 gobject.TYPE_NONE,
-                (object,))
+                (object,)),
+            "disposed" : (gobject.SIGNAL_RUN_FIRST,
+                gobject.TYPE_NONE,
+                ())
     }
 
-    def __init__(self, session_manager, peer, euf_guid="", application_id=0,
-            message=None):
+    def __init__(self, session_manager, peer, peer_guid=None, euf_guid="",
+            application_id=0, message=None):
         gobject.GObject.__init__(self)
         self._session_manager = session_manager
+        self._transport_manager = session_manager._transport_manager
+        self._client = session_manager._client
         self._peer = peer
+        self._peer_guid = peer_guid
 
         self._euf_guid = euf_guid
         self._application_id = application_id
+        self._completed = False
+
+        self._version = 1
+        if self._client.profile.client_id.supports_p2pv2 and \
+                peer.client_capabilities.supports_p2pv2:
+            self._version = 2
 
         if message is not None:
             self._id = message.body.session_id
@@ -95,6 +111,10 @@
         return self._incoming
 
     @property
+    def completed(self):
+        return self._completed
+
+    @property
     def call_id(self):
         return self._call_id
 
@@ -102,59 +122,78 @@
     def peer(self):
         return self._peer
 
-    def set_receive_data_buffer(self, buffer, total_size):
-        blob = MessageBlob(self._application_id, buffer, total_size, self.id)
-        self._session_manager._transport_manager.register_writable_blob(blob)
+    @property
+    def peer_guid(self):
+        return self._peer_guid
+
+    @property
+    def local_id(self):
+        if self._version >= 2:
+            return "%s;{%s}" % (self._client.profile.account,
+                    self._client.machine_guid)
+        return self._client.profile.account
+
+    @property
+    def remote_id(self):
+        if self._version >= 2:
+            return "%s;{%s}" % (self._peer.account, self._peer_guid)
+        return self._peer.account
+
+    def set_receive_data_buffer(self, buffer, size):
+        self._transport_manager.register_data_buffer(self.id, buffer, size)
 
     def _invite(self, context):
         body = SLPSessionRequestBody(self._euf_guid, self._application_id,
                 context, self._id)
         message = SLPRequestMessage(SLPRequestMethod.INVITE,
-                "MSNMSGR:" + self._peer.account,
-                to=self._peer.account,
-                frm=self._session_manager._client.profile.account,
+                "MSNMSGR:" + self.remote_id,
+                to=self.remote_id,
+                frm=self.local_id,
                 branch=self._branch,
                 cseq=self._cseq,
                 call_id=self._call_id)
         message.body = body
-        self._send_p2p_data(message)
+        self._send_slp_message(message)
 
     def _transreq(self):
         self._cseq = 0
         body = SLPTransferRequestBody(self._id, 0, 1)
         message = SLPRequestMessage(SLPRequestMethod.INVITE,
-                "MSNMSGR:" + self._peer.account,
-                to=self._peer.account,
-                frm=self._session_manager._client.profile.account,
+                "MSNMSGR:" + self.remote_id,
+                to=self.remote_id,
+                frm=self.local_id,
                 branch=self._branch,
                 cseq=self._cseq,
                 call_id=self._call_id)
         message.body = body
-        self._send_p2p_data(message)
+        self._send_slp_message(message)
 
     def _respond(self, status_code):
         body = SLPSessionRequestBody(session_id=self._id, capabilities_flags=None,
                 s_channel_state=None)
         self._cseq += 1
         response = SLPResponseMessage(status_code,
-            to=self._peer.account,
-            frm=self._session_manager._client.profile.account,
+            to=self.remote_id,
+            frm=self.local_id,
             cseq=self._cseq,
             branch=self._branch,
             call_id=self._call_id)
         response.body = body
-        self._send_p2p_data(response)
+        self._send_slp_message(response)
+
+        # close other end points so we are the only one answering
+        self._close_end_points(status_code)
 
     def _respond_transreq(self, transreq, status, body):
         self._cseq += 1
         response = SLPResponseMessage(status,
-            to=self._peer.account,
-            frm=self._session_manager._client.profile.account,
+            to=self.remote_id,
+            frm=self.local_id,
             cseq=self._cseq,
             branch=transreq.branch,
             call_id=self._call_id)
         response.body = body
-        self._send_p2p_data(response)
+        self._send_slp_message(response)
 
     def _accept_transreq(self, transreq, bridge, listening, nonce, local_ip,
             local_port, extern_ip, extern_port):
@@ -165,39 +204,66 @@
     def _decline_transreq(self, transreq):
         body = SLPTransferResponseBody(session_id=self._id)
         self._respond_transreq(transreq, 603, body)
+        self._dispose()
 
-    def _close(self, context=None):
+    def _close(self, context=None, reason=None):
         body = SLPSessionCloseBody(context=context, session_id=self._id,
-                s_channel_state=0)
+                reason=reason, s_channel_state=0)
         self._cseq = 0
         self._branch = "{%s}" % uuid.uuid4()
         message = SLPRequestMessage(SLPRequestMethod.BYE,
-                "MSNMSGR:" + self._peer.account,
-                to=self._peer.account,
-                frm=self._session_manager._client.profile.account,
+                "MSNMSGR:" + self.remote_id,
+                to=self.remote_id,
+                frm=self.local_id,
                 branch=self._branch,
                 cseq=self._cseq,
                 call_id=self._call_id)
         message.body = body
-        self._send_p2p_data(message)
+        self._send_slp_message(message)
         self._dispose()
 
+    def _close_end_points(self, status):
+        """Send BYE to other end points; this client already answered.
+            @param status: response we sent to the peer"""
+        if len(self._peer.end_points) > 0:
+            return # if the peer supports MPOP, let him do the work
+
+        for end_point in self._client.profile.end_points.values():
+            if end_point.id == self._client.machine_guid:
+                continue
+            self._close_end_point(end_point, status)
+
+    def _close_end_point(self, end_point, status):
+        reason = (status, self._client.machine_guid)
+        body = SLPSessionCloseBody(session_id=self._id, reason=reason,
+                s_channel_state=0)
+        self._cseq = 0
+        self._branch = "{%s}" % uuid.uuid4()
+        message = SLPRequestMessage(SLPRequestMethod.BYE,
+                "MSNMSGR:" + self._client.profile.account,
+                to=self._client.profile.account,
+                frm=self._peer.account,
+                branch=self._branch,
+                cseq=self._cseq,
+                call_id=self._call_id,
+                on_behalf=self._peer.account)
+        message.body = body
+        self._transport_manager.send_slp_message(self._client.profile,
+                end_point.id, self._application_id, message)
+
     def _dispose(self):
+        logger.info("Session %s disposed" % self._id)
+        self._session_manager._transport_manager.cleanup(self._id)
         self._session_manager._unregister_session(self)
+        self.emit("disposed")
 
-    def _send_p2p_data(self, data_or_file, is_file=False):
-        if isinstance(data_or_file, SLPMessage):
-            session_id = 0
-            data = str(data_or_file)
-            total_size = len(data)
-        else:
-            session_id = self._id
-            data = data_or_file
-            total_size = None
-
-        blob = MessageBlob(self._application_id,
-                data, total_size, session_id, None, is_file)
-        self._session_manager._transport_manager.send(self.peer, blob)
+    def _send_slp_message(self, message):
+        self._transport_manager.send_slp_message(self.peer, self.peer_guid,
+                self._application_id, message)
+
+    def _send_data(self, data):
+        self._transport_manager.send_data(self.peer, self.peer_guid,
+                self._application_id, self._id, data)
 
     def _on_blob_sent(self, blob):
         if blob.session_id == 0:
@@ -225,11 +291,14 @@
                     print "Unhandled signaling blob :", message
             elif isinstance(message, SLPResponseMessage):
                 if isinstance(message.body, SLPSessionRequestBody):
-                    if message.status is 200:
-                        self._on_session_accepted()
+                    if message.status == 200:
                         self.emit("accepted")
-                    elif message.status is 603:
+                        self._on_session_accepted()
+                    elif message.status == 603:
+                        self.emit("rejected")
                         self._on_session_rejected(message)
+                else:
+                    print "Unhandled response blob :", message
             return
 
         self._on_data_blob_received(blob)
@@ -246,12 +315,14 @@
 
     def _on_data_blob_sent(self, blob):
         logger.info("Session data transfer completed")
-        blob.data.seek(0, 0)
+        blob.data.seek(0, os.SEEK_SET)
+        self._completed = True
         self.emit("completed", blob.data)
 
     def _on_data_blob_received(self, blob):
         logger.info("Session data transfer completed")
-        blob.data.seek(0, 0)
+        blob.data.seek(0, os.SEEK_SET)
+        self._completed = True
         self.emit("completed", blob.data)
         self._close()
 
@@ -261,12 +332,12 @@
         pass
 
     def _on_bye_received(self, message):
-        pass
+        self._dispose()
 
     def _on_session_accepted(self):
         pass
 
     def _on_session_rejected(self, message):
-        pass
+        self._dispose()
 
 gobject.type_register(P2PSession)
--- papyon/msnp2p/session_manager.py
+++ papyon/msnp2p/session_manager.py
@@ -61,14 +61,14 @@
 
     def _register_session(self, session):
         self._sessions[session.id] = session
+        self._transport_manager.remove_from_blacklist(session.id)
 
     def _unregister_session(self, session):
         del self._sessions[session.id]
+        self._transport_manager.add_to_blacklist(session.id)
 
     def _on_chunk_transferred(self, chunk):
-        session_id = chunk.header.session_id
-        if session_id == 0:
-            return
+        session_id = chunk.session_id
         session = self._get_session(session_id)
         if session is None:
             return
@@ -86,6 +86,21 @@
                 return session
         return None
 
+    def _find_contact(self, account):
+        if ';' in account:
+            account, guid = account.split(';', 1)
+            guid = guid[1:-1]
+        else:
+            guid = None
+
+        if account == self._client.profile.account:
+            peer = self._client.profile
+        else:
+            peer = self._client.address_book.search_or_build_contact(
+                    account, papyon.profile.NetworkID.MSN)
+
+        return peer, guid
+
     def _blob_to_session(self, blob):
         session_id = blob.session_id
 
@@ -144,12 +159,11 @@
                     message.method == SLPRequestMethod.INVITE:
                 if isinstance(message.body, SLPSessionRequestBody):
                     # Find the contact we received the message from
-                    peer = self._client.address_book.search_or_build_contact(
-                            message.frm, papyon.profile.NetworkID.MSN)
+                    peer, guid = self._find_contact(message.frm)
                     try:
                         for handler in self._handlers:
                             if handler._can_handle_message(message):
-                                session = handler._handle_message(peer, message)
+                                session = handler._handle_message(peer, guid, message)
                                 if session is not None:
                                     self._register_session(session)
                                     break
--- papyon/msnp2p/test.py
+++ papyon/msnp2p/test.py
@@ -10,6 +10,7 @@
 
 import logging
 import gobject
+import os
 
 logging.basicConfig(level=logging.DEBUG)
 
@@ -41,9 +42,9 @@
             path = self._client.msn_object_path
             f = open(path, 'r')
             old_pos = f.tell()
-            f.seek(0, 2)
+            f.seek(0, os.SEEK_END)
             size = f.tell()
-            f.seek(old_pos,0)
+            f.seek(old_pos, os.SEEK_SET)
             msn_object = \
                 papyon.p2p.MSNObject(self._client.profile,
                                     size, papyon.p2p.MSNObjectType.DISPLAY_PICTURE,
--- papyon/msnp2p/transport/TLP.py
+++ papyon/msnp2p/transport/TLP.py
@@ -18,11 +18,13 @@
 # along with this program; if not, write to the Free Software
 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
+from papyon.msnp2p.transport import TLPv1, TLPv2
 import papyon.util.string_io as StringIO
 
 import struct
 import random
 import logging
+import os
 
 __all__ = ['MessageBlob']
 
@@ -39,153 +41,28 @@
     """
     return random.randint(1000, max)
 
-_previous_chunk_id = _generate_id(MAX_INT32 - 1)
-def _chunk_id():
-    global _previous_chunk_id
-    _previous_chunk_id += 1
-    if _previous_chunk_id == MAX_INT32:
-        _previous_chunk_id = 1
-    return _previous_chunk_id
-
-class TLPHeader(object):
-    SIZE = 48
-
-    def __init__(self, *header):
-        header = list(header)
-        header[len(header):] = [0] * (9 - len(header))
-
-        self.session_id = header[0]
-        self.blob_id = header[1]
-        self.blob_offset = header[2]
-        self.blob_size = header[3]
-        self.chunk_size = header[4]
-        self.flags = header[5]
-        self.dw1 = header[6]
-        self.dw2 = header[7]
-        self.qw1 = header[8]
-
-    def __str__(self):
-        return struct.pack("<LLQQLLLLQ", self.session_id,
-                self.blob_id,
-                self.blob_offset,
-                self.blob_size,
-                self.chunk_size,
-                self.flags,
-                self.dw1,
-                self.dw2,
-                self.qw1)
-
-    @staticmethod
-    def parse(header_data):
-        header = struct.unpack("<LLQQLLLLQ", header_data[:48])
-        session_id = header[0]
-        blob_id = header[1]
-        blob_offset = header[2]
-        blob_size = header[3]
-        chunk_size = header[4]
-        flags = header[5]
-        dw1 = header[6]
-        dw2 = header[7]
-        qw1 = header[8]
-        return TLPHeader(session_id, blob_id, blob_offset, blob_size,
-                chunk_size, flags, dw1, dw2, qw1)
-
-
-class TLPFlag(object):
-    NAK = 0x1
-    ACK = 0x2
-    RAK = 0x4
-    RST = 0x8
-    FILE = 0x10
-    EACH = 0x20
-    CAN = 0x40
-    ERR = 0x80
-    KEY = 0x100
-    CRYPT = 0x200
-    UNKNOWN = 0x1000000
-
-
 class MessageChunk(object):
-    def __init__(self, header=None, body=None):
-        if header is None:
-            header = TLPHeader()
-        self.header = header
-        if body is None:
-            body = ""
-        self.body = body
-        self.application_id = 0
-
-    def __str__(self):
-        return str(self.header) + str(self.body)
-
-    def is_control_chunk(self):
-        return self.header.flags & 0xCF
-
-    def is_ack_chunk(self):
-        return self.header.flags & (TLPFlag.NAK | TLPFlag.ACK)
-
-    def is_nonce_chunk(self):
-        return self.header.flags & TLPFlag.KEY
-
-    def is_data_preparation_chunk(self):
-        return (self.header.chunk_size == 4 and self.header.blob_size == 4 and
-                self.body == "\x00\x00\x00\x00" and
-                not self.header.flags & TLPFlag.FILE)
-
-    def has_progressed(self):
-        return self.header.flags & TLPFlag.EACH
-
-    def require_ack(self):
-        if self.is_ack_chunk():
-            return False
-        current_size = self.header.chunk_size + self.header.blob_offset
-        if current_size == self.header.blob_size:
-            return True
-        return False
-
-    def get_nonce(self):
-        """Get the nonce from the chunk. The chunk needs to have the KEY flag
-           for that nonce to make sense (use is_nonce_chunk to check that)"""
-
-        if not self.is_nonce_chunk():
-            return "00000000-0000-0000-0000-000000000000"
-
-        bytes = ""
-        bytes += struct.pack(">L", self.header.dw1)
-        bytes += struct.pack(">H", self.header.dw2 & 0xFFFF)
-        bytes += struct.pack(">H", self.header.dw2 >> 16)
-        bytes += struct.pack("<Q", self.header.qw1)
-
-        nonce = [("%X" % ord(byte)).zfill(2) for byte in bytes]
-        for idx in (4, 7, 10, 13):
-            nonce.insert(idx, '-')
-        return "".join(nonce)
-
-    def set_nonce(self, nonce):
-        """Set the chunk headers from a nonce and make it a nonce chunk by
-           adding the KEY flag."""
-
-        nonce = filter(lambda c: c not in '{-}', nonce)
-        bytes = ""
-        for i in range(0, len(nonce), 2):
-            bytes += chr(int(nonce[i:i+2], 16))
-
-        self.header.dw1 = struct.unpack(">L", bytes[0:4])[0]
-        self.header.dw2 = struct.unpack(">H", bytes[4:6])[0]
-        self.header.dw2 += struct.unpack(">H", bytes[6:8])[0] << 16
-        self.header.qw1 = struct.unpack("<Q", bytes[8:16])[0]
-        self.header.flags |= TLPFlag.KEY
-
     @staticmethod
-    def parse(data):
-        header = TLPHeader.parse(data[:48])
-        body = data[48:]
-        return MessageChunk(header, body)
+    def create(version, app_id, session_id, blob_id, offset, blob_size,
+            max_size, sync):
+        if version in (1, 2):
+            module = globals()["TLPv%i" % version]
+            return module.MessageChunk.create(app_id, session_id, blob_id,
+                    offset, blob_size, max_size, sync)
+        else:
+            return None
 
+    @staticmethod
+    def parse(version, data):
+        if version in (1, 2):
+            module = globals()["TLPv%i" % version]
+            return module.MessageChunk.parse(data)
+        else:
+            return None
 
 class MessageBlob(object):
     def __init__(self, application_id, data, total_size=None,
-            session_id=None, blob_id=None, is_file=False):
+            session_id=None, blob_id=None):
         if data is not None:
             if isinstance(data, str):
                 if len(data) > 0:
@@ -195,9 +72,9 @@
                     data = StringIO.StringIO()
 
             if total_size is None:
-                data.seek(0, 2) # relative to the end
+                data.seek(0, os.SEEK_END) # relative to the end
                 total_size = data.tell()
-                data.seek(0, 0)
+                data.seek(0, os.SEEK_SET)
         else:
             total_size = 0
 
@@ -208,8 +85,9 @@
         if session_id is None:
             session_id = _generate_id()
         self.session_id = session_id
-        self.id = blob_id or _generate_id()
-        self.is_file = is_file
+        if blob_id is None:
+            blob_id = _generate_id()
+        self.id = blob_id
 
     def __del__(self):
         #if self.data is not None:
@@ -240,68 +118,31 @@
     def is_complete(self):
         return self.transferred == self.total_size
 
-    def is_data_blob(self):
-        return not self.is_control_blob()
-
-    def is_control_blob(self):
-        return False
-
     def read_data(self):
-        self.data.seek(0, 0)
+        self.data.seek(0, os.SEEK_SET)
         data = self.data.read()
-        self.data.seek(0, 0)
+        self.data.seek(0, os.SEEK_SET)
         return data
 
-    def get_chunk(self, max_size):
-        blob_offset = self.transferred
+    def get_chunk(self, version, max_size, sync=False):
+        chunk = MessageChunk.create(version, self.application_id, self.session_id,
+                self.id, self.transferred, self.total_size, max_size, sync)
 
         if self.data is not None:
-            self.data.seek(blob_offset, 0)
-            data = self.data.read(max_size - TLPHeader.SIZE)
+            self.data.seek(self.transferred, os.SEEK_SET)
+            data = self.data.read(chunk.size)
             assert len(data) > 0, "Trying to read more data than available"
         else:
             data = ""
         
-        header = TLPHeader()
-        header.session_id = self.session_id
-        header.blob_id = self.id
-        header.blob_offset = blob_offset
-        header.blob_size = self.total_size
-        header.chunk_size = len(data)
-        header.dw1 = _chunk_id()
-        if self.session_id != 0 and self.total_size != 4 and data != '\x00' * 4:
-            header.flags = TLPFlag.UNKNOWN | TLPFlag.EACH
-            if self.is_file:
-                header.flags |= TLPFlag.FILE
-
-        chunk = MessageChunk(header, data)
-        chunk.application_id = self.application_id
-        self.current_size += header.chunk_size
+        chunk.set_data(data)
+        self.current_size += chunk.size
         return chunk
 
     def append_chunk(self, chunk):
         assert self.data is not None, "Trying to write to a Read Only blob"
-        assert self.session_id == chunk.header.session_id, "Trying to append a chunk to the wrong blob"
-        assert self.id == chunk.header.blob_id, "Trying to append a chunk to the wrong blob"
-        self.data.seek(chunk.header.blob_offset, 0)
+        assert self.session_id == chunk.session_id, "Trying to append a chunk to the wrong blob"
+        assert self.id == chunk.blob_id, "Trying to append a chunk to the wrong blob"
+        self.data.seek(0, os.SEEK_END)
         self.data.write(chunk.body)
-        self.data.seek(0, 2)
         self.current_size = self.data.tell()
-
-
-class ControlBlob(MessageBlob):
-    def __init__(self, session_id, flags, dw1=0, dw2=0, qw1=0):
-        MessageBlob.__init__(self, 0, None)
-        header = TLPHeader(session_id, self.id, 0, 0, 0,
-                flags, dw1, dw2, qw1)
-        self.chunk = MessageChunk(header, "")
-
-    def __repr__(self):
-        return "<ControlBlob id=%x session_id=%x>" % (self.id, self.session_id)
-
-    def get_chunk(self, max_size):
-        return self.chunk
-    
-    def is_control_blob(self):
-        return True
-
--- papyon/msnp2p/transport/TLPv1.py
+++ papyon/msnp2p/transport/TLPv1.py
+# -*- coding: utf-8 -*-
+#
+# papyon - a python client library for Msn
+#
+# Copyright (C) 2010 Collabora Ltd.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+from papyon.msnp2p.constants import ApplicationID
+from papyon.util import debug
+from papyon.util.decorator import rw_property
+
+import struct
+import random
+import logging
+
+logger = logging.getLogger('papyon.msnp2p.transport')
+
+
+MAX_INT32 = 2147483647
+
+def _generate_id(max=MAX_INT32):
+    return random.randint(1000, max)
+
+
+class TLPFlag(object):
+    NAK = 0x1
+    ACK = 0x2
+    RAK = 0x4
+    RST = 0x8
+    FILE = 0x10
+    EACH = 0x20
+    CAN = 0x40
+    ERR = 0x80
+    KEY = 0x100
+    CRYPT = 0x200
+    UNKNOWN = 0x1000000
+
+
+class TLPHeader(object):
+    size = 48
+
+    def __init__(self, *header):
+        header = list(header)
+        header[len(header):] = [0] * (9 - len(header))
+
+        self.session_id = header[0]
+        self.blob_id = header[1]
+        self.blob_offset = header[2]
+        self.blob_size = header[3]
+        self.chunk_size = header[4]
+        self.flags = header[5]
+        self.dw1 = header[6]
+        self.dw2 = header[7]
+        self.qw1 = header[8]
+
+    def __str__(self):
+        return struct.pack("<LLQQLLLLQ", self.session_id,
+                self.blob_id,
+                self.blob_offset,
+                self.blob_size,
+                self.chunk_size,
+                self.flags,
+                self.dw1,
+                self.dw2,
+                self.qw1)
+
+    def parse(self, data):
+        fields = struct.unpack("<LLQQLLLLQ", data[:48])
+        self.session_id = fields[0]
+        self.blob_id = fields[1]
+        self.blob_offset = fields[2]
+        self.blob_size = fields[3]
+        self.chunk_size = fields[4]
+        self.flags = fields[5]
+        self.dw1 = fields[6]
+        self.dw2 = fields[7]
+        self.qw1 = fields[8]
+
+
+class MessageChunk(object):
+    def __init__(self, header=TLPHeader(), body="", application_id=0):
+        self.header = header
+        self.body = body
+        self.application_id = application_id
+
+    def __str__(self):
+        return str(self.header) + str(self.body)
+
+    @rw_property
+    def id():
+        def fget(self):
+            return self.header.dw1
+        def fset(self, value):
+            self.header.dw1 = value
+        return locals()
+
+    @property
+    def next_id(self):
+        if self.id + 1 == MAX_INT32:
+            return 1
+        return self.id + 1
+
+    @property
+    def session_id(self):
+        return self.header.session_id
+
+    @property
+    def blob_id(self):
+        return self.header.blob_id
+
+    @property
+    def ack_id(self):
+        return (self.header.blob_id, self.header.dw1)
+
+    @property
+    def acked_id(self):
+        return (self.header.dw1, self.header.dw2)
+
+    @property
+    def size(self):
+        return self.header.chunk_size
+
+    @property
+    def blob_size(self):
+        return self.header.blob_size
+
+    @property
+    def version(self):
+        return 1
+
+    def is_control_chunk(self):
+        return self.header.flags & 0xCF
+
+    def is_ack_chunk(self):
+        return self.header.flags & TLPFlag.ACK
+
+    def is_nak_chunk(self):
+        return self.header.flags & TLPFlag.NAK
+
+    def is_nonce_chunk(self):
+        return self.header.flags & TLPFlag.KEY
+
+    def is_data_preparation_chunk(self):
+        return (self.header.chunk_size == 4 and self.header.blob_size == 4 and
+                self.body == "\x00\x00\x00\x00" and
+                not self.header.flags & TLPFlag.FILE)
+
+    def is_signaling_chunk(self):
+        return (self.session_id == 0)
+
+    def has_progressed(self):
+        return self.header.flags & TLPFlag.EACH
+
+    def set_data(self, data):
+        self.body = data
+        self.header.chunk_size = len(data)
+
+        if self.session_id != 0 and self.blob_size != 4 and data != '\x00' * 4:
+            self.header.flags = TLPFlag.UNKNOWN | TLPFlag.EACH
+            if self.application_id is ApplicationID.FILE_TRANSFER:
+                self.header.flags |= TLPFlag.FILE
+
+    def require_ack(self):
+        if self.is_ack_chunk():
+            return False
+        current_size = self.header.chunk_size + self.header.blob_offset
+        if current_size == self.header.blob_size:
+            return True
+        return False
+
+    def create_ack_chunk(self):
+        flags = TLPFlag.ACK
+        if self.header.flags & TLPFlag.RAK:
+            flags |= TLPFlag.RAK
+
+        blob_id = _generate_id()
+        header = TLPHeader(self.header.session_id, blob_id, 0, 0, 0, flags,
+            self.header.blob_id, self.header.dw1, self.header.blob_size)
+        return MessageChunk(header)
+
+    def get_nonce(self):
+        """Get the nonce from the chunk. The chunk needs to have the KEY flag
+           for that nonce to make sense (use is_nonce_chunk to check that)"""
+
+        if not self.is_nonce_chunk():
+            return "00000000-0000-0000-0000-000000000000"
+
+        bytes = ""
+        bytes += struct.pack(">L", self.header.dw1)
+        bytes += struct.pack(">H", self.header.dw2 & 0xFFFF)
+        bytes += struct.pack(">H", self.header.dw2 >> 16)
+        bytes += struct.pack("<Q", self.header.qw1)
+
+        nonce = [("%X" % ord(byte)).zfill(2) for byte in bytes]
+        for idx in (4, 7, 10, 13):
+            nonce.insert(idx, '-')
+        return "".join(nonce)
+
+    def set_nonce(self, nonce):
+        """Set the chunk headers from a nonce and make it a nonce chunk by
+           adding the KEY flag."""
+
+        nonce = filter(lambda c: c not in '{-}', nonce)
+        bytes = ""
+        for i in range(0, len(nonce), 2):
+            bytes += chr(int(nonce[i:i+2], 16))
+
+        self.header.dw1 = struct.unpack(">L", bytes[0:4])[0]
+        self.header.dw2 = struct.unpack(">H", bytes[4:6])[0]
+        self.header.dw2 += struct.unpack(">H", bytes[6:8])[0] << 16
+        self.header.qw1 = struct.unpack("<Q", bytes[8:16])[0]
+        self.header.flags |= TLPFlag.KEY
+
+
+    @staticmethod
+    def create(app_id, session_id, blob_id, offset, blob_size, max_size, sync):
+        header = TLPHeader()
+        header.session_id = session_id
+        header.blob_id = blob_id
+        header.blob_offset = offset
+        header.blob_size = blob_size
+        header.chunk_size = min(blob_size - offset, max_size - header.size)
+        return MessageChunk(header, application_id=app_id)
+
+    @staticmethod
+    def parse(data):
+        header = TLPHeader()
+        header.parse(data[:48])
+        body = data[48:]
+        return MessageChunk(header, body)
+
+    def __repr__(self):
+        string = "TLPv1 chunk 0x%x: " % self.id
+        string += "blob %i, " % self.blob_id
+        if self.session_id:
+            string += "session %d, " % self.session_id
+        if self.is_ack_chunk():
+            string += "ACK 0x%x 0x%x" % self.acked_id
+        elif self.is_nak_chunk():
+            string += "NAK 0x%x 0x%x" % self.acked_id
+        if self.size > 0:
+            if self.session_id:
+                string += "data [%d bytes]" % self.size
+            else:
+                string += "SLP Message"
+                string += "\n\t" + debug.escape_string(self.body).\
+                        replace("\r\n", "\\r\\n\n\t")
+        return string
--- papyon/msnp2p/transport/TLPv2.py
+++ papyon/msnp2p/transport/TLPv2.py
+# -*- coding: utf-8 -*-
+#
+# papyon - a python client library for Msn
+#
+# Copyright (C) 2010 Collabora Ltd.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+from papyon.msnp2p.constants import ApplicationID, PeerInfo
+from papyon.util import debug
+from papyon.util.decorator import rw_property
+from papyon.util.tlv import TLV
+
+import struct
+import random
+import logging
+
+logger = logging.getLogger('msnp2p:transport')
+
+class TLPFlag(object):
+    NONE = 0x0
+    SYN  = 0x1
+    RAK  = 0x2
+
+class TLPParamType(object):
+    PEER_INFO = 0x1
+    ACK_SEQ = 0x2
+    NAK_SEQ = 0x3
+
+class DLPType(object):
+    SLP = 0x0
+    MSN_OBJECT = 0x4
+    FILE_TRANSFER = 0x6
+
+class DLPParamType(object):
+    DATA_REMAINING = 0x1
+
+TLPParamLength = {
+    TLPParamType.PEER_INFO: 12,
+    TLPParamType.ACK_SEQ: 4,
+    TLPParamType.NAK_SEQ: 4
+}
+
+DLPParamLength = {
+    DLPParamType.DATA_REMAINING: 8,
+}
+
+class TLPHeader(object):
+    """Transport Layer Protocol header v2:
+
+       0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1  ....
+       +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+       |L|O|Len|ChunkID|if L>8 then TLV else skip   .... | Payload
+       +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+
+       L        : Header Length
+       O        : Operation Code (see TLPFlag)
+       Len      : Length of payload (Data Header + Data)
+       ChunkID  : ID of chunk (last chunk ID + last chunk Len)
+       TLV      : See TLPParamType for possible types
+       Payload  : Data Header + Data
+
+       Data Header (if data length > 0)
+
+       0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1  ....
+       +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+       |L|T|PN |Session|if L>8 then TLV else skip   .... | Data
+       +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+
+       L        : Data Header Length
+       T        : Type-First combination (see DLPType for types)
+       PN       : Package number (common between chunks of the same blob)
+       Session  : Session Ientifier
+       TLV      : See DLPParamType for possible types"""
+
+    def __init__(self):
+        self.op_code = 0
+        self.chunk_size = 0
+        self.chunk_id = 0
+        self.session_id = 0
+        self.data_type = 0
+        self.first = False
+        self.package_number = 0
+        self.tlv = TLV(TLPParamLength)
+        self.data_tlv = TLV(DLPParamLength)
+
+    @property
+    def size(self):
+        size = 8 + len(self.tlv)
+        if self.chunk_size > 0:
+            size += 8 + len(self.data_tlv)
+        return size
+
+    @property
+    def data_size(self):
+        size = self.chunk_size
+        if self.chunk_size > 0:
+            size += 8 + len(self.data_tlv)
+        return size
+
+    @rw_property
+    def peer_info():
+        def fget(self):
+            return self.tlv.get(TLPParamType.PEER_INFO, "")
+        def fset(self, value):
+            self.tlv.update(TLPParamType.PEER_INFO, value)
+        return locals()
+
+    @rw_property
+    def ack_seq():
+        def fget(self):
+            return self.tlv.get(TLPParamType.ACK_SEQ, 0)
+        def fset(self, value):
+            self.tlv.update(TLPParamType.ACK_SEQ, value)
+        return locals()
+
+    @rw_property
+    def nak_seq():
+        def fget(self):
+            return self.tlv.get(TLPParamType.NAK_SEQ, 0)
+        def fset(self, value):
+            self.tlv.update(TLPParamType.NAK_SEQ, value)
+        return locals()
+
+    @rw_property
+    def tf_combination():
+        def fget(self):
+            return self.data_type | self.first
+        def fset(self, value):
+            self.first = bool(value & 0x01)
+            self.data_type = value & 0xFE
+        return locals()
+
+    @rw_property
+    def data_remaining():
+        def fget(self):
+            return self.data_tlv.get(DLPParamType.DATA_REMAINING, 0)
+        def fset(self, value):
+            self.data_tlv.update(DLPParamType.DATA_REMAINING, value)
+        return locals()
+
+    def __str__(self):
+        size = 8 + len(self.tlv)
+        data_size = self.chunk_size
+        data_header = ""
+        if data_size > 0:
+            data_header_size, data_header = self.build_data_header()
+            data_size += data_header_size
+        header = struct.pack(">BBHL", size, self.op_code, data_size,
+                self.chunk_id)
+        header += str(self.tlv)
+        header += data_header
+        return header
+    
+    def parse(self, data):
+        fields = struct.unpack(">BBHL", data[:8])
+        size = fields[0]
+        self.op_code = fields[1]
+        self.chunk_size = fields[2]
+        self.chunk_id = fields[3]
+        self.tlv.parse(data[8:], size - 8)
+        if self.chunk_size > 0:
+            dph_size = self.parse_data_header(data[size:])
+            self.chunk_size -= dph_size
+            size += dph_size
+        return size
+
+    def build_data_header(self):
+        size = len(self.data_tlv) + 8
+        header = struct.pack(">BBHL", size, self.tf_combination,
+                self.package_number, self.session_id)
+        header += str(self.data_tlv)
+        return size, header
+
+    def parse_data_header(self, data):
+        fields = struct.unpack(">BBHI", data[:8])
+        size = fields[0]
+        self.tf_combination = fields[1]
+        self.package_number = fields[2]
+        self.session_id = fields[3]
+        self.data_tlv.parse(data[8:], size - 8)
+        return size
+
+
+class MessageChunk(object):
+    def __init__(self, header, body="", application_id=0):
+        self.header = header
+        self.body = body
+        self.application_id = application_id
+
+    @rw_property
+    def id():
+        def fget(self):
+            return self.header.chunk_id
+        def fset(self, value):
+            self.header.chunk_id = value
+        return locals()
+
+    @property
+    def next_id(self):
+        return self.id + self.header.data_size
+
+    @rw_property
+    def application_id():
+        def fget(self):
+            return self._application_id
+        def fset(self, value):
+            self._application_id = value
+            if self.session_id == 0 or self.is_data_preparation_chunk():
+                self.header.data_type = DLPType.SLP
+            elif value is ApplicationID.FILE_TRANSFER:
+                self.header.data_type = DLPType.FILE_TRANSFER
+            elif value is ApplicationID.CUSTOM_EMOTICON_TRANSFER or \
+                 value is ApplicationID.DISPLAY_PICTURE_TRANSFER:
+                self.header.data_type = DLPType.MSN_OBJECT
+        return locals()
+
+    @property
+    def session_id(self):
+        return self.header.session_id
+
+    @property
+    def blob_id(self):
+        return self.header.package_number
+
+    @property
+    def ack_id(self):
+        return self.header.chunk_id + self.header.data_size
+
+    @property
+    def acked_id(self):
+        return self.header.ack_seq
+
+    @property
+    def naked_id(self):
+        return self.header.nak_seq
+
+    @property
+    def size(self):
+        return self.header.chunk_size
+
+    @property
+    def blob_size(self):
+        if not self.header.first:
+            return 0
+        return self.header.data_remaining + self.size
+
+    @property
+    def version(self):
+        return 2
+
+    def is_control_chunk(self):
+        return self.is_ack_chunk() or self.is_nak_chunk() or \
+                (self.require_ack() and self.size == 0)
+
+    def is_ack_chunk(self):
+        return self.header.ack_seq > 0
+
+    def is_nak_chunk(self):
+        return self.header.nak_seq > 0
+
+    def is_syn_request(self):
+        return (self.header.op_code & TLPFlag.SYN) and not self.is_ack_chunk()
+
+    def is_signaling_chunk(self):
+        return (self.header.data_type == DLPType.SLP)
+
+    def is_data_preparation_chunk(self):
+        return (self.header.first and self.size == 4)
+
+    def require_ack(self):
+        return self.header.op_code & TLPFlag.RAK
+
+    def has_progressed(self):
+        return True
+
+    def create_ack_chunk(self):
+        header = TLPHeader()
+        header.ack_seq = self.header.chunk_id + self.header.chunk_size
+
+        # if we received a SYN request, send a SYN/ACK with a copy of the
+        # peer info and ask for an ACK
+        if self.is_syn_request():
+            header.peer_info = self.header.peer_info
+            header.op_code = TLPFlag.SYN | TLPFlag.RAK
+
+        return MessageChunk(header)
+
+    def set_data(self, data):
+        self.body = data
+        self.header.chunk_size = len(data)
+
+    @staticmethod
+    def create(app_id, session_id, blob_id, offset, blob_size, max_size, sync):
+        header = TLPHeader()
+        header.session_id = session_id
+        header.first = (offset == 0)
+
+        # if first message of a P2P session, add Peer Info to TLVs
+        if sync:
+            header.op_code = TLPFlag.SYN | TLPFlag.RAK
+            header.peer_info = struct.pack(">HHHHI",
+                    PeerInfo.PROTOCOL_VERSION, PeerInfo.IMPLEMENTATION_ID,
+                    PeerInfo.VERSION, 0, PeerInfo.CAPABILITIES)
+
+        max_chunk_size = max_size - header.size
+        data_remaining = blob_size - offset
+        if max_chunk_size >= data_remaining:
+            chunk_size = data_remaining
+        else:
+            chunk_size = max_chunk_size
+        header.chunk_size = chunk_size
+        header.data_remaining = data_remaining - chunk_size
+
+        # signaling messages require acknowledgement (last chunk only)
+        if session_id == 0 and header.data_remaining == 0:
+            header.op_code |= TLPFlag.RAK
+
+        # set package number for split signaling messages
+        header.package_number = 0
+        if session_id == 0 and (not header.first or header.data_remaining > 0):
+            header.package_numer = blob_id & 0xFFFF
+
+        return MessageChunk(header, application_id=app_id)
+
+    @staticmethod
+    def parse(data):
+        header = TLPHeader()
+        header_size = header.parse(data)
+        body = data[header_size:]
+        return MessageChunk(header, body)
+
+    def __str__(self):
+        return str(self.header) + str(self.body)
+
+    def __repr__(self):
+        string = "TLPv2 chunk 0x%x: " % self.id
+        string += "blob %i, " % self.blob_id
+        if self.session_id:
+            string += "session %d, " % self.session_id
+        if self.is_ack_chunk():
+            string += "ACK 0x%x, " % self.acked_id
+        if self.is_nak_chunk():
+            string += "NAK 0x%x, " % self.naked_id
+        if self.require_ack():
+            string += "RAK, "
+        if self.header.op_code & TLPFlag.SYN:
+            string += "SYN, "
+        if self.size > 0:
+            if self.session_id:
+                string += "data [%d bytes]" % self.size
+            else:
+                string += "SLP Message"
+                string += "\n\t" + debug.escape_string(self.body).\
+                        replace("\r\n", "\\r\\n\n\t")
+        return string
--- papyon/msnp2p/transport/__init__.py
+++ papyon/msnp2p/transport/__init__.py
@@ -19,5 +19,4 @@
 
 """MSNP2P transport layer"""
 
-from TLP import *
 from transport_manager import *
--- papyon/msnp2p/transport/base.py
+++ papyon/msnp2p/transport/base.py
@@ -18,16 +18,21 @@
 # along with this program; if not, write to the Free Software
 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
-from papyon.msnp2p.transport.TLP import TLPFlag, MessageChunk, ControlBlob
+from papyon.msnp2p.transport.TLP import MessageBlob
 
 import gobject
 import logging
+import random
+import threading
 import weakref
 
 __all__ = ['BaseP2PTransport']
 
 logger = logging.getLogger('papyon.msnp2p.transport')
 
+
+MAX_INT32 = 2147483647
+
 class BaseP2PTransport(gobject.GObject):
     __gsignals__ = {
             "chunk-received": (gobject.SIGNAL_RUN_FIRST,
@@ -37,6 +42,14 @@
             "chunk-sent": (gobject.SIGNAL_RUN_FIRST,
                 gobject.TYPE_NONE,
                 (object,)),
+
+            "blob-received": (gobject.SIGNAL_RUN_FIRST,
+                gobject.TYPE_NONE,
+                (object,)),
+
+            "blob-sent": (gobject.SIGNAL_RUN_FIRST,
+                gobject.TYPE_NONE,
+                (object,)),
             }
 
     def __init__(self, transport_manager, name):
@@ -44,8 +57,13 @@
         self._transport_manager = weakref.proxy(transport_manager)
         self._client = transport_manager._client
         self._name = name
+        self._source = None
+
+        self._local_chunk_id = None
+        self._remote_chunk_id = None
 
         self._transport_manager._register_transport(self)
+        self._queue_lock = threading.Lock()
         self._reset()
 
     @property
@@ -64,98 +82,158 @@
     def max_chunk_size(self):
         raise NotImplementedError
 
-    def send(self, blob, callback=None, errback=None):
-        if blob.is_control_blob():
-            self._control_blob_queue.append((blob, callback, errback))
+    @property
+    def version(self):
+        if self._client.profile.client_id.supports_p2pv2 and \
+                self.peer.client_capabilities.supports_p2pv2:
+            return 2
         else:
-            self._data_blob_queue.append((blob, callback, errback))
-        gobject.timeout_add(200, self._process_send_queues)
-        self._process_send_queues()
+            return 1
+
+    def can_send(self, peer, peer_guid, blob, bootstrap=False):
+        raise NotImplementedError
+
+    def send(self, peer, peer_guid, blob):
+        self._queue_lock.acquire()
+        self._data_blob_queue.append((peer, peer_guid, blob))
+        self._queue_lock.release()
+        self._start_processing()
+
+    def cleanup(self, session_id):
+        # remove this session's blobs from the data queue
+        self._queue_lock.acquire()
+        canceled_blobs = []
+        for blob in self._data_blob_queue:
+            if blob[2].session_id == session_id:
+                canceled_blobs.append(blob)
+        for blob in canceled_blobs:
+            self._data_blob_queue.remove(blob)
+        self._queue_lock.release()
 
     def close(self):
         self._transport_manager._unregister_transport(self)
 
-    def _send_chunk(self, chunk):
+    def _ready_to_send(self):
         raise NotImplementedError
 
-    # Helper methods
+    def _send_chunk(self, peer, peer_guid, chunk):
+        raise NotImplementedError
+
+    # Helper methods ---------------------------------------------------------
+
     def _reset(self):
-        self._control_blob_queue = []
+        self._queue_lock.acquire()
+        self._first = True
         self._data_blob_queue = []
-        self._pending_blob = {} # last_chunk : (blob, callback, errback)
-        self._pending_ack = {} # blob_id : [blob_offset1, blob_offset2 ...]
-
-    def _add_pending_ack(self, blob_id, chunk_id=0):
-        if blob_id not in self._pending_ack:
-            self._pending_ack[blob_id] = set()
-        self._pending_ack[blob_id].add(chunk_id)
+        self._pending_blob = {} # ack_id : (blob, callback, errback)
+        self._pending_ack = set()
+        self._signaling_blobs = {} # blob_id : blob
+        self._queue_lock.release()
+
+    def _add_pending_ack(self, ack_id):
+        self._pending_ack.add(ack_id)
+
+    def _del_pending_ack(self, ack_id):
+        self._pending_ack.discard(ack_id)
+
+    def _add_pending_blob(self, ack_id, blob):
+        if self.version == 1:
+            self._pending_blob[ack_id] = blob
+        else:
+            self.emit("blob-sent", blob)
 
-    def _del_pending_ack(self, blob_id, chunk_id=0):
-        if blob_id not in self._pending_ack:
+    def _del_pending_blob(self, ack_id):
+        if not ack_id in self._pending_blob:
             return
-        self._pending_ack[blob_id].discard(chunk_id)
+        blob = self._pending_blob.pop(ack_id)
+        self.emit("blob-sent", blob)
 
-        if len(self._pending_ack[blob_id]) == 0:
-            del self._pending_ack[blob_id]
+    def _on_chunk_received(self, peer, peer_guid, chunk):
+        if chunk.is_data_preparation_chunk():
+            return
 
-    def _on_chunk_received(self, chunk):
         if chunk.require_ack():
-            self._send_ack(chunk)
+            ack_chunk = chunk.create_ack_chunk()
+            self.__send_chunk(peer, peer_guid, ack_chunk)
 
-        if chunk.header.flags & TLPFlag.ACK:
-            self._del_pending_ack(chunk.header.dw1, chunk.header.dw2)
-            if chunk.header.dw1 in self._pending_blob:
-                blob, callback, errback = self._pending_blob[chunk.header.dw1]
-                del self._pending_blob[blob.id]
-                if callback:
-                    callback[0](*callback[1:])
+        if chunk.is_ack_chunk() or chunk.is_nak_chunk():
+            self._del_pending_ack(chunk.acked_id)
+            self._del_pending_blob(chunk.acked_id)
 
-        #FIXME: handle all the other flags
+        #FIXME: handle all the other flags (NAK...)
 
         if not chunk.is_control_chunk():
-            self.emit("chunk-received", chunk)
+            if chunk.is_signaling_chunk(): # signaling chunk
+                self._on_signaling_chunk_received(chunk)
+            else: # data chunk (buffered by the transport manager)
+                self.emit("chunk-received", chunk)
+
+        self._start_processing()
+
+    def _on_signaling_chunk_received(self, chunk):
+        blob_id = chunk.blob_id
+        if blob_id in self._signaling_blobs:
+            blob = self._signaling_blobs[blob_id]
+        else:
+            # create an in-memory blob
+            blob = MessageBlob(chunk.application_id, "",
+                chunk.blob_size, chunk.session_id, blob_id)
+            self._signaling_blobs[blob_id] = blob
 
-        self._process_send_queues()
+        blob.append_chunk(chunk)
+        if blob.is_complete():
+            self.emit("blob-received", blob)
+            del self._signaling_blobs[blob_id]
 
     def _on_chunk_sent(self, chunk):
         self.emit("chunk-sent", chunk)
-        if chunk in self._pending_blob:
-            blob, callback, errback = self._pending_blob.pop(chunk)
-            if callback:
-                callback[0](*callback[1:])
-        self._process_send_queues()
-
-    def _process_send_queues(self):
-        if len(self._control_blob_queue) > 0:
-            queue = self._control_blob_queue
-        elif len(self._data_blob_queue) > 0:
-            queue = self._data_blob_queue
-        else:
+        self._start_processing()
+
+    def _start_processing(self):
+        if self._source is None:
+            self._source = gobject.timeout_add(200, self._process_send_queue)
+        self._process_send_queue()
+
+    def _stop_processing(self):
+        if self._source is not None:
+            gobject.source_remove(self._source)
+            self._source = None
+
+    def _process_send_queue(self):
+        if not self._queue_lock.acquire(False):
+            return True
+        if len(self._data_blob_queue) == 0:
+            self._queue_lock.release()
+            self._stop_processing()
             return False
+        if not self._ready_to_send():
+            logger.info("Transport is not ready to send, bail out")
+            self._queue_lock.release()
+            self._stop_processing()
+            return True
+
+        sync = self._first
+        self._first = False
+        (peer, peer_guid, blob) = self._data_blob_queue[0]
+        chunk = blob.get_chunk(self.version, self.max_chunk_size, sync)
+        self.__send_chunk(peer, peer_guid, chunk)
 
-        blob, callback, errback = queue[0]
-        chunk = blob.get_chunk(self.max_chunk_size)
         if blob.is_complete():
-            queue.pop(0)
-            self._pending_blob[chunk] = (blob, callback, errback)
-
-        if chunk.require_ack() :
-            self._add_pending_ack(chunk.header.blob_id, chunk.header.dw1)
-        self._send_chunk(chunk)
+            self._data_blob_queue.pop(0)
+            self._add_pending_blob(chunk.ack_id, blob)
+        self._queue_lock.release()
         return True
 
-    def _send_ack(self, received_chunk):
-        flags = received_chunk.header.flags
+    def __send_chunk(self, peer, peer_guid, chunk):
+        # add local identifier to chunk
+        if self._local_chunk_id is None:
+            self._local_chunk_id = random.randint(1000, MAX_INT32)
+        chunk.id = self._local_chunk_id
+        self._local_chunk_id = chunk.next_id
 
-        flags = TLPFlag.ACK
-        if received_chunk.header.flags & TLPFlag.RAK:
-            flags |= TLPFlag.RAK
-
-        ack_blob = ControlBlob(received_chunk.header.session_id, flags,
-                dw1 = received_chunk.header.blob_id,
-                dw2 = received_chunk.header.dw1,
-                qw1 = received_chunk.header.blob_size)
+        if chunk.require_ack() :
+            self._add_pending_ack(chunk.ack_id)
 
-        self.send(ack_blob)
+        self._send_chunk(peer, peer_guid, chunk)
 
 gobject.type_register(BaseP2PTransport)
--- papyon/msnp2p/transport/notification.py
+++ papyon/msnp2p/transport/notification.py
+# -*- coding: utf-8 -*-
+#
+# papyon - a python client library for Msn
+#
+# Copyright (C) 2010 Collabora Ltd.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+from papyon.msnp.constants import UserNotificationTypes
+from papyon.msnp2p.transport.TLP import MessageBlob
+from papyon.msnp2p.transport.base import BaseP2PTransport
+
+import gobject
+import struct
+import logging
+
+__all__ = ['NotificationP2PTransport']
+
+logger = logging.getLogger('papyon.msnp2p.transport.notification')
+
+class NotificationP2PTransport(BaseP2PTransport):
+    def __init__(self, client, transport_manager):
+        BaseP2PTransport.__init__(self, transport_manager, "notification")
+        self._protocol = client._protocol
+        self._protocol.connect("buddy-notification-received",
+                self._on_notification_received)
+
+    def close(self):
+        BaseP2PTransport.close(self)
+
+    @property
+    def rating(self):
+        return 0
+
+    @property
+    def max_chunk_size(self):
+        return 7500
+
+    def can_send(self, peer, peer_guid, blob, bootstrap=False):
+        if not bootstrap:
+            return False # can only handle bootstrap signaling
+        if blob.total_size > self.max_chunk_size:
+            return False # can't split in chunks
+        if not peer.client_capabilities.p2p_bootstrap_via_uun:
+            return False # peer needs to support UUN bootstrap
+        return True
+
+    def send(self, peer, peer_guid, blob):
+        data = blob.read_data()
+        self._protocol.send_user_notification(data, peer, peer_guid,
+                UserNotificationTypes.P2P_DATA, self._on_notification_sent, data)
+
+    def _on_notification_received(self, protocol, peer, peer_guid, type, data):
+        if type is not UserNotificationTypes.P2P_DATA:
+            return
+        blob = MessageBlob(0, data, None, 0)
+        self.emit("blob-received", blob)
+
+    def _on_notification_sent(self, data):
+        blob = MessageBlob(0, data, None, 0)
+        self.emit("blob-sent", blob)
--- papyon/msnp2p/transport/switchboard.py
+++ papyon/msnp2p/transport/switchboard.py
@@ -21,7 +21,7 @@
 from papyon.msnp.message import MessageAcknowledgement
 from papyon.msnp2p.transport.TLP import MessageChunk
 from papyon.msnp2p.transport.base import BaseP2PTransport
-from papyon.switchboard_manager import SwitchboardClient
+from papyon.switchboard_manager import SwitchboardHandler
 
 import gobject
 import struct
@@ -29,14 +29,19 @@
 
 __all__ = ['SwitchboardP2PTransport']
 
-logger = logging.getLogger('papyon.msnp2p.transport')
+logger = logging.getLogger('papyon.msnp2p.transport.switchboard')
 
 
-class SwitchboardP2PTransport(BaseP2PTransport, SwitchboardClient):
-    def __init__(self, client, contacts, transport_manager):
-        SwitchboardClient.__init__(self, client, contacts)
-        BaseP2PTransport.__init__(self, transport_manager, "switchboard")
+class SwitchboardP2PTransport(BaseP2PTransport, SwitchboardHandler):
+
+    MAX_OUTSTANDING_SENDS = 5
 
+    def __init__(self, client, switchboard, contacts, peer, peer_guid, transport_manager):
+        self._oustanding_sends = 0
+        self._peer = peer
+        self._peer_guid = peer_guid
+        SwitchboardHandler.__init__(self, client, switchboard, contacts)
+        BaseP2PTransport.__init__(self, transport_manager, "switchboard")
 
     def close(self):
         BaseP2PTransport.close(self)
@@ -47,11 +52,32 @@
         content_type = message.content_type[0]
         return content_type == 'application/x-msnmsgrp2p'
 
+    @staticmethod
+    def handle_peer(client, peer, peer_guid, transport_manager):
+        return SwitchboardP2PTransport(client, None, (peer,), peer, peer_guid,
+            transport_manager)
+
+    @staticmethod
+    def handle_message(client, switchboard, message, transport_manager):
+        guid = None
+        peer = None
+        if 'P2P-Src' in message.headers and ';' in message.headers['P2P-Src']:
+            account, guid = message.headers['P2P-Src'].split(';', 1)
+            guid = guid[1:-1]
+            if account == client.profile.account:
+                peer = client.profile
+        if peer is None:
+            peer = switchboard.participants.values()[0]
+        return SwitchboardP2PTransport(client, switchboard, (), peer, guid,
+            transport_manager)
+
     @property
     def peer(self):
-        for peer in self.total_participants:
-            return peer
-        return None
+        return self._peer
+
+    @property
+    def peer_guid(self):
+        return self._peer_guid
 
     @property
     def rating(self):
@@ -61,21 +87,72 @@
     def max_chunk_size(self):
         return 1250 # length of the chunk including the header but not the footer
 
-    def _send_chunk(self, chunk):
-        headers = {'P2P-Dest': self.peer.account}
+    def can_send(self, peer, peer_guid, blob, bootstrap=False):
+        return (self._peer == peer and self._peer_guid == peer_guid)
+
+    def __parse_guid(self, message, header):
+        if header not in message.headers or ';' not in message.headers[header]:
+            return None
+        return message.headers[header].split(';', 1)[1][1:-1]
+
+    def _ready_to_send(self):
+        return (self._oustanding_sends < self.MAX_OUTSTANDING_SENDS)
+
+    def _send_chunk(self, peer, peer_guid, chunk):
+        logger.debug(">>> %s" % repr(chunk))
+        if chunk.version is 1 or peer_guid is None:
+            headers = {'P2P-Dest': self.peer.account}
+        elif chunk.version is 2:
+            headers = {'P2P-Src' : self._client.profile.account + ";{" +
+                                   self._client.machine_guid + "}",
+                       'P2P-Dest': peer.account + ";{" +
+                                   peer_guid + "}"}
         content_type = 'application/x-msnmsgrp2p'
         body = str(chunk) + struct.pack('>L', chunk.application_id)
+        self._oustanding_sends += 1
         self._send_message(content_type, body, headers,
-                MessageAcknowledgement.MSNC, self._on_chunk_sent, (chunk,))
+                MessageAcknowledgement.MSNC, (self._on_message_sent, chunk),
+                (self._on_message_error, chunk))
 
     def _on_message_received(self, message):
-        chunk = MessageChunk.parse(message.body[:-4])
+        version = 1
+        # if destination contains a GUID, the protocol should be TLPv2
+        dest_guid = self.__parse_guid(message, 'P2P-Dest')
+        src_guid = self.__parse_guid(message, 'P2P-Src')
+        if dest_guid and src_guid:
+            version = 2
+            if dest_guid != self._client.machine_guid or \
+               src_guid != self._peer_guid:
+                return # this chunk is not for us
+
+        chunk = MessageChunk.parse(version, message.body[:-4])
         chunk.application_id = struct.unpack('>L', message.body[-4:])[0]
-        self._on_chunk_received(chunk)
+        logger.debug("<<< %s" % repr(chunk))
+        self._on_chunk_received(self._peer, self._peer_guid, chunk)
+
+    def _on_message_sent(self, chunk):
+        self._oustanding_sends -= 1
+        self._on_chunk_sent(chunk)
+
+    def _on_message_error(self, chunk):
+        self._oustanding_sends -= 1
+
+    def _on_switchboard_closed(self):
+        pass
+
+    def _on_closed(self):
+        BaseP2PTransport.close(self)
+
+    def _on_error(self, error_type, error):
+        logger.info("Received error %i (type=%i)" % (error, error_type))
 
     def _on_contact_joined(self, contact):
         pass
 
     def _on_contact_left(self, contact):
-        self.close()
+        if contact == self._peer:
+            self.close()
 
+    def __repr__(self):
+        return '<SwitchboardP2PTransport peer="%s" guid="%s">' % \
+                (self.peer.account, self.peer_guid)
--- papyon/msnp2p/transport/transport_manager.py
+++ papyon/msnp2p/transport/transport_manager.py
@@ -19,11 +19,13 @@
 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
 from papyon.msnp2p.transport.switchboard import *
+from papyon.msnp2p.transport.notification import *
 from papyon.msnp2p.transport.TLP import MessageBlob
 
 import gobject
 import struct
 import logging
+import os
 
 __all__ = ['P2PTransportManager']
 
@@ -51,88 +53,104 @@
         self._client = client
         switchboard_manager = self._client._switchboard_manager
         switchboard_manager.register_handler(SwitchboardP2PTransport, self)
-        self._default_transport = \
-                lambda transport_mgr, peer : \
-                        SwitchboardP2PTransport(client, (peer,), transport_mgr)
+        self._default_transport = lambda peer, peer_guid : \
+            SwitchboardP2PTransport.handle_peer(client, peer, peer_guid, self)
         self._transports = set()
         self._transport_signals = {}
-        self._signaling_blobs = {} # blob_id => blob
         self._data_blobs = {} # session_id => blob
+        self._blacklist = set() # blacklist of session_id
+        uun_transport = NotificationP2PTransport(client, self)
 
     def _register_transport(self, transport):
+        logger.info("Registering transport %s" % repr(transport))
         assert transport not in self._transports, "Trying to register transport twice"
         self._transports.add(transport)
         signals = []
-        signals.append(transport.connect("chunk-received", self._on_chunk_received))
-        signals.append(transport.connect("chunk-sent", self._on_chunk_sent))
+        signals.append(transport.connect("chunk-received",
+            self._on_chunk_received))
+        signals.append(transport.connect("chunk-sent",
+            self._on_chunk_sent))
+        signals.append(transport.connect("blob-received",
+            self._on_blob_received))
+        signals.append(transport.connect("blob-sent",
+            self._on_blob_sent))
         self._transport_signals[transport] = signals
 
     def _unregister_transport(self, transport):
+        if transport not in self._transports:
+            return
+        logger.info("Unregistering transport %s" % repr(transport))
         self._transports.discard(transport)
-        signals = self._transport_signals[transport]
+        signals = self._transport_signals.pop(transport, [])
         for signal in signals:
             transport.disconnect(signal)
-        del self._transport_signals[transport]
 
-    def _get_transport(self, peer):
+    def _get_transport(self, peer, peer_guid, blob):
         for transport in self._transports:
-            if transport.peer == peer:
+            if transport.can_send(peer, peer_guid, blob):
                 return transport
-        return self._default_transport(self, peer)
+        return self._default_transport(peer, peer_guid)
 
     def _on_chunk_received(self, transport, chunk):
         self.emit("chunk-transferred", chunk)
-        session_id = chunk.header.session_id
-        blob_id = chunk.header.blob_id
+        session_id = chunk.session_id
+        blob_id = chunk.blob_id
+
+        if session_id in self._blacklist:
+            return
 
-        if session_id == 0: # signaling blob
-            if blob_id in self._signaling_blobs:
-                blob = self._signaling_blobs[blob_id]
-            else:
-                # create an in-memory blob
-                blob = MessageBlob(chunk.application_id, "",
-                    chunk.header.blob_size,
-                    session_id, chunk.header.blob_id)
-                self._signaling_blobs[blob_id] = blob
-        else: # data blob
-            if chunk.is_data_preparation_chunk():
-                return
-
-            if session_id in self._data_blobs:
-                blob = self._data_blobs[session_id]
-                if blob.transferred == 0:
-                    blob.id = chunk.header.blob_id
-            else:
-                # create an in-memory blob
-                blob = MessageBlob(chunk.application_id, "",
-                        chunk.header.blob_size,
-                        session_id, chunk.header.blob_id)
-                self._data_blobs[session_id] = blob
+        if session_id in self._data_blobs:
+            blob = self._data_blobs[session_id]
+            if blob.transferred == 0:
+                blob.id = chunk.blob_id
+        else:
+            # create an in-memory blob
+            blob = MessageBlob(chunk.application_id, "",
+                    chunk.blob_size, session_id, chunk.blob_id)
+            self._data_blobs[session_id] = blob
 
         blob.append_chunk(chunk)
         if blob.is_complete():
-            blob.data.seek(0, 0)
+            del self._data_blobs[session_id]
             self.emit("blob-received", blob)
-            if session_id == 0:
-                del self._signaling_blobs[blob_id]
-            else:
-                del self._data_blobs[session_id]
 
     def _on_chunk_sent(self, transport, chunk):
         self.emit("chunk-transferred", chunk)
 
+    def _on_blob_received(self, transport, blob):
+        self.emit("blob-received", blob)
+
     def _on_blob_sent(self, transport, blob):
         self.emit("blob-sent", blob)
 
-    def send(self, peer, blob):
-        transport = self._get_transport(peer)
-        transport.send(blob, (self._on_blob_sent, transport, blob))
+    def send_slp_message(self, peer, peer_guid, application_id, message):
+        self.send_data(peer, peer_guid, application_id, 0, str(message))
+
+    def send_data(self, peer, peer_guid, application_id, session_id, data):
+        blob = MessageBlob(application_id, data, None, session_id, None)
+        transport = self._get_transport(peer, peer_guid, blob)
+        transport.send(peer, peer_guid, blob)
 
-    def register_writable_blob(self, blob):
-        if blob.session_id in self._data_blobs:
+    def register_data_buffer(self, session_id, buffer, size):
+        if session_id in self._data_blobs:
             logger.warning("registering already registered blob "\
                     "with session_id=" + str(session_id))
             return
-        self._data_blobs[blob.session_id] = blob
+        blob = MessageBlob(0, buffer, size, session_id)
+        self._data_blobs[session_id] = blob
+
+    def cleanup(self, session_id):
+        if session_id in self._data_blobs:
+            del self._data_blobs[session_id]
+        for transport in self._transports:
+            transport.cleanup(session_id)
+
+    def add_to_blacklist(self, session_id):
+        # ignore data chunks received for this session_id:
+        # we want to ignore chunks received shortly after closing a session
+        self._blacklist.add(session_id)
+
+    def remove_from_blacklist(self, session_id):
+        self._blacklist.discard(session_id)
 
 gobject.type_register(P2PTransportManager)
--- papyon/msnp2p/webcam.py
+++ papyon/msnp2p/webcam.py
@@ -32,6 +32,7 @@
 import gobject
 import logging
 import base64
+import os
 import random
 
 from papyon.media import MediaCall, MediaCandidate, MediaCandidateEncoder, \
@@ -44,14 +45,14 @@
 
 class WebcamSession(P2PSession, MediaCall, EventsDispatcher):
 
-    def __init__(self, producer, session_manager, peer,
+    def __init__(self, producer, session_manager, peer, peer_guid,
             euf_guid,  message=None):
         if producer:
             type = MediaSessionType.WEBCAM_SEND
         else:
             type = MediaSessionType.WEBCAM_RECV
 
-        P2PSession.__init__(self, session_manager, peer, euf_guid,
+        P2PSession.__init__(self, session_manager, peer, peer_guid, euf_guid,
                 ApplicationID.WEBCAM, message)
         MediaCall.__init__(self, type)
         EventsDispatcher.__init__(self)
@@ -62,6 +63,10 @@
         self._session_id = self._generate_id(9999)
         self._xml_needed = False
 
+    @property
+    def producer(self):
+        return self._producer
+
     def invite(self):
         self._answered = True
         context = "{B8BE70DE-E2CA-4400-AE03-88FF85B9F4E8}"
@@ -80,12 +85,12 @@
         self._answered = True
         self._respond(603)
 
-    def end(self):
+    def end(self, reason=None):
         if not self._answered:
             self.reject()
         else:
             context = '\x74\x03\x00\x81'
-            self._close(context)
+            self._close(context, reason)
         self.dispose()
 
     def dispose(self):
@@ -111,9 +116,10 @@
 
     def _on_session_rejected(self, message):
         self._dispatch("on_call_rejected", message)
+        self.dispose()
 
     def _on_data_blob_received(self, blob):
-        blob.data.seek(0, 0)
+        blob.data.seek(0, os.SEEK_SET)
         data = blob.data.read()
         data = unicode(data[10:], "utf-16-le").rstrip("\x00")
 
@@ -136,7 +142,7 @@
         message_bytes = data.encode("utf-16-le") + "\x00\x00"
         id = (self._generate_id() << 8) | 0x80
         header = struct.pack("<LHL", id, 8, len(message_bytes))
-        self._send_p2p_data(header + message_bytes)
+        self._send_data(header + message_bytes)
 
     def send_binary_syn(self):
         self.send_data('syn')
--- papyon/p2p.py
+++ papyon/p2p.py
@@ -30,8 +30,10 @@
 from msnp2p.webcam import WebcamSession
 from msnp2p import EufGuid, ApplicationID
 from msnp2p.exceptions import ParseError
+from msnp2p.constants import SLPStatus
 from profile import NetworkID, Contact, Profile
 
+from papyon.util.encoding import b64_decode
 import papyon.util.element_tree as ElementTree
 import papyon.util.string_io as StringIO
 
@@ -41,6 +43,7 @@
 import base64
 import hashlib
 import logging
+import os
 
 __all__ = ['MSNObjectType', 'MSNObject', 'MSNObjectStore',
            'FileTransferManager', 'WebcamHandler']
@@ -67,30 +70,6 @@
     LOCATION = 14
     "Location"
 
-def _decode_shad(shad, warning=True):
-    try:
-        shad = base64.b64decode(shad)
-    except TypeError:
-        # See fd.o#27672 for details on this workaround.
-        if ' ' in shad:
-            parts = shad.split(' ')
-
-            # Try the first part.
-            shad = _decode_shad(parts[0], False)
-
-            # Try the second part.
-            if shad is None:
-                shad = _decode_shad(parts[1], False)
-
-        else:
-            # Only display this warning if we're not in a nested call otherwise the
-            # warning will be confusing.
-            if warning:
-                logger.warning("Invalid SHA1D in MSNObject: %s" % shad)
-            shad = None
-
-    return shad
-
 class MSNObject(object):
     "Represents an MSNObject."
     def __init__(self, creator, size, typ, location, friendly,
@@ -130,7 +109,7 @@
 
         if shad is None:
             if data is None:
-                raise NotImplementedError
+                raise ValueError
             shad = self.__compute_data_hash(data)
         self._data_sha = shad
         self.__data = data
@@ -157,9 +136,9 @@
             return
 
         old_pos = data.tell()
-        data.seek(0, 2)
+        data.seek(0, os.SEEK_END)
         self._size = data.tell()
-        data.seek(old_pos, 0)
+        data.seek(old_pos, os.SEEK_SET)
 
         self.__data = data
         self._checksum_sha = self.__compute_checksum()
@@ -186,33 +165,43 @@
             location = "0"
 
         if "Friendly" in element:
-            friendly = base64.b64decode(xml.unescape(element["Friendly"]))
+            friendly = b64_decode(xml.unescape(element["Friendly"]))
         else:
-            friendly = base64.b64decode('AAA=')
+            friendly = '\x00\x00'
 
         shad = element.get("SHA1D", None)
         if shad is not None:
-            shad = _decode_shad(shad)
+            try:
+                shad = b64_decode(shad)
+            except TypeError:
+                logger.warning("Invalid SHA1D in MSNObject: %s" % shad)
+                shad = None
+
         shac = element.get("SHA1C", None)
         if shac is not None:
             try:
-                shac = base64.b64decode(shac)
+                shac = b64_decode(shac)
             except TypeError:
                 logger.warning("Invalid SHA1C in MSNObject: %s" % shac)
                 shac = None
 
-        result = MSNObject(creator, size, type, location, friendly, shad, shac)
-        result._repr = xml_data
+        try:
+            result = MSNObject(creator, size, type, location, friendly, shad, shac)
+            result._repr = xml_data
+        except ValueError:
+            logger.warning("Invalid MSNObject")
+            return None
+
         return result
 
     def __compute_data_hash(self, data):
         digest = hashlib.sha1()
-        data.seek(0, 0)
+        data.seek(0, os.SEEK_SET)
         read_data = data.read(1024)
         while len(read_data) > 0:
             digest.update(read_data)
             read_data = data.read(1024)
-        data.seek(0, 0)
+        data.seek(0, os.SEEK_SET)
         return digest.digest()
 
     def __compute_checksum(self):
@@ -238,12 +227,38 @@
         return dump
 
 
-class MSNObjectStore(object):
+class P2PSessionHandler(gobject.GObject):
 
     def __init__(self, client):
+        gobject.GObject.__init__(self)
         self._client = client
-        self._outgoing_sessions = {} # session => (handle_id, callback, errback)
-        self._incoming_sessions = {}
+        self._sessions = []
+        self._handles = {} # session : handles
+
+    def _add_session(self, session):
+        self._connect_session(session)
+        self._sessions.append(session)
+
+    def _connect_session(self, session):
+        handles = []
+        handles.append(session.connect("disposed", self._on_session_disposed))
+        self._handles[session] = handles
+
+    def _disconnect_session(self, session):
+        handles = self._handles.pop(session)
+        for handle_id in handles:
+            session.disconnect(handle_id)
+
+    def _on_session_disposed(self, session):
+        self._disconnect_session(session)
+        self._sessions.remove(session)
+
+
+class MSNObjectStore(P2PSessionHandler):
+
+    def __init__(self, client):
+        P2PSessionHandler.__init__(self, client)
+        self._callbacks = {} # session => (callback, errback, msn_object)
         self._published_objects = set()
 
     def _can_handle_message (self, message):
@@ -253,46 +268,71 @@
         else:
             return False
 
-    def _handle_message(self, peer, message):
+    def _handle_message(self, peer, guid, message):
         session = MSNObjectSession(self._client._p2p_session_manager,
-                peer, message.body.application_id, message)
-
-        handle_id = session.connect("completed",
-                        self._incoming_session_transfer_completed)
-        self._incoming_sessions[session] = handle_id
+                peer, guid, message.body.application_id, message)
         try:
-            msn_object = MSNObject.parse(self._client, session._context)
+            msn_object = MSNObject.parse(self._client, session.context)
         except ParseError:
+            logger.error("Error while parsing MSN object from request")
             session.reject()
             return
+
+        self._add_session(session)
+        self._callbacks[session] = (None, None, msn_object)
+
         for obj in self._published_objects:
             if obj._data_sha == msn_object._data_sha:
                 session.accept(obj._data)
                 return session
-        session.reject()
+        logger.warning("Unknown MSN object, another end point might have it")
+
+    def _connect_session(self, session):
+        P2PSessionHandler._connect_session(self, session)
+        self._handles[session].append(session.connect("completed",
+            self._on_session_completed))
+        self._handles[session].append(session.connect("rejected",
+            self._on_session_completed))
+
+    def _on_session_completed(self, session, data):
+        if session in self._callbacks:
+            callback, errback, msn_object = self._callbacks[session]
+            if callback:
+                msn_object._data = data
+                callback[0](msn_object, *callback[1:])
+
+    def _on_session_rejected(self, session):
+        if session in self._callbacks:
+            callback, errback, msn_object = self._callbacks[session]
+            if errback:
+                errback[0](msn_object, *errback[1:])
+
+    ### Public API
 
     def request(self, msn_object, callback, errback=None, peer=None):
         if msn_object._data is not None:
             callback[0](msn_object, *callback[1:])
+            return
 
         if peer is None:
             peer = self._client.address_book.search_contact(msn_object._creator,
                     NetworkID.MSN)
 
         if msn_object._type == MSNObjectType.CUSTOM_EMOTICON:
-            application_id = ApplicationID.CUSTOM_EMOTICON_TRANSFER
+            app_id = ApplicationID.CUSTOM_EMOTICON_TRANSFER
         elif msn_object._type == MSNObjectType.DISPLAY_PICTURE:
-            application_id = ApplicationID.DISPLAY_PICTURE_TRANSFER
+            app_id = ApplicationID.DISPLAY_PICTURE_TRANSFER
         else:
             raise NotImplementedError
 
-        session = MSNObjectSession(self._client._p2p_session_manager,
-                peer, application_id)
-        handle_id = session.connect("completed",
-                self._outgoing_session_transfer_completed)
-        self._outgoing_sessions[session] = \
-                (handle_id, callback, errback, msn_object)
-        session.invite(repr(msn_object))
+        context = repr(msn_object)
+        session = MSNObjectMetaSession(self._client, peer, app_id, context)
+        self._add_session(session)
+        self._callbacks[session] = (callback, errback, msn_object)
+        logger.info("Requesting a MSNObject from %s (%i end points)" %
+                (peer.account, session.count))
+        session.invite()
+        return session
 
     def publish(self, msn_object):
         if msn_object._data is None:
@@ -300,21 +340,8 @@
         else:
             self._published_objects.add(msn_object)
 
-    def _outgoing_session_transfer_completed(self, session, data):
-        handle_id, callback, errback, msn_object = self._outgoing_sessions[session]
-        session.disconnect(handle_id)
-        msn_object._data = data
-
-        callback[0](msn_object, *callback[1:])
-        del self._outgoing_sessions[session]
-
-    def _incoming_session_transfer_completed(self, session, data):
-        handle_id = self._incoming_sessions[session]
-        session.disconnect(handle_id)
-        del self._incoming_sessions[session]
 
-
-class FileTransferManager(gobject.GObject):
+class FileTransferManager(P2PSessionHandler):
 
     __gsignals__ = {
             "transfer-requested" : (gobject.SIGNAL_RUN_FIRST,
@@ -323,42 +350,29 @@
     }
 
     def __init__(self, client):
-        gobject.GObject.__init__(self)
-        self._client = client
-        self._sessions = {}
+        P2PSessionHandler.__init__(self, client)
 
     def _can_handle_message(self, message):
         euf_guid = message.body.euf_guid
         return (euf_guid == EufGuid.FILE_TRANSFER)
 
-    def _handle_message(self, peer, message):
+    def _handle_message(self, peer, guid, message):
         session = FileTransferSession(self._client._p2p_session_manager,
-                peer, message.body.application_id, message)
-        self._connect_session(session)
+                peer, guid, message)
+        self._add_session(session)
         self.emit("transfer-requested", session)
         return session
 
+    ### Public API
+
     def send(self, peer, filename, size):
-        session = FileTransferSession(self._client._p2p_session_manager,
-                peer, ApplicationID.FILE_TRANSFER)
+        session = FileTransferMetaSession(self._client, peer)
+        self._add_session(session)
         session.invite(filename, size)
-        self._connect_session(session)
         return session
 
-    def _on_transfer_completed(self, session, data):
-        self._disconnect_session(session)
-        del self._sessions[session]
-
-    def _connect_session(self, session):
-        handle_id = session.connect("completed", self._on_transfer_completed)
-        self._sessions[session] = handle_id
-
-    def _disconnect_session(self, session):
-        handle_id = self._sessions[session]
-        session.disconnect(handle_id)
-
 
-class WebcamHandler(gobject.GObject):
+class WebcamHandler(P2PSessionHandler):
 
     __gsignals__ = {
             "session-created" : (gobject.SIGNAL_RUN_FIRST,
@@ -367,9 +381,7 @@
     }
 
     def __init__(self, client):
-        gobject.GObject.__init__(self)
-        self._client = client
-        self._sessions = []
+        P2PSessionHandler.__init__(self, client)
 
     def _can_handle_message (self, message):
         euf_guid = message.body.euf_guid
@@ -379,7 +391,7 @@
         else:
             return False
 
-    def _handle_message (self, peer, message):
+    def _handle_message (self, peer, guid, message):
         euf_guid = message.body.euf_guid
         if (euf_guid == EufGuid.MEDIA_SESSION):
             producer = False
@@ -387,19 +399,225 @@
             producer = True
 
         session = WebcamSession(producer, self._client._p2p_session_manager, \
-                                    peer, message.body.euf_guid, message)
-        self._sessions.append(session)
+                                    peer, guid, message.body.euf_guid, message)
+        self._add_session(session)
         self.emit("session-created", session, producer)
         return session
 
+    ### Public API
+
     def invite(self, peer, producer=True):
-        print "Creating New Send Session"
+        """Invite a contact for a uni-directionnal webcam session
+           @param producer: if true, we want to send webcam"""
+
+        session = WebcamMetaSession(self._client, peer, producer)
+        self._add_session(session)
+        session.invite()
+        return session
+
+
+class P2PMetaSession(gobject.GObject):
+    """ A P2PMetaSession is used to wrap multiple outgoing p2p sessions
+        together. This way, we can send a invite to each end point of a peer
+        and still have only one session object for methods and signals. """
+
+    __gsignals__ = {
+            "accepted" : (gobject.SIGNAL_RUN_FIRST,
+                gobject.TYPE_NONE,
+                ()),
+            "rejected" : (gobject.SIGNAL_RUN_FIRST,
+                gobject.TYPE_NONE,
+                ()),
+            "completed" : (gobject.SIGNAL_RUN_FIRST,
+                gobject.TYPE_NONE,
+                (object,)),
+            "progressed" : (gobject.SIGNAL_RUN_FIRST,
+                gobject.TYPE_NONE,
+                (object,)),
+            "disposed" : (gobject.SIGNAL_RUN_FIRST,
+                gobject.TYPE_NONE,
+                ())
+    }
+
+    def __init__(self, client, peer, *args):
+        gobject.GObject.__init__(self)
+        self._sessions = []
+        self._handles = {}
+
+        if len(peer.end_points) == 0:
+            session = self._create_session(client, peer, None, *args)
+            self._add_session(session)
+        for end_point in peer.end_points.values():
+            if peer == client.profile and end_point.id == client.machine_guid:
+                continue
+            session = self._create_session(client, peer, end_point.id, *args)
+            self._add_session(session)
+
+    @property
+    def session(self):
+        if len(self._sessions) > 0:
+            return self._sessions[0]
+        return None
+
+    @property
+    def count(self):
+        return len(self._sessions)
+
+    def __getattr__(self, name):
+        if self.session:
+            return getattr(self.session, name)
+        raise AttributeError
+
+    def _connect_session(self, session):
+        handles = []
+        handles.append(session.connect("accepted",
+            self._on_session_accepted))
+        handles.append(session.connect("rejected",
+            self._on_session_rejected))
+        handles.append(session.connect("completed",
+            self._on_session_completed))
+        handles.append(session.connect("progressed",
+            self._on_session_progressed))
+        handles.append(session.connect("disposed",
+            self._on_session_disposed))
+        self._handles[session] = handles
+
+    def _disconnect_session(self, session):
+        handles = self._handles.pop(session, [])
+        for handle in handles:
+            session.disconnect(handle)
+
+    def _add_session(self, session):
+        if session is None:
+            return
+        self._connect_session(session)
+        self._sessions.append(session)
+
+    def _remove_all(self):
+        for session in self._sessions:
+            self._disconnect_session(session)
+        self._sessions = []
+
+    def _cancel_all(self):
+        sessions = self._sessions[:]
+        self._remove_all()
+        for session in sessions:
+            self._cancel_session(session)
+
+    def _keep_session(self, session_to_keep):
+        if session_to_keep not in self._sessions:
+            return False
+        logger.info("Keeping only session %s in meta session" %
+                session_to_keep.id)
+        self._sessions.remove(session_to_keep)
+        handles = self._handles.pop(session_to_keep)
+        self._cancel_all()
+        self._sessions = [session_to_keep]
+        self._handles[session_to_keep] = handles
+
+    def _on_session_accepted(self, session):
+        self._keep_session(session)
+        self.emit("accepted")
+
+    def _on_session_rejected(self, session):
+        self._keep_session(session)
+        self.emit("rejected")
+
+    def _on_session_completed(self, session, data):
+        self.emit("completed", data)
+
+    def _on_session_progressed(self, session, data):
+        self.emit("progressed", data)
+
+    def _on_session_disposed(self, disposed_session):
+        self._remove_all()
+        self.emit("disposed")
+
+
+class FileTransferMetaSession(P2PMetaSession):
+
+    __gsignals__ = {
+            "canceled" : (gobject.SIGNAL_RUN_FIRST,
+                gobject.TYPE_NONE,
+                ())
+    }
+
+    def __init__(self, client, peer):
+        P2PMetaSession.__init__(self, client, peer)
+
+    def _create_session(self, client, peer, guid):
+        session = FileTransferSession(client._p2p_session_manager, peer, guid)
+        return session
+
+    def _connect_session(self, session):
+        P2PMetaSession._connect_session(self, session)
+        self._handles[session].append(session.connect("canceled",
+            self._on_session_canceled))
+
+    def _cancel_session(self, session):
+        session.cancel()
+
+    def _on_session_canceled(self, canceled_session):
+        self.emit("canceled")
+
+    def invite(self, filename, size):
+        for session in self._sessions:
+            session.invite(filename, size)
+
+    def cancel(self):
+        for session in self._sessions:
+            session.cancel()
+
+
+class MSNObjectMetaSession(P2PMetaSession):
+
+    def __init__(self, client, peer, application_id, context):
+        P2PMetaSession.__init__(self, client, peer, application_id, context)
+
+    def _create_session(self, client, peer, guid, application_id, context):
+        session = MSNObjectSession(client._p2p_session_manager, peer, guid,
+                application_id, context=context)
+        return session
+
+    def _cancel_session(self, session):
+        session.cancel()
+
+    def invite(self):
+        for session in self._sessions:
+            session.invite()
+
+    def cancel(self):
+        for session in self._sessions:
+            session.cancel()
+
+
+class WebcamMetaSession(P2PMetaSession):
+
+    def __init__(self, client, peer, producer):
+        P2PMetaSession.__init__(self, client, peer, producer)
+
+    def _create_session(self, client, peer, guid, producer):
+        if guid and peer.end_points[guid]:
+            has_webcam = peer.end_points[guid].capabilities.has_webcam
+            if not producer and not has_webcam:
+                return None
+
         if producer:
             euf_guid = EufGuid.MEDIA_SESSION
         else:
             euf_guid = EufGuid.MEDIA_RECEIVE_ONLY
-        session = WebcamSession(producer, self._client._p2p_session_manager, \
-                                    peer, euf_guid)
-        self._sessions.append(session)
-        session.invite()
+
+        session = WebcamSession(client._p2p_session_manager, peer, guid,
+                producer, euf_guid)
         return session
+
+    def _cancel_session(self, session):
+        session.end()
+
+    def invite(self):
+        for session in self._sessions:
+            session.invite()
+
+    def end(self, reason=None):
+        for session in self._sessions:
+            session.end(reason)
--- papyon/profile.py
+++ papyon/profile.py
@@ -29,10 +29,13 @@
 from papyon.util.decorator import rw_property
 
 import gobject
+import logging
 
-__all__ = ['Profile', 'Contact', 'Group',
+__all__ = ['Profile', 'Contact', 'Group', 'EndPoint',
         'Presence', 'Membership', 'ContactType', 'Privacy', 'NetworkID', 'ClientCapabilities']
 
+logger = logging.getLogger('papyon.profile')
+
 
 class ClientCapabilities(gobject.GObject):
     """Capabilities of the client. This allow adverstising what the User Agent
@@ -168,7 +171,7 @@
 
     _EXTRA = {
             'supports_rtc_video': 0x00000010,
-            'unknown': 0x00000020
+            'supports_p2pv2': 0x00000030
             }
 
     def __init__(self, msnc=0, client_id="0:0"):
@@ -336,91 +339,250 @@
     the contact dropped it"""
 
 
-class Profile(gobject.GObject):
-    """Profile of the User connecting to the service
+class ContactFlag(object):
+    """Internal contact flag"""
 
-        @undocumented: __gsignals__, __gproperties__, do_get_property"""
+    EXTENDED_PRESENCE_KNOWN = 1
+    """Set once we receive the extended presence (UBX) for a buddy"""
+
+
+class BaseContact(gobject.GObject):
+
+    __gsignals__ =  {
+            "end-point-added": (gobject.SIGNAL_RUN_FIRST,
+                gobject.TYPE_NONE,
+                (object,)),
+
+            "end-point-removed": (gobject.SIGNAL_RUN_FIRST,
+                gobject.TYPE_NONE,
+                (object,)),
+            }
 
     __gproperties__ = {
+            "client-capabilities": (gobject.TYPE_STRING,
+                "Client capabilities",
+                "The client capabilities of the contact 's client",
+                "",
+                gobject.PARAM_READABLE),
+
+            "current-media": (gobject.TYPE_PYOBJECT,
+                "Current media",
+                "The current media that the user wants to display",
+                gobject.PARAM_READABLE),
+
             "display-name": (gobject.TYPE_STRING,
                 "Friendly name",
                 "A nickname that the user chooses to display to others",
                 "",
                 gobject.PARAM_READABLE),
 
+            "end-points": (gobject.TYPE_PYOBJECT,
+                "End points",
+                "List of locations where the user is connected",
+                gobject.PARAM_READABLE),
+
+            "flags": (gobject.TYPE_UINT,
+                "Flags",
+                "Contact flags.",
+                0, 1, 0, gobject.PARAM_READABLE),
+
+            "msn-object": (gobject.TYPE_STRING,
+                "MSN Object",
+                "MSN Object attached to the user, this generally represent "
+                "its display picture",
+                "",
+                gobject.PARAM_READABLE),
+
             "personal-message": (gobject.TYPE_STRING,
                 "Personal message",
                 "The personal message that the user wants to display",
                 "",
                 gobject.PARAM_READABLE),
 
-            "current-media": (gobject.TYPE_PYOBJECT,
-                "Current media",
-                "The current media that the user wants to display",
+            "presence": (gobject.TYPE_STRING,
+                "Presence",
+                "The presence to show to others",
+                Presence.OFFLINE,
                 gobject.PARAM_READABLE),
 
             "signature-sound": (gobject.TYPE_PYOBJECT,
                 "Signature sound",
                 "The sound played by others' client when the user connects",
                 gobject.PARAM_READABLE),
+            }
+
+    def __init__(self):
+        gobject.GObject.__init__(self)
+
+        self._client_capabilities = ClientCapabilities()
+        self._current_media = None
+        self._display_name = ""
+        self._end_points = {}
+        self._flags = 0
+        self._personal_message = ""
+        self._presence = Presence.OFFLINE
+        self._msn_object = None
+        self._signature_sound = None
+
+    @property
+    def account(self):
+        """Contact account
+            @rtype: utf-8 encoded string"""
+        return self._account
+
+    @property
+    def client_id(self):
+        """The user capabilities
+            @rtype: ClientCapabilities"""
+        return self._client_capabilities
+
+    @property
+    def client_capabilities(self):
+        """The user capabilities
+            @rtype: ClientCapabilities"""
+        return self._client_capabilities
+
+    @property
+    def current_media(self):
+        """Contact current media
+            @rtype: (artist: string, track: string)"""
+        return self._current_media
+
+    @property
+    def display_name(self):
+        """Contact display name
+            @rtype: utf-8 encoded string"""
+        return self._display_name
+
+    @property
+    def end_points(self):
+        """List of contact's locations
+           @rtype: list of string"""
+        return self._end_points
+
+    @property
+    def flags(self):
+        """Internal contact flags
+            @rtype: bitmask of L{Membership<papyon.profile.ContactFlag}s"""
+        return self._flags
+
+    @property
+    def id(self):
+        """Contact identifier in a GUID form
+            @rtype: GUID string"""
+        return self._id
+
+    @property
+    def msn_object(self):
+        """Contact MSN Object
+            @type: L{MSNObject<papyon.p2p.MSNObject>}"""
+        return self._msn_object
+
+    @property
+    def network_id(self):
+        """Contact network ID
+            @rtype: L{NetworkID<papyon.profile.NetworkID>}"""
+        return self._network_id
+
+    @property
+    def personal_message(self):
+        """Contact personal message
+            @rtype: utf-8 encoded string"""
+        return self._personal_message
+
+    @property
+    def presence(self):
+        """Contact presence
+            @rtype: L{Presence<papyon.profile.Presence>}"""
+        return self._presence
+
+    @property
+    def signature_sound():
+        """Contact signature sound
+            @type: string"""
+        return self._signature_sound
+
+    ### flags management
+    def has_flag(self, flags):
+        return (self.flags & flags) == flags
+
+    def _set_flags(self, flags):
+        logger.info("Set contact %s flags to %i" % (self._account, flags))
+        self._flags = flags
+        self.notify("flags")
+
+    def _add_flag(self, flag):
+        self._set_flags(self._flags | flag)
+
+    def _remove_flag(self, flag):
+        self._set_flags(self._flags & ~flag)
+
+    def _server_property_changed(self, name, value):
+        if name == "client-capabilities":
+            value = ClientCapabilities(client_id=value)
+        attr_name = "_" + name.lower().replace("-", "_")
+        old_value = getattr(self, attr_name)
+        if value != old_value:
+            setattr(self, attr_name, value)
+            self.notify(name)
+        if name == "end-points":
+            self._diff_end_points(old_value, value)
+
+    def _diff_end_points(self, old_eps, new_eps):
+        added_eps = set(new_eps.keys()) - set(old_eps.keys())
+        removed_eps = set(old_eps.keys()) - set(new_eps.keys())
+        for ep in added_eps:
+            self.emit("end-point-added", new_eps[ep])
+        for ep in removed_eps:
+            self.emit("end-point-removed", old_eps[ep])
+
+    def do_get_property(self, pspec):
+        name = pspec.name.lower().replace("-", "_")
+        return getattr(self, name)
+gobject.type_register(BaseContact)
 
+
+class Profile(BaseContact):
+    """Profile of the User connecting to the service"""
+
+    __gproperties__ = {
             "profile": (gobject.TYPE_PYOBJECT,
                 "Profile",
                 "the text/x-msmsgsprofile sent by the server",
                 gobject.PARAM_READABLE),
 
-            "presence": (gobject.TYPE_STRING,
-                "Presence",
-                "The presence to show to others",
-                Presence.OFFLINE,
-                gobject.PARAM_READABLE),
-
             "privacy": (gobject.TYPE_STRING,
                 "Privacy",
                 "The privacy policy to use",
                 Privacy.BLOCK,
                 gobject.PARAM_READABLE),
-
-            "msn-object": (gobject.TYPE_STRING,
-                "MSN Object",
-                "MSN Object attached to the user, this generally represent "
-                "its display picture",
-                "",
-                gobject.PARAM_READABLE),
             }
 
     def __init__(self, account, ns_client):
-        gobject.GObject.__init__(self)
+        BaseContact.__init__(self)
         self._ns_client = ns_client
         self._account = account[0]
         self._password = account[1]
 
+        self._id = "00000000-0000-0000-0000-000000000000"
         self._profile = ""
+        self._network_id = NetworkID.MSN
         self._display_name = self._account.split("@", 1)[0]
-        self._presence = Presence.OFFLINE
         self._privacy = Privacy.BLOCK
-        self._personal_message = ""
-        self._current_media = None
-        self._signature_sound = None
         self._end_point_name = ""
 
-        self._client_id = ClientCapabilities(10)
-        self._client_id.supports_sip_invite = True
-        #self.client_id.supports_tunneled_sip = True
-        self._client_id.connect("capability-changed", self._client_capability_changed)
-
-        self._msn_object = None
+        self._client_capabilities = ClientCapabilities(10)
+        self._client_capabilities.supports_sip_invite = True
+        self._client_capabilities.supports_tunneled_sip = True
+        self._client_capabilities.supports_p2pv2 = True
+        self._client_capabilities.p2p_bootstrap_via_uun = True
+        self._client_capabilities.connect("capability-changed",
+                self._client_capability_changed)
 
-        self.__pending_set_presence = [self._presence, self._client_id, self._msn_object]
+        self.__pending_set_presence = [self._presence, self._client_capabilities, self._msn_object]
         self.__pending_set_personal_message = [self._personal_message, self._current_media]
 
     @property
-    def account(self):
-        """The user account
-            @rtype: utf-8 encoded string"""
-        return self._account
-
-    @property
     def password(self):
         """The user password
             @rtype: utf-8 encoded string"""
@@ -432,18 +594,6 @@
             @rtype: dict of fields"""
         return self._profile
 
-    @property
-    def id(self):
-        """The user identifier in a GUID form
-            @rtype: GUID string"""
-        return "00000000-0000-0000-0000-000000000000"
-
-    @property
-    def client_id(self):
-        """The user capabilities
-            @rtype: ClientCapabilities"""
-        return self._client_id
-
     @rw_property
     def display_name():
         """The display name shown to you contacts
@@ -474,7 +624,7 @@
         """The default privacy, can be either Privacy.ALLOW or Privacy.BLOCK
             @type: L{Privacy<papyon.profile.Privacy>}"""
         def fset(self, privacy):
-            pass #FIXME: set the privacy setting
+            self._ns_client.set_privacy(privacy)
         def fget(self):
             return self._privacy
         return locals()
@@ -573,27 +723,18 @@
         self._ns_client.send_url_request(('PROFILE', '0x0409'), callback)
 
     def _client_capability_changed(self, client, name, value):
-        self.__pending_set_presence[1] = self._client_id
+        self.__pending_set_presence[1] = self._client_capabilities
         self._ns_client.set_presence(*self.__pending_set_presence)
 
     def _server_property_changed(self, name, value):
-        attr_name = "_" + name.lower().replace("-", "_")
-        if attr_name == "_msn_object" and value is not None:
-            value = self.__pending_set_presence[2]
-        old_value = getattr(self, attr_name)
-        if value != old_value:
-            setattr(self, attr_name, value)
-            self.notify(name)
-
-    def do_get_property(self, pspec):
-        name = pspec.name.lower().replace("-", "_")
-        return getattr(self, name)
+        if name == "msn-object" and value is not None:
+            self.__pending_set_presence[2] = value
+        BaseContact._server_property_changed(self, name, value)
 gobject.type_register(Profile)
 
 
-class Contact(gobject.GObject):
-    """Contact related information
-        @undocumented: __gsignals__, __gproperties__, do_get_property"""
+class Contact(BaseContact):
+    """Contact related information"""
 
     __gsignals__ =  {
             "infos-changed": (gobject.SIGNAL_RUN_FIRST,
@@ -605,35 +746,7 @@
             "memberships": (gobject.TYPE_UINT,
                 "Memberships",
                 "Membership relation with the contact.",
-                0, 15, 0, gobject.PARAM_READABLE),
-
-            "display-name": (gobject.TYPE_STRING,
-                "Friendly name",
-                "A nickname that the user chooses to display to others",
-                "",
-                gobject.PARAM_READWRITE),
-
-            "personal-message": (gobject.TYPE_STRING,
-                "Personal message",
-                "The personal message that the user wants to display",
-                "",
-                gobject.PARAM_READABLE),
-
-            "current-media": (gobject.TYPE_PYOBJECT,
-                "Current media",
-                "The current media that the user wants to display",
-                gobject.PARAM_READABLE),
-
-            "signature-sound": (gobject.TYPE_PYOBJECT,
-                "Signature sound",
-                "The sound played by others' client when the user connects",
-                gobject.PARAM_READABLE),
-
-            "presence": (gobject.TYPE_STRING,
-                "Presence",
-                "The presence to show to others",
-                Presence.OFFLINE,
-                gobject.PARAM_READABLE),
+                0, 31, 0, gobject.PARAM_READABLE),
 
              "groups": (gobject.TYPE_PYOBJECT,
                  "Groups",
@@ -649,43 +762,23 @@
                 "Contact type",
                 "The contact automatic update status flag",
                  gobject.PARAM_READABLE),
-
-            "client-capabilities": (gobject.TYPE_STRING,
-                "Client capabilities",
-                "The client capabilities of the contact 's client",
-                "",
-                gobject.PARAM_READABLE),
-
-            "msn-object": (gobject.TYPE_STRING,
-                "MSN Object",
-                "MSN Object attached to the contact, this generally represent "
-                "its display picture",
-                "",
-                gobject.PARAM_READABLE),
             }
 
     def __init__(self, id, network_id, account, display_name, cid=None,
             memberships=Membership.NONE, contact_type=ContactType.REGULAR):
         """Initializer"""
-        gobject.GObject.__init__(self)
+        BaseContact.__init__(self)
         self._id = id or "00000000-0000-0000-0000-000000000000"
         self._cid = cid or "00000000-0000-0000-0000-000000000000"
         self._network_id = network_id
         self._account = account
-
         self._display_name = display_name
-        self._presence = Presence.OFFLINE
-        self._personal_message = ""
-        self._current_media = None
-        self._signature_sound = None
-        self._groups = set()
 
+        self._attributes = {'icon_url' : None}
+        self._groups = set()
+        self._infos = {}
         self._memberships = memberships
         self._contact_type = contact_type
-        self._client_capabilities = ClientCapabilities()
-        self._msn_object = None
-        self._infos = {}
-        self._attributes = {'icon_url' : None}
 
     def __repr__(self):
         def memberships_str():
@@ -706,12 +799,6 @@
         return template % (self._id, self._network_id, self._account, memberships_str())
 
     @property
-    def id(self):
-        """Contact identifier in a GUID form
-            @rtype: GUID string"""
-        return self._id
-
-    @property
     def attributes(self):
         """Contact attributes
             @rtype: {key: string => value: string}"""
@@ -724,48 +811,6 @@
         return self._cid
 
     @property
-    def network_id(self):
-        """Contact network ID
-            @rtype: L{NetworkID<papyon.profile.NetworkID>}"""
-        return self._network_id
-
-    @property
-    def account(self):
-        """Contact account
-            @rtype: utf-8 encoded string"""
-        return self._account
-
-    @property
-    def presence(self):
-        """Contact presence
-            @rtype: L{Presence<papyon.profile.Presence>}"""
-        return self._presence
-
-    @property
-    def display_name(self):
-        """Contact display name
-            @rtype: utf-8 encoded string"""
-        return self._display_name
-
-    @property
-    def personal_message(self):
-        """Contact personal message
-            @rtype: utf-8 encoded string"""
-        return self._personal_message
-
-    @property
-    def current_media(self):
-        """Contact current media
-            @rtype: (artist: string, track: string)"""
-        return self._current_media
-
-    @property
-    def signature_sound():
-        """Contact signature sound
-            @type: string"""
-        return self._signature_sound
-
-    @property
     def groups(self):
         """Contact list of groups
             @rtype: set(L{Group<papyon.profile.Group>}...)"""
@@ -790,18 +835,6 @@
         return self._contact_type
 
     @property
-    def client_capabilities(self):
-        """Contact client capabilities
-            @rtype: L{ClientCapabilities}"""
-        return self._client_capabilities
-
-    @property
-    def msn_object(self):
-        """Contact MSN Object
-            @type: L{MSNObject<papyon.p2p.MSNObject>}"""
-        return self._msn_object
-
-    @property
     def domain(self):
         """Contact domain, which is basically the part after @ in the account
             @rtype: utf-8 encoded string"""
@@ -838,22 +871,9 @@
         self.notify("memberships")
 
     def _remove_membership(self, membership):
-        """removes the given membership from the contact
-
-            @param membership: the membership to remove
-            @type membership: int L{Membership}"""
         self._memberships ^= membership
         self.notify("memberships")
 
-    def _server_property_changed(self, name, value): #FIXME, should not be used for memberships
-        if name == "client-capabilities":
-            value = ClientCapabilities(client_id=value)
-        attr_name = "_" + name.lower().replace("-", "_")
-        old_value = getattr(self, attr_name)
-        if value != old_value:
-            setattr(self, attr_name, value)
-            self.notify(name)
-
     def _server_attribute_changed(self, name, value):
         self._attributes[name] = value
 
@@ -866,6 +886,7 @@
         self._id = "00000000-0000-0000-0000-000000000000"
         self._cid = "00000000-0000-0000-0000-000000000000"
         self._groups = set()
+        self._flags = 0
 
         self._server_property_changed("presence", Presence.OFFLINE)
         self._server_property_changed("display-name", self._account)
@@ -873,19 +894,15 @@
         self._server_property_changed("current-media", None)
         self._server_property_changed("msn-object", None)
         self._server_property_changed("client-capabilities", "0:0")
+        self._server_property_changed("end-points", {})
         self._server_infos_changed({})
 
-
     ### group management
     def _add_group_ownership(self, group):
         self._groups.add(group)
 
     def _delete_group_ownership(self, group):
         self._groups.discard(group)
-
-    def do_get_property(self, pspec):
-        name = pspec.name.lower().replace("-", "_")
-        return getattr(self, name)
 gobject.type_register(Contact)
 
 
@@ -930,3 +947,21 @@
         name = pspec.name.lower().replace("-", "_")
         return getattr(self, name)
 gobject.type_register(Group)
+
+
+class EndPoint(object):
+    def __init__(self, id, caps):
+        self.id = id
+        self.capabilities = ClientCapabilities(client_id=caps)
+        self.name = ""
+        self.idle = False
+        self.state = ""
+        self.client_type = 0
+
+    def __eq__(self, endpoint):
+        return (self.id == endpoint.id and
+                self.capabilities == endpoint.capabilities and
+                self.name == endpoint.name and
+                self.idle == endpoint.idle and
+                self.state == endpoint.state and
+                self.client_type == endpoint.client_type)
--- papyon/service/AddressBook/address_book.py
+++ papyon/service/AddressBook/address_book.py
@@ -139,6 +139,10 @@
                 gobject.TYPE_NONE,
                 (object,)),
 
+            "contact-pending"         : (gobject.SIGNAL_RUN_FIRST,
+                gobject.TYPE_NONE,
+                 (object,)),
+
             "contact-deleted"         : (gobject.SIGNAL_RUN_FIRST,
                 gobject.TYPE_NONE,
                  (object,)),
@@ -265,10 +269,14 @@
             contact = profile.Contact(None, network_id, account, account)
         return contact
 
-    def check_pending_invitations(self):
+    def check_pending_invitations(self, done_cb=None, failed_cb=None):
+        def callback(memberships):
+            self.__update_memberships(memberships)
+            self.__common_callback('contact-pending', done_cb,
+                    self.contacts.search_by_memberships(Membership.PENDING))
         cp = scenario.CheckPendingInviteScenario(self._sharing,
-                 (self.__update_memberships,),
-                 (self.__common_errback, None))
+                 (callback,),
+                 (self.__common_errback, failed_cb))
         cp()
 
     def accept_contact_invitation(self, pending_contact, add_to_contact_list=True,
--- papyon/service/AddressBook/scenario/contacts/check_pending_invite.py
+++ papyon/service/AddressBook/scenario/contacts/check_pending_invite.py
@@ -44,5 +44,10 @@
 
     def __membership_findall_errback(self, error_code):
         errcode = AddressBookError.UNKNOWN
+        if error_code == 'FullSyncRequired':
+            self.__sharing.FindMembership((self.__membership_findall_callback,),
+                    (self.__membership_findall_errback,),
+                    self._scenario, ['Messenger'], False)
+            return
         self.errback(errcode)
 
--- papyon/service/ContentRoaming/scenario/get_stored_profile.py
+++ papyon/service/ContentRoaming/scenario/get_stored_profile.py
@@ -40,18 +40,19 @@
         self.__storage.GetProfile((self.__get_profile_callback,),
                                   (self.__get_profile_errback,),
                                   self._scenario, self.cid,
-                                  True, True, True, True, True, True, 
+                                  True, True, True, True, True, True,
                                   True, True, True, True, True)
 
-    def __get_profile_callback(self, profile_rid, expression_profile_rid, 
-                               display_name, personal_msg, photo_rid, 
-                               photo_mime_type, photo_data_size, photo_url):
+    def __get_profile_callback(self, profile_rid, expression_profile_rid,
+                               display_name, personal_msg, user_tile_url,
+                               photo_rid, photo_mime_type, photo_data_size,
+                               photo_url):
         callback = self._callback
-        callback[0](profile_rid, expression_profile_rid, display_name, 
+        callback[0](profile_rid, expression_profile_rid, display_name,
             personal_msg, photo_rid, *callback[1:])
 
         if photo_rid is not None:
-            self.__storage.get_display_picture(photo_url, 
+            self.__storage.get_display_picture(photo_url, user_tile_url,
                                (self.__get_display_picture_callback,),
                                (self.__get_display_picture_errback,))
 
@@ -71,4 +72,3 @@
         errback = self._errback[0]
         args = self._errback[1:]
         errback(errcode, *args)
-
--- papyon/service/ContentRoaming/storage.py
+++ papyon/service/ContentRoaming/storage.py
@@ -22,6 +22,8 @@
 
 from papyon.gnet.protocol import ProtocolFactory
 
+import urllib
+
 __all__ = ['Storage']
 
 class Storage(SOAPService):
@@ -58,6 +60,7 @@
 
         display_name = expression_profile.findtext('./st:DisplayName')
         personal_msg = expression_profile.findtext('./st:PersonalStatus')
+        user_tile_url = expression_profile.findtext('./st:StaticUserTilePublicURL')
 
         photo = expression_profile.find('./st:Photo')
         if photo is not None:
@@ -70,7 +73,7 @@
             photo_rid = photo_mime_type = photo_data_size = photo_url = None
         
         callback[0](profile_rid, expression_profile_rid, display_name, personal_msg,
-                    photo_rid, photo_mime_type, photo_data_size, photo_url,
+                    user_tile_url, photo_rid, photo_mime_type, photo_data_size, photo_url,
                     *callback[1:])
 
     def UpdateProfile(self, callback, errback, scenario, profile_rid,
@@ -126,28 +129,35 @@
         self._soap_request(method, (scenario, token), args, callback, errback)
 
     @RequireSecurityTokens(LiveService.STORAGE)
-    def get_display_picture(self, pre_auth_url, callback, errback):
+    def get_display_picture(self, pre_auth_url, user_tile_url, callback, errback):
         token = str(self._tokens[LiveService.STORAGE])
 
-        scheme = 'http'
-        host = 'byfiles.storage.msn.com'
-        port = 80
-        resource = '?'.join([pre_auth_url, token.split('&')[0]])
-
-        def request_callback(transport, http_response):
-            type = http_response.get_header('Content-Type')#.split('/')[1]
+        def success(transport, http_response):
+            type = http_response.get_header('Content-Type')
             data = http_response.body
             callback[0](type, data, *callback[1:])
 
+        def total_fail(transport, error_code):
+            errback[0](*errback[1:])
+
+        def request_static_tile(transport, error_code):
+            # Request using the PreAuthURL didn't work, try with static tilephoto
+            scheme, host, port, resource = url_split(user_tile_url)
+            self.get_resource(scheme, host, resource, success, total_fail)
+
+        scheme, host, port, resource = url_split(pre_auth_url)
+        resource += '?t=' + urllib.quote(token.split('&')[0][2:])
+        self.get_resource(scheme, host, resource, success, request_static_tile)
+
+    def get_resource(self, scheme, host, resource, callback, errback):
         http_headers = {}
         http_headers["Accept"] = "*/*"
         http_headers["Proxy-Connection"] = "Keep-Alive"
         http_headers["Connection"] = "Keep-Alive"
         
-        proxy = self._proxies.get(scheme, None)
-        transport = ProtocolFactory(scheme, host, port, proxy=proxy)
-        transport.connect("response-received", request_callback)
+        transport = ProtocolFactory(scheme, host, proxies=self._proxies)
+        transport.connect("response-received", callback)
         transport.connect("request-sent", self._request_handler)
-        transport.connect("error", errback[0], *errback[1:])
+        transport.connect("error", errback)
 
         transport.request(resource, http_headers, method='GET')
--- papyon/service/OfflineIM/offline_messages_box.py
+++ papyon/service/OfflineIM/offline_messages_box.py
@@ -24,9 +24,11 @@
 from papyon.service.SOAPUtils import *
 from papyon.service.OfflineIM.constants import *
 
+from papyon.msnp import Message
 from papyon.profile import NetworkID
 
 from papyon.util.decorator import throttled
+from papyon.util.encoding import decode_rfc2047_string
 
 import papyon.util.element_tree as ElementTree
 import papyon.util.string_io as StringIO
@@ -270,13 +272,7 @@
 
             # Get the name of the sender
             name = m.findtext('./N');
-            # Decode it according to RFC 2047
-            from email.header import decode_header
-            parts = decode_header(name)
-            name = ''
-            for part in parts:
-                name += part[0].decode(part[1]) if part[1] else part[0]
-
+            name = decode_rfc2047_string(name)
             date = m.find('./RT')
             if date is not None:
                 date = date.text
@@ -288,6 +284,14 @@
         if len(self.__messages) > 0:
             self.emit('messages-received', self.__messages)
 
+    def __send_unmanaged_message(self, recipient, body):
+        """ Send offline message through a UUM command when using MSNP18. """
+        message = Message(self._client.profile)
+        message.content_type = ("text/plain", "utf-8")
+        message.add_header("Dest-Agent", "client")
+        message.body = body
+        self._client._protocol.send_unmanaged_message(recipient, message)
+
     # Public API
     def fetch_messages(self, messages=None):
         if messages is None:
@@ -303,8 +307,12 @@
         fm.message_ids = [m.id for m in messages]
         fm()
 
-    @throttled(1000, list())
+    @throttled(1, list())
     def send_message(self, recipient, message):
+        if self._client.protocol_version >= 18:
+            self.__send_unmanaged_message(recipient, message)
+            return
+            
         if recipient.network_id == NetworkID.EXTERNAL:
             return
 
--- papyon/service/SOAPService.py
+++ papyon/service/SOAPService.py
@@ -220,9 +220,7 @@
             return #FIXME: propagate the error up
 
         if not soap_response.is_fault():
-            handler = getattr(self,
-                    "_Handle" + request_id + "Response",
-                    None)
+            handler = getattr(self, "_Handle" + request_id + "Response", None)
             method = getattr(self._service, request_id)
             response = method.process_response(soap_response)
 
@@ -232,9 +230,7 @@
                 self._HandleSOAPResponse(request_id, callback, errback,
                         response, user_data)
         else:
-            handler = getattr(self,
-                    "_Handle" + request_id + "Fault",
-                    None)
+            handler = getattr(self, "_Handle" + request_id + "Fault", None)
             if handler is not None:
                 handler(callback, errback, soap_response, user_data)
             else:
@@ -270,11 +266,10 @@
             transport = trans[0]
             trans[1].append((request_id, callback, errback, user_data))
         else:
-            proxy = self._proxies.get(scheme, None)
             transport = papyon.gnet.protocol.ProtocolFactory(scheme,
-                    host, port, proxy=proxy)
-            handler_id = [transport.connect("response-received",
-                    self._response_handler),
+                    host, port, proxies=self._proxies)
+            handler_id = [
+                transport.connect("response-received", self._response_handler),
                 transport.connect("request-sent", self._request_handler),
                 transport.connect("error", self._error_handler)]
 
--- papyon/service/SingleSignOn.py
+++ papyon/service/SingleSignOn.py
@@ -115,6 +115,10 @@
 
 class SingleSignOn(SOAPService):
     def __init__(self, username, password, proxies=None):
+        # Passwords can only be up to 16 characters in length
+        if len(password) > 16:
+            password = password[0:16]
+
         self.__credentials = (username, password)
         self.__storage = {}
 
--- papyon/service/description/AB/common.py
+++ papyon/service/description/AB/common.py
@@ -25,7 +25,7 @@
 
     return """
         <ABApplicationHeader xmlns="http://www.msn.com/webservices/AddressBook">
-           <ApplicationId xmlns="http://www.msn.com/webservices/AddressBook">996CDE1E-AA53-4477-B943-2BE802EA6166</ApplicationId>
+           <ApplicationId xmlns="http://www.msn.com/webservices/AddressBook">CFE80F9D-180F-4399-82AB-413F33A1FA11</ApplicationId>
            <IsMigration xmlns="http://www.msn.com/webservices/AddressBook">false</IsMigration>
            <PartnerScenario xmlns="http://www.msn.com/webservices/AddressBook">%s</PartnerScenario>
        </ABApplicationHeader>
--- papyon/service/description/Sharing/common.py
+++ papyon/service/description/Sharing/common.py
@@ -27,7 +27,7 @@
 
     return """
         <ABApplicationHeader xmlns="http://www.msn.com/webservices/AddressBook">
-           <ApplicationId xmlns="http://www.msn.com/webservices/AddressBook">996CDE1E-AA53-4477-B943-2BE802EA6166</ApplicationId>
+           <ApplicationId xmlns="http://www.msn.com/webservices/AddressBook">CFE80F9D-180F-4399-82AB-413F33A1FA11</ApplicationId>
            <IsMigration xmlns="http://www.msn.com/webservices/AddressBook">false</IsMigration>
            <PartnerScenario xmlns="http://www.msn.com/webservices/AddressBook">%s</PartnerScenario>
        </ABApplicationHeader>
--- papyon/service/description/SingleSignOn/RequestMultipleSecurityTokens.py
+++ papyon/service/description/SingleSignOn/RequestMultipleSecurityTokens.py
@@ -21,7 +21,7 @@
 import xml.sax.saxutils as xml
 
 class LiveService(object):
-    CONTACTS = ("contacts.msn.com", "?fs=1&id=24000&kv=7&rn=93S9SWWw&tw=0&ver=2.1.6000.1")
+    CONTACTS = ("contacts.msn.com", "MBI")
     MESSENGER = ("messenger.msn.com", "?id=507")
     MESSENGER_CLEAR = ("messengerclear.live.com", "MBI_KEY_OLD")
     MESSENGER_SECURE = ("messengersecure.live.com", "MBI_SSL")
--- papyon/sip/__init__.py
+++ papyon/sip/__init__.py
@@ -18,4 +18,4 @@
 # along with this program; if not, write to the Free Software
 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
-from connection_manager import SIPConnectionManager
+from call_manager import SIPCallManager
--- papyon/sip/call.py
+++ papyon/sip/call.py
@@ -2,7 +2,7 @@
 #
 # papyon - a python client library for Msn
 #
-# Copyright (C) 2009 Collabora Ltd.
+# Copyright (C) 2009-2010 Collabora Ltd.
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -19,605 +19,278 @@
 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
 from papyon.event import EventsDispatcher
-from papyon.media import MediaCall, MediaSessionType
-from papyon.profile import Presence
+from papyon.media import MediaCall, MediaSessionType, RTCActivity
+from papyon.profile import NetworkID, Presence
 from papyon.service.SingleSignOn import *
 from papyon.sip.constants import *
-from papyon.sip.message import SIPRequest, SIPResponse
 from papyon.sip.sdp import SDPMessage
-from papyon.sip.turn import TURNClient
+from papyon.util.timer import Timer
 
-import base64
 import gobject
 import logging
-import re
-import uuid
 
-__all__ = ['SIPCall', 'SIPRegistration']
+__all__ = ['SIPCall']
 
 logger = logging.getLogger('papyon.sip.call')
 
+class SIPCall(gobject.GObject, MediaCall, RTCActivity, EventsDispatcher, Timer):
 
-class SIPBaseCall(gobject.GObject):
-    """Base class representing a transaction between two entities. A transaction
-       between an user and a registrar is a SIPRegistration and a transaction
-       between two SIP end points is a SIPCall. This class mainly contains
-       utility functions to build and parse request/responses."""
+    __gsignals__ = {
+        'ended': (
+            gobject.SIGNAL_RUN_FIRST,
+            gobject.TYPE_NONE,
+            ())
+    }
 
-    def __init__(self, connection, client, id=None):
+    def __init__(self, client, core, id, peer=None, invite=None):
         gobject.GObject.__init__(self)
-        self._connection = connection
+        MediaCall.__init__(self, MediaSessionType.TUNNELED_SIP)
+        RTCActivity.__init__(self, client)
+        EventsDispatcher.__init__(self)
+        Timer.__init__(self)
+
         self._client = client
-        self._ip = "127.0.0.1"
-        self._port = 50390
-        self._transport_protocol = connection.transport.protocol
-        self._account = client.profile.account
+        self._core = core
         self._id = id
-        self._cseq = 0
-        self._remote = None
-        self._route = None
-        self._uri = None
-        self._timeout_sources = {}
+        self._peer = peer
+        self._invite = invite
 
-    @property
-    def id(self):
-        if not self._id:
-            self._id = uuid.uuid4().get_hex()
-        return self._id
+        self._incoming = (invite is not None)
+        self._ringing = False
+        self._pending_invite = False
+        self._pending_cancel = False
 
-    def get_cseq(self, incr=False):
-        if incr:
-            self._cseq += 1
-        return self._cseq
-
-    def get_epid(self):
-        if not hasattr(self, '_epid'):
-            self._epid = uuid.uuid4().get_hex()[:10]
-        return self._epid
-
-    def get_mepid(self):
-        if self._connection.tunneled:
-            mepid = self._connection._client.machine_guid
-            mepid = filter(lambda c: c not in "{-}", mepid).upper()
-            return ";mepid=%s" % mepid
-        else:
-            return ""
+        self._dialogs = []
+        self._handles = {}
 
-    def get_tag(self):
-        if not hasattr(self, '_tag'):
-            self._tag = uuid.uuid4().get_hex()
-        return self._tag
-
-    def get_sip_instance(self):
-        return SIP_INSTANCE
-
-    def send(self, message, registration=False):
-        message.call = self
-        self._connection.send(message, registration)
-
-    def find_contact(self, email):
-        contacts = self._client.address_book.contacts.search_by_account(email)
-        if not contacts:
-            return None
-        return contacts[0]
+        if self._incoming:
+            self._build_from_invite(invite)
 
-    def parse_contact(self, message):
-        if type(message) is SIPRequest:
-            header = "From"
-        elif type(message) is SIPResponse:
-            header = "To"
-        else:
-            return None
-        email = self.parse_email(message, header)
-        return self.find_contact(email)
 
-    def parse_email(self, message, name):
-        header = message.get_header(name)
-        if header is not None:
-            return re.search("<sip:([^;>]*)(;|>)", header).group(1)
-
-    def parse_uri(self, message, name):
-        header = message.get_header(name)
-        if header is not None:
-            return re.search("<([^>]*)>", header).group(1)
-
-    def parse_sip(self, message, name):
-        header = message.get_header(name)
-        if header is not None:
-            return re.search("<sip:[^>]*>", header).group(0)
-
-    def build_from_header(self, name="0"):
-        return '"%s" <sip:%s%s>;tag=%s;epid=%s' % \
-            (name, self._account, self.get_mepid(), self.get_tag(),
-             self.get_epid())
-
-    def build_request(self, code, uri, to, name="0", incr=False):
-        request = SIPRequest(code, uri)
-        request.add_header("Via", "SIP/2.0/%s %s:%s" %
-            (self._transport_protocol, self._ip, self._port))
-        request.add_header("Max-Forwards", 70)
-        request.add_header("Call-ID", self.id)
-        request.add_header("CSeq", "%i %s" % (self.get_cseq(incr), code))
-        request.add_header("To", to)
-        request.add_header("From", self.build_from_header(name))
-        request.add_header("User-Agent", USER_AGENT)
-        return request
-
-    def build_response(self, request, status, reason=None):
-        response = SIPResponse(status, reason)
-        response.clone_headers("From", request)
-        response.add_header("To", self.build_from_header())
-        response.clone_headers("CSeq", request)
-        response.clone_headers("Record-Route", request)
-        response.clone_headers("Via", request)
-        response.add_header("Call-ID", self.id)
-        response.add_header("Max-Forwards", 70)
-        response.add_header("User-Agent", USER_AGENT)
-        return response
-
-    def on_message_received(self, msg):
-        route = self.parse_sip(msg, "Record-Route")
-        if route is not None:
-            self._route = route
-        uri = self.parse_uri(msg, "Contact")
-        if uri is not None:
-            self._uri = uri
-
-        if type(msg) is SIPResponse:
-            self._remote = msg.get_header("To")
-            handler_name = "on_%s_response" % msg.code.lower()
-        elif type(msg) is SIPRequest:
-            self._remote = msg.get_header("From")
-            handler_name = "on_%s_received" % msg.code.lower()
-        handler = getattr(self, handler_name, None)
-        if handler is not None:
-            handler(msg)
-        else:
-            logger.warning("Unhandled %s message" % msg.code)
+    # Public API -------------------------------------------------------------
 
     @property
-    def timeouts(self):
-        return self._timeout_sources.keys()
-
-    def start_timeout(self, name, time):
-        self.stop_timeout(name)
-        source = gobject.timeout_add(time * 1000, self.on_timeout, name)
-        self._timeout_sources[name] = source
-
-    def stop_timeout(self, name):
-        source = self._timeout_sources.get(name, None)
-        if source is not None:
-            gobject.source_remove(source)
-            del self._timeout_sources[name]
-
-    def stop_all_timeout(self):
-        for (name, source) in self._timeout_sources.items():
-            if source is not None:
-                gobject.source_remove(source)
-        self._timeout_sources.clear()
-
-    def on_timeout(self, name):
-        self.stop_timeout(name)
-        handler = getattr(self, "on_%s_timeout" % name, None)
-        if handler is not None:
-            handler()
-
-
-class SIPCall(SIPBaseCall, MediaCall, EventsDispatcher):
-    """Represent a SIP dialog between two end points. A call must be initiated
-       by sending or receiving an INVITE request. It is disposed when
-       receiving or by sending a CANCEL or BYE request.
-
-       For a more complete description of the transactions, see the SIP
-       standard memo (RFC 3261) : http://tools.ietf.org/html/rfc3261"""
-
-    def __init__(self, connection, client, peer=None, invite=None, id=None):
-        session_type = connection.tunneled and MediaSessionType.TUNNELED_SIP \
-                or MediaSessionType.SIP
-        SIPBaseCall.__init__(self, connection, client, id)
-        MediaCall.__init__(self, session_type)
-        EventsDispatcher.__init__(self)
-
-        self._incoming = (id is not None)
-        self._accepted = False
-        self._rejected = False
-        self._answer_sent = False
-        self._early = False
-        self._state = None
-        self._relay_requested = False
-
-        if peer is None and invite is not None:
-            peer = self.parse_contact(invite)
-        self._peer = peer
-        self._invite = invite
+    def id(self):
+        return self._id
 
     @property
     def peer(self):
         return self._peer
 
     @property
-    def conversation_id(self):
-        if self.media_session.has_video:
-            return 1
-        else:
-            return 0
+    def incoming(self):
+        return self._incoming
 
     @property
-    def answered(self):
-        return (self._accepted or self._rejected) and self._answer_sent
+    def peer_uri(self):
+        return "sip:" + self._peer.account
 
     @property
-    def incoming(self):
-        return self._incoming
-
-    def build_invite_contact(self):
-        if self._connection.tunneled:
-            m = "<sip:%s%s>;proxy=replace;+sip.instance=\"<urn:uuid:%s>\"" % (
-                self._account, self.get_mepid(), self.get_sip_instance())
-        else:
-            m = "<sip:%s:%i;maddr=%s;transport=%s>;proxy=replace" % (
-                self._account, self._port, self._ip, self._transport_protocol)
-        return m
-
-    def build_invite_request(self, uri, to):
-        message = SDPMessage(session=self.media_session)
-        request = self.build_request("INVITE", uri, to, incr=True)
-        request.add_header("Ms-Conversation-ID", "f=%s" % self.conversation_id)
-        request.add_header("Contact", self.build_invite_contact())
-        request.set_content(str(message), "application/sdp")
-        return request
+    def can_answer(self):
+        if not self._incoming:
+            logger.warning("Can't answer to outgoing call %s" % self._id)
+            return False
+        if self._dialog is None:
+            logger.error("No dialog for the incoming call %s" % self._id)
+            return False
+        if self._dialog.answered:
+            logger.warning("Call %s has already been answered" % self._id)
+            return False
+        return True
 
     def invite(self):
-        if not self.media_session.prepared:
-            return
-        logger.info("Send call invitation to %s", self._peer.account)
-        self._state = "CALLING"
-        self._early = False
-        self._uri = "sip:%s" % self._peer.account
-        self._remote = "<%s>" % self._uri
-        self._invite = self.build_invite_request(self._uri, self._remote)
-        self.start_timeout("invite", 30)
-        self.send(self._invite)
+        """Send the invitation to the peer. Invite is sent by the UA Core
+           because dialogs are created when receiving response."""
 
-    def reinvite(self):
-        if self._incoming or not self.media_session.ready:
+        if self._incoming or self._invite:
+            return
+        if not self.media_session.prepared:
+            self._pending_invite = True
             return
-        self._state = "REINVITING"
-        self._invite = self.build_invite_request(self._uri, self._remote)
-        self._invite.add_header("Route", self._route)
-        self._invite.add_header("Supported", "ms-dialog-route-set-update")
-        self.start_timeout("invite", 10)
-        self.send(self._invite)
-
-    def answer(self, status):
-        response = self.build_response(self._invite, status)
-        if status == 200:
-            message = SDPMessage(session=self.media_session)
-            response.add_header("Contact", self.build_invite_contact())
-            response.set_content(str(message), "application/sdp")
-        self.send(response)
+        logger.info("Send call invitation to %s" % self._peer.account)
+        offer = SDPMessage(session=self.media_session)
+        self._pending_invite = False
+        self._invite = self._core.invite(self._id, self.peer_uri, offer)
 
     def ring(self):
-        if self._invite is None :
+        if not self.can_answer:
             return
+        if self._ringing:
+            return
+        self._ringing = True
         self.start_timeout("response", 50)
-        self.answer(180)
+        self._dialog.ring()
         self._dispatch("on_call_incoming")
 
     def accept(self):
-        if self.answered:
-            return
-        self._accepted = True
-        if not self.media_session.prepared:
+        if not self.can_answer:
             return
         self.stop_timeout("response")
-        self.start_timeout("ack", 5)
-        self._answer_sent = True
-        self.answer(200)
+        self._dialog.accept()
+        self._accept_activity()
 
     def reject(self, status=603):
-        if self.answered:
+        if not self.can_answer:
             return
-        self._state = "DISCONNECTING"
         self.stop_timeout("response")
-        self.start_timeout("ack", 5)
-        self._rejected = True
-        self._answer_sent = True
-        self.answer(status)
-
-    def reaccept(self):
-        if not self.media_session.ready:
-            return
-        self._state = "CONFIRMED"
-        self.answer(200)
-
-    def send_ack(self, response):
-        request = self.build_request("ACK", self._uri, self._remote)
-        request.add_header("Route", self._route)
-        self.send(request)
+        self._dialog.reject(status)
+        self._decline_activity()
 
     def end(self):
-        if self._state in ("INCOMING"):
-            self.reject()
-        elif self._state in ("CALLING", "REINVITING"):
-            self.cancel()
-        elif self._peer.presence == Presence.OFFLINE:
-            self.force_dispose()
-        else:
-            self.send_bye()
+        if self._peer.presence == Presence.OFFLINE:
+            self._dispose()
 
-    def cancel(self):
-        if self._state not in ("CALLING", "REINVITING"):
-            return
+        if not self._invite:
+            self._dispose()
+        elif not self._dialogs:
+            self._pending_cancel = True
+        else:
+            for dialog in self._dialogs:
+                dialog.end()
+
+    ### Messages Handling (Outside Dialog) -----------------------------------
+
+    def handle_invite_request(self, invite):
+        logger.info("Establish new UAS dialog to handle INVITE request")
+        dialog = self._core.establish_UAS_dialog(invite, 100)
+        self._add_dialog(dialog)
+        return dialog.handle_request(invite)
+
+    def handle_invite_response(self, response):
+        logger.info("Establish new UAC dialog to handle INVITE response")
+        response.request = self._invite
+        dialog = self._core.establish_UAC_dialog(response)
+        self._add_dialog(dialog)
+        return dialog.handle_response(response)
 
-        if self._state == "CALLING":
-            if self._invite is None:
-                self.force_dispose()
-                return
-            elif not self._invite.sent:
-                self._invite.cancelled = True
-                self.force_dispose()
-                return
-
-        self._state = "DISCONNECTING"
-        request = self.build_request("CANCEL", self._invite.uri, None)
-        request.clone_headers("To", self._invite)
-        request.clone_headers("Route", self._invite)
-        self.start_timeout("cancel", 5)
-        self.send(request)
+    ### Private API ----------------------------------------------------------
 
-    def send_bye(self):
-        if self._state == "DISCONNECTING":
-            return
-
-        self._state = "DISCONNECTING"
-        request = self.build_request("BYE", self._uri, self._remote, incr=True)
-        request.add_header("Route", self._route)
-        self.start_timeout("bye", 5)
-        self.send(request)
+    @property
+    def _dialog(self):
+        if len(self._dialogs) == 0:
+            return None
+        return self._dialogs[0]
 
-    def force_dispose(self):
-        self._state = "DISCONNECTING"
-        self.stop_all_timeout()
-        self.dispose()
+    def _build_from_invite(self, invite):
+        account = invite.contact.uri.replace("sip:", "")
+        #FIXME hack
+        if ";mepid=" in account:
+            account = account.split(";mepid=")[0]
+        self._peer = self._client.address_book.search_or_build_contact(account,
+                NetworkID.MSN)
 
-    def dispose(self):
-        if self.timeouts:
-            return # we have to wait some responses
+    def _dispose(self):
+        logger.info("Call has been ended")
         MediaCall.dispose(self)
-        self._state = "DISCONNECTED"
+        self.stop_all_timeout()
+        self.emit("ended")
         self._dispatch("on_call_ended")
-        self._connection.remove_call(self)
-
-    def on_invite_received(self, invite):
-        self._invite = invite
-        self.answer(100)
-
-        try:
-            message = SDPMessage(body=invite.body)
-        except:
-            logger.error("Malformed body in incoming call invitation")
-            self.reject(488)
-            return
-
-        if self._state is None:
-            self._state = "INCOMING"
-            self.start_timeout("response", 50)
-            self.media_session.process_remote_message(message, True)
-        elif self._state == "CONFIRMED":
-            self._state = "REINVITED"
-            self.media_session.process_remote_message(message, False)
-            self.reaccept()
-        else:
-            self.answer(488) # not acceptable here
-
-    def on_ack_received(self, ack):
-        self.stop_timeout("ack")
-        if self._rejected:
-            self.dispose()
-        else:
-            self._state = "CONFIRMED"
+        self._dispose_activity()
 
-    def on_cancel_received(self, cancel):
-        if self.incoming and not self.answered:
-            self.reject(487)
-        response = self.build_response(cancel, 200)
-        self.send(response)
-        self.dispose()
-
-    def on_bye_received(self, bye):
-        response = self.build_response(bye, 200)
-        self.send(response)
-        self.dispose()
-
-    def on_invite_response(self, response):
-        if self._state == "REINVITING":
-            return self.on_reinvite_response(response)
-        elif self._incoming:
-            return
+    ### Dialogs Handling -----------------------------------------------------
 
-        self._remote = response.get_header("To")
-        if response.status >= 200:
-            self.send_ack(response)
-            self.stop_timeout("invite")
-
-        if response.status is 100:
-            self._early = True
-        elif response.status is 180:
+    def _add_dialog(self, dialog):
+        """Add dialog to call"""
+        self._connect_dialog(dialog)
+        self._dialogs.append(dialog)
+
+        # We were waiting for a response to cancel...
+        if self._pending_cancel:
+            self.end()
+
+    def _keep_dialog(self, dialog_to_keep):
+        """Remove all dialogs except the one."""
+        if dialog_to_keep not in self._dialogs:
+            return False
+        self._dialogs.remove(dialog_to_keep)
+        handles = self._handles.pop(dialog_to_keep)
+        self._remove_all_dialogs()
+        self._dialogs = [dialog_to_keep]
+        self._handles[dialog_to_keep] = handles
+        return True
+
+    def _remove_all_dialogs(self):
+        """Remove and disconnect all dialogs."""
+        for dialog in self._dialogs:
+            self._disconnect_dialog(dialog)
+        self._dialogs = []
+
+    def _connect_dialog(self, dialog):
+        handles = []
+        handles.append(dialog.connect("ringing", self._on_dialog_ringing))
+        handles.append(dialog.connect("accepted", self._on_dialog_accepted))
+        handles.append(dialog.connect("rejected", self._on_dialog_rejected))
+        handles.append(dialog.connect("ended", self._on_dialog_ended))
+        handles.append(dialog.connect("offer-received", self._on_offer_received))
+        self._handles[dialog] = handles
+
+    def _disconnect_dialog(self, dialog):
+        if not dialog in self._handles:
+            return
+        handles = self._handles.pop(dialog)
+        for handle in handles:
+            dialog.disconnect(handle)
+
+    def _on_dialog_ringing(self, dialog):
+        if not self._ringing:
+            logger.info("Call is ringing")
+            self._ringing = True
             self._dispatch("on_call_ringing")
-        elif response.status is 200:
-            self._state = "CONFIRMED"
-            try:
-                message = SDPMessage(body=response.body)
-                self.media_session.process_remote_message(message)
-            except Exception, err:
-                logger.error("Malformed body in invite response")
-                logger.exception(err)
-                self.send_bye()
-            else:
-                logger.info("Call invitation has been accepted")
-                self._dispatch("on_call_accepted")
-                self.reinvite()
-        elif response.status in (408, 480, 486, 487, 504, 603):
-            logger.info("Call invitation has been rejected (%i)", response.status)
-            self._dispatch("on_call_rejected", response)
-            self.dispose()
-        else:
-            self._dispatch("on_call_error", response)
-            self.send_bye()
-
-    def on_reinvite_response(self, response):
-        if response.status >= 200:
-            self.send_ack(response)
-            self.stop_timeout("invite")
-
-        if response.status in (100, 180):
-            pass
-        elif response.status in (200, 488):
-            self._state = "CONFIRMED"
-        else:
-            self.send_bye()
 
-    def on_cancel_response(self, response):
-        self.stop_timeout("cancel")
-        self.dispose()
-
-    def on_bye_response(self, response):
-        self.stop_timeout("bye")
-        self.dispose()
-
-    def on_media_session_prepared(self, session):
-        if self._state is None:
-            self.invite()
-        elif self._state == "INCOMING" and self._accepted:
-            self.accept()
+    def _on_dialog_accepted(self, dialog, response):
+        logger.info("Call invitation has been accepted")
+        # ignore other call dialogs
+        if self._keep_dialog(dialog):
+            self._dispatch("on_call_accepted")
+
+    def _on_dialog_rejected(self, dialog, response):
+        if response.status/100 == 6: # global rejection
+            # we don't have any chance to receive a positive answer
+            # keep that dialog for now, it will be disposed soon
+            self._keep_dialog(dialog)
+
+        remaining = len(self._dialogs) - 1
+        logger.info("Call invitation has been rejected (%i)", response.status)
+        logger.info("%i pending dialog(s) remaining for that call" % remaining)
+        if remaining <= 0:
+            self._dispatch("on_call_rejected", response)
 
-    def on_media_session_ready(self, session):
-        if self._state == "REINVITED":
-            self.reaccept()
-        elif self._state == "CONFIRMED":
-            self.reinvite()
+    def _on_dialog_ended(self, dialog):
+        self._disconnect_dialog(dialog)
+        self._dialogs.remove(dialog)
+        if len(self._dialogs) == 0:
+            self._dispose()
 
-    def on_invite_timeout(self):
-        self.cancel()
+    ### Timer Callbacks ------------------------------------------------------
 
     def on_response_timeout(self):
-        self.reject(408)
+        self.reject(408) #FIXME
         self._dispatch("on_call_missed")
 
-    def on_ack_timeout(self):
-        self.dispose()
+    ### Media Session Callbacks ----------------------------------------------
 
-    def on_cancel_timeout(self):
-        self.dispose()
-
-    def on_bye_timeout(self):
-        self.dispose()
-
-    def on_end_timeout(self):
-        self.dispose()
-
-    def request_turn_relays(self, streams_count):
-        # FIXME Request TURN relays before to send an invite or to accept one
-        turn_client = TURNClient(self._client._sso, self._account)
-        turn_client.connect("done", self.on_turn_relays_discovered)
-        turn_client.request_shared_secret(None, None, streams_count * 2)
-
-    def on_turn_relays_discovered(self, turn_client, relays):
-        logger.debug("Discovered %i TURN relays" % len(relays))
-
-
-class SIPRegistration(SIPBaseCall):
-
-    __gsignals__ = {
-        'registered': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ([])),
-        'unregistered': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ([])),
-        'failed': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ([]))
-    }
-
-    def __init__(self, connection, client):
-        SIPBaseCall.__init__(self, connection, client)
-        self._state = "NEW"
-        self._sso = client._sso
-        self._src = None
-        self._request = None
-        self._pending_unregister = False
-        self._tokens = {}
-
-    @property
-    def registered(self):
-        return (self._state == "REGISTERED")
+    def _on_offer_received(self, dialog, offer, initial=False):
+        #FIXME shouldn't need initial
+        if self.media_session.process_remote_message(offer, initial):
+            dialog.accept_remote_offer()
+        else:
+            dialog.reject_remote_offer()
 
-    def build_register_request(self, timeout, auth):
-        uri = "sip:%s" % self._account.split('@')[1]
-        to =  "<sip:%s>" % self._account
-        request = self.build_request("REGISTER", uri, to, incr=1)
-        request.add_header("ms-keep-alive", "UAC;hop-hop=yes")
-        request.add_header("Contact", "<sip:%s:%s;transport=%s>;proxy=replace" %
-            (self._ip, self._port, self._transport_protocol))
-        request.add_header("Event", "registration")
-        request.add_header("Expires", timeout)
-        request.add_header("Authorization", "Basic %s" % auth)
-        return request
+    def on_media_session_prepared(self, session):
+        if self._pending_invite:
+            self.invite()
+        elif self._incoming:
+            offer = SDPMessage(session=self.media_session)
+            self._dialog.update_local_offer(offer)
 
-    def register(self):
-        if self._state in ("REGISTERING", "REGISTERED", "CANCELLED"):
-            return
-        self._state = "REGISTERING"
-        self._do_register(None, None)
+    def on_media_session_ready(self, session):
+        offer = SDPMessage(session=self.media_session)
+        self._dialog.update_local_offer(offer)
 
-    @RequireSecurityTokens(LiveService.MESSENGER_SECURE)
-    def _do_register(self, callback, errback):
-        # Check if state changed while requesting security token
-        if self._state != "REGISTERING":
-            return
-        auth = "msmsgs:RPS_%s" % self._tokens[LiveService.MESSENGER_SECURE]
-        auth = base64.b64encode(auth).replace("\n", "")
-        self._request = self.build_register_request(900, auth)
-        self.send(self._request, True)
+    ### RTC Activity Callbacks -----------------------------------------------
 
-    def unregister(self):
-        if self._state in ("NEW", "UNREGISTERING", "UNREGISTERED", "CANCELLED"):
-            return
-        elif self._state == "REGISTERING":
-            if self._request is None:
-                self._state = "CANCELLED"
-                self.emit("unregistered")
-            else:
-                self._pending_unregister = True
-            return
+    def on_activity_accepted(self):
+        self._dispose()
 
-        self._state = "UNREGISTERING"
-        self._pending_unregister = False
-        if self._src is not None:
-            gobject.source_remove(self._src)
-        self._src = None
-        self._do_unregister(None, None)
-
-    @RequireSecurityTokens(LiveService.MESSENGER_SECURE)
-    def _do_unregister(self, callback, errback):
-        auth = "%s:%s" % (self._account, self._tokens[LiveService.MESSENGER_SECURE])
-        auth = base64.encodestring(auth).replace("\n", "")
-        request = self.build_register_request(0, auth)
-        self.send(request, True)
-
-    def on_expire(self):
-        self.register()
-        return False
-
-    def on_register_response(self, response):
-        if self._state == "UNREGISTERING":
-            self._state = "UNREGISTERED"
-            self.emit("unregistered")
-        elif self._state != "REGISTERING":
-            return # strange !?
-        elif response.status is 200:
-            self._state = "REGISTERED"
-            self.emit("registered")
-            timeout = int(response.get_header("Expires", 30))
-            self._src = gobject.timeout_add(timeout * 1000, self.on_expire)
-            if self._pending_unregister:
-                self.unregister()
-        else:
-            self._state = "UNREGISTERED"
-            self.emit("failed")
+    def on_activity_declined(self):
+        self._dispose()
--- papyon/sip/call_manager.py
+++ papyon/sip/call_manager.py
+# -*- coding: utf-8 -*-
+#
+# papyon - a python client library for Msn
+#
+# Copyright (C) 2010 Collabora Ltd.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+from papyon.msnp.constants import *
+from papyon.sip.call import SIPCall
+from papyon.sip.core import SIPCore
+
+import gobject
+import logging
+import uuid
+
+logger = logging.getLogger('papyon.sip.call_manager')
+
+class SIPCallManager(gobject.GObject):
+
+    __gsignals__ = {
+        'incoming-call': (
+            gobject.SIGNAL_RUN_FIRST,
+            gobject.TYPE_NONE,
+            (object,))
+    }
+
+    def __init__(self, client):
+        gobject.GObject.__init__(self)
+        self._client = client
+        self._core = SIPCore(self._client)
+        self._core.connect("invite-received", self._on_invite_received)
+        self._core.connect("invite-answered", self._on_invite_answered)
+
+        self._calls = {} # Call-ID => call, handle_id
+
+    def create_call(self, peer):
+        #FIXME check if busy
+        id = self._generate_id()
+        call = SIPCall(self._client, self._core, id, peer=peer)
+        self._add_call(call)
+        return call
+
+    def find_call(self, message):
+        return self._calls.get(message.call_id, (None, None))[0]
+
+    def _add_call(self, call):
+        handle = call.connect("ended", self._remove_call)
+        self._calls[call.id] = (call, handle)
+
+    def _remove_call(self, call):
+        call, handle = self._calls.pop(call.id)
+        call.disconnect(handle)
+
+    def _on_invite_received(self, core, invite):
+        #FIXME check if busy
+        id = invite.call_id
+        if id in self._calls:
+            logger.warning("Call with same id (%s) already exists" % id)
+            return
+
+        call = SIPCall(self._client, core, id, invite=invite)
+        if not call.handle_invite_request(invite):
+            logger.warning("Call ended before we could signal it (%s)" % id)
+            return
+
+        self._add_call(call)
+        self.emit("incoming-call", call)
+
+    def _on_invite_answered(self, core, response):
+        call_id = response.call_id
+        call, handle = self._calls.get(call_id, (None, None))
+        if not call:
+            logger.warning("No call matches with INVITE answer (%s)" % call_id)
+            return
+        call.handle_invite_response(response)
+
+    def _generate_id(self):
+        id = None
+        while True:
+            id = uuid.uuid4().get_hex()
+            if id not in self._calls: break
+        return id
--- papyon/sip/connection.py
+++ papyon/sip/connection.py
-# -*- coding: utf-8 -*-
-#
-# papyon - a python client library for Msn
-#
-# Copyright (C) 2009 Collabora Ltd.
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation; either version 2 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
-
-from papyon.sip.call import SIPCall, SIPRegistration
-from papyon.sip.constants import *
-from papyon.sip.message import SIPRequest
-
-import gobject
-import logging
-
-__all__ = ['SIPConnection', 'SIPTunneledConnection']
-
-logger = logging.getLogger('papyon.sip.connection')
-
-
-class SIPBaseConnection(gobject.GObject):
-
-    __gsignals__ = {
-        'invite-received': (gobject.SIGNAL_RUN_FIRST,
-            gobject.TYPE_NONE,
-            (object,)),
-        'disconnecting': (gobject.SIGNAL_RUN_FIRST,
-            gobject.TYPE_NONE,
-            ()),
-        'disconnected': (gobject.SIGNAL_RUN_FIRST,
-            gobject.TYPE_NONE,
-            ())
-    }
-
-    def __init__(self, client, transport):
-        gobject.GObject.__init__(self)
-        self._calls = {}
-        self._client = client
-        self._transport = transport
-        self._transport.connect("message-received", self.on_message_received)
-
-    @property
-    def transport(self):
-        return self._transport
-
-    def create_call(self, peer=None, invite=None, id=None):
-        call = SIPCall(self, self._client, peer, invite, id)
-        self.add_call(call)
-        return call
-
-    def add_call(self, call):
-        self._calls[call.id] = call
-
-    def remove_call(self, call):
-        if call.id in self._calls:
-            del self._calls[call.id]
-
-    def get_call(self, callid):
-        return self._calls.get(callid, None)
-
-    def send(self, message, registration=False):
-        message.sent = True
-        self._transport.send(message)
-
-    def on_message_received(self, parser, message):
-        callid = message.get_header("Call-ID")
-        call = self.get_call(callid)
-        if call is None:
-            if isinstance(message, SIPRequest) and message.code == "INVITE":
-                logger.info("Call invitation received")
-                call = self.create_call(invite=message, id=callid)
-                self.emit("invite-received", call)
-            else:
-                logger.info("Message with invalid call-id received")
-                if self.registered:
-                    call = SIPCall(self, self._client, invite=message, id=callid)
-                    response = call.build_response(message, 481)
-                    call.send(response) # call/transaction does not exist
-                return
-        call.on_message_received(message)
-
-
-class SIPConnection(SIPBaseConnection):
-
-    def __init__(self, client, transport):
-        SIPBaseConnection.__init__(self, client, transport)
-        self._tokens = {}
-        self._msg_queue = []
-        self._registration = SIPRegistration(self, self._client)
-        self._registration.connect("registered", self.on_registration_success)
-        self._registration.connect("unregistered", self.on_unregistration_success)
-        self.add_call(self._registration)
-
-    @property
-    def registered(self):
-        return self._registration.registered
-
-    @property
-    def tunneled(self):
-        return False
-
-    def register(self):
-        self._registration.register()
-
-    def unregister(self):
-        self.emit("disconnecting")
-        self._msg_queue = []
-        self._registration.unregister()
-
-    def send(self, message, registration=False):
-        if self.registered or registration:
-            message.sent = True
-            self._transport.send(message)
-        else:
-            self._msg_queue.append(message)
-            self.register()
-
-    def remove_call(self, call):
-        SIPBaseConnection.remove_call(self, call)
-        if len(self._calls) == 1:
-            self.unregister()
-
-    def on_registration_success(self, registration):
-        while len(self._msg_queue) > 0:
-            msg = self._msg_queue.pop(0)
-            if not msg.cancelled:
-                self.send(msg)
-
-    def on_unregistration_success(self, registration):
-        self.emit("disconnected")
-
-
-class SIPTunneledConnection(SIPBaseConnection):
-
-    @property
-    def registered(self):
-        return True
-
-    @property
-    def tunneled(self):
-        return True
--- papyon/sip/connection_manager.py
+++ papyon/sip/connection_manager.py
-# -*- coding: utf-8 -*-
-#
-# papyon - a python client library for Msn
-#
-# Copyright (C) 2009 Collabora Ltd.
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation; either version 2 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
-
-from papyon.msnp.constants import *
-from papyon.sip.connection import *
-from papyon.sip.transport import *
-
-import gobject
-
-class SIPConnectionManager(gobject.GObject):
-
-    __gsignals__ = {
-        'invite-received': (gobject.SIGNAL_RUN_FIRST,
-            gobject.TYPE_NONE,
-            ([object]))
-    }
-
-    server = "vp.sip.messenger.msn.com"
-    port = 443
-
-    def __init__(self, client, protocol):
-        gobject.GObject.__init__(self)
-        self._client = client
-        self._protocol = protocol
-        self._protocol.connect("buddy-notification-received",
-                self.on_notification_received)
-        self._connections = {}
-        self._disconnecting = []
-
-        self.create_connection(True, "NS")
-
-    def create_connection(self, tunneled, host=None):
-        if tunneled:
-            transport = SIPTunneledTransport(self._protocol)
-            connection = SIPTunneledConnection(self._client, transport)
-        else:
-            transport = SIPTransport(host, self.port)
-            connection = SIPConnection(self._client, transport)
-        connection.connect("invite-received", self.on_invite_received)
-        connection.connect("disconnecting", self.on_connection_disconnecting)
-        connection.connect("disconnected", self.on_connection_disconnected)
-        self._connections[host] = connection
-        return connection
-
-    def remove_connection(self, connection):
-        host = None
-        for k, v in self._connections.iteritems():
-            if v == connection:
-                host = k
-        if host is not None:
-            del self._connections[host]
-
-    def get_connection(self, tunneled, host=None):
-        if tunneled:
-            host = "NS"
-        connection = self._connections.get(host, None)
-        if connection is None:
-            connection = self.create_connection(tunneled, host)
-        return connection
-
-    def create_call(self, peer):
-        tunneled = (self._client.profile.client_id.supports_tunneled_sip and
-                    peer.client_capabilities.supports_tunneled_sip)
-        connection = self.get_connection(tunneled, self.server)
-        call = connection.create_call(peer=peer)
-        return call
-
-    def on_notification_received(self, protocol, type, notification):
-        if type is not UserNotificationTypes.SIP_INVITE:
-            return
-        args = notification.payload.split()
-        if len(args) == 3 and args[0] == "INVITE":
-            # Register to the server so we can take the call
-            connection = self.get_connection(False, args[1])
-            connection.register()
-
-    def on_invite_received(self, connection, call):
-        self.emit("invite-received", call)
-
-    def on_connection_disconnecting(self, connection):
-        self.remove_connection(connection)
-        if connection not in self._disconnecting:
-            self._disconnecting.append(connection)
-
-    def on_connection_disconnected(self, connection):
-        if connection in self._disconnecting:
-            self._disconnecting.remove(connection)
-        else:
-            self.remove_connection(connection)
--- papyon/sip/constants.py
+++ papyon/sip/constants.py
@@ -2,7 +2,7 @@
 #
 # papyon - a python client library for Msn
 #
-# Copyright (C) 2009 Collabora Ltd.
+# Copyright (C) 2009-2010 Collabora Ltd.
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -104,3 +104,19 @@
 
 SIP_INSTANCE = "0E04CFC3-0272-5A5C-B7C3-6FBE8DA71EAD"
 USER_AGENT = "papyon/0.1"
+
+T1 = 0.5 # seconds
+T2 = 4   # seconds
+
+TIMER_A = T1
+TIMER_B = int(64 * T1) # INVITE timer
+TIMER_F = int(64 * T1) # non-INVITE timer
+TIMER_H = int(64 * T1)
+
+class SIPTransactionError(object):
+    TIMEOUT = 1
+    TRANSPORT_ERROR = 2
+
+class SIPMode(object):
+    CLIENT = "UAC"
+    SERVER = "UAS"
--- papyon/sip/core.py
+++ papyon/sip/core.py
+# -*- coding: utf-8 -*-
+#
+# papyon - a python client library for Msn
+#
+# Copyright (C) 2010 Collabora Ltd.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+from papyon.sip.extensions import init_extensions
+from papyon.sip.constants import T1, SIPMode, SIPTransactionError, USER_AGENT
+from papyon.sip.dialog import SIPDialog
+from papyon.sip.message import SIPRequest, SIPResponse, SIPContact, SIPCSeq, SIPVia
+from papyon.sip.transaction import SIPTransactionLayer
+from papyon.sip.transport import SIPTunneledTransport
+from papyon.util.decorator import rw_property
+from papyon.util.timer import Timer
+
+import gobject
+import logging
+import uuid
+
+__all__ = ['SIPCore']
+
+logger = logging.getLogger('papyon.sip.core')
+
+class SIPCore(gobject.GObject, Timer):
+    """ The set of processing functions required at a UAS and a UAC that
+        resides above the transaction and transport layers.
+        
+        The core keeps track of the ongoing dialogs with peers. Apart from
+        that, the core is stateless and there is only one instance per UA."""
+
+    __gsignals__ = {
+        'register-answered': (
+            gobject.SIGNAL_RUN_FIRST,
+            gobject.TYPE_NONE,
+            (object,)),
+        'invite-received': (
+            gobject.SIGNAL_RUN_FIRST,
+            gobject.TYPE_NONE,
+            (object,)),
+        'invite-answered': (
+            gobject.SIGNAL_RUN_FIRST,
+            gobject.TYPE_NONE,
+            (object,)),
+        'cancel-received': (
+            gobject.SIGNAL_RUN_FIRST,
+            gobject.TYPE_NONE,
+            (object,))
+    }
+
+    @property
+    def self_uri(self):
+        return "sip:" + self._client.profile.account
+
+    def __init__(self, client):
+        gobject.GObject.__init__(self)
+        Timer.__init__(self)
+
+        self._client = client
+        self._transport = SIPTunneledTransport(client._protocol)
+        self._transaction_layer = SIPTransactionLayer(self._transport)
+        self._transaction_layer.connect("request-received",
+                self._on_request_received)
+        self._transaction_layer.connect("response-received",
+                self._on_response_received)
+        self._transaction_layer.connect("error",
+                self._on_transaction_layer_error)
+
+        self._supported_methods = set(["INVITE", "ACK", "CANCEL", "BYE",
+            "OPTIONS", "INFO", "UPDATE", "REFER", "NOTIFY", "BENOTIFY"])
+        self._supported_extensions = set(init_extensions(client, self))
+        self._supported_content_types = set(["application/sdp"])
+
+        self._dialogs = {} # (call-id, local-tag, remote-tag) => dialog
+        self._dialog_handles = {} # dialog => handle id
+
+    def send(self, message, use_transaction=True):
+        """Apply extensions and send message to transaction layer."""
+        #self._add_supported_methods(message)
+        self._apply_extensions(message)
+        if not use_transaction:
+            return self._transport.send(message)
+        return self._transaction_layer.send(message)
+
+    def answer(self, request, status, tag=None, extra_headers={}, content=None):
+        """Create response with given status and send to transaction layer."""
+        response = self.create_response(request, status, tag=tag)
+        for (name, value) in extra_headers:
+            response.add_header(name, value)
+        if content is not None:
+            response.set_content(content)
+        return self.send(response)
+
+    # Public API -------------------------------------------------------------
+
+    def register(self, call_id, cseq, timeout, auth):
+        request = self._create_register_request(call_id, cseq, timeout, auth)
+        self.send(request)
+        return request
+
+    def invite(self, call_id, uri, offer):
+        request = self._create_invite_request(call_id, uri, offer)
+        self.request_UAC_dialog(request)
+        return request
+
+    def cancel(self, canceled_request):
+        request = self._create_cancel_request(canceled_request)
+        self.start_timeout("cancel", int(64 * T1), canceled_request)
+        self.send(request)
+        return request
+
+    # Dialog Management ------------------------------------------------------
+
+    def establish_UAS_dialog(self, request, status):
+        # 12.1.1 UAS behavior (Creation of a Dialog)
+        self_tag = self._generate_tag()
+        response = self.create_response(request, status, tag=self_tag)
+        response.clone_headers("Record-Route", request)
+        #response.add_header("Contact", SIPContact(None, self.self_uri, None))
+        request.transaction.send(response)
+        return self._create_dialog(request, response, SIPMode.SERVER)
+
+    def request_UAC_dialog(self, request):
+        # 12.1.2 UAC behavior (Creation of a Dialog)
+        self_tag = request.From.tag
+        request.add_header("Contact", SIPContact(None, self.self_uri, self_tag))
+        self.send(request)
+
+    def establish_UAC_dialog(self, response):
+        # 12.1.2 UAC behavior (Creation of a Dialog)
+        return self._create_dialog(response.request, response, SIPMode.CLIENT)
+
+    def _create_dialog(self, request, response, mode):
+        dialog = SIPDialog(self, request, response, mode)
+        key = (dialog.call_id, dialog.local_tag, dialog.remote_tag)
+        logger.info("Create dialog id=%s, local_tag=%s, remote_tag=%s" % key)
+        handle = dialog.connect("disposed", self._on_dialog_disposed)
+        self._dialogs[key] = dialog
+        self._dialog_handles[dialog] = handle
+        return dialog
+
+    def _find_dialog(self, message):
+        call_id = message.call_id
+        if type(message) is SIPRequest:
+            local_tag = message.To.tag
+            remote_tag = message.From.tag
+        else:
+            local_tag = message.From.tag
+            remote_tag = message.To.tag
+
+        return self._dialogs.get((call_id, local_tag, remote_tag), None)
+
+    def _forward_to_dialog(self, message):
+        dialog = self._find_dialog(message)
+        if dialog is not None:
+            if type(message) is SIPRequest:
+                dialog.handle_request(message)
+            elif type(message) is SIPResponse:
+                dialog.handle_response(message)
+
+    def _on_dialog_disposed(self, dialog):
+        key = (dialog.call_id, dialog.local_tag, dialog.remote_tag)
+        del self._dialogs[key]
+        handle = self._dialog_handles.pop(dialog)
+        dialog.disconnect(handle)
+
+    # Generating Messages -----------------------------------------------------
+
+    def create_request(self, code, to_uri, uri=None, tag=None, call_id=None,
+            cseq=None):
+        """ Create a request outside of a dialog """
+        # 8.1.1 Generating the Request (UAC Behavior)
+
+        if uri is None:
+            uri = to_uri
+        if tag is None:
+            tag = self._generate_tag()
+        if call_id is None:
+            call_id = self._generate_call_id()
+        if cseq is None:
+            cseq = self._generate_cseq()
+
+        request = SIPRequest(code, uri)
+        request.add_header("To", SIPContact("0", to_uri))
+        request.add_header("From", SIPContact("0", self.self_uri, tag))
+        request.add_header("Call-Id", call_id)
+        request.add_header("CSeq", SIPCSeq(cseq, code))
+        return request
+
+    def create_response(self, request, status, tag=None):
+        """ Create a response outside of a dialog """
+        # 8.2.6 Generating the Response (UAS Behavior)
+        response = SIPResponse(status)
+        response.request = request
+
+        # 8.2.6.1 Sending a Provisional Response
+        if status/100 == 1 and request.code != "INVITE":
+            return
+        if status == 100:
+            response.clone_headers("Timestamp", request)
+
+        # 8.2.6.2 Headers and Tags
+        response.clone_headers("To", request)
+        response.clone_headers("From", request)
+        response.clone_headers("Call-ID", request)
+        response.clone_headers("CSeq", request)
+        response.clone_headers("Via", request)
+
+        # Add To tag if missing
+        if not response.to.tag:
+            if tag is None:
+                tag = self._generate_tag()
+            response.to.tag = tag
+
+        return response
+
+    # Extensions Management --------------------------------------------------
+
+    def _apply_extensions(self, message):
+        for extension in self._supported_extensions:
+            extension.extend(message)
+
+    def _add_supported_methods(self, message):
+        methods = ", ".join(self._supported_methods)
+        message.add_header("Allow", methods)
+
+    # Messages Handling ------------------------------------------------------
+
+    def _on_response_received(self, transaction_layer, response):
+        # 8.1.3 Processing Responses (UAC Behavior)
+
+        if len(response.get_headers("Via", [])) >= 2:
+            return # discard message (8.1.3.3)
+
+        if response.status/100 == 3:
+            return # 3xx responses are not handled
+
+        if response.status/100 == 4:
+            pass # 4xx responses are not handled (Bad Request)
+
+        handler = getattr(self, "_handle_%s_response" % response.code.lower(), None)
+        if handler is not None:
+            handler(response)
+        else:
+            self._forward_to_dialog(response)
+
+    def _on_request_received(self, transaction_layer, request):
+        # 8.2 UAS Behavior (General User Agent Behavior)
+        if not self._inspect_request(request):
+            return
+
+        # 8.2.5 Processing the request:
+        handler = getattr(self, "_handle_%s_request" % request.code.lower(), None)
+        if handler is not None:
+            handler(request)
+        else:
+            self._forward_to_dialog(request)
+
+    def _inspect_request(self, request):
+        # 8.2.1 Method Inspection
+
+        # 8.2.2 Header Inspection
+        if not request.uri.startswith("sip:"):
+            logger.warning("Unsupported URI Scheme: %s" % request.uri)
+            self.answer(request, 416)
+            return False    # Unsupported URI Scheme
+        #if request.uri != self.self_uri:
+        #    logger.warning("Request URI is Not Found: %s" % request.uri)
+        #    self.answer(request, 404)
+        #    return False    # Not Found
+        if not request.to.tag and self._transaction_layer.is_merged_request(request):
+            logger.warning("Received merged request (Loop Detected)")
+            self.answer(request, 482)
+            return False    # Loop Detected
+        unsupported = request.required_extensions - self._supported_extensions
+        if not request.code in ("CANCEL", "ACK") and unsupported:
+            headers = {"Unsupported": " ".join(unsupported)}
+            logger.warning("Unsupported Extensions: %s" % unsupported)
+            self.answer(request, 420, headers)
+            return False    # Bad Extension
+
+        # 8.2.3 Content Processing
+        if (request.content_type and
+           request.content_type not in self._supported_content_types):
+            logger.warning("Unsupport Media Type: %s" % request.content_type)
+            self.answer(request, 415)
+            return False    # Unsupported Media Type
+
+        return True
+
+    # CANCEL Method ----------------------------------------------------------
+
+    def _create_cancel_request(self, original_request):
+        # 9.1 Client Behavior (Canceling a request)
+        request = SIPRequest("CANCEL", original_request.uri)
+        request.clone_headers("Route", original_request)
+        request.clone_headers("To", original_request)
+        request.clone_headers("From", original_request)
+        request.clone_headers("Call-ID", original_request)
+        cseq = original_request.cseq.number
+        request.add_header("CSeq", SIPCSeq(cseq, "CANCEL"))
+        return request
+
+    def _handle_cancel_request(self, request):
+        # 9.2 Server Behavior (Canceling a Request)
+        transaction = self._transaction_layer.find_canceled_transaction(request)
+        if transaction is None:
+            logger.warning("Transaction to cancel doesn't exist.")
+            return self.answer(request, 481) # Transaction Does Not Exist
+        if type(transaction) is SIPClientTransaction:
+            logger.warning("Can't remotely cancel client transaction.")
+            return self.answer(request, 500) # Server Error
+        if transaction.answered:
+            return
+
+        dialog = self._find_dialog(request)
+        if dialog is None:
+            self.emit("cancel-received", request)
+        else:
+            dialog.handle_request(request)
+        self.answer(request, 200)
+
+    # REGISTER Method --------------------------------------------------------
+
+    def _create_register_request(self, tag, call_id, cseq, timeout, auth):
+        # 10.2 Constructing the REGISTER Request
+        uri = "sip:%s" % self.self_uri('@')[1]
+        to =  "sip:%s" % self.self_uri
+        request = self.create_request("REGISTER", to, uri, tag, call_id, cseq)
+        request.add_header("Contact", "<sip:%s:%s;transport=%s>" %
+            (self._ip, self._port, self._transport_protocol))
+        request.add_header("Event", "registration")
+        request.add_header("Expires", timeout)
+        request.add_header("Authorization", "Basic %s" % auth)
+        return request
+
+    def _handle_register_response(self, response):
+        registration = self._registrations.get(call_id, None)
+        if registration:
+            registration.handle_response(response)
+
+    # OPTIONS Method ---------------------------------------------------------
+
+    def _create_options_request(self, uri):
+        # 11.1 Construction of OPTIONS Request
+        request = self.create_request("OPTIONS", uri)
+        request.add_header("Accept", "application/sdp")
+        return request
+
+    def _handle_options_request(self, request):
+        self.emit("options-received", request)
+
+    def _handle_options_response(self, response):
+        self.emit("options-answered", response)
+
+    # INVITE Method ----------------------------------------------------------
+
+    def _create_invite_request(self, call_id, uri, offer):
+        # 13.2.1 Creating the Initial INVITE
+        request = self.create_request("INVITE", uri, call_id=call_id)
+        request.add_header("User-Agent", USER_AGENT)
+        request.set_content(offer)
+        return request
+
+    def _handle_invite_response(self, response):
+        # 13.2.2 Processing INVITE Responses (UAC Processing)
+        dialog = self._find_dialog(response)
+        if dialog is None:
+            self.emit("invite-answered", response)
+        else:
+            dialog.handle_response(response)
+
+    def _handle_invite_request(self, request):
+        # 13.3.1 Processing of the INVITE (UAS Processing)
+        dialog = self._find_dialog(request)
+        if dialog is not None:
+            dialog.handle_request(request)
+        elif request.to.tag:
+            logger.warning("Dialog for target refresh (INVITE) doesn't exist.")
+            self.answer(request, 481) # Call Leg Does Not Exist
+        else:
+            self.emit("invite-received", request)
+
+    # BYE Method -------------------------------------------------------------
+
+    def _handle_bye_request(self, request):
+        # 15.1.2 UAS Behavior (Terminating a Session with a BYE Request)
+        dialog = self._find_dialog(request)
+        if dialog is None:
+            logger.warning("Dialog to terminate (BYE) doesn't exist.")
+            self.answer(request, 481) # Call Leg Does Not Exist
+        else:
+            dialog.handle_request(request)
+            self.answer(request, 200)
+
+    # Timeouts Callbacks -----------------------------------------------------
+
+    def on_cancel_timeout(self, canceled_request):
+        transaction = canceled_request.transaction
+        if transaction is not None:
+            logger.info("Canceled request not responsed, destroy transaction")
+            transaction.destroy()
+
+    # Errors Handling --------------------------------------------------------
+
+    def _on_transaction_layer_error(self, transaction_layer, transaction, error):
+        # 8.1.3.1 Transaction Layer Errors
+        if error is SIPTransactionError.TIMEOUT: # Treat as 408
+            logger.error("Received Timeout error from transaction layer")
+            fake_response = self.create_response(transaction.request, 408)
+            self._on_response_received(transaction_layer, fake_response)
+        elif error is SIPTransactionError.TRANSPORT_ERROR: # Treat as 503
+            logger.error("Received Transport error from transaction layer")
+            fake_response = self.create_response(transaction.request, 503)
+            self._on_response_received(transaction_layer, fake_response)
+
+    # Utils Functions --------------------------------------------------------
+
+    def _generate_tag(self):
+        # 19.3 Tags
+        return uuid.uuid4().get_hex()
+
+    def _generate_call_id(self):
+        # 20.8 Call-ID
+        return uuid.uuid4().get_hex()
+
+    def _generate_cseq(self):
+        return 1 #FIXME random cseq
--- papyon/sip/dialog.py
+++ papyon/sip/dialog.py
+# -*- coding: utf-8 -*-
+#
+# papyon - a python client library for Msn
+#
+# Copyright (C) 2010 Collabora Ltd.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+from papyon.sip.constants import SIPMode, T1, T2
+from papyon.sip.message import SIPRequest, SIPContact, SIPCSeq, SIPRoute
+from papyon.sip.sdp import SDPMessage
+from papyon.util.decorator import rw_property
+from papyon.util.timer import Timer
+
+import gobject
+import logging
+import re
+
+__all__ = ['SIPDialog']
+
+logger = logging.getLogger('papyon.sip.dialog')
+
+class SIPDialog(gobject.GObject, Timer):
+    """Represent a SIP dialog between two end points. A dialog must be
+       initiated by sending or receiving a response to an INVITE request.
+       It is disposed when receiving or sending a CANCEL or BYE request.
+
+       See details in section 12 of RFC 3261"""
+
+    __gsignals__ = {
+        'ringing': (
+            gobject.SIGNAL_RUN_FIRST,
+            gobject.TYPE_NONE,
+            ()),
+        'accepted': (
+            gobject.SIGNAL_RUN_FIRST,
+            gobject.TYPE_NONE,
+            (object,)),
+        'rejected': (
+            gobject.SIGNAL_RUN_FIRST,
+            gobject.TYPE_NONE,
+            (object,)),
+        'offer-received': (
+            gobject.SIGNAL_RUN_FIRST,
+            gobject.TYPE_NONE,
+            (object, object)),
+        'ended': (
+            gobject.SIGNAL_RUN_FIRST,
+            gobject.TYPE_NONE,
+            ()),
+        'disposed': (
+            gobject.SIGNAL_RUN_FIRST,
+            gobject.TYPE_NONE,
+            ())
+    }
+
+    @property
+    def answered(self):
+        return (self._state != "INVITED" or self._pending_accept)
+
+    @property
+    def call_id(self):
+        return self._call_id
+
+    @property
+    def local_tag(self):
+        return self._local_tag
+
+    @property
+    def remote_tag(self):
+        return self._remote_tag
+
+    def __init__(self, core, request, response, mode):
+        gobject.GObject.__init__(self)
+        Timer.__init__(self)
+        self._core = core
+        self._initial_request = request
+        self._last_request = request
+
+        self._incoming = False
+        self._pending_incoming_requests = []
+        self._pending_outgoing_requests = []
+        self._local_offer = None
+        self._pending_local_offer = False
+        self._pending_remote_offer = False
+        self._pending_accept = False
+
+        self._remote_target = None
+        self._route_set = []
+
+        self.__state = None
+        self._early = bool(response.status/100 == 1)
+
+        if mode is SIPMode.SERVER:
+            self._build_UAS_dialog(request, response)
+        elif mode is SIPMode.CLIENT:
+            self._build_UAC_dialog(request, response)
+
+    # Public API -------------------------------------------------------------
+
+    def ring(self):
+        # 13.3.1.1 Progress (Processing of the INVITE)
+        if self._state != "INVITED":
+            return
+        self.on_ringing_timeout()
+
+    def accept(self):
+        """Accept last request"""
+        if self._state not in ("INVITED", "REINVITED"):
+            return
+        if not self._local_offer or self._pending_remote_offer:
+            self._pending_accept = True
+            return
+        self._pending_accept = False
+        self._early = False
+        self.stop_timeout("ringing")
+        self.start_timeout("ack", int(64 * T1))
+        self.on_accept_timeout()
+
+    def reject(self, status):
+        """Reject last request"""
+        if self._state not in ("INVITED", "REINVITED"):
+            return
+        self._state = "CONFIRMED"
+        self._early = False
+        self._pending_accept = False
+        self.stop_timeout("ringing")
+        self.answer(self._last_request, status)
+
+        # when rejecting the initial request, the dialog is ended
+        if self._initial_request == self._last_request:
+            self._state = "ENDED"
+
+    def ack(self, response):
+        """Acknowledge response"""
+        if self._state not in ("INVITING", "REINVITING"):
+            return
+        self._state = "CONFIRMED"
+        request = self._create_ack_request(response)
+        self._core.send(request, use_transaction=False)
+
+    def reinvite(self):
+        if self._state == "ENDED":
+            return
+        if self._state != "CONFIRMED":
+            self._pending_local_offer = True
+            return
+        logger.info("Send re-invite to %s", self._remote_uri)
+        self._state = "REINVITING"
+        self._pending_local_offer = None
+        request = self._create_reinvite_request(self._local_offer)
+        self.send_request(request)
+
+    def end(self):
+        if self._state == "ENDED":
+            return
+        if self._state == "INVITED":
+            self.reject(603)
+        elif self._state == "INVITING":
+            self.cancel()
+        else:
+            bye = self._create_bye_request()
+            self.send_request(bye)
+        self._state = "ENDED"
+
+    def reset(self):
+        self.stop_all_timeout()
+        self._pending_incoming_requests = []
+        self._pending_local_offer = False
+        self._pending_remote_offer = False
+        self._pending_accept = False
+
+    def dispose(self):
+        if self._pending_outgoing_requests:
+            return
+        self._state = "DISPOSED"
+
+    # States Handling --------------------------------------------------------
+
+    @rw_property
+    def _state():
+        def fget(self):
+            return self.__state
+        def fset(self, value):
+            if value == self.__state:
+                return
+            old_state = self.__state
+            self.__state = value
+            self._on_state_changed(old_state)
+        return locals()
+
+    def _on_state_changed(self, old_state):
+        if self._state == "CONFIRMED":
+            self.stop_timeout("accept")
+            self.stop_timeout("ack")
+            if self._pending_local_offer and not self._incoming:
+                self.reinvite()
+        elif self._state == "ENDED":
+            for request in self._pending_incoming_requests:
+                logger.info("Respond to pending incoming %s request" % request.code)
+                self._core.answer(request, 487)
+            self.reset()
+            self.emit("ended")
+            self.dispose()
+        elif self._state == "DISPOSED":
+            self.emit("disposed")
+
+    # Offer/Answer Session Negotiation----------------------------------------
+
+    def update_local_offer(self, offer):
+        if self._local_offer == offer:
+            return
+        self._local_offer = offer
+        if self._pending_accept:
+            self.accept()
+        elif not self._incoming:
+            self.reinvite()
+
+    def accept_remote_offer(self):
+        if not self._pending_remote_offer:
+            logger.warning("No pending remote session offer to accept")
+            return True
+        self._pending_remote_offer = False
+        if self._state == "REINVITED":
+            self.accept()
+        return True
+
+    def reject_remote_offer(self):
+        if not self._pending_remote_offer:
+            logger.warning("No pending remote session offer to reject")
+            return False
+        self._pending_remote_offer = False
+        if self._state == "INVITED":
+            self.reject(488)
+        elif self._state == "INVITING":
+            self.end()
+        elif self._state == "REINVITED":
+            self.reject(403)
+        else:
+            self.end()
+        return True
+
+    def _handle_offer(self, message, initial):
+        if not message.body:
+            logger.info("Message doesn't contain a media session offer")
+            return True
+        try:
+            offer = SDPMessage(body=message.body)
+            self._pending_remote_offer = True
+            self.emit("offer-received", offer, initial)
+        except Exception, err:
+            logger.error("Malformed body in media session offer")
+            logger.exception(err)
+            return False
+        return True
+
+    # Creation of Dialogs ----------------------------------------------------
+
+    def _build_UAS_dialog(self, request, response):
+        # 12.1.1 UAS behavior (Creation of Dialog)
+        if self._early:
+            self._pending_incoming_requests.append(request)
+        if response.record_route:
+            self._route_set = response.record_route.route_set
+        self._incoming = True
+        self._local_cseq = None
+        self._remote_cseq = request.cseq.number
+        self._call_id = request.call_id
+        self._local_tag = response.to.tag
+        self._remote_tag = request.From.tag
+        self._local_uri = request.to.uri
+        self._remote_uri = request.From.uri
+
+    def _build_UAC_dialog(self, request, response):
+        # 12.1.2 UAC behavior (Creation of Dialog)
+        self._state = "INVITING"
+        self._incoming = False
+        self._pending_outgoing_requests.append(request)
+        if response.record_route:
+            self._route_set = response.record_route.route_set.reverse()
+        if response.contact:
+            self._remote_target = response.contact.uri
+        self._local_cseq = request.cseq.number
+        self._remote_cseq = None
+        self._call_id = response.call_id
+        self._local_tag = response.From.tag
+        self._remote_tag = response.to.tag
+        self._local_uri = response.From.uri
+        self._remote_uri = response.to.uri
+
+    def _create_request(self, code, cseq=None):
+        # 12.2.1 UAC Behavior (Requests within a Dialog)
+        route = self._route_set
+        if not route:
+            uri = self._remote_target
+        elif ';lr' in route[0]:
+            uri = self._remote_target
+        else:
+            uri = route.pop(0)
+            route.append(self._remote_target)
+
+        if not self._local_cseq:
+            self._local_cseq = 1 # FIXME 8.1.1.5
+        if not cseq:
+            self._local_cseq += 1
+            cseq = self._local_cseq
+
+        request = SIPRequest(code, self._remote_target)
+        if route:
+            request.add_header("Route", SIPRoute(route))
+        request.add_header("To", SIPContact(None, self._remote_uri, self._remote_tag))
+        request.add_header("From", SIPContact(None, self._local_uri, self._local_tag))
+        request.add_header("Call-ID", self._call_id)
+        request.add_header("CSeq", SIPCSeq(cseq, code))
+        return request
+
+    # Messages Handling ------------------------------------------------------
+
+    def send_request(self, request):
+        self._pending_outgoing_requests.append(request)
+        self._core.send(request)
+
+    def answer(self, request, status, extra_headers={}, content=None):
+        if request in self._pending_incoming_requests and status >= 200:
+            self._pending_incoming_requests.remove(request)
+        return self._core.answer(request, status, tag=self._local_tag,
+                extra_headers=extra_headers, content=content)
+
+    def handle_response(self, response):
+        # 12.2.1 UAC Behavior (Requests within a Dialog)
+        request = response.request
+        if request in self._pending_outgoing_requests and response.status >= 200:
+            self._pending_outgoing_requests.remove(request)
+        if self._state == "ENDED" and not self._pending_outgoing_requests:
+            self.dispose()
+            return False
+
+        if response.status in (408, 481) and not self._early:
+            self.end()
+            return False
+
+        # Target refresh
+        if response.code == "INVITE" and response.contact:
+            self._remote_target = response.contact.uri
+
+        # Method specific handler
+        handler = getattr(self, "_handle_%s_response" % response.code.lower(), None)
+        if handler is not None:
+            return handler(response)
+        return True
+
+    def handle_request(self, request):
+        # 12.2.2 UAS Behavior (Requests within a Dialog)
+        if self._state == "ENDED":
+            return
+
+        self._last_request = request
+        self._pending_incoming_requests.append(request)
+        if not self._remote_cseq:
+            self._remote_cseq = request.cseq.number
+        elif self._remote_cseq > request.cseq.number: # Out of order
+            logger.warning("CSeq is out of order (%i > %i)" %
+                    (self._remote_cseq, request.cseq.number))
+            self.answer(request, 500)
+            return False
+
+        # Target refresh
+        if request.code == "INVITE" and request.contact:
+            self._remote_target = request.contact.uri
+
+        # Method specific handler
+        handler = getattr(self, "_handle_%s_request" % request.code.lower(), None)
+        if handler is not None:
+            return handler(request)
+        return True
+
+    # INVITE Method ----------------------------------------------------------
+
+    def _handle_invite_response(self, response):
+        # 13.2.2 Processing INVITE Responses (UAC Processing)
+        if not self._early:
+            return self._handle_reinvite_response(response)
+
+        if response.status == 180:
+            self.emit("ringing")
+            return True
+
+        if response.status >= 200:
+            self._early = False
+
+        # 13.2.2.3 4xx, 5xx and 6xx Responses
+        if response.status >= 300:
+            self.emit("rejected", response)
+            self._state = "ENDED"
+            return False
+
+        # 13.2.2.4 2xx Responses
+        if response.status/100 == 2:
+            success = self._handle_offer(response, False)
+            self.ack(response)
+            if success:
+                logger.info("Call dialog is confirmed")
+                self.emit("accepted", response)
+                return True
+            else:
+                self.end()
+                return False
+
+    def _handle_invite_request(self, request):
+        # 13.3.1 Processing of the INVITE (UAS Processing)
+        if self._state is not None:
+            return self._handle_reinvite_request(request)
+        self._state = "INVITED"
+        if not self._handle_offer(request, True):
+            self.answer(request, 488)
+            self._state = "ENDED"
+            return False
+        return True
+
+    # re-INVITE Method -------------------------------------------------------
+
+    def _create_reinvite_request(self, offer):
+        # 14.1 UAC Behavior (Modifying an Existing Session)
+        request = self._create_request("INVITE")
+        request.add_header("Contact", SIPContact(None, self._local_uri, self._local_tag))
+        request.set_content(offer)
+        return request
+
+    def _handle_reinvite_response(self, response):
+        # 14.1 UAC Behavior (Modifying an Existing Session)
+        if response.status/100 == 2:
+            self._handle_offer(response, False)
+            self.ack(response)
+        #FIXME return
+
+    def _handle_reinvite_request(self, request):
+        # 14.2 UAS Behavior (Modifying an Existing Session)
+        self.answer(request, 100)
+        if self._state in ("INVITED", "REINVITED"):
+            logger.warning("Already invited, can't handle incoming INVITE")
+            self.answer(request, 500)
+            return False
+        elif self._state in ("INVITING", "REINVITING"):
+            logger.warning("Already inviting, can't handle incoming INVITE")
+            self.answer(request, 491)
+            return False
+        self._state = "REINVITED"
+        if not self._handle_offer(request, False):
+            self.answer(request, 488)
+            return False
+        return True
+
+    # ACK Method -------------------------------------------------------------
+
+    def _create_ack_request(self, response):
+        request = self._create_request("ACK", cseq=response.cseq.number)
+        return request
+
+    def _handle_ack_request(self, request):
+        self._pending_incoming_requests.remove(request) # No need to answer
+        self._state = "CONFIRMED"
+
+    # CANCEL Method ----------------------------------------------------------
+
+    def cancel(self):
+        if not self._early:
+            return
+        request = self._core.cancel(self._initial_request)
+        self._pending_outgoing_requests.append(request)
+        self._state = "ENDED"
+
+    def _handle_cancel_request(self, request):
+        self._pending_incoming_requests.remove(request) # UA core will respond
+        self._state = "ENDED"
+
+    def _handle_cancel_response(self, response):
+        self.dispose()
+
+    # BYE Method -------------------------------------------------------------
+
+    def _create_bye_request(self):
+        request = self._create_request("BYE")
+        return request
+
+    def _handle_bye_request(self, request):
+        self._pending_incoming_requests.remove(request) # UA core will respond
+        self._state = "ENDED"
+
+    def _handle_bye_response(self, response):
+        self.dispose()
+
+    # Timeout Callbacks ------------------------------------------------------
+
+    def on_ringing_timeout(self):
+        self.answer(self._initial_request, 180)
+        self.start_timeout("ringing", 60)
+
+    def on_accept_timeout(self, time=T1):
+        if self._state not in ("INVITED", "REINVITED"):
+            return
+        header = ("Contact", SIPContact(None, self._local_uri, self._local_tag))
+        self.answer(self._last_request, 200, content=self._local_offer,
+                extra_headers=[header])
+
+        # [...] an interval that starts at T1 seconds and doubles for each
+        # retransmission until it reaches T2 seconds (Section 13.3.1.4)
+        next_time = int(min(time * 2, T2))
+        self.start_timeout("accept", time, next_time)
+
+    def on_ack_timeout(self):
+        self._state = "CONFIRMED"
+        self.end()
--- papyon/sip/extensions
+++ papyon/sip/extensions
+(directory)
--- papyon/sip/extensions/__init__.py
+++ papyon/sip/extensions/__init__.py
+# -*- coding: utf-8 -*-
+#
+# papyon - a python client library for Msn
+#
+# Copyright (C) 2010 Collabora Ltd.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+from ms_conversation_id import MSConversationIDExtension
+from ms_proxy_replace import MSProxyReplaceExtension
+from ms_epid import MSEpidExtension
+from ms_mepid import MSMepidExtension
+from outbound import OutboundExtension
+
+def init_extensions(client, core):
+    extensions = []
+    extensions.append(MSConversationIDExtension(client, core))
+    extensions.append(MSProxyReplaceExtension(client, core))
+    extensions.append(MSMepidExtension(client, core))
+    extensions.append(MSEpidExtension(client, core))
+    extensions.append(OutboundExtension(client, core))
+    return extensions
--- papyon/sip/extensions/base.py
+++ papyon/sip/extensions/base.py
+# -*- coding: utf-8 -*-
+#
+# papyon - a python client library for Msn
+#
+# Copyright (C) 2010 Collabora Ltd.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+from papyon.sip.message import SIPResponse, SIPRequest
+
+class SIPExtension(object):
+
+    def __init__(self, client, core):
+        self._client = client
+        self._core = core
+
+    def extend(self, message):
+        if type(message) is SIPResponse:
+            self.extend_response(message)
+        elif type(message) is SIPRequest:
+            self.extend_request(message)
+
+    def extend_response(self, reponse):
+        pass
+
+    def extend_request(self, request):
+        pass
--- papyon/sip/extensions/ms_conversation_id.py
+++ papyon/sip/extensions/ms_conversation_id.py
+# -*- coding: utf-8 -*-
+#
+# papyon - a python client library for Msn
+#
+# Copyright (C) 2010 Collabora Ltd.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+from papyon.sip.extensions.base import SIPExtension
+
+class MSConversationIDExtension(SIPExtension):
+
+    def __init__(self, client, core):
+        SIPExtension.__init__(self, client, core)
+
+    def extend_request(self, message):
+        call = self._client.call_manager.find_call(message)
+        if call is not None and call.media_session.has_video:
+            conversation_id = 1
+        else:
+            conversation_id = 0
+        message.add_header("Ms-Conversation-ID", "f=%s" % conversation_id)
--- papyon/sip/extensions/ms_epid.py
+++ papyon/sip/extensions/ms_epid.py
+# -*- coding: utf-8 -*-
+#
+# papyon - a python client library for Msn
+#
+# Copyright (C) 2010 Collabora Ltd.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59contact.params["proxy"] = "replace" Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+from papyon.sip.extensions.base import SIPExtension
+
+import uuid
+
+class MSEpidExtension(SIPExtension):
+
+    def __init__(self, client, core):
+        SIPExtension.__init__(self, client, core)
+        self._epid = uuid.uuid4().get_hex()[:10]
+
+    def extend_request(self, message):
+        mepid = self._client.machine_guid.upper().replace("-", "")
+        mepid = mepid.replace("{", "")
+        mepid = mepid.replace("}", "")
+        if message.From and "epid" not in message.From.params:
+            message.From.params["epid"] = self._epid
--- papyon/sip/extensions/ms_keep_alive.py
+++ papyon/sip/extensions/ms_keep_alive.py
+# -*- coding: utf-8 -*-
+#
+# papyon - a python client library for Msn
+#
+# Copyright (C) 2010 Collabora Ltd.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+# MS-Keep-Alive: UAC;hop-hop=yes
+# http://msdn.microsoft.com/en-us/library/cc431508%28v=office.12%29.aspx
+
+from papyon.sip.extensions.base import SIPExtension
+
+class MSKeepAliveExtension(SIPExtension):
+
+    def __init__(self, client, core):
+        SIPExtension.__init__(self, client, core)
+
+    def extend_request(self, message):
+        message.add_header("MS-Keep-Alive", "UAC;hop-hop=yes")
--- papyon/sip/extensions/ms_mepid.py
+++ papyon/sip/extensions/ms_mepid.py
+# -*- coding: utf-8 -*-
+#
+# papyon - a python client library for Msn
+#
+# Copyright (C) 2010 Collabora Ltd.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59contact.params["proxy"] = "replace" Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+from papyon.sip.extensions.base import SIPExtension
+
+class MSMepidExtension(SIPExtension):
+
+    def __init__(self, client, core):
+        SIPExtension.__init__(self, client, core)
+
+    def extend_response(self, message):
+        mepid = self._client.machine_guid.upper().replace("-", "")
+        mepid = mepid.replace("{", "")
+        mepid = mepid.replace("}", "")
+        if message.To and ";mepid=" not in message.To.uri:
+            message.To.uri += ";mepid=" + mepid
+
+    def extend_request(self, message):
+        mepid = self._client.machine_guid.upper().replace("-", "")
+        mepid = mepid.replace("{", "")
+        mepid = mepid.replace("}", "")
+        if message.contact:
+            message.contact.uri += ";mepid=" + mepid
+        if message.From and ";mepid=" not in message.From.uri:
+            message.From.uri += ";mepid=" + mepid
+
+        #FIXME hack
+        if ";mepid=" in message.uri:
+            message.uri = message.uri.split(";")[0]
--- papyon/sip/extensions/ms_proxy_replace.py
+++ papyon/sip/extensions/ms_proxy_replace.py
+# -*- coding: utf-8 -*-
+#
+# papyon - a python client library for Msn
+#
+# Copyright (C) 2010 Collabora Ltd.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59contact.params["proxy"] = "replace" Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+from papyon.sip.extensions.base import SIPExtension
+
+class MSProxyReplaceExtension(SIPExtension):
+
+    def __init__(self, client, core):
+        SIPExtension.__init__(self, client, core)
+
+    def extend_request(self, message):
+        if message.contact:
+            message.contact.params["proxy"] = "replace"
--- papyon/sip/extensions/outbound.py
+++ papyon/sip/extensions/outbound.py
+# -*- coding: utf-8 -*-
+#
+# papyon - a python client library for Msn
+#
+# Copyright (C) 2010 Collabora Ltd.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+# http://tools.ietf.org/id/draft-ietf-sip-outbound-11.txt
+
+from papyon.sip.extensions.base import SIPExtension
+
+class OutboundExtension(SIPExtension):
+
+    def __init__(self, client, core):
+        SIPExtension.__init__(self, client, core)
+
+    def extend_request(self, message):
+        uuid = "0E04CFC3-0272-5A5C-B7C3-6FBE8DA71EAD"
+        if message.contact:
+            message.contact.params["+sip.instance"] = '"<urn:uuid:%s>"' % uuid
--- papyon/sip/ice.py
+++ papyon/sip/ice.py
@@ -153,13 +153,13 @@
         if draft is 19:
             cand.priority = int(cand.priority)
         if draft is 6:
+            cand.username = fix_b64_padding(cand.username)
+            cand.password = fix_b64_padding(cand.password)
             cand.priority = int(float(cand.priority) * 1000)
             if cand.priority < 0.5:
                 cand.type = "relay"
 
         cand.component_id = int(cand.component_id)
-        cand.username = fix_b64_padding(cand.username)
-        cand.password = fix_b64_padding(cand.password)
         cand.port = int(cand.port)
         if cand.base_port is not None:
             cand.base_port = int(cand.base_port)
--- papyon/sip/message.py
+++ papyon/sip/message.py
@@ -2,7 +2,7 @@
 #
 # papyon - a python client library for Msn
 #
-# Copyright (C) 2009 Collabora Ltd.
+# Copyright (C) 2009-2010 Collabora Ltd.
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -23,6 +23,8 @@
 
 import gobject
 import logging
+import re
+import weakref
 
 __all__ = ['SIPMessage', 'SIPRequest', 'SIPResponse', 'SIPMessageParser']
 
@@ -62,6 +64,10 @@
     def length(self):
         return int(self.get_header("Content-Length", 0))
 
+    @property
+    def required_extensions(self):
+        return set(self.get_header("Require", "").split())
+
     def normalize_name(self, name):
         name = name.lower()
         if len(name) is 1:
@@ -77,6 +83,18 @@
         else:
             self._headers.setdefault(name, []).append(value)
 
+    def parse_header(self, name, value):
+        name = self.normalize_name(name)
+        if name in ("contact", "from", "to"):
+            value = SIPContact.build(value)
+        elif name == "cseq":
+            value = SIPCSeq.build(value)
+        elif name in ("record-route", "route"):
+            value = SIPRoute.build(value)
+        elif name == "via":
+            value = SIPVia.build(value)
+        self.add_header(name, value)
+
     def set_header(self, name, value):
         name = self.normalize_name(name)
         self._headers[name] = [value]
@@ -91,6 +109,17 @@
             return value[0]
         return value
 
+    def match_header(self, name, other):
+        value = self.get_header(name)
+        other_value = other.get_header(name)
+        return value == other_value
+
+    def match_headers(self, names, other):
+        for name in names:
+            if not self.match_header(name, other):
+                return False
+        return True
+
     def clone_headers(self, name, other, othername=None):
         if othername is None:
             othername = name
@@ -98,22 +127,34 @@
         othername = self.normalize_name(othername)
         values = other.get_headers(othername)
         if values is not None:
-            self._headers[name] = values
-
-    def set_content(self, content, type=None):
-        if type:
-            self.set_header("Content-Type", type)
-        self.set_header("Content-Length", len(content))
-        self._body = content
+            cloned_values = []
+            for value in values:
+                if hasattr(value, "clone"):
+                    cloned_value = value.clone()
+                else:
+                    cloned_value = value
+                cloned_values.append(cloned_value)
+            self._headers[name] = cloned_values
+
+    def set_content(self, content):
+        if hasattr(content, "type"):
+            self.set_header("Content-Type", content.type)
+        body = str(content)
+        self.set_header("Content-Length", len(body))
+        self._body = body
 
     def get_header_line(self):
         raise NotImplementedError
 
+    def __getattr__(self, name):
+        name = name.replace('_', '-')
+        return self.get_header(name)
+
     def __str__(self):
         s = [self.get_header_line()]
         for k, v in self._headers.items():
             for value in v:
-                s.append("%s: %s" % (k, value))
+                s.append("%s: %s" % (k, str(value)))
         s.append("")
         s.append(self._body)
         return "\r\n".join(s)
@@ -123,39 +164,56 @@
 
     def __init__(self, code, uri):
         SIPMessage.__init__(self)
-        self._code = code
-        self._uri = uri
-
-    @property
-    def code(self):
-        return self._code
+        self.code = code
+        self.uri = uri
+        self._transaction_ref = None
 
-    @property
-    def uri(self):
-        return self._uri
+    @rw_property
+    def transaction():
+        def fget(self):
+            if self._transaction_ref is None:
+                return None
+            return self._transaction_ref()
+        def fset(self, value):
+            if value is None:
+                self._transaction_ref = None
+            else:
+                self._transaction_ref = weakref.ref(value)
+        return locals()
 
     def get_header_line(self):
-        return "%s %s SIP/2.0" % (self._code, self._uri)
+        return "%s %s SIP/2.0" % (self.code, self.uri)
 
     def __repr__(self):
-        return "<SIP Request %d:%s %s>" % (id(self), self._code, self._uri)
+        return "<SIP Request %d:%s %s>" % (id(self), self.code, self.uri)
 
 
 class SIPResponse(SIPMessage):
 
     def __init__(self, status, reason=None):
         SIPMessage.__init__(self)
+        self._request_ref = None
         self._status = status
         if not reason:
             reason = RESPONSE_CODES[status]
         self._reason = reason
 
+    @rw_property
+    def request():
+        def fget(self):
+            if self._request_ref is None:
+                return None
+            return self._request_ref()
+        def fset(self, value):
+            if value is None:
+                self._request_ref = None
+            else:
+                self._request_ref = weakref.ref(value)
+        return locals()
+
     @property
     def code(self):
-        cseq = self.get_header("CSeq")
-        if not cseq:
-            return None
-        return cseq.split()[1]
+        return self.cseq.method
 
     @property
     def status(self):
@@ -198,7 +256,8 @@
             while not finished:
                 finished = self.parse_buffer()
         except Exception, err:
-            logger.error("Error while parsing received message: %s", err)
+            logger.error("Error while parsing received message")
+            logger.exception(err)
             self.reset()
 
     def parse_buffer(self):
@@ -222,7 +281,7 @@
                 self._state = "body"
             else:
                 name, value = line.split(":", 1)
-                self._message.add_header(name, value.strip())
+                self._message.parse_header(name, value.strip())
 
         if self._state == "body":
             missing = self._message.length - len(self._message.body)
@@ -259,3 +318,117 @@
             ret = self._buffer[0:count]
             self._buffer = self._buffer[count:]
         return ret
+
+
+### SIP Message Headers
+
+class SIPContact(object):
+    def __init__(self, name, uri, tag=None, params=None):
+        self.name = name
+        self.uri = uri
+        self.tag = tag
+        if params is None:
+            params = {}
+        self.params = params
+
+    @staticmethod
+    def build(line):
+        contact_spec = "(\"(?P<name>[^\"]*)\")? *\<(?P<uri>[^>]*)\>(;tag=(?P<tag>[^;>]*))?(?P<params>;[^;>]*)*"
+        m = re.match(contact_spec, line)
+        if not m:
+            return None
+        params = {}
+        if m.group("params"):
+            for param in m.group("params").split(";"):
+                if "=" in param:
+                    key, value = param.split("=")
+                    params[key] = value
+        return SIPContact(m.group("name"), m.group("uri"), m.group("tag"), params)
+
+    def clone(self):
+        params = self.params.copy()
+        return SIPContact(self.name, self.uri, self.tag, params)
+
+    def __str__(self):
+        line = ""
+        if self.name:
+            line += "\"%s\" " % self.name
+        line += "<%s>" % str(self.uri)
+        if self.tag:
+            line += ";tag=%s" % self.tag
+        for key,value in self.params.items():
+            line += ";%s=%s" % (key, str(value))
+        return line
+
+    def __eq__(self, other):
+        return (self.uri == other.uri and
+                self.tag == other.tag and
+                self.params == other.params)
+
+class SIPCSeq(object):
+    def __init__(self, number, method):
+        self.number = number
+        self.method = method
+
+    @staticmethod
+    def build(line):
+        number, method = line.split(" ", 2)
+        return SIPCSeq(int(number), method)
+
+    def clone(self):
+        return SIPCSeq(self.number, self.method)
+
+    def __str__(self):
+        return "%i %s" % (self.number, self.method)
+
+    def __eq__(self, other):
+        return (self.number == other.number and self.method == other.method)
+
+class SIPRoute(object):
+    def __init__(self, route_set):
+        self.route_set = route_set
+
+    @staticmethod
+    def build(line):
+        items = line.split(",")
+        route_set = map(lambda item: re.search("<([^>]*)>", item).group(1), items)
+        return SIPRoute(route_set)
+
+    def clone(self):
+        return SIPRoute(self.route_set[:])
+
+    def __str__(self):
+        return ",".join(map(lambda item: "<%s>" % item, self.route_set))
+
+    def __eq__(self, other):
+        return (self.route_set == other.route_set)
+
+class SIPVia(object):
+    def __init__(self, protocol=None, ip=None, port=None, branch=None):
+        self.protocol = protocol.upper()
+        self.ip = ip
+        self.port = port
+        self.branch = branch
+
+    @staticmethod
+    def build(line):
+        via_spec = "SIP/2.0/(?P<protocol>[a-zA-Z]*) (?P<ip>[0-9\.]*):(?P<port>[0-9]*)(;branch=(?P<branch>[^;>]*))?"
+        m = re.match(via_spec, line)
+        if not m:
+            return None
+        return SIPVia(m.group("protocol"), m.group("ip"),
+                int(m.group("port")), m.group("branch"))
+
+    def clone(self):
+        return SIPVia(self.protocol, self.ip, self.port, self.branch)
+
+    def __str__(self):
+        line = "SIP/2.0/%s %s:%d" % (self.protocol, self.ip, self.port)
+        if self.branch:
+            line += ";branch=%s" % self.branch
+        return line
+
+    def __eq__(self, other):
+        return (self.protocol == other.protocol and
+                self.ip == other.ip and self.port == self.port and
+                self.branch == other.branch)
--- papyon/sip/registration.py
+++ papyon/sip/registration.py
+# -*- coding: utf-8 -*-
+#
+# papyon - a python client library for Msn
+#
+# Copyright (C) 2010 Collabora Ltd.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+from papyon.util import Timer
+
+import gobject
+import logger
+
+__all__ = ['SIPRegistration']
+
+logger = logging.getLogger('papyon.sip.registration')
+
+class SIPRegistration(gobject.GObject):
+
+    __gsignals__ = {
+        'registered': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ([])),
+        'unregistered': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ([])),
+        'failed': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ([]))
+    }
+
+    def __init__(self, client, core):
+        gobject.GObject.__init__(self)
+        self._client = client
+        self._core = core
+        self._state = "NEW"
+        self._sso = client._sso
+        self._src = None
+        self._request = None
+        self._pending_unregister = False
+        self._tokens = {}
+
+        self._tag = None
+        self._call_id = None
+        self._cseq = None
+
+    @property
+    def registered(self):
+        return (self._state == "REGISTERED")
+
+    def register(self):
+        if self._state in ("REGISTERING", "REGISTERED", "CANCELLED"):
+            return
+        self._state = "REGISTERING"
+        self._do_register(None, None)
+
+    def unregister(self):
+        if self._state in ("NEW", "UNREGISTERING", "UNREGISTERED", "CANCELLED"):
+            return
+        elif self._state == "REGISTERING":
+            if self._request is None:
+                self._state = "CANCELLED"
+                self.emit("unregistered")
+            else:
+                self._pending_unregister = True
+            return
+
+        self._state = "UNREGISTERING"
+        self._pending_unregister = False
+        if self._src is not None:
+            gobject.source_remove(self._src)
+        self._src = None
+        self._do_unregister(None, None)
+
+    def _send_register(self, timeout, auth):
+        self._request = self._core.register(self._tag, self._call_id,
+                self._cseq, timeout, auth)
+        self._tag = self._request.From.tag
+        self._call_id = self._request.call_id
+        self._cseq = self._request.cseq.number + 1
+
+    @RequireSecurityTokens(LiveService.MESSENGER_SECURE)
+    def _do_register(self, callback, errback):
+        # Check if state changed while requesting security token
+        if self._state != "REGISTERING":
+            return
+        auth = "msmsgs:RPS_%s" % self._tokens[LiveService.MESSENGER_SECURE]
+        auth = base64.b64encode(auth).replace("\n", "")
+        self._send_register(900, auth)
+
+    @RequireSecurityTokens(LiveService.MESSENGER_SECURE)
+    def _do_unregister(self, callback, errback):
+        auth = "%s:%s" % (self._account, self._tokens[LiveService.MESSENGER_SECURE])
+        auth = base64.encodestring(auth).replace("\n", "")
+        self._send_register(0, auth)
+
+    def _on_expire(self):
+        self.register()
+        return False
+
+    def handle_response(self, response):
+        if self._state == "UNREGISTERING":
+            self._state = "UNREGISTERED"
+            self.emit("unregistered")
+        elif self._state != "REGISTERING":
+            return # strange !?
+        elif response.status is 200:
+            self._state = "REGISTERED"
+            self.emit("registered")
+            timeout = int(response.get_header("Expires", 30))
+            self._src = gobject.timeout_add_seconds(timeout, self._on_expire)
+            if self._pending_unregister:
+                self.unregister()
+        else:
+            self._state = "UNREGISTERED"
+            self.emit("failed")
--- papyon/sip/sdp.py
+++ papyon/sip/sdp.py
@@ -36,6 +36,10 @@
         MediaSessionMessage.__init__(self, session, body)
 
     @property
+    def type(self):
+        return "application/sdp"
+
+    @property
     def ip(self):
         if self._ip == "" and len(self._descriptions) > 0:
             return self._descriptions[0].ip
--- papyon/sip/transaction.py
+++ papyon/sip/transaction.py
+# -*- coding: utf-8 -*-
+#
+# papyon - a python client library for Msn
+#
+# Copyright (C) 2010 Collabora Ltd.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+from papyon.sip.constants import SIPTransactionError, TIMER_B, TIMER_F, TIMER_H
+from papyon.sip.message import SIPResponse, SIPRequest, SIPCSeq
+from papyon.util.decorator import rw_property
+from papyon.util.timer import Timer
+
+import gobject
+import logging
+
+logger = logging.getLogger('papyon.sip.transaction')
+
+class SIPTransactionLayer(gobject.GObject):
+    """ This class represents the SIP transaction layer as described in
+        section 17 of RFC 2361.
+
+        The transaction layer handles application-layer retransmissions,
+        matching of responses to requests, and application-layer timeouts.
+        Any task that a user agent client (UAC) accomplishes takes place
+        using a series of transactions.
+
+        TODO: Use branch parameter in Via to identify transactions"""
+
+    __gsignals__ = {
+        'request-received' : (gobject.SIGNAL_RUN_FIRST,
+            gobject.TYPE_NONE,
+            (object,)),
+        'response-received' : (gobject.SIGNAL_RUN_FIRST,
+            gobject.TYPE_NONE,
+            (object,)),
+        'error' : (gobject.SIGNAL_RUN_FIRST,
+            gobject.TYPE_NONE,
+            (object, object))
+    }
+
+    def __init__(self, transport):
+        gobject.GObject.__init__(self)
+        self._transport = transport
+        self._transport.connect("message-received", self._on_message_received)
+        self._transactions = {} # transaction => event handles
+
+    def is_merged_request(self, request):
+        for transaction in self._transactions:
+            if transaction is request.transaction: continue
+            if request.match_headers(("From", "Call-Id", "CSeq"),
+                    transaction.request):
+                return True
+        return False
+
+    def find_canceled_transaction(self, cancel):
+        return self._find_transaction(cancel, False)
+
+    def send(self, message):
+        transaction = None
+        if type(message) is SIPRequest:
+            transaction = SIPClientTransaction(self._transport)
+            self._add_transaction(transaction)
+        elif type(message) is SIPResponse:
+            transaction = message.request.transaction
+
+        if transaction is not None:
+            transaction.send(message)
+
+    def _add_transaction(self, transaction):
+        handles = []
+        handles.append(transaction.connect("request-received",
+            self._on_request_received))
+        handles.append(transaction.connect("response-received",
+            self._on_response_received))
+        handles.append(transaction.connect("terminated",
+            self._on_transaction_terminated))
+        handles.append(transaction.connect("error",
+            self._on_transaction_error))
+        self._transactions[transaction] = handles
+
+    def _del_transaction(self, transaction):
+        handles = self._transactions.pop(transaction)
+        for handle in handles:
+            transaction.disconnect(handle)
+
+    def _find_transaction(self, msg, match_method=True):
+        for transaction in self._transactions:
+            if match_message_to_transaction(msg, transaction, match_method):
+                return transaction
+        return None
+
+    def _on_message_received(self, transport, msg):
+        transaction = self._find_transaction(msg)
+        if type(msg) is SIPResponse:
+            if transaction is not None:
+                transaction._on_response_received(msg)
+            else:
+                logger.info("Can't find matching transaction for response")
+                self.emit("response-received", msg)
+        elif type(msg) is SIPRequest:
+            if transaction is None and msg.code != "ACK":
+                transaction = SIPServerTransaction(self._transport, msg)
+                self._add_transaction(transaction)
+            if transaction is not None:
+                transaction._on_request_received(msg)
+            else:
+                self.emit("request-received", msg)
+
+    def _on_request_received(self, transaction, request):
+        self.emit("request-received", request)
+
+    def _on_response_received(self, transaction, response):
+        self.emit("response-received", response)
+
+    def _on_transaction_terminated(self, transaction):
+        self._del_transaction(transaction)
+
+    def _on_transaction_error(self, transaction, error):
+        self.emit("error", transaction, error)
+        self._del_transaction(transaction)
+
+
+class SIPTransaction(gobject.GObject, Timer):
+    """ A SIP transaction consists of a single request and any responses to
+        that request, which include zero or more provisional responses and
+        one or more final responses. In the case of an INVITE transaction, the
+        transaction also includes the ACK only if the final response was not
+        a 2xx response."""
+
+    __gsignals__ = {
+        'request-received' : (gobject.SIGNAL_RUN_FIRST,
+            gobject.TYPE_NONE,
+            (object,)),
+        'response-received' : (gobject.SIGNAL_RUN_FIRST,
+            gobject.TYPE_NONE,
+            (object,)),
+        'terminated' : (gobject.SIGNAL_RUN_FIRST,
+            gobject.TYPE_NONE,
+            ()),
+        'error' : (gobject.SIGNAL_RUN_FIRST,
+            gobject.TYPE_NONE,
+            (object,)),
+    }
+
+    def __init__(self, transport):
+        gobject.GObject.__init__(self)
+        Timer.__init__(self)
+        self._transport = transport
+        self._branch = None
+        self._method = None
+        self._request = None
+        self.__state = None
+
+    @property
+    def answered(self):
+        """Final response received or not"""
+        return bool(self.__state in ("COMPLETED", "TERMINATED"))
+
+    @rw_property
+    def _state():
+        def fget(self):
+            return self.__state
+        def fset(self, value):
+            if self.__state != value:
+                old_state = self.__state
+                self.__state = value
+                self._on_state_changed(old_state)
+        return locals()
+
+    @property
+    def request(self):
+        return self._request
+
+    @property
+    def is_invite(self):
+        return self.request.code == "INVITE"
+
+    def destroy(self):
+        self._state = "TERMINATED"
+
+    def _send(self, message):
+        self._transport.send(message) #FIXME add errcb
+
+    def _on_state_changed(self, old_state):
+        raise NotImplementedError
+
+    def _on_transport_error(self):
+        self.emit("error", SIPTransactionError.TRANSPORT_ERROR)
+        self._state = "TERMINATED"
+
+
+# Client Transaction ---------------------------------------------------------
+
+class SIPClientTransaction(SIPTransaction):
+    """ Client component of the transaction layer used to send request.
+        It's basically a state machine, working differently wether the request
+        is an INVITE one or not.
+
+        See section 17.1 of RFC 2361 for details.
+
+        Note: We assume that the transport layer is reliable. """
+
+    def __init__(self, transport):
+        SIPTransaction.__init__(self, transport)
+
+    def send(self, message):
+        if type(message) is SIPResponse:
+            logger.error("Tried to send a response from client transaction")
+            self._state = "TERMINATED"
+            return False
+        if self._state is not None:
+            logger.error("Can't send more thant one request per transaction")
+            return False
+
+        self._state = "TRYING"
+        self._request = message
+        self._request.transaction = self
+        self._send(message)
+        if self.is_invite:
+            self.start_timeout("timerB", TIMER_B)
+        else:
+            self.start_timeout("timerF", TIMER_F)
+        return True
+
+    def _ack(self, response):
+        if not self.is_invite:
+            return # no need for ACK in non-INVITE transactions
+        logger.info("Acking non-2xx INVITE response")
+        request = self._create_ack_request(response)
+        self._send(request)
+
+    def _create_ack_request(self, response):
+        # 17.1.1.3 Construction of the ACK Request
+        request = SIPRequest("ACK", self._request.uri)
+        request.clone_headers("Route", self._request)
+        request.clone_headers("To", response)
+        request.clone_headers("From", self._request)
+        request.clone_headers("Call-ID", self._request)
+        request.clone_headers("Via", self._request) #FIXME single via
+        request.add_header("CSeq", SIPCSeq(self._request.cseq.number, "ACK"))
+        return request
+
+    def _on_request_received(self, request):
+        logger.error("Received a request in client transaction")
+
+    def _on_response_received(self, response):
+        response.request = self.request
+        if response.status/100 == 1: # provisional response
+            if self._state == "TRYING":
+                self._state = "PROCEEDING"
+            self.emit("response-received", response)
+        elif response.status/100 == 2: # OK final response
+            if self._state in ("TRYING", "PROCEEDING"):
+                self.emit("response-received", response)
+                self._state = "TERMINATED"
+        elif response.status >= 300: # other final response
+            if self._state in ("TRYING", "PROCEEDING"):
+                self.emit("response-received", response)
+            self._ack(response)
+            self._state = "COMPLETED"
+
+    def _on_state_changed(self, old_state):
+        if self._state == "COMPLETED":
+            self._state = "TERMINATED" # skip COMPLETED state
+        elif self._state == "TERMINATED":
+            self.stop_all_timeout()
+            self.emit("terminated")
+
+    def on_timerB_timeout(self):
+        if self._state == "TRYING":
+            self.emit("error", SIPTransactionError.TIMEOUT)
+            self._state = "TERMINATED"
+
+    def on_timerF_timeout(self):
+        if self._state in ("TRYING", "PROCEEDING"):
+            self.emit("error", SIPTransactionError.TIMEOUT)
+            self._state = "TERMINATED"
+
+    def _on_transport_error(self):
+        self.emit("error", SIPTransactionError.TRANSPORT_ERROR)
+        self._state = "TERMINATED"
+
+
+# Server Transaction ---------------------------------------------------------
+
+class SIPServerTransaction(SIPTransaction):
+    """ Server component of the transaction layer created when receiving requests.
+        It's basically a state machine, working differently wether the request
+        is an INVITE one or not.
+
+        See section 17.2 of RFC 2361 for details.
+
+        Note: We assume that the transport layer is reliable. """
+
+    def __init__(self, transport, request):
+        SIPTransaction.__init__(self, transport)
+        self._request = request
+        self._last_response = None
+        request.transaction = self
+
+        if self.is_invite:
+            self._state = "PROCEEDING"
+        else:
+            self._state = "TRYING"
+
+
+    def send(self, message):
+        if type(message) is SIPRequest:
+            logger.error("Tried to send a request from server transaction")
+            return False
+
+        if self._state not in ("TRYING", "PROCEEDING"):
+            return False
+
+        self._last_response = message
+        if message.status/100 == 1:
+            self._send(message)
+            if self._state == "TRYING":
+                self._state = "PROCEEDING"
+        elif message.status/100 >= 2:
+            self._send(message)
+            if self.is_invite and message.status/100 == 2:
+                self._state = "TERMINATED"
+            else:
+                self._state = "COMPLETED"
+        return True
+
+    def _on_request_received(self, request):
+        # receive ACK for response
+        if request.code == "ACK":
+            self._on_ack_received(request)
+        # request retransmission => send last response received from TU
+        elif self._last_response is not None and \
+             self._state in ("PROCEEDING", "COMPLETED"):
+            self.info("Request retransmission detected => send last response")
+            self._send(self._last_response)
+        else:
+            self.emit("request-received", request)
+
+    def _on_ack_received(self, ack):
+        if self._state != "COMPLETED":
+            return
+        self._state = "CONFIRMED"
+
+    def _on_response_received(self, response):
+        logger.error("Received a response in server transaction")
+
+    def _on_state_changed(self, old_state):
+        if self._state == "COMPLETED":
+            if not self.is_invite:
+                self._state = "TERMINATED" # skip CONFIRMED state
+            else:
+                self.start_timeout("timerH", TIMER_H)
+        elif self._state == "CONFIRMED":
+            self.stop_timeout("timerH")
+            self._state = "TERMINATED" # skip CONFIRMED state
+        elif self._state == "TERMINATED":
+            self.stop_all_timeout()
+            self.emit("terminated")
+
+    def on_timerH_timeout(self):
+        self._state = "TERMINATED"
+        self.emit("error", SIPTransactionError.TIMEOUT)
+
+
+
+# Transaction Matching Util Functions ----------------------------------------
+
+def match_response_to_transaction(response, transaction, match_method=True):
+    """ RFC3261: 17.1.3 Matching Responses to Client Transactions """
+    # Reponse To header might have a mepid and tag added
+    if not response.To.uri.startswith(transaction.request.To.uri):
+        return False
+    headers = ("Call-ID", "CSeq", "From")
+    return response.match_headers(headers, transaction.request)
+
+def match_request_to_transaction(request, transaction, match_method=True):
+    """ RFC3261: 17.2.3 Matching Requests to Server Transactions
+
+        INVITE:         Request-URI, Call-Id, top Via, Cseq, From tag, To tag
+        ACK:            Request-URI, Call-Id, top Via, Cseq number, From tag
+        Other methods:  Request-URI, Call-Id, top Via, Cseq, From tag, To tag
+    """
+    if (request.uri != transaction.request.uri or
+       not request.match_headers(("Call-Id", "Via"), transaction.request) or
+       request.From.tag != transaction.request.From.tag):
+        return False
+
+    if request.code == "ACK" or not match_method:
+        if request.cseq.number != transaction.request.cseq.number:
+            return False
+    elif not request.match_header("CSeq", transaction.request):
+        return False
+
+    if request.code != "ACK" and request.to.tag != transaction.request.to.tag:
+        return False
+
+    return True
+
+def match_message_to_transaction(msg, transaction, match_method=True):
+    if type(msg) is SIPResponse:
+        return match_response_to_transaction(msg, transaction)
+    elif type(msg) is SIPRequest:
+        return match_request_to_transaction(msg, transaction, match_method)
+    return False
+
--- papyon/sip/transport.py
+++ papyon/sip/transport.py
@@ -2,7 +2,7 @@
 #
 # papyon - a python client library for Msn
 #
-# Copyright (C) 2009 Collabora Ltd.
+# Copyright (C) 2009-2010 Collabora Ltd.
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -21,7 +21,7 @@
 from papyon.gnet.constants import *
 from papyon.gnet.io import *
 from papyon.msnp.constants import *
-from papyon.sip.message import SIPMessageParser
+from papyon.sip.message import SIPResponse, SIPRequest, SIPMessageParser, SIPVia
 
 import base64
 import gobject
@@ -39,6 +39,18 @@
             ([object]))
     }
 
+    @property
+    def protocol(self):
+        raise NotImplementedError
+
+    @property
+    def ip(self):
+        raise NotImplementedError
+
+    @property
+    def port(self):
+        raise NotImplementedError
+
     def __init__(self):
         gobject.GObject.__init__(self)
         self._parser = SIPMessageParser()
@@ -48,6 +60,12 @@
         self.emit("message-received", message)
 
     def send(self, message):
+        if type(message) is SIPRequest:
+            message.add_header("Via", SIPVia(self.protocol, self.ip, self.port))
+            message.add_header("Max-Forwards", 70)
+        self._send(message)
+
+    def _send(self, message):
         raise NotImplementedError
 
     def log_message(self, prefix, message):
@@ -55,81 +73,9 @@
             logger.debug(prefix + " " + line)
 
 
-class SIPTransport(SIPBaseTransport):
-    """Default transport on older MSNP versions. The messages are sent over a
-       typical SSL TCP connection."""
-
-    def __init__(self, host, port):
-        SIPBaseTransport.__init__(self)
-        self._client = SSLTCPClient(host, port)
-        self._client.connect("received", self.on_received)
-        self._client.connect("notify::status", self.on_status_changed)
-        self._alive_src = None
-        self._closing = False
-        self._msg_queue = []
-
-    @property
-    def protocol(self):
-        return "tls"
-
-    def send(self, message):
-        data = str(message)
-        if self._client.status == IoStatus.OPEN:
-            self.log_message(">>", data)
-            self._send(data)
-        else:
-            self._msg_queue.append(message)
-            self._open()
-
-    def _open(self):
-        if self._client.status == IoStatus.OPEN:
-            return
-        self._close()
-        self._closing = False
-        self._client.open()
-        self._start_keep_alive()
-
-    def _close(self):
-        if self._client.status == IoStatus.CLOSED:
-            return
-        self._stop_keep_alive()
-        self._closing = True
-        self._client.close()
-
-    def _send(self, data):
-        self._client.send(data)
-
-    def _start_keep_alive(self):
-        self._alive_src = gobject.timeout_add(5000, self._on_keep_alive)
-
-    def _stop_keep_alive(self):
-        if self._alive_src is not None:
-            gobject.source_remove(self._alive_src)
-            self._alive_src = None
-
-    def _on_keep_alive(self):
-        if self._client.status == IoStatus.OPEN:
-            self._send("\r\n\r\n\r\n\r\n")
-            return True
-        else:
-            return False
-
-    def on_received(self, client, chunk, len):
-        self.log_message("<<", chunk)
-        self._parser.append(chunk)
-
-    def on_status_changed(self, client, param):
-        if self._client.status == IoStatus.OPEN:
-            while self._msg_queue:
-                self.send(self._msg_queue.pop(0))
-        elif self._client.status == IoStatus.CLOSED:
-            if not self._closing:
-                self._open()
-
-
 class SIPTunneledTransport(SIPBaseTransport):
     """Default SIP transport with newer MSNP versions (>= 18). The messages
-       are base64 encoded and sent to the notication server using a UBX
+       are base64 encoded and sent to the notication server using a UBN
        command."""
 
     def __init__(self, protocol):
@@ -142,21 +88,38 @@
     def protocol(self):
         return "tcp"
 
-    def send(self, message):
-        call_id = message.call.id
-        contact = message.call.contact
+    @property
+    def ip(self):
+        return "127.0.0.1"
+
+    @property
+    def port(self):
+        return 50390
+
+    def _send(self, message):
+        call_id = message.call_id
+        if type(message) is SIPResponse:
+            contact = message.From.uri.replace("sip:", "")
+        else:
+            contact = message.To.uri.replace("sip:", "")
+        #FIXME hack
+        guid = None
+        if ";mepid=" in contact:
+            contact, guid = contact.split(";mepid=")
+            guid = "%s-%s-%s-%s-%s" % (guid[0:8], guid[8:12], guid[12:16],
+                    guid[16:20], guid[20:32])
+            guid = guid.lower()
         self.log_message(">>", str(message))
         data = base64.b64encode(str(message))
         data = '<sip e="base64" fid="1" i="%s"><msg>%s</msg></sip>' % \
                 (call_id, data)
         data = data.replace("\r\n", "\n").replace("\n", "\r\n")
-        self._protocol.send_user_notification(data, contact,
+        self._protocol.send_user_notification(data, contact, guid,
                 UserNotificationTypes.TUNNELED_SIP)
 
-    def on_notification_received(self, protocol, type, notification):
+    def on_notification_received(self, protocol, peer, peer_guid, type, message):
         if type is not UserNotificationTypes.TUNNELED_SIP:
             return
-        message = notification.payload
         try:
             doc = xml.dom.minidom.parseString(message)
             sip = doc.firstChild
--- papyon/switchboard_manager.py
+++ papyon/switchboard_manager.py
@@ -28,6 +28,7 @@
 import weakref
 
 import papyon.msnp as msnp
+from papyon.profile import Presence
 from papyon.transport import ServerType
 from papyon.util.weak import WeakSet
 from papyon.event import ConversationErrorType, ContactInviteError, MessageError
@@ -36,23 +37,25 @@
 
 logger = logging.getLogger('papyon.protocol.switchboard_manager')
 
-class SwitchboardClient(object):
-    def __init__(self, client, contacts, priority=99):
+class SwitchboardHandler(object):
+    def __init__(self, client, switchboard, contacts, priority=99):
         self._client = client
         self._switchboard_manager = weakref.proxy(self._client._switchboard_manager)
         self.__switchboard = None
         self._switchboard_requested = False
         self._switchboard_priority = priority
 
-        self._pending_invites = set(contacts)
+        self.participants = set()
+        self._pending_invites = set()
         self._pending_messages = []
-        self._delivery_callbacks = {}
+        self._pending_handles = {}
+        self._delivery_callbacks = {} # transaction_id => (callback, errback)
 
-        if self._client.protocol_version >= 16:
-            self._pending_invites.add(self._client.profile)
+        for contact in contacts:
+            self.__add_pending(contact)
 
-        self.participants = set()
-        self._process_pending_queues()
+        if switchboard is not None:
+            self._switchboard = switchboard
 
     @staticmethod
     def _can_handle_message(message, switchboard_client=None):
@@ -72,6 +75,8 @@
 
         self.switchboard.connect("notify::inviting",
                 lambda sb, pspec: self.__on_user_inviting_changed())
+        self.switchboard.connect("notify::state",
+                lambda sb, pspec: self.__on_switchboard_state_changed())
         self.switchboard.connect("user-joined",
                 lambda sb, contact: self.__on_user_joined(contact))
         self.switchboard.connect("user-left",
@@ -82,7 +87,8 @@
                 lambda sb, trid: self.__on_message_delivered(trid))
         self.switchboard.connect("message-undelivered",
                 lambda sb, trid: self.__on_message_undelivered(trid))
-        logger.info("New switchboard attached")
+        logger.info("Handler %s attached to switchboard %s" %
+                (repr(self), switchboard.session_id))
         def process_pending_queues():
             self._process_pending_queues()
             return False
@@ -93,7 +99,7 @@
 
     # protected
     def _send_message(self, content_type, body, headers={},
-            ack=msnp.MessageAcknowledgement.HALF, callback=None, cb_args=()):
+            ack=msnp.MessageAcknowledgement.HALF, callback=None, errback=None):
         message = msnp.Message(self._client.profile)
         message.add_header('MIME-Version', '1.0')
         message.content_type = content_type
@@ -101,11 +107,11 @@
             message.add_header(key, value)
         message.body = body
 
-        self._pending_messages.append((message, ack, callback, cb_args))
+        self._pending_messages.append((message, ack, callback, errback))
         self._process_pending_queues()
 
     def _invite_user(self, contact):
-        self._pending_invites.add(contact)
+        self.__add_pending(contact)
         self._process_pending_queues()
 
     def _leave(self):
@@ -124,41 +130,79 @@
     def _on_contact_left(self, contact):
         raise NotImplementedError
 
+    def _on_switchboard_closed(self):
+        raise NotImplementedError
+
+    def _on_closed(self):
+        raise NotImplementedError
+
     def _on_error(self, error_type, error):
         raise NotImplementedError
 
     # private
+    def __add_pending(self, contact):
+        if contact in self._pending_invites or contact is self._client.profile:
+            return
+        self._pending_invites.add(contact)
+        handle = contact.connect("notify::presence",
+                lambda contact, pspec: self.__on_user_presence_changed(contact))
+        self._pending_handles[contact] = handle
+
+    def __remove_pending(self, contact):
+        self._pending_invites.discard(contact)
+        if contact in self._pending_handles:
+            contact.disconnect(self._pending_handles[contact])
+            del self._pending_handles[contact]
+
     def __on_user_inviting_changed(self):
         if not self.switchboard.inviting:
             self._process_pending_queues()
 
     def __on_user_joined(self, contact):
         self.participants.add(contact)
-        self._pending_invites.discard(contact)
+        self.__remove_pending(contact)
         self._on_contact_joined(contact)
 
     def __on_user_left(self, contact):
         self._on_contact_left(contact)
         self.participants.remove(contact)
         if len(self.participants) == 0:
-            self._pending_invites.add(contact)
-            try:
-                self._switchboard.leave()
-            except:
-                pass
+            self.__add_pending(contact)
+
+    def __on_user_presence_changed(self, contact):
+        if (self._switchboard and self.switchboard.state == msnp.ProtocolState.OPEN) or self._switchboard_requested:
+            return
+        for contact in self._pending_invites:
+            if contact.presence != Presence.OFFLINE:
+                return
+        # Switchboard was already closed and there is (almost) no chance
+        # a new one will be created now.
+        logger.info("All pending invites are now offline, closing handler")
+        self._leave()
+
+    def __on_switchboard_state_changed(self):
+        if self._switchboard.state == msnp.ProtocolState.CLOSED:
+            for contact in self._pending_invites:
+                if contact.presence != Presence.OFFLINE:
+                    return
+            # Might be that an "appear offline" contact closed the
+            # switchboard or that the switchboard has been explicitely closed
+            self._leave()
 
     def __on_user_invitation_failed(self, contact):
-        self._pending_invites.discard(contact)
+        self.__remove_pending(contact)
         self._on_error(ConversationErrorType.CONTACT_INVITE,
                 ContactInviteError.NOT_AVAILABLE)
 
-    def __on_message_delivered(self, transaction_id):
-        if transaction_id in self._delivery_callbacks:
-            callback, cb_args = self._delivery_callbacks.pop(transaction_id)
-            if callback:
-                callback(*cb_args)
-
-    def __on_message_undelivered(self, transaction_id):
+    def __on_message_delivered(self, trid):
+        callback, errback = self._delivery_callbacks.pop(trid, (None, None))
+        if callback:
+            callback[0](*callback[1:])
+
+    def __on_message_undelivered(self, trid):
+        callback, errback = self._delivery_callbacks.pop(trid, (None, None))
+        if errback:
+            errback[0](*errback[1:])
         self._on_error(ConversationErrorType.MESSAGE,
                 MessageError.DELIVERY_FAILED)
 
@@ -174,17 +218,20 @@
         for contact in self._pending_invites:
             if contact not in self.participants:
                 self.switchboard.invite_user(contact)
+        for contact, handle in self._pending_handles.items():
+            contact.disconnect(handle)
         self._pending_invites = set()
+        self._pending_handles = dict()
 
         if not self.switchboard.inviting:
-            for message, ack, callback, cb_args in self._pending_messages:
+            for message, ack, callback, errback in self._pending_messages:
                 # if ack type is FULL or MSNC, wait for ACK before calling back
                 if ack in (msnp.MessageAcknowledgement.FULL,
                            msnp.MessageAcknowledgement.MSNC):
                     transaction_id = self.switchboard.send_message(message, ack)
-                    self._delivery_callbacks[transaction_id] = (callback, cb_args)
+                    self._delivery_callbacks[transaction_id] = (callback, errback)
                 else:
-                    self.switchboard.send_message(message, ack, callback, cb_args)
+                    self.switchboard.send_message(message, ack, callback)
 
             self._pending_messages = []
 
@@ -194,9 +241,9 @@
             return False
         if self._switchboard_requested:
             return True
-        logger.info("requesting new switchboard")
         self._switchboard_requested = True
-        self._pending_invites |= self.participants
+        for participant in self.participants:
+            self.__add_pending(participant)
         self.participants = set()
         self._switchboard_manager.request_switchboard(self, self._switchboard_priority) # may set the switchboard immediatly
         return self._switchboard_requested
@@ -224,6 +271,7 @@
         self._orphaned_handlers = WeakSet()
         self._switchboards = {}
         self._orphaned_switchboards = set()
+        self._requested_switchboards = {}
         self._pending_switchboards = {}
 
         self._client._protocol.connect("switchboard-invitation-received",
@@ -242,6 +290,8 @@
 
     def request_switchboard(self, handler, priority=99):
         handler_participants = handler.total_participants
+        participants = ", ".join(map(lambda c: c.account, handler_participants))
+        logger.info("Requesting switchboard for participant(s) %s" % participants)
 
         # If the Handler was orphan, then it is no more
         self._orphaned_handlers.discard(handler)
@@ -250,6 +300,8 @@
         for switchboard in self._switchboards.keys():
             switchboard_participants = set(switchboard.participants.values())
             if handler_participants == switchboard_participants:
+                logger.info("Using already opened switchboard %s" %
+                        switchboard.session_id)
                 self._switchboards[switchboard].add(handler)
                 handler._switchboard = switchboard
                 return
@@ -258,25 +310,38 @@
         for switchboard in list(self._orphaned_switchboards):
             switchboard_participants = set(switchboard.participants.values())
             if handler_participants == switchboard_participants:
+                logger.info("Using orphaned switchboard %s" %
+                        switchboard.session_id)
                 self._switchboards[switchboard] = set([handler]) #FIXME: WeakSet ?
                 self._orphaned_switchboards.discard(switchboard)
                 handler._switchboard = switchboard
                 return
 
-        # Check being requested switchboards
+        # Check pending switchboards
         for switchboard, handlers in self._pending_switchboards.iteritems():
             pending_handler = handlers.pop()
             handlers.add(pending_handler)
             switchboard_participants = pending_handler.total_participants
             if handler_participants == switchboard_participants:
                 self._pending_switchboards[switchboard].add(handler)
+                logger.info("Using pending switchboard")
                 return
 
-        self._client._protocol.\
-                request_switchboard(priority, self._ns_switchboard_request_response, handler)
+        # Check switchboards being requested for same participants
+        if participants in self._requested_switchboards:
+            self._requested_switchboards[participants].add(handler)
+            logger.info("Using already requested switchboard for same contacts")
+            return
+
+        logger.info("Requesting new switchboard")
+        self._requested_switchboards[participants] = set([handler])
+        self._client._protocol.request_switchboard(priority,
+                self._ns_switchboard_request_response, participants)
 
     def close_handler(self, handler):
+        logger.info("Closing switchboard handler %s" % repr(handler))
         self._orphaned_handlers.discard(handler)
+        handler._on_closed()
         for switchboard in self._switchboards.keys():
             handlers = self._switchboards[switchboard]
             handlers.discard(handler)
@@ -292,9 +357,10 @@
                 del self._pending_switchboards[switchboard]
                 self._orphaned_switchboards.add(switchboard)
 
-    def _ns_switchboard_request_response(self, session, handler):
+    def _ns_switchboard_request_response(self, session, participants):
         switchboard = self._build_switchboard(session)
-        self._pending_switchboards[switchboard] = set([handler]) #FIXME: WeakSet ?
+        handlers = self._requested_switchboards.pop(participants, set())
+        self._pending_switchboards[switchboard] = handlers
 
     def _ns_switchboard_invite(self, protocol, session, inviter):
         switchboard = self._build_switchboard(session)
@@ -350,6 +416,7 @@
             if switchboard in self._switchboards.keys():
                 for handler in self._switchboards[switchboard]:
                     self._orphaned_handlers.add(handler)
+                    handler._on_switchboard_closed()
                 del self._switchboards[switchboard]
             self._orphaned_switchboards.discard(switchboard)
 
@@ -367,9 +434,9 @@
                     continue
                 if not handler_class._can_handle_message(message):
                     continue
-                handler = handler_class(self._client, (), *extra_args)
+                handler = handler_class.handle_message(self._client,
+                        switchboard, message, *extra_args)
                 handlers.add(handler)
-                handler._switchboard = switchboard
                 self.emit("handler-created", handler_class, handler)
                 handler._on_message_received(message)
 
@@ -377,9 +444,9 @@
             for handler_class, extra_args in self._handlers_class:
                 if not handler_class._can_handle_message(message):
                     continue
-                handler = handler_class(self._client, (), *extra_args)
+                handler = handler_class.handle_message(self._client,
+                        switchboard, message, *extra_args)
                 self._switchboards[switchboard] = set([handler]) #FIXME: WeakSet ?
                 self._orphaned_switchboards.discard(switchboard)
-                handler._switchboard = switchboard
                 self.emit("handler-created", handler_class, handler)
                 handler._on_message_received(message)
--- papyon/transport.py
+++ papyon/transport.py
@@ -5,6 +5,7 @@
 # Copyright (C) 2005-2006 Ali Sabil <ali.sabil at gmail.com>
 # Copyright (C) 2006  Johann Prieur <johann.prieur at gmail.com>
 # Copyright (C) 2006  Ole André Vadla Ravnås <oleavr at gmail.com>
+# Copyright (C) 2010  Collabora Ltd.
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -30,6 +31,8 @@
 The classes of this module are structured as follow:
 G{classtree BaseTransport}"""
 
+from gnet.proxy.factory import ProxyFactory
+
 import gnet
 import gnet.protocol
 import msnp
@@ -225,10 +228,7 @@
     def __init__(self, server, server_type=ServerType.NOTIFICATION, proxies={}):
         BaseTransport.__init__(self, server, server_type, proxies)
 
-        transport = gnet.io.TCPClient(server[0], server[1])
-        transport.connect("notify::status", self.__on_status_change)
-        transport.connect("error", self.__on_error)
-
+        transport = self._setup_transport(server[0], server[1], proxies)
         receiver = gnet.parser.DelimiterParser(transport)
         receiver.connect("received", self.__on_received)
 
@@ -242,6 +242,14 @@
 
     __init__.__doc__ = BaseTransport.__init__.__doc__
 
+    def _setup_transport(self, host, port, proxies):
+        transport = gnet.io.TCPClient(host, port)
+        if proxies:
+            transport = ProxyFactory(transport, proxies, 'direct')
+        transport.connect("notify::status", self.__on_status_change)
+        transport.connect("error", self.__on_error)
+        return transport
+
     ### public commands
 
     @property
@@ -292,7 +300,7 @@
 
     def __handle_ping_reply(self, command):
         timeout = int(command.arguments[0])
-        self.__png_timeout = gobject.timeout_add(timeout * 1000, self.enable_ping)
+        self.__png_timeout = gobject.timeout_add_seconds(timeout, self.enable_ping)
 
     ### callbacks
     def __on_status_change(self, transport, param):
@@ -350,28 +358,25 @@
         self._target_server = server
         server = ("gateway.messenger.hotmail.com", 80)
         BaseTransport.__init__(self, server, server_type, proxies)
-        self._setup_transport()
+        self._setup_transport(server[0], server[1], proxies)
         
         self._command_queue = []
         self._waiting_for_response = False # are we waiting for a response
         self._session_id = None
         self.__error = False
 
-    def _setup_transport(self):
-        server = self.server
-        proxies = self.proxies
-        if 'http' in proxies:
-            transport = gnet.protocol.HTTP(server[0], server[1], proxies['http'])
-        else:
-            transport = gnet.protocol.HTTP(server[0], server[1])
-        transport.connect("response-received", self.__on_received)
-        transport.connect("request-sent", self.__on_sent)
-        transport.connect("error", self.__on_error)
+    def _setup_transport(self, host, port, proxies):
+        handles = []
+        transport = gnet.protocol.HTTP(host, port, proxies)
+        handles.append(transport.connect("response-received", self.__on_received))
+        handles.append(transport.connect("request-sent", self.__on_sent))
+        handles.append(transport.connect("error", self.__on_error))
+        self._transport_handles = handles
         self._transport = transport
 
     def establish_connection(self):
         logger.debug('<-> Connecting to %s:%d' % self.server)
-        self._polling_source_id = gobject.timeout_add(5000, self._poll)
+        self._polling_source_id = gobject.timeout_add_seconds(5, self._poll)
         self.emit("connection-success")
 
     def lose_connection(self):
@@ -386,6 +391,16 @@
             self._target_server = server
         self.emit("connection-reset")
 
+    def change_gateway(self, server):
+        if self.server == server:
+            return
+        logger.debug('<-> Changing gateway to %s:%d' % server)
+        self.server = server
+        for handle in self._transport_handles:
+            self._transport.disconnect(handle)
+        self._transport.close()
+        self._setup_transport(server[0], server[1], self.proxies)
+
     def send_command(self, command, increment=True, callback=None, *cb_args):
         self._command_queue.append((command, increment, callback, cb_args))
         self._send_command()
@@ -431,12 +446,12 @@
     
     def __on_error(self, transport, reason):
         self.__error = True
+        if reason == 403:
+            reason = TransportError.PROXY_FORBIDDEN
         self.emit("connection-lost", reason)
+        self.lose_connection()
         
     def __on_received(self, transport, http_response):
-        if http_response.status == 403:
-            self.emit("connection-lost", TransportError.PROXY_FORBIDDEN)
-            self.lose_connection()
         if 'X-MSN-Messenger' in http_response.headers:
             data = http_response.headers['X-MSN-Messenger'].split(";")
             for elem in data:
@@ -444,8 +459,7 @@
                 if key == 'SessionID':
                     self._session_id = value
                 elif key == 'GW-IP':
-                    self.server = (value, self.server[1])
-                    self._setup_transport()
+                    self.change_gateway((value, self.server[1]))
                 elif key == 'Session'and value == 'close':
                     #self.lose_connection()
                     pass
--- papyon/util/decorator.py
+++ papyon/util/decorator.py
@@ -99,12 +99,12 @@
         def process_queue():
             if len(self._queue) != 0:
                 func, args, kwargs = self._queue.pop(0)
-                self._last_call_time = time.time() * 1000
+                self._last_call_time = time.time()
                 func(*args, **kwargs)
             return False
 
         def new_function(*args, **kwargs):
-            now = time.time() * 1000
+            now = time.time()
             if self._last_call_time is None or \
                     now - self._last_call_time >= self._min_delay:
                 self._last_call_time = now
@@ -113,7 +113,7 @@
                 self._queue.append((func, args, kwargs))
                 last_call_delta = now - self._last_call_time
                 process_queue_timeout = int(self._min_delay * len(self._queue) - last_call_delta)
-                gobject.timeout_add(process_queue_timeout, process_queue)
+                gobject.timeout_add_seconds(process_queue_timeout, process_queue)
 
         new_function.__name__ = func.__name__
         new_function.__doc__ = func.__doc__
--- papyon/util/encoding.py
+++ papyon/util/encoding.py
@@ -18,12 +18,73 @@
 #
 
 import base64
+import email.quoprimime
+import email.base64mime
+import re
+
+
+PADDING = ('', '=', '==', 'A', 'A=', 'A==')
 
 def fix_b64_padding(string):
-    while True:
+    for pad in PADDING:
         try:
-            base64.b64decode(string)
-            break
+            base64.b64decode(string + pad)
+            return string + pad
         except:
-            string += "="
+            continue
     return string
+
+def b64_decode(string):
+    for pad in PADDING:
+        try:
+            return base64.b64decode(string + pad)
+        except:
+            continue
+    raise TypeError
+
+
+# Match encoded-word strings in the form =?charset?q?Hello_World?=
+ecre = re.compile(r'''
+  =\?                   # literal =?
+  (?P<charset>[^?]*?)   # non-greedy up to the next ? is the charset
+  \?                    # literal ?
+  (?P<encoding>[qb])    # either a "q" or a "b", case insensitive
+  \?                    # literal ?
+  (?P<encoded>.*?)      # non-greedy up to the next ?= is the encoded string
+  \?=                   # literal ?=
+  (?=[ \t]|$)           # whitespace or the end of the string
+  ''', re.VERBOSE | re.IGNORECASE | re.MULTILINE)
+
+def decode_rfc2047_string(string):
+    """ Decode it according to RFC 2047. This code has been adapted from
+        Python code (email.header.decode_header), except we don't strip the
+        unencoded parts and we decode all the parts instead of just returning
+        the encoded parts and charsets. """
+    # If no encoding, just return the string
+    if not ecre.search(string):
+        return string
+
+    decoded = ""
+
+    try:
+        for line in string.splitlines():
+            parts = ecre.split(line)
+            while parts:
+                unenc = parts.pop(0)
+                # don't append single spaces between encoded words to the result
+                if not decoded or not parts or unenc != ' ':
+                    decoded += unenc
+                if parts:
+                    charset, encoding = [s.lower() for s in parts[0:2]]
+                    encoded = parts[2]
+                    dec = encoded
+                    if encoding == 'q':
+                        dec = email.quoprimime.header_decode(encoded)
+                    elif encoding == 'b':
+                        dec = email.base64mime.decode(encoded)
+
+                    decoded += dec.decode(charset) if charset else dec
+                del parts[0:3]
+    except:
+        return string
+    return decoded
--- papyon/util/timer.py
+++ papyon/util/timer.py
+# -*- coding: utf-8 -*-
+#
+# papyon - a python client library for Msn
+#
+# Copyright (C) 2010 Collabora Ltd.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+import gobject
+
+__all__ = ['Timer']
+
+class Timer(object):
+
+    def __init__(self):
+        self._timeout_sources = {} # name => source
+        self._timeout_args = {} # name => callback args
+
+    @property
+    def timeouts(self):
+        return self._timeout_sources.keys()
+
+    def start_timeout(self, name, time, *cb_args):
+        self.stop_timeout(name)
+        source = gobject.timeout_add_seconds(time, self.on_timeout, name)
+        self._timeout_sources[name] = source
+        self._timeout_args[name] = cb_args
+
+    def stop_timeout(self, name):
+        source = self._timeout_sources.get(name, None)
+        if source is not None:
+            gobject.source_remove(source)
+            del self._timeout_sources[name]
+        if name in self._timeout_args:
+            return self._timeout_args.pop(name)
+        return []
+
+    def stop_all_timeout(self):
+        for (name, source) in self._timeout_sources.items():
+            if source is not None:
+                gobject.source_remove(source)
+        self._timeout_sources.clear()
+        self._timeout_args.clear()
+
+    def on_timeout(self, name):
+        cb_args = self.stop_timeout(name)
+        handler = getattr(self, "on_%s_timeout" % name, None)
+        if handler is not None:
+            handler(*cb_args)
--- papyon/util/tlv.py
+++ papyon/util/tlv.py
+# -*- coding: utf-8 -*-
+#
+# papyon - a python client library for Msn
+#
+# Copyright (C) 2010 Collabora Ltd.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+import struct
+
+__all__ = ['TLV']
+
+class TLV(object):
+    """Utility class to build and parse data in a TLV representation.
+       Each TLV (type-length-value) element has a 1-byte field for the type,
+       a 1-byte field for the length and a variant size field for the value.
+       The data is padded with null byte (0x0) to the next 4-bytes boundary."""
+
+    def __init__(self, length_dict):
+        """Initialize a TLV object
+           @param length_dict: dict of possible types with their length"""
+
+        self._length_dict = length_dict
+        self._data = {}
+        self._formats = {1: "B", 2: "H", 4: "I", 8: "Q"}
+
+    def size_to_packed_format(self, size):
+        """Determine the correct format to unpack a value (as used by
+           'struct' module)."""
+        if size in self._formats:
+            return self._formats[size]
+        else:
+            return "%ss" % size
+
+    def get(self, key, default):
+        """Get the value for the given type in the TLV or return the default
+           value if this field is not present in the TLV."""
+        return self._data.get(key, default)
+
+    def update(self, key, value):
+        """Update the value for this type. If the value is null, delete
+           the sequence from the TLV."""
+        if value:
+            self._data[key] = value
+        elif key in self._data:
+            del self._data[key]
+
+    def __len__(self):
+        size = 0
+        for (t, v) in self._data.items():
+            size += 2 + self._length_dict[t]
+        if (size % 4) != 0:
+            size += 4 - (size % 4)
+        return size
+
+    def __str__(self):
+        """Pack data in a string and add padding."""
+        string = ""
+        for (t, v) in self._data.items():
+            if not t in self._length_dict: continue
+            l = self._length_dict[t]
+            f = self.size_to_packed_format(l)
+            string += struct.pack(">BB%s" % f, t, l, v)
+        if (len(string) % 4) != 0:
+            string += '\x00' * (4 - (len(string) % 4))
+        return string
+
+    def parse(self, data, size):
+        """Parse the given TLV data and add values to the internal dict."""
+        offset = 0
+        while offset < size:
+            if ord(data[offset]) is 0: break # ignore padding bytes
+            t = ord(data[offset])
+            l = ord(data[offset + 1])
+            f = self.size_to_packed_format(l)
+            end = offset + 2 + l
+            self._data[t] = struct.unpack(">%s" % f, data[offset + 2:end])[0]
+            offset = end
--- test.py
+++ test.py
@@ -38,14 +38,14 @@
             for contact in self._client.address_book.contacts:
                 print contact
             #self._client.profile.personal_message = "Testing papyon, and freeing the pandas!"
-            gobject.timeout_add(5000, self._client.start_conversation)
+            gobject.timeout_add_seconds(5, self._client.start_conversation)
 
     def on_client_error(self, error_type, error):
         print "ERROR :", error_type, " ->", error
 
 class AnnoyingConversation(papyon.event.ConversationEventInterface):
     def on_conversation_user_joined(self, contact):
-        gobject.timeout_add(5000, self.annoy_user)
+        gobject.timeout_add_seconds(5, self.annoy_user)
 
     def annoy_user(self):
         msg = "Let's free the pandas ! (testing papyon)"

++++++ papyon.yaml
--- papyon.yaml
+++ papyon.yaml
@@ -1,6 +1,6 @@
 Name: papyon
 Summary: Python libraries for MSN Messenger network
-Version: 0.4.9
+Version: 0.5.2
 Release: 1
 Group: System/Libraries
 License: GPLv2+




More information about the MeeGo-commits mailing list