/***************************************************************************
                          msnswitchboardconnection.cpp  -  description
                             -------------------
    begin                : Fri Jan 24 2003
    copyright            : (C) 2003 by Mike K. Bennett
    email                : mkb137b@hotmail.com
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   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.                                   *
 *                                                                         *
 ***************************************************************************/

#include "msnswitchboardconnection.h"

#include "../contact/contact.h"
#include "../contact/invitedcontact.h"  // for cast
#include "../contact/msnobject.h"
#include "../currentaccount.h"
#include "../emoticonmanager.h"
#include "../kmessapplication.h"
#include "../kmessdebug.h"
#include "applications/applicationlist.h"
#include "soap/offlineimservice.h"
#include "chatinformation.h"
#include "chatmessage.h"
#include "mimemessage.h"
#include "msnnotificationconnection.h"
#include "multipacketmessage.h"
#include "p2pmessage.h"

#include "config-kmess.h"

#include <QFile>
#include <QRegExp>
#include <QTimer>
#include <QUrl>

#include <KLocale>

#include <math.h>

#ifdef KMESSDEBUG_SWITCHBOARD
  #define KMESSDEBUG_SWITCHBOARD_GENERAL
  #define KMESSDEBUG_SWITCHBOARD_P2P
  #define KMESSDEBUG_SWITCHBOARD_EMOTICONS
//   #define KMESSDEBUG_SWITCHBOARD_CONTACTS
//   #define KMESSDEBUG_SWITCHBOARD_KEEPALIVE
//   #define KMESSDEBUG_SWITCHBOARD_ACKS
#endif



// The constructor
MsnSwitchboardConnection::MsnSwitchboardConnection()
 : MsnConnection( MsnSocketBase::SERVER_SWITCHBOARD ),
   abortingApplications_(false),
   acksPending_(0),
   autoDeleteLater_(false),
   backgroundConnection_(true),
   closingConnection_(false),
   connectionState_(SB_DISCONNECTED),
   currentAccount_(0),
   initialized_(false),
   userStartedChat_(false),
   keepAliveTimer_(0),
   keepAlivesRemaining_(0),
   offlineImService_(0)
{
  setObjectName("MsnSwitchboardConnection");
}



// The copy constructor
MsnSwitchboardConnection::MsnSwitchboardConnection( const MsnSwitchboardConnection &other )
 : MsnConnection( MsnSocketBase::SERVER_SWITCHBOARD ),
   abortingApplications_(false),
   acksPending_(0),
   autoDeleteLater_( other.autoDeleteLater_ ),
   backgroundConnection_( other.backgroundConnection_ ),
   chatId_( other.chatId_ ),
   closingConnection_(false),
   connectionState_(SB_DISCONNECTED),
   currentAccount_( other.currentAccount_ ),
   firstContact_( other.firstContact_ ),
   initialized_(false),
   lastContact_( other.lastContact_ ),
   pendingMessages_( other.pendingMessages_ ),
   userStartedChat_( other.userStartedChat_ ),
   keepAliveTimer_(0),
   keepAlivesRemaining_(0),
   offlineImService_(0)
{
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
  kmDebug() << "entering copy constructor";
#endif

  setObjectName("MsnSwitchboardConnection");
}



// The destructor
MsnSwitchboardConnection::~MsnSwitchboardConnection()
{
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
  kmDebug() << "entering destructor";
#endif

  // Close the connection
  closeConnection();

  // If present, destroy the offline message sending service
  delete offlineImService_;

  emit deleteMe( this );

#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
  kmDebug() << "DESTROYED.";
#endif
}



// Initialize or restart the last activity timer, for keep alive messages
void MsnSwitchboardConnection::activity()
{
  // Keepalives are not needed in multi-user chats.
  if( ! isConnected() || contactsInChat_.count() > 1 )
  {
    return;
  }

  // Initialize the timer on demand.
  if( keepAliveTimer_ == 0 )
  {
    keepAliveTimer_ = new QTimer( this );
    keepAliveTimer_->setObjectName( "keepalive-msgs" );
    keepAliveTimer_->setSingleShot( false );

    // Keepalives are disabled for background switchboard connections. Instead, use the timer
    // as a disconnection timeout. It's also made server-side, but here it will save memory.
    connect( keepAliveTimer_, SIGNAL( timeout() ), SLOT( sendKeepAlive() ) );
  }

  // Reset the number of remaining keep alives.
  if( ! backgroundConnection_ )
  {
    // A chat connection with a contact will be kept open at least for 15 minutes.
    keepAlivesRemaining_ = 18;
  }
  else
  {
    // We're much more strict when managing background connections. Make them last at most 5 minutes without activity.
    keepAlivesRemaining_ = 6;
  }

  // Reset the timer.
  keepAliveTimer_->stop();
  keepAliveTimer_->start( 50000 );

#ifdef KMESSDEBUG_SWITCHBOARD_KEEPALIVE
  kmDebug() << ( backgroundConnection_ ? "Background" : "Chatting" )
           << "keepalive session restarted." << endl;
#endif
}



// Clean the old unacked messages
void MsnSwitchboardConnection::cleanUnackedMessages()
{
  // Standard chat messages are sent with a 'ACK_NAK_ONLY' flag.
  // When the messages is received, nothing is sent.
  // When the messsage can't be delivered,. a 'NAK' is returned.
  // KMess caches the chat messages for 5 minutes to show the
  // "the following message could not be delivered:" messages.
  // This method cleans up that cache for messages that did not receive a NAK after 5 minutes.

#ifdef KMESSDEBUG_SWITCHBOARD_ACKS
  uint mapCount = unAckedMessages_.count();
#endif

  uint minTime = QDateTime::currentDateTime().toTime_t() - ( 5 * 60 );

  // Find find the entries, then delete.
  QList<int> removeAcks;
  QHashIterator<int,UnAckedMessage*> it( unAckedMessages_ );
  while( it.hasNext() )
  {
    it.next();

    // Get message info
    const UnAckedMessage* unAcked = it.value();
    if( unAcked->time < minTime )
    {
      // Message is expired, remove it.
      removeAcks.append( it.key() );
    }
  }


  // Remove list
  if( ! removeAcks.isEmpty() )
  {
#ifdef KMESSDEBUG_SWITCHBOARD_ACKS
    QString join;
    foreach( int key, unAckedMessages_.keys() )
    {
      if( join.isEmpty() ) join += ",";
      join += QString::number( key );
    }
    kmDebug() << "removing expired messages " << join << ".";
#endif

    foreach( int key, removeAcks )
    {
      delete unAckedMessages_.take( key );
    }
  }


#ifdef KMESSDEBUG_SWITCHBOARD_ACKS
  kmDebug() << "removed " << ( mapCount - unAckedMessages_.size() ) << " messages, "
           << "kept " << unAckedMessages_.size() << " messages until those expire." << endl;
#endif
}



// Close the connection
void MsnSwitchboardConnection::closeConnection()
{
  connectionState_ = SB_DISCONNECTING;

  // If there are still contacts, it means contactLeft() was not initiated,
  // and the user closed the chat window earlier.
  if( contactsInChat_.count() > 0 )
  {
    // Keep a default for re-connecting.
    lastContact_ = contactsInChat_[0];

    // Make sure all contacts are removed, which will also abort their applications in ApplicationList.
    // going in reverse means the lastContact_ set above is always the last to be removed.
    for( int i = contactsInChat_.count() - 1; i >=0; i-- )
    {
      QString &handle = contactsInChat_[i];
      ContactBase *contact = currentAccount_->getContactByHandle( handle );
      if(! KMESS_NULL(contact))
      {
        contact->removeSwitchboardConnection(this, true);  // could cause applications to abort.
      }

      contactsInChat_.removeAll( handle );

      // Also signal they're not in chat anymore
      emit contactLeftChat( contact, false );
    }

    contactsInChat_.clear();
  }


  if( isConnected() )
  {
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
    kmDebug() << "Still connected, closing the connection.";
#endif

    disconnectFromServer();
  }

  // Reset state, so messages are queued when the connection
  // is back up but the contact is not yet in the chat.
  connectionState_ = SB_DISCONNECTED;
  closingConnection_ = false;

  // Also stop the activity timer
  if( keepAliveTimer_ != 0 )
  {
    keepAliveTimer_->stop();
  }

#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
  kmDebug() << "Session ended.";
#endif
}



