The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
=head1 NAME

t/server/start - simple POP3 server for testing Mail::Transport::POP3

=head1 SYNOPSIS

 open( $pop3,"$^X t/server/start t/messages | " );

 open( $pop3,"$^X t/server/start t/messages minimal | " );

 open( $pop3,"$^X t/server/start t/messages apoponly | " );

 open( $pop3,"$^X t/server/start t/messages autodelete | " );

 open( $pop3,"$^X t/server/start t/messages noextra | " );

 open( $pop3,"$^X t/server/start t/messages standardport | " );

=head1 DESCRIPTION

This POP3 server is created for testing the Mail::Transport::POP3 only.  It
B<cannot be used> as real POP3 server (yet).

The server takes on a randomly selected, free port to prevent interference
with existing applications. Start the server by running this script from
another script while capturing the output to STDOUT, e.g. like:

  open( my $pop3,"$^X t/server/start t/messages |" )
   or die "Could not start POP3 server: $!\n";
  my $port = <$pop3>;

The returned $pop3 file handle produces informational texts: it will tell
you the port which is occupied by the server, and when the server shuts down.
It will also report some statistics on the performance of the server.

The server will be bound to localhost (127.0.0.1) at the port number of the
first line that is printed to STDOUT by this script.

The first parameter to the script indicates the directory in which the actual
messages (each message as a seperate file) are located.  In the example, this
is "t/messages".

Any other parameters to the script are optional: they consist of keywords to
indicate any settings or peculiarities of certain POP3 server implementations.
The following keywords are recognised:

=over 2

=item minimal

If the keyword "minimal" is specified, only the minimal set of POP3 commands
will be allowed (i.e. USER, PASS, STAT, LIST, RETR, DELE, RSET, NOOP and QUIT).
The optional POP3 commands (APOP, TOP and UIDL) are also supported if this
keyword is B<not> specified.

=item apoponly

If the keyword "apoponly" is specified, then authorization will only be
allowed with the APOP command (i.e. authorization with USER will yield a
negative response).  Please note that you cannot use this together with the
"minimal" keyword, as APOP is one of the optional POP3 commands (which is
excluded if you use the "minimal" keyword).

=item autodelete

If the keyword "autodelete" is specified, any messages that are completely
retrieved with RETR or TOP (without specification of number of lines in the
body to return) will be automatically marked for deletion.  This will cause
those messages to be deleted if the session is finished with a QUIT command.
This coincides with system resource restrictions imposed by some providers.

=item noextra

If the keyword "noextra" is specified, then all messages will be served with
a check for a CRLF pair at the end of the original messasge: if a CRLF is
found, then only ".\r\n" will be added to indicate the end of a message that
are retrieved with RETR or TOP.

=item standardport

If the keyword "standardport" is specified, then an attempt will be made to
start the POP3 server on port 110, the standard POP3 port.  Please note that
this will only be successful if the current user has sufficient privileges
(usually only the root user will be allowed to listen on ports < 1024).

=back

User name is always "user" and the correct password is always "password".
Any other combination will always fail.  APOP authorization can be used if
the "minimal" keyword is B<not> specified.  The following script will help
you in debugging APOP authorization:

  use Digest::MD5 qw(md5_hex);
  while (<>) {
    s#\r?\n?$##s;
    print md5_hex( $_.'password' )."\n";
  }

Copy the string that was sent by the initial greeting of the server (including
the <> brackets), paste this into the running script, press ENTER.  The script
will respond with a 32 character hexadecimal string.  Copy that and the enter
the authorization thus:

  APOP user 0123456789abcdef0123456789abcdef

Note that the above hex string is only an example of course.

The following commands do B<not> exist in the POP3 protocol, but are intended
to simulate certain events.

The BREAK command can be used to simulate the breaking of a connection.
After a BREAK is received, the connection is broken by the server (without
sending a response to the client).  No messages will be deleted even if any
messages were marked for deletion.  This can also be used to simulate a
timeout, of course.

