Logo Search packages:      
Sourcecode: gaim-librvp version File versions  Download package

rvp.c

/*
 *  File: rvp.c - Gaim RVP plug-in implementation.
 *  Copyright (C) 2003  Weihua Sun (weihua@lucent.com)
 *  Updated 2005/2006 Waider <waider@waider.ie>
 *
 *  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
 */

#define GAIM_PLUGINS /* this switches the GAIM_INIT_PLUGIN declaration
                        from in-line build to outboard build */

#include "rvp.h"

#include <netdb.h>
#include <iconv.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <arpa/nameser.h>
#ifdef HAVE_ARPA_NAMESER_COMPAT_H
# include <arpa/nameser_compat.h>
#endif
#include <resolv.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>

#if TIME_WITH_SYS_TIME
# include <sys/time.h>
# include <time.h>
#else
# if HAVE_SYS_TIME_H
#  include <sys/time.h>
# else
#  include <time.h>
# endif
#endif

#include "random.h"

/* gaim includes */
#include <accountopt.h>
#include <conversation.h>
#include <debug.h>
#include <ft.h>
#include <gtkutils.h>
#include <network.h>
#include <notify.h>
#include <plugin.h>
#include <pluginpref.h>
#include <privacy.h>
#include <request.h>
#include <server.h>
#include <util.h>
#include <version.h>
#include <xmlnode.h>

/* Let's do the compatibility dance!
   My aim here is that the 'default' code should be Gaim 2 compatible,
   and that any hacks to enable compatibility - macros, etc. - should
   be to make gaim-2 bits appear palatable to gaim-1.
*/
#if GAIM_MAJOR_VERSION < 2
/* gack. they dropped this. the digest-auth code is incomplete anyway */
#include <md5.h>

/* this is actually pretty sane */
#define GAIM_CONV_TYPE_CHAT GAIM_CONV_CHAT
#define GaimMenuAction GaimBlistNodeAction
#define GaimMessageFlags GaimConvImFlags
#define gaim_menu_item_new( label, callback, thing1, thing2 ) \
  gaim_blist_node_action_new( label, callback, thing1 )
#define gaim_find_conv_with_account( type, recip, ac ) \
  gaim_find_conversation_with_account( recip, ac )

/* this is sort of silly, but allows me to handle function signature changes */
#define rvpleconst
#define rvpxrr_size ssize_t
#define rvpxrr_buf char

/* wrapper for old version */
gboolean rvp_network_listen_range( int low, int high, int type /* NOTUSED */,
                                   void (*callback)( int, gpointer ),
                                   gpointer data ) {
  int sockfd = gaim_network_listen_range( low, high );

  if ( sockfd == -1 ) {
    return FALSE;
  }

  callback( sockfd, data );

  return TRUE;
}

#include <internal.h> /* this is here for the gettext bits */
#else

#define rvp_network_listen_range gaim_network_listen_range
#define gaim_menu_item_new( label, callback, thing1, thing2 ) \
  gaim_menu_action_new( label, GAIM_CALLBACK(callback), thing1, thing2 )
#define gaim_find_conv_with_account( type, recip, ac ) \
  gaim_find_conversation_with_account( type, recip, ac )

#define rvpleconst const
#define rvpxrr_size gssize
#define rvpxrr_buf guchar

/* ripped from internal.h */
#ifdef ENABLE_NLS
#  include <locale.h>
#  include <libintl.h>
#  define _(x) ((const char *)gettext(x))
#  ifdef gettext_noop
#    define N_(String) gettext_noop (String)
#  else
#    define N_(String) (String)
#  endif
#else
#  include <locale.h>
#  define N_(String) (String)
#  ifndef _
#    define _(x) ((const char *)x)
#  endif
#  define ngettext(Singular, Plural, Number) ((Number == 1) ? ((const char *)Singular) : ((const char *)Plural))
#endif

/* why isn't this exported ? */
#define BUF_LEN 2048
#endif

#ifdef DEBUG
#include <libxml/debugXML.h>
#endif

static char HEX_ELM[17] = { '0','1','2','3','4','5','6','7',
                            '8','9','A','B','C','D','E','F','\0' };

static int rvp_send_notify( GaimConnection *, const char *, gint, gchar *,
                            void * );
static GaimFetchUrlData *rvp_send_request( GaimConnection *, gchar *, gchar ** );
static void rvp_add_buddy( GaimConnection *, GaimBuddy *, GaimGroup * );
static void rvp_set_buddy_status( GaimConnection *, gchar *, char * );
static const char *rvp_normalize( const GaimAccount *, const char * );
static void rvp_parse_principal( gchar *, gchar **, gchar ** );
static gchar *rvp_get_sessid( void );

/* async url stuff */
static gint rvp_request_timeout( gpointer );
static void rvp_async_data( void *, GaimFetchUrlData *, size_t );
static void url_fetched_cb( gpointer, gint, GaimInputCondition );
static void destroy_fetch_url_data( GaimFetchUrlData * );

static void rvp_add_permit(GaimConnection *, const char *);
static void rvp_set_acl( GaimConnection *, gchar *, guint16, guint16 );
static void rvp_set_away_old( GaimConnection *, const char *, const char * );
static void rvp_chat_user_left( GaimConnection *, int, GaimBuddy * );

static void rvp_xfer_cancel_send( GaimXfer * );

/* header structure for file transfer blocks */
typedef struct _msnftphdr {
  guint8 done;
  guint8 sizelsb;
  guint8 sizemsb;
} msnftphdr;

static away_t awaymsgs[RVP_UNKNOWN];

/*
 * This horror is just a temporary hack until I get around to redoing
 * it properly... right now I just want the SRV record ASAP.
 *
 * correct version:
 * 1. asynchronous, if possible. see the main gaim source
 *    1a. need to handle callbacks for this, which complicates code
 *        elsewhere
 * 2. need to keep track of use of SRV records and balance them
 *    accordingly. This is the intention of the unused 'last' param.
 * 3. cache what we've learned and short circuit responses. NB TTL!
 */
/* record type: 2 bytes
   record class: 2 bytes
   TTL: 4 bytes
   data length: 2 bytes */
typedef struct _rechdr { /* offset */
  guint16 type;          /* + 0 */
  guint16 class;         /* + 2 */
  guint32 ttl;           /* + 4 */
  guint16 length;        /* + 8 */
} rechdr;

/* SRV record:
   priority: 2 bytes
   weight: 2 bytes
   port: 2 bytes
   target: remainder
*/
typedef struct _srv {/* offset */
  guint16 priority;  /* + 10   */
  guint16 weight;    /* + 12   */
  guint16 port;      /* + 14   */
  guchar *target;    /* + 16   */
} srv_t;

/* cache for SRV records */
static GHashTable *srvcache = NULL;

/*
 * fetch a srv record for SRVNAME; LAST, if set, is the one we used
 * last time around. (needed for priority)
 * http://www.ietf.org/rfc/rfc2782.txt
 * http://www.ietf.org/rfc/rfc2181.txt [referenced]
 * absolutely have to do at some point: async this FIXME
 */
srvrec *gethostbysrv( char *srvname, srvrec *last ) {
  HEADER *hdr;
  unsigned char answer[1024];
  int len = 0;
  char name[1024];
  srvrec *retval = NULL;
  unsigned char *blob;
  int i, l;

  if ( srvcache == NULL ) {
    gaim_debug_misc( __FUNCTION__, "Created SRV cache\n" );
    srvcache = g_hash_table_new( g_str_hash, g_str_equal );
  }

  if (( retval = (srvrec *)g_hash_table_lookup( srvcache, srvname ))
      != NULL ) {
    if ( retval->expiry > time( NULL )) {
      return retval;
    } else {
      gaim_debug_misc( __FUNCTION__, "cached %s expired\n", srvname );
      retval = NULL;
    }
  } else {
    gaim_debug_misc( __FUNCTION__, "%s not in SRV cache\n", srvname );
  }

  bzero( answer, 1024 );
  bzero( name, 1024 );

  _res.options |= RES_DEBUG;
  if (( len = res_search( srvname, ns_c_in, ns_t_srv, answer, sizeof( answer )))
      != -1 ) {
    hdr = (HEADER *)answer;
    if ( ntohs( hdr->ancount ) == 0 ) {
      gaim_debug_warning( __FUNCTION__, "res_search: no records found\n" );
      goto out;
    }

    blob = &answer[HFIXEDSZ];

    for ( i = 0; i < ntohs( hdr->qdcount ); i++ ) {
      l = dn_expand( answer, answer + len, blob, name, 1024 );
      if ( l < 0 ) {
        gaim_debug_error( __FUNCTION__, "dn_expand failed (1)\n" );
        goto out;
      }
      blob += l + QFIXEDSZ;
    }

    for ( i = 0; i < ntohs( hdr->ancount ); i++ ) {
      rechdr *header;
      srv_t *server;

      l = dn_expand( answer, answer + len, blob, name, 1024 );
      if ( l < 0 ) {
        gaim_debug_error( __FUNCTION__, "dn_expand failed (2)\n" );
        goto out;
      }

      blob += l;

      header = (rechdr *)&blob[0];
      server = (srv_t *)&blob[10]; /* errr. magic number. */

      l = dn_expand( answer, answer + len, blob + 16, name, 1024 );
      if ( l < 0 ) {
        gaim_debug_error( __FUNCTION__, "dn_expand failed (3)\n" );
        goto out;
      }

      retval = g_new0( srvrec, 1 );
      retval->host = g_strdup( name );
      retval->port = ntohs( server->port );

      retval->expiry = time( NULL ) + ntohl( header->ttl );
    }
  } else {
    gaim_debug_error( __FUNCTION__, "res_search for %s failed (%s)\n",
                      srvname, strerror( errno ));
  }

 out:
  if ( retval == NULL ) {
    retval = g_new0( srvrec, 1 );
    retval->host = NULL;
    retval->port = 0;
    retval->expiry = 0; /* expires immediately */
  }

  g_hash_table_replace( srvcache, g_strdup( srvname ), retval );

  return retval;
}
/* end of horror */

/*
 * debug functions to dump out a buddy structure
 */
static void rvp_dump_acl( const gchar *caller, guint16 acl ) {
  if ( acl & RVP_ACL_ASSERTION )
    gaim_debug_misc( caller, "CRED: assertion\n" );
  if ( acl & RVP_ACL_DIGEST )
    gaim_debug_misc( caller, "CRED: digest\n" );
  if ( acl & RVP_ACL_NTLM )
    gaim_debug_misc( caller, "CRED: ntlm\n" );

  if ( acl & RVP_ACL_LIST )
    gaim_debug_misc( caller, "ACL: list\n" );
  if ( acl & RVP_ACL_READ )
    gaim_debug_misc( caller, "ACL: read\n" );
  if ( acl & RVP_ACL_WRITE )
    gaim_debug_misc( caller, "ACL: write\n" );
  if ( acl & RVP_ACL_SEND_TO )
    gaim_debug_misc( caller, "ACL: send-to\n" );
  if ( acl & RVP_ACL_RECEIVE_FROM )
    gaim_debug_misc( caller, "ACL: receive-from\n" );
  if ( acl & RVP_ACL_READACL )
    gaim_debug_misc( caller, "ACL: readacl\n" );
  if ( acl & RVP_ACL_WRITEACL )
    gaim_debug_misc( caller, "ACL: writeacl\n" );
  if ( acl & RVP_ACL_PRESENCE )
    gaim_debug_misc( caller, "ACL: presence\n" );
  if ( acl & RVP_ACL_SUBSCRIPTIONS )
    gaim_debug_misc( caller, "ACL: subscriptions\n" );
  if ( acl & RVP_ACL_SUBSCRIBE_OTHERS )
    gaim_debug_misc( caller, "ACL: subscribe-others\n" );
}

static void rvp_dump_buddy( const gchar *caller, GaimBuddy *buddy ) {
  if ( buddy != NULL ) {
    RVPBuddy *rbud = buddy->proto_data;

    gaim_debug_misc( caller, "---------------\n" );
    gaim_debug_misc( caller,     "ptr       %p (rvp @ %p)\n", buddy,
                     rbud );
    gaim_debug_misc( caller,     "buddy     %s\n", buddy->name );
    if ( buddy->alias != NULL )
      gaim_debug_misc( caller,   "alias     %s\n", buddy->alias );
    if ( buddy->server_alias != NULL )
      gaim_debug_misc( caller,   "srvalias  %s\n", buddy->server_alias );

    if ( rbud != NULL ) {
      if ( rbud->email != NULL )
        gaim_debug_misc( caller, "email     %s\n", rbud->email );
      if ( rbud->principal != NULL )
        gaim_debug_misc( caller, "principal %s\n", rbud->principal );
      if ( rbud->sessionid != NULL )
        gaim_debug_misc( caller, "sessionid %s\n", rbud->sessionid );
      if ( rbud->subs_id )
        gaim_debug_misc( caller, "subs id   %d\n", rbud->subs_id );
      if ( rbud->state != NULL )
        gaim_debug_misc( caller, "state     %s (%p)\n", rbud->state,
                         rbud->state );

      rvp_dump_acl( caller, rbud->acl );
    }
  }
  gaim_debug_misc( caller, "---------------\n" );
}

/*
 * Free up the RVP portions of a buddy structure. Probably incomplete.
 */
static void rvp_free_buddy( GaimBuddy *buddy ) {
  RVPBuddy *rb;

  if ( buddy == NULL ) {
    /* wtf? */
    return;
  }

  if (( rb = buddy->proto_data ) == NULL ) {
    return;
  }

  if ( rb->principal != NULL ) {
    g_free( rb->principal );
  }

  if ( rb->sendcookies != NULL ) {
    g_hash_table_destroy( rb->sendcookies );
  }

  if ( rb->recvcookies != NULL ) {
    g_hash_table_destroy( rb->recvcookies );
  }

  g_free( rb );
  buddy->proto_data = NULL; /* avoid nasty accidents */
}

/*
 * used in logout: set buddy offline and clean out RVP data
 */
static void rvp_cleanup( GaimConnection *gc, GaimBuddy *buddy ) {
  if ( buddy->name != NULL ) { /* should be, but let's be careful */
    rvp_set_buddy_status( gc, buddy->name, "offline" );
  }
  rvp_free_buddy( buddy );
}

static void rvp_close_unsubscribe( GaimConnection *gc, GaimBuddy *buddy ) {
  RVPBuddy *rb = buddy->proto_data;
  gchar *args [1];
  GaimFetchUrlData *gfud;
  RVPData *rd = gc->proto_data;

  if ( rb != NULL && rb->principal != NULL ) {
    args[0] = (gchar *)buddy;
    gfud = rvp_send_request( gc, "UNSUBSCRIBE", args );

    zero_time;
    while( !g_hash_table_lookup( rd->pending, gfud )) {
      while( gtk_events_pending()) {
        report_sent( gfud );
        gtk_main_iteration();

        /* race condition */
        if ( rb->subs_id == 0 ) {
          break;
        }
      }
    }
  }
}

static void rvp_conv_closed( GaimConnection *gc, const char *who ) {
  gaim_debug_misc( __FUNCTION__, "Enter\n" );
  gaim_debug_misc( __FUNCTION__, "Exit\n" );
}

/*
 * keep track of conversations with people who aren't on the buddy list
 */
static GaimBuddy *rvp_get_temp_buddy1( GaimConnection *gc,
                                       GaimBuddy *buddy,
                                       gboolean createp ) {
  RVPData *rd = gc->proto_data;
  RVPBuddy *rb;
  GaimBuddy *retval = NULL;

  if ( rd->nonbuddy == NULL ) {
    gaim_debug_misc( __FUNCTION__, "creating hash table\n" );
    rd->nonbuddy = g_hash_table_new( g_str_hash, g_str_equal );
  }

  rb = buddy->proto_data;

  /* we may have to populate the nick field if I've been lazy */
  if ( buddy->name == NULL ) {
    gchar *nick = rindex( rb->principal, '/' );
    if ( nick != NULL ) {
      buddy->name = g_strdup( &nick[1] );
    }
  }

  /* these are edge cases which I should really catch elsewhere */
  if ( rb->buddy == NULL ) {
    rb->buddy = buddy;
  }

  if ( buddy->proto_data == NULL ) {
    buddy->proto_data = rb;
  }

  /* special case: me */
  if ( !strcmp( rb->principal, rd->principal )) {
    return rd->me.buddy;
  }

  retval = (GaimBuddy *)g_hash_table_lookup( rd->nonbuddy, rb->principal );
  if ( retval == NULL ) {
    if ( createp == TRUE ) {
      gaim_debug_misc( __FUNCTION__, "didn't find %s, creating at %p\n",
                       rb->principal, buddy );
      g_hash_table_insert( rd->nonbuddy, g_strdup( rb->principal ), buddy );
      retval = buddy;
    }
  } else {
    gaim_debug_misc( __FUNCTION__, "found %s in hash at %p\n", rb->principal,
                     retval );
  }

  return retval;
}

static GaimBuddy *rvp_get_temp_buddy_create( GaimConnection *gc,
                                             GaimBuddy *buddy ) {
  GaimBuddy *retval = rvp_get_temp_buddy1( gc, buddy, TRUE );

  if ( retval != NULL ) { /* and it shouldn't be */
  } else {
    gaim_debug_error( __FUNCTION__, "Doh. NULL buddy created?\n" );
  }

  return retval;
}

static GaimBuddy *rvp_get_temp_buddy( GaimConnection *gc, GaimBuddy *buddy ) {
  return rvp_get_temp_buddy1( gc, buddy, FALSE );
}

/*
 * parse an XML fragment identifying a contact or contacts
 */
static GaimBuddy **parse_contact( xmlDocPtr doc, xmlNodePtr from ) {
  xmlNodePtr contact = from;
  GaimBuddy **blist = NULL;
  gint bcount = 0;

  while( contact ) {
    xmlNodePtr ptr = contact->xmlChildrenNode;
    GaimBuddy *buddy = NULL;

    while( ptr ) {
      if ( !xmlStrcmp( ptr->name, BAD_CAST "href" )) {
        xmlChar *href = xmlNodeListGetString( doc, ptr->xmlChildrenNode, 1 );
        if ( href != NULL ) {
          gchar *ptr;
          if ( buddy == NULL ) {
            buddy = g_new0( GaimBuddy, 1 );
          }
          buddy->proto_data = g_new0( RVPBuddy, 1 );
          ((RVPBuddy *)buddy->proto_data)->buddy = buddy;
          ((RVPBuddy *)buddy->proto_data)->principal =
            g_strdup( (gchar *)href );

          ptr = buddy->name;
          rvp_parse_principal( (gchar *)href, &buddy->name, NULL );
          if ( buddy->name != ptr ) {
            g_free( ptr );
          }

          xmlFree( href );
        }
      } else if ( !xmlStrcmp( ptr->name, BAD_CAST "description")) {
        xmlChar *desc = xmlNodeListGetString( doc, ptr->xmlChildrenNode, 1 );
        if ( desc != NULL ) {
          if ( buddy == NULL ) {
            buddy = g_new0( GaimBuddy, 1 );
          }
          buddy->server_alias = g_strdup((gchar *)desc );
          xmlFree( desc );
        }
      }

      ptr = ptr->next;
    }

    blist = g_realloc( blist, sizeof( GaimBuddy *) * ( bcount + 1 ));
    blist[bcount] = buddy;
    bcount++;

    contact = contact->next;
  }

  /* null terminate */
  blist = g_realloc( blist, sizeof( GaimBuddy *) * ( bcount + 1 ));
  blist[bcount] = NULL;

  if ( bcount > 1 ) {
    gaim_debug_misc( __FUNCTION__, "multiple contacts found!\n" );
  }

  return blist;
}

/*
 * build an xml fragment representing a contact
 */
static xmlNodePtr build_contact( gchar *host, gchar *nick, gchar *desc,
                                 gchar *principal ) {
  xmlNodePtr contact, node, content;
  gchar *url;

  if ( principal == NULL ) {
    url = g_strdup_printf( "http://%s/instmsg/aliases/%s", host, nick );
  } else {
    url = g_strdup( principal );
  }
  contact = xmlNewNode( NULL, BAD_CAST "r:contact" );
  node = xmlNewNode( NULL, BAD_CAST "d:href" );
  content = xmlNewText( BAD_CAST url );
  xmlAddChild( node, content );
  xmlAddChild( contact, node );

  node = xmlNewNode( NULL, BAD_CAST "r:description" );
  if ( desc != NULL ) {
    content = xmlNewText( BAD_CAST desc );
    xmlAddChild( node, content );
  }
  xmlAddChild( contact, node );

  return contact;
}

/*
 * call a function for all buddies
 */
static void rvp_do_for_buddies( GaimConnection *gc,
                                void (*func)( GaimConnection *,
                                              GaimBuddy * )) {
  GaimBuddyList *blist;
  GaimBlistNode *group, *cnode, *bnode;
  GaimBuddy *buddy;

  blist = gaim_get_blist();
  for ( group = blist->root; group; group = group->next ) {
    for ( cnode = group->child; cnode; cnode = cnode->next ) {
      if ( !GAIM_BLIST_NODE_IS_CONTACT(cnode)) {
        continue;
      }
      for ( bnode = cnode->child; bnode; bnode = bnode->next ) {
        if (!GAIM_BLIST_NODE_IS_BUDDY( bnode )) {
          continue;
        }

        buddy = (GaimBuddy *)bnode;

        /* only deal with things attached to my account! */
        if ( buddy->account != gaim_connection_get_account( gc )) {
          continue;
        }

        func( gc, buddy );
      }
    }
  }
}


/*
 * clear out the specified session ID from any buddies that have it
 */
static void rvp_clear_sessid( GaimConnection *gc, gchar *sessid ) {
  GaimBuddyList *blist;
  GaimBlistNode *group, *cnode, *bnode;
  GaimBuddy *retval = NULL;

  blist = gaim_get_blist();
  for ( group = blist->root; group; group = group->next ) {
    for ( cnode = group->child; cnode; cnode = cnode->next ) {
      if ( !GAIM_BLIST_NODE_IS_CONTACT(cnode)) {
        continue;
      }
      for ( bnode = cnode->child; bnode; bnode = bnode->next ) {
        RVPBuddy *target;

        if (!GAIM_BLIST_NODE_IS_BUDDY( bnode )) {
          continue;
        }
        retval = (GaimBuddy *)bnode;

        if ( retval->account != gaim_connection_get_account( gc )) {
          continue;
        }

        target = retval->proto_data;

        /*
         * async notifications can cause this to be triggered
         * before we've set up the proto_data for the buddies
         */
        if ( target == NULL ) {
          continue;
        }

        if ( target->sessionid != NULL &&
             !strcmp( target->sessionid, sessid )) {
          g_free( target->sessionid );
          target->sessionid = NULL;
        }
      }
    }
  }
}

/*
 * look up a buddy by rvp name, principal, or session-id
 */
static GaimBuddy *rvp_find_buddy( GaimConnection *gc, gchar *name,
                                  gchar *principal, gchar *session ) {
  GaimBuddyList *blist;
  GaimBlistNode *group, *cnode, *bnode;
  GaimBuddy *retval = NULL;

  blist = gaim_get_blist();
  for ( group = blist->root; group; group = group->next ) {
    for ( cnode = group->child; cnode; cnode = cnode->next ) {
      if ( !GAIM_BLIST_NODE_IS_CONTACT(cnode)) {
        continue;
      }
      for ( bnode = cnode->child; bnode; bnode = bnode->next ) {
        RVPBuddy *target;

        if (!GAIM_BLIST_NODE_IS_BUDDY( bnode )) {
          continue;
        }
        retval = (GaimBuddy *)bnode;

        if ( retval->account != gaim_connection_get_account( gc )) {
          continue;
        }

        if ( name != NULL ) {
          if ( !strcmp( retval->name, name )) {
            goto gotcha;
          } else {
            continue;
          }
        }

        /*
         * Anything below here requires that the RVPBuddy structure
         * is valid.
         */
        target = retval->proto_data;

        /*
         * async notifications can cause this to be triggered
         * before we've set up the proto_data for the buddies
         */
        if ( target == NULL ) {
          continue;
        }

        if ( principal != NULL ) {
          if ( target->principal != NULL &&
               !strcmp( target->principal, principal )) {
            goto gotcha;
          }
        }

        if ( session != NULL ) {
          if ( target->sessionid != NULL &&
               !strcmp( target->sessionid, session )) {
            goto gotcha;
          }
        }
      }
    }
  }

  /* avoid nasty accidents */
  retval = NULL;

 gotcha:
  return retval;
}

#define rvp_find_buddy_by_name( g, n ) rvp_find_buddy( g, n, NULL, NULL )
#define rvp_find_buddy_by_principal( g, p ) rvp_find_buddy( g, NULL, p, NULL )
#define rvp_find_buddy_by_session( g, s ) rvp_find_buddy( g, NULL, NULL, s )

/*
 * parse the head of an XML response. ROOT is the expected root node.
 */
static xmlDocPtr parse_preamble( const gchar *buffer, gint size,
                                 const gchar *root, xmlNodePtr *cur ) {
  xmlDocPtr doc = NULL;

#ifdef LOUD
    gaim_debug_misc( __FUNCTION__, "parsing:\n" );
    gaim_debug_misc( __FUNCTION__, "%*.*s\n", size, size, buffer );
#endif

  /* by preference, use xmlReadMemory as it can be told not to whine
     about the invalid 'DAV:' namespace */
#ifdef HAVE_XMLREADMEMORY
  if (( doc = xmlReadMemory( buffer, size, NULL, NULL, XML_PARSE_NOWARNING )) == NULL ) {
    gaim_debug_error( __FUNCTION__, "Document not parsed successfully.\n" );
    return NULL;
  }
#else
  if (( doc = xmlParseMemory( buffer, size )) == NULL ) {
    gaim_debug_error( __FUNCTION__, "Document not parsed successfully.\n" );
    return NULL;
  }
#endif

  if (( *cur = xmlDocGetRootElement( doc )) == NULL ) {
    gaim_debug_error( __FUNCTION__, "empty document\n" );
    xmlFreeDoc( doc );
    return NULL;
  }

  if ( xmlStrcmp( (*cur)->name, BAD_CAST root )) {
    gaim_debug_error( __FUNCTION__, "unexpected doc root\n" );
    xmlFreeDoc( doc );
    return NULL;
  }

  return doc;
}

/*
 * Parse a SUBSCRIPTIONS response to get our buddy list
 */
static void parse_subscriptions( GaimConnection *gc, GaimFetchUrlData *gfud ) {
  xmlDocPtr doc;
  xmlNodePtr cur, subscription;

  gaim_debug_misc( __FUNCTION__, "enter\n" );

  if (( doc = parse_preamble( gfud->response.webdata, gfud->len,
                              "subscriptions", &cur )) == NULL ) {
    goto out;
  }

  if (( subscription = cur->xmlChildrenNode ) == NULL ) {
    gaim_debug_misc( __FUNCTION__, "no subscriptions\n" );
    goto out;
  }

  while( subscription != NULL ) {
    xmlNodePtr ptr = subscription->xmlChildrenNode;
    gint subs_id, timeout;
    gchar *principal = NULL, *href = NULL;

#ifdef DEBUG
    xmlDebugDumpNode( stderr, subscription, TRUE );
#endif

    while( ptr ) {
      if ( !xmlStrcmp( ptr->name, BAD_CAST "subscription-id" )) {
        xmlChar *sid = xmlNodeListGetString( doc, ptr->xmlChildrenNode, 1 );
        subs_id = atoi((gchar *)sid );
      } else if ( !xmlStrcmp( ptr->name, BAD_CAST "href" )) {
        href = (gchar *)xmlNodeListGetString( doc, ptr->xmlChildrenNode, 1 );
      } else if ( !xmlStrcmp( ptr->name, BAD_CAST "timeout" )) {
        xmlChar *to = xmlNodeListGetString( doc, ptr->xmlChildrenNode, 1 );
        timeout = atoi((gchar *)to );
      } else if ( !xmlStrcmp( ptr->name, BAD_CAST "principal" )) {
        xmlNodePtr rvpp = ptr->xmlChildrenNode;
        if ( rvpp != NULL && !xmlStrcmp( rvpp->name,
                                         BAD_CAST "rvp-principal" )) {
          principal =
            (gchar *)xmlNodeListGetString( doc, rvpp->xmlChildrenNode, 1 );
        }
      } else {
        /* unknown item, ignore */
        gaim_debug_error( __FUNCTION__, "what's a %s?\n", (gchar *)ptr->name );
      }

      ptr = ptr->next;
    }

    /* this is the only thing we really need to retrieve the
       subscription */
    if ( principal != NULL ) {
      GaimBuddy *buddy;
      gchar *ptr;
      gboolean temp = FALSE;

      /* hmm. this doesn't currently recognise ME... */
      if (( buddy = rvp_find_buddy_by_principal( gc, principal )) == NULL ) {
        buddy = g_new0( GaimBuddy, 1 );
        temp = TRUE;
      }

      if ( buddy->proto_data == NULL ) {
        buddy->proto_data = g_new0( RVPBuddy, 1 );
        ((RVPBuddy *)buddy->proto_data)->buddy = buddy;
      }

      ((RVPBuddy *)buddy->proto_data)->principal = g_strdup( principal );

      if ( temp ) {
        buddy = rvp_get_temp_buddy_create( gc, buddy );
      }

      /* funnily enough, doing this causes all manner of
         trouble. don't do it. */
      /* ((RVPBuddy *)buddy->proto_data)->subs_id = subs_id;*/

      ptr = buddy->name;
      rvp_parse_principal( principal, &buddy->name, NULL );
      if ( buddy->name != ptr ) {
        g_free( ptr );
      }
    } else {
      gaim_debug_error( __FUNCTION__, "principal null\n" );
    }

    subscription = subscription->next;
  }

 out:
  if ( doc != NULL ) {
    xmlFree( doc );
  }

  gaim_debug_misc( __FUNCTION__, "out\n" );
}

/*
 * Parse an ACL collection
 */
