#!/usr/bin/perl
#
# Copyright (C) 2001-2013 Graeme Walker <graeme_walker@users.sourceforge.net>
# 
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# 
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
# ===
#!/usr/bin/perl -w
#
# emailrelay-test.pl
#
# Tests the E-MailRelay system.
#
# This script is best run as root. Some parameters might need tweaking 
# depending on the speed of the machine.
#
# usage: emailrelay-test.pl [-d <bin-dir>] [-k] [-v] [<test-name> ...]
#      -d  - directory containing emailrelay binary
#      -k  - keep going after a failed test
#      -v  - verbose logging from this script
#
# Ubuntu package required: libnet-telnet-perl
#
# See also: man Net::Telnet
#


use strict ;
die "emailrelay-test.pl: dont use msys perl" if $^O eq "msys" ;
use Carp ;
use FileHandle ;
use Server ;
use TestServer ;
use Port ;
use Helper ;
use AdminClient ;
use SmtpClient ;
use PopClient ;
use Check ;
use System ;
use Scanner ;
use Verifier ;
use Filter ;
use Getopt::Std ;

$| = 1 ;

# parse the command line
my %opts = () ;
getopts( 'd:kv' , \%opts ) ;
sub opt_bin_dir { return defined($opts{'d'}) ? $opts{'d'} : $_[0] }
sub opt_keep_going { return exists $opts{'k'} }
my $bin_dir = opt_bin_dir("../src/main") ;
$Server::bin_dir = $bin_dir ;
$System::bin_dir = $bin_dir ;
$Helper::bin_dir = "." ;
$TestServer::bin_dir = "." ;
$System::verbose = 1 if exists $opts{'v'} ;

my $run_all_ok = 1 ;

sub requireUnix
{
	if( ! System::unix() )
	{
		die "skipped: not unix" ;
	}
}

sub requireRoot
{
	# (typically called after requireUnix())
	my $id = `id -u` ;
	if( $id != 0 )
	{
		die "skipped: not root" ;
	}
}

sub requireDebug
{
	my $server = new Server() ;
	if( ! $server->hasDebug() )
	{
		die "skipped: not a debug build" ;
	}
}

sub requirePop
{
	my ( $server ) = @_ ;
	if( ! $server->canDo( "pop" , 1 ) )
	{
		die "skipped: not a pop build" ;
	}
}

sub requireTestAccount
{
	die "skipped: no non-root test account" if( getTestAccount() eq "root" ) ;
}

sub getTestAccount
{
	if( System::mac() )
	{
		my $user = $ENV{LOGNAME} ;
		$user = $ENV{SUDO_USER} if( $user eq "root" ) ;
		return $user ;
	}
	else
	{
		return "bin" ;
	}
}

# ===

sub testServerShowsHelp
{
	# setup
	my %args = (
		Help => 1 ,
	) ;
	requireUnix() ; # could do better
	my $server = new Server() ;

	# test help output
	Check::ok( $server->run(\%args,undef,undef,0) , "failed to run" , $server->message() , $server->command() ) ;
	Check::that( $server->rc() == 0 , "non-zero exit" ) ;
	Check::fileNotEmpty( $server->stdout() ) ;
	Check::fileEmpty( $server->stderr() ) ;

	# tear down
	$server->cleanup() ;
}

sub testServerStartsAndStops
{
	# setup
	my %args = (
		AsServer => 1 ,
		Port => 1 ,
		PidFile => 1 ,
		SpoolDir => 1 ,
	) ;
	requireUnix() ;
	my $server = new Server() ;
	Check::that( $server->canRun() , "cannot run: port(s) already open" ) ;

	# test that the server starts up
	Check::ok( $server->run(\%args) , "failed to run" ) ;
	Check::that( $server->rc() == 0 , "immediate error" ) ;
	Check::fileExists( $server->stdout() , "stdout" ) ;
	Check::fileExists( $server->stderr() , "stderr" ) ;
	Check::fileExists( $server->pidFile() , "pid file" ) ;
	Check::numeric( $server->pid() , "pid file" ) ;
	Check::running( $server->pid() , $server->message() ) ;

	# test that the server stops on a signal
	my $c = new SmtpClient( $server->smtpPort() ) ;
	Check::ok( $c->open(0) , "cannot connect for smtp" , $server->smtpPort() ) ;
	$server->kill() ;
	Check::notRunning( $server->pid() ) ;
	Check::fileDeleted( $server->pidFile() , "pid file" ) if System::unix() ;
	Check::fileEmpty( $server->stdout() , "stdout" ) ;
	Check::fileEmpty( $server->stderr() , "stderr" ) ;
	
	# tear down
	$server->cleanup() ;
}

sub testServerAdminTerminate
{
	# setup
	my %args = (
		AsServer => 1 ,
		Port => 1 ,
		Admin => 1 ,
		PidFile => 1 ,
		SpoolDir => 1 ,
		AdminTerminate => 1 ,
	) ;
	my $server = new Server() ;
	Check::ok( $server->run(\%args) , "failed to run" ) ;
	Check::that( $server->rc() == 0 , "immediate error" ) ;
	Check::running( $server->pid() , $server->message() ) ;

	# test that the server can be terminated through the admin interface
	my $c = new AdminClient( $server->adminPort() ) ;
	Check::ok( $c->open() , "cannot connect for admin" , $server->smtpPort() ) ;
	$c->doHelp() ;
	$c->doHelp() ;
	$c->doTerminate() ;
	$server->wait( 100 ) ;
	Check::notRunning( $server->pid() ) ;
	Check::fileDeleted( $server->pidFile() , "pid file" ) if System::unix() ;
	Check::fileEmpty( $server->stderr() , "stderr" ) ;

	# tear down
	$server->cleanup() ;
}

sub testSubmit
{
	# setup
	my $spool_dir = System::createSpoolDir() ;
	my $path = System::createSmallMessageFile() ;

	# test that the submit utility works
	my $exe = System::weirdpath( System::exe( $bin_dir , "emailrelay-submit" ) ) ;
	my $cmd = System::commandline( "$exe --from me\@here.local --spool-dir $spool_dir me\@there.local < $path" , {background=>0} ) ;
	my $rc = system( $cmd ) ;
	Check::that( $rc == 0 , "failed to submit" ) ;
	Check::fileMatchCount( $spool_dir."/emailrelay.*.content" , 1 ) ;
	Check::fileMatchCount( $spool_dir."/emailrelay.*.envelope" , 1 ) ;
	Check::fileNotEmpty( System::match($spool_dir."/emailrelay.*.content") ) ;
	Check::fileNotEmpty( System::match($spool_dir."/emailrelay.*.envelope") ) ;

	# tear down
	System::deleteSpoolDir($spool_dir) ;
	unlink $path ;
}

sub testPasswd
{
	# test that the password utility works (could do better)
	my $exe = System::weirdpath( System::exe( $bin_dir , "emailrelay-passwd" ) ) ;
	my $fh = new FileHandle( "echo foobar | $exe |" ) ;
	my $result = <$fh> ;
	chomp $result ;
	my $ok = ( $result eq "3507327710.2524437411.674156941.3275202688.3577739107.3692417735.1542828932.27631078" ) ;
	Check::that( $ok , "password digest generation failed" ) ;
}

sub testSubmitPermissions
{
	# setup -- group-suid-daemon exe, group-daemon directory (requires world execute on all directories up to /)
	requireUnix() ;
	requireRoot() ;
	requireTestAccount() ;
	my $exe = System::tempfile("submit","/tmp") ;
	my $rc = system( "cp $bin_dir/emailrelay-submit $exe" ) ;
	$rc += system( "chown daemon:daemon $exe" ) ;
	$rc += system( "chmod 755 $exe" ) ;
	$rc += system( "chmod g+s $exe" ) ;
	Check::that( $rc == 0 , "cannot create suid submit exe" ) ;
	my $spool_dir = System::createSpoolDir() ;
	$rc = system( "chgrp daemon $spool_dir" ) ;
	$rc += system( "chmod 770 $spool_dir" ) ;
	Check::that( $rc == 0 , "cannot set spool dir permissions" ) ;
	my $path = System::createSmallMessageFile() ;
	$rc = system( "chmod 440 $path" ) ;
	Check::that( $rc == 0 , "cannot file permissions" ) ;

	# test that group-suid-daemon submit executable creates files correctly
	my $cmd = "$exe --from me\@here.local --spool-dir $spool_dir me\@there.local" ;
	my $someuser = getTestAccount() ;
	$rc = system( "cat $path | su -m $someuser -c \"$cmd\"" ) ;
	Check::that( $rc == 0 , "failed to submit" ) ;
	Check::fileMatchCount( $spool_dir."/emailrelay.*.content" , 1 ) ;
	Check::fileMatchCount( $spool_dir."/emailrelay.*.envelope" , 1 ) ;
	Check::fileNotEmpty( System::match($spool_dir."/emailrelay.*.content") ) ;
	Check::fileNotEmpty( System::match($spool_dir."/emailrelay.*.envelope") ) ;
	Check::fileOwner( System::match($spool_dir."/emailrelay.*.content") , $someuser ) ;
	Check::fileOwner( System::match($spool_dir."/emailrelay.*.envelope") , $someuser ) ;
	Check::fileGroup( System::match($spool_dir."/emailrelay.*.content") , "daemon" ) ;
	Check::fileGroup( System::match($spool_dir."/emailrelay.*.envelope") , "daemon" ) ;
	Check::fileMode( System::match($spool_dir."/emailrelay.*.content") , 0660 ) ;
	Check::fileMode( System::match($spool_dir."/emailrelay.*.envelope") , 0660 ) ;

	# tear down
	System::deleteSpoolDir($spool_dir) ;
	unlink $path ;
}

