/*
** pork_aim.c - Interface to libfaim.
** Copyright (C) 2002-2004 Ryan McCabe <ryan@numb.org>
**
** This file is heavily based on src/protocols/oscar/oscar.c from gaim,
** which is more or less to say that chunks of code have been mostly
** copied from there. In turn, I think that oscar.c file was mostly lifted
** from faimtest. Any copyrights there apply here.
**
** This program is free software; you can redistribute it and/or modify
** it under the terms of the GNU General Public License, version 2,
** as published by the Free Software Foundation.
*/

#include <config.h>

#include <unistd.h>
#include <ncurses.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <sys/types.h>
#include <time.h>
#include <sys/time.h>

#include <libfaim/aim.h>
#include <libfaim/faimconfig.h>

#include <pork.h>
#include <pork_missing.h>
#include <pork_util.h>
#include <pork_list.h>
#include <pork_buddy.h>
#include <pork_imwindow.h>
#include <pork_proto.h>
#include <pork_acct.h>
#include <pork_misc.h>
#include <pork_cstr.h>
#include <pork_color.h>
#include <pork_conf.h>
#include <pork_html.h>
#include <pork_events.h>
#include <pork_io.h>
#include <pork_format.h>
#include <pork_screen.h>
#include <pork_screen_cmd.h>
#include <pork_chat.h>
#include <pork_inet.h>
#include <pork_transfer.h>
#include <pork_msg.h>
#include <pork_opt.h>
#include <pork_aim.h>

static void aim_kill_pending_chats(struct pork_acct *acct);
static int aim_join_chatroom(struct pork_acct *acct, char *name, char *args);
static int aim_chat_print_users(struct pork_acct *acct, struct chatroom *chat);
static dlist_t *aim_find_chat(	struct pork_acct *acct,
								const char *name,
								int exchange);

static int aim_sock_connect(const char *ip,
							struct sockaddr_storage *laddr,
							int *sock);

static int aim_report_idle(struct pork_acct *acct, int mode);
static int aim_connect_abort(struct pork_acct *acct);
static int aim_set_away(struct pork_acct *acct, char *away_msg);
static int aim_login(struct pork_acct *acct);
static int aim_send_msg_auto(struct pork_acct *acct, char *dest, char *msg);
static int aim_update_buddy(struct pork_acct *acct,
							struct buddy *buddy,
							void *data);

static u_int32_t pork_caps =	AIM_CAPS_CHAT | AIM_CAPS_INTEROPERATE |
								AIM_CAPS_SENDFILE;

static char *msgerrreason[] = {
	"Invalid error",
	"Invalid SNAC",
	"Rate to host",
	"Rate to client",
	"Not logged in",
	"Service unavailable",
	"Service not defined",
	"Obsolete SNAC",
	"Not supported by host",
	"Not supported by client",
	"Refused by client",
	"Reply too big",
	"Responses lost",
	"Request denied",
	"Busted SNAC payload",
	"Insufficient rights",
	"In local permit/deny",
	"Sender's warning level is too high",
	"Receiver's warning level is too high",
	"User temporarily unavailable",
	"No match",
	"List overflow",
	"Request ambiguous",
	"Queue full",
	"Not while on AOL"
};

static void aim_debug(	aim_session_t *session __notused,
						int level __notused,
						const char *format __notused,
						va_list va __notused)
{
#ifdef ENABLE_DEBUGGING
	char buf[8192];

	vsnprintf(buf, sizeof(buf), format, va);
	debug("LIBFAIM DEBUG: lev: %d: %s", level, buf);
#endif
}

static int aim_sock_connect(const char *ip,
							struct sockaddr_storage *laddr,
							int *sock)
{
	struct sockaddr_storage ss;
	char *addr;
	char *port;
	in_port_t port_num = FAIM_LOGIN_PORT;

	port = getenv("AIM_PORT");
	if (port != NULL) {
		u_int32_t temp;

		if (str_to_uint(port, &temp) != -1)
			port_num = temp;
	}

	addr = xstrdup(ip);

	port = strchr(addr, ':');
	if (port != NULL) {
		*port++ = '\0';

		if (get_port(port, &port_num) != 0) {
			screen_err_msg("Error: Invalid port: %s", port);
			goto err_out;
		}
	}

	if (get_addr(addr, &ss) != 0) {
		screen_err_msg("Error: Invalid host: %s", addr);
		goto err_out;
	}

	free(addr);
	return (nb_connect(&ss, laddr, port_num, sock));

err_out:
	free(addr);
	return (-1);
}

static void aim_print_info(	char *user,
							char *profile,
							char *away_msg,
							u_int32_t warn_level,
							u_int32_t idle_time,
							u_int32_t online_since,
							u_int32_t member_since,
							int print_all)
{
	char output[8192];
	struct imwindow *imwindow = cur_window();

	if (print_all) {
		fill_format_str(OPT_FORMAT_WHOIS_NAME, output, sizeof(output),
			user, warn_level, idle_time, online_since, member_since);
		screen_win_msg(imwindow, 0, 0, 1, "%s", output);

		if (idle_time > 0) {
			fill_format_str(OPT_FORMAT_WHOIS_IDLE, output, sizeof(output),
				user, warn_level, idle_time, online_since, member_since);
			screen_win_msg(imwindow, 0, 0, 1, "%s", output);
		}

		if (warn_level > 0) {
			fill_format_str(OPT_FORMAT_WHOIS_WARNLEVEL, output, sizeof(output),
				user, warn_level, idle_time, online_since, member_since);
			screen_win_msg(imwindow, 0, 0, 1, "%s", output);
		}

		if (online_since) {
			fill_format_str(OPT_FORMAT_WHOIS_SIGNON, output, sizeof(output),
				user, warn_level, idle_time, online_since, member_since);
			screen_win_msg(imwindow, 0, 0, 1, "%s", output);
		}

		if (member_since) {
			fill_format_str(OPT_FORMAT_WHOIS_MEMBER, output, sizeof(output),
				user, warn_level, idle_time, online_since, member_since);
			screen_win_msg(imwindow, 0, 0, 1, "%s", output);
		}

		if (profile) {
			fill_format_str(OPT_FORMAT_WHOIS_USERINFO, output, sizeof(output),
				user, warn_level, idle_time, online_since, member_since);

			if (strchr(profile, '\n') == NULL)
				screen_win_msg(imwindow, 0, 0, 1, "%s %s", output, profile);
			else
				screen_win_msg(imwindow, 0, 0, 1, "%s\n%s", output, profile);
		}
	}

	if (away_msg) {
		fill_format_str(OPT_FORMAT_WHOIS_AWAY, output, sizeof(output),
			user, warn_level, idle_time, online_since, member_since);

		if (strchr(away_msg, '\n') == NULL)
			screen_win_msg(imwindow, 0, 0, 1, "%s %s", output, away_msg);
		else
			screen_win_msg(imwindow, 0, 0, 1, "%s\n%s", output, away_msg);
	}
}

static int __aim_free_chat(struct pork_acct *acct, void *data) {
	struct aim_chat *a_chat = data;

	if (a_chat->conn != NULL) {
		struct aim_priv *priv = acct->data;

		pork_io_del(a_chat->conn);
		aim_conn_kill(&priv->aim_session, &a_chat->conn);
	}

	free(a_chat->fullname);
	free(a_chat->fullname_quoted);
	free(a_chat->title);
	free(a_chat);

	return (0);
}

static void aim_free_chat(struct pork_acct *acct, dlist_t *node) {
	struct chatroom *chat = node->data;

	__aim_free_chat(acct, chat->data);
}

static char *get_chatname(const char *orig) {
	char *p;
	char *s;
	char *ret;

	if (orig == NULL)
		return (xstrdup(orig));

	p = strrchr(orig, '-');
	if (p == NULL)
		return (xstrdup(orig));

	s = xstrdup(++p);
	ret = s;

	/*
	** The length of s will always be less than or equal to the length of p.
	*/

	while (*p != '\0') {
		if (*p != '%')
			*s = *p++;
		else {
			char buf[3];

			xstrncpy(buf, ++p, sizeof(buf));
			p += 2;

			*s = (char) strtol(buf, NULL, 16);
		}

		s++;
	}

	*s = '\0';
	return (ret);
}

static inline void aim_disconnected_chat(	struct pork_acct *acct,
											struct chatroom *chat)
{
	chat_forced_leave(acct, chat->title, "the server", "disconnected");
}

static void aim_listen_conn_event(int sock, u_int32_t cond, void *data)
{
	aim_conn_t *conn = (aim_conn_t *) data;
	aim_session_t *session = conn->sessv;
	struct pork_acct *acct = session->aux_data;
	struct aim_priv *priv = acct->data;

	if (cond == IO_COND_DEAD ||
		aim_handlerendconnect(&priv->aim_session, conn) < 0)
	{
		struct file_transfer *xfer = conn->priv;

		pork_sock_err(acct, sock);
		close(sock);
		transfer_lost(xfer);
	}
}

static void aim_conn_event(int sock, u_int32_t cond, void *data) {
	aim_conn_t *conn = (aim_conn_t *) data;
	aim_session_t *session = conn->sessv;
	struct pork_acct *acct = session->aux_data;
	struct aim_priv *priv = acct->data;

	if (cond == IO_COND_DEAD) {
		pork_sock_err(acct, sock);

		switch (conn->type) {
			case AIM_CONN_TYPE_BOS:
				debug("bos con for %s died", acct->username);
				aim_connect_abort(acct);
				pork_acct_disconnected(acct);
				break;

			case AIM_CONN_TYPE_CHAT:
				debug("chat con for %s died", acct->username);
				aim_disconnected_chat(acct, conn->priv);
				break;

			case AIM_CONN_TYPE_CHATNAV:
				debug("chatnav con for %s died", acct->username);
				pork_io_del(conn);
				aim_kill_pending_chats(acct);
				aim_conn_kill(session, &conn);
				priv->chatnav_conn = NULL;
				break;

			case AIM_CONN_TYPE_RENDEZVOUS:
				debug("rendezvous con for %s died", acct->username);
				if (conn->subtype == AIM_CONN_SUBTYPE_OFT_SENDFILE)
					transfer_lost(conn->priv);
				else {
					debug("rendezvous con for %s died (not sendfile", acct->username);
					pork_io_del(conn);
					aim_conn_kill(session, &conn);
				}

				break;
		}

		return;
	}

	if (cond & IO_COND_READ) {
		if (aim_get_command(session, conn) >= 0) {
			aim_rxdispatch(session);

			/*
			** If the whole session is terminated (e.g. after a failed
			** login, we can't destroy the account inside the callback
			** (before returning from aim_rxdispatch), or it will crash
			** the client. Instead, this will have to do.
			*/

			if (session->connlist == NULL) {
				pork_acct_disconnected(acct);
				return;
			}
		} else {
			if (conn->type == AIM_CONN_TYPE_RENDEZVOUS &&
				conn->subtype == AIM_CONN_SUBTYPE_OFT_SENDFILE)
			{
				aim_conn_kill(session, &conn);
			}
		}
	}

	if (cond & IO_COND_WRITE)
		aim_tx_flushqueue(session);
}

static void aim_connected(int sock, u_int32_t cond __notused, void *data) {
	aim_conn_t *conn = data;
	aim_session_t *session = conn->sessv;
	struct pork_acct *acct = session->aux_data;
	int ret;

	pork_io_del(conn);

	ret = sock_is_error(sock);
	if (ret != 0) {
		close(sock);
		screen_err_msg("network error: %s: %s", acct->username, strerror(ret));

		switch (conn->type) {
			case AIM_CONN_TYPE_CHATNAV:
				aim_kill_pending_chats(acct);
				aim_conn_kill(session, &conn);
				screen_err_msg("%s is unable to connect to the chatnav server",
					acct->username);
				break;

			case AIM_CONN_TYPE_CHAT: {
				struct chatroom *chat = conn->priv;

				chat_forced_leave(acct, chat->title, "the server", "can't connect");
				break;
			}

			case AIM_CONN_TYPE_AUTH:
			case AIM_CONN_TYPE_BOS:
				screen_err_msg("Unable to login as %s", acct->username);
				pork_acct_disconnected(acct);
				break;

			case AIM_CONN_TYPE_RENDEZVOUS:
				if (conn->subtype == AIM_CONN_SUBTYPE_OFT_SENDFILE)
					transfer_lost(conn->priv);
				break;
		}
	} else {
		conn->fd = sock;
		sock_setflags(sock, 0);
		aim_conn_completeconnect(session, conn);

		pork_io_add(sock, IO_COND_RW, conn, conn,
			aim_conn_event);

		if (conn->type == AIM_CONN_TYPE_RENDEZVOUS &&
			conn->subtype == AIM_CONN_SUBTYPE_OFT_SENDFILE)
		{
			struct file_transfer *xfer = conn->priv;
			struct aim_oft_info *oft_info = xfer->data;

			xfer->sock = sock;
			aim_im_sendch2_sendfile_accept(oft_info->sess, oft_info);
		}
	}
}