static void parse_acls( GaimConnection *gc, GaimFetchUrlData *gfud ) {
  GaimAccount *ac;
  RVPData *rd = gc->proto_data;
  xmlDocPtr doc;
  xmlNodePtr cur, acl, ace;

  if (( doc = parse_preamble( gfud->response.webdata, gfud->len, "rvpacl",
                              &cur )) == NULL ) {
    return;
  }

  if (( acl = cur->xmlChildrenNode ) == NULL ) {
    gaim_debug_error( __FUNCTION__, "no acls\n" );
    goto out;
  }

  ac = gaim_connection_get_account( gc );

  while( acl ) {
    ace = acl->xmlChildrenNode;

    while( ace ) {
      GaimBuddy *buddy = NULL;
      xmlChar *rvp_principal = NULL;
      xmlNodePtr credentials = NULL;
      xmlNodePtr bits = ace->xmlChildrenNode;

      /* skip the 'inheritance: none' crap */
      if ( xmlStrcmp( ace->name, BAD_CAST "ace" )) {
        goto loop;
      }

      /* the horror, the horror */
      if ( !xmlStrcmp( bits->name, BAD_CAST "principal" )) {
        xmlNodePtr pbits = bits->xmlChildrenNode;

        while( pbits != NULL ) {
          if ( !xmlStrcmp( pbits->name, BAD_CAST "rvp-principal" )) {
            if (( rvp_principal =
                  xmlNodeListGetString( doc, pbits->xmlChildrenNode, 1 ))
                != NULL ) {
              buddy = rvp_find_buddy_by_principal( gc,
                                                   (gchar *)rvp_principal );

              if ( buddy == NULL ) {
                /* need to fake enough of a GaimBuddy structure to
                   keep this happy, which maybe I should do
                   elsewhere... */
                GaimBuddy *fake = g_new0( GaimBuddy, 1 );
                RVPBuddy *faker = g_new0( RVPBuddy, 1 );
                fake->proto_data = faker;
                ((GaimBlistNode *)fake)->type = GAIM_BLIST_BUDDY_NODE;
                faker->principal = (gchar *)rvp_principal;
                buddy = rvp_get_temp_buddy_create( gc, fake );
              }
            }
          } else if ( !xmlStrcmp( pbits->name, BAD_CAST "credentials" )) {
            credentials = pbits->xmlChildrenNode;
          } else if ( !xmlStrcmp( pbits->name, BAD_CAST "allprincipals" )) {
            GaimBuddy *fake = g_new0( GaimBuddy, 1 );
            RVPBuddy *faker = g_new0( RVPBuddy, 1 );
            fake->proto_data = faker;
            ((GaimBlistNode *)fake)->type = GAIM_BLIST_BUDDY_NODE;
            faker->principal = g_strdup( "allprincipals" );
            fake->server_alias = g_strdup( "Other Exchange users" );
            fake->name = g_strdup( "allprincipals" );
            buddy = rvp_get_temp_buddy_create( gc, fake );
          }
          pbits = pbits->next;
        }
      }

      if ( buddy ) { /* this should be a guaranteed */
        RVPBuddy *rbuddy = buddy->proto_data;
        xmlNodePtr grants = NULL, denies = NULL;

        while( bits ) {
          if ( !xmlStrcmp( bits->name, BAD_CAST "grant" )) {
            grants = bits->xmlChildrenNode;
          } else if ( !xmlStrcmp( bits->name, BAD_CAST "deny" )) {
            denies = bits->xmlChildrenNode;
          }
          bits = bits->next;
        }

        /* acls are absolute, not cumulative, so start with an empty
           acl */
        rbuddy->acl = 0;

        while ( credentials ) {
          if ( !xmlStrcmp( credentials->name, BAD_CAST "assertion" )) {
            rbuddy->acl |= RVP_ACL_ASSERTION;
          } else if ( !xmlStrcmp( credentials->name, BAD_CAST "digest" )) {
            rbuddy->acl |= RVP_ACL_DIGEST;
          } else if ( !xmlStrcmp( credentials->name, BAD_CAST "ntlm" )) {
            rbuddy->acl |= RVP_ACL_NTLM;
          }

          credentials = credentials->next;
        }

        while ( grants ) {
          if ( !xmlStrcmp( grants->name, BAD_CAST "list" )) {
            rbuddy->acl |= RVP_ACL_LIST;
          } else if ( !xmlStrcmp( grants->name, BAD_CAST "read" )) {
            rbuddy->acl |= RVP_ACL_READ;
          } else if ( !xmlStrcmp( grants->name, BAD_CAST "write" )) {
            rbuddy->acl |= RVP_ACL_WRITE;
          } else if ( !xmlStrcmp( grants->name, BAD_CAST "send-to" )) {
            rbuddy->acl |= RVP_ACL_SEND_TO;
          } else if ( !xmlStrcmp( grants->name, BAD_CAST "receive-from" )) {
            rbuddy->acl |= RVP_ACL_RECEIVE_FROM;
          } else if ( !xmlStrcmp( grants->name, BAD_CAST "readacl" )) {
            rbuddy->acl |= RVP_ACL_READACL;
          } else if ( !xmlStrcmp( grants->name, BAD_CAST "writeacl" )) {
            rbuddy->acl |= RVP_ACL_WRITEACL;
          } else if ( !xmlStrcmp( grants->name, BAD_CAST "presence" )) {
            rbuddy->acl |= RVP_ACL_PRESENCE;
          } else if ( !xmlStrcmp( grants->name, BAD_CAST "subscriptions" )) {
            rbuddy->acl |= RVP_ACL_SUBSCRIPTIONS;
          } else if ( !xmlStrcmp( grants->name,
                                  BAD_CAST "subscribe-others" )) {
            rbuddy->acl |= RVP_ACL_SUBSCRIBE_OTHERS;
          } else {
            gaim_debug_warning( __FUNCTION__, "unrecognised ACL %s\n",
                                grants->name );
          }

          grants = grants->next;
        }

        while ( denies ) {
          if ( !xmlStrcmp( denies->name, BAD_CAST "list" )) {
            rbuddy->acl &= ~RVP_ACL_LIST;
          } else if ( !xmlStrcmp( denies->name, BAD_CAST "read" )) {
            rbuddy->acl &= ~RVP_ACL_READ;
          } else if ( !xmlStrcmp( denies->name, BAD_CAST "write" )) {
            rbuddy->acl &= ~RVP_ACL_WRITE;
          } else if ( !xmlStrcmp( denies->name, BAD_CAST "send-to" )) {
            rbuddy->acl &= ~RVP_ACL_SEND_TO;
          } else if ( !xmlStrcmp( denies->name, BAD_CAST "receive-from" )) {
            rbuddy->acl &= ~RVP_ACL_RECEIVE_FROM;
          } else if ( !xmlStrcmp( denies->name, BAD_CAST "readacl" )) {
            rbuddy->acl &= ~RVP_ACL_READACL;
          } else if ( !xmlStrcmp( denies->name, BAD_CAST "writeacl" )) {
            rbuddy->acl &= ~RVP_ACL_WRITEACL;
          } else if ( !xmlStrcmp( denies->name, BAD_CAST "presence" )) {
            rbuddy->acl &= ~RVP_ACL_PRESENCE;
          } else if ( !xmlStrcmp( denies->name, BAD_CAST "subscriptions" )) {
            rbuddy->acl &= ~RVP_ACL_SUBSCRIPTIONS;
          } else if ( !xmlStrcmp( denies->name,
                                  BAD_CAST "subscribe-others" )) {
            rbuddy->acl &= ~RVP_ACL_SUBSCRIBE_OTHERS;
          } else {
            gaim_debug_warning( __FUNCTION__, "unrecognised ACL %s\n",
                                denies->name );
          }

          denies = denies->next;
        }

#ifdef DEBUG
        rvp_dump_buddy( __FUNCTION__, buddy );
#endif

        if ( !strcmp( rbuddy->principal, "allprincipals" )) {
          gaim_debug_misc( __FUNCTION__, "generic ACL\n" );
          memcpy( &(rd->defaultacl), &(rbuddy->acl), sizeof( guint16 ));
          rvp_dump_acl( __FUNCTION__, rd->defaultacl );

          if (( rd->defaultacl & RVP_ACL_BUDDY ) == RVP_ACL_BUDDY ) {
            /* "Allow all buddies except" */
            ac->perm_deny = GAIM_PRIVACY_DENY_USERS;
            /* this would be nice, but Gaim makes it not work because it
               normalises the second param. I may be able to hack at
               this through the privacyops stuff */
            /* gaim_privacy_permit_add( ac, "Other Exchange users",
               TRUE ); */
          } else {
            ac->perm_deny = GAIM_PRIVACY_ALLOW_USERS;
            /* gaim_privacy_deny_add( ac, "Other Exchange users", TRUE
               ); */
          }
        } else {
          if (( rbuddy->acl & RVP_ACL_BUDDY ) == RVP_ACL_BUDDY ) {
            /* we're parsing off the server, so don't bother sending
               data back */
            gaim_privacy_permit_add( ac, buddy->name, TRUE );
          } else {
            gaim_privacy_deny_add( ac, buddy->name, TRUE );
          }

          /* make sure any changes get reflected in the buddy list -
             of course, first make sure the buddy is in the buddy list... */
          if ( rvp_find_buddy_by_principal( gc, (gchar *)rvp_principal )) {
            if (((GaimBlistNode*)buddy)->type == GAIM_BLIST_BUDDY_NODE ) {
              gaim_blist_update_buddy_icon( buddy );
            } else {
              gaim_debug_misc( __FUNCTION__, "%s: not a buddy (%d)\n",
                               buddy->name, ((GaimBlistNode*)buddy)->type );
            }
          }
        }
      }

    loop:
      ace = ace->next;
    }

    acl = acl->next;
  }

 out:
  xmlFreeDoc( doc );
}

#ifdef LOUD
void dumpheader( gpointer key, gpointer value, gpointer userdata ) {
  gaim_debug_misc( (gchar *)userdata, "%s: %s\n", (gchar *)key,
                   (gchar *)value );
}
#endif

/*
 * extract a given header from the header block and return its
 * content. by side-effect parses the header block into a hash so that
 * subsequent lookups are faster.
 * partly RFC-compliant, FWIW
 */
static gchar *get_header_content( gchar *hblock, gchar *header,
                                  GHashTable **parsedheaders ) {
  gchar **headers = NULL;

  /* duuuude! */
  if ( parsedheaders == NULL ) {
    abort();
  }

  if ( *parsedheaders == NULL ) {
    *parsedheaders = g_hash_table_new( g_str_hash, g_str_equal );

    /* technically \r\n, but be lenient in what you accept &c. */
    headers = g_strsplit( hblock, "\n", 0 );

    if ( headers != NULL ) {
      gint i;
      gchar **bits;
      for ( i = 0; headers[ i ]; i++ ) {
        gchar *key;
        gchar *content = NULL;
        gchar *oldcontent;

        /* shouldn't happen */
        if ( strlen( headers[ i ] ) == 0 ) {
          continue;
        }

        /* end of headers */
        if ( headers[i][0] == '\r' ) {
          break;
        }

        bits = g_strsplit( headers[ i ], ":", 2 );

        if ( bits == NULL ) {
          continue; /* malformed header */
        }

        key = g_ascii_strdown( bits[ 0 ], strlen( bits[ 0 ] ));

        if ( bits[1] != NULL ) {
          /* clean up the content */
          /* NB g_strstrip works in-place */
          content = g_strdup( g_strstrip( bits[ 1 ] ));

          /* RFC 822: Unfolding is accomplished by regarding CRLF
             immediately followed by a LWSP-char as equivalent to the
             LWSP-char. */
          while( headers[ i + 1 ] ) {
            if ( headers[ i + 1][ 0 ] == ' ' ||
                 headers[ i + 1][ 0 ] == '\t' ) {
              gchar *morecontent;
              morecontent = g_strconcat( content, headers[ i + 1 ], NULL );
              g_free( content ); /* otherwise we leak! */
              content = morecontent;
              /* trim trailing whitespace */
              g_strchomp( content );
              i++;
            } else {
              break;
            }
          }
        }

        if (( oldcontent = g_hash_table_lookup( *parsedheaders, key ))
            != NULL ) {
          /* This specification permits multiple occurrences of most
             fields.  Except as noted, their interpretation is not
             specified here, and their use is discouraged. */
          /* I'll just fold 'em */
          if ( content != NULL ) {
            gchar *morecontent = g_strconcat( oldcontent, " ", content, NULL );
            g_free( content );
            content = morecontent;
          }
        }

        if ( content != NULL ) {
          g_hash_table_replace( *parsedheaders, key, content );
        }

        g_strfreev( bits );
      }
      g_strfreev( headers );
    }

#ifdef LOUD
    gaim_debug_misc( __FUNCTION__, "Parsed headers in %p:\n", *parsedheaders );
    g_hash_table_foreach( *parsedheaders, dumpheader, (gpointer)__FUNCTION__ );
#endif
  }

#ifdef LOUD
  gaim_debug_misc( __FUNCTION__, "requested header %s from %p is %s\n", header,
                   *parsedheaders, g_hash_table_lookup( *parsedheaders,
                                                        header ));
#endif

  return g_hash_table_lookup( *parsedheaders, header );
}

/*
 * Parse a 207 Multistatus response and apply its data to whatever
 * we've got in core.
 */
static void parse_multistatus( GaimConnection *gc, GaimFetchUrlData *gfud ) {
  xmlDocPtr doc;
  xmlNodePtr cur, response;
  gchar *subs_id, *lifetime;

  if (( doc = parse_preamble( gfud->response.webdata, gfud->len,
                              "multistatus", &cur )) == NULL ) {
    return;
  }

  /* curious:
     the subscription id & expiry are in the header, despite the fact
     that it's a potential multiple-status response */
  /* Subscription-Id: \d
     Subscription-Lifetime: \d
  */
  subs_id = get_header_content( gfud->response.header,
                                "subscription-id", &gfud->parsedheaders );
  lifetime = get_header_content( gfud->response.header,
                                 "subscription-lifetime",
                                 &gfud->parsedheaders );



  /* children of a multistatus:
   * - response {set}
   *   - href of principal concerned
   *   - propstat
   *     - prop
   *       - r:state, d:displayname
   *     - status
   */
  response = cur->xmlChildrenNode;

  while ( response != NULL ) {
    xmlNodePtr ptr, href = NULL, propstat = NULL, prop = NULL, status = NULL;
    xmlChar *val;
    RVPData *rd = gc->proto_data;
    RVPBuddy *target = NULL, *metoo = NULL;
    GaimBuddy *buddy = NULL;
    size_t i;

    if ( xmlStrcmp( response->name, BAD_CAST "response" )) {
      gaim_debug_error( __FUNCTION__, "expected <response> here\n" );
      goto loop;
    }

    ptr = response->xmlChildrenNode;
    while ( ptr != NULL ) {
      if ( !xmlStrcmp( ptr->name, BAD_CAST "href" )) {
        href = ptr;
      } else if ( !xmlStrcmp( ptr->name, BAD_CAST "propstat" )) {
        propstat = ptr;
      } else {
        gaim_debug_error( __FUNCTION__, "unknown response part\n" );
      }

      ptr = ptr->next;
    }

    if ( href == NULL || propstat == NULL ) {
      goto loop;
    }

    if (( val =
          xmlNodeListGetString( doc, href->xmlChildrenNode, 1 )) == NULL ) {
      goto loop;
    }

    /*
     * using metoo here in case you've got yourself on your buddy
     * list. This has triggered a bug in the past, btw, and I don't
     * recommend it.
     *
     * GRR. turns out this is getting more and more problematic. I
     * really need to separate out data pertaining to my own login
     * subscription vs. data pertaining to my own 'watch'
     * subscription.
     */
    target = NULL;
    metoo = NULL;

    if ( !strcmp( rd->principal, (char *)val )) {
      metoo = &(rd->me);
    }

    buddy = rvp_find_buddy_by_principal( gc, (char *)val );
    if ( buddy == NULL ) {
      target = metoo;
    } else {
      target = buddy->proto_data;
    }

    if ( target == NULL ) {
      gaim_debug_warning( __FUNCTION__, "don't know this principal (%s)\n",
                          (char *)val );
      goto loop;
    } else if ( target == metoo ) {
      gaim_debug_misc( __FUNCTION__, "setting state for myself\n" );
    }

    /* now handle the header props */
    if ( subs_id != NULL ) {
      target->subs_id = atoi( subs_id );
      gaim_debug_misc( __FUNCTION__, "setting subs_id to %d\n",
                       target->subs_id );
    }

    if ( lifetime != NULL ) {
      time_t timeout = atol( lifetime );
      if ( timeout != 0 ) {
        target->expires = timeout + time( NULL );
      }
    }

    /* get the status */
    ptr = propstat->xmlChildrenNode;
    while( ptr != NULL ) {
      if ( !xmlStrcmp( ptr->name, BAD_CAST "prop" )) {
      } else if ( !xmlStrcmp( ptr->name, BAD_CAST "status" )) {
        status = ptr;
      } else {
        gaim_debug_error( __FUNCTION__, "unknown response part\n" );
      }

      ptr = ptr->next;
    }

    if ( status == NULL ) {
      gaim_debug_error( __FUNCTION__, "No status in property\n" );
      goto loop;
    }

    /* status will (should) look like 'HTTP/1.1 200 Successful' */
    if (( val =
          xmlNodeListGetString( doc, status->xmlChildrenNode, 1 )) == NULL ) {
      goto loop;
    }
    i = strcspn( (gchar *)val, " " );
    i++;
    if ( strlen( (gchar *)&val[i] )) {
      guint32 s = atoi( (gchar *)&val[i] );
      gaim_debug_misc( __FUNCTION__, "returned status code is %s %d\n",
                       (gchar *)&val[i], s );
    }

    /* now do the props */
    ptr = propstat->xmlChildrenNode;
    while( ptr != NULL ) {
      if ( !xmlStrcmp( ptr->name, BAD_CAST "prop" )) {
        prop = ptr->xmlChildrenNode;

        while( prop != NULL ) {
          if ( !xmlStrcmp( prop->name, BAD_CAST "displayname" )) {
            if (( val =
                  xmlNodeListGetString( doc, prop->xmlChildrenNode, 1 ))
                != NULL ) {
              if ( target->buddy->server_alias != NULL ) {
                g_free( target->buddy->server_alias );
              }
              target->buddy->server_alias = g_strdup( (gchar *)val );

              if ( metoo != NULL && target != metoo ) {
                if ( metoo->buddy->server_alias != NULL ) {
                  g_free( metoo->buddy->server_alias );
                }
                metoo->buddy->server_alias = g_strdup( (gchar *)val );
                gaim_account_set_alias( gaim_connection_get_account( gc ),
                                        g_strdup(( gchar *) val ));
              }

              xmlFree( val );
            }

            /* update the buddy list view */
            if ( target != metoo && target->buddy->server_alias != NULL ) {
              gaim_blist_update_buddy_icon( target->buddy );
            }

          } else if ( !xmlStrcmp( prop->name, BAD_CAST "state" )) {
            /* big state or little state? */
            xmlNodePtr state = NULL, snode = prop->xmlChildrenNode;

            while( snode ) {
              if ( !xmlStrcmp( snode->name, BAD_CAST "leased-value" )) {
                /* big state */
                xmlNodePtr bits = snode->xmlChildrenNode;

                while( bits ) {
                  if ( !xmlStrcmp( bits->name, BAD_CAST "value" )) {
                    state = bits->xmlChildrenNode;
                  } else if ( !xmlStrcmp( bits->name, BAD_CAST "timeout" )) {
                    xmlChar *to =
                      xmlNodeListGetString( doc, bits->xmlChildrenNode , 1 );
                    if ( to != NULL ) {
                      rd->view_expiry = atol( (gchar *)to ) +
                        time( NULL );
                      gaim_debug_misc( __FUNCTION__,
                                       "view expires in %u seconds (%s)\n",
                                       atol((gchar *)to ), to );
                    }
                  }

                  bits = bits->next;
                }
              } else if ( !xmlStrcmp( snode->name, BAD_CAST "view-id" )) {
                gchar *viewid =
                  (gchar *)xmlNodeListGetString( doc, snode->xmlChildrenNode,
                                                 1 );
                if ( viewid != NULL ) {
                  rd->view_id = atol( viewid );
                  gaim_debug_misc( __FUNCTION__, "my view id: %d\n",
                                   rd->view_id );
                } else {
                  gaim_debug_error( __FUNCTION__, "can't parse view-id\n" );
                }
              } else {
                /* little state */
                state = snode;
              }

              snode = snode->next;
            }

            if ( state != NULL ) {
              rvp_set_buddy_status( gc, target->buddy->name,
                                    (gchar *)state->name );
            }
          } else {
            xmlChar *v = xmlNodeListGetString( doc, prop->xmlChildrenNode, 1 );

            if ( !xmlStrcmp( prop->name, BAD_CAST "email" )) {
              target->email = g_strdup( (gchar *)v );
            } else if ( !xmlStrcmp( prop->name, BAD_CAST "mobile-state" )) {
              target->mobile_state = atoi((gchar *)v );
            } else if ( !xmlStrcmp( prop->name,
                                    BAD_CAST "mobile-description" )) {
              target->mobile_description = g_strdup((gchar *)v);
            } else {
              gaim_debug_warning( __FUNCTION__, "unknown property %s = %s\n",
                                  (gchar *)prop->name, (gchar *)v );
            }
          }
          prop = prop->next;
        }
      }

      ptr = ptr->next;
    }

    /* do we need to set an ACL for this user? */
    if ( target->acl == 0 ) {
      if ( !GAIM_CONNECTION_IS_CONNECTED( gc )) {
        /* we'll pick it up later */
        gaim_debug_misc( __FUNCTION__, "delaying ACL\n" );
      } else {
        gaim_debug_misc( __FUNCTION__, "adding ACL\n" );
        rvp_set_acl( gc, target->principal,
                     RVP_ACL_CREDENTIALS | RVP_ACL_BUDDY, 0 );
      }
    }

    rvp_dump_buddy( __FUNCTION__, target->buddy );

  loop:
    response = response->next;
  }

  xmlFreeDoc( doc );
}

/*
 * parse the name and host out of principal
 *
 * From Microsoft's RVP doc:
 *
 * There are two types of URLs: logical URLs and physical URLs. A
 * logical URL is the default and determines which domain the node is
 * located on. For example, an e-mail address such as
 * beverlyj@domain1.com could map to a logical URL
 * http://im.domain1.com/instmsg/aliases/beverlyj.
 *
 * A physical URL can be the same as a logical URL, or it can provide
 * extra information as to which home server is responsible for the
 * entity. A physical URL is used when a router redirects requests for
 * a specific entity. For example, if the e-mail address
 * garrettv@domain2.com is within a domain with a router and several
 * home servers, any requests to the router can be redirected to the
 * physical URL
 * http://imhome1.domain2.com/instmsg/local/im.domain2.com/instmsg/aliases/garrettv.
 * This physical URL is then used to access the node at the home
 * server imhome1.domain2.com.
 */
static void rvp_parse_principal( gchar *principal, gchar **name, gchar **host ) {
  gchar **split;
  gint elt = 0;

  split = g_strsplit( principal, "/", 0 );
  while( split[elt] != NULL ) { elt++; }
  if ( strstr( principal, "/instmsg/local" ) != NULL ) {
    /* physical URL */
    if ( elt == 9 ) {
      if ( name != NULL ) {
        *name = g_strconcat( split[8], "@", split[2], NULL );
      }
      if ( host != NULL ) {
        *host = g_strdup( split[2] );
      }
    } else {
      gaim_debug_error( __FUNCTION__, "can't parse principal %s\n",
                        principal );
    }
  } else {
    if ( elt == 6 ) {
      if ( name != NULL ) {
        *name = g_strconcat( split[5], "@", split[2], NULL );
      }
      if ( host != NULL ) {
        *host = g_strdup( split[2] );
      }
    } else {
      gaim_debug_error( __FUNCTION__, "can't parse principal %s\n",
                        principal );
    }
  }

  if ( split ) {
    g_strfreev( split );
  }
}

/*
 * build a principal from an email address. don't forget to free it
 * when you're done.
 */
static gchar *rvp_principal_from_address( GaimConnection *gc,
                                          const gchar *addr ) {
  gchar **bits = g_strsplit( addr, "@", 2 );
  gchar *srvname, *host = NULL, *principal = NULL;
  srvrec *service = NULL;
  gint port = RVP_PORT;

  if ( bits == NULL || bits[0] == NULL ) {
    goto out;
  }

  if ( bits[1] == NULL ) {
    const gchar *newname = rvp_normalize( gaim_connection_get_account( gc ),
                                          addr );
    g_strfreev( bits );
    bits = g_strsplit( newname, "@", 2 );
  }

  host = g_strdup( bits[1] );

  srvname = g_strconcat( "_rvp._tcp.", bits[1], NULL );
  service = gethostbysrv( srvname, NULL );
  g_free( srvname );

  if ( service->host == NULL ) {
    struct hostent *h;
    if (( h = gethostbyname( bits[1] )) == NULL ) {
      goto out;
    }
  } else {
    g_free( host );
    host = g_strdup( service->host );
    port = service->port;
  }

  if ( port == RVP_PORT ) {
    principal = g_strdup_printf( "http://%s/instmsg/aliases/%s",
                                 host, bits[0] );
  } else {
    principal = g_strdup_printf( "http://%s:%d/instmsg/aliases/%s",
                                 host, port, bits[0] );
  }


 out:
  if ( host ) {
    g_free( host );
  }
  if ( bits != NULL ) {
    g_strfreev( bits );
  }

  return principal;
}

/*
 * Build a minimal buddy structure from an email address
 */
static GaimBuddy *rvp_buddy_from_address( GaimConnection *gc,
                                          const gchar *fullname ) {
  GaimBuddy *retval = NULL;
  RVPBuddy *rb = NULL;
  gchar **bits = g_strsplit( fullname, "@", 2 );
  gchar *principal = NULL;
  RVPData *rd;

  gaim_debug_misc( __FUNCTION__, "creating buddy from address %s\n",
                   fullname );

  principal = rvp_principal_from_address( gc, fullname );

  if ( gc ) {
    rd = gc->proto_data;
    retval = (GaimBuddy *)g_hash_table_lookup( rd->nonbuddy, principal );
  } else {
    /* this should never happen */
    gaim_debug_error( __FUNCTION__, "called with no gc. wtf?\n" );
  }

  if ( retval == NULL ) {
    retval = g_new0( GaimBuddy, 1 );
    retval->proto_data = g_new0( RVPBuddy, 1 );
    rb = retval->proto_data;
    rb->principal = principal;
    rb->buddy = retval;
    if ( gc ) {
      retval->name =
        g_strdup( rvp_normalize( gaim_connection_get_account( gc ),
                                 fullname ));
    } else {
      /* use chunk of data preceeding the '@' sign as the buddy name */
      retval->name = g_strdup( bits[0] );
    }

  } else {
    g_free( principal );
  }

  if ( bits != NULL ) {
    g_strfreev( bits );
  }
  return retval;
}

/*
 * file transfer bits
 */
static void rvp_xfer_cancel_recv( GaimXfer *xfer ) {
  RVPInvite *inv = xfer->data;
  GaimAccount *ac = gaim_xfer_get_account( xfer );
  GaimConnection *gc = gaim_account_get_connection( ac );
  gchar *filexfer;

  switch( xfer->status ) {

    /* if it was cancelled by the other end, no need to do anything
       more */
  case GAIM_XFER_STATUS_CANCEL_REMOTE:
    break;

  default:
    gaim_debug_misc( __FUNCTION__, "cancelling because %d\n", xfer->status );

    /* fixme: check what the reason is (xfer->status) */
    /* REJECT => user rejected
       FTTIMEOUT => timeout in file transfer
       TIMEOUT => user retracted ?
    */

    filexfer = g_strdup_printf( "Invitation-Command: CANCEL\r\n"
                                "Invitation-Cookie: %d\r\n"
                                "Cancel-Code: REJECT\r\n\r\n",
                                inv->cookie );
    rvp_send_notify( gc, inv->who, RVP_MSG_INVITE, filexfer, 0 );
    g_free( filexfer );
  }
}

/*
 * read a block of file-transfer data
 */
static rvpxrr_size rvp_xfer_recv_read( rvpxrr_buf **buffer, GaimXfer *xfer ) {
  RVPInvite *inv = xfer->data;
  ssize_t s, r;

  gaim_debug_misc( __FUNCTION__, "Enter\n" );

  if ( inv->blocksize == 0 ) {
    msnftphdr *hdr = (msnftphdr *)inv->hdr;

    r = read( xfer->fd, &(inv->hdr[inv->hdrread]), sizeof( msnftphdr )
              - inv->hdrread );
    if ( r <= 0 && errno != EAGAIN ) {
      r = -1;
      goto out;
    }
    inv->hdrread += r;
    if ( inv->hdrread < sizeof( msnftphdr )) {
      r = 0;
      goto out;
    }

    /* now we've established that we've got the header... */
    inv->hdrread = 0;

    if ( hdr->done ) {
      gaim_debug_misc( __FUNCTION__, "received completion notice\n" );
      gaim_xfer_set_completed( xfer, TRUE );
    } else {
      inv->blocksize = hdr->sizemsb * 256 + hdr->sizelsb;
      gaim_debug_misc( __FUNCTION__, "expecting a block of %d bytes\n",
                       inv->blocksize );
    }
  } else {
      gaim_debug_misc( __FUNCTION__, "inv blocksize ok\n" );
  }

  if ( !gaim_xfer_is_completed( xfer )) {
    s = inv->blocksize - inv->blockgot;

    *buffer = g_malloc0(s);
    r = read( xfer->fd, *buffer, s );

    gaim_debug_misc( __FUNCTION__, "read %d bytes of %d (%d of %d total)\n",
                     r, s, gaim_xfer_get_bytes_sent( xfer ) + r,
                     gaim_xfer_get_size( xfer ));

    if ( r == s &&
         ( gaim_xfer_get_bytes_sent( xfer ) + r >=
           gaim_xfer_get_size( xfer ))) {
      gaim_debug_misc( __FUNCTION__, "sending bye\n" );
      write( xfer->fd, "BYE 16777989\r\n", 16 ); /* buffered */

      /* I shouldn't have to do this, but suddenly I do. I have no
         idea why. */
      gaim_xfer_set_completed( xfer, TRUE );
    } else {
      gaim_debug_misc( __FUNCTION__, "some sort of mismatch\n" );
    }

    if ( r > 0 ) {
      inv->blockgot += r;
    } else if( r <= 0 ) {
      r = -1;
    }
  } else {
    r = 0;
  }

  if ( inv->blocksize == inv->blockgot ) {
    /* reset for next header */
    inv->blocksize = 0;
    inv->blockgot = 0;
    /* excellent. without this, the MSN client will drop the
       connection after sending the first block. */
    write( xfer->fd, "", 0 );
  }

 out:
  gaim_debug_misc( __FUNCTION__, "exit %d\n", r );
  return r;
}

static void rvp_xfer_init_recv( GaimXfer *xfer ) {
  RVPInvite *inv = xfer->data;
  GaimAccount *ac = gaim_xfer_get_account( xfer );
  GaimConnection *gc = gaim_account_get_connection( ac );

  gaim_debug_misc( __FUNCTION__, "Enter, xfer status is %d\n", xfer->status );
  /* we need an IP address from the server */
  if ( gaim_xfer_get_remote_ip( xfer ) == NULL ) {
    gchar *filexfer;
    filexfer = g_strdup_printf( "Invitation-Command: ACCEPT\r\n"
                                "Invitation-Cookie: %d\r\n"
                                "Launch-Application: FALSE\r\n"
                                "Request-Data: IP-Address:\r\n\r\n",
                                inv->cookie );
    rvp_send_notify( gc, inv->who, RVP_MSG_INVITE, filexfer, 0 );
    g_free( filexfer );
  } else {
    gaim_debug_misc( __FUNCTION__, "we've been here before...\n" );
  }
}