// Clean up, close the connection, destroy this object
void MsnSwitchboardConnection::closeConnectionLater( bool autoDelete )
{
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
  kmDebug() << "aborting applications nicely and closing connection.";
#endif

  ContactBase *contact;
  bool hasAbortingApplications = false;
  closingConnection_ = true;

  // this method is called when the user wants to close the chat window (allowing everything to close nicely).
  // If there are no contacts in the chat, we can close directly.
  if( ! contactsInChat_.isEmpty() )
  {
    // There are still contacts.
    // Verify whether they still have applications running.
    // Allow those applications to abort, and report back when the connection can be closed.
    QList<ContactBase*> contactsToRemove;
    foreach( const QString &handle, contactsInChat_ )
    {
      contact = currentAccount_->getContactByHandle( handle );
      if( ! KMESS_NULL(contact) && contact->hasApplicationList() )
      {
        ApplicationList *appList = contact->getApplicationList();
        bool aborting = appList->contactLeavingChat(this, true);
        if( aborting )
        {
          connect(appList, SIGNAL(     applicationsAborted(const QString&) ),
                  this,    SLOT  ( slotApplicationsAborted(const QString&) ));
          hasAbortingApplications = true;
        }
        else
        {
          contactsToRemove.append(contact);
        }
      }
    }

    // All contacts that don't need any aborting are removed now (not in the iterator loop)
    // The remaining ones are removed in slotApplicationsAborted().
    foreach( ContactBase *contact, contactsToRemove )
    {
      contactsInChat_.removeAll( contact->getHandle() );
      contact->removeSwitchboardConnection( this, true );
    }
  }


  if( hasAbortingApplications )
  {
    // Wait for all applications to abort.
    // Set variables to use in slotApplicationsAborted.
    abortingApplications_ = true;
    autoDeleteLater_      = autoDelete;
  }
  else
  {
    // No applications are aborting.
    // Close the connection directly
    closeConnection();

    // Automatically delete ourself
    if(autoDelete)
    {
      this->deleteLater();
    }
  }
}



// Do what's required when a contact joined
void MsnSwitchboardConnection::contactJoined( const QString& handle, const QString& friendlyName, const uint capabilities )
{
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
  kmDebug() << "Contact " << handle << " has joined.";
#endif

  // Update states
  connectionState_ = SB_CHAT_STARTED;

  // Add the contact to the list if the contact isn't there already
  if ( ! contactsInChat_.contains( handle ) )
  {
    contactsInChat_.append( handle );
  }

  // Stop the keep-alive timer when initiating a group chat
  if( keepAliveTimer_ && contactsInChat_.count() > 1 )
  {
#ifdef KMESSDEBUG_SWITCHBOARD_KEEPALIVE
    kmDebug() << "Starting group chat: stopping keepalive session." << endl;
#endif
    keepAliveTimer_->stop();
    delete keepAliveTimer_;
    keepAliveTimer_ = 0;
  }

  // Indicate the contact is active in this session.
  ContactBase *contact = currentAccount_->getContactByHandle(handle);
  if( contact == 0 ) // We don't have this contact in our list
  {
    contact = currentAccount_->addInvitedContact( handle, friendlyName, capabilities );
  }

  // Add switchboard connection to the contact.
  if( ! KMESS_NULL(contact) )
  {
    contact->addSwitchboardConnection( this );
  }

  // Inform the contact about our version
  // Also do this when the contact left and re-entered the chat, it might connect with a different client.
  sendClientCaps();

  // Send all pending messages
  sendPendingMessages();

  // Notify the join to the Chat Master.
  emit contactJoinedChat( contact );
}



// Remove a contact from the list of contacts in the chat
void MsnSwitchboardConnection::contactLeft(const QString& handle)
{
#ifdef KMESSTEST
  KMESS_ASSERT( connectionState_ == SB_CHAT_STARTED );
#endif
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
  kmDebug() << "Contact " << handle << " has left.";
#endif

  // Check if the contact is in the chat..
  contactsInChat_.removeAll( handle );

  // update the last contact.
  if ( lastContact_ == handle && contactsInChat_.count() > 0)
  {
    lastContact_ = contactsInChat_[0];
  }

#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
  kmDebug() << "emitting that '" << handle << "' left chat.";
#endif

  // Start the keep-alive timer again when ending a group chat
  if( contactsInChat_.count() == 1 )
  {
    activity();
  }

  // Indicate the contact is not active anymore in this session.
  ContactBase *contact = currentAccount_->getContactByHandle(handle);
  if( ! KMESS_NULL(contact) )
  {
    contact->removeSwitchboardConnection(this, false);
  }

  // Emit the signal to the Chat Window.
  if( ! backgroundConnection_ )
  {

    if( ! KMESS_NULL(contact) )
    {
      // The conversation went idle if isExpired() returns true
      emit contactLeftChat( contact, isExpired() );
    }
  }
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
  else
  {
    kmDebug() << "Not sending contactLeftChat() signal for background chats.";
  }
#endif

  // Check if all contacts went away
  if( contactsInChat_.count() == 0 )
  {
    // Store contact to have a default when re-connecting.
    lastContact_ = handle;

    // The last contact left the chat.
    connectionState_ = SB_CONTACTS_LEFT;

    // NOTE: Behavior changed since WLM 8+, emoticon data must be sent every time.
    // Reset the list of sent emoticons, so they will be sent again if the chat restarts
    // sentEmoticons_.clear();

#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
    kmDebug() << "last contact left chat, closing connection.";
#endif
    // No contacts left in the chat, close connection since it is of little use.
    // - the connection can still be used to 'CAL' the last contact again
    // - when the contact resumes the connection, it uses a different server,
    //   so startChat() needs to reconnect.
    closeConnection();
  }
}



// Convert an html format (#RRGGBB) color to an msn format (BBGGRR) color
const QString MsnSwitchboardConnection::convertHtmlColorToMsnColor( const QString &color ) const
{
#ifdef KMESSTEST
  KMESS_ASSERT( color.length() == 7 );
#endif

  // Get the color components
  const QString red  ( color.mid(1, 2) );
  const QString green( color.mid(3, 2) );
  const QString blue ( color.mid(5, 2) );

  // Reassemble the color
  if ( blue != "00" )
  {
    return blue + green + red;
  }
  else if ( green != "00" )
  {
    return green + red;
  }
  else if ( red != "00" )
  {
    return red;
  }
  else
  {
    return "0";
  }
}



// Convert and msn format color (BBGGRR) to an html format (#RRGGBB) color
const QString MsnSwitchboardConnection::convertMsnColorToHtmlColor( QString& color ) const
{
  // If the color isn't present, use black
  if( color == "0" )
  {
    return "#000000";
  }

  // Remove any character apart from the last six (stripping any initial '#' character)
  if( color.length() > 6 )
  {
    color = color.right( 6 );
  }
  // Fill the color out to six characters
  else if( color.length() < 6 )
  {
    color = color.rightJustified( 6, '0' );
  }

  // Get the color components
  const QString blue ( color.mid( 0, 2 ) );
  const QString green( color.mid( 2, 2 ) );
  const QString red  ( color.mid( 4, 2 ) );

  // Reassemble the components
  return "#" + red + green + blue;
}



// Make a list of the contacts in the chat
const QStringList MsnSwitchboardConnection::getContactsInChat() const
{
  if( contactsInChat_.isEmpty() )
  {
    return QStringList( lastContact_ );
  }

  // Note this object may contain no contacts at all, and lastContact_ has the last one to re-invite.
  return contactsInChat_;
}



// Return the first contact the chat started with.
const QString & MsnSwitchboardConnection::getFirstContact() const
{
  return firstContact_;
}


// Return the last contact who left the chat.
const QString & MsnSwitchboardConnection::getLastContact() const
{
  return lastContact_;
}



// Return whether the user started the chat
bool MsnSwitchboardConnection::getUserStartedChat() const
{
  return userStartedChat_;
}



// Received a positive delivery message.
void MsnSwitchboardConnection::gotAck( const QStringList& command )
{
#ifdef KMESSTEST
  KMESS_ASSERT( unAckedMessages_.count() > 0 );
#endif

  // Remove the ACKed message from the queue.
  int ackNumber = command[1].toUInt();
  if( ! unAckedMessages_.contains( ackNumber ) )
  {
    kmWarning() << "Received an ACK message but message is not in sent queue.";
    return;
  }

  delete unAckedMessages_.take( ackNumber );
  acksPending_--;

#ifdef KMESSDEBUG_SWITCHBOARD_ACKS
  kmDebug() << "Received one ACK message, still " << acksPending_ << " unacked.";
#endif

  // Signal that the switchboard is no longer busy and can accept new application messages.
  if( acksPending_ < 2 )
  {
    emit readySend();
  }
}



// Received notification that a contact is no longer in session.
void MsnSwitchboardConnection::gotBye(const QStringList& command)
{
  contactLeft( command[1].toLower() );
}



// Received the initial roster information for new contacts joining a session.
void MsnSwitchboardConnection::gotIro(const QStringList& command)
{
  const QString handle       ( command[4].toLower() );
        QString friendlyName ( QUrl::fromPercentEncoding( command[5].toUtf8() ) );
  uint          capabilities = command[6].toUInt();

  const QString altFriendlyName( currentAccount_->getContactFriendlyNameByHandle( handle, STRING_CLEANED ) );
  if( ! altFriendlyName.isEmpty() )
  {
    friendlyName = altFriendlyName;
  }

  contactJoined( handle, friendlyName, capabilities );
}



// Received notification of a new client in the session.
void MsnSwitchboardConnection::gotJoi(const QStringList& command)
{
  const QString handle      ( command[1].toLower() );
        QString friendlyName( QUrl::fromPercentEncoding( command[2].toUtf8() ) );
  uint          capabilities = command[3].toUInt();

  const QString altFriendlyName( currentAccount_->getContactFriendlyNameByHandle( handle, STRING_CLEANED ) );
  if( ! altFriendlyName.isEmpty() )
  {
    friendlyName = altFriendlyName;
  }

  contactJoined( handle, friendlyName, capabilities );
}



