Blob Blame History Raw

/*
  Meanwhile - Unofficial Lotus Sametime Community Client Library
  Copyright (C) 2004  Christopher (siege) O'Brien
  
  This library is free software; you can redistribute it and/or
  modify it under the terms of the GNU Library General Public
  License as published by the Free Software Foundation; either
  version 2 of the License, or (at your option) any later version.
  
  This library 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
  Library General Public License for more details.
  
  You should have received a copy of the GNU Library General Public
  License along with this library; if not, write to the Free
  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/

#include <glib.h>
#include <glib/glist.h>
#include <string.h>

#include "mw_channel.h"
#include "mw_debug.h"
#include "mw_error.h"
#include "mw_message.h"
#include "mw_service.h"
#include "mw_session.h"
#include "mw_srvc_im.h"
#include "mw_util.h"


#define PROTOCOL_TYPE  0x00001000
#define PROTOCOL_VER   0x00000003


/* data for the addtl blocks of channel creation */
#define mwImAddtlA_NORMAL  0x00000001

#define mwImAddtlB_NORMAL      0x00000001  /**< standard */
#define mwImAddtlB_PRECONF     0x00000019  /**< pre-conference chat */
#define mwImAddtlB_NOTESBUDDY  0x00033453  /**< notesbuddy */

#define mwImAddtlC_NORMAL  0x00000002


/* send-on-channel message type */
#define msg_MESSAGE  0x0064  /**< IM message */


#define BREAKUP  2048


/* which type of im? */
enum mwImType {
  mwIm_TEXT  = 0x00000001,  /**< text message */
  mwIm_DATA  = 0x00000002,  /**< status message (usually) */
};


/* which type of data im? */
enum mwImDataType {
  mwImData_TYPING   = 0x00000001,  /**< common use typing indicator */
  mwImData_SUBJECT  = 0x00000003,  /**< notesbuddy IM topic */
  mwImData_HTML     = 0x00000004,  /**< notesbuddy HTML message */
  mwImData_MIME     = 0x00000005,  /**< notesbuddy MIME message, w/image */
  mwImData_TIMESTAMP = 0x00000006, /**< notesbuddy timestamp */

  mwImData_INVITE   = 0x0000000a,  /**< Places invitation */

  mwImData_MULTI_START  = 0x00001388,
  mwImData_MULTI_STOP   = 0x00001389,
};


/** @todo might be appropriate to make a couple of hashtables to
    reference conversations by channel and target */
struct mwServiceIm {
  struct mwService service;

  enum mwImClientType features;

  struct mwImHandler *handler;
  GList *convs;  /**< list of struct im_convo */
};


struct mwConversation {
  struct mwServiceIm *service;  /**< owning service */
  struct mwChannel *channel;    /**< channel */
  struct mwIdBlock target;      /**< conversation target */

  gboolean ext_id;              /**< special treatment, external ID */

  /** state of the conversation, based loosely on the state of its
      underlying channel */
  enum mwConversationState state;

  enum mwImClientType features;

  GString *multi;               /**< buffer for multi-chunk message */
  enum mwImSendType multi_type; /**< type of incoming multi-chunk message */

  struct mw_datum client_data;
};


/** momentarily places a mwLoginInfo into a mwIdBlock */
static void login_into_id(struct mwIdBlock *to, struct mwLoginInfo *from) {
  to->user = from->user_id;
  to->community = from->community;
}


static struct mwConversation *convo_find_by_user(struct mwServiceIm *srvc,
						 struct mwIdBlock *to) {
  GList *l;

  for(l = srvc->convs; l; l = l->next) {
    struct mwConversation *c = l->data;
    if(mwIdBlock_equal(&c->target, to))
       return c;
  }

  return NULL;
}


static const char *conv_state_str(enum mwConversationState state) {
  switch(state) {
  case mwConversation_CLOSED:
    return "closed";

  case mwConversation_OPEN:
    return "open";

  case mwConversation_PENDING:
    return "pending";

  case mwConversation_UNKNOWN:
  default:
    return "UNKNOWN";
  }
}


static void convo_set_state(struct mwConversation *conv,
			    enum mwConversationState state) {

  g_return_if_fail(conv != NULL);

  if(conv->state != state) {
    g_info("setting conversation (%s, %s) state: %s",
	   NSTR(conv->target.user), NSTR(conv->target.community),
	   conv_state_str(state));
    conv->state = state;
  }
}