static int aim_send_buddy_list(aim_session_t *session, struct pork_acct *acct) {
	dlist_t *cur;

	cur = acct->buddy_pref->group_list;
	while (cur != NULL) {
		struct bgroup *group = cur->data;
		dlist_t *cur_buddy = group->member_list;

		while (cur_buddy != NULL) {
			struct buddy *buddy = cur_buddy->data;

			if (aim_ssi_itemlist_exists(session->ssi.local, buddy->nname)) {
				char *alias;

				alias = aim_ssi_getalias(session->ssi.local,
							group->name, buddy->nname);

				if (alias != NULL) {
					free(buddy->name);
					buddy->name = alias;
				} else {
					aim_ssi_aliasbuddy(session,
						group->name, buddy->nname, buddy->name);
				}
			} else {
				aim_ssi_addbuddy(session, buddy->nname,
					group->name, buddy->name, NULL, NULL, 0);
			}

			cur_buddy = cur_buddy->next;
		}

		cur = cur->next;
	}

	return (1);
}

static int aim_send_permit_list(aim_session_t *session, struct pork_acct *acct)
{
	if (acct->buddy_pref->permit_list != NULL) {
		dlist_t *cur;

		cur = acct->buddy_pref->permit_list;
		while (cur != NULL) {
			aim_ssi_addpermit(session, (char *) cur->data);
			cur = cur->next;
		}
	}

	return (1);
}

static int aim_send_block_list(aim_session_t *session, struct pork_acct *acct) {
	if (acct->buddy_pref->block_list != NULL) {
		dlist_t *cur;

		cur = acct->buddy_pref->block_list;
		while (cur != NULL) {
			aim_ssi_adddeny(session, (char *) cur->data);
			cur = cur->next;
		}
	}

	return (1);
}

/*
** libfaim callback handlers.
*/

FAIM_CB(aim_conn_dead) {
	aim_conn_t *conn;

	va_list ap;

	va_start(ap, fr);
	conn = va_arg(ap, aim_conn_t *);
	va_end(ap);

	if (conn != NULL) {
		aim_session_t *session = aim_conn_getsess(conn);

		if (session != NULL && session->aux_data != NULL && conn->fd >= 0)
			pork_sock_err(session->aux_data, conn->fd);

		pork_io_dead(conn);
	}

	return (1);
}

FAIM_CB(aim_connerr) {
	u_int16_t code;
	char *msg;
	va_list ap;

	va_start(ap, fr);
	code = (u_int16_t) va_arg(ap, int);
	msg = va_arg(ap, char *);
	va_end(ap);

	debug("aim_connerr: msg=%s code=%hu", msg, code);

	if (fr == NULL || fr->conn == NULL)
		return (1);

	debug("conn=%p, fd=%d, type=%d", fr->conn, fr->conn->fd, fr->conn->type);

	if (fr->conn->type == AIM_CONN_TYPE_BOS) {
		if (code == 0x01) {
			debug("somebody signed on from another location with this screenname.");
		} else {
			debug("Unknown error");
		}
	}

	return (1);
}

FAIM_CB(aim_file_transfer_dead) {
	aim_conn_t *conn;
	va_list ap;
	struct file_transfer *xfer;

	va_start(ap, fr);
	conn = va_arg(ap, aim_conn_t *);
	va_end(ap);

	if (conn == NULL) {
		debug("file xfer conn null");
		return (-1);
	}

	xfer = conn->priv;
	if (xfer != NULL) {
		pork_sock_err(xfer->acct, conn->fd);
		pork_io_del(xfer);
	} else {
		debug("xfer died with conn->priv NULL");
	}

	pork_io_del(conn);
	xfer->protocol_flags |= AIM_XFER_IN_HANDLER;
	transfer_lost(xfer);
	return (-1);
}

FAIM_CB(aim_recv_bos_rights) {
	va_list ap;
	struct pork_acct *acct = session->aux_data;
	struct aim_priv *priv = acct->data;

	priv->bos_conn = fr->conn;

	va_start(ap, fr);
	priv->rights.max_permit = va_arg(ap, unsigned int);
	priv->rights.max_deny = va_arg(ap, unsigned int);
	va_end(ap);

	aim_ssi_reqrights(session);
	aim_ssi_reqdata(session);

	return (1);
}

FAIM_CB(aim_recv_buddy_rights) {
	va_list ap;
	struct pork_acct *acct = session->aux_data;
	struct aim_priv *priv = acct->data;

	va_start(ap, fr);
	priv->rights.max_buddies = va_arg(ap, unsigned int);
	priv->rights.max_watchers = va_arg(ap, unsigned int);
	va_end(ap);

	return (1);
}

FAIM_CB(aim_recv_conn_complete) {
	pork_io_set_cond(fr->conn, IO_COND_READ);

	aim_reqpersonalinfo(session, fr->conn);
	aim_locate_reqrights(session);
	aim_buddylist_reqrights(session, fr->conn);
	aim_im_reqparams(session);
	aim_bos_reqrights(session, fr->conn);

	return (1);
}

FAIM_CB(aim_recv_typing) {
	va_list ap;
	u_int16_t type1;
	u_int16_t type2;
	char *source;
	struct imwindow *conv_window;
	struct pork_acct *acct = session->aux_data;

	va_start(ap, fr);
	type1 = (u_int16_t) va_arg(ap, unsigned int);
	source = va_arg(ap, char *);
	type2 = (u_int16_t) va_arg(ap, unsigned int);
	va_end(ap);

	conv_window = imwindow_find(acct, source);
	if (conv_window == NULL)
		return (0);

	if (type2 > 2)
		return (0);

	conv_window->typing = type2;
	return (0);
}

FAIM_CB(aim_recv_err_loc) {
	va_list ap;
	char *dest;
	char *err_str;
	u_int16_t code;

	va_start(ap, fr);
	code = (u_int16_t) va_arg(ap, unsigned int);
	dest = va_arg(ap, char *);
	va_end(ap);

	if (code < array_elem(msgerrreason))
		err_str = msgerrreason[code];
	else
		err_str = "Reason unknown";

	screen_output("User information for %s is unavailable: %s", dest, err_str);
	return (1);
}

FAIM_CB(aim_recv_err_msg) {
	va_list ap;
	char *dest;
	char *err_str;
	u_int16_t code;
	struct pork_acct *acct = session->aux_data;

	va_start(ap, fr);
	code = (u_int16_t) va_arg(ap, unsigned int);
	dest = va_arg(ap, char *);
	va_end(ap);

	if (code < array_elem(msgerrreason))
		err_str = msgerrreason[code];
	else
		err_str = "Reason unknown";

	if (dest != NULL) {
		screen_win_target_msg(acct, dest, 0,
			"%s's message to %s was not sent: %s",
			acct->username, dest, err_str);
	} else {
		screen_err_msg("The last message by %s was not sent: %s",
			acct->username, err_str);
	}

	return (1);
}

FAIM_CB(aim_recv_err_other) {
	va_list ap;
	u_int16_t code;
	char *err_str;

	va_start(ap, fr);
	code = (u_int16_t) va_arg(ap, unsigned int);
	va_end(ap);

	if (code < array_elem(msgerrreason))
		err_str = msgerrreason[code];
	else
		err_str = "Reason unknown";

	screen_err_msg("AIM Error: %s", err_str);
	return (1);
}

FAIM_CB(aim_recv_evil) {
	va_list ap;
	u_int16_t warn_level;
	aim_userinfo_t *userinfo;
	struct pork_acct *acct = session->aux_data;
	char buf[4096];

	va_start(ap, fr);
	warn_level = va_arg(ap, unsigned int);
	userinfo = va_arg(ap, aim_userinfo_t *);
	va_end(ap);

	warn_level = (float) warn_level / 10;
	if (warn_level <= acct->warn_level) {
		acct->warn_level = warn_level;
		return (1);
	}

	acct->warn_level = warn_level;

	if (event_generate(acct->events, EVENT_RECV_WARN, userinfo->sn, warn_level,
		acct->refnum))
	{
		return (1);
	}

	if (blank_str(userinfo->sn)) {
		fill_format_str(OPT_FORMAT_WARN, buf, sizeof(buf), acct->username,
			opt_get_str(OPT_TEXT_WARN_ANONYMOUS), warn_level);

		screen_output("%s", buf);
	} else {
		fill_format_str(OPT_FORMAT_WARN, buf, sizeof(buf),
			acct->username, buddy_name(acct, userinfo->sn), warn_level);

		screen_win_target_msg(acct, userinfo->sn, 1, "%s", buf);
	}

	return (1);
}

FAIM_CB(aim_recv_icbm_param_info) {
	va_list ap;
	struct aim_icbmparameters *params;

	va_start(ap, fr);
	params = va_arg(ap, struct aim_icbmparameters *);
	va_end(ap);

	params->flags = 0x0000000b;
	params->maxmsglen = 8000;
	params->minmsginterval = 0;

	aim_im_setparams(session, params);
	return (1);
}

static int parse_im_chan1(	aim_session_t *session __notused,
							aim_conn_t *conn __notused,
							aim_userinfo_t *userinfo,
							struct aim_incomingim_ch1_args *args)
{
	char *buf;
	int autoresp = 0;
	int ret;
	struct pork_acct *acct = session->aux_data;
	char *p;

	if (args->msg == NULL || args->msglen == 0)
		return (-1);

	if (args->icbmflags & AIM_IMFLAGS_AWAY)
		autoresp = 1;

	if (args->charset == AIM_CHARSET_UNICODE) {
		int i;
		char *msg;

		buf = xmalloc(args->msglen + 1);
		msg = buf;

		for (i = 0 ; i < args->msglen ; i += 2) {
			u_int16_t uc_char;

			uc_char = ((args->msg[i] & 0xff) << 8) | (args->msg[i + 1] & 0xff);

			if (uc_char < 255)
				*msg++ = (char) uc_char;
			else
				*msg++ = '?';
		}

		*msg = '\0';
	} else
		buf = xstrdup(args->msg);

	if (!strncasecmp(buf, "/me ", 3)) {
		ret = pork_recv_action(acct, acct->username,
				userinfo->sn, NULL, &buf[4]);
	} else if ((p = strstr(buf, "/me ")) != NULL) {
		char *tmp = strip_html(buf);

		if (!strncasecmp(tmp, "/me ", 4)) {
			memmove(p, &p[4], strlen(p) - 4 + 1);
			ret = pork_recv_action(acct, acct->username,
					userinfo->sn, NULL, buf);
		} else {
			ret = pork_recv_msg(acct, acct->username,
					userinfo->sn, NULL, buf, autoresp);
		}

		free(tmp);
	} else {
		ret = pork_recv_msg(acct, acct->username,
				userinfo->sn, NULL, buf, autoresp);
	}

	free(buf);
	return (ret);
}

static int parse_im_chan2(	aim_session_t *session,
							aim_conn_t *conn,
							aim_userinfo_t *userinfo,
							struct aim_incomingim_ch2_args *args)
{
	struct pork_acct *acct = session->aux_data;