// Received a negative acknowledgement of the receipt of a message.
void MsnSwitchboardConnection::gotNak( const QStringList& command )
{
#ifdef KMESSTEST
  KMESS_ASSERT( unAckedMessages_.count() > 0 );
#endif

  // Check if the ACK exists in the map.
  int ackNumber = command[1].toUInt();
  if( ! unAckedMessages_.contains( ackNumber ) )
  {
    kmWarning() << "Received a NAK message but message is not in sent queue.";
    return;
  }

  // Get message from queue, copy the MimeMessage, and remove it.
  UnAckedMessage *unAcked = unAckedMessages_[ ackNumber ];
  const MimeMessage mimeMessage( unAcked->message );
  delete unAckedMessages_.take( ackNumber );
  acksPending_--;

#ifdef KMESSDEBUG_SWITCHBOARD_ACKS
  kmDebug() << "Received one NAK message, still " << acksPending_ << " unacked.";
#endif



  // Signal that the switchboard is no longer busy and can accept new application messages.
  if( acksPending_ < 2 )
  {
    emit readySend();
  }

  // Do not notify errors for background connections
  if( backgroundConnection_ )
  {
#ifdef KMESSDEBUG_SWITCHBOARD_ACKS
  kmDebug() << "Not displaying undelivered message for background chats.";
#endif
    return;
  }

  QString sender;
  if( mimeMessage.hasField( "To" ) )
  {
    sender = mimeMessage.getValue( "To" );
  }
  else if( mimeMessage.hasField( "P2P-Dest" ) )
  {
    sender = mimeMessage.getValue( "P2P-Dest" );
  }
#ifdef KMESSDEBUG_SWITCHBOARD_ACKS
  else
  {
    kmDebug() << "Received notice of undelivered message for all contacts.";
  }
#endif

  // Don't display failure notices for P2P or other kinds of MIME messages
  const QString contentType( mimeMessage.getValue( "Content-Type" ) );
  if( ! contentType.startsWith( "text/plain" )
  &&  ! contentType.startsWith( "text/x-msnmsgr-datacast" )
  &&  ! contentType.startsWith( "image/gif" )
  &&  ! contentType.startsWith( "application/x-ms-ink" ) )
  {
#ifdef KMESSDEBUG_SWITCHBOARD_ACKS
    kmDebug() << "Not displaying undelivered message for a service message of type" << contentType;
#endif
    return;
  }

  // Just be sure the switchboard is linked to a chat window.
  // For example, the user closes the chat and receives a NAK before the switchboard is closed.
  emit requestChatWindow( this );

  // The message could not be delivered to a specific recipient
  if( ! sender.isEmpty() )
  {
#ifdef KMESSDEBUG_SWITCHBOARD_ACKS
    kmDebug() << "Signaling sending failure for contact" << sender;
#endif
    ContactBase *contact = CurrentAccount::instance()->getContactByHandle( sender );
    if( KMESS_NULL(contact) )
    {
      return;
    }

    emit contactJoinedChat( contact );

    // Let the user know that the message wasn't delivered
    emit sendingFailed( sender, mimeMessage );
  }
  else
  {
#ifdef KMESSDEBUG_SWITCHBOARD_ACKS
    kmDebug() << "Signaling sending failure for all participants";
#endif
    // The message couldn't be delivered to any of the recipients:
    // let the user know that the message wasn't delivered,
    // but give a wildcard sender
    emit sendingFailed( "*", mimeMessage );
  }
}



// Received notification of the termination of a client-server session.
void MsnSwitchboardConnection::gotOut(const QStringList& /*command*/)
{
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
  kmDebug() << "Switchboard - got OUT.";
#endif

  closeConnection();
}



// Received a client-server authentication message.
void MsnSwitchboardConnection::gotUsr(const QStringList& command)
{
#ifdef KMESSTEST
  KMESS_ASSERT( ! firstContact_.isEmpty() );
  KMESS_ASSERT( connectionState_ == SB_AUTHORIZING );
#endif

  // This should just be a confirmation
  if ( command[2] != "OK" )
  {
    kmWarning() << "Switchboard authentication failed";
    return;
  }

#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
  kmDebug() << "authentication successful, inviting " << firstContact_;
#endif

  connectionState_ = SB_INVITING_CONTACTS;

  // Now call the other user to the conversation.
  sendCommand( "CAL", firstContact_ );

  // If there are other pending invites, send them now
  if( pendingInvitations_.count() > 0 )
  {
    foreach( const QString &handle, pendingInvitations_ )
    {
      inviteContact( handle );
    }
    pendingInvitations_.clear();
  }
}



// Initialize the object, optionally presetting a contact to reinvite
bool MsnSwitchboardConnection::initialize( QString handle )
{
  if ( initialized_ )
  {
    kmDebug() << "already initialized!";
    return false;
  }
  if ( ! MsnConnection::initialize() )
  {
    kmDebug() << "Couldn't initialize base class.";
    return false;
  }

  currentAccount_ = CurrentAccount::instance();

  // Determine which payload commands can be received by the Switchboard Connection
  const QStringList payloadCommands( "MSG" );

  setAcceptedPayloadCommands( payloadCommands );

  firstContact_ = lastContact_ = handle;

  initialized_ = true;
  return true;
}



// Invite a contact into the chat
void MsnSwitchboardConnection::inviteContact( const QString& handle )
{
  if( handle.isEmpty() )
  {
    return;
  }

  if( ! isConnected() )
  {
    // Request a new switchboard session
    emit requestNewSwitchboard( lastContact_ );
    // Add this contact to the list of pending invitations
    pendingInvitations_.append( handle );

#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
  kmDebug() << "Added pending invitation for contact " << handle;
#endif

    return;
  }

  if ( backgroundConnection_ )
  {
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
    kmDebug() << "Chat is still in the background, requesting chat window.";
#endif

    backgroundConnection_ = false;
    emit requestChatWindow( this );
  }

  sendCommand( "CAL", handle );

#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
  kmDebug() << "Invited contact " << handle;
#endif
}



// Check if a certain contact is in the chat
bool MsnSwitchboardConnection::isContactInChat( const QString& handle ) const
{
#ifdef KMESSDEBUG_SWITCHBOARD_CONTACTS
  kmDebug() << "Checking handle " << handle << " - participants are: " << contactsInChat_.join(",") << " - lastContact: " << lastContact_;
  kmDebug() << "returning " << ( ( (contactsInChat_.count() == 0) ? (lastContact_ == handle) : (contactsInChat_.contains( handle )) ) ? "true" : "false" );
#endif

  return ( (contactsInChat_.count() == 0) ? (lastContact_ == handle) : (contactsInChat_.contains( handle )) );
}



// Check whether the switchboard is buzy (has too many pending messages)
bool MsnSwitchboardConnection::isBusy() const
{
  // unAckedMessages_ also contains messages what have a "NAK_ONLY" flag.
  // keep a special variable that only lists the normal ACKs.
  return acksPending_ > 3;
}



// Check if all contacts left
bool MsnSwitchboardConnection::isEmpty() const
{
  return contactsInChat_.empty();
}



// Check if only the given contact is in the chat
bool MsnSwitchboardConnection::isExclusiveChatWithContact(const QString& handle) const
{
#ifdef KMESSDEBUG_SWITCHBOARD_CONTACTS
  kmDebug() << "Checking if chat is exclusive with " << handle
            << " (contacts=" << contactsInChat_.join(",") << ", lastContact=" << lastContact_ << ")" << endl;
#endif

  // Also check for last contact, contact can be re-invited to resume the session.
  // Previously, one contact was always left in the contactsInChat_ list.

  int count = contactsInChat_.count();
  bool result = ( count == 1 && contactsInChat_[0] == handle)
             || ( count == 0 && lastContact_       == handle);

#ifdef KMESSDEBUG_SWITCHBOARD_CONTACTS
  kmDebug() << "returning " << ( result ? "true" : "false" );
#endif

  return result;
}



// Check whether the connection is idle
bool MsnSwitchboardConnection::isExpired() const
{
    // If we're using the keepalive mechanism, and there are no keepalives left, then the session has expired.
  return ( ! isConnected() || ( keepAliveTimer_ != 0 && keepAlivesRemaining_ < 1 ) );
}



// Check whether the switchboard is currently disconnected or disconnecting.
bool MsnSwitchboardConnection::isInactive() const
{
  return ( connectionState_ == SB_DISCONNECTED || connectionState_ == SB_DISCONNECTING );
}



// Check whether the switchboard is waiting for a new connection
bool MsnSwitchboardConnection::isWaiting() const
{
  return ( connectionState_ == SB_REQUESTING_CHAT );
}



// Parse a regular command
void MsnSwitchboardConnection::parseCommand(const QStringList& command)
{
  if ( command[0] == "ACK" )
  {
    gotAck( command );
  }
  else if ( command[0] == "ANS" )
  {
    // Do nothing.
  }
  else if ( command[0] == "BYE" )
  {
    gotBye( command );
  }
  else if ( command[0] == "CAL" )
  {
    // Do nothing
  }
  else if ( command[0] == "IRO" )
  {
    gotIro( command );
  }
  else if ( command[0] == "JOI" )
  {
    gotJoi( command );
  }
  else if ( command[0] == "NAK" )
  {
    gotNak( command );
  }
  else if ( command[0] == "OUT" )
  {
    gotOut( command );
  }
  else if ( command[0] == "USR" )
  {
    gotUsr( command );
  }
  else
  {
    kmDebug() << "got unhandled command " << command[0] << " (contacts=" << contactsInChat_ << ").";
  }
}