sub testServerIdentityRunningAsRoot
{
	# setup
	my %args = (
		AsServer => 1 ,
		Port => 1 ,
		PidFile => 1 ,
		SpoolDir => 1 ,
		User => 1 ,
	) ;
	requireUnix() ;
	requireRoot() ;
	my $server = new Server() ;
	$server->run( \%args , "sudo " ) ;
	Check::running( $server->pid() , $server->message() ) ;

	# test that the effective id comes from --user
	Check::processRealUser( $server->pid() , "root" ) ;
	Check::processRealGroup( $server->pid() , "root" ) ;
	Check::processEffectiveUser( $server->pid() , $server->user() ) ;
	Check::processEffectiveGroup( $server->pid() , $server->user() ) ;

	# tear down
	$server->kill() ;
	Check::notRunning( $server->pid() ) ;
	Check::fileDeleted( $server->pidFile() , "pid file" ) if System::unix() ;
	$server->cleanup() ;
}

sub testServerIdentityRunningSuidRoot
{
	# setup
	my %args = (
		AsServer => 1 ,
		Port => 1 ,
		PidFile => 1 ,
		SpoolDir => 1 ,
		User => 1 ,
	) ;
	requireUnix() ;
	requireRoot() ;
	my $server = new Server() ;
	my $exe = System::tempfile("emailrelay") ;
	my $rc = system( "cp ".$server->exe()." $exe" ) ;
	$rc += system( "chmod 755 $exe" ) ;
	$rc += system( "sudo chown root $exe" ) ;
	$rc += system( "sudo chmod u+s $exe" ) ;
	Check::that( $rc == 0 ) ;
	$server->set_exe( $exe ) ;
	my $someuser = getTestAccount() ;
	$server->run( \%args , "sudo -u $someuser " ) ;
	Check::running( $server->pid() , $server->message() ) ;

	# test that the effective id is the real id ($someuser) and not root or --user
	Check::processRealUser( $server->pid() , $someuser ) ;
	Check::processEffectiveUser( $server->pid() , $someuser ) ;
	Check::processSavedUser( $server->pid() , "root" ) ;

	# tear down
	$server->kill() ;
	Check::notRunning( $server->pid() ) ;
	Check::fileDeleted( $server->pidFile() , "pid file" ) if System::unix() ;
	$server->cleanup() ;
	unlink $exe ;
}

sub testServerSmtpSubmit
{
	# setup
	my %args = (
		AsServer => 1 ,
		Port => 1 ,
		PidFile => 1 ,
		SpoolDir => 1 ,
		Verbose => 1 ,
		Debug => 1 ,
	) ;
	my $server = new Server() ;
	$server->run( \%args ) ;
	Check::running( $server->pid() , $server->message() ) ;
	my $client = new SmtpClient( $server->smtpPort() ) ;
	$client->open() ;
	$client->submit_start() ;
	my $line = "lkjldkjfglkjdfglkjdferoiwuoiruwoeiur" ;
	for( my $i = 0 ; $i < 100 ; $i++ ) { $client->submit_line($line) }
	$client->submit_end() ;

	# test that message files appear in the spool directory
	Check::fileMatchCount( $server->spoolDir()."/emailrelay.*.content" , 1 ) ;
	Check::fileMatchCount( $server->spoolDir()."/emailrelay.*.envelope" , 1 ) ;
	Check::fileNotEmpty( System::match($server->spoolDir()."/emailrelay.*.envelope") ) ;
	my $content = System::match($server->spoolDir()."/emailrelay.*.content") ;
	Check::fileNotEmpty( $content ) ;
	Check::fileLineCount( $content , 1 , "Received" ) ;
	Check::fileLineCount( $content , 100 , $line ) ;

	# tear down
	$server->kill() ;
	Check::notRunning( $server->pid() ) ;
	Check::fileDeleted( $server->pidFile() , "pid file" ) if System::unix() ;
	$server->cleanup() ;
	System::deleteSpoolDir( $server->spoolDir() ) ;
}

sub testServerPermissions
{
	# setup
	my %args = (
		Log => 1 ,
		Port => 1 ,
		PidFile => 1 ,
		SpoolDir => 1 ,
		User => 1 ,
		Verbose => 1 ,
		#Debug => 1 ,
	) ;
	requireUnix() ;
	requireRoot() ;
	my $server = new Server() ;
	# for bsd/mac the file group is inherited from the directory - also assume the 
	# group name is the same as the user name
	my $rc = 0 ;
	$rc = system( "chgrp ".$server->user()." ".$server->spoolDir() ) if ( System::bsd() || System::mac() ) ;
	$rc += system( "chmod 770 ".$server->spoolDir() ) if ( System::bsd() || System::mac() ) ;
	Check::that( $rc == 0 , "chgrp/chmod of spool directory failed" ) ;
	$server->run( \%args , "sudo " ) ;
	Check::running( $server->pid() , $server->message() ) ;
	my $client = new SmtpClient( $server->smtpPort() ) ;
	$client->open() ;
	$client->submit() ;

	# test permissions of created files
	Check::fileOwner( $server->pidFile() , "root" ) ;
	Check::fileGroup( $server->pidFile() , $server->user() ) unless ( System::bsd() || System::mac() ) ;
	Check::fileOwner( System::match($server->spoolDir()."/emailrelay.*.content") , "root" ) ;
	Check::fileGroup( System::match($server->spoolDir()."/emailrelay.*.content") , $server->user() ) ;
	Check::fileMode( System::match($server->spoolDir()."/emailrelay.*.content") , 0660 ) ;
	Check::fileOwner( System::match($server->spoolDir()."/emailrelay.*.envelope") , "root" ) ;
	Check::fileGroup( System::match($server->spoolDir()."/emailrelay.*.envelope") , $server->user() ) ;
	Check::fileMode( System::match($server->spoolDir()."/emailrelay.*.envelope") , 0660 ) ;

	# tear down
	$server->kill() ;
	Check::notRunning( $server->pid() ) ;
	Check::fileDeleted( $server->pidFile() , "pid file" ) if System::unix() ;
	$server->cleanup() ;
	System::deleteSpoolDir( $server->spoolDir() ) ;
}

sub testServerPop
{
	# setup
	my %args = (
		Pop => 1 ,
		PopAuth => 1 ,
		PopPort => 1 ,
		NoSmtp => 1 ,
		Log => 1 ,
		PidFile => 1 ,
		SpoolDir => 1 ,
		Verbose => 1 ,
	) ;
	my $server = new Server() ;
	requirePop( $server ) ;
	System::createFile( $server->popSecrets() , "server login me secret" ) ;
	$server->run( \%args ) ;
	Check::running( $server->pid() , $server->message() ) ;
	my $pop = new PopClient( $server->popPort() ) ;
	System::submitSmallMessage($server->spoolDir()) ;
	System::chmod_r( $server->spoolDir() , "700" , "600" ) ;

	# test that the pop client can log in and get a message list
	Check::that( Port::isFree($server->smtpPort()) ) ;
	Check::ok( $pop->open() , "cannot connect to pop port" ) ;
	Check::ok( $pop->login( "me" , "secret" ) ) ;
	my @list = $pop->list(1,10) ;
	Check::that( scalar(@list) == 1 , "invalid message list" ) ;

	# tear down
	$server->kill() ;
	Check::notRunning( $server->pid() ) ;
	Check::fileDeleted( $server->pidFile() , "pid file" ) if System::unix() ;
	$server->cleanup() ;
	System::deleteSpoolDir( $server->spoolDir() ) ;
}

