#!/usr/bin/perl

use lib "./lib";

BEGIN {
    delete $ENV{ENV};
    delete $ENV{BASH_ENV};
    $ENV{PATH} = '/bin:/usr/bin:/var/qmail/bin:/usr/local/bin';
}

# Profiling - requires Devel::Profiler 0.05
#BEGIN { $Devel::Profiler::NO_INIT = 1; }
#use Devel::Profiler;

use strict;
use vars qw($DEBUG);
use FindBin qw();

# TODO: need to make this taint friendly
use lib "$FindBin::Bin/lib";
use Danga::Socket;
use Danga::Client;
use Qpsmtpd::PollServer;
use Qpsmtpd::ConfigServer;
use Qpsmtpd::Constants;
use IO::Socket;
use Carp;
use POSIX qw(WNOHANG);
use Getopt::Long;
use List::Util qw(shuffle);

$|++;

use Socket
  qw(SOMAXCONN IPPROTO_TCP SO_KEEPALIVE TCP_NODELAY SOL_SOCKET AF_UNIX SOCK_STREAM PF_UNSPEC);

$SIG{'PIPE'} = "IGNORE";    # handled manually

$DEBUG = 0;

my $CONFIG_PORT      = 20025;
my $CONFIG_LOCALADDR = '127.0.0.1';

my $PORT      = 2525;
my $LOCALADDR = '0.0.0.0';
my $PROCS     = 1;
my $USER      = (getpwuid $>)[0];    # user to suid to
$USER = "smtpd" if $USER eq "root";
my $PAUSED    = 0;
my $NUMACCEPT = 20;
my $PID_FILE  = '';
my $ACCEPT_RSET;
my $DETACH;                          # daemonize on startup

# make sure we don't spend forever doing accept()
use constant ACCEPT_MAX => 1000;

sub reset_num_accept {
    $NUMACCEPT = 20;
}

sub help {
    print <<EOT;
Usage:
    qpsmtpd [OPTIONS]

Options:
 -l, --listen-address addr : listen on a specific address; default 0.0.0.0
 -p, --port P              : listen on a specific port; default 2525
 --config-port             : config server port; default 20025
 -u, --user U              : run as a particular user; defualt 'smtpd'
 -j, --procs J             : spawn J processes; default 1
 -d, --detach              : detach from controlling terminal (daemonize)
     --pid-file P          : print main servers PID to file P
 
 -h, --help                : this page
     --use-poll            : force use of poll() instead of epoll()/kqueue()
EOT
    exit(0);
}

GetOptions(
           'p|port=i'           => \$PORT,
           'l|listen-address=s' => \$LOCALADDR,
           'j|procs=i'          => \$PROCS,
           'v|verbose+'         => \$DEBUG,
           'u|user=s'           => \$USER,
           'pid-file=s'         => \$PID_FILE,
           'd|detach'           => \$DETACH,
           'h|help'             => \&help,
           'config-port=i'      => \$CONFIG_PORT,
          )
  || help();

# detaint the commandline
if ($PORT =~ /^(\d+)$/) { $PORT = $1 }
else                    { &help }
if ($LOCALADDR =~ /^([\d\w\-.]+)$/) { $LOCALADDR = $1 }
else                                { &help }
if ($USER =~ /^([\w\-]+)$/) { $USER = $1 }
else                        { &help }
if ($PROCS =~ /^(\d+)$/) { $PROCS = $1 }
else                     { &help }

sub force_poll {
    $Danga::Socket::HaveEpoll  = 0;
    $Danga::Socket::HaveKQueue = 0;
}

my $POLL = "with "
  . (
       $Danga::Socket::HaveEpoll  ? "epoll()"
     : $Danga::Socket::HaveKQueue ? "kqueue()"
     :                              "poll()"
    );

my $SERVER;
my $CONFIG_SERVER;

use constant READY      => 1;
use constant ACCEPTING  => 2;
use constant RESTARTING => 999;

my %childstatus = ();

if ($PID_FILE && -r $PID_FILE) {
    open PID, "<$PID_FILE"
      or die "open_pidfile $PID_FILE: $!\n";
    my $running_pid = <PID> || '';
    chomp $running_pid;
    if ($running_pid =~ /^(\d+)/) {
        if (kill 0, $running_pid) {
            die "Found an already running qpsmtpd with pid $running_pid.\n";
        }
    }
    close(PID);
}

run_as_server();
exit(0);