// Parse a normal plain text chat message
void MsnSwitchboardConnection::parseChatMessage( const QString &contactHandle, const QString &friendlyName, const QString &contactPicture, const MimeMessage &message )
{
  // The P4-Context field can override the default contact name.
  // It's typically used by plugins of the official client (e.g. Xiaoi's Qun).
  QString messageFriendlyName( friendlyName );
  if( message.hasField( "P4-Context" ) )
  {
    const QString friendlyNameP4( message.getValue( "P4-Context" ) );
    if( ! friendlyNameP4.isEmpty() )
    {
      messageFriendlyName = friendlyNameP4;
    }
  }

#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
  kmDebug() << "Got message by " << contactHandle << ": " << message.getBody();
#endif

  // Get the font and color from the format string
  QString        family( message.getSubValue( "X-MMS-IM-Format", "FN" ) );
  const QString effects( message.getSubValue( "X-MMS-IM-Format", "EF" ) );
  QString         color( message.getSubValue( "X-MMS-IM-Format", "CO" ) );

  family = QUrl::fromPercentEncoding( family.toUtf8() );
  color  = convertMsnColorToHtmlColor( color );

  QFont font;
  font.setFamily( family );
  font.setBold(      effects.contains("B") );
  font.setItalic(    effects.contains("I") );
  font.setUnderline( effects.contains("U") );

  // Send the chat message
  emit chatMessage( ChatMessage( ChatMessage::TYPE_INCOMING,
                                 ChatMessage::CONTENT_MESSAGE,
                                 true,
                                 message.getBody(),
                                 contactHandle,
                                 messageFriendlyName,
                                 contactPicture,
                                 font,
                                 color ) );
}



// Parse the clientcaps message, exchanged by a lot of third party clients.
void MsnSwitchboardConnection::parseClientCapsMessage( const QString &contactHandle, const MimeMessage &message )
{
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
  kmDebug() << "Got third-party client info message. (message dump follows)" << endl
           << message.getMessage().data() << endl;
#endif

  // Example message
  // Client-Name: Client-Name/Version-Major.Version-Minor
  // Chat-Logging: Y

  // Possible values for Chat-Logging:
  // Y: Nonsecure logging is enabled.
  // S: log is encrypted
  // N: not logging

  // Retrieve client identifier from the message
  const MimeMessage& subMessage = message.getBody();
  const QString clientFullName( subMessage.getValue("Client-Name") );

  // store in contact extension, except if contact is an InvitedContact
  ContactBase *contact = currentAccount_->getContactByHandle( contactHandle );
  if( KMESS_NULL(contact) ) return;   // should always be a InvitedContact here if not in the list.

  contact->setClientFullName( clientFullName );
}



// Parse a datacast message (e.g. nudge or voice clip)
void MsnSwitchboardConnection::parseDatacastMessage( const QString &contactHandle, const MimeMessage &message )
{
  // Get the contact
  ContactBase *contact = CurrentAccount::instance()->getContactByHandle( contactHandle );
  if( KMESS_NULL(contact) ) return;

  int dataType = message.getValue( "ID" ).toInt();

#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
  kmDebug() << "Parsing datacast message (ID=" << dataType << ")";
#endif

  // Request a chat window when the chat is still in the background.
  if( backgroundConnection_ )
  {
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
    kmDebug() << "chat is still in the background, requesting chat window.";
#endif
    backgroundConnection_ = false;
    emit requestChatWindow( this );
    emit contactJoinedChat( contact );
  }

  // Each ID has a different meaning.
  switch( dataType )
  {
    case 1: // A nudge
      emit receivedNudge( contact );
      break;

    case 2: // A wink
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
      kmDebug() << "Datacast message contains an msn object (wink), signalling ChatMaster to download it.";
#endif

      // A wink from a contact
      emit gotMsnObject( message.getValue( "Data" ), contactHandle );
      break;

    case 4: // An action message
      // Not supported yet
      kmDebug() << "Received unhandled action message from contact" << contactHandle;

      emit showWarning( WARNING_UNSUPPORTED_ACTIONMESSAGE, contact );
      break;

    case 3: // A voice clip
      // Not supported yet
      kmDebug() << "Received unhandled voice clip from contact" << contactHandle;

      emit showWarning( WARNING_UNSUPPORTED_VOICECLIP, contact );
      break;

    default: // Completely unknown message type
      // Not supported yet
      kmDebug() << "Received unhandled datacast message (ID" << dataType << ") from contact" << contactHandle << ":";
      message.print(); // So that we may discover what it is

      emit showWarning( WARNING_UNSUPPORTED_UNKNOWN, contact );
      break;
  }
}



// Parse an emoticon message
void MsnSwitchboardConnection::parseEmoticonMessage( const QString &contactHandle, const QString &messageBody )
{
  // Get the contact
  ContactBase *contact = currentAccount_->getContactByHandle( contactHandle );
  if( KMESS_NULL(contact) ) return;

#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
  kmDebug() << "Received custom emoticon list for " << contactHandle << ".";
#endif

  // Request a chat window when the chat is still in the background.
  if( backgroundConnection_ )
  {
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
    kmDebug() << "Chat is still in the background, requesting chat window.";
#endif
    backgroundConnection_ = false;
    emit requestChatWindow( this );
    emit contactJoinedChat( contact );
  }

  // Emoticon data consists of a tab-separated list of pairs, each formed by an emoticon definition and the related msn object:
  // [Shortcut] TAB [MSN Object] TAB [Shortcut] TAB [MSN Object] TAB ...
  // |___first emoticon________|     |____second emoticon______|
  QString emoticonCode;
  QString msnObjectData;

  // Extract emoticon definitions and msn objects
  QStringList msnObjects( messageBody.trimmed().split("\t") );
  for( QStringList::Iterator it = msnObjects.begin(); it != msnObjects.end(); ++it )
  {
    emoticonCode = *it;

    ++it;

    // If the number of fields is odd, this custom emoticons list contains errors.
    if( it == msnObjects.end() )
    {
      kmWarning() << "Emoticon message has an unexpected format: odd number of fields! "
                 << "(ignoring msnobject, contact=" << contactHandle << ")." << endl;
      break;
    }

    msnObjectData = *it;

    // Perform a syntax check.
    if( msnObjectData.length() < 20 )
    {
      kmWarning() << "Emoticon message has an unexpected format "
                 << "(ignoring msnobject, contact=" << contactHandle << ", message='" << msnObjectData << "')." << endl;
      continue;
    }

#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
    kmDebug() << "Adding emoticon code " << emoticonCode << ".";
#endif

    // Store the emoticon code for the contact.
    const MsnObject msnObject( msnObjectData );
    contact->addEmoticonDefinition( emoticonCode, msnObject.getDataHash() );

    // Ask the ChatMaster to download emoticons.
    emit gotMsnObject( msnObjectData, contactHandle );
  }
}



// Parse an error command
void MsnSwitchboardConnection::parseError( const QStringList& command, const QByteArray &payloadData )
{
  // TODO: Check if any payload is delivered for these errors: it may contain
  // info about the error. For 215 it may specify which contact was invited
  // twice.
  if ( command[0] == "215" )
  {
    kmWarning() << "A contact was invited twice!";
  }
  else if ( command[0] == "712" )
  {
    kmWarning() << "The SB session is overloaded.";
  }
  else if( command[0] == "216" || command[0] == "217" )
  {
    // The contact is invisible now, send Offline IMs
    if( backgroundConnection_ )
    {
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
      kmDebug() << "Ignoring a 'contact is offline/invisible' error in background chats.";
#endif
      return;
    }

    // CAL invite failed.
    // Make sure sendMimeMessageWhenReady() does not hang on this state.
    if( connectionState_ == SB_INVITING_CONTACTS && contactsInChat_.isEmpty() )
    {
      connectionState_ = SB_CONTACTS_LEFT;
    }
  }
  else if ( command[0] == "282" )
  {
    // Got it once when I sent a bad P2P message or something.
    kmWarning() << "got the mysterious 282 error response (contacts=" << contactsInChat_ << ").";
  }
  else if ( command[0] == "911" )
  {
    kmDebug() << "authentication failed.";
  }
  else
  {
    // Relay the error detection to the base class
    MsnConnection::parseError( command, payloadData );
  }
}



