The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.
#
# $Date: 2009-01-11 21:41:07 $
#
# Copyright (c) 2007-2008 Alexandre Aufrere
# Licensed under the terms of the GPL (see perldoc MRIM.pm)
#

use 5.008;
use strict;

# below is just an utility class
package Net::MRIM::Message;

use constant {
 TYPE_UNKNOWN	=> 0,
 TYPE_MSG		=> 1,
 TYPE_LOGOUT_FROM_SRV	=> 2,
 TYPE_CONTACT_LIST	=> 3,
 TYPE_SERVER	=> 4
};

sub new {
	my ($pkgname)=@_;
	my $self={}; 
	$self->{_type}=TYPE_UNKNOWN;
	$self->{TYPE_SERVER_NOTIFY}=0;
	$self->{TYPE_SERVER_ANKETA}=1;
	$self->{TYPE_SERVER_AUTH_REQUEST}=2;
	bless $self;
	return $self;
}

sub set_message {
	my ($self, $from, $to, $message)=@_;
	$self->{_type}=TYPE_MSG;
	$self->{_from}=$from;
	$self->{_to}=$to;
	$self->{_message}=$message;
}

sub is_message{
	my ($self)=@_;
	return ($self->{_type}==TYPE_MSG);
}

sub get_from {
	my ($self)=@_;
	return $self->{_from};
}

sub get_to {
	my ($self)=@_;
	return $self->{_to};
}

sub get_message {
	my ($self)=@_;
	return $self->{_message};
}

sub set_logout_from_server {
	my ($self)=@_;
	$self->{_type}=TYPE_LOGOUT_FROM_SRV;
}

sub is_logout_from_server {
	my ($self)=@_;
	return ($self->{_type}==TYPE_LOGOUT_FROM_SRV);
}

sub set_server_msg {
	my ($self,$stype,$to,$message,$svalue)=@_;
	$self->{_type}=TYPE_SERVER;
	$self->{_stype}=$stype;
	$self->{_from}='SERVER';
	$self->{_to}=$to;
	$self->{_message}=$message;
	$self->{_from}=$svalue if ($stype==$self->{TYPE_SERVER_AUTH_REQUEST});
}

sub is_server_msg {
	my ($self)=@_;
	return ($self->{_type}==TYPE_SERVER);
}

sub get_subtype {
	my ($self)=@_;
	return $self->{_stype};
}

sub set_contact_list {
	my ($self, $groups, $contacts)=@_;
	$self->{_type}=TYPE_CONTACT_LIST;
	$self->{_groups}=$groups;
	$self->{_contacts}=$contacts;
}

sub is_contact_list {
	my ($self)=@_;
	return ($self->{_type}==TYPE_CONTACT_LIST);
}

sub get_groups {
	my ($self)=@_;
	return $self->{_groups};
}

sub get_contacts {
	my ($self)=@_;
	return $self->{_contacts};
}

package Net::MRIM::Contact;

sub new {
	my ($pkgname,$email,$name,$status)=@_;
	my $self={};
	$self->{_email}=$email;
	$self->{_name}=$name;
	$self->{_status}=$status;
	$self->{STATUS_ONLINE}=0x00000001;
	$self->{STATUS_AWAY}=0x00000002;
	bless $self;
	return $self;
}

sub get_email {
	my $self=shift;
	return $self->{_email};
}

sub get_name {
	my $self=shift;
	return $self->{_name};
}

sub get_status {
	my $self=shift;
	return $self->{_status};
}

sub set_status {
	my ($self,$status)=@_;
	$self->{_status}=$status;
}

package Net::MRIM;

our $VERSION='1.11';

=pod

=head1 NAME

Net::MRIM - Perl implementation of mail.ru agent protocol

=head1 DESCRIPTION

This is a Perl implementation of the mail.ru agent protocol, which specs can be found at http://agent.mail.ru/protocol.html

=head1 SYNOPSIS

To construct and connect to MRIM's servers:

 my $mrim=Net::MRIM->new(
 			Debug=>0,
 			PollFrequency=>5
 			);
 $mrim->hello();

To log in:

 if (!$mrim->login("login\@mail.ru","password")) {
	print "LOGIN REJECTED\n";
	exit;
 } else {
	print "LOGGED IN\n";
 }

To authorize a user:

 my $ret=$mrim->authorize_user("friend\@mail.ru");

To add a user to contact list (sends automatically auth request):

 $ret=$mrim->add_contact("friend\@mail.ru");

To remove a user from contact list:

 $ret=$mrim->remove_contact("friend\@mail.ru");

To send a message:

 $ret=$mrim->send_message("friend\@mail.ru","hello");

To change user status:

 $ret=$mrim->change_status(status);

Where status=0 means online and status=1 means away

Get information for a contact:

 $ret=$mrim->contact_info("friend\@mail.ru");
 
Search for users:

 $ret=$mrim->search_user(email, sex, country, online);

Where sex=(1|2), country can be found at http://agent.mail.ru/region.txt or in Net::MRIM::Data.pm, and online=(0|1)