struct mwConversation *mwServiceIm_findConversation(struct mwServiceIm *srvc,
						    struct mwIdBlock *to) {
  g_return_val_if_fail(srvc != NULL, NULL);
  g_return_val_if_fail(to != NULL, NULL);

  return convo_find_by_user(srvc, to);
}


struct mwConversation *mwServiceIm_getConversation(struct mwServiceIm *srvc,
						   struct mwIdBlock *to) {
  struct mwConversation *c;

  g_return_val_if_fail(srvc != NULL, NULL);
  g_return_val_if_fail(to != NULL, NULL);

  c = convo_find_by_user(srvc, to);
  if(! c) {
    c = g_new0(struct mwConversation, 1);
    c->service = srvc;
    mwIdBlock_clone(&c->target, to);
    c->state = mwConversation_CLOSED;
    c->features = srvc->features;

    /* mark external users */
    /* c->ext_id = g_str_has_prefix(to->user, "@E "); */

    srvc->convs = g_list_prepend(srvc->convs, c);
  }

  return c;
}


static void convo_create_chan(struct mwConversation *c) {
  struct mwSession *s;
  struct mwChannelSet *cs;
  struct mwChannel *chan;
  struct mwLoginInfo *login;
  struct mwPutBuffer *b;

  /* we only should be calling this if there isn't a channel already
     associated with the conversation */
  g_return_if_fail(c != NULL);
  g_return_if_fail(mwConversation_isPending(c));
  g_return_if_fail(c->channel == NULL);

  s = mwService_getSession(MW_SERVICE(c->service));
  cs = mwSession_getChannels(s);

  chan = mwChannel_newOutgoing(cs);
  mwChannel_setService(chan, MW_SERVICE(c->service));
  mwChannel_setProtoType(chan, PROTOCOL_TYPE);
  mwChannel_setProtoVer(chan, PROTOCOL_VER);

  /* offer all known ciphers */
  mwChannel_populateSupportedCipherInstances(chan);

  /* set the target */
  login = mwChannel_getUser(chan);
  login->user_id = g_strdup(c->target.user);
  login->community = g_strdup(c->target.community);

  /* compose the addtl create, with optional FANCY HTML! */
  b = mwPutBuffer_new();
  guint32_put(b, mwImAddtlA_NORMAL);
  guint32_put(b, c->features);
  mwPutBuffer_finalize(mwChannel_getAddtlCreate(chan), b);

  c->channel = mwChannel_create(chan)? NULL: chan;
  if(c->channel) {
    mwChannel_setServiceData(c->channel, c, NULL);
  }
}


void mwConversation_open(struct mwConversation *conv) {
  g_return_if_fail(conv != NULL);
  g_return_if_fail(mwConversation_isClosed(conv));

  convo_set_state(conv, mwConversation_PENDING);
  convo_create_chan(conv);
}


static void convo_opened(struct mwConversation *conv) {
  struct mwImHandler *h = NULL;

  g_return_if_fail(conv != NULL);
  g_return_if_fail(conv->service != NULL);

  convo_set_state(conv, mwConversation_OPEN);
  h = conv->service->handler;

  g_return_if_fail(h != NULL);

  if(h->conversation_opened)
    h->conversation_opened(conv);
}


static void convo_free(struct mwConversation *conv) {
  struct mwServiceIm *srvc;

  mwConversation_removeClientData(conv);

  srvc = conv->service;
  srvc->convs = g_list_remove_all(srvc->convs, conv);

  mwIdBlock_clear(&conv->target);
  g_free(conv);
}


static int send_accept(struct mwConversation *c) {

  struct mwChannel *chan = c->channel;
  struct mwSession *s = mwChannel_getSession(chan);
  struct mwUserStatus *stat = mwSession_getUserStatus(s);

  struct mwPutBuffer *b;
  struct mwOpaque *o;

  b = mwPutBuffer_new();
  guint32_put(b, mwImAddtlA_NORMAL);
  guint32_put(b, c->features);
  guint32_put(b, mwImAddtlC_NORMAL);
  mwUserStatus_put(b, stat);

  o = mwChannel_getAddtlAccept(chan);
  mwOpaque_clear(o);
  mwPutBuffer_finalize(o, b);

  return mwChannel_accept(chan);
}