	if (args->reqclass & AIM_CAPS_CHAT) {
		char *chat_name;
		char chat_fullname[512];

		/*
		** They accepted our invitation.
		*/

		if (args->info.chat.roominfo.name == NULL && args->msg == NULL) {
			struct chatroom *chat = conn->priv;

			if (chat != NULL)
				chat_user_joined(acct, chat, userinfo->sn, NULL, 0);

			return (1);
		}

		chat_name = get_chatname(args->info.chat.roominfo.name);
		if (chat_name == NULL)
			chat_name = xstrdup(args->info.chat.roominfo.name);

		snprintf(chat_fullname, sizeof(chat_fullname), "%s/%d",
			chat_name, args->info.chat.roominfo.exchange);
		free(chat_name);

		chat_got_invite(acct, chat_fullname, userinfo->sn, NULL, (char *) args->msg);
		return (1);
	} else if (args->reqclass & AIM_CAPS_SENDFILE) {
		if (args->status == AIM_RENDEZVOUS_PROPOSE) {
			struct file_transfer *xfer;
			struct aim_oft_info *oft_info;
			char *p;

			if (!args->cookie ||
				!VALID_PORT(args->port) ||
				args->verifiedip == NULL ||
				args->info.sendfile.filename == NULL ||
				args->info.sendfile.totsize == 0||
				args->info.sendfile.totfiles == 0)
			{
				screen_err_msg(
					"%s [%s:%d] has sent an invalid request to send a file",
					userinfo->sn, args->verifiedip, args->port);
				return (0);
			}

			p = strrchr(args->info.sendfile.filename, '\\');
			if (p != NULL && p[1] == '*') {
				screen_err_msg("%s [%s:%d] has attempted to send you a directory (%s). This isn't supported yet. Sorry.",
					userinfo->sn, args->verifiedip, args->port,
					args->info.sendfile.filename);

				oft_info = aim_oft_createinfo(session, args->cookie,
							userinfo->sn, args->clientip, args->port,
							0, 0, NULL);
				oft_info->verifiedip = xstrdup(args->verifiedip);

				aim_im_sendch2_sendfile_cancel(oft_info->sess, oft_info);
				aim_oft_destroyinfo(oft_info);
				return (0);
			}

			xfer = transfer_new(acct, userinfo->sn, TRANSFER_DIR_RECV,
					args->info.sendfile.filename, args->info.sendfile.totsize);
			if (xfer == NULL) {
				debug("transfer_new failed for %s from %s (size: %u)", args->info.sendfile.filename,
					userinfo->sn, args->info.sendfile.totsize);
				return (0);
			}

			xfer->fport = args->port;
			xstrncpy(xfer->faddr_ip, args->verifiedip, sizeof(xfer->faddr_ip));
			if (get_addr(args->verifiedip, &xfer->faddr) != 0) {
				screen_err_msg(
					"%s [%s:%d] has sent an invalid request to send a file",
					userinfo->sn, args->verifiedip, args->port);
			}

			oft_info = aim_oft_createinfo(session, args->cookie, userinfo->sn,
						args->clientip, args->port, 0, 0, NULL);
			oft_info->verifiedip = xstrdup(args->verifiedip);
			xfer->data = oft_info;

			transfer_request_recv(xfer);
		}
	}

	return (0);
}

FAIM_CB(aim_recv_msg) {
	va_list ap;
	int channel;
	aim_userinfo_t *userinfo;
	int ret = 0;

	va_start(ap, fr);
	channel = va_arg(ap, int);
	userinfo = va_arg(ap, aim_userinfo_t *);

	switch (channel) {
		case 1: {
			struct aim_incomingim_ch1_args *args;

			args = va_arg(ap, struct aim_incomingim_ch1_args *);
			ret = parse_im_chan1(session, fr->conn, userinfo, args);
			break;
		}

		case 2: {
			struct aim_incomingim_ch2_args *args;

			args = va_arg(ap, struct aim_incomingim_ch2_args *);
			ret = parse_im_chan2(session, fr->conn, userinfo, args);
			break;
		}

		default:
			debug("unhandled case in recv_msg: chan=%d", channel);
			break;
	}

	va_end(ap);
	return (ret);
}

FAIM_CB(aim_recv_locrights) {
	va_list ap;
	u_int16_t max_len;
	struct pork_acct *acct = session->aux_data;
	struct aim_priv *priv = acct->data;

	va_start(ap, fr);
	max_len = va_arg(ap, int);
	va_end(ap);

	priv->rights.max_profile_len = max_len;
	priv->rights.max_away_len = max_len;

	if (acct->profile != NULL) {
		char *profile_html;
		size_t profile_len;

		profile_html = text_to_html(acct->profile);
		profile_len = strlen(profile_html);

		aim_locate_setprofile(session, "us-ascii", profile_html,
			profile_len, NULL, NULL, 0);
	}

	if (acct->away_msg != NULL) {
		char *away_html;
		size_t away_len;

		away_html = text_to_html(acct->away_msg);
		away_len = strlen(away_html);

		aim_locate_setprofile(session, NULL, NULL, 0,
			"us-ascii", away_html, away_len);
	}

	if (acct->marked_idle)
		aim_srv_setidle(&priv->aim_session, acct->idle_time);

	aim_locate_setcaps(session, pork_caps);
	return (1);
}

FAIM_CB(aim_recv_missed) {
	va_list ap;
	u_int16_t chan;
	u_int16_t num_missed;
	u_int16_t reason;
	aim_userinfo_t *userinfo;
	char buf[1024];
	struct pork_acct *acct = session->aux_data;

	va_start(ap, fr);
	chan = (u_int16_t) va_arg(ap, unsigned int);
	userinfo = va_arg(ap, aim_userinfo_t *);
	num_missed = (u_int16_t) va_arg(ap, unsigned int);
	reason = (u_int16_t) va_arg(ap, unsigned int);
	va_end(ap);

	switch(reason) {
		case 0:
			snprintf(buf, sizeof(buf),
				num_missed == 1 ?
					"%s missed %d message from %s because it was invalid" :
					"%s missed %d messages from %s because they were invalid",
				acct->username, num_missed, userinfo->sn);
			break;

		case 1:
			snprintf(buf, sizeof(buf),
				num_missed == 1 ?
					"%s missed %d message from %s because it was too large" :
					"%s missed %d messages from %s because they were too large",
				acct->username, num_missed, userinfo->sn);
			break;

		case 2:
			snprintf(buf, sizeof(buf),
				num_missed == 1 ?
					"%s missed %d message from %s because the rate limit has been exceeded" :
					"%s missed %d messages from %s because the rate limit has been exceeded",
				acct->username, num_missed, userinfo->sn);
			break;

		case 3:
			snprintf(buf, sizeof(buf),
				num_missed == 1 ?
					"%s missed %d message from %s because the sender's warning level is too high" :
					"%s missed %d messages from %s because the sender's warning level is too high",
				acct->username, num_missed, userinfo->sn);
			break;

		case 4:
			snprintf(buf, sizeof(buf),
				num_missed == 1 ?
					"%s missed %d message from %s because your warning level is too high" :
					"%s missed %d messages from %s because your warning level is too high",
				acct->username, num_missed, userinfo->sn);
			break;

		default:
			snprintf(buf, sizeof(buf),
				num_missed == 1 ?
					"%s missed %d message from %s for unknown reasons" :
					"%s missed %d messages from %s for unknown reasons",
				acct->username, num_missed, userinfo->sn);
			break;
	}

	return (1);
}

FAIM_CB(aim_recv_selfinfo) {
	va_list ap;
	aim_userinfo_t *info;
	struct pork_acct *acct = session->aux_data;

	va_start(ap, fr);
	info = va_arg(ap, aim_userinfo_t *);
	va_end(ap);

	acct->warn_level = (float) info->warnlevel / 10;
	acct->idle_time = info->idletime;

	return (1);
}

FAIM_CB(aim_recv_motd) {
	char *msg;
	u_int16_t id;
	va_list ap;

	va_start(ap, fr);
	id = (u_int16_t) va_arg(ap, unsigned int);
	msg = va_arg(ap, char *);
	va_end(ap);

	if (msg != NULL)
		screen_output("AIM MOTD: %s", msg);

	return (1);
}

FAIM_CB(aim_recv_offgoing) {
	va_list ap;
	aim_userinfo_t *userinfo;
	struct pork_acct *acct = session->aux_data;

	va_start(ap, fr);
	userinfo = va_arg(ap, aim_userinfo_t *);
	va_end(ap);

	if (userinfo == NULL) {
		debug("recv offgoing is NULL");
		return (-1);
	}

	return (buddy_went_offline(acct, userinfo->sn));
}

FAIM_CB(aim_recv_oncoming) {
	va_list ap;
	struct buddy *buddy;
	aim_userinfo_t *userinfo;
	struct pork_acct *acct = session->aux_data;

	va_start(ap, fr);
	userinfo = va_arg(ap, aim_userinfo_t *);
	va_end(ap);

	if (userinfo == NULL) {
		debug("recv oncoming is NULL");
		return (-1);
	}

	buddy = buddy_find(acct, userinfo->sn);
	if (buddy == NULL) {
		debug("recv oncoming for unknown user: %s", userinfo->sn);
		return (-1);
	}

	if (buddy->status == STATUS_OFFLINE)
		buddy_came_online(acct, userinfo->sn, userinfo);
	else {
		int was_away = buddy->status == STATUS_AWAY;
		int is_away = (userinfo->flags & AIM_FLAG_AWAY) != 0;

		if (was_away != is_away) {
			if (was_away)
				buddy_went_unaway(acct, userinfo->sn);
			else
				buddy_went_away(acct, userinfo->sn);
		} else if (buddy->idle_time && !userinfo->idletime)
			buddy_went_unidle(acct, userinfo->sn);
		else if (!buddy->idle_time && userinfo->idletime)
			buddy_went_idle(acct, userinfo->sn, userinfo->idletime);

		buddy_update(acct, buddy, userinfo);
	}

	return (1);
}

FAIM_CB(aim_recv_rate_change) {
	struct pork_acct *acct = session->aux_data;
	va_list ap;
	u_int16_t rate_code;
	u_int16_t rate_class;
	u_int32_t window_size;
	u_int32_t rate_clear;
	u_int32_t alert;
	u_int32_t limit;
	u_int32_t disconnect;
	u_int32_t current_avg;
	u_int32_t max_avg;

	va_start(ap, fr);

	rate_code = (u_int16_t) va_arg(ap, unsigned int);
	rate_class = (u_int16_t) va_arg(ap, unsigned int);
	window_size = (u_int32_t) va_arg(ap, u_int32_t);
	rate_clear = (u_int32_t) va_arg(ap, u_int32_t);
	alert = (u_int32_t) va_arg(ap, u_int32_t);
	limit = (u_int32_t) va_arg(ap, u_int32_t);
	disconnect = (u_int32_t) va_arg(ap, u_int32_t);
	current_avg = (u_int32_t) va_arg(ap, u_int32_t);
	max_avg = (u_int32_t) va_arg(ap, u_int32_t);

	va_end(ap);

	switch (rate_code) {
		case AIM_RATE_CODE_CHANGE:
			if (current_avg >= rate_clear)
				aim_conn_setlatency(fr->conn, 0);
			break;

		case AIM_RATE_CODE_WARNING:
			aim_conn_setlatency(fr->conn, window_size / 4);
			break;

		case AIM_RATE_CODE_LIMIT:
			aim_conn_setlatency(fr->conn, 10);
			screen_err_msg("The last message from %s was not sent because you are over the rate limit. Please wait 10 seconds, and then try again", acct->username);
			break;

		case AIM_RATE_CODE_CLEARLIMIT:
			aim_conn_setlatency(fr->conn, 0);
			break;

		default:
			debug("unknown rate change code: %hu", rate_code);
			break;
	}

	return (1);
}