Analyze the return of the message:

 if ($ret->is_message()) {
	print "From: ".$ret->get_from()." Message: ".$ret->get_message()." \n";
 } elsif ($ret->is_server_msg()) {
 	print $ret->get_message()." \n";
 }

Looping to get messages:

 while (1) {
	sleep(1);
	$ret=$mrim->ping();
	if ($ret->is_message()) {
		print "From: ".$ret->get_from()." Message: ".$ret->get_message()." \n";
	}
 }

Disconnecting:

 $mrim->disconnect();

=head1 AUTHOR

Alexandre Aufrere <aau@cpan.org>

=head1 COPYRIGHT

Copyright (c) 2007-2008 Alexandre Aufrere. This code may be used under the terms of the GPL version 2 (see at http://www.gnu.org/licenses/old-licenses/gpl-2.0.html). The protocol remains the property of Mail.Ru (see at http://www.mail.ru).

=cut

use IO::Socket::INET;
use IO::Select;

# the definitions below come straight from the protocol documentation
use constant {
 CS_MAGIC		=> 0xDEADBEEF,
 PROTO_VERSION	=> 0x10008,

 MRIM_CS_HELLO 	=> 0x1001,	## C->S, empty   
 MRIM_CS_HELLO_ACK 	=> 0x1002,	## S->C, UL mrim_connection_params_t

 MRIM_CS_LOGIN2      => 0x1038,	## C->S, LPS login, LPS password, UL status, LPS useragent
 MRIM_CS_LOGIN_ACK 	=> 0x1004,	## S->C, empty
 MRIM_CS_LOGIN_REJ 	=> 0x1005,	## S->C, LPS reason
 MRIM_CS_LOGOUT		=> 0x1013,    ## S->C, UL reason

 MRIM_CS_PING 	=> 0x1006,	## C->S, empty

 MRIM_CS_USER_STATUS	=> 0x100f,	## S->C, UL status, LPS user
  STATUS_OFFLINE	 => 0x00000000,
  STATUS_ONLINE    => 0x00000001,
  STATUS_AWAY      => 0x00000002,
  STATUS_UNDETERMINED   => 0x00000003,
 MRIM_CS_USER_INFO		=> 0x1015,
 MRIM_CS_ADD_CONTACT 	=> 0x1019,  # C->S UL flag, UL group_id, LPS email, LPS name
  CONTACT_FLAG_VISIBLE	=> 0x00000008,
  CONTACT_FLAG_REMOVED	=> 0x00000001,
  CONTACT_FLAG_SMS	=> 0x00100000,
 MRIM_CS_ADD_CONTACT_ACK	=> 0x101A,
  CONTACT_OPER_SUCCESS	=> 0x00000000,
  CONTACT_OPER_USER_EXISTS	=> 0x00000005,
 MRIM_CS_AUTHORIZE		=> 0x1020,	# C -> S, LPS user
 MRIM_CS_MODIFY_CONTACT		=> 0x101B,	# C -> S, UL id, UL flags, UL group_id, LPS email, LPS name, LPS unused
 MRIM_CS_MODIFY_CONTACT_ACK	=> 0x101C,
 MRIM_CS_AUTHORIZE_ACK	=> 0x1021,	# C -> S, LPS user

 MRIM_CS_MESSAGE 		=> 0x1008,	## C->S, UL flags, LPS to, LPS message, LPS rtf-message
  MESSAGE_FLAG_OFFLINE	=> 0x00000001,
  MESSAGE_FLAG_NORECV	=> 0x00000004,
  MESSAGE_FLAG_AUTHORIZE	=> 0x00000008,
  MESSAGE_FLAG_SYSTEM	=> 0x00000040,
  MESSAGE_FLAG_RTF		=> 0x00000080,
  MESSAGE_FLAG_NOTIFY	=> 0x00000400,
  MESSAGE_FLAG_UNKOWN   => 0x00100000,
 MRIM_CS_MESSAGE_RECV	=> 0x1011,
 MRIM_CS_MESSAGE_STATUS	=> 0x1012, # S->C
 MRIM_CS_MESSAGE_ACK			=> 0x1009, #S->C
 MRIM_CS_OFFLINE_MESSAGE_ACK	=> 0x101D, #S->C UIDL, LPS message
 MRIM_CS_DELETE_OFFLINE_MESSAGE	=> 0x101E, #C->S UIDL

 MRIM_CS_CONNECTION_PARAMS =>0x1014, # S->C 

 MRIM_CS_CHANGE_STATUS	=> 0x1022,
 MRIM_CS_GET_MPOP_SESSION	=> 0x1024,
 MRIM_CS_MPOP_SESSION	=> 0x1025,

 MRIM_CS_ANKETA_INFO	=> 0x1028, # S->C
 MRIM_CS_WP_REQUEST		=>0x1029, # C->S
 MRIM_CS_MAILBOX_STATUS	=> 0x1033,
 MRIM_CS_CONTACT_LIST2	=> 0x1037, # S->C UL status, UL grp_nb, LPS grp_mask, LPS contacts_mask, grps, contacts

 MRIM_CS_SMS		=> 0x1039, # C->S UL unkown, LPS number, LPS message
 MRIM_CS_SMS_ACK	=> 0x1040, # S->C UL status

 # Don't look for file transfer, it's simply not handled
 # Mail.Ru only partially documented the old, unused P2P file transfer
 # the new file transfer simply gives an (unusable) RFC1918 address
 # when getting the MRIM_CS_FILE_TRANSFER packet
 # Needs some reverse-engineering. Has been done by Miranda's MRA plugin guys,
 # but for some reason i can't find the source

 MRIMUA => "Net::MRIM.pm v. "
};

use bytes;

# the constructor takes only one optionnal parameter: debug (true or false);
sub new {
	my ($pkgname,%params)=@_;
	my ($host, $port) = _get_host_port();
	my $sock = IO::Socket::INET->new(
				PeerAddr		=> $host,
				PeerPort		=> $port,
				Proto			=> 'tcp',
				Type			=> SOCK_STREAM,
				TimeOut			=> 20
			);
	die "couldn't connect" if (!defined($sock));
	print "DEBUG Connected to $host:$port\n" if (($params{Debug})&&($params{Debug}==1));
	my $self={};
	$self->{_sock}=$sock;
	$self->{_seq_real}=0;
	$self->{_ping_period}=30; # value by default
	# this stores the contact list:
	$self->{_contacts}={};
	# this stores the MRIM's UIDs for contacts (internal use only)
	$self->{_all_contacts}={};
	$self->{_debug}=$params{Debug} if (($params{Debug})&&($params{Debug}==1));
	$self->{_freq}=$params{PollFrequency} || 5;
	$self->{_freq}=30 if ($self->{_freq}>30);
	$self->{_last_seq}=-1;
	$self->{_last_type}=-1;
	$self->{_last_time}=time();
	print "DEBUG Poll Frequency: ".$self->{_freq}."\n" if ($self->{_debug});
	bless $self;
	return $self;
}

# this is the technical "hello" header
#  as a side note, it seems to me that this protocol was created by people who were used to e-mail ;-)
sub hello {
	my ($self)=@_;
	my $ret=$self->{_sock}->send(_make_mrim_packet($self,MRIM_CS_HELLO,""),0);
	my($msgrcv,$datarcv,$dlen)=_receive_data($self);
	$datarcv=unpack("V",$datarcv);
	$self->{_ping_period} = $datarcv;
	$self->{_seq_real}++;
	print "DEBUG Connected to MRIM. Ping period is $datarcv\n" if ($datarcv&&($self->{_debug}));
}

# normally useless
sub get_ping_period {
	my ($self)=@_;
	return $self->{_ping_period};
}

# the server should be ping'ed regularly to avoid being disconnected
sub ping {	
	my ($self)=@_;
	print "DEBUG [ping]\n" if ($self->{_debug});
	my $curtime=time();
	if (($curtime-$self->{_last_time})>=($self->{_ping_period}-10)) {
		$self->{_sock}->send(_make_mrim_packet($self,MRIM_CS_PING,""),0);	
		$self->{_seq_real}++;
		$self->{_last_time}=$curtime;
	}
	my ($msgrcv,$datarcv,$dlen)=_receive_data($self);
	return _analyze_received_data($self,$msgrcv,$datarcv,$dlen);
}

# this is to log in...
sub login {
	my ($self,$login,$pass)=@_;
	my $status=STATUS_ONLINE;
	print "DEBUG [status]: $status\n" if ($self->{_debug});
	my $data=_to_lps($login)._to_lps($pass).pack("V",$status)._to_lps("".MRIMUA.$VERSION);
	$self->{_sock}->send(_make_mrim_packet($self,MRIM_CS_LOGIN2,$data));
	$self->{_seq_real}++;
	$self->{_login}=$login;
	my($msgrcv,$datarcv,$dlen)=_receive_data($self);
	my $norace=0;
	while (($msgrcv==0)&&($norace<50)) {
		($msgrcv,$datarcv,$dlen)=_receive_data($self);
		$norace++;
	}
	print "DEBUG [rcv login ack] $msgrcv\n" if ($self->{_debug});
	return ($msgrcv==MRIM_CS_LOGIN_ACK)?1:0;
}

# this is to send a message
sub send_message {
	my ($self,$to,$message)=@_;
	print "DEBUG [send message]: $message\n" if ($self->{_debug});
	my $data=pack("V",MESSAGE_FLAG_NORECV)._to_lps($to)._to_lps($message)._to_lps("");
	$self->{_sock}->send(_make_mrim_packet($self,MRIM_CS_MESSAGE,$data));
	$self->{_seq_real}++;
	my ($msgrcv,$datarcv,$dlen)=_receive_data($self);
	return _analyze_received_data($self,$msgrcv,$datarcv,$dlen);
}

# send SMS
# the implementation is awful: it adds and then remove an SMS entry to the contact list, and tries to send SMS in between.
# why ? i'm French, live in France, and don't have any access to a russian mobile phone... so it's impossible for me to test, i just use some web literature i found on the topic
# wish to help ? contact me - aau@cpan.org (vozmozhno i po-russkiy - lyudi! pomogite! ;-))))
sub send_sms {
	my ($self,$numberto,$message)=@_;
	my $dontremove=0;
	print "DEBUG [send SMS]: $message\n" if ($self->{_debug});
	# first, we should "add" it as SMS contact...
	my $data=pack("V",CONTACT_FLAG_SMS).pack("V",0xffffffff)._to_lps($numberto)._to_lps("SMS")._to_lps($numberto);
	$self->{_sock}->send(_make_mrim_packet($self,MRIM_CS_ADD_CONTACT,$data));
	$self->{_seq_real}++;
	my ($msgrcv,$datarcv,$dlen)=_receive_data($self);
	my @datas=_from_mrim_us("uu",$datarcv);
	my $cid=$datas[1];
	# This is ugly: in case some message is in between, return without sending the SMS actually
	if ($msgrcv != MRIM_CS_ADD_CONTACT_ACK) {
		print "DEBUG [send SMS]: $message was NOT sent\n" if ($self->{_debug});
		return _analyze_received_data($self,$msgrcv,$datarcv,$dlen);
	}
	# In case adding the contact failed, return without sending the SMS actually, but with an error message
	if (($datas[0] != CONTACT_OPER_SUCCESS)&&($datas[0] != CONTACT_OPER_USER_EXISTS)) {
		my $data=new Net::MRIM::Message();
		$data->set_server_msg($data->{TYPE_SERVER_NOTIFY},$self->{_login},"CONTACT_OPER_ERROR_SMS: Error adding contact for SMS sending");
		return $data;
	}
	$dontremove=1 if ($datas[0] == CONTACT_OPER_USER_EXISTS);
	$data=pack("V",0)._to_lps($numberto)._to_lps($message);
	$self->{_sock}->send(_make_mrim_packet($self,MRIM_CS_SMS,$data));
	$self->{_seq_real}++;
	my ($msgrcvsms,$datarcvsms,$dlensms)=_receive_data($self);
	# if contact was already in contact list, do not try to remove it.
	return _analyze_received_data($self,$msgrcvsms,$datarcvsms,$dlensms) if ($dontremove==1);
	$data=pack("V",$cid).pack("V",CONTACT_FLAG_SMS|CONTACT_FLAG_REMOVED).pack("V",0xffffffff)._to_lps($numberto)._to_lps("SMS")._to_lps($numberto);
	$self->{_sock}->send(_make_mrim_packet($self,MRIM_CS_MODIFY_CONTACT,$data));
	$self->{_seq_real}++;
	($msgrcv,$datarcv,$dlen)=_receive_data($self);
	# This is ugly: in case some message is in between, return without knowing if the SMS was actually sent
	if ($msgrcv != MRIM_CS_MODIFY_CONTACT_ACK) {
		return _analyze_received_data($self,$msgrcv,$datarcv,$dlen);
	}
	return _analyze_received_data($self,$msgrcvsms,$datarcvsms,$dlensms);
}

# to authorize a user to add us to the contact list
sub authorize_user {
	my ($self,$user)=@_;
	my $data=_to_lps($user);
	$self->{_sock}->send(_make_mrim_packet($self,MRIM_CS_AUTHORIZE,$data));
	$self->{_seq_real}++;	
	my ($msgrcv,$datarcv,$dlen)=_receive_data($self);
	return _analyze_received_data($self,$msgrcv,$datarcv,$dlen);	
}

# change user's status: 0=online, 1=away
sub change_status {
	my ($self,$status)=@_;
	my $data=pack('V',STATUS_ONLINE);
	$data=pack('V',STATUS_AWAY) if ($status==1);
	$self->{_sock}->send(_make_mrim_packet($self,MRIM_CS_CHANGE_STATUS,$data));
	$self->{_seq_real}++;	
	my ($msgrcv,$datarcv,$dlen)=_receive_data($self);
	return _analyze_received_data($self,$msgrcv,$datarcv,$dlen);	
}

# to add a contact to the contact list
sub add_contact {
	my ($self, $email)=@_;
	print "DEBUG [add contact]: $email\n" if ($self->{_debug});
	my $data=pack("V",0).pack("V",0xffffffff)._to_lps($email).pack("V",0).pack("V",0);
	$self->{_sock}->send(_make_mrim_packet($self,MRIM_CS_ADD_CONTACT,$data));
	$self->{_seq_real}++;
	# not in the protocol: after sending an add request, one should send an auth message !
	$data=pack("V",MESSAGE_FLAG_AUTHORIZE)._to_lps($email)._to_lps("Please authorize me")._to_lps("");
	$self->{_sock}->send(_make_mrim_packet($self,MRIM_CS_MESSAGE,$data));
	$self->{_seq_real}++;
	my ($msgrcv,$datarcv,$dlen)=_receive_data($self);
	return _analyze_received_data($self,$msgrcv,$datarcv,$dlen);	
}

# to remove a contact from the contact list
sub remove_contact {
	my ($self, $email)=@_;
	print "DEBUG [remove contact]: $email ".$self->{_all_contacts}->{$email}."\n" if ($self->{_debug});
	return new Net::MRIM::Message if (!defined($self->{_all_contacts}->{$email}));
	# C -> S, UL id, UL flags, UL group_id, LPS email, LPS name, LPS unused
	my $data=pack("V",$self->{_all_contacts}->{$email}).pack("V",CONTACT_FLAG_REMOVED).pack("V",0xffffffff)._to_lps($email).pack("V",0).pack("V",0);
	$self->{_sock}->send(_make_mrim_packet($self,MRIM_CS_MODIFY_CONTACT,$data));
	$self->{_seq_real}++;
	my ($msgrcv,$datarcv,$dlen)=_receive_data($self);
	if ($msgrcv==MRIM_CS_MODIFY_CONTACT_ACK) {
		my @datas=_from_mrim_us("uu",$datarcv.pack("V",0));
		if ($datas[0]==0) {
			print "DEBUG $email removed from CL\n" if ($self->{_debug});
			$self->{_contacts}->{$email}=undef;
			$self->{_all_contacts}->{$email}=undef;
		}
	}
	return _analyze_received_data($self,$msgrcv,$datarcv,$dlen);
}

# get contact info from server (send contact info request)
sub contact_info {
	my ($self, $email)=@_;
	$email=~m/^([a-z0-9\_\-\.]+)\@([a-z0-9\_\-\.]+)$/i;
	my $cuser=$1;
	my $cdomain=$2;
	my $data=pack("V",0x00000000)._to_lps($cuser).pack("V",0x00000001)._to_lps($cdomain).pack("V",0x00000009)._to_lps('1');
	print "DEBUG Getting infor for $cuser $cdomain\n" if ($self->{_debug});
	$self->{_sock}->send(_make_mrim_packet($self,MRIM_CS_WP_REQUEST,$data));
	$self->{_seq_real}++;
	my ($msgrcv,$datarcv,$dlen)=_receive_data($self);
	return _analyze_received_data($self,$msgrcv,$datarcv,$dlen);
}

# get contact avatar url
sub get_contact_avatar_url {
	my ($self, $email)=@_;
	$email=~m/^([a-z0-9\_\-\.]+)\@([a-z0-9\_\-]+)\.([a-z0-9\_\-]+)$/i;
	my $cuser=$1;
	my $cdomain=$2;
	return "http://avt.foto.mail.ru/$cdomain/$cuser/_avatar";	
}

# search users. for now only by nickname, sex, country
sub search_user {
	my ($self, $email, $sex, $country, $online)=@_;
	$email=~m/^([a-z0-9\_\-\.]+)\@([a-z0-9\_\-\.]+)$/i;
	my $cuser=$1;
	my $cdomain=$2;
	my $data='';
	$data.=pack("V",0x00000000)._to_lps($cuser).pack("V",0x00000001)._to_lps($cdomain) if ($email ne '');
	$data.=pack("V",0x00000005)._to_lps("$sex") if (($sex ne '')&&($sex ne '0'));
	$data.=pack("V",0x0000000F)._to_lps("$country") if ($country ne '');
	$data.=pack("V",0x00000009)._to_lps('1') if ($online == 1);
	$self->{_sock}->send(_make_mrim_packet($self,MRIM_CS_WP_REQUEST,$data));
	$self->{_seq_real}++;
	my ($msgrcv,$datarcv,$dlen)=_receive_data($self);
	return _analyze_received_data($self,$msgrcv,$datarcv,$dlen);
}

# and finally to disconnect
sub disconnect {
	my ($self)=@_;
	$self->{_sock}->close;
}

#### private methods below ####

# build the MRIM packet accordingly to the protocol specs
sub _make_mrim_packet {
	my ($self,$msg, $data) = @_;
	my ($magic, $proto, $seq, $from, $fromport) = (CS_MAGIC, PROTO_VERSION, $self->{_seq_real}, 0, 0);
	# actually, i'm not even sure this is needed...
	$seq=$self->{_last_seq} if ($msg==MRIM_CS_MESSAGE_RECV);
	my $dlen = 0;
	$dlen = length($data) if $data;
	my $mrim_packet = pack("V7", $magic, $proto, $seq, $msg, $dlen, $from, $fromport);
	$mrim_packet.=pack("C[16]",0);
	$mrim_packet .= $data if $data;
	printf("DEBUG [send packet]: MAGIC=$magic, PROTO=$proto, SEQ=$seq, TYP=0x%04x, LEN=$dlen\n",$msg) if ($self->{_debug});
	return $mrim_packet;
}

# retrieve a real host:port, as "mrim.mail.ru" can be several servers
# note that we connect on port 443, as this will always work for sure...
sub _get_host_port {	
	my $sock = new IO::Socket::INET (		
			PeerAddr  => 'mrim.mail.ru',		
			PeerPort  => 443,		
			PeerProto => 'tcp', 		
			TimeOut   => 10	);
	my $data="";
	$sock->recv($data, 18);	
	close $sock;	
	chomp $data;
   	return split /:/,  $data;	
}

# reading the data from server
sub _receive_data {
	my ($self)=@_;
	my $buffer="";
	my $data="";
	my $typ=0;
	print "DEBUG [recv packet]: waiting for header data\n" if ($self->{_debug});
	return (MRIM_CS_LOGOUT,"",0) if ((!($self->{_sock}))||(!$self->{_sock}->connected()));
	my $s = IO::Select->new();
	$s->add($self->{_sock});
	# check, since socket registration *could* fail
	return (MRIM_CS_LOGOUT,"",0) if (!defined($s->exists($self->{_sock})));
	my $dllen=0;
	# this stuff is to not wait for ever data from the server
	# note that we're mixing a bit unbuffered and buffered I/O, this is not 100% great	
	if ($s->can_read(int($self->{_ping_period}/$self->{_freq}))) {
		$self->{_sock}->recv($buffer,44);
		my ($magic, $proto, $seq, $msg, $dlen, $from, $fromport, $r1, $r2, $r3, $r4) = unpack ("V11", $buffer);
		use bytes;
		if (($seq>0)&&($seq<=$self->{_last_seq})&&($msg==$self->{_last_type})) {
			# this should work, but it doesn't. since i don't understand, better leave it deactivated.
			#return(-1,"",0);
		} else {
			$self->{_last_type}=$msg;
			$self->{_last_seq}=$seq if ($seq>0);
		}
		$self->{_sock}->recv($buffer,$dlen);
		$data=$buffer;
		$typ=$msg;
		$dllen=$dlen;
		# unfortunately "buffer I/O" isn't that buffered... 
		while (length($data)<$dlen) {
			$self->{_sock}->recv($buffer,$dlen-length($data));
			$data.=$buffer;
		}
		printf("DEBUG [recv packet]: MAGIC=$magic, PROTO=$proto, SEQ=$seq, LASTSEQ=$self->{_last_seq}, TYP=0x%04x, LEN=$dlen ".length($data)."\n",$msg) if ($self->{_debug});
	}
	return ($typ,$data,$dllen);	
}

# the packet analyzer
sub _analyze_received_data {
	my ($self,$msgrcv,$datarcv,$dlen)=@_;
	$dlen = 0 if (!defined($dlen));
	my $data=new Net::MRIM::Message();
	if (!defined($msgrcv)) {
		$data->set_logout_from_server();
	} elsif ($msgrcv==MRIM_CS_OFFLINE_MESSAGE_ACK) {
		my $msg='';
		my @datas=_from_mrim_us("s",substr($datarcv,8,-1));
		LINE: foreach my $msgline (split(/\n/,$datas[1])) {
			# some headers cleanup
			if ($msgline!~m/^(Boundary:|Version:|X-MRIM-Flags:|Subject:|\-\-)/) {
				$msg.=$msgline."\n";
			}
			# remove everything past the boundary
			elsif ($msgline=~m/^\-\-[0-9A-Z]+/) {
				last LINE;
			}
		}
		$data->set_message("OFFLINE",$self->{_login},$msg);
		$self->{_sock}->send(_make_mrim_packet($self,MRIM_CS_DELETE_OFFLINE_MESSAGE,substr($datarcv,0,8)));
	} elsif ($msgrcv==MRIM_CS_MESSAGE_ACK) {
		my @datas=_from_mrim_us("uusss",$datarcv);
		# below is a work-around: it seems that sometimes message_flag is left to 0...
		# as well, it seems the flags can be combined...
		# lastly, this flag was recently added, i don't know why...
		while ($datas[1]>=MESSAGE_FLAG_UNKOWN) {
			$datas[1]=$datas[1] - MESSAGE_FLAG_UNKOWN;
		}
		if (($datas[1]==MESSAGE_FLAG_NORECV)||($datas[1]==MESSAGE_FLAG_OFFLINE)) {
			$data->set_message($datas[2],$self->{_login},"".$datas[3]);
		} elsif (($datas[1]==0)||($datas[1]==MESSAGE_FLAG_RTF)) {
			$data->set_message($datas[2],$self->{_login},"".$datas[3]);
			$self->{_sock}->send(_make_mrim_packet($self,MRIM_CS_MESSAGE_RECV,_to_lps($datas[2]).pack("V",$datas[0])));
		} elsif (($datas[1]==MESSAGE_FLAG_NOTIFY)||($datas[1]==(MESSAGE_FLAG_NOTIFY+MESSAGE_FLAG_NORECV))) {
			$data->set_message($datas[2],$self->{_login},"pishu") if ($self->{_debug});
		} elsif (($datas[1]==MESSAGE_FLAG_AUTHORIZE)
			||($datas[1]==(MESSAGE_FLAG_AUTHORIZE+MESSAGE_FLAG_NORECV))
			||($datas[1]==(MESSAGE_FLAG_AUTHORIZE+MESSAGE_FLAG_OFFLINE))
			) {
			$data->set_server_msg($data->{TYPE_SERVER_AUTH_REQUEST},$self->{_login},$datas[3],$datas[2]);
		} elsif (($datas[1]==MESSAGE_FLAG_SYSTEM)
			||($datas[1]==(MESSAGE_FLAG_SYSTEM+MESSAGE_FLAG_NORECV))
			||($datas[1]==(MESSAGE_FLAG_SYSTEM+MESSAGE_FLAG_OFFLINE))
			) {
			$data->set_server_msg($data->{TYPE_SERVER_NOTIFY},$self->{_login},$datas[3]);
		} else {
			print "DEBUG: ack msg $datas[1] from $datas[2] text: $datas[3]\n" if ($self->{_debug});
		}
	} elsif ($msgrcv==MRIM_CS_LOGOUT) {
		$data->set_logout_from_server();
	} elsif ($msgrcv==MRIM_CS_MAILBOX_STATUS) {
		my @datas=_from_mrim_us("u",$datarcv);
		$data->set_server_msg($data->{TYPE_SERVER_NOTIFY},$self->{_login},"NEW_MAIL: ".$datas[0]);
	} elsif ($msgrcv==MRIM_CS_CONTACT_LIST2) {
		# S->C UL status, UL grp_nb, LPS grp_mask, LPS contacts_mask, grps, contacts
		my @datas=_from_mrim_us("uuss",$datarcv);
		my $nb_groups=$datas[1];
		my $gr_mask=$datas[2];
		my $ct_mask=$datas[3];
		print "DEBUG: found $datas[1] groups, $datas[2] gr mask, $datas[3] contact mask\n" if ($self->{_debug});
		$datarcv=$datas[4];
		my $groups={};
		for (my $i=0; $i<$nb_groups; $i++) {
			my ($grp_id,$grp_name)=(0,"");
			($grp_id,$grp_name,$datarcv)=_from_mrim_us($gr_mask,$datarcv);
			print "DEBUG: Found group $grp_name of id $grp_id\n" if ($self->{_debug});
			$groups->{$grp_id}=$grp_name;
		}
		my $contacts=$self->{_contacts};
		my $all_contacts=$self->{_all_contacts};
		my $i=scalar(keys(%{$all_contacts}))+1;
		$i=20 if ($i<10);
		my $clen=8+length($gr_mask)+length($ct_mask);
		while ((length($datarcv)>1)||($clen < $dlen)) {
			# TODO works only with current pattern uussuus . if it changes, will break...
			my ($flags,$group, $email, $name, $sflags, $status, $unk)=(0,"");
			($flags,$group, $email, $name, $sflags, $status, $unk, $datarcv)=_from_mrim_us($ct_mask,$datarcv);
			$name=~s/\n//g;
			print "DEBUG: Found contact $name of id $email flags $flags $sflags $status $group unknown: $unk clen $clen dlen $dlen\n" if ($self->{_debug});
			$name=$email if (length($name)<1);
			$status=STATUS_OFFLINE if (($flags==CONTACT_FLAG_REMOVED)||($flags==CONTACT_FLAG_SMS)||($flags==(CONTACT_FLAG_SMS|CONTACT_FLAG_REMOVED))); # to take care about SMS contacts, if any
			$contacts->{$email}=new Net::MRIM::Contact($email,$name,$status) if (($status != STATUS_OFFLINE)&&($status != STATUS_UNDETERMINED)&&(length($email)>1));
			$all_contacts->{$email}=$i;
			$clen=16+length($name)+length($email)+length($unk)+$clen;
			$datarcv="" if($clen>$dlen);
			$i++;
		}
		$self->{_contacts}=$contacts;
		$self->{_all_contacts}=$all_contacts;
		$self->{_groups}=$groups;
		$data->set_contact_list($groups,$contacts);
	} elsif (($msgrcv==MRIM_CS_USER_STATUS)||($msgrcv==MRIM_CS_AUTHORIZE_ACK)) {
		# if user changes status, or has accepted to be added to our list,
		# then we should update the contact list accordingly
		my @datas=();
		if ($msgrcv==MRIM_CS_USER_STATUS) {
			@datas=_from_mrim_us("us",$datarcv);
		} else {
			my @tmp=_from_mrim_us("s",$datarcv);
			@datas=(STATUS_ONLINE,$tmp[0]);
		}
		my $contacts=$self->{_contacts};
		my $all_contacts=$self->{_all_contacts};
		my $groups=$self->{_groups};
		my @ckeys=keys%{$contacts};
		my $i=scalar(keys(%{$all_contacts}))+1;
		$i=20 if ($i<10);
		if (($datas[0] != STATUS_OFFLINE)&&($datas[0] != STATUS_UNDETERMINED)) {
			$contacts->{$datas[1]}=new Net::MRIM::Contact($datas[1],$datas[1],$datas[0]);
			$all_contacts->{$datas[1]}=$i;
		} elsif (($datas[0] == STATUS_OFFLINE)&&(grep(/$datas[1]/,@ckeys))) {
			$contacts->{$datas[1]}=undef;
			$all_contacts->{$datas[1]}=undef;
		}
		$self->{_contacts}=$contacts;
		$self->{_all_contacts}=$all_contacts;
		$data->set_contact_list($groups,$contacts);
	} elsif (($msgrcv==MRIM_CS_ADD_CONTACT_ACK)||($msgrcv==MRIM_CS_MODIFY_CONTACT_ACK)) {
		# this is useless for now, as the contact list only stores online users
		my @datas=_from_mrim_us("uu",$datarcv.pack("V",0));
		print "DEBUG add_contact_ack: $datas[0] $datas[1]\n" if ($self->{_debug});
		$data->set_contact_list($self->{_groups},$self->{_contacts});
		$data->set_server_msg($data->{TYPE_SERVER_NOTIFY},$self->{_login},"CONTACT_OPER_ERROR: Error adding/removing contact") if ($datas[0] != CONTACT_OPER_SUCCESS);
	} elsif ($msgrcv==MRIM_CS_ANKETA_INFO) {
		my @datas=_from_mrim_us("uuuu",$datarcv);
		my $dataparse="";
		for (my $i=0; $i<$datas[1]; $i++) { $dataparse.='ss'; }
		my $fulldata="INFO\n";
		my $fentr=0;
		print "DEBUG anketa_info: found ".$datas[0].' '.$datas[1].' '.$datas[2].' '.$datas[3]." entries\n" if ($self->{_debug});
		while (($fentr<$datas[2])&&($fentr<50)) {
			@datas=_from_mrim_us("uuuu".$dataparse,$datarcv);
			# this flag will trace if a record was found
			my $found=1;
			for (my $i=4;$i<($datas[1]+4);$i++) {
				my $label=$datas[$i];
				my $value=$datas[($i+$datas[1])];
				my $entry.=_to_lps($value);
				# this is to remove the entry from received data, to allow "iteration" ammong values
				$entry=~s/(\W)/\\$1/g;
				$datarcv=~s/$entry//;
				if ($label eq 'Username') {
					$found=0 if ($value eq '');
					$fulldata.="User\t\t: $value\@" if ($found==1);
				} elsif ($label eq 'Domain') {
					$fulldata.=$value."\n" if ($found==1);
				} elsif ($label eq 'Sex') {
					if ($value eq '1') {
						$value='Male';
					} elsif ($value eq '2') {
						$value='Female';
					} else {
						$value='Unknown';
					}
					$fulldata.=$label."\t\t: ".$value."\n" if ($found==1);
				} else {			
					$fulldata.=$label."\t: ".$value."\n" if ($found==1);
				}
			}
			$fentr++;
			# this is the separator between two entries
			$fulldata.="----------------------------------------\n" if ($found==1);
		}
		print "DEBUG anketa_info: $fulldata\n" if ($self->{_debug});
		$data->set_server_msg($data->{TYPE_SERVER_ANKETA},$self->{_login},$fulldata);
	} elsif ($msgrcv==MRIM_CS_USER_INFO) {
		my @datas=_from_mrim_us("ssss",$datarcv);
		$data->set_server_msg($data->{TYPE_SERVER_NOTIFY},$self->{_login},"$datas[0]: $datas[1] | $datas[2]: $datas[3]");
	} elsif ($msgrcv==MRIM_CS_SMS_ACK) {
		my @datas=_from_mrim_us("u",$datarcv);
		# actually, MRIM seems to return always "1"... so i leave the outpout only for debug
		$data->set_message("DEBUG",$self->{_login},"SMS ACK: $datas[0]") if ($self->{_debug});
		# wild guess that "0" should mean "SUCCESS"
		$data->set_server_msg($data->{TYPE_SERVER_NOTIFY},$self->{_login},"SMS_SENT") if ($datas[0]==0);
	} else {
		$data->set_message("DEBUG",$self->{_login},$datarcv) if ($self->{_debug});
	}
	return $data;
}

# this is to decode mrim's combination of ulong and lps that is sent as message data
sub _from_mrim_us {
	my ($pattern,$data)=@_;
	my @res=();
	for (my $i=0;$i<length($pattern);$i++) {
		my $datatype=substr($pattern,$i,1);
		if ($datatype eq 'u') {
			if ( $data=~m/^(\C{4})(\C*)/) {
				my $item=unpack("V",$1);
				$data=$2;
				push @res,$item;
			} else {
				push @res,0;
			}
		} elsif ($datatype eq 's') {
			$data=~m/^(\C{4})(\C*)/s;
			my $itemlength=$1;
			if ($itemlength) {
				$data=$2;
				$itemlength=unpack("V",$itemlength);
				if ($itemlength<4096) {
					$data=~m/^(\C{$itemlength})(\C*)/;
					my $item=$1;
					$data=$2;
					push @res,$item;
				} else {
					$data=~s/^\0//;
					push @res, "";
				}
			} else {
				push @res, "";
			}
		}
	}
	push @res,$data;
	return @res;
}

# convert to LPS (read the protocol !)
sub _to_lps {
	my ($str)=@_;
	return pack("V",length($str)).$str;
}

1;