static void recv_channelCreate(struct mwService *srvc,
			       struct mwChannel *chan,
			       struct mwMsgChannelCreate *msg) {

  /* - ensure it's the right service,proto,and proto ver
     - check the opaque for the right opaque junk
     - if not, close channel
     - compose & send a channel accept
  */

  struct mwServiceIm *srvc_im = (struct mwServiceIm *) srvc;
  struct mwImHandler *handler;
  struct mwSession *s;
  struct mwUserStatus *stat;
  guint32 x, y, z;
  struct mwGetBuffer *b;
  struct mwConversation *c = NULL;
  struct mwIdBlock idb;

  handler = srvc_im->handler;
  s = mwChannel_getSession(chan);
  stat = mwSession_getUserStatus(s);

  /* ensure the appropriate service/proto/ver */
  x = mwChannel_getServiceId(chan);
  y = mwChannel_getProtoType(chan);
  z = mwChannel_getProtoVer(chan);

  if( (x != mwService_IM) || (y != PROTOCOL_TYPE) || (z != PROTOCOL_VER) ) {
    g_warning("unacceptable service, proto, ver:"
	      " 0x%08x, 0x%08x, 0x%08x", x, y, z);
    mwChannel_destroy(chan, ERR_SERVICE_NO_SUPPORT, NULL);
    return;
  }

  /* act upon the values in the addtl block */
  b = mwGetBuffer_wrap(&msg->addtl);
  guint32_get(b, &x);
  guint32_get(b, &y);
  z = (guint) mwGetBuffer_error(b);
  mwGetBuffer_free(b);

  if(z /* buffer error, BOOM! */ ) {
    g_warning("bad/malformed addtl in IM service");
    mwChannel_destroy(chan, ERR_FAILURE, NULL);
    return;

  } else if(x != mwImAddtlA_NORMAL) {
    g_message("unknown params: 0x%08x, 0x%08x", x, y);
    mwChannel_destroy(chan, ERR_IM_NOT_REGISTERED, NULL);
    return;
    
  } else if(y == mwImAddtlB_PRECONF) {
    if(! handler->place_invite) {
      g_info("rejecting place-invite channel");
      mwChannel_destroy(chan, ERR_IM_NOT_REGISTERED, NULL);
      return;

    } else {
      g_info("accepting place-invite channel");
    }

  } else if(y != mwImClient_PLAIN && y != srvc_im->features) {
    /** reject what we do not understand */
    mwChannel_destroy(chan, ERR_IM_NOT_REGISTERED, NULL);
    return;

  } else if(stat->status == mwStatus_BUSY) {
    g_info("rejecting IM channel due to DND status");
    mwChannel_destroy(chan, ERR_CLIENT_USER_DND, NULL);
    return;
  }

  login_into_id(&idb, mwChannel_getUser(chan));

#if 0
  c = convo_find_by_user(srvc_im, &idb);
#endif

  if(! c) {
    c = g_new0(struct mwConversation, 1);
    c->service = srvc_im;
    srvc_im->convs = g_list_prepend(srvc_im->convs, c);
  }

#if 0
  /* we're going to re-associate any existing conversations with this
     new channel. That means closing any existing ones */
  if(c->channel) {
    g_info("closing existing IM channel 0x%08x", mwChannel_getId(c->channel));
    mwConversation_close(c, ERR_SUCCESS);
  }
#endif

  /* set up the conversation with this channel, target, and be fancy
     if the other side requested it */
  c->channel = chan;
  mwIdBlock_clone(&c->target, &idb);
  c->features = y;
  convo_set_state(c, mwConversation_PENDING);
  mwChannel_setServiceData(c->channel, c, NULL);

  if(send_accept(c)) {
    g_warning("sending IM channel accept failed");
    mwConversation_free(c);

  } else {
    convo_opened(c);
  }
}


static void recv_channelAccept(struct mwService *srvc, struct mwChannel *chan,
			       struct mwMsgChannelAccept *msg) {

  struct mwConversation *conv;

  conv = mwChannel_getServiceData(chan);
  if(! conv) {
    g_warning("received channel accept for non-existant conversation");
    mwChannel_destroy(chan, ERR_FAILURE, NULL);
    return;
  }

  convo_opened(conv);
}