FAIM_CB(aim_recv_chatnav_info) {
	va_list ap;
	u_int16_t type;
	struct pork_acct *acct = session->aux_data;
	struct aim_priv *priv = acct->data;

	va_start(ap, fr);
	type = (u_int16_t) va_arg(ap, unsigned int);

	switch (type) {
		case 0x0002: {
			u_int8_t max_rooms;
			int exchange_count;
			struct aim_chat_exchangeinfo *exchanges;
			dlist_t *cur;

			max_rooms = (u_int8_t) va_arg(ap, unsigned int);
			exchange_count = va_arg(ap, int);
			exchanges = va_arg(ap, struct aim_chat_exchangeinfo *);

			cur = priv->chat_create_list;
			while (cur != NULL) {
				struct chatroom_info *chat = cur->data;
				dlist_t *next = cur->next;

				aim_chatnav_createroom(session, fr->conn, chat->name,
					chat->exchange);

				priv->chat_create_list =
					dlist_remove(priv->chat_create_list, cur);

				free(chat->name);
				free(chat);

				cur = next;
			}

			break;
		}

		case 0x0008: {
			char *fqcn;
			char *name;
			char *ck;
			u_int16_t instance;
			u_int16_t flags;
			u_int16_t maxmsglen;
			u_int16_t maxoccupancy;
			u_int16_t unknown;
			u_int16_t exchange;
			u_int8_t createperms;
			u_int32_t createtime;

			fqcn = va_arg(ap, char *);
			instance = (u_int16_t) va_arg(ap, unsigned int);
			exchange = (u_int16_t) va_arg(ap, unsigned int);
			flags = (u_int16_t) va_arg(ap, unsigned int);
			createtime = va_arg(ap, u_int32_t);
			maxmsglen = (u_int16_t) va_arg(ap, unsigned int);
			maxoccupancy = (u_int16_t) va_arg(ap, unsigned int);
			createperms = (u_int8_t) va_arg(ap, unsigned int);
			unknown = (u_int16_t) va_arg(ap, unsigned int);
			name = va_arg(ap, char *);
			ck = va_arg(ap, char *);

			aim_chat_join(session, priv->bos_conn, exchange, ck, instance);
			break;
		}

		default:
			debug("unknown chatnav type: %x", type);
			break;
	}

	va_end(ap);
	return (1);
}

FAIM_CB(aim_recv_chat_join) {
	struct pork_acct *acct = session->aux_data;
	struct chatroom *chat = fr->conn->priv;
	aim_userinfo_t *userinfo;
	va_list ap;
	u_int32_t num_users;
	u_int32_t old_num_users = chat->num_users;

	va_start(ap, fr);
	num_users = va_arg(ap, int);
	userinfo = va_arg(ap, aim_userinfo_t *);
	va_end(ap);

	if (old_num_users == 0) {
		if (num_users == 1) {
			chat_created(acct, chat);
			chat_user_joined(acct, chat, userinfo[0].sn, NULL, 1);
		} else {
			u_int32_t i;

			for (i = 0 ; i < num_users ; i++) {
				if (!aim_sncmp(userinfo[i].sn, acct->username))
					chat_user_joined(acct, chat, userinfo[i].sn, NULL, 0);
				else
					chat_user_joined(acct, chat, userinfo[i].sn, NULL, 1);
			}

			aim_chat_print_users(acct, chat);
		}
	} else
		chat_user_joined(acct, chat, userinfo->sn, NULL, 0);

	return (1);
}

FAIM_CB(aim_recv_chat_leave) {
	struct pork_acct *acct = session->aux_data;
	struct chatroom *chat = fr->conn->priv;
	va_list ap;
	int num_users;
	aim_userinfo_t *info;

	va_start(ap, fr);
	num_users = va_arg(ap, int);
	info = va_arg(ap, aim_userinfo_t *);
	va_end(ap);

	/*
	** Sometimes we'll receive a notice that we're leaving
	** a chat room. I don't know what makes this happen, but
	** it means that we've been disconnected somehow.
	*/

	if (!aim_sncmp(info->sn, acct->username)) {
		debug("chat_recv_leave: user is us");
		aim_disconnected_chat(acct, chat);
		return (1);
	}

	chat_user_left(acct, chat, info->sn, 0);
	return (1);
}

FAIM_CB(aim_recv_chat_info_update) {
	va_list ap;
	aim_userinfo_t *userinfo;
	struct aim_chat_roominfo *roominfo;
	char *roomname;
	int usercount;
	char *roomdesc;
	u_int16_t unknown_c9, unknown_d2, unknown_d5, maxmsglen, maxvisiblemsglen;
	u_int32_t creationtime;
	struct chatroom *chat = fr->conn->priv;
	struct aim_chat *a_chat = chat->data;

	va_start(ap, fr);
	roominfo = va_arg(ap, struct aim_chat_roominfo *);
	roomname = va_arg(ap, char *);
	usercount = va_arg(ap, int);
	userinfo = va_arg(ap, aim_userinfo_t *);
	roomdesc = va_arg(ap, char *);
	unknown_c9 = (u_int16_t) va_arg(ap, unsigned int);
	creationtime = (u_int32_t) va_arg(ap, u_int32_t);
	maxmsglen = (u_int16_t) va_arg(ap, unsigned int);
	unknown_d2 = (u_int16_t) va_arg(ap, unsigned int);
	unknown_d5 = (u_int16_t) va_arg(ap, unsigned int);
	maxvisiblemsglen = (u_int16_t) va_arg(ap, unsigned int);
	va_end(ap);

	a_chat->max_msg_len = maxmsglen;
	a_chat->max_visible_len = maxvisiblemsglen;
	a_chat->created = (time_t) creationtime;

	return (1);
}

FAIM_CB(aim_recv_chat_msg) {
	va_list ap;
	aim_userinfo_t *info;
	char *msg;
	struct pork_acct *acct = session->aux_data;
	struct chatroom *chat = fr->conn->priv;
	int len;
	char *charset;
	char *p;

	va_start(ap, fr);
	info = va_arg(ap, aim_userinfo_t *);
	len = va_arg(ap, int);
	msg = va_arg(ap, char *);
	charset = va_arg(ap, char *);
	va_end(ap);

	if (!strncasecmp(msg, "/me ", 4)) {
		chat_recv_action(acct, chat,
			chat->title_quoted, info->sn, NULL, &msg[4]);
	} else if ((p = strstr(msg, "/me ")) != NULL) {
		char *tmp = strip_html(msg);

		if (!strncasecmp(tmp, "/me ", 4)) {
			memmove(p, &p[4], strlen(p) - 4 + 1);
			chat_recv_action(acct, chat,
				chat->title_quoted, info->sn, NULL, msg);
		} else
			chat_recv_msg(acct, chat, chat->title_quoted, info->sn, NULL, msg);

		free(tmp);
	} else
		chat_recv_msg(acct, chat, chat->title_quoted, info->sn, NULL, msg);

	return (1);
}

FAIM_CB(recv_chat_conn) {
	pork_io_set_cond(fr->conn, IO_COND_READ);

	aim_conn_addhandler(session, fr->conn, AIM_CB_FAM_CHT, AIM_CB_CHT_ERROR, aim_recv_err_other, 0);
	aim_conn_addhandler(session, fr->conn, AIM_CB_FAM_CHT, AIM_CB_CHT_USERJOIN, aim_recv_chat_join, 0);
	aim_conn_addhandler(session, fr->conn, AIM_CB_FAM_CHT, AIM_CB_CHT_USERLEAVE, aim_recv_chat_leave, 0);
	aim_conn_addhandler(session, fr->conn, AIM_CB_FAM_CHT, AIM_CB_CHT_ROOMINFOUPDATE, aim_recv_chat_info_update, 0);
	aim_conn_addhandler(session, fr->conn, AIM_CB_FAM_CHT, AIM_CB_CHT_INCOMINGMSG, aim_recv_chat_msg, 0);

	aim_clientready(session, fr->conn);
	return (1);
}

FAIM_CB(recv_chatnav_conn) {
	struct pork_acct *acct = session->aux_data;
	struct aim_priv *priv = acct->data;

	pork_io_set_cond(fr->conn, IO_COND_READ);

	aim_conn_addhandler(session, fr->conn, AIM_CB_FAM_CTN, AIM_CB_CTN_ERROR, aim_recv_err_other, 0);
	aim_conn_addhandler(session, fr->conn, AIM_CB_FAM_CTN, AIM_CB_CTN_INFO, aim_recv_chatnav_info, 0);

	aim_clientready(session, fr->conn);
	aim_chatnav_reqrights(session, fr->conn);

	priv->chatnav_conn = fr->conn;

	return (1);
}

FAIM_CB(aim_recv_redirect) {
	struct aim_redirect_data *redirect;
	va_list ap;
	struct pork_acct *acct = session->aux_data;

	va_start(ap, fr);
	redirect = va_arg(ap, struct aim_redirect_data *);
	va_end(ap);

	if (redirect->group == AIM_CONN_TYPE_CHATNAV) {
		aim_conn_t *chatnav;
		int sock;
		int ret;

		chatnav = aim_newconn(session, AIM_CONN_TYPE_CHATNAV, NULL);
		if (chatnav == NULL) {
			screen_err_msg("Unable to connect to the chatnav server: %s",
				strerror(errno));
			return (0);
		}

		ret = aim_sock_connect(redirect->ip, &acct->laddr, &sock);
		if (ret == 0) {
			aim_connected(sock, 0, chatnav);
		} else if (ret == -EINPROGRESS) {
			chatnav->status |= AIM_CONN_STATUS_INPROGRESS;
			pork_io_add(sock, IO_COND_WRITE, chatnav, chatnav, aim_connected);
		} else {
			aim_conn_kill(session, &chatnav);
			screen_err_msg("Unable to connect to the chatnav server: %s",
				strerror(errno));
			return (0);
		}

		aim_conn_addhandler(session, chatnav, AIM_CB_FAM_SPECIAL, AIM_CB_SPECIAL_CONNDEAD, aim_conn_dead, 0);
		aim_conn_addhandler(session, chatnav, AIM_CB_FAM_SPECIAL, AIM_CB_SPECIAL_CONNERR, aim_connerr, 0);
		aim_conn_addhandler(session, chatnav, AIM_CB_FAM_SPECIAL, AIM_CB_SPECIAL_CONNINITDONE, recv_chatnav_conn, 0);

		aim_sendcookie(session, chatnav, redirect->cookielen, redirect->cookie);
	} else if (redirect->group == AIM_CONN_TYPE_CHAT) {
		aim_conn_t *chat_conn;
		struct chatroom *chat;
		struct aim_chat *a_chat;
		struct imwindow *win;
		char buf[128];
		int sock;
		int ret;
		char *chat_title;

		chat_conn = aim_newconn(session, AIM_CONN_TYPE_CHAT, NULL);
		if (chat_conn == NULL) {
			screen_err_msg("Unable to connect to the chat server");
			return (0);
		}

		ret = aim_sock_connect(redirect->ip, &acct->laddr, &sock);
		if (ret == 0) {
			aim_connected(sock, 0, chat_conn);
		} else if (ret == -EINPROGRESS) {
			chat_conn->status |= AIM_CONN_STATUS_INPROGRESS;
			pork_io_add(sock, IO_COND_WRITE, chat_conn, chat_conn,
				aim_connected);
		} else {
			aim_conn_kill(session, &chat_conn);
			screen_err_msg("Unable to connect to the chatnav server");
			return (0);
		}

		chat_title = get_chatname(redirect->chat.room);

		snprintf(buf, sizeof(buf), "%s/%d",
			chat_title, redirect->chat.exchange);

		win = imwindow_find_chat_target(acct, buf);
		if (win == NULL) {
			debug("unable to find chat window for %s (acct: %s)", buf, acct->username);
			return (-1);
		}

		chat = chat_new(acct, buf, (char *) redirect->chat.room, win);

		a_chat = xcalloc(1, sizeof(*a_chat));
		a_chat->fullname = xstrdup(redirect->chat.room);
		a_chat->fullname_quoted = color_quote_codes(a_chat->fullname);
		a_chat->title = chat_title;
		a_chat->exchange = redirect->chat.exchange;
		a_chat->instance = redirect->chat.instance;
		a_chat->conn = chat_conn;

		chat->data = a_chat;
		chat_conn->priv = chat;

		aim_conn_addhandler(session, chat_conn, AIM_CB_FAM_SPECIAL, AIM_CB_SPECIAL_CONNDEAD, aim_conn_dead, 0);
		aim_conn_addhandler(session, chat_conn, AIM_CB_FAM_SPECIAL, AIM_CB_SPECIAL_CONNERR, aim_connerr, 0);
		aim_conn_addhandler(session, chat_conn, AIM_CB_FAM_SPECIAL, AIM_CB_SPECIAL_CONNINITDONE, recv_chat_conn, 0);
		aim_sendcookie(session, chat_conn, redirect->cookielen, redirect->cookie);
	}

	return (1);
}

