#!/usr/local/bin/perl # # Copyright (c) 1997-1998 Kevin Johnson . # # All rights reserved. This program is free software; you can # redistribute it and/or modify it under the same terms as Perl # itself. # # $Id: xAP.pm,v 1.1 1998/04/05 15:40:46 kjj Exp $ require 5.004; package Net::xAP; use strict; =head1 NAME Net::xAP - An interface to the protocol beneath IMAP, ACAP, and ICAP. B =head1 SYNOPSIS C =head1 DESCRIPTION This base class implements the protocol that is common across the IMAP, ACAP, ICAP protocols. It provides the majority of the interface to the network calls and implements a small amount of glue to assist in implementing interfaces to this protocol family. =head1 METHODS =cut use Exporter (); use IO::Socket; use Carp; use vars qw(@EXPORT_OK); @EXPORT_OK = qw($xAP_ATOM $xAP_ASTRING $xAP_PARENS $xAP_STRING); use vars qw($VERSION $SEQUENCE @ISA @EXPORT_OK $xAP_ATOM $xAP_ASTRING $xAP_PARENS $xAP_STRING); @ISA = qw(Exporter); $VERSION = '0.01'; $SEQUENCE = 0; my $Debug = 0; $xAP_ATOM = 0; $xAP_ASTRING = 1; $xAP_PARENS = 2; $xAP_STRING = 3; =head1 METHODS =head2 new ($host, $peerport [, %options]) Create a new instance of Net::xAP and returns a reference to the object. The C<$host> parameter is the name of the host to contact. The C<$peerport> parameter is the tcp port to connect to. The parameter should be in the syntax understood by Cnew>). C<%options> specifies any options to use. Currently, the only option that C uses is C. All of the options are passed to a call to Cnew>. =cut sub new { my $class = shift; my $type = ref($class) || $class; my $host = shift; my $peerport = shift; my %options = @_; $options{Debug} ||= $Debug; my $self = bless {}, $class; $self->{Options} = {%options}; $self->{Connection} = IO::Socket::INET->new(PeerAddr => $host, PeerPort => $peerport, Proto => 'tcp', %options) or return undef; $self->{Connection}->autoflush(1); $self->{Pending} = (); $self->{Sequence} = 0; return $self; } =head2 command ($callback, $command [, @args]) The C is used to send commands to the connected server and to setup callbacks for subsequent use by the C method. The C<$callback> parameter should be a reference to a subroutine that will be called when input is received. This callback is responsible for processing any of the responses from the server that pertain the given command. C<@args> is a list of C<$type>-C<$value> pairs. The C<$type> says what type of data type to use for C<$value>. The mechanism is used to control the encoding necessary to pass the command arguments to the server. The following C<$type>s are understood: =over 2 =item * $xAP_ATOM The data will sent raw to the server. =item * $xAP_ASTRING The data will be sent to the server as an atom, a quoted string, or a literal depending on the content of C<$value>. =item * $xAP_PARENS The data in C<$value> will be interpreted as an array reference and be sent inside a pair of parentheses. =item * $xAP_STRING The data will be sent to the server as either a quoted string or literal depending on the content of C<$value>. =back =cut sub command { my $self = shift; my $callback = shift; my $cmd = shift; return undef unless ($#_ % 2); # TODO: need an error msg here $self->{Sequence}++; my $str = "$self->{Sequence} $cmd"; while (my ($type, $value) = splice @_, 0, 2) { $str .= ' '; if (($type == $xAP_ASTRING) || ($type == $xAP_STRING)){ my $astring = ($type == $xAP_ASTRING) ? $self->as_astring($value) : $self->as_string($value); if (ref($astring) eq 'ARRAY') { $str .= "{" . $astring->[0] . "}"; push @{$self->{PendingLiterals}}, $astring; } else { $str .= $astring; } } elsif ($type == $xAP_ATOM) { # maybe should check for non-ATOMCHARs $str .= $value; } elsif ($type == $xAP_PARENS) { $str .= '(' . join(' ', @{$value}) . ')'; } else { croak "unknown argument type: $type"; } } return undef unless (($str eq '') || $self->send_command($str)); $self->{LastCmdTime} = time; $self->{Pending}{$self->{Sequence}} = $callback; return $self->response if (defined($self->{Options}{Synchronous}) && $self->{Options}{Synchronous}); return $self->{Sequence}; } =head2 parse_line =cut sub parse_line { my $self = shift; my $str = shift; my @list; my @stack = ([]); my $pos = 0; my $len = length($str); while ($pos < $len) { my $c = substr($str, $pos, 1); if ($c eq ' ') { $pos++; } elsif ($c eq '(') { push @{$stack[-1]}, []; push @stack, $stack[-1]->[-1]; $pos++; } elsif ($c eq ')') { pop(@stack); $pos++; } elsif (substr($str, $pos) =~ /^(\"(?:[^\\\"]|\\\")*\")[\s\)]?/) { # qstring push @{$stack[-1]}, $1; $pos += length $1; } elsif (substr($str, $pos) =~ /^\{(\d+)\}/) { # literal $pos += length($1) + 2; # soak up the literal payload push @{$stack[-1]}, substr($str, $pos, $1); $pos += $1; } elsif (substr($str, $pos) =~ /^([^\x00-\x1f\x7f\(\)\{\s\"]+)[\s\)]?/) { # atom push @{$stack[-1]}, $1; $pos += length $1; } else { croak "parse_line: eeeek! bad parse at position $pos [$str]\n"; } } return @{$stack[0]}; } =head2 as_astring =cut sub as_astring { my $self = shift; my $str = shift; my $type = 0; my $len = length $str; if (($str =~ /[\x00\x0a\x0d\"\\\x80-\xff]/) || ($len > 1024)) { # literal return [($len, $str)]; } elsif ($str =~ /[\x01-\x20\x22\x25\x28-\x2a\{]/) { # qstring return "\"$str\""; } elsif ($str eq '') { return '""'; } else { return $str; } } =head2 as_string =cut sub as_string { my $self = shift; my $str = shift; my $type = 0; my $len = length $str; if (($str =~ /[\x00\x0a\x0d\"\\\x80-\xff]/) || ($len > 1024)) { # literal return [($len, $str)]; } elsif ($str eq '') { return '""'; } else { return "\"$str\""; } } =head2 send_command =cut sub send_command { my $self = shift; my $str = shift; my $len = length $str; $self->debug_print(1, $str) if $self->debug; (($self->{Connection}->syswrite($str, $len) == $len) && ($self->{Connection}->syswrite("\r\n", 2) == 2)) or return undef; } =head2 response =cut sub response { my $self = shift; # Currently returns undef if there's nothing pending. This isn't the # technically correct thing to do, but it's probably ok for now. # At some point, it should do a select on the socket and reap # unsolicited responses if any are present and pass them through # default_callback. return undef unless scalar keys %{$self->{Pending}}; my $response; while (1) { my $list = $self->getline; $self->debug_print(0, join(' ', @{$list})) if $self->debug; my $found_one = 0; if ($list->[0] eq '+') { my $lit = pop(@{$self->{PendingLiterals}}); (($self->{Connection}->syswrite($lit->[1], $lit->[0]) == $lit->[0]) && ($self->{Connection}->syswrite("\r\n", 2) == 2)) or croak "eek! can't send literal payload"; $found_one++; } else { # rifle through the callbacks of the pending commands and ask each # of them if the resposne belongs to them. If it does, then stop # looking for a match. for my $seq (sort { $a <=> $b } keys %{$self->{Pending}}) { my $ret = &{$self->{Pending}{$seq}}($list); if ($ret == 0) { # callback didn't claim it } elsif ($ret < 0) { # maybe need to call an error callback or something... } else { # the callback returned an object $found_one++; $self->debug_print(0, "callback $seq") if $self->debug; $response = $ret; # TODO: we should check for an actual object last; } } } # if none of the pending command callbacks claimed the response then # pass it to a default callback. if (!$found_one && defined($self->{DefaultCallback})) { if (&{$self->{DefaultCallback}}($list)) { $self->debug_print(0, "default callback") if $self->debug; } else { carp "response not claimed by a callback: [", join(' ', @{$list}), "]"; } } last if (($list->[0] =~ /^\d+$/) && ($list->[1] =~ /^OK|NO|BAD$/i)); } my $tag = $response->tag; delete $self->{Pending}{$tag} if defined($self->{Pending}{$tag}); return $response; } =head2 getline Gets one line of data from the server, parses it into a list of fields and returns a reference to the list. C uses the C method to do the parsing. =cut sub getline { my $self = shift; my @list; my $pstr; while (1) { my $str = $self->{Connection}->getline or return undef; $str =~ s/\r?\n$//; # push @list, $self->parse_line($str); if ($str =~ /\{(\d+)\}$/) { # if it's a literal then read in the payload and replace the {\d+} # with the payload. my $amt = $1; my $morestr; $self->{Connection}->read($morestr, $amt) == $amt or return undef; $str .= $morestr; # $list[-1] = $morestr; $pstr .= $str; } else { $pstr .= $str; last; } } push @list, $self->parse_line($pstr); return [@list]; } =head2 last_command_time Return what time the most recent command was sent to the server. It returns value as a C