sub disabled_testServerPopList
{
	# setup
	my %args = (
		Pop => 1 ,
		PopAuth => 1 ,
		PopPort => 1 ,
		NoSmtp => 1 ,
		Log => 1 ,
		PidFile => 1 ,
		SpoolDir => 1 ,
		Verbose => 1 ,
	) ;
	requireDebug() ;
	my $bufferbloat = !System::linux() ; # cannot check flow control on bufferbloated systems

	my $server = new Server() ;
	requirePop( $server ) ;
	System::createFile( $server->popSecrets() , "server login me secret" ) ;
	$server->run( \%args , undef , "log-flow-control_large-pop-list" ) ; # G_TEST
	Check::running( $server->pid() , $server->message() ) ;
	my $pop = new PopClient( $server->popPort() ) ;
	my $message_count = 40 ; # enough to trigger flow control? - tweak this
	for( my $i = 0 ; $i < $message_count ; $i++ ) { System::submitSmallMessage($server->spoolDir()) }
	System::chmod_r( $server->spoolDir() , "700" , "600" ) ;

	# test that the pop client can log in and get a message list and that flow control was exercised
	Check::that( Port::isFree($server->smtpPort()) ) ;
	Check::ok( $pop->open() , "cannot connect to pop port" ) ;
	Check::ok( $pop->login( "me" , "secret" ) ) ;
	my @list = $pop->list(1,10) ;
	# (must be a debug build for this to work)
	Check::that( scalar(@list) == ($message_count * 1001) , "invalid message list (".scalar(@list).")" ) ;
	Check::fileContains( $server->stderr() , "flow control asserted" , "edit the test to add more messages" ) 
		unless $bufferbloat ;

	# tear down
	$server->kill() ;
	Check::notRunning( $server->pid() ) ;
	Check::fileDeleted( $server->pidFile() , "pid file" ) if System::unix() ;
	$server->cleanup() ;
	System::deleteSpoolDir( $server->spoolDir() ) ;
}

sub testServerPopDisconnect
{
	# setup
	my %args = (
		Pop => 1 ,
		PopAuth => 1 ,
		PopPort => 1 ,
		NoSmtp => 1 ,
		Log => 1 ,
		PidFile => 1 ,
		SpoolDir => 1 ,
		Verbose => 1 ,
		Debug => 1 ,
	) ;
	my $server = new Server() ;
	requirePop( $server ) ;
	System::createFile( $server->popSecrets() , "server login me secret" ) ;
	$server->run( \%args ) ;
	Check::running( $server->pid() , $server->message() ) ;
	my $pop = new PopClient( $server->popPort() ) ;
	$pop->open() ;
	$pop->login( "me" , "secret" ) ;

	# test that the server sees the client disconnect
	Check::fileContains( $server->stderr() , "pop connection from" ) ;
	$pop->disconnect() ;
	Server::sleep_cs( 10 ) ;
	Check::fileContains( $server->stderr() , "pop connection closed" ) ;

	# tear down
	$server->kill() ;
	Check::notRunning( $server->pid() ) ;
	Check::fileDeleted( $server->pidFile() , "pid file" ) if System::unix() ;
	$server->cleanup() ;
}

sub testServerFlushNoMessages
{
	# setup
	my %args = (
		AsServer => 1 ,
		Port => 1 ,
		Admin => 1 ,
		SpoolDir => 1 ,
		ForwardTo => 1 ,
		PidFile => 1 ,
	) ;
	my $spool_dir = System::createSpoolDir() ;
	my $server = new Server(undef,undef,undef,$spool_dir) ;
	Check::ok( $server->run(\%args) ) ;
	Check::running( $server->pid() , $server->message() ) ;

	# test for an appropriate protocol response if nothing to send
	my $c = new AdminClient( $server->adminPort() ) ;
	Check::ok( $c->open() ) ;
	$c->doFlush() ;
	my $line = $c->getline() ;
	chomp $line ;
	Check::that( $line eq "error: no messages to send" , "unexpected response" , $line ) ;

	# tear down
	$server->kill() ;
	Check::notRunning( $server->pid() ) ;
	Check::fileDeleted( $server->pidFile() , "pid file" ) if System::unix() ;
	$server->cleanup() ;
	System::deleteSpoolDir( $spool_dir ) ;
}

sub testServerFlushNoServer
{
	# setup
	my %args = (
		AsServer => 1 ,
		Port => 1 ,
		Admin => 1 ,
		SpoolDir => 1 ,
		ForwardTo => 1 ,
		PidFile => 1 ,
	) ;
	my $spool_dir = System::createSpoolDir() ;
	System::submitSmallMessage( $spool_dir ) ;
	my $server = new Server(undef,undef,undef,$spool_dir) ;
	$server->run(\%args) ;
	Check::running( $server->pid() , $server->message() ) ;
	Check::fileMatchCount( $spool_dir ."/emailrelay.*.content", 1 ) ;
	Check::fileMatchCount( $spool_dir ."/emailrelay.*.envelope", 1 ) ;

	# test for an appropriate admin response and files unaffected if cannot connect on the flush command
	my $c = new AdminClient( $server->adminPort() ) ;
	Check::ok( $c->open() ) ;
	$c->doFlush() ;
	my $line = $c->getline() ;
	chomp $line ;
	Check::match( $line , "^error: dns error: no such host" , "unexpected response" ) ;
	Check::fileMatchCount( $spool_dir ."/emailrelay.*.envelope", 1 ) ;

	# tear down
	$server->kill() ;
	Check::notRunning( $server->pid() ) ;
	Check::fileDeleted( $server->pidFile() , "pid file" ) if System::unix() ;
	$server->cleanup() ;
	System::deleteSpoolDir($spool_dir) ;
}

sub testServerFlush
{
	# setup
	my %args = (
		Log => 1 ,
		Port => 1 ,
		Admin => 1 ,
		SpoolDir => 1 ,
		ForwardTo => 1 ,
		PidFile => 1 ,
	) ;
	my $spool_dir_1 = System::createSpoolDir(undef,undef,"spool-1") ;
	my $spool_dir_2 = System::createSpoolDir(undef,undef,"spool-2") ;
	my $server_1 = new Server(undef,undef,undef,$spool_dir_1) ;
	my $server_2 = new Server($server_1->smtpPort()+100,undef,$server_1->adminPort()+100,$spool_dir_2) ;
	$server_1->set_dst("localhost:".$server_2->smtpPort()) ;
	System::submitMessage( $spool_dir_1 , undef , 10000 ) ;
	System::submitMessage( $spool_dir_1 , undef , 10000 ) ;
	Check::ok( $server_1->run(\%args) ) ;
	Check::ok( $server_2->run(\%args) ) ;
	Check::running( $server_1->pid() , $server_1->message() ) ;
	Check::running( $server_2->pid() , $server_2->message() ) ;
	my $c = new AdminClient( $server_1->adminPort() ) ;
	Check::ok( $c->open() ) ;
	Check::fileMatchCount( $spool_dir_1 ."/emailrelay.*.content", 2 ) ;
	Check::fileMatchCount( $spool_dir_1 ."/emailrelay.*.envelope", 2 ) ;
	Check::fileMatchCount( $spool_dir_2 ."/emailrelay.*.content", 0 ) ;
	Check::fileMatchCount( $spool_dir_2 ."/emailrelay.*.envelope", 0 ) ;

	# test that messages are sent from an admin flush command
	$c->doFlush() ;
	my $line = $c->getline() ;
	chomp $line ;
	Check::that( $line eq "OK" , "unexpected response" , $line ) ;
	Check::fileMatchCount( $spool_dir_1 ."/emailrelay.*.content", 0 ) ;
	Check::fileMatchCount( $spool_dir_1 ."/emailrelay.*.envelope", 0 ) ;
	Check::fileMatchCount( $spool_dir_2 ."/emailrelay.*.content", 2 ) ;
	Check::fileMatchCount( $spool_dir_2 ."/emailrelay.*.envelope", 2 ) ;

	# tear down
	$server_1->kill() ;
	$server_2->kill() ;
	Check::notRunning( $server_1->pid() ) ;
	Check::notRunning( $server_2->pid() ) ;
	$server_1->cleanup() ;
	$server_2->cleanup() ;
	System::deleteSpoolDir( $spool_dir_1 ) ;
	System::deleteSpoolDir( $spool_dir_2 ) ;
}