FAIM_CB(aim_recv_search_error) {
	va_list ap;
	char *address;
	struct pork_acct *acct = session->aux_data;

	va_start(ap, fr);
	address = va_arg(ap, char *);
	va_end(ap);

	if (event_generate(acct->events, EVENT_RECV_SEARCH_RESULT, address, NULL,
		acct->refnum))
	{
		return (1);
	}

	screen_output("No results were found for %s", address);
	return (1);
}

FAIM_CB(aim_recv_search_reply) {
	va_list ap;
	char *address;
	char *usernames;
	char buf[2048];
	int i = 0;
	size_t len = sizeof(buf);
	int j;
	int num;
	int ret;
	struct pork_acct *acct = session->aux_data;

	va_start(ap, fr);
	address = va_arg(ap, char *);
	num = va_arg(ap, int);
	usernames = va_arg(ap, char *);
	va_end(ap);

	if (num < 1) {
		if (event_generate(acct->events, EVENT_RECV_SEARCH_RESULT,
			address, NULL, acct->refnum))
		{
			return (1);
		}

		screen_output("No results were found for %s", address);
		return (1);
	}

	ret = snprintf(buf, len, "%s has registered the following screen names: %s",
			address, &usernames[0]);
	if (ret < 0 || (size_t) ret >= len) {
		screen_output("The results for %s were too long to display", address);
		return (1);
	}
	len -= ret;
	i += ret;

	for (j = 1 ; j < num ; j++) {
		ret = snprintf(&buf[i], len, ", %s", &usernames[j * (MAXSNLEN + 1)]);
		if (ret < 0 || (size_t) ret >= len) {
			screen_output("The results for %s were too long to display",
				address);
			return (1);
		}

		len -= ret;
		i += ret;
	}

	if (event_generate(acct->events, EVENT_RECV_SEARCH_RESULT, address, buf,
		acct->refnum))
	{
		return (1);
	}

	screen_output("%s", buf);
	return (1);
}

FAIM_CB(aim_ssi_recv_rights) {
	struct pork_acct *acct = session->aux_data;
	struct aim_priv *priv = acct->data;
	int num;
	u_int16_t *items;
	va_list ap;

	va_start(ap, fr);
	num = va_arg(ap, int);
	items = va_arg(ap, u_int16_t *);
	va_end(ap);

	if (num >= 0)
		priv->rights.max_buddies = items[0];

	if (num >= 1)
		priv->rights.max_groups = items[1];

	if (num >= 2)
		priv->rights.max_permit = items[2];

	if (num >= 3)
		priv->rights.max_deny = items[3];

	return (1);
}

FAIM_CB(aim_ssi_recv_list) {
	struct aim_ssi_item *cur;
	struct pork_acct *acct = session->aux_data;

	aim_ssi_enable(session);
	aim_ssi_cleanlist(session);

	/*
	** Merge the buddy list on the server with the locally
	** stored buddy list. By this point, the local
	** buddy list will have already been processed.
	*/

	for (cur = session->ssi.local ; cur != NULL ; cur = cur->next) {
		if (cur->type == 0) {
			char *group_name;
			struct buddy *buddy;
			struct bgroup *group;
			char *alias;

			if (cur->name == NULL)
				continue;

			group_name = aim_ssi_itemlist_findparentname(session->ssi.local,
							cur->name);

			alias = aim_ssi_getalias(session->ssi.local, group_name, cur->name);

			if (group_name == NULL)
				group_name = "orphans";

			buddy = buddy_find(acct, cur->name);
			if (buddy != NULL) {
				if (alias != NULL) {
					free(buddy->name);
					buddy->name = alias;
				}

				continue;
			}

			group = group_add(acct, group_name);
			buddy = buddy_add(acct, cur->name, group, 0);
			if (alias != NULL)
				buddy_alias(acct, buddy, alias, 0);
			else
				buddy_alias(acct, buddy, cur->name, 0);

			free(alias);
		} else if (cur->type == 2) {
			if (cur->name == NULL)
				continue;

			buddy_add_permit(acct, cur->name, 0);
		} else if (cur->type == 3) {
			/* Deny */
			if (cur->name == NULL)
				continue;

			buddy_add_block(acct, cur->name, 0);
		} else if (cur->type == 4) {
			u_int32_t privacy_mode = aim_ssi_getpermdeny(session->ssi.local);

			acct->buddy_pref->privacy_mode = privacy_mode;
		} else if (cur->type == 5) {
			u_int32_t report_idle = aim_ssi_getpresence(session->ssi.local);

			if (report_idle & 0x0000400)
				acct->report_idle = 1;
			else
				acct->report_idle = 0;
		}
	}

	if (!acct->connected) {
		aim_report_idle(acct, acct->report_idle);

		aim_ssi_setpermdeny(session,
			acct->buddy_pref->privacy_mode, 0xffffffff);
	}

	aim_send_buddy_list(session, acct);
	aim_send_permit_list(session, acct);
	aim_send_block_list(session, acct);

	if (!acct->connected) {
		struct aim_priv *priv = acct->data;

		aim_clientready(session, fr->conn);

		pork_acct_connected(acct);
		time(&priv->last_update);
	}

	return (1);
}

FAIM_CB(aim_recv_userinfo) {
	struct pork_acct *acct = session->aux_data;
	aim_userinfo_t *userinfo;
	va_list ap;
	char *profile = NULL;
	char *away_msg = NULL;
	u_int32_t idle_time = 0;
	u_int32_t online_since = 0;
	u_int32_t member_since = 0;
	u_int32_t warn_level;
	int print_all = 1;

	va_start(ap, fr);
	userinfo = va_arg(ap, aim_userinfo_t *);
	va_end(ap);

	if (userinfo->present & AIM_USERINFO_PRESENT_ONLINESINCE)
		online_since = userinfo->onlinesince;

	if (userinfo->present & AIM_USERINFO_PRESENT_MEMBERSINCE)
		member_since = userinfo->membersince;

	if (userinfo->present & AIM_USERINFO_PRESENT_IDLE)
		idle_time = userinfo->idletime;

	warn_level = (float) userinfo->warnlevel / 10;

	if ((userinfo->flags & AIM_FLAG_AWAY) && userinfo->away_len > 0 &&
		userinfo->away != NULL)
	{
		char *temp = NULL;

		if (*userinfo->away != '\0') {
			temp = xmalloc(userinfo->away_len + 1);

			/*
			** libfaim isn't nul-terminating the string anymore,
			** so we can't call xstrncpy.
			*/

			memcpy(temp, userinfo->away, userinfo->away_len);
			temp[userinfo->away_len] = '\0';
		}

		if (event_generate(acct->events, EVENT_RECV_AWAYMSG, userinfo->sn,
			member_since, online_since, idle_time, warn_level,
			temp, acct->refnum))
		{
			free(temp);
			return (1);
		}

		if (temp != NULL) {
			away_msg = strip_html(temp);
			free(temp);
		}
	}

	if (userinfo->info_len > 0 && userinfo->info != NULL) {
		char *temp = NULL;

		if (*userinfo->info != '\0') {
			temp = xmalloc(userinfo->info_len + 1);
			memcpy(temp, userinfo->info, userinfo->info_len);
			temp[userinfo->info_len] = '\0';
		}

		if (event_generate(acct->events, EVENT_RECV_PROFILE,
			userinfo->sn, member_since, online_since, idle_time,
			warn_level, temp, acct->refnum))
		{
			free(temp);
			return (1);
		}

		if (temp != NULL) {
			profile = strip_html(temp);
			free(temp);
		}
	}

	aim_print_info(userinfo->sn, profile, away_msg, warn_level,
		idle_time, online_since, member_since, print_all);

	free(away_msg);
	free(profile);
	return (1);
}

FAIM_CB(aim_parse_login) {
	struct pork_acct *acct = session->aux_data;
	char *key;
	va_list ap;
	static struct client_info_s client_info = CLIENTINFO_AIM_KNOWNGOOD;

	va_start(ap, fr);
	key = va_arg(ap, char *);
	va_end(ap);

	aim_send_login(session, fr->conn,
		acct->username, acct->passwd, &client_info, key);

	return (1);
}

FAIM_CB(aim_file_send_done) {
	va_list ap;
	aim_conn_t *conn;
	fu8_t *cookie;
	struct aim_fileheader_t *fh;
	struct file_transfer *xfer;

	va_start(ap, fr);
	conn = va_arg(ap, aim_conn_t *);
	cookie = va_arg(ap, fu8_t *);
	fh = va_arg(ap, struct aim_fileheader_t *);
	va_end(ap);

	xfer = conn->priv;
	pork_io_del(conn);
	return (transfer_send_complete(xfer));
}

FAIM_CB(aim_file_send_ready) {
	va_list ap;
	fu8_t *cookie;
	struct aim_fileheader_t *header;
	aim_conn_t *conn;
	struct file_transfer *xfer;

	va_start(ap, fr);
	conn = va_arg(ap, aim_conn_t *);
	cookie = va_arg(ap, fu8_t *);
	header = va_arg(ap, struct aim_fileheader_t *);
	va_end(ap);

	xfer = conn->priv;
	pork_io_del(conn);
	pork_io_add(xfer->sock, IO_COND_WRITE, xfer, xfer,
		transfer_send_data);

	return (0);
}

FAIM_CB(aim_file_send_accepted) {
	aim_conn_t *conn;
	aim_conn_t *listen_conn;
	va_list ap;
	struct file_transfer *xfer;
	struct aim_oft_info *oft_info;

	va_start(ap, fr);
	conn = va_arg(ap, aim_conn_t *);
	listen_conn = va_arg(ap, aim_conn_t *);
	va_end(ap);

	xfer = listen_conn->priv;
	xfer->sock = conn->fd;

	oft_info = xfer->data;
	oft_info->conn = conn;
	oft_info->conn->priv = xfer;
	xfer->data = oft_info;

	pork_io_del(listen_conn);
	aim_conn_kill(session, &listen_conn);

	if (get_peer_addr(xfer->sock, &xfer->faddr) != 0) {
		xfer->protocol_flags |= AIM_XFER_IN_HANDLER;
		transfer_abort(xfer);
		return (-1);
	}

	get_ip(&xfer->faddr, xfer->faddr_ip, sizeof(xfer->faddr_ip));
	xfer->fport = sin_port(&xfer->faddr);

	pork_io_add(xfer->sock, IO_COND_READ, conn, conn, aim_conn_event);

	aim_clearhandlers(conn);

	aim_conn_addhandler(session, conn, AIM_CB_FAM_OFT, AIM_CB_OFT_ACK, aim_file_send_ready, 0);
	aim_conn_addhandler(session, conn, AIM_CB_FAM_OFT, AIM_CB_OFT_DONE, aim_file_send_done, 0);
	aim_conn_addhandler(session, conn, AIM_CB_FAM_SPECIAL, AIM_CB_SPECIAL_CONNDEAD, aim_file_transfer_dead, 0);

	aim_oft_sendheader(session, AIM_CB_OFT_PROMPT, oft_info);
	transfer_send_accepted(xfer);
	return (0);
}

FAIM_CB(aim_file_recv_accept) {
	fu8_t *cookie;
	aim_conn_t *conn;
	struct aim_fileheader_t *header;
	va_list ap;
	struct file_transfer *xfer;
	struct aim_oft_info *oft_info;

	va_start(ap, fr);
	conn = va_arg(ap, aim_conn_t *);
	cookie = va_arg(ap, fu8_t *);
	header = va_arg(ap, struct aim_fileheader_t *);
	va_end(ap);

	if (conn == NULL || cookie == NULL || header == NULL)
		return (1);

	xfer = conn->priv;
	oft_info = xfer->data;

	memcpy(&oft_info->fh, header, sizeof(oft_info->fh));
	memcpy(&oft_info->fh.bcookie, oft_info->cookie,
		sizeof(oft_info->fh.bcookie));

	aim_oft_sendheader(oft_info->sess, AIM_CB_OFT_ACK, oft_info);

	pork_io_del(conn);
	aim_clearhandlers(conn);

	if (get_local_addr(xfer->sock, &xfer->laddr) != 0) {
		xfer->protocol_flags |= AIM_XFER_IN_HANDLER;
		transfer_abort(xfer);
		return (-1);
	}

	xfer->lport = sin_port(&xfer->laddr);

	pork_io_add(xfer->sock, IO_COND_READ, xfer, xfer,
		transfer_recv_data);

	return (0);
}

