# $Id$

package Qpsmtpd::ConfigServer;

use base ('Danga::Client');
use Qpsmtpd::Constants;

use strict;

use fields qw(
    _auth
    _commands
    _config_cache
    _connection
    _transaction
    _test_mode
    _extras
    other_fds
);

my $PROMPT = "Enter command: ";

sub new {
    my Qpsmtpd::ConfigServer $self = shift;
    
    $self = fields::new($self) unless ref $self;
    $self->SUPER::new( @_ );
    $self->write($PROMPT);
    return $self;
}

sub max_idle_time { 3600 } # one hour

sub process_line {
    my $self = shift;
    my $line = shift || return;
    if ($::DEBUG > 1) { print "$$:".($self+0)."C($self->{mode}): $line"; }
    local $SIG{ALRM} = sub {
        my ($pkg, $file, $line) = caller();
        die "ALARM: $pkg, $file, $line";
    };
    my $prev = alarm(2); # must process a command in < 2 seconds
    my $resp = eval { $self->_process_line($line) };
    alarm($prev);
    if ($@) {
        print STDERR "Error: $@\n";
    }
    return $resp || '';
}

sub respond {
    my $self = shift;
    my (@messages) = @_;
    while (my $msg = shift @messages) {
        $self->write("$msg\r\n");
    }
    return;
}

sub fault {
  my $self = shift;
  my ($msg) = shift || "program fault - command not performed";
  print STDERR "$0 [$$]: $msg ($!)\n";
  $self->respond("Error - " . $msg);
  return $PROMPT;
}

sub _process_line {
    my $self = shift;
    my $line = shift;

    $line =~ s/\r?\n//;
    my ($cmd, @params) = split(/ +/, $line);
    my $meth = "cmd_" . lc($cmd);
    if (my $lookup = $self->can($meth)) {
        my $resp = eval {
            $lookup->($self, @params);
        };
        if ($@) {
            my $error = $@;
            chomp($error);
            Qpsmtpd->log(LOGERROR, "Command Error: $error");
            return $self->fault("command '$cmd' failed unexpectedly");
        }
        return "$resp\n$PROMPT";
    }
    else {
        # No such method - i.e. unrecognized command
        return $self->fault("command '$cmd' unrecognised");
    }
}

my %helptext = (
    help => "HELP [CMD] - Get help on all commands or a specific command",
    status => "STATUS - Returns status information about current connections",
    list => "LIST [LIMIT] - List the connections, specify limit or negative limit to shrink list",
    kill => "KILL (\$IP | \$REF) - Disconnect all connections from \$IP or connection reference \$REF",
    pause => "PAUSE - Stop accepting new connections",
    continue => "CONTINUE - Resume accepting connections",
    reload => "RELOAD - Reload all plugins and config",
    quit => "QUIT - Exit the config server",
    );

sub cmd_help {
    my $self = shift;
    my ($subcmd) = @_;
    
    $subcmd ||= 'help';
    $subcmd = lc($subcmd);
    
    if ($subcmd eq 'help') {
        my $txt = join("\n", map { substr($_, 0, index($_, "-")) } sort values(%helptext));
        return "Available Commands:\n\n$txt\n";
    }
    my $txt = $helptext{$subcmd} || "Unrecognised help option. Try 'help' for a full list.";
    return "$txt\n";
}

sub cmd_quit {
    my $self = shift;
    $self->close;
}

sub cmd_shutdown {
    exit;
}

sub cmd_pause {
    my $self = shift;
    
    my $other_fds = $self->OtherFds;
    
    $self->{other_fds} = { %$other_fds };
    %$other_fds = ();
    return "PAUSED";
}

sub cmd_continue {
    my $self = shift;
    
    my $other_fds = $self->{other_fds};
    
    $self->OtherFds( %$other_fds );
    %$other_fds = ();
    return "UNPAUSED";
}