The EXIT command can be used for test-suites: when sent from the client, it
will cause the server to shut down (as if an EXIT was sent) whenever the
client does a QUIT command.  When the servers shuts down, its prints its
statistics on STDOUT.  Statistics returned are:

 - number of succesful logins
 - each command + frequency in alphabetical order

so a statistics list for one successful session could be:

 1
 DELE 102
 EXIT 1
 LIST 1
 PASS 1
 QUIT 1
 RETR 102
 STAT 1
 UIDL 1
 USER 1

=cut

# Make sure we do everything by the book
# Make sure we can do sockets
# Make sure we can do digests

use strict;
use IO::Socket;
use Digest::MD5 qw(md5_hex);

# Obtain the directory to work on
# Remove trailing slash if any
# Die now if there is no directory
# Die now if we can't work with it

my $directory = shift;
$directory =~ s#/$##;
die qq(Must specify directory to work with\n) unless $directory;
die qq(Trouble using directory "$directory": $!\n)
 unless -d $directory and -w _;

# Initialize the flag settings

my $minimal = 0;
my $apoponly = 0;
my $autodelete = 0;
my $noextra = 0;
my $exitonquit = 0;
my $exitnow = 0;
my @port;

# While there are keywords specified
#  Set appropriate flags if so specified

while (my $keyword = shift) {
  $minimal = ($keyword eq 'minimal');
  $apoponly = ($keyword eq 'apoponly');
  $autodelete = ($keyword eq 'autodelete');
  $noextra = ($keyword eq 'noextra');
  @port = qw(LocalPort 110) if $keyword eq 'standardport';
}

# Make sure no buffering takes place
# Create a server that can only take one connection at a time

$| = 1;
my $server = IO::Socket::INET->new(
  Type      => SOCK_STREAM,
  Listen    => 1,
  @port,
) or die "Couldn't start a POP3 server:\n $@\n";

# Find out the port we're running on
# Let the caller know which port we're running on

my $port = $server->sockport;
print "$port\n";

# Initialize the connected flag
# Initialize the list of available messages
# Initialize the hash of message ordinal numbers to delete
# Initialize the hash of message ordinal numbers to delete automatically

my $connected = 0;
my @message;
my %delete;
my %autodelete;

# Initialize user
# Initialize digest password field (used by APOP only)
# Initialize the line ending on output

my $user = '';
my $digest;
my $lf = "\x0D\x0A";  # always CRLF

# Number of successful logins performed
# Hash with frequency of each command

my $logins = 0;
my %command;

# While the server is running and we got a new client
#  Initialize the APOP initialization string
#  If this is a minimal POP3 server
#   Don't make it appear we can do POP3
#  Else
#   Create the APOP authentication string
#   Let the client know we're there and we can do APOP