static void recv_channelDestroy(struct mwService *srvc, struct mwChannel *chan,
				struct mwMsgChannelDestroy *msg) {

  struct mwConversation *c;

  c = mwChannel_getServiceData(chan);
  g_return_if_fail(c != NULL);

  c->channel = NULL;

  if(mwChannel_isState(chan, mwChannel_ERROR)) {

    /* checking for failure on the receiving end to accept html
       messages. Fail-over to a non-html format on a new channel for
       the convo */
    if(c->features != mwImClient_PLAIN
       && (msg->reason == ERR_IM_NOT_REGISTERED ||
	   msg->reason == ERR_SERVICE_NO_SUPPORT)) {

      g_debug("falling back on a plaintext conversation");
      c->features = mwImClient_PLAIN;
      convo_create_chan(c);
      return;
    }
  }

  mwConversation_close(c, msg->reason);
}


static void convo_recv(struct mwConversation *conv, enum mwImSendType type,
		       gconstpointer msg) {

  struct mwServiceIm *srvc;
  struct mwImHandler *handler;

  g_return_if_fail(conv != NULL);

  srvc = conv->service;
  g_return_if_fail(srvc != NULL);

  handler = srvc->handler;
  if(handler && handler->conversation_recv)
    handler->conversation_recv(conv, type, msg);
}


static void convo_multi_start(struct mwConversation *conv) {
  g_return_if_fail(conv->multi == NULL);
  conv->multi = g_string_new(NULL);
}


static void convo_multi_stop(struct mwConversation *conv) {

  g_return_if_fail(conv->multi != NULL);

  /* actually send it */
  convo_recv(conv, conv->multi_type, conv->multi->str);

  /* clear up the multi buffer */
  g_string_free(conv->multi, TRUE);
  conv->multi = NULL;
}


static void recv_text(struct mwServiceIm *srvc, struct mwChannel *chan,
		      struct mwGetBuffer *b) {

  struct mwConversation *c;
  char *text = NULL;

  mwString_get(b, &text);

  if(! text) return;

  c = mwChannel_getServiceData(chan);
  if(c) {
    if(c->multi) {
      g_string_append(c->multi, text);

    } else {
      convo_recv(c, mwImSend_PLAIN, text); 
    }
  }

  g_free(text);
}


static void convo_invite(struct mwConversation *conv,
			 struct mwOpaque *o) {

  struct mwServiceIm *srvc;
  struct mwImHandler *handler;

  struct mwGetBuffer *b;
  char *title, *name, *msg;
  char *unkn, *host;
  guint16 with_who;

  g_info("convo_invite");

  srvc = conv->service;
  handler = srvc->handler;

  g_return_if_fail(handler != NULL);
  g_return_if_fail(handler->place_invite != NULL);

  b = mwGetBuffer_wrap(o);
  mwGetBuffer_advance(b, 4);
  mwString_get(b, &title);
  mwString_get(b, &msg);
  mwGetBuffer_advance(b, 19);
  mwString_get(b, &name);

  /* todo: add a mwString_skip */
  mwString_get(b, &unkn);
  mwString_get(b, &host);
  g_free(unkn);
  g_free(host);

  /* hack. Sometimes incoming convo invitation channels won't have the
     owner id block filled in */
  guint16_get(b, &with_who);
  if(with_who && !conv->target.user) {
    char *login;
    mwString_get(b, &conv->target.user);
    mwString_get(b, &login); g_free(login);
    mwString_get(b, &conv->target.community);
  }  

  if(mwGetBuffer_error(b)) {
    mw_mailme_opaque(o, "problem with place invite over IM service");
  } else {
    handler->place_invite(conv, msg, title, name);
  }

  mwGetBuffer_free(b);
  g_free(msg);
  g_free(title);
  g_free(name);
}