/*
 * write a block of data to the receiver
 */
static rvpxrr_size rvp_xfer_send_write( const rvpxrr_buf *buf, size_t len,
                                        GaimXfer *xfer ) {
  msnftphdr hdr;
  RVPInvite *inv = xfer->data;
  size_t nw = 0;

  if ( inv->blocksize == inv->blockgot ) {
    if ( len ) {
      hdr.done = 0;
      hdr.sizemsb = len >> 8;
      hdr.sizelsb = len % 256;
      inv->blocksize = len;
    } else {
      hdr.done = 1;
      hdr.sizemsb = 0;
      hdr.sizelsb = 0;
      inv->blocksize = 0;
    }
    inv->outbuffer = g_malloc0( 3 );
    memcpy( inv->outbuffer, &hdr, 3 );
    inv->outbuflen = 3;
  }

  if ( inv->outbuffer != NULL ) {
    nw = write( inv->xfersock, inv->outbuffer, inv->outbuflen );
    if ( nw < 0 ) {
      /* fall through */
    } else {
      if ( nw == inv->outbuflen ) {
        g_free( inv->outbuffer );
        inv->outbuffer =NULL;
        inv->blockgot = 0;
        gaim_debug_misc( __FUNCTION__, "wrote header for %d bytes\n",
                         inv->blocksize );
      } else {
        memmove( inv->outbuffer, &inv->outbuffer[nw], inv->outbuflen - nw );
      }
      inv->outbuflen -= nw;
      nw = 0;
    }
  }

  if ( inv->outbuffer == NULL ) {
    if ( buf != NULL ) {
      nw = write( inv->xfersock, buf, len );
      if ( nw > 0 ) {
        inv->blockgot += nw;
        gaim_debug_misc( __FUNCTION__, "wrote %d of %d bytes in block\n",
                         inv->blockgot, inv->blocksize );
      }
    }
  }

  /* in theory we should poll for a BYE or CCL message here */
  /* ugh. shouldn't ft.c be doing this? (it does, but only if you
     don't have a custom write function) */
  if ( gaim_xfer_get_bytes_sent( xfer ) + nw == gaim_xfer_get_size( xfer )) {
    gaim_xfer_set_completed( xfer, TRUE );
  } else {
    gaim_debug_misc( __FUNCTION__, "transferred %d of %d bytes\n",
                     gaim_xfer_get_bytes_sent( xfer ) + nw,
                     gaim_xfer_get_size( xfer ));
  }

  return nw;
}

/*
 * all-round generic cleanup
 */
static void rvp_xfer_cleanup( GaimXfer *xfer ) {
  RVPInvite *inv;

  if ( xfer == NULL ) {
    return;
  }
  inv = xfer->data;
  if ( inv != NULL ) {
    if ( inv->xferinp ) {
      gaim_input_remove( inv->xferinp );
      inv->xferinp = 0;
    }
    if ( inv->inbuffer != NULL ) {
      g_free( inv->inbuffer );
      inv->inbuffer = NULL;
    }
    if ( inv->outbuffer != NULL ) {
      g_free( inv->outbuffer );
      inv->outbuffer = NULL;
    }
    if ( inv->who != NULL ) {
      g_free( inv->who );
      inv->who = NULL;
    }
    inv->data = NULL;
  }

  /* safety */
  gaim_xfer_unref( xfer );
}

/*
 * end of transfer: send a block indicating that transfer is complete
 */
static void rvp_xfer_send_end( GaimXfer *xfer ) {
  rvp_xfer_send_write( NULL, 0, xfer );
}

/*
 * end of transfer: call cleanup
 */
static void rvp_xfer_recv_end( GaimXfer *xfer ) {
  rvp_xfer_cleanup( xfer );
}

/*
 * Handle the out-of-band/handshake data in a file transfer
 */
static void rvp_xfer_cb( gpointer data, int source, GaimInputCondition cond ) {
  GaimXfer *xfer = data;
  GaimAccount *ac = gaim_xfer_get_account( xfer );
  GaimConnection *gc = gaim_account_get_connection( ac );
  RVPData *rd = gc->proto_data;
  RVPInvite *inv = xfer->data;
  gboolean eof = FALSE;

  if ( inv == NULL ) {
    gaim_debug_error( __FUNCTION__, "we shouldn't be here!\n" );
  }

  if ( cond & GAIM_INPUT_READ ) {
    inv->inbuflen++;
    inv->inbuffer = g_realloc( inv->inbuffer, inv->inbuflen );

    if ( read( source, &(inv->inbuffer[ inv->inbuflen - 1 ]) , 1 ) > 0 ||
         errno == EWOULDBLOCK ) {
      if ( errno == EWOULDBLOCK ) {
        errno = 0;
        inv->inbuflen--;
        return;
      }

      if ( inv->inbuffer[ inv->inbuflen - 1 ] == '\n' ) {
        inv->inbuffer[ inv->inbuflen - 1 ] = '\0';
        if ( inv->inbuflen >= 3 ) {
          if ( !strncmp( inv->inbuffer, "VER", 3 )) {
            if ( gaim_xfer_get_type( xfer ) == GAIM_XFER_SEND ) {
              if ( strstr( inv->inbuffer, "MSNFTP" )) {
                inv->outbuffer = strdup( "VER MSNFTP\r\n" );
              } else {
                /* if you ever see this error, I'll laugh */
                gaim_notify_error( NULL, _("Error sending file"),
                                   "No compatible file transfer protocol "
                                   "available", NULL );
                eof = TRUE;
              }
            } else {
              /* you need to auth for a transfer as user@authhost
                 rather than user@authdomain. While I can think of
                 reasons for this, it annoys me. */
              gchar **bits;

              bits = g_strsplit( rvp_normalize( ac, ac->username ), "@", 2 );

              if ( bits == NULL ) {
                gaim_notify_error( NULL, _("Error sending file"),
                                   "Impossible error. Congratulations.",
                                   NULL );
                eof = TRUE;
              } else {
                inv->outbuffer = g_strdup_printf( "USR %s@%s %d\r\n",
                                                  bits[0],
                                                  rd->authhost,
                                                  inv->authcookie );
                g_strfreev( bits );
              }
            }
          } else if ( !strncmp( inv->inbuffer, "USR", 3 )) {
            gchar **auth;
            auth = g_strsplit( inv->inbuffer, " ", 0 );
            if ( auth == NULL ) {
              gaim_notify_error( NULL, _("Error sending file"),
                                 "Unparseable auth", NULL );
              eof = TRUE;
            } else {
              if ( auth[0] != NULL && auth[1] != NULL && auth[2] != NULL ) {
                gint authcookie = atoi( auth[2] );
                /* GAH. Windows uses username@authserver rather than
                   username@domain, so I have to go figure out the
                   username@authserver to do this properly. For now
                   I'm just going to kill the strcmp and fix it later */

                if ( /* strcmp( auth[1], rvp_normalize( ac, xfer->who)) || */
                     authcookie != inv->authcookie ) {
                  gaim_debug_error( __FUNCTION__,
                                    "got %s/%d instead of %s/%d\n",
                                    auth[1], authcookie,
                                    rvp_normalize( ac, xfer->who ),
                                    inv->authcookie );
                  gaim_notify_error( NULL, _("Error sending file"),
                                     "Invalid auth", NULL );
                  eof = TRUE;
                } else {
                  inv->outbuffer = g_strdup_printf( "FIL %d\r\n", xfer->size );
                }
              } else {
                gaim_notify_error( NULL, _("Error sending file"),
                                   "Unparseable auth", NULL );
                eof = TRUE;
              }
              g_strfreev( auth );
            }
          } else if ( !strncmp( inv->inbuffer, "CCL", 3 )) {
            /* cancelled */
            eof = TRUE;
          } else if ( !strncmp( inv->inbuffer, "TFR", 3 )) {
            /* good to send */
            gaim_xfer_set_write_fnc( xfer, rvp_xfer_send_write );
            gaim_xfer_set_end_fnc( xfer, rvp_xfer_send_end );

            /* WAIT! */
            gaim_input_remove( inv->xferinp );
            inv->xferinp = gaim_input_add( inv->xfersock,
                                           GAIM_INPUT_READ,
                                           rvp_xfer_cb,
                                           xfer );

            gaim_xfer_start( xfer, xfer->fd, NULL, 0 );
          } else if ( !strncmp( inv->inbuffer, "FIL", 3 )) {
            size_t realsize;

            if ( inv->inbuflen > 4 ) {
              realsize = atoi( &inv->inbuffer[4] );
              gaim_xfer_set_size( xfer, realsize );
            }

            /* good to read */
            gaim_xfer_set_end_fnc( xfer, rvp_xfer_recv_end );
            gaim_xfer_set_read_fnc( xfer, rvp_xfer_recv_read );
            inv->outbuffer = strdup( "TFR\r\n" );

            inv->blocksize = 0;
            inv->blockgot = 0;
            inv->hdrread = 0;

            gaim_input_remove( inv->xferinp );
            inv->xferinp = gaim_input_add( inv->xfersock,
                                           GAIM_INPUT_WRITE,
                                           rvp_xfer_cb,
                                           xfer );

            gaim_xfer_start( xfer, xfer->fd, NULL, 0 );
          } else if ( !strncmp( inv->inbuffer, "BYE", 3 )) {
            gint code = 0;
            if ( inv->inbuflen > 4 ) {
              code = atoi( &(inv->inbuffer[4] ));
            }
            gaim_debug_error( __FUNCTION__, "bye code: %d\n", code );
            eof = TRUE;
          } else {
            gaim_debug_error( __FUNCTION__,
                              "unknown file transfer command '%*.*s'\n",
                              inv->inbuflen, inv->inbuflen, inv->inbuffer );
          }
        } else {
          gaim_debug_error( __FUNCTION__, "short command read\n" );
        }

        gaim_debug_misc( __FUNCTION__, "received command '%s'\n",
                         inv->inbuffer );

        if ( inv->outbuffer != NULL ) {
          inv->outbuflen = strlen( inv->outbuffer );
          gaim_debug_misc( __FUNCTION__, "sending response '%s'\n",
                           inv->outbuffer );
        }

        /* we've had a complete command, so discard the buffer */
        g_free( inv->inbuffer );
        inv->inbuffer = NULL;
        inv->inbuflen = 0;
      }
    } else if ( errno != ETIMEDOUT ) {
      if ( errno != 0 ) {
        gaim_debug_error( __FUNCTION__, "read: %s\n", strerror( errno ));
        eof = TRUE;
      } else {
        gaim_debug_misc( __FUNCTION__, "zero-byte read\n" );
      }
    } else {
      gaim_input_remove( inv->xferinp );
      close( inv->xfersock );
      gaim_debug_misc( __FUNCTION__, "closed xfersock (timeout)\n" );
      inv->xfersock = 0;
    }

    if ( eof ) {
      gaim_input_remove( inv->xferinp );
      close( inv->xfersock );
      gaim_debug_misc( __FUNCTION__, "closed xfersock (eof)\n" );
      inv->xfersock = 0;
    }

    /* woop, socket closed, cancel the transfer */
    if ( inv->xfersock == 0 && !gaim_xfer_is_completed( xfer )) {
      gaim_xfer_cancel_remote( xfer );
    }
  }

  if ( cond & GAIM_INPUT_WRITE ) {
    if ( inv->outbuffer != NULL ) {
      int nw = write( source, inv->outbuffer, inv->outbuflen );
      if ( nw > 0 ) {
        if ( nw == inv->outbuflen ) {
          g_free( inv->outbuffer );
          inv->outbuffer = NULL;
        } else {
          memmove( inv->outbuffer, &( inv->outbuffer[ nw ] ),
                   inv->outbuflen - nw );
        }
        inv->outbuflen -= nw;
      } else {
        if ( nw < 0 ) {
          gaim_notify_error( NULL, _("Error transferring file"),
                             "Write to source failed", NULL );
          gaim_xfer_cancel_local( xfer );
        }
      }
    }
  }
}

/*
 * connected to person sending us a file
 */
static void rvp_xfer_connect_callback( gpointer data, gint source,
                                       GaimInputCondition cond ) {
  GaimXfer *xfer = data;
  RVPInvite *inv = xfer->data;

  xfer->fd = source;

  gaim_xfer_ref( xfer );
  inv->xfersock = source;
  inv->xferinp = gaim_input_add( source,
                                 GAIM_INPUT_READ|GAIM_INPUT_WRITE,
                                 rvp_xfer_cb,
                                 xfer );

  /* initial RECV command */
  inv->outbuffer = strdup( "VER MSNFTP\r\n" );
  inv->outbuflen = strlen( inv->outbuffer );
}

static void rvp_xfer_accept_callback( gpointer data, gint source,
                                      GaimInputCondition cond ) {
  GaimXfer *xfer = data;
  RVPInvite *inv = xfer->data;

  if (( inv->xfersock = accept( source, 0, 0 )) < 0 ) {
    perror( "Accept failed" );
    return;
  } else {
    gaim_debug_misc( __FUNCTION__, "Accepted Port%s connection\n",
                     source == inv->sock ? "" : "x" );
  }

  gaim_xfer_ref( xfer ); /* don't let Gaim yank this out from under me */
  inv->xferinp = gaim_input_add( inv->xfersock,
                                 GAIM_INPUT_READ|GAIM_INPUT_WRITE,
                                 rvp_xfer_cb,
                                 xfer );
}

/*
 * callback for invite listener
 */
static void rvp_invite_listener_callback( int listenfd, gpointer data ) {
  RVPInvite *invite = data;
  GaimXfer *xfer = invite->data;
  GaimAccount *ac = gaim_xfer_get_account( xfer );
  GaimConnection *gc = gaim_account_get_connection( ac );
  RVPData *rd = gc->proto_data;
  gint port;
  gchar *msg;

  invite->sock = listenfd;

  if ( invite->sock == -1 ) {
    gaim_notify_error( NULL, _("Error sending file"),
                       "No available ports to listen on",
                       NULL );
    gaim_xfer_cancel_local( xfer );
    return;
  }

  port = gaim_network_get_port_from_fd( invite->sock );
  invite->inp = gaim_input_add( invite->sock, GAIM_INPUT_READ,
                                rvp_xfer_accept_callback, xfer );

  /* strange things happen with the presence of PortX, so
     I'm leaving it out until I've figured out what's
     broken */
  msg = g_strdup_printf( "Invitation-Command: ACCEPT\r\n"
                         "Invitation-Cookie: %d\r\n"
                         "IP-Address: %s\r\n"
                         "Port: %d\r\n"
                         /*"PortX: %d\r\n"*/
                         "AuthCookie: %d\r\n"
                         "Launch-Application: FALSE\r\n"
                         "Request-Data: IP-Address:\r\n\r\n",
                         invite->cookie, rd->client_host,
                         port, /*portx,*/ invite->authcookie );

  rvp_send_notify( gc, invite->who, RVP_MSG_INVITE, msg, 0 );
}

/*
 * parse an invite message
 */
static void rvp_parse_invite( GaimConnection *gc, GaimBuddy *buddy,
                              gchar *data ) {
  RVPBuddy *rb;
  GaimXfer *xfer = NULL;
  GHashTable *parsedheaders = NULL;
  enum rvp_invite_type inv = RVP_INV_UNKNOWN;
  RVPInvite *invite = NULL;
  gchar *guid;
  int cookie = 0;
  gchar *type, *cookiestr;

  if ( buddy != NULL ) {
    rb = buddy->proto_data;
  } else {
    gaim_debug_error( __FUNCTION__, "Woah, no buddy\n" );
    goto out;
  }

  type = get_header_content( data, "invitation-command", &parsedheaders );
  if ( type == NULL ) {
    gaim_debug_misc( __FUNCTION__, "unparseable invite\n" );
    goto out;
  }

  if ( !strcmp( type, "INVITE" )) {
    inv = RVP_INV_INVITE;
  } else if ( !strcmp( type, "ACCEPT" )) {
    inv = RVP_INV_ACCEPT;
  } else if ( !strcmp( type, "CANCEL" )) {
    inv = RVP_INV_CANCEL;
  } else {
    gaim_debug_error( __FUNCTION__, "Unrecognised invite command '%s'\n",
                      type );
  }

  if (( cookiestr = get_header_content( data, "invitation-cookie",
                                        &parsedheaders )) != NULL ) {
    cookie = atoi( cookiestr );
    if ( cookie == 0 ) {
      gaim_debug_error( __FUNCTION__, "Unparseable cookie %s\n", cookiestr );
      goto out;
    }
  }

  /* so, what have we got? */
  switch( inv ) {
  case RVP_INV_INVITE:
    /* what kind of invite? */
    guid = get_header_content( data, "application-guid", &parsedheaders );
    if ( guid == NULL ) {
      gaim_debug_misc( __FUNCTION__, "NULL GUID\n" );
    }
    if ( strcmp( guid, RVP_GUID_FILE_TRANSFER )) {
      /* flat-out reject these. fixme, see if there's an unknown guid
         cancel-code */
      gchar *msg = g_strdup_printf( "Invitation-Command: CANCEL\r\n"
                                    "Invitation-Cookie: %d\r\n"
                                    "Cancel-Code: REJECT\r\n\r\n",
                                    cookie );
      gaim_debug_misc( __FUNCTION__, "Unsupported GUID\n" );
      rvp_send_notify( gc, buddy->name, RVP_MSG_INVITE, msg, 0 );
    } else {
      invite = g_new0( RVPInvite, 1 );
      invite->cookie = cookie;
      invite->who = buddy->name;

      if ( !strcmp( guid, RVP_GUID_FILE_TRANSFER )) {
        gchar *filename = get_header_content( data, "application-file",
                                              &parsedheaders );
        gchar *filesize = get_header_content( data, "application-filesize",
                                              &parsedheaders );

        xfer = gaim_xfer_new( gaim_connection_get_account( gc ),
                              GAIM_XFER_RECEIVE,
                              buddy->server_alias );
        gaim_xfer_set_filename( xfer, filename );
        gaim_xfer_set_size( xfer, atoi( filesize ));

        gaim_xfer_set_init_fnc( xfer, rvp_xfer_init_recv );
        gaim_xfer_set_request_denied_fnc( xfer, rvp_xfer_cancel_recv );
        gaim_xfer_set_cancel_recv_fnc( xfer, rvp_xfer_cancel_recv );

        /* keep a handle on it */
        xfer->data = invite;
        invite->data = xfer;

        gaim_debug_misc( __FUNCTION__, "doing xfer_request\n" );
        gaim_xfer_request( xfer );
      } else {
        /* unhandled guid, handled above; this is to keep the compiler
           happy */
        break;
      }

      /* stash the cookie */
      if ( gaim_xfer_get_type( xfer ) == GAIM_XFER_RECEIVE ) {
        if ( rb->recvcookies == NULL ) {
          rb->recvcookies = g_hash_table_new( g_direct_hash, g_direct_equal );
        }
        g_hash_table_replace( rb->recvcookies, (gpointer)cookie, invite );
      } else {
        if ( rb->sendcookies == NULL ) {
          rb->sendcookies = g_hash_table_new( g_direct_hash, g_direct_equal );
        }
        g_hash_table_replace( rb->sendcookies, (gpointer)cookie, invite );
      }
    }
    break;

  case RVP_INV_ACCEPT:
  case RVP_INV_CANCEL:
    /*
     * in the pathological case where we are sending a file to
     * ourselves in-client, and possibly less pathological cases,
     * determining the xfertype from the cookie tables will screw
     * things up. Better to look at the invite message and guess from
     * that.
     */
    if ( get_header_content( data, "port", &parsedheaders ) != NULL ) {
      /* we've been sent a port, that's a good indication that we're
         receiving a file */
      if (( rb->recvcookies == NULL ) ||
          (( invite = g_hash_table_lookup( rb->recvcookies,
                                           (gconstpointer)cookie ))
           == NULL )) {
        gaim_debug_error( __FUNCTION__,
                          "got an accept for an unknown cookie [recv]\n" );
        break;
      }
    } else if ( get_header_content( data, "launch-application",
                                    &parsedheaders )) {
      if (( rb->sendcookies == NULL ) ||
          (( invite = g_hash_table_lookup( rb->sendcookies,
                                           (gconstpointer)cookie ))
           == NULL )) {
        gaim_debug_error( __FUNCTION__,
                          "got an accept for an unknown cookie [send]\n" );
      }
    }

    /* if we're sending a file to ourselves, it doesn't matter who
       cancelled! */
    if ( invite == NULL ) {
      if ((( rb->recvcookies == NULL ) ||
           (( invite = g_hash_table_lookup( rb->recvcookies,
                                            (gconstpointer)cookie ))
            == NULL )) &&
          (( rb->sendcookies == NULL ) ||
           (( invite = g_hash_table_lookup( rb->sendcookies,
                                            (gconstpointer)cookie ))
            == NULL ))) {
        gaim_debug_error( __FUNCTION__,
                          "got a cancel for an unknown cookie %d\n",
                          cookie );
        break;
      }
    }

    if ( invite != NULL ) {
      GaimXfer *xfer = invite->data;

      if ( inv == RVP_INV_CANCEL ) {
        if ( xfer != NULL &&
             gaim_xfer_get_status( xfer ) != GAIM_XFER_STATUS_CANCEL_LOCAL &&
             gaim_xfer_get_status( xfer ) != GAIM_XFER_STATUS_CANCEL_REMOTE ) {
          gaim_xfer_cancel_remote( xfer );
        }
        if ( gaim_xfer_get_type( xfer ) == GAIM_XFER_RECEIVE ) {
          g_hash_table_remove( rb->recvcookies,
                               (gconstpointer)invite->cookie );
        } else {
          g_hash_table_remove( rb->sendcookies,
                               (gconstpointer)invite->cookie );
        }
      } else if ( inv == RVP_INV_ACCEPT ) {
        gchar *ipaddr = get_header_content( data, "ip-address",
                                            &parsedheaders );
        gchar *port = get_header_content( data, "port", &parsedheaders );
        /*        gchar *portx = get_header_content( data, "portx",
                  &parsedheaders );*/
        gchar *auth = get_header_content( data, "authcookie", &parsedheaders );
        gchar *req = get_header_content( data, "request-data",
                                         &parsedheaders );

        if ( auth != NULL ) {
          invite->authcookie = atoi( auth );
        }

        if ( xfer != NULL ) {
          if ( gaim_xfer_get_type( xfer ) == GAIM_XFER_SEND ) {
            if ( req != NULL && !strcmp( req, "IP-Address:" )) {
              /* set up listeners */
              invite->authcookie = random_integer( 1, 1 << 31 );

              if ( !rvp_network_listen_range( 0, 0, SOCK_STREAM,
                                              rvp_invite_listener_callback,
                                              invite )) {
                gaim_debug_misc( __FUNCTION__, "listener callback not ok\n" );
                gaim_notify_error( NULL, _("Error sending file"),
                                   "No available ports to listen on",
                                   NULL );
                gaim_xfer_cancel_local( xfer );
              }
            }
          } else { /* type = RECV */
            gaim_proxy_connect( xfer->account, ipaddr, atoi( port ),
                                rvp_xfer_connect_callback, xfer );
          }
        }
      }
    } else {
      /* crappity */
      gaim_debug_error( __FUNCTION__, "can't figure out who sent what\n" );
    }
    break;

  default:
    gaim_debug_misc( __FUNCTION__, "Don't know what to do with this\n" );
    break;
  }

 out:
  if ( parsedheaders != NULL ) {
    g_hash_table_destroy( parsedheaders );
  }
}

/* cleanup function for when we're shutting down */
static void rvp_cancel_xfer( gpointer key, gpointer value,
                             gpointer userdata ) {
  GaimXfer *xfer = value;

  switch( gaim_xfer_get_type( xfer )) {
  case GAIM_XFER_SEND:
    rvp_xfer_cancel_send( xfer );
    break;

  case GAIM_XFER_RECEIVE:
    rvp_xfer_cancel_recv( xfer );
    break;

  default:
    /* warn, maybe? not really important */
    break;
  }
}

/*
 * apply markup to a message
 * see http://www.hypothetic.org/docs/msn/client/plaintext.php
 * there is far too much memory copying in this function.
 */
static gchar *rvp_format( gchar *msg, gchar *format ) {
  gchar *msgbody = NULL;
  gchar *open = NULL, *close = NULL, *mods = NULL;
  gchar **bits;

  bits = g_strsplit( format, "; ", 0 );

  open = g_strdup( "<FONT " );
  close = g_strdup( "</FONT>" );
  mods = g_strdup( "" );

  /* redo this:
     capture font face, font pitch, font whatever, save in variables
     at the end, apply <FONT FACE='%s' SIZE='%d' YADDA='%YADDA'>
  */

  if ( bits != NULL ) {
    int b;

    for ( b = 0; bits[ b ] != NULL; b++ ) {
      if ( !strncmp( bits[ b ], "FN=", 3 )) {
        int len, c;
        gchar *newopen;

        newopen = g_strdup_printf( "%sFACE='", open );
        g_free( open );

        len = strlen( newopen );
        newopen = g_realloc( newopen, len + strlen( bits[b] ) - 2 );
        for ( c = 3; c < strlen( bits[b] ); c++ ) {
          if ( !strncmp( &bits[b][c], "%20", 3 )) {
            newopen[len] = ' ';
            c += 2;
          } else {
            newopen[len] = bits[b][c];
          }
          len++;
        }
        newopen[len] = '\0';
        open = g_strdup_printf( "%s' ", newopen );
      } else if ( !strncmp( bits[ b ], "EF=", 3 )) {
        /* "effect" - modifies font selection
           (B)old, (I)talic, (U)nderline and (S)trikethrough. */
        int c;

        for ( c = 3; c < strlen( bits[b] ); c++ ) {
          if ( toupper( bits[b][c] ) == 'B' ) {
            mods = g_strconcat( mods, "<B>", NULL );
            close = g_strconcat( "</B>", close, NULL );
          } else if ( toupper( bits[b][c] ) == 'I' ) {
            mods = g_strconcat( mods, "<I>", NULL );
            close = g_strconcat( "</I>", close, NULL );
          } else if ( toupper( bits[b][c] ) == 'U' ) {
            mods = g_strconcat( mods, "<U>", NULL );
            close = g_strconcat( "</U>", close, NULL );
          } else if ( toupper( bits[b][c] ) == 'S' ) {
            mods = g_strconcat( mods, "<S>", NULL );
            close = g_strconcat( "</S>", close, NULL );
          } else {
            /* unknown mod, skip it */
          }
        }
      } else if ( !strncmp( bits[ b ], "CS=", 3 )) {
        /* charset */
        /*

0 - ANSI_CHARSET
    ANSI characters
1 - DEFAULT_CHARSET
    Font is chosen based solely on name and size. If the described font is not available on the system, Windows will substitute another font.
2 - SYMBOL_CHARSET
    Standard symbol set
4d - MAC_CHARSETLT
    Macintosh characters
80 - SHIFTJIS_CHARSET
    Japanese shift-JIS characters
81 - HANGEUL_CHARSET
    Korean characters (Wansung)
82 - JOHAB_CHARSET
    Korean characters (Johab)
86 - GB2312_CHARSET
    Simplified Chinese characters (Mainland China)
88 - CHINESEBIG5_CHARSET
    Traditional Chinese characters (Taiwanese)
a1 - GREEK_CHARSET
    Greek characters
a2 - TURKISH_CHARSET
    Turkish characters
a3 - VIETNAMESE_CHARSET
    Vietnamese characters
b1 - HEBREW_CHARSET
    Hebrew characters
b2 - ARABIC_CHARSET
    Arabic characters
ba - BALTIC_CHARSET
    Baltic characters
cc - RUSSIAN_CHARSET_DEFAULT
    Cyrillic characters
de - THAI_CHARSET
    Thai characters
ee - EASTEUROPE_CHARSET
    Sometimes called the "Central European" character set, this includes diacritical marks for Eastern European countries
ff - OEM_DEFAULT
    Depends on the codepage of the operating system

        */
      } else if ( !strncmp( bits[ b ], "PF=", 3 )) {
        /* Pitch and Family */
        /* sort of redundantly overlaps FN; ignoring for now as I'm
           relying on the font subsystem to do the dirty work here */
        /* for the record:
           PF & 0xF0 == 0x00 -> FF_DONTCARE
           PF & 0xF0 == 0x10 -> FF_ROMAN
           PF & 0xF0 == 0x20 -> FF_SWISS
           PF & 0xF0 == 0x30 -> FF_MODERN
           PF & 0xF0 == 0x40 -> FF_SCRIPT
           PF & 0xF0 == 0x50 -> FF_DECORATIVE

           PF & 0x0F == 0x00 -> DEFAULT_PITCH
           PF & 0x0F == 0x01 -> FIXED_PITCH
           PF & 0x0F == 0x02 -> VARIABLE_PITCH
        */
      } else if ( !strncmp( bits[ b ], "RL=", 3 )) {
        /* Right Align */
      } else if ( !strncmp( bits[ b ], "CO=", 3 )) {
        /* blue/green/red, in hex, with leading zeros omitted */
        int len, clen;
        gchar *newopen;

        newopen = g_strdup_printf( "%sCOLOR='#000000'", open );
        g_free( open );
        open = newopen;
        len = strlen( open );
        clen = strlen( bits[ b ] ) - 3;

        /* I could do this with a complex for loop, I'm sure. modulus
           would be involved, and it'd be (more) incomprehensible. */
        if ( clen > 0 ) { /* have blue component */
          open[ len - 6 ] = bits[b][clen + 2];
        }
        if ( clen > 1 ) {
          open[ len - 7 ] = bits[b][clen + 1];
        }
        if ( clen > 2 ) { /* have green */
          open[ len - 4 ] = bits[b][clen];
        }
        if ( clen > 3 ) {
          open[ len - 5 ] = bits[b][clen - 1];
        }
        if ( clen > 4 ) { /* have red */
          open[ len - 2 ] = bits[b][clen - 2];
        }
        if ( clen > 5 ) {
          open[ len - 3 ] = bits[b][clen - 3];
        }
      }
    }

    g_strfreev( bits );
  }

  gaim_debug_misc( __FUNCTION__, "applying: %s>%s%s\n", open, mods, close );

  msgbody = g_strconcat( open, ">", mods, msg, close, NULL );
  g_free( open );
  g_free( close );

  return msgbody;
}

/*
 * parse a notification
 */