// Parse a message command
void MsnSwitchboardConnection::parseMimeMessage(const QStringList& command, const MimeMessage &message)
{
  // Get the message type from the head
  const QString contentType( message.getSubValue( "Content-Type" ) );
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
  kmDebug() << "Received mime message of type '" << contentType << "'.";
#endif

  // Get the sender's handle and friendly name
  const QString contactHandle( command[1] );
  QString  friendlyName;
  QString  contactPicture;

  // Get the contact details
  ContactBase *contact = currentAccount_->getContactByHandle( contactHandle );
  if( contact == 0 )
  {
    // There was no current friendly name, so get one from the message
    friendlyName = QUrl::fromPercentEncoding( command[2].toUtf8() );
  }
  else
  {
    // get name from contact
    friendlyName   = contact->getFriendlyName( STRING_ORIGINAL );
    contactPicture = contact->getContactPicturePath();
  }


  // Link a chat window to this switchboard if it's needed.
  if( backgroundConnection_ )
  {
    if( contentType == "text/plain"
    ||  contentType == "text/x-msnmsgr-datacast"
    ||  contentType == "image/gif"
    ||  contentType == "application/x-ms-ink" )
    {
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
      kmDebug() << "chat is still in the background, requesting chat window.";
#endif

      // We've just received a message over a background connection: whoever
      // had started it initially, consider it as started by a contact.
      // That's because if we send a message first, the chat is immediately
      // changed to a foreground one.
      userStartedChat_ = false;

      backgroundConnection_ = false;
      emit requestChatWindow( this );
      emit contactJoinedChat( contact );
    }
  }


  if( contentType == "text/plain" )
  {
    // A normal chat message
    parseChatMessage( contactHandle, friendlyName, contactPicture, message );
  }
  else if( contentType == "text/x-msmsgscontrol" )
  {
    // A typing notification
    parseTypingMessage( contactHandle, message );
  }
  else if( contentType == "text/x-msmsgsinvite" )
  {
    // This is a mime application message, the old format for invitations.
    // Extract the actual MIME message from the body of the Mime container, pass it to the ChatMaster/ApplicationList.
    const MimeMessage& subMessage = message.getBody();
    emit gotMessage( subMessage, contactHandle );
  }
  else if( contentType == "application/x-msnmsgrp2p" )
  {
    // This is an p2p message, the new format for invitations.
    parseP2PMessage( contactHandle, message );
  }
  else if( contentType == "text/x-mms-emoticon" || contentType == "text/x-mms-animemoticon" )
  {
    // Message contains the MSN objects for the emoticons.
    parseEmoticonMessage( contactHandle, message.getBody() );
  }
  else if( contentType == "text/x-msnmsgr-datacast" )
  {
    // This is a datacast message, contact wants to send a nudge or voice clip
    const MimeMessage& subMessage = message.getBody();
    parseDatacastMessage( contactHandle, subMessage );
  }
  else if( contentType == "image/gif" )
  {
    // This is a GIF ink message.
    // Source: http://msdn.microsoft.com/en-us/library/ms818340.aspx
    // warning: WLM doesn't even seem to adhere to that standard...
    emit gotInkMessage( message.getBody(), contactHandle, FORMAT_GIF );
  }
  else if( contentType == "application/x-ms-ink" )
  {
    // This is an ISF (Ink Serialized Format) ink message.
    emit gotInkMessage( message.getBody(), contactHandle, FORMAT_ISF );
  }
  else if( contentType == "text/x-clientcaps" )
  {
    // This is a message exchanged by a lot of third party clients.
    parseClientCapsMessage( contactHandle, message );
  }
  else if( contentType == "text/x-keepalive" )
  {
#ifdef KMESSDEBUG_SWITCHBOARD_KEEPALIVE
    kmDebug() << "Keep alive message from " << contactHandle << ".";
#endif
  }
  else
  {
    kmDebug() << "got unhandled message type (type=" << contentType << " contact=" << contactHandle << ").";
  }

  // Messages which are part of the switchboard session management, will not be considered "activity"
  if( contentType != "text/x-clientcaps" && contentType != "text/x-keepalive" )
  {
    // Signal presence of activity
    activity();
  }
}



// Parse a payload command
void MsnSwitchboardConnection::parsePayloadMessage(const QStringList &command, const QByteArray &/*payload*/)
{
  // Switchboard has no payload commands yet.
  // This method is added because the functionality is generic in the base class.
  kmWarning() << "Unhandled payload command: " << command[0] << "!";
}



// Parse a p2p message, used for invitations
void MsnSwitchboardConnection::parseP2PMessage( const QString &contactHandle, const MimeMessage &message )
{
  // The switchboad is a "broadcast" channel for all messages,
  // so in a multi-chat the message could be for an other contact.
  const QString p2pDest( message.getValue("P2P-Dest") );
  if( p2pDest.isEmpty() )
  {
    // P2P dest is empty if we produce an error when a session is not initiated yet (MSNSLP header also has To: <msnmsgr:> set)
    // Also seen with amsn 0.97 once.
    kmWarning() << "Unable to handle P2P message, P2P-Dest field is empty "
                  "(contact=" << contactHandle << ")." << endl;
    return;
  }
  else if( p2pDest != currentAccount_->getHandle() )
  {
    // Ignore messages ment for other contacts
#ifdef KMESSDEBUG_SWITCHBOARD_P2P
    kmDebug() << "Received a P2P message, but it's for '" << p2pDest << "'.";
#endif
    return;
  }

  // Extract the actual P2P message from the body of the Mime container.
  // Dispatch the message to the central ApplicationList of the Contact (maintained by ChatMaster).
  const P2PMessage& subMessage = message.getBinaryBody();
  emit gotMessage( subMessage, contactHandle );
}



// Parse a contact is typing message.
void MsnSwitchboardConnection::parseTypingMessage( const QString &/*contactHandle*/, const MimeMessage &message )
{
  // Avoid crashes due race conditions
  if( closingConnection_ )
  {
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
    kmDebug() << "Not emitting typing message because switchboard is closing.";
#endif
    return;
  }

  // The contact informs it's typing a normal message
  const QString typingUser( message.getValue( "TypingUser" ) );

  ContactBase *contact = currentAccount_->getContactByHandle( typingUser );

  if( KMESS_NULL(contact) )
  {
    return;
  }

  emit contactTyping( contact );
}



// Deliver a message for an P2PApplication object.
void MsnSwitchboardConnection::sendApplicationMessage( const MimeMessage &message )
{
#ifdef KMESSTEST
  KMESS_ASSERT( ! isBusy() );
#endif

  // Check which type of message is sent
  const QString contentType( message.getValue("Content-Type") );
  if( contentType == "application/x-msnmsgrp2p" )
  {
    // Message contains binary P2P message data
#ifdef KMESSTEST
    KMESS_ASSERT( message.getBinaryBody().size() >= 52 );  // header is 48, footer is 4
#endif

    sendMimeMessageWhenReady( ACK_ALWAYS_P2P, message );
  }
  else if( contentType == "text/x-msmsgsinvite"
           ||  contentType.section(";", 0, 0) == "text/x-msmsgsinvite" )  // for ; charset= suffix
  {
    // Message contains old-style invitation fields.
    sendMimeMessageWhenReady( ACK_NAK_ONLY, message );
  }
  else
  {
    kmWarning() << "unknown message type '" << contentType << "', can't send message!" << endl;
    return;
  }

  // Signal the presence of activity on this switchboard
  activity();
}


/**
 * Send a message to the contact(s)
 *
 * If there is at least one custom emoticon in our message, also send a message to tell our contact's clients
 * that they have to download from us the corresponding pictures.
 */