sub testServerPolling
{
	# setup
	my %args = (
		Log => 1 ,
		Port => 1 ,
		Admin => 1 ,
		SpoolDir => 1 ,
		ForwardTo => 1 ,
		PidFile => 1 ,
		Verbose => 1 ,
		Poll => 1 ,
		ConnectionTimeout => 1 ,
	) ;
	my $spool_dir_1 = System::createSpoolDir(undef,undef,"spool-1") ;
	my $spool_dir_2 = System::createSpoolDir(undef,undef,"spool-2") ;
	my $server_1 = new Server(undef,undef,undef,$spool_dir_1) ;
	my $server_2 = new Server($server_1->smtpPort()+100,undef,$server_1->adminPort()+100,$spool_dir_2) ;
	$server_1->set_dst("localhost:".$server_2->smtpPort()) ;
	System::submitMessage( $spool_dir_1 , undef , 10000 ) ;
	$server_1->run(\%args) ;
	$server_2->run(\%args) ;
	Check::running( $server_1->pid() , $server_1->message() ) ;
	Check::running( $server_2->pid() , $server_2->message() ) ;
	my $c = new AdminClient( $server_1->adminPort() ) ;
	Check::ok( $c->open() ) ;

	# test that the message gets forwarded
	Check::ok( System::drain($server_1->spoolDir()) , "message not forwarded" ) ;
	Check::fileMatchCount( $spool_dir_1 ."/emailrelay.*.content", 0 ) ;
	Check::fileMatchCount( $spool_dir_2 ."/emailrelay.*.content", 1 ) ;

	# tear down
	$server_1->kill() ;
	$server_2->kill() ;
	Check::notRunning( $server_1->pid() ) ;
	Check::notRunning( $server_2->pid() ) ;
	Check::fileDeleted( $server_1->pidFile() , "pid file" ) if System::unix() ;
	Check::fileDeleted( $server_2->pidFile() , "pid file" ) if System::unix() ;
	$server_1->cleanup() ;
	$server_2->cleanup() ;
	System::deleteSpoolDir($spool_dir_1) ;
	System::deleteSpoolDir($spool_dir_2) ;
}

sub testServerPollingOverlap
{
	# setup
	my %args = (
		Log => 1 ,
		LogTime => 1 ,
		Port => 1 ,
		Admin => 1 ,
		SpoolDir => 1 ,
		ForwardTo => 1 ,
		PidFile => 1 ,
		Verbose => 1 ,
		Poll => 1 ,
	) ;
	my $spool_dir = System::createSpoolDir() ;
	my $message_count = 1 ;
	System::submitMessages( $spool_dir , undef , $message_count , 10000 ) ;
	Check::fileMatchCount( $spool_dir ."/emailrelay.*.content", $message_count ) ;
	my $emailrelay = new Server(undef,undef,undef,$spool_dir) ;
	my $test_server = new TestServer(10125) ;
	$emailrelay->set_dst("localhost:".$test_server->port()) ;
	$test_server->run("--slow") ;
	$emailrelay->run(\%args) ;
	Check::running( $emailrelay->pid() , $emailrelay->message() ) ;
	Check::running( $test_server->pid() ) ;

	# test that rapid polling and slow forwarding results in a still-busy log message
	Check::ok( System::drain($emailrelay->spoolDir(),30) , "message not forwarded" ) ;
	Check::fileContains( $emailrelay->stderr() , "still busy from last time" ) ;

	# tear down
	$emailrelay->kill() ;
	$test_server->kill() ;
	Check::notRunning( $emailrelay->pid() ) ;
	Check::notRunning( $test_server->pid() ) ;
	Check::fileDeleted( $emailrelay->pidFile() , "pid file" ) if System::unix() ;
	$emailrelay->cleanup() ;
	$test_server->cleanup() ;
	System::deleteSpoolDir($spool_dir) ;
}