static gint parse_notify( GaimConnection *gc, GaimFetchUrlData *gfud ) {
  xmlDocPtr doc = NULL;
  xmlNodePtr cur, notification;
  gint retval = 200;
  GaimBuddy *buddy = NULL, *from_contact = NULL, **from_contacts = NULL,
    *to_contact = NULL, **to_contacts = NULL;
  RVPData *rd = gc->proto_data;
  GaimConversation *conv = NULL;

  if (( doc = parse_preamble( gfud->response.webdata, gfud->len,
                              "notification", &cur )) == NULL ) {
    retval = 400;
    goto out;
  }

  notification = cur->xmlChildrenNode;

  while( notification != NULL ) {
    /* what type of notification? */
    if ( !xmlStrcmp( notification->name, BAD_CAST "message" ) ||
         !xmlStrcmp( notification->name, BAD_CAST "propnotification" )) {
      xmlNodePtr ptr = notification->xmlChildrenNode;
      xmlNodePtr from = NULL, to = NULL, msg = NULL, props = NULL;
      xmlChar *val = NULL;

      gaim_debug_misc( __FUNCTION__, "Notify type: %s\n",
                       (gchar *)notification->name );

      while( ptr ) {
        if ( !xmlStrcmp( ptr->name, BAD_CAST "notification-from" )) {
          from = ptr->xmlChildrenNode;
        } else if ( !xmlStrcmp( ptr->name,
                                BAD_CAST "notification-to" )) {
          to = ptr->xmlChildrenNode;
        } else if ( !xmlStrcmp( ptr->name, BAD_CAST "msgbody" )) {
          msg = ptr->xmlChildrenNode;
        } else if ( !xmlStrcmp( ptr->name, BAD_CAST "propertyupdate" )) {
          props = ptr->xmlChildrenNode;
        } else {
          gaim_debug_error( __FUNCTION__, "unknown notification part\n" );
        }
        ptr = ptr->next;
      }

      /* nothing to do? bail! */
      if ( !msg && !props ) {
        goto loop;
      }

      if ( !from ) { /* go digging for a session id! */
        goto loop;
      } else {
        /* who the hell are you? */
        from_contacts = parse_contact( doc, from );
        if ( from_contacts == NULL ) {
          gaim_notify_error( NULL, _("No sender"),
                             "Received an anonymous message - discarding!",
                             NULL );
          gaim_debug_error( __FUNCTION__, "no/unparseable sender\n" );
          goto loop;
        } else {
          /* we do not expect to see multiple senders... */
          RVPBuddy *rb_from;
          from_contact = from_contacts[ 0 ];
          rb_from = from_contact->proto_data;

          buddy = rvp_find_buddy_by_principal( gc, rb_from->principal );
        }
      }

      if ( to ) {
        to_contacts = parse_contact( doc, to );
        if ( to_contacts == NULL ) {
          gaim_debug_error( __FUNCTION__, "no/unparseable recipient\n" );
        } else {
          to_contact = to_contacts[ 0 ];
        }
      }

      if ( msg != NULL && !xmlStrcmp( msg->name, BAD_CAST "mime-data" )) {
        if (( val =
              xmlNodeListGetString( doc, msg->xmlChildrenNode, 1 )) != NULL ) {
          gchar **parts = NULL;

          if ( strstr( (gchar *)val, "\r\n\r\n" )) {
            parts = g_strsplit( (gchar *)val, "\r\n\r\n", 2 );
          } else if ( strstr( (gchar *)val, "\n\n" )) {
            parts = g_strsplit( (gchar *)val, "\n\n", 2 );
          } else {
            /* what sort of mutant are you, exactly? */
            gaim_debug_error( __FUNCTION__,
                              "can't find header delimiter in \n%s\n",
                              (gchar *)val );
          }

          if ( parts[0] != NULL ) {
            gchar *typist = NULL;
            gchar *sessid = NULL;
            gchar *format = NULL;
            gint id = 0; /* for chats */
            gboolean is_chat = FALSE;
            gint msgtype = RVP_MSG_UNKNOWN;
            gchar *contenttype;
            GHashTable *parsedheaders = NULL;

            contenttype = get_header_content( parts[0], "content-type",
                                              &parsedheaders );
            if ( contenttype ) {
              if ( !strncmp( contenttype, "text/plain",
                             strlen( "text/plain" ))) {
                /* the rest may or may not specify a charset */
                /* fixme should grab it if it's there and behave
                   accordingly */
                msgtype = RVP_MSG_IM;
              } else if ( !strncmp( contenttype, "text/x-msmsgscontrol",
                                    strlen( "text/x-msmsgscontrol" ))) {
                msgtype = RVP_MSG_TYPING;
              } else if ( !strncmp( contenttype, "text/x-msmsgsinvite",
                                    strlen( "text/x-msmsgsinvite" ))) {
                msgtype = RVP_MSG_INVITE;
              } else if ( !strncmp( contenttype, "text/x-imleave",
                                    strlen( "text/x-imleave" ))) {
                msgtype = RVP_MSG_CHAT_LEAVE;
              }
            }

            /* todo: MIME-Version (ignored) */
            sessid = get_header_content( parts[0], "session-id",
                                         &parsedheaders );
            typist = get_header_content( parts[0], "typinguser",
                                         &parsedheaders );
            format = get_header_content( parts[0], "x-mms-im-format",
                                         &parsedheaders );

            if ( sessid != NULL ) {
              /* get rid of the curlies */
              if ( sessid[0] == '{' ) {
                sessid = memmove( sessid, &sessid[1], strlen( sessid ) - 1 );
              }
              if ( sessid[strlen( sessid ) - 2] == '}' ) {
                sessid[strlen( sessid ) - 2] = '\0';
              }

              gaim_debug_misc( __FUNCTION__, "session id %s\n", sessid );

              /* see if this is a multi-user chat! */
              if ( rd->chats ) {
                conv = g_hash_table_lookup( rd->chats, sessid );

                if ( conv != NULL ) {
                  if ( gaim_conversation_get_type( conv ) ==
                       GAIM_CONV_TYPE_CHAT ) {
                    gaim_debug_misc( __FUNCTION__,
                                     "that's apparently a chat\n" );
                    id = gaim_conv_chat_get_id( GAIM_CONV_CHAT( conv ));
                    is_chat = TRUE;
                  } else {

                  }
                }
              }

              /* you've been press-ganged into a multi-user chat */
              if ( !is_chat && to_contacts[0] != NULL &&
                   to_contacts[1] != NULL && msgtype != RVP_MSG_CHAT_LEAVE ) {
                GHashTable *chat = g_hash_table_new( g_str_hash, g_str_equal );
                is_chat = TRUE;

                gaim_debug_misc( __FUNCTION__, "we appear to be in a chat\n" );

                g_hash_table_insert( chat, "sessid", sessid );
                g_hash_table_insert( chat, "from", from_contact );
                g_hash_table_insert( chat, "others", to_contacts );

                serv_join_chat( gc, chat );
              }

              if ( is_chat ) {
                rvp_clear_sessid( gc, sessid );
              }

              /* see if we've got a handle to work with */
              if ( from_contact ) {
                RVPBuddy *p;
                if ( buddy == NULL ) {
                  buddy = rvp_get_temp_buddy_create( gc, from_contact );
                }

                /* this won't help for users not in your buddy list
                   because gaim explictly ignores their
                   server_aliases. OH well. */
                if ( buddy->server_alias == NULL ) {
                  buddy->server_alias = g_strdup( from_contact->server_alias );
                }

                p = buddy->proto_data;

                if ( p ) {
                  /* trash the old session id only if this is a
                     private chat */
                  if ( !is_chat ) {
                    if ( p->sessionid != NULL ) {
                      g_free( p->sessionid );
                    }
                    p->sessionid = g_strdup( sessid );
                  }
                } else {
                  /* can't happen, ish */
                  gaim_debug_error( __FUNCTION__, "no proto data\n" );
                }
              } else {
                /* errrr. */
              }
            }

            if ( parts[1] != NULL ) {
              gchar *msgbody;

              switch( msgtype ) {
              case RVP_MSG_IM:
                if ( format != NULL ) {
                  msgbody = rvp_format( (gchar *)parts[1], format );
                } else {
                  msgbody = (gchar *)parts[1];
                }

                if ( is_chat ) {
                  serv_got_chat_in( gc, id, buddy->name, 0, msgbody,
                                    time( NULL ));
                } else {
                  serv_got_im( gc, buddy->name, msgbody, 0, time( NULL ));
                  /*
                  GaimConversation *conv;
                  conv = gaim_find_conversation_with_account( buddy->name, gaim_connection_get_account( gc ));
                  g_hash_table_replace( rd->chats, sessid, conv );
                  */
                }

                if ( format != NULL ) {
                  g_free( msgbody );
                }

                break;

              case RVP_MSG_TYPING:
                /* hrm. not only is there an at-sign in the typist, but
                   the domain is technically incorrect. I could probably
                   look it up in the buddy list or via the session ID,
                   but for now: */
                if ( typist == NULL ) {
                  /* this happens when a remote user pops up a dialog
                     box to talk to you but hasn't started typing
                     yet. this can be used to pop a dialog box
                     asking if you want to receive an unsolicited
                     message. if there are multiple recipients for
                     this then it's an invite to a multi-user chat */
                  /* More importantly it gives you the session ID for
                     the user who wants to talk to you! */
                  if ( to_contacts[ 1 ] != NULL ) {
                    GHashTable *chat = g_hash_table_new( g_str_hash,
                                                         g_str_equal );

                    g_hash_table_insert( chat, "sessid", sessid );
                    g_hash_table_insert( chat, "from", from_contact );
                    g_hash_table_insert( chat, "others", to_contacts );

                    /* the proper windows-like logic here is to only
                       do a serv_join_chat here if we're already in
                       conversation; otherwise we should note the
                       sessid and punt until we receive a message from
                       this conversation */
                    /* serv_got_chat_invite( gc, "Multi-User Conversation",
                       from_contact->name, NULL, chat ); */
                    serv_join_chat( gc, chat );
                  } else {
                    /* nothing to do for now, since ACLs seem to be
                       managed on the server? */
                    /* should save the session ID, except I think I've
                       already done that... */
                  }
                } else {
                  if ( buddy != NULL && buddy->name == NULL ) {
                    gaim_debug_misc( __FUNCTION__, "added buddy name %s\n",
                                     typist );
                    buddy->name = g_strdup( typist );
                  }

                  serv_got_typing( gc, typist, EXCHANGE_TYPING_TIMEOUT,
                                   GAIM_TYPING );
                }
                break;

              case RVP_MSG_CHAT_LEAVE:
                /* user has left a chat */
                rvp_chat_user_left( gc, id, from_contact );
                break;

                /* invite gets used for a bunch of messages... */
              case RVP_MSG_INVITE:
                rvp_parse_invite( gc, buddy, (char *)parts[1] );
                break;

              default:
                gaim_debug_misc( __FUNCTION__, "unknown messagetype %s\n",
                                 contenttype );
                break;
              }
            } else {
              gaim_debug_misc( __FUNCTION__, "empty message\n" );
            }

            g_strfreev( parts );
          } else {
            /* no parts, but we've already warned */
          }
          xmlFree( val );
        }
      } else {
        xmlNodePtr prop = props->xmlChildrenNode;
        while( prop ) {
          xmlNodePtr name = prop->xmlChildrenNode;
          xmlNodePtr value = NULL;

          if ( name != NULL ) {
            value = name->xmlChildrenNode;
          } else {
            /* we expect a null here, as it happens */
            val = xmlNodeListGetString( doc, name, 1 );
            if ( val != NULL ) {
              gaim_debug_misc( __FUNCTION__, "non-null prop\n" );
            }
            goto proploop;
          }

          if ( buddy == NULL ) {
            buddy = rvp_get_temp_buddy( gc, from_contact );
          }

          if ( !xmlStrcmp( name->name, BAD_CAST "state")) {
            if ( buddy != NULL ) {
              rvp_set_buddy_status( gc, buddy->name,
                                    (gchar *)value->name );
            } else {
              RVPBuddy *rb_from = from_contact->proto_data;
              gaim_debug_misc( __FUNCTION__,
                               "got state for unknown buddy %s\n",
                               rb_from->principal );
            }
          } else {
            gaim_debug_misc( __FUNCTION__, "unknown property %s\n",
                             name->name );
          }

        proploop:
          prop = prop->next;
        }
      }
    } else {
      gaim_debug_error( __FUNCTION__, "unhandled notification\n" );
#ifdef DEBUG
      xmlDebugDumpString( stderr, notification->name );
#endif
#ifdef DEBUG
      xmlDebugDumpNode( stderr, notification, 0 );
#endif
    }

  loop:

#if 0
    /* This stuff isn't right just yet, and I don't know why not.
       at least some of the problem is caused by passing this stuff
       into hash tables to the chat code */
    if ( from_contacts ) {
      int i = 0;
      while(( from_contact = from_contacts[ i ] ) != NULL ) {
        if ( from_contact->proto_data ) {
          rvp_free_buddy( from_contact );
        }
        if ( from_contact->name ) {
          g_free( from_contact->name );
        }
        if ( from_contact->server_alias ) {
          g_free( from_contact->server_alias );
        }
      }
      g_free( from_contacts );
      from_contacts = NULL;
    }

    if ( to_contacts ) {
      int i = 0;
      while(( to_contact = to_contacts[ i ] ) != NULL ) {
        if ( to_contact->proto_data ) {
          rvp_free_buddy( to_contact->proto_data );
        }
        if ( to_contact->name ) {
          g_free( to_contact->name );
        }
        if ( to_contact->server_alias ) {
          g_free( to_contact->server_alias );
        }
        g_free( to_contact );
      }
      g_free( to_contacts );
    }
#endif

    notification = notification->next;
  }

 out:
  if ( doc ) {
    xmlFreeDoc( doc );
  }
  gaim_debug_misc( __FUNCTION__, "exit %d\n", retval );

  return retval;
}

/*
 * Using ACCOUNT, generate a normalised version of the username S
 */
static const char *rvp_normalize( const GaimAccount *account, const char *s ) {
  static char buf[BUF_LEN]; /* gaim requirement, alas */
  gchar **bits;
  gchar *host = NULL;

  g_return_val_if_fail( s != NULL, NULL );

  bits = g_strsplit( s, "@", 2 );

  if ( bits == NULL || bits[0] == NULL ) {
    gaim_debug_error( __FUNCTION__, "failed to split %s\n", s );
    return s;
  }

  if ( bits[1] == NULL ) {
    /* no hostname specified: use the same one as our account */
    GaimConnection *gc = gaim_account_get_connection( account );
    RVPData *rd;
    if ( gc != NULL ) {
      rd = gc->proto_data;
      if ( rd->authdomain != NULL ) {
        host = g_strdup( rd->authdomain );
      } else {
        gaim_debug_error( __FUNCTION__, "doooh 1\n" );
        return s;
      }
    } else {
      /* GaimConnection is unavailable, so we'll need to use
         GaimAccount */
      gchar **bits2 = g_strsplit( gaim_account_get_username( account ),
                                  "@", 2 );
      if ( bits2 == NULL || bits[0] == NULL ) {
        gaim_debug_error( __FUNCTION__, "failed to split %s\n",
                          gaim_account_get_username( account ));
        return s;
      }
      if ( bits2[1] == NULL ) {
        /* you're really starting to annoy me */
        host = g_strdup( gaim_account_get_string( account, "host", NULL ));
      } else {
        host = g_strdup( bits2[1] );
      }
      g_strfreev( bits2 );
    }
  } else {
    host = g_strdup( bits[1] );
  }

  g_snprintf( buf, sizeof( buf ), "%s@%s", bits[0], host );
  g_strfreev( bits );
  g_free( host );

  gaim_debug_misc( __FUNCTION__, "%s -> %s\n", s, buf );

  return buf;
}

/*
 * generate a session ID
 * from the expired RFC:
 *
 * 10.2.19.  Session-ID
 *
 *  Sent with a NOTIFY message when there is no subscription-ID; used
 *  to maintain context in replies to that notification.
 *
 * The Microsoft documentation has no information on this. From
 * observation, Microsoft Messenger preserves session ID only for as
 * long as its login persists. As soon as you sign out and back in
 * again, it creates a new session ID.
 */
static gchar *rvp_get_sessid( void ) {
  gchar *my_sessid = g_malloc0( 37 );
  int i, r;

  for (i = 0; i < 36; i++) {
    if ( i == 8 || i == 13 || i == 18 || i == 23 )
      my_sessid[i] = '-';
    else {
      r = random_integer( 0, 15 );
      my_sessid[ i ] = HEX_ELM[ r ];
    }
  }

  gaim_debug_misc( __FUNCTION__, "generated new ID %s\n", my_sessid );

  return my_sessid;
}

/*
 * Return the name of an icon for this protocol. This is also used for
 * looking up smileys and for logging, so while I'd like to piggyback
 * on MSN, it just confuses things.
 * not really sure why this is a /function/...
 */
static const char *rvp_list_icon( GaimAccount *a, GaimBuddy *b ) {
  return "rvp";
}

/*
 * Figure out what state a buddy is in
 *
 */
static void rvp_list_emblems( GaimBuddy *b, rvpleconst char **se,
                              rvpleconst char **sw, rvpleconst char **nw,
                              rvpleconst char **ne ) {
  RVPBuddy *rb = b->proto_data;
  rvpleconst char *emblems[4] = { NULL, NULL, NULL, NULL };
  GaimAccount *ac = b->account;
  int i = 0;
#if GAIM_MAJOR_VERSION >= 2
  GaimPresence *presence;

  presence = gaim_buddy_get_presence( b );
#endif

  if ( !GAIM_BUDDY_IS_ONLINE( b )) {
    emblems[i++] = "offline";
  } else {
#if GAIM_MAJOR_VERSION < 2
    switch( b->uc ) { /* seekrit away_type flag */
    case RVP_IDLE:
    case RVP_BRB:
    case RVP_AWAY:
    case RVP_LUNCH:
      emblems[i++] = "away";
      break;

    case RVP_BUSY:
    case RVP_PHONE:
      emblems[i++] = "occupied"; /* or maybe dnd */
      break;

    default:
      break;
    }
#else
    if ( gaim_presence_is_status_active( presence, "busy" ) ||
         gaim_presence_is_status_active( presence, "phone" )) {
      emblems[i++] = "occupied";
    } else if ( !gaim_presence_is_available( presence )) {
      emblems[i++] = "away";
    }

#endif
  }

  if ( rb != NULL ) {
    if (( rb->acl & RVP_ACL_BUDDY ) != RVP_ACL_BUDDY ) {
      /* maybe we've just got a default allow */
      /* need to check the allprincipals ACL */
      switch ( ac->perm_deny ) {
      case GAIM_PRIVACY_ALLOW_ALL:
      case GAIM_PRIVACY_DENY_USERS:
        /* only deny listed users; implicit allow */
        break;
      default:
        /* only allow listed users; implicit deny */
        emblems[i++] = "dnd"; /* looks better than "ignored" */
        break;
      }
    }
  }

  *se = emblems[0];
  *sw = emblems[1];
  *nw = emblems[2];
  *ne = emblems[3];
}

/*
 * This seems to get called a /lot/.
 */
static char *rvp_status_text( GaimBuddy *b ) {
  RVPBuddy *rb;
  gint i;

  gaim_debug_misc( __FUNCTION__, "enter\n" );

  rb = b->proto_data;
  if ( rb != NULL ) {
    if ( rb->state != NULL ) {
      /* find the current state in our list of states */
      for ( i = RVP_ONLINE; i < RVP_UNKNOWN; i++ ) {
        if ( !strcmp( awaymsgs[i].tag, rb->state )) {
          return g_strdup( awaymsgs[i].text );
        }
      }
    }
  }

  /* Fall through */
  return NULL;
}

/*
 * close the RVP connection
 */
static void rvp_close( GaimConnection *gc ) {
  RVPData *rd = gc->proto_data;
  GaimAccount *ac = gaim_connection_get_account( gc );
  gchar *args[1] = { "offline" };

  gaim_debug_misc( __FUNCTION__, "enter\n" );

  /* immediately flag me as being away */
  /* silly gaim. account is nulled out before we get here, which means
     that the input callbacks won't work. */
  if ( gaim_account_get_connection( ac ) == NULL ) {
    gaim_account_set_connection( ac, gc );
  }

  if ( GAIM_CONNECTION_IS_CONNECTED( gc )) {
    GaimFetchUrlData *gfud;

    /* apparently the correct order of things is unsub me, unsub
       everyone else, proppatch me */

    gaim_debug_misc( __FUNCTION__, "sending self unsub\n" );

    /* unsubscribe myself */
    gfud = rvp_send_request( gc, "UNSUBSCRIBE", NULL );
    if ( gfud != NULL ) {
      /* wait for it to get sent */
      zero_time;
      while( !g_hash_table_lookup( rd->pending, gfud )) {
        while( gtk_events_pending()) {
          report_sent( gfud );
          gtk_main_iteration();
          /* there's a race condition that can cause the above to
             never detect the send (basically, the send and receive
             happens before the loop gets to check if the message was
             ever sent) so we'll check for the side-effects of the
             unsubscribe also.

             And thus I am entering the legendary realm of the comment
             on a bug that takes up more space and time than fixing
             the bug would take. Maybe. */
          if ( rd->subs_id == 0 ) {
            break;
          }
        }
      }
    }

    if ( gaim_prefs_get_bool( "/plugins/prpl/rvp/fast_logout" )) {
      gaim_debug_misc( __FUNCTION__, "skipping full unsubscribe\n" );
    } else {
      /* unsubscribe anyone we'd subscribed to */
      rvp_do_for_buddies( gc, rvp_close_unsubscribe );
    }

    /*
       Cancel any running file transfers, which is the sort of thing
       you'd expect to be in gaim's core...
    */
    if ( rd->me.sendcookies != NULL ) {
      g_hash_table_foreach( rd->me.sendcookies, rvp_cancel_xfer, NULL );
      g_hash_table_destroy( rd->me.sendcookies );
      rd->me.sendcookies = NULL;
    }
    if ( rd->me.recvcookies != NULL ) {
      g_hash_table_foreach( rd->me.recvcookies, rvp_cancel_xfer, NULL );
      g_hash_table_destroy( rd->me.recvcookies );
      rd->me.recvcookies = NULL;
    }

    /* now proppatch my local viewid to be offline */
    gfud = rvp_send_request( gc, "PROPPATCH", args );

    if ( gfud != NULL ) {
      /* wait for it to get sent */
      zero_time;
      while( !g_hash_table_lookup( rd->pending, gfud )) {
        while( gtk_events_pending()) {
          report_sent( gfud );
          gtk_main_iteration();

          /* see race condition mentioned elsewhere */
          /* XXX check for proppatch result */
        }
      }
    } else {
      gaim_debug_error( __FUNCTION__, "Failed to send PROPPATCH\n" );
    }

    /* now wait for all pending requests to clear */
    while( g_hash_table_size( rd->pending )) {
      while( gtk_events_pending()) {
        report_free( rd->pending );
        gtk_main_iteration();
        /* xxx once again, race condition. bah. */
      }
    }

  } else {
    gaim_debug_misc( __FUNCTION__, "apparently I'm not logged in\n" );
  }

  /* now, where were we? */
  gaim_account_set_connection( ac, NULL );

  /* destroy all the RVP buddy data */
  rvp_do_for_buddies( gc, rvp_cleanup );

  /* shut down the listener */
  if ( rd != NULL ) {
    if ( rd->linpa ) {
      gaim_input_remove( rd->linpa );
    }
    rd->linpa = 0;

    if ( rd->listener_fd > 0 ) {
      close( rd->listener_fd );
    }
    rd->listener_fd = 0;

    if ( rd->nonbuddy != NULL ) {
      g_hash_table_destroy( rd->nonbuddy );
    }

    if ( rd->principal != NULL ) {
      gaim_debug_misc( __FUNCTION__, "freeing %p principal\n", rd->principal );
      g_free( rd->principal );
    }
  }

  /* ideally this is in an exit function somewhere... */
  xmlCleanupParser();

  gc->proto_data = NULL;

  gaim_debug_misc( __FUNCTION__, "exit\n" );
}

/*
 * Keepalive: repatch view and subscriptions when they're about to
 * expire. Windows client appears to use 5 minutes of slop. Anything
 * less than two from the server makes the Windows client deeply,
 * deeply unhappy.
 */
static void rvp_keepalive( GaimConnection *gc ) {
  RVPData *rd = gc->proto_data;

  if ( rd->view_expiry - time( NULL ) < 120 ) {
    gaim_debug_misc( __FUNCTION__, "view expires in %d seconds, renewing\n",
                     rd->view_expiry - time( NULL ));
    rvp_send_request( gc, "PROPPATCH", NULL );
  }

  /* two minutes. windows client is at least this antsy */
  if ( rd->subs_expiry - time( NULL ) < 120 ) {
    GaimBuddyList *blist;
    GaimBlistNode *group, *cnode, *bnode;
    GaimBuddy *buddy;

    gaim_debug_misc( __FUNCTION__,
                     "main sub expires in %d seconds, renewing\n",
                     rd->subs_expiry - time( NULL ));

    /* resubscribe:
       send a SUBSCRIBE for me, and one for everything I'm subscribed
       to, and instead of a Notification-Type header use the existing
       subscription ID. */
    blist = gaim_get_blist();
    for ( group = blist->root; group; group = group->next ) {
      for ( cnode = group->child; cnode; cnode = cnode->next ) {
        if ( !GAIM_BLIST_NODE_IS_CONTACT(cnode)) {
          continue;
        }
        for ( bnode = cnode->child; bnode; bnode = bnode->next ) {
          if (!GAIM_BLIST_NODE_IS_BUDDY( bnode )) {
            continue;
          }

          buddy = (GaimBuddy *)bnode;

          /* only subscribe things attached to my account! */
          if ( buddy->account != gaim_connection_get_account( gc )) {
            continue;
          }

#if GAIM_MAJOR_VERSION < 2
          serv_add_buddy( gc, buddy );
#else
          gaim_account_add_buddy( gaim_connection_get_account( gc ), buddy );
#endif
        }
      }
    }
    rvp_send_request( gc, "SUBSCRIBE", NULL );
  }
}

/*
 * ACL routines
 */
static void rvp_set_permit_deny( GaimConnection *gc ) {
  RVPData *rd = gc->proto_data;
  GaimAccount *ac = gaim_connection_get_account( gc );
  int rvp_perm_deny, gaim_perm_deny;

  if ( !GAIM_CONNECTION_IS_CONNECTED( gc )) {
    /* prematurely called */
    return;
  }

  /* per server.c:
   * this is called when either you import a buddy list, and make lots
   * of changes that way, or when the user toggles the permit/deny
   * mode in the prefs. In either case you should probably be
   * resetting and resending the permit/deny info when you get this.
   */

  /* what's our current setting? */
  if (( rd->defaultacl & RVP_ACL_BUDDY ) == RVP_ACL_BUDDY ) {
    rvp_perm_deny = GAIM_PRIVACY_DENY_USERS;
  } else {
    rvp_perm_deny = GAIM_PRIVACY_ALLOW_USERS;
  }

  /* remap Gaim's setting to something we actually agree with */
  switch ( ac->perm_deny ) {
  case GAIM_PRIVACY_ALLOW_ALL:
  case GAIM_PRIVACY_DENY_USERS:
    gaim_perm_deny = GAIM_PRIVACY_DENY_USERS;
    break;
  default:
    gaim_perm_deny = GAIM_PRIVACY_ALLOW_USERS;
    break;
  }

  /* flip the default ACL */
  if ( gaim_perm_deny != rvp_perm_deny ) {
    if ( gaim_perm_deny == GAIM_PRIVACY_DENY_USERS ) {
      rvp_set_acl( gc, "allprincipals", RVP_ACL_CREDENTIALS | RVP_ACL_BUDDY, 0 );
    } else {
      /* this is what happens when you block "Other exchange users" */
      rvp_set_acl( gc, "allprincipals",
                   RVP_ACL_CREDENTIALS | RVP_ACL_LIST | RVP_ACL_READ, 0 );
    }

    if ( ac->perm_deny == GAIM_PRIVACY_ALLOW_BUDDYLIST ) {
      /* fixme: go through buddy list and remove any blocks */
    }
  }
}

static void rvp_add_permit( GaimConnection *gc, const char *who ) {
  gchar *principal;

  gaim_debug_misc( __FUNCTION__, "%s\n", who );

  principal = rvp_principal_from_address( gc, who );

  if ( principal ) {
    rvp_set_acl( gc, principal,
                 RVP_ACL_CREDENTIALS | RVP_ACL_BUDDY, 0 );
    g_free( principal );
  }
}

static void rvp_rem_permit( GaimConnection *gc, const char *who ) {
  gchar *principal;

  gaim_debug_misc( __FUNCTION__, "%s\n", who );

  principal = rvp_principal_from_address( gc, who );

  if ( principal ) {
    if ( gaim_find_buddy( gaim_connection_get_account( gc ), who )) {
      rvp_set_acl( gc, principal, RVP_ACL_CREDENTIALS | RVP_ACL_BUDDY, 0 );
    } else {
      rvp_set_acl( gc, principal, RVP_ACL_CREDENTIALS, 0 );
    }
    g_free( principal );
  }
}

/*
 * block
 */
static void rvp_add_deny( GaimConnection *gc, const char *who ) {
  gchar *principal;

  gaim_debug_misc( __FUNCTION__, "%s\n", who );

  principal = rvp_principal_from_address( gc, who );

  if ( principal ) {
    /* first clear all privs */
    /*rvp_set_acl( gc, principal, RVP_ACL_CREDENTIALS, 0 );*/
    /* then set a block */
    rvp_set_acl( gc, principal, RVP_ACL_CREDENTIALS,
                 RVP_ACL_BUDDY );
    g_free( principal );
  }
}

/*
 * unblock
 */
static void rvp_rem_deny( GaimConnection *gc, const char *who ) {
  gchar *principal;

  gaim_debug_misc( __FUNCTION__, "%s\n", who );

  principal = rvp_principal_from_address( gc, who );

  if ( principal ) {
    /* first clear all privs */
    /*rvp_set_acl( gc, principal, RVP_ACL_CREDENTIALS, 0 );*/
    /* now unblock */
    if ( gaim_find_buddy( gaim_connection_get_account( gc ), who )) {
      rvp_set_acl( gc, principal,
                   RVP_ACL_CREDENTIALS | RVP_ACL_BUDDY, 0 );
    } else {
      rvp_set_acl( gc, principal, RVP_ACL_CREDENTIALS, 0 );
    }

    g_free( principal );
  }
}

/*
 * Populate buddy info window. Doesn't actually *get* information,
 * since that's an asynchronous process and I've not set it up yet.
 */