void MsnSwitchboardConnection::sendChatMessage( const QString& text )
{
  int maxSendableSingleMessageLength = 1400;

  if ( currentAccount_ == 0 )
  {
    kmWarning() << "currentAccount_ is null!";
    return;
  }

  // Check if any custom emoticon is being sent in the message.
  // This has to be sent first so the receiving client will be aware that
  // there will be custom emoticons in the next message.
  QString code;
  QString pictureFile;
  QString emoticonObjects;
  QStringList addedEmoticons;
  int lastPos = 0;
  int matchStart = 0;
  EmoticonManager*              manager           = EmoticonManager::instance();
  const QRegExp&                emoticonRegExp    = manager->getPattern( true );
  const QHash<QString,QString>& emoticonPictures  = manager->getFileNames( true );
  const QString&                emoticonThemePath = manager->getThemePath( true );

  // We'll loop until we reach the end of the string or there are no more emoticons to parse
  if( ! emoticonRegExp.isEmpty() )
  {
    while( true )
    {
      // First find if there's any custom emoticon
      matchStart = emoticonRegExp.indexIn( text, lastPos );
      if( matchStart == -1 )
      {
        break;
      }

      // Find out what emoticon has matched
      code = text.mid( matchStart, emoticonRegExp.matchedLength() );

      // Find where the emoticon code ends and the image corresponding to that code
      lastPos = matchStart + emoticonRegExp.matchedLength();
      pictureFile = emoticonPictures[ code ];

      // Do not add emoticons to the list more than once
      if( addedEmoticons.contains( code ) )
      {
        continue;
      }

      // We cannot send more than 7 different custom emoticons in each message: skip the other ones.
      // TODO add some visual confirmation or message about this.
      if( addedEmoticons.count() >= 7 )
      {
        emit showWarning( WARNING_TOO_MANY_EMOTICONS, 0 );
        break;
      }

      addedEmoticons.append( code );

      // NOTE: Behavior changed since WLM 8+, emoticon data must be sent every time.
      // Before, we could send any emoticon's msnobject just once per session.

      // No match? Strange.. but go on anyways
      if( pictureFile.isEmpty() )
      {
#ifdef KMESSDEBUG_SWITCHBOARD_EMOTICONS
        kmDebug() << "Custom emoticon '" << code << "' not found!";
#endif

        continue;
      }

#ifdef KMESSDEBUG_SWITCHBOARD_EMOTICONS
      kmDebug() << "Found custom emoticon '" << code << "' which file is '"
                << ( emoticonThemePath + pictureFile ) << "'." << endl;
#endif

      QFile iFile( emoticonThemePath + pictureFile );
      if( ! iFile.open( QIODevice::ReadOnly ) )
     {
#ifdef KMESSDEBUG_SWITCHBOARD_EMOTICONS
        kmDebug() << "Unable to read picture '" <<  pictureFile << "'!";
#endif
        iFile.close();
        continue;
      }

      // Read the file and create an MSNObject of the emoticon
      const QByteArray data( iFile.readAll() );
      iFile.close();
      MsnObject test( currentAccount_->getHandle(), pictureFile, QString::null, MsnObject::EMOTICON, data );

      // Only divide items between each other
      if( ! emoticonObjects.isEmpty() )
      {
        emoticonObjects += "\t";
      }

      emoticonObjects += code + "\t" + test.objectString();
    }
  }

  // Don't send the message if there is no emoticon to send
  if( ! emoticonObjects.isEmpty() )
  {
    MimeMessage emoticonMessage;
    emoticonMessage.addField("MIME-Version",    "1.0");
    emoticonMessage.addField("Content-Type",    "text/x-mms-emoticon");
    emoticonMessage.setBody( emoticonObjects );

    sendMimeMessageWhenReady( ACK_NAK_ONLY, emoticonMessage );
  }

  // Then send the real text message

  // Get the formatting properties and convert them to MSN's IM format
  const QFont&  font      = currentAccount_->getFont();
  const QString fontFamily( QUrl::toPercentEncoding( font.family() ) );
  const QString color     ( convertHtmlColorToMsnColor( currentAccount_->getFontColor() ) );

  // Determine effects
  QString effects;
  if( font.bold()      ) effects += "B";
  if( font.italic()    ) effects += "I";
  if( font.underline() ) effects += "U";

  // Determine text direction
  const QString rtl( text.isRightToLeft() ? "; RL=1" : "" );

  // Create the message
  MimeMessage message;
  message.addField("MIME-Version",    "1.0");
  message.addField("Content-Type",    "text/plain; charset=UTF-8");
  message.addField("X-MMS-IM-Format", "FN=" + fontFamily + "; EF=" + effects + "; CO=" + color + "; CS=0; PF=0" + rtl);
  message.setBody(text);

  // Send the message
  int bodyLength = text.toUtf8().length();
  if( bodyLength > maxSendableSingleMessageLength )
  {
    // Check if the remote clients support the recieving of huge messages.
    ContactBase *contact;
    bool capable = true;
    foreach( const QString &handle, contactsInChat_ )
    {
      contact = currentAccount_->getContactByHandle( handle );
      if( ! contact->hasCapability( ContactBase::MSN_CAP_MULTI_PACKET ) )
      {
        capable = false;
        break;
      }
    }

    if( capable )
    {
      // Create a multipacket message
      MultiPacketMessage multiMessage( message );
      sendMimeMessageWhenReady( ACK_NAK_ONLY, multiMessage );
    }
    else
    {
      // Split the huge message and send all parts
      int pos = 0;
      int prevPos = 0;
      QString messagePart;
      const QString& body = message.getBody();

      int parts = (int) ceil( ( (float) bodyLength / (float) maxSendableSingleMessageLength ) );

      for( int i = 0; i < parts - 1; i++ )
      {
        prevPos = pos;
        pos += maxSendableSingleMessageLength;

        // search backwards from the new split position.
        int spacePos = body.lastIndexOf( " ", pos );

        messagePart = body.mid( prevPos, pos - prevPos );

        if( spacePos > prevPos )
        {
          // use that one!
          pos++;
        }

        // send messagePart
        sendMimeMessageWhenReady( ACK_NAK_ONLY, message );
      }

      messagePart = body.mid( pos, body.length() - pos );

      sendMimeMessageWhenReady( ACK_NAK_ONLY, message );
    }
  }
  else
  {
    // Send the simple MimeMessage
    sendMimeMessageWhenReady( ACK_NAK_ONLY, message );
  }

  // Signal the presence of activity on this switchboard
  activity();
}



// Send a ink to the contact(s)
void MsnSwitchboardConnection::sendInk( InkFormat format, const QByteArray& ink )
{
  bool capable = true;
  ContactBase *contact;

  int inkCapabilityFlag;
  switch( format )
  {
    case FORMAT_ISF:
      inkCapabilityFlag = ContactBase::MSN_CAP_INK_ISF;
      break;
    case FORMAT_GIF:
      inkCapabilityFlag = ContactBase::MSN_CAP_INK_GIF;
      break;
  }

  // Verify whether all contacts are capable to view this Ink message or not
  ContactBase *unsupportingContact = 0;
  foreach( const QString &handle, contactsInChat_ )
  {
    contact = currentAccount_->getContactByHandle( handle );
    if( ! contact->hasCapability( ContactBase::MSN_CAP_MULTI_PACKET )
    ||  ! contact->hasCapability( (ContactBase::MsnClientCapabilities)inkCapabilityFlag ) )
    {
      unsupportingContact = contact;
      capable = false;
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
      kmDebug() << "Contact" << handle << "does not support our wink. "
               << "Has multipacket?" << contact->hasCapability( ContactBase::MSN_CAP_MULTI_PACKET )
               << "Has this ink type support?" << contact->hasCapability( (ContactBase::MsnClientCapabilities)inkCapabilityFlag );
#endif
      break;
    }
  }

  if( ! capable )
  {
    emit showWarning( WARNING_INK_UNSUPPORTED_BY_CONTACT, unsupportingContact );

    // If the only contact in chat doesn't support it, don't send it at all.
    if( contactsInChat_.count() == 1 )
    {
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
      kmDebug() << "None of the contacts support the wink, aborting.";
#endif
      return;
    }
  }

  // Create the Ink message
  MimeMessage message;
  message.addField( "MIME-Version", "1.0" );

  switch( format )
  {
    case FORMAT_ISF:
      // Source: http://msdn.microsoft.com/en-us/library/ms818340.aspx
      // According to the specs we'd also need to send:
      // message.addField( "Content-Transfer-Encoding", "base64" );
      // but even WLM itself doesn't do that.

      message.addField( "Content-Type", "application/x-ms-ink" );
      break;
    case FORMAT_GIF:
      message.addField( "Content-Type", "image/gif" );
      break;
  }

  message.setBody( "base64:" + QString( ink.toBase64() ) );

  // Send the ink packet, if necessary within a Multi Packet message
  if( message.getBody().size() > 1400 )
  {
    MultiPacketMessage multiPartMessage( message );

    sendMimeMessageWhenReady( MsnConnection::ACK_NAK_ONLY, multiPartMessage );
  }
  else
  {
    sendMimeMessageWhenReady( MsnConnection::ACK_NAK_ONLY, message );
  }
}



// Send a client caps message to the contacts
void MsnSwitchboardConnection::sendClientCaps()
{
  // All third-party clients send this message.
  // It also makes debugging easier.
  MimeMessage message;
  message.addField("MIME-Version", "1.0");
  message.addField("Content-Type", "text/x-clientcaps");
  message.setBody( "Client-Name: KMess/" KMESS_VERSION "\r\n" );
  sendMimeMessageWhenReady( ACK_NONE, message );
}



// Send a "ping" to avoid MSN closing the connection
void MsnSwitchboardConnection::sendKeepAlive()
{
  // Stop the keepalive sending if it's passed too much time
  if( keepAlivesRemaining_ < 1 )
  {
#ifdef KMESSDEBUG_SWITCHBOARD_KEEPALIVE
    kmDebug() << "Session has expired, letting it timeout.";
#endif

    if( keepAliveTimer_ != 0 )
    {
      keepAliveTimer_->stop();
    }

    // Close background connections immediately when the timer expires
    if( backgroundConnection_ )
    {
      closeConnectionLater( true );
    }

    return;
  }
  else
  {
    keepAlivesRemaining_--;
  }

  // Do not send keepalives for background connections, there's no need for it
  if( backgroundConnection_ )
  {
#ifdef KMESSDEBUG_SWITCHBOARD_KEEPALIVE
    kmDebug() << "Not sending keep alives for background connections.";
#endif
    return;
  }

  // Sending messages to keep a connection open makes no sense if there's no connection, isn't it
  if( ! isConnected() )
  {
#ifdef KMESSDEBUG_SWITCHBOARD_KEEPALIVE
    kmDebug() << "Cannot send keep alive while disconnected!";
#endif
    return;
  }

  // Also check if there actually is a receiver for the message
  if( contactsInChat_.isEmpty() )
  {
#ifdef KMESSDEBUG_SWITCHBOARD_KEEPALIVE
    kmDebug() << "Stopping keep alive, no contacts in chat.";
#endif

    keepAliveTimer_->stop();
    return;
  }

#ifdef KMESSDEBUG_SWITCHBOARD_KEEPALIVE
  kmDebug() << "Sending a keep alive message," << keepAlivesRemaining_ << "left before expiration.";
#endif

  // Build the keepalive message
  MimeMessage message;
  message.addField( "MIME-Version", "1.0" );
  message.addField( "Content-Type", "text/x-keepalive" );

  sendMimeMessage( ACK_NONE, message );
}