FAIM_CB(aim_parse_authresp) {
	va_list ap;
	struct aim_authresp_info *authresp;
	aim_conn_t *bos_conn;
	struct pork_acct *acct = session->aux_data;
	int ret;
	int sock;
	struct sockaddr_storage local;

	pork_io_del(fr->conn);
	aim_conn_kill(session, &fr->conn);

	va_start(ap, fr);
	authresp = va_arg(ap, struct aim_authresp_info *);
	va_end(ap);

	if (authresp->errorcode || !authresp->bosip || !authresp->cookie) {
		switch (authresp->errorcode) {
			case 0x05:
				screen_err_msg("Error logging in %s: Incorrect username or password",
					acct->username);
				break;

			case 0x11:
				screen_err_msg("Error logging in %s: This account has been suspended",
					acct->username);
				break;

			case 0x14:
				screen_err_msg("Error logging in %s: This service is temporarily unavailable",
					acct->username);
				break;

			case 0x18:
				screen_err_msg("Error logging in %s: This account has been connecting too frequently",
					acct->username);
				break;

			case 0x1c:
				screen_err_msg("Error logging in %s: Client is too old",
					acct->username);
				break;

			default:
				screen_err_msg("Error logging in %s: Authentication failed",
					acct->username);
				break;
		}

		return (1);
	}

	bos_conn = aim_newconn(session, AIM_CONN_TYPE_BOS, NULL);
	if (bos_conn == NULL) {
		aim_conn_kill(session, &bos_conn);
		screen_err_msg("Unable to connect to the AIM BOS server");
		return (1);
	}

	memcpy(&local, &acct->laddr, sizeof(local));
	sin_set_port(&local, acct->lport);

	ret = aim_sock_connect(authresp->bosip, &local, &sock);
	if (ret == 0) {
		aim_connected(sock, 0, bos_conn);
	} else if (ret == -EINPROGRESS) {
		bos_conn->status |= AIM_CONN_STATUS_INPROGRESS;
		pork_io_add(sock, IO_COND_WRITE, bos_conn, bos_conn,
			aim_connected);
	} else {
		aim_conn_kill(session, &bos_conn);
		screen_err_msg("Unable to connect to the BOS server");
		return (0);
	}

	aim_conn_addhandler(session, bos_conn, AIM_CB_FAM_SPECIAL, AIM_CB_SPECIAL_CONNDEAD, aim_conn_dead, 0);
	aim_conn_addhandler(session, bos_conn, AIM_CB_FAM_SPECIAL, AIM_CB_SPECIAL_CONNERR, aim_connerr, 0);
	aim_conn_addhandler(session, bos_conn, AIM_CB_FAM_SPECIAL, AIM_CB_SPECIAL_CONNINITDONE, aim_recv_conn_complete, 0);
	aim_conn_addhandler(session, bos_conn, AIM_CB_FAM_BOS, AIM_CB_BOS_RIGHTS, aim_recv_bos_rights, 0);
	aim_conn_addhandler(session, bos_conn, AIM_CB_FAM_ACK, AIM_CB_ACK_ACK, NULL, 0);
	aim_conn_addhandler(session, bos_conn, AIM_CB_FAM_GEN, AIM_CB_GEN_REDIRECT, aim_recv_redirect, 0);
	aim_conn_addhandler(session, bos_conn, AIM_CB_FAM_LOC, AIM_CB_LOC_RIGHTSINFO, aim_recv_locrights, 0);
	aim_conn_addhandler(session, bos_conn, AIM_CB_FAM_BUD, AIM_CB_BUD_RIGHTSINFO, aim_recv_buddy_rights, 0);
	aim_conn_addhandler(session, bos_conn, AIM_CB_FAM_BUD, AIM_CB_BUD_ONCOMING, aim_recv_oncoming, 0);
	aim_conn_addhandler(session, bos_conn, AIM_CB_FAM_BUD, AIM_CB_BUD_OFFGOING, aim_recv_offgoing, 0);
	aim_conn_addhandler(session, bos_conn, AIM_CB_FAM_MSG, AIM_CB_MSG_INCOMING, aim_recv_msg, 0);
	aim_conn_addhandler(session, bos_conn, AIM_CB_FAM_LOC, AIM_CB_LOC_ERROR, aim_recv_err_loc, 0);
	aim_conn_addhandler(session, bos_conn, AIM_CB_FAM_MSG, AIM_CB_MSG_MISSEDCALL, aim_recv_missed, 0);
	aim_conn_addhandler(session, bos_conn, AIM_CB_FAM_GEN, AIM_CB_GEN_RATECHANGE, aim_recv_rate_change, 0);
	aim_conn_addhandler(session, bos_conn, AIM_CB_FAM_GEN, AIM_CB_GEN_EVIL, aim_recv_evil, 0);
	aim_conn_addhandler(session, bos_conn, AIM_CB_FAM_LOK, AIM_CB_LOK_ERROR, aim_recv_search_error, 0);
	aim_conn_addhandler(session, bos_conn, AIM_CB_FAM_LOK, 0x0003, aim_recv_search_reply, 0);
	aim_conn_addhandler(session, bos_conn, AIM_CB_FAM_MSG, AIM_CB_MSG_ERROR, aim_recv_err_msg, 0);
	aim_conn_addhandler(session, bos_conn, AIM_CB_FAM_MSG, AIM_CB_MSG_MTN, aim_recv_typing, 0);
	aim_conn_addhandler(session, bos_conn, AIM_CB_FAM_LOC, AIM_CB_LOC_USERINFO, aim_recv_userinfo, 0);
	aim_conn_addhandler(session, bos_conn, AIM_CB_FAM_MSG, AIM_CB_MSG_PARAMINFO, aim_recv_icbm_param_info, 0);
	aim_conn_addhandler(session, bos_conn, AIM_CB_FAM_GEN, AIM_CB_GEN_ERROR, aim_recv_err_other, 0);
	aim_conn_addhandler(session, bos_conn, AIM_CB_FAM_BUD, AIM_CB_BUD_ERROR, aim_recv_err_other, 0);
	aim_conn_addhandler(session, bos_conn, AIM_CB_FAM_BOS, AIM_CB_BOS_ERROR, aim_recv_err_other, 0);
	aim_conn_addhandler(session, bos_conn, AIM_CB_FAM_GEN, AIM_CB_GEN_SELFINFO, aim_recv_selfinfo, 0);
	aim_conn_addhandler(session, bos_conn, AIM_CB_FAM_GEN, AIM_CB_GEN_MOTD, aim_recv_motd, 0);
	aim_conn_addhandler(session, bos_conn, AIM_CB_FAM_SSI, AIM_CB_SSI_RIGHTSINFO, aim_ssi_recv_rights, 0);
	aim_conn_addhandler(session, bos_conn, AIM_CB_FAM_SSI, AIM_CB_SSI_LIST, aim_ssi_recv_list, 0);
	aim_conn_addhandler(session, bos_conn, AIM_CB_FAM_SSI, AIM_CB_SSI_NOLIST, aim_ssi_recv_list, 0);

	aim_sendcookie(session, bos_conn, authresp->cookielen, authresp->cookie);

	return (1);
}

static int aim_login(struct pork_acct *acct) {
	struct aim_priv *priv = acct->data;
	int ret;
	int sock;
	aim_conn_t *auth_conn;
	char *server;

	server = getenv("AIM_SERVER");
	if (server == NULL)
		server = FAIM_LOGIN_SERVER;

	acct->connected = 0;
	screen_status_msg_ts("Logging in as %s...", acct->username);

	auth_conn = aim_newconn(&priv->aim_session, AIM_CONN_TYPE_AUTH, NULL);
	if (auth_conn == NULL) {
		screen_err_msg("Connection error while logging in as %s",
			acct->username);

		return (-1);
	}

	ret = aim_sock_connect(server, &acct->laddr, &sock);
	if (ret == 0) {
		aim_connected(sock, 0, auth_conn);
	} else if (ret == -EINPROGRESS) {
		auth_conn->status |= AIM_CONN_STATUS_INPROGRESS;
		pork_io_add(sock, IO_COND_WRITE, auth_conn, auth_conn, aim_connected);
	} else {
		screen_err_msg("Error connecting to the authorizer server as %s",
			acct->username);
		aim_conn_kill(&priv->aim_session, &auth_conn);
		return (-1);
	}

	aim_conn_addhandler(&priv->aim_session, auth_conn, AIM_CB_FAM_SPECIAL, AIM_CB_SPECIAL_CONNERR, aim_connerr, 0);
	aim_conn_addhandler(&priv->aim_session, auth_conn, AIM_CB_FAM_ATH, AIM_CB_ATH_AUTHRESPONSE, aim_parse_login, 0);
	aim_conn_addhandler(&priv->aim_session, auth_conn, AIM_CB_FAM_ATH, AIM_CB_ATH_LOGINRESPONSE, aim_parse_authresp, 0);

	aim_request_login(&priv->aim_session, auth_conn, acct->username);
	return (1);
}

static int aim_setup(struct pork_acct *acct) {
	struct aim_priv *priv = acct->data;
	aim_session_t *session = &priv->aim_session;

	memset(session, 0, sizeof(*session));

	aim_session_init(session, 0, 0);
	aim_setdebuggingcb(session, aim_debug);

	aim_tx_setenqueue(session, AIM_TX_IMMEDIATE, NULL);
	session->aux_data = acct;

	return (0);
}

static int aim_send_msg_auto(struct pork_acct *acct, char *dest, char *msg) {
	struct aim_priv *priv = acct->data;
	char *msg_html = text_to_html(msg);

	return (aim_im_sendch1(&priv->aim_session, dest, 1, msg_html));
}

static int aim_send_msg(struct pork_acct *acct, char *dest, char *msg) {
	struct aim_priv *priv = acct->data;
	char *msg_html = text_to_html(msg);

	return (aim_im_sendch1(&priv->aim_session, dest, 0, msg_html));
}

static int aim_action(struct pork_acct *acct, char *dest, char *msg) {
	char buf[8192];
	int ret;

	if (!strncasecmp(msg, "<html>", 6))
		ret = snprintf(buf, sizeof(buf), "<HTML>/me %s", &msg[6]);
	else
		ret = snprintf(buf, sizeof(buf), "/me %s", msg);

	if (ret < 0 || (size_t) ret >= sizeof(buf)) {
		debug("snprintf failed");
		return (-1);
	}

	return (aim_send_msg(acct, dest, buf));
}

static int aim_add_block(struct pork_acct *acct, char *target) {
	struct aim_priv *priv = acct->data;

	if (priv->aim_session.ssi.received_data == 0)
		return (-1);

	return (aim_ssi_adddeny(&priv->aim_session, target));
}

static int aim_unblock(struct pork_acct *acct, char *target) {
	struct aim_priv *priv = acct->data;

	if (priv->aim_session.ssi.received_data == 0) {
		debug("unblock failed");
		return (-1);
	}

	return (aim_ssi_deldeny(&priv->aim_session, target));
}

static int aim_add_permit(struct pork_acct *acct, char *target) {
	struct aim_priv *priv = acct->data;

	if (priv->aim_session.ssi.received_data == 0) {
		debug("add permit failed");
		return (-1);
	}

	return (aim_ssi_addpermit(&priv->aim_session, target));
}

static int aim_remove_permit(struct pork_acct *acct, char *target) {
	struct aim_priv *priv = acct->data;

	if (priv->aim_session.ssi.received_data == 0) {
		debug("remove permit failed");
		return (-1);
	}

	return (aim_ssi_delpermit(&priv->aim_session, target));
}

static int aim_buddy_add(struct pork_acct *acct, struct buddy *buddy) {
	int ret;
	struct aim_priv *priv = acct->data;
	aim_session_t *session = &priv->aim_session;

	if (session->ssi.received_data == 0) {
		debug("buddy add failed");
		return (-1);
	}

	if (aim_ssi_itemlist_exists(session->ssi.local, buddy->nname)) {
		debug("buddy %s already exists for %s", buddy->nname, acct->username);
		return (-1);
	}

	ret = aim_ssi_addbuddy(session, buddy->nname,
			buddy->group->name, buddy->name, NULL, NULL, 0);

	return (ret);
}

