#!/usr/bin/perl -w # Perl SMTP client template - send EMail using Perl Net::Telnet module # # XXX The symbol XXX flags things that need fixing or removing. # XXX Remove or replace these XXX comments when you fix the code. # Perl: flags Perl comments. Remove all these before you submit. # # XXX (CST8165 Lab template - to be completed by the student.) # # XXX this will be the full calling sequence when finished: # # $0 --to user@domain.com --from sender@domain.com \ # --smtpserver mail.domain.com --port 25 # # XXX Right now (under development) it uses: $0 [ hostname [ port ] ] # XXX Exit status: # XXX Mail clients should exit using only status codes defined in # XXX This program uses "die", which doesn't do that. Fix it. # XXX Comments labelled "Perl:" are teacher comments explaining to # XXX students of the language how Perl works. They would never appear # XXX as comments in a production Perl program, since they have nothing to # XXX do with how the program works. Don't write comments like these in # XXX your submitted Perl programs (unless you are a teacher). # XXX This program needs proper argument checking and handling added. # XXX Right now (under development) it uses: $0 [ hostname [ port ] ] # XXX with no argument validation or checking. Fix this! # XXX When finished, the script will use the proper calling sequence # XXX and will validate each argument properly before using it. # Perl: Always use "perl -w" and "use strict" in your Perl programs! # use strict; use Net::Telnet; $| = 1; # all output will be flushed (not buffered) # Perl: You can fetch any environment variable inside Perl this way: # Perl: In strings, you must escape the \@ if not expanding an array variable # &Debug("Hello $ENV{USER} ! Is your email $ENV{USER}\@gmail.com ?"); # Some constants (set these small for debugging and testing): # my $SMTP_OPEN_TIMEOUT = 10; # seconds to wait for connection to SMTP server my $SMTP_LINE_TIMEOUT = 10; # seconds to wait for each SMTP response # Perl: Some people prefer to declare variables when first used. # Perl: Others prefer to declare variables at the start of the program. # Perl: Do it one way or the other. Add comments! # my $conn; # the single Net::Telnet connection handle my $ret; # for testing return values of various things my @lines; # array of lines returned by SMTP server my $code; # first three characters on line returned by SMTP server my $want; # what SMTP response code we are expecting to receive my $maxmsgsize; # size of message from SMTP EHLO SIZE option ############################################################################ # Step 1 - Parse the command line and get to, from, smtpserver, and port. # Do some validation of the syntax of each argument. # Use reasonable defaults for some of the arguments. # Print a Usage message and exit if the parsing fails. # Perl: The command line arguments are in array @ARGV. # Perl: You can find the number of elements in an array using $#ARGV # Perl: or even @ARGV in a scalar (not array) context, e.g. $x = @ARGV;. # Perl: Check out a nice argument parsing library: man Getopt::Long # XXX You need to write this code here ... # XXX Make sure you exit with a code on error. ############################################################################ # Step 2 - Create a new connection; turn off TELNET-specific mode. # XXX you need to fix this to use parsed and validated command line arguments $conn = new Net::Telnet( Telnetmode => 0 ); $ret = $conn->open( Host => $ARGV[0] || 'localhost', # XXX fix to use command line args Port => $ARGV[1] || 25, # XXX fix to use command line args Errmode => 'return', # don't kill program on error; return undef Timeout => $SMTP_OPEN_TIMEOUT # timeout waiting for open to complete ); unless( $ret ){ # Perl: Function calls have to be outside the quoted string to work. # Perl: (Perl $variables expand in quoted strings; function calls do not.) # Perl: Both "warn()" and "die()" print on standard error. # Perl: Both print line numbers unless the string ends in a newline. # Perl: We could replace warn()+exit() with just die(), below; # Perl: but, die() doesn't exit with the right return code. # warn "$0: open error: ", $conn->errmsg(), "\n"; # XXX Should exit only with the values/reasons defined in # XXX Fix this exit status to conform to : exit(1); } ############################################################################ # Step 3 - Receive the SMTP greeting issued by the remote SMTP server. # Ref: RFC2821 Section 4.3.2 # # In SMTP, the server issues the first line, not the client. # So we, as client, do a read first: # ($code,@lines) = &MyGetline($conn); &Debug("server's first response is:\n", join("\n ",@lines)); # XXX this repeated code below should probably be moved to a function # Perl: "ne" is a string comparison, "!=" only works on numbers; # Perl: we must use "ne" in case $code is not numeric. # $want = 220; if ( $code ne $want ){ # Perl: Here are two ways to write exactly the same thing, one with # Perl: variables inside the quotes and one with them outside: # XXX Should exit only with the values/reasons defined in # XXX Fix this exit status to conform to : die "Got '$code': Did not get $want response from server: $lines[0]\n"; # die "Got '", $code, "': Did not get ", $want, # " response from server: ", $lines[0], "\n"; } # XXX We must more flexible in accepting "ok" responses from the server; # XXX insisting on the exact 220 code match is wrong according to the RFC: # XXX See RFC2821 4.2 and 4.2.1 p.40-42 and especially # XXX note the reference to "An unsophisticated SMTP client" # Perl: Showing more of Perl's features: # Perl: Do a case-insensitive regexp match to see if ESMTP is present. # Perl: Use parentheses in the regexp to capture the rest of the line # Perl: into variable $1 for output in the line below. # Perl: Multiple pairs of parentheses can capture into $1, $2, $3, etc. # if ( $lines[0] =~ / ESMTP (.*)/i ) { &Debug("this server is an Extended SMTP server:\n $1."); } ############################################################################ # Step 4 - Ask for an extended SMTP session with EHLO # # Perl: you can run any shell command and capture the output using `...` my $hostname = `hostname`; # shell command to get the name of this machine chomp($hostname); Debug ("sending to server: EHLO $hostname"); $conn->print("EHLO $hostname"); sleep 1; # this is useful when running under the autotest_smtp.sh script # Get the server's response to EHLO # ($code,@lines) = &MyGetline($conn); &Debug("server's second response is:\n", join("\n ",@lines)); # XXX this repeated code below should probably be moved to a function # must use "ne" in case $code is not numeric # $want = 250; if ( $code ne $want ){ # XXX Should exit only with the values/reasons defined in # XXX Fix this exit status to conform to : die "Got '$code': Did not get $want response from server: $lines[0]\n"; } # XXX We should be more flexible in accepting "ok" responses from the server # Perl: Showing more of Perl's features: # Perl: The expression 1..$#lines generates a new array with the first # Perl: element missing (the first 250 response line is not an SMTP option). # Perl: The for() loop iterates $line over each element in this new array. # Perl: The split() returns an array; we limit the split to two fields # Perl: and assign those two fields to another two-field array # Perl: and throw away the first field, saving only the second. # # The 250 response lines should have all the EHLO SMTP options. # Show just the SMTP options coming back in response to EHLO. # for my $line ( @lines[1..$#lines] ) { my (undef,$option) = split(/[- ]+/,$line,2); # split on blanks or dash &Debug("SMTP Option: '$option'"); # Perl: do a case-insensitive regexp match to find the SIZE option if ( $option =~ /^SIZE /i ){ (undef,$maxmsgsize) = split(/ +/,$option,2); # split option on blanks &Debug("option SIZE found: max message size is '$maxmsgsize'"); } } ############################################################################ # Step 5 and more - # XXX more code to be added here by you, to actually send a message... # XXX For best results using the autotest_smtp.sh script, add a "sleep 1" # XXX in your SMTP client after every write to the server, so that the fake # XXX SMTP server has a chance to echo the line onto the screen before # XXX your client goes on to do the next line. ############################################################################ # Last Step - clean up and exit. # # That's all for now. Quit the SMTP connection, close it, and exit. # Debug ("sending to server: QUIT"); $conn->print("QUIT"); sleep 1; # this is useful when running under the autotest_smtp.sh script # Any response will do after a QUIT! # ($code,@lines) = &MyGetline($conn); &Debug("server's last response is:\n", join("\n ",@lines)); $conn->close(); exit 0; # XXX this value should be $EX_OK copied from #----------------------------------------------------------------------------- # sub MyGetline ( $connection ): # This function gets all of the lines of a continued SMTP server response. # The first and only arg is the open Net::Telnet connection. # Received lines are saved in an array with the trailing \n removed. # The leading 3-character SMTP code and the whole array are returned. # Note that we return the first three characters, even if they aren't # numeric; so, you have to check for valid codes in the caller. # If the connection dies or times out while we are collecting, we # should print a message and exit with a "try again" exit status. # XXX Should exit only using the values/reasons defined in # # Perl: The Net::Telnet module converts between LF and CR+LF on I/O # Perl: so we don't have to worry about it. # sub MyGetline { my $conn = shift; # first and only arg is open Net::Telnet connection my $MIN_LENGTH = 3; # min length from SMTP server is 3-digit code my @getline; # array of lines collected from SMTP server # Loop, reading lines from the SMTP connection. # Append each line read to the end of the @getline array. # Keep getting lines as long as we see SMTP continuation markers. # Exit the looping when there is no more continuation marker. # @getline = (); # array starts empty; each line is appended in the loop for(;;){ # &Debug("start of loop"); # The Timeout here says how long to wait for the line to be read: # Make sure we set Errmode to return instead of killing us! my $tmpline = $conn->getline( Errmode => 'return', # don't kill program on error; return undef Timeout => $SMTP_LINE_TIMEOUT ); # XXX Should exit only with the values/reasons defined in # XXX Fix this exit status to conform to : die "$0: Server connection broken or time out: ", $conn->errmsg() unless $tmpline; # remove any trailing newline from the line before doing length chomp $tmpline; # XXX Should exit only with the values/reasons defined in # XXX Fix this exit status to conform to : die "$0: Server line '$tmpline' shorter than $MIN_LENGTH\n" if length($tmpline) < $MIN_LENGTH; # &Debug("line is '$tmpline'"); # append this line to the array of lines being collected push(@getline,$tmpline); # A line shorter than four characters cannot be a continuation line; # but, we have to accept it anyway. (See RFC2821 section 4.2.) # Extract the fourth character, if any - the SMTP continuation char # my $cont = (length($tmpline)>=4) ? substr($tmpline,3,1) : ' '; # &Debug("cont is '$cont'"); last if $cont ne '-'; # exit loop if no longer continuing # &Debug("bottom of loop"); } # We have collected all the lines, including any continuation # lines. Collected lines have had the newlines removed by chomp(). # Extract just the leading 3-digit SMTP code from the start of the # first line; return the code and the whole array of lines collected. # Note: We don't assume that the code is all-digits! # my $code = substr($getline[0],0,3); # Perl: note that we return an array of values! # Perl: the first array item is the code, everything else is lines return ($code,@getline); } # write all the arguments onto stderr along with program name sub Debug { my $cmd = $0; # copy the program name $cmd =~ s|.*/||; # remove everything up to the last slash warn "$cmd: DEBUG @_\n"; }