sub cmd_status {
    my $self = shift;
 
# Status should show:
#  - Total time running
#  - Total number of mails received
#  - Total number of mails rejected (5xx)
#  - Total number of mails tempfailed (5xx)
#  - Avg number of mails/minute
#  - Number of current connections
#  - Number of outstanding DNS queries
    
    my $output = "Current Status as of " . gmtime() . " GMT\n\n";
    
    if (defined &Qpsmtpd::Plugin::stats::get_stats) {
        # Stats plugin is loaded
        $output .= Qpsmtpd::Plugin::stats->get_stats;
    }
    
    my $descriptors = Danga::Socket->DescriptorMap;
    
    my $current_connections = 0;
    my $current_dns = 0;
    foreach my $fd (keys %$descriptors) {
        my $pob = $descriptors->{$fd};
        if ($pob->isa("Qpsmtpd::PollServer")) {
            $current_connections++;
        }
        elsif ($pob->isa("ParaDNS::Resolver")) {
            $current_dns = $pob->pending;
        }
    }
    
    $output .= "Curr Connections: $current_connections / $::MAXconn\n".
               "Curr DNS Queries: $current_dns";
    
    return $output;
}

sub cmd_list {
    my $self = shift;
    my ($count) = @_;
    
    my $descriptors = Danga::Socket->DescriptorMap;
    
    my $list = "Current" . ($count ? (($count > 0) ? " Oldest $count" : " Newest ".-$count) : "") . " Connections: \n\n";
    my @all;
    foreach my $fd (keys %$descriptors) {
        my $pob = $descriptors->{$fd};
        if ($pob->isa("Qpsmtpd::PollServer")) {
            next unless $pob->connection->remote_ip; # haven't even started yet
            push @all, [$pob+0, $pob->connection->remote_ip,
                        $pob->connection->remote_host, $pob->uptime];
        }
    }
    
    @all = sort { $a->[3] <=> $b->[3] } @all;
    if ($count) {
        if ($count > 0) {
            @all = @all[$#all-($count-1) .. $#all];
        }
        else {
            @all = @all[0..(abs($count) - 1)];
        }
    }
    foreach my $item (@all) {
        $list .= sprintf("%x : %s [%s] Connected %0.2fs\n", map { defined()?$_:'' } @$item);
    }
    
    return $list;
}

sub cmd_kill {
    my $self = shift;
    my ($match) = @_;
    
    return "SYNTAX: KILL (\$IP | \$REF)\n" unless $match;
    
    my $descriptors = Danga::Socket->DescriptorMap;
    
    my $killed = 0;
    my $is_ip = (index($match, '.') >= 0);
    foreach my $fd (keys %$descriptors) {
        my $pob = $descriptors->{$fd};
        if ($pob->isa("Qpsmtpd::PollServer")) {
            if ($is_ip) {
                next unless $pob->connection->remote_ip; # haven't even started yet
                if ($pob->connection->remote_ip eq $match) {
                    $pob->write("550 Your connection has been killed by an administrator\r\n");
                    $pob->disconnect;
                    $killed++;
                }
            }
            else {
                # match by ID
                if ($pob+0 == hex($match)) {
                    $pob->write("550 Your connection has been killed by an administrator\r\n");
                    $pob->disconnect;
                    $killed++;
                }
            }
        }
    }
    
    return "Killed $killed connection" . ($killed > 1 ? "s" : "") . "\n";
}

sub cmd_dump {
    my $self = shift;
    my ($ref) = @_;
    
    return "SYNTAX: DUMP \$REF\n" unless $ref;
    require Data::Dumper;
    $Data::Dumper::Indent=1;
    
    my $descriptors = Danga::Socket->DescriptorMap;
    foreach my $fd (keys %$descriptors) {
        my $pob = $descriptors->{$fd};
        if ($pob->isa("Qpsmtpd::PollServer")) {
            if ($pob+0 == hex($ref)) {
                return Data::Dumper::Dumper($pob);
            }
        }
    }
    
    return "Unable to find the connection: $ref. Try the LIST command\n";
}

1;
__END__

=head1 NAME

Qpsmtpd::ConfigServer - a configuration server for qpsmtpd

=head1 DESCRIPTION

When qpsmtpd runs in multiplex mode it also provides a config server that you
can connect to. This allows you to view current connection statistics and other
gumph that you probably don't care about.

=cut