void MsnSwitchboardConnection::sendMimeMessageWhenReady( AckType ackType, MultiPacketMessage &message )
{
  while( ! message.isComplete() )
  {
    sendMimeMessageWhenReady( ackType, message.getNextPart() );
  }
}



// Send a message to the contact(s), or leave it pending until a connection is restored
void MsnSwitchboardConnection::sendMimeMessageWhenReady(AckType ackType, const MimeMessage &message)
{
  // If the connection is ready, send the message
  if( isConnected() && ! contactsInChat_.empty() )
  {
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
    kmDebug() << "Sending mime message of type '" << message.getValue("Content-Type") << "'.";
#endif
#ifdef KMESSTEST
    KMESS_ASSERT( connectionState_ == SB_CHAT_STARTED );
#endif

    const QString contentType( message.getSubValue( "Content-Type" ) );

    if( backgroundConnection_ )
    {
      if( contentType == "text/plain"
          ||  contentType == "text/x-msnmsgr-datacast"
          ||  contentType == "image/gif"
          ||  contentType == "application/x-ms-ink" )
      {
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
        kmDebug() << "chat is still in the background, requesting chat window.";
#endif
        backgroundConnection_ = false;
        emit requestChatWindow( this );
      }
    }

    // Send and store for acknowledgement.
    int ack = sendMimeMessage(ackType, message);
    storeMessageForAcknowledgement(ack, ackType, message);

    // Clean cache of old entries, it does not need to run with every storeMessage..() call.
    cleanUnackedMessages();

    return;
  }

  // There's no active connection, store as pending message
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
  kmDebug() << "Chat is not active, queueing message to send later.";
  kmDebug() << "Status info -- Connected:" << isConnected()
           << "Contacts in chat:" << contactsInChat_
           << "Status code:" << connectionState_;
#endif

  // Store the message
  QPair<AckType,MimeMessage> *newPendingMessage = new QPair<AckType,MimeMessage>( ackType, message );
  pendingMessages_.append( newPendingMessage );


  // See what needs to be done to restore the chat.
  if( isConnected() )
  {
    switch( connectionState_ )
    {
      case SB_AUTHORIZING:
        // Connected but contacts are not in the chat yet.
        // See if we need to invite the contacts

#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
        kmDebug() << "Already waiting for the connection to be authorized...";
#endif
        return;

      case SB_CONNECTING:
        // Still waiting for a connection to the switchboard server.
        // This condition verifies when we're connected but the USR has not been sent yet:
        // without it, isConnected() returns true, so any unsent message gets sent before USR, causing a disconnection.

#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
        kmDebug() << "Already waiting for connection to establish...";
#endif
        return;

      case SB_INVITING_CONTACTS:
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
        kmDebug() << "Already waiting for contacts to join the chat...";
#endif
        return;

      default:
#ifdef KMESSTEST
        KMESS_ASSERT( connectionState_ == SB_CONTACTS_LEFT );
#endif
        if( contactsInChat_.count() > 1 )
        {
          kmWarning() << "failed to re-connect; multiple contacts are left in chat!";
          return;
        }

        if( message.hasField("P2P-Dest") && message.getValue("P2P-Dest") != lastContact_ )
        {
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
          kmDebug() << "All contacts left the chat, calling the contact from the P2P-Dest field.";
#endif
          connectionState_ = SB_INVITING_CONTACTS;
          sendCommand( "CAL", message.getValue("P2P-Dest") );
        }
        else
        {
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
          kmDebug() << "All contacts left the chat, calling the last contact left.";
#endif

          if( lastContact_.isEmpty() )
          {
            kmWarning() << "Switchboard failed to re-connect; no contact left to invite!";
            return;
          }

          connectionState_ = SB_INVITING_CONTACTS;
          sendCommand( "CAL", lastContact_ );
        }

        break;
    }

    return;
  } // if( isConnected() )


  // There is no active connection.
  switch( connectionState_ )
  {
    case SB_REQUESTING_CHAT:
      // The connection was closed, request a new one from the notification server.
      // It's not possible to simply re-open it, because we need a new authcookie for the USR command.

#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
      kmDebug() << "Already waiting for MsnNotificationConnection to request a new chat...";
#endif
      return;

    case SB_CHAT_STARTED:
      // Chat was open but the connection has been closed:
      // this condition verifies in case of a network disconnect, when we've received a BYE or all contacts
      // have left but the connection state has not been changed yet, and other similar improbable events.
      // Simply restore it.

#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
      kmDebug() << "Connection was closed unexpectedly.";
#endif

      // Reset the connection's status
      connectionState_ = SB_DISCONNECTED;
      break; // Will run the connection reestablishment code below

    case SB_DISCONNECTED:
    {
      // Send offline messages if the contact is not online

      // Should never happen, but you never know.
      if( lastContact_.isEmpty() )
      {
        kmWarning() << "Cannot find a contact to send offline messages to.";
        return;
      }

      const ContactBase *contact = currentAccount_->getContactByHandle( lastContact_ );

      // Only send the offline message if the contact definitely appears to be offline.
      // If it's online, we'll just reestablish a switchboard connection to it.
      if( ! contact || ! contact->isOffline() )
      {
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
        kmDebug() << "Not sending offline message to an" << (contact?"online":"unknown") << "contact.";
#endif
        break;
      }

      // Only send chat messages
      const QString contentType( message.getValue( "Content-Type" ) );
      if( ! contentType.startsWith( "text/plain" ) )
      {
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
        kmDebug() << "Ignoring message of type" << contentType << "in an offline messaging session.";
#endif
        pendingMessages_.removeAll( newPendingMessage );
        return;
      }

#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
      kmDebug() << "Sending offline message...";
#endif
      // Only load the OIM service on demand
      if( offlineImService_ == 0 )
      {
        offlineImService_ = new OfflineImService( this );

        connect( offlineImService_, SIGNAL( sendMessageFailed(const QString&,const MimeMessage&) ),
                 this,              SIGNAL(     sendingFailed(const QString&,const MimeMessage&) ) );
      }

      // Send the message (only the text contained in the body)
      offlineImService_->sendMessage( lastContact_, message.getBody() );

      // The message has been sent as an OIM, remove it to avoid it
      // being sent twice
      pendingMessages_.removeAll( newPendingMessage );
      return;
    }

    default:
      // For other states, just try reestablishing a connection.

      break;
  } // Switch for connectionState_ ends here

#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
  kmDebug() << "Connection is closed, requesting a new chat from the notification server...";
#endif
#ifdef KMESSTEST
  KMESS_ASSERT( connectionState_ == SB_DISCONNECTED );
#endif

  connectionState_ = SB_REQUESTING_CHAT;

  // Request a new switchboard session
  emit requestNewSwitchboard( lastContact_ );
}



// Send messages that weren't sent because a contact had to be re-called
void MsnSwitchboardConnection::sendPendingMessages()
{
  // Do nothing if we are not connected.
  if( ! isConnected() || connectionState_ != SB_CHAT_STARTED )
  {
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
    kmDebug() << "Skipping message sending, the connection is not ready.";
#endif
    return;
  }

  // A contact should be connected already, but check to make sure
  // The switchboard should still be (re-)initializing
  if( contactsInChat_.isEmpty() )
  {
    kmWarning() << "No contacts available in the chat.";
    return;
  }

  // Send all messages
  QPair<AckType,MimeMessage> *pendingMessage; // doesn't seam to fit in foreach()
  foreach( pendingMessage, pendingMessages_ )
  {
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
    kmDebug() << "Sending pending messages, " << ( pendingMessages_.count() -1 ) << " remaining.";
#endif

    // Send the message
    int ack = sendMimeMessage( pendingMessage->first, pendingMessage->second );
    storeMessageForAcknowledgement( ack, pendingMessage->first, pendingMessage->second );
  }

  // Clear the pending messages
  qDeleteAll( pendingMessages_ );
  pendingMessages_.clear();

  // Clean cache of old entries, it does not need to run with every storeMessage..() call.
  cleanUnackedMessages();
}



// The user is typing so send a typing message
void MsnSwitchboardConnection::sendTypingMessage()
{
  if( contactsInChat_.isEmpty() )
  {
    if( lastContact_.isEmpty() )
    {
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
      kmDebug() << "Not sending typing notification, no contacts in chat, nor one to re-invite.";
#endif
      return;
    }


    // When we need to re-invite a contact to send the typing message, see if the contact is offline
    // This avoids repeated "the contact is offline" typing messages.
    const ContactBase *contact = currentAccount_->getContactByHandle(lastContact_);
    if( contact == 0 || contact->isOffline() )
    {
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
      kmDebug() << "Not sending typing notification, last contact is offline.";
#endif
      return;
    }
  }

#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
  kmDebug() << "Sending typing notification.";
#endif

  // Build the typing notification message
  MimeMessage message;
  message.addField("MIME-Version", "1.0");
  message.addField("Content-Type", "text/x-msmsgscontrol");
  message.addField("TypingUser",   currentAccount_->getHandle());

  // if disconnected, reconnect to send the typing message
  // (maybe the other contact still has it's window open)
  sendMimeMessageWhenReady(ACK_NONE, message);

  // Signal the presence of activity on this switchboard
  activity();
}