static void rvp_get_info( GaimConnection *gc, const char *who ) {
  GaimBuddy *buddy = rvp_find_buddy_by_name( gc, (gchar *)who );
  RVPBuddy *rb = NULL;
  gchar *info = NULL;
  gchar *title = NULL;

  /*
   * this will look up the nonbuddy hashtable
   */
  if ( buddy == NULL ) {
    buddy = rvp_buddy_from_address( gc, who );
  }

  if ( buddy == NULL || buddy->proto_data == NULL ) {
    info = g_strdup_printf( "Sorry, no info available on %s", who );
  } else {
    rb = buddy->proto_data;

    /* I'd like to put an icon here, but I've not read up that yet. */

    info = g_strdup_printf( "Sign-in Name: %s<br>"
                            "Status: %s<br>"
                            "Service: Microsoft Exchange Instant Messaging<br>"
                            "E-mail: %s",
                            buddy->name,
                            rb->state ? rb->state : "unknown",
                            rb->email ? rb->email : "unknown"
                            );

    title = buddy->server_alias ? buddy->server_alias : buddy->name;

#ifdef DEBUG
  if ( rb->acl & RVP_ACL_ASSERTION )
    gaim_debug_misc( __FUNCTION__, "CRED: assertion\n" );
  if ( rb->acl & RVP_ACL_DIGEST )
    gaim_debug_misc( __FUNCTION__, "CRED: digest\n" );
  if ( rb->acl & RVP_ACL_NTLM )
    gaim_debug_misc( __FUNCTION__, "CRED: ntlm\n" );

  if ( rb->acl & RVP_ACL_LIST )
    gaim_debug_misc( __FUNCTION__, "ACL: list\n" );
  if ( rb->acl & RVP_ACL_READ )
    gaim_debug_misc( __FUNCTION__, "ACL: read\n" );
  if ( rb->acl & RVP_ACL_WRITE )
    gaim_debug_misc( __FUNCTION__, "ACL: write\n" );
  if ( rb->acl & RVP_ACL_SEND_TO )
    gaim_debug_misc( __FUNCTION__, "ACL: send-to\n" );
  if ( rb->acl & RVP_ACL_RECEIVE_FROM )
    gaim_debug_misc( __FUNCTION__, "ACL: receive-from\n" );
  if ( rb->acl & RVP_ACL_READACL )
    gaim_debug_misc( __FUNCTION__, "ACL: readacl\n" );
  if ( rb->acl & RVP_ACL_WRITEACL )
    gaim_debug_misc( __FUNCTION__, "ACL: writeacl\n" );
  if ( rb->acl & RVP_ACL_PRESENCE )
    gaim_debug_misc( __FUNCTION__, "ACL: presence\n" );
  if ( rb->acl & RVP_ACL_SUBSCRIPTIONS )
    gaim_debug_misc( __FUNCTION__, "ACL: subscriptions\n" );
  if ( rb->acl & RVP_ACL_SUBSCRIBE_OTHERS )
    gaim_debug_misc( __FUNCTION__, "ACL: subscribe-others\n" );
#endif

  }

  gaim_notify_formatted( gc, _("Buddy Information"), title ? title : "",
                         NULL, info, NULL, NULL );
  g_free( info );

}

/*
 * list of supported away states
 */
#if GAIM_MAJOR_VERSION < 2
static GList *rvp_away_states( GaimConnection *gc ) {
  GList *m = NULL;
  gint i = 0;

  /* build the list from our static list */
  for ( i = RVP_ONLINE; i < RVP_UNKNOWN; i++ ) {
    m = g_list_append( m, awaymsgs[ i ].text );
  }

  return m;
}
#else
static GList *rvp_away_states( GaimAccount *ac ) {
  GList *m = NULL;
  gint i = 0;
  GaimStatusType *type;

  /* build the list from our static list */
  for ( i = RVP_ONLINE; i < RVP_UNKNOWN; i++ ) {
    GaimStatusPrimitive prim = GAIM_STATUS_AWAY;

    if ( !strcmp( awaymsgs[i].tag, "online" )) {
      prim = GAIM_STATUS_AVAILABLE;
    }
    if ( !strcmp( awaymsgs[i].tag, "offline" )) {
      prim = GAIM_STATUS_OFFLINE;
    }

    type = gaim_status_type_new( prim, awaymsgs[i].tag, awaymsgs[ i ].text,
                                 TRUE );

    gaim_debug_misc( __FUNCTION__, "created state for %s\n", awaymsgs[i].tag );

    m = g_list_append( m, type );
  }

  return m;
}
#endif

/*
 * mark me as being away
 */
static void rvp_set_away_old( GaimConnection *gc, const char *state,
                              const char *msg ) {
  RVPData *rd = gc->proto_data;
  gchar *away = NULL;
  gchar *args[1];
  gint i = 0;

  gaim_debug_misc( __FUNCTION__, "setting myself %s with message %s\n",
                   state, msg == NULL ? "<none>" : msg );

  /* identify the appropriate state tag */
  for ( i = RVP_ONLINE; i < RVP_UNKNOWN; i++ ) {
    if ( !strcmp( awaymsgs[i].text, state )) {
      away = awaymsgs[i].tag;
      break;
    }
  }

  /* override: if msg is set, state is away (state is probably Custom) */
  if ( away == NULL ) {
    if ( msg != NULL ) {
      away = "away";
    } else {
      away = "online";
    }
  }

  if ( rd->me.state != NULL ) {
    g_free( rd->me.state );
  }
  rd->me.state = g_strdup( away );

  args[ 0 ] = away;
  rvp_send_request( gc, "PROPPATCH", args );

  /* let Gaim know we're away */
  if ( !strcmp( away, "online" )) {
#if GAIM_MAJOR_VERSION < 2
    if ( gc->away ) {
      g_free( gc->away );
      gc->away = NULL;
    }
#endif
  } else {
#if GAIM_MAJOR_VERSION < 2
    if ( msg == NULL ) {
      gc->away = g_strdup( away );
    } else {
      gc->away = g_strdup( msg );
    }
#endif
  }
}
#if GAIM_MAJOR_VERSION < 2
#define rvp_set_away rvp_set_away_old
#else
static void rvp_set_away( GaimAccount *account, GaimStatus *status ) {
  const gchar *msg;
  gaim_debug_info( __FUNCTION__, "Set status to %s\n",
                   gaim_status_get_name( status ));
  if ( !gaim_status_is_active( status ) ||
       !gaim_account_is_connected( account )) {
    return;
  }

  msg = gaim_status_get_attr_string( status, "message" );

  rvp_set_away_old( gaim_account_get_connection( account ),
                    gaim_status_get_name( status ), msg );
}
#endif

/*
 * automatically set away when idle
 */
static void rvp_set_idle( GaimConnection *gc, int idle ) {
  RVPData *rd = gc->proto_data;
  gchar *away;
  gchar *who;
  gchar *state;
  GaimAccount *ac;

  if ( gc->wants_to_die == TRUE ) {
    /* don't do this when we're shutting down! */
    return;
  }

  ac = gaim_connection_get_account( gc );

  if ( idle ) {
    away = awaymsgs[ RVP_IDLE ].tag; /* idle */
  } else {
    away = awaymsgs[ RVP_ONLINE ].tag; /* online */
  }

  rvp_set_away_old( gc, away, NULL );
  who = (gchar *)rvp_normalize( ac, gaim_account_get_username( ac ));
  state = g_strdup( rd->me.state );
  rvp_set_buddy_status( gc, who, state );
  g_free( state );
}

/*
 * support functions for context-click
 */
static void rvp_block( GaimBlistNode *node, gpointer data ) {
  GaimBuddy *buddy = ( GaimBuddy *) node;
  GaimAccount *ac;
  GaimConnection *gc;

  ac = buddy->account;
  gc = gaim_account_get_connection( ac );
  rvp_add_deny( gc, buddy->name );
}

static void rvp_unblock( GaimBlistNode *node, gpointer data ) {
  GaimBuddy *buddy = ( GaimBuddy *) node;
  GaimAccount *ac;
  GaimConnection *gc;

  ac = buddy->account;
  gc = gaim_account_get_connection( ac );

  rvp_rem_deny( gc, buddy->name );
}

static void rvp_send_email( GaimBlistNode *node, gpointer ignore ) {
  GaimBuddy *buddy = ( GaimBuddy *) node;
  RVPBuddy *rb = buddy->proto_data;
  gchar *command = NULL;

  /* this is all gtknotify does for mailto URLs */
  /* ok, it does more, but it's checking for 50 million mail clients
     and AGAIN they've not exposed the bloody API to us plugin
     authors. Tip: if you write useful code in your app, think about
     whether it might be of use to someone else, too */
  /* even better: gaim_running_gnome actually fails on my
     gnome-running office desktop. Brilliant stuff. */
  if ( gaim_running_gnome() == TRUE ) {
    command = g_strdup_printf( "gnome-open \"%s\"", rb->email );
  } else {
    gaim_notify_error( NULL, _("Error sending mail"),
                       "This only works under Gnome, and even then "
                       "pretty poorly", NULL );
  }
}

/*
 * This is invoked when you context-click on a buddy. Its job is to
 * return menu items for the buddy.
 */
static GList *rvp_buddy_menu( GaimBlistNode *node ) {
  GList *m = NULL;
  GaimBuddy *buddy;
  RVPBuddy *rb;
  GaimMenuAction *act;
  gchar *label;

  if ( !GAIM_BLIST_NODE_IS_BUDDY( node )) {
    return NULL;
  }

  buddy = (GaimBuddy *)node;
  rb = buddy->proto_data;

  /*
   * caution: if you add a bogus user to your list, there's a good
   * chance that there's no valid data in the rb structure.
   */
  if ( rb == NULL ) {
    return NULL;
  }

  if ( rb->email != NULL ) {
    label = g_strdup_printf( "%s (%s)", _("Send E-Mail"), rb->email );
    act = gaim_menu_item_new( label, rvp_send_email, NULL, NULL );
    m = g_list_append( m, act );
  }

  /* -- sep -- */

  if (( rb->acl & RVP_ACL_BUDDY ) == RVP_ACL_BUDDY ) {
    label = g_strdup_printf( "%s", _("Block"));
    act = gaim_menu_item_new( label, rvp_block, NULL, NULL );
  } else {
    label = g_strdup_printf( "%s", _("Unblock"));
    act = gaim_menu_item_new( label, rvp_unblock, NULL, NULL );
  }
  m = g_list_append( m, act );

  return m;
}

/*
 * set the state of a buddy
 */
static void rvp_set_buddy_status( GaimConnection *gc, gchar *nick,
                                  gchar *status ) {
  GaimAccount *ac = gaim_connection_get_account( gc );
  int online, stat;
  GaimBuddy *buddy;
  GaimBuddy *t = NULL;
  RVPBuddy *rb;
  RVPData *rd = gc->proto_data;

  if ( nick == NULL ) { /* wtf? */
    gaim_debug_misc( __FUNCTION__, "ignoring null nick\n" );
    return;
  }

  /* might happen when logging out */
  if ( rd == NULL ) {
    return;
  }

  if ( !strcmp( nick, rvp_normalize( ac, gaim_account_get_username( ac )))) {
    buddy = rd->me.buddy;
  } else {
    buddy = gaim_find_buddy( ac, nick );

    if ( buddy == NULL ) {
      t = rvp_buddy_from_address( gc, nick );
      if ( t == NULL ) {
        gaim_debug_error( __FUNCTION__, "failed to generate buddy from %s\n",
                          nick );
        return;
      }
      buddy = rvp_get_temp_buddy_create( gc, t );
    }
  }

  gaim_debug_misc( __FUNCTION__, "%s is %s\n", buddy->name, status );
  rb = buddy->proto_data;

  if ( rb != NULL ) {
    if ( rb->state != NULL ) {
      g_free( rb->state );
    }
    rb->state = g_strdup( status );
  }

  online = 1;

  /* set buddy online status */
  if ( !strcmp( status, "online" )) {
    stat = RVP_ONLINE;
  } else if ( !strcmp( status, "offline" ) ||
              !strcmp( status, "invisible" )) {
    online = 0;
    stat = RVP_OFFLINE;
  } else if ( !strcmp( status, "idle" )) {
    stat = RVP_IDLE;
  } else if ( !strcmp( status, "away" )) {
    stat = RVP_AWAY;
  } else if ( !strcmp( status, "busy" )) {
    stat = RVP_BUSY;
  } else if ( !strcmp( status, "back-soon" )) {
    stat = RVP_BRB;
  } else if ( !strcmp( status, "on-phone" )) {
    stat = RVP_PHONE;
  } else if ( !strcmp( status, "at-lunch" )) {
    stat = RVP_LUNCH;
  } else {
    /* catch new status and pop up a message window */
    gaim_notify_error( gc, _("Unknown status"), status, NULL );
    gaim_debug_error( __FUNCTION__, "unknown status '%s'\n", status );
    stat = RVP_UNKNOWN;
    return;
  }

  /* Don't let anyone set online, if we're shutting down */
  if ( !gc->wants_to_die ||  !online ) {
#if GAIM_MAJOR_VERSION < 2
    serv_got_update(gc, buddy->name, online, 0, 0, 0, stat );
#else
    gaim_prpl_got_user_status( gc->account, buddy->name,
                               ( online ? "online" : "offline" ), NULL );
#endif
  }
}

/*
 * set ACLs for a buddy
 */
static void rvp_set_acl( GaimConnection *gc, gchar *principal,
                         guint16 grantacl, guint16 denyacl ) {
  xmlDocPtr doc;
  xmlNodePtr root, acl, ace, node1, node2, node3, grant, deny;
  xmlChar *content;
  gint content_length;
  gchar *args[4];
  RVPData *rd = gc->proto_data;

  gaim_debug_misc( __FUNCTION__, "setting ACL for %s\n", principal );

  /* avoid accidents */
  if ( !strcmp( principal, rd->principal )) {
    grantacl |= RVP_ACL_ALL;
    denyacl = 0;
  }

  doc = xmlNewDoc( BAD_CAST "1.0" );
  root = xmlNewNode( NULL, BAD_CAST "a:rvpacl" );
  xmlNewNs( root, BAD_CAST "DAV:", BAD_CAST "d" );
  xmlNewNs( root, BAD_CAST "http://schemas.microsoft.com/rvp/",
            BAD_CAST "r" );
  xmlNewNs( root, BAD_CAST "http://schemas.microsoft.com/rvp/acl/",
            BAD_CAST "a" );
  xmlDocSetRootElement( doc, root );

  acl = xmlNewNode( NULL, BAD_CAST "a:acl" );
  node1 = xmlNewNode( NULL, BAD_CAST "a:inheritance" );
  node2 = xmlNewText( BAD_CAST "none" );
  xmlAddChild( node1, node2 );
  xmlAddChild( acl, node1 );

  ace = xmlNewNode( NULL, BAD_CAST "a:ace" );

  node1 = xmlNewNode( NULL, BAD_CAST "a:principal" );
  node2 = xmlNewNode( NULL, BAD_CAST "a:rvp-principal" );
  node3 = xmlNewText( BAD_CAST principal );
  xmlAddChild( node2, node3 );
  xmlAddChild( node1, node2 );
  xmlAddChild( ace, node1 );

  node2 = xmlNewNode( NULL, BAD_CAST "a:credentials" );

  if ( gaim_prefs_get_bool( "/plugins/prpl/rvp/no_assertions" )) {
    /* don't send assertion! */
    gaim_debug_misc( __FUNCTION__, "not sending assertions\n" );
  } else {
    if ( grantacl & RVP_ACL_ASSERTION ) {
      node3 = xmlNewNode( NULL, BAD_CAST "a:assertion" );
      xmlAddChild( node2, node3 );
    }
  }

  if ( grantacl & RVP_ACL_DIGEST ) {
    node3 = xmlNewNode( NULL, BAD_CAST "a:digest" );
    xmlAddChild( node2, node3 );
  }
  if ( grantacl & RVP_ACL_NTLM ) {
    node3 = xmlNewNode( NULL, BAD_CAST "a:ntlm" );
    xmlAddChild( node2, node3 );
  }
  xmlAddChild( node1, node2 );
  xmlAddChild( ace, node1 );

  grant = xmlNewNode( NULL, BAD_CAST "a:grant" );
  deny = xmlNewNode( NULL, BAD_CAST "a:deny" );

  if ( grantacl & RVP_ACL_LIST ) {
    node2 = xmlNewNode( NULL, BAD_CAST "a:list" );
    xmlAddChild( grant, node2 );
  }
  if ( denyacl & RVP_ACL_LIST ) {
    node2 = xmlNewNode( NULL, BAD_CAST "a:list" );
    xmlAddChild( deny, node2 );
  }
  if ( grantacl & RVP_ACL_READ ) {
    node2 = xmlNewNode( NULL, BAD_CAST "a:read" );
    xmlAddChild( grant, node2 );
  }
  if ( denyacl & RVP_ACL_READ ) {
    node2 = xmlNewNode( NULL, BAD_CAST "a:read" );
    xmlAddChild( deny, node2 );
  }
  if ( grantacl & RVP_ACL_WRITE ) {
    node2 = xmlNewNode( NULL, BAD_CAST "a:write" );
    xmlAddChild( grant, node2 );
  }
  if ( denyacl & RVP_ACL_WRITE ) {
    node2 = xmlNewNode( NULL, BAD_CAST "a:write" );
    xmlAddChild( deny, node2 );
  }
  if ( grantacl & RVP_ACL_SEND_TO ) {
    node2 = xmlNewNode( NULL, BAD_CAST "a:send-to" );
    xmlAddChild( grant, node2 );
  }
  if ( denyacl & RVP_ACL_SEND_TO ) {
    node2 = xmlNewNode( NULL, BAD_CAST "a:send-to" );
    xmlAddChild( deny, node2 );
  }
  if ( grantacl & RVP_ACL_RECEIVE_FROM ) {
    node2 = xmlNewNode( NULL, BAD_CAST "a:receive-from" );
    xmlAddChild( grant, node2 );
  }
  if ( denyacl & RVP_ACL_RECEIVE_FROM ) {
    node2 = xmlNewNode( NULL, BAD_CAST "a:receive-from" );
    xmlAddChild( deny, node2 );
  }
  if ( grantacl & RVP_ACL_READACL ) {
    node2 = xmlNewNode( NULL, BAD_CAST "a:readacl" );
    xmlAddChild( grant, node2 );
  }
  if ( denyacl & RVP_ACL_READACL ) {
    node2 = xmlNewNode( NULL, BAD_CAST "a:readacl" );
    xmlAddChild( deny, node2 );
  }
  if ( grantacl & RVP_ACL_WRITEACL ) {
    node2 = xmlNewNode( NULL, BAD_CAST "a:writeacl" );
    xmlAddChild( grant, node2 );
  }
  if ( denyacl & RVP_ACL_WRITEACL ) {
    node2 = xmlNewNode( NULL, BAD_CAST "a:writeacl" );
    xmlAddChild( deny, node2 );
  }
  if ( grantacl & RVP_ACL_PRESENCE ) {
    node2 = xmlNewNode( NULL, BAD_CAST "a:presence" );
    xmlAddChild( grant, node2 );
  }
  if ( denyacl & RVP_ACL_PRESENCE ) {
    node2 = xmlNewNode( NULL, BAD_CAST "a:presence" );
    xmlAddChild( deny, node2 );
  }
  if ( grantacl & RVP_ACL_SUBSCRIPTIONS ) {
    node2 = xmlNewNode( NULL, BAD_CAST "a:subscriptions" );
    xmlAddChild( grant, node2 );
  }
  if ( denyacl & RVP_ACL_SUBSCRIPTIONS ) {
    node2 = xmlNewNode( NULL, BAD_CAST "a:subscriptions" );
    xmlAddChild( deny, node2 );
  }
  if ( grantacl & RVP_ACL_SUBSCRIBE_OTHERS ) {
    node2 = xmlNewNode( NULL, BAD_CAST "a:subscribe-others" );
    xmlAddChild( grant, node2 );
  }
  if ( denyacl & RVP_ACL_SUBSCRIBE_OTHERS ) {
    node2 = xmlNewNode( NULL, BAD_CAST "a:subscribe-others" );
    xmlAddChild( deny, node2 );
  }

  xmlAddChild( ace, grant );
  xmlAddChild( ace, deny );
  xmlAddChild( acl, ace );
  xmlAddChild( root, acl );

  xmlDocDumpFormatMemory( doc, &content, &content_length, 0 );
  xmlFreeDoc( doc );

  args[0] = g_strdup_printf( principal );
  args[1] = (gchar *)content;
  args[2] = (gchar *)content_length;
  args[3] = NULL;

  rvp_send_request( gc, "ACL", args );
}

/*
 * add a buddy
 */
static void rvp_add_buddy( GaimConnection *gc, GaimBuddy *buddy,
                           GaimGroup *group ) {
  RVPData *rd = gc->proto_data;
  GaimBuddy *dup;
  RVPBuddy *rb;
  gchar *args[4];

  gaim_debug_misc( __FUNCTION__, "enter\n" );

  rb = buddy->proto_data;

  if ( rb == NULL ) {
    rb = g_new0( RVPBuddy, 1 );
    buddy->proto_data = rb;
    rb->buddy = buddy;
  }

  if ( rb->principal == NULL ) {
    rb->principal = rvp_principal_from_address( gc, buddy->name );
  }

  dup = rvp_get_temp_buddy( gc, buddy );
  if ( dup && dup != buddy ) {
#ifdef DEBUG
    /* I really, really, really hate this chunk of code */
    gaim_debug_misc( __FUNCTION__, "found a duplicate, cloning\n" );
    rvp_dump_buddy( __FUNCTION__, dup );
    gaim_debug_misc( __FUNCTION__, "post-cloning\n" );
#endif

    if ( rb != dup->proto_data ) {
      /*
        leaky, but freeing this is causing me grief
      if ( rb->principal != NULL ) {
        g_free( rb->principal );
      }
      */
      memcpy( rb, dup->proto_data, sizeof( RVPBuddy ));
      rb->buddy = buddy; /* fixup */
    }

    /* not me? remove from temp table! */
    if ( dup != rd->me.buddy ) {
      g_hash_table_remove( rd->nonbuddy, rb->principal );
    }

    /* also, update the buddy's state in the UI */
    if ( GAIM_CONNECTION_IS_CONNECTED( gc ) && rb->state ) {
      gchar *state = g_strdup( rb->state );
      rvp_set_buddy_status( gc, buddy->name, state );
      g_free( state );
    }
  }

#ifdef DEBUG
  rvp_dump_buddy( __FUNCTION__, buddy );
#endif

  /* don't subscribe if we're not connected (duh) */
  if ( GAIM_CONNECTION_IS_CONNECTED( gc )) {
    args[0] = (gchar *)buddy;
    args[1] = NULL;
    rvp_send_request( gc, "SUBSCRIBE", args );
  } else {
    gaim_debug_warning( __FUNCTION__, "not connected\n" );
  }
}

/*
 * remove a buddy from the buddy list
 */
static void rvp_rem_buddy(GaimConnection *gc, GaimBuddy *buddy,
                          GaimGroup *group) {
  RVPData *rd = gc->proto_data;
  RVPBuddy *rb = buddy->proto_data;
  gchar *args[1];

  args[0] = (gchar *)buddy;

  /* don't unsubscribe myself */
  if ( strcmp( rb->principal, rd->principal )) {
    rvp_send_request( gc, "UNSUBSCRIBE", args );
  }
}

static gint gc_strcmp_name( gconstpointer a, gconstpointer b ) {
  GaimConvChatBuddy *cb = (GaimConvChatBuddy *)a;
  gaim_debug_misc( __FUNCTION__, "%s <=> %s\n", cb->name, b );
  return strcmp( cb->name, b );
}

/* silly wrapper */
static gint gc_strcmp( gconstpointer a, gconstpointer b ) {
  return strcmp( a, b );
}

/*
 * generic notify function
 */
static int rvp_send_notify( GaimConnection *gc, const char *who, gint msgtype,
                            gchar *message, void *data ) {
  int typing = FALSE;
  GaimConversation *conv = NULL;
  GaimMessageFlags flags;
  GaimAccount *ac = gaim_connection_get_account( gc );
  GList *recipients = NULL, *r;
  GaimBuddy *buddy = NULL;
  RVPBuddy *rb = NULL;
  RVPData *rd = gc->proto_data;
  gchar *fullname;
  gchar *msg = NULL, *sessid = NULL;
  gchar *args[4];
  xmlDocPtr doc;
  xmlNodePtr root, xmlmsg, node, xmlcontact, body, mime;
  xmlChar *content = NULL;
  gint content_length;
  GList *buddies;
  gchar *format = NULL;

  gaim_debug_misc( __FUNCTION__, "Enter\n" );

  switch( msgtype ) {
  case RVP_MSG_TYPING:
    typing = *(int*)data;
    break;

  case RVP_MSG_IM:
    flags = *(GaimMessageFlags*)data;
    break;

  case RVP_MSG_INVITE:
    break;

  case RVP_MSG_CHAT_INVITE:
  case RVP_MSG_CHAT:
  case RVP_MSG_CHAT_LEAVE:
    conv = (GaimConversation *)data;
    sessid = gaim_conversation_get_data( conv, "sessid" );
    break;

  default:
    gaim_debug_error( __FUNCTION__, "unknown msgtype %d (1)\n", msgtype );
    break;
  }

  if ( msgtype == RVP_MSG_TYPING && typing != TRUE ) {
    return EXCHANGE_TYPING_TIMEOUT;
  }

  if ( sessid == NULL || msgtype == RVP_MSG_CHAT_INVITE ) {
    /* pick up a hostname if we don't have one */
    fullname = g_strdup( rvp_normalize( gaim_connection_get_account( gc ), who ));

    /*
     * Is it someone we know about?
     */
    buddy = gaim_find_buddy( ac, who );
    if ( buddy == NULL ) {
      buddy = gaim_find_buddy( ac, fullname );
    }

    if ( buddy == NULL ) {
      GaimBuddy *gb = rvp_buddy_from_address( gc, (gchar *)fullname );
      /* step 2, is it someone we're already conversing with? */
      /* gaim_get_conversations returns a glist of conversations from
         which I should be able to go digging for people */

      if ( gb == NULL ) {
        gaim_debug_misc( __FUNCTION__,
                         "rvp_buddy_from_address returned null for %s\n",
                         fullname );
        g_free( fullname );
        return ( msgtype == RVP_MSG_TYPING ? EXCHANGE_TYPING_TIMEOUT : 0 );
      }

      buddy = rvp_get_temp_buddy_create( gc, gb );
    }
    g_free( fullname );

    rb = buddy->proto_data;
    if ( rb == NULL ) {
      gaim_debug_misc( __FUNCTION__,
                       "you've not done the proto_data hookup\n" );
      rvp_dump_buddy( __FUNCTION__, buddy );
      return 0;
    }
  }

  if ( sessid == NULL ) {
    if ( rb->sessionid == NULL ) {
      rb->sessionid = rvp_get_sessid(); /* fixme may not be necessary! */
    }

    if ( sessid == NULL ) {
      sessid = rb->sessionid;
    }
  }

  /* gussy up the message some. we don't support proper formatting,
     and most definitely don't support putting URLs in as <a href="..."> */
  if ( msgtype == RVP_MSG_IM || msgtype == RVP_MSG_CHAT ) {
    format = g_strdup( "FN=MS%%20Shell%%20Dlg; EF=; CO=0; CS=0; PF=0" );
  }

  switch( msgtype ) {
  case RVP_MSG_TYPING:
    msg =
      g_strdup_printf( "MIME-Version: 1.0\r\n"
                       "Content-Type: text/x-msmsgscontrol; charset=UTF-8\r\n"
                       "TypingUser: %s\r\n"
                       "Session-Id: {%s}\r\n\r\n",
                       rvp_normalize( ac, gaim_account_get_username( ac )),
                       sessid );
    break;

  case RVP_MSG_CHAT_INVITE:
    msg =
      g_strdup_printf( "MIME-Version: 1.0\r\n"
                       "Content-Type: text/x-msmsgscontrol; charset=UTF-8\r\n"
                       "Session-Id: {%s}\r\n\r\n\r\n",
                       sessid );
    break;

  case RVP_MSG_IM:
  case RVP_MSG_CHAT:
    msg = g_strdup_printf( "MIME-Version: 1.0\r\n"
                           "Content-Type: text/plain; charset=UTF-8\r\n"
                           "X-MMS-IM-Format: %s\r\n"
                           "Session-Id: {%s}\r\n\r\n%s",
                           format,
                           sessid,
                           message );
    break;

  case RVP_MSG_CHAT_LEAVE:
    msg = g_strdup_printf( "MIME-Version: 1.0\r\n"
                           "Content-Type: text/x-imleave; charset=UTF-8\r\n"
                           "Session-Id: {%s}\r\n\r\n\r\n",
                           sessid );
    break;

  case RVP_MSG_INVITE:
    msg =
      g_strdup_printf( "MIME-Version: 1.0\r\n"
                       "Content-Type: text/x-msmsgsinvite; charset=UTF-8\r\n"
                       "Session-Id: {%s}\r\n\r\n"
                       "%s",
                       sessid,
                           message );
    break;

  default:
    gaim_debug_error( __FUNCTION__, "unknown msgtype %d (2)\n", msgtype );
    break;
  }

  /* xml content */
  doc = xmlNewDoc( BAD_CAST "1.0" );
  root = xmlNewNode( NULL, BAD_CAST "r:notification" );
  xmlNewNs( root, BAD_CAST "DAV:", BAD_CAST "d" );
  xmlNewNs( root, BAD_CAST "http://schemas.microsoft.com/rvp/",
            BAD_CAST "r" );
  xmlNewNs( root, BAD_CAST "http://schemas.microsoft.com/rvp/acl/",
            BAD_CAST "a" );
  xmlDocSetRootElement( doc, root );

  xmlmsg = xmlNewNode( NULL, BAD_CAST "r:message" );

  /* sender */
  node = xmlNewNode( NULL, BAD_CAST "r:notification-from" );
  xmlcontact = build_contact( NULL, NULL, rd->me.buddy->server_alias,
                              rd->principal );
  xmlAddChild( node, xmlcontact );
  xmlAddChild( xmlmsg, node );

  /* recipient */
  node = xmlNewNode( NULL, BAD_CAST "r:notification-to" );
  switch( msgtype ) {
  case RVP_MSG_TYPING:
  case RVP_MSG_IM:
  case RVP_MSG_INVITE:
    xmlcontact = build_contact( NULL, NULL, buddy->server_alias,
                                rb->principal );
    xmlAddChild( node, xmlcontact );
    gaim_debug_misc( __FUNCTION__, "recip: %s\n", rb->principal );
    recipients = g_list_append( recipients, g_strdup( rb->principal ));
    break;

  case RVP_MSG_CHAT_INVITE:
  case RVP_MSG_CHAT:
  case RVP_MSG_CHAT_LEAVE:
    if ( msgtype == RVP_MSG_CHAT_INVITE ) {
      recipients = g_list_append( recipients, g_strdup( rb->principal ));
      xmlcontact = build_contact( NULL, NULL, "", rb->principal );
      xmlAddChild( node, xmlcontact );
    }
    /* who's all up in there? */
    buddies = gaim_conv_chat_get_users( GAIM_CONV_CHAT( conv ));
    while( buddies ) {
      GaimConvChatBuddy *cb = (GaimConvChatBuddy *)buddies->data;
      GaimBuddy *b;

      gaim_debug_misc( __FUNCTION__, "buddy: %s\n", cb->name );

      /* gah. chat only stores the username */
      if (( b = rvp_find_buddy_by_name( gc, cb->name )) == NULL ) {
        b = rvp_buddy_from_address( gc, cb->name ); /* leak */
        if ( b == NULL ) {
          gaim_debug_error( __FUNCTION__, "failed to generate buddy from %s\n",
                            cb->name );
          continue;
        }
      }

      rb = b->proto_data;
      if ( !g_list_find_custom( recipients, rb->principal, gc_strcmp )) {
        xmlcontact = build_contact( NULL, NULL, "", rb->principal );
        xmlAddChild( node, xmlcontact );
        if ( msgtype != RVP_MSG_CHAT_INVITE ) {
          recipients = g_list_append( recipients, g_strdup( rb->principal ));
        }
      }
      buddies = buddies->next;
    }
    break;

  default:
    gaim_debug_error( __FUNCTION__, "unknown msgtype %d (1)\n", msgtype );
    break;
  }

  xmlAddChild( xmlmsg, node );

  /* message */
  body = xmlNewNode( NULL, BAD_CAST "r:msgbody" );
  mime = xmlNewNode( NULL, BAD_CAST "r:mime-data" );

  node = xmlNewCDataBlock( doc, BAD_CAST msg, strlen( msg ));
  xmlAddChild( mime, node );
  xmlAddChild( body, mime );
  xmlAddChild( xmlmsg, body );
  xmlAddChild( root, xmlmsg );

  /* render it */
  xmlDocDumpFormatMemory( doc, &content, &content_length, 0 );
  xmlFreeDoc( doc );

  /* everyone needs a 303^WNOTIFY */
  for ( r = recipients; r != NULL; r = r->next ) {
    gaim_debug_misc( __FUNCTION__, "notify to %s\n", (gchar *)r->data );

    /* shouldn't happen! */
    if ( r->data == NULL ) {
      continue;
    }

    args[0] = (gchar *)r->data;
    /* this gets freed elsewhere, so we have to send a duplicate */
    args[1] = g_malloc0( content_length );
    memcpy( args[1], content, content_length );
    args[2] = (gchar *)content_length;

    /* oooook. lazy waider. */
    if ( message != NULL ) {
      args[3] = g_strdup( message );
    } else {
      args[3] = NULL;
    }

    rvp_send_request( gc, "NOTIFY", args );
  }
  g_free( content );
  g_list_free( recipients );
  recipients = NULL;

  if ( msgtype == RVP_MSG_TYPING ) {
    return EXCHANGE_TYPING_TIMEOUT;
  } else {
    return 1;
  }
}