static int aim_buddy_remove(struct pork_acct *acct, struct buddy *buddy) {
	struct aim_priv *priv = acct->data;
	int ret;

	if (priv->aim_session.ssi.received_data == 0) {
		debug("buddy remove failed");
		return (-1);
	}

	ret = aim_ssi_delbuddy(&priv->aim_session, buddy->nname,
			buddy->group->name);

	return (ret);
}

static int aim_buddy_alias(struct pork_acct *acct, struct buddy *buddy) {
	struct aim_priv *priv = acct->data;
	int ret;

	if (priv->aim_session.ssi.received_data == 0) {
		debug("buddy alias failed");
		return (-1);
	}

	ret = aim_ssi_aliasbuddy(&priv->aim_session,
			buddy->group->name, buddy->nname, buddy->name);

	return (ret);
}

static int aim_warn(struct pork_acct *acct, char *target) {
	struct aim_priv *priv = acct->data;

	return (aim_im_warn(&priv->aim_session, priv->bos_conn, target, 0));
}

static int aim_warn_anon(struct pork_acct *acct, char *target) {
	struct aim_priv *priv = acct->data;
	int ret;

	ret = aim_im_warn(&priv->aim_session, priv->bos_conn,
		target, AIM_WARN_ANON);

	return (ret);
}

static int aim_set_back(struct pork_acct *acct) {
	struct aim_priv *priv = acct->data;
	int ret;

	ret = aim_locate_setprofile(&priv->aim_session, NULL, NULL, 0, NULL, "", 0);
	return (ret);
}

static int aim_set_away(struct pork_acct *acct, char *away_msg) {
	size_t len;
	int ret;
	char *msg_html;
	struct aim_priv *priv = acct->data;

	msg_html = text_to_html(away_msg);

	len = strlen(msg_html);
	if (len > priv->rights.max_away_len) {
		screen_err_msg("%s's away message is too long. The maximum length is %u characters",
			acct->username, priv->rights.max_away_len);
		return (-1);
	}

	ret = aim_locate_setprofile(&priv->aim_session,
			NULL, NULL, 0, "us-ascii", msg_html, len);

	return (ret);
}

static int aim_keepalive(struct pork_acct *acct) {
	struct aim_priv *priv = acct->data;

	return (aim_flap_nop(&priv->aim_session, priv->bos_conn));
}

static int aim_set_idle(struct pork_acct *acct, u_int32_t idle_secs) {
	struct aim_priv *priv = acct->data;
	int ret;

	ret = aim_srv_setidle(&priv->aim_session, idle_secs);
	if (ret >= 0) {
		if (idle_secs > 0)
			acct->marked_idle = 1;
		else
			acct->marked_idle = 0;
	}

	return (ret);
}

static int aim_set_profile(struct pork_acct *acct, char *profile) {
	int ret;
	size_t len;
	char *profile_html;
	struct aim_priv *priv = acct->data;

	if (profile == NULL) {
		ret = aim_locate_setprofile(&priv->aim_session,
				NULL, "", 0, NULL, NULL, 0);

		return (ret);
	}

	profile_html = text_to_html(profile);

	len = strlen(profile_html);
	if (len > priv->rights.max_profile_len) {
		screen_err_msg("%s's profile is too long. The maximum length is %u characters",
			acct->username, priv->rights.max_profile_len);
		return (-1);
	}

	ret = aim_locate_setprofile(&priv->aim_session, "us-ascii",
			profile_html, len, NULL, NULL, 0);

	return (ret);
}

static int aim_get_away_msg(struct pork_acct *acct, char *buddy) {
	struct aim_priv *priv = acct->data;

	return (aim_locate_getinfoshort(&priv->aim_session, buddy, 0x00000002));
}

static int aim_whois(struct pork_acct *acct, char *buddy) {
	struct aim_priv *priv = acct->data;

	return (aim_locate_getinfoshort(&priv->aim_session, buddy, 0x00000003));
}

static void aim_kill_pending_chats(struct pork_acct *acct) {
	struct aim_priv *priv = acct->data;
	dlist_t *cur;

	cur = priv->chat_create_list;
	while (cur != NULL) {
		struct chatroom_info *info = cur->data;
		dlist_t *next = cur->next;

		free(info->name);
		free(info);
		free(cur);

		cur = next;
	}
}

static int aim_kill_all_conn(struct pork_acct *acct) {
	struct aim_priv *priv = acct->data;
	aim_conn_t *conn;
	dlist_t *cur;

	if (priv == NULL)
		return (-1);

	conn = priv->aim_session.connlist;

	if (acct->connected) {
		aim_rxdispatch(&priv->aim_session);
		aim_tx_flushqueue(&priv->aim_session);
	}

	while (conn != NULL) {
		pork_io_del(conn);
		conn = conn->next;
	}

	aim_kill_pending_chats(acct);

	cur = acct->chat_list;
	while (cur != NULL) {
		dlist_t *next = cur->next;

		aim_free_chat(acct, cur);
		cur = next;
	}

	aim_session_kill(&priv->aim_session);
	return (0);
}

static int aim_connect_abort(struct pork_acct *acct) {
	aim_kill_all_conn(acct);
	aim_setup(acct);

	return (0);
}

static int aim_set_privacy_mode(struct pork_acct *acct, int mode) {
	struct aim_priv *priv = acct->data;

	if (mode >= 0 && mode <= 5) {
		acct->buddy_pref->privacy_mode = mode;
		aim_ssi_setpermdeny(&priv->aim_session,
			acct->buddy_pref->privacy_mode, 0xffffffff);
	}

	return (acct->buddy_pref->privacy_mode);
}

static int aim_report_idle(struct pork_acct *acct, int mode) {
	struct aim_priv *priv = acct->data;

	u_int32_t report_idle = aim_ssi_getpresence(priv->aim_session.ssi.local);
	int ret;

	if (mode != 0)
		report_idle |= 0x0000400;
	else
		report_idle &= ~0x0000400;

	acct->report_idle = ((report_idle & 0x0000400) != 0);
	ret = aim_ssi_setpresence(&priv->aim_session, report_idle);
	return (ret);
}

static int aim_chat_parse_name(const char *name, struct chatroom_info *info) {
	int exchange;
	char *p;

	p = strchr(name, '/');
	if (p != NULL) {
		p++;

		if (str_to_int(p, &exchange) != 0)
			return (-1);

		if (exchange < 4)
			exchange = 4;
		else if (exchange > 16)
			exchange = 16;

		info->name = xstrndup(name, p - name - 1);
		info->exchange = exchange;
	} else {
		info->name = xstrdup(name);
		info->exchange = AIM_DEFAULT_CHAT_EXCHANGE;
	}

	return (0);
}

static int aim_chat_get_name(	const char *str,
								char *buf,
								size_t len,
								char *arg_buf,
								size_t arg_len)
{
	struct chatroom_info info;
	int ret;

	if (aim_chat_parse_name(str, &info) != 0) {
		debug("aim_chat_parse_name failed for %s", str);
		return (-1);
	}

	ret = snprintf(buf, len, "%s/%d", info.name, info.exchange);
	free(info.name);

	if (ret < 0 || (size_t) ret >= len)
		return (-1);

	return (0);
}

static dlist_t *aim_find_chat(	struct pork_acct *acct,
								const char *name,
								int exchange)
{
	dlist_t *cur;

	cur = acct->chat_list;
	while (cur != NULL) {
		struct chatroom *chat = cur->data;
		struct aim_chat *a_chat = chat->data;

		if (!strcasecmp(name, a_chat->title) && exchange == a_chat->exchange)
			return (cur);

		cur = cur->next;
	}

	return (NULL);
}

static dlist_t *aim_find_chat_name(struct pork_acct *acct, char *name) {
	dlist_t *ret;
	struct chatroom_info info;

	if (aim_chat_parse_name(name, &info) != 0) {
		debug("aim_chat_parse_name failed for %s", name);
		return (NULL);
	}

	ret = aim_find_chat(acct, info.name, info.exchange);
	free(info.name);
	return (ret);
}

static struct chatroom *aim_find_chat_name_data(struct pork_acct *acct,
												char *name)
{
	dlist_t *ret;

	ret = aim_find_chat_name(acct, name);
	if (ret == NULL || ret->data == NULL)
		return (NULL);

	return (ret->data);
}

static int aim_leave_chatroom(struct pork_acct *acct, struct chatroom *chat) {
	struct aim_priv *priv = acct->data;
	struct aim_chat *a_chat;

	a_chat = chat->data;

	pork_io_del(a_chat->conn);
	aim_conn_kill(&priv->aim_session, &a_chat->conn);

	a_chat->conn = NULL;
	return (0);
}

static int aim_join_chatroom(struct pork_acct *acct, char *name, char *args) {
	int ret;
	struct aim_priv *priv = acct->data;
	aim_conn_t *chatnav_conn;
	struct chatroom_info info;
	struct chatroom *chat;

	chat = chat_find(acct, name);
	if (chat != NULL && chat->data != NULL)
		return (0);

	if (aim_chat_parse_name(name, &info) != 0) {
		debug("aim_chat_parse_name failed for %s", name);
		return (-1);
	}

	chatnav_conn = priv->chatnav_conn;
	if (chatnav_conn == NULL) {
		struct chatroom_info *chat_info = xcalloc(1, sizeof(*chat_info));

		chat_info->name = xstrdup(info.name);
		chat_info->exchange = (u_int16_t) info.exchange;

		priv->chat_create_list = dlist_add_head(priv->chat_create_list,
									chat_info);
		aim_reqservice(&priv->aim_session, priv->bos_conn,
			AIM_CONN_TYPE_CHATNAV);

		free(info.name);
		return (0);
	}

	ret = aim_chatnav_createroom(&priv->aim_session, chatnav_conn,
			info.name, info.exchange);

	free(info.name);
	return (ret);
}

static int aim_chat_send(	struct pork_acct *acct,
							struct chatroom *chat,
							char *target __notused,
							char *msg)
{
	char *msg_html;
	int msg_len;
	int ret;
	struct aim_priv *priv = acct->data;
	struct aim_chat *a_chat;

	if (msg == NULL) {
		debug("aim_chat_send with NULL msg");
		return (-1);
	}

	a_chat = chat->data;

	msg_html = text_to_html(msg);
	msg_len = strlen(msg_html);

	if (msg_len > a_chat->max_msg_len) {
		debug("msg len too long");
		return (-1);
	}

	ret = aim_chat_send_im(&priv->aim_session, a_chat->conn,
			AIM_CHATFLAGS_NOREFLECT, msg_html, msg_len, "us-ascii", "en");

	return (ret);
}

static int aim_chat_action(	struct pork_acct *acct,
							struct chatroom *chat,
							char *target __notused,
							char *msg)
{
	char buf[8192];
	int ret;

	if (!strncasecmp(msg, "<html>", 6))
		ret = snprintf(buf, sizeof(buf), "<HTML>/me %s", &msg[6]);
	else
		ret = snprintf(buf, sizeof(buf), "/me %s", msg);

	if (ret < 0 || (size_t) ret >= sizeof(buf))
		return (-1);

	return (aim_chat_send(acct, chat, chat->title_quoted, buf));
}

static int aim_chat_send_invite(struct pork_acct *acct,
								struct chatroom *chat,
								char *dest,
								char *msg)
{
	struct aim_priv *priv = acct->data;
	int ret;
	struct aim_chat *a_chat;

	a_chat = chat->data;

	if (msg == NULL)
		msg = "";

	ret = aim_im_sendch2_chatinvite(&priv->aim_session, dest, msg,
			a_chat->exchange, a_chat->fullname, 0);

	return (ret);
}

static int aim_chat_print_users(struct pork_acct *acct __notused,
								struct chatroom *chat)
{
	int ret;
	dlist_t *cur;
	char buf[2048];
	size_t i = 0;
	size_t len = sizeof(buf);
	struct chat_user *chat_user;
	struct aim_chat *a_chat;
	struct imwindow *win = chat->win;

	a_chat = chat->data;
	cur = chat->user_list;
	if (cur == NULL) {
		screen_win_msg(win, 0, 0, 1,
			"%%D--%%m--%%M--%%x No %%Cu%%csers%%x in %%C%s%%x (%%W%s%%x)",
			chat->title_quoted, chat->title_full_quoted);

		return (0);
	}