sub _fork {
    my $pid = fork;
    if (!defined($pid)) { die "Cannot fork: $!" }
    return $pid if $pid;

    # Fixup Net::DNS randomness after fork
    srand($$ ^ time);

    local $^W;
    delete $INC{'Net/DNS/Header.pm'};
    require Net::DNS::Header;

    # cope with different versions of Net::DNS
    eval {
        $Net::DNS::Resolver::global{id} = 1;
        $Net::DNS::Resolver::global{id} =
          int(rand(Net::DNS::Resolver::MAX_ID()));

        # print "Next DNS ID: $Net::DNS::Resolver::global{id}\n";
    };
    if ($@) {

        # print "Next DNS ID: " . Net::DNS::Header::nextid() . "\n";
    }

    # Fixup lost kqueue after fork
    $Danga::Socket::HaveKQueue = undef;
}

sub spawn_child {
    my $plugin_loader = shift || Qpsmtpd::SMTP->new;

    socketpair(my $reader, my $writer, AF_UNIX, SOCK_STREAM, PF_UNSPEC)
      || die "Unable to create a pipe";
    $writer->autoflush(1);
    $reader->autoflush(1);

    if (my $pid = _fork) {
        $childstatus{$pid} = $writer;
        return $pid;
    }

    $SIG{CHLD} = $SIG{INT} = $SIG{TERM} = 'DEFAULT';
    $SIG{PIPE} = 'IGNORE';
    $SIG{HUP}  = 'IGNORE';

    close $CONFIG_SERVER;

    Qpsmtpd::PollServer->Reset;

    Qpsmtpd::PollServer->OtherFds(
                            fileno($reader) => sub { command_handler($reader) },
                            fileno($SERVER) => \&accept_handler,);

    $ACCEPT_RSET = Danga::Socket->AddTimer(30, \&reset_num_accept);

    $plugin_loader->run_hooks('post-fork');

    Qpsmtpd::PollServer->EventLoop();
    exit;
}

# Note this is broken on KQueue because it requires that it handle signals itself or it breaks the event loop.
sub sig_hup {
    for my $writer (values %childstatus) {
        print $writer "hup\n";
    }
}

sub sig_chld {
    my $spawn_count = 0;
    while ((my $child = waitpid(-1, WNOHANG)) > 0) {
        if (!defined $childstatus{$child}) {
            next;
        }

        last unless $child > 0;
        print "SIGCHLD: child $child died\n";
        delete $childstatus{$child};
        $spawn_count++;
    }
    if ($spawn_count) {
        for (1 .. $spawn_count) {

            # restart a new child if in poll server mode
            my $pid = spawn_child();
        }
    }
    $SIG{CHLD} = \&sig_chld;
}

sub HUNTSMAN {
    $SIG{CHLD} = 'DEFAULT';
    kill 'INT' => keys %childstatus;
    if ($PID_FILE && -e $PID_FILE) {
        unlink $PID_FILE or ::log(LOGERROR, "unlink: $PID_FILE: $!");
    }
    exit(0);
}

sub run_as_server {

    # establish SERVER socket, bind and listen.
    $SERVER = IO::Socket::INET->new(
                                    LocalPort => $PORT,
                                    LocalAddr => $LOCALADDR,
                                    Type      => SOCK_STREAM,
                                    Proto     => IPPROTO_TCP,
                                    Blocking  => 0,
                                    Reuse     => 1,
                                    Listen    => SOMAXCONN
                                   )
      or die "Error creating server $LOCALADDR:$PORT : $@\n";

    IO::Handle::blocking($SERVER, 0);
    binmode($SERVER, ':raw');

    $CONFIG_SERVER =
      IO::Socket::INET->new(
                            LocalPort => $CONFIG_PORT,
                            LocalAddr => $CONFIG_LOCALADDR,
                            Type      => SOCK_STREAM,
                            Proto     => IPPROTO_TCP,
                            Blocking  => 0,
                            Reuse     => 1,
                            Listen    => 1
                           )
      or die "Error creating server $CONFIG_LOCALADDR:$CONFIG_PORT : $@\n";

    IO::Handle::blocking($CONFIG_SERVER, 0);
    binmode($CONFIG_SERVER, ':raw');

    # Drop priviledges
    my (undef, undef, $quid, $qgid) = getpwnam $USER
      or die "unable to determine uid/gid for $USER\n";
    my $groups = "$qgid $qgid";
    while (my (undef, undef, $gid, $members) = getgrent) {
        my @m = split(/ /, $members);
        if (grep { $_ eq $USER } @m) {
            $groups .= " $gid";
        }
    }
    endgrent;
    $) = $groups;
    POSIX::setgid($qgid)
      or die "unable to change gid: $!\n";
    POSIX::setuid($quid)
      or die "unable to change uid: $!\n";
    $> = $quid;

    # Load plugins here
    my $plugin_loader = Qpsmtpd::SMTP->new();
    $plugin_loader->load_plugins;

    if ($DETACH) {
        open STDIN,  '/dev/null'  or die "/dev/null: $!";
        open STDOUT, '>/dev/null' or die "/dev/null: $!";
        open STDERR, '>&STDOUT'   or die "open(stderr): $!";
        defined(my $pid = fork) or die "fork: $!";
        exit 0 if $pid;
        POSIX::setsid or die "setsid: $!";
    }

    if ($PID_FILE) {
        open PID, ">$PID_FILE" || die "$PID_FILE: $!";
        print PID $$, "\n";
        close PID;
    }

    $plugin_loader->log(LOGINFO,
                            'Running as user '
                          . (getpwuid($>) || $>)
                          . ', group '
                          . (getgrgid($)) || $))
                       );

    $SIG{INT} = $SIG{TERM} = \&HUNTSMAN;