/*
 * send a typing notification
 */
static int rvp_send_typing( GaimConnection *gc, const char *who, int typing ) {
  return rvp_send_notify( gc, who, RVP_MSG_TYPING, NULL, &typing );
}

/*
 * send an instant message
 */
static int rvp_send_im( GaimConnection *gc, const char *who,
                        const char *message, GaimMessageFlags flags ) {
  return rvp_send_notify( gc, who, RVP_MSG_IM, (gchar *)message, &flags );
}

/*
 * send a file
 */
static void rvp_xfer_init_send( GaimXfer *xfer ) {
  GaimAccount *ac = gaim_xfer_get_account( xfer );
  GaimConnection *gc = gaim_account_get_connection( ac );
  gchar *filexfer;
  gint cookie = 0;
  GaimBuddy *buddy;
  RVPBuddy *rb;
  RVPInvite *inv;

  buddy = rvp_find_buddy_by_name( gc, xfer->who );
  if ( buddy == NULL ) {
    gaim_debug_misc( __FUNCTION__, "can't find buddy!" );
    gaim_xfer_cancel_local( xfer );
    return;
  }
  rb = buddy->proto_data;
  if ( rb->sendcookies == NULL ) {
    rb->sendcookies = g_hash_table_new( g_direct_hash, g_direct_equal );
  }

  while( cookie == 0 ) {
    cookie = random_integer( 1, 1 << 31 );
    if ( g_hash_table_lookup( rb->sendcookies, (gconstpointer)cookie )) {
      cookie = 0;
    }
  }

  inv = g_new0( RVPInvite, 1 );
  inv->data = xfer;
  inv->cookie = cookie;
  inv->type = RVP_INV_INVITE; /* not really important */
  inv->who = g_strdup( xfer->who );
  xfer->data = inv;
  g_hash_table_insert( rb->sendcookies, (gpointer)cookie, inv );

  filexfer = g_strdup_printf( "Application-Name: File Transfer\r\n"
                              "Application-GUID: %s\r\n"
                              "Invitation-Command: INVITE\r\n"
                              "Invitation-Cookie: %d\r\n"
                              "Application-File: %s\r\n"
                              "Application-FileSize: %d\r\n\r\n",
                              RVP_GUID_FILE_TRANSFER,
                              inv->cookie,
                              xfer->filename,
                              xfer->size );

  rvp_send_notify( gc, xfer->who, RVP_MSG_INVITE, filexfer, 0 );
  g_free( filexfer );
}

/*
 * cancel a send-in-progress. includes hitting the cancel button when
 * you're sending.
 */
static void rvp_xfer_cancel_send( GaimXfer *xfer ) {
  RVPInvite *inv = xfer->data;
  GaimAccount *ac = gaim_xfer_get_account( xfer );
  GaimConnection *gc = gaim_account_get_connection( ac );
  gchar *filexfer;

  gaim_debug_misc( __FUNCTION__, "Enter\n" );

  if ( inv != NULL ) {
    filexfer = g_strdup_printf( "Invitation-Command: CANCEL\r\n"
                                "Invitation-Cookie: %d\r\n"
                                "Cancel-Code: TIMEOUT\r\n\r\n",
                                inv->cookie );
    rvp_send_notify( gc, inv->who, RVP_MSG_INVITE, filexfer, 0 );
    g_free( filexfer );
  }
}

/*
 * This gets called twice: the first time to find out what file you
 * want to send, and the second time to set up the file send request.
 */
static void rvp_send_file( GaimConnection *gc, const char *who,
                           const char *filename ) {
  GaimAccount *ac = gaim_connection_get_account( gc );
  GaimXfer *xfer = gaim_xfer_new( ac, GAIM_XFER_SEND, who );

  gaim_xfer_set_init_fnc( xfer, rvp_xfer_init_send );
  gaim_xfer_set_cancel_send_fnc( xfer, rvp_xfer_cancel_send );
  gaim_xfer_set_request_denied_fnc( xfer, rvp_xfer_cancel_send );

  if ( filename ) {
    gaim_xfer_request_accepted( xfer, filename );
  } else {
    gaim_xfer_request( xfer );
  }
}

/*
 * essentially a no-op function since we don't block users from
 * sending us files.
 */
static gboolean rvp_can_receive_file( GaimConnection *gc, const char *who ) {
  return TRUE;
}

/*
 * invite someone to a chat
 */
void rvp_chat_invite( GaimConnection *gc, int id, const char *msg,
                      const char *who ) {
  GaimAccount *ac = gaim_connection_get_account( gc );
  GSList *bcs = gc->buddy_chats;
  GaimConversation *conv = NULL;
  gchar *norm;
  gchar *sessid;

  while( bcs ) {
    conv = (GaimConversation *)bcs->data;
    if ( id == gaim_conv_chat_get_id( GAIM_CONV_CHAT( conv ))) {
      break;
    }
    conv = NULL;
    bcs = bcs->next;
  }

  if ( !conv ) {
    return;
  }

  sessid = gaim_conversation_get_data( conv, "sessid" );

  norm = g_strdup( rvp_normalize( ac, who ));

  if ( !g_list_find_custom( gaim_conv_chat_get_users( GAIM_CONV_CHAT( conv )),
                            norm, gc_strcmp )) {
    gaim_debug_misc( __FUNCTION__, "inviting %s to chat %d (%s)\n", norm, id,
                     sessid );

    rvp_send_notify( gc, norm, RVP_MSG_CHAT_INVITE, NULL, conv );
    gaim_conv_chat_add_user( GAIM_CONV_CHAT( conv ), g_strdup( norm ), NULL,
                             GAIM_CBFLAGS_NONE, TRUE );
  } else {
    /* tell the user they're a berk */
  }

  g_free( norm );
}

/*
 * join a chat
 */
void rvp_chat_join( GaimConnection *gc, GHashTable *components ) {
  GaimConversation *conv = NULL;
  GaimBuddy *buddy = NULL;
  GaimBuddy **others = NULL;
  gchar *sessid = NULL;
  gint id = 0;
  RVPData *rd = gc->proto_data;

  gaim_debug_misc( __FUNCTION__, "Enter\n" );

  if ( components != NULL ) {
    sessid = g_hash_table_lookup( components, "sessid" );
    buddy = g_hash_table_lookup( components, "from" );
  }

  /* self-created chat */
  if ( sessid == NULL ) {
    gaim_debug_error( __FUNCTION__, "session id is null\n" );
    sessid = rvp_get_sessid();
  }
  if ( buddy == NULL ) {
    gaim_debug_error( __FUNCTION__, "chat instigator is null\n" );
    buddy = rd->me.buddy;
  }

  conv = g_hash_table_lookup( rd->chats, sessid );
  if ( conv == NULL ) {
    id = rd->chatid++;
  } else {
    id = gaim_conv_chat_get_id( GAIM_CONV_CHAT( conv ));
  }

  if ( conv == NULL ) {
    /* not sure about the third param */
    conv = serv_got_joined_chat( gc, id, sessid );
    rvp_dump_buddy( __FUNCTION__, buddy );
    gaim_debug_misc( __FUNCTION__, "%s created new chat %p\n", buddy->name,
                     conv );
  }
  g_hash_table_replace( rd->chats, g_strdup( sessid ), conv );
  gaim_conversation_set_data( conv, "sessid", sessid );

  /* to be sure to be sure */
  rvp_clear_sessid( gc, sessid );

  if ( !g_list_find_custom( gaim_conv_chat_get_users( GAIM_CONV_CHAT( conv )),
                            buddy->name, gc_strcmp_name )) {
    gaim_conv_chat_add_user( GAIM_CONV_CHAT( conv ), g_strdup( buddy->name ),
                             NULL, GAIM_CBFLAGS_NONE, TRUE );
    gaim_debug_misc( __FUNCTION__, "added instigator %s to chat\n",
                     buddy->name );
  }

  others = g_hash_table_lookup( components, "others" );
  if ( others != NULL ) {
    int i;
    GList *ulist;

    for ( i = 0; others[ i ] != NULL; i++ ) {
      ulist = gaim_conv_chat_get_users( GAIM_CONV_CHAT( conv ));

      if ( !g_list_find_custom( ulist, others[i]->name, gc_strcmp_name )) {
        gaim_conv_chat_add_user( GAIM_CONV_CHAT( conv ),
                                 g_strdup( others[i]->name ), NULL,
                                 GAIM_CBFLAGS_NONE, TRUE );
        gaim_debug_misc( __FUNCTION__, "added %s to chat\n", others[i]->name );
      } else {
        gaim_debug_misc( __FUNCTION__, "%s is already here\n",
                         others[i]->name );
      }
    }

    gaim_conversation_set_title( conv, "Multi-user conversation" );
  } else {
    gaim_debug_error( __FUNCTION__, "others is null\n" );
  }

  gaim_debug_misc( __FUNCTION__, "exit\n" );
}

void rvp_chat_leave( GaimConnection *gc, int id ) {
  RVPData *rd = gc->proto_data;
  GSList *bcs = gc->buddy_chats;
  GaimConversation *b = NULL;
  GaimAccount *ac;
  gchar *sessid;

  while( bcs ) {
    b = (GaimConversation *)bcs->data;
    if ( id == gaim_conv_chat_get_id( GAIM_CONV_CHAT( b ))) {
      break;
    }
    b = NULL;
    bcs = bcs->next;
  }

  if ( !b ) {
    return;
  }

  sessid = gaim_conversation_get_data( b, "sessid" );
  gaim_debug_misc( __FUNCTION__, "leaving conv %p\n", b );

  ac = gaim_connection_get_account( gc );
  rvp_send_notify( gc, rvp_normalize( ac, gaim_account_get_username( ac )), RVP_MSG_CHAT_LEAVE, NULL,
                   b );
  g_hash_table_remove( rd->chats, sessid );
}

static void rvp_chat_user_left( GaimConnection *gc, int id, GaimBuddy *buddy ) {
  GSList *bcs = gc->buddy_chats;
  GaimConversation *b = NULL;

  while( bcs ) {
    b = (GaimConversation *)bcs->data;
    if ( id == gaim_conv_chat_get_id( GAIM_CONV_CHAT( b ))) {
      break;
    }
    b = NULL;
    bcs = bcs->next;
  }

  if ( !b ) {
    return;
  }

  gaim_conv_chat_remove_user( GAIM_CONV_CHAT(b), buddy->name, NULL );
}

static int rvp_chat_send( GaimConnection *gc, int id, const char *message,
                          GaimMessageFlags flags ) {
  GSList *bcs = gc->buddy_chats;
  GaimConversation *b = NULL;
  GaimAccount *ac;

  while( bcs ) {
    b = (GaimConversation *)bcs->data;
    if ( id == gaim_conv_chat_get_id( GAIM_CONV_CHAT( b ))) {
      break;
    }
    b = NULL;
    bcs = bcs->next;
  }

  if ( !b ) {
    return 0;
  }

  gaim_debug_misc( __FUNCTION__, "sending message to conv %p\n", b );
  ac = gaim_connection_get_account( gc );
  return rvp_send_notify( gc, rvp_normalize( ac, gaim_account_get_username( ac )), RVP_MSG_CHAT,
                          (gchar *)message, b );
}

/* 1.x shim */
#if GAIM_MAJOR_VERSION < 2
static int rvp_chat_send_old( GaimConnection *gc, int id,
                              const char *message ) {
  return rvp_chat_send( gc, id, message, 0 );
}
#endif

/*
 * this is mandatory if we're to instigate chats.
 * furthermore, it must return at least one item (only one?) which is
 * a proto_chat_entry struct
 */
static GList *rvp_chat_info( GaimConnection *gc ) {
  struct proto_chat_entry *pce;
  GList *retval = NULL;

  pce = g_new0( struct proto_chat_entry, 1 );
  retval = g_list_append( retval, pce );
  pce->label = g_strdup( "RVP Chat" );
  pce->identifier = g_strdup( "sessid" ); /* this is looked up for the
                                             chat title */

  return retval;
}

static char *rvp_cb_real_name( GaimConnection *gc, int id, const char *who ) {
  GaimBuddy *buddy;

  /* we don't really care what the id is. */
  gaim_debug_misc( __FUNCTION__, "getting %s's name in chat %d\n",
                   who, id );
  buddy = rvp_find_buddy_by_name( gc, (gchar *)who );
  if ( buddy != NULL ) {
    if ( buddy->server_alias != NULL ) {
      return g_strdup( buddy->server_alias ); /* freed by caller */
    }
  }

  return NULL;
}

static void rvp_get_cb_info( GaimConnection *gc, int id, const char *who ) {
  rvp_get_info( gc, who );
}

/*
 * this is largely cribbed from Gaim's util.c
 */
static void destroy_fetch_url_data( GaimFetchUrlData *gfud ) {
  GaimConnection *gc = (GaimConnection *)gfud->user_data;
  RVPData *rd = gc->proto_data;

  gaim_debug_misc( __FUNCTION__, "Enter %p\n", gfud );

  if ( gfud->timeout ) {
    gaim_timeout_remove( gfud->timeout );
  }

  if ( gfud->inpa ) {
    gaim_input_remove( gfud->inpa );
  }

  if ( gfud->request.webdata != NULL)
    g_free( gfud->request.webdata );
  if ( gfud->request.header != NULL ) {
    gaim_debug_misc( __FUNCTION__, "freeing header %p\n",
                     gfud->request.header );
    g_free( gfud->request.header );
  }
  if ( gfud->response.webdata != NULL )
    g_free( gfud->response.webdata );
  if ( gfud->response.header != NULL )
    g_free( gfud->response.header );
  if ( gfud->url != NULL )
    g_free( gfud->url );
  if ( gfud->method != NULL )
    g_free( gfud->method );
  if ( gfud->website.address != NULL )
    g_free( gfud->website.address );
  if ( gfud->website.page != NULL )
    g_free( gfud->website.page );
  if ( gfud->website.user != NULL )
    g_free( gfud->website.user );
  if ( gfud->website.passwd  != NULL )
    g_free( gfud->website.passwd );

  if ( gfud->parsedheaders != NULL ) {
    g_hash_table_destroy( gfud->parsedheaders );
    gfud->parsedheaders = NULL;
  }

  gaim_debug_misc( __FUNCTION__, "removing %p from pending list\n", gfud );
  g_hash_table_remove( rd->pending, gfud );

  g_free( gfud );

  gaim_debug_misc( __FUNCTION__, "Exit\n" );
}

static gboolean parse_content_len( const char *data, size_t data_len,
                                   size_t *content_len ) {
  const char *p = NULL;
  *content_len = 0;

  /* This is still technically wrong, since headers are
   * case-insensitive [RFC 2616, section 4.2], though this ought to
   * catch the normal case.  Note: data is _not_ nul-terminated.
   */
  if ( data_len > 16 ) {
    p = strncasecmp( data, "Content-Length: ", 16 ) == 0 ? data : NULL;
    if ( !p ) {
      p = g_strstr_len( data, data_len, "\nContent-Length: " );
      if ( p )
        p += 1;
    }
  }

  if ( p != NULL && g_strstr_len( p, data_len - ( p - data ), "\n")) {
    *content_len = atoi( &p[strlen( "Content-Length: " )]);
    return TRUE;
  }

  return FALSE;
}

/*
 * callback for async URL connection.
 */
static void url_fetched_cb( gpointer url_data, gint sock,
                            GaimInputCondition cond ) {
  GaimFetchUrlData *gfud = url_data;
  char data;
  gboolean got_eof = FALSE;
  GaimConnection *gc = (GaimConnection *)gfud->user_data;
  RVPData *rd = gc->proto_data;
  gint timeout;
  int rc = 0;

  gfud->sock = sock;
  if (sock == -1) {
    gfud->callback(gfud->user_data, gfud, 0);
    if ( !gfud->preserve ) {
      destroy_fetch_url_data(gfud);
    } else {
      gaim_debug_misc( __FUNCTION__, "preserving gfud %p", gfud );
    }
    return;
  }

 resend:
  if (!gfud->sentreq) {
    gchar *cb;
    gchar *buf = NULL;
    gsize content_length = 0;

    gfud->resend = FALSE;

    /* we need to encode the outgoing buffer in the Microsofty way */
    if ( gfud->request.webdata && !gfud->encoded ) {
      GError *error = NULL;
      gsize read, i;
      gchar *bigbuf = NULL;
      gchar *converted = NULL;

      if ( gfud->request.length ) {
        read = gfud->request.length;
      } else {
        gaim_debug_misc( __FUNCTION__, "no length set, you fule\n" );
        read = strlen( gfud->request.webdata );
        gfud->request.length = read;
      }

      /* we need to convert from UCS-2LE. Unfortunately what we've got
         is more like UCS-1, which doesn't exist. So we fake it. */
      bigbuf = g_malloc0( read * 2 );
      for ( i = 0; i < read; i++ ) {
        bigbuf[ i * 2 ] = gfud->request.webdata[ i ];
      }

      converted = g_convert( bigbuf, gfud->request.length * 2,
                             "UTF-8", "UCS-2LE", &read,
                             &content_length, &error );
      if ( converted == NULL ) {
        gaim_notify_error( NULL, _("Error encoding message"),
                           "Something broke. Bug waider about this.",
                           NULL ); /* xxx */
      } else {
        g_free( gfud->request.webdata );

        /* content_length gets set to " the number of bytes stored in
           the output buffer (not including the terminating nul)." */
        /* so, there's a terminating null character, and
           content_length doesn't mention it. We need to make sure of
           the terminating null on account of the use of
           g_strdup_printf below; ideally I'd get rid of it. */
        gfud->request.webdata = g_realloc( converted, content_length + 1 );

        /* we also need to update this, because we may be looping back
           through this function post-authentication */
        gfud->request.length = content_length;
      }
    } else {
      content_length = gfud->request.length;
    }

    gfud->encoded = TRUE;

    /* we may not know our own callback address until we're connected */
    if ( rd->client_host == NULL ) {
      const char *h = NULL;

      /* preferred: use gaim's idea of the IP address */
#if GAIM_MAJOR_VERSION >= 2
      /* except for Gaim 2, which really wants to use the UPNP
         address, damn it */
      if ( gaim_account_get_bool( gaim_connection_get_account( gc ),
                                  "no_upnp_nat", TRUE )) {
        if ((( h = gaim_network_get_local_system_ip( sock )) != NULL ) &&
            strlen( h ) > 0 ) {
          rd->client_host = strdup( h );
        }
      }
#endif
      if ( h == NULL ) { /*  noop for gaim 1.x */
        if ((( h = gaim_network_get_my_ip( sock )) != NULL ) &&
            strlen( h ) > 0 ) {
          rd->client_host = strdup( h );
        }
      }

      /* maybe we've got a value hardcoded */
      if ((( h = gaim_account_get_string( gaim_connection_get_account( gc ),
                                          "myhost", NULL )) != NULL ) &&
          strlen( h ) > 0 ) {
        /*
          this is a last resort, really, so I guess I'll leave it in.
        gaim_notify_warning( gc, _("Deprecated option"),
                             "The My Host option on the RVP page is "
                             "deprecated! Please use the IP Address option "
                             "on the Network preferences panel.", NULL );
        */
        if ( rd->client_host != NULL ) {
          g_free( rd->client_host );
        }
        rd->client_host = strdup( h );
      }

      if ( rd->client_host == NULL ) {
        gaim_notify_error( NULL, _("Error"),
                           "Error identifying local hostname", NULL );
        /* this should be a macro or something, since I keep using it */
        if ( gfud->inpa ) {
          gaim_input_remove( gfud->inpa );
          gfud->inpa = 0;
        }
        gfud->callback( gfud->user_data, gfud, 0 );
        if ( gfud->resend ) {
          goto resend;
        }
        close( sock );
        if ( !gfud->preserve ) {
          destroy_fetch_url_data( gfud );
        }
      } else {
        /* client_host is not null, nothing to do. */
      }
    }

    /* patch in the Callback header if necessary */
    if (( gfud->request.header != NULL ) &&
        (( cb = strstr( gfud->request.header, "%C" )) != NULL )) {
      gchar *callback, *hdr;

      callback = g_strdup_printf( "Call-Back: http://%s:%d\r\n",
                                  rd->client_host, rd->listener_port );
      hdr = gfud->request.header;
      gfud->request.header = g_realloc( gfud->request.header,
                                        strlen( gfud->request.header ) - 2 +
                                        strlen( callback ) + 1 );
      if ( hdr != gfud->request.header ) {
        gaim_debug_misc( __FUNCTION__, "moved header %p to %p\n",
                         hdr, gfud->request.header );
      }
      cb = strstr( gfud->request.header, "%C" );
      memcpy( cb, callback, strlen( callback ));
      cb[strlen( callback )] = '\0';
      g_free( callback );
    }

    buf = g_strdup_printf( "%s %s%s HTTP/1.1\r\n"
                           "Content-Length: %u\r\n"
                           "RVP-Notifications-Version: 0.2\r\n"
                           "Host: %s\r\n%s\r\n%s" ,
                           gfud->method,
                           (gfud->full ? "" : "/"),
                           (gfud->full ? gfud->url : gfud->website.page),
                           content_length,
                           gfud->website.address,
                           gfud->request.header ? gfud->request.header : "",
                           gfud->request.webdata ?
                           gfud->request.webdata : "" );

    gaim_debug_misc( __FUNCTION__, "Requesting %p:\n%s\n", gfud, buf );

    /*
     * xxx this will block - we only set the socket nonblocking after
     * writing. this is pretty bad. And we don't handle errors (in
     * fact, previously we didn't even detect them) so this is REALLY
     * bad.
     */
    rc = write( sock, buf, strlen( buf ));
    if ( rc != strlen( buf )) {
      gaim_debug_misc( __FUNCTION__, "Only wrote %d of %d bytes.\n", rc,
                       strlen( buf ));
      /* we should at the very least blow up here */
    }
    fcntl( sock, F_SETFL, O_NONBLOCK );
    gfud->sentreq = TRUE;

    /* stack it up */
    timeout = gaim_prefs_get_int( "/plugins/prpl/rvp/timeout" );
    if ( gfud->timeout ) {
      gaim_timeout_remove( gfud->timeout );
    }
    if ( timeout ) {
      gfud->timeout = gaim_timeout_add( timeout * 1000, rvp_request_timeout,
                                        gfud );
    } else {
      gfud->timeout = 0;
    }
    gaim_debug_misc( __FUNCTION__, "inserting %p into pending list\n", gfud );
    g_hash_table_insert( rd->pending, gfud, gfud );

    gfud->inpa = gaim_input_add( sock, GAIM_INPUT_READ, url_fetched_cb,
                                 url_data );
    gfud->data_len = 4096;
    gfud->response.webdata = g_malloc( gfud->data_len );
    g_free( buf );

    return;
  }

  if ( read( sock, &data, 1 ) > 0 || errno == EWOULDBLOCK ) {
    if ( errno == EWOULDBLOCK ) {
      errno = 0;
      return;
    }

    gfud->len++;

    if (gfud->len == gfud->data_len + 1) {
      gfud->data_len += (gfud->data_len) / 2;

      gfud->response.webdata =
        g_realloc(gfud->response.webdata, gfud->data_len);
    }

    gfud->response.webdata[gfud->len - 1] = data;

    if (!gfud->startsaving) {
      if (data == '\r')
        return;

      if (data == '\n') {
        if (gfud->newline) {
          size_t content_len;
          gfud->startsaving = TRUE;

#ifdef LOUD
          gaim_debug_misc( __FUNCTION__, "Response headers:\n%*.*s\n",
                           gfud->len, gfud->len, gfud->response.webdata );
#else
          gaim_debug_misc( __FUNCTION__, "got headers\n" );
#endif

          /* See if we can find a content length. */
          gfud->has_explicit_data_len =
            parse_content_len( gfud->response.webdata, gfud->len,
                               &content_len );

          if ( !gfud->has_explicit_data_len ) {
            /* We'll stick with an initial 8192 */
            content_len = 8192;
          }

          /* Out with the old... */
          gfud->response.header = gfud->response.webdata;
          gfud->response.header =
            g_realloc( gfud->response.header, gfud->len + 1 );
          gfud->response.header[ gfud->len ] = 0;
          gfud->len = 0;
          gfud->response.webdata = NULL;

          /* In with the new. */
          gfud->data_len = content_len;
          if ( content_len ) {
            gfud->response.webdata = g_try_malloc( gfud->data_len );
          }
          if ( gfud->response.webdata == NULL ) {
            if ( content_len ) {
              gaim_debug_error( "gaim_url_fetch",
                                "Failed to allocate %u bytes: %s\n",
                                gfud->data_len, strerror( errno ));
            }
            gaim_input_remove( gfud->inpa );
            gfud->inpa = 0;
            gfud->callback( gfud->user_data, gfud, 0 );

            if ( gfud->resend ) {
              goto resend;
            }

            gaim_debug_misc( __FUNCTION__, "closing socket for %p\n", gfud );
            close(sock);
            if ( !gfud->preserve ) {
              destroy_fetch_url_data( gfud );
            } else {
              gaim_debug_misc( __FUNCTION__, "preserving gfud %p", gfud );
            }
          }
        } else {
          gfud->newline = TRUE;
        }
        return;
      }

      gfud->newline = FALSE;
    } else if ( gfud->has_explicit_data_len && gfud->len == gfud->data_len ) {
      got_eof = TRUE;
    }
  } else if (errno != ETIMEDOUT) {
    got_eof = TRUE;
  } else {
    gaim_input_remove( gfud->inpa );
    gfud->inpa = 0;
    gfud->callback(gfud->user_data, gfud, 0);

    if ( gfud->resend ) {
      goto resend;
    }

    gaim_debug_misc( __FUNCTION__, "closing socket for %p\n", gfud );
    close(sock);

    if ( !gfud->preserve ) {
      destroy_fetch_url_data( gfud );
    } else {
      gaim_debug_misc( __FUNCTION__, "preserving gfud %p", gfud );
    }

  }

  if ( got_eof ) {
    gfud->response.webdata = g_realloc( gfud->response.webdata,
                                        gfud->len + 1 );
    gfud->response.webdata[ gfud->len ] = 0;
    gaim_input_remove( gfud->inpa );
    gfud->inpa = 0;
    gfud->callback( gfud->user_data, gfud, gfud->len );
    if ( gfud->resend ) {
      goto resend;
    }
    gaim_debug_misc( __FUNCTION__, "closing socket for %p\n", gfud );
    close( sock );

    if ( !gfud->preserve ) {
      destroy_fetch_url_data( gfud );
    } else {
      gaim_debug_misc( __FUNCTION__, "preserving gfud %p", gfud );
    }
  }
}

/* cribbed from LWP::Authen::Digest */
/* CAUTION currently does not handle qop */
#ifdef md5_state_t /* stupid */
static gchar *get_auth_digest( GaimFetchUrlData *gfud, gchar *header,
                               gchar *user, gchar *pass ) {
  gchar **auth_param = NULL;
  gint i = 0;
  gchar *nonce = NULL, *realm = NULL;
  gchar *retval = NULL, hexresp[33];
  md5_state_t md5;
  md5_byte_t seekrit1[16], seekrit2[16], response[16];
  gchar *uri;

  /* slightly naive */
  gaim_debug_misc( __FUNCTION__, "parsing header %s\n", header );
  auth_param = g_strsplit( &header[strlen( "Digest ")], ", ", 0 );

  /* find the nonce */
  for ( i = 0; auth_param[i]; i++ ) {
    gaim_debug_misc( __FUNCTION__, "%d: %s\n", i, auth_param[i] );
    if ( g_str_has_prefix( auth_param[i], "nonce=\"" )) {
      nonce = g_strdup( &auth_param[i][7] );
      nonce[strlen( nonce ) - 1] = '\0';
    } else if ( g_str_has_prefix( auth_param[i], "realm=\"" )) {
      realm = g_strdup( &auth_param[i][7] );
      realm[strlen( realm ) - 1] = '\0';
    }
  }

  if ( nonce == NULL || realm == NULL ) {
    gaim_debug_error( __FUNCTION__,
                      "Can't find one of nonce or realm in header\n" );
    return NULL;
  }

  if ( gfud->website.page[0] != '/' ) {
    uri = g_strconcat( "/", gfud->website.page, NULL );
  } else {
    uri = g_strdup( gfud->website.page );
  }

  md5_init( &md5 );
  md5_append( &md5, (md5_byte_t *)user, strlen( user ));
  md5_append( &md5, (md5_byte_t *)":", 1 );
  md5_append( &md5, (md5_byte_t *)realm, strlen( realm ));
  md5_append( &md5, (md5_byte_t *)":", 1 );
  md5_append( &md5, (md5_byte_t *)pass, strlen( pass ));
  md5_finish( &md5, seekrit1 );

  /* digest = ( seekrit1 ) */
  /* digest = ( seekrit1, nonce ) */

  md5_init( &md5 );
  md5_append( &md5, (md5_byte_t *)gfud->method, strlen( gfud->method ));
  md5_append( &md5, (md5_byte_t *)":", 1 );
  md5_append( &md5, (md5_byte_t *)uri, strlen( uri));
  md5_finish( &md5, seekrit2 );

  /* digest = ( seekrit1, nonce, seekrit2 ) */

  md5_init( &md5 );
  for ( i = 0; i < 16; i++ ) {
    gchar *hex = g_strdup_printf( "%02x", seekrit1[i] );
    md5_append( &md5, (md5_byte_t *)hex, 2 );
    g_free( hex );
  }
  md5_append( &md5, (md5_byte_t *)":", 1 );
  md5_append( &md5, (md5_byte_t *)nonce, strlen( nonce ));
  md5_append( &md5, (md5_byte_t *)":", 1 );
  for ( i = 0; i < 16; i++ ) {
    gchar *hex = g_strdup_printf( "%02x", seekrit2[i] );
    md5_append( &md5, (md5_byte_t *)hex, 2 );
    g_free( hex );
  }
  md5_finish( &md5, response );

  for ( i = 0; i < 16; i++ ) {
    sprintf( &hexresp[i * 2], "%02x", response[i] );
  }
  hexresp[32] = '\0';

  retval = g_strdup_printf( "Digest "
                            "username=\"%s\", "
                            "realm=\"%s\", "
                            "algorithm=\"%s\", "
                            "uri=\"%s\", "
                            "nonce=\"%s\", "
                            "response=\"%s\"",
                            user,
                            realm,
                            "MD5",
                            uri,
                            nonce,
                            hexresp );

  if ( auth_param != NULL ) {
    g_strfreev( auth_param );
  }

  return retval;
}
#endif

