package Net::IMAP::Server::Command; use warnings; use strict; use bytes; use base 'Class::Accessor'; use Regexp::Common qw/delimited balanced/; __PACKAGE__->mk_accessors( qw(server connection command_id options_str command _parsed_options _literals _pending_literal) ); =head1 NAME Net::IMAP::Server::Command - A command in the IMAP server =head1 DESCRIPTION Commands the IMAP server knows about should be subclasses of this. They will want to override the L and L methods. =head1 METHODS =head2 new Called by the connection to create a new command. =cut sub new { my $class = shift; my $self = $class->SUPER::new(@_); $self->_parsed_options( [] ); $self->_literals( [] ); return $self; } =head2 server Gets or sets the L associated with this command. =cut =head2 connection Gets or sets the L associated with this command. =cut =head2 validate Called before the command is run. If it returns a false value, the command is not run; it will probably want to inspect L. If C returns a false value, it is responsible for calling L or L to notify the client of the failure. Handily, these return a false value. =cut sub validate { return 1; } =head2 run Does the guts of the command. The return value is ignored; the command is in charge of eventually sending one of L, L, or L to the client. The default implementation simply always response with L. =cut sub run { my $self = shift; $self->bad_command( "command '" . $self->command . "' not recognized" ); } =head2 has_literal Analyzes the options line, and returns true if the line has literals (as defined in the FRC, a literal is of the form C<{42}>). If the line has literals, installs a L callback to continue the parsing, and returns true. =cut sub has_literal { my $self = shift; unless ( $self->options_str =~ /\{(\d+)(\+)?\}[\r\n]*$/ ) { $self->parse_options; return; } my $options = $self->options_str; my $next = $#{ $self->_literals } + 1; $options =~ s/\{(\d+)(\+)?\}[\r\n]*$/{{$next}}/; $self->_pending_literal($1); $self->options_str($options); # Pending $self->connection->pending( sub { my $content = shift; if ( length $content <= $self->_pending_literal ) { $self->_literals->[$next] .= $content; $self->_pending_literal( $self->_pending_literal - length $content ); } else { $self->_literals->[$next] .= substr( $content, 0, $self->_pending_literal, "" ); $self->connection->pending(undef); $self->options_str( $self->options_str . $content ); return if $self->has_literal; $self->run if $self->validate; } } ); $self->out("+ Continue") unless $2; return 1; } =head2 parse_options Parses the options, and puts the results (which may be a data structure) into L. =cut sub parse_options { my $self = shift; my $str = shift; return $self->_parsed_options if not defined $str and not defined $self->options_str; my @parsed; for my $term ( grep {/\S/} split /($RE{delimited}{-delim=>'"'}{-esc=>'\\'}|$RE{balanced}{-parens=>'()'}|\S+$RE{balanced}{-parens=>'()[]<>'}|\S+)/, defined $str ? $str : $self->options_str ) { if ( $term =~ /^$RE{delimited}{-delim=>'"'}{-esc=>'\\'}{-keep}$/ ) { my $value = $3; $value =~ s/\\([\\"])/$1/g; push @parsed, $value; } elsif ( $term =~ /^$RE{balanced}{-parens=>'()'}$/ ) { $term =~ s/^\((.*)\)$/$1/; push @parsed, [ $self->parse_options($term) ]; } elsif ( $term =~ /^\{\{(\d+)\}\}$/ ) { push @parsed, $self->_literals->[$1]; } else { push @parsed, $term; } } return @parsed if defined $str; $self->options_str(undef); $self->_parsed_options( [ @{ $self->_parsed_options }, @parsed ] ); } =head2 command_id Returns the (arbitrary) string that the client identified the command with. =cut =head2 parsed_options Returns the list of options to the command. =cut sub parsed_options { my $self = shift; return @{ $self->_parsed_options(@_) }; } =head2 options_str Returns the flat string represetnation of the options the client gave. =cut =head2 data_out DATA Returns a string representing the most probable IMAP string that conveys the C. =over =item * Array references are converted into "paranthesized lists," and each element is recursively output. =item * Scalar references are dereferenced and returned as-is. =item * C is output as C. =item * Scalar values containing special characters are output as literals =item * Purely numerical scalra values are output with no change =item * All other scalar values are ouput within quotes. =back Since the IMAP specification contains nothing which is similar to a hash, hash references are treated specially; specifically, the C key is taken to be how the C key should be output. Options for C are C or C. =cut sub data_out { my $self = shift; my $data = shift; if ( ref $data eq "ARRAY" ) { return "(" . join( " ", map { $self->data_out($_) } @{$data} ) . ")"; } elsif ( ref $data eq "SCALAR" ) { return $$data; } elsif ( ref $data eq "HASH" ) { if ( $data->{type} eq "string" ) { if ( $data =~ /[{"\r\n%*\\\[]/ ) { return "{" . ( length( $data->{value} ) ) . "}\r\n$data"; } else { return '"' . $data->{value} . '"'; } } elsif ( $data->{type} eq "literal" ) { return "{" . ( length( $data->{value} ) ) . "}\r\n$data"; } } elsif ( not ref $data ) { if ( not defined $data ) { return "NIL"; } elsif ( $data =~ /[{"\r\n%*\\\[]/ ) { return "{" . ( length($data) ) . "}\r\n$data"; } elsif ( $data =~ /^\d+$/ ) { return $data; } else { return qq{"$data"}; } } return ""; } =head2 untagged_response STRING Sends an untagged response to the client. =cut sub untagged_response { my $self = shift; $self->connection->untagged_response(@_); } =head2 tagged_response Sends a tagged response to the client. =cut sub tagged_response { my $self = shift; $self->untagged_response( uc( $self->command ) . " $_" ) for grep defined, @_; } =head2 poll_after Returns a true value if the command should send untagged updates abou tthe selected mailbox after the command completes. Defaults to always true. =cut sub poll_after {1} =head2 send_untagged Sends untagged updates about the currently selected inbox to the client using L, but only if the command has a true L. =cut sub send_untagged { my $self = shift; $self->connection->send_untagged(@_) if $self->poll_after; } =head2 ok_command MESSAGE [, RESPONSECODE => STRING, ...] Sends untagged OK responses for any C pairs, then outputs untagged messages via L, then sends a tagged OK with the given C. =cut sub ok_command { my $self = shift; my $message = shift; my %extra_responses = (@_); for ( keys %extra_responses ) { $self->untagged_response( "OK [" . uc($_) . "] " . $extra_responses{$_} ); } $self->send_untagged; $self->out( $self->command_id . " OK $message" ); return 1; } =head2 ok_completed [RESPONSECODE => STRING] Sends an C tagged response to the client. =cut sub ok_completed { my $self = shift; my %extra_responses = (@_); $self->ok_command( uc( $self->command ) . " COMPLETED", %extra_responses ); } =head2 no_command MESSAGE [, RESPONSECODE => STRING, ...] Sends untagged NO responses for any C pairs, then outputs untagged messages via L, then sends a tagged OK with the given C. =cut sub no_command { my $self = shift; my $message = shift; my %extra_responses = (@_); for ( keys %extra_responses ) { $self->untagged_response( "NO [" . uc($_) . "] " . $extra_responses{$_} ); } $self->out( $self->command_id . " NO $message" ); return 0; } =head2 bad_command REASON Sends any untagged updates to the client using L, then sents a tagged C response with the given C. =cut sub bad_command { my $self = shift; my $reason = shift; $self->send_untagged; $self->out( $self->command_id . " BAD $reason" ); return 0; } =head2 log MESSAGE Identical to L. =cut sub log { my $self = shift; $self->connection->log(@_); } =head2 out MESSAGE Identical to L. =cut sub out { my $self = shift; $self->connection->out(@_); } 1;