######################
    # more Profiling code

=pod
    $plugin_loader->run_hooks('post-fork');

    Devel::Profiler->set_options(
        bad_subs => [qw(Danga::Socket::EventLoop)],
        sub_filter => sub {
                        my ($pkg, $sub) = @_;
                        return 0 if $sub eq 'AUTOLOAD';
                        return 0 if $pkg =~ /ParaDNS::XS/;
                        return 1; 
                      },
    );
    Devel::Profiler->init();
    
    Qpsmtpd::PollServer->OtherFds(
        fileno($SERVER) => \&accept_handler,
        fileno($CONFIG_SERVER) => \&config_handler,    ); 

    Qpsmtpd::PollServer->EventLoop;
    exit;
=cut

#####################

    for (1 .. $PROCS) {
        my $pid = spawn_child($plugin_loader);
    }
    $plugin_loader->log(LOGDEBUG,
                        "Listening on $PORT with $PROCS children $POLL");
    $SIG{CHLD} = \&sig_chld;
    $SIG{HUP}  = \&sig_hup;

    Qpsmtpd::PollServer->OtherFds(fileno($CONFIG_SERVER) => \&config_handler,);

    Qpsmtpd::PollServer->EventLoop;

    exit;

}

sub config_handler {
    my $csock = $CONFIG_SERVER->accept();
    if (!$csock) {

        # warn("accept failed on config server: $!");
        return;
    }
    binmode($csock, ':raw');

    printf("Config server connection\n") if $DEBUG;

    IO::Handle::blocking($csock, 0);
    setsockopt($csock, IPPROTO_TCP, TCP_NODELAY, pack("l", 1)) or die;

    my $client = Qpsmtpd::ConfigServer->new($csock);
    $client->watch_read(1);
    return;
}

sub command_handler {
    my $reader = shift;

    chomp(my $command = <$reader>);

    #print "Got command: $command\n";

    my $real_command = "cmd_$command";

    no strict 'refs';
    $real_command->();
}

sub cmd_hup {

    # clear cache
    print "Clearing cache\n";
    Qpsmtpd::clear_config_cache();

    # should also reload modules... but can't do that yet.
}

# Accept all new connections
sub accept_handler {
    for (1 .. $NUMACCEPT) {
        return unless _accept_handler();
    }

    # got here because we have accept's left.
    # So double the number we accept next time.
    $NUMACCEPT *= 2;
    $NUMACCEPT = ACCEPT_MAX if $NUMACCEPT > ACCEPT_MAX;
    $ACCEPT_RSET->cancel if defined $ACCEPT_RSET;
    $ACCEPT_RSET = Danga::Socket->AddTimer(30, \&reset_num_accept);
}

use Errno qw(EAGAIN EWOULDBLOCK);

sub _accept_handler {
    my $csock = $SERVER->accept();
    if (!$csock) {

        # warn("accept() failed: $!");
        return;
    }
    binmode($csock, ':raw');

    printf("Listen child making a Qpsmtpd::PollServer for %d.\n",
           fileno($csock))
      if $DEBUG;

    IO::Handle::blocking($csock, 0);

    #setsockopt($csock, IPPROTO_TCP, TCP_NODELAY, pack("l", 1)) or die;

    #print "Got connection\n";
    my $client = Qpsmtpd::PollServer->new($csock);

    if ($PAUSED) {
        $client->write("451 Sorry, this server is currently paused\r\n");
        $client->close;
        return 1;
    }

    $client->process_line("Connect\n");
    $client->watch_read(1);
    $client->pause_read();
    return 1;
}

########################################################################

sub log {
    my ($level, $message) = @_;

    # $level not used yet.  this is reimplemented from elsewhere anyway
    warn("$$ fd:? $message\n");
}

sub pause {
    my ($pause) = @_;
    $PAUSED = $pause;
}