/*
 * request timed out - kill the timer and bounce to rvp_async_data
 */
static gint rvp_request_timeout( gpointer data ) {
  GaimFetchUrlData *gfud = (GaimFetchUrlData *)data;
  GaimConnection *gc;

  gaim_debug_error( __FUNCTION__, "%p timed out\n", gfud );
  /* need to dig up a gc */
  gc = gfud->user_data; /* that was easy */

  gaim_timeout_remove( gfud->timeout );
  rvp_async_data( gc, gfud, 0 );

  return FALSE;
}

/*
 * handle data from async notifications
 */
static void rvp_async_data( void *udata, GaimFetchUrlData *gfd, size_t size ) {
  GaimConnection *gc = udata;
  GaimAccount *ac = gaim_connection_get_account( gc );
  RVPData *rd = gc->proto_data;
  size_t i;
  const char *p = NULL;
  gboolean is_xml = FALSE;
  gint code = 0;

  /* Oh KWATZ. */
  gaim_debug_misc( __FUNCTION__, "callback on gfud %p\n", gfd );

  /* to be sure this gets done right... */
  gfd->preserve = FALSE;

  /* Rare, but can happen */
  if ( rd == NULL ) {
    gaim_debug_misc( __FUNCTION__, "Woah, this isn't right!\n" );
    return;
  }

  if ( gfd->response.header == NULL ) {
    gchar *msg =
      g_strdup_printf( _("Server returned empty response to %s request"),
                       gfd->method ? gfd->method : _("unknown"));
    if ( gfd->method != NULL ) {
      if ( !GAIM_CONNECTION_IS_CONNECTED( gc )) {
        gc->wants_to_die = TRUE;
        gaim_connection_error( gc, msg );
      }
    } else {
      gaim_notify_error( NULL, _("Error"), msg, NULL );
      gaim_debug_error( __FUNCTION__, "Someone's probably hacking you...\n" );
    }

    g_free( msg );
    return;
  }

  if ( gfd->method != NULL ) {
    /* find out what the status code is */
    if (( !strncmp( gfd->response.header, "HTTP/", 5 )) &&
        strlen( gfd->response.header ) >= 12 &&
        ( code = atoi( &gfd->response.header[9] )) > 99 && code < 1000 ) {
      gaim_debug_misc( __FUNCTION__, "got a %d response to %p %s %s\n", code,
                       gfd, gfd->method, gfd->website.page );
    } else {
      gaim_connection_error( gc, _("Unexpected response from server" ));
      return;
    }
  } else {
    gaim_debug_misc( __FUNCTION__, "async notification\n" );
  }

  if ( gfd->response.header == NULL ) {
      gaim_connection_error( gc, _("No headers in response from server" ));
      return;
  }

  /* redirect */
  if ( code == 302 ) {
    gchar *newurl = get_header_content( gfd->response.header, "location",
                                        &gfd->parsedheaders );
    if ( newurl != NULL ) {
      gchar *oldhost = NULL;
      gaim_debug_warning( __FUNCTION__, "lightly tested redirection code\n" );
      g_free( gfd->response.header );
      gfd->response.header = NULL;
      g_free( gfd->response.webdata );
      gfd->response.webdata = NULL;
      gfd->len = 0;
      gfd->sentreq = FALSE;
      gfd->startsaving = FALSE;
      gfd->newline = FALSE;
      gfd->resend = TRUE;

      /* criminy! */
      if ( gfd->parsedheaders != NULL ) {
        g_hash_table_destroy( gfd->parsedheaders );
        gfd->parsedheaders = NULL;
      }

      /* In theory, we'd patch the buddy that caused the redirect so
         that future messages go to the correct URL. In practice, this
         appears to be a bad idea, so I've not done it. */
      if ( gfd->website.address ) {
        oldhost = g_strdup( gfd->website.address );
      }

      if ( gfd->url             != NULL) g_free( gfd->url );
      if ( gfd->website.address != NULL) g_free( gfd->website.address );
      if ( gfd->website.page    != NULL) g_free( gfd->website.page );
      if ( gfd->website.user    != NULL) g_free( gfd->website.user );
      if ( gfd->website.passwd  != NULL) g_free( gfd->website.passwd );

      gfd->url = g_strdup( newurl );
      gfd->tried_auth = FALSE; /* hmm */
      gaim_url_parse( gfd->url, &gfd->website.address, &gfd->website.port,
                      &gfd->website.page, &gfd->website.user,
                      &gfd->website.passwd );

      if ( oldhost != NULL ) {
        if ( strcmp( oldhost, gfd->website.address )) {
          /* we need to reconnect */
          int sock;

          gaim_debug_misc( __FUNCTION__, "Reconnection required (%s)\n",
                           gfd->website.address );

          if (( sock = gaim_proxy_connect( ac, gfd->website.address,
                                           gfd->website.port, url_fetched_cb,
                                           gfd )) < 0 ) {
            rvp_async_data( gc, gfd, 0 );
          } else {
            gfd->preserve = TRUE;
            gfd->resend = FALSE; /* non-intuitively, this is correct! */
          }
        }
        g_free( oldhost );
      }

      return;
    }
    /*
     * This is a 'can't happen', so we'll just flag the error and
     * abandon it. fixme if this is our main subscription or something
     * equally important, we need to bail out.
     */
    gaim_connection_error( gc, _( "Got a redirect with no location" ));
    return;
  }

  /* look for an auth failure */
  if ( code == 401 ) {
    gchar *newheaders = NULL;
    gchar *authtype = NULL;

    /* this happens every once in a while due to interactions I've not
       fully tracked down, and causes a core dump. Stopping that bus
       HERE. */
    if ( gaim_account_get_password( ac ) == NULL ) {
      gc->wants_to_die = TRUE; /* probably already the case */
      return;
    }

    if ( !GAIM_CONNECTION_IS_CONNECTED( gc )) {
      gaim_connection_update_progress( gc, _("Authenticating"), rd->login_step,
                                       MAX_LOGIN_STEPS );
    }

    /* headers[0] is the HTTP response line */
    authtype = get_header_content( gfd->response.header, "www-authenticate",
                                   &gfd->parsedheaders );
    if ( authtype == NULL ) {
      gaim_connection_error( gc, _("Auth error, but no auth type requested" ));
      return;
    }

    if ( !strcmp( authtype, "NTLM" ) ||
         ( strstr( authtype, "NTLM" ) && strstr( authtype, "Negotiate" ))) {
      gchar *ntlm = get_ntlm_msg1( rd->domain, rd->client_host );
      rd->auth_type = RVP_AUTH_NTLM;
      newheaders = g_strdup_printf( "Authorization: %s\r\n", ntlm );
      /* gfd->tried_auth = FALSE; HMMM. Fixme. we will get stuck in a
         loop if we're not careful. */
    } else if ( g_str_has_prefix( authtype, "NTLM " )) {
      gchar *ntlm;

      if ( gfd->tried_auth ) {
        gaim_debug_warning( __FUNCTION__, "tried authing already!\n" );
        if ( strcmp( gfd->method, "UNSUBSCRIBE" ) == 0 ) {
          /* actually should be fixed */
          gaim_debug_error( __FUNCTION__, "weird unsubscribe bug\n" );
        } else {
          /* prompt for a password? */
          gaim_connection_error( gc, _("Auth failed"));
          gaim_account_set_password( ac, NULL );
          gc->wants_to_die = TRUE;
        }
        return;
      }

      gfd->tried_auth = TRUE;

      ntlm = get_auth_ntlm( authtype, (char *)gaim_account_get_username( ac ),
                            (char *)gaim_account_get_password( ac ),
                            rd->client_host, rd->domain,
                            rd->authid );

      newheaders = g_strdup_printf( "Authorization: %s\r\n", ntlm );
#ifdef md5_state_t
    } else if ( g_str_has_prefix( authtype, "Digest" )) {
      gchar *digest = NULL;

      if ( gfd->tried_auth ) {
        /* prompt for a password! */
        gaim_connection_error( gc, _("Auth failed"));
        gaim_account_set_password( ac, NULL );
        gc->wants_to_die = TRUE;
        return;
      }
      gfd->tried_auth = TRUE;

      digest = get_auth_digest( gfd, authtype, gaim_account_get_username( ac ),
                                gaim_auth_get_password( ac ));

      newheaders = g_strdup_printf( "Authorization: %s\r\n", digest );
#endif
    } else {
      gaim_connection_error( gc, _("unrecognised auth type"));
      gaim_debug_error( __FUNCTION__, "unrecognised auth type %s\n",
                        authtype );
      gc->wants_to_die = TRUE;
      return;
    }

    /* a bit silly, esp since newheaders MUST NOT be NULL */
    if ( newheaders != NULL ) {
      gchar **oh = g_strsplit( gfd->request.header, "\r\n", 0 );
      gchar **nh = g_strsplit( newheaders, "\r\n", 0 );
      gchar *newh = NULL, *build = NULL;
      gchar *oldh = gfd->request.header;
      int j;

      if ( nh != NULL && oh != NULL ) {
        for ( j = 0; oh[j] != NULL ; j++ ) {
          gchar **bits = g_strsplit( oh[j], ":", 2 ); /* overkill */
          gchar *hdr = oh[j];

          if ( bits[0] == NULL ) {
            continue;
          }

          for ( i = 0; nh[i]; i++ ) {
            gchar **b2 = g_strsplit( nh[i], ":", 2 );
            if ( b2[0] == NULL ) {
              continue;
            }
            if ( !strcmp( b2[0], bits[0] )) {
              hdr = nh[i];
            }
            g_strfreev( b2 );
          }

          if ( hdr && strlen( hdr )) {
            if ( newh == NULL ) {
              newh = g_strdup_printf( "%s\r\n", hdr );
            } else {
              build = g_strdup_printf( "%s%s\r\n", newh, hdr );
              g_free( newh );
              newh = build;
            }
          }

          g_strfreev( bits );
        }

        for ( i = 0; nh[i]; i++ ) {
          gchar **b2 = g_strsplit( nh[i], ":", 2 );
          gchar *hdr = nh[i];

          for ( j = 0; oh[j]; j++ ) {
            gchar **bits = g_strsplit( oh[j], ":", 2 ); /* overkill */

            if ( bits[0] == NULL ) {
              continue;
            }

            if ( b2[0] == NULL ) {
              continue;
            }
            if ( !strcmp( b2[0], bits[0] )) {
              hdr = NULL;
            }
            if ( bits != NULL ) {
              g_strfreev( bits );
            }
          }

          if ( hdr && strlen( hdr )) {
            if ( newh == NULL ) {
              newh = g_strdup_printf( "%s\r\n", hdr );
            } else {
              build = g_strdup_printf( "%s%s\r\n", newh, hdr );
              g_free( newh );
              newh = build;
            }
          }

          g_strfreev( b2 );
        }

        g_free( gfd->request.header );
        gfd->request.header = g_strdup( newh );
        gaim_debug_misc( __FUNCTION__, "moved header %p to %p\n",
                         oldh, gfd->request.header );
        g_free( newh );

        g_strfreev( oh );
        g_strfreev( nh );
        g_free( newheaders );
      } else {
        gaim_debug_misc( __FUNCTION__, "split %s failed\n", newheaders );
      }

      gfd->data_len = 0;
      gfd->len = 0;
      gfd->sentreq = FALSE;
      gfd->startsaving = FALSE;
      gfd->newline = FALSE;

      if (( p = get_header_content( gfd->response.header, "connection",
                                    &gfd->parsedheaders ))
          != NULL && strstr( p, "close" )) {
        /* NTLM will be the death of me */
        int sock;

        gaim_debug_misc( __FUNCTION__,
                         "stupid NTLM closed connection. reopening\n" );

        if ( gfd->parsedheaders != NULL ) {
          g_hash_table_destroy( gfd->parsedheaders );
          gfd->parsedheaders = NULL;
        }

        if (( sock = gaim_proxy_connect( ac, gfd->website.address,
                                         gfd->website.port, url_fetched_cb,
                                         gfd )) < 0 ) {
          rvp_async_data( gc, gfd, 0 );
          destroy_fetch_url_data( gfd );
        } else {
          g_free( gfd->response.webdata );
          gfd->response.webdata = NULL;
          gfd->preserve = TRUE;
        }
      } else {
        if ( gfd->parsedheaders != NULL ) {
          g_hash_table_destroy( gfd->parsedheaders );
          gfd->parsedheaders = NULL;
        }
        gfd->resend = TRUE;
      }
      g_free( gfd->response.header );
      gfd->response.header = NULL;
      return;
    }
  }

  p = get_header_content( gfd->response.header, "content-type",
                          &gfd->parsedheaders );
  if ( p && !strcmp( p, "text/xml" )) {
    is_xml = TRUE;
  }

  /* decode the buffer - this has been determined by trial and error
     to be approximately correct, albeit stupid */
  if ( size > 0 ) {
    gchar *converted;
    GError *error = NULL;
    gsize read, written;

    converted = g_convert( gfd->response.webdata, size, "UCS-2LE" /* to */,
                           "UTF-8" /* from */, &read, &written, &error );
    if ( converted ) {
      gsize i;
      gfd->response.webdata = g_realloc( gfd->response.webdata, written / 2 );
      size = written / 2;
      for ( i = 0; i < written; i+= 2 ) {
        gfd->response.webdata[ i / 2 ] = converted[ i ];
      }
      g_free( converted );

      /* also update the gfud structure */
      gfd->len = written / 2;
    }
  }

  /*
   * if code is unset then we're handling an async message, which has
   * to be carrying an XML payload.
   */
  if ( code == 0 && is_xml ) {
    gint rc;
    /*
     * parse an async notification (e.g. IM, status change)
     */
    rc = parse_notify( gc, gfd );

    /* for now, send a 200 response regardless fixme */
    if ( gfd->sock != -1 ) {
      gchar *reply;

      reply = g_strdup_printf( "HTTP/1.1 %d %s\r\n"
                               "RVP-Notifications-Version: 0.2\r\n\r\n",
                               rc,
                               rc == 200 ? "Successful" : "Parse Error" );
      write( gfd->sock, reply, strlen( reply ));
      g_free( reply );
    }
  } else {
    /* if we're not connected, keep ticking the powerbar FIXME this is
       far too messy */
    if ( !GAIM_CONNECTION_IS_CONNECTED( gc ) &&
         code >= 200 &&
         code < 300
         && rd != NULL &&
         rd->login_step < MAX_LOGIN_STEPS ) {
      rd->login_step++;
    }

    switch ( code ) {
    case 200:
      if ( !strcmp( gfd->method, "SUBSCRIBE" )) {
        /* extract the useful info for this subscription */
        gchar *subs_id = get_header_content( gfd->response.header,
                                             "subscription-id",
                                             &gfd->parsedheaders );
        gchar *lifetime = get_header_content( gfd->response.header,
                                              "subscription-lifetime",
                                              &gfd->parsedheaders );

        rd->login_flags |= RVP_LOGIN_SUBSCRIBE;

        if ( subs_id != NULL ) {
          /* make sure it's the self-subscribe! */
          /* Fixme, don't do this dance. find out who owns the info
             and put the info into their buddy structure */
          gchar *user = rindex( gfd->website.page, '/' );
          gchar *u1, *u2;

          /* probably overkill */
          u1 = g_strdup( rvp_normalize( ac, &user[1] ));
          u2 = g_strdup( rvp_normalize( ac, gaim_account_get_username( ac ) ));

          if ( !strcmp( u1, u2 )) {
            gaim_debug_misc( __FUNCTION__, "saving subsid %s\n", subs_id );
            rd->subs_id = atoi( subs_id );
          } else {
            gaim_debug_misc( __FUNCTION__, "not saving subsid %s for %s\n",
                             subs_id, gfd->website.page );
          }

          g_free( u1 );
          g_free( u2 );
        }
        if ( lifetime != NULL ) {
          time_t timeout = atol( lifetime );
          if ( timeout == 0 ) {
            gaim_debug_error( __FUNCTION__, "can't parse timeout %s\n",
                              lifetime );
          } else {
            gaim_debug_misc( __FUNCTION__,
                             "main subscription expires in %u seconds\n",
                             timeout );
            timeout = timeout + time( NULL );
            rd->subs_expiry = timeout;
          }
        }

        if ( !GAIM_CONNECTION_IS_CONNECTED( gc )) {
          gchar *cmd[2];

          gaim_connection_update_progress( gc, _("Subscribed"), rd->login_step,
                                           MAX_LOGIN_STEPS );

          rvp_send_request( gc, "PROPPATCH", NULL );

          cmd[0] = g_strdup( "d:displayname" );
          cmd[1] = NULL;
          rvp_send_request( gc, "PROPFIND", cmd );
          g_free( cmd[0] );

          rvp_send_request( gc, "SUBSCRIPTIONS", NULL );
        } else {
          /* reset timeout on subscription */
          gaim_debug_misc( __FUNCTION__, "got subs response\n" );
        }
      } else if ( !strcmp( gfd->method, "SUBSCRIPTIONS" )) {
        rd->login_flags |= RVP_LOGIN_SUBSCRIPTIONS;
        if ( !GAIM_CONNECTION_IS_CONNECTED( gc )) {
          gaim_connection_update_progress( gc, _("Fetched subscriptions"),
                                           rd->login_step, MAX_LOGIN_STEPS );


          /* parse the subscriptions response */
          parse_subscriptions( gc, gfd );

          /* send an ACL self-check */
          rvp_send_request( gc, "ACL", NULL );
        } else {
          /* unlikely... */
          gaim_debug_misc( __FUNCTION__, "got subscriptions response\n" );
        }
      } else if ( !strcmp( gfd->method, "ACL" )) {
        if ( gfd->request.length == 0 ) {
          rd->login_flags |= RVP_LOGIN_ACL;
        }
        parse_acls( gc, gfd );
      } else if ( !strcmp( gfd->method, "UNSUBSCRIBE" )) {
        GaimBuddy *buddy =
          rvp_find_buddy_by_principal( gc, gfd->website.page );

        if ( buddy != NULL ) {
          RVPBuddy *rb = buddy->proto_data;

          if ( rb != NULL ) {
            rb->subs_id = 0;
          }

          if ( !strcmp( gfd->website.page, rd->principal )) {
            rd->subs_id = 0;
          }
        }
      }
      break;

    case 207:
      /* has to be XML by definition */
      if ( !strcmp( gfd->method, "PROPFIND" )) {
        rd->login_flags |= RVP_LOGIN_PROPFIND;
      } else if ( !strcmp( gfd->method, "PROPPATCH" )) {
        rd->login_flags |= RVP_LOGIN_PROPPATCH;
      }

      if ( !is_xml ) {
        gaim_connection_error( gc, _("Unparseable reply from server" ));
      } else {
        parse_multistatus( gc, gfd );
      }
      break;

    case 400:
      if ( !GAIM_CONNECTION_IS_CONNECTED( gc )) {
        gaim_connection_error( gc, _("Bad request"));
        gc->wants_to_die = TRUE;
      } else {
        /* hmm. should be able to recover from this. fixme. */
      }
      break;

    case 403:
      if ( !GAIM_CONNECTION_IS_CONNECTED( gc )) {
        gaim_connection_error( gc, _("Permission denied"));
        gc->wants_to_die = TRUE;
      } else {
        /* ? */
      }
      break;

    case 404:
      /* treat this as a 412 */
    case 412:
      if ( !strcmp( gfd->method, "NOTIFY" )) {
        GaimConversation *conv = NULL;
        gchar *recip;
        gchar *msg =
          g_strdup_printf( "%s\n<span color='red'>%s</span>",
                           "The following message could not be delivered to "
                           "all recipients:",
                           gfd->message == NULL ?
                           "[waider will eventually fix this]" :
                           gfd->message );

        /* a bit clunky */
        recip = rindex( gfd->website.page, '/' );
        if ( recip != NULL ) {
          recip = g_strdup( &recip[1] );
          conv = gaim_find_conv_with_account( GAIM_CONV_TYPE_ANY, recip, ac );

          if ( conv != NULL && !g_strstr_len( gfd->request.webdata,
                                              gfd->request.length,
                                              "\r\nTypingUser: " )) {
            gaim_conversation_write( conv, NULL, msg,
                                     GAIM_MESSAGE_NO_LOG|GAIM_MESSAGE_ERROR,
                                     time( NULL ));
          }
          g_free( recip );
        }

        g_free( msg );
      } else {
        gaim_debug_error( __FUNCTION__, "%d from %s /%s\n", code,
                          gfd->method, gfd->website.page );
      }
      break;

    case 500:
    case 501: /* this is from my own server */
      if ( !strcmp( gfd->method, "NOTIFY" )) {
        /* this happens. the message has probably been
           delivered. Microsoft = teh sux0r */
        /* the docs say:
           This code indicates the absence of a PRINCIPAL from a
           session. For example, when a PRINCIPAL has left a
           discussion with multiple PRINCIPALS and its INSTANT INBOX
           receives notification of the session, it uses this return
           code. The sender of the INSTANT MESSAGE is then able to
           indicate that the PRINCIPAL has left the conversation. */

      } else {
        gaim_connection_error( gc, _("Internal server error" ));
        if ( !GAIM_CONNECTION_IS_CONNECTED( gc )) {
          gc->wants_to_die = TRUE;
        }
      }
      break;

    default:
      gaim_debug_misc( __FUNCTION__, "don't know what to do with %d\n", code );
      break;
    }

    if ( !GAIM_CONNECTION_IS_CONNECTED( gc )) {
      if ( rd != NULL && rd->login_flags == RVP_LOGIN_ALL ) {
        GaimBlistNode *group, *cnode, *bnode;
        GaimBuddy *buddy;
        RVPBuddy *rb;
        GaimBuddyList *blist = gaim_get_blist();
        gchar *args[2];

        gaim_connection_set_state( gc, GAIM_CONNECTED );
#if GAIM_MAJOR_VERSION < 2
        serv_finish_login( gc );
#endif

        /* send a self-subscribe to update/propchange as well as
           pragma/notify - unless we're already subscribed! */
        buddy = rvp_find_buddy_by_principal( gc, rd->principal );
        if ( buddy == NULL ) {
          args[0] = ( gchar *)rd->me.buddy;
          args[1] = NULL;
          rvp_send_request( gc, "SUBSCRIBE", args );
        }

        /* recheck acls, since Gaim tries to check them before we're
           fully logged in */
        /* do the allprincipals flip */
        rvp_set_permit_deny( gc );

        /* now do the buddy list */
        for ( group = blist->root; group; group = group->next ) {
          for ( cnode = group->child; cnode; cnode = cnode->next ) {
            if ( !GAIM_BLIST_NODE_IS_CONTACT( cnode )) {
              continue;
            }
            for ( bnode = cnode->child; bnode; bnode = bnode->next ) {
              if ( !GAIM_BLIST_NODE_IS_BUDDY( bnode )) {
                continue;
              }
              buddy = (GaimBuddy *)bnode;
              if ( buddy->account != gaim_connection_get_account( gc )) {
                continue;
              }
              rb = buddy->proto_data;

#if GAIM_MAJOR_VERSION >= 2
              /* seems like gaim 2 doesn't automatically add buddies? */
              gaim_account_add_buddy( ac, buddy );
#endif

              if ( rb == NULL ) {
                continue; /* shouldn't be */
              }
              if ( rb->acl == 0 ) {
                /* we have no ACL for this buddy, so add the default */
                gaim_debug_misc( __FUNCTION__, "setting ACL for %s\n",
                                 buddy->name );
                rvp_set_acl( gc, rb->principal, RVP_ACL_CREDENTIALS |
                             RVP_ACL_SEND_TO | RVP_ACL_PRESENCE, 0 );
              } else {
                gaim_debug_misc( __FUNCTION__, "ACL for %s is %d\n",
                                 buddy->name, rb->acl );
              }
            }
          }
        }
      } else {
        gaim_debug_misc( __FUNCTION__, "not yet connected, flags == %x\n",
                         rd->login_flags );
      }
    }
  }
  /* gfud code will clean up for us */
}