static void recv_data(struct mwServiceIm *srvc, struct mwChannel *chan,
		      struct mwGetBuffer *b) {

  struct mwConversation *conv;
  guint32 type, subtype;
  struct mwOpaque o = { 0, 0 };
  char *x;

  guint32_get(b, &type);
  guint32_get(b, &subtype);
  mwOpaque_get(b, &o);

  if(mwGetBuffer_error(b)) {
    mwOpaque_clear(&o);
    return;
  }

  conv = mwChannel_getServiceData(chan);
  if(! conv) return;

  switch(type) {
  case mwImData_TYPING:
    convo_recv(conv, mwImSend_TYPING, GINT_TO_POINTER(! subtype));
    break;

  case mwImData_HTML:
    if(o.len) {
      if(conv->multi) {
	g_string_append_len(conv->multi, (char *) o.data, o.len);
	conv->multi_type = mwImSend_HTML;

      } else {
	x = g_strndup((char *) o.data, o.len);
	convo_recv(conv, mwImSend_HTML, x);
	g_free(x);
      }
    }
    break;

  case mwImData_SUBJECT:
    x = g_strndup((char *) o.data, o.len);
    convo_recv(conv, mwImSend_SUBJECT, x);
    g_free(x);
    break;

  case mwImData_MIME:
    if(conv->multi) {
      g_string_append_len(conv->multi, (char *) o.data, o.len);
      conv->multi_type = mwImSend_MIME;

    } else {
      x = g_strndup((char *) o.data, o.len);
      convo_recv(conv, mwImSend_MIME, x);
      g_free(x);
    }
    break;

  case mwImData_TIMESTAMP:
    /* todo */
    break;

  case mwImData_INVITE:
    convo_invite(conv, &o);
    break;

  case mwImData_MULTI_START:
    convo_multi_start(conv);
    break;

  case mwImData_MULTI_STOP:
    convo_multi_stop(conv);
    break;

  default:
    
    mw_mailme_opaque(&o, "unknown data message type in IM service:"
		     " (0x%08x, 0x%08x)", type, subtype);
  }

  mwOpaque_clear(&o);
}


static void recv(struct mwService *srvc, struct mwChannel *chan,
		 guint16 type, struct mwOpaque *data) {

  /* - ensure message type is something we want
     - parse message type into either mwIMText or mwIMData
     - handle
  */

  struct mwGetBuffer *b;
  guint32 mt;

  g_return_if_fail(type == msg_MESSAGE);

  b = mwGetBuffer_wrap(data);
  guint32_get(b, &mt);

  if(mwGetBuffer_error(b)) {
    g_warning("failed to parse message for IM service");
    mwGetBuffer_free(b);
    return;
  }

  switch(mt) {
  case mwIm_TEXT:
    recv_text((struct mwServiceIm *) srvc, chan, b);
    break;

  case mwIm_DATA:
    recv_data((struct mwServiceIm *) srvc, chan, b);
    break;

  default:
    g_warning("unknown message type 0x%08x for IM service", mt);
  }

  if(mwGetBuffer_error(b))
    g_warning("failed to parse message type 0x%08x for IM service", mt);

  mwGetBuffer_free(b);
}


static void clear(struct mwServiceIm *srvc) {
  struct mwImHandler *h;

  while(srvc->convs)
    convo_free(srvc->convs->data);

  h = srvc->handler;
  if(h && h->clear)
    h->clear(srvc);
  srvc->handler = NULL;
}


static const char *name(struct mwService *srvc) {
  return "Instant Messaging";
}


static const char *desc(struct mwService *srvc) {
  return "IM service with Standard and NotesBuddy features";
}


static void start(struct mwService *srvc) {
  mwService_started(srvc);
}


static void stop(struct mwServiceIm *srvc) {

  while(srvc->convs)
    mwConversation_free(srvc->convs->data);

  mwService_stopped(MW_SERVICE(srvc));
}


struct mwServiceIm *mwServiceIm_new(struct mwSession *session,
				    struct mwImHandler *hndl) {

  struct mwServiceIm *srvc_im;
  struct mwService *srvc;

  g_return_val_if_fail(session != NULL, NULL);
  g_return_val_if_fail(hndl != NULL, NULL);

  srvc_im = g_new0(struct mwServiceIm, 1);
  srvc = MW_SERVICE(srvc_im);

  mwService_init(srvc, session, mwService_IM);
  srvc->recv_create = recv_channelCreate;
  srvc->recv_accept = recv_channelAccept;
  srvc->recv_destroy = recv_channelDestroy;
  srvc->recv = recv;
  srvc->clear = (mwService_funcClear) clear;
  srvc->get_name = name;
  srvc->get_desc = desc;
  srvc->start = start;
  srvc->stop = (mwService_funcStop) stop;

  srvc_im->features = mwImClient_PLAIN;
  srvc_im->handler = hndl;

  return srvc_im;
}