SERVER: while (my $client = $server->accept()) {
  my $apop = '';
  if ($minimal) {
    print $client qq(+OK Welcome to the test-suite POP3 server$lf);
  } else {
    $apop = "<$$.".time().'@localhost>';
    print $client qq(+OK $apop$lf);
  }

#  Obtain list of files in message directory
#  Reset the messages to be (automatically) deleted hashes

  @message = <$directory/*>;
  %autodelete = %delete = ();

#  While the client is asking us stuff to do
#   Lose the line ending (whatever it is)
#   Split into a command and parameters
#   Make sure the command is always uppercase (easier checks later)
#   Make sure the parameters are defined (if empty)

  while (<$client>) {
    s#\r?\n$##s;
    my ($command,$parameters) = split( /\s+/,$_,2 );
    $command = uc($command);
    $parameters = '' unless defined($parameters);

#   Count this command for the statistics
#   Outloop if quitting this client

    $command{$command}++;
    last if $command eq 'BREAK';

#   If we're connected
#    Allow for variable references
#    If there is a subroutine for this command
#     Execute it with the given parameters and return result
#     Send result to client if there is something to connect
#     Stop server is so requested
#     Outloop if we're no longer connected
#    Else
#     Indicate it's not implemented

    if ($connected) {
      no strict 'refs';
      if (exists( &$command )) {
        my @return = &{$command}( split( /\s+/,$parameters ) );
        print $client @return if @return;
	last SERVER if $exitnow;
        last unless $connected;
      } else {
        print $client "-ERR unimplemented$lf";
      }

#   Elseif we're quitting without a connection
#    Show that we agree
#    And outloop

    } elsif ($command eq 'QUIT') {
      print $client "+OK$lf";
      last;

#   Elseif we're trying APOP authentication
#    If we have a minimal POP3 server
#     Show that this isn't implemented
#     And reloop

    } elsif ($command eq 'APOP') {
      if ($minimal) {
        print $client "-ERR unimplemented$lf";
        next;
      }

#    Obtain the user name and the digest
#    Log the user in if client gives the right credentials
#    Send the result to the client

      ($user,$digest) = split( /\s+/,$parameters );
      my @return = login(
       $user eq 'user' and
       $digest eq md5_hex( $apop.'password')
      );
      print $client @return;

#   Elseif we have a user name (and we're not connected yet)
#    Log the user in if client gives the right credentials now and before
#    Send the result to the client

    } elsif ($user) {
      my @return = login(
       $command eq 'PASS' and
       $user eq 'user' and
       $parameters eq 'password'
      );
      print $client @return;

#   Elseif the user name is passed (and none given before)
#    If we only allow APOP
#     Let the client know it's not ok
#    Else
#     Save the user name (for later checking with PASS)
#     Let the client know it's ok so far

    } elsif ($command eq 'USER') {
      if ($apoponly) {
        print $client "-ERR APOP authorization allowed only$lf";
      } else {
        $user = $parameters;
        print $client "+OK$lf";
      }

#   Elseif the password is given (but no user name before)
#    Let the client know it's wrong
#   Else (attempting to do anything else without authorization)
#    Let the client know it's wrong

    } elsif ($command eq 'PASS') {
      print $client "-ERR user first$lf";
    } else {
      print $client "-ERR authorization first$lf";
    }
  }

#  Reset user name
#  Reset connected flag
#  Shut down the client connection

  $user = '';
  $connected = 0;
  close( $client );
}

# Show number of successful logins
# For all the commands that were issued
#  Return name and frequency of it
# And shut down the server

print "$logins\n";
foreach (sort keys %command) {
  print "$_ $command{$_}\n";
}
close($server);

#------------------------------------------------------------------------
# OUT: 1 whatever needs to be sent to client

sub STAT {

# Initialize number of messages
# Initialize number of bytes they have
# Initialize ordinal number

  my $messages = 0;
  my $octets = 0;
  my $ordinal = 0;

# For all of the messages
#  Reloop if message marked as delete, incrementing ordina on the fly
#  Increment number of messages
#  Add number of bytes
# Return the result

  foreach (@message) {
    next if exists( $delete{$ordinal++} );
    $messages++;
    $octets += -s;
  }
  return "+OK $messages $octets$lf";
} #STAT

#------------------------------------------------------------------------
# OUT: 1 whatever needs to be sent to client

sub UIDL {

# Return now if running a minimal POP3 server

  return "-ERR unimplemented$lf" if $minimal;

# Initialize message number
# If a number was specified
#  Obtain ordinal number and possible error message
#  Return error message if there is one
#  Return the message number and the identifier of the message otherwise

  my $number = shift;
  if (defined($number)) {
    my ($ordinal,$error) = ordinal( $number,1 );
    return $error if $error;
    return "+OK $number $message[$ordinal]$lf";
  }

# Initialize ordinal number
# Initialize text to be returned
# For all of the messages
#  Reloop if message marked as deleted, incrementing ordinal on the fly
#  Add the ordinal number and the identifier (just use filename for that)
# Return the result with an extra . at the end to indicate end of list

  my $ordinal = 0;
  my $text = "+OK$lf";
  foreach (@message) {
    next if exists( $delete{$ordinal++} );
    $text .= "$ordinal $_$lf"; # external numbers 1-based, internal 0-based
  }
  return "$text.$lf";
} #UIDL

#------------------------------------------------------------------------
#  IN: 1 message to obtain (optionally)
# OUT: 1 whatever needs to be sent to client

sub LIST {

# Initialize message number
# If a number was specified
#  Obtain ordinal number and possible error message
#  Return error message if there is one
#  Return the message number and size of message otherwise

  my $number = shift;
  if (defined($number)) {
    my ($ordinal,$error) = ordinal( $number,1 );
    return $error if $error;
    return "+OK $number ".(-s $message[$ordinal]).$lf;
  }

# Initialize ordinal number
# Initialize text to be returned
# For all of the messages
#  Reloop if message marked as deleted, incrementing ordinal on the fly
#  Add the ordinal number and the identifier (just use filename for that)
# Return the result with an extra . at the end to indicate end of list
    
  my $ordinal = 0;
  my $text = "+OK$lf";
  foreach (@message) {
    next if exists( $delete{$ordinal++} );
    $text .= "$ordinal ".(-s).$lf; # external numbers 1-based, internal 0-based
  }
  return "$text.$lf";
} #LIST

#------------------------------------------------------------------------
#  IN: 1 ordinal number of message to retrieve
# OUT: 1 whatever needs to be sent to client

sub RETR {

# Obtain ordinal number and possible error message
# Return now if there was an error message

  my ($ordinal,$error) = ordinal( shift,1 );
  return $error if $error;

# Open file for reading or return with empty message
# Initialize text to be returned
# While there are lines to be returned
#  Make sure any period at the start of the line becomes a double period
#  Add the line to the text to be returned

  open( my $handle,'<',$message[$ordinal] ) or return "+OK$lf.$lf";
  my $text = "+OK$lf";
  while (<$handle>) {
    s#^\.#..#;
    $text .= $_;
  }

# Mark this message to be deleted automatically if flag set
# Add the right marker to the text
# Return the finished text

  $autodelete{$ordinal} = undef if $autodelete;
  addmarker( \$text );
  $text;
} #RETR

#------------------------------------------------------------------------
#  IN: 1 ordinal number of message to retrieve
#      2 number of lines of the message to retrieve
# OUT: 1 whatever needs to be sent to client

sub TOP {

# Return now if running a minimal POP3 server
# Obtain ordinal number and possible error message
# Return now if there was an error message

  return "-ERR unimplemented$lf" if $minimal;
  my ($ordinal,$error) = ordinal( shift,1 );
  return $error if $error;

# Open file for reading or return with empty message
# Initialize text to be returned

  open( my $handle,'<',$message[$ordinal] ) or return "+OK$lf.$lf";
  my $text = "+OK$lf";

# Obtain the number of lines
# If a number of lines was specified
#  While there are lines to be returned
#   Make sure any period at the start of the line becomes a double period
#   Add the line to the text to be returned
#   Outloop if we're reached the end of the headers

  my $lines = shift;
  if (defined($lines)) {
    while (<$handle>) {
      s#^\.#..#;
      $text .= $_;
      last if m#^\s+$#s;
    }

#  While there are lines to be fetched
#   Outloop if no line left to be fetched
#   Make sure any period at the start of the line becomes a double period
#   Add the line to the text to be returned

    while ($lines--) {
      last unless defined($_ = <$handle>);
      s#^\.#..#;
      $text .= $_;
    }

# Else (no limit)
#  While there are lines to be returned
#   Make sure any period at the start of the line becomes a double period
#   Add the line to the text to be returned
#  Mark this message to be deleted automatically if flag set

  } else {
    while (<$handle>) {
      s#^\.#..#;
      $text .= $_;
    }
    $autodelete{$ordinal} = undef if $autodelete;
  }

# Add the right marker to the text
# Return the result with an extra . at the end to indicate end of list

  addmarker( \$text );
  $text;
} #TOP

#------------------------------------------------------------------------
#  IN: 1 ordinal number of message to delete
# OUT: 1 whatever needs to be sent to client

sub DELE {

# Obtain ordinal number and possible error message
# Return now if there was an error message
# Mark this message as deletable
# Return the result with an extra . at the end to indicate end of list

  my ($ordinal,$error) = ordinal( shift,1 );
  return $error if $error;
  $delete{$ordinal} = undef;
  return "+OK$lf";
} #DELE

#------------------------------------------------------------------------
#  IN: 1 ordinal number of message to undelete
# OUT: 1 whatever needs to be sent to client

sub RSET {

# Obtain ordinal number and possible error message
# Return now if there was an error message
# Unmark this message as deletable
# Return the result with an extra . at the end to indicate end of list

  my ($ordinal,$error) = ordinal( shift );
  return $error if $error;
  delete( $delete{$ordinal} );
  return "+OK$lf";
} #RSET

#------------------------------------------------------------------------
# OUT: 1 whatever needs to be sent to client

sub NOOP { "+OK$lf" } #NOOP

#------------------------------------------------------------------------

sub EXIT { $exitonquit = 1; return } #EXIT

#------------------------------------------------------------------------
# OUT: 1 whatever needs to be sent to client

sub QUIT {

# Remove all of the files that were supposed to be deleted
# Remove all of the files that were supposed to be deleted automatically
# Set exit now flag if QUIT is to operate as EXIT
# Mark the connection as ended
# Let the client now it was fun while it lasted

  unlink( map {$message[$_]} keys %delete );
  unlink( map {$message[$_]} keys %autodelete );
  $exitnow = $exitonquit;
  $connected = 0;
  return "+OK$lf";
} #QUIT

#------------------------------------------------------------------------
#  IN: 1 flag whether login successful
# OUT: 1 what needs to be returned to the client

sub login {

# If successful
#  Increment number of successful logins
#  Set connected flag
#  Let the client know it's ok

  if (shift) {
    $logins++;
    $connected = 1;
    return "+OK$lf";
  }

# Reset the user that was entered before
# Let the client know authorization has failed

  $user = '';
  return "-ERR authorization failed$lf";
} #login

#------------------------------------------------------------------------
#  IN: 1 ordinal number of message
#      2 flag: check whether message deleted already
# OUT: 1 normalize message number
#      2 error message (if any)

sub ordinal {

# Obtain the message number
# Initialize error message
# Set error if too low
# Set error if zero
# Set error if too high

  my $ordinal = shift;
  my $error = '';
  $error ||= "-ERR syntax error$lf" if $ordinal < 0;
  $error ||= "-ERR messages are counted from 1$lf" if $ordinal == 0;
  $error ||= "-ERR not that many messages$lf" if $ordinal > @message;

# Normalize for arrays
# Set error if checking for deletion and already deleted
# Return the result

  $ordinal--;
  $error ||= "-ERR already deleted$lf" if shift and exists( $delete{$ordinal} );
  return ($ordinal,$error);
} #ordinal

#------------------------------------------------------------------------
#  IN: 1 reference to text (to add the right end-of-data marker to)

sub addmarker {

# Obtain the reference to the text
# If we should check for extra newlines at the end
#  Add the right stuff depending on the end of the text so far
# Else
#  Add it as most POP3 servers do

  my $textref = shift;
  if ($noextra) {
    $$textref .= ($$textref =~ m#\r\n$#so ? ".$lf" : "$lf.$lf");
  } else {
    $$textref .= "$lf.$lf";
  }
}