static GaimFetchUrlData *rvp_send_request( GaimConnection *gc, gchar *cmd,
                                           gchar **args ) {
  RVPData *rd = gc->proto_data;
  GaimFetchUrlData *gfud = NULL;
  GaimAccount *ac = gaim_connection_get_account( gc );
  xmlChar *content = NULL;
  gint content_length = 0;
  gchar *headers = NULL;
  gchar *url = NULL;
  int sock;

  if ( gc->wants_to_die && strcmp( cmd, "UNSUBSCRIBE" ) &&
       strcmp( cmd, "PROPPATCH" )) {
    gaim_debug_misc( __FUNCTION__, "We're dying. Not sending your %s\n", cmd );
    return NULL;
  }

  /* client login/resubscribe */
  if ( !strcmp( cmd, "SUBSCRIBE" )) {
    int subtime = RVP_SUBS_TIME;

#ifdef DEBUG
    subtime = gaim_prefs_get_int( "/plugins/prpl/rvp/subtime" );
#endif

    if ( args == NULL ) {
      url = g_strdup( rd->principal );
      if ( rd->subs_id ) {
        gaim_debug_misc( __FUNCTION__, "using %d for self\n", rd->subs_id );
        headers = g_strdup_printf( "Subscription-Lifetime: %d\r\n"
                                   "Subscription-Id: %d\r\n"
                                   "RVP-From-Principal: %s\r\n",
                                   subtime,
                                   rd->subs_id,
                                   rd->principal );
      } else {
        if ( rd->client_host == NULL ) {
          headers = g_strdup_printf( "Notification-Type: pragma/notify\r\n"
                                     "RVP-From-Principal: %s\r\n"
                                     "Subscription-Lifetime: %d\r\n"
                                     "%%C",
                                     rd->principal,
                                     subtime );
        } else {
          /* Save ourselves a few cycles */
          headers = g_strdup_printf( "Notification-Type: pragma/notify\r\n"
                                     "RVP-From-Principal: %s\r\n"
                                     "Subscription-Lifetime: %d\r\n"
                                     "Call-Back: http://%s:%d\r\n",
                                     rd->principal,
                                     subtime,
                                     rd->client_host,
                                     rd->listener_port );
        }
      }
      /* no content */
    } else {
      GaimBuddy *buddy = (GaimBuddy *)args[0];
      RVPBuddy *rb = buddy->proto_data;

      url = g_strdup( rb->principal );
      if ( rb->subs_id ) {
        gaim_debug_misc( __FUNCTION__, "using %d for %s\n",
                         rb->subs_id, rb->principal );
        headers = g_strdup_printf( "Subscription-Id: %d\r\n"
                                   "Subscription-Lifetime: %d\r\n"
                                   "RVP-From-Principal: %s\r\n",
                                   rb->subs_id,
                                   subtime,
                                   rd->principal );
      } else {
        headers = g_strdup_printf( "Notification-Type: update/propchange\r\n"
                                   "RVP-From-Principal: %s\r\n"
                                   "Subscription-Lifetime: %d\r\n"
                                   "Call-Back: %s\r\n",
                                   rd->principal,
                                   subtime,
                                   rd->principal );
      }
    }
  } else if ( !strcmp( cmd, "PROPPATCH" )) {
    xmlDocPtr doc = xmlNewDoc( BAD_CAST "1.0" );
    xmlNodePtr root = xmlNewNode( NULL, BAD_CAST "d:propertyupdate" );
    xmlNodePtr node1, node2, prop;
    gchar *state;
    gchar *view_timeout;

    if ( args != NULL ) {
      state = g_strdup_printf( "r:%s", (gchar *)args[0] );
    } else {
      if ( rd->me.state != NULL ) {
        state = g_strdup_printf( "r:%s", rd->me.state );
      } else {
        state = g_strdup_printf( "r:online" );
      }
    }

    xmlNewNs( root, BAD_CAST "DAV:", BAD_CAST "d" );
    xmlNewNs( root, BAD_CAST "http://schemas.microsoft.com/rvp/",
              BAD_CAST "r" );
    xmlNewNs( root, BAD_CAST "http://schemas.microsoft.com/rvp/acl/",
              BAD_CAST "a" );
    xmlDocSetRootElement( doc, root );

    if ( !strcmp( state, "r:offline" )) {
      /* much abbreviated version, may apply to more than just
         offline, but certainly required for talking to a real RVP
         server. On cursory inspection of some network traces, it
         appears that the short version only applies to going
         offline. */
      prop = xmlNewNode( NULL, BAD_CAST "r:state" );
      node1 = xmlNewNode( NULL, BAD_CAST state );
      xmlAddChild( prop, node1 );
      if ( rd->view_id ) { /* kinda mandatory, I think */
        gchar *viewid = g_strdup_printf( "%d", rd->view_id );
        node1 = xmlNewNode( NULL, BAD_CAST "r:view-id" );
        node2 = xmlNewText( BAD_CAST viewid );
        xmlAddChild( node1, node2 );
        xmlAddChild( prop, node1 );
      }

      node1 = prop; /* to sync up with code below */
    } else {
      prop = xmlNewNode( NULL, BAD_CAST "r:leased-value" );

      node1 = xmlNewNode( NULL, BAD_CAST "r:value" );
      node2 = xmlNewNode( NULL, BAD_CAST state );
      xmlAddChild( node1, node2 );
      xmlAddChild( prop, node1 );
      g_free( state );

      node1 = xmlNewNode( NULL, BAD_CAST "r:default-value" );
      node2 = xmlNewNode( NULL, BAD_CAST "r:offline" );
      xmlAddChild( node1, node2 );
      xmlAddChild( prop, node1 );

      view_timeout = g_strdup_printf( "%d", RVP_VIEW_TIME );
      node1 = xmlNewNode( NULL, BAD_CAST "d:timeout" );
      node2 = xmlNewText( BAD_CAST view_timeout );
      xmlAddChild( node1, node2 );
      xmlAddChild( prop, node1 );
      g_free( view_timeout );

      node1 = xmlNewNode( NULL, BAD_CAST "r:state" );
      xmlAddChild( node1, prop );

      if ( rd->view_id ) {
        gchar *viewid = g_strdup_printf( "%d", rd->view_id );
        prop = xmlNewNode( NULL, BAD_CAST "r:view-id" );
        node2 = xmlNewText( BAD_CAST viewid );
        xmlAddChild( prop, node2 );
        xmlAddChild( node1, prop );
      }
    }

    node2 = xmlNewNode( NULL, BAD_CAST "d:prop" );
    xmlAddChild( node2, node1 );
    node1 = xmlNewNode( NULL, BAD_CAST "d:set" );
    xmlAddChild( node1, node2 );
    xmlAddChild( root, node1 );

    xmlDocDumpFormatMemory( doc, &content, &content_length, 0 );
    xmlFreeDoc( doc );

    headers = g_strdup_printf( "Content-Type: text/xml\r\n"
                               "RVP-From-Principal: %s\r\n",
                               rd->principal );

    url = g_strdup( rd->principal );
  } else if ( !strcmp( cmd, "PROPFIND" )) {
    gint propnum;
    xmlDocPtr doc = xmlNewDoc( BAD_CAST "1.0" );
    xmlNodePtr root = xmlNewNode( NULL, BAD_CAST "d:propfind" );

    xmlNewNs( root, BAD_CAST "DAV:", BAD_CAST "d" );
    xmlNewNs( root, BAD_CAST "http://schemas.microsoft.com/rvp/",
              BAD_CAST "r" );
    xmlNewNs( root, BAD_CAST "http://schemas.microsoft.com/rvp/acl/",
              BAD_CAST "a" );
    xmlDocSetRootElement( doc, root );

    /* practically speaking this is overkill */
    for ( propnum = 0; args[propnum]; propnum++ ) {
      xmlNodePtr prop = xmlNewNode( NULL, BAD_CAST "d:prop" );
      xmlNodePtr propname = xmlNewNode( NULL, BAD_CAST args[propnum] );
      xmlAddChild( prop, propname );
      xmlAddChild( root, prop );
    }

    xmlDocDumpFormatMemory( doc, &content, &content_length, 0 );
    xmlFreeDoc( doc );

    headers = g_strdup_printf( "Depth: 0\r\n" /* always 0 */
                               "Content-Type: text/xml\r\n"
                               "RVP-From-Principal: %s\r\n",
                               rd->principal );

    url = g_strdup( rd->principal );
  } else if ( !strcmp( cmd, "SUBSCRIPTIONS" )) {
    headers = g_strdup_printf( "Notification-Type: update/propchange\r\n"
                               "RVP-From-Principal: %s\r\n",
                               rd->principal );
    url = g_strdup( rd->principal );
  } else if ( !strcmp( cmd, "ACL" )) {
    headers = g_strdup_printf( "RVP-From-Principal: %s\r\n",
                               rd->principal );
    if ( args != NULL ) {
      /* I think this is wrong */
      /* url = g_strdup( args[0] ); */
      /* and this is right */
      url = g_strdup( rd->principal );
      content = (xmlChar *)args[1];
      content_length = (gint)args[2];
    } else {
      url = g_strdup( rd->principal );
    }
  } else if ( !strcmp( cmd, "NOTIFY" )) {
    headers = g_strdup_printf( "RVP-From-Principal: %s\r\n"
                               "Content-Type: text/xml\r\n"
                               "RVP-Hop-Count: 1\r\n"
                               "RVP-Ack-Type: DeepOr\r\n",
                               rd->principal );

    url = (gchar *)args[0];
    content = (xmlChar *)args[1];
    content_length = (gint)args[2];
  } else if ( !strcmp( cmd, "UNSUBSCRIBE" )) {
    if ( args == NULL ) {
      /* self-unsubscribe */
      url = g_strdup( rd->principal );
      headers = g_strdup_printf( "RVP-From-Principal: %s\r\n"
                                 "Subscription-Id: %d\r\n",
                                 rd->principal,
                                 rd->subs_id );
    } else {
      GaimBuddy *buddy = (GaimBuddy *)args[0];
      RVPBuddy *rbuddy = buddy->proto_data;

      if ( rbuddy == NULL ||
           rbuddy->subs_id == 0 ||
           rbuddy->principal == NULL ) {
        gaim_debug_error( __FUNCTION__,
                          "Buddy problem unsubbing (rbuddy %p, subs_id %d, principal %s)\n",
                          rbuddy, rbuddy ? rbuddy->subs_id : 0,
                          rbuddy ? ( rbuddy->principal ? rbuddy->principal :
                                     "null" ) : "null pointer"
                          );    /* xxx */
        rvp_dump_buddy( __FUNCTION__, buddy );
        return NULL;
      }

      url = g_strdup( rbuddy->principal );
      headers = g_strdup_printf( "RVP-From-Principal: %s\r\n"
                                 "Subscription-Id: %d\r\n",
                                 rd->principal,
                                 rbuddy->subs_id );
    }
  }

  if ( rd->auth_type == RVP_AUTH_NTLM ) {
    gchar *ntlm, *newheaders;
    gaim_debug_misc( __FUNCTION__, "Preemptively adding NTLM\n" );

    ntlm = get_ntlm_msg1( rd->domain, rd->client_host );
    newheaders = g_strdup_printf( "Authorization: %s\r\n%s", ntlm, headers );

    g_free( headers );
    headers = newheaders;
  }

  gaim_debug_misc( __FUNCTION__, "allocated header %p for %s %s\n", headers,
                   cmd, url );

  gfud = g_new0( GaimFetchUrlData, 1 );
  gfud->request.header = headers;
  gfud->request.webdata = (gchar *)content; /* fixme cast */
  gfud->request.length = content_length;
  gfud->method = g_strdup( cmd );
  gfud->callback = rvp_async_data;
  gfud->user_data = gc;
  gfud->url = url;
  gaim_url_parse( gfud->url, &gfud->website.address, &gfud->website.port,
                  &gfud->website.page, &gfud->website.user,
                  &gfud->website.passwd );


  if ( !strcmp( cmd, "NOTIFY" ) && args[3] != NULL ) {
    gfud->message = g_strdup( args[3] );
  }

  if (( sock =
        gaim_proxy_connect( ac, gfud->website.address,
                            gfud->website.port, url_fetched_cb, gfud )) < 0 ) {
    rvp_async_data( gc, gfud, 0 );
    destroy_fetch_url_data( gfud );
    gfud = NULL;
  }

  return gfud;
}

/*
 * attached to the listening port
 */
static void rvp_listener_callback( gpointer data, gint source,
                                   GaimInputCondition cond ) {
  GaimConnection *gc = data;
  GaimFetchUrlData *fetch = NULL;
  RVPData *rd = gc->proto_data;
  int sd;

  /* this might have happened because of a disconnection... */
  if ( rd == NULL ) {
    gaim_debug_misc( __FUNCTION__, "erk. rd is null\n" );
    return;
  }

  if (( sd = accept( rd->listener_fd, 0, 0 )) < 0 ) {
      perror( "Accept failed.\n" );
      /* fixme drop the listener and make a new one? */
      gc->wants_to_die = TRUE;
      return;
  }

  /* These are incoming connections that we only expect to read from */
  fetch = g_new0( GaimFetchUrlData, 1 );
  fetch->callback = rvp_async_data;
  fetch->user_data = gc;
  fetch->sentreq = TRUE;
  fetch->data_len = 4096;
  fetch->response.webdata = g_malloc( fetch->data_len );
  fetch->sock = sd;
  fetch->inpa = gaim_input_add( sd, GAIM_INPUT_READ, url_fetched_cb, fetch );
}

/*
 * Gaim 2.0 uses async code to listen, so we need a callback
 */
static void rvp_main_listener_callback( int listenfd, gpointer data ) {
  GaimConnection *gc = data;
  RVPData *rd = gc->proto_data;

  rd->listener_fd = listenfd;

  /* verify that this error works... it's being called from inside a
     callback, so it may not. */
  if ( rd->listener_fd == -1 ) {
    gaim_connection_error(gc, _("Unable to create listener"));
    gc->wants_to_die = TRUE;
    return;
  }

  rd->listener_port = gaim_network_get_port_from_fd( rd->listener_fd );

  gaim_debug_misc( __FUNCTION__, "listening on port %d, fd %d\n",
                   rd->listener_port, rd->listener_fd );

  /* connect the listener up to Gaim's event loop */
  gaim_debug_misc( __FUNCTION__, "listener hookup\n" );
  rd->linpa =
    gaim_input_add( rd->listener_fd, GAIM_INPUT_READ, rvp_listener_callback,
                    gc );

  /* Create a session ID */
  rd->session_id = rvp_get_sessid();

  /* tracking session ids */
  rd->chats = g_hash_table_new( g_str_hash, g_str_equal );
  rd->chatid = 1;

  /*
   * push a request out
   */
  rvp_send_request( gc, "SUBSCRIBE", NULL );
  gaim_debug_misc( __FUNCTION__, "exit\n" );
}

/*
 * This is invoked via Gaim's event loop from rvp_login
 */
static void rvp_login_connect( gpointer data, gint source,
                               GaimInputCondition cond ) {
  GaimConnection *gc = data;

  gaim_debug_misc( __FUNCTION__,  "enter\n" );

  if ( source == -1 ) {
    gaim_connection_error(gc, _("Unable to connect"));
    gc->wants_to_die = TRUE;
    return;
  }

  /*
   * Set up the callback socket
   */
  if ( !rvp_network_listen_range( 0, 0, SOCK_STREAM,
                                  rvp_main_listener_callback, gc )) {
    gaim_debug_misc( __FUNCTION__, "listener callback not ok\n" );
    /* xxx we need to break, here */
  }

  gaim_debug_misc( __FUNCTION__, "exit\n" );
}

/*
 * Initial entrypoint for establishing a connection
 *
 * invokes rvp_login_connect via Gaim's event loop.
 */
static void rvp_login( GaimAccount *account ) {
  GaimConnection *gc = gaim_account_get_connection( account );
  RVPData *rd;
  const char *host = NULL, *domain, *id, *authhost = NULL;
  const char *username;
  gchar **split;
  gchar *msg, *srvname;

  /* gc->proto_data *should* be null */
  if ( gc->proto_data != NULL ) {
    gaim_debug_misc( __FUNCTION__, "discarding non-null proto_data\n" );
    g_free( gc->proto_data );
  }
  gc->proto_data = g_new0( RVPData, 1 );
  rd = gc->proto_data;

  rd->login_step = 0;

  /* don't add any destroy functions to this */
  rd->pending = g_hash_table_new_full( g_direct_hash, g_direct_equal, NULL,
                                      NULL );

  /* The MSN code does this, but the problem is that it causes
     hyperlinks to be sent with <a href... wrapped around them, and
     brokets get entity-encoded. And the Official Client doesn't
     understand that sort of thing. */
  /*  gc->flags |= GAIM_CONNECTION_HTML | GAIM_CONNECTION_FORMATTING_WBFO |
    GAIM_CONNECTION_NO_BGCOLOR | GAIM_CONNECTION_NO_FONTSIZE |
    GAIM_CONNECTION_NO_URLDESC;*/

  username = gaim_account_get_username( account );
  authhost = gaim_account_get_string( account, "host", NULL );
  if ( authhost != NULL ) {
    if ( !strlen( authhost )) {
      authhost = NULL;
    }
  }
  split = g_strsplit( username, "@", 2 );

  if ( split == NULL ) {
    gc->wants_to_die = TRUE;
    gaim_connection_error( gc, _("Invalid username."));
    return;
  }

  if ( split[1] != NULL ) {
    host = g_strdup( split[1] );
  } else {
    host = g_strdup( authhost );
  }

  if (( host == NULL || ( strlen( host ) == 0 ))) {
    gc->wants_to_die = TRUE;
    gaim_connection_error( gc, _("No login host specified."));
    return;
  }

  /* a configured 'host' option should override everything */
  rd->port = 80; /* default */
  if ( authhost == NULL ) {
    rd->authdomain = g_strdup( host ); /* xxx leak */
    srvname = g_strconcat( "_rvp._tcp.", host, NULL );

    msg = g_strdup_printf( _("Locating RVP server for %s"), host );
    gaim_debug_misc( __FUNCTION__, "%s\n", msg );
    g_free( msg );

    rd->service = gethostbysrv( srvname, rd->service );
    g_free( srvname );
  } else {
    /* not hugely happy with this, but it makes a sort of sense */
    rd->authdomain = g_strdup( authhost ); /* xxx leak */
    rd->service = NULL;
  }

  if ( rd->service != NULL && rd->service->host != NULL ) {
    host = rd->service->host;
    rd->port = rd->service->port;
  } else {
    /* if we don't find a srv record, host is whatever we found above.
       For completeness, we should gethostbyname() it; if it's not
       valid, the final attempt is to do a NetBIOS lookup on it, after
       which we bail out. FIXME.
    */
    gaim_debug_misc( __FUNCTION__, "No srv record, falling back on %s\n",
                     host );
  }

  if ( rd->port != 80 ) {
    rd->principal = g_strdup_printf( "http://%s:%d/instmsg/aliases/%s",
                                     host, rd->port, split[0] );
  } else {
    rd->principal = g_strdup_printf( "http://%s/instmsg/aliases/%s",
                                     host, split[0] );
  }
  gaim_debug_misc( __FUNCTION__, "Allocated %p for rd->principal\n",
                   rd->principal );

  rd->me.buddy = g_new0( GaimBuddy, 1 );
  rd->me.buddy->proto_data = rd;
  rd->me.buddy->name = g_strconcat( split[0], "@", rd->authdomain, NULL );

  rd->me.principal = g_strdup( rd->principal );

  rd->authhost = g_strdup( host );

  g_strfreev( split );

  msg = g_strdup_printf( _("Connecting to %s:%d"), rd->authhost, rd->port );
  gaim_connection_update_progress( gc, msg, rd->login_step, MAX_LOGIN_STEPS );
  gaim_debug_misc( __FUNCTION__, "%s\n", msg );
  g_free( msg );

  domain = gaim_account_get_string( account, "domain", NULL );
  id = gaim_account_get_string( account, "id", NULL );

  /* do the preferred_port dance */
  if ( gaim_prefs_get_bool( "/core/network/ports_range_use" )) {
    rd->port_low = gaim_prefs_get_int( "/core/network/ports_range_start" );
    rd->port_high = gaim_prefs_get_int( "/core/network/ports_range_end" );
    /* make sure they're the right way around */
    if ( rd->port_high < rd->port_low ) {
      int tmp = rd->port_low;
      rd->port_low = rd->port_high;
      rd->port_high = tmp;
    }
  }

  rd->domain = g_strdup( domain );
  rd->authid = g_strdup( id );

  if ( gaim_proxy_connect( account, rd->authhost, rd->port,
                           rvp_login_connect, gc ) < 0 ) {
    gaim_connection_error(gc, _("Failed to connect to server."));
  }
}

/*
 * import a single buddy
 * based on code in the gadu-gadu plugin, and hacked until it works
 */
static void rvp_import_buddy( GaimConnection *gc, gchar *name ) {
  GaimBuddy *b;
  GaimGroup *g;

  /* Default group */
  gchar *group = g_strdup( "Microsoft Exchange Instant Messaging" );

  /* Add Buddy to our userlist */
  if ( !( g = gaim_find_group( group ))) {
    g = gaim_group_new( group );
    gaim_blist_add_group( g, NULL );
  }

  b = gaim_buddy_new( gc->account, name, NULL );
  gaim_blist_add_buddy( b, NULL, g, NULL );
  rvp_add_buddy( gc, b, g );
  g_free( group );
}

/*
 * Import an older buddy list
 */
static void rvp_import_buddies_ok_cb( void *user_data, const char *filename ) {
  FILE *buds;
  gchar line[BUF_LEN];
  GaimConnection *gc = user_data;

  if (( buds = fopen( filename, "rb" )) != NULL ) {
    /* check the first line of the file to see what type it is */
    fgets( line, BUF_LEN, buds );
    if ( g_str_has_prefix( line, "<?xml" )) {
      GError *error;
      gchar *contents = NULL;
      gsize length;
      xmlnode *gaim, *service, *blist;

      /* this could be a regular blist file, which, ironically, we
         can't read, or it could be a microsoft export, which we can't
         read. */
      fclose( buds );

      if ( !g_file_get_contents( filename, &contents, &length, &error )) {
        gaim_debug_error( __FUNCTION__, "Error reading %s\n", error->message );
        g_error_free(error);
        return;
      }

      gaim = xmlnode_from_str( contents, length );
      g_free( contents );

      if ( !gaim ) {
        gaim_debug_error( __FUNCTION__, "Error parsing %s\n", filename );
        return;
      }

      /* should verify that gaim's tagname is messenger and service's
         name is "Microsoft Exchange Instant Messaging" */
      if (( service = xmlnode_get_child( gaim, "service" )) != NULL ) {
        blist = xmlnode_get_child( service, "contactlist" );
        if ( blist ) {
          xmlnode *contact;

          for ( contact = xmlnode_get_child( blist, "contact" ); contact;
                contact = xmlnode_get_next_twin( contact )) {
            gchar *name = xmlnode_get_data( contact );

            if ( rvp_find_buddy_by_name( gc, name )) {
              gaim_debug_misc( __FUNCTION__, "already have %s\n", name );
            } else {
              rvp_import_buddy( gc, name );
            }
          }
        }
      } else {
        gaim_debug_misc( __FUNCTION__, "unknown file type\n" );
      }

      xmlnode_free( gaim );
    } else if ( line[0] == 'm' && strlen( filename ) > 6 &&
         g_str_has_suffix( filename, ".blist" )) {
      gaim_debug_misc( __FUNCTION__, "importing old-style gaim buddy list\n" );
      while( fgets( line, BUF_LEN, buds )) {
        if ( line[ strlen( line ) - 1] == '\n' ) {
          line[ strlen( line ) - 1 ] = '\0';
        }
        if ( line[ 0 ] == 'g' ) {
          /* we don't do groups */
        } else if ( line[ 0 ] == 'b' ) {
          if ( rvp_find_buddy_by_name( gc, &line[ 2 ] )) {
            gaim_debug_misc( __FUNCTION__, "already have %s\n", &line[2] );
          } else {
            rvp_import_buddy( gc, &line[ 2 ] );
          }
        } else {
          gaim_debug_error( __FUNCTION__, "don't know what to do with %s\n",
                            line );
        }
      }
    } else {
      /* some other file type */
    }
  } else {
    gaim_notify_error( NULL, _("Error opening file"),
                       strerror( errno ), NULL );
  }
}

static void rvp_import_buddies( GaimPluginAction *action ) {
  GaimConnection *gc = action->context;
  gaim_request_file( NULL, "Select contact list to import", NULL, FALSE,
                     G_CALLBACK( rvp_import_buddies_ok_cb ), NULL, gc );
}

static GList *rvp_actions( GaimPlugin *plugin, gpointer context ) {
  GList *m = NULL;
  GaimPluginAction *act;

  act = gaim_plugin_action_new( _("Import Buddies"), rvp_import_buddies );
  m = g_list_append( m, act );

  return m;
}

static gboolean rvp_load( GaimPlugin *plugin ) {
  gaim_debug_misc( __FUNCTION__, "rvp_load\n" );

  /* initialise the random-number code */
  init_seed( 0 );

  /* initialise away messages */
  awaymsgs[RVP_ONLINE].tag = g_strdup( "online" );
  awaymsgs[RVP_ONLINE].text = g_strdup( _( "Online" ));
  awaymsgs[RVP_BUSY].tag = g_strdup( "busy" );
  awaymsgs[RVP_BUSY].text = g_strdup( _( "Busy" ));
  awaymsgs[RVP_IDLE].tag = g_strdup( "idle" );
  awaymsgs[RVP_IDLE].text = g_strdup( _("Idle"));
  awaymsgs[RVP_BRB].tag = g_strdup( "back-soon" );
  awaymsgs[RVP_BRB].text = g_strdup( _("Be Right Back"));
  awaymsgs[RVP_AWAY].tag = g_strdup( "away" );
  awaymsgs[RVP_AWAY].text = g_strdup( _("Away From Computer"));
  awaymsgs[RVP_PHONE].tag = g_strdup( "on-phone" );
  awaymsgs[RVP_PHONE].text = g_strdup( _("On The Phone"));
  awaymsgs[RVP_LUNCH].tag = g_strdup( "at-lunch" );
  awaymsgs[RVP_LUNCH].text = g_strdup( _("Out To Lunch"));
  awaymsgs[RVP_OFFLINE].tag = g_strdup( "offline" );
  awaymsgs[RVP_OFFLINE].text = g_strdup( _("Offline")); /* yeah yeah */
  awaymsgs[RVP_HIDDEN].tag = g_strdup( "offline" );
  awaymsgs[RVP_HIDDEN].text = g_strdup( _("Appear Offline"));
  /* fixme free these in _unload */

  return TRUE;
}

static gboolean rvp_unload( GaimPlugin *plugin ) {
  gaim_debug_misc( __FUNCTION__,  "rvp_unload\n" );
  return TRUE;
}

static void rvp_destroy( GaimPlugin *plugin ) {
  gaim_debug_misc( __FUNCTION__, "rvp_destroy\n" );
}

#if GAIM_MAJOR_VERSION < 2
static GaimPluginPrefFrame *get_plugin_pref_frame( GaimPlugin *plugin ) {
  GaimPluginPrefFrame *frame;
  GaimPluginPref *ppref;

  frame = gaim_plugin_pref_frame_new();

  /* display the version */
#ifdef DEBUG
  ppref =
    gaim_plugin_pref_new_with_label( _( g_strdup_printf( "Version %s (built %s %s)", PACKAGE_VERSION, __DATE__, __TIME__ )));
#else
  ppref =
    gaim_plugin_pref_new_with_label( _( g_strdup_printf( "Version %s", PACKAGE_VERSION )));
#endif
  gaim_plugin_pref_frame_add( frame, ppref );

  ppref =
    gaim_plugin_pref_new_with_name_and_label( "/plugins/prpl/rvp/no_assertions",
                                              _("Disable \"assertion\" credentials"));
  gaim_plugin_pref_frame_add( frame, ppref );

  ppref =
    gaim_plugin_pref_new_with_name_and_label( "/plugins/prpl/rvp/fast_logout",
                                              _("Fast Logout"));
  gaim_plugin_pref_frame_add( frame, ppref );

  ppref =
    gaim_plugin_pref_new_with_name_and_label( "/plugins/prpl/rvp/timeout",
                                              _("Connection timeout in seconds"));
  gaim_plugin_pref_set_bounds( ppref, 10, 60 );
  gaim_plugin_pref_frame_add( frame, ppref );

#ifdef DEBUG
  ppref =
    gaim_plugin_pref_new_with_label( _("Debug Options"));
  gaim_plugin_pref_frame_add( frame, ppref );

  ppref =
    gaim_plugin_pref_new_with_name_and_label( "/plugins/prpl/rvp/subtime",
                                              _("Subscription Timeout"));
  gaim_plugin_pref_set_bounds( ppref, 300, 14400 );
  gaim_plugin_pref_frame_add( frame, ppref );
#endif

  return frame;
}

static GaimPluginUiInfo prefs_info = {
  get_plugin_pref_frame,
};
#endif

static GaimPluginProtocolInfo prpl_info = {
  0, /* no options */
  NULL,                   /* user_splits */
  NULL,                   /* protocol_options */
  { "png", 0, 0, 96, 96, GAIM_ICON_SCALE_SEND },    /* icon_spec */
  rvp_list_icon,          /* list_icon */
  rvp_list_emblems,       /* list_emblems */
  rvp_status_text,        /* status_text */
  NULL,                   /* tooltip_text */        /* not useful */
  rvp_away_states,        /* away_states */
  rvp_buddy_menu,         /* blist_node_menu */
  rvp_chat_info,          /* chat_info */
  NULL,                   /* chat_info_defaults */

  rvp_login,              /* login */
  rvp_close,              /* close */
  rvp_send_im,            /* send_im */
  NULL,                   /* set_info */           /* PROPPATCH - permitted? */
  rvp_send_typing,        /* send_typing */
  rvp_get_info,           /* get_info */           /* PROPFIND */
  rvp_set_away,           /* set_away */
  rvp_set_idle,           /* set_idle */
  NULL,                   /* change_passwd */      /* not permitted? */
  rvp_add_buddy,          /* add_buddy */
  NULL,                   /* add_buddies */        /* no UI */
  rvp_rem_buddy,          /* remove_buddy */
  NULL,                   /* remove_buddies */     /* no UI */
  rvp_add_permit,         /* add_permit */
  rvp_add_deny,           /* add_deny */
  rvp_rem_permit,         /* rem_permit */
  rvp_rem_deny,           /* rem_deny */
  rvp_set_permit_deny,    /* set_permit_deny */
#if GAIM_MAJOR_VERSION < 2
  NULL,                   /* warn */
#endif
  rvp_chat_join,          /* join_chat */
  NULL,                   /* reject chat invite */ /* not available? */
  NULL,                   /* get_chat_name */
  rvp_chat_invite,        /* chat_invite */
  rvp_chat_leave,         /* chat_leave */
  NULL,                   /* chat_whisper */
#if GAIM_MAJOR_VERSION < 2
  rvp_chat_send_old,      /* chat_send */
#else
  rvp_chat_send,          /* chat_send */
#endif
  rvp_keepalive,          /* keepalive */
  NULL,                   /* register_user */      /* not permitted */
  rvp_get_cb_info,        /* get_cb_info */
  NULL,                   /* get_cb_away */
  NULL,                   /* alias_buddy */        /* not permitted */
  NULL,                   /* group_buddy */
  NULL,                   /* rename_group */
  rvp_free_buddy,         /* buddy_free */
  rvp_conv_closed,        /* convo_closed */
  rvp_normalize,          /* normalize */
  NULL,                   /* set_buddy_icon */
  NULL,                   /* remove_group */
  rvp_cb_real_name,       /* get_cb_real_name */
  NULL,                   /* set_chat_topic */
  NULL,                   /* find_blist_chat */
  NULL,                   /* roomlist_get_list */
  NULL,                   /* roomlist_cancel */
  NULL,                   /* roomlist_expand_category */
  rvp_can_receive_file,   /* can_receive_file */
  rvp_send_file,          /* send_file */
#if GAIM_MAJOR_VERSION >= 2
  NULL,                   /* new_xfer */
  NULL,                   /* offline message */
  NULL,                   /* whiteboard_prpl_ops */
#endif
};

static GaimPluginInfo info = {
  GAIM_PLUGIN_MAGIC,
  GAIM_MAJOR_VERSION,
  GAIM_MINOR_VERSION,
  GAIM_PLUGIN_PROTOCOL,                             /**< type           */
  NULL,                                             /**< ui_requirement */
  0,                                                /**< flags          */
  NULL,                                             /**< dependencies   */
  GAIM_PRIORITY_DEFAULT,                            /**< priority       */

  PROTO_RVP,                                        /**< id             */
  "RVP",                                            /**< name           */
  VERSION,                                          /**< version        */
                                                    /**  summary        */
  N_("RVP Protocol Plugin"),
  N_("RVP Protocol Plugin"),                        /**  description    */
  "Weihua Sun <weihua@lucent.com>, Waider <waider@waider.ie>", /**< author */
  "http://www.waider.ie/hacks/workshop/c/librvp",   /**< homepage       */

  rvp_load,                                         /**< load           */
  rvp_unload,                                       /**< unload         */
  rvp_destroy,                                      /**< destroy        */

  NULL,                                             /**< ui_info        */
  &prpl_info,                                       /**< extra_info     */
#if GAIM_MAJOR_VERSION < 2
  &prefs_info,                                      /**< prefs_info     */
#else
  NULL,
#endif
  rvp_actions                                       /**< plugin actions */
  /* actually these show up on the account-actions menu */
};

static void init_plugin( GaimPlugin *plugin ) {
  GaimAccountOption *option;
  gchar *authid = NULL, *myhost = NULL;

  option = gaim_account_option_string_new(_("Auth Host"), "host", "" );
  prpl_info.protocol_options = g_list_append( prpl_info.protocol_options,
                                              option );

  option = gaim_account_option_string_new(_("Auth Domain"), "domain", "" );
  prpl_info.protocol_options = g_list_append( prpl_info.protocol_options,
                                              option );

  option = gaim_account_option_string_new(_("Auth ID"), "id", "" );
  prpl_info.protocol_options = g_list_append( prpl_info.protocol_options,
                                              option );

  option = gaim_account_option_string_new( _("My Hostname"), "myhost",
                                           myhost );
  prpl_info.protocol_options = g_list_append( prpl_info.protocol_options,
                                              option );

  /* seems to be the new way, since a prpl can't have a prefs_info */
#if GAIM_MAJOR_VERSION >= 2

  /* it makes sense to default this to true since RVP is generally
     only available on corporate networks anyway. */
  option =
    gaim_account_option_bool_new( _("Don't use UPNP NAT Address"),
                                  "no_upnp_nat", TRUE );
  prpl_info.protocol_options = g_list_append( prpl_info.protocol_options,
                                              option );

  option =
    gaim_account_option_bool_new( _("Disable \"assertion\" credentials"),
                                  "no_assertions", FALSE );
  prpl_info.protocol_options = g_list_append( prpl_info.protocol_options,
                                              option );
  option =
    gaim_account_option_bool_new( _("Fast Logout"), "fast_logout", TRUE );
  prpl_info.protocol_options = g_list_append( prpl_info.protocol_options,
                                              option );

  option = gaim_account_option_int_new( _("Connection timeout in seconds" ),
                                        "timeout", 30 );
  prpl_info.protocol_options = g_list_append( prpl_info.protocol_options,
                                              option );

#ifdef DEBUG
  option = gaim_account_option_int_new( _("Subscription Timeout" ),
                                        "timeout", RVP_SUBS_TIME );

  prpl_info.protocol_options = g_list_append( prpl_info.protocol_options,
                                              option );

#endif
#endif

  gaim_prefs_add_none( "/plugins/prpl/rvp");
  gaim_prefs_add_bool( "/plugins/prpl/rvp/no_assertions", FALSE );
  gaim_prefs_add_bool( "/plugins/prpl/rvp/fast_logout", TRUE );
  gaim_prefs_add_int( "/plugins/prpl/rvp/timeout", 30 );
#ifdef DEBUG
  gaim_prefs_add_int ( "/plugins/prpl/rvp/subtime", RVP_SUBS_TIME );
#endif
}

GAIM_INIT_PLUGIN(rvp, init_plugin, info);

Generated by  Doxygen 1.6.0   Back to index