struct mwImHandler *mwServiceIm_getHandler(struct mwServiceIm *srvc) {
  g_return_val_if_fail(srvc != NULL, NULL);
  return srvc->handler;
}


gboolean mwServiceIm_supports(struct mwServiceIm *srvc,
			      enum mwImSendType type) {

  g_return_val_if_fail(srvc != NULL, FALSE);

  switch(type) {
  case mwImSend_PLAIN:
  case mwImSend_TYPING:
    return TRUE;

  case mwImSend_SUBJECT:
  case mwImSend_HTML:
  case mwImSend_MIME:
  case mwImSend_TIMESTAMP:
    return srvc->features == mwImClient_NOTESBUDDY;

  default:
    return FALSE;
  }
}


void mwServiceIm_setClientType(struct mwServiceIm *srvc,
			       enum mwImClientType type) {

  g_return_if_fail(srvc != NULL);
  srvc->features = type;
}


enum mwImClientType mwServiceIm_getClientType(struct mwServiceIm *srvc) {
  g_return_val_if_fail(srvc != NULL, mwImClient_UNKNOWN);
  return srvc->features;
}


static int convo_send_data(struct mwConversation *conv,
			   guint32 type, guint32 subtype,
			   struct mwOpaque *data) {
  struct mwPutBuffer *b;
  struct mwOpaque o;
  struct mwChannel *chan;
  int ret;

  chan = conv->channel;
  g_return_val_if_fail(chan != NULL, -1);

  b = mwPutBuffer_new();

  guint32_put(b, mwIm_DATA);
  guint32_put(b, type);
  guint32_put(b, subtype);
  mwOpaque_put(b, data);

  mwPutBuffer_finalize(&o, b);

  ret = mwChannel_sendEncrypted(chan, msg_MESSAGE, &o, !conv->ext_id);
  mwOpaque_clear(&o);

  return ret;
}


static int convo_send_multi_start(struct mwConversation *conv) {
  return convo_send_data(conv, mwImData_MULTI_START, 0x00, NULL);
}


static int convo_send_multi_stop(struct mwConversation *conv) {
  return convo_send_data(conv, mwImData_MULTI_STOP, 0x00, NULL);
}


/* breaks up a large message into segments, sends a start_segment
   message, then sends each segment in turn, then sends a stop_segment
   message */
static int
convo_sendSegmented(struct mwConversation *conv, const char *message,
		    int (*send)(struct mwConversation *conv,
				const char *msg)) {
  char *buf = (char *) message;
  gsize len;
  int ret = 0;

  len = strlen(buf);
  ret = convo_send_multi_start(conv);

  while(len && !ret) {
    char tail;
    gsize seg;

    seg = BREAKUP;
    if(len < BREAKUP)
      seg = len;

    /* temporarily NUL-terminate this segment */
    tail = buf[seg];
    buf[seg] = 0x00;

    ret = send(conv, buf);

    /* restore this segment */
    buf[seg] = tail;
    
    buf += seg;
    len -= seg;
  }

  if(! ret)
    ret = convo_send_multi_stop(conv);

  return ret;
}


static int convo_sendText(struct mwConversation *conv, const char *text) {
  struct mwPutBuffer *b;
  struct mwOpaque o;
  int ret;
  
  b = mwPutBuffer_new();

  guint32_put(b, mwIm_TEXT);
  mwString_put(b, text);

  mwPutBuffer_finalize(&o, b);
  ret = mwChannel_sendEncrypted(conv->channel, msg_MESSAGE, &o, !conv->ext_id);
  mwOpaque_clear(&o);

  return ret;
}


static int convo_sendTyping(struct mwConversation *conv, gboolean typing) {
  return convo_send_data(conv, mwImData_TYPING, !typing, NULL);
}


static int convo_sendSubject(struct mwConversation *conv,
			     const char *subject) {
  struct mwOpaque o;

  o.len = strlen(subject);
  o.data = (guchar *) subject;

  return convo_send_data(conv, mwImData_SUBJECT, 0x00, &o);
}