	screen_win_msg(win, 0, 0, 1,
		"%%D--%%m--%%M--%%x %u %%Cu%%csers%%x in %%C%s%%x (%%W%s%%x)",
		chat->num_users, chat->title_quoted, chat->title_full_quoted);

	while (cur != NULL) {
		chat_user = cur->data;

		if (chat_user->ignore)
			ret = snprintf(&buf[i], len, "[%%R%s%%x] ", chat_user->name);
		else
			ret = snprintf(&buf[i], len, "[%%B%s%%x] ", chat_user->name);

		if (ret < 0 || (size_t) ret >= len) {
			screen_output("The results were too long to display");
			return (0);
		}

		len -= ret;
		i += ret;

		cur = cur->next;
	}

	if (i > 0 && buf[i - 1] == ' ')
		buf[i - 1] = '\0';

	screen_write(win, buf);
	return (0);
}

static int aim_search(struct pork_acct *acct, char *str) {
	struct aim_priv *priv = acct->data;

	return (aim_search_address(&priv->aim_session, priv->bos_conn, str));
}

static int aim_update_buddy(struct pork_acct *acct __notused,
							struct buddy *buddy,
							void *data)
{
	aim_userinfo_t *userinfo = data;

	buddy->signon_time = userinfo->onlinesince;
	buddy->warn_level = (float) userinfo->warnlevel / 10;

	if (userinfo->present & AIM_USERINFO_PRESENT_FLAGS)
		buddy->type = userinfo->flags;

	if (userinfo->present & AIM_USERINFO_PRESENT_SESSIONLEN)
		buddy->idle_time = userinfo->idletime;

	buddy->status = STATUS_ACTIVE;

	if (buddy->idle_time > 0)
		buddy->status = STATUS_IDLE;

	if (userinfo->flags & AIM_FLAG_AWAY)
		buddy->status = STATUS_AWAY;

	return (0);
}

static int aim_acct_update(struct pork_acct *acct) {
	struct aim_priv *priv;

	if (!acct->connected)
		return (-1);

	/*
	** We've got to keep track of AIM buddies' idle time
	** ourselves; the AIM server reports an idle time
	** for them only when there's a change (i.e. they
	** go idle, come back from idle or their client reports
	** a new idle time). This routine will run every minute
	** and increment the idle time of buddies where appropriate.
	*/

	priv = acct->data;
	if (time(NULL) >= priv->last_update + 60) {
		aim_keepalive(acct);
		buddy_update_idle(acct);
		aim_cleansnacs(&priv->aim_session, 59);
		time(&priv->last_update);
	}

	return (0);
}

static int aim_acct_init(struct pork_acct *acct) {
	char buf[NUSER_LEN];
	struct aim_priv *priv;

	normalize(buf, acct->username, sizeof(buf));

	/*
	** You can't have the same screen name logged in more than
	** once with AIM.
	*/
	if (pork_acct_find_name(buf, PROTO_AIM) != NULL)
		return (-1);

	free(acct->username);
	acct->username = xstrdup(buf);

	priv = xcalloc(1, sizeof(*priv));
	acct->data = priv;

	if (aim_setup(acct) != 0) {
		free(priv);
		return (-1);
	}

	if (acct->profile == NULL)
		acct->profile = xstrdup(DEFAULT_AIM_PROFILE);

	return (0);
}

static int aim_acct_free(struct pork_acct *acct) {
	aim_kill_all_conn(acct);
	free(acct->data);
	return (0);
}

static int aim_connect(struct pork_acct *acct, char *args) {
	if (acct->passwd == NULL) {
		if (args != NULL && !blank_str(args)) {
			acct->passwd = xstrdup(args);
			memset(args, 0, strlen(args));
		} else {
			char buf[128];

			screen_prompt_user("Password: ", buf, sizeof(buf));
			if (buf[0] == '\0') {
				screen_err_msg("There was an error reading your password");
				return (-1);
			}

			acct->passwd = xstrdup(buf);
			memset(buf, 0, sizeof(buf));
		}
	}

	return (aim_login(acct));
}

static int aim_read_config(struct pork_acct *acct) {
	return (read_user_config(acct));
}

static int aim_write_config(struct pork_acct *acct) {
	return (save_user_config(acct));
}

static int aim_file_recv_data(struct file_transfer *xfer, char *buf, size_t len)
{
	struct aim_oft_info *oft_info = xfer->data;

	oft_info->fh.recvcsum = aim_oft_checksum_chunk(buf, len,
								oft_info->fh.recvcsum);

	if (xfer->bytes_sent + xfer->start_offset >= xfer->file_len)
		transfer_recv_complete(xfer);

	return (0);
}

static int aim_file_recv_complete(struct file_transfer *xfer) {
	struct aim_oft_info *oft_info = xfer->data;

	oft_info->fh.nrecvd = xfer->bytes_sent;

	aim_oft_sendheader(oft_info->sess, AIM_CB_OFT_DONE, oft_info);

	aim_clearhandlers(oft_info->conn);
	aim_conn_kill(oft_info->sess, &oft_info->conn);
	aim_oft_destroyinfo(oft_info);

	pork_io_del(xfer);
	return (0);
}

static int aim_file_send_complete(struct file_transfer *xfer) {
	struct aim_oft_info *oft_info = xfer->data;

	aim_clearhandlers(oft_info->conn);
	aim_conn_kill(oft_info->sess, &oft_info->conn);
	aim_oft_destroyinfo(oft_info);

	pork_io_del(xfer);
	return (0);
}

static int aim_file_send(struct file_transfer *xfer) {
	struct aim_priv *priv = xfer->acct->data;
	struct aim_oft_info *oft_info;

	if (transfer_bind_listen_sock(xfer, priv->bos_conn->fd) == -1)
		return (-1);

	oft_info = aim_oft_createinfo(&priv->aim_session, NULL,
				xfer->peer_username, xfer->laddr_ip, xfer->lport,
				xfer->file_len, 0, xfer->fname_base);

	oft_info->fh.checksum = aim_oft_checksum_file(xfer->fname_local);
	oft_info->port = xfer->lport;

	aim_sendfile_listen(&priv->aim_session, oft_info, xfer->sock);

	if (oft_info->conn == NULL) {
		aim_oft_destroyinfo(oft_info);
		return (-1);
	}

	oft_info->conn->priv = xfer;
	xfer->data = oft_info;

	pork_io_add(xfer->sock, IO_COND_RW, oft_info->conn, oft_info->conn,
		aim_listen_conn_event);

	aim_im_sendch2_sendfile_ask(&priv->aim_session, oft_info);
	aim_conn_addhandler(&priv->aim_session, oft_info->conn, AIM_CB_FAM_OFT, AIM_CB_OFT_ESTABLISHED, aim_file_send_accepted, 0);

	transfer_request_send(xfer);
	return (0);
}

static int aim_file_send_data(	struct file_transfer *xfer,
								char *buf __notused,
								size_t len __notused)
{
	if (xfer->status == TRANSFER_STATUS_COMPLETE) {
		pork_io_del(xfer);
		return (transfer_send_complete(xfer));
	}

	return (0);
}

static int aim_file_abort(struct file_transfer *xfer) {
	struct aim_oft_info *oft_info = xfer->data;

	pork_io_del(xfer);

	if (oft_info != NULL) {
		pork_io_del(oft_info->conn);
		aim_im_sendch2_sendfile_cancel(oft_info->sess, oft_info);
		aim_clearhandlers(oft_info->conn);

		if (!(xfer->protocol_flags & AIM_XFER_IN_HANDLER))
			aim_conn_kill(oft_info->sess, &oft_info->conn);
		else
			aim_conn_close(oft_info->conn);

		aim_oft_destroyinfo(oft_info);
	}

	return (0);
}

static int aim_file_accept(struct file_transfer *xfer) {
	struct aim_oft_info *oft_info;
	struct aim_priv *priv = xfer->acct->data;
	char buf[512];
	int ret;
	int sock;

	oft_info = xfer->data;

	snprintf(buf, sizeof(buf), "%s:%d", oft_info->verifiedip,
		oft_info->port);

	oft_info->conn = aim_newconn(&priv->aim_session,
						AIM_CONN_TYPE_RENDEZVOUS, NULL);

	if (oft_info->conn == NULL) {
		screen_err_msg("Error connecting to %s@%s while receiving %s",
			xfer->peer_username, buf, xfer->fname_local);
		return (-1);
	}

	oft_info->conn->subtype = AIM_CONN_SUBTYPE_OFT_SENDFILE;
	oft_info->conn->priv = xfer;

	ret = aim_sock_connect(buf, &xfer->acct->laddr, &sock);
	if (ret == 0) {
		aim_connected(sock, 0, oft_info->conn);
	} else if (ret == -EINPROGRESS) {
		oft_info->conn->status |= AIM_CONN_STATUS_INPROGRESS;
		pork_io_add(sock, IO_COND_WRITE, oft_info->conn, oft_info->conn,
			aim_connected);
	} else {
		aim_conn_kill(&priv->aim_session, &oft_info->conn);
		screen_err_msg("Error connecting to %s@%s while receiving %s",
			xfer->peer_username, buf, xfer->fname_local);
		return (-1);
	}

	aim_conn_addhandler(&priv->aim_session, oft_info->conn,
		AIM_CB_FAM_OFT, AIM_CB_OFT_PROMPT, aim_file_recv_accept, 0);
	aim_conn_addhandler(&priv->aim_session, oft_info->conn,
		AIM_CB_FAM_SPECIAL, AIM_CB_SPECIAL_CONNDEAD, aim_file_transfer_dead, 0);

	transfer_recv_accepted(xfer);
	return (0);
}

static char *aim_filter_text_out(char *msg) {
	if (!strncasecmp(msg, "<HTML", 5))
		return (strip_html(msg));

	return (color_quote_codes(msg));
}

int aim_proto_init(struct pork_proto *proto) {
	proto->buddy_add = aim_buddy_add;
	proto->buddy_alias = aim_buddy_alias;
	proto->buddy_block = aim_add_block;
	proto->buddy_permit = aim_add_permit;
	proto->buddy_remove = aim_buddy_remove;
	proto->buddy_remove_permit = aim_remove_permit;
	proto->buddy_unblock = aim_unblock;
	proto->buddy_update = aim_update_buddy;

	proto->chat_free = __aim_free_chat;
	proto->chat_find = aim_find_chat_name_data;
	proto->chat_action = aim_chat_action;
	proto->chat_invite = aim_chat_send_invite;
	proto->chat_join = aim_join_chatroom;
	proto->chat_leave = aim_leave_chatroom;
	proto->chat_name = aim_chat_get_name;
	proto->chat_send = aim_chat_send;
	proto->chat_users = aim_chat_print_users;
	proto->chat_who = aim_chat_print_users;

	proto->who = aim_search;
	proto->connect = aim_connect;
	proto->connect_abort = aim_connect_abort;
	proto->reconnect = aim_connect;
	proto->free = aim_acct_free;
	proto->get_away_msg = aim_get_away_msg;
	proto->get_profile = aim_whois;
	proto->init = aim_acct_init;
	proto->keepalive = aim_keepalive;
	proto->filter_text = strip_html;
	proto->filter_text_out = aim_filter_text_out;
	proto->normalize = normalize;
	proto->user_compare = aim_sncmp;
	proto->read_config = aim_read_config;
	proto->send_msg = aim_send_msg;
	proto->send_msg_auto = aim_send_msg_auto;
	proto->set_away = aim_set_away;
	proto->set_back = aim_set_back;
	proto->set_idle_time = aim_set_idle;
	proto->set_privacy_mode = aim_set_privacy_mode;
	proto->set_profile = aim_set_profile;
	proto->set_report_idle = aim_report_idle;
	proto->update = aim_acct_update;
	proto->warn = aim_warn;
	proto->send_action = aim_action;
	proto->warn_anon = aim_warn_anon;
	proto->write_config = aim_write_config;

	proto->file_send = aim_file_send;
	proto->file_send_data = aim_file_send_data;
	proto->file_accept = aim_file_accept;
	proto->file_abort = aim_file_abort;
	proto->file_recv_data = aim_file_recv_data;
	proto->file_recv_complete = aim_file_recv_complete;
	proto->file_send_complete = aim_file_send_complete;

	return (0);
}
