=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 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 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 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 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"; } }