static int convo_sendHtml(struct mwConversation *conv, const char *html) {
  struct mwOpaque o;

  o.len = strlen(html);
  o.data = (guchar *) html;

  if(o.len > BREAKUP) {
    return convo_sendSegmented(conv, html, convo_sendHtml);
  } else {
    return convo_send_data(conv, mwImData_HTML, 0x00, &o);
  }
}


static int convo_sendMime(struct mwConversation *conv, const char *mime) {
  struct mwOpaque o;

  o.len = strlen(mime);
  o.data = (guchar *) mime;

  if(o.len > BREAKUP) {
    return convo_sendSegmented(conv, mime, convo_sendMime);
  } else {
    return convo_send_data(conv, mwImData_MIME, 0x00, &o);
  }
}


int mwConversation_send(struct mwConversation *conv, enum mwImSendType type,
			gconstpointer msg) {

  g_return_val_if_fail(conv != NULL, -1);
  g_return_val_if_fail(mwConversation_isOpen(conv), -1);
  g_return_val_if_fail(conv->channel != NULL, -1);

  switch(type) {
  case mwImSend_PLAIN:
    return convo_sendText(conv, msg);
  case mwImSend_TYPING:
    return convo_sendTyping(conv, GPOINTER_TO_INT(msg));
  case mwImSend_SUBJECT:
    return convo_sendSubject(conv, msg);
  case mwImSend_HTML:
    return convo_sendHtml(conv, msg);
  case mwImSend_MIME:
    return convo_sendMime(conv, msg);

  default:
    g_warning("unsupported IM Send Type, 0x%x", type);
    return -1;
  }
}


enum mwConversationState mwConversation_getState(struct mwConversation *conv) {
  g_return_val_if_fail(conv != NULL, mwConversation_UNKNOWN);
  return conv->state;
}


struct mwServiceIm *mwConversation_getService(struct mwConversation *conv) {
  g_return_val_if_fail(conv != NULL, NULL);
  return conv->service;
}


gboolean mwConversation_supports(struct mwConversation *conv,
				 enum mwImSendType type) {
  g_return_val_if_fail(conv != NULL, FALSE);

  switch(type) {
  case mwImSend_PLAIN:
  case mwImSend_TYPING:
    return TRUE;

  case mwImSend_SUBJECT:
  case mwImSend_HTML:
  case mwImSend_MIME:
    return conv->features == mwImClient_NOTESBUDDY;

  default:
    return FALSE;
  }
}


enum mwImClientType
mwConversation_getClientType(struct mwConversation *conv) {
  g_return_val_if_fail(conv != NULL, mwImClient_UNKNOWN);
  return conv->features;
}


struct mwIdBlock *mwConversation_getTarget(struct mwConversation *conv) {
  g_return_val_if_fail(conv != NULL, NULL);
  return &conv->target;
}


struct mwLoginInfo *mwConversation_getTargetInfo(struct mwConversation *conv) {
  g_return_val_if_fail(conv != NULL, NULL);
  g_return_val_if_fail(conv->channel != NULL, NULL);
  return mwChannel_getUser(conv->channel);
}


void mwConversation_setClientData(struct mwConversation *conv,
				  gpointer data, GDestroyNotify clean) {
  g_return_if_fail(conv != NULL);
  mw_datum_set(&conv->client_data, data, clean);
}


gpointer mwConversation_getClientData(struct mwConversation *conv) {
  g_return_val_if_fail(conv != NULL, NULL);
  return mw_datum_get(&conv->client_data);
}


void mwConversation_removeClientData(struct mwConversation *conv) {
  g_return_if_fail(conv != NULL);
  mw_datum_clear(&conv->client_data);
}


void mwConversation_close(struct mwConversation *conv, guint32 reason) {
  struct mwServiceIm *srvc;
  struct mwImHandler *h;

  g_return_if_fail(conv != NULL);

  convo_set_state(conv, mwConversation_CLOSED);

  srvc = conv->service;
  g_return_if_fail(srvc != NULL);

  h = srvc->handler;
  if(h && h->conversation_closed)
    h->conversation_closed(conv, reason);

  if(conv->channel) {
    mwChannel_destroy(conv->channel, reason, NULL);
    conv->channel = NULL;
  }
}


void mwConversation_free(struct mwConversation *conv) {
  g_return_if_fail(conv != NULL);

  if(! mwConversation_isClosed(conv))
    mwConversation_close(conv, ERR_SUCCESS);

  convo_free(conv);
}