// Send wink
void MsnSwitchboardConnection::sendWink( const MsnObject& msnobject )
{
  MimeMessage message;
  message.addField("MIME-Version", "1.0");
  message.addField("Content-Type", "text/x-msnmsgr-datacast");
  message.setBody( "ID: 2\r\nData: " + msnobject.objectString() + "\r\n" );

  MultiPacketMessage multiMessage( message );
  sendMimeMessageWhenReady( ACK_NAK_ONLY, multiMessage );
}



// Set true if the user request the starting of the chat
void MsnSwitchboardConnection::setUserStartedChat( bool startedByUser )
{
  userStartedChat_ = startedByUser;
}


// An ApplicationList object indicated it aborted all it's applications.
void MsnSwitchboardConnection::slotApplicationsAborted(const QString &handle)
{
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
  kmDebug() << "All applications of contact '" << handle << "' have been aborted.";
#endif
#ifdef KMESSTEST
  KMESS_ASSERT( abortingApplications_ );
#endif

  // Remove the contact from the chat now.
  contactsInChat_.removeAll(handle);

  // Disconnect from signal source again.
  ContactBase *contact = currentAccount_->getContactByHandle(handle);
  if( ! KMESS_NULL(contact) )
  {
    if( ! KMESS_NULL(contact->getApplicationList()) )
    {
      disconnect(contact->getApplicationList(), SIGNAL(      applicationsAborted(const QString&) ),
                 this,                          SLOT  (  slotApplicationsAborted(const QString&) ));
    }

    // Remove the reference to the switchboard here or we could get crashes later!
    contact->removeSwitchboardConnection(this, true);
  }


  // If all contacts have aborted, close connection.
  if( contactsInChat_.isEmpty() )
  {
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
    kmDebug() << "No other contacts need to abort, closing connection.";
#endif

    abortingApplications_ = false;
    closeConnection();

    // If the switchboard should clean up afterwards, do so.
    if( autoDeleteLater_ )
    {
      this->deleteLater();
    }
  }
  else
  {
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
    kmDebug() << "Waiting for applications of " << contactsInChat_.count() << " other contacts to abort...";
#endif
  }
}



// The connection was established, so send the version command.
void MsnSwitchboardConnection::slotConnected()
{
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
  kmDebug() << "Connected to server, sending authentication.";
#endif
#ifdef KMESSTEST
  KMESS_ASSERT( connectionState_ == SB_CONNECTING || connectionState_ == SB_REQUESTING_CHAT );
#endif

  connectionState_ = SB_AUTHORIZING;

  if( userStartedChat_ )
  { // This is a user-initiated chat
    // Set the usr information to the server.
    sendCommand( "USR", currentAccount_->getHandle() + " " + authorization_ );
  }
  else
  { // This is a contact-initiated chat
    // Answer the chat with the authorization
    sendCommand( "ANS", currentAccount_->getHandle() + " " + authorization_ + " " + chatId_ );
  }
}



/**
 * @brief Shows error dialog boxes
 *
 * All switchboard errors are annoying and mostly useless. So we ignore most of them.
 *
 * @param  error  The error reason or explanation.
 * @param  type   The type of error.
 */
void MsnSwitchboardConnection::slotError( QString error, MsnSocketBase::ErrorType type )
{
  kmWarning() << "MSN Switchboard Connection error type" << type << " (contacts=" << contactsInChat_ << "):" << error;

  // Decide what kind of message to show
  switch( type )
  {
    case MsnSocketBase::ERROR_DROP:
      // FIXME Completely unreliable, at least with WLM.
      // Only warn the user if the chat had recent activity. Cannot use isExpired() here
      // because it also checks if we're connected, but here we're not.
//       if( keepAliveTimer_ == 0 || keepAlivesRemaining_ > 0 )
//       {
//         emit showWarning( WARNING_CONNECTION_DROP, 0 );
//       }

      // Then disconnect
      closeConnection();
      break;

    default:
      // Other errors are simply ignored :>
      break;
  }
}



// Send a nudge to a contact
void MsnSwitchboardConnection::sendNudge()
{
  // Create the message
  MimeMessage message;
  message.addField( "MIME-Version", "1.0" );
  message.addField( "Content-Type", "text/x-msnmsgr-datacast" );
  message.setBody( "ID: 1\r\n" );  // ID 1 indicates it's a nudge

  // Sent the message, re-establishing the connection if it was lost.
  sendMimeMessageWhenReady( ACK_NAK_ONLY, message );

  // Signal the presence of activity on this switchboard
  activity();
}



// Start a switchboard connection
void MsnSwitchboardConnection::start( const ChatInformation &chatInfo )
{
#ifdef KMESSTEST
  KMESS_ASSERT( currentAccount_ != 0 );
#endif

  if(isConnected())
  {
    if( ! contactsInChat_.isEmpty() && ! isExclusiveChatWithContact(chatInfo.getContactHandle()))
    {
      kmWarning() << "already connected, can't start new chat with '" << chatInfo.getContactHandle() << "'!" << endl;
      return;
    }

#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
    kmDebug() << "Resuming a connection with a different server, reconnecting.";
#endif
    closeConnection();
  }

  // Remove any contact from this switchboard session.
  if( ! contactsInChat_.isEmpty() )
  {
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
      kmDebug() << "Deleting active contacts: " << contactsInChat_.join(",");
#endif

    // Make sure all contacts are removed, which will also abort their applications in ApplicationList.
    ContactBase *contact;
    foreach( const QString &handle, contactsInChat_ )
    {
      contact = currentAccount_->getContactByHandle( handle );
      if( KMESS_NULL(contact) ) continue;
      contact->removeSwitchboardConnection(this, true);  // could cause applications to abort.
    }

    contactsInChat_.clear();
  }

  // Prepare the class for the new connection, setting some values to good defaults
  // sentEmoticons_.clear(); // NOTE: Behavior changed since WLM 8+, emoticon data must be sent every time.
  closingConnection_  = false;
  firstContact_       = // Assign chatInfo.getContactHandle() to both first and last contact.
  lastContact_        = chatInfo.getContactHandle();

  // If requested, start an offline connection instead of a normal one
  if( chatInfo.getType() == ChatInformation::CONNECTION_OFFLINE )
  {
#ifdef KMESSTEST
    KMESS_ASSERT( connectionState_ == SB_DISCONNECTED );
#endif

#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
    kmDebug() << "Initializing offline connection with " << chatInfo.getContactHandle() << ".";
#endif

    // lastContact_ gets set even when making offline connections, so isExclusiveChatWithContact() returns true;
    // otherwise a chat window would be spawned for every offline message.

    userStartedChat_      = chatInfo.getUserStartedChat();
    backgroundConnection_ = false;

    // Ask for a chat window; if one already exists, it will be raised.
    emit requestChatWindow( this );
    return;
  }


#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
  kmDebug() << "Initializing connection with " << chatInfo.getContactHandle()
            << ": Connecting to SB " << chatInfo.getIp() << ":" << chatInfo.getPort() << "." << endl;
#endif

  // Store information from the chatinfo object
  authorization_        = chatInfo.getAuthorization();
  chatId_               = chatInfo.getChatId();
  userStartedChat_      = chatInfo.getUserStartedChat();

  // Connect to the server.
#ifdef KMESS_NETWORK_WINDOW
  KMESS_NET_INIT(this, "SB " + chatInfo.getIp());
#endif

  connectionState_ = SB_CONNECTING;
  connectToServer( chatInfo.getIp(), chatInfo.getPort() );

  // Link a chat window to this switchboard if it's needed.
  if( backgroundConnection_ && chatInfo.getType() == ChatInformation::CONNECTION_CHAT )
  {
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
    kmDebug() << "requesting chat window for initiated chat invitation.";
#endif
    emit requestChatWindow( this );
    backgroundConnection_ = false;
  }
}



// Store a message for later acknowledgement
void MsnSwitchboardConnection::storeMessageForAcknowledgement(int ack, AckType ackType, const MimeMessage& message)
{
  // don't store if the ack-type indicates so
  if(ackType == ACK_NONE) return;

  // Only update pending ack list when we'll always get an ack back.
  if(ackType == ACK_ALWAYS || ackType == ACK_ALWAYS_P2P)
  {
    acksPending_++;
  }

  // Create a record of the unacked message
  UnAckedMessage* unAcked = new UnAckedMessage();
  unAcked->ackType = ackType;
  unAcked->time    = QDateTime::currentDateTime().toTime_t();
  unAcked->message = message;  // no problem with data size, uses shared reference.

  // Add to QHash
  unAckedMessages_.insert( ack, unAcked );

#ifdef KMESSDEBUG_SWITCHBOARD_ACKS
  kmDebug() << "Stored message for acknowledgement. "
            << "There are currently " << unAckedMessages_.count() << " messages kept, "
            << acksPending_ << " need to be ACKed." << endl;
#endif
}



#include "msnswitchboardconnection.moc"
