#!/usr/bin/perl -W
#  Hiya Emacs! This file is: -*- perl-indent-level: 8; coding: utf-8 -*-
#
#  MSNre - Perl implementation of a MSN Instant Messager Clone
#
#  Copyright (C) 2003 <incoming@tiscali.cz>
#
#  This program is free software; you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 2 of the License, or
#  (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software
#  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
#  $Id: msnre,v 1.25 2003/07/17 08:12:47 incoming Exp $

package MSNre;
require 5.006;

use Carp;
use Curses;
use Encode;
use FileHandle;
use Getopt::Long;
use POE qw(Wheel::Curses Wheel::ReadWrite);
use Net::MsnMessenger;
use POSIX;
use Symbol qw(gensym);
use strict qw(subs vars);
use vars qw($VERSION $DATE $NAME $msn $st_time $rh $colors $Rows $Columns);

BEGIN
{
	$|++;
	$VERSION = '0.91';
	$DATE = '20030717';
	$NAME = 'MSNre';

	$rh->{message_highlight} =   # Pattern to highlight message parts
	{
		bold         => qr{  ( _.+?_ | \*.+?\*  )                                               }x,
		email        => qr{  ( [A-Za-z0-9\.\-\+_]+ \@ [A-Za-z0-9\.\-\+_]+? \. \w{2,3} )         }x,
		exclamations => qr{  ( [?!] {3,} )                                                      }x,
		smiley       => qr{  ( [:;8] [\'\`]? [-^o]? [\(\)\[\]><\@\\\/]+ (?!\S) | \(\S\) )       }x,
		url          => qr{  ( \b\S+?://\S+?\.\S+\.\S*\b | \b(?:www|ftp)\.\S+\.\S{2,3}\S*?\b )  }x,
	};

	@{$rh->{callbacks}} =        # Protocol callbacks

	    qw(ADD_BY_USER ADD_GROUP ADD_USER ALERT ALLOW_USER BLOCK_USER CHANGE_MY_FRIENDLY_NAME
	       CHANGE_MY_STATUS CHANGE_STATUS CHANGE_STATUS_OFFLINE CLOSE_CHAT DEBUG DEBUG_CONNECTION
	       DISCONNECT DISCONNECT_FORCED EMAIL_ACTIVE EMAIL_INVITE_SUCCESS EMAIL_NEW EMAIL_UNREAD
	       FILE_ACCEPT FILE_INVITED FILE_RECEIVE_CANCEL FILE_RECEIVE_PROGRESS FILE_RECEIVE_START
	       FILE_RECEIVE_SUCCESS FILE_REJECT FILE_SEND_CANCEL FILE_SEND_PROGRESS FILE_SEND_START
	       FILE_SEND_SUCCESS FIND_INVITE_SUCCESS FIND_RESULTS INITIAL_STATUS JOIN_SESSION_USER
	       LEAVE_SESSION_USER LOGIN_PROGRESS NETMEETING_ACCEPT NETMEETING_CANCEL NETMEETING_INVITED
	       NETMEETING_LAUNCH NETMEETING_REJECT OPEN_CHAT PONG RECEIVE_MESSAGE REMOVE_BY_USER
	       REMOVE_GROUP REMOVE_USER RENAME_GROUP SEND_MESSAGE_NOT_RECEIVED SEND_MESSAGE_RECEIVED
	       SEND_MESSAGE_SENT SERVER_ERROR SERVER_MESSAGE SIGNED_IN SWB_TIMEOUT SYNC_DONE TYPING_USER
	       UNALLOW_USER UNBLOCK_USER UNIMPLEMENTED UNKNOWN_REJECT URL);

	$rh->{command} =             # Client commands
	{
		about       => { func  => \&cmd_about,
				 help  => "Print information about the client",
				 usage => "",
			       },
		account     => { func  => \&cmd_account,
				 help  => "Launch the browser and update your preferences",
				 usage => "",
			       },
		addforward  => { func  => \&cmd_add_forward,
				 help  => "Forward all the messages to an user or window",
				 usage => "<user> | <window>",
			       },
		addgroup    => { func  => \&cmd_add_group,
				 help  => "Add a new group to the contact list",
				 usage => "<new group>",
			       },
		adduser     => { func  => \&cmd_add_user,
				 help  => "Add an user to the contact list",
				 usage => "<user> [ <group name> ]",
			       },
		alert       => { func  => \&cmd_alert,
				 help  => "Manipulate received MSN Alerts",
				 usage => "[ LIST | GO | CLEAR | REMOVE ]",
			       },
		aliascmd    => { func  => \&cmd_alias_command,
				 help  => "Create an alias for a command",
				 usage => "[ <alias> <real command> ]",
			       },
		aliascn     => { func  => \&cmd_alias_contact,
				 help  => "Create an alias for a contact",
				 usage => "[ <user> <alias> ]",
			       },
		allow       => { func  => \&cmd_allow_user,
				 help  => "Allow an user to see your status",
				 usage => "<user1> [ <user2> ... <userN> ]",
			       },
		awaymsg     => { func  => \&cmd_away_msg,
				 help  => "Print (update) your away messages",
				 usage => "[ <message type> ] [ <message> ]",
			       },
		block       => { func  => \&cmd_block_user,
				 help  => "Block an user",
				 usage => "<user1> [ <user2> ... <userN> ]",
			       },
		chprofile   => { func  => \&cmd_change_profile,
				 help  => "Change a profile on the fly",
				 usage => "[ <profile name> ]",
			       },
		clear       => { func  => sub { cmd_clear($rh->{w}->{current}) },
				 help  => "Clear the current window",
				 usage => "[ <window number> ]",
			       },
		clearall    => { func  => \&cmd_clear_all,
				 help  => "Clear all the existing windows",
				 usage => "",
			       },
		connect     => { func  => \&cmd_connect,
				 help  => "Connect and login to the server",
				 usage => "",
			       },
		disconnect  => { func  => \&cmd_disconnect,
				 help  => "Disconnect from the server",
				 usage => "",
			       },
		exec        => { func  => \&cmd_exec,
				 help  => "Execute a shell command and print the output",
				 usage => "<shell command>",
			       },
		exit        => { func  => \&cmd_quit,
				 help  => "Disconnect and quit the client",
				 usage => "",
			       },
		fileaccept  => { func  => \&cmd_file_accept,
				 help  => "Accept a file transfer",
				 usage => "<file session number>",
			       },
		filecancel  => { func  => \&cmd_file_cancel,
				 help  => "Cancel a file that is being transfered",
				 usage => "<file session number>",
			       },
		filelist    => { func  => \&cmd_file_list,
				 help  => "Print a list of file transfers",
				 usage => "",
			       },
		filereject  => { func  => \&cmd_file_reject,
				 help  => "Reject a file transfer",
				 usage => "<file session number>",
			       },
		filesend    => { func  => \&cmd_file_send,
				 help  => "Send a file to an user",
				 usage => "[ <user> | <window> ] <file path>",
			       },
		find        => { func  => \&cmd_find,
				 help  => "Find an user in hotmail member directory",
				 usage => "<first name> <last name> [ <country> <state> <city> ]",
			       },
		findinvite  => { func  => \&cmd_find_invite,
				 help  => "Send an invitation to an user found by /FIND",
				 usage => "<search result number> <message>",
			       },
		help        => { func  => \&cmd_help,
				 help  => "Print the help screen or help for a command",
				 usage => "[ <command> ]",
			       },
		history     => { func  => \&cmd_history,
				 help  => "Show or clear the command history",
				 usage => "[ CLEAR ]",
			       },
		hotmail     => { func  => \&cmd_hotmail,
				 help  => "Launch the browser and go to Hotmail page",
				 usage => "addressbook | compose [ <user> ] | folders | inbox | mobile",
			       },
		ignore      => { func  => \&cmd_ignore_user,
				 help  => "Add an user to the local ignore list",
				 usage => "<user1> [ <user2> ... <userN> ]",
			       },
		info        => { func  => \&cmd_info_user,
				 help  => "Show an info about an user in the contact list",
				 usage => "<user>",
			       },
		istatus     => { func  => \&cmd_initial_status,
				 help  => "Set the initial status (to be set after login)",
				 usage => "<initial status>",
			       },
		invite      => { func  => \&cmd_invite,
				 help  => "Invite another user to a chat session",
				 usage => "[ <window> ] <user>",
 			       },
		invitemsn   => { func  => \&cmd_invite_messenger,
				 help  => "Invite (with e-mail) someone to use MSN messenger",
				 usage => "<user>",
			       },
		killgroup   => { func  => \&cmd_remove_group_with_users,
				 help  => "Remove a group including all the users inside",
				 usage => "<group name>",
			       },
		l           => { func  => sub { print_contacts(list => 'forward_list') },
				 help  => "Print the full contact list",
				 usage => "",
			       },
		la          => { func  => sub { print_contacts(list => 'allow_list') },
				 help  => "Print the full allow list",
				 usage => "",
			       },
		lao         => { func  => sub { print_contacts(list => 'allow_list', online => 1) },
				 help  => "Print online contacts in the allow list",
				 usage => "",
			       },
		lb          => { func  => sub { print_contacts(list => 'block_list') },
				 help  => "Print the full block list",
				 usage => "",
			       },
		lbo         => { func  => sub { print_contacts(list => 'block_list', online => 1) },
				 help  => "Print online contacts in the block list",
				 usage => "",
			       },
		lg          => { func  => \&cmd_list_group,
				 help  => "Print a list of groups or users in group",
				 usage => "[ <group name> ]",
			       },
		lgo         => { func  => \&cmd_list_group_ol,
				 help  => "Print a list of groups or online users in group",
				 usage => "[ <group name> ]",
			       },
		li          => { func  => sub { print_contacts(ignore => 1) },
				 help  => "Print a list of locally ignored contacts",
				 usage => "",
			       },
		lio         => { func  => sub { print_contacts(ignore => 1, online => 1) },
				 help  => "Print a list of online ignored contacts",
				 usage => "",
			       },
		ll          => { func  => sub { print_contacts(locals => 1) },
				 help  => "Print a list of local contacts",
				 usage => "",
			       },
		lo          => { func  => sub { print_contacts(list => 'forward_list', online => 1) },
				 help  => "Print online contacts in the contact list",
				 usage => "",
			       },
		lr          => { func  => sub { print_contacts(list => 'reverse_list') },
				 help  => "Print the full reverse list",
				 usage => "",
			       },
		lro         => { func  => sub { print_contacts(list => 'reverse_list', online => 1) },
				 help  => "Print online contacts in the reverse list",
				 usage => "",
			       },
		move        => { func  => \&cmd_move_user,
				 help  => "Move an user to a different group",
				 usage => "<user> <new group>",
			       },
		msg         => { func  => \&cmd_message,
				 help  => "Send a message to an user or to a chat window",
				 usage => "<user> | <window>  <message>",
			       },
		msgexec     => { func  => \&cmd_message_exec,
				 help  => "Execute a command and message the output",
				 usage => "<user> <shell command>",
			       },
		nmaccept    => { func  => \&cmd_netmeeting_accept,
				 help  => "Accept a netmeeting/gnomemeeting invitation",
				 usage => "<netmeeting session>",
			       },
		nminvite    => { func  => \&cmd_netmeeting_invite,
				 help  => "Invite someone to use netmeeting/gnomemeeting",
				 usage => "[ <user> | <window> ]",
			       },
		nmreject    => { func  => \&cmd_netmeeting_reject,
				 help  => "Reject a netmeeting/gnomemeeting invitation",
				 usage => "<netmeeting session>",
			       },
		noaliascmd  => { func  => \&cmd_unalias_command_all,
				 help  => "Remove all the command aliases",
				 usage => "",
			       },
		noaliascn   => { func  => \&cmd_unalias_contact_all,
				 help  => "Remove all the contact aliases",
				 usage => "",
			       },
		noforward   => { func  => \&cmd_unforward_all,
				 help  => "Remove all the messages forwardings",
				 usage => "",
			       },
		pager       => { func  => \&cmd_pager,
				 help  => "Send a message to user's cellular phone",
				 usage => "<user> <message>",
			       },
		part        => { func  => \&cmd_part,
				 help  => "Part a chat session",
				 usage => "[ <window> ]",
			       },
		partall     => { func  => \&cmd_part_all,
				 help  => "Part all the established chat sessions",
				 usage => "",
			       },
		phone       => { func  => \&cmd_phone,
				 help  => "Print (update) your phone numbers",
				 usage => "home | mobile | work [ <new number> ]",
			       },
		profile     => { func  => \&cmd_profile,
				 help  => "Launch the browser and go to profile page",
				 usage => "[ <user> ]",
			       },
		quit        => { func  => \&cmd_quit,
				 help  => "Disconnect and quit the client",
				 usage => "",
			       },
		reconnect   => { func  => \&cmd_reconnect,
				 help  => "Disconnect and then immediately reconnect",
				 usage => "",
			       },
		remgroup    => { func  => \&cmd_remove_group,
				 help  => "Remove a group and move users to another one",
				 usage => "<group name>",
			       },
		remuser     => { func  => \&cmd_remove_user,
				 help  => "Remove an user from the contact list",
				 usage => "<user> [ <group> ]",
			       },
		rengroup    => { func  => \&cmd_rename_group,
				 help  => "Rename a group",
				 usage => "<group name> <new name>",
			       },
		save        => { func  => \&cmd_save,
				 help  => "Write the settings to the configuration file",
				 usage => "[ <file path> ]",
			       },
		search      => { func  => \&cmd_search_contact,
				 help  => "Launch the browser and search for a contact",
				 usage => "",
			       },
		searchintr  => { func  => \&cmd_search_contact_interest,
				 help  => "Launch the browser and search by an interest",
				 usage => "",
			       },
		set         => { func  => \&cmd_set,
				 help  => "Set a configuration option",
				 usage => "<option> <new value>",
			       },
		setpwd      => { func  => \&cmd_set_pwd,
				 help  => "Set your login password",
				 usage => "<your password>",
			       },
		setfname    => { func  => \&cmd_set_fname,
				 help  => "Change your friendly user name",
				 usage => "<new screen name>",
			       },
		setport     => { func  => \&cmd_set_port,
				 help  => "Set the server port to connect to",
				 usage => "<port number>",
			       },
		setserver   => { func  => \&cmd_set_server,
				 help  => "Set the login (dispatch) server",
				 usage => "<server address>",
			       },
		setuser     => { func  => \&cmd_set_user,
				 help  => "Set your user passport (E-mail address)",
				 usage => "[ <your passport> ]",
			       },
		sockets     => { func  => \&cmd_sockets,
				 help  => "Print a list all connected sockets",
				 usage => "",
			       },
		status      => { func  => \&cmd_status,
				 help  => "Change your status",
				 usage => "<status> | help",
			       },
		unaliascmd  => { func  => \&cmd_unalias_command,
				 help  => "Remove a command alias",
				 usage => "<command alias>",
			       },
		unaliascn   => { func  => \&cmd_unalias_contact,
				 help  => "Remove a contact alias",
				 usage => "<contact alias>",
			       },
		unallow     => { func  => \&cmd_unallow_user,
				 help  => "Remove an user from the allow list",
				 usage => "<user> [ <user2> ... <userN> ]",
			       },
		unblock     => { func  => \&cmd_unblock_user,
				 help  => "Unblock an user",
				 usage => "<user> [ <user2> ... <userN> ]",
			       },
		unforward   => { func  => \&cmd_unforward,
				 help  => "Remove messages forwarding",
				 usage => "<user> | <window>",
			       },
		unignore    => { func  => \&cmd_unignore_user,
				 help  => "Remove an user from the local ignore list",
				 usage => "<user> [ <user2> ... <userN> ]",
			       },
		uptime      => { func  => \&cmd_uptime,
				 help  => "Show the uptime",
				 usage => "",
			       },
		url         => { func  => \&cmd_url,
				 help  => "View, save, clear grabbed URLs",
				 usage => "",
			       },
		window      => { func  => \&cmd_window,
				 help  => "Manipulate the windows",
				 usage => "[ Various commands (/WINDOW HELP) | <number> ]",
			       },
	};

	$rh->{clr}->{color_theme} =                                         # Color Themes
	{
		1 => {                                                      # ===== 1 =====
			title_background       => 'blue',
			title_text             => 'white bold',

			status_background      => 'black',
			status_text1           => 'cyan',
			status_text2           => 'cyan bold',
			status_text_bracket    => 'red',

			status2_background     => 'black',
			status2_text1          => 'cyan',
			status2_text2          => 'cyan bold',
			status2_text_bracket   => 'red',

			contact_att_alias      => 'green bold',
			contact_att_blocked    => 'red',
			contact_att_ignored    => 'red',
			contact_att_mobile     => 'green bold',

			contact_group          => 'blue bold',
			contact_status         => 'white',
			contact_fname          => 'cyan bold',
			contact_passport       => 'cyan',

			contact_norev_status   => 'white',
			contact_norev_fname    => 'red bold',
			contact_norev_passport => 'red',

			contact_local_status   => 'white',
			contact_local_fname    => 'green bold',
			contact_local_passport => 'green',

			prompt_bracket         => 'red',
			prompt_text            => 'blue bold',

			text_bracket           => 'black bold',
			text_error             => 'red bold',
			text_login             => 'red bold',
			text_msnre             => 'red bold',
			text_srv_message       => 'red bold',
			text_timestamp         => 'red',
			text_highlight         => 'white bold',

			text_fname             => 'cyan bold',
			text_passport          => 'cyan',

			header_bracket         => 'white',
			header_line            => 'blue bold',
			header_text            => 'cyan',
			header_text_first      => 'cyan bold',

			message_user_own       => 'yellow bold',
			message_user           => 'red bold',

			message_h_bold         => 'blue bold',
			message_h_email        => 'red bold',
			message_h_exclamations => 'red',
			message_h_smiley       => 'yellow bold',
			message_h_url          => 'white bold',

			message_m_incoming     => 'blue bold',
			message_m_outgoing     => 'white',
			message_m_sent         => 'black bold',
			message_m_undelivered  => 'red',
		},
		2 => {                                                      # ===== 2 =====
			title_background       => 'blue',
			title_text             => 'white bold',

			status_background      => 'blue',
			status_text1           => 'white',
			status_text2           => 'white bold',
			status_text_bracket    => 'cyan',

			status2_background     => 'blue',
			status2_text1          => 'white',
			status2_text2          => 'white bold',
			status2_text_bracket   => 'cyan',

			contact_att_alias      => 'green bold',
			contact_att_blocked    => 'red',
			contact_att_ignored    => 'red',
			contact_att_mobile     => 'green bold',

			contact_group          => 'blue bold',
			contact_status         => 'white',
			contact_fname          => 'cyan bold',
			contact_passport       => 'cyan',

			contact_norev_status   => 'white',
			contact_norev_fname    => 'red bold',
			contact_norev_passport => 'red',

			contact_local_status   => 'white',
			contact_local_fname    => 'green bold',
			contact_local_passport => 'green',

			prompt_bracket         => 'white',
			prompt_text            => 'white',

			text_bracket           => 'white bold',
			text_error             => 'green bold',
			text_login             => 'green bold',
			text_msnre             => 'green bold',
			text_srv_message       => 'green bold',
			text_timestamp         => 'green',
			text_highlight         => 'white bold',

			text_fname             => 'cyan bold',
			text_passport          => 'cyan',

			header_bracket         => 'green',
			header_line            => 'green bold',
			header_text            => 'white bold',
			header_text_first      => 'red bold',

			message_user_own       => 'blue bold',
			message_user           => 'blue',

			message_h_bold         => 'white bold',
			message_h_email        => 'white bold',
			message_h_exclamations => 'cyan',
			message_h_smiley       => 'cyan bold',
			message_h_url          => 'white bold',

			message_m_incoming     => 'white',
			message_m_outgoing     => 'white',
			message_m_sent         => 'black bold',
			message_m_undelivered  => 'red',
		},
		3 => {                                                      # ===== 3 =====
			title_background       => 'black',
			title_text             => 'white bold',

			status_background      => 'black',
			status_text1           => 'white',
			status_text2           => 'white bold',
			status_text_bracket    => 'black bold',

			status2_background     => 'black',
			status2_text1          => 'white',
			status2_text2          => 'white bold',
			status2_text_bracket   => 'black bold',

			contact_att_alias      => 'black bold',
			contact_att_blocked    => 'black bold',
			contact_att_ignored    => 'black bold',
			contact_att_mobile     => 'black bold',

			contact_group          => 'blue bold',
			contact_status         => 'white bold',
			contact_fname          => 'white',
			contact_passport       => 'black bold',

			contact_norev_status   => 'white bold',
			contact_norev_fname    => 'red bold',
			contact_norev_passport => 'red',

			contact_local_status   => 'white bold',
			contact_local_fname    => 'cyan bold',
			contact_local_passport => 'cyan',

			prompt_bracket         => 'black bold',
			prompt_text            => 'white bold',

			text_bracket           => 'black bold',
			text_error             => 'white bold',
			text_login             => 'white bold',
			text_msnre             => 'white bold',
			text_srv_message       => 'white bold',
			text_timestamp         => 'black bold',
			text_highlight         => 'white bold',

			text_fname             => 'white',
			text_passport          => 'black bold',

			header_bracket         => 'cyan bold',
			header_line            => 'white bold',
			header_text            => 'cyan',
			header_text_first      => 'cyan bold',

			message_user_own       => 'white bold',
			message_user           => 'white',

			message_h_bold         => 'white bold',
			message_h_email        => 'white bold',
			message_h_exclamations => 'white bold',
			message_h_smiley       => 'white bold',
			message_h_url          => 'white bold',

			message_m_incoming     => 'cyan bold',
			message_m_outgoing     => 'white',
			message_m_sent         => 'black bold',
			message_m_undelivered  => 'red',
		},
	};

	$colors =       # Classic colors
	{
		g => COLOR_GREEN,   b => COLOR_BLUE,   c => COLOR_CYAN,   r => COLOR_RED,
		m => COLOR_MAGENTA, y => COLOR_YELLOW, w => COLOR_WHITE,  k => COLOR_BLACK,
	};

	$msn = Net::MsnMessenger->new;
	$st_time = time;

	# --- For a better debugging experience set this to 1

	$rh->{debug}->{extra} = 0;

	if ($rh->{debug}->{extra})
	{
		require diagnostics;
		$rh->{debug}->{debug_log_dir} = "/home/incoming/work/msnre_temp/debug";
		$msn->testing(1);

		push @{$rh->{callbacks}}, qw(RA_ACCEPT RA_CANCEL RA_INVITED RA_REJECT VOICE_ACCEPT
					     VOICE_CANCEL VOICE_REJECT);
	}
}

# -------------------- POE Loop events -------------------- #

# &MSNre::poe_start_client ()
# ----------------------------------------------------------------------
# Initial POE Loop.

sub poe_start_client
{
	my $kernel = $_[KERNEL];

	$rh->{config}->{env_dir} = "$ENV{HOME}/.msnre";                          # Defaults - never changed
	$rh->{config}->{config_file} = "$rh->{config}->{env_dir}/msnre.conf";
	$rh->{config}->{contacts_path} = "$rh->{config}->{env_dir}/contacts";

	my $stderr = Symbol::gensym;     # Pipe STDERR to the debugging window if debugging is enabled,
	pipe ($stderr, STDERR);          # otherwise don't print anything.

	$rh->{w}->{stderr} = POE::Wheel::ReadWrite->new(
						Handle     => $stderr,
						Filter     => POE::Filter::Line->new,
						Driver     => POE::Driver::SysRW->new,
						InputEvent => 'output_stderr',
	);

	initscr();                       # Initialize the curses

	if (has_colors)
	{
		$rh->{misc}->{use_colors} = 1;
		start_color();
	}
	getmaxyx(stdscr, $Rows, $Columns);

	$rh->{w}->{console} = POE::Wheel::Curses->new(InputEvent => 'input_read');
	$rh->{w}->{input}   = newwin(1, 0, $Rows - 1, 0);

	if ($rh->{misc}->{use_colors})
	{
		for (keys %{$colors})           # Initial color pair (basic colors)
		{
			init_pair(++$rh->{clr}->{min_cpair}, $colors->{$_}, $colors->{k});
			$rh->{clr}->{color}->{lc $_} = COLOR_PAIR($rh->{clr}->{min_cpair}) | A_NORMAL;
			$rh->{clr}->{color}->{uc $_} = COLOR_PAIR($rh->{clr}->{min_cpair}) | A_BOLD;
		}
	}

	read_config();                  # Read the configuration
	register_bindings();            # Prepare the key bindings

	$rh->{misc}->{use_colors} = undef if !config_lookup('use_colors');

	$rh->{w}->{input_v}->{position}         = 0;         # Current cursor position
	$rh->{w}->{input_v}->{overwrite}        = 0;         # Overwrite enabled 1/0
	$rh->{w}->{input_v}->{history_position} = -1;        # Current position in the history

	# Associate the callbacks to client functions
	$msn->add_callback($_, \&{"cb_".lc($_)}) for @{$rh->{callbacks}};

	window_title_update();
	window_status_update();
	window_input_update();

	$kernel->yield($_) for 'status_update', 'main_loop', 'timers';   # Initial loops

	eval { require Time::HiRes };
	if (!$@)
	{
		$rh->{time_hires} = 1;
		msnre_debug("Using Time::HiRes for high time resolution");
	}
	$SIG{WINCH} = \&resize_window;
	$SIG{INT}   = \&cmd_quit;

	if (config_lookup('auto_connect'))
	{
		cmd_connect();
	}
	else
	{
		print_event(1, "Auto-connect option is not set. Use the <%text_highlight>/CONNECT<%n> command " .
			    "to connect to the server.\n");
	}
	1;
}

# &MSNre::poe_close_client ()
# ----------------------------------------------------------------------
# Handle the client exit. Disconnect all the connections and correctly quit.

sub poe_close_client
{
	contacts_write();
	disconnect_msn() if $msn->connected;

	if ($rh->{fhs})             # Close all the possibly opened file handles
	{
		$rh->{fhs}->{$_}->close for
		    grep {defined $rh->{fhs}->{$_} && defined fileno($rh->{fhs}->{$_})} keys %{$rh->{fhs}};
	}
	if (!$rh->{w}->{main_v}->[$rh->{w}->{current}]->{scrolled})
	{
		print_event($rh->{w}->{current}, "\n" . "Signon time  :\t" . scalar localtime($st_time));
		print_event($rh->{w}->{current},        "Signoff time :\t" . scalar localtime(time));
		print_event($rh->{w}->{current},        "Total uptime :\t" . get_time(time - $st_time));
		window_main_update();
	}
	if ($rh->{fhs_to_del})      # Files to be deleted (hotmail pages)
	{
		unlink $_ for grep {-f $_} @{$rh->{fhs_to_del}};
	}
	$rh->{debug}->{debug_log_fh}->close if
	    defined $rh->{debug}->{debug_log_fh} && defined fileno $rh->{debug}->{debug_log_fh};

	curs_set(1);                # Restore the defaults
	endwin;

	$SIG{INT}   = 'DEFAULT';
	$SIG{WINCH} = 'DEFAULT';
	1;
}

# &MSNre::poe_main_loop ()
# ----------------------------------------------------------------------
# Main program loop. Watch for incoming data on the sockets and print protocol error messages
# if any errors occur.

sub poe_main_loop
{
	my $kernel = $_[KERNEL];
	my $update;

	if ($rh->{w}->{main_v}->[$rh->{w}->{current}]->{update})   # Update the main window
	{
		window_main_update();
		$rh->{w}->{main_v}->[$rh->{w}->{current}]->{update} = undef;
	}
	$msn->do_one_loop if $msn->connected;

	if ($msn->error)                                           # Protocol error
	{
		print_event(1, "<%text_error>[Error]<%n> " . $msn->get_error);

		if ($rh->{misc}->{connecting} && !$msn->connected)
		{
			if ($rh->{misc}->{connecting_lattemp} && $rh->{misc}->{connecting_lattemp} == 4)
			{
				print_event($rh->{w}->{current}, "Couldn't connect after 5 attems. Giving up.\n");
			}
			elsif (config_lookup('auto_reconnect'))    # Probably a connection error
			{
				$rh->{misc}->{connecting} = undef;
				$rh->{misc}->{connecting_lattemp}++;
				cmd_connect();
			}
		}
	}
	$kernel->yield('main_loop');
	1;
}

# &MSNre::poe_main_loop_timers ()
# ----------------------------------------------------------------------
# Handle the timers. It runs every one second and watches the timers - mostly for timeouts.

sub poe_main_loop_timers
{
	my $kernel = $_[KERNEL];

	if (defined $rh->{timers}->{idle} && defined $rh->{timers}->{idle}->{start})  # Idle timer
	{
		if (!$msn->connected || $msn->status ne 'online')                     # This shouldn't happen
		{
			$rh->{timers}->{idle}->{start} = undef;
		}

		if (defined $rh->{timers}->{idle}->{start} &&
		    (time - $rh->{timers}->{idle}->{start} >= config_lookup('auto_idle_time')))
		{
			$rh->{timers}->{idle}->{start} = undef;
			$rh->{timers}->{idle}->{setting} = 'idle';

			print_event(1, "\nAuto-changing the status to <%text_highlight>Idle<%n>.");
			$msn->change_status('idle');
		}
	}

	if ($rh->{timers}->{typing})  # Typing User timers
	{
		for (my $i = 0; $i < @{$rh->{timers}->{typing}}; $i++)
		{
			if (defined $rh->{timers}->{typing}->[$i]->{typing_start} &&
			    (time - $rh->{timers}->{typing}->[$i]->{typing_start}) >= 10)
			{
				$rh->{sessions}->[$i]->{typing} = undef;
				$rh->{timers}->{typing}->[$i]->{typing_start} = undef;

				# Print the User is typing message to the session window's title bar
				window_title_update($rh->{sessions}->[$i]->{window});
			}
		}
	}

	if ($rh->{s_establishing} && config_lookup('chat_timeout'))   # Switchboard timers
	{
		my $c_timeout = config_lookup('chat_timeout');

		for (my $i = 0; $i < @{$rh->{s_establishing}}; $i++)
		{
			next if !defined $rh->{s_establishing}->[$i]->{start} ||
			    (time - $rh->{s_establishing}->[$i]->{start} < $c_timeout);

			# Window to print to
			my $win = find_chat_with_user($rh->{s_establishing}->[$i]->{user});
			$win = undef if !defined $rh->{main}->[$win];

			(defined $win)
			    ? print_event($win, "New connection timed out ($c_timeout seconds). Disconnected.")
			    : print_event(1,
					"New chat session for <%text_highlight>$rh->{s_establishing}->[$i]->{user}".
					"<%n> timed out (20 seconds).");

			# Remove the queue for this user
			for (my $j = 0; $j < @{$rh->{s_queue}}; $j++)
			{
				splice @{$rh->{s_queue}}, $j--, 1 if $rh->{s_queue}->[$j]->{user} eq
				    $rh->{s_establishing}->[$i]->{user};
			}

			$msn->disconnect_swb_pending($rh->{s_establishing}->[$i]->{user});
			splice @{$rh->{s_establishing}}, $i--, 1;
		}
	}

	if ($rh->{timers}->{file} && config_lookup('ft_timeout'))  # File transfer timers
	{
		for (my $i = 0; $i < @{$rh->{timers}->{file}}; $i++)
		{
			next if !defined $rh->{timers}->{file}->[$i];

			my $f_hash = file_find_ref_by_session($i);
			next if !$f_hash->{active};

			my $last_pkt = $msn->get_last_packet_file($f_hash->{swb_session}, $f_hash->{file_session});
			my $last_pkt_real = $last_pkt;

			$last_pkt = $rh->{timers}->{file}->[$i] if
			    !$last_pkt ||                                # Connecting
			    $last_pkt == -1;                             # Pending (not connecting yet)

			next if (time - $last_pkt) < config_lookup('ft_timeout');

			$f_hash->{status} = "Canceled (Timeout)";
			my $reason = "File transfer timed out (".config_lookup('ft_timeout')." seconds)";

			if ($f_hash->{type} eq 'outgoing' && defined $last_pkt_real && $last_pkt_real > 0)
			{
				# This should be safe beacuse we are sending the file continuously and then
				# it can timeout when waiting for client's bye, so catch this

				$msn->file_leave_timeout($f_hash->{swb_session}, $f_hash->{file_session});
			}
			else    # Be sure it gets successfully canceled on the other side
			{
				$msn->file_cancel($f_hash->{swb_session}, $f_hash->{file_session});
			}
			disconnect_file($f_hash->{session});

			($f_hash->{type} eq 'incoming')
			    ? cb_file_receive_cancel($f_hash->{swb_session}, $f_hash->{file_session}, $reason)
			    : cb_file_send_cancel($f_hash->{swb_session}, $f_hash->{file_session}, $reason);
		}
	}

	if ($rh->{msg_pending})
	{
		for (my $i = 0; $i < @{$rh->{msg_pending}}; $i++)
		{
			next if time - $rh->{msg_pending}->[$i]->{time} <= 5;

			window_main_create_chat($rh->{msg_pending}->[$i]->{session}) if
			    need_window_chat($rh->{msg_pending}->[$i]->{session});

			print_event_message($rh->{msg_pending}->[$i]->{session}, $msn->passport,
					    $rh->{msg_pending}->[$i]->{msg}, -1);

			splice @{$rh->{msg_pending}}, $i--, 1;
		}
	}

	if (defined $rh->{timers}->{ping}->{sent})   # Ping sent and waiting for a response
	{
		my $p_timeout = config_lookup('ping_timeout');

		if ($msn->last_packet > $rh->{timers}->{ping}->{sent})
		{
			# The response doesn't seem to be sent if some other packet arrives after sending
			# the ping

			$rh->{timers}->{sent} = undef;

			$rh->{timers}->{lag_current} = undef;
			$rh->{timers}->{lag_last} = 0;

			window_status_update() if $rh->{w}->{main_v}->[$rh->{w}->{current}]->{is_double};
		}

		elsif (defined $p_timeout)
		{
			if (time - $rh->{timers}->{ping}->{sent} >= $p_timeout)
			{
				print_event(1, "Ping timed out ($p_timeout seconds). Disconnected.");
				cb_disconnect_forced();
			}
			else    # Current lag
			{
				$rh->{timers}->{ping}->{lag_current} = time - $rh->{timers}->{ping}->{sent};
				$rh->{timers}->{ping}->{lag_last} = undef;

				window_status_update() if $rh->{w}->{main_v}->[$rh->{w}->{current}]->{is_double};
			}
		}
	}

	elsif ($msn->signed_in && time - $msn->last_packet >= 60)  # Nothing sent last 60 seconds, ping the server
	{
		$rh->{timers}->{ping}->{sent} = time;
		$rh->{timers}->{ping}->{lag_current} = 0;
		$rh->{timers}->{ping}->{lag_last} = undef;
		$msn->ping;
	}

	if (!$msn->connected && defined $rh->{timers}->{reconnect})         # Reconnecting
	{
		cmd_connect() if time - $rh->{timers}->{reconnect} >= 10;   # Reconnect after 10 seconds timeout
	}

	$kernel->delay('timers', 1);
	1;
}

# &MSNre::poe_status_update ()
# ----------------------------------------------------------------------
# Update the status bar every 60 seconds.

sub poe_status_update
{
	my $kernel = $_[KERNEL];

	window_status_update();

	# Schedule another status bars update to the next minute
	$kernel->delay(status_update => 60 - sprintf strftime("%S", localtime));
	1;
}

# ---------- Print functions ---------- #

# &MSNre::print_align ( TEXT )
# ----------------------------------------------------------------------
# Pre-align the text before printing to the screen.

# XXX: TODO: FIXME:
# Rewrite this one, add a better support for displaying UTF-8, make it faster.
# Text::WrapI18N seemed fine with this, it could be used here but I don't want the dependencies
# list to grow. Also it must not count with the colors tags ( <%some_color> ).

sub print_align
{
	my $message = shift;
	return () if !defined $message;
	chomp $message while $message =~ /\n\n$/;     # Remove unwanted extra newlines

	my $max_len       = $Columns - real_length(get_timestamp()) - 1;
	my $t_line_length = 0;
	my @t_message     = split /(<(?!<)%\S+?>)/s, $message;
	my @line_to_print = "";

	for (my $i = 0; $i < @t_message; $i++)
	{
		splice @t_message, $i--, 1 if length($t_message[$i]) == 0;

		if ($t_message[$i] =~ /.\n./)         # Split the newlines
		{
			splice @t_message, $i, 1, ($t_message[$i] =~ /^(.[^\n]+?(?:\n+|$))/msg)
		}
	}

	for (@t_message)
	{
		if (/<\%(\S+?)>/)                                 # Do not add a color tag to the length
		{
			$line_to_print[$#line_to_print] .= $_;
			next;
		}
		my $t_c_line_print = $max_len - $t_line_length;   # Space left to print on the current line

		if ($t_c_line_print <= 0)
		{
			$line_to_print[$#line_to_print] .= "\n";    $t_line_length = 0;
			push @line_to_print, "";
		}

		if (/^(.[^\n]+)\n+$/ && real_length($1) < $t_c_line_print)
		{
			$line_to_print[$#line_to_print] .= $_;      $t_line_length = 0;

			push @line_to_print, "";
			next;
		}

		if (s/^(.{$t_c_line_print})//)
		{
			my $to_add = $1;
			my $extra = $2 if $to_add =~ s/(^|\s)(\S+)$/$1/;

			$line_to_print[$#line_to_print] .= $to_add;  $t_line_length += real_length($to_add);

			if (defined $extra)                           # The word won't fit on the current line
			{
				if (s/^(\S+)//)
				{
					if (real_length($extra.$1) > $max_len)  # The string is longer than 1 line
					{
						$line_to_print[$#line_to_print] .= $extra . "\n";
						$t_line_length = 0;
						push @line_to_print, "";

						$_ .= $1;  redo;
					}
					else { $extra .= $1 }
				}

				$line_to_print[$#line_to_print] .= "\n";   $t_line_length = real_length($extra);
				push @line_to_print, $extra;
			}
			redo if length($_);
		}
		else
		{
			if (real_length($_) > $t_c_line_print && real_length($_) <= $max_len)
			{
				push @line_to_print, $_;

				$t_line_length = real_length($_);
				next;
			}
			$line_to_print[$#line_to_print] .= $_;
			$t_line_length += real_length($_);
		}
	}

	return @line_to_print;
}


# &MSNre::print_commands ( @COMMANDS )
# ----------------------------------------------------------------------
# Print a list of all the MSNre commands uncluding the help.

sub print_commands
{
	my @commands = @_;
	my $max_len  = 0;
	my $number   = 0;

	for (@commands) { $max_len = length($_) if length($_) > $max_len }
	print_event($rh->{w}->{current}, "<%text_highlight>$NAME<%n>: Commands list:");

	for (@commands)
	{
		$number++;
		print_event($rh->{w}->{current}, " " x 3 .
			    "/" . uc($_) . ' ' x ($max_len - length($_)).'  - '.$rh->{command}->{lc($_)}->{help});

		window_status_update($rh->{w}->{current}) if $number % 8;
	}
	print_window($rh->{w}->{current}, "\n");
	1;
}

# &MSNre::print_contacts ( %ARGS )
# ----------------------------------------------------------------------
# Print the contact list (a requested part of the contact list).

sub print_contacts
{
	check_connection() || return undef;
	my %args   = @_;
	my $w      = (defined $args{window}) ? $args{window} : $rh->{w}->{current};
	my @t_cont = ();

	@t_cont = sort $msn->get_users_list(lc $args{list}) if defined $args{list};

	if (!defined $args{list} && $args{iln})    # Initial status of users
	{
		if ($rh->{misc}->{login_iln})
		{
			while (my $iln = shift @{$rh->{misc}->{login_iln}})
			{
				my $user = $msn->get_user($iln);

				# It is possible the a user goes offline after ILN but before printing the initial
				# contact list.
				push @t_cont, $user if defined $user && $user->status ne 'offline';
			}
		}
	}

	if (defined $args{group})                  # Users in a group
	{
		my @new_t_cont = ();
		for my $c(@t_cont)
		{
			push @new_t_cont, $c if grep {$_ eq $args{group}} @{$c->{group}};
		}
		@t_cont = @new_t_cont;
	}

	if ($args{online})                        # Online contacts
	{
		@t_cont = grep {$_->{connected}} @t_cont;
	}

	if ($args{ignore})                        # Ignored contacts
	{
		push @t_cont, sort {$a->{passport} cmp $b->{passport}} contacts_get_ignored();
	}

	if ($args{locals})                        # Local contacts
	{
		@t_cont = sort {$a->{passport} cmp $b->{passport}} contacts_get_locals();
	}

	# ---------- Sort the contact lists

	my ($t_list, $row_name, @presort, @contacts);

	# For the forward list
	if ($args{iln} || (defined $args{list} && $args{list} eq 'forward_list'))
	{
		for my $c(@t_cont)
		{
			my @groups = (defined $args{group}) ? $args{group} : @{$c->group};

			for my $group(@groups)
			{
				# Create a new Contact object with only the neccessary values and the current group

				my $t_group = utf8_l($msn->url_decode($msn->get_group($group)->name));

				# This should be autochanges, if it is isn't yet change it here
				$t_group = 'Other contacts' if $t_group eq '~';

				my $contact = Net::MsnMessenger::Contact->new(passport => $c->passport,
					fname => contacts_get_fname($c->passport), status => $c->status,
					group => $t_group, mobile_device => $c->mobile_device);

				$contact->{group_id} = $group;
				$contact->add_to_list($_) for @{$c->{c_list}};

				push @{$t_list->{$group}}, $contact;     # Complete contact
			}
		}

		# Sort the groups by the name
		@presort = sort {uc(utf8_l($msn->url_decode($msn->get_group($a)->{name}))) cmp
				 uc(utf8_l($msn->url_decode($msn->get_group($b)->{name})))} keys %{$t_list};
	}
	else
	{
		for my $c(@t_cont)
		{
			# Not printing the forward list, ignore the group
			my $r_user = $msn->get_user($c->{passport});
			my $status = (defined $r_user) ? $r_user->status : 'Unknown';

			my $contact = Net::MsnMessenger::Contact->new(passport => $c->{passport},
				fname => contacts_get_fname($c->{passport}), status => $status);

			$contact->{local} = 1 if $args{locals};

			if (defined $r_user)
			{
				my @t_c_list = ($args{ignore}) ? @{$r_user->c_list}
				    : grep {$_ ne 'forward_list'} @{$r_user->c_list};

				$contact->add_to_list($_) for @t_c_list;
			}
			push @{$t_list->{none}}, $contact;   # Dummy group to not do the same thing twice
		}
		@presort = 'none';
	}
	@t_cont = ();

	if (defined $t_list)
	{
		for my $group(@presort)           # Sort the contacts in their groups
		{
			push @t_cont,
			  sort {uc(contacts_get_fname($a->{passport})) cmp uc(contacts_get_fname($b->{passport}))}
			  grep {$_->{status} ne 'offline'} sort {$a->{status} cmp $b->{status}} @{$t_list->{$group}};

			push @t_cont,
			  sort {uc(contacts_get_fname($a->{passport})) cmp uc(contacts_get_fname($b->{passport}))}
			  grep {$_->{status} eq 'offline'} @{$t_list->{$group}};
		}
	}

	for my $c(@t_cont)                             # Add the aliases and complete the contact list
	{
		push @contacts, $c;

		if ($args{iln} || (defined $args{list} && $args{list} eq 'forward_list'))
		{
			for my $alias(alias_get_user($c->passport))
			{
				my $group = utf8_l($msn->url_decode($msn->get_group($c->{group_id})->name));
				$group = 'Other contacts' if $group eq '~';
	
				my $contact =
				    Net::MsnMessenger::Contact->new(passport => $c->passport, fname => $alias,
					status => $c->status, group => $group, mobile_device => $c->mobile_device);

				if ($c->c_list)
				{
					$contact->add_to_list($_) for @{$c->c_list};
				}
				$contact->{alias} = 1;
				push @contacts, $contact;
			}
		}
	}

	if (!$args{online} && !defined $args{group} && defined $args{list} && $args{list} eq 'forward_list')
	{
		for my $loc(contacts_get_locals())   # Print also the local contacts
		{
			my $contact = Net::MsnMessenger::Contact->new(passport => $loc->{passport},
				fname => $loc->{fname}, status => 'Unknown');

			$contact->{local} = 1;
			push @contacts, $contact;
		}
	}

	# The name on the top of the row
	# Prefer a group name (for /LG and /LGO)
	# Then an initial status and the name of the contact list

	if (defined $args{group})
	{
		$row_name = utf8_l($msn->url_decode($msn->get_group($args{group})->name));
	}
	elsif ($args{iln})
	{
		$row_name = 'Initial Status';
	}
	elsif ($args{ignore})
	{
		$row_name = 'Ignore List';
	}
	elsif ($args{locals})
	{
		$row_name = 'Local contacts';
	}

	if (!defined $row_name)
	{
		$args{list} = 'Contact List' if $args{list} eq 'forward_list';
		$args{list} =~ s/_/ /g;  $args{list} =~ s/(\s)(\S)/$1 . ucfirst($2)/eg;

		$row_name = ucfirst $args{list};
	}
	$row_name .= ' (Online users)' if $args{online} || $args{iln};

	print_event($w, "\n" . print_line($row_name));

	if (@contacts)
	{
		my ($contact_num, $max_st, $local_start, $act_group) = (0, 0, 0, undef);

		# Maximal length of a status
		for (@contacts) { $max_st = length($_->status) if length($_->status) > $max_st }

		for my $c(@contacts)
		{
			my ($f_color, $p_color);

			if ($w == $rh->{w}->{current})                                  # Update the main window
			{
				window_main_update($w) if $contact_num % $rh->{w}->{main_v}->[$w]->{screen_size};
				$contact_num++;
			}

			if (defined $c->{group} && defined $c->group->[0] &&            # Print the group name
			    (!defined $act_group || $act_group ne $c->group->[0]))
			{
				print_event($w, "<%contact_group>" . $c->group->[0]);
				$act_group = $c->group->[0];
			}

			elsif ($c->{local} && !$contacts[0]->{local} && !$local_start)  # Separate local contacts
			{
				print_event($w, "");    # Empty line with a timestamp
				$local_start = 1;
			}

			my $c_user    = contacts_get_contact($c->passport);
			my $fname     = $c->fname;
			my $fname_len = real_length($fname);
			my $status    = ucfirst $c->status;  $status =~ s/_/ /g;

			my $alias     = ($c->{alias})                       ? "<%contact_att_alias>A"   : " ";
			my $blocked   = ($c->is_user_in_list('block_list')) ? "<%contact_att_blocked>B" : " ";
			my $ignored   = ($c_user->{ignore})                 ? "<%contact_att_ignored>I" : " ";
			my $mobile    = (defined $c->mobile_device && $c->mobile_device eq 'Y')
			    ? "<%contact_att_mobile>M" : " ";

			my $max_s_len = ($fname_len+length($c->passport.$status)) + ($max_st-length($status) +
				real_length(get_timestamp())) + 17;    

			# Choose the color depending on the list the user is in
			if ($c->{local})
			{
				$f_color = "<%contact_local_fname>";     $p_color = "<%contact_local_passport>";
			}
			elsif ($c->is_user_in_list('reverse_list'))
			{
				$f_color = "<%contact_fname>";           $p_color = "<%contact_passport>";
			}
			else
			{
				$f_color = "<%contact_norev_fname>";     $p_color = "<%contact_norev_passport>";
			}

			# Truncate the length of the friendly name so everything fits on a signle line
			if ($max_s_len > $Columns - 1)
			{
				truncate_name(\$fname, $fname_len - ($max_s_len - $Columns - 1));
			}

			print_event($w, "$alias $blocked $ignored $mobile <%text_bracket>[<%contact_status>$status".
				    " " x ($max_st - length($status)).
				    "<%text_bracket>] $f_color$fname $p_color<" . $c->passport . ">");
		}
	}
	else
	{
		print_event($w, "<%K>No contacts available");
	}

	print_window($w, "\n");
	1;
}

# &MSNre::print_event ( WINDOW_ID, MESSAGE [, NO_COLORS_FLAG ] )
# ----------------------------------------------------------------------
# Print text to a window with a timestamp including tabs expand and word wrap.

sub print_event
{
	my ($window_id, $message, $no_colors) = @_;
	require Text::Tabs;

	my $timestamp     = get_timestamp();
	my $max_len       = $Columns - real_length($timestamp) - 1;
	my $t_line_length = 0;

	$message =~ s/(\r+)?\n/\n/g;
	$message = fix_colors(Text::Tabs::expand($message));

	if (@{$rh->{w}->{main_v}->[$window_id]->{lines}}==1 && !length($rh->{w}->{main_v}->[$window_id]->{lines}->[0]))
	{
		# The window is still empty - there is no text inside of it. If the current text begins with
		# newlines, do not print them.
		$message =~ s/^(\n+)//;
	}

	if ($message =~ s/^(\n+)// && (!defined $rh->{w}->{main_v}->[$window_id]->{to_print} ||
				       ($rh->{w}->{main_v}->[$window_id]->{to_print} !~ /\n\n$/)))
	{
		# Do not allow more than two newlines together in a text. Always print only one newline if there are
		# not 2 of them in the previous text.
		print_window($window_id, "\n");
	}
	my @lines = print_align($message);

	print_window($window_id, fix_colors("<%n><%text_timestamp>$timestamp<%n>"));  # Initial timestamp

	($no_colors)
	    ? print_window_no_colors($window_id, shift @lines)
	    : print_window($window_id, shift @lines);

	map {s/^(\s*)/$1 . " " x real_length($timestamp)/e if defined $_ && length($_)} @lines;

	for (@lines)
	{
		($no_colors)
		    ? print_window_no_colors($window_id, $_)
		    : print_window($window_id, $_);
	}
	print_window($window_id, "<%n>") if $window_id == 0;
	print_window($window_id, "\n");

	$rh->{w}->{main_v}->[$window_id]->{update}++;
	$rh->{w}->{main}->[0]->standend if $window_id == 0;
	1;
}

# &MSNre::print_event_message ( SWB_SESSION, SENDER, MESSAGE, DELIVERED )
# ----------------------------------------------------------------------
# Print sent or received chat messages to a main window. If messages logging is selected in the
# configuration, add this text to a log.

sub print_event_message
{
	my ($session, $sender, $r_message, $correct) = (shift, shift, utf8_l(shift), shift);
	my ($to_print, $real_user, $event);

	my $max_len  = 10;
	my $message  = $r_message;
	my $user_ref = $msn->get_user($sender) if $sender ne $msn->passport;
	my @aliases  = alias_get_user($sender) if $sender ne $msn->passport;

	my $fname = (@aliases) ? $aliases[0] : (defined $user_ref) ? utf8_l($msn->url_decode($user_ref->fname)) :
	    ($sender eq $msn->passport) ? utf8_l($msn->url_decode($msn->fname)) : contacts_get_fname($sender);

	$fname = $1 if !defined $fname && $sender =~ /^(\S+?)\@/;

	$real_user = (config_lookup('truncate_name_chat'))
	    ? substr $fname, 0, $max_len : (real_length($fname) > ($Columns/2)) ? $sender : $fname;

	$real_user .= " " x ($max_len - real_length($real_user)) if config_lookup('truncate_name_chat') &&
	    real_length($real_user) < $max_len;

	# ----- Save the URLs

	if (config_lookup('http_grab') || config_lookup('ftp_grab') && $sender ne $msn->passport)
	{
		for my $url($message =~ /$rh->{message_highlight}->{url}/g)
		{
			# Insert to HTTP/FTP internal buffer
			if (substr ($url, 0, 3) eq 'ftp' && config_lookup('ftp_grab'))     # FTP grabber
			{
				$url = 'ftp://' . $url if substr($url, 0, 6) ne 'ftp://';
				$url = "[FTP] $sender: " . $url;

				push @{$rh->{url_buffer}}, $url
				    if !$rh->{url_buffer} || !grep {$url eq $_} @{$rh->{url_buffer}};
			}

			elsif (config_lookup('http_grab'))                                 # HTTP grabber
			{
				$url = 'http://' . $url if substr ($url, 0, 7) ne 'http://';
				$url = "[HTTP] $sender: " . $url;

				push @{$rh->{url_buffer}}, $url
				    if !$rh->{url_buffer} || !grep {$url eq $_} @{$rh->{url_buffer}};
			}
		}
	}

	if (config_lookup('highlight_msg'))   # Highlight the message parts
	{
		# We need to bring back the message color.
		my $to_add = ($sender eq $msn->passport) ? "<%message_m_outgoing>" : "<%message_m_incoming>";

		for (sort keys %{$rh->{message_highlight}})
		{
			$message =~ s/($rh->{message_highlight}->{$_})/"<%message_h_".$_.">".$1."<%n>$to_add"/eg;
		}

		for my $t_rem($to_add, '<%n>')
		{
			$message =~ s/\Q$t_rem\E$//g;
		}
	}

	$event  = ($sender eq $msn->passport) ? '<%message_user_own>' : '<%message_user>';
	$event .= "$real_user<%n>: ";

	if (defined $correct && $correct eq -1)    # Not acknowledged yet
	{
		$event .= '<%message_m_sent>' . $message;
	}

	elsif ($correct)                           # Delivered
	{
		$event .= '<%message_m_outgoing>' . $message if $sender eq $msn->passport;
		$event .= '<%message_m_incoming>' . $message if $sender ne $msn->passport;
	}

	else                                       # Undelivered
	{
		$event .= '<%text_highlight>[Undelivered]<%n> <%message_m_undelivered>' . $message;
	}

	print_event($rh->{sessions}->[$session]->{window}, $event);    # Print the message to the window

	# Put the message into log
	if (($sender eq $msn->passport && config_lookup('log_outgoing')) ||
	    ($sender ne $msn->passport && config_lookup('log_incoming')) && defined config_lookup('history'))
	{
		my @to_log;
		push @to_log, $_->passport for $msn->get_swb_users($session);

		for (@to_log)
		{
			my $fh = get_filehandle($_);
			next if !defined $fh;
	    
			# Log Format
			# Delivered:   [14/May/2003 21:34:27] me@hotmail.com: Hi how are ya?
			# Undelivered: [14/May/2003 21:34:27] me@hotmail.com: [Undelivered] Hi how are ya?

			if (defined $correct && $correct eq -1)
			{
				$r_message = "[Sent] $r_message";
			}
			elsif (!$correct)
			{
				$r_message = "[Undelivered] $r_message";
			}

			$fh->print(sprintf strftime("[%d/%b/%Y %H:%M:%S]", localtime) . " $sender: $r_message\n");
		}
	}
	1;
}

# &MSNre::print_groups ()
# ----------------------------------------------------------------------
# Print a list of groups in the forward list.

sub print_groups
{
	check_connection() || return undef;

	my @groups;
	my $w = $rh->{w}->{current};

	for ($msn->get_all_groups)
	{
		my $name = utf8_l($msn->url_decode($_->name));
		push @groups, { name => $name, users => $_->users, ID => $_->ID };
	}

	if (@groups)
	{
		my ($max_len_n, $max_len_u) = (0, 0);

		print_event($w, "\n" . print_line('Groups list'));
		for (@groups)
		{
			# Length of a name and users number for the alignment

			$max_len_n = real_length($_->{name}) if real_length($_->{name}) > $max_len_n;
			$max_len_u = real_length($_->{users}) if real_length($_->{users}) > $max_len_u;
		}

		for (sort {uc($a->{name}) cmp (uc($b->{name}))} @groups)  # Sort by the name and print it
		{
			print_event($w,
				    "Group name <%text_bracket>[ <%text_highlight>$_->{name}" .
				    " " x ($max_len_n - real_length($_->{name})) .

				    " <%text_bracket>]<%n>  Users:  <%text_highlight>$_->{users}" .
				    " " x ($max_len_u - real_length($_->{users})) .
				    "   Server ID: <%text_highlight>$_->{ID}");
		}

		print_window($w, "\n");
		window_main_update($w);
	}
	else
	{
		print_event($w, "<%K>No group available");
	}
	1;
}

# &MSNre::print_line ( TEXT )
# ----------------------------------------------------------------------
# Create a colored row with (optionally) some text inside and return it.

sub print_line
{
	my $text = shift;
	my $new_row;

	if (defined $text)
	{
		my $text_len = real_length($text);

		$text =~ s/^(\S)(.*)$/<%header_text_first>$1<%n><%header_text>$2<%n>/;
		$new_row = "<%header_line>---<%header_bracket>[ $text <%header_bracket>]<%header_line>" .
		    "-" x (($Columns - $text_len) - real_length(get_timestamp()) - 9) . "<%n>";
	}
	else
	{
		$new_row = "<%header_line>" . "-" x ($Columns - real_length(get_timestamp()) - 2) . "<%n>";
	}
	return $new_row;
}

# &MSNre::print_line_text ( WINDOW_ID, TEXT, VALUE [, COLOR ] )
# ----------------------------------------------------------------------
# Print a format text.

sub print_line_text
{
	my ($window_id, $text, $value, $text_color) = @_;
	return if !defined $text;

	$window_id = $rh->{w}->{current} if !defined $window_id;
	$value = '' if !defined $value;

	print_event($window_id, "<%text_bracket>[<%n> $text <%text_bracket>]<%n>  <%text_highlight>$value");
	1;
}

# &MSNre::print_usage ( COMMAND )
# ----------------------------------------------------------------------
# Print the help and usage of a specified MSNre command.

sub print_usage
{
	my $r_cmd  = shift;
	my $usage = $rh->{command}->{$r_cmd}->{usage} if defined $rh->{command}->{$r_cmd};

	$usage =~ s/(<|>)/<%Y>$1<%n>/g;     # Add some coloring
	$usage =~ s/(\[|\])/<%B>$1<%n>/g;
	$usage =~ s/\|/<%W>\|<%n>/g;

	print_event($rh->{w}->{current}, "\n" . "Usage : <%W>/<%R>" . uc($r_cmd) . "<%n>  $usage");
	print_event($rh->{w}->{current},        "Help  : $rh->{command}->{$r_cmd}->{help}\n");
	1;
}

# &MSNre::print_window ( WINDOW_ID, TEXT [, FLUSH_BUFFER ] )
# ----------------------------------------------------------------------
# Really print the text to a window.

sub print_window
{
	my ($window_id, $string, $flush) = @_;                       # FIXME: NO_COLORS work for scrolling
	my $cur_attr = undef;

	return undef if !defined $window_id || !defined $string;

	# The window can be either a main window (it's ID) or a real window (title, status, input)
	my $window = (!ref($window_id) && defined $rh->{w}->{main}->[$window_id])
	    ? $rh->{w}->{main}->[$window_id]
	    : $window_id;

	if ($window ne $window_id)           # Extra newlines in a _main_ window
	{
		return undef if $rh->{misc}->{closing_down} && $window_id != $rh->{w}->{current};

		if (defined $rh->{w}->{main_v}->[$window_id]->{to_print})
		{
			$string = (defined $string)
			    ? $rh->{w}->{main_v}->[$window_id]->{to_print} . $string
			    : $rh->{w}->{main_v}->[$window_id]->{to_print};

			$rh->{w}->{main_v}->[$window_id]->{to_print} = undef;
		}

		$rh->{w}->{main_v}->[$window_id]->{to_print} .= $1 if !$flush && defined $string &&
		    $string =~ s/(\n+)$//;
	}
	return 1 if !defined $string;
	my @parts = ($string =~ /<%\S+?>/) ? split /(<(?!<)%\S+?>)/s, $string : $string;

	# Don't physically print anything if the window is currently scrolled
	if ($window eq $window_id || ($window ne $window_id && !$rh->{w}->{main_v}->[$window_id]->{scrolled}))
	{
		for (@parts)
		{
			if (/<%n>/ && $rh->{clr}->{color})                       # Null the colors
			{
				next if !$rh->{misc}->{use_colors};

				$window->standend;
				$cur_attr = undef;
				next;
			}
			elsif (/<%(\S+)>/)                                       # Color attribute
			{
				next if !$rh->{misc}->{use_colors};

				my $cur_col = $1;
				if (POSIX::isdigit($cur_col))
				{
					$window->standend if defined $cur_attr;  # Clear the previous attribute(s)
					$window->attron($cur_col);
					$cur_attr = $cur_col;
					next;
				}
			}
			$window->addstr($_);
		}
	}

	# The rest is usless if we are not printing to a main window
	return 1 if $window eq $window_id;

	if ($window ne $window_id && $window_id != $rh->{w}->{current} && !$flush && !$rh->{misc}->{closing_down})
	{
		if ($rh->{w}->{active} && scalar @{$rh->{w}->{active}})   # Not printing to the current window
		{
			push @{$rh->{w}->{active}}, $window_id unless grep {$_ == $window_id} @{$rh->{w}->{active}};
		}
		else { push @{$rh->{w}->{active}}, $window_id }
		window_status_update();
	}

	# Save what was printed on the screen to an internal buffer, skip this is the window is being scrolled
	return 1 if $rh->{w}->{main_v}->[$window_id]->{scrolling};

	my $cur_pos = $#{$rh->{w}->{main_v}->[$window_id]->{lines}};

	for (split '', $string)
	{
		$rh->{w}->{main_v}->[$window_id]->{lines}->[$cur_pos] .= $_;
		next if $_ ne "\n";

		push @{$rh->{w}->{main_v}->[$window_id]->{lines}}, "";
		$cur_pos++;

		if ($rh->{w}->{main_v}->[$window_id]->{scrolled})
		{
			$rh->{w}->{main_v}->[$window_id]->{more}++;
			window_status_update() if $rh->{w}->{main_v}->[$window_id]->{more} == 1 ||
			    $rh->{w}->{main_v}->[$window_id]->{more} % 10;

			next;
		}

		$rh->{w}->{main_v}->[$window_id]->{buffer_begin}++ if $cur_pos >
		    $rh->{w}->{main_v}->[$window_id]->{screen_size};
	}

	if ($window ne $window_id && $window_id != $rh->{w}->{current} && !$flush && !$rh->{misc}->{closing_down})
	{
		if ($rh->{w}->{active} && scalar @{$rh->{w}->{active}})   # Not printing to the current window
		{
			push @{$rh->{w}->{active}}, $window_id unless grep {$_ == $window_id} @{$rh->{w}->{active}};
		}
		else { push @{$rh->{w}->{active}}, $window_id }
		window_status_update();
	}
	1;
}

# &MSNre::print_window_no_colors ( WINDOW_ID, TEXT [, FLUSH_BUFFER ] )
# ----------------------------------------------------------------------
# Print the text to a window ignoring all the color attributes.

sub print_window_no_colors
{
	local $rh->{clr}->{colors} = {};
	print_window(@_);
	1;
}

# ---------- Windowing functions ---------- #


# &MSNre::window_input_complete ()
# ----------------------------------------------------------------------
# Command-line completition (input window).

sub window_input_complete
{
	my ($completed, $prefix, @candidates, @to_complete);
	my ($incomplete, $incomplete_last, $incomplete_l1, $incomplete_l2);
	my $level = 0;

	if (defined $rh->{w}->{input_v}->{buffer})  # Completing the current input buffer
	{
		# Don't use the completion if the cursor is not at the end of the screen
		return if $rh->{w}->{input_v}->{position} < real_length($rh->{w}->{input_v}->{buffer});

		if ($rh->{w}->{input_v}->{buffer} =~ /^\/(\S+)\s+(.*)$/)
		{
			$prefix = $1;
			my $rest = $2 if defined $2;

			if (defined $rest)
			{
				$level = 1;
				$level = 2 if $rest =~ s/^\s*\S+\s+//;

				# Part that can be used for the completion
				$incomplete = $rest;

				($incomplete_last) = $rh->{w}->{input_v}->{buffer} =~ m!^/\S+.*\s(\S*)$!;
				($incomplete_l1)   = $rh->{w}->{input_v}->{buffer} =~ m!^/\S+\s+(\S+)!;
				($incomplete_l2)   = $rh->{w}->{input_v}->{buffer} =~ m!^/\S+\s+\S+\s+(.*)$!;

				$incomplete_last = '' if !defined $incomplete_last;
				$incomplete_l2   = '' if !defined $incomplete_l2;
			}
		}
		else { $incomplete = $rh->{w}->{input_v}->{buffer} }
	}
	else    # Nothing on the input line - print /CONNECT or /MSG if connected and skip the rest
	{
		if ($msn->connected)
		{
			my @users = get_all_users(online => 1, locals => 1);
			@to_complete = (@users) ? "/MSG $users[0]" : "/MSG ";
		}
		else { push @to_complete, "/CONNECT " }

		$incomplete = '';
	}

	if (defined $prefix && !@to_complete)
	{
		# ----- C: /BLOCK; /IGNORE; /UNALLOW; /UNBLOCK;

		if ($prefix =~ /^(block|ignore|unallow|unblock|)$/i && $msn->connected)
		{
			my $c_list;
			$incomplete = $incomplete_last;

			$c_list = 'block_list' if lc($prefix) eq 'unblock';
			$c_list = 'allow_list' if $prefix =~ /(block|unallow|ignore)/;

			@candidates = get_all_users(list => $c_list);

			if ($rh->{complete_to_al_bl} && (lc($prefix) eq 'block' || lc($prefix) eq 'ignore'))
			{
				# Users that were added to the reverse list, they have to be added to the
				# allow or block list

				for my $to_al_bl(@{$rh->{complete_to_al_bl}})
				{
					push @candidates, $to_al_bl if !grep {$to_al_bl eq $_} @candidates;
				}
			}
		}

		# ----- C: /LG; /LGO; /KILLGROUP; /REMGROUP; /RENGROUP;

		elsif ($prefix =~ /^(lgo?|killgroup|remgroup|rengroup)$/i && $msn->connected)
		{
			($incomplete) = $rh->{w}->{input_v}->{buffer} =~ m!^\S+\s+(.*)$!;
			push @candidates, utf8_l($msn->url_decode($_->name)) for $msn->get_all_groups;
		}

		# ----- C: /ADDFORWARD;

		elsif (lc($prefix) eq 'addforward' && $msn->connected)
		{
			@candidates = get_all_users();
		}

		# ----- C: /ADDUSER;

		elsif (lc($prefix) eq 'adduser' && $msn->connected)
		{
			if ($level == 1)      # Completing users for the 1st level
			{
				@candidates = get_all_users(locals => 1);

				if ($rh->{complete_to_add})
				{
					# Reverse users
					for my $to_add(@{$rh->{complete_to_add}})
					{
						unshift @candidates, $to_add if !grep {$_ eq $to_add} @candidates;
					}
				}
				else
				{
					unshift @candidates, get_all_users(list => 'forward_list');
				}
			}

			elsif ($level == 2)   # Groups for the 2nd level
			{
				$incomplete = $incomplete_l2;
				push @candidates, utf8_l($msn->url_decode($_->name)) for $msn->get_all_groups;
			}
		}

		# ----- C: /ALERT;

		elsif (lc($prefix) eq 'alert' && $level == 1)
		{
			@candidates = qw(LIST GO CLEAR REMOVE);
		}

		# ----- C: /ALIASCN;

		elsif (lc($prefix) eq 'aliascn' && $level == 1)
		{
			@candidates = get_all_users();
		}

		# ----- C: /ALLOW;

		elsif (lc($prefix) eq 'allow' && $msn->connected && $level == 1)
		{
			$incomplete = $incomplete_last;
			@candidates = @{$rh->{complete_to_al_bl}} if $rh->{complete_to_al_bl};
		}

		# ----- C: /AWAYMSG;

		elsif (lc($prefix) eq 'awaymsg' && $level == 1)
		{
			@candidates = qw(IDLE AWAY BRB BUSY LUNCH PHONE);
		}

		# ----- C: /CHPROFILE;

		elsif (lc($prefix) eq 'chprofile' && $level == 1)
		{
			@candidates = keys %{$rh->{profile}};
		}

		# ----- C: /EXEC;

		elsif (lc($prefix) eq 'exec')
		{
			$incomplete = $incomplete_last;

			# Completing an absolute path
			@candidates = glob("$incomplete*") if substr($incomplete, 0, 1) eq '/';
		}

		# ----- C: /FILEACCEPT;

		elsif (lc($prefix) eq 'fileaccept' && $msn->connected && $level == 1 && $rh->{ft_sessions})
		{
			push @candidates, $_->{session} for grep {$_->{can_accept}} @{$rh->{ft_sessions}};
		}

		# ----- C: /FILECANCEL;

		elsif (lc($prefix) eq 'filecancel' && $msn->connected && $level == 1 && $rh->{ft_sessions})
		{
			push @candidates, $_->{session} for grep {$_->{can_cancel}} @{$rh->{ft_sessions}};
		}

		# ----- C: /FILEREJECT;

		elsif (lc($prefix) eq 'filereject' && $msn->connected && $level == 1 && $rh->{ft_sessions})
		{
			push @candidates, $_->{session} for grep {$_->{can_reject}} @{$rh->{ft_sessions}};
		}

		# ----- C: /FILESEND;

		elsif (lc($prefix) eq 'filesend' && $msn->connected)
		{
			# Complete users if it's supposed to be the first parameter
			if ($level == 1 && !defined find_session_by_window($rh->{w}->{current}))
			{
				@candidates = get_all_users(list => 'forward_list', online => 1);
			}

			# Complete an absolute path
			elsif (defined $incomplete && substr($incomplete, 0, 1) eq '/')
			{
				@candidates = glob("$incomplete*");
			}
		}

		# ----- C: /HELP;

		elsif (lc($prefix) eq 'help' && $level == 1)
		{
			@candidates = map {uc($_)} keys %{$rh->{command}};
		}

		# ----- C: /HISTORY;

		elsif (lc($prefix) eq 'history' && $level == 1)
		{
			@candidates = 'CLEAR';
		}

		# ----- C: /HOTMAIL;

		elsif (lc($prefix) eq 'hotmail')
		{
			if ($level == 1)
			{
				@candidates = qw(ADDRESSBOOK COMPOSE FOLDERS INBOX);
			}

			# Mail composition for a user
			elsif ($level == 2 && $msn->connected && lc($incomplete_l1) eq 'compose')
			{
				@candidates = get_all_users();
			}
		}

		# ----- C: /INFO;

		elsif (lc($prefix) eq 'info' && $msn->connected)
		{
			@candidates = get_all_users(locals => 1);
		}

		# ----- C: /ISTATUS; /STATUS;

		elsif ((lc($prefix) eq 'istatus' || lc($prefix) eq 'status'))
		{
			($incomplete) = $rh->{w}->{input_v}->{buffer} =~ m!^/\S+\s+(.*)$!;

			for (grep {$_ ne 'offline'} keys %Net::MsnMessenger::Data::Status)
			{
				my $t_status = ucfirst $_;    $t_status =~ s/_/ /g;
				push @candidates, $t_status;
			}
		}

		# ----- C: /INVITE;

		elsif (lc($prefix) eq 'invite' && $msn->connected && $level == 1)
		{
			@candidates = get_all_users(online => 1, list => 'forward_list', locals => 1);
		}

		# ----- C: /INVITEMSN;

		elsif (lc($prefix) eq 'invitemsn' && $msn->connected && $level == 1)
		{
			@candidates = get_all_users();
		}

		# ----- C: /MOVE;

		elsif (lc($prefix) eq 'move' && $msn->connected)
		{
			if ($level == 1)      # User to move
			{
				@candidates = get_all_users(list => 'forward_list');
			}
			elsif ($level == 2)   # Group name where to move the user
			{
				push @candidates, utf8_l($msn->url_decode($_->name)) for $msn->get_all_groups;
			}
		}

		# ----- C: /MSG;

		elsif (lc($prefix) eq 'msg' && $msn->connected && $level == 1)
		{
			@candidates = get_all_users(online => 1, locals => 1);
		}

		# ----- C: /MSGEXEC;

		elsif (lc($prefix) eq 'msgexec' && $msn->connected)
		{
			if ($level == 1)
			{
				@candidates = get_all_users(online => 1, locals => 1);
			}
			else    # The same like /EXEC
			{
				$incomplete = $incomplete_last;
				@candidates = globa("$incomplete*") if substr($incomplete, 0, 1) eq '/';
			}
		}

		# ----- C: /NMACCEPT;

		elsif (lc($prefix) eq 'nmaccept' && $msn->connected && $level == 1 && $rh->{nm_sessions})
		{
			push @candidates, $_->{session} for grep {$_->{can_accept}} @{$rh->{nm_sessions}};
		}

		# ----- C: /NMINVITE;

		elsif (lc($prefix) eq 'nminvite' && $msn->connected && $level == 1)
		{
			@candidates = get_all_users(online => 1, list => 'forward_list', locals => 1);
		}

		# ----- C: /NMREJECT;

		elsif (lc($prefix) eq 'nmreject' && $msn->connected && $level == 1 && $rh->{nm_sessions})
		{
			push @candidates, $_->{session} for grep {$_->{can_reject}} @{$rh->{nm_sessions}};
		}

		# ----- C: /PAGER;

		elsif (lc($prefix) eq 'pager' && $msn->connected && $level == 1)
		{
			@candidates = get_all_users(mobile => 1);
		}

		# ----- C: /PHONE;

		elsif (lc($prefix) eq 'phone' && $level == 1)
		{
			@candidates = qw(HOME MOBILE WORK);
		}

		# ----- C: /PROFILE;

		elsif (lc($prefix) eq 'profile' && $msn->connected && $level == 1)
		{
			@candidates = get_all_users();
		}

		# ----- C: /REMUSER;

		elsif (lc($prefix) eq 'remuser' && $msn->connected)
		{
			if ($level == 1)
			{
				@candidates = get_all_users(list => 'forward_list', locals => 1);
			}

			# A group(s) the removed user is in
			elsif ($level == 2)
			{
				my $r_user = $msn->get_user($incomplete_l1);
				if (defined $r_user && $r_user->group)
				{
					push @candidates, utf8_l($msn->url_decode($_->name)) for
					    map {$msn->get_group($_)} @{$r_user->group};
				}
			}
		}

		# ----- C: /SAVE;

		elsif (lc($prefix) eq 'save' && $level == 1)
		{
			@candidates = $rh->{config}->{config_file};
		}

		# ----- C: /SET;

		elsif (lc($prefix) eq 'set' && $level == 1)
		{
			my $c_hash = get_config_defaults();

			for (keys %{$c_hash})
			{
				push @candidates, $_ if $_ ne 'friendly_name' && $_ ne 'port' && $_ ne 'password' &&
				    $_ ne 'server' && $_ ne 'user' && $_ ne 'profile_use';
			}
		}

		# ----- C: /UNALIASCMD;

		elsif (lc($prefix) eq 'unaliascmd' && $level == 1)
		{
			push @candidates, keys %{$rh->{profile}->{$rh->{profile_use}}->{aliases}->{COMMAND}} if
			    $rh->{profile}->{$rh->{profile_use}}->{aliases}->{COMMAND};

			if ($rh->{profile_use} ne 'DEFAULT' && $rh->{profile}->{DEFAULT}->{aliases}->{COMMAND})
			{
				for my $alias(keys %{$rh->{profile}->{DEFAULT}->{aliases}->{COMMAND}})
				{
					push @candidates, $alias if !grep {$alias eq $_} @candidates;
				}
			}
		}

		# ----- C: /UNALIASCN;

		elsif (lc($prefix) eq 'unaliascn' && $level == 1)
		{
			push @candidates, keys %{$rh->{profile}->{$rh->{profile_use}}->{aliases}->{CONTACT}} if
			    $rh->{profile}->{$rh->{profile_use}}->{aliases}->{CONTACT};

			if ($rh->{profile_use} ne 'DEFAULT' && $rh->{profile}->{DEFAULT}->{aliases}->{CONTACT})
			{
				for my $alias(keys %{$rh->{profile}->{DEFAULT}->{aliases}->{CONTACT}})
				{
					push @candidates, $alias if !grep {$alias eq $_} @candidates;
				}
			}
		}

		# ----- C: /UNFORWARD;

		elsif (lc($prefix) eq 'unforward')
		{
			for ('window', 'user')
			{
				push @candidates, @{$rh->{forward}->{$_}} if $rh->{forward}->{$_};
			}
		}

		# ----- C: /UNIGNORE;

		elsif (lc($prefix) eq 'unignore' && $msn->connected)
		{
			$incomplete = $incomplete_last;
			push @candidates, $_->{passport} for contacts_get_ignored();
		}

		# ----- C: /URL;

		elsif (lc($prefix) eq 'url')
		{
			my ($prev) = $rh->{w}->{input_v}->{buffer} =~ /\s(\S+)\s+\S*$/;
			$incomplete = $incomplete_last;

			if (defined $prev && (uc($prev) eq 'HTTP' || uc($prev) eq 'FTP'))
			{
				@candidates = qw(ON OFF TOGGLE);
			}
			elsif (!defined $prev || (uc($prev) ne 'GO' && uc($prev) ne 'HELP'))
			{
				@candidates = qw(HTTP FTP LIST SAVE CLEAR GO HELP);
			}
		}

		# ----- C: /WINDOW;

		elsif (lc($prefix) eq 'window')
		{
			my ($prev) = $rh->{w}->{input_v}->{buffer} =~ /\s(\S+)\s\S*$/;
			$incomplete = $incomplete_last;

			if (defined $prev && (uc($prev) eq 'TITLE' || uc($prev) eq 'DOUBLE'))
			{
				@candidates = qw(ON OFF TOGGLE);
			}
			else
			{
				@candidates = qw(NEW NEXT PREVIOUS KILL LIST HELP TITLE DOUBLE);
			}
		}

		# Something else, unknown, don't bother with it
		else { return undef }
	}

	elsif (!@to_complete && $rh->{w}->{input_v}->{buffer} =~ m!^/!)   # Completing a command
	{
		push @candidates, uc('/' . $_ . ' ') for keys %{$rh->{command}};
	}

	for (my $i = 0; $i < @candidates; $i++)
	{
		if (defined $incomplete && length($incomplete))
		{
			push @to_complete, $candidates[$i] if $candidates[$i] =~ /^\Q$incomplete\E.*/i;
		}
		else { push @to_complete, $candidates[$i]; next }

		if (!@to_complete && $candidates[$i] eq $incomplete)   # Full match already - complete next one
		{
			($i < scalar @candidates-1)
			    ? push @to_complete, $candidates[++$i]
			    : push @to_complete, $candidates[0];
			last;
		}
	}

	if (scalar @to_complete == 1 && -d $to_complete[0] && substr($to_complete[0], -1) ne '/')
	{
		my @all = glob("$to_complete[0]*");     # Completing an absolute path

		if (@all && @all == 1)
		{
			require File::Spec;
			$to_complete[0] .= '/' if File::Spec->file_name_is_absolute($all[0]);
		}
	}
	my $n_real1;

	if (@to_complete && scalar @to_complete > 1)
	{
		# Complete at least the first matching part (if possible)
		my $f_match = undef; my $pos = 0;

	      F_P_LOOP:
		while (1)
		{
			$f_match = substr $to_complete[0], 0, ++$pos;
			for (@to_complete)
			{
				last F_P_LOOP if $pos > real_length($_);         # At the end of the string

				next if lc(substr $_, 0, $pos) eq lc($f_match);  # First nonmatching char
				chop $f_match if length($f_match);               # Remove the last (wrong) char
				last F_P_LOOP;
			}
		}

		# More possibilities - arrange and print a list of them

		print_event($rh->{w}->{current}, "<%text_highlight>$NAME<%n>: Available for the completion:");

		my ($max_len, $num, $string, $cur_len) = (0, 0, '', real_length(get_timestamp()));
		my @comp_a = ();

		for (@to_complete)
		{
			$max_len = real_length($_) if real_length($_) > $max_len;
		}

		@to_complete = sort @to_complete;

		while (@to_complete)
		{
			my $current = shift @to_complete;
			my $t_string = ' ' . $current . " " x ($max_len - real_length($current)) . ' ';

			if ($cur_len + (real_length($t_string) + 3) >= $Columns - 1)
			{
				unshift @to_complete, $current;

				print_event($rh->{w}->{current}, $string);
				($string, $cur_len) = ('', real_length(get_timestamp()));

				if ($num % $rh->{w}->{main_v}->[$rh->{w}->{current}]->{screen_size})
				{
					window_main_update();
				}
				$num++;  next;
			}

			if (!grep {$current eq $_} @comp_a)
			{
				$cur_len += real_length($t_string) + 3;
				$string  .= " <%text_bracket>[<%n>$t_string<%text_bracket>]";

				push @comp_a, $current;
			}
		}

		print_event($rh->{w}->{current}, $string) if length $string;  # The rest to print

		print_window($rh->{w}->{current}, "\n");
		window_main_update($rh->{w}->{current});

		@to_complete = $f_match if defined $f_match;
		$n_real1 = 1;
	}

	if (scalar @to_complete == 1)   # Only one posibility (or one left) - complete it
	{
		$to_complete[0] .= ' ' if !$n_real1 && $to_complete[0] !~ /\s$/ && !-e $to_complete[0];

		$rh->{w}->{input_v}->{buffer} =~ s/\Q$incomplete\E$/$to_complete[0]/ if defined $prefix;
		$rh->{w}->{input_v}->{buffer} = $to_complete[0] if !defined $prefix;

		$rh->{w}->{input_v}->{position} = real_length($rh->{w}->{input_v}->{buffer}) if defined
		    $rh->{w}->{input_v}->{buffer};
	}
	1;
}

# &MSNre::window_input_handle ()
# ----------------------------------------------------------------------
# Process the input line after accept-line.

sub window_input_handle
{
	my ($h_command, @h_command_p, $message);
	my $buffer = $rh->{w}->{input_v}->{buffer};

	# Put the line to the history buffer
	unshift @{$rh->{w}->{input_v}->{history}}, $rh->{w}->{input_v}->{buffer};

	$rh->{w}->{input_v}->{buffer} =~ s/\s*$//;       # Remove leading whitespaces

	$rh->{w}->{input_v}->{buffer}   = undef;         # Erase the input line and set it's defaults
	$rh->{w}->{input_v}->{position} = 0;

	$rh->{w}->{input_v}->{history_position} = -1;    # Beginning of the history
	window_input_update();

	if ($buffer =~ m!^/(\S+)(?:\s+)?(.+)?$!)  # A client command
	{
		$h_command = lc $1;
		@h_command_p = (defined $2) ? split /\s+/, $2 : ();
	}
	else    # A switchboard message
	{
		$message = $buffer;
	}

	if (defined $message)
	{
		check_connection() || return undef;
		return cmd_message($rh->{w}->{current}, $message);      # Send the message
	}

	if (exists $rh->{command}->{$h_command} && defined $rh->{command}->{$h_command}->{func})
	{
		# A real client command exists
		return &{$rh->{command}->{$h_command}->{func}}(@h_command_p);
	}

	# A classic command with this name doesn't exist, try to find an alias in the current and
	# in the DEFAULT profile

	my $alias_ref;
	my @to_lookup = $rh->{profile_use};

	push @to_lookup, 'DEFAULT' if $rh->{profile_use} ne 'DEFAULT';

	for (@to_lookup)
	{
		if (exists $rh->{profile}->{$_}->{aliases}->{COMMAND}->{$h_command})
		{
			$alias_ref = $rh->{profile}->{$_}->{aliases}->{COMMAND}->{$h_command};
			last;
		}
	}

	if (!defined $alias_ref)
	{
		print_event($rh->{w}->{current}, "<%text_highlight>$NAME<%n>: Unknown command: " .
			    "<%text_highlight>[$h_command]<%n>. Try the <%text_highlight>/HELP<%n> command.");
		return undef;
	}

	# An alias definitely exists
	my $code = \&{$rh->{command}->{ $alias_ref->{old} }->{func}};

	if ($alias_ref->{old_params})            # Process the command's parameters
	{
		my $params = join ' ', @{$alias_ref->{old_params}};
		my $skip_prev;

		while ($params =~ /(?:^|\s)\$\*(?:\s|$)/)      # Variable - $*
		{
			my $to_subst = (@h_command_p) ? join(' ', @h_command_p) : '';

			$params =~ s/\$\*/$to_subst/g;
			$skip_prev++;
		}

		while ($params =~ /(?:^|\s)\$(\d+)(?:\s|$)/)   # Variable - $0 ... $n
		{
			my ($var, $var_name) = ($1, '$' . $1);
			my $to_subst = (@h_command_p && $var < scalar @h_command_p) ? $h_command_p[$var] : '';

			$params =~ s/\Q$var_name\E/$to_subst/g;
			$skip_prev++;
		}

		if (!$skip_prev)
		{
			unshift @h_command_p, @{$alias_ref->{old_params}};
		}
		else    # Skip the rest of the parameters (only add the ones for variables)
		{
			@h_command_p = split /\s+/, $params;
		}
	}

	msnre_debug("Alias command to exec: $alias_ref->{old} " . join(' ', @h_command_p));
	return &$code(@h_command_p);
}

# &MSNre::window_input_read ()
# ----------------------------------------------------------------------
# Read a keystroke. If it has a binded action, execute it. Also handle auto status change from
# idle back to online and send typing notifications.

sub window_input_read
{
	my ($keystroke, $wheel_id) = @_[ARG0, ARG1];

	$keystroke = uc(unctrl($keystroke))  if $keystroke lt ' ';
	$keystroke = uc(keyname($keystroke)) if $keystroke =~ /^\d{2,}$/;

	$rh->{timers}->{idle}->{start} = time if defined $rh->{timers}->{idle} && $rh->{timers}->{idle}->{start};

	if ($keystroke eq 'KEY_RESIZE')
	{
		window_main_change_active($rh->{w}->{current});
		return;
	}

	# --- Prefix keys

	$keystroke = "<meta>"  if $keystroke eq 'KEY_ESC' || $keystroke eq '^[';
	$keystroke = "<ctrl> " . lc($1) if $keystroke =~ /^\^(\S+)$/;

	if ($keystroke eq '<meta>' || $keystroke eq '<ctrl> x')
	{
		$rh->{w}->{input_v}->{prefix} = $keystroke;
		return 1;
	}

	# --- Additional 'special' characters

	$keystroke = "<bspace>"      if $keystroke eq 'KEY_BACKSPACE';
	$keystroke = "<left>"        if $keystroke eq 'KEY_LEFT';
	$keystroke = "<right>"       if $keystroke eq 'KEY_RIGHT';
	$keystroke = "<up>"          if $keystroke eq 'KEY_UP';
	$keystroke = "<down>"        if $keystroke eq 'KEY_DOWN';
	$keystroke = "<insert>"      if $keystroke eq 'KEY_IC';
	$keystroke = "<delete>"      if $keystroke eq 'KEY_DC';
	$keystroke = "<home>"        if $keystroke eq 'KEY_HOME';
	$keystroke = "<end>"         if $keystroke eq 'KEY_END';
	$keystroke = "<ppage>"       if $keystroke eq 'KEY_PPAGE';
	$keystroke = "<npage>"       if $keystroke eq 'KEY_NPAGE';
	$keystroke = "<enter>"       if $keystroke eq 'KEY_ENTER';
	$keystroke = "<tab>"         if $keystroke eq "<ctrl> i";

	# Going back online from auto-idle
	if ($rh->{timers}->{idle}->{set} &&
	    (!$rh->{timers}->{idle}->{setting} || $rh->{timers}->{idle}->{setting} ne 'online'))
	{
		print_event(1, "\nAuto-changing the status back to <%text_highlight>Online<%n> after being idle ".
			    "for <%text_highlight>" . get_time(time - $rh->{timers}->{idle}->{set_time}) . "<%n>.");

		$rh->{timers}->{idle}->{setting} = 'online';
		$msn->change_status('online');
	}

	# Try to find a binded action for the current keystroke

	my @h_actions = lookup_key_bind($keystroke);

	if (@h_actions)
	{
		for my $action(@h_actions)
		{
			(defined $rh->{key_bind}->{command}->{$action})
			    ? &{$rh->{key_bind}->{command}->{$action}} : msnre_error("Unknown key-bind action: $_");
		}
		window_input_update();
		return 1;
	}

	return 1 if $keystroke =~ /^<\S+?>/ || $keystroke =~ /^KEY_/;

	if (!defined $rh->{w}->{input_v}->{buffer})           # Print the character to the input line
	{
		$rh->{w}->{input_v}->{buffer} = $keystroke;
	}
	else
	{
		if ($rh->{w}->{input_v}->{overwrite} && $rh->{w}->{input_v}->{position} <   # Overwrite ON
		    real_length($rh->{w}->{input_v}->{buffer}))
		{
			substr($rh->{w}->{input_v}->{buffer}, $rh->{w}->{input_v}->{position}, 1) = $keystroke;
		}
		else                                                                        # Overwrite OFF
		{
			substr($rh->{w}->{input_v}->{buffer}, $rh->{w}->{input_v}->{position}, 0) = $keystroke;
		}
	}

	# If we are typing to a connected window send the User is typing message, otherwise try
	# to reestablish the switchboard session.

	my $can_find = 1 if defined $rh->{w}->{input_v}->{buffer} && length($rh->{w}->{input_v}->{buffer}) &&
	    $rh->{w}->{input_v}->{buffer} !~ /^\// && $msn->connected;

	my $s_t = find_session_by_window_established($rh->{w}->{current}) if $can_find;

	if (defined $s_t)
	{
		if (!$rh->{timers}->{typing}->[$s_t] || !$rh->{timers}->{typing}->[$s_t]->{last_sent} ||
		    ((time - $rh->{timers}->{typing}->[$s_t]->{last_sent}) >= 5))
		{
			$msn->send_typing_user($s_t);
			$rh->{timers}->{typing}->[$s_t]->{last_sent} = time;
		}
	}
	else
	{
		# See if we are typing to a disconnected window
		$s_t = find_session_by_window($rh->{w}->{current}) if $can_find;

		if (defined $s_t && defined $rh->{sessions}->[$s_t]->{first_user})
		{
			# If the user we are establishing the session with is not in the contact list he can
			# be possibly offline. To don't get ugly and continuous server errors, delay creating
			# the new session until the message is really sent.

			my $r_user = $msn->get_user($rh->{sessions}->[$s_t]->{first_user});
			create_session(undef, $rh->{sessions}->[$s_t]->{first_user}) if defined $r_user &&
			    $r_user->status ne 'offline';
		}
	}

	$rh->{w}->{input_v}->{position}++;
	window_input_update();
	1;
}

# &MSNre::window_input_update ()
# ----------------------------------------------------------------------
# Update the input window - print the prompt, current buffer and place the cursor on the position.

sub window_input_update
{
	my $prompt_len = length($rh->{w}->{current}) + 3;  # Prompt => [window_number]
	my $real_cols = $Columns - $prompt_len;

	$rh->{w}->{input}->erase;  $rh->{w}->{input}->move(0, 0);  $rh->{w}->{input}->noutrefresh;
	print_window($rh->{w}->{input},
		     fix_colors("<%prompt_bracket>[<%prompt_text>$rh->{w}->{current}<%prompt_bracket>]<%n> "));

	$rh->{w}->{input}->move(0, $prompt_len);

	if (defined $rh->{w}->{input_v}->{buffer})
	{
		$rh->{w}->{input_v}->{buffer} =~ s/\r?\n//g;

		# Print the buffer, calculate the current cursor position

		if ($rh->{w}->{input_v}->{position} < $real_cols)  # The buffer fits on line line
		{
			my $to_print = (real_length($rh->{w}->{input_v}->{buffer}) < $real_cols)
			    ? $rh->{w}->{input_v}->{buffer} : substr $rh->{w}->{input_v}->{buffer}, 0, $real_cols;

			$rh->{w}->{input}->addstr($to_print);
			$rh->{w}->{input}->move(0, $prompt_len + $rh->{w}->{input_v}->{position});
		}
		else    # It doesn't fit there, so scroll the line and print only the visible part
		{
			my $left_len = $real_cols * int($rh->{w}->{input_v}->{position} / $real_cols);

			$rh->{w}->{input}->addstr(substr $rh->{w}->{input_v}->{buffer}, $left_len);
			$rh->{w}->{input}->move(0, $prompt_len + ($rh->{w}->{input_v}->{position} - $left_len));
		}

		$rh->{w}->{input_v}->{buffer} = undef if !$rh->{w}->{input_v}->{position} &&
		    !length($rh->{w}->{input_v}->{buffer});
	}

	$rh->{w}->{main}->[$rh->{w}->{current}]->noutrefresh;
	$rh->{w}->{input}->noutrefresh;
	doupdate;
	1;
}

# &MSNre::window_main_change_active ( WINDOW_ID )
# ----------------------------------------------------------------------
# Change the currently active main window.

sub window_main_change_active
{
	my $window_id = shift;

	if (!defined $rh->{w}->{main}->[$window_id] || $window_id < 0)
	{
		print_event($rh->{w}->{current},
			    "<%text_highlight>$NAME<%n>: No such window: <%text_highlight>$window_id");
		return;
	}
	$rh->{w}->{current} = $window_id;

	# If the new active window was previously marked as active, remove this mark
	if ($rh->{w}->{active} && scalar @{$rh->{w}->{active}})
	{
		for (my $i = 0; $i < @{$rh->{w}->{active}}; $i++)
		{
			splice @{$rh->{w}->{active}}, $i, 1 if $rh->{w}->{active}->[$i] == $rh->{w}->{current};
		}
	}

	# Re-draw everything on the screen
	curs_set(0);

	$rh->{w}->{title}->[$window_id]->redrawwin if $rh->{w}->{main_v}->[$window_id]->{is_title};
	$rh->{w}->{status2}->[$window_id]->redrawwin if $rh->{w}->{main_v}->[$window_id]->{is_double};

	$rh->{w}->{$_}->[$window_id]->redrawwin for 'main', 'status';  # These are always enabled
	$rh->{w}->{main}->[$window_id]->noutrefresh;

	window_status_update();
	window_title_update();
	window_input_update();

	curs_set(1);
	1;
}

# &MSNre::window_main_create ( WINDOW_ID [, DONT_MAKE_ACTIVE ] )
# ----------------------------------------------------------------------
# Create a new main window.

sub window_main_create
{
	my $window_id = shift;
	my $no_change = shift;

	$rh->{w}->{title}->[$window_id] = newwin(1, 0, 0, 0);
	$rh->{w}->{status2}->[$window_id] = newwin(1, 0, $Rows - 2, 0);

	if ($window_id > 1)   # The second status bar is only present in the first window
	{
		$rh->{w}->{main}->[$window_id] = newwin($Rows - 3, 0, 1, 0);

		$rh->{w}->{main_v}->[$window_id]->{screen_size} = $Rows - 3;   # The size of the main window
		$rh->{w}->{main_v}->[$window_id]->{is_double} = 0;             # The second status bar

		$rh->{w}->{status}->[$window_id] = newwin(1, 0, $Rows - 2, 0);
	}
	else
	{
		$rh->{w}->{main}->[$window_id] = newwin($Rows - 4, 0, 1, 0);

		$rh->{w}->{main_v}->[$window_id]->{screen_size} = $Rows - 4;
		$rh->{w}->{main_v}->[$window_id]->{is_double} = 1;

		$rh->{w}->{status}->[$window_id] = newwin(1, 0, $Rows - 3, 0);
	}
	$rh->{w}->{main_v}->[$window_id]->{is_title} = 1;                      # The title bar (always enabled)

	$rh->{w}->{$_}->[$window_id]->leaveok(1) for 'title', 'status', 'status2';

	if ($rh->{misc}->{use_colors})
	{
		$rh->{w}->{$_}->[$window_id]->attron(color_lookup("$_"."_text1")) for 'status', 'status2';
	}
	$rh->{w}->{title}->[$window_id]->attron(color_lookup('title_text'));

	$rh->{w}->{main}->[$window_id]->scrollok(1);
	$rh->{w}->{main}->[$window_id]->leaveok(1);

	$rh->{w}->{main_v}->[$window_id]->{buffer_begin} = 0;
	$rh->{w}->{main_v}->[$window_id]->{scrolled}     = 0;
	$rh->{w}->{main_v}->[$window_id]->{scrolling}    = 0;

	$rh->{w}->{main_v}->[$window_id]->{lines}->[0] = "";

	if ($window_id && !$no_change)
	{
		window_main_change_active($window_id);   # Make the new window active
	}
	return $window_id;
}

# &MSNre::window_main_create_chat ( SWB_SESSION )
# ----------------------------------------------------------------------
# Create a new chat window.

sub window_main_create_chat
{
	my $session = shift;
	my $win_id = $rh->{sessions}->[$session]->{window};
	my $user = $rh->{sessions}->[$session]->{first_user};

	my $no_change = (!config_lookup('auto_change_window')) ? 1 : 0;

	msnre_debug("Creating window $win_id for the chat");
	window_main_create($win_id, $no_change);

	if ($no_change)
	{
		print_event($rh->{w}->{current},
			    "\nNew chat window created for user <%text_passport><$user><%n> in window " .
			    "<%text_highlight>[$win_id]<%n>\n");
	}

	my $fname = contacts_get_fname($user);

	print_event($win_id, "Starting conversation with <%text_fname>$fname <%text_passport><$user>\n");

	my $inc_enabled = (config_lookup('log_incoming')) ? "Enabled" : "Disabled";
	my $out_enabled = (config_lookup('log_outgoing')) ? "Enabled" : "Disabled";

	print_event($win_id, "\n" . print_line('Chat session'));
	print_event($win_id, " Chat started at <%text_highlight>" . localtime(time));
	print_event($win_id, " Chat logging    Incoming messages logging is <%text_highlight>$inc_enabled");
	print_event($win_id, "                 Outgoing messages logging is <%text_highlight>$out_enabled");
	print_event($win_id, print_line() . "\n");
	window_title_update();
	1;
}

# &MSNre::window_main_kill ( WINDOW_ID )
# ----------------------------------------------------------------------
# Kill the specified main window.

sub window_main_kill
{
	my $window_id = shift;

	if ($window_id == 1)
	{
		print_event($rh->{w}->{current}, "<%text_highlight>$NAME<%n>: You can't kill the main window.");
		return;
	}

	# If the debugging window is going to be killed, unset the debugging flags
	elsif ($window_id == 0)
	{
		config_set("debug_".$_, 0) for 'msnre', 'protocol', 'connection';

		$msn->debug(0);
		$msn->debug_connection(0);
	}

	# Disconnect the swb sessions if there are any established
	while (defined (my $session = find_session_by_window_established($window_id)))
	{
		disconnect_swb($session);
	}

	if (exists $rh->{w}->{main}->[$window_id])        # Destroy the window
	{
		delwin $rh->{w}->{main}->[$window_id];

		undef $rh->{w}->{$_}->[$window_id] for 'main', 'main_v';
	}

	# Switch to the previous (or first) window
	($window_id == 0)
	    ? window_main_change_active(1) : &{$rh->{key_bind}->{command}->{'previous-window'}};
	1;
}

# &MSNre::window_main_redraw ( WINDOW_ID, SCROLL_LINES )
# ----------------------------------------------------------------------
# Redraw the specified main window (mostly for scrolling).

sub window_main_redraw
{
	my ($window_id, $scroll) = @_;

	my $old = $rh->{w}->{main_v}->[$window_id]->{buffer_begin};
	my $no_scroll = 0;

	return if !$rh->{w}->{main_v}->[$window_id]->{lines};

	if (defined $scroll && $scroll =~ /^-?\d+$/)
	{
		$rh->{w}->{main_v}->[$window_id]->{buffer_begin} += $scroll;
	}
	else { $no_scroll++ }   # Do not really scroll, only reprint the current screen content

	# End of the buffer
	my $max =
	    scalar @{$rh->{w}->{main_v}->[$window_id]->{lines}} - $rh->{w}->{main_v}->[$window_id]->{screen_size};

	# Beginning of the buffer
	$rh->{w}->{main_v}->[$window_id]->{buffer_begin} =
	    $max if $rh->{w}->{main_v}->[$window_id]->{buffer_begin} > $max;

	# Beginning of the buffer
	$rh->{w}->{main_v}->[$window_id]->{buffer_begin} = 0 if $rh->{w}->{main_v}->[$window_id]->{buffer_begin} < 0;

	# Scrolling is impossible
	$no_scroll++ if $max <= 0 || $rh->{w}->{main_v}->[$window_id]->{buffer_begin} == $old;

	# Number of lines to print
	my $lines = $rh->{w}->{main_v}->[$window_id]->{buffer_begin}+$rh->{w}->{main_v}->[$window_id]->{screen_size}-1;

	# Save the to_print buffer, it's highly unwanted to print it now
	my $to_print;
	if (defined $rh->{w}->{main_v}->[$window_id]->{to_print})
	{
		$to_print = $rh->{w}->{main_v}->[$window_id]->{to_print};
		$rh->{w}->{main_v}->[$window_id]->{to_print} = undef;
	}

	if (!$no_scroll || (defined $scroll && $scroll eq 0))
	{
		curs_set(0);

		$rh->{w}->{main}->[$window_id]->erase;
		$rh->{w}->{main}->[$window_id]->move(0, 0);

		$rh->{w}->{main_v}->[$window_id]->{scrolled} = 0;

		for my $line($rh->{w}->{main_v}->[$window_id]->{buffer_begin} .. $lines)  # Print everything
		{
			print_window($window_id, $rh->{w}->{main_v}->[$window_id]->{lines}->[$line]);

			# XXX: TODO: FIXME:
			# Limit string length to the current line (when resized - smaller window)
		}
		window_main_update($window_id);
		curs_set(1);
	}

	$rh->{w}->{main_v}->[$window_id]->{to_print} = $to_print if defined $to_print;
	$rh->{w}->{main_v}->[$window_id]->{scrolling} = 0 if $rh->{w}->{main_v}->[$window_id]->{scrolling};

	# Scrolling is impossible. Only unset the flag
	return undef if $no_scroll;

	if ($lines == scalar @{$rh->{w}->{main_v}->[$window_id]->{lines}} - 1)
	{
		# Scrolled to the end of the current window
		$rh->{w}->{main_v}->[$window_id]->{scrolled} = 0;

		if ($rh->{w}->{main_v}->[$window_id]->{more})
		{
			$rh->{w}->{main_v}->[$window_id]->{more} = 0;
			window_status_update();
		}
	}
	else
	{
		$rh->{w}->{main_v}->[$window_id]->{scrolled} = 1;
	}

	$rh->{w}->{main}->[$window_id]->noutrefresh if $window_id == $rh->{w}->{current};
	1;
}

# &MSNre::window_main_update ( [ WINDOW_ID ] )
# ----------------------------------------------------------------------
# Refresh the specified main window.

sub window_main_update
{
	my $window_id = shift;
	$window_id = $rh->{w}->{current} if !defined $window_id;

	return undef if $window_id != $rh->{w}->{current};

	$rh->{w}->{main}->[$window_id]->noutrefresh;

	curs_set(0);                       # Update the screen for the current window
	$rh->{w}->{input}->noutrefresh;
	doupdate;
	curs_set(1);
	1;
}

# &MSNre::window_output_stderr ()
# ----------------------------------------------------------------------
# For debugging the STDERR output goes to the debugging window.

sub window_output_stderr
{
	my $arg = $_[ARG0];

	if (config_lookup('debug_msnre'))
	{
		print_event(0, "<%g>$arg<%n>");
		debug_log($arg) if $rh->{debug}->{debug_log};
	}
	1;
}

# &MSNre::window_status_update ()
# ----------------------------------------------------------------------
# Reprint the status bars.

sub window_status_update
{
	# Reprint the status bars.

	my $cur_win = $rh->{w}->{current};
	my $status  = $rh->{w}->{status}->[$cur_win];
	my $status2 = $rh->{w}->{status2}->[$cur_win] if $rh->{w}->{main_v}->[$cur_win]->{is_double};
	curs_set(0);

	# Erase the 1st status bar
	$status->erase;  $status->addstr(' ' x $Columns);  $status->move(0, 0);
	$status->standend;

	# Erase the 2nd status bar
	if (defined $status2)
	{
		$status2->erase;  $status2->addstr(' ' x $Columns);  $status2->move(0, 0);
		$status2->standend;
	}

	# ---------- Print the _1st_ status bar ---------- #

	my $s_cur_time  = strftime "%I:%M%P", localtime(time);
	my $s_act_str   = '';
	my $s_more_str  = '';
	my $s_total_len = 3;

	my $s_status = ucfirst $msn->status;  $s_status =~ s/_/ /g;

	my ($s_fr_name, $s_fr_name_l);

	if ($msn->signed_in)   # The friendly name and it's length
	{
		$s_fr_name = utf8_l($msn->url_decode($msn->fname));
		$s_fr_name_l = real_length($s_fr_name);
	}
	else
	{
		$s_fr_name = "<Not logged in to the server>";
		$s_fr_name_l = length($s_fr_name);
	}

	# Auto-changed status
	$s_status .= ' (Auto)' if $s_status eq 'Idle' && $rh->{timers}->{idle}->{set};

	if ($rh->{w}->{main_v}->[$rh->{w}->{current}]->{more})
	{
		# Count the extra lines while the window is scrolled

		my $diff = $rh->{w}->{main_v}->[$rh->{w}->{current}]->{more};
		$s_more_str  = "-- more --";
		$s_more_str .= " (".($diff-($diff%10)).")" if $diff >= 10;
	}

	if ($rh->{w}->{active} && scalar @{$rh->{w}->{active}})      # List of the active windows
	{
		for (@{$rh->{w}->{active}})
		{
			$s_act_str .= ',' if $s_act_str =~ /\S/;
			$s_act_str .= $_;
		}
		chop $s_act_str if $s_act_str =~ /,$/;
		$s_act_str = "Act: $s_act_str";
	}
	for ($s_cur_time, $s_status, $s_act_str, $s_more_str)        # Total length of the complete bar
	{
		$s_total_len += length($_) + 2 if $_ =~ /\S/;
	}

	$s_total_len += $s_fr_name_l + 2;
	truncate_name(\$s_fr_name, $s_fr_name_l - ($s_total_len-$Columns)) if $s_total_len > $Columns;

	print_window($status, create_status("[$s_cur_time] [$s_fr_name] [$s_status]"));

	if ($s_act_str =~ /\S/)   # Print the active windows
	{
		$status->move(0, $Columns-length($s_act_str)-2);

		print_window($status, create_status("[$s_act_str]"));
		$status->move(0, $Columns-length($s_act_str.$s_more_str)-3) if $s_more_str =~ /\S/;
	}
	if ($s_more_str =~ /\S/)  # Print the 'more' counter
	{
		$status->move(0, $Columns-length($s_more_str)-1) if $s_act_str !~ /\S/;
		print_window($status, fix_colors("<%status_text1>$s_more_str<%n>"));
	}

	# ---------- Print the _2nd status bar ---------- #

	if (defined $status2)
	{
		# The current lag counter

		my $lag = (defined $rh->{timers}->{ping}->{lag_last}) ? $rh->{timers}->{ping}->{lag_last} :
		    (defined $rh->{timers}->{ping}->{lag_current}) ? $rh->{timers}->{ping}->{lag_current} : 'N/A';

		$lag = ' ' . $lag if length($lag) == 1;

		$status2->move(0, 0);
		print_window($status2, create_status2("[Lag <%status_text2>$lag]"));

		# Online/Offline users, number of unread e-mails
		my ($on_off, $unread) = ("", "");  

		if ($msn->signed_in && !$rh->{misc}->{login})
		{
			my ($online, $offline) = (0, 0);

			for ($msn->get_users_list('forward_list'))
			{
				($_->connected) ? $online++ : $offline++;
			}

			$on_off = "Online: <%status_text2>$online  <%status_text1>Offline: <%status_text2>$offline";

			$unread = (defined $rh->{misc}->{unread_mail}) ? $rh->{misc}->{unread_mail}
			    : (!$msn->can_use_hotmail) ? 'N/A' : 0;

			$unread = "Unread E-Mails: <%status_text2>$unread";
		}

		print_window($status2, create_status2(" [$on_off] [$unread<%n>]"));

		my $o_profile = (defined $rh->{profile_use}) ? $rh->{profile_use} : "";
		my $o_toggle = ($rh->{w}->{input_v}->{overwrite}) ? "Ovr" : "Ins";

		$status2->move(0, $Columns - real_length($o_profile) - length($o_toggle) - 5);

		print_window($status2, create_status2("[$o_profile] [$o_toggle]"));
		$status2->noutrefresh;
	}

	$status->noutrefresh;
	$rh->{w}->{main}->[$rh->{w}->{current}]->noutrefresh;
	$rh->{w}->{input}->noutrefresh;
	doupdate;
	curs_set(1);
	1;
}

# &MSNre::window_title_update ( [ WINDOW_ID ] )
# ----------------------------------------------------------------------
# Reprint the title bar.

sub window_title_update
{
	my $window_id = shift;
	return undef if defined $window_id && $window_id != $rh->{w}->{current};

	$window_id = $rh->{w}->{current} if !defined $window_id;
	return undef if !$rh->{w}->{main_v}->[$window_id]->{is_title} || !defined $rh->{w}->{main}->[$window_id];

	my $title = $rh->{w}->{title}->[$window_id];                  # The title bar
	my $title_str = $rh->{w}->{main_v}->[$window_id]->{title};    # The text inside the title bar

	# Erase the title bar
	$title->erase;  $title->addstr(' ' x $Columns);  $title->move(0, 0);

	if (!defined $title_str)       
	{
		if ($window_id == 0)
		{
			$title_str = 'Debug window';
		}
		elsif ($window_id == 1)
		{
			$title_str = "$NAME version $VERSION ($DATE)";
		}
	}

	my $session = find_session_by_window_established($window_id);
	$session = find_session_by_window($window_id) if !defined $session;

	if (defined $session)   # Title bar for a chat window
	{
		my $users;

		if ($rh->{sessions}->[$session]->{established})
		{
			my @u  = $msn->get_swb_users($session);
			$users = scalar @u + 1 if @u;
		}
		$users ||= 1;

		$title_str  = "Users in the session: $users";
		$title_str .= " [Connected] " if $rh->{sessions}->[$session]->{established};
		$title_str .= " [Disconnected] " if !$rh->{sessions}->[$session]->{established};

		if (defined $rh->{sessions}->[$session]->{typing})
		{
			my $typing = "Typing: $rh->{sessions}->[$session]->{typing}";

			$title_str .= " " x ($Columns - real_length($typing) - length($title_str));
			$title_str .= $typing;
		}
	}

	$title->addstr($title_str) if defined $title_str;       # Print the title bar's text
	$title->noutrefresh;

	$rh->{w}->{main}->[$rh->{w}->{current}]->noutrefresh;
	$rh->{w}->{input}->noutrefresh;
	doupdate;
	1;
}

# -------------------- Helper functions -------------------- #

# &MSNre::add_alias_cmd ( PROFILE, NEW_COMMAND )
# ----------------------------------------------------------------------
# Create an alias to a command.

sub add_alias_cmd
{
	my $profile = shift;
	my $new_command = shift;

	my $old_all = join ' ', @_;
	my ($old_command, $old_command_params) = $old_all =~ /^\s*\/?(\S+)\s*(.*?)\s*$/;

	return undef if (!defined $rh->{command}->{lc $old_command});

	# Make the new command alias available in the help

	$rh->{command}->{lc $new_command}->{help}  = "Alias for /" . uc($old_command) . " $old_command_params";
	$rh->{command}->{lc $new_command}->{usage} = "";
	$rh->{command}->{lc $new_command}->{alias} = 1;

	$rh->{profile}->{$profile}->{aliases}->{COMMAND}->{lc $new_command}->{old} = lc $old_command;

	# Predefined parameters

	@{$rh->{profile}->{$profile}->{aliases}->{COMMAND}->{lc $new_command}->{old_params}} =
	    split /\s+/, $old_command_params if $old_command_params;

	msnre_debug("New alias - " . $rh->{profile}->{$profile}->{aliases}->{COMMAND}->{lc $new_command}->{old}
		    . ": $old_command_params");
	1;
}

# &MSNre::alias_get_user ( USER )
# ----------------------------------------------------------------------
# Find aliases for an user and return them.

sub alias_get_user
{
	my $user = shift;

	if (defined $user)
	{
		return undef if !defined $msn->get_user($user) && !defined contacts_get_contact($user);

		if (exists $rh->{profile}->{ $rh->{profile_use} }->{aliases}->{CONTACT}->{$user})
		{
			# The current profile
			return @{$rh->{profile}->{ $rh->{profile_use} }->{aliases}->{CONTACT}->{$user}};
		}
		elsif ($rh->{profile_use} ne 'DEFAULT')
		{
			# The DEFAULT profile
			return @{$rh->{profile}->{DEFAULT}->{aliases}->{CONTACT}->{$user}} if exists
			    $rh->{profile}->{DEFAULT}->{aliases}->{CONTACT}->{$user};
		}
	}
	return ();
}

# &MSNre::browser_start ( PAGE )
# ----------------------------------------------------------------------
# Launch an external browser and go to a speicifed page (or file).

sub browser_start
{
	my $page = shift;
	my $browser_path = config_lookup('external_browser');

	if (defined $browser_path)
	{
		if (!-f $browser_path)
		{
			msnre_error("The path to the external browser is not valid");

			unlink $page if -f $page;
			return undef;
		}

		if (!-f $page && $page !~ m!^(http|ftp)://!)
		{
			msnre_error("The requested html page doesn't exist");
			msnre_debug("Invalid page: $page");

			unlink $page if -f $page;
			return undef;
		}
		msnre_debug("Launching the browser for $page");
		run_command($browser_path, $page, '&');

		if (-f $page)   # Delete the file with the page and the end
		{
			push @{$rh->{fhs_to_del}}, $page unless
			    defined $rh->{fhs_to_del} && grep {$_ eq $page} @{$rh->{fhs_to_del}};
		}
		return 1;
	}
	else
	{
		print_event($rh->{w}->{current},
			    "<%text_highlight>$NAME<%n>: No external browser is defined in the configuration.");
	}
	undef;
}

# &MSNre::check_connection ( [ WINDOW_ID ] )
# ----------------------------------------------------------------------
# Check if logged in to the server.

sub check_connection
{
	my $window_id = shift;  $window_id = $rh->{w}->{current} if !defined $window_id;

	if (!$msn->signed_in)
	{
		print_event($window_id,
			    "You are not logged in to the server. Use the <%text_highlight>/CONNECT<%n> command.");
		return undef;
	}
	1;
}

# &MSNre::color_add ( PROFILE, COLOR_TYPE, COLOR, @ATTRIBUTES )
# ----------------------------------------------------------------------
# Add a color from the configuration for later usage.

sub color_add
{
	my ($profile, $c_type, $color, @c_attributes) = @_;

	# The real color
	$rh->{profile}->{$profile}->{color}->{$c_type}->{color} = "COLOR_" . uc($color);

	# The color attributes
	@{$rh->{profile}->{$profile}->{color}->{$c_type}->{attributes}} = (@c_attributes)
	    ? @c_attributes : ();
	1;
}

# &MSNre::color_init_pair ( FOREGROUND, BACKGROUND )
# ----------------------------------------------------------------------
# Initialize a curses color pair.

sub color_init_pair
{
	my ($fore, $back) = @_;

	$rh->{clr}->{current_cpair} = $rh->{clr}->{min_cpair} if !$rh->{clr}->{current_cpair} ||
	    $rh->{clr}->{current_cpair} >= COLOR_PAIRS();

	init_pair(++$rh->{clr}->{current_cpair}, $fore, $back);
	$rh->{clr}->{current_cpair};
}

# &MSNre::color_lookup ( COLOR_PART )
# ----------------------------------------------------------------------
# Find a color by the name and return it.

sub color_lookup
{
	my $part = shift;
	return $rh->{clr}->{color_part}->{lc $part} if defined $rh->{clr}->{color_part}->{lc $part};
	undef;
}

# &MSNre::color_register ()
# ----------------------------------------------------------------------
# Create the color pairs so the colors are ready to use.

sub color_register
{
	return undef if !$rh->{misc}->{use_colors};

	my $theme   = config_lookup('color_theme');
	my @types   = keys %{$rh->{clr}->{color_theme}->{1}};
	my $profile = \$rh->{profile}->{$rh->{profile_use}}->{color};

	my $theme_correct = 1 if defined $theme && ($theme eq 1 || $theme eq 2 || $theme eq 3);

	$theme = 1 if !$theme_correct;
	my $col_hash;

	if ($rh->{clr}->{color_part})
	{
		$rh->{clr}->{color_part} = undef for keys %{$rh->{clr}->{color_part}};
	}

	for my $type(@types)
	{
		my $t_color_ok = 0;

		if (!$theme_correct)   # Don't use custom colors if COLOR_THEME option is specified and correct
		{
			# Prefer a custom color
			if (defined $$profile && defined $$profile->{$type} && defined $$profile->{$type}->{color})
			{
				eval { &{$$profile->{$type}->{color}} };
				if (!$@)
				{
					$col_hash->{$type}->{color} = $$profile->{$type}->{color};

					@{$col_hash->{$type}->{attributes}} = @{$$profile->{$type}->{attributes}} if
					    $$profile->{$type}->{attributes};

					$t_color_ok = 1;
				}
			}
		}

		# Custom color not available or not valid, use one from the selected theme
		if (!$t_color_ok)
		{
			$rh->{clr}->{color_theme}->{$theme}->{$type} =~ /^(\S+)\s*(.*)$/;
			$col_hash->{$type}->{color} = "COLOR_" . uc($1);

			@{$col_hash->{$type}->{attributes}} = map {"A_" . uc($_)} split /\s+/, $2 if defined $2;
		}
	}

	# ----- Title bar ----- #

	$rh->{clr}->{color_part}->{title_text} =
	    COLOR_PAIR(color_init_pair(&{$col_hash->{title_text}->{color}},
				       &{$col_hash->{title_background}->{color}}));

	# ----- Status bars ----- #

	for my $part('status', 'status2')
	{
		for my $text('text1', 'text2', 'text_bracket')
		{
			$rh->{clr}->{color_part}->{$part."_".$text} =
			    COLOR_PAIR(color_init_pair(&{$col_hash->{$part."_".$text}->{color}},
						       &{$col_hash->{$part."_background"}->{color}}));
		}
	}

	for (grep {$_ !~ /^(status|title)/i && $_ !~ /background$/i} @types)               # All the other colors
	{
		$rh->{clr}->{color_part}->{$_} =
		    COLOR_PAIR(color_init_pair(&{$col_hash->{$_}->{color}}, $colors->{k}));
	}

	for my $type(grep {$_ !~ /background$/i && $col_hash->{$_}->{attributes}} @types)  # Add the attributes
	{
		for my $attr(@{$col_hash->{$type}->{attributes}})
		{
			eval { &$attr };
			$rh->{clr}->{color_part}->{$type} |= &$attr if !$@;
		}
	}

	$rh->{clr}->{current_cpair} = $rh->{clr}->{min_cpair};
	1;
}

# &MSNre::config_lookup ( OPTION )
# ----------------------------------------------------------------------
# Find a configuration option in the current or DEFAULT profile and return it.

sub config_lookup
{
	my $option = shift;
	my $value;

	return undef if !defined $option;

	if (defined $rh->{profile_use} && exists $rh->{profile}->{ $rh->{profile_use} }->{opt}->{$option})
	{
		$value = $rh->{profile}->{ $rh->{profile_use} }->{opt}->{$option};
	}

	if (!defined $value)
	{
		$value = $rh->{profile}->{DEFAULT}->{opt}->{$option};
	}
	return (defined $value && length($value)) ? $value : undef;
}

# &MSNre::config_set ( OPTION, NEW_VALUE )
# ----------------------------------------------------------------------
# Set a configuration option for the current or DEFAULT profile.

sub config_set
{
	my ($option, $value) = @_;

	return if !defined $option;

	if (defined $rh->{profile_use} && exists $rh->{profile}->{ $rh->{profile_use} }->{opt}->{$option})
	{
		$rh->{profile}->{ $rh->{profile_use} }->{opt}->{$option} = $value;
	}
	else
	{
		$rh->{profile}->{DEFAULT}->{opt}->{$option} = $value;
	}
	$value;
}

# &MSNre::contacts_add ( %ARGS )
# ----------------------------------------------------------------------
# Add a single contact to the local contacts or update one or more of it's values.

sub contacts_add
{
	my %args = @_;

	if ($args{passport} !~ /^\S+?\@\S+?\.\w{2,3}$/)
	{
		msnre_debug("BUG: Adding user with invalid passport: $args{passport}");
		return undef;
	}

	# Passport
	$rh->{contacts}->{$msn->passport}->{$args{passport}}->{passport} = $args{passport};

	# Friendly name
	$rh->{contacts}->{$msn->passport}->{$args{passport}}->{fname} = utf8_l($msn->url_decode($args{fname}))
	    if defined $args{fname};

	# Last seen
	$rh->{contacts}->{$msn->passport}->{$args{passport}}->{last_seen} = scalar localtime($args{last_seen})
	    if defined $args{last_seen};

	# Ignored
	$rh->{contacts}->{$msn->passport}->{$args{passport}}->{ignore} = $args{ignore} if defined $args{ignore};

	# Removed
	$rh->{contacts}->{$msn->passport}->{$args{passport}}->{removed} = $args{removed} if defined $args{removed};

	if (!defined $rh->{contacts}->{$msn->passport}->{$args{passport}}->{fname})  # No friendly name yet
	{
		$rh->{contacts}->{$msn->passport}->{$args{passport}}->{fname} = $args{passport};
	}
	1;
}

# &MSNre::contacts_create ()
# ----------------------------------------------------------------------
# Create the contacts file and write the header.

sub contacts_create
{
	if (!-f $rh->{config}->{contacts_path} && -e $rh->{config}->{contacts_path})
	{
		msnre_error("Couldn't create contacts file. It already exists and it's not a regular file");
		return undef;
	}
	$rh->{fhs}->{contacts_fh} = new FileHandle "> $rh->{config}->{contacts_path}";

	if (!defined $rh->{fhs}->{contacts_fh})
	{
		msnre_error("Couldn't write to $rh->{config}->{contacts_path}: $!");
		return undef;
	}

	$rh->{fhs}->{contacts_fh}->print( "## $rh->{config}->{contacts_path} created by $NAME v$VERSION\n" );
	$rh->{fhs}->{contacts_fh}->print( "##\n" );
	$rh->{fhs}->{contacts_fh}->print( "## File format:\n" );
	$rh->{fhs}->{contacts_fh}->print( "##  ac = account (current user's passport)\n" );
	$rh->{fhs}->{contacts_fh}->print( "##  pp = user's passport\n" );
	$rh->{fhs}->{contacts_fh}->print( "##  fn = user's friendly name\n" );
	$rh->{fhs}->{contacts_fh}->print( "##  ls = last seen (last time the user was online)\n\n" );

	$rh->{fhs}->{contacts_fh}->flush;
	1;
}

# &MSNre::contacts_get_contact ( USER )
# ----------------------------------------------------------------------
# Return a local contact.

sub contacts_get_contact
{
	my $passport = shift;
	return undef if !exists $rh->{contacts} || !exists $rh->{contacts}->{$msn->passport};

	return $rh->{contacts}->{$msn->passport}->{$passport} if exists $rh->{contacts}->{$msn->passport} &&
	    exists $rh->{contacts}->{$msn->passport}->{$passport} &&
	    !$rh->{contacts}->{$msn->passport}->{$passport}->{removed};

	undef;
}

# &MSNre::contacts_get_fname ( USER )
# ----------------------------------------------------------------------
# Return contact's friendly name.

sub contacts_get_fname
{
	my $passport = shift;
	return undef if !exists $rh->{contacts} || !exists $rh->{contacts}->{$msn->passport};

	return $rh->{contacts}->{$msn->passport}->{$passport}->{fname} if exists $rh->{contacts}->{$msn->passport} &&
	    exists $rh->{contacts}->{$msn->passport}->{$passport};

	undef;
}

# &MSNre::contacts_get_ignored ()
# ----------------------------------------------------------------------
# Return an array with the ignored contacts.

sub contacts_get_ignored
{
	return () if !exists $rh->{contacts} || !exists $rh->{contacts}->{$msn->passport};
	my @contacts = ();

	@contacts = grep {$rh->{contacts}->{$msn->passport}->{$_}->{ignored}} contacts_get_locals();
	return @contacts;
}

# &MSNre::contacts_get_locals ()
# ----------------------------------------------------------------------
# Return an array with the local contacts.

sub contacts_get_locals
{
	return () if !exists $rh->{contacts} || !exists $rh->{contacts}->{$msn->passport};
	my @contacts = ();

	for (grep {!defined $msn->get_user($rh->{contacts}->{$msn->passport}->{$_}->{passport}) &&
		!$rh->{contacts}->{$msn->passport}->{$_}->{removed}} sort keys %{$rh->{contacts}->{$msn->passport}})
	{
		push @contacts, $rh->{contacts}->{$msn->passport}->{$_};
	}

	return @contacts;
}

# &MSNre::contacts_read ()
# ----------------------------------------------------------------------
# Read the contacts file.

sub contacts_read
{
	return if !-f $rh->{config}->{contacts_path};

	my $c_fh = new FileHandle "< $rh->{config}->{contacts_path}";

	if (!defined $c_fh)
	{
		msnre_error("Couldn't read $rh->{config}->{contacts_path}: $!");
		return undef;
	}

	while (<$c_fh>)
	{
		my ($to_read) = $_ =~ /^\^\[(.+)\]\s*$/;
		next if !defined $to_read;

		my ($ac) = $to_read =~ /  ac \s* = \s* \" (\S+?) \"  \s*        /ix;    # Account
		my ($pp) = $to_read =~ /  pp \s* = \s* \" (\S+?) \"  \s*        /ix;    # Passport
		my ($fn) = $to_read =~ /  fn \s* = \s* \" (.*?)  \"  \s* ls =   /ix;    # Friendly name
		my ($ls) = $to_read =~ /  ls \s* = \s* \" (.*?)  \"             /ix;    # Last seen
		my ($ig) = $to_read =~ /  ignore \s* = \s* \" (.*?) \"          /ix;    # Ignored
		my ($rm) = $to_read =~ /  removed \s* = \s* \" (.*?) \"         /ix;    # Removed

		next if !defined $ac || !defined $pp;

		if ($pp !~ /^\S+?\@\S+?\.\w{2,3}/)
		{
			msnre_debug("Invalid passport in contacts: $pp");
			next;
		}

		$rh->{contacts}->{$ac}->{$pp}->{passport} = $pp;
		$rh->{contacts}->{$ac}->{$pp}->{fname} = $fn;
		$rh->{contacts}->{$ac}->{$pp}->{last_seen} = $ls if defined $ls && length($ls);
		$rh->{contacts}->{$ac}->{$pp}->{ignore} = $ig    if defined $ig && length($ig);
		$rh->{contacts}->{$ac}->{$pp}->{removed} = $rm   if defined $rm && length($rm);
	}
	1;
}

# &MSNre::contacts_remove_contact ( USER )
# ----------------------------------------------------------------------
# Remove a local contact.

sub contacts_remove_contact
{
	my $passport = shift;

	if (defined $rh->{contacts}->{$msn->passport} && defined $rh->{contacts}->{$msn->passport}->{$passport})
	{
		$rh->{contacts}->{$msn->passport}->{$passport}->{removed} = 1;  # Local contact
		return 1;
	}
	undef;
}

# &MSNre::contacts_write ()
# ----------------------------------------------------------------------
# Write the contacts file.

sub contacts_write
{
	return 1 if !$rh->{contacts};

	contacts_create();
	return undef if !defined $rh->{fhs}->{contacts_fh};

	if ($rh->{sessions})  # Add users from currently established switchboard sessions
	{
		for (my $i = 0; $i < @{$rh->{sessions}}; $i++)
		{
			next if !$rh->{sessions}->[$i]->{established};
			for ($msn->get_swb_users($i))
			{
				contacts_add(passport => $_->passport, fname => $_->fname, last_seen => time) if
				    !defined $msn->get_user($_->passport);
			}
		}
	}

	for my $account(sort keys %{$rh->{contacts}})                     # Accounts
	{
		for my $c(sort keys %{$rh->{contacts}->{$account}})       # Contacts for this account
		{
			$rh->{contacts}->{$account}->{$c}->{fname} = $rh->{contacts}->{$account}->{$c}->{passport} if
			    !defined $rh->{contacts}->{$account}->{$c}->{fname};

			if (!defined $rh->{contacts}->{$account}->{$c}->{last_seen})
			{
				if ($account eq $msn->passport)
				{
					# If the user is online and we are using the current account, set the
					# last_seen time to 'now'

					my $r_user = $msn->get_user($c);

					$rh->{contacts}->{$account}->{$c}->{last_seen} =
					    (defined $r_user && $r_user->status ne 'offline')
					    ? scalar localtime(time) : "";
				}
				else
				{
					$rh->{contacts}->{$account}->{$c}->{last_seen} = "";
				}
			}

			my $line = '^[ac="' . $account . '" pp="' . $c . '" fn="' .
			    $rh->{contacts}->{$account}->{$c}->{fname} . '" ls="' .
			    $rh->{contacts}->{$account}->{$c}->{last_seen} . '"';

			$line .= ' ignore="' . $rh->{contacts}->{$account}->{$c}->{ignore} . '"' if defined
			    $rh->{contacts}->{$account}->{$c}->{ignore};

			$line .= ' removed="' . $rh->{contacts}->{$account}->{$c}->{removed} . '"' if defined
			    $rh->{contacts}->{$account}->{$c}->{removed};

			$line .= "]\n";

			$rh->{fhs}->{contacts_fh}->print($line);
		}
	}

	$rh->{fhs}->{contacts_fh}->close;
	1;
}

# &MSNre::create_session ( TYPE, USER [, MESSAGE | FILE_PATH ] )
# ----------------------------------------------------------------------
# Create a new switchboard session (delay a message or invitation).

sub create_session
{
	my $type = shift;
	my $user = shift;
	my ($file_path, $message);

	if (defined $type)
	{
		$file_path = shift if $type eq 'file';   # Sending a file
		$message = shift if $type eq 'message';  # Sending a message
	}

	my $real_user = $msn->get_user($user) if defined $user;

	return undef if defined $real_user && $real_user->is_user_in_list('forward_list') &&
	    $real_user->status eq 'offline';

	if (!defined $type && $rh->{s_establishing})
	{
		# Quit if the new session is already establishing
		return undef if grep {$_->{user} eq $user} @{$rh->{s_establishing}};
	}

	my $window = find_chat_with_user($user);

	# If we are sending a message we will need to open a window for sure. But in case the chat will
	# timeout or not open for some other reason delay creating this window. Only find a new one to use.

	if (!defined $window)
	{
		$window = find_free_window_for_chat();
		msnre_debug("First chat to be created for $user [$window]");
	}

	my $s_hash = { user => $user, window => $window };

	$s_hash->{message}    = $message if defined $message;                  # SWB message
	$s_hash->{file}       = $file_path if defined $file_path;              # File transfer
	$s_hash->{netmeeting} = 1 if defined $type && $type eq 'netmeeting';   # NetMeeting

	msnre_debug("Queued message for $user") if defined $message;
	push @{$rh->{s_queue}}, $s_hash;

	if ($rh->{s_establishing})
	{
		# Here is one more check because of adding to the queue.
		return undef if grep {$_->{user} eq $user} @{$rh->{s_establishing}};
	}

	push @{$rh->{s_establishing}}, { user => $user, start => time };
	$msn->invite_switchboard($user);
	1;
}

# &MSNre::create_status ( TEXT )
# ----------------------------------------------------------------------
# Create and return text for the first status bar.

sub create_status
{
	my $string = shift;

	$string =~ s/\[(.*?)\]/<%status_text_bracket>\[<%status_text1>$1<%status_text_bracket>\]/g;
	return fix_colors($string);
}

# &MSNre::create_status2 ( TEXT )
# ----------------------------------------------------------------------
# Create and return text for the second status bar.

sub create_status2
{
	my $string = shift;

	$string =~ s/\[(.*?)\]/<%status2_text_bracket>\[<%status2_text1>$1<%status2_text_bracket>\]/g;
	return fix_colors($string);
}

# &MSNre::debug_log ( DEBUG_TEXT )
# ----------------------------------------------------------------------
# Print a debugging message to a log file.

sub debug_log
{
	my $debug_msg = shift;

	# Debugging not enabled
	return if !$rh->{debug}->{debug_log} || !defined $rh->{debug}->{debug_log_dir} || !defined $debug_msg;

	# Create and check the filehandle if it is not done yet

	if (!defined $rh->{debug}->{debug_log_fh})
	{
		require DirHandle;
		my ($new_file_path, $old_n, $dh);

		if (!-d $rh->{debug}->{debug_log_dir})
		{
			# The directory for the log doesn't exist, create it

			if (-e $rh->{debug}->{debug_log_dir})
			{
				# It already exists but it's not a directory

				$rh->{debug}->{debug_log} = undef;
				return undef;
			}
			require File::Path;

			if (!File::Path::mkpath($rh->{debug}->{debug_log_dir}))
			{
				# Failed to create the directory

				$rh->{debug}->{debug_log} = undef;
				return undef;
			}
		}

		# Read the directory content and try to find the newest log file, if there isn't any begin
		# with 0.

		$dh = DirHandle->new($rh->{debug}->{debug_log_dir});

		for ($dh->read)
		{
			if ($_ =~ /^msnre_debug\.(\d+)$/)
			{
				$old_n = $1 if !defined $old_n || $1 > $old_n;
			}
		}

		$old_n = (defined $old_n) ? $old_n + 1 : 0;
		$new_file_path = $rh->{debug}->{debug_log_dir} . '/msnre_debug.' . $old_n;

		$rh->{debug}->{debug_log_fh} = new FileHandle "> $new_file_path";

		if (!defined $rh->{debug}->{debug_log_fh})
		{
			# Couldn't open the log

			$rh->{debug}->{debug_log} = undef;
			return undef;
		}

		$rh->{debug}->{debug_log_fh}->print("Created at " . scalar localtime(time) . "\n\n");
	}

	$rh->{debug}->{debug_log_fh}->print(strftime("%H:%M:%S", localtime));
	$rh->{debug}->{debug_log_fh}->print("\n" . "-" x 75 . "\n" . $debug_msg . "\n" . "-" x 75 . "\n");
	$rh->{debug}->{debug_log_fh}->flush;

	1;
}

# &MSNre::disconnect_file ( SESSION )
# ----------------------------------------------------------------------
# Disconnect a file transfer session.

sub disconnect_file
{
	my $session = shift;
	my $f_hash = file_find_ref_by_session($session);

	$msn->disconnect_file($f_hash->{swb_session}, $f_hash->{file_session});

	file_remove_timer($f_hash->{session});
	$f_hash->{active} = 0;
	1;
}

# &MSNre::disconnect_msn ()
# ----------------------------------------------------------------------
# Disconnect the main server and all the established chat sessions.

sub disconnect_msn
{
	if ($rh->{sessions})   # Switchboard sessions
	{
		for (my $i = 0; $i < @{$rh->{sessions}}; $i++)
		{
			disconnect_swb($i) if $rh->{sessions}->[$i]->{established};
		}
	}

	# Main server and possible some others (all existing connections)
	$msn->disconnect;

	# Print the message about disconnecting to every main window
	for (my $i = 1; $i < @{$rh->{w}->{main}}; $i++)
	{
		if (defined $rh->{w}->{main}->[$i])
		{
			print_event($i, "<%text_highlight>***<%n> Disconnected from the server ".
				    "<%text_highlight>***<%n>\n");
		}
	}

	window_status_update();

	# Cleanup the client's stuff, needed for reconnecting

	$rh->{timers}->{idle}->{start} = undef;      # Idle timer
	$rh->{timers}->{idle}->{set} = undef;
	$rh->{timers}->{idle}->{setting} = undef;

	$rh->{timers}->{typing} = ();                # Other timers
	$rh->{timers}->{swb} = ();
	$rh->{timers}->{ping} = {};
	$rh->{timers}->{connection} = undef;

	$rh->{sessions} = ();                        # Switchboard stuff
	$rh->{s_queue} = ();
	$rh->{s_establishing} = ();

	$rh->{complete_to_add} = ();                 # Passports to add/allow/block
	$rh->{complete_to_al_bl} = ();

	$rh->{misc}->{connecting} = undef;
	$rh->{misc}->{connecting_lattemp} = 0;
	$rh->{misc}->{unread_mail} = undef;

	1;
}

# &MSNre::disconnect_swb ( SWB_SESSION )
# ----------------------------------------------------------------------
# Disconnect a single switchboard session.

sub disconnect_swb
{
	my $session = shift;

	if ($rh->{sessions}->[$session]->{established})
	{
		# Disconnect a connected session

		$rh->{sessions}->[$session]->{established} = 0;
		$msn->disconnect_swb($session);
	}

	if (defined $rh->{sessions}->[$session]->{typing})
	{
		# Delete the user is typing message

		$rh->{sessions}->[$session]->{typing} = undef;
		$rh->{timers}->{typing}->[$session]->{typing_start} = undef;
	}

	window_title_update();
	1;
}

# &MSNre::event_script_call ( EVENT_TYPE, @PARAMETERS )
# ----------------------------------------------------------------------
# Call the even script with specified parameters.

sub event_script_call
{
	return undef if !config_lookup('event_script_use') ||
	    !defined config_lookup('event_script_path') || !-f config_lookup('event_script_path');

	my $event_type = shift;
	my @event_params = @_;

	run_command(config_lookup('event_script_path'), 'MSN', $event_type, @event_params, ' > /dev/null &');
	1;
}

# &MSNre::file_add_timer ( SESSION )
# ----------------------------------------------------------------------
# Add a timer for file transfer. If it stays idle for a long time, cancel (disconnect) it.

sub file_add_timer
{
	my $session = shift;

	$rh->{timers}->{file}->[$session] = time if defined $session;
	1;
}

# &MSNre::file_find_ref_by_session ( SESSION )
# ----------------------------------------------------------------------
# Find a hash reference for file transfer by the (MSNre internal) file session number.

sub file_find_ref_by_session
{
	my $session = shift;
	return if !$rh->{ft_sessions} || !defined $session || !POSIX::isdigit($session);

	for (@{$rh->{ft_sessions}})
	{
		return $_ if $_->{session} == $session;
	}

	undef;
}

# &MSNre::file_find_session ( SWB_SESSION, FILE_SESSION )
# ----------------------------------------------------------------------
# Find a hash reference for file transfer by the real swb and file sessions.

sub file_find_session
{
	my ($swb_session, $file_session) = @_;
	return if !$rh->{ft_sessions} || !defined $swb_session || !defined $file_session;

	for (@{$rh->{ft_sessions}})
	{
		return $_ if $_->{swb_session} == $swb_session && $_->{file_session} == $file_session;
	}

	undef;
}

# &MSNre::file_remove_timer ( SESSION )
# ----------------------------------------------------------------------
# Remove the timer if the file transfer is done.

sub file_remove_timer
{
	my $session = shift;
	return undef if !defined $session || !$rh->{timers}->{file};

	for (my $i = 0; $i < @{$rh->{timers}->{file}}; $i++)
	{
		if ($i == $session)
		{
			splice @{$rh->{timers}->{file}}, $i, 1;
			return 1;
		}
	}

	undef;
}

# &MSNre::file_save_session ( SWB_SESSION, FILE_SESSION, USER, FILE_NAME, FILE_SIZE )
# ----------------------------------------------------------------------
# Create and save a new file transfer session.

sub file_save_session
{
	my ($swb_session, $file_session, $passport, $file_name, $file_size) = @_;
	my $session = ($rh->{ft_sessions}) ? scalar @{$rh->{ft_sessions}} : 0;     # Number of the session

	require File::Basename;

	my $f_hash = {
		swb_session  => $swb_session,
		file_session => $file_session,
		file_name    => File::Basename::basename($file_name),
		file_size    => $file_size,
		session      => $session,
		status       => "Pending",
		user         => $passport,
		active       => 1,                # Internal flag to see 'usable' transfers
	};

	push @{$rh->{ft_sessions}}, $f_hash;
	return $f_hash->{session};
}

# &MSNre::find_chat_with_user ( USER )
# ----------------------------------------------------------------------
# Find a chat session with the specified user.

sub find_chat_with_user
{
	my $user = shift;
	my $window;
	return undef if !$rh->{sessions};

	for (my $i = scalar @{$rh->{sessions}}-1; $i >= 0; $i--)
	{
		next if !defined $rh->{sessions}->[$i]->{window} ||
		    !defined $rh->{w}->{main}->[$rh->{sessions}->[$i]->{window}];

		if ($rh->{sessions}->[$i]->{established})                  # Established session
		{
			# Only sessions with one single user. There is always only one if the session is
			# closed.

			my @u = $msn->get_swb_users($i);
			$window = $rh->{sessions}->[$i]->{window} if scalar @u == 1 && $u[0]->passport eq $user;

			if (defined $window)
			{
				msnre_debug("Found window for $user (EST): [$window]");

				# The session is established for the window
				return $window;
			}
		}

		elsif (defined $rh->{sessions}->[$i]->{first_user})        # Not established (already closed)
		{
			$window = $rh->{sessions}->[$i]->{window} if $rh->{sessions}->[$i]->{first_user} eq $user;
		}

		if (defined $window)  # Check if the window doesn't newly belong to another session
		{
			my $s_e = find_session_by_window_established($window);
			my $s_ne = find_session_by_window($window) if !defined $s_e;

			if (defined $s_e && $s_e ne $i)
			{
				my @u_w = $msn->get_swb_users($s_e);
				undef $window if @u_w && (scalar @u_w != 1 || $u_w[0]->passport ne $user);
			}
			elsif ($s_ne ne $i)   # Exists for sure
			{
				undef $window if defined $rh->{sessions}->[$s_ne]->{first_user} &&
				    $rh->{sessions}->[$s_ne]->{first_user} ne $user;
			}
		}

		if (defined $window)
		{
			msnre_debug("Found window for user: [$window]");
			return $window;
		}
	}
	undef;
}

# &MSNre::find_executable ( FILE_NAME )
# ----------------------------------------------------------------------
# Find an executable and set it as a configuaration option.

sub find_executable
{
	my $name = shift;
	my $conf_opt = config_lookup($name."_path");

	return if defined $conf_opt && -f $conf_opt;

	for my $dir(split /:/, $ENV{PATH})
	{
		if (-f "$dir/$name" && -x "$dir/$name")
		{
			config_set($name."_path", "$dir/$name");
			return 1;
		}
	}

	undef;
}

# &MSNre::find_free_window ()
# ----------------------------------------------------------------------
# Find a free window and return it's number.

sub find_free_window
{
	my $free_win_id;

	for (my $i = 1; $i < @{$rh->{w}->{main}}; $i++)
	{
		return $i if !defined $rh->{w}->{main}->[$i];
	}

	return scalar @{$rh->{w}->{main}};
}

# &MSNre::find_free_window_for_chat ()
# ----------------------------------------------------------------------
# Find a free window usable for chat and return it's number.

sub find_free_window_for_chat
{
	my $free_win_id;

	for (my $i = 1; $i < @{$rh->{w}->{main}}; $i++)
	{
		my $session = find_session_by_window($i);
		return $i if !defined $session && $i > 1;
	}

	return scalar @{$rh->{w}->{main}};
}

# &MSNre::find_session_by_window ( WINDOW_ID )
# ----------------------------------------------------------------------
# Find a switchboard session in a window.

sub find_session_by_window
{
	my $window_id = shift;
	my $session;

	if ($rh->{sessions})
	{
		for (my $i = 0; $i < @{$rh->{sessions}}; $i++)
		{
			next if !defined $rh->{sessions}->[$i]->{window} ||
			    !defined $rh->{w}->{main}->[ $rh->{sessions}->[$i]->{window} ];

			$session = $i if $rh->{sessions}->[$i]->{window} == $window_id;
		}
	}

	$session;
}

# &MSNre::find_session_by_window_established ( WINDOW_ID )
# ----------------------------------------------------------------------
# Find an established switchboard session in a window.

sub find_session_by_window_established
{
	my $window_id = shift;
	my $session;

	if ($rh->{sessions})
	{
		for (my $i = 0; $i < @{$rh->{sessions}}; $i++)
		{
			next if !defined $rh->{sessions}->[$i]->{window} || !defined
			    $rh->{w}->{main}->[ $rh->{sessions}->[$i]->{window} ];
			
			$session = $i if $rh->{sessions}->[$i]->{window} == $window_id &&
			    $rh->{sessions}->[$i]->{established};
		}
	}

	$session;
}

# &MSNre::find_update ()
# ----------------------------------------------------------------------
# Look for MSNre updates on the sf.net.

sub find_update
{
	$rh->{misc}->{no_version_lookup}++;

	eval { require LWP::Simple };

	if ($@)
	{
		msnre_debug("LWP::Simple not installed. Version lookup aborted.");
		return undef;;
	}

	my @versions;
	my $newest = $VERSION;

	print_event(1, "\n<%text_msnre>[$NAME]<%n> *** Looking for new versions of $NAME");
	window_main_update();

	# Downlaod the page using LWP::Simple
	my $msnre_page = LWP::Simple::get('http://prdownloads.sourceforge.net/msnre');

	if (defined $msnre_page)
	{
		# Save the versions from the hyperlinks
		push @versions, $msnre_page =~ m{  <A\s+HREF=\"/msnre/msnre-(\d+\.\S+?)\.tar\S+?\"  }gix;
	}

	# Find the newest version
	if (@versions)
	{
	      V_LOOP: for (@versions)
		{
			my @t_version = split /\./, $_;
			my @n_version = split /\./, $newest;

			for (my $i=0; $i<@n_version; $i++)
			{
				next if (!defined $t_version[$i] && defined $n_version[$i]) ||
				    ($t_version[$i] < $n_version[$i]);

				if ($t_version[$i] > $n_version[$i])
				{
					$newest = $_;
					next V_LOOP;
				}
			}
		}
	}

	if ($newest ne $VERSION)
	{
		print_event(1,
			    "<%text_msnre>[$NAME]<%n> *** A newer version of $NAME is available: ".
			    "<%text_highlight>$newest<%n>\n");
	}
	else
	{
		print_event(1,
			    "<%text_msnre>[$NAME]<%n> *** Your version of $NAME is up to date\n");
	}
	1;
}

# &MSNre::fix_colors ( TEXT )
# ----------------------------------------------------------------------
# Substitute the named color tags with real colors.

sub fix_colors
{
	my $message = shift;
	my $t_message = (ref($message)) ? $$message : $message;
	my $color;

	if ($rh->{misc}->{use_colors})
	{
		while ($t_message =~ m{ < \%(\S+?) > }gsmx)
		{
			my $name = $1;
			next if $name eq 'n' || POSIX::isdigit($name);

			$color = (length($name) == 1) ? $rh->{clr}->{color}->{$name} : color_lookup($name);
			$t_message =~ s/<\%$name>/<\%$color>/g if defined $color;
		}
	}

	if (ref($message))
	{
		$$message = $t_message;
		return 1;
	}

	$t_message;
}

# &MSNre::get_all_users ( %ARGS )
# ----------------------------------------------------------------------
# Return an array with users.

sub get_all_users
{
	my %args = @_;
	my @t_users = $msn->get_all_users;

	my @users;

	@t_users = grep {$_->is_user_in_list($args{list})} @t_users if defined $args{list};
	@t_users = grep {$_->connected} @t_users if defined $args{online};
	@t_users = grep {defined $_->mobile_device && $_->mobile_device eq 'Y'} @t_users if $args{mobile};

	push @users, $_->{passport} for sort {$a->{passport} cmp $b->{passport}} @t_users;

	if ((!$args{list} && !$args{online} && !$args{mobile}) || $args{locals})
	{
		push @users, $_->{passport} for sort {$a->{passport} cmp $b->{passport}} contacts_get_locals();
	}

	@users;
}

# &MSNre::get_filehandle ( USER )
# ----------------------------------------------------------------------
# Create or find and return a filehandle for messages logging.

sub get_filehandle
{
	my $passport = shift;
	$passport =~ s/\@(\S[^.]+?)\..*$/__at__$1/;

	return undef if !defined config_lookup('history');
	return $rh->{fhs}->{$passport} if defined $rh->{fhs}->{$passport};

	my $h_directory = config_lookup('history');
	return undef if !defined $h_directory || !-d $h_directory;  # Should be created in read_config

	my $file_name = "$h_directory/$passport.log";

	# Create a new filehandle
	$rh->{fhs}->{$passport} = new FileHandle ">> $file_name";

	if (!defined $rh->{fhs}->{$passport})
	{
		msnre_error("Couldn't write to history file ($file_name)\n");
		return undef;
	}

	if (!-f $file_name)
	{
		$rh->{fhs}->{$passport}->print("History file created on " . scalar localtime(time) . "\n\n");
	}
	else
	{
		$rh->{fhs}->{$passport}->print("\n");
	}

	$rh->{fhs}->{$passport};
}

# &MSNre::get_time ( TIME )
# ----------------------------------------------------------------------
# Create a readable time and return it.

sub get_time
{
	my $time = shift;
	my $days = $time / 86400; $time %= 86400;
	my $hours = $time / 3600; $time %= 3600;
	my $minutes = $time / 60; $time %= 60;

	return sprintf("%dd %02dh %02dm %02ds", $days, $hours, $minutes, $time);
}

# &MSNre::get_timestamp ()
# ----------------------------------------------------------------------
# Create and return a timestamp.

sub get_timestamp
{
	my $c_strftime = config_lookup('timestamp_format');
	my $time = "";

	if (defined $c_strftime)
	{
		$time = strftime($c_strftime . " ", localtime);
	}
	$time;
}

# &MSNre::lookup_key_bind ( KEYSTROKE )
# ----------------------------------------------------------------------
# Find an array with actions for a key binding.

sub lookup_key_bind
{
	my $to_lookup;
	my @all_binds;

	if (defined $rh->{w}->{input_v}->{prefix})
	{
		# Prefix is <meta> or <ctrl> x, add it to the current keystroke

		$to_lookup = $rh->{w}->{input_v}->{prefix};
		$rh->{w}->{input_v}->{prefix} = undef;
	}

	$to_lookup .= shift;
	$to_lookup =~ s/\s//g;

	# The current profile

	if (exists $rh->{profile}->{ $rh->{profile_use} }->{key_bind}->{bind})
	{
		for (keys %{$rh->{profile}->{ $rh->{profile_use} }->{key_bind}->{bind}})
		{
			my $_bind = $_;     $_bind =~ s/\s//g;

			return @{$rh->{profile}->{ $rh->{profile_use} }->{key_bind}->{bind}->{$_}} if
			    $_bind eq $to_lookup;
		}
	}

	# The DEFAULT profile

	if ($rh->{profile_use} ne 'DEFAULT' && exists $rh->{profile}->{DEFAULT}->{key_bind}->{bind})
	{
		for (keys %{$rh->{profile}->{DEFAULT}->{key_bind}->{bind}})
		{
			my $_bind = $_;     $_bind =~ s/\s//g;

			return @{$rh->{profile}->{DEFAULT}->{key_bind}->{bind}->{$_}} if
			    $_bind eq $to_lookup;
		}
	}

	return ();
}

# &MSNre::msnre_debug ( TEXT )
# ----------------------------------------------------------------------
# Print a debugging message to the debug window if it's enabled.

sub msnre_debug
{
	if (config_lookup('debug_msnre') && defined $rh->{w}->{main}->[0] && defined $_[0])
	{
		print_window(0, strftime("%H:%M:%S ", localtime) . $_[0] . "\n");
		debug_log($_[0]) if $rh->{debug}->{debug_log};

		$rh->{w}->{main}->[0]->noutrefresh if $rh->{w}->{current} == 0;
	}
	1;
}

# &MSNre::msnre_error ( TEXT )
# ----------------------------------------------------------------------
# Print an error message.

sub msnre_error
{
	my $error = shift;

	print_event(1, "<%text_error>[Error]<%n> $error");
	1;
}

# &MSNre::need_window_chat ( SWB_SESSION )
# ----------------------------------------------------------------------
# Return 1 if the switchboard session needs a new window to be created.

sub need_window_chat
{
	my $session = shift;

	if ($rh->{sessions} && defined $rh->{sessions}->[$session]->{window})
	{
		return 1 if !defined $rh->{w}->{main}->[ $rh->{sessions}->[$session]->{window} ];
	}
	undef;
}

# &MSNre::netmeeting_find_session ( SWB_SESSION, NM_SESSION )
# ----------------------------------------------------------------------
# Find a netmeeting session by the server sessions and return it.

sub netmeeting_find_session
{
	my ($swb_session, $nm_session) = @_;
	return undef if !$rh->{nm_sessions};

	for (my $i = 0; $i < @{$rh->{nm_sessions}}; $i++)
	{
		return $rh->{nm_sessions}->[$i] if
		    $rh->{nm_sessions}->[$i]->{swb_session} eq $swb_session &&
		    $rh->{nm_sessions}->[$i]->{nm_session} eq $nm_session;
	}

	undef;
}

# &MSNre::netmeeting_invite ( SWB_SESSION )
# ----------------------------------------------------------------------
# Invite a user to use netmeeting.

sub netmeeting_invite
{
	my $swb_session = shift;
	my $local_session = ($rh->{nm_sessions}) ? scalar @{$rh->{nm_sessions}} : 0;
	my $w = $rh->{sessions}->[$swb_session]->{window};

	$rh->{nm_sessions}->[$local_session] = { user => $msn->passport, swb_session => $swb_session,
						 session => $local_session, active => 1 };

	window_main_create_chat($swb_session) if need_window_chat($swb_session);
	print_event($w, "Outgoing NetMeeting invitation: NetMeeting session <%text_highlight>[$local_session]");

	$rh->{nm_sessions}->[$local_session]->{nm_session} = $msn->netmeeting_invite($swb_session);
	1;
}

# &MSNre::real_length ( TEXT )
# ----------------------------------------------------------------------
# Return a length of a string with unicode characters.

# XXX: TODO: FIXME:
# This is only a very temporary fix to not print uglies in UTF-8.

sub real_length
{
	my $string = shift;
	my $length = 0;

	if (defined $string)
	{
		for (split '', $string)
		{
			$length++ if !(ord($_) & 0x80) || (ord($_) & 0x40);
		}
	}

	$length;
}

# &MSNre::resize_window ()
# ----------------------------------------------------------------------
# Resize the curses window after SIGWINCH.

sub resize_window
{
	endwin;
	refresh;

	getmaxyx(stdscr, $Rows, $Columns);

	for (my $i = 0; $i <= @{$rh->{w}->{main}}; $i++)   # For every main window
	{
		next if !defined $rh->{w}->{main}->[$i];

		# Move and resize the title bar

		$rh->{w}->{title}->[$i]->mvwin(0, 0);
		$rh->{w}->{title}->[$i]->resize(1, $Columns);

		# Move the main window

		($rh->{w}->{main_v}->[$i]->{is_title})
		    ? $rh->{w}->{main}->[$i]->mvwin(1, 0) : $rh->{w}->{main}->[$i]->mvwin(0, 0);

		# Lines in the main window

		my $lines = $Rows - 2;

		$lines-- if $rh->{w}->{main_v}->[$i]->{is_title};
		$lines-- if $rh->{w}->{main_v}->[$i]->{is_double};

		# Resize the main window

		$rh->{w}->{main}->[$i]->resize($lines, $Columns);
		$rh->{w}->{main_v}->[$i]->{screen_size} = $lines;

		# Move and resize the first status bar

		($rh->{w}->{main_v}->[$i]->{is_double})
		    ? $rh->{w}->{status}->[$i]->mvwin($Rows - 3, 0)
		    : $rh->{w}->{status}->[$i]->mvwin($Rows - 2, 0);

		$rh->{w}->{status}->[$i]->resize(1, $Columns);

		# Move and resize the second status bar

		$rh->{w}->{status2}->[$i]->mvwin($Rows - 2, 0);
		$rh->{w}->{status2}->[$i]->resize(1, $Columns);
	}

	# Move and resize the input window

	$rh->{w}->{input}->mvwin($Rows - 1, 0);
	$rh->{w}->{input}->resize(1, $Columns);

	# Re-draw and update the windows
	$rh->{w}->{main}->[$rh->{w}->{current}]->redrawwin;
	$rh->{w}->{main}->[$rh->{w}->{current}]->noutrefresh;

	window_title_update();
	window_status_update();
	window_input_update();

	1;
}

# &MSNre::run_command ( COMMAND )
# ----------------------------------------------------------------------
# Run a shell command.

sub run_command
{
	my $command = join ' ', @_ if @_;

	if (defined $command)
	{
		msnre_debug("Running command: $command");
		return system $command;
	}
	undef;
}

# &MSNre::save_local_params ( SWB_SESSION, USER )
# ----------------------------------------------------------------------
# Save parameters for local contacts (contacts in established swb session that are not in the
# contact list).

sub save_local_params
{
	my ($swb_session, $user) = @_;
	return if !defined $swb_session || !defined $user || defined $msn->get_user($user);

	my @to_find = qw(chat_logging ip_address user_agent);   # Stuff to save
	my $contact;

	my $c_user = contacts_get_contact($user);

	if (!defined $c_user)
	{
		contacts_add(passport => $user);
		$c_user = contacts_get_contact($user);
	}

	for ($msn->get_swb_users($swb_session))
	{
		if ($_->passport eq $user)
		{
			$contact = $_;  last;
		}
	}
	return undef if !defined $contact;

	for my $cat(@to_find)
	{
		$c_user->{$cat} = $contact->$cat if defined $contact->$cat && !defined $c_user->{$cat};
	}
	1;
}

# &MSNre::send_file ( SWB_SESSION, FILE_PATH )
# ----------------------------------------------------------------------
# Send a file to a switchboard session.

sub send_file
{
	my ($swb_session, $file_path) = @_;

	require File::Spec;
	$file_path = File::Spec->rel2abs($file_path) if !File::Spec->file_name_is_absolute($file_path);

	my $file_session = $msn->file_invite($swb_session, $file_path);
	my $w = $rh->{sessions}->[$swb_session]->{window};

	my $f_hash = file_find_ref_by_session(
		file_save_session($swb_session, $file_session, $msn->passport, $file_path, -s $file_path));

	$f_hash->{type} = 'outgoing';
	$f_hash->{status} = "Invited; Pending";
	$f_hash->{can_cancel} = 1;

	file_add_timer($f_hash->{session});
	window_main_create_chat($swb_session) if need_window_chat($swb_session);

	my $ft_size = sprintf "%.3f kB", $f_hash->{file_size} / 1024;

	print_event($w, "\n" . print_line('Outgoing File Transfer'));

	print_line_text($w, 'File name    ', $f_hash->{file_name});
	print_line_text($w, 'File size    ', $ft_size);
	print_line_text($w, 'File session ', $f_hash->{session} . "\n");
	1;
}

# &MSNre::send_message ( SWB_SESSION, MESSAGE )
# ----------------------------------------------------------------------
# Send a message to a switchboard session.

sub send_message
{
	my ($session, $message) = @_;
	my %attributes;
 
	$attributes{user_agent} = "$NAME/$VERSION";   # Send client name and version with the message

	$msn->send_message($session, $message, %attributes);

	if ($rh->{timers}->{typing}->[$session] && $rh->{timers}->{typing}->[$session]->{last_sent})
	{
		# Do not continue sending the User is typing... message
		$rh->{timers}->{typing}->[$session]->{last_sent} = undef;
	}
	1;
}

# &MSNre::truncate_name ( NAME, LENGTH )
# ----------------------------------------------------------------------
# Truncate given name to a specified length.

sub truncate_name
{
	my ($name, $length) = @_;

	if (defined $$name && defined $length)
	{
		substr ($$name, $length-3) = '...';
	}

	$$name;
}

# &MSNre::utf8_e ( TEXT )
# ----------------------------------------------------------------------
# Decode a string in the current locale and encode it in UTF-8.

sub utf8_e
{
	my $string = shift;

	if ($rh->{config}->{encoding} !~ /^utf-?8$/i)
	{
		my $new_string = Encode::encode_utf8(Encode::decode($rh->{config}->{encoding}, $string));
		return $new_string;
	}

	$string;
}

# &MSNre::utf8_l ( TEXT )
# ----------------------------------------------------------------------
# Decode UTF-8 string and encode it in the current system locale.

sub utf8_l
{
	my $string = shift;

	if ($rh->{config}->{encoding} !~ /^utf-?8$/i)
	{
		my $new_string = Encode::encode($rh->{config}->{encoding}, Encode::decode_utf8($string));
		return $new_string;
	}

	$string;
}

# &MSNre::validate_group ( GROUP_NAME )
# ----------------------------------------------------------------------
# Check if a group is valid and return 1 if it is.

sub validate_group
{
	my $group = join ' ', @_ if @_;

	if (defined $group)
	{
		# Get group ID for the group name
		my $group_id = $msn->get_group_id_by_name($msn->url_encode($group));

		if (!defined $group_id)
		{
			print_event($rh->{w}->{current},
				    "Group <%text_highlight>$group<%n> is not present in your contact list.");
			return undef;
		}
		return 1;
	}
	undef;
}

# &MSNre::validate_user ( USER )
# ----------------------------------------------------------------------
# Check if a user passport is valid and return 1 if it is.

sub validate_user
{
	my $user = shift;
	my %args = @_;

	if (defined $user)
	{
		if ($user !~ /^\S+?\@\S+/)   # See if it's a valid passport
		{
			print_event($rh->{w}->{current},
				    "Passport <%text_highlight>$user<%n> is not a valid e-mail address.");
			return undef;
		}

		my $real_user = $msn->get_user($user);

		if (!defined $real_user || (defined $args{list} && !$real_user->is_user_in_list(lc($args{list}))))
		{
			my $list = 'contact list';

			if (defined $args{list} && $args{list} ne 'forward_list')
			{
				$list = $args{list};   $list =~ s/_/ /g;
			}

			print_event($rh->{w}->{current},
				    "User <%text_highlight>$user<%n> is not present in your $list.");
			return undef;
		}
		return 1;
	}
	undef;
}

# -------------------- Client commands -------------------- #

# &MSNre::cmd_about ()
# ----------------------------------------------------------------------
# /ABOUT

sub cmd_about
{
	print_event($rh->{w}->{current}, "\n" . print_line('About the client'));

	print_line_text(undef, "Client version     ", "$NAME $VERSION ($DATE)");
	print_line_text(undef, "Client uptime      ", get_time(time - $st_time));
	print_line_text(undef, "Running since      ", scalar localtime($st_time));
	print_line_text(undef, "$NAME Project page ", 'http://www.sourceforge.net/projects/msnre');
	print_line_text(undef, "$NAME Author       ", 'incoming@tiscali.cz' . "\n");
	1;
}

# &MSNre::cmd_account ()
# ----------------------------------------------------------------------
# /ACCOUNT

sub cmd_account
{
	check_connection() || return undef;

	if ($msn->passport !~ /\@(hotmail|msn)\.com$/i)   # Needs a valid @hotmail.com or @msn.com address
	{
		print_event($rh->{w}->{current},
			"You need to have hotmail.com or msn.com e-mail address to be able to use these features.");
		return undef;
	}
	$msn->get_hotmail('PERSON');
	1;
}

# &MSNre::cmd_add_forward ( WINDOW | USER )
# ----------------------------------------------------------------------
# /ADDFORWARD

sub cmd_add_forward
{
	check_connection() || return undef;

	if (!@_ || !defined $_[0])
	{
		print_usage('addforward');
		return undef;
	}

	if (POSIX::isdigit($_[0]))     # The 1st parameter - window number
	{
		if ($rh->{forward}->{window} && grep {$_ eq $_[0]} @{$rh->{forward}->{window}})
		{
			print_event($rh->{w}->{current},
				    "Forward already defined for window <%text_highlight>[$_[0]]");
			return undef;
		}

		# Add the window to the list for forwarding
		push @{$rh->{forward}->{window}}, $_[0];
		print_event($rh->{w}->{current}, "Messages will be forwarded to window <%text_highlight>[$_[0]]");
	}
	else                           # User passport
	{
		if ($_[0] !~ /^\S+?\@\S+/)   # Needs a valid passport
		{
			print_event($rh->{w}->{current},
				    "You need to specify a valid e-mail address or window number.");
			return undef;
		}

		if ($rh->{forward}->{user} && grep {$_ eq $_[0]} @{$rh->{forward}->{user}})
		{
			print_event($rh->{w}->{current},
				    "Forward already defined for user <%text_highlight>$_[0]<%n>.");
			return undef;
		}

		# Add the user to the list for forwarding
		push @{$rh->{forward}->{user}}, $_[0];
		print_event($rh->{w}->{current}, "Messages will be forwarded to user <%text_highlight>$_[0]");
	}
	1;
}

# &MSNre::cmd_add_group ( GROUP_NAME )
# ----------------------------------------------------------------------
# /ADDGROUP

sub cmd_add_group
{
	check_connection() || return undef;

	my $group = join ' ', @_ if @_;      # The name of the new group

	if (defined $group)
	{
		# Do not allow two groups with the same name
		if (defined $msn->get_group_id_by_name($msn->url_encode($group)))
		{
			print_event($rh->{w}->{current}, "Group <%text_highlight>$group<%n> already exists.");
			return undef;
		}

		if (real_length($msn->url_encode($group)) > 60)     # 60 characters max.
		{
			print_event($rh->{w}->{current}, "Group name must be up to 60 characters long.");
			return undef;
		}

		return $msn->add_group(utf8_e($group));   # Encode the name in UTF-8
	}
	print_usage('addgroup');
	undef;
}

# &MSNre::cmd_add_user ( USER [, GROUP_NAME ] )
# ----------------------------------------------------------------------
# /ADDUSER

sub cmd_add_user
{
	check_connection($rh->{w}->{current}) || return undef;

	my $user = shift;
	my $group = join ' ', @_ if @_;

	if (defined $user)
	{
		my $real_user = $msn->get_user($user);
		my $group_id;

		if ($user !~ /\S+?\@\S+/)   # Needs a valid passport
		{
			print_event($rh->{w}->{current}, "Passport <%text_highlight>$user<%n> is not a valid ".
				    "e-mail address.");
			return undef;
		}

		if (!defined $group)        # Add to an implicit group
		{
			if (defined $real_user && $real_user->is_user_in_list('forward_list'))
			{
				print_event($rh->{w}->{current},
					    "User <%text_highlight>$user<%n> is already present in the contact list.");
				return undef;
			}

			$msn->add_user($user, 'forward_list');
		}
		else                        # Add to a specific group
		{
			# Get the group ID for the specified name
			$group_id = $msn->get_group_id_by_name($msn->url_encode($group));

			if (!defined $group_id)
			{
				print_event($rh->{w}->{current},
					    "Group <%text_highlight>$group<%n> is not present in your contact list.");
				return undef;
			}

			if (defined $real_user && $real_user->is_user_in_list('forward_list') &&
			    $real_user->group && grep {$group_id == $_} @{$real_user->group})
			{
				print_event($rh->{w}->{current}, "User <%text_highlight>$user<%n> is already ".
					    "in group <%text_highlight>$group<%n>.");
				return undef;
			}

			$msn->add_user($user, 'forward_list', $group_id);
		}
		return 1;
	}
	print_usage('adduser');
	undef;
}

# &MSNre::cmd_alias_command ( COMMAND [, VALUE  ] )
# ----------------------------------------------------------------------
# /ALERT

sub cmd_alert
{
	my $command = shift;
	$command = 'LIST' if !defined $command;

	if (uc($command) eq 'LIST')    # Print a list of received alerts
	{
		if (!$rh->{alert})
		{
			print_event($rh->{w}->{current},
				    "<%text_highlight>$NAME<%n>: You have not received any alerts yet.");
			return undef;
		}
		my $win = $rh->{w}->{current};

		for (my $i = 0; $i < @{$rh->{alert}}; $i++)
		{
			print_event($win, "<%text_highlight>[$i]<%n>");
			print_event($win, "Source URL : <%text_highlight>$rh->{alert}->[$i]->{source}");
			print_event($win, "Text       : <%text_highlight>$rh->{alert}->[$i]->{text}\n");
		}
		return 1;
	}

	if (uc($command) eq 'GO')      # Go to an alert URL
	{
		if (!defined $_[0] || !POSIX::isdigit($_[0]) || !$rh->{alert} || !defined $rh->{alert}->[$_[0]])
		{
			print_event($rh->{w}->{current},
				    "<%text_highlight>$NAME<%n>: The parameter has to be a valid alert number.");
			return undef;
		}
		return browser_start($rh->{alert}->[$_[0]]->{action});
	}

	if (uc($command) eq 'CLEAR')   # Clear all the alerts
	{
		delete $rh->{alert};
		print_event($rh->{w}->{current}, "<%text_highlight>$NAME<%n>: Alerts list was cleared.");
		return 1;
	}

	if (uc($command) eq 'REMOVE')  # Remove a single alert
	{
		if (!defined $_[0] || !POSIX::isdigit($_[0]) || !$rh->{alert} || !defined $rh->{alert}->[$_[0]])
		{
			print_event($rh->{w}->{current},
				    "<%text_highlight>$NAME<%n>: The parameter has to be a valid alert number.");
			return undef;
		}

		$rh->{alert}->[$_[0]] = undef;
		print_event($rh->{w}->{current},
			    "<%text_highlight>$NAME<%n>: Alert <%text_highlight>[$_[0]]<%n> was removed.");
		return 1;
	}

	print_event($rh->{w}->{current}, "<%text_highlight>$NAME<%n>: Unknown /ALERT command: " .
		    "[<%text_highlight>$command<%n>]");
	undef;
}

# &MSNre::cmd_alias_command ( [ COMMAND, PARAMETERS ] )
# ----------------------------------------------------------------------
# /ALIASCMD

sub cmd_alias_command
{
	if (!@_)   # Print existing command aliases
	{
		if (!$rh->{profile}->{ $rh->{profile_use} }->{aliases}->{COMMAND} &&
		    !$rh->{profile}->{DEFAULT}->{aliases}->{COMMAND})
		{
			print_event($rh->{w}->{current}, "No command aliases defined.");
			return undef;
		}

		my @to_lookup = $rh->{profile_use};
		push @to_lookup, 'DEFAULT' if $rh->{profile_use} ne 'DEFAULT';

		# Read from the current and DEFAULT profiles

		for (grep {exists $rh->{profile}->{$_}->{aliases}->{COMMAND} &&
			   keys %{$rh->{profile}->{$_}->{aliases}->{COMMAND}}} @to_lookup)
		{
			my $cmd_ref = $rh->{profile}->{$_}->{aliases}->{COMMAND};
			my $max_len = 0;

			print_event($rh->{w}->{current},
				    "<%text_highlight>$NAME<%n>: Aliases for <%text_highlight>$_<%n> profile");

			for (keys %{$cmd_ref}) { $max_len = real_length($_) if real_length($_) > $max_len }

			for (sort keys %{$cmd_ref})
			{
				my $cur_params = join ' ', @{$cmd_ref->{$_}->{old_params}};

				print_event($rh->{w}->{current}, " " x 5 . '/'.uc($_) . " " x 5 .
				    " " x ($max_len-real_length($_)).'/'.uc($cmd_ref->{$_}->{old})." $cur_params");
			}
			print_window($rh->{w}->{current}, "\n");
		}
		return 1;
	}

	elsif (@_ < 2)
	{
		print_usage('alias');
		return undef;
	}

	# Adding a new command alias
	my $alias_name = shift;

	if (exists $rh->{command}->{lc($alias_name)} && !$rh->{command}->{lc($alias_name)}->{alias})
	{
		print_event($rh->{w}->{current},
			    "Command [<%text_highlight>".uc($alias_name)."<%n>] already exists.");
		return undef;
	}

	my $command = uc shift;
	$command .= ' ' . join ' ', @_ if @_;

	if (add_alias_cmd($rh->{profile_use}, $alias_name, $command))
	{
		print_event($rh->{w}->{current},
			    "New alias command <%text_highlight>".uc($alias_name)."<%n> was added ".
			    "<%text_highlight>[$command]");
	}
	else
	{
		print_event($rh->{w}->{current}, "Failed to create the alias command.");
	}
	1;
}

# &MSNre::cmd_alias_contact ( [ USER, ALIAS ] )
# ----------------------------------------------------------------------
# /ALIASCN

sub cmd_alias_contact
{
	if (!@_)   # Print existing contact aliases
	{
		if (!$rh->{profile}->{ $rh->{profile_use} }->{aliases}->{CONTACT} &&
		    !$rh->{profile}->{DEFAULT}->{aliases}->{CONTACT})
		{
			print_event($rh->{w}->{current}, "No aliases for contacts defined.");
			return undef;
		}

		my @to_lookup = $rh->{profile_use};
		push @to_lookup, 'DEFAULT' if $rh->{profile_use} ne 'DEFAULT';

		for (grep {exists $rh->{profile}->{$_}->{aliases}->{CONTACT} &&
			   keys %{$rh->{profile}->{$_}->{aliases}->{CONTACT}}} @to_lookup)
		{
			my $con_ref = $rh->{profile}->{$_}->{aliases}->{CONTACT};
			my $max_len = 0;

			print_event($rh->{w}->{current},
				    "<%text_highlight>$NAME<%n>: Aliases for <%text_highlight>$_<%n> profile");

			for (keys %{$con_ref}) { $max_len = real_length($_) if real_length($_) > $max_len }

			for (sort keys %{$con_ref})
			{
				my @c_aliases = @{$con_ref->{$_}};
				print_event($rh->{w}->{current}, "  $_  " . " " x ($max_len-real_length($_)) .
					    shift @c_aliases);

				for (@c_aliases)
				{
					print_event($rh->{w}->{current},
						    " " x (real_length($_)+4+($max_len-real_length($_))).
						    shift @c_aliases);
				}
			}
			print_window($rh->{w}->{current}, "\n");
		}
		return 1;
	}

	elsif (@_ < 2)
	{
		print_usage('aliascn');
		return undef;
	}

	# Adding a new contact alias
	my $user = shift;
	my $alias = join ' ', @_;

	if ($user !~ /^\S+?\@\S+/)   # Needs a valid passport
	{
		print_event($rh->{w}->{current},
			    "Passport <%text_highlight>$user<%n> is not a valid e-mail address.");
		return undef;
	}

	push @{$rh->{profile}->{ $rh->{profile_use} }->{aliases}->{CONTACT}->{$user}}, $alias;
	print_event($rh->{w}->{current}, "Alias for <%text_highlight>$user<%n> was added.");
	1;
}

# &MSNre::cmd_allow_user ( @USERS )
# ----------------------------------------------------------------------
# /ALLOW

sub cmd_allow_user
{
	check_connection() || return undef;
	my @users = @_;

	if (!@users)
	{
		print_usage('allow');
		return undef;
	}

	for my $user(@users)   # Try to allow every user specified on the command line
	{
		my $real_user = $msn->get_user($user);

		if (defined $real_user)
		{
			if ($real_user->is_user_in_list('allow_list'))
			{
				print_event($rh->{w}->{current},
					    "User <%text_highlight>$user<%n> is already in the allow list.");
				next;
			}

			if ($real_user->is_user_in_list('block_list'))
			{
				print_event($rh->{w}->{current}, "User <%text_highlight>$user<%n> is blocked.");
				next;
			}

			$user = $real_user->passport;
		}

		elsif ($user !~ /^\S+?\@\S+/)   # Needs a valid passport
		{
			print_event($rh->{w}->{current}, "Passport <%text_highlight>$user<%n> is not ".
				    "a valid e-mail address.");
			next;
		}

		$msn->add_user($user, 'allow_list');
	}
	1;
}

# &MSNre::cmd_away_msg ( [ TYPE ] )
# ----------------------------------------------------------------------
# /AWAYMSG

sub cmd_away_msg
{
	my $type = shift;
	my $message = join ' ', @_ if @_;

	# Set a new auto away message for the specified status

	if (defined $type)
	{
		if ($type =~ /^(idle|away|brb|busy|lunch|phone)$/i)
		{
			if (!defined $message)
			{
				print_event($rh->{w}->{current}, "Specify a new auto-away message to change it.");
			}
			else
			{
				config_set("auto_".lc($type)."_msg", $message);
				print_event($rh->{w}->{current},
					    "<%text_highlight>".uc($type) . "<%n> message was changed to: $message");
			}
		}
		else
		{
			print_event($rh->{w}->{current}, "Unknown message type: [<%text_highlight>$type<%n>]");
			print_event($rh->{w}->{current},
				    "Available message types are: IDLE AWAY BRB BUSY LUNCH PHONE\n");
		}
		return 1;
	}

	# Print all the auto away messages

	my $used = (config_lookup('auto_away_use')) ? "<Yes>" : "<No>";

	my $idle  = config_lookup('auto_idle_msg')    || "<Not defined>";
	my $away  = config_lookup('auto_away_msg')    || "<Not defined>";
	my $brb   = config_lookup('auto_brb_msg')     || "<Not defined>";
	my $busy  = config_lookup('auto_busy_msg')    || "<Not defined>";
	my $lunch = config_lookup('auto_lunch_msg')   || "<Not defined>";
	my $phone = config_lookup('auto_phone_msg')   || "<Not defined>";

	print_event($rh->{w}->{current}, "\n" . print_line('Auto-away Messages'));

	print_line_text(undef, 'Auto-away messages used        ',   $used);
	print_line_text(undef, 'Auto message for Idle          ',   $idle);
	print_line_text(undef, 'Auto message for Away          ',   $away);
	print_line_text(undef, 'Auto message for Be Right Back ',   $brb);
	print_line_text(undef, 'Auto message for Busy          ',   $busy);
	print_line_text(undef, 'Auto message for Out to Lunch  ',   $lunch);
	print_line_text(undef, 'Auto message for On the Phone  ',   $phone . "\n");

	1;
}

# &MSNre::cmd_block_user ( @USERS )
# ----------------------------------------------------------------------
# /BLOCK

sub cmd_block_user
{
	check_connection() || return undef;
	my @users = @_;

	if (!@users)
	{
		print_usage('block');
		return undef;
	}

	for my $user(@users)
	{
		my $real_user = $msn->get_user($user);

		if (defined $real_user)
		{
			if ($real_user->is_user_in_list('block_list'))
			{
				print_event($rh->{w}->{current},
					    "User <%text_highlight>$user<%n> is already blocked.");
				next;
			}

			$user = $real_user->passport;
		}
		elsif ($user !~ /^\S+?\@\S+/)   # Needs a valid passport
		{
			print_event($rh->{w}->{current}, "Passport <%text_highlight>$user<%n> is not ".
				    "a valid e-mail address.");
			next;
		}

		$msn->block_user($user);
	}
	1;
}

# &MSNre::cmd_change_profile ( PROFILE_NAME )
# ----------------------------------------------------------------------
# /CHPROFILE

sub cmd_change_profile
{
	my $profile_name = join ' ', @_ if @_;

	if (defined $profile_name)   # Switch to a new profile
	{
		if ($profile_name eq $rh->{profile_use})
		{
			print_event($rh->{w}->{current},
				    "Profile <%text_highlight>$profile_name<%n> is already used.");
			return undef;
		}
		if (!defined $rh->{profile}->{$profile_name})
		{
			print_event($rh->{w}->{current},
				    "Profile <%text_highlight>$profile_name<%n> is not available.");
			return undef;
		}

		$rh->{profile_use} = $profile_name;
		color_register();  # Register the colors for the new profile

		# Re-set the color attributes for all the windows
		for (my $i=0; $i<@{$rh->{w}->{main}}; $i++)
		{
			if (defined $rh->{w}->{main}->[$i])
			{
				# Set the color attributes to the new ones

				$rh->{w}->{title}->[$i]->attron(color_lookup('title_text'));
				$rh->{w}->{status}->[$i]->attron(color_lookup('status_text1'));
				$rh->{w}->{status2}->[$i]->attron(color_lookup('status2_text1'));
			}
		}

		# Redraw and update the current window
		$rh->{w}->{main_v}->[$rh->{w}->{current}]->{scrolling} = 1;
		window_main_redraw($rh->{w}->{current}, 0);

		window_title_update();
		window_status_update();
		window_input_update();

		print_event($rh->{w}->{current},
			    "The current profile was changed to <%text_highlight>$profile_name<%n>.");
	}
	else    # Print a list of available profiles
	{
		print_event($rh->{w}->{current}, "<%text_highlight>$NAME<%n>: Available profiles");

		my $num = 0;
		my $total = keys %{$rh->{profile}};
		for (sort keys %{$rh->{profile}})
		{
			my $name = $_;  $name .= "  [<%text_highlight>Current<%n>]" if $rh->{profile_use} eq $_;

			print_event($rh->{w}->{current},
				    " " x 5 . "<%text_bracket>[<%n>" . ++$num . "<%text_bracket>]<%n>  " . 
				    " " x (real_length($total)-real_length($_)) . $name);
		}
		print_window($rh->{w}->{current}, "\n");
	}
	1;
}

# &MSNre::cmd_clear ( [ WINDOW_ID ] )
# ----------------------------------------------------------------------
# /CLEAR

sub cmd_clear
{
	my $window_id = shift;
	$window_id = $rh->{w}->{current} if !defined $window_id || !defined $rh->{w}->{main}->[$window_id];

	print_window($window_id, "", 1);            # Flush the newlines
	$rh->{w}->{main}->[$window_id]->erase;

	if ($window_id == $rh->{w}->{current})
	{
		$rh->{w}->{main}->[$window_id]->noutrefresh;
		doupdate;
	}
	1;
}

# &MSNre::cmd_clear_all ()
# ----------------------------------------------------------------------
# /CLEARALL

sub cmd_clear_all
{
	for (0..scalar @{$rh->{w}->{main}})  # All the existing main windows
	{
		cmd_clear($_) if defined $rh->{w}->{main}->[$_];
	}
	1;
}

# &MSNre::cmd_connect ()
# ----------------------------------------------------------------------
# /CONNECT

sub cmd_connect
{
	my $error;
	my $user = config_lookup('user');

	$rh->{timers}->{reconnect} = undef if defined $rh->{timers}->{reconnect};

	if (!defined config_lookup('server'))    # No server
	{
		print_event($rh->{w}->{current},
			    "The server is not specified. Use the <%text_highlight>/SETSERVER<%n>> command.");
		$error++;
	}

	if (!defined config_lookup('port'))      # No port
	{
		print_event($rh->{w}->{current},
			    "The port is not specified. Use the <%text_highlight>/SETPORT<%n> command.");
		$error++;
	}

	if (!$user)                              # No user passport
	{
		print_event($rh->{w}->{current},
			    "Your passport is not specified. Use the <%text_highlight>/SETUSER<%n> command.");
		$error++;
	}

	elsif ($user !~ /^\S+?\@\S+/)            # Invalid user passport
	{
		print_event($rh->{w}->{current},
			    "Passport <%text_highlight>$user<%n> is not a valid e-mail address.");
		print_event($rh->{w}->{current}, "Use the <%text_highlight>/SETUSER<%n> command to change it.");
		$error++;
	}

	if (!defined config_lookup('password'))  # No password
	{
		print_event($rh->{w}->{current},
			    "Your password is not specified. Use the <%text_highlight>/SETPWD<%n> command.");
		$error++;
	}

	return undef if $error;

	$rh->{debug}->{debug_log} = 1 if $rh->{debug}->{extra};  # Enable the log debugging
	$rh->{misc}->{connecting} = 1;

	return $msn->connect;
}

# &MSNre::cmd_disconnect ()
# ----------------------------------------------------------------------
# /DISCONNECT

sub cmd_disconnect
{
	if (!$msn->connected && defined $rh->{timers}->{reconnect})
	{
		# Reconnecting is pending
		$rh->{timers}->{reconnect} = undef;
		print_event(1, "Reconnecting was canceled.");
		return 1;
	}

	check_connection() || return undef if !$msn->connected;   # Not connected (or logged in)
	disconnect_msn();
	1;
}

# &MSNre::cmd_exec ( COMMAND )
# ----------------------------------------------------------------------
# /EXEC

sub cmd_exec
{
	my $shell = join ' ', @_ if @_;

	if (defined $shell)
	{
		my $fh = new FileHandle "$shell < /dev/null |";
		if (!defined $fh)
		{
			msnre_error("Couldn't run <%text_highlight>$shell<%n> command");
			return undef;
		}

		while (<$fh>)   # Run the command and send the output to the current window
		{
			chomp;
			print_event($rh->{w}->{current}, $_, 1);
			window_main_update();
		}
		$fh->close;

		print_window($rh->{w}->{current}, "\n");
		return 1;
	}

	print_usage('exec');
	undef;
}

# &MSNre::cmd_file_accept ( SESSION )
# ----------------------------------------------------------------------
# /FILEACCEPT

sub cmd_file_accept
{
	check_connection() || return undef;
	my $session = shift;

	if (defined $session)
	{
		my $f_hash = file_find_ref_by_session($session);

		if (!defined $f_hash)
		{
			print_event($rh->{w}->{current}, "You must specify a valid file session.");
			return undef;
		}

		if (!$f_hash->{can_accept})
		{
			print_event($rh->{w}->{current}, "The file cannot be accepted at this stage.");
			return undef;
		}

		if (!defined config_lookup('received'))  # Path for the files is not specified/invalid
		{
			print_event($rh->{w}->{current},
				    "No valid path for received files defined. Rejecting the file transfer.");

			cmd_file_reject($session);
			return undef;
		}

		$f_hash->{status} = "Accepted; Pending";
		$f_hash->{can_accept} = undef;
		$f_hash->{can_reject} = undef;

		if (!defined $msn->get_user($f_hash->{user}))
		{
			contacts_add(passport => $f_hash->{user}, last_seen => time);
		}

		file_add_timer($f_hash->{session});
		return $msn->file_accept($f_hash->{swb_session}, $f_hash->{file_session}, config_lookup('received'));
	}

	print_usage('fileaccept');
	undef;
}

# &MSNre::cmd_file_cancel ( SESSION )
# ----------------------------------------------------------------------
# /FILECANCEL

sub cmd_file_cancel
{
	check_connection() || return undef;
	my $session = shift;

	if (defined $session)
	{
		my $f_hash = file_find_ref_by_session($session);

		if (!defined $f_hash)
		{
			print_event($rh->{w}->{current}, "You must specify a valid file session.");
			return undef;
		}

		if (!$f_hash->{can_cancel})
		{
			print_event($rh->{w}->{current}, "The file cannot be canceled at this stage.");
			return undef;
		}

		$f_hash->{status} = "Canceled";
		$f_hash->{active} = 0;
		$f_hash->{can_cancel} = undef;

		if (defined $f_hash->{user} && !defined $msn->get_user($f_hash->{user})) # Local contact
		{
			contacts_add(passport => $f_hash->{user}, last_seen => time);
		}

		file_remove_timer($f_hash->{session});
		$msn->file_cancel($f_hash->{swb_session}, $f_hash->{file_session});

		print_event($rh->{sessions}->[$f_hash->{swb_session}]->{window},
			    "\nCancelling file transfer <%text_highlight>[$f_hash->{session}]");
		return 1;
	}

	print_usage('filecancel');
	undef;
}

# &MSNre::cmd_file_list ()
# ----------------------------------------------------------------------
# /FILELIST

sub cmd_file_list
{
	check_connection() || return undef;
	my ($user_len, $fname_len, $fsize_len, $session_len) = (0,0,0,0);

	if ($rh->{ft_sessions})
	{
		for my $f(@{$rh->{ft_sessions}})
		{
			$user_len    = length($f->{user})           if length($f->{user})           > $user_len;
			$fname_len   = real_length($f->{file_name}) if real_length($f->{file_name}) > $fname_len;
			$fsize_len   = length($f->{file_size})      if length($f->{file_size})      > $fsize_len;
			$session_len = length($f->{session})        if length($f->{session})        > $session_len;
		}
		my $head = "User  "  . " " x (++$user_len-6)     . "File name  " . " " x (++$fname_len-11) .
		    "File size  "      . " " x (++$fsize_len-11) . "Session  "   . " " x (++$session_len-9) .
		    "Current status  ";

		print_event($rh->{w}->{current}, "\n" . print_line('File Transfers List'));
		print_event($rh->{w}->{current}, "<%text_highlight>$head<%n>");

		$user_len    = ($user_len    > 8)  ? $user_len    : 8;
		$fname_len   = ($fname_len   > 11) ? $fname_len   : 11;
		$fsize_len   = ($fsize_len   > 11) ? $fsize_len   : 11;
		$session_len = ($session_len > 9)  ? $session_len : 9;

		for my $f(@{$rh->{ft_sessions}})
		{
			my $row = "$f->{user}"      . " " x ($user_len    - length($f->{user}));
			$row   .= "$f->{file_name}" . " " x ($fname_len   - real_length($f->{file_name}));
			$row   .= "$f->{file_size}" . " " x ($fsize_len   - length($f->{file_size}));
			$row   .= "[$f->{session}]" . " " x ($session_len - (length($f->{session})+2));
			$row   .= "$f->{status}";

			print_event($rh->{w}->{current}, $row);
		}
		print_window($rh->{w}->{current}, "\n");
		return 1;
	}

	print_event($rh->{w}->{current}, "There are currently no file transfers in progress");
	undef;
}

# &MSNre::cmd_file_reject ()
# ----------------------------------------------------------------------
# /FILEREJECT

sub cmd_file_reject
{
	check_connection() || return undef;
	my $session = shift;

	if (defined $session)
	{
		my $f_hash = file_find_ref_by_session($session);

		if (!defined $f_hash)
		{
			print_event($rh->{w}->{current}, "You must specify a valid file session.");
			return undef;
		}

		if (!$f_hash->{can_reject})
		{
			print_event($rh->{w}->{current}, "The file cannot be rejected at this stage.");
			return undef;
		}

		$f_hash->{status} = "Rejected";
		$f_hash->{active} = 0;
		$f_hash->{can_accept} = 0;
		$f_hash->{can_reject} = 0;

		if (!defined $msn->get_user($f_hash->{user}))   # Local contact
		{
			contacts_add(passport => $f_hash->{user}, last_seen => time);
		}

		file_remove_timer($f_hash->{session});
		return $msn->file_reject($f_hash->{swb_session}, $f_hash->{file_session});
	}

	print_usage('filereject');
	undef;
}

# &MSNre::cmd_file_send ( USER | WINDOW, FILE_PATH )
# ----------------------------------------------------------------------
# /FILESEND

sub cmd_file_send
{
	check_connection() || return undef;
	my ($user, $session_e, $session_ne, $file_path);

	if ($msn->status eq 'offline' || $msn->status eq 'appear_offline')
	{
		print_event($rh->{w}->{current}, "You cannot send any files when you are offline.");
	}

	if (!@_ || !defined $_[0])
	{
		print_usage('filesend');
		return undef;
	}
	if (@_ == 1)
	{
		# See if we are sending a file to a chat window
		$session_e = find_session_by_window_established($rh->{w}->{current});
		$session_ne = find_session_by_window($rh->{w}->{current}) if !defined $session_e;

		if (!defined $session_e && !defined $session_ne)
		{
			print_event($rh->{w}->{current}, "The current window is not a chat window.");
			return undef;
		}
		$file_path = shift;
	}
	else
	{
		# Read the user or window number as the first parameter, 2 params min

		if (POSIX::isdigit($_[0]))   # Sending to a window
		{
			$session_e = find_session_by_window_established($_[0]);
			$session_ne = find_session_by_window($_[0]) if !defined $session_ne;
			if (!defined $session_e && !defined $session_ne)
			{
				# The specified window is unusable
				print_event($rh->{w}->{current},
					    "Window <%text_highlight>[$_[0]]<%n> is not a valid chat window.");
				return undef;
			}
			shift;
		}
		else    # Sending to a user
		{
			$user = shift;

			if ($user !~ /^\S+?\@\S+/)   # Needs a valid passport
			{
				print_event($rh->{w}->{current}, "Passport <%text_highlight>$user<%n> is not a ".
					    "valid e-mail address.");
				return undef;
			}

			my $r_user = $msn->get_user($user);
			$user = $r_user->passport if defined $r_user;
		}
		$file_path = shift;
	}

	if (!-f $file_path)
	{
		print_event($rh->{w}->{current}, "<%text_highlight>$file_path<%n> is not a valid file.");
		return undef;
	}

	# Parameters are ok, now look if we need to create a new session

	if (!defined $user && !defined $session_e)
	{
		my @u = $msn->get_swb_users($session_ne);
		$user = (@u) ? $u[0]->passport : $rh->{sessions}->[$session_ne]->{first_user};
	}

	if (defined $session_e)
	{
		# Great we have a window with a chat already established
		send_file($session_e, $file_path);
		return 1;
	}

	my $r_user = $msn->get_user($user);
	if (defined $r_user && $r_user->is_user_in_list('forward_list') && $r_user->status eq 'offline')
	{
		print_event($rh->{w}->{current},
			    "User <%text_highlight>$user<%n> is offline. The invitation was not sent.");
		return undef;
	}

	# New session to create
	create_session('file', $user, $file_path);
	1;
}

# &MSNre::cmd_find ( FNAME, LNAME [, COUNTRY, STATE, CITY ] )
# ----------------------------------------------------------------------
# /FIND

sub cmd_find
{
	check_connection() || return undef;
	my ($fname, $lname, $country, $state, $city) = @_;

	if (defined $fname && defined $lname)   # First and last name are required
	{
		my @params = ($fname, $lname);

		push @params, $country if defined $country;
		push @params, $state if defined $state;
		push @params, $city if defined $city;

		return $msn->find_member(@params);
	}

	print_usage('find');
	undef;
}

# &MSNre::cmd_find_invite ( SEARCH_NR )
# ----------------------------------------------------------------------
# /FINDINVITE

sub cmd_find_invite
{
	check_connection() || return undef;
	my $search_nr = shift;
	my $message = join ' ', @_ if @_;

	if (defined $search_nr && defined $message)
	{
		if (!POSIX::isdigit($search_nr))
		{
			print_event($rh->{w}->{current}, "<%text_highlight>$search_nr<%n> is not a valid number.");
			return undef;
		}

		if (defined $rh->{find_invite} && $search_nr <= $rh->{find_invite} && $search_nr > 0)
		{
			return $msn->find_member_invite($search_nr, $message);
		}
		else
		{
			print_event($rh->{w}->{current}, "<%text_highlight>$search_nr<%n> is not a valid entry ".
				    "from a member search");
			return undef;
		}
	}

	print_usage('findinvite');
	undef;
}

# &MSNre::cmd_help ( [ COMMAND ] )
# ----------------------------------------------------------------------
# /HELP

sub cmd_help
{
	my $cmd = shift;

	if (defined $cmd)   # Only the specified command
	{
		(exists $rh->{command}->{lc $cmd})
		    ? print_usage(lc $cmd)
		    : print_event($rh->{w}->{current}, "Command [<%text_highlight>$cmd<%n>] doesn't exist.");
		return 1;
	}

	print_commands(sort keys %{$rh->{command}});   # All the commands
	1;
}

# &MSNre::cmd_history ()
# ----------------------------------------------------------------------
# /HISTORY

sub cmd_history
{
	my $param = shift;

	if (defined $param)
	{
		if (uc($param) eq 'CLEAR')   # Clear the history
		{
			delete $rh->{w}->{input_v}->{history} if $rh->{w}->{input_v}->{history};
			print_event($rh->{w}->{current}, "The current command history was cleared.");
			return 1;
		}

		print_event($rh->{w}->{current}, "<%text_highlight>$NAME<%n>: Unknown <%text_highlight>/HISTORY<%n> ".
			  "parameter: <%text_highlight>$param<%n>\n");
	}

	# Print the history

	print_event($rh->{w}->{current}, "Command history:");
	my $num = 1;
	my $total = scalar @{$rh->{w}->{input_v}->{history}};

	for (reverse @{$rh->{w}->{input_v}->{history}})
	{
		print_event($rh->{w}->{current}, " <%text_bracket>[<%n>" . " " x (length($total)-length($num)) .
			    $num++ . "<%text_bracket>]<%n>: $_");
	}

	print_window($rh->{w}->{current}, "\n");
	1;
}

# &MSNre::cmd_hotmail ()
# ----------------------------------------------------------------------
# /HOTMAIL

sub cmd_hotmail
{
	check_connection() || return undef;
	my $type = shift;

	if ($msn->passport !~ /\@(hotmail|msn)\.com$/i)
	{
		print_event($rh->{w}->{current},
		"You need to have hotmail.com or msn.com e-mail address to be able to use the Hotmail features.");
		return undef;
	}

	if (defined $type)
	{
		if ($type !~ /^(addressbook|compose|folders|inbox|mobile)$/i)
		{
			print_event($rh->{w}->{current},
				    "<%text_highlight>$NAME<%n>: Unknown parameter. Try ".
				    "<%text_highlight>/HELP HOTMAIL<%n>.");
			return undef;
		}
		$type = 'addrbook' if lc($type) eq 'addressbook';
		$type = 'chgmob' if lc($type) eq 'mobile';

		my $param = shift if lc($type) eq 'compose' && @_;
		return $msn->get_hotmail($type, $param);
	}

	print_usage('hotmail');
	undef;
}

# &MSNre::cmd_ignore_user ()
# ----------------------------------------------------------------------
# /IGNORE

sub cmd_ignore_user
{
	check_connection() || return undef;
	my @users = @_;

	if (!@users)
	{
		print_usage('ignore');
		return undef;
	}

	for my $user(@users)
	{
		if ($user !~ /^\S+?\@\S+/)   # Needs a valid passport
		{
			print_event($rh->{w}->{current},
				    "Passport <%text_highlight>$user<%n> is not a valid e-mail address.");
			next;
		}

		contacts_add(passport => $user, ignore => 1);
		print_event($rh->{w}->{current}, "User <%text_highlight>$user<%n> is now ignored.");
	}

	return 1;
}

# &MSNre::cmd_info_user ()
# ----------------------------------------------------------------------
# /INFO

sub cmd_info_user
{
	check_connection() || return undef;
	my @t_users = join ' ', @_ if @_;

	my @users = ();
	my @own_vals;

	if (@t_users)
	{
		for my $user(@t_users)
		{
			if ($user !~ /^\S+?\@\S+/)   # Invalid passport
			{
				print_event($rh->{w}->{current},
					    "Passport <%text_highlight>$user<%n> is not a valid e-mail address.");
				next;
			}

			if ($user eq $msn->passport)
			{
				push @users, $msn->passport;
				next;
			}

			if (defined contacts_get_contact($user))
			{
				push @users, $user;
				next;
			}

			# Invalid contact
			print_event($rh->{w}->{current}, "No info for user <%text_highlight>$user<%n> available.");
		}
	}
	else
	{
		@users = $msn->passport;
		@own_vals = qw(age birthday country gender kid postalcode wallet LCID);
	}
	return undef if !@users;    # All the entered passports were invalid

	for my $user(@users)
	{
		my $c;
		my $r_user = $msn->get_user($user);
		my $c_user = contacts_get_contact($user);

		$r_user = $msn if $user eq $msn->passport;   # Own account

		# Friendly name
		$c->{fname} = (defined $c_user) ? $c_user->{fname} : utf8_l($msn->url_decode($r_user->fname));

		if (defined $r_user)
		{
			# Contact lists
			$c->{contact_lists} = join ', ', map {ucfirst} @{$r_user->c_list};
			$c->{contact_lists} =~ s/_/ /g;

			# Groups
			$c->{groups} =
			    join ', ', map {utf8_l($msn->url_decode($msn->get_group($_)->name))} @{$r_user->group};
		}

		$c->{status} = (defined $r_user) ? ucfirst($r_user->status) : 'Unknown';
		$c->{status} =~ s/_/ /g;

		# Phone numbers
		my $_phone = ($user eq $msn->passport) ? $msn->get_phone_own : $msn->get_phone($user);

		if (defined $_phone)
		{
			$c->{phone_home}   = utf8_l($msn->url_decode($_phone->{home})) if defined $_phone->{home};
			$c->{phone_work}   = utf8_l($msn->url_decode($_phone->{work})) if defined $_phone->{work};
			$c->{phone_mobile} = utf8_l($msn->url_decode($_phone->{mobile})) if defined $_phone->{mobile};
		}

		$c->{phone_home}   = '<Not filled in>' if !defined $c->{phone_home};
		$c->{phone_work}   = '<Not filled in>' if !defined $c->{phone_work};
		$c->{phone_mobile} = '<Not filled in>' if !defined $c->{phone_mobile};

		$c->{phone_msn} = (defined $r_user && $r_user->is_user_in_list('forward_list'))
		    ? (defined $r_user->mobile_device && $r_user->mobile_device eq 'Y')
		    ? '<Yes>' : '<No>' : '<Unknown>';


		for ('ip_address', 'user_agent', 'chat_logging')
		{
			$c->{$_} = (defined $c_user && defined $c_user->{$_}) ? $c_user->{$_} :
			    (defined $r_user && defined $r_user->$_) ? $r_user->$_ : '<Unknown>';
		}


		if (defined $c->{chat_logging})
		{
			$c->{chat_logging} = '<Enabled>' if uc($c->{chat_logging}) eq 'Y';
			$c->{chat_logging} = '<Disabled>' if uc($c->{chat_logging}) eq 'N';

			$c->{chat_logging} = '<Enabled> (Secure)' if uc($c->{chat_logging}) eq 'S';
		}


		$c->{aliases} = join ', ', alias_get_user($user);
		$c->{aliases} = '<No aliases defined>' if !defined $c->{aliases} || !length($c->{aliases});

		# Save own-only values
		if ($user eq $msn->passport)
		{
			for my $v('age', 'country', 'postalcode')
			{
				$c->{$v} = $r_user->$v;
				$c->{$v} = '<Unknown>' if !defined $c->{$v};
			}

			$c->{gender} = (defined $r_user->gender)
			    ? (lc($r_user->gender) eq 'm') ? 'Male' : 'Female' : '<Unknown>';

			$c->{kid} = (defined $r_user->kid) ? ($r_user->kid eq 1) ? '<Yes>' : '<No>' : '<Unknown>';

			$c->{wallet} = (defined $r_user->wallet)
			    ? ($r_user->wallet eq 1) ? '<Yes>' : '<No>' : '<Unknown>';

			$c->{LCID} = (defined $r_user->LCID) ? $msn->lcid2string($r_user->LCID) : '<Unknown>';

			$c->{mobile_device} = (defined $r_user->mobile_device_my && $r_user->mobile_device_my eq 'Y')
			    ? '<Yes>' : '<No>';
		}

		# How long is a user logged in
		$c->{logged_in} = get_time(time - $rh->{timers}->{login_time}->{$user}) if
		    $c->{status} ne 'Offline' && defined $rh->{timers}->{login_time}->{$user};

		# Last seen
		$c->{last_seen} = $c_user->{last_seen} if $c->{status} ne 'Offline' &&
		    defined $c_user && defined $c_user->{last_seen};

		# How long is a user idle/away
		$c->{away_idle} = get_time(time - $rh->{timers}->{away_idle}->{$user}) if defined
		    $rh->{timers}->{away_idle}->{$user} && ($c->{status} eq 'Idle' || $c->{status} eq 'Away');

		my $window = $rh->{w}->{current};

		print_event($window, "\n" . print_line("User Info ($c_user->{passport})"));

		print_line_text($window, 'Passport      ', $user);
		print_line_text($window, 'Friendly name ', $c->{fname});
		print_line_text($window, 'Contact lists ', $c->{contact_lists})      if defined $c->{contact_lists};
		print_line_text($window, 'Groups        ', $c->{groups})             if defined $c->{groups};
		print_line_text($window, 'Status        ', $c->{status});
		print_line_text($window, 'Home phone    ', $c->{phone_home});
		print_line_text($window, 'Work phone    ', $c->{phone_work});
		print_line_text($window, 'Mobile phone  ', $c->{phone_mobile});
		print_line_text($window, 'Mobile device ', $c->{mobile_device})      if defined $c->{mobile_device};
		print_line_text($window, 'MSN Mobile    ', $c->{phone_msn});
		print_line_text($window, 'IP Address    ', $c->{ip_address});
		print_line_text($window, 'User Agent    ', $c->{user_agent});
		print_line_text($window, 'Chat logging  ', $c->{chat_logging})       if defined $c->{chat_logging};
		print_line_text($window, 'Aliases       ', $c->{aliases})            if defined $c->{aliases};
		print_line_text($window, 'Age           ', $c->{age})                if defined $c->{age};
		print_line_text($window, 'Kid           ', $c->{kid})                if defined $c->{kid};
		print_line_text($window, 'Gender        ', $c->{gender})             if defined $c->{gender};
		print_line_text($window, 'Country       ', $c->{country})            if defined $c->{country};
		print_line_text($window, 'Postal code   ', $c->{postalcode})         if defined $c->{postalcode};
		print_line_text($window, 'Language      ', $c->{LCID})               if defined $c->{LCID};
		print_line_text($window, 'MS Wallet     ', $c->{wallet})             if defined $c->{wallet};
		print_line_text($window, 'Logged in     ', $c->{logged_in})          if defined $c->{logged_in};
		print_line_text($window, $c->{status}.' for      ', $c->{away_idle}) if defined $c->{away_idle};
		print_line_text($window, 'Last seen     ', $c->{last_seen})          if defined $c->{last_seen};
	}
	1;
}

# &MSNre::cmd_initial_status ()
# ----------------------------------------------------------------------
# /ISTATUS

sub cmd_initial_status
{
	my $istatus = join '_', map {lc} @_ if @_;

	if (defined $istatus)
	{
		if (!defined $Net::MsnMessenger::Data::Status{$istatus})
		{
			print_event($rh->{w}->{current},
				    "<%text_highlight>$NAME<%n>: Unknown status: [<%text_highlight>$istatus<%n>]");
			return undef;
		}

		config_set('initial_status', $istatus);
		$msn->initial_status($istatus);
		return 1;
	}

	my $c_status = config_lookup('initial_status') || 'online';
	$c_status =~ s/_/ /g;

	print_event($rh->{w}->{current}, "The initial status is set to: <%text_highlight>" . ucfirst($c_status));
	1;
}

# &MSNre::cmd_invite ()
# ----------------------------------------------------------------------
# /INVITE

sub cmd_invite
{
	check_connection() || return undef;
	my ($user, $window, $session);

	if (defined $_[0] && POSIX::isdigit($_[0]))   # Inviting to a window
	{
		my $session = find_session_by_window_established($_[0]);

		if (!defined $rh->{w}->{main}->[$window])
		{
			print_event($rh->{w}->{current},
				    "<%text_highlight>$window<%n> is not a valid window number.");
			return undef;
		}

		elsif (!defined $session)
		{
			print_event($rh->{w}->{current}, "Cannot invite users to not established chat session.");
			return undef;
		}
	}

	if (defined $_[0])   # Inviting a user
	{
		my $user = shift;

		if ($user !~ /^\S+?\@\S+/)   # Needs a valid passport
		{
			print_event($rh->{w}->{current}, "Passport <%text_highlight>$user<%n> is not a valid ".
				    "e-mail address.");
			return undef;
		}
		my $real_user = $msn->get_user($user);

		$window = $rh->{w}->{current} if !defined $window;
		$session = find_session_by_window_established($window);

		if (!defined $window)
		{
			print_event($rh->{w}->{current}, "Cannot invite users to not established chat session.");
			return undef;
		}

		my @u = $msn->get_swb_users($session);
		$user = $real_user->passport if defined $real_user;

		if (grep {$user eq $_->passport} @u)
		{
			print_event($rh->{w}->{current}, "The user is already in the chat.");
			return undef;
		}
		return $msn->invite_switchboard($user, $session);
	}
	print_usage('invite');
	undef;
}

# &MSNre::cmd_invite_messenger ()
# ----------------------------------------------------------------------
# /INVITEMSN

sub cmd_invite_messenger
{
	check_connection() || return undef;
	my $passport = shift;

	if (defined $passport)
	{
		if ($passport !~ /^\S+?\@\S+/)   # Needs a valid passport
		{
			print_event($rh->{w}->{current},
				    "Passport <%text_highlight>$passport<%n> is not a valid e-mail address.");
			return undef;
		}
		return $msn->send_email_invitation($passport);
	}
	print_usage('invitemsn');
	undef;
}

# &MSNre::cmd_list_group ()
# ----------------------------------------------------------------------
# /LG

sub cmd_list_group
{
	check_connection() || return undef;
	my $group = join ' ', @_ if @_;

	if (defined $group)
	{
		validate_group($group) || return undef;
		my $group_id = $msn->get_group_id_by_name($msn->url_encode($group));

		return print_contacts(list => 'forward_list', group => $group_id);
	}

	print_groups();
	1;
}

# &MSNre::cmd_list_group_ol ()
# ----------------------------------------------------------------------
# /LGO

sub cmd_list_group_ol
{
	check_connection() || return undef;
	my $group = join ' ', @_ if @_;

	if (defined $group)
	{
		validate_group($group) || return undef;
		my $group_id = $msn->get_group_id_by_name($msn->url_encode($group));

		return print_contacts(list => 'forward_list', group => $group_id, online => 1);
	}

	print_groups();
	1;
}

# &MSNre::cmd_move_user ()
# ----------------------------------------------------------------------
# /MOVE

sub cmd_move_user
{
	check_connection() || return undef;
	my $user = shift;
	my $new_group = join ' ', @_ if @_;

	if (defined $user && defined $new_group)
	{
		validate_user($user, list => 'forward_list') || return undef;
		my $real_user = $msn->get_user($user);
		my $real_group_id = $msn->get_group_id_by_name($msn->url_encode($new_group));

		if (scalar @{$real_user->group} > 1)
		{
			print_event($rh->{w}->{current}, "User <%text_highlight> is already present in more ".
				    "than one group.");
			return undef;
		}
		if (!defined $real_group_id)
		{
			print_event($rh->{w}->{current},
				    "Group <%text_highlight>$new_group<%n> doesn't exist in your contact list.");
			return undef;
		}
		my $source_group_id = $real_user->group->[0];
		return $msn->move_to_group($real_user->passport, $source_group_id, $real_group_id);
	}

	print_usage('move');
	undef;
}

# &MSNre::cmd_message ()
# ----------------------------------------------------------------------
# /MSG

sub cmd_message
{
	check_connection() || return undef;
	my ($session_e, $session_ne, $user, %attributes);

	if ($msn->status eq 'offline' || $msn->status eq 'appear_offline')
	{
		print_event($rh->{w}->{current}, "You cannot send any messages when you are offline.");
		return undef;
	}

	if (!@_ || !defined $_[0])
	{
		print_usage('msg');
		return undef;
	}

	if (POSIX::isdigit($_[0]))   # Window number specified
	{
		$session_e = find_session_by_window_established($_[0]);
		$session_ne = find_session_by_window($_[0]) if !defined $session_e;

		if (!defined $session_e && !defined $session_ne)
		{
			print_event($rh->{w}->{current},
				    "Window [<%text_highlight>$_[0]<%n>] is not a valid chat window.");
			return undef;
		}
		shift;
	}
	else    # Sending to a user
	{
		$user = shift;

		if ($user !~ /^\S+?\@\S+/)
		{
			print_event($rh->{w}->{current}, "Passport <%text_highlight>$user<%n> is not a valid ".
				    "e-mail address.");
			return undef;
		}
		my $r_user = $msn->get_user($user);
		$user = $r_user->passport if defined $r_user;
	}

	my $message = join ' ', @_ if defined $_[0];

	if (!defined $message)
	{
		print_event($rh->{w}->{current}, "<%text_highlight>$NAME<%n>: You need to enter a message.");
		return undef;
	}

	if (defined $session_e)
	{
		send_message($session_e, $message);
		return 1;
	}
	elsif (defined $session_ne)   # Re-establish an old one
	{
		create_session('message', $rh->{sessions}->[$session_ne]->{first_user}, $message);
		return 1;
	}

	if (defined $user)
	{
		my $w = find_chat_with_user($user);

		my $r_user = $msn->get_user($user);
		if (defined $r_user && $r_user->is_user_in_list('forward_list') && $r_user->status eq 'offline')
		{
			my $window = $w if defined $w && defined $rh->{w}->{main}->[$w];
			$window = $rh->{w}->{current} if !defined $window;

			print_event($window,
				    "User <%text_highlight>$user<%n> is offline. The message was not sent.");
			return undef;
		}
		$session_e = find_session_by_window_established($w) if defined $w;

		if (defined $session_e)
		{
			send_message($session_e, $message);
			return 1;
		}
		create_session('message', $user, $message);
	}
	1;
}

# &MSNre::cmd_message_exec ()
# ----------------------------------------------------------------------
# /MSGEXEC

sub cmd_message_exec
{
	check_connection() || return undef;

	my $user = shift;
	my $shell_cmd = join ' ', @_ if @_;

	if (defined $user && defined $shell_cmd)
	{
		if ($user !~ /^\S+?\@\S+/)
		{
			print_event($rh->{w}->{current}, "Passport <%text_highlight>$user<%n> is not a valid ".
				    "e-mail address.");
			return undef;
		}

		# Read the command's output and send it as a message

		my $fh = new FileHandle "$shell_cmd < /dev/null |";
		if (!defined $fh)
		{
			msnre_error("Couldn't run <%text_highlight>$shell_cmd<%n> command: $!");
			return undef;
		}

		while (<$fh>)
		{
			if (!cmd_message($user, $_))   # Failed to send the message
			{
				$fh->close;
				return undef;
			}
		}

		$fh->close;
		return 1;
	}

	elsif (defined $user)
	{
		print_event($rh->{w}->{current}, "You need to enter a shell command.");
		return undef;
	}

	print_usage('msgexec');
	undef;
}

# &MSNre::cmd_netmeeting_accept ()
# ----------------------------------------------------------------------
# /NMACCEPT

sub cmd_netmeeting_accept
{
	check_connection() || return undef;
	my $nm_session = shift;

	if (defined $nm_session)
	{
		if (POSIX::isdigit($nm_session) && $rh->{nm_sessions} && exists $rh->{nm_sessions}->[$nm_session] &&
		    $rh->{nm_sessions}->[$nm_session]->{can_accept})
		{
			$msn->netmeeting_accept($rh->{nm_sessions}->[$nm_session]->{swb_session},
						$rh->{nm_sessions}->[$nm_session]->{nm_session});

			$rh->{nm_sessions}->[$nm_session]->{can_accept} = 0;
			$rh->{nm_sessions}->[$nm_session]->{can_reject} = 0;
			return 1;
		}
		else
		{
			print_event($rh->{w}->{current}, "You must specify a valid session.");
			return undef;
		}
	}

	print_usage('nmaccept');
	undef;
}

# &MSNre::cmd_netmeeting_invite ()
# ----------------------------------------------------------------------
# /NMINVITE

sub cmd_netmeeting_invite
{
	check_connection() || return undef;

	if (!find_executable('gnomemeeting'))
	{
		print_event($rh->{w}->{current},
			    "<%text_highlight>$NAME<%n>: gnomemeeting executable not found in your PATH.");
		return undef;
	}

	if (!@_ || !defined $_[0] || POSIX::isdigit($_[0]))   # Sending to a window
	{
		my $window = (defined $_[0]) ? shift : $rh->{w}->{current};

		my $s_e = find_session_by_window_established($window);
		my $s_ne = find_session_by_window($window) if !defined $s_e;

		if (defined $s_e)
		{
			return netmeeting_invite($s_e);
		}
		elsif (defined $s_ne)
		{
			return create_session('netmeeting', $rh->{sessions}->[$s_ne]->{first_user});
		}
		else
		{
			print_event($rh->{w}->{current}, "Window [<%text_highlight>$_[0]] is not a valid " .
				    "chat window.");
			return undef;
		}
	}

	my $user = shift;   # Sending to user

	if ($user !~ /^\S+?\@\S+/)   # Needs a valid passport
	{
		print_event($rh->{w}->{current},
			    "Passport <%text_highlight>$user<%n> is not a valid e-mail address.");
		return undef;
	}

	create_session('netmeeting', $user);
	1;
}

# &MSNre::cmd_netmeeting_reject ()
# ----------------------------------------------------------------------
# /NMREJECT

sub cmd_netmeeting_reject
{
	check_connection() || return undef;
	my $nm_session = shift;

	if (defined $nm_session)
	{
		if (POSIX::isdigit($nm_session) && $rh->{nm_sessions} && exists $rh->{nm_sessions}->[$nm_session] &&
		    $rh->{nm_sessions}->[$nm_session]->{can_reject})
		{
			$msn->netmeeting_reject($rh->{nm_sessions}->[$nm_session]->{swb_session},
						$rh->{nm_sessions}->[$nm_session]->{nm_session});

			$rh->{nm_sessions}->[$nm_session]->{can_accept} = 0;
			$rh->{nm_sessions}->[$nm_session]->{can_reject} = 0;
			return 1;
		}
		else
		{
			print_event($rh->{w}->{current}, "You must specify a valid session.");
			return undef;
		}
	}

	print_usage('nmreject');
	undef;
}

# &MSNre::cmd_pager ()
# ----------------------------------------------------------------------
# /PAGER

sub cmd_pager
{
	check_connection() || return undef;
	my $user = shift;
	my $message = join ' ', @_ if @_;

	if (defined $user)
	{
		if ($user !~ /^\S+?\@\S+/)   # Needs a valid passport
		{
			print_event($rh->{w}->{current}, "Passport <%text_highlight>$user<%n> is not a valid ".
				    "e-mail address.");
			return undef;
		}
		my $real_user = $msn->get_user($user);

		if (!defined $real_user || !defined $real_user->mobile_device || $real_user->mobile_device eq 'N')
		{
			print_event($rh->{w}->{current},
				    "The user must be in your contact list and use MSN Mobile.");
			return undef;
		}
		my $number_type = config_lookup('pager_number');
		my $rc = $msn->send_pager($real_user->passport, $number_type, utf8_e($message));

		if ($rc)
		{
			print_event($rh->{w}->{current}, "The pager message was successfully sent to " .
				    "<%text_passport><$user><%n>.");
			return 1;
		}
		else
		{
			print_event($rh->{w}->{current}, "Failed to sent the pager message to " .
				    "<%text_passport><$user><%n>.");
		}
	}

	print_usage('pager');
	undef;
}

# &MSNre::cmd_part ()
# ----------------------------------------------------------------------
# /PART

sub cmd_part
{
	check_connection() || return undef;

	my $window = shift;
	$window = $rh->{w}->{current} if !defined $window;
	my $session = find_session_by_window_established($window);

	if (!defined $session && !defined find_session_by_window($window))
	{
		print_event($rh->{w}->{current},
			    "Not a valid chat window specified and current window is not a chat window.");
		return undef;
	}

	if (defined $session)
	{
		disconnect_swb($session);
		print_event($window, "The current chat session was disconnected.");
	}
	1;
}

# &MSNre::cmd_part_all ()
# ----------------------------------------------------------------------
# /PARTALL

sub cmd_part_all
{
	check_connection() || return undef;

	for (my $i = 2; $i <= @{$rh->{w}->{main}}; $i++)
	{
		# Part every chat session that is established
		cmd_part($i) if defined $rh->{w}->{main}->[$i] && defined find_session_by_window_established($i);
	}
	1;
}

# &MSNre::cmd_phone ()
# ----------------------------------------------------------------------
# /PHONE

sub cmd_phone
{
	check_connection() || return undef;
	my ($number_type, $new_number) = @_;
	my @to_print;

	if (defined $number_type)   # Setting a new number
	{
		if ($number_type !~ /^(home|mobile|work)$/i)
		{
			print_event($rh->{w}->{current}, "<%text_highlight>$NAME<%n>: Unknown phone number type. ".
				    "Try home, mobile or work.");
			return undef;
		}
		if (defined $new_number)
		{
			print_event($rh->{w}->{current}, "Settings your <%text_highlight>".ucfirst(lc($number_type)).
				    "<%n> number to <%text_highlight>$new_number");
			return $msn->change_phone_number($number_type, $new_number);
		}
		@to_print = lc $number_type;
	}

	@to_print = ('home', 'mobile', 'work') if !@to_print;  # Valid number types

	for (@to_print)  # Print the requested number(s)
	{
		my $number = $msn->get_phone_own($_);  $number = "<Not filled in>" if !defined $number;
		print_event($rh->{w}->{current}, "Your $_ number  " . " " x (6-length($_)) .
			    ":  <%text_highlight>" . utf8_l($msn->url_decode($number)));
	}

	print_window($rh->{w}->{current}, "\n");
	1;
}

# &MSNre::cmd_profile ()
# ----------------------------------------------------------------------
# /PROFILE

sub cmd_profile
{
	check_connection() || return undef;

	my $user = shift;
	my $account;

	if (defined $user)
	{
		if ($user !~ /^\S+?\@\S+/)   # Needs a valid passport
		{
			print_event($rh->{w}->{current}, "Passport <%text_highlight>$user<%n> is not a valid ".
				    "e-mail address.");
			return undef;
		}

		my $real_user = $msn->get_user($user);

		$account = $real_user->passport if defined $real_user;
		$account ||= $user;
		my $url = 'http://members.msn.com/default.msnw?mem=' . $account;

		browser_start($url);
		return 1;
	}

	if ($msn->passport !~ /\@(hotmail|msn)\.com$/i)   # Own profile
	{
		print_event($rh->{w}->{current},
			    "<%text_highlight>$NAME<%n>You need to have hotmail.com or msn.com e-mail ".
			    "address to be able to use this feature.\n");
		return undef;
	}
	return $msn->get_hotmail('PROFILE');
}

# &MSNre::cmd_quit ()
# ----------------------------------------------------------------------
# /EXIT | /QUIT

sub cmd_quit
{
	$rh->{misc}->{closing_down} = 1;
	poe_close_client();
	exit 0;
}

# &MSNre::cmd_reconnect ()
# ----------------------------------------------------------------------
# /RECONNECT

sub cmd_reconnect
{
	$rh->{timers}->{reconnect} = undef if defined $rh->{timers}->{reconnect};

	cmd_disconnect() || return undef;
	cmd_connect();
	1;
}

# &MSNre::cmd_remove_group ()
# ----------------------------------------------------------------------
# /RENGROUP

sub cmd_remove_group
{
	check_connection() || return undef;
	my $group = join ' ', @_;

	if (defined $group)
	{
		validate_group($group) || return undef;
		my $group_id = $msn->get_group_id_by_name($msn->url_encode($group));

		# Move the users to a different group
		for ($msn->get_users_group($group_id))
		{
			if (scalar @{$_->group} > 1)
			{
				msnre_debug("Not moving " . $_->passport . "; is present in more groups");
			}
			else
			{
				if (!$msn->move_to_group($_->passport, $group_id, 0))
				{
					msnre_debug("Failed to move " . $_->passport . " to $group_id");
					return undef;
				}
			}
		}
		return $msn->remove_group($group_id);  # The group is safe to be removed
	}

	print_usage('remgroup');
	undef;
}

# &MSNre::cmd_remove_group_with_users ()
# ----------------------------------------------------------------------
# /KILLGROUP

sub cmd_remove_group_with_users
{
	check_connection() || return undef;
	my $group = join ' ', @_ if @_;

	if (defined $group)
	{
		validate_group($group) || return undef;
		my $group_id = $msn->get_group_id_by_name($msn->url_encode($group));

		return $msn->remove_group($group_id);
	}

	print_usage('killgroup');
	undef;
}

# &MSNre::cmd_remove_user ()
# ----------------------------------------------------------------------
# /REMUSER

sub cmd_remove_user
{
	check_connection() || return undef;
	my $user = shift;
	my $group_name = join ' ', @_ if @_;

	if (defined $user)
	{
		my $real_user = $msn->get_user($user);
		my $c_user = contacts_get_contact($user);
		my $group_id;

		if (!defined $real_user && defined $c_user)
		{
			contacts_remove_contact($user);
			print_event($rh->{w}->{current},
				    "Temporary contact <%text_highlight>$user<%n> was removed.");
			return 1;
		}
		validate_user($user, list => 'forward_list') || return undef;

		if (defined $group_name)
		{
			$group_id = $msn->get_group_id_by_name($msn->url_encode($group_name));

			if (!defined $group_id || !grep {$group_id == $_} @{$real_user->group})
			{
				print_event($rh->{w}->{current},
					    "User <%text_highlight>$user<%n> is not in group ".
					    "<%text_highlight>$group_name<%n>");
				return undef;
			}
			return $msn->remove_user($real_user->passport, 'forward_list', $group_id);
		}
		return $msn->remove_user($real_user->passport, 'forward_list');
	}

	print_usage('remuser');
	undef;
}

# &MSNre::cmd_rename_group ()
# ----------------------------------------------------------------------
# /RENGROUP

sub cmd_rename_group
{
	check_connection() || return undef;
	my $old_group = join ' ', @_;
	my $tmp_group = $old_group;

	# Try to find where the first group name end and the second group name begins. This
	# is never really accurate, but it's good enough.

	while (!defined $msn->get_group_id_by_name($msn->url_encode($old_group)))
	{
		if ($old_group =~ /^\S+$/)
		{
			$old_group = undef;
			last;
		}
		$old_group =~ s/\s*\S+$//;
	}
	my $new_group = substr $tmp_group, length($old_group);
	$new_group =~ s/^\s*//;

	$new_group = utf8_e($new_group);

	if (defined $old_group)
	{
		if (!defined $new_group || !length($new_group))
		{
			print_event($rh->{w}->{current}, "You must specify the name for the group.");
			return undef;
		}

		validate_group($old_group) || return undef;
		my $old_group_id = $msn->get_group_id_by_name($msn->url_encode($old_group));

		if (defined $msn->get_group_id_by_name($msn->url_encode($new_group)))
		{
			print_event($rh->{w}->{current},
				    "Group with name <%text_highlight>$new_group<%n> already exists.");
			return undef;
		}

		if (real_length($new_group) > 60)
		{
			print_event($rh->{w}->{current}, "Group name must be up to 60 characters long.");
			return undef;
		}
		return $msn->rename_group($old_group_id, $new_group);
	}

	print_usage('rengroup');
	undef;
}

# &MSNre::cmd_save ()
# ----------------------------------------------------------------------
# /SAVE

sub cmd_save
{
	my $file_path = shift;
	$file_path = $rh->{config}->{config_file} if !defined $file_path;

	if (write_config($file_path) == 1)
	{
		print_event($rh->{w}->{current}, "Configuration has been saved to <%text_highlight>$file_path");
		return 1;
	}
	undef;
}

# &MSNre::cmd_search_contact ()
# ----------------------------------------------------------------------
# /SEARCH

sub cmd_search_contact
{
	browser_start('http://members.msn.com/find.msnw?pgmarket=en-en');
	1;
}

# &MSNre::cmd_search_contact_interest ()
# ----------------------------------------------------------------------
# /SEARCHINTR

sub cmd_search_contact_interest
{
	browser_start('http://members.msn.com/rootcat.msnw?pgmarket=en-en');
	1;
}

# &MSNre::cmd_set ()
# ----------------------------------------------------------------------
# /SET

sub cmd_set
{
	my ($option, $new_value) = @_;

	if (defined $option && defined $new_value)
	{
		$option = lc $option;

		# Do not allow to set these options here because they have their own commands and they
		# also need to be set in the protocol

		return undef if $option eq 'friendly_name' || $option eq 'port' || $option eq 'password' ||
		    $option eq 'server' || $option eq 'user' || $option eq 'profile_use';

		config_set($option, $new_value);

		register_config();
		return 1;
	}

	my ($max_len, $num) = (0, 0);
	my $c_hash = get_config_defaults();

	for (keys %{$c_hash}) { $max_len = length($_) if length($_) > $max_len }

	print_event($rh->{w}->{current}, "\n" . print_line('Configuration options'));

	for (sort keys %{$c_hash})    # Print all the configuration options
	{
		next if $_ eq 'user' || $_ eq 'password' || $_ eq 'server ' || $_ eq 'port';

		if ($num % $rh->{w}->{main_v}->[$rh->{w}->{current}]->{screen_size})
		{
			window_main_update();
		}

		# The current option
		my $opt = config_lookup($_);  $opt = '' if !defined $opt;

		print_event($rh->{w}->{current},
			"<%text_bracket>[<%n> $_ " . " " x ($max_len - length($_)) . "<%text_bracket>]<%n>  $opt");
		$num++;
	}

	print_window($rh->{w}->{current}, "\n");
	1;
}

# &MSNre::cmd_set_fname ()
# ----------------------------------------------------------------------
# /SETFNAME

sub cmd_set_fname
{
	check_connection() || return undef;
	my $new_name = join ' ', @_ if defined $_[0];

	if (defined $new_name)
	{
		$new_name = utf8_e($new_name);

		if (real_length($new_name) > 129)
		{
			print_event($rh->{w}->{current}, "The new friendly name is too long.");
			return undef;
		}
		return $msn->change_friendly_name($msn->passport, $new_name);
	}

	print_usage('fname');
	undef;
}

# &MSNre::cmd_set_port ()
# ----------------------------------------------------------------------
# /SETPORT

sub cmd_set_port
{
	my $port = shift;

	if (defined $port)
	{
		if (POSIX::isdigit($port))
		{
			config_set('port', $port);
			$msn->port($port);

			print_event($rh->{w}->{current}, "Port set to: <%text_highlight>$port");
			return 1;
		}
		print_event($rh->{w}->{current}, "Invalid port: <%text_highlight>$port");
		return undef;
	}

	$port = config_lookup('port');

	(defined $port)
	    ? print_event($rh->{w}->{current}, "The current port is set to: <%text_highlight>$port<%n>")
	    : print_event($rh->{w}->{current}, "The port is <%text_highlight>not<%n> set");
	1;
}

# &MSNre::cmd_set_server ()
# ----------------------------------------------------------------------
# /SETSERVER

sub cmd_set_server
{
	my $server = shift;

	if (defined $server)
	{
		config_set('server', $server);
		$msn->server($server);

		print_event($rh->{w}->{current}, "Server set to: <%text_highlight>$server");
		return 1;
	}
	$server = config_lookup('server');

	(defined $server)
	    ? print_event($rh->{w}->{current}, "The current server is set to: <%text_highlight>$server<%n>")
	    : print_event($rh->{w}->{current}, "The server is <%text_highlight>not<%n> set");
	1;
}

# &MSNre::cmd_set_pwd ()
# ----------------------------------------------------------------------
# /SETPWD

sub cmd_set_pwd
{
	my $password = shift;

	if (defined $password)
	{
		if ($msn->connected)
		{
			print_event($rh->{w}->{current}, "You are already connected to the server.");
			return undef;
		}

		config_set('password', $password);
		$msn->password($password);
		return 1;
	}
	print_usage('setpwd');
	undef;
}

# &MSNre::cmd_set_user ()
# ----------------------------------------------------------------------
# /SETUSER

sub cmd_set_user
{
	my $user = shift;

	if (defined $user)
	{
		if ($msn->connected)
		{
			print_event($rh->{w}->{current},
				    "You are already connected as <%text_highlight>".$msn->passport."<%n>");
			return undef;
		}

		if ($user !~ /^\S+?\@\S+/)    # Needs a valid passport
		{
			print_event($rh->{w}->{current},
				    "Passport <%text_highlight>$user<%n> is not a valid e-mail address.");
			return undef;
		}

		config_set('user', $user);
		$msn->passport($user);
		$msn->fname(undef);           # The friendly name would be changed to the last one

		return 1;
	}
	my $c_user = config_lookup('user');

	(defined $c_user)
	    ? print_event($rh->{w}->{current}, "The current passport is set to <%text_highlight>$c_user<%n>")
	    : print_event($rh->{w}->{current}, "The passport is <%text_highlight>not<%n> set.");
	1;
}

# &MSNre::cmd_sockets ()
# ----------------------------------------------------------------------
# /SOCKETS

sub cmd_sockets
{
	check_connection() || return undef;
	my %args = @_;
	my @sockets = $msn->get_connections;
	my ($host_len, $port_len, $connection_len, $server_len) = (0,0,0,0);

	for my $s(@sockets)
	{
		$s->{server_type} = 'Dispatch'     if $s->{server_type} eq 'DS';
		$s->{server_type} = 'Notification' if $s->{server_type} eq 'NS';
		$s->{server_type} = 'Switchboard'  if $s->{server_type} eq 'SS';

		$host_len       = length($s->{host})            if length($s->{host})            > $host_len;
		$port_len       = length($s->{port})            if length($s->{port})            > $port_len;
		$connection_len = length($s->{connection_type}) if length($s->{connection_type}) > $connection_len;
		$server_len     = length($s->{server_type})     if length($s->{server_type})     > $server_len;
	}

	my $head = "No.    Host (IP Address)  " . " " x (++$host_len-19) . "Port  " . " " x (++$port_len-6) .
	    "Connection  " . " " x (++$connection_len-12) . "Server Type  " . " " x (++$server_len-13) .
	    "Protocol";

	print_event($rh->{w}->{current}, "\n" . print_line('Sockets'));
	print_event($rh->{w}->{current}, "<%text_highlight>$head<%n>");

	$host_len       = ($host_len       > 19) ? $host_len       : 19;
	$port_len       = ($port_len       > 6)  ? $port_len       : 6;
	$connection_len = ($connection_len > 12) ? $connection_len : 12;
	$server_len     = ($server_len     > 13) ? $server_len     : 13;
	my $num = 0;
	my $total = scalar @sockets;

	for my $s(@sockets)
	{
		my $row =
		    "<%text_bracket>[<%n>" . ++$num . " " x (length($total) - length($num)) . "<%text_bracket>]<%n>";

		$row .= " " x (7 - (length($row) - 38));

		$row .= lc($s->{host})                 . " " x ($host_len       - length($s->{host}));
		$row .= ucfirst($s->{port})            . " " x ($port_len       - length($s->{port}));
		$row .= ucfirst($s->{connection_type}) . " " x ($connection_len - length($s->{connection_type}));
		$row .= ucfirst($s->{server_type})     . " " x ($server_len     - length($s->{server_type}));
		$row .= uc($s->{protocol});

		print_event($rh->{w}->{current}, $row);
	}
	print_window($rh->{w}->{current}, "\n");
	1;
}

# &MSNre::cmd_status ()
# ----------------------------------------------------------------------
# /STATUS

sub cmd_status
{
	check_connection() || return undef;
	my $status = join '_', map {lc} @_ if @_;

	if (defined $status)
	{
		if ($status eq $msn->status)
		{
			print_event($rh->{w}->{current},
				"Status <%text_highlight>" . ucfirst(join ' ',map {lc} @_) . "<%n> is already set.");
			return undef;
		}
		for (keys %Net::MsnMessenger::Data::Status) { return $msn->change_status($status) if $_ eq $status }

		if (lc($status) eq 'help')
		{
			my @states;
			for (sort keys %Net::MsnMessenger::Data::Status)
			{
				push @states, ucfirst $_;
				$states[$#states] =~ s/_/ /g;
			}

			print_event($rh->{w}->{current}, "Available user states:");
			print_event($rh->{w}->{current}, "\t<%text_highlight>$_<%n>") for @states;
			print_window($rh->{w}->{current}, "\n");
			return 1;
		}

		print_event($rh->{w}->{current},
			    "<%text_highlight>$NAME<%n>: Unknown status: [<%text_highlight>$status<%n>]");
		return undef;
	}

	print_usage('status');
	return undef;
}

# &MSNre::cmd_unalias_command ()
# ----------------------------------------------------------------------
# /UNALIASCMD

sub cmd_unalias_command
{
	my $command = shift;
	my $real_profile = $rh->{profile_use};

	$real_profile = 'DEFAULT' if
	    $real_profile ne 'DEFAULT' && !exists $rh->{profile}->{$real_profile}->{COMMAND}->{lc $command};

	if (defined $command)
	{
		if (exists $rh->{profile}->{$real_profile}->{aliases}->{COMMAND}->{lc $command})
		{
			delete $rh->{profile}->{$real_profile}->{aliases}->{COMMAND}->{lc $command};
			delete $rh->{profile}->{$real_profile}->{aliases}->{COMMAND} if
			    !keys %{$rh->{profile}->{$real_profile}->{aliases}->{COMMAND}};
		}
		else
		{
			print_event($rh->{w}->{current},
				    "No such command alias: <%text_highlight>$command");
			return undef;
		}

		print_event($rh->{w}->{current},
			    "<%text_highlight>$NAME<%n>: Command alias <%text_highlight>$command<%n> was removed.");
		return 1;
	}

	print_usage('unaliascmd');
	undef;
}

# &MSNre::cmd_unalias_command_all ()
# ----------------------------------------------------------------------
# /NOALIASCMD

sub cmd_unalias_command_all
{
	my $real_profile = $rh->{profile_use};

	$real_profile = 'DEFAULT' if
	    $real_profile ne 'DEFAULT' && !exists $rh->{profile}->{$rh->{profile_use}}->{aliases}->{COMMAND};

	if (exists $rh->{profile}->{$real_profile}->{aliases}->{COMMAND})
	{
		delete $rh->{profile}->{$real_profile}->{aliases}->{COMMAND} if exists
		    $rh->{profile}->{$real_profile}->{aliases}->{COMMAND};
	}

	print_event($rh->{w}->{current}, "<%text_highlight>$NAME<%n>: All the command aliases were removed.");
	1;
}

# &MSNre::cmd_unalias_contact ()
# ----------------------------------------------------------------------
# /UNALIASCN

sub cmd_unalias_contact
{
	my $alias = shift;
	my $real_profile = $rh->{profile_use};

	$real_profile = 'DEFAULT' if
	    $real_profile ne 'DEFAULT' && !exists $rh->{profile}->{$real_profile}->{CONTACT}->{lc $alias};

	if (defined $alias)
	{
		if (exists $rh->{profile}->{$real_profile}->{aliases}->{CONTACT}->{lc $alias})
		{
			delete $rh->{profile}->{$real_profile}->{aliases}->{CONTACT}->{lc $alias};
			delete $rh->{profile}->{$real_profile}->{aliases}->{CONTACT} if
			    !keys %{$rh->{profile}->{$real_profile}->{aliases}->{CONTACT}};
		}
		else
		{
			print_event($rh->{w}->{current},
				    "No such contact alias: <%text_highlight>$alias<%n>");
			return undef;
		}

		print_event($rh->{w}->{current},
			    "<%text_highlight>$NAME<%n>: Contact alias <%text_highlight>$alias<%n> was removed.");
		return 1;
	}

	print_usage('unaliascn');
	undef;
}

# &MSNre::cmd_unalias_contact_all ()
# ----------------------------------------------------------------------
# /NOALIASCN

sub cmd_unalias_contact_all
{
	my $real_profile = $rh->{profile_use};

	$real_profile = 'DEFAULT' if
	    $real_profile ne 'DEFAULT' && !exists $rh->{profile}->{$rh->{profile_use}}->{aliases}->{CONTACT};

	if (exists $rh->{profile}->{$real_profile}->{aliases}->{CONTACT})
	{
		delete $rh->{profile}->{$real_profile}->{aliases}->{CONTACT} if exists
		    $rh->{profile}->{$real_profile}->{aliases}->{CONTACT};
	}

	print_event($rh->{w}->{current}, "<%text_highlight>$NAME<%n>: All the contact aliases were removed");
	1;
}

# &MSNre::cmd_unallow_user ()
# ----------------------------------------------------------------------
# /UNALLOW

sub cmd_unallow_user
{
	check_connection() || return undef;
	my @users = @_;

	if (!@users)
	{
		print_usage('unallow');
		return undef;
	}

	for my $user(@users)
	{
		validate_user($user, list => 'allow_list') || return undef;

		my $real_user = $msn->get_user($user);
		$msn->remove_user($real_user->passport, 'allow_list');
	}
	1;
}

# &MSNre::cmd_unblock_user ()
# ----------------------------------------------------------------------
# /UNBLOCK

sub cmd_unblock_user
{
	check_connection() || return undef;
	my @users = @_;

	if (!@users)
	{
		print_usage('unblock');
		return undef;
	}

	for my $user(@users)
	{
		validate_user($user, list => 'block_list') || return undef;

		my $real_user = $msn->get_user($user);
		$msn->unblock_user($real_user->passport);
	}
	1;
}

# &MSNre::cmd_unforward ()
# ----------------------------------------------------------------------
# /UNFORWARD

sub cmd_unforward
{
	if (!@_ || !defined $_[0])
	{
		print_usage('unforward');
		return undef;
	}

	if (POSIX::isdigit($_[0]))   # A window
	{
		if ($rh->{forward}->{window} && grep {$_ eq $_[0]} @{$rh->{forward}->{window}})
		{
			for (my $i = 0; $i < @{$rh->{forward}->{window}}; $i++)
			{
				splice @{$rh->{forward}->{window}}, $i, 1 if $rh->{forward}->[$i] eq $_[0];
			}

			print_event($rh->{w}->{current},
				    "Forwarding was removed for window <%text_highlight>[$_[0]]");
			return 1;
		}

		print_event($rh->{w}->{current}, "Forwarding not defined for window <%text_highlight>[$_[0]]");
		return undef;
	}
	else                         # An user
	{
		if ($rh->{forward}->{user} && grep {$_ eq $_[0]} @{$rh->{forward}->{user}})
		{
			for (my $i = 0; $i < @{$rh->{forward}->{user}}; $i++)
			{
				splice @{$rh->{forward}->{user}}, $i, 1 if $rh->{forward}->[$i] eq $_[0];
			}
			print_event($rh->{w}->{current},
				    "Forwarding was removed for window <%text_highlight>$_[0]");
			return 1;
		}

		print_event($rh->{w}->{current}, "Forwarding not defined for user <%text_highlight>$_[0]");
		return undef;
	}
}

# &MSNre::cmd_unforward_all ()
# ----------------------------------------------------------------------
# /NOFORWARD

sub cmd_unforward_all
{
	if (exists $rh->{forward})
	{
		delete $rh->{forward}->{window};
		delete $rh->{forward}->{user};
	}

	print_event($rh->{w}->{current}, "<%text_highlight>$NAME<%n>: All the forwards were removed.");
	1;
}

# &MSNre::cmd_unignore_user ()
# ----------------------------------------------------------------------
# /UNIGNORE

sub cmd_unignore_user
{
	check_connection() || return undef;
	my @users = @_;

	if (!@users)
	{
		print_usage('unignore');
		return undef;
	}

	for my $user(@users)
	{
		my $c_user = contacts_get_contact($user);

		if (!defined $c_user || !$c_user->{ignore})
		{
			print_event($rh->{w}->{current}, "User <%text_highlight>$user<%n> is not ignored.");
			next;
		}

		delete $c_user->{ignore};
		print_event($rh->{w}->{current}, "User <%text_highlight>$user<%n> is no longer ignored.");
	}
	1;
}

# &MSNre::cmd_uptime ()
# ----------------------------------------------------------------------
# /UPTIME

sub cmd_uptime
{
	my $conn_time = ($msn->signed_in)
	    ? get_time(time - $rh->{timers}->{connection}) : "Not connected to the server";

	print_event($rh->{w}->{current}, "\nCurrent Uptime is : <%text_highlight>".get_time(time - $st_time));
	print_event($rh->{w}->{current},   "Connection time   : <%text_highlight>$conn_time\n");
	1;
}

# &MSNre::cmd_url ()
# ----------------------------------------------------------------------
# /URL

sub cmd_url
{
	my %u_vars;
	my @commands = qw(HTTP FTP LIST SAVE CLEAR HELP);

	while (@_)
	{
		my $arg = shift;

		if (lc($arg) eq 'http' || lc($arg) eq 'ftp' || lc($arg) eq 'go')
		{
			$u_vars{lc($arg)} = shift @_ if @_;
			$u_vars{lc($arg)."_none"}++ if !defined $u_vars{lc($arg)};
		}
		elsif (grep {lc($arg) eq lc($_)} @commands)
		{
			$u_vars{lc $arg} = 1;
		}
		else
		{
			if ($arg eq '+')                                       # Same as LIST
			{
				$u_vars{list} = 1;
			}
			elsif (substr ($arg, 0, 1) eq '-')
			{
				if (length($arg) > 1 && $arg =~ /^-(\d+)$/)    # -number
				{
					my $number = $1;
					if (!$rh->{url_buffer} || scalar @{$rh->{url_buffer}}-1 < $number)
					{
						print_event($rh->{w}->{current}, "<%text_highlight>$NAME<%n>: ".
							    "URL [<%text_highlight>$number<%n>] doesn't exist.");
					}
					else
					{
						splice @{$rh->{url_buffer}}, $number, 1;
						print_event($rh->{w}->{current}, "<%text_highlight>$NAME<%n>: ".
							    "URL [<%text_highlight>$number<%n>] was removed.");
					}
				}
				else
				{
					$u_vars{clear} = 1;
				}
				$u_vars{dummy}++;
			}
			else
			{
				print_event($rh->{w}->{current}, "<%text_highlight>$NAME<%n>: ".
					    "Unknown /URL command: [<%text_highlight>$arg<%n>]");
			}
		}
	}

	if ($u_vars{list})
	{
		if ($rh->{url_buffer})
		{
			for (my $i = 0; $i < @{$rh->{url_buffer}}; $i++)
			{
				print_event($rh->{w}->{current},
				"<%text_highlight>$NAME<%n> [<%text_highlight>$i<%n>]   $rh->{url_buffer}->[$i]");
			}
		}
		else
		{
			print_event($rh->{w}->{current}, "<%text_highlight>$NAME<%n>: The URL List is empty.");
		}
	}

	if (defined $u_vars{go_none})
	{
		print_event($rh->{w}->{current},
			    "<%text_highlight>$NAME<%n>: Usage: /URL GO [ number ]");
	}
	if (defined $u_vars{go})
	{
		if ($rh->{url_buffer} && POSIX::isdigit($u_vars{go}) && defined $rh->{url_buffer}->[$u_vars{go}])
		{
			$rh->{url_buffer}->[$u_vars{go}] =~ /((?:http|ftp)\S+)/;
			browser_start($1);
		}
		else
		{
			print_event($rh->{w}->{current},
				    "<%text_highlight>$NAME<%n>: The parameter has to be a valid URL entry number.");
		}
	}

	if ($u_vars{http_none})
	{
		print_event($rh->{w}->{current},
			    "<%text_highlight>$NAME<%n>: Available HTTP commands: [ ON | OFF | TOGGLE ]");
	}
	if (defined $u_vars{http})
	{
		if (lc($u_vars{http}) eq 'toggle')
		{
			$u_vars{http} = (config_lookup('http_grab')) ? 'off' : 'on';
		}

		if (lc($u_vars{http}) eq 'on')
		{
			config_set('http_grab', 1);
		}
		elsif (lc($u_vars{http}) eq 'off')
		{
			config_set('http_grab', 0);
		}
		else
		{
			print_event($rh->{w}->{current},
			"<%text_highlight>URL<%n>: Unknown HTTP option: [<%text_highlight>$u_vars{http}<%n>]");
		}
	}

	if ($u_vars{ftp_none})
	{
		print_event($rh->{w}->{current},
			    "<%text_highlight>$NAME<%n>: Available FTP commands: [ ON | OFF | TOGGLE ]");
	}
	if (defined $u_vars{ftp})
	{
		if (lc($u_vars{ftp}) eq 'toggle')
		{
			$u_vars{ftp} = (config_lookup('ftp_grab')) ? 'off' : 'on';
		}

		if (lc($u_vars{ftp}) eq 'on')
		{
			config_set('ftp_grab', 1);
		}
		elsif (lc($u_vars{ftp}) eq 'off')
		{
			config_set('ftp_grab', 0);
		}
		else
		{
			print_event($rh->{w}->{current},
			"<%text_highlight>URL<%n>: Unknown FTP option: [<%text_highlight>$u_vars{ftp}<%n>]");
		}
	}

	if ($u_vars{save})
	{
		if ($rh->{url_buffer})
		{
			my $fh = new FileHandle ">> $rh->{config}->{env_dir}/msnre.url";

			if (defined $fh)
			{
				for (@{$rh->{url_buffer}})
				{
					$fh->print($_ . "\n");
				}
				$fh->close;
				print_event($rh->{w}->{current}, "The URLs list was written to " .
					    "<%text_highlight>$rh->{config}->{env_dir}/msnre.url<%n> file.");

				$rh->{url_buffer} = ();
			}
			else
			{
				print_event($rh->{w}->{current}, "Couldn't write to ".
					    "<%text_highlight>$rh->{config}->{env_dir}/msnre.url<%n> file.");
			}
		}
		else
		{
			print_event($rh->{w}->{current}, "There are no URLs to be saved.");
		}
	}

	if ($u_vars{help})
	{
		print_event($rh->{w}->{current}, "<%text_highlight>$NAME<%n>: Available URL commands:");

		for (@commands, 'GO')
		{
			my $line =
			    " " x 5 . "<%text_bracket>[<%n> $_" . " " x (5-length($_)) . " <%text_bracket>]<%n>";

			if ($_ eq 'HTTP' || $_ eq 'FTP')
			{
				$line .= " <%text_bracket>[<%n> ON | OFF <%text_bracket>]<%n>";
			}
			if ($_ eq 'SAVE')
			{
				$line .= " <%text_bracket>[<%n> <file name> <%text_bracket>]<%n>";
			}
			if ($_ eq 'GO')
			{
				$line .= " <%text_bracket>[<%n> <URL Number> <%text_bracket>]<%n>";
			}
			print_event($rh->{w}->{current}, $line);
		}

		print_window($rh->{w}->{current}, "\n");
	}

	if ($u_vars{clear})
	{
		$rh->{url_buffer} = ();
		print_event($rh->{w}->{current}, "<%text_highlight>URL<%n>: URL List was cleared.");
	}

	if (!%u_vars || $u_vars{http} || $u_vars{ftp})
	{
		my $http_grab = config_lookup('http_grab');
		my $ftp_grab  = config_lookup('ftp_grab');

		$http_grab = ($http_grab) ? "ON" : "OFF";
		$ftp_grab = ($ftp_grab) ? "ON" : "OFF";

		print_event($rh->{w}->{current}, "<%text_highlight>URL<%n>:".
			    "  HTTP Grab: <%text_bracket>[<%n><%text_highlight>$http_grab<%n><%text_bracket>]<%n>".
			    "  FTP Grab: <%text_bracket>[<%n><%text_highlight>$ftp_grab<%n><%text_bracket>]<%n>");
	}

	1;
}

# &MSNre::cmd_window ()
# ----------------------------------------------------------------------
# /WINDOW

sub cmd_window
{
	my %w_vars;
	my @commands = qw(NEW NEXT PREVIOUS KILL LIST HELP);

	while (my $p = shift @_)
	{
		if (grep {lc($p) eq lc($_)} @commands)
		{
			$w_vars{lc $p} = 1; next;
		}
		if (POSIX::isdigit($p)) { $w_vars{change} = $p; next }

		if (lc($p) eq 'title' || lc($p) eq 'double')
		{
			$w_vars{lc($p)} = shift @_ if @_;
			$w_vars{lc($p)."_none"}++ if !defined $w_vars{lc($p)};
			next;
		}
		print_event($rh->{w}->{current}, "<%text_highlight>$NAME<%n>: Unknown /WINDOW command: ".
			    "[<%text_highlight>$p<%n>]");
		$w_vars{unknown}++;
	}

	if (!%w_vars)
	{
		# Print some information about the current window
		my $w = $rh->{w}->{current};

		my $size = $Columns . 'x' . $rh->{w}->{main_v}->[$w]->{screen_size};
		my $title = ($rh->{w}->{main_v}->[$w]->{is_title}) ? "<On>" : "<Off>";
		my $status = ($rh->{w}->{main_v}->[$w]->{is_double}) ? "<On>" : "<Off>";

		my $type = ($w == 0) ? 'Debug window' : ($w == 1) ? 'Main Window' :
		    (defined find_session_by_window($w)) ? 'Chat session' : 'Unused window';

		print_event($w, "\n" . print_line('Current Window'));

		print_line_text($w, 'Window number     ', $w);
		print_line_text($w, 'Window type       ', $type);
		print_line_text($w, 'Window size       ', $size);
		print_line_text($w, 'Title bar is      ', $title);
		print_line_text($w, '2nd status bar is ', $status);

		if ($type eq 'Chat session')
		{
			print_event($w, "\n" . "Users in the current chat session:");
			my $session = find_session_by_window_established($w);
			$session = find_session_by_window($w) if !defined $w;

			my @users = ($msn, $msn->get_swb_users($session));
			my $num = 0;
			my $total = scalar @users;

			for (@users)
			{
				my ($fname, $fname_len);

				if (defined $_->fname)
				{
					$fname = utf8_l($msn->url_decode($_->fname));
					$fname_len = real_length($fname);
				}
				else
				{
					$fname = $_->passport;
					$fname_len = length $fname;
				}

				my $n_t_p = ++$num . " " x (length($total)-length($num));
				my $max_len = length($_->passport.$n_t_p) + $fname_len + length(get_timestamp()) + 7;

				if ($max_len > $Columns - 1)
				{
					truncate_name(\$fname, $fname_len-($max_len-$Columns) - 1);
				}

				print_event($w,
					    "<%text_bracket>[<%n>$n_t_p<%text_bracket>]<%n> ".
					    "<%text_fname>$fname<%n> <%text_passport><".$_->passport."><%n>");
			}
		}
		print_window($w, "\n");
		return 1;
	}

	window_main_create(find_free_window()) if $w_vars{new};
	window_main_kill($rh->{w}->{current}) if $w_vars{kill};
	window_main_change_active($w_vars{change}) if defined $w_vars{change};

	&{$rh->{key_bind}->{command}->{'next-window'}} if $w_vars{next};
	&{$rh->{key_bind}->{command}->{'previous-window'}} if $w_vars{previous};

	if ($w_vars{help})
	{
		print_event($rh->{w}->{current}, "<%text_highlight>$NAME<%n>: Available WINDOW commands:");
		for (@commands, 'TITLE', 'DOUBLE')
		{
			my $line =
			    " " x 5 . "<%text_bracket>[<%n> $_" . " " x (8 - length($_)) . " <%text_bracket>]<%n>";

			if ($_ eq 'TITLE' || $_ eq 'DOUBLE')
			{
				$line .= "  <%text_bracket>[<%n> ON | OFF | TOGGLE <%text_bracket>]";
			}
			print_event($rh->{w}->{current}, $line);
		}
		print_window($rh->{w}->{current}, "\n");
	}

	if ($w_vars{title_none})
	{
		print_event($rh->{w}->{current},
			    "<%text_highlight>$NAME<%n>: Available TITLE commands: [ ON | OFF | TOGGLE ]");
	}

	elsif (defined $w_vars{title})
	{
		if (lc($w_vars{title}) eq 'toggle')
		{
			$w_vars{title} = ($rh->{w}->{main_v}->[$rh->{w}->{current}]->{is_title}) ? 'off' : 'on';
		}
		if (lc($w_vars{title}) eq 'on')
		{
			if ($rh->{w}->{main_v}->[$rh->{w}->{current}]->{is_title})
			{
				print_event($rh->{w}->{current}, "The title bar is already ON");
			}
			else
			{
				my $rows =
				    ($rh->{w}->{main_v}->[$rh->{w}->{current}]->{is_double}) ? $Rows-4 : $Rows-3;

				my $window = $rh->{w}->{main}->[$rh->{w}->{current}];

				print_event($rh->{w}->{current},
				"The title bar for window <%text_highlight>[$rh->{w}->{current}]<%n> is now ON");

				$window->mvwin(1, 0);
				$window->resize($rows, $Columns);
				$rh->{w}->{main_v}->[$rh->{w}->{current}]->{screen_size} = $rows-1;
				$rh->{w}->{main_v}->[$rh->{w}->{current}]->{is_title} = 1;

				window_title_update();
				$rh->{w}->{main_v}->[$rh->{w}->{current}]->{scrolling} = 1;
				window_main_redraw($rh->{w}->{current}, 1);
			}
		}

		elsif (lc($w_vars{title}) eq 'off')
		{
			if (!$rh->{w}->{main_v}->[$rh->{w}->{current}]->{is_title})
			{
				print_event($rh->{w}->{current}, "The title bar is already OFF");
			}
			else
			{
				my $rows =
				    ($rh->{w}->{main_v}->[$rh->{w}->{current}]->{is_double}) ? $Rows-3 : $Rows-2;

				my $window = $rh->{w}->{main}->[$rh->{w}->{current}];

				print_event($rh->{w}->{current},
				"The title bar for window <%text_highlight>[$rh->{w}->{current}]<%n> is now OFF");

				$window->mvwin(0, 0);
				$window->resize($rows, $Columns);
				$rh->{w}->{main_v}->[$rh->{w}->{current}]->{screen_size} = $rows-1;
				$rh->{w}->{main_v}->[$rh->{w}->{current}]->{is_title} = undef;

				$rh->{w}->{main_v}->[$rh->{w}->{current}]->{scrolling} = 1;
				window_main_redraw($rh->{w}->{current}, 0);
			}
		}

		else
		{
			print_event($rh->{w}->{current},
				    "Invalid TITLE command: [<%text_highlight>$w_vars{title}<%n>]");
		}
	}

	if ($w_vars{double_none})
	{
		print_event($rh->{w}->{current},
			    "<%text_highlight>$NAME<%n>: Available DOUBLE commands: [ ON | OFF | TOGGLE ]");
	}
	elsif (defined $w_vars{double})
	{
		if (lc($w_vars{double}) eq 'toggle')
		{
			$w_vars{double} = ($rh->{w}->{main_v}->[$rh->{w}->{current}]->{is_double}) ? 'off' : 'on';
		}
		if (lc($w_vars{double}) eq 'on')
		{
			if ($rh->{w}->{main_v}->[$rh->{w}->{current}]->{is_double})
			{
				print_event($rh->{w}->{current}, "The second status bar is already ON");
			}
			else
			{
				my $begin = ($rh->{w}->{main_v}->[$rh->{w}->{current}]->{is_title}) ? 1 : 0;
				my $rows = $Rows-3-$begin;
				my $window = $rh->{w}->{main}->[$rh->{w}->{current}];

				print_event($rh->{w}->{current}, "The second status bar for window ".
					    "<%text_highlight>[$rh->{w}->{current}]<%n> is now ON");

				$window->mvwin($begin, 0);
				$window->resize($rows, $Columns);
				$rh->{w}->{main_v}->[$rh->{w}->{current}]->{screen_size} = $rows-1;
				$rh->{w}->{main_v}->[$rh->{w}->{current}]->{is_double} = 1;

				# Move the status bar
				$rh->{w}->{status}->[$rh->{w}->{current}]->mvwin($Rows-3, 0);
				window_status_update();

				$rh->{w}->{main_v}->[$rh->{w}->{current}]->{scrolling} = 1;
				window_main_redraw($rh->{w}->{current}, 1);
			}
		}

		elsif (lc($w_vars{double}) eq 'off')
		{
			if (!$rh->{w}->{main_v}->[$rh->{w}->{current}]->{is_double})
			{
				print_event($rh->{w}->{current}, "The second status bar is already OFF");
			}
			else
			{
				my $begin = ($rh->{w}->{main_v}->[$rh->{w}->{current}]->{is_title}) ? 1 : 0;
				my $rows = $Rows-2-$begin;
				my $window = $rh->{w}->{main}->[$rh->{w}->{current}];

				print_event($rh->{w}->{current}, "The second status bar for window ".
					    "<%text_highlight>[$rh->{w}->{current}]<%n> is now OFF");

				$window->mvwin($begin, 0);
				$window->resize($rows, $Columns);
				$rh->{w}->{main_v}->[$rh->{w}->{current}]->{screen_size} = $rows-1;
				$rh->{w}->{main_v}->[$rh->{w}->{current}]->{is_double} = undef;

				# Move the status bar
				$rh->{w}->{status}->[$rh->{w}->{current}]->mvwin($Rows-2, 0);
				window_status_update();

				$rh->{w}->{main_v}->[$rh->{w}->{current}]->{scrolling} = 1;
				window_main_redraw($rh->{w}->{current}, 1);
			}
		}

		else
		{
			print_event($rh->{w}->{current},
				    "Invalid DOUBLE command: [<%text_highlight>$w_vars{double}<%n>]");
		}
	}

	if ($w_vars{list})
	{
		print_event($rh->{w}->{current}, "\n" . print_line('Windows List'));

		for (grep defined $rh->{w}->{main}->[$_], 0..scalar @{$rh->{w}->{main}}-1)
		{
			my $message;
			next if !defined $rh->{w}->{main}->[$_];
			$message = 'Main window' if $_ == 1;
			$message = 'Debug window' if $_ == 0;
			my $session = find_session_by_window($_);

			if (defined $session && !defined $message)
			{
				$message = "Chat session; Users: " . (scalar ($msn->get_swb_users($session))+1) . '; ';
				$message .=
				    (defined find_session_by_window_established($rh->{sessions}->[$session]->{window}))
				    ? 'Connected' : 'Disconnected';
			}
			$message ||= 'Unused window';
			print_event($rh->{w}->{current}, " <%text_bracket>[<%n> Window <%text_highlight>[$_]<%n> ".
				    "<%text_bracket>]  <%text_highlight>$message");
		}
		print_window($rh->{w}->{current}, "\n");
	}
	1;
}

# -------------------- Callback functions -------------------- #

# &MSNre::cb_add_by_user ()
# ----------------------------------------------------------------------
# ADD_BY_USER

sub cb_add_by_user
{
	my ($passport, $fname) = @_;
	my $real_user = $msn->get_user($passport);

	contacts_add(passport => $passport, fname => $fname);
	$fname = utf8_l($msn->url_decode($fname));

	print_event(1, "<%text_fname>$fname <%text_passport><$passport><%n> added you to the contact list");

	if (!$real_user->is_user_in_list('allow_list') && !$real_user->is_user_in_list('block_list'))
	{
		if (!config_lookup('auto_allow'))
		{
			print_event(1, "Use the <%text_highlight>/ALLOW<%n> or <%text_highlight>/BLOCK<%n> ".
				    "command to allow or block the user.");

			# Be able to allow/block the user with the completion
			push @{$rh->{complete_to_al_bl}}, $real_user->passport;
		}
		else { $msn->add_user($passport, 'allow_list') }
	}

	if (!$real_user->is_user_in_list('forward_list'))
	{
		print_event(1,
			    "Use the <%text_highlight>/ADDUSER<%n> command to add the user to your contact list.\n");

		# Be able to add the user with the completion
		push @{$rh->{complete_to_add}}, $real_user->passport;
	}
	1;
}

# &MSNre::cb_add_group ()
# ----------------------------------------------------------------------
# ADD_GROUP

sub cb_add_group
{
	my $group_name = utf8_l($msn->url_decode(shift));
	print_event($rh->{w}->{current}, "Group <%text_highlight>$group_name<%n> was added");
	1;
}

# &MSNre::cb_add_user ()
# ----------------------------------------------------------------------
# ADD_USER

sub cb_add_user
{
	my ($passport, $group_name) = @_;
	my $real_user = $msn->get_user($passport);

	$group_name = utf8_l($msn->url_decode($group_name)) if defined $group_name;

	(defined $real_user && defined $real_user->fname)
	    ? contacts_add(passport => $passport, fname => $real_user->fname) : contacts_add(passport => $passport);

	if (defined $group_name)   # Added to a specific group
	{
		if (defined $real_user && defined $real_user->fname)   # Friendly name known
		{
			print_event($rh->{w}->{current},
			"<%text_fname>" . utf8_l($msn->url_decode($real_user->fname)) .
			"<%text_passport> <$passport><%n> was added to group <%text_highlight>$group_name<%n>");
		}
		else
		{
			print_event($rh->{w}->{current}, "User <%text_passport><$passport><%n> was added to group ".
				    "<%text_highlight>$group_name<%n>");
		}
	}

	elsif (defined $real_user && defined $real_user->fname)   # Group not known
	{
		my $fname = utf8_l($msn->url_decode($real_user->fname));
		my $fname_len = real_length($fname);
		my $real_len = length($passport) + $fname_len + real_length(get_timestamp()) + 14;

		if ($real_len > $Columns-1 && config_lookup('truncate_name_not'))
		{
			truncate_name(\$fname, $fname_len-($real_len-$Columns)-1);
		}
		print_event($rh->{w}->{current},
			    "<%text_fname>$fname <%text_passport><$passport><%n> was added");
	}
	else    # Group and friendly name both unknown
	{
		print_event($rh->{w}->{current}, "User <%text_passport><$passport><%n> was added");
	}

	if (!$real_user->is_user_in_list('allow_list') && !$real_user->is_user_in_list('block_list'))
	{
		$msn->add_user($real_user->passport, 'allow_list');
	}
	1;
}

# &MSNre::cb_alert ()
# ----------------------------------------------------------------------
# ALERT

sub cb_alert
{
	my ($url_from, $url_action, $text) = @_;
	my $window = $rh->{w}->{current};
	my $session = ($rh->{alert}) ? scalar @{$rh->{alert}} : 0;

	# Save the alert so the user can view it
	$rh->{alert}->[$session] =
	    { source => $url_from, action => $url_action, session => $session, text => $text };

	print_event($window, "\n" . print_line('MSN Alert'));
	print_event($window, "You just received an MSN alert");
	print_event($window, "From URL   : <%text_highlight>$url_from");
	print_event($window, "Alert ID   : <%text_highlight>[$session]");
	print_event($window, "Alert text : <%text_highlight>$text\n");

	1;
}

# &MSNre::cb_allow_user ()
# ----------------------------------------------------------------------
# ALLOW_USER

sub cb_allow_user
{
	my $passport = shift;
	my $real_user = $msn->get_user($passport);

	(defined $real_user && defined $real_user->fname)
	    ? contacts_add(passport => $passport, fname => $real_user->fname) : contacts_add(passport => $passport);

	if (defined $real_user && defined $real_user->fname)
	{
		my $fname = utf8_l($msn->url_decode($real_user->fname));
		my $fname_len = real_length($fname);
		my $real_len = length($passport) + $fname_len + real_length(get_timestamp()) + 16;

		if ($real_len > $Columns - 1 && config_lookup('truncate_name_not'))
		{
			truncate_name(\$fname, $fname_len - ($real_len - $Columns) - 1);
		}

		# Friendly name known
		print_event($rh->{w}->{current},
			    "<%text_fname>$fname <%text_passport><$passport><%n> was allowed");
		return 1;
	}

	# Friendly name not known
	print_event($rh->{w}->{current}, "User <%text_passport><$passport><%n> was allowed");
	1;
}

# &MSNre::cb_block_user ()
# ----------------------------------------------------------------------
# BLOCK_USER

sub cb_block_user
{
	my $passport = shift;
	my $real_user = $msn->get_user($passport);

	(defined $real_user && defined $real_user->fname)
	    ? contacts_add(passport => $passport, fname => $real_user->fname) : contacts_add(passport => $passport);

	if (defined $real_user && defined $real_user->fname)
	{
		my $fname = utf8_l($msn->url_decode($real_user->fname));
		my $fname_len = real_length($fname);
		my $real_len = length($passport) + $fname_len + real_length(get_timestamp()) + 16;

		if ($real_len > $Columns - 1 && config_lookup('truncate_name_not'))
		{
			truncate_name(\$fname, $fname_len - ($real_len - $Columns) - 1);
		}

		# Friendly name known
		print_event($rh->{w}->{current},
			    "<%text_fname>$fname <%text_passport><$passport><%n> was blocked");
		return 1;
	}

	# Friendly name not known
	print_event($rh->{w}->{current}, "User <%text_passport><$passport><%n> was blocked");
	1;
}

# &MSNre::cb_change_my_friendly_name ()
# ----------------------------------------------------------------------
# CHANGE_MY_FRIENDLY_NAME

sub cb_change_my_friendly_name
{
	my $new_fname = utf8_l($msn->url_decode(shift));

	print_event($rh->{w}->{current}, "Your friendly name was changed to <%text_highlight>$new_fname");
	window_status_update();
	1;
}

# &MSNre::cb_change_my_status ()
# ----------------------------------------------------------------------
# CHANGE_MY_STATUS

sub cb_change_my_status
{
	my $status = ucfirst(shift);  $status =~ s/_/ /g;
	my $msg = "Your status was changed to <%text_highlight>$status<%n>";
	my $window = $rh->{w}->{current};

	$rh->{auto_away_sent} = undef;

	if (defined $rh->{timers}->{idle}->{setting})  # The status was auto-changed
	{
		if ($status eq 'Online' && $rh->{timers}->{idle}->{setting} eq 'online')  # ... to online
		{
			$rh->{timers}->{idle}->{start} = time;
			$rh->{timers}->{idle}->{setting} = undef;
			$rh->{timers}->{idle}->{set} = 0;
			$rh->{timers}->{idle}->{set_time} = undef;

			$msg .= " (Auto)";
			$window = 1;
		}

		elsif ($status eq 'Idle' && $rh->{timers}->{idle}->{setting} eq 'idle')   # ... to idle
		{
			$rh->{timers}->{idle}->{setting} = undef;
			$rh->{timers}->{idle}->{set} = 1;
			$msg .= " (Auto)";
			$window = 1;

			$rh->{timers}->{idle}->{set_time} = time;
		}
	}

	print_event($window, $msg . '.');
	window_status_update();
	1;
}

# &MSNre::cb_change_status ()
# ----------------------------------------------------------------------
# CHANGE_STATUS

sub cb_change_status
{
	my ($passport, $fname, $status, $o_status) = @_;
	$status = ucfirst($status);
	$o_status = ucfirst($o_status) if defined $o_status;

	contacts_add(passport => $passport, fname => $fname);

	$fname = utf8_l($msn->url_decode($fname));
	$status =~ s/_/ /g;

	my $fname_len = real_length($fname);
	my $real_len = length($passport.$status) + $fname_len + real_length(get_timestamp()) + 12;

	my $middle = ($status eq 'Be right back') ? 'will' : 'is now';

	if ($status eq 'Online' && defined $rh->{timers}->{away_idle}->{$passport})
	{
		# User is back online from away/idle
		print_event(1,
			    "<%text_fname>$fname <%text_passport><$passport><%n> $middle " .
			    "<%text_highlight>$status<%n> after being $o_status for <%text_highlight>".
			    get_time(time - $rh->{timers}->{away_idle}->{$passport}));

		$rh->{timers}->{away_idle}->{$passport} = undef;
	}
	else
	{
		if ($real_len > $Columns && config_lookup('truncate_name_not'))
		{
			truncate_name(\$fname, $fname_len - ($real_len - $Columns) - 1);
		}
		$rh->{timers}->{away_idle}->{$passport} = undef if defined $rh->{timers}->{away_idle}->{$passport};

		print_event(1,
			    "<%text_fname>$fname <%text_passport><$passport><%n> $middle <%text_highlight>$status");

		if (lc($status) eq 'away' || lc($status) eq 'idle')
		{
			$rh->{timers}->{away_idle}->{$passport} = time;
		}
	}

	window_status_update();
	event_script_call('status', $passport, $status);

	return 1 if $status ne 'Online' || !defined $o_status || $o_status ne 'Offline';

	# Print that the user logged in also to every chat window the user was previously in

	if ($rh->{sessions} && scalar @{$rh->{w}->{main}} > 1)
	{
		for (my $i = 2; $i <= @{$rh->{w}->{main}}; $i++)
		{
			next if !defined $rh->{w}->{main}->[$i];
			my $session = find_session_by_window($i);

			if ($rh->{sessions}->[$session]->{first_user} eq $passport)
			{
				print_event($rh->{sessions}->[$session]->{window},
					    "User <%text_highlight>$passport<%n> logged in.");
			}
		}
	}

	$rh->{timers}->{login_time}->{$passport} = time;
	1;
}

# &MSNre::cb_change_status_offline ()
# ----------------------------------------------------------------------
# CHANGE_STATUS_OFFLINE

sub cb_change_status_offline
{
	my $passport = shift;
	my $real_user = $msn->get_user($passport);

	contacts_add(passport => $passport, fname => $real_user->fname, last_seen => time);

	if ($rh->{sessions} && scalar @{$rh->{w}->{main}} > 1)
	{
		# Print that the user logged out to every chat window the user was in
		for (my $i = 2; $i <= @{$rh->{w}->{main}}; $i++)
		{
			next if !defined $rh->{w}->{main}->[$i];

			my $s_e = find_session_by_window_established($i);
			my $s_ne = find_session_by_window($i);

			if (defined $s_e)
			{
				next if !grep {$_->passport eq $passport} $msn->get_swb_users($s_e);
			}
			elsif (defined $s_ne)
			{
				next if $rh->{sessions}->[$s_ne]->{first_user} ne $passport;
			}

			print_event($i, "User <%text_highlight>$passport<%n> logged out.");
		}
	}

	$rh->{timers}->{login_time}->{$passport} = undef;
	$rh->{timers}->{away_idle}->{$passport} = undef if defined $rh->{timers}->{away_idle}->{$passport};

	if (keys %{$rh->{auto_away_sent}})   # Clear the autoaway messages for this user
	{
		for (keys %{$rh->{auto_away_sent}})
		{
			$rh->{auto_away_sent}->{$_}->{u}->{$passport} = undef if
			    defined $rh->{auto_away_sent}->{$_}->{u} &&
			    defined $rh->{auto_away_sent}->{$_}->{u}->{$passport};
		}
	}

	my $c_user = contacts_get_contact($passport);

	if (defined $c_user)
	{
		# Delete temporary values that could be also stored
		for ('chat_logging', 'ip_address', 'user_agent', 'clientcaps_sent')
		{
			delete $c_user->{$_} if exists $c_user->{$_};
		}
	}

	print_event(1, "User <%text_passport><$passport><%n> logged out");
	window_status_update();
	event_script_call('status', $passport, 'Offline');

	return 1 if !$rh->{ft_sessions};

	# Disconnect file transfers
	for (my $i = 0; $i < @{$rh->{ft_sessions}}; $i++)
	{
		disconnect_file($rh->{ft_sessions}->[$i]->{session}) if
		    defined $rh->{ft_sessions}->[$i]->{user} && $rh->{ft_sessions}->[$i]->{user} eq $passport;
	}

	1;
}

# &MSNre::cb_close_chat ()
# ----------------------------------------------------------------------
# CLOSE_CHAT

sub cb_close_chat
{
	my ($session, $user) = @_;

	if (defined $user && !exists $rh->{sessions}->[$session])
	{
		# Error while inviting the first user to the session, remove the timer
		for (my $i = 0; $i < @{$rh->{s_establishing}}; $i++)
		{
			splice @{$rh->{s_establishing}}, $i--, 1 if $rh->{s_establishing}->[$i]->{user} eq $user;
		}

		# Remove the queued messages
		for (my $i = 0; $i < @{$rh->{s_queue}}; $i++)
		{
			splice @{$rh->{s_queue}}, $i--, 1 if $rh->{s_queue}->[$i]->{user} eq $user;
		}

		msnre_debug("SWB Failed while inviting for: $user; Temp session: $session");
		return 1;
	}

	$rh->{sessions}->[$session]->{established} = 0;
	$rh->{sessions}->[$session]->{typing} = undef;
	$rh->{timers}->{typing}->[$session]->{typing_start} = undef;

	# Get rid of possibly unclosed sessions
	if ($rh->{sessions})
	{
		for (my $i = 0; $i < @{$rh->{sessions}}; $i++)
		{
			disconnect_swb($i) if defined $rh->{sessions}->[$session]->{window} && defined
			    $rh->{sessions}->[$i]->{window} &&
			    $rh->{sessions}->[$i]->{window} == $rh->{sessions}->[$session]->{window} &&
			    $rh->{sessions}->[$i]->{established};
		}
	}

	window_title_update($rh->{sessions}->[$session]->{window});
	1;
}

# &MSNre::cb_debug ()
# ----------------------------------------------------------------------
# DEBUG

sub cb_debug
{
	if (config_lookup('debug_protocol'))
	{
		my $message = join ' ', @_;  $message =~ s/\r//g;

		print_window_no_colors(0, $message . "\n");
		debug_log($message) if $rh->{debug}->{debug_log};

		$rh->{w}->{main}->[0]->noutrefresh if $rh->{w}->{current} == 0;
	}
	1;
}

# &MSNre::cb_debug_connection ()
# ----------------------------------------------------------------------
# DEBUG_CONNECTION

sub cb_debug_connection
{
	if (config_lookup('debug_connection'))
	{
		my $message = join ' ', @_;  $message =~ s/\r//g;

		print_window_no_colors(0, $message . "\n");
		debug_log($message) if $rh->{debug}->{debug_log};

		$rh->{w}->{main}->[0]->noutrefresh if $rh->{w}->{current} == 0;
	}
	1;
}

# &MSNre::cb_disconnect ()
# ----------------------------------------------------------------------
# DISCONNECT

sub cb_disconnect
{
	my $reason = shift;

	print_event(1, "<%R>[Disconnect]<%n> $reason") if defined $reason;
	disconnect_msn();
	1;
}

# &MSNre::cb_disconnect_forced ()
# ----------------------------------------------------------------------
# DISCONNECT_FORCES

sub cb_disconnect_forced
{
	cb_disconnect(@_);
	return 1 if !config_lookup('auto_reconnect');

	# Kicked by the server. Try to reconnect
	if (!defined $rh->{timers}->{reconnect})
	{
		print_event(1, "Reconnecting automatically in 10 seconds. ".
			    "Use <%text_highlight>/DISCONNECT<%n> to cancel it.");

		$rh->{timers}->{reconnect} = time;
	}
	else
	{
		$rh->{timers}->{reconnect} = undef;
	}

	1;
}

# &MSNre::cb_email_active ()
# ----------------------------------------------------------------------
# EMAIL_ACTIVE

sub cb_email_active
{
	my ($src, $dest, $delta) = @_;

	$rh->{misc}->{unread_mail} -= $delta if defined $rh->{misc}->{unread_mail};
	window_status_update();
	1;
}

# &MSNre::cb_email_invite_success ()
# ----------------------------------------------------------------------
# EMAIL_INVITE_SUCCESS

sub cb_email_invite_success
{
	my $passport = shift;

	print_event($rh->{w}->{current}, "The invitation e-mail for <%text_highlight>$passport<%n> was ".
		    "successfully sent.");
	1;
}

# &MSNre::cb_email_new ()
# ----------------------------------------------------------------------
# EMAIL_NEW

sub cb_email_new
{
	my ($from_addr, $from, $subject) = @_;

	$from_addr = utf8_l($from_addr) if defined $from_addr;
	$from = utf8_l($from) if defined $from;

	print_event(1, "\n" . print_line('New E-mail'));

	print_line_text(1, 'From (address) ', $from_addr);
	print_line_text(1, 'From (name)    ', $from)        if defined $from;
	print_line_text(1, 'Subject        ', $subject)     if defined $subject;

	print_window(1, "\n");

	# Add it to the unread e-mails
	$rh->{misc}->{unread_mail}++ if defined $rh->{misc}->{unread_mail};
	$rh->{misc}->{unread_mail} ||= 1;
	window_status_update();

	# Fixes for the event script

	$from_addr = "" if !defined $from_addr;
	$from      = "" if !defined $from;
	$subject   = "" if !defined $subject;

	my $e_from    = $from;
	my $e_subject = $subject;

	$e_from    =~ s/(?<!\\)\"/\\\"/g;
	$e_subject =~ s/(?<!\\)\"/\\\"/g;

	event_script_call('email', $from_addr, '"' . $e_from . '"', '"' . $subject . '"');
	1;
}

# &MSNre::cb_email_unread ()
# ----------------------------------------------------------------------
# EMAIL_UNREAD

sub cb_email_unread
{
	my $messages = shift;

	$rh->{misc}->{unread_mail} = $messages;
	1;
}

# &MSNre::cb_file_accept ()
# ----------------------------------------------------------------------
# FILE_ACCEPT

sub cb_file_accept
{
	my ($swb_session, $file_session, $user) = @_;
	return 1 if $rh->{sessions}->[$swb_session]->{ignore};

	my $f_hash = file_find_session($swb_session, $file_session);
	my $w = $rh->{sessions}->[$swb_session]->{window} if defined $rh->{sessions}->[$swb_session]->{window};

	$f_hash->{user} = $user;

	if (defined $w)
	{
		if (defined $user)
		{
			print_event($w, "File transfer <%text_highlight>[$f_hash->{session}]<%n> was accepted ".
				    "by <%text_passport>$f_hash->{user}");
		}
		else
		{
			print_event($w, "File transfer <%text_highlight>[$f_hash->{session}]<%n> was accepted ".
				    "by the user");
		}
	}

	$f_hash->{status} = "Accepted";
	$f_hash->{can_cancel} = 1;

	file_add_timer($f_hash->{session});
	1;
}

# &MSNre::cb_file_invited ()
# ----------------------------------------------------------------------
# FILE_INVITED

sub cb_file_invited
{
	my ($swb_session, $file_session, $sender, $sender_name, $file_name, $file_size) = @_;

	contacts_add(passport => $sender, fname => $sender_name, last_seen => time);

	if ($rh->{sessions}->[$swb_session]->{ignore})
	{
		msnre_debug("File transfer ignored (Session $swb_session)");
		return 1;
	}

	# This could be also the first message in the chat
	window_main_create_chat($swb_session) if need_window_chat($swb_session);

	my $w         = $rh->{sessions}->[$swb_session]->{window};
	my $user_ref  = $msn->get_user($sender);

	my $real_user = (defined $user_ref && defined $user_ref->fname &&
			 real_length(utf8_l($msn->url_decode($user_ref->fname))) < ($Columns/2))
	                 ? utf8_l($msn->url_decode($user_ref->fname)) : $sender;

	my $t_session = file_save_session($swb_session, $file_session, $sender, $file_name, $file_size);
	my $f_hash = file_find_ref_by_session($t_session);

	$f_hash->{type} = 'incoming';
	$f_hash->{status} = 'Invited; Pending';

	$f_hash->{can_accept} = 1;
	$f_hash->{can_reject} = 1;

	print_event($w, "\n" . print_line('Incoming File Transfer'));
	print_event($w, "<%contact_passport>$real_user<%n> is sending you a file");

	print_line_text($w, 'File name    ', $file_name);
	print_line_text($w, 'File size    ', sprintf("%.3f", $file_size / 1024) . ' kB<%n>');
	print_line_text($w, 'File session ', "[$f_hash->{session}]\n");

	if (config_lookup('ft_auto_accept'))  # Auto-accept configuration option
	{
		print_event($w, "The file was Auto-Accepted\n");

		cmd_file_accept($f_hash->{session});
		file_add_timer($f_hash->{session});
		return 1;
	}

	print_event($w, "Use the <%text_highlight>/FILEACCEPT<%n> or <%text_highlight>/FILEREJECT<%n> command.");
	1;
}

# &MSNre::cb_file_receive_cancel ()
# ----------------------------------------------------------------------
# FILE_RECEIVE_CANCEL

sub cb_file_receive_cancel
{
	my ($swb_session, $file_session, $reason) = @_;
	my $f_hash = file_find_session($swb_session, $file_session);
	my $w = $rh->{sessions}->[$swb_session]->{window} if defined $rh->{sessions}->[$swb_session]->{window};

	save_local_params($swb_session, $f_hash->{user});
	return 1 if $rh->{sessions}->[$swb_session]->{ignore};

	if (defined $w && defined $rh->{w}->{main}->[$w])
	{
		print_event($w,
		"The file <%text_highlight>[$f_hash->{session}]<%n> couldn't be received because it was canceled.");
		print_event($w, "Cancel reason: <%text_highlight>$reason");
	}

	$f_hash->{status} = "Canceled";
	$f_hash->{active} = 0;
	$f_hash->{can_accept} = 0;
	$f_hash->{can_cancel} = 0;
	$f_hash->{can_reject} = 0;

	file_remove_timer($f_hash->{session});

	my $c_received   = config_lookup('received');
	my $c_received_p = config_lookup('received_partial');

	return if !defined $c_received || !defined $c_received_p;

	# Move the file to partial if it exists
	my $file_path_old = config_lookup('received') . '/' . $f_hash->{file_name};

	if (-f $file_path_old)
	{
		require File::Copy;
		File::Copy::move($file_path_old,
				 config_lookup('received_partial') . '/' . $f_hash->{file_name});
	}
	1;
}

# &MSNre::cb_file_receive_progress ()
# ----------------------------------------------------------------------
# FILE_RECEIVE_PROGRESS

sub cb_file_receive_progress
{
	my ($swb_session, $file_session, $transfered, $total) = @_;
	return 1 if $rh->{sessions}->[$swb_session]->{ignore};

	my $f_hash = file_find_session($swb_session, $file_session);

	$f_hash->{transfered} = $transfered;
	$f_hash->{status} = "Receiving (<%text_highlight>" . int (($transfered/$total)*100) . "%<%n> Done)";
	file_add_timer($f_hash->{session});
	1;
}

# &MSNre::cb_file_receive_start ()
# ----------------------------------------------------------------------
# FILE_RECEIVE_START

sub cb_file_receive_start
{
	my ($swb_session, $file_session) = @_;
	my $f_hash = file_find_session($swb_session, $file_session);
	my $w = $rh->{sessions}->[$swb_session]->{window} if defined $rh->{sessions}->[$swb_session]->{window};

	save_local_params($swb_session, $f_hash->{user});
	return 1 if $rh->{sessions}->[$swb_session]->{ignore};

	if (defined $w && defined $rh->{w}->{main}->[$w])
	{
		print_event($w,
			    "File transfer <%text_highlight>[$f_hash->{session}]<%n> started at " .
			    strftime("%H:%M:%S", localtime));

		print_event($w,
			    "Receiving <%text_highlight>[" . sprintf("%.3f", $f_hash->{file_size}/1024) . " kB]<%n> ".
			    "from <%contact_passport>$f_hash->{user}<%n>\n");
	}

	$f_hash->{tfr_start} = ($rh->{time_hires}) ? Time::HiRes::time : time;
	$f_hash->{status} = "Receiving";
	$f_hash->{can_cancel} = 1;

	file_add_timer($f_hash->{session});
	1;
}

# &MSNre::cb_file_receive_success ()
# ----------------------------------------------------------------------
# FILE_RECEIVE_SUCCESS

sub cb_file_receive_success
{
	my ($swb_session, $file_session) = @_;
	return 1 if $rh->{sessions}->[$swb_session]->{ignore};

	my $f_hash = file_find_session($swb_session, $file_session);
	my $w = $rh->{sessions}->[$swb_session]->{window} if defined $rh->{sessions}->[$swb_session]->{window};

	my $ft_time = ($rh->{time_hires})
	    ? sprintf("%.5f", Time::HiRes::time-$f_hash->{tfr_start}) : time-$f_hash->{tfr_start};

	my $ft_speed = ($rh->{time_hires})
	    ? sprintf("%.2f", ($f_hash->{file_size}/1024)/$ft_time) : ($f_hash->{file_size}/1024)/$ft_time;

	if (defined $w && defined $rh->{w}->{main}->[$w])
	{
		print_event($w,
			    "File transfer <%text_highlight>[$f_hash->{session}]<%n> finished at " .
			    strftime("%H:%M:%S", localtime));

		print_event($w, "File <%text_highlight>[" . sprintf("%.3f", $f_hash->{file_size}/1024) . " kB]<%n> ".
			    "transfered in <%text_highlight>$ft_time<%n> secs ($ft_speed kB/s)\n");
	}

	$f_hash->{status} = "Done";
	$f_hash->{active} = 0;
	$f_hash->{can_cancel} = 0;

	file_remove_timer($f_hash->{session});
	1;
}

# &MSNre::cb_file_reject ()
# ----------------------------------------------------------------------
# FILE_REJECT

sub cb_file_reject
{
	my ($swb_session, $file_session, $user) = @_;
	return 1 if $rh->{sessions}->[$swb_session]->{ignore};

	my $f_hash = file_find_session($swb_session, $file_session);
	my $w = $rh->{sessions}->[$swb_session]->{window} if defined $rh->{sessions}->[$swb_session]->{window};

	$f_hash->{user} = $user;

	if (defined $w && defined $rh->{w}->{main}->[$w])
	{
		if (defined $user)
		{
			print_event($w, "File transfer <%text_highlight>[$f_hash->{session}]<%n> was accepted ".
				    "by <%text_passport>$f_hash->{user}");
		}
		else
		{
			print_event($w, "File transfer <%text_highlight>[$f_hash->{session}]<%n> was accepted ".
				    "by the user");
		}
	}

	$f_hash->{status} = "Rejected";
	$f_hash->{active} = 0;
	$f_hash->{can_cancel} = 0;

	file_remove_timer($f_hash->{session});
	1;
}

# &MSNre::cb_file_send_cancel ()
# ----------------------------------------------------------------------
# FILE_SEND_CANCEL

sub cb_file_send_cancel
{
	my ($swb_session, $file_session, $reason) = @_;
	my $f_hash = file_find_session($swb_session, $file_session);
	my $w = $rh->{sessions}->[$swb_session]->{window} if defined $rh->{sessions}->[$swb_session]->{window};

	if (defined $f_hash->{user} && $f_hash->{user} ne $msn->passport)
	{
		save_local_params($swb_session, $f_hash->{user});
	}
	return 1 if $rh->{sessions}->[$swb_session]->{ignore};

	if (defined $w && defined $rh->{w}->{main}->[$w])
	{
		print_event($w,
		    "The file <%text_highlight>[$f_hash->{session}]<%n> couldn't be sent because it was canceled.");

		print_event($w, "Cancel reason: <%text_highlight>$reason");
	}

	$f_hash->{status} = "Canceled";
	$f_hash->{active} = 0;
	$f_hash->{can_cancel} = 0;

	file_remove_timer($f_hash->{session});
	1;
}

# &MSNre::cb_file_send_progress ()
# ----------------------------------------------------------------------
# FILE_SEND_PROGRESS

sub cb_file_send_progress
{
	my ($swb_session, $file_session, $transfered, $total) = @_;
	return 1 if $rh->{sessions}->[$swb_session]->{ignore};

	my $f_hash = file_find_session($swb_session, $file_session);

	$f_hash->{transfered} = $transfered;
	$f_hash->{status} = "Sending (<%text_highlight>" . int (($transfered/$total)*100) . "%<%n> Done)";
	1;
}

# &MSNre::cb_file_send_start ()
# ----------------------------------------------------------------------
# FILE_SEND_START

sub cb_file_send_start
{
	my ($swb_session, $file_session, $user) = @_;
	my $f_hash = file_find_session($swb_session, $file_session);
	my $w = $rh->{sessions}->[$swb_session]->{window} if defined $rh->{sessions}->[$swb_session]->{window};

	$f_hash->{user} = $user;

	save_local_params($swb_session, $f_hash->{user});
	return 1 if $rh->{sessions}->[$swb_session]->{ignore};

	if (defined $w && defined $rh->{w}->{main}->[$w])
	{
		print_event($w,
			    "File transfer <%text_highlight>[$f_hash->{session}]<%n> started at " .
			    strftime("%H:%M:%S", localtime));

		print_event($w,
			    "Sending <%text_highlight>[" . sprintf("%.3f", $f_hash->{file_size}/1024) . " kB]<%n> ".
			    "to <%contact_passport>$f_hash->{user}<%n>\n");
	}

	$f_hash->{tfr_start} = ($rh->{time_hires}) ? Time::HiRes::time : time;
	$f_hash->{status} = "Sending";
	$f_hash->{can_cancel} = 1;

	file_add_timer($f_hash->{session});
	1;
}

# &MSNre::cb_file_send_success ()
# ----------------------------------------------------------------------
# FILE_SEND_SUCCESS

sub cb_file_send_success
{
	my ($swb_session, $file_session) = @_;
	return 1 if $rh->{sessions}->[$swb_session]->{ignore};

	my $f_hash = file_find_session($swb_session, $file_session);
	my $w = $rh->{sessions}->[$swb_session]->{window} if defined $rh->{sessions}->[$swb_session]->{window};

	my $ft_time = ($rh->{time_hires})
	    ? sprintf("%.5f", Time::HiRes::time-$f_hash->{tfr_start}) : time-$f_hash->{tfr_start};

	my $ft_speed = ($rh->{time_hires})
	    ? sprintf("%.2f", ($f_hash->{file_size}/1024)/$ft_time) : ($f_hash->{file_size}/1024)/$ft_time;

	if (defined $w && defined $rh->{w}->{main}->[$w])
	{
		print_event($w,
			    "File transfer <%text_highlight>[$f_hash->{session}]<%n> finished at " .
			    strftime("%H:%M:%S", localtime));

		print_event($w, "File <%text_highlight>[" . sprintf("%.3f", $f_hash->{file_size}/1024) . " kB]<%n> " .
			    "transfered in <%text_highlight>$ft_time<%n> secs ($ft_speed kB/s)\n");
	}

	$f_hash->{status} = "Done";
	$f_hash->{active} = 0;
	$f_hash->{can_cancel} = 0;

	file_remove_timer($f_hash->{session});
	1;
}

# &MSNre::cb_find_invite_success ()
# ----------------------------------------------------------------------
# FIND_INVITE_SUCCESS

sub cb_find_invite_success
{
	my $search_nr = shift;

	print_event($rh->{w}->{current}, "Invitation e-mail to the member was successfully sent.");
	1;
}

# &MSNre::cb_find_results ()
# ----------------------------------------------------------------------
# FIND_RESULTS

sub cb_find_results
{
	my (@results) = @_;
	my ($fname_len, $lname_len, $city_len, $state_len) = (0,0,0,0);
	my @all_keys = ('fname', 'lname', 'city', 'state', 'country');

	if (@results == 1 && !grep {defined $results[0]->{$_}} keys %{$results[0]})
	{
		print_event($rh->{w}->{current}, "No matches for the member search.");
		return undef;
	}

	if (@results)
	{
		for my $r(@results)
		{
			map {(!defined $r->{$_}) ? $r->{$_} = "<Unknown>" : $r->{$_} .= "  "} @all_keys;
			map {$r->{$_} = utf8_l($msn->url_decode($r->{$_}))} @all_keys;

			$fname_len   = length($r->{fname})   if length($r->{fname})   > $fname_len;
			$lname_len   = length($r->{lname})   if length($r->{lname})   > $lname_len;
			$city_len    = length($r->{city})    if length($r->{city})    > $city_len;
			$state_len   = length($r->{state})   if length($r->{state})   > $state_len;
		}

		my $head = "No.    First name  " . " " x (++$fname_len-12) . "Last name  " . " " x (++$lname_len-11) .
		    "City  "    . " " x (++$city_len-6)             . "State  "     . " " x (++$state_len-7) .
		    "Country";

		print_event($rh->{w}->{current}, "\n" . print_line('Member search results'));
		print_event($rh->{w}->{current}, "<%text_highlight>$head<%n>");

		$fname_len   = ($fname_len   > 12) ? $fname_len   : 12;
		$lname_len   = ($lname_len   > 11) ? $lname_len   : 11;
		$city_len    = ($city_len    > 6)  ? $city_len    : 6;
		$state_len   = ($state_len   > 7)  ? $state_len   : 7;
		my $num = 0;
		my $total = scalar @results;

		for my $r(@results)
		{
			my $row = "<%text_bracket>[<%n>" . ++$num . " " x (length($total) - length($num)) .
			    "<%text_bracket>]<%n>";

			$row .= " " x (7 - (length($row) - 38));

			$row .= ucfirst($r->{fname})   . " " x ($fname_len   - length($r->{fname}));
			$row .= ucfirst($r->{lname})   . " " x ($lname_len   - length($r->{lname}));
			$row .= ucfirst($r->{city})    . " " x ($city_len    - length($r->{city}));
			$row .= ucfirst($r->{state})   . " " x ($state_len   - length($r->{state}));
			$row .= ucfirst($r->{country});

			print_event($rh->{w}->{current}, $row);
		}
		print_window($rh->{w}->{current}, "\n");
		$rh->{find_invite} = $num;
	}
	1;
}

# &MSNre::cb_initial_status ()
# ----------------------------------------------------------------------
# INITIAL_STATUS

sub cb_initial_status
{
	my ($passport, $fname, $status) = @_;

	contacts_add(passport => $passport, fname => $fname);

	$fname = utf8_l($msn->url_decode($fname));
	my $fname_len = real_length($fname);

	$status = ucfirst($status);  $status =~ s/_/ /g;

	if ($rh->{misc}->{login})   # Logging in - save this one and print it later in the contact list
	{
		push @{$rh->{misc}->{login_iln}}, $passport;
		return 1;
	}

	# Sent after a user gets added to the list

	my $real_len = length($passport.$status) + $fname_len + length(get_timestamp()) + 8;
	if ($real_len > $Columns - 1 && config_lookup('truncate_name_not'))
	{
		truncate_name(\$fname, $fname_len - ($real_len - $Columns) - 1);
	}
	print_event(1, "<%text_fname>$fname<%n> <%text_passport><$passport><%n> is <%text_highlight>$status<%n>");
	1;
}

# &MSNre::cb_join_session_user ()
# ----------------------------------------------------------------------
# JOIN_SESSION_USER

sub cb_join_session_user
{
	my ($session, $passport, $fname) = @_;
	my $win;

	contacts_add(passport => $passport, fname => $fname);

	if ($rh->{s_establishing})   # Find the session and removed from pending
	{
		for (my $i = 0; $i < @{$rh->{s_establishing}}; $i++)
		{
			splice @{$rh->{s_establishing}}, $i--, 1 if $rh->{s_establishing}->[$i]->{user} eq $passport;
		}
	}

	if (!defined $rh->{sessions}->[$session]->{first_user})
	{
		$rh->{sessions}->[$session]->{first_user} = $passport;
		my $c_user = contacts_get_contact($passport);

		if ($c_user->{ignore})   # The user is ignored - ignore the whole switchboard session
		{
			$rh->{sessions}->[$session]->{ignore} = 1;
			msnre_debug("Session $session will be ignored (for $passport)");
			return 1;
		}
	}

	if (defined $rh->{sessions}->[$session]->{window} && (scalar ($msn->get_swb_users($session))) > 1)
	{
		my $fname = contacts_get_fname($passport);

		if (defined $fname)
		{
			$fname = utf8_l($msn->url_decode($fname));

			my $fname_len = real_length($fname);
			my $real_len = length($passport) + $fname_len + length(get_timestamp()) + 20;

			if ($real_len > $Columns - 1 && $rh->{truncate_name_not})
			{
				truncate_name(\$fname, $fname_len - ($real_len - $Columns) - 1);
			}

			print_event($rh->{sessions}->[$session]->{window},
				    "<%text_fname>$fname<%n> <%text_passport><$passport><%n> joined the chat");
		}
		else
		{
			print_event($rh->{sessions}->[$session]->{window},
				    "User <%text_highlight>$passport<%n> joined the chat");
		}
	}

	# TODO: Check if this is still needed
	if (!defined $rh->{sessions}->[$session]->{window})   # Find a window for the chat
	{
		$win = find_chat_with_user($passport);
		$rh->{sessions}->[$session]->{window} = $win if defined $win;
	}
	1;
}

# &MSNre::cb_leave_session_user ()
# ----------------------------------------------------------------------
# LEAVE_SESSION_USER

sub cb_leave_session_user
{
	my ($session, $passport) = @_;
	my $window = $rh->{sessions}->[$session]->{window};
	my $real_user = $msn->get_user($passport);

	if (!defined $real_user)
	{
		contacts_add(passport => $passport, last_seen => time);
	}

	if (!defined $real_user || $real_user->status ne 'offline')
	{
		($msn->get_swb_users($session) > 1)
		    ? print_event($window, "User <%text_highlight>$passport<%n> left the session.")
		    : print_event($window, "User <%text_highlight>$passport<%n> closed the conversation window.");
	}

	window_title_update();
	1;
}

# &MSNre::cb_login_progress ()
# ----------------------------------------------------------------------
# LOGIN_PROGRESS

sub cb_login_progress
{
	my $message = shift;

	print_event(1, "<%text_login>[Login]<%n> *** $message");
	$rh->{misc}->{login} ||= 1;

	window_main_update(1);
	1;
}

# &MSNre::cb_netmeeting_accept ()
# ----------------------------------------------------------------------
# NETMEETING_ACCEPT

sub cb_netmeeting_accept
{
	my ($swb_session, $nm_session, $user) = @_;
	my $nm_hash = netmeeting_find_session($swb_session, $nm_session);

	save_local_params($swb_session, $user);
	$nm_hash->{user} = $user;
	contacts_add(passport => $user, last_seen => time);

	print_event($rh->{sessions}->[$swb_session]->{window},
		    "Your netmeeting invitation <%text_highlight>[$nm_hash->{session}]<%n> was accepted");
	1;
}

# &MSNre::cb_netmeeting_cancel ()
# ----------------------------------------------------------------------
# NETMEETING_CANCEL

sub cb_netmeeting_cancel
{
	my ($swb_session, $nm_session, $c_reason) = @_;
	my $nm_hash = netmeeting_find_session($swb_session, $nm_session);

	if (defined $nm_hash->{user} && !defined $msn->get_user($nm_hash->{user}))
	{
		contacts_add(passport => $nm_hash->{user}, last_seen => time);
	}

	print_event($rh->{sessions}->[$swb_session]->{window},
		    "Netmeeting session <%text_highlight>[$nm_hash>{session}]<%n> was canceled");

	print_event($rh->{sessions}->[$swb_session]->{window},
		    "Cancel reason: <%text_highlight>$c_reason");

	$nm_hash->{active} = 0;
	1;
}

# &MSNre::cb_netmeeting_invited ()
# ----------------------------------------------------------------------
# NETMEETING_INVITED

sub cb_netmeeting_invited
{
	my ($swb_session, $nm_session, $passport, $fname) = @_;
	my $window = $rh->{sessions}->[$swb_session]->{window};
	my %args = (passport => $passport, fname => $fname);

	if (!defined $msn->get_user($passport))
	{
		$args{last_seen} = time;
	}
	contacts_add(%args);

	if ($rh->{sessions}->[$swb_session]->{ignore})
	{
		msnre_debug("Netmeeting invitation ignored (Session $swb_session)");
		return 1;
	}

	window_main_create_chat($swb_session) if need_window_chat($swb_session);
	print_event($window, "\n" . print_line('Incoming NetMeeting invitation'));
	print_line_text($window, 'Invited by         ', $passport);

	if (!find_executable('gnomemeeting'))   # Gnomemeeting is not installed
	{
		print_event($window,
		"\n<%text_highlight>$NAME<%n>: gnomemeeting executable not found in your path. Rejecting.\n");
		$msn->reject_not_installed($swb_session, 'netmeeting', $nm_session);
		return undef;
	}

	# Create the local session
	my $local_session = ($rh->{nm_sessions}) ? scalar @{$rh->{nm_sessions}} : 0;
	$rh->{nm_sessions}->[$local_session] = { user => $passport, swb_session => $swb_session,
						 nm_session => $nm_session, can_accept => 1, can_reject => 1,
						 session => $local_session, active => 1 };

	print_line_text($window, 'NetMeeting session ', $local_session . "\n");
	print_event($window, "Use the <%text_highlight>/NMACCEPT<%n> or <%text_highlight>/NMREJECT<%n> command.");
	1;
}

# &MSNre::cb_netmeeting_launch ()
# ----------------------------------------------------------------------
# NETMEETING_LAUNCH

sub cb_netmeeting_launch
{
	my ($swb_session, $nm_session, $ip_address) = @_;

	my $command = config_lookup('gnomemeeting_path');
	my $args    = config_lookup('gnomemeeting_flags');
	my $session = netmeeting_find_session($swb_session, $nm_session);

	my $fh_name = 'netmeeting_'.$session->{session};

	$command .= $args if defined $args;
	$command .= " -c callto://$ip_address";

	# TODO: Launch the apps, save the pid and when the app gets closed remove the invitation
	# (+ add a way to do this in the protocol).

	my $rv = system($command);

	if ($rv != 0)
	{
		print_event($rh->{sessions}->[$swb_session]->{window},
			    "NetMeeting session <%text_highlight>[$session->{session}]<%n> was canceled");
		print_event($rh->{sessions}->[$swb_session]->{window},
			    "Cancel reason: <%text_highlight>Couldn't launch gnomemeeting: $!");

		$msn->netmeeting_cancel($swb_session, $nm_session);
		return undef;
	}
}

# &MSNre::cb_netmeeting_reject ()
# ----------------------------------------------------------------------
# NETMEETING_REJECT

sub cb_netmeeting_reject
{
	my ($swb_session, $nm_session, $user) = @_;
	my $nm_hash = netmeeting_find_session($swb_session, $nm_session);

	contact_add(passport => $user, last_seen => time);
	$nm_hash->{user} = $user;

	print_event($rh->{sessions}->[$swb_session]->{window},
		    "Your netmeeting invitation <%text_highlight>[$nm_hash->{session}]<%n>was rejected");

	$nm_hash->{active} = 0;
	1;
}

# &MSNre::cb_open_chat ()
# ----------------------------------------------------------------------
# OPEN_CHAT

sub cb_open_chat
{
	my $session = shift;
	$rh->{sessions}->[$session]->{established} = 1;

	# Send queued messages
	# These are only sent for the first user in the session. If there were more users in the session
	# and more of them had queued message they 'usualy' belong to another session.

	my $passport = (($msn->get_swb_users($session))[0])->passport;
	my $c_user = contacts_get_contact($passport);

	if (!defined $c_user)
	{
		contacts_add(passport => $passport);
		$c_user = contacts_get_contact($passport);
	}

	if (!$c_user->{clientcaps_sent})   # Send the text/x-clientcaps message
	{
		my %args = (client_name => "$NAME/$VERSION");
		$args{chat_logging} = (config_lookup('log_incoming') || config_lookup('log_outgoing'))
		    ? 'Y' : 'N';

		$msn->send_clientcaps($session, %args);
		$c_user->{clientcaps_sent} = 1;
	}

	if (!$rh->{s_queue})
	{
		# Look for old window for this user
		my $window = find_chat_with_user($passport);

		$rh->{sessions}->[$session]->{window} =
		    (defined $window) ? $window : find_free_window_for_chat();

		msnre_debug("Window defined for S[$session] (NO QUEUE): $rh->{sessions}->[$session]->{window}");
		window_title_update($rh->{sessions}->[$session]->{window});
		return 1;
	}

	for (my $i = 0; $i < @{$rh->{s_queue}}; $i++)
	{
		if (defined $rh->{s_queue}->[$i]->{user} && $rh->{s_queue}->[$i]->{user} eq $passport)
		{
			$rh->{sessions}->[$session]->{window} = $rh->{s_queue}->[$i]->{window};

			if (defined $rh->{s_queue}->[$i]->{message})   # Send a queued message
			{
				send_message($session, $rh->{s_queue}->[$i]->{message});
			}

			if (defined $rh->{s_queue}->[$i]->{file})      # Send a queued file
			{
				send_file($session, $rh->{s_queue}->[$i]->{file});
			}

			if ($rh->{s_queue}->[$i]->{netmeeting})        # Invite to use netmeeting
			{
				netmeeting_invite($session);
			}
		}
		splice @{$rh->{s_queue}}, $i--, 1;
	}

	if (!defined $rh->{sessions}->[$session]->{window})
	{
		my $w = find_chat_with_user($rh->{sessions}->[$session]->{first_user});
		$rh->{sessions}->[$session]->{window} = (defined $w) ? $w : find_free_window_for_chat();
	}

	msnre_debug("Window defined for S[$session]: $rh->{sessions}->[$session]->{window}");
	window_title_update($rh->{sessions}->[$session]->{window});
	1;
}

# &MSNre::cb_pong ()
# ----------------------------------------------------------------------
# PONG

sub cb_pong
{
	$rh->{timers}->{ping}->{lag_last} = $rh->{timers}->{ping}->{lag_current};
	$rh->{timers}->{ping}->{lag_current} = undef;
	$rh->{timers}->{ping}->{sent} = undef;

	window_status_update() if $rh->{w}->{main_v}->[$rh->{w}->{current}]->{is_double};
	1;
}

# &MSNre::cb_receive_message ()
# ----------------------------------------------------------------------
# RECEIVE_MESSAGE

sub cb_receive_message
{
	my ($session, $sender, $message, %attributes) = @_;
	save_local_params($session, $sender);

	if ($rh->{sessions}->[$session]->{ignore})   # The user is ignored, don't print anything
	{
		msnre_debug("Instant message from $sender ignored (Session $session)");
		return 1;
	}

	# First message received for the window
	window_main_create_chat($session) if need_window_chat($session);

	if (defined $rh->{sessions}->[$session]->{typing})
	{
		$rh->{sessions}->[$session]->{typing} = undef;
		$rh->{timers}->{typing}->[$session]->{typing_start} = undef;
		window_title_update($rh->{sessions}->[$session]->{window});
	}

	# Print the message
	print_event_message($session, $sender, $message, 1);

	my $e_message = utf8_l($message);   # Fix the message for the event script
	$e_message =~ s/(?<!\\)\"/\\\"/g;
	event_script_call('message', $sender, '"' . $e_message . '"');

	# Send an auto-away message
	if ($msn->status ne 'online' && !defined $rh->{auto_away_sent}->{$msn->status}->{u}->{$sender} &&
	    !defined $rh->{auto_away_sent}->{$msn->status}->{s}->{$session})
	{
		my $auto_away_msg;
		$auto_away_msg = 'auto_idle_msg'  if $msn->status eq 'idle';
		$auto_away_msg = 'auto_away_msg'  if $msn->status eq 'away';
		$auto_away_msg = 'auto_brb_msg'   if $msn->status eq 'be_right_back';
		$auto_away_msg = 'auto_busy_msg'  if $msn->status eq 'busy';
		$auto_away_msg = 'auto_lunch_msg' if $msn->status eq 'out_to_lunch';
		$auto_away_msg = 'auto_phone_msg' if $msn->status eq 'on_the_phone';
		$auto_away_msg = config_lookup($auto_away_msg);

		if (defined $auto_away_msg)
		{
			for ($msn->get_swb_users($session))
			{
				$rh->{auto_away_sent}->{$msn->status}->{u}->{$_->passport} = 1;
			}

			$rh->{auto_away_sent}->{$msn->status}->{s}->{$session} = 1;
			send_message($session, "[Auto message]: $auto_away_msg");
		}
	}

	if ($rh->{forward}->{window})   # Forwarding the message to a window(s)
	{
		for (@{$rh->{forward}->{window}})
		{
			cmd_message($_, "[$sender] $message") if defined find_session_by_window_established($_) ||
			    defined find_session_by_window($_);
		}
	}

	if ($rh->{forward}->{user})     # Forwarding the message to an user(s)
	{
		for (@{$rh->{forward}->{user}})
		{
			cmd_message($_, "[$sender] $message");
		}
	}
	1;
}

# &MSNre::cb_remove_by_user ()
# ----------------------------------------------------------------------
# REMOVE_BY_USER

sub cb_remove_by_user
{
	my $passport = shift;
	my $real_user = $msn->get_user($passport);

	# TODO: Use contacts_get_fname() is all the callbacks like this.
	if (defined $real_user && defined $real_user->fname)
	{
		my $fname = utf8_l($msn->url_decode($real_user->fname));

		print_event(1,
			"<%text_fname>$fname<%n> <%text_passport><$passport><%n> removed you from the contact list");
		return 1;
	}

	print_event(1, "User <%text_passport><$passport><%n> removed you from the contact list");
	1;
}

# &MSNre::cb_remove_group ()
# ----------------------------------------------------------------------
# REMOVE_GROUP

sub cb_remove_group
{
	my $group_name = utf8_l($msn->url_decode(shift));

	print_event($rh->{w}->{current}, "Group <%text_highlight>$group_name<%n> was removed");
	1;
}

# &MSNre::cb_remove_user ()
# ----------------------------------------------------------------------
# REMOVE_USER

sub cb_remove_user
{
	my ($passport, $group_name) = @_;
	$group_name = utf8_l($msn->url_decode($group_name)) if defined $group_name;
	my $real_user = $msn->get_user($passport);

	# Do not change anything in the local contact list. If this user is not present in any contact
	# lists anymore, keep him as a local one.

	if (defined $group_name)   # Group specified
	{
		if (defined $real_user && defined $real_user->fname)
		{
			print_event($rh->{w}->{current},
				    "<%text_fname>" . utf8_l($msn->url_decode($real_user->fname)) . "<%n> " .
				    "<%text_passport><$passport><%n> was removed from group ".
				    "<%text_highlight>$group_name<%n>");
			return 1;
		}
		print_event($rh->{w}->{current},
			    "User <%text_passport><$passport><%n> was removed from group ".
			    "<%text_highlight>$group_name<%n>");
		return 1;
	}

	elsif (defined $real_user && defined $real_user->fname)   # Friendly name known
	{
		my $fname = utf8_l($msn->url_decode($real_user->fname));
		my $fname_len = real_length($fname);
		my $real_len = length($passport) + $fname_len + real_length(get_timestamp()) + 16;

		if ($real_len > $Columns - 1 && config_lookup('truncate_name_not'))
		{
			truncate_name(\$fname, $fname_len - ($real_len - $Columns) - 1);
		}
		print_event($rh->{w}->{current},
			    "<%text_fname>$fname<%n> <%text_passport><$passport><%n> was removed");
		return 1;
	}

	# Both group and friendly name unknown
	print_event($rh->{w}->{current}, "User <%text_passport><$passport><%n> was removed");
	1;
}

# &MSNre::cb_rename_group ()
# ----------------------------------------------------------------------
# RENAME_GROUP

sub cb_rename_group
{
	my ($group_id, $group_name_old, $group_name_new) =
	    (shift, utf8_l($msn->url_decode(shift)), utf8_l($msn->url_decode(shift)));

	return 1 if $group_name_old eq '~' && $group_name_new eq 'Other contacts';   # Auto renamed

	print_event($rh->{w}->{current},
		"Group <%text_highlight>$group_name_old<%n> was renamed to <%text_highlight>$group_name_new<%n>");
	1;
}

# &MSNre::cb_send_message_not_received ()
# ----------------------------------------------------------------------
# SEND_MESSAGE_NOT_RECEIVED

sub cb_send_message_not_received
{
	my ($session, $message) = @_;

	for (my $i = 0; $i < @{$rh->{msg_pending}}; $i++)
	{
		if ($rh->{msg_pending}->[$i]->{session} eq $session && $rh->{msg_pending}->[$i]->{msg} eq $message)
		{
			splice @{$rh->{msg_pending}}, $i, 1;
			last;
		}
	}

	# First message sent to the window ( yup this is sad:( )
	window_main_create_chat($session) if need_window_chat($session);
	print_event_message($session, $msn->passport, $message, 0);

	if ($msn->status ne 'online')
	{
		$rh->{auto_away_sent}->{$msn->status}->{u}->{$_} = 1 for $msn->get_swb_users($session);
		$rh->{auto_away_sent}->{$msn->status}->{s}->{$session} = 1;
	}
	1;
}

# &MSNre::cb_send_message_received ()
# ----------------------------------------------------------------------
# SEND_MESSAGE_RECEIVED

sub cb_send_message_received
{
	my ($session, $message) = @_;

	for (my $i = 0; $i < @{$rh->{msg_pending}}; $i++)
	{
		if ($rh->{msg_pending}->[$i]->{session} eq $session && $rh->{msg_pending}->[$i]->{msg} eq $message)
		{
			splice @{$rh->{msg_pending}}, $i, 1;
			last;
		}
	}

	# First message sent to the window
	window_main_create_chat($session) if need_window_chat($session);
	print_event_message($session, $msn->passport, $message, 1);

	# If someone likes talking and not being 'online', deny sending the auto messages
	if ($msn->status ne 'online')
	{
		$rh->{auto_away_sent}->{$msn->status}->{u}->{$_} = 1 for $msn->get_swb_users($session);
		$rh->{auto_away_sent}->{$msn->status}->{s}->{$session} = 1;
	}
	1;
}

# &MSNre::cb_server_error ()
# ----------------------------------------------------------------------
# SEND_MESSAGE_SENT

sub cb_send_message_sent
{
	my ($session, $message) = @_;

	push @{$rh->{msg_pending}}, { msg => $message, session => $session, time => time };
	1;
}

# &MSNre::cb_server_error ()
# ----------------------------------------------------------------------
# SERVER_ERROR

sub cb_server_error
{
	my $error = shift; chomp $error;
	print_event(1, "\n<%text_error>[Error]<%n> $error\n");
	1;
}

# &MSNre::cb_server_message ()
# ----------------------------------------------------------------------
# SERVER_MESSAGE

sub cb_server_message
{
	my $message = shift;
	print_event(1, "\n<%text_srv_message>[Server Message]<%n> $message\n");
	1;
}

# &MSNre::cb_signed_in ()
# ----------------------------------------------------------------------
# SIGNED_IN

sub cb_signed_in
{
	if (!-f $rh->{config}->{contacts_path} || !defined $rh->{contacts}->{$msn->passport})
	{
		# TODO: Remove this when it gets stable
		contacts_add(passport => 'a37405941@hotmail.com', fname => "MSNre Author");
	}

	elsif (-f $rh->{config}->{contacts_path})   # Read the local contact list
	{
		contacts_read();
	}

	$rh->{misc}->{connecting} = undef;
	$rh->{misc}->{connecting_lattemp} = undef;
	$rh->{timers}->{connection} = time;

	print_event(1, "<%text_login>[Login]<%n> *** Synchronizing the contact lists");
	window_status_update();
	1;
}

# &MSNre::cb_swb_timeout ()
# ----------------------------------------------------------------------
# SWB_TIMEOUT

sub cb_swb_timeout
{
	my $session = shift;
	$rh->{sessions}->[$session]->{established} = 0;

	if (defined $rh->{sessions}->[$session]->{window} &&
	    defined $rh->{w}->{main}->[ $rh->{sessions}->[$session]->{window} ])
	{
		print_event($rh->{sessions}->[$session]->{window},
			    "The session was disconnected due to server timeout");
		window_title_update($rh->{sessions}->[$session]->{window});
	}

	for ($msn->get_swb_users($session))
	{
		contacts_add(passport => $_->passport, fname => $_->fname, last_seen => time) if !defined
		    $msn->get_user($_->passport);
	}
	1;
}

# &MSNre::cb_sync_done ()
# ----------------------------------------------------------------------
# SYNC_DONE

sub cb_sync_done
{
	if (config_lookup('check_version') && !$rh->{misc}->{no_version_lookup})
	{
		find_update();
	}

	print_contacts(iln => 1, window => 1);     # Print the initial contact list

	$rh->{misc}->{login} = 0;
	$rh->{timers}->{idle}->{start} = time if config_lookup('auto_idle_time');
	window_status_update();

	contacts_read() if -f $rh->{config}->{contacts_path};

	for my $c($msn->get_all_users)
	{
		contacts_add(passport => $c->passport, fname => $c->fname);
	}

	print_event(1, "You successfully logged in as <%text_highlight>" . $msn->passport . "<%n>");

	my $group0 = $msn->get_group(0);

	# Rename the first group
	$msn->rename_group(0, 'Other contacts') if defined $group0 && $msn->url_decode($group0->name) eq '~';
	1;
}

# &MSNre::cb_typing_user ()
# ----------------------------------------------------------------------
# TYPING_USER

sub cb_typing_user
{
	my ($session, $passport) = @_;

	$rh->{sessions}->[$session]->{typing} = $passport;
	$rh->{timers}->{typing}->[$session]->{typing_start} = time;

	window_title_update($rh->{sessions}->[$session]->{window});
	1;
}

# &MSNre::cb_unallow_user ()
# ----------------------------------------------------------------------
# UNALLOW_USER

sub cb_unallow_user
{
	my $passport = shift;
	my $fname = contacts_get_fname($passport);

	if (defined $fname)
	{
		my $fname_len = real_length($fname);
		my $real_len = length($passport) + $fname_len + real_length(get_timestamp()) + 18;

		if ($real_len > $Columns - 1 && config_lookup('truncate_name_not'))
		{
			truncate_name(\$fname, $fname_len - ($real_len - $Columns) - 1);
		}
		print_event($rh->{w}->{current},
			    "<%text_fname>$fname<%n> <%text_passport><$passport><%n> was unallowed");
		return 1;
	}

	print_event($rh->{w}->{current}, "User <%text_passport><$passport><%n> was unallowed");
	1;
}

# &MSNre::cb_unblock_user ()
# ----------------------------------------------------------------------
# UNBLOCK_USER

sub cb_unblock_user
{
	my $passport = shift;
	my $fname = contacts_get_fname($passport);

	if (defined $fname)
	{
		my $fname_len = real_length($fname);
		my $real_len = length($passport) + $fname_len + real_length(get_timestamp()) + 18;

		if ($real_len > $Columns - 1 && config_lookup('truncate_name_not'))
		{
			truncate_name(\$fname, $fname_len - ($real_len - $Columns) - 1);
		}
		print_event($rh->{w}->{current},
			    "<%text_fname>$fname<%n> <%text_passport><$passport><%n> was unblocked");
		return 1;
	}

	print_event($rh->{w}->{current}, "User <%text_passport><$passport><%n> was unblocked");
	1;
}

# &MSNre::cb_unimplemented ()
# ----------------------------------------------------------------------
# UNIMPLEMENTED

sub cb_unimplemented
{
	my $message = shift;
	print_event(0, "<%text_highlight>Unimplemented Event:\n$message<%n>") if config_lookup('debug_msnre');
	1;
}

# &MSNre::cb_unknown_reject ()
# ----------------------------------------------------------------------
# UNKNOWN_REJECT

sub cb_unknown_reject
{
	my ($swb_session, $sender, $sender_name, $app) = @_;
	my $window = $rh->{sessions}->[$swb_session]->{window};

	my %args = (passport => $sender, fname => $sender_name);

	if (!defined $msn->get_user($sender))
	{
		$args{last_seen} = time;
	}
	contacts_add(%args);

	if ($rh->{sessions}->[$swb_session]->{ignore})
	{
		msnre_debug("Unknown invitation ignored (Session $swb_session)");
		return 1;
	}

	window_main_create_chat($swb_session) if need_window_chat($swb_session);

	print_event($window, "You were invited to use $app by $sender.");
	print_event($window, "The invitation was rejected because it is not supported.\n");
	1;
}

# &MSNre::cb_url ()
# ----------------------------------------------------------------------
# URL

sub cb_url
{
	my $page_path = shift;
	browser_start($page_path);
	1;
}

# &MSNre::ra_accept ()
# ----------------------------------------------------------------------
# RA_ACCEPT

sub cb_ra_accept
{
	my ($swb_session, $ra_session, $sender) = @_;
}

# &MSNre::cb_ra_invited ()
# ----------------------------------------------------------------------
# RA_INVITED

sub cb_ra_invited
{
	my ($swb_session, $ra_session, $sender, $sender_name) = @_;

	$msn->ra_accept($swb_session, $ra_session);
}

# &MSNre::cb_ra_reject ()
# ----------------------------------------------------------------------
# RA_REJECT

sub cb_ra_reject
{
	my ($swb_session, $ra_session, $sender) = @_;
}

# &MSNre::voice_accept ()
# ----------------------------------------------------------------------
# VOICE_ACCEPT

sub cb_voice_accept
{
	my ($swb_session, $voice_session, $sender) = @_;
}

# &MSNre::cb_voice_invited ()
# ----------------------------------------------------------------------
# VOICE_INVITED

sub cb_voice_invited
{
	my ($swb_session, $voice_session, $sender, $sender_name) = @_;

	$msn->voice_accept($swb_session, $voice_session);
}

# &MSNre::cb_voice_reject ()
# ----------------------------------------------------------------------
# VOICE_REJECT

sub cb_voice_reject
{
	my ($swb_session, $voice_session, $sender) = @_;
}


# -------------------- Command line options -------------------- #


# &MSNre::getoptions ()
# ----------------------------------------------------------------------
# Read the command line options.

sub getoptions
{
	Getopt::Long::Configure("bundling", "permute", "prefix_pattern=(--|-)");

	Getopt::Long::GetOptions(
				 'config|c=s'     => \$rh->{config}->{config_file},
				 'help|h'         => \&print_help_cmd,
				 'profile|p=s'    => \$rh->{profile_use},
				 'version|v'      => \&print_version_cmd,
	);
}

# -------------------- Configuration -------------------- #

# &MSNre::get_config_defaults ()
# ----------------------------------------------------------------------
# Return default configuration options.

sub get_config_defaults
{
	my $c_hash = {
		profile_use        => { option => undef,
					help   => "Default profile name to use",
				      },
		history            => { option => "$rh->{config}->{env_dir}/history",
					help   => undef,
				      },
		received           => { option => "$rh->{config}->{env_dir}/received",
					help   => "Path for received files",
				      },
		received_partial   => { option => "$rh->{config}->{env_dir}/received/partial",
					help   => "Path for not fully received files",
				      },
		external_browser   => { option => undef,
					help   => "Path to an external browser (hotmail pages)",
				      },
		gnomemeeting_flags => { option => undef,
					help   => "Your options to pass to gnomemeeting",
				      },

		debug_msnre        => { option => "0",
					help   => "Set to 1 to enable internal $NAME debugging",
				      },
		debug_protocol     => { option => "0",
					help   => "Set to 1 to enable Net::MsnMessenger module debugging",
				      },
		debug_connection   => { option => "0",
					help   => "Set to 1 to enable connections debugging",
				      },
		encoding           => { option => "auto",
					help   => "Set your system locale or auto to auto-detect",
				      },
		use_colors         => { option => "1",
					help   => "Set to 1 to use colors",
				      },

		server             => { option => "messenger.hotmail.com",
					help   => "Login (dispatch) server address",
				      },
		port               => { option => "1863",
					help   => undef,
 				      },
		user               => { option => undef,
					help   => "Your user passport (E-mail address)",
				      },
		password           => { option => undef,
					help   => "Your user password",
				      },
		friendly_name      => { option => undef,
					help   => "Set your friendly name or leave blank to use the one on the server",
				      },
		initial_status     => { option => "online",
					help   => undef,
				      },
		auto_connect       => { option => "0",
					help   => "Set to 1 to connect immediately after running $NAME",
				      },
		auto_reconnect     => { option => "1",
					help   => "Set to 1 to automatically reconnect after disconnected",
				      },
		auto_allow         => { option => "0",
					help   => "Automatically allow people that add you to their lists",
				      },
		block_privacy      => { option => "0",
					help   => "Set to 1 to receive messages only from allowed users",
				      },
		ping_timeout       => { option => "60",
					help   => "Delay (in seconds) to wait for pong before disconnecting",
				      },
		chat_timeout       => { option => "20",
					help   => "Delay (in seconds) to wait for a chat to be established",
				      },

		pager_number       => { option => "mobile",
					help   => "Type of phone number sent with pager messages (home, mobile, work)",
				      },

		socks_use          => { option => "0",
					help   => "Set to 1 and fill the host and port to use socks server",
				      },
		socks_host         => { option => undef,
					help   => undef,
				      },
		socks_port         => { option => undef,
					help   => undef,
				      },
		socks_auth         => { option => "0",
					help   => "Set to 1 and fill user, password if you need socks authentication",
				      },
		socks_user         => { option => undef,
					help   => undef,
				      },
		socks_password     => { option => undef,
					help   => undef,
				      },
		socks_version      => { option => "5",
					help   => "Socks version (4 or 5)",
				      },

		auto_idle_time     => { option => "600",
					help   => "Automatically go idle after the timeout (in seconds). 0 to disable",
				      },
		auto_away_use      => { option => "0",
					help   => "Set to 1 to use (send) auto-away messages",
				      },
		auto_idle_msg      => { option => undef,
					help   => "Auto message for the Idle status. Leave blank to not use it",
				      },
		auto_away_msg      => { option => undef,
					help   => "Auto message for the Away status.",
				      },
		auto_brb_msg       => { option => undef,
					help   => "Auto message for the Be Right Back status.",
				      },
		auto_busy_msg      => { option => undef,
					help   => "Auto message for the Busy status.",
				      },
		auto_lunch_msg     => { option => undef,
					help   => "Auto message for the Out To Lunch status.",
				      },
		auto_phone_msg     => { option => undef,
					help   => "Auto message for the On The Phone status.",
				      },
		log_incoming       => { option => "1",
					help   => "Set to 1 to log all the incoming messages",
				      },
		log_outgoing       => { option => "1",
					help   => "Set to 1 to log all the outgoing messages",
				      },
		truncate_name_chat => { option => "1",
					help   => "Set to 1 to truncate friendly names in chats to 10 characters",
				      },
		truncate_name_not  => { option => "1",
					help   => "Set to 1 to truncate friendly names in notifications",
				      },
		auto_change_window => { option => "1",
					help   => "Set to 1 to automatically switch to a new chat window",
				      },
		check_version      => { option => "1",
					help   => "Check for new versions of MSNre after logged in",
				      },
		timestamp_format   => { option => "[%H:%M:%S]",
					help   => "Format of the timestamp (see: man 3 strftime)",
				      },
		
		event_script_use   => { option => "0",
					help   => "Set to 1 and enter a path to an event script below to use it",
				      },
		event_script_path  => { option => undef,
					help   => "Path to the event script",
				      },

		ft_auto_accept     => { option => "1",
					help   => "Automatically accept file transfers",
				      },
		ft_timeout         => { option => "60",
					help   => "Timeout (in seconds) for the file transfers",
				      },

		http_grab          => { option => "1",
					help   => "Save HTTP URLs into an internal buffer",
				      },
		ftp_grab           => { option => "1",
					help   => "Save FTP URLs into an internal buffer",
				      },
		highlight_msg      => { option => "1",
					help   => "Highlight some parts (URLs, smileys, ...) of a message",
				      },

		color_theme        => { option => "1",
					help   => undef,
				      },
	};
	return $c_hash;
}

# &MSNre::set_config_defaults ()
# ----------------------------------------------------------------------
# Set default configuration options.

sub set_config_defaults
{
	my $c_hash = get_config_defaults();

	for (grep {defined $c_hash->{$_}->{option}} keys %{$c_hash})
	{
		$rh->{profile}->{DEFAULT}->{opt}->{$_} = $c_hash->{$_}->{option};
	}

	# Default colors
	if (!$rh->{profile}->{DEFAULT}->{color})
	{
		for (keys %{$rh->{clr}->{color_theme}->{1}})
		{
			my ($color, $attributes) = $rh->{clr}->{color_theme}->{1}->{$_} =~ /^(\S+)\s*(.*)$/;

			$rh->{profile}->{DEFAULT}->{color}->{$_}->{color} = $1;
			@{$rh->{profile}->{DEFAULT}->{color}->{$_}->{attributes}} = split /\s+/, $2 if defined $2;
		}
	}

	# Default key bindings
	if (!$rh->{profile}->{DEFAULT}->{key_bind}->{bind})
	{
		$rh->{profile}->{DEFAULT}->{key_bind}->{bind} = {
			'<ctrl> a'       => ['beginning-of-line'       ],
			'<home>'         => ['scroll-up-buffer'        ],
			'<ctrl> e'       => ['end-of-line'             ],
			'<end>'          => ['scroll-down-buffer'      ],
			'<ctrl> f'       => ['forward-char'            ],
			'<right>'        => ['forward-char'            ],
			'<ctrl> b'       => ['backward-char'           ],
			'<left>'         => ['backward-char'           ],
			'<meta> f'       => ['forward-word'            ],
			'<meta> b'       => ['backward-word'           ],
			'<ctrl> l'       => ['refresh-screen'          ],
			'<ctrl> p'       => ['previous-history'        ],
			'<up>'           => ['previous-history'        ],
			'<ctrl> n'       => ['next-history'            ],
			'<down>'         => ['next-history'            ],
			'<meta> <'       => ['beginning-of-history'    ],
			'<meta> >'       => ['end-of-history'          ],
			'<delete>'       => ['delete-char'             ],
			'<ctrl> d'       => ['delete-char'             ],
			'<ctrl> h'       => ['backward-delete-char'    ],
			'<bspace>'       => ['backward-delete-char'    ],
			'<ctrl> t'       => ['transpose-chars'         ],
			'<meta> t'       => ['transpose-words'         ],
			'<meta> u'       => ['upcase-word'             ],
			'<meta> l'       => ['downcase-word'           ],
			'<meta> c'       => ['capitalize-word'         ],
			'<insert>'       => ['overwrite-mode'          ],
			'<ctrl> k'       => ['kill-line'               ],
			'<ctrl> u'       => ['unix-line-discard'       ],
			'<meta> d'       => ['kill-word'               ],
			'<ctrl> w'       => ['unix-word-rubout'        ],
			'<ctrl> g'       => ['kill-buffer'             ],
			'<ctrl> c'       => ['kill-buffer'             ],
			'<meta> \\'      => ['delete-horizontal-space' ],
			'<ctrl> y'       => ['yank'                    ],
			'<tab>'          => ['complete'                ],
			'<ctrl> j'       => ['accept-line'             ],
			'<ctrl> m'       => ['accept-line'             ],
			'<meta> e'       => ['shell-execute'           ],
			'<meta> p'       => ['previous-window'         ],
			'<meta> n'       => ['next-window'             ],
			'<meta> 1'       => ['switch-to-window-1'      ],
			'<meta> 2'       => ['switch-to-window-2'      ],
			'<meta> 3'       => ['switch-to-window-3'      ],
			'<meta> 4'       => ['switch-to-window-4'      ],
			'<meta> 5'       => ['switch-to-window-5'      ],
			'<meta> 6'       => ['switch-to-window-6'      ],
			'<meta> 7'       => ['switch-to-window-7'      ],
			'<meta> 8'       => ['switch-to-window-8'      ],
			'<meta> 9'       => ['switch-to-window-9'      ],
			'<meta> 0'       => ['switch-to-window-10'     ],
			'<ppage>'        => ['scroll-up-page'          ],
			'<npage>'        => ['scroll-down-page'        ],
			'<enter>'        => ['accept-line'             ],
		};
	}
	1;
}

# &MSNre::read_config ()
# ----------------------------------------------------------------------
# Read the configuration from a file.

sub read_config
{
	my $conf_fh = new FileHandle "< $rh->{config}->{config_file}" if -f $rh->{config}->{config_file};
	my @cur_profile = ();

	if (defined $conf_fh)   # Read the configuration
	{
		read_config_handle($conf_fh);
	}
	else                    # Set the defaults
	{
		set_config_defaults();
	}

	if (defined config_lookup('profile_use') && !defined $rh->{profile_use})
	{
		$rh->{profile_use} = config_lookup('profile_use');
	}

	if (!defined $rh->{profile_use} ||
	    (defined $rh->{profile_use} && !exists $rh->{profile}->{$rh->{profile_use}}))
	{
		my $to_use = (defined $rh->{config}->{profile_first}) ? $rh->{config}->{profile_first} : 'DEFAULT';
		$rh->{profile_use} = $to_use;
	}

	color_register();        # Set up the colors
	window_main_create(1);   # Create the first window
	print_ascii_art();       # Print the ascii art logo

	register_config();

	if (!defined $rh->{main}->[0])
	{
		window_main_create(0) if config_lookup('debug_msnre') || config_lookup('debug_protocol') ||
		    config_lookup('debug_connection');
	}


	if (!-f $rh->{config}->{config_file})
	{
		write_config($rh->{config}->{config_file});
		print_event(1, "Configuration file doesn't exists. New file created using the default options.");
	}
	else
	{
		print_event(1, "Configuration successfully loaded.");
	}

	print_event(1, "Used encoding:  <%text_highlight>$rh->{config}->{encoding}");
	print_event(1, "Used profile:   <%text_highlight>$rh->{profile_use}<%n>\n");
	1;
}

# &MSNre::read_config_handle ()
# ----------------------------------------------------------------------
# Read the configuration from a file handle.

sub read_config_handle
{
	my $fh = shift;

	my @cur_profile = ();
	my @read_opts = ();

	my %read_colors = ();

	while (<$fh>)
	{
		$_ = (/^(.*?)\s*\#/) ? $1 : $_;     # Comments
		$_ =~ s/^\s*//;                     # Whitespaces at the beginning
		$_ =~ s/\s*$//;                     # Whitespaces at the end

		if (/^\[(.*)\]/)                    # Profile
		{
			my $all = $1;
			my @parts = split /\s+/, $all;

			if (lc($parts[0]) eq 'profile')   # Beginning of a profile part
			{
				$all =~ s/\S+\s+//;
				my ($name) = $all =~ /name=\"(.+)\"/;

				if (defined $name)
				{
					$rh->{config}->{profile_first} = $name if !defined
					    $rh->{config}->{profile_first};

					push @cur_profile, $name unless grep {$_ eq $name} @cur_profile;
				}
			}
			elsif (lc($parts[0]) eq 'end' && lc($parts[1]) eq 'profile')   # End of a profile part
			{
				$all =~ s/\S+\s+\S+\s+//;
				my ($name) = $all =~ /name=\"(.+)\"/;

				if (defined $name && @cur_profile)
				{
					for (my $i = 0; $i < @cur_profile; $i++)
					{
						splice @cur_profile, $i--, 1 if $cur_profile[$i] eq $name;
					}
				}
			}
			next;
		}

		# Aliases

		if (/^\s*ALIAS\s+(\S+?)\s+(\S+?)\s+(\S.*?)$/i && defined $2 && defined $3)
		{
			if (uc($1) eq 'CONTACT')   # Contact alias
			{
				if (@cur_profile && scalar @cur_profile)   # Current profile(s)
				{
					for (@cur_profile)
					{
						push @{$rh->{profile}->{$_}->{aliases}->{CONTACT}->{lc $2}}, $3;
					}
				}
				else    # DEFAULT profile
				{
					push @{$rh->{profile}->{DEFAULT}->{aliases}->{CONTACT}->{lc $2}}, $3;
				}
			}
			elsif (uc($1) eq 'COMMAND')   # Command alias
			{
				if (@cur_profile && scalar @cur_profile)   # Current profile(s)
				{
					for (@cur_profile)
					{
						add_alias_cmd($_, $2, $3);
					}
				}
				else    # DEFAULT profile
				{
					add_alias_cmd('DEFAULT', $2, $3);
				}
			}
			next;
		}

		# Key bindings

		if (/^\s*BIND\s+(\S.*?)\s*(\S+?)$/i)
		{
			my ($key, $action) = (lc $1, lc $2);

			if (@cur_profile && scalar @cur_profile)   # Current profile(s)
			{
				for (@cur_profile)
				{
					push @{$rh->{profile}->{$_}->{key_bind}->{bind}->{$key}}, $action;
				}
			}
			else    # DEFAULT profile
			{
				push @{$rh->{profile}->{DEFAULT}->{key_bind}->{bind}->{$key}},
					$action;
			}
			next;
		}

		# Custom colors

		if (/^\s*COLOR\s+(\S+?)\s+(\S.+?)$/i)
		{
			my ($part, $color) = ($1, $2);
			my @attributes = split /\s+/, $2 if $color =~ s/^(\S+)\s+(.+)$/$1/;

			if (@cur_profile && scalar @cur_profile)   # Current profile(s)
			{
				for (@cur_profile)
				{
					color_add($_, $part, $color, @attributes);
				}
			}
			else    # DEFAULT profile
			{
				color_add('DEFAULT', $part, $color, @attributes);
				$read_colors{$part} = $color;
				
			}
		}

		# A classic configuration option

		if (/^\s*(\S+?)\s*=\s*(.*)\s*$/)
		{
			my $option = $1;
			my $value = $2 if defined $2;

			if (@cur_profile && scalar @cur_profile)   # Current profile(s)
			{
				for (@cur_profile)
				{
					$rh->{profile}->{$_}->{opt}->{lc $option} = $value;
				}
			}
			else    # DEFAULT profile
			{
				$rh->{profile}->{DEFAULT}->{opt}->{lc $option} = $value;
			}
			push @read_opts, lc $option;
		}
	}

	$fh->close;

	# Add missing options and/or colors to the configuration

	my $c_hash = get_config_defaults();
	my $modified = 0;

	for my $def_opt(keys %{$c_hash})
	{
		if (!grep {$_ eq $def_opt} @read_opts)
		{
			config_set($def_opt, $c_hash->{$def_opt}->{option});  # Adding an option
			$modified++;
		}
	}

	for my $part(keys %{$rh->{clr}->{color_theme}->{1}})
	{
		if (!grep {$_ eq $part} keys %read_colors)
		{
			$rh->{clr}->{color_theme}->{1}->{$part} =~ /^(\S+)\s*(.*)$/;
	
			my $color = $1;
			my @attributes = (defined $2) ? split /\s+/, $2 : ();

			color_add('DEFAULT', $part, $color, @attributes);     # Adding a color
			$modified++;
		}
	}

	# Write the configuration if there were any options added
	write_config($rh->{config}->{config_file}) if $modified;
	1;
}

# &MSNre::register_config ()
# ----------------------------------------------------------------------
# Register the configuration option in the protocol.

sub register_config
{
	for ('history', 'received', 'received_partial')
	{
		my $directory = config_lookup($_);
		if (defined $directory && !-d $directory)
		{
			if (-e $directory)
			{
				msnre_error("$directory already exists and it's not a directory");
				config_set($_, undef);
			}
			else
			{
				require File::Path;
				File::Path::mkpath($directory) || msnre_error("Couldn't create $directory: $!");
			}
		}
	}

	# ----- Find an usable characters encoding

	$rh->{config}->{encoding} = undef;

	my $real_enc = undef;
	my $valid_enc = 0;

	config_set('encoding', 'auto') if !defined config_lookup('encoding');

	if (lc(config_lookup('encoding')) eq 'auto')
	{
		for ('LC_CTYPE', 'LC_ALL', 'LANG')
		{
			next if !$ENV{$_};
			if ($ENV{$_} !~ /\.(\S[^.]+)$/)
			{
				if ($ENV{$_} =~ /^en_\S{2}/) { $real_enc = "iso-8859-1" }
			}
			else { $real_enc = $1 }

			eval { Encode::encode($real_enc, '') };
			$real_enc = undef if $@;

			last if defined $real_enc;
		}

		if (!defined $real_enc)
		{
			print_event($rh->{w}->{current},
				    "Couldn't read your locale. Using iso-8859-1 for characters encoding.");
			print_event($rh->{w}->{current},
				    "Specify your encoding in the configuration file manually.\n");

			$rh->{config}->{encoding} = 'iso-8859-1';
		}
		else
		{
			$rh->{config}->{encoding} = $real_enc;
		}
	}
	else
	{
		# Specific encoding, let's see if it's available in Encode
		eval { Encode::encode(config_lookup('encoding'), '') };

		if ($@)
		{
			print_event(1, "Invalid encoding specified (" . config_lookup('encoding') .
				    "). Using iso-8859-1.");

			$rh->{config}->{encoding} = 'iso-8859-1';
		}
		else
		{
			$rh->{config}->{encoding} = config_lookup('encoding');
		}
	}

	# Set the options also in the protocol

	if (defined config_lookup('user'))
	{
		msnre_debug("User set to " . config_lookup('user'));
		$msn->passport(config_lookup('user'));
	}

	if (defined config_lookup('password'))
	{
		$msn->password(config_lookup('password'));
	}

	$msn->fname(utf8_e(config_lookup('friendly_name'))) if defined config_lookup('friendly_name');

	$msn->initial_status(config_lookup('initial_status')) if defined config_lookup('initial_status');
	$msn->debug(1) if config_lookup('debug_protocol');
	$msn->debug_connection(1) if config_lookup('debug_connection');

	$msn->auto_allow(config_lookup('auto_allow')) if config_lookup('auto_allow');
	$msn->block_privacy(config_lookup('block_privacy')) if config_lookup('block_privacy');

	if (config_lookup('socks_use'))
	{
		$msn->socks_host(config_lookup('socks_host')) if defined config_lookup('socks_host');
		$msn->socks_port(config_lookup('socks_port')) if defined config_lookup('socks_port');

		$msn->socks_user(config_lookup('socks_user')) if defined config_lookup('socks_user');
		$msn->socks_password(config_lookup('socks_password')) if defined config_lookup('socks_password');

		$msn->socks_version(config_lookup('socks_version')) if defined config_lookup('socks_version');
	}

	# Check the timeouts to not get possible errors
	for ('ping_timeout', 'chat_timeout', 'auto_idle_time', 'ft_timeout')
	{
		my $option = config_lookup($_);
		if (defined $option && !POSIX::isdigit($option))
		{
			msnre_debug("Invalid $_ option!");
			config_set($_, undef);
		}
	}
	1;
}

# &MSNre::write_config ()
# ----------------------------------------------------------------------
# Write the configuration to a file.

sub write_config
{
	my $file_path = shift;

	if (!-f $file_path)   # The file doesn't exist, try to create it
	{
		if (-e $file_path)
		{
			msnre_error("Couldn't create $file_path: already exists and it's not a file");
			return undef;
		}
		require File::Basename;
		my $dir = File::Basename::dirname($file_path);

		if (!-d $dir)
		{
			if (-e $dir)
			{
				msnre_error("Couldn't create $dir: already exists and it's not a directory");
				return undef;
			}
			require File::Path;
			if (!File::Path::mkpath($dir))
			{
				msnre_error("Couldn't create $dir: $!");
				return undef;
			}
		}
	}
	my $conf_fh = new FileHandle "> $file_path";

	if (!defined $conf_fh)
	{
		msnre_error("Couldn't write to $file_path: $!");
		return undef;
	}

	my @o1 = qw(user password friendly_name initial_status);
	my @o2 = qw(server port auto_connect auto_reconnect auto_allow block_privacy ping_timeout chat_timeout
		    pager_number);
	my @o3 = qw(debug_msnre debug_protocol debug_connection encoding use_colors external_browser);
	my @o4 = qw(profile_use history received received_partial);
	my @o5 = qw(auto_idle_time auto_away_use auto_idle_msg auto_away_msg auto_brb_msg auto_busy_msg
		    auto_busy_msg auto_lunch_msg auto_phone_msg log_incoming log_outgoing
		    truncate_name_chat truncate_name_not auto_change_window check_version timestamp_format);
	my @o6 = qw(socks_use socks_host socks_port socks_auth socks_user socks_password socks_version);
	my @o7 = qw(event_script_use event_script_path);
	my @o8 = qw(ft_auto_accept ft_timeout);
	my @o9 = qw(http_grab ftp_grab highlight_msg);


	# ----- Configuration (msnre.conf)

	$conf_fh->print( "## Global configuration file for $NAME version $VERSION\n");
	$conf_fh->print( "## Generated by $NAME at " . localtime(time) . "\n\n");

	$conf_fh->print( "# All the default settings are used for a default profile (named DEFAULT).\n");
	$conf_fh->print( "# You can add your own per-profile settings in this file.\n\n");
	$conf_fh->print( "# Example:\n");
	$conf_fh->print( "# " . '-' x 60 . "\n");
	$conf_fh->print( "# [Profile name=\"my_profile\"]\n");
	$conf_fh->print( "#\n");
	$conf_fh->print( "#  USER = me\@hotmail.com\n");
	$conf_fh->print( "#  PASSWORD = my_password\n");
	$conf_fh->print( "#  INITIAL_STATYS = appear_offline\n");
	$conf_fh->print( "#\n");
	$conf_fh->print( "# [END Profile name=\"my_profile\"]\n");
	$conf_fh->print( "# " . '-' x 60 . "\n");

	for (grep {$_ ne 'DEFAULT'} sort keys %{$rh->{profile}})     # Write all the non-DEFAULT profiles
	{
		$conf_fh->print( "\n[Profile name=\"$_\"]\n\n");

		for my $opt(sort keys %{$rh->{profile}->{$_}->{opt}})
		{
			$conf_fh->print(' ' .
					write_config_get_line(undef, $opt, $rh->{profile}->{$_}->{opt}->{lc $opt}));
		}

		$conf_fh->print("\n # Custom per-profile key bindings\n");
		if (!write_config_bindings($conf_fh, $_, ' '))
		{
			$conf_fh->print(" # <none>\n\n");
		}

		$conf_fh->print(" # Custom per-profile colors\n");
		if (!write_config_colors($conf_fh, $_, ' '))
		{
			$conf_fh->print(" # <none>\n\n");
		}

		$conf_fh->print(" # Custom per-profile aliases\n");
		if (!write_config_aliases($conf_fh, $_, ' '))
		{
			$conf_fh->print(" # <none>\n");
		}
		$conf_fh->print("\n[END Profile name=\"$_\"]\n");
	}

	# DEFAULT options

	my $c_hash = get_config_defaults();
	my $d_profile = $rh->{profile}->{DEFAULT}->{opt};

	for (@o1)
	{
		$conf_fh->print(write_config_get_line($c_hash->{$_}->{help}, $_, $d_profile->{$_}));
	}

	$conf_fh->print("\n## " . '-' x 60);
	$conf_fh->print("\n## Protocol specific options\n");

	for (@o2)
	{
		$conf_fh->print(write_config_get_line($c_hash->{$_}->{help}, $_, $d_profile->{$_}));
	}

	$conf_fh->print("\n## " . '-' x 60);
	$conf_fh->print("\n## $NAME specific options\n");

	for (@o3)
	{
		$conf_fh->print(write_config_get_line($c_hash->{$_}->{help}, $_, $d_profile->{$_}));
	}

	for (@o4)
	{
		$conf_fh->print(write_config_get_line($c_hash->{$_}->{help}, $_, $d_profile->{$_}));
	}

	for (@o5)
	{
		$conf_fh->print(write_config_get_line($c_hash->{$_}->{help}, $_, $d_profile->{$_}));
	}

	$conf_fh->print("\n## " . '-' x 60);
	$conf_fh->print("\n## SOCKS5 Connection options\n");

	for (@o6)
	{
		$conf_fh->print(write_config_get_line($c_hash->{$_}->{help}, $_, $d_profile->{$_}));
	}

	$conf_fh->print("\n## " . '-' x 60);
	$conf_fh->print("\n## Events script options\n");

	for (@o7)
	{
		$conf_fh->print(write_config_get_line($c_hash->{$_}->{help}, $_, $d_profile->{$_}));
	}

	$conf_fh->print("\n## " . '-' x 60);
	$conf_fh->print("\n## File Transfer options\n");

	for (@o8)
	{
		$conf_fh->print(write_config_get_line($c_hash->{$_}->{help}, $_, $d_profile->{$_}));
	}

	$conf_fh->print("\n## " . '-' x 60);
	$conf_fh->print("\n## Additional options\n");

	for (@o9)
	{
		$conf_fh->print(write_config_get_line($c_hash->{$_}->{help}, $_, $d_profile->{$_}));
	}
	$conf_fh->print("\n");

	# ----- Key bindings

	$conf_fh->print("## " . '-' x 60 . "\n");
	$conf_fh->print("## Key bindings\n");
	$conf_fh->print("## You can customize the key bindings as explained in msnre(1)\n");

	write_config_bindings($conf_fh, 'DEFAULT');

	# ----- Colors

	$conf_fh->print("## " . '-' x 60 . "\n");
	$conf_fh->print("## Colors\n\n");

	$conf_fh->print("# Set color theme (1, 2 or 3) or unset and customize the settings below\n");
	$conf_fh->print(write_config_get_line(undef, 'color_theme', $d_profile->{color_theme}));

	write_config_colors($conf_fh, 'DEFAULT');

	# ----- Aliases

	$conf_fh->print("## " . '-' x 60 . "\n");
	$conf_fh->print("## Aliases\n\n");

	if (!write_config_aliases($conf_fh, 'DEFAULT'))
	{
		$conf_fh->print("## <none>\n");
	}

	$conf_fh->close;
	1;
}

# &MSNre::write_config_aliases ()
# ----------------------------------------------------------------------
# Write aliases for a specified profile to the configuration file.

sub write_config_aliases
{
	my ($conf_fh, $profile, $prefix) = @_;
	my $max_a_len = 0;

	return undef if !$rh->{profile}->{$profile}->{aliases} ||
	    !keys %{$rh->{profile}->{$profile}->{aliases}->{CONTACT}};

	$conf_fh->print("\n");

	for (keys %{$rh->{profile}->{$profile}->{aliases}->{CONTACT}})
	{
		$max_a_len = length($_) if length($_) > $max_a_len;
	}

	for (sort keys %{$rh->{profile}->{$profile}->{aliases}->{CONTACT}})
	{
		for my $alias(@{$rh->{profile}->{$profile}->{aliases}->{CONTACT}->{$_}})
		{
			$conf_fh->print($prefix) if defined $prefix;
			$conf_fh->print("ALIAS   CONTACT   $_   " . " " x ($max_a_len-length($_)) . "$alias\n");
		}
	}

	return 1 if !keys %{$rh->{profile}->{$profile}->{aliases}->{COMMAND}};
	$conf_fh->print("\n");
	$max_a_len = 0;

	for (keys %{$rh->{profile}->{$profile}->{aliases}->{COMMAND}})
	{
		$max_a_len = length($_) if length($_) > $max_a_len;
	}

	for (sort keys %{$rh->{profile}->{$profile}->{aliases}->{COMMAND}})
	{
		$conf_fh->print($prefix) if defined $prefix;
		$conf_fh->print("ALIAS   COMMAND   $_   " . " " x ($max_a_len-length($_)));
		$conf_fh->print($rh->{profile}->{$profile}->{aliases}->{COMMAND}->{$_}->{old});

		$conf_fh->print(" " .
			join(' ', @{$rh->{profile}->{$profile}->{aliases}->{COMMAND}->{$_}->{old_params}})) if
			$rh->{profile}->{$profile}->{aliases}->{COMMAND}->{$_}->{old_params};

		$conf_fh->print("\n");
	}
	1;
}

# &MSNre::write_config_bindings ()
# ----------------------------------------------------------------------
# Write key bindings for a specified profile to the configuration file

sub write_config_bindings
{
	my ($conf_fh, $profile, $prefix) = @_;
	my $max_b_len = 0;

	return undef if !keys %{$rh->{profile}->{$profile}->{key_bind}->{bind}};

	$conf_fh->print("\n");

	for (keys %{$rh->{profile}->{$profile}->{key_bind}->{bind}})
	{
		$max_b_len = length($_) if length($_) > $max_b_len;
	}

	for (sort keys %{$rh->{profile}->{$profile}->{key_bind}->{bind}})
	{
		for my $bind(@{$rh->{profile}->{$profile}->{key_bind}->{bind}->{$_}})
		{
			$conf_fh->print($prefix) if defined $prefix;
			$conf_fh->print("BIND   $_   " . " " x ($max_b_len-length($_)) . "$bind\n");
		}
	}

	$conf_fh->print("\n");
	1;
}

# &MSNre::write_config_colors ()
# ----------------------------------------------------------------------
# Write colors for a specified profile to the configuration file.

sub write_config_colors
{
	my ($conf_fh, $profile, $prefix) = @_;
	my $max_c_len = 0;

	return undef if !keys %{$rh->{profile}->{$profile}->{color}};

	$conf_fh->print("\n");

	for (keys %{$rh->{profile}->{$profile}->{color}})
	{
		$max_c_len = length($_) if length($_) > $max_c_len;
	}

	for (sort keys %{$rh->{profile}->{$profile}->{color}})
	{
		my $color = $rh->{profile}->{$profile}->{color}->{$_}->{color};
		my $attributes = join ' ', @{$rh->{profile}->{$profile}->{color}->{$_}->{attributes}} if
		    $rh->{profile}->{$profile}->{color}->{$_}->{attributes};

		$color =~ s/^COLOR_(\S+)$/$1/;
		$attributes =~ s/A_(\S+)/$1/g if defined $attributes;

		$attributes = (defined $attributes) ? ' ' . $attributes : '';

		$conf_fh->print($prefix) if defined $prefix;
		$conf_fh->print("COLOR   $_   " . " " x ($max_c_len-length($_)) . lc($color) . lc($attributes)."\n");
	}

	$conf_fh->print("\n");
	1;
}

# &MSNre::write_config_get_line ()
# ----------------------------------------------------------------------
# Return a line for the option and optionally help (for configuration).

sub write_config_get_line
{
	my ($help, $option, $value) = @_;
	my $line = "";

	$line .= "\n# $help\n" if defined $help;
	$line .= uc($option) . ' =';
	$line .= ' ' . $value if defined $value;
	$line .= "\n";

	$line;
}

# &MSNre::register_bindings ()
# ----------------------------------------------------------------------
# Associate functions to the key binding actions.

sub register_bindings
{
	$rh->{key_bind}->{command} = {

	'beginning-of-line' => sub { $rh->{w}->{input_v}->{position} = 0 },
	'end-of-line' =>
	sub
	{
		$rh->{w}->{input_v}->{position} = real_length($rh->{w}->{input_v}->{buffer}) if
		    defined $rh->{w}->{input_v}->{buffer};
	},
	'forward-char' =>
	sub
	{
		$rh->{w}->{input_v}->{position}++ if $rh->{w}->{input_v}->{buffer} &&
		    $rh->{w}->{input_v}->{position} < real_length($rh->{w}->{input_v}->{buffer});
	},
	'backward-char' =>
	sub
	{
		$rh->{w}->{input_v}->{position}-- if $rh->{w}->{input_v}->{position};
	},
	'forward-word' =>
	sub
	{
		if (defined $rh->{w}->{input_v}->{buffer} &&
		    $rh->{w}->{input_v}->{position} < real_length($rh->{w}->{input_v}->{buffer}))
		{
			my $buffer_next = substr $rh->{w}->{input_v}->{buffer}, $rh->{w}->{input_v}->{position};
			if ($buffer_next =~ /^(\s*\S+?)(?:$|\s)/)
			{
				$rh->{w}->{input_v}->{position} += real_length($1);
			}
		}
	},
	'backward-word' =>
	sub
	{
		if (defined $rh->{w}->{input_v}->{buffer})
		{
			my $buffer_prev = substr $rh->{w}->{input_v}->{buffer}, 0, $rh->{w}->{input_v}->{position};
			if ($buffer_prev =~ /\s(\S+\s*)$/)
			{
				$rh->{w}->{input_v}->{position} -= real_length($1);
			}
			else
			{
				$rh->{w}->{input_v}->{position} = 0;
			}
		}
	},
	'upcase-word' =>
	sub
	{
		if (defined $rh->{w}->{input_v}->{buffer})
		{
			my $buffer_next = substr $rh->{w}->{input_v}->{buffer}, $rh->{w}->{input_v}->{position};
			my ($to_uc) = $buffer_next =~ /(\s*\S+?)(?:$|\s)/;

			if (defined $to_uc)
			{
				substr ($rh->{w}->{input_v}->{buffer},$rh->{w}->{input_v}->{position},
					real_length($to_uc)) = uc $to_uc;

				$rh->{w}->{input_v}->{position} += real_length($to_uc);
			}
		}
	},
	'downcase-word' =>
	sub
	{
		if (defined $rh->{w}->{input_v}->{buffer})
		{
			my $buffer_next = substr $rh->{w}->{input_v}->{buffer}, $rh->{w}->{input_v}->{position};
			my ($to_lc) = $buffer_next =~ /(\s*\S+?)(?:$|\s)/;

			if (defined $to_lc)
			{
				substr ($rh->{w}->{input_v}->{buffer},$rh->{w}->{input_v}->{position},
					real_length($to_lc)) = lc $to_lc;

				$rh->{w}->{input_v}->{position} += real_length($to_lc);
			}
		}
	},
	'capitalize-word' =>
	sub
	{
		if (defined $rh->{w}->{input_v}->{buffer})
		{
			my $buffer_next = substr $rh->{w}->{input_v}->{buffer}, $rh->{w}->{input_v}->{position};
			my ($to_c) = $buffer_next =~ /(\s*\S+?)(?:$|\s)/;

			if (defined $to_c)
			{
				$to_c =~ s/^(\s*)(\S)(\S+)$/$1 . ucfirst($2) . lc($3)/e;
				substr ($rh->{w}->{input_v}->{buffer}, $rh->{w}->{input_v}->{position},
					real_length($to_c)) = $to_c;

				$rh->{w}->{input_v}->{position} += real_length($to_c);
			}
		}
	},
	'kill-buffer' =>
	sub
	{
		if (defined $rh->{w}->{input_v}->{buffer})
		{
			$rh->{w}->{input_v}->{last_killed} = $rh->{w}->{input_v}->{buffer};
			$rh->{w}->{input_v}->{position}    = 0;
			$rh->{w}->{input_v}->{buffer}      = undef;
		}
	},
	'kill-line' =>
	sub
	{
		return if !defined $rh->{w}->{input_v}->{buffer} ||
		    $rh->{w}->{input_v}->{position} < real_length($rh->{w}->{input_v}->{buffer});

		$rh->{w}->{input_v}->{last_killed} =
		    substr $rh->{w}->{input_v}->{buffer}, $rh->{w}->{input_v}->{position},
	            real_length($rh->{w}->{input_v}->{buffer}) - $rh->{w}->{input_v}->{position};

		$rh->{w}->{input_v}->{buffer} =~ s/$rh->{w}->{input_v}->{last_killed}$//;
	},
	'kill-word' =>
	sub
	{
		if (defined $rh->{w}->{input_v}->{buffer})
		{
			my $buffer_t = substr $rh->{w}->{input_v}->{buffer}, $rh->{w}->{input_v}->{position};
			my $length_t = real_length($buffer_t);
			$buffer_t =~ s/^\s*\S+//;

			substr ($rh->{w}->{input_v}->{buffer}, $rh->{w}->{input_v}->{position}, $length_t) = $buffer_t;
		}
	},
	'unix-line-discard' =>
	sub
	{
		return if !defined $rh->{w}->{input_v}->{buffer} || !$rh->{w}->{input_v}->{position};

		$rh->{w}->{input_v}->{last_killed} = substr $rh->{w}->{input_v}->{buffer}, 0,
		    $rh->{w}->{input_v}->{position};

		$rh->{w}->{input_v}->{buffer} =~ s/^$rh->{w}->{input_v}->{last_killed}//;
		$rh->{w}->{input_v}->{position} = 0;
	},
	'unix-word-rubout' =>
	sub
	{
		return if !defined $rh->{w}->{input_v}->{buffer} || !$rh->{w}->{input_v}->{position};

		my $buffer_kill = substr $rh->{w}->{input_v}->{buffer}, 0, $rh->{w}->{input_v}->{position};

		$buffer_kill = '' if $buffer_kill !~ /\S/;
		$buffer_kill =~ s/\S+\s*$//;

		substr ($rh->{w}->{input_v}->{buffer}, 0, $rh->{w}->{input_v}->{position}) = $buffer_kill;
		$rh->{w}->{input_v}->{position} -= $rh->{w}->{input_v}->{position} - real_length($buffer_kill);
	},
	'delete-char' =>
	sub
	{
		substr($rh->{w}->{input_v}->{buffer}, $rh->{w}->{input_v}->{position}, 1) = '' if defined
		    $rh->{w}->{input_v}->{buffer} &&
		    $rh->{w}->{input_v}->{position} <= real_length($rh->{w}->{input_v}->{buffer});
	},
	'backward-delete-char' =>
	sub
	{
		substr($rh->{w}->{input_v}->{buffer}, --$rh->{w}->{input_v}->{position}, 1) = ''
		    if defined $rh->{w}->{input_v}->{buffer} && $rh->{w}->{input_v}->{position};
	},
	'delete-horizontal-space' =>
	sub
	{
		return if !defined $rh->{w}->{input_v}->{buffer};

		my $buffer_prev = substr $rh->{w}->{input_v}->{buffer}, 0, $rh->{w}->{input_v}->{position};
		my $buffer_next = substr $rh->{w}->{input_v}->{buffer}, $rh->{w}->{input_v}->{position};

		$buffer_prev =~ s/\s*$//;
		$buffer_next =~ s/^\s*//;

		$rh->{w}->{input_v}->{buffer} = $buffer_prev . $buffer_next;
		$rh->{w}->{input_v}->{position} = real_length($buffer_prev);
	},
	'yank' =>
	sub
	{
		return if !defined $rh->{w}->{input_v}->{last_killed};

		if (defined $rh->{w}->{input_v}->{buffer})
		{
			substr($rh->{w}->{input_v}->{buffer}, $rh->{w}->{input_v}->{position}, 0) =
			    $rh->{w}->{input_v}->{last_killed};
			$rh->{w}->{input_v}->{position} += real_length($rh->{w}->{input_v}->{last_killed});
		}
		else
		{
			$rh->{w}->{input_v}->{buffer}   = $rh->{w}->{input_v}->{last_killed};
			$rh->{w}->{input_v}->{position} = real_length($rh->{w}->{input_v}->{last_killed});
		}
        },
	'transpose-chars' =>
	sub
	{
		return if !defined $rh->{w}->{input_v}->{buffer} || !$rh->{w}->{input_v}->{position};

		my $buffer_trans = substr $rh->{w}->{input_v}->{buffer}, 0, $rh->{w}->{input_v}->{position}+1;
		$buffer_trans =~ s/(.)(.)$/$2$1/;

		substr ($rh->{w}->{input_v}->{buffer}, 0, $rh->{w}->{input_v}->{position}+1) = $buffer_trans;
		$rh->{w}->{input_v}->{position}++ if
		    $rh->{w}->{input_v}->{position} < real_length($rh->{w}->{input_v}->{buffer});
	},
	'transpose-words',
	sub
	{
		return if !defined $rh->{w}->{input_v}->{buffer};

		if ($rh->{w}->{input_v}->{buffer} =~ /^(\S+)\s*\S/ &&
		    $rh->{w}->{input_v}->{position} >= real_length($1))
		{
			my ($to_trans_pre, $to_trans);
			my $buffer_trans1 = substr $rh->{w}->{input_v}->{buffer}, 0, $rh->{w}->{input_v}->{position};
			my $buffer_trans2 = substr $rh->{w}->{input_v}->{buffer}, $rh->{w}->{input_v}->{position};

			$buffer_trans2 =~ /^(\s*\S*)/;
			$to_trans_pre = $to_trans = $buffer_trans1 . $1;

			$to_trans =~ s/(\S+)(\s*)(\S+)\s*$/$3$2$1/;

			$rh->{w}->{input_v}->{buffer} =~ s/^$to_trans_pre/$to_trans/;
			$rh->{w}->{input_v}->{position} = real_length($to_trans);
		}
	},
	'previous-history' =>
	sub
	{
		return if !$rh->{w}->{input_v}->{history} ||
		    $rh->{w}->{input_v}->{history_position} >= scalar @{$rh->{w}->{input_v}->{history}} - 1;

		my $history = $rh->{w}->{input_v}->{history}->[++$rh->{w}->{input_v}->{history_position}];

		if ($rh->{w}->{input_v}->{history_position} == 0)
		{
			$rh->{w}->{input_v}->{old_buffer} = $rh->{w}->{input_v}->{buffer};
		}
		$rh->{w}->{input_v}->{buffer}   = $history;
		$rh->{w}->{input_v}->{position} = real_length($rh->{w}->{input_v}->{buffer});
        },
	'next-history' =>
	sub
	{
		if ($rh->{w}->{input_v}->{history} && $rh->{w}->{input_v}->{history_position} == 0)
		{
			if (defined $rh->{w}->{input_v}->{old_buffer})
			{
				$rh->{w}->{input_v}->{buffer}     = $rh->{w}->{input_v}->{old_buffer};
				$rh->{w}->{input_v}->{position}   = real_length($rh->{w}->{input_v}->{buffer});
				$rh->{w}->{input_v}->{old_buffer} = undef;
			}
			else
			{
				$rh->{w}->{input_v}->{buffer}   = undef;
				$rh->{w}->{input_v}->{position} = 0;
			}
			$rh->{w}->{input_v}->{history_position}-- if $rh->{w}->{input_v}->{history_position} == 0;
		}
		elsif ($rh->{w}->{input_v}->{history} && $rh->{w}->{input_v}->{history_position} > 0)
		{
			my $history = $rh->{w}->{input_v}->{history}->[--$rh->{w}->{input_v}->{history_position}];
			$rh->{w}->{input_v}->{buffer}   = $history;
			$rh->{w}->{input_v}->{position} = real_length($rh->{w}->{input_v}->{buffer});
		}
	},
	'beginning-of-history' =>
	sub
	{
		return if !$rh->{w}->{input_v}->{history};

		$rh->{w}->{input_v}->{old_buffer} = $rh->{w}->{input_v}->{buffer} if
		    $rh->{w}->{input_v}->{history_position} <= 0;

		my $history = $rh->{w}->{input_v}->{history_position} = scalar @{$rh->{w}->{input_v}->{history}}-1;

		$rh->{w}->{input_v}->{buffer} = $rh->{w}->{input_v}->{history}->[$history];
		$rh->{w}->{input_v}->{position} = real_length($rh->{w}->{input_v}->{history}->[$history]);
	},
	'end-of-history' =>
	sub
	{
		return if !$rh->{w}->{input_v}->{history} || !$rh->{w}->{input_v}->{history_position};

		my $history = (defined $rh->{w}->{input_v}->{old_buffer})
		    ? $rh->{w}->{input_v}->{old_buffer}
		    : undef;

		$rh->{w}->{input_v}->{buffer} = $history;
		$rh->{w}->{input_v}->{position} = (defined $history) ? real_length($history) : 0;
		$rh->{w}->{input_v}->{history_position} = -1;
	},
	'overwrite-mode' =>
	sub
	{
		$rh->{w}->{input_v}->{overwrite} = !$rh->{w}->{input_v}->{overwrite};
		window_status_update();
	},
	'previous-window' =>
	sub
	{
		my $new_win = $rh->{w}->{current};
		while (--$new_win > 0)
		{
			if (defined $rh->{w}->{main}->[$new_win])
			{
				window_main_change_active($new_win);
				return 1;
			}
		}
		window_main_change_active($rh->{w}->{current} - 1);
	},
	'next-window' =>
	sub
	{
		my $new_win = $rh->{w}->{current};
		while (++$new_win < scalar @{$rh->{w}->{main}})
		{
			if (defined $rh->{w}->{main}->[$new_win])
			{
				window_main_change_active($new_win);
				return 1;
			}
		}
		window_main_change_active($rh->{w}->{current} + 1);
	},
        'switch-to-window-0'  => sub { window_main_change_active(0)  if $rh->{w}->{current} != 0 },
	'switch-to-window-1'  => sub { window_main_change_active(1)  if $rh->{w}->{current} != 1 },
	'switch-to-window-2'  => sub { window_main_change_active(2)  if $rh->{w}->{current} != 2 },
	'switch-to-window-3'  => sub { window_main_change_active(3)  if $rh->{w}->{current} != 3 },
	'switch-to-window-4'  => sub { window_main_change_active(4)  if $rh->{w}->{current} != 4 },
	'switch-to-window-5'  => sub { window_main_change_active(5)  if $rh->{w}->{current} != 5 },
	'switch-to-window-6'  => sub { window_main_change_active(6)  if $rh->{w}->{current} != 6 },
	'switch-to-window-7'  => sub { window_main_change_active(7)  if $rh->{w}->{current} != 7 },
	'switch-to-window-8'  => sub { window_main_change_active(8)  if $rh->{w}->{current} != 8 },
	'switch-to-window-9'  => sub { window_main_change_active(9)  if $rh->{w}->{current} != 9 },
	'switch-to-window-10' => sub { window_main_change_active(10) if $rh->{w}->{current} != 10 },

	'refresh-screen' =>
        sub
        {
		curs_set(0);
		$curscr->erase;  $curscr->redrawwin;  $curscr->noutrefresh;
		$rh->{w}->{main}->[$rh->{w}->{current}]->redrawwin;
		$rh->{w}->{main}->[$rh->{w}->{current}]->noutrefresh;

		window_title_update();
		window_status_update();
		window_input_update();
		curs_set(1);
	},
	'complete'       => sub { window_input_complete() },
	'accept-line'    => sub { window_input_handle() if defined $rh->{w}->{input_v}->{buffer} },

        'scroll-up-buffer' =>
        sub
        {
		$rh->{w}->{main_v}->[$rh->{w}->{current}]->{scrolling} = 1;
		window_main_redraw($rh->{w}->{current}, -$rh->{w}->{main_v}->[$rh->{w}->{current}]->{buffer_begin});
	},
        'scroll-down-buffer' =>
        sub 
        {
		$rh->{w}->{main_v}->[$rh->{w}->{current}]->{scrolling} = 1;
		window_main_redraw($rh->{w}->{current}, scalar @{$rh->{w}->{main_v}->[$rh->{w}->{current}]->{lines}}-
				   $rh->{w}->{main_v}->[$rh->{w}->{current}]->{screen_size});
	},
        'scroll-up-page' =>
         sub
         {
		 $rh->{w}->{main_v}->[$rh->{w}->{current}]->{scrolling} = 1;
		 window_main_redraw($rh->{w}->{current}, int -($Rows / 2));
	 },
         'scroll-down-page' =>
         sub
         {
		 $rh->{w}->{main_v}->[$rh->{w}->{current}]->{scrolling} = 1;
		 window_main_redraw($rh->{w}->{current}, int ($Rows / 2));
	 },
         'scroll-up-line' =>
         sub
         {
		 $rh->{w}->{main_v}->[$rh->{w}->{current}]->{scrolling} = 1;
		 window_main_redraw($rh->{w}->{current}, -1);
	 },
         'scroll-down-line' =>
         sub
         {
		 $rh->{w}->{main_v}->[$rh->{w}->{current}]->{scrolling} = 1;
		 window_main_redraw($rh->{w}->{current}, 1);
	 },

         'msnre-clear-screen'      => \&cmd_clear,
         'msnre-clear-screen-all'  => \&cmd_clear_all,
         'msnre-connect'           => \&cmd_connect,
         'msnre-disconnect'        => \&cmd_disconnect,
         'msnre-help'              => \&cmd_help,
         'msnre-save-config'       => \&cmd_save,
         'msnre-status-online'     => sub { cmd_status('online') },
         'msnre-status-offline'    => sub { cmd_status('offline') },
         'msnre-status-idle'       => sub { cmd_status('idle') },
         'msnre-status-away'       => sub { cmd_status('away') },
         'msnre-status-busy'       => sub { cmd_status('busy') },
         'msnre-status-brb'        => sub { cmd_status('be_right_back') },
         'msnre-status-lunch'      => sub { cmd_status('out_to_lunch') },
         'msnre-status-phone'      => sub { cmd_status('on_the_phone') },
         'msnre-quit'              => \&cmd_quit,
         'msnre-uptime'            => \&cmd_uptime,
        };
        1;
}

# &MSNre::print_help_cmd ()
# ----------------------------------------------------------------------
#

sub print_help_cmd
{
	print "$NAME v$VERSION\n";
	print "Usage: msnre [OPTIONS]...\n\n";

	print " -c --config=path      Specify a different configuration file to read\n";
	print " -h --help             Print this help screen and quit MSNre\n";
	print " -p --profile=name     Start with the specified profile\n";
	print " -v --version          Print the program version and license and quit MSNre\n\n";
	print "Report bugs to <incoming\@tiscali.cz>.\n";
	exit 0;
}

# &MSNre::print_version_cmd ()
# ----------------------------------------------------------------------
#

sub print_version_cmd
{
	print "$NAME version $VERSION ($DATE)\n";
	print "Copyright (C) 2003 <incoming\@tiscali.cz>\n\n";

	print "$NAME is free software; you can redistribute it and/or modify\n";
	print "it under the terms of the GNU General Public License as published by the\n";
	print "Free Software Foundation; either version 2 of the License, or\n";
	print "(at your option) any later version. See COPYING file for details.\n";
	exit 0;
}

# &MSNre::print_ascii_art ()
# ----------------------------------------------------------------------
#

sub print_ascii_art
{
	print_window(1, "\n");
	print_window(1, fix_colors('<%n>'.'      ' .    'A8Q?b     dgF$  ld8f8ff.  r8hb    q8l! ' . "\n"));
	print_window(1, fix_colors('<%n>'.'      ' .    '828V8b   d8g8B d8kP  Y88l ng88b   8G#Q ' . "\n"));
	print_window(1, fix_colors('<%n>'.'      ' .    'E88eHTb.d8f8nb YV8b.      c8n8wb  g8SR ' . "\n"));
	print_window(1, fix_colors('<%n>'.'      ' .    'gkDCY8NGf8Pefn  "Yn8$b.   Qr8YQgb l;w8 <%r>Qh8f8k8 .djfb.  ' . "\n"));
	print_window(1, fix_colors('<%n>'.' <%W>:<%n>    h!v8 Y88wP f8D     "Ym8h. 8n8 Y88b8t8G <%r>8HjP"  d8P  Y8b ' . "\n"));
	print_window(1, fix_colors('<%n>'.' <%W>:<%n>    $8IN  Y8P  Q8A       "8ga a8.  YqnN8fH <%r>F8e    8dn8hkHf ' . "\n"));
	print_window(1, fix_colors('<%n>'.' <%W>:<%n>    8mb8   "   h9m YHj6  dm8P #88   NWn2h$ <%r>ej,    Y8b.     ' . "\n"));
	print_window(1, fix_colors('<%n>'.' <%W>:<%n>    J88x       58l  "YNE8kP"  8Th    YGnmn <%r>fmr     "Yd8wl   <%M>V<%m>ersion <%W>' . $VERSION . "\n"));
	print_window(1, fix_colors('<%n>'.' <%W>:........................................................................' . "<%n>\n\n"));

	window_main_update(1);
	1;
}

getoptions();

# -------------------- Main loop

POE::Session->create(
		     inline_states => {
			     _start         => \&poe_start_client,
			     main_loop      => \&poe_main_loop,
			     input_read     => \&window_input_read,
			     status_update  => \&poe_status_update,
			     timers         => \&poe_main_loop_timers,
			     output_stderr  => \&window_output_stderr,
			     _stop          => \&poe_close_client,
		     },
);

$poe_kernel->run();
exit 0;

__END__

=head1 NAME

MSNre - Console based MSN Instant Messenger Clone

=head1 SYNOPSIS

  msnre [--config] [--help] [---version]

=head1 DESCRIPTION

MSNre is a lightweight but powerful console-based MSN Instant Messenger clone. It has an easy-to-use
curses BitchX-like user interface. It has many features, including instant messaging, sending
messages to cell phones, file transfer, complete contacts/groups management, email notifications,
auto-away messages, netmeeting support, aliases, messages logging, event scripts, local contact list,
multi-user chat support, ability to encode characters in the system locale, and much more.

=head1 OPTIONS

=over 5

=item B<-c>,  B<--config=path>

Specify a different configuration file. Do not read the configuration from the default path
(~/.msnre/msnre.com) but from this file.

=item B<-h>,  B<--help>

Print the help screen with a list of the command-line options and quit MSNre.

=item B<-p>,  B<--profile>

Load a different profile from the configuration file instead of the default one.

=item B<-v>,  B<--version>

Print the version number and the license and quit MSNre.

=back

=head1 USAGE

The usage is similar to the BitchX client. All the commands have to start with / (slash) character.
You can use the Tab completion for almost everything. However the windowing is much easier and
less flexible than in BitchX. You can use the /WINDOW command to do some basic tasks with the
windows.

=head2 Contact Lists

If you request the contact list using one of the /L commands, the contacts are printed in the
following format:

  A B I M  [Current Status]  Friendly name  <passport>

  A - The current contact is an alias defined in the configuration
  B - The current contact is blocked
  I - The current contact is (locally) ignored
  M - The current contact can receive message on the cell phone (/PAGER)

=head2 Character Encodings

MSNre internally uses UTF-8 character set. However it is able to encode the characters in the
current system locale. Most of the conversions are lossy so if you have the possibility to use
UTF-8 terminal (uxterm) you should prefer it. MSNre also encodes all the input from the current
system locale in UTF-8 before sending the data to the server. You shouldn't expect any trouble
if your locales are configured properly.

=head1 CONFIGURATION

At the first run MSNre generates a configuration file in ~/.msnre/msnre.conf. This section
describes some parts of the configuration.

=head2 Aliases

In MSNre there are two types of aliases you can create. Aliases for commands and aliases
for contact list members.

To create an alias for a person in the contact list, use the /ALIASCN command or add the
following line to your msnre.conf:

 ALIAS CONTACT  someone@hotmail.com  An Alias

To create an alias for a command, you can do it using the /ALIASCMD command (see /HELP ALIASCMD)
or add the foollowing line to your msnre.conf file:

 ALIAS COMMAND  new_command  old_command

 Examples:
  ALIAS COMMAND  WN  WINDOW NEW TITLE ON DOUBLE ON  (adds /WN command)
  ALIAS COMMAND  AG  ADDGROUP  (adds /AG command - same as /ADDGROUP)

You can also use variables in the commands. Variables like $0 for the first argument, $1 for
the second one and so forth. Use $* to add all the arguments at once.

 Example:
  ALIAS COMMAND  MM  MSG $0 Hello how are you??

=head2 Key bindings

When MSNre generaates the configuration file it places the default key bindings there. You can
change the default key bindings or add your own.

The following 'special' keys are recognized:

  <ctrl>            <end>
  <meta>            <ppage>  (Page UP)
  <return>          <npage>  (Page Down)
  <tab>             <left>   (Arrow keys)
  <bspace>          <right>
  <delete>          <up>
  <insert>          <down>
  <home>            <enter>  (Numeric)
  <end>

The following actions are recognized:

  beginning-of-line                  switch-to-window-5
  end-of-line                        switch-to-window-6
  forward-char                       switch-to-window-7
  backward-char                      switch-to-window-8
  forward-word                       switch-to-window-9
  backward-word                      switch-to-window-10
  upcase-word                        refersh-screen
  downcase-word                      complete
  capitalize-word                    accept-line
  kill-buffer                        scroll-up-buffer
  kill-line                          scroll-down-buffer
  kill-word                          scroll-up-page
  unix-line-discard                  scroll-down-page
  unix-word-rubout                   scroll-up-line
  delete-char                        scroll-down-line
  backward-delete-char               msnre-clear-screen
  delete-horizontal-space            msnre-clear-screen-all
  yank                               msnre-connect
  transpose-chars                    msnre-disconnect
  previous-history                   msnre-help
  next-history                       msnre-save-config
  beginning-of-history               msnre-status-online
  end-of-history                     msnre-status-offline
  overwrite-mode                     msnre-status-idle
  previous-window                    msnre-status-away
  next-window                        msnre-status-busy
  switch-to-window-0                 msnre-status-brb
  switch-to-window-1                 msnre-status-lunch
  switch-to-window-2                 msnre-status-phone
  switch-to-window-3                 msnre-quit
  switch-to-window-4                 msnre-uptime

'<meta>' and '<ctrl> x' are always used as a prefix. You can create new key bindings as follows:

 BIND  key_binding  action

 Example: BIND  <ctrl> x <ctrl> c  msnre-quit

=head2 Colors

In the configuration file you can specify your own colors for some specific parts of the windows.
There are 3 predefined color themes. To be able to set your own colors you have to unset the
COLOR_THEME option first. It uses the following syntax:

 COLOR  part_to_colorify  color attribute1 attribute2 ...

 Example: COLOR   timestamp   red    bold underline

For background colors the attributes are ignored.

Valid colors are:

 BLACK         MAGENTA
 BLUE          RED
 CYAN          WHITE
 GREEN         YELLOW

Valid attributes are:

 BLINK      - Blinking text
 BOLD       - Extra bright (bold) text
 DIM        - Half bright text
 UNDERLINE  - Underlined text

=head2 Profiles

Profiles allow you to use different configuration (or only some parts of the configuration) with
a single user. The default profile is named DEFAULT and all the options that don't belong to a
specific profile belong to DEFAULT. If you create your own profile and MSNre need a configuration
variable that is not present in this profile it also searches in DEFAULT. You can use the --profile
command line option to request a profile and also the /CHPROFILE option to change the profile
on-the-fly.

In the configuration the profiles are created the following way:

 [Profile name="your_profile_name"]

 ... Profile-specific options ...

 [END Profile name="your_profile_name"]

As profile-specific can be used any options. Option in a profile selected by you will be always
prefered over the global (DEFAULT) ones.

Aliases, key bindings and colors can be also profile-specific.

You can use the profile ability to use more accounts but also to choose a color theme or different
key bindings.

=head1 EVENT SCRIPT

In the configuration you can specify an event script that will be called after an event
occurs. The event script is called with the following arguments:

 event_script ( Protocol, Event_Type, Current_event_parameters )

Protocol is always 'MSN'.

The following event types are supported:

 email   - called when you receive a new e-mail to your hotmail inbox
 message - called when you receive a message
 status  - called when someone changes the status

These events are called with the following arguments:

 email   ( From_address, From_name, Subject )
 message ( Sender, Message )
 status  ( User, New_status )

For example, if you receive a message, the script would be called as follows:

 event_script("MSN", "message", "my_friend@hotmail.com", "Hi!")

You can find an example of an event script in the examples/ directory.

=head1 FILES

=over 8

=item B<~/.msnre>

Default per-user directory for the configuration and all the other user-specific files.

=item B<~/.msnre/msnre.conf>

Default configuration file.

=item B<~/.msnre/contacts>

File where the local contact list including "local contacts" is stored.

=back

=head1 AUTHOR

 Contact:      incoming@tiscali.cz
 Project page: http://www.sourceforge.net/projects/msnre

=head1 COPYRIGHT

MSNre is free software; you can redistribute it and/or modify it under the terms of the GNU
General Public License as published by the Free Software Foundation; either version 2 of the
License, or (at your option) any later version.

Please refer to the COPYING file for details.

=cut