sub testServerWithBadClient
{
	# setup
	my %args = (
		Log => 1 ,
		Port => 1 ,
		Admin => 1 ,
		SpoolDir => 1 ,
		PidFile => 1 ,
		Verbose => 1 ,
	) ;
	my $server = new Server() ;
	Check::ok( $server->run(\%args) ) ;
	Check::running( $server->pid() , $server->message() ) ;

	# test that the server drops the connection if we mess up the client protocol
	my $c = new SmtpClient( $server->smtpPort() ) ;
	Check::ok( $c->open() ) ;
	$c->doBadHelo() ;
	my $seen_reset = 0 ;
	for( my $i = 0 ; $i < 10 ; $i++ )
	{
		my $error = $c->doBadCommand() ;
		my $was_reset = 
			( $error =~ m/Connection reset by peer/ ) ||
			( $error =~ m/pattern match read eof/ ) ||
			( $error =~ m/filehandle isn't open/ ) ;
		$seen_reset = $seen_reset || $was_reset ;
		System::sleep_cs( 20 ) ;
	}
	Check::that( $seen_reset , "connection not dropped" ) ;

	# tear down
	$server->kill() ;
	Check::notRunning( $server->pid() ) ;
	Check::fileDeleted( $server->pidFile() , "pid file" ) if System::unix() ;
	$server->cleanup() ;
	System::deleteSpoolDir( $server->spoolDir() ) ;
}

sub testServerSizeLimit
{
	# setup
	my %args = (
		Log => 1 ,
		Port => 1 ,
		SpoolDir => 1 ,
		PidFile => 1 ,
		MaxSize => 1 ,
		Verbose => 1 ,
	) ;
	my $server = new Server() ;
	Check::ok( $server->run(\%args) ) ;
	Check::running( $server->pid() , $server->message() ) ;
	my $c = new SmtpClient( $server->smtpPort() ) ;
	Check::ok( $c->open() ) ;
	my $line = "0123456789 123456789 123456789 123456789 123456789" ;
	$line .= $line ;

	# test that if the server aborts the connection if the client submits a big message
	$c->submit_start() ;
	for( my $i = 0 ; $i < 12 ; $i++ )
	{
		$c->submit_line( $line ) ;
	}
	eval { $c->submit_end() } ;
	my $error = $@ ;
	Check::that( $error ne "" , "large message submission did not fail as it should have" ) ;
	Check::fileMatchCount( $server->spoolDir()."/emailrelay.*.content" , 0 ) ;
	Check::fileMatchCount( $server->spoolDir()."/emailrelay.*.envelope*" , 0 ) ;
	Check::fileContains( $server->stderr() , "554 message too big" ) ; # requires verbose

	# tear down
	$server->kill() ;
	Check::notRunning( $server->pid() ) ;
	Check::fileDeleted( $server->pidFile() , "pid file" ) if System::unix() ;
	$server->cleanup() ;
	System::deleteSpoolDir( $server->spoolDir() ) ;
}

sub testClientContinuesIfNoSecrets
{
	# setup
	my %server_args = (
		Log => 1 ,
		LogTime => 1 ,
		Port => 1 ,
		SpoolDir => 1 ,
		PidFile => 1 ,
		Verbose => 1 ,
		ServerSecrets => 1 ,
	) ;
	my %client_args = (
		Log => 1 ,
		Verbose => 1 ,
		LogTime => 1 ,
		SpoolDir => 1 ,
		Verbose => 1 ,
		Forward => 1 ,
		ForwardTo => 1 ,
		DontServe => 1 ,
		NoDaemon => 1 ,
		Hidden => 1 ,
	) ;
	my $server = new Server() ;
	my $client = new Server() ;
	$client->set_dst( "localhost:".$server->smtpPort() ) ;
	System::createFile( $server->serverSecrets() , "server login me secret" ) ;
	System::submitSmallMessage( $client->spoolDir() ) ;
	Check::ok( $server->run(\%server_args) ) ;
	System::sleep_cs( 50 ) ; # just in case of slow startup

	# test that the client gets as far as issuing the mail command
	my $ok = $client->run( \%client_args ) ;
	Check::ok( $ok ) ;
	System::sleep_cs( 50 ) ; # let the logs flush
	Check::fileContains( $client->stderr() , "MAIL FROM:" ) ;
	Check::fileContains( $client->stderr() , "530 authentication required" ) ;

	# tear down
	$server->kill() ;
	Check::notRunning( $server->pid() ) ;
	Check::fileDeleted( $server->pidFile() , "pid file" ) if System::unix() ;
	$server->cleanup() ;
	$client->cleanup() ;
	System::deleteSpoolDir( $server->spoolDir() ) ;
	System::deleteSpoolDir( $client->spoolDir() , 1 ) ;
}

sub testClientSavesReasonCode
{
	# setup
	my %args = (
		Log => 1 ,
		SpoolDir => 1 ,
		Verbose => 1 ,
		ForwardTo => 1 ,
		Forward => 1 ,
		DontServe => 1 ,
		NoDaemon => 1 ,
	) ;
	my $test_server = new TestServer() ;
	my $client = new Server() ;
	$client->set_dst( "localhost:".$test_server->port() ) ;
	$test_server->run( "--fail-at 1" ) ;
	System::submitSmallMessage( $client->spoolDir() ) ;
	System::submitSmallMessage( $client->spoolDir() ) ;

	# test that the client runs but the failed message envelope has a reason code
	Check::ok( $client->run(\%args) ) ;
	System::sleep_cs( 50 ) ;
	Check::fileMatchCount( $client->spoolDir()."/emailrelay.*.content" , 1 , "content" ) ;
	Check::fileMatchCount( $client->spoolDir()."/emailrelay.*.envelope*bad" , 1 , "bad envelope" ) ;
	my @files = System::glob_( $client->spoolDir()."/emailrelay*bad" ) ;
	Check::fileContains( $files[0] , "X-MailRelay-ReasonCode: 452" ) ;

	# tear down
	$test_server->cleanup() ;
	System::deleteSpoolDir( $client->spoolDir() , 1 ) ;
}

sub testFilter
{
	# setup
	my %args = (
		Log => 1 ,
		Port => 1 ,
		SpoolDir => 1 ,
		PidFile => 1 ,
		Filter => 1 ,
	) ;
	my $server = new Server() ;
	my $outputfile = System::tempfile("output",System::unix()?"/tmp":undef) ; # /tmp is writeable by daemon
	Filter::create( $server->filter() , {
			unix => [
				"echo \"\$\@\" | sed 's/^/arg: /' > $outputfile" ,
				"env | sed 's/^/env: /' >> $outputfile" , 
				"exit 0" ,
			] ,
			win32 => [
				"var fs = WScript.CreateObject(\"Scripting.FileSystemObject\");" ,
				"var out_ = fs.OpenTextFile( \"$outputfile\" , 8 , true ) ;" ,
				"out_.WriteLine( \"arg: \" + WScript.Arguments(0) ) ;" ,
				"WScript.Quit(0);" ,
			] ,
		} ) ;
	Check::ok( $server->run(\%args) ) ;
	Check::running( $server->pid() , $server->message() ) ;
	my $c = new SmtpClient( $server->smtpPort() ) ;
	Check::ok( $c->open() ) ;

	# test that the filter is executed with the correct environment
	$c->submit() ;
	Check::that( -f $outputfile , "filter did not create an output file" ) ;
	Check::fileContains( $outputfile , "arg: " , "filter did not generate output" ) ;
	Check::fileContains( $outputfile , "arg: /" , "filter not passed an absolute path" ) if System::unix() ;
	Check::fileLineCountLessThan( $outputfile , 7 , "env: " , "wrong number of environment variables" ) if System::unix() ;
	Check::fileMatchCount( $server->spoolDir()."/emailrelay.*.content" , 1 ) ;
	Check::fileMatchCount( $server->spoolDir()."/emailrelay.*.envelope" , 1 ) ;

	# tear down
	unlink( $outputfile ) ;
	$server->kill() ;
	Check::notRunning( $server->pid() ) ;
	Check::fileDeleted( $server->pidFile() , "pid file" ) if System::unix() ;
	$server->cleanup() ;
	System::deleteSpoolDir( $server->spoolDir() ) ;
}

sub testFilterIdentity
{
	# setup
	my %args = (
		Log => 1 ,
		Port => 1 ,
		SpoolDir => 1 ,
		PidFile => 1 ,
		Filter => 1 ,
		User => 1 ,
	) ;
	requireUnix() ;
	requireRoot() ;
	my $server = new Server() ;
	Check::ok( $server->run(\%args) ) ;
	Check::running( $server->pid() , $server->message() ) ;
	my $outputfile = System::tempfile("output","/tmp") ;
	Filter::create( $server->filter() , {
			unix => [
				'ps -p $$'." -o uid,ruid | sed 's/  */ /g' | sed 's/^ *//' > $outputfile" , 
				"exit 0" ,
			] ,
			win32 => [
				# not used
			] ,
		} ) ;
	my $c = new SmtpClient( $server->smtpPort() ) ;
	Check::ok( $c->open() ) ;

	# test that the filter is executed with the correct identity
	$c->submit() ;
	Check::that( -f $outputfile , "filter did not create an output file" ) ;
	my $uid1 = System::uid( $server->user() ) ;
	my $uid2 = $uid1 - 4294967296 ; # negative ids on mac
	Check::fileContainsEither( $outputfile , "$uid1 $uid1" , "$uid2 $uid2" , "filter not run as uid $uid1" ) ;

	# tear down
	unlink( $outputfile ) ;
	$server->kill() ;
	Check::notRunning( $server->pid() ) ;
	Check::fileDeleted( $server->pidFile() , "pid file" ) if System::unix() ;
	$server->cleanup() ;
	System::deleteSpoolDir( $server->spoolDir() ) ;
}

sub testFilterFailure
{
	# setup
	my %args = (
		Log => 1 ,
		LogTime => 1 ,
		Port => 1 ,
		SpoolDir => 1 ,
		PidFile => 1 ,
		Filter => 1 ,
	) ;
	my $server = new Server() ;
	Check::ok( $server->run(\%args) ) ;
	Check::running( $server->pid() , $server->message() ) ;
	Filter::create( $server->filter() , {
			unix => [
				"echo aaa" , 
				"echo '<<foo bar>> yy'" , 
				"echo zzz" , 
				"exit 3" ,
			] ,
			win32 => [
				"WScript.StdOut.WriteLine(\"aaa\");" ,
				"WScript.StdOut.WriteLine(\"<<foo bar>> yy\");" ,
				"WScript.StdOut.WriteLine(\"zzz\");" ,
				"WScript.Quit(3);" ,
			] ,
		} ) ;
	my $c = new SmtpClient( $server->smtpPort() ) ;
	Check::ok( $c->open() ) ;

	# test that if the filter rejects the message then the submit fails and no files are spooled
	$c->submit( 1 ) ;
	Check::fileMatchCount( $server->spoolDir()."/emailrelay.*.content" , 0 ) ;
	Check::fileMatchCount( $server->spoolDir()."/emailrelay.*.envelope*" , 0 ) ;
	Check::fileContains( $server->stderr() , "error storing message: foo bar" ) ;

	# tear down
	$server->kill() ;
	Check::notRunning( $server->pid() ) ;
	Check::fileDeleted( $server->pidFile() , "pid file" ) if System::unix() ;
	$server->cleanup() ;
	System::deleteSpoolDir( $server->spoolDir() ) ;
}

sub testFilterWithBadFileDeletion
{
	_testFilterWithFileDeletion(0,1) ;
}

sub testFilterWithGoodFileDeletion
{
	_testFilterWithFileDeletion(100,0) ;
}

sub _testFilterWithFileDeletion
{
	my ( $exit_code , $expect_submit_error ) = @_ ;

	# setup
	my %args = (
		Log => 1 ,
		Port => 1 ,
		SpoolDir => 1 ,
		PidFile => 1 ,
		Filter => 1 ,
		Verbose => 1 ,
		ForwardTo => 1 ,
		Immediate => 1 ,
	) ;
	my $server_1 = new Server() ;
	my $server_2 = new Server($server_1->smtpPort()+100) ;
	$server_1->set_dst("localhost:".$server_2->smtpPort()) ;
	Check::ok( $server_1->run(\%args) ) ;
	delete $args{Filter} ;
	delete $args{ForwardTo} ;
	delete $args{Immediate} ;
	Check::ok( $server_2->run(\%args) ) ;
	Check::running( $server_1->pid() , $server_1->message() ) ;
	Check::running( $server_2->pid() , $server_2->message() ) ;
	Filter::create( $server_1->filter() , {
			unix => [
				'rm `dirname $1`/emailrelay.*' , 
				"exit $exit_code" ,
			] ,
			win32 => [
				"var content = WScript.Arguments(0) ;" ,
				"var spool_dir = content.substr(0,content.lastIndexOf(\"\\\\\")) ;" ,
				"var fs = WScript.CreateObject(\"Scripting.FileSystemObject\") ;" ,
				"fs.DeleteFile( spool_dir + \"\\\\emailrelay.*\" ) ;" ,
				"WScript.Quit($exit_code);" ,
			] ,
		} ) ;
	my $c = new SmtpClient( $server_1->smtpPort() ) ;
	Check::ok( $c->open() ) ;

	# test that if the filter deletes the message files then proxying succeeds or fails depending on the exit code
	$c->submit($expect_submit_error) ;

	# tear down
	$server_1->kill() ;
	$server_2->kill() ;
	Check::notRunning( $server_1->pid() ) ;
	Check::notRunning( $server_2->pid() ) ;
	Check::fileDeleted( $server_1->pidFile() , "pid file" ) if System::unix() ;
	Check::fileDeleted( $server_2->pidFile() , "pid file" ) if System::unix() ;
	$server_1->cleanup() ;
	$server_2->cleanup() ;
	System::deleteSpoolDir( $server_1->spoolDir() ) ;
	System::deleteSpoolDir( $server_2->spoolDir() ) ;
}

sub testFilterPollTimeout
{
	# setup
	my %args = (
		Log => 1 ,
		Port => 1 ,
		SpoolDir => 1 ,
		PidFile => 1 ,
		Filter => 1 ,
		Poll => 1 ,
		ForwardTo => 1 ,
		Verbose => 1 ,
	) ;
	my $server = new Server() ;
	$server->set_pollTimeout(999) ;
	Check::ok( $server->run(\%args) , $server->message() ) ;
	Check::running( $server->pid() , $server->message() ) ;
	Filter::create( $server->filter() , {
			unix => [
				"exit 103" ,
			] ,
			win32 => [
				"WScript.Quit(103) ;" ,
			] ,
		} ) ;
	my $c = new SmtpClient( $server->smtpPort() ) ;
	Check::ok( $c->open() ) ;

	# test that the poll timer is triggered
	$c->submit() ;
	Server::sleep_cs( 10 ) ;
	Check::fileContains( $server->stderr() , "info: polling" , "no polling message in the log file" ) ;

	# tear down
	$server->kill() ;
	Check::notRunning( $server->pid() ) ;
	Check::fileDeleted( $server->pidFile() , "pid file" ) if System::unix() ;
	$server->cleanup() ;
	System::deleteSpoolDir( $server->spoolDir() ) ;
}

sub testScannerPass
{
	# setup
	my %args = (
		Log => 1 ,
		Port => 1 ,
		SpoolDir => 1 ,
		PidFile => 1 ,
		Scanner => 1 ,
		Verbose => 1 ,
	) ;
	my $server = new Server() ;
	my $scanner = new Scanner( $server->scannerPort() ) ;
	Check::ok( $server->run(\%args) , $server->message() ) ;
	Check::running( $server->pid() , $server->message() ) ;
	$scanner->run() ;
	my $c = new SmtpClient( $server->smtpPort() ) ;
	Check::ok( $c->open() ) ;

	# test that the scanner is used
	$c->submit_start() ;
	$c->submit_line( "send ok" ) ; # (the test scanner treats the message body as a script)
	$c->submit_end() ;
	Check::fileContains( $scanner->logfile() , "new connection from" ) ;
	Check::fileContains( $scanner->logfile() , "send ok" ) ;
	Check::fileDoesNotContain( $server->stderr() , "message processing failed:" ) ;
	Check::fileMatchCount( $server->spoolDir()."/emailrelay.*.envelope" , 1 ) ;

	# tear down
	$server->kill() ;
	$scanner->kill() ;
	Check::notRunning( $server->pid() ) ;
	Check::fileDeleted( $server->pidFile() , "pid file" ) if System::unix() ;
	Check::notRunning( $scanner->pid() ) ;
	$server->cleanup() ;
	$scanner->cleanup() ;
	System::deleteSpoolDir( $server->spoolDir() ) ;
}

sub testScannerBlock
{
	# setup
	my %args = (
		Log => 1 ,
		Port => 1 ,
		SpoolDir => 1 ,
		PidFile => 1 ,
		Scanner => 1 ,
		Verbose => 1 ,
	) ;
	my $server = new Server() ;
	my $scanner = new Scanner( $server->scannerPort() ) ;
	Check::ok( $server->run(\%args) , $server->message() ) ;
	Check::running( $server->pid() , $server->message() ) ;
	$scanner->run() ;
	my $c = new SmtpClient( $server->smtpPort() ) ;
	Check::ok( $c->open() ) ;

	# test that the scanner is used
	$c->submit_start() ;
	$c->submit_line( "send foobar" ) ; # (the test scanner treats the message body as a script)
	$c->submit_end( 1 ) ; # 1 <= expect the "." command to fail
	Check::fileContains( $server->stderr() , "message processing failed: foobar" ) ;
	Check::fileMatchCount( $server->spoolDir()."/emailrelay.*.envelope" , 0 ) ;

	# tear down
	$server->kill() ;
	$scanner->kill() ;
	Check::notRunning( $server->pid() ) ;
	Check::fileDeleted( $server->pidFile() , "pid file" ) if System::unix() ;
	Check::notRunning( $scanner->pid() ) ;
	$server->cleanup() ;
	$scanner->cleanup() ;
	System::deleteSpoolDir( $server->spoolDir() ) ;
}

sub testScannerTimeout
{
	# setup
	my %args = (
		Log => 1 ,
		Port => 1 ,
		SpoolDir => 1 ,
		PidFile => 1 ,
		Scanner => 1 ,
		Verbose => 1 ,
		FilterTimeout => 1 ,
	) ;
	my $server = new Server() ;
	my $scanner = new Scanner( $server->scannerPort() ) ;
	Check::ok( $server->run(\%args) , $server->message() ) ;
	Check::running( $server->pid() , $server->message() ) ;
	$scanner->run() ;
	my $c = new SmtpClient( $server->smtpPort() ) ;
	Check::ok( $c->open() ) ;

	# test that the scanner is used
	$c->submit_start() ;
	$c->submit_line( "sleep 3" ) ;
	$c->submit_line( "send foobar" ) ;
	$c->submit_end( 1 ) ;
	Check::fileDoesNotContain( $server->stderr() , "message processing failed: foobar" ) ;
	Check::fileContains( $server->stderr() , "message processing failed:.*time.*out" ) ;

	# tear down
	$server->kill() ;
	$scanner->kill() ;
	Check::notRunning( $server->pid() ) ;
	Check::fileDeleted( $server->pidFile() , "pid file" ) if System::unix() ;
	Check::notRunning( $scanner->pid() ) ;
	$server->cleanup() ;
	$scanner->cleanup() ;
	System::deleteSpoolDir( $server->spoolDir() ) ;
}

sub testNetworkVerifierPass
{
	# setup
	my %args = (
		Log => 1 ,
		Port => 1 ,
		SpoolDir => 1 ,
		PidFile => 1 ,
		Verifier => 1 ,
		Verbose => 1 ,
	) ;
	my $server = new Server() ;
	my $verifier = new Verifier( $server->verifierPort() ) ;
	Check::ok( $server->run(\%args) , $server->message() ) ;
	Check::running( $server->pid() , $server->message() ) ;
	$verifier->run() ;
	my $c = new SmtpClient( $server->smtpPort() ) ;
	Check::ok( $c->open() ) ;

	# test that the verifier is used
	$c->submit_start( "OK\@here" ) ; # the test verifier interprets this string
	$c->submit_line( "just testing" ) ;
	$c->submit_end() ;
	Check::fileContains( $verifier->logfile() , "sending valid" ) ;
	Check::fileDoesNotContain( $server->stderr() , "message processing failed:" ) ;
	Check::fileMatchCount( $server->spoolDir()."/emailrelay.*.envelope" , 1 ) ;

	# tear down
	$server->kill() ;
	$verifier->kill() ;
	Check::notRunning( $server->pid() ) ;
	Check::fileDeleted( $server->pidFile() , "pid file" ) if System::unix() ;
	Check::notRunning( $verifier->pid() ) ;
	$server->cleanup() ;
	$verifier->cleanup() ;
	System::deleteSpoolDir( $server->spoolDir() ) ;
}

sub testNetworkVerifierFail
{
	# setup
	my %args = (
		Log => 1 ,
		Port => 1 ,
		SpoolDir => 1 ,
		PidFile => 1 ,
		Verifier => 1 ,
		Verbose => 1 ,
	) ;
	my $server = new Server() ;
	my $verifier = new Verifier( $server->verifierPort() ) ;
	Check::ok( $server->run(\%args) , $server->message() ) ;
	Check::running( $server->pid() , $server->message() ) ;
	$verifier->run() ;
	my $c = new SmtpClient( $server->smtpPort() ) ;
	Check::ok( $c->open() ) ;

	# test that the verifier can reject
	$c->submit_start( "fail\@here" , 1 ) ; # the test verifier interprets this string
	Check::fileContains( $verifier->logfile() , "sending error" ) ;
	Check::fileContains( $server->stderr() , "mailbox unavailable" ) ;
	Check::fileMatchCount( $server->spoolDir()."/emailrelay.*.envelope" , 0 ) ;

	# tear down
	$server->kill() ;
	$verifier->kill() ;
	Check::notRunning( $server->pid() ) ;
	Check::fileDeleted( $server->pidFile() , "pid file" ) if System::unix() ;
	Check::notRunning( $verifier->pid() ) ;
	$server->cleanup() ;
	$verifier->cleanup() ;
	System::deleteSpoolDir( $server->spoolDir() ) ;
}

sub testProxyConnectsOnce
{
	# setup
	my %args = (
		Log => 1 ,
		Port => 1 ,
		Admin => 1 ,
		SpoolDir => 1 ,
		ForwardTo => 1 ,
		Immediate => 1 ,
		PidFile => 1 ,
	) ;
	my $spool_dir_1 = System::createSpoolDir(undef,undef,"spool-1") ;
	my $spool_dir_2 = System::createSpoolDir(undef,undef,"spool-2") ;
	my $server_1 = new Server(undef,undef,undef,$spool_dir_1) ;
	my $server_2 = new Server($server_1->smtpPort()+100,undef,$server_1->adminPort()+100,$spool_dir_2) ;
	$server_1->set_dst("localhost:".$server_2->smtpPort()) ;
	Check::ok( $server_1->run(\%args) ) ;
	delete $args{Immediate} ;
	delete $args{ForwardTo} ;
	Check::ok( $server_2->run(\%args) ) ;
	Check::running( $server_1->pid() , $server_1->message() ) ;
	Check::running( $server_2->pid() , $server_2->message() ) ;
	my $c = new SmtpClient( $server_1->smtpPort() ) ;
	Check::ok( $c->open() ) ;
	Check::fileMatchCount( $spool_dir_1 ."/emailrelay.*", 0 ) ;
	Check::fileMatchCount( $spool_dir_2 ."/emailrelay.*", 0 ) ;

	# test that the proxy uses one connection to forward multiple messages
	my $n = 4 ;
	for( my $i = 0 ; $i < $n ; $i++ )
	{
		$c->submit_start() ;
		$c->submit_line( "foo bar" ) ;
		$c->submit_end() ;
	}
	$c->close() ;
	$server_1->wait( 100 ) ; # allow for logs to be written
	Check::fileMatchCount( $spool_dir_1 ."/emailrelay.*", 0 ) ;
	Check::fileMatchCount( $spool_dir_2 ."/emailrelay.*.content", $n ) ;
	Check::fileMatchCount( $spool_dir_2 ."/emailrelay.*.envelope", $n ) ;
	Check::fileLineCount(  $server_2->stderr() , 1 , "smtp connection from" ) ;
	Check::fileLineCount(  $server_2->stderr() , 1 , "smtp connection closed" ) ;

	# tear down
	$server_1->kill() ;
	$server_2->kill() ;
	Check::notRunning( $server_1->pid() ) ;
	Check::notRunning( $server_2->pid() ) ;
	$server_1->cleanup() ;
	$server_2->cleanup() ;
	System::deleteSpoolDir( $spool_dir_1 ) ;
	System::deleteSpoolDir( $spool_dir_2 ) ;
}

sub testClientFilterPass
{
	# setup
	my %args = (
		Log => 1 ,
		SpoolDir => 1 ,
		Forward => 1 ,
		ForwardTo => 1 ,
		ClientFilter => 1 ,
		DontServe => 1 ,
		NoDaemon => 1 ,
	) ;
	my $spool_dir_1 = System::createSpoolDir(undef,undef,"spool-1") ;
	my $spool_dir_2 = System::createSpoolDir(undef,undef,"spool-2") ;
	System::submitSmallMessage($spool_dir_1) ;
	System::submitSmallMessage($spool_dir_1) ;
	Check::fileMatchCount( $spool_dir_1 ."/emailrelay.*.envelope", 2 ) ;
	my $server_1 = new Server(undef,undef,undef,$spool_dir_1) ;
	my $server_2 = new Server($server_1->smtpPort()+100,undef,$server_1->adminPort()+100,$spool_dir_2) ;
	my %server_args = %args ;
	delete $server_args{Forward} ;
	delete $server_args{ForwardTo} ;
	delete $server_args{ClientFilter} ;
	delete $server_args{DontServe} ;
	delete $server_args{NoDaemon} ;
	$server_args{PidFile} = 1 ;
	$server_args{Port} = 1 ;
	Check::ok( $server_2->run(\%server_args) ) ;
	Check::running( $server_2->pid() , $server_2->message() ) ;
	$server_1->set_dst("localhost:".$server_2->smtpPort()) ;
	my $outputfile = System::tempfile("output",System::unix()?"/tmp":undef) ; # /tmp is writeable by daemon
	Filter::create( $server_1->clientFilter() , {
			unix => [
				"echo \"\$\@\" | sed 's/^/arg: /' > $outputfile" ,
				"env | sed 's/^/env: /' >> $outputfile" , 
				"exit 0" ,
			] ,
			win32 => [
				"var fs = WScript.CreateObject(\"Scripting.FileSystemObject\");" ,
				"var out_ = fs.OpenTextFile( \"$outputfile\" , 8 , true ) ;" ,
				"out_.WriteLine( \"arg: \" + WScript.Arguments(0) ) ;" ,
				"WScript.Quit(0) ;" ,
			] ,
		} ) ;
	Check::ok( $server_1->run(\%args) ) ;
	System::sleep_cs( 50 ) ;

	# test that the client filter runs and the messages are forwarded
	Check::fileExists( $outputfile , "no output file generated by the client filter" ) ;
	Check::fileMatchCount( $spool_dir_1 ."/emailrelay.*", 0 ) ;
	Check::fileMatchCount( $spool_dir_2 ."/emailrelay.*.envelope", 2 ) ;
	Check::fileMatchCount( $spool_dir_2 ."/emailrelay.*.content", 2 ) ;

	# tear down
	$server_2->kill() ;
	Check::notRunning( $server_2->pid() ) ;
	$server_1->cleanup() ;
	$server_2->cleanup() ;
	System::deleteSpoolDir( $spool_dir_1 ) ;
	System::deleteSpoolDir( $spool_dir_2 ) ;
	unlink( $outputfile ) ;
}

sub testClientFilterBlock
{
	# setup
	my %args = (
		Log => 1 ,
		SpoolDir => 1 ,
		Forward => 1 ,
		ForwardTo => 1 ,
		ClientFilter => 1 ,
		DontServe => 1 ,
		NoDaemon => 1 ,
		Hidden => 1 ,
	) ;
	my $spool_dir_1 = System::createSpoolDir(undef,undef,"spool-1") ;
	my $spool_dir_2 = System::createSpoolDir(undef,undef,"spool-2") ;
	System::submitSmallMessage($spool_dir_1) ;
	System::submitSmallMessage($spool_dir_1) ;
	Check::fileMatchCount( $spool_dir_1 ."/emailrelay.*.envelope", 2 ) ;
	my $server_1 = new Server(undef,undef,undef,$spool_dir_1) ;
	my $server_2 = new Server($server_1->smtpPort()+100,undef,$server_1->adminPort()+100,$spool_dir_2) ;
	my %server_args = %args ;
	delete $server_args{Forward} ;
	delete $server_args{ForwardTo} ;
	delete $server_args{ClientFilter} ;
	delete $server_args{DontServe} ;
	delete $server_args{NoDaemon} ;
	delete $server_args{Hidden} ;
	$server_args{PidFile} = 1 ;
	$server_args{Port} = 1 ;
	Check::ok( $server_2->run(\%server_args) ) ;
	Check::running( $server_2->pid() , $server_2->message() ) ;
	$server_1->set_dst("localhost:".$server_2->smtpPort()) ;
	my $outputfile = System::tempfile("output",System::unix()?"/tmp":undef) ; # /tmp is writeable by daemon
	Filter::create( $server_1->clientFilter() , {
			unix => [
				"echo '<<foo bar>>sldkfj'" , 
				"echo a > $outputfile" , 
				"exit 13" ,
			] ,
			win32 => [
				"WScript.StdOut.WriteLine(\"<<foo bar>>sldkfj\") ;" , 
				"var fs = WScript.CreateObject(\"Scripting.FileSystemObject\");" ,
				"var out_ = fs.OpenTextFile( \"$outputfile\" , 8 , true ) ;" ,
				"out_.WriteLine(\"a\") ;" ,
				"WScript.Quit(13) ;" ,
			] ,
		} ) ;
	Check::ok( $server_1->run(\%args) ) ;
	System::sleep_cs( 50 ) ;

	# test that the client filter runs and the messages are failed
	Check::fileExists( $outputfile , "no output file generated by the client filter" ) ;
	Check::fileMatchCount( $spool_dir_1 ."/emailrelay.*.content", 2 ) ;
	Check::fileMatchCount( $spool_dir_1 ."/emailrelay.*.envelope.bad", 2 ) ;
	Check::fileMatchCount( $spool_dir_2 ."/emailrelay.*", 0 ) ;

	# tear down
	$server_2->kill() ;
	Check::notRunning( $server_2->pid() ) ;
	$server_1->cleanup() ;
	$server_2->cleanup() ;
	System::deleteSpoolDir( $spool_dir_1 , 1 ) ;
	System::deleteSpoolDir( $spool_dir_2 , 1 ) ;
	unlink( $outputfile ) ;
}

sub testClientGivenUnknownMechanisms
{
	# setup
	my %args = (
		Log => 1 ,
		SpoolDir => 1 ,
		Forward => 1 ,
		ForwardTo => 1 ,
		DontServe => 1 ,
		NoDaemon => 1 ,
		ClientAuth => 1 ,
		Hidden => 1 ,
	) ;
	my $spool_dir = System::createSpoolDir() ;
	my $test_server = new TestServer() ;
	$test_server->run( "--auth-foo-bar" ) ;
	System::submitSmallMessage($spool_dir) ;
	System::submitSmallMessage($spool_dir) ;
	Check::fileMatchCount( $spool_dir ."/emailrelay.*.envelope", 2 ) ;
	my $emailrelay = new Server( undef , undef , undef , $spool_dir ) ;
	System::createFile( $emailrelay->clientSecrets() , "client login me secret" ) ;
	$emailrelay->set_dst( "localhost:".$test_server->port() ) ;

	# test that protocol fails and one message fails (change for 1.9)
	Check::ok( $emailrelay->run( \%args ) ) ;
	System::sleep_cs( 50 ) ;
	Check::fileContains( $emailrelay->stderr() , "cannot do authentication mandated by " ) ;
	Check::fileMatchCount( $spool_dir ."/emailrelay.*.envelope", 1 ) ;
	Check::fileMatchCount( $spool_dir ."/emailrelay.*.envelope.bad", 1 ) ;

	# tear down
	$emailrelay->cleanup() ;
	$test_server->cleanup() ;
	System::deleteSpoolDir( $spool_dir , 1 ) ;
}

sub testClientAuthenticationFailure
{
	# setup
	my %args = (
		Log => 1 ,
		SpoolDir => 1 ,
		Forward => 1 ,
		ForwardTo => 1 ,
		DontServe => 1 ,
		NoDaemon => 1 ,
		ClientAuth => 1 ,
		Hidden => 1 ,
	) ;
	my $spool_dir = System::createSpoolDir() ;
	my $test_server = new TestServer() ;
	$test_server->run( "--auth-login" ) ;
	System::submitSmallMessage($spool_dir) ;
	System::submitSmallMessage($spool_dir) ;
	Check::fileMatchCount( $spool_dir ."/emailrelay.*.envelope", 2 ) ;
	my $emailrelay = new Server( undef , undef , undef , $spool_dir ) ;
	System::createFile( $emailrelay->clientSecrets() , "client login me secret" ) ;
	$emailrelay->set_dst( "localhost:".$test_server->port() ) ;

	# test that protocol fails and one message fails (1.9)
	Check::ok( $emailrelay->run( \%args ) ) ;
	System::sleep_cs( 50 ) ;
	Check::fileContains( $emailrelay->stderr() , "authentication error" ) ;
	Check::fileMatchCount( $spool_dir ."/emailrelay.*.envelope", 1 ) ;
	Check::fileMatchCount( $spool_dir ."/emailrelay.*.envelope.bad", 1 ) ;

	# tear down
	$emailrelay->cleanup() ;
	$test_server->cleanup() ;
	System::deleteSpoolDir( $spool_dir , 1 ) ;
}

sub testClientMessageFailure
{
	# setup
	my %args = (
		Log => 1 ,
		SpoolDir => 1 ,
		Forward => 1 ,
		ForwardTo => 1 ,
		DontServe => 1 ,
		NoDaemon => 1 ,
		Hidden => 1 ,
	) ;
	my $spool_dir = System::createSpoolDir() ;
	my $test_server = new TestServer() ;
	$test_server->run( "--fail-at 2" ) ;
	System::submitSmallMessage($spool_dir) ;
	System::submitSmallMessage($spool_dir) ;
	System::submitSmallMessage($spool_dir) ;
	System::submitSmallMessage($spool_dir) ;
	Check::fileMatchCount( $spool_dir ."/emailrelay.*.envelope", 4 ) ;
	my $emailrelay = new Server( undef , undef , undef , $spool_dir ) ;
	$emailrelay->set_dst( "localhost:".$test_server->port() ) ;

	# test that two of the four messages are left as ".bad"
	Check::ok( $emailrelay->run( \%args ) ) ;
	System::sleep_cs( 50 ) ;
	Check::fileMatchCount( $spool_dir ."/emailrelay.*.content", 2 ) ;
	Check::fileMatchCount( $spool_dir ."/emailrelay.*.envelope.bad", 2 ) ;
	Check::fileMatchCount( $spool_dir ."/emailrelay.*.envelope", 0 ) ;

	# tear down
	$emailrelay->cleanup() ;
	$test_server->cleanup() ;
	System::deleteSpoolDir( $spool_dir , 1 ) ;
}

sub testClientInvalidRecipients
{
	# setup
	my %args = (
		Log => 1 ,
		SpoolDir => 1 ,
		Forward => 1 ,
		ForwardTo => 1 ,
		DontServe => 1 ,
		NoDaemon => 1 ,
		Hidden => 1 ,
	) ;
	my $spool_dir = System::createSpoolDir() ;
	my $test_server = new TestServer() ;
	$test_server->run() ;
	System::submitSmallMessage( $spool_dir , undef , "acceptme\@there.com" ) ;
	System::submitSmallMessage( $spool_dir , undef , "acceptme1\@there.com" , "acceptme2\@there.com" ) ;
	System::submitSmallMessage( $spool_dir , undef , "acceptme\@there.com" , "rejectme\@there.com" ) ;
	System::submitSmallMessage( $spool_dir , undef , "rejectme\@there.com" ) ;
	System::submitSmallMessage( $spool_dir , undef , "rejectme1\@there.com" , "rejectme2\@there.com" ) ;
	Check::fileMatchCount( $spool_dir ."/emailrelay.*.envelope", 5 ) ;
	my $emailrelay = new Server( undef , undef , undef , $spool_dir ) ;
	$emailrelay->set_dst( "localhost:".$test_server->port() ) ;

	# test that the three "rejectme" messages out of five are left as ".bad"
	Check::ok( $emailrelay->run( \%args ) ) ;
	System::sleep_cs( 50 ) ;
	Check::fileMatchCount( $spool_dir ."/emailrelay.*.content", 3 ) ;
	Check::fileMatchCount( $spool_dir ."/emailrelay.*.envelope.bad", 3 ) ;
	Check::fileMatchCount( $spool_dir ."/emailrelay.*.envelope", 0 ) ;
	Check::allFilesContain( $spool_dir ."/emailrelay.*.envelope.bad" , "one or more recipients rejected" ) ;

	# tear down
	$emailrelay->cleanup() ;
	$test_server->cleanup() ;
	System::deleteSpoolDir( $spool_dir , 1 ) ;
}

# ===

sub run
{
	my ( $name ) = @_ ;
	print "running $name ... " ;
	eval $name."()";
	my $error = $@ ;
	if( $error )
	{
		chomp $error ;
		if( $error =~ m/^skip/ )
		{
			print $error , "\n" ;
		}
		else
		{
			print "failed: $error\n" ;
			$run_all_ok = 0 ;
			if( ! opt_keep_going )
			{
				return 0 ;
			}
		}
	}
	else
	{
		print "ok\n" ;
	}
	return 1 ;
}

# introspection
my @tests = () ;
my $f = new FileHandle( $0 ) ;
while( <$f> )
{
	my $line = $_ ; chomp $line ;
	if( $line =~ m/^sub test/ )
	{
		my @line_part = split( /\s+/ , $line ) ;
		push @tests , $line_part[1] ;
	}
}

# start from a clean state
if( !System::unix() )
{
	System::killall( "emailrelay-*" ) ;
}

my %run_tests = () ;
my %skip_tests = () ;
my $run_tests = 0 ;
for my $arg ( @ARGV ) 
{ 
	if( $arg =~ m/^-/ )
	{
		$skip_tests{substr($arg,1)} = 1 ;
	}
	else
	{
		$arg =~ s/^\+// ;
		$run_tests{$arg} = 1 ;
		$run_tests++ ;
	}
}
for my $test ( @tests )
{
	if( ( $run_tests && !exists $run_tests{$test} ) || $skip_tests{$test} )
	{
		print "(skipping $test)\n" ;
	}
	else
	{
		last if !run( $test ) ;
	}
}

if( !$run_all_ok )
{
	print "failed\n" ;
	cleanup() ;
	exit 1 ;
}

sub cleanup
{
	for my $pid ( @Server::pid_list , @Helper::pid_list , @TestServer::pid_list )
	{
		if( defined($pid) && $pid > 0 )
		{
			#print "killing $pid\n" ;
			kill 15 , $pid ;
		}
	}
}

END
{
	cleanup() ;
}

exit 0 ;

