Creating mail.pl

Now that we know the requirements, let’s create the code. To keep the code as cross-platform as possible, we start by carefully invoking the Perl interpreter. The two hyphens (--) end further options from taking effect, in case some system parses multiple lines or extra characters and tries to pass them to Perl.

#!/usr/bin/perl --

Next, we need to include (“use” in Perl terminology) some Perl packages. The Socket package allows us to open TCP network sockets, which we will need in order to speak SMTP to our mail server. The other package, strict, forces more careful programming style in Perl. Variables, for instance, need to be explicitly declared. As a matter of course, one should probably use strict whenever doing network programming. It can save a lot of aggravation.

# Required packages
use strict;
use Socket;          # Required for network communications

Note

While the ability to “slang” in Perl is wonderful for fast scripts, it can make Perl code hard to read and/or maintain. For code that will be around year after year, it is better to use strict. Additionally, strict is a good idea when performing any kind of network programming, because one is affecting action at a distance. Mistakes can be harder to overcome, especially if a bug has the potential to crash a server at your Internet service provider or other sensitive site.

Some constants need to be defined. We’ll put them in one spot so we can find them. Some of these are probably overkill (such as $padchar), but defining them in this way can make the code easier to read rather than hard-coding “magic values” in the script.

Since mail messages need to adhere to the canonical network-standard format described in Chapter 2, Simple Text Messages and Chapter 3, Multipurpose Internet Mail Extensions, this script will need to construct messages with CRLF line breaks. Similarly, we will restrict line lengths to 76 characters (for base64-encoded content). As in all current MIME implementations, our MIME version number is “1.0”.

An array of characters for base64 encoding is needed, so that we can look into the array with the value of the bit stream that we are encoding (which will equate to the index of the array):

# ---------------------------------------------------------
#  | CONSTANTS |
# ---------------------------------------------------------
my ($CRLF) = "
";
my ($DEFAULT_CONTENT_TYPE) = "application/octet-stream";
my ($padchar) = "=";
my ($LINESIZE) = 76;
my ($MIMEversion) = "1.0";

# MIME Base 64 encode characters table
my(@encode_table) = (
    'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
    'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
    'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
    'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/' 
);

Since we’re strict, we need to predeclare all of our variables. Obviously, variables will be needed for the mail server to which the script will send messages. The value for this variable should be changed to reflect your local mail server. For completeness (and testing), a mail port is also defined. Port 25 is the default SMTP port.

Similarly, we need variables for each message header that will be created by the script. Initially, most are to be set to a null string, except the Subject header, which has a default value. Each of these headers can be set via command-line options:

# ---------------------------------------------------------
# |USER-DEFINABLE VARIABLES |
# ---------------------------------------------------------
my($mailhost) = "mailhost.plugged.net.au";
my($mailport) = 25;
my($to) = "";
my($recipient);
my(@recipients);
my($from) = "[email protected]";
my($cc) = "";
my($bcc) = "";
my($replyto) = "";
my($subject) = "No Subject";

Some variables (such as state flags)[20] require initial values, others do not. We have to predeclare them all.

Each of these variables will be described in their turn, at the location where they are used:

# ---------------------------------------------------------
# |INITIALIZE REMAINING VARIABLES |
# ---------------------------------------------------------

# Initialize all variables that require an initial value:
my ($content_type) = "";
my ($_attachment_encoded) = 0;
my ($boundary) = "";
my ($message) = "";
my ($_filename) = "";
my ($last_processed) = "-1";
my ($body) = "";
my ($attachment_list) = "";
my ($no_stdin) = 0;

# Variables used in the SMTP connection:
my  ( $mailcmd, $in_addr, $proto, $addr );
my  ( $mailtest, $error_test );

#Finally, initialize all remaining variables used:
my($i, $j, @attachments, $filename, $file_buffer,
@attachments_encoded, $attachment, $flag, $extension,
$random_int, $the_header, $tmp_incoming_data, $header,
@decoded_attachment_data, Splain_text, $element,
$encoded_attachment, @buffer, $encode_count, $buffer_string,
$tmp_string, @file);

Now that the variables are declared, the next step is to handle the command line. This script requires at least one command-line argument (a To address). Therefore, we should probably return a usage message if there are no arguments:

# ---------------------------------------------------------
# | OPERATE ON THE COMMAND LINE |
# ---------------------------------------------------------
# If this script was called without any command line information,
# there is not enough information to proceed. Instead, return
# a usage message.
unless ($#ARGV > 0) {
        &usage;
}

The subroutine that returns the usage message looks like this:

sub usage {

    print <<ENDOFUSAGE;
        mail.pl [-u] [-h] [-f from] [-m mailhost] [-p port]
                [-s subject] [-c cc-addr] [-b bcc-addr] [-r replyto-addr]
                [-a attachment#1[:attachment#2...]] [-w] to_addresses
        -a              List of files to attach, colon separated
        -b              List of blind carbon copy addresses
        -c              List of carbon copy addresses
        -f              Email sender's address (FROM:)
        -h              Shows the man page (help)
        -m              SMTP mailhost (name or IP address)
        -p              Port to use when connecting to mailhost

        -r              Email reply-to address
        -s              Subject to use
        -u              Shows this usage information
        -w              Do not wait on STDIN for message part
        to_addresses    Email recipient's address (TO:) in a comma
                        separated list

ENDOFUSAGE

  exit(0);

} # End sub usage

If we haven’t exited, we can safely parse the command line for arguments:

# Parse the command line to gather all necessary information.
&parseCmdLine;

The subroutine that parses the command line simply reads a series of flags and their associated data, then sets flags or calls subroutines as needed to set up the script for its run.

If the -u flag is given, we simply call the usage subroutine to print out a usage statement. Similarly, the -h flag calls the manpage subroutine in order to return a manual page to the user. The manpage subroutine is discussed later in this chapter. In both cases, the script will exit after the information is displayed to the user.

Each of the command-line options that set message headers (except To) consists of a flag followed by the appropriate data. Therefore, the handler for these flags must get the following element in the arguments array and increment the number of arguments handled by 2, not just 1.

Files to be attached are given in a colon-separated list, following an -a flag. The handler for the -a flag puts the list into the $attachment_list variable, then splits that variable on the colons. The results of this split become the contents of the @attachments array, each of which is a single filename. Later, we will step through this array in order to encode each attachment.

Finally, any command-line arguments that have not been accounted for are assumed to be To addresses. This works the same way that /bin/mail does.

As a simple check, we ensure that each address has an at sign (@) in it. Any remaining arguments that do not include an at sign are thrown away. Note that we shouldn’t really do this, since an address may be valid but not contain an at sign.[21] Extending this script to handle this special case is left to the reader.

The parseCmdLine subroutine looks like this:

sub parseCmdLine {

     foreach $i (0...$#ARGV){

    if ( $ARGV[$i] =~ /^-(w) $/ ) {

        $flag = $1;
        $_ = $flag;

      SWITCH: {

          /u/     and &usage, last SWITCH;
          /h/     and &manpage, last SWITCH;
          /f/     and $from = $ARGV[$i+1], $last-processed += 2,
                      last SWITCH;
          /r/     and $replyto = $ARGV[$i+1], $last-processed += 2,
                      last SWITCH;
          /m/     and $mailhost = $ARGV[$i+1], $last-processed += 2,
                      last SWITCH;
          /w/     and $no_stdin = 1, $last-processed++, last SWITCH;
          /p/     and $mailport = $ARGV[$i+1], $last-processed += 2,
                      last SWITCH;
          /a/     and $attachment_list = $ARGV[$i+1], $last-processed += 2,
                      last SWITCH;
          /s/     and $subject = $ARGV[$i+1], $last-processed += 2,
                      last SWITCH;
          /c/     and $cc = $ARGV[$i+1], $last-processed += 2, last SWITCH;
          /b/     and $bcc = $ARGV[$i+1], $last-processed += 2, last SWITCH;

      } # end SWITCH
    } # end if/else
    } # end foreach

    #If attachments were found, put them into the attachments array
    if  (($attachment_list) > 0) {
         @attachments = split(/:/, $attachment_list);
    }

    # Any arguments left on the command line after the last
    # switch processed should be To: addresses.
    foreach $i (($last-processed + 1)...$#ARGV) {
        if ($to) {
                $to .= ",";
        }
        # Provide some basic error checking. Don't append anything
        # to the To: list unless it has an "@" sign in it.
        if ($ARGV[$i] =~ /@/) {
                $to .= $ARGV[$i];
        }
    }
} # end sub parseCmdLine

The old /bin/mail read a simple text mail message on STDIN when the program was called. In order to maintain our drop-in capability, we need to provide the same feature. However, since we may wish to send only attached files, we have provided the -n flag. If the -n flag is given on the command line, no attempt will be made to read the standard input before sending the message.

/bin/mail also reports the characters “EOT” to STDOUT when the input is completed. This script does the same, in case someone would like to parse it upon completion:

unless ($no_stdin) {
    while (<STDIN>) {
        if ($_ =~ /^./) {
                last;
        }
        $body .= $_;
    }
    print STDOUT "EOT
";
}

At this point, we have all of the information from the user, both off of the command line and (possibly) from standard input. The next step is to MIME-encode any files to be attached. Files that are simple 7-bit US-ASCII text will not need to be encoded.

For simplicity, all attachments that require encoding will use base64. It is much more efficient to use quoted-printable encoding, where possible, but one has to decide if it is possible. We’ll keep this implementation easy by avoiding that particular decision.

Each file in the list of files to attach is opened and read into a variable. The last part of the filename (the file’s name itself, stripped of the directory information) is retained and used as the value of the optional “name” parameter to the attachment’s Content-Type header. The filename is put into a variable, and the variable holding the contents is passed to the printEncoded subroutine for encoding. That routine determines the content type, creates the appropriate headers, and encodes the data if need be.

# ---------------------------------------------------------
# | CREATE MIME ATTACHMENTS |
# ---------------------------------------------------------

# Create MIME attachments for each file named as an attachment
if ($attachment_list) {
         for $j (0...$#attachments) {
             $filename = $attachments[$j];
             open (INCOMING, $filename) || die "Can't open $filename
";
             $file_buffer = "";
             while (<INCOMING>) {
                 $file_buffer .= $_;
             }
             close (INCOMING);

             # Find the file name from the path info.
             @file = split(///, $filename);
             $_filename = @file[$#file];

             #Encode the file as appropriate.
             $attachments_encoded[$j]       = &printEncoded("$file_buffer");
             $j++;

          }
}

The printEncoded subroutine is called to review a file’s contents and encode it with the base64 algorithm if it is not text. It is a long and brute-force way of implementing base64, but it should be a reasonably clear step-by-step approach as described in Chapter 3. For a shorter and sweeter method of implementing base64, see Gisle Aas’s MIME::Base64 Perl module or the MIME::Tools by Eryq. Both of these fine modules are available from CPAN (see Chapter 13, Email-Related Perl Modules).

The contents of a file are passed in a variable to printEncoded. The data is then split into characters and placed in a character array, called @decoded_attachment_ data:[22] The content type of the data is guessed by the call to the setContentType subroutine, which is described later.

The createHeader subroutine is called to create the MIME headers for this specific attachment. The variable $encoded_attacbment will eventually hold the entire attachment when the encoding process is complete. Initially, though, just the headers are written to it.

If the content type of the data is some form of text (e.g., text/plain or text/html), determined solely by file extension, the data is not encoded in any way. If it is of any other form, the content is base64 encoded. This works but is rather excessive. A production script would probably expand the capabilities of this somewhat to include quoted-printable encoding (where appropriate) and be more careful to determine if the data is already text.

When it is determined that the data is to be base64 encoded, the character stream is read to convert every three characters into four characters in the base64 alphabet, exactly as shown in Chapter 3.

Padding the encoded attachment (with one or two “=” characters) may be needed, depending on the length of the data. Chapter 3 also describes this procedure in detail.

sub printEncoded {

    # The contents of the file to be encoded are passed in as a parameter.
    $tmp_incoming_data = pop(@_);

    # Turn the string input into an array of characters for processing.
    @decoded_attachment_data = split(//,$tmp_incoming_data);

    # encoded attachment size counter
    $encode_count = 0;

    # is there anything to encode?
    if (@decoded_attachment_data 0) {
         # nothing to encode
         return "null";
    }

    # Set the MIME content type for this attachment
    &setContentType;

    # Create the MIME header
    $header = &createHeader;

    # Create the encoded content string, starting with the newly-created MIME header.
    $encoded_attachment = $header;

    # Is the content type text of some form?
    if ($content_type =~ /^text/i) {
         # the encoded attachment is plain text -no need to encode
         $plain_text = "";
         foreach $element (@decoded_attachment_data) {
              $plain_text .= $element; 
         }

         $encoded_attachment .= $plain_text;
    } else {
      # The content type is NOT text: encoding !

      # create a suitable buffer - each 3 decoded bytes, becomes 4 encoded
      @buffer = (''),

      # go through the attachment
      for ($i=0; $i < ( @decoded_attachment_data - 2 ); $i+=3) {
      # convert 3 bytes to 4 encoded characters
      $buffer[$encode_count++] =
          $encode_table[((vec($decoded_attachment_data[$i],0,8)
                  & 0xFC) >> 2) ];
      $buffer[$encode_count++] =
          $encode_table[(((vec($decoded_attachment_data[$i],0,8)
                  & 0x03) << 4) |
                  ((vec($decoded_attachment_data[$i+1],0,8)
                  & 0xF0) >> 4)) ];
      $buffer[$encode_count++] =
                 $encode_table[(((vec($decoded_attachment_data[$i+1],0,8)
                 & 0x0F) << 2) |
                 ((vec($decoded_attachment_data[$i+2],0,8)
                 & 0xC0) >> 6)) ];
      $buffer[$encode_count++] =
                 $encode_table[(vec($decoded_attachment_data[$i+2],0,8)
                       & 0x3F) ]; 
    }

    # check to see if padding required
    if ( $i < @decoded_attachment_data ) {
         # Padding is required. At least one byte is left over.
         # Reset the character counter backward. so we can recalculate.
         $i -= 3;

         $buffer[$encode_count++] =
                  $encode_table[((vec($decoded_attachment_data[$i],0,8)
                  & 0xFC) >> 2)];
         $buffer[$encode_count] =
                  $encode_table[((vec($decoded_attachment_data[$i],0,8)
                  & 0x03) << 4)];
         # move to the next char; if any
         $i++;

         # any more bytes left over?
         if ($i < @decoded_attachment_data ) {
                 # yes, include it

                 # need to redo the current character
                 $buffer[$encode_count++] =
                   $encode_table[((vec($decoded_attachment_data[$i-1],0,8)
                   & 0x03) <<4) |
                   ((vec($decoded_attachment_data[$i],0,8)
                   & 0xF0) >> 4))];
                 $buffer[$encode_count++] =
                   $encode_table[((vec($decoded_attachment_data[$i],0,8)
                   & 0x0F) << 2)];
         } else {
                # need a pad character
                $buffer[$encode_count++] = $padchar;
         }
         # need at least one pad character
         $buffer[$encode_count++] = $padchar;

    }

    # Convert the encoded buffer to a string
    # and put a new line every LINESIZE chars
    $buffer_string = "";
    foreach $element (@buffer) {
          $buffer_string .= $element;
    }
    for ($i=0; $i<=$encode_count-$LINESIZE; $i+=$LINESIZE) {
          # convert LINESIZE bytes to string
          $tmp_string = substr($buffer_string,$i,$LINESIZE);

          # add this string to the encoded string, plus a newline
          $encoded_attachment .= $tmp_string . "
";
    }

    # Do any leftover chars
    if ($i != $encode_count) {
          # convert the remaining bytes to string
          #tmp_string = new String(buffer,i,encode_count-i);
          $tmp_string = substr($buffer_string,$i,$encode_count - $i);

          # add this string to the encoded string, plus a newline
          $encoded_attachment .= $tmp_string . "
";

      }

    }  End else (content type NOT text)

    # set flag to indicate attachment has been encoded
    $_attachment_encoded = 1;

    # attachment is appropriately encoded by now
    return $encoded_attachment;

} # sub printEncoded

The preceding printEncoded subroutine calls a subroutine to determine the content type of the file being worked. To do so, this script provides a very basic routine to examine any possible file extensions and match them to a content type. Anytime a content type cannot be determined, application/octet-stream is presumed, which will result in base64 encoding.

sub setContentType {
      # Attempt to determine the content type from the filename

      # set a default content type in case nothing can be found
      $content_type = $DEFAULT_CONTENT_TYPE;

      # get the start of the filename extension
       if ( $_filename  =~  /.*. (w{3,4})$/ ) {

      # extract the extension
      $extension = $1;

      # try some common extensions
      if ($extension eq "gif") {
           # GIF
           $content_type = "image/gif";
      } elsif ($extension eq "jpg" || $extension eq "jpeg") {
           #JPEG
           $content_type = "image/jpeg";
      } elsif ($extension eq "txt") {
           # text
           $content_type = "text/plain";
      } elsif ($extension eq "htm" || $extension eq "html") {
           #HTML
           $content_type = "text/html";
      } elsif ($extension eq "mpg" || $extension eq "mpeg") {
           # MPEG
           $content_type = "video/mpeg";
      } # end if extension...
    } # end if start_extension...

} # end sub setContentType

The printEncoded subroutine also calls createHeader in order to create the MIME headers for each attachment. In this simple case, one of two possible headers is created: one for text message parts, and another for base64-encoded message parts.

sub createHeader {
    # the header to be created
    $the_header = "";

    # is the content type text?
    if  ($content_type =~ /^text/ ) {
     # yes, it's text, create a text header
       $the_header =    "Content-Type: $content_type; " .
                         "charset=us-ascii; name="$_filename"" . $CRLF" .
                         "Content-Transfer-Encoding: 7bit" . $CRLF.
                         "Content-Disposition: inline; " .
                         "filename="$_filename"" . $CRLF . $CRLF;
    } else {
      # not text, therefore the content will be base64 encoded
      $the_header =      "Content-Type: $content_type; " .
                         "name="$_filename"" . $CRLF .
                         "Content-Transfer-Encoding: base64" . $CRLF .
                         "Content-Disposition: inline; " .
                         "filename="$_filename"" . $CRLF . $CRLF;
    }
    return $the_header;
} # end sub createHeader

Once the attachments, if any, are encoded, the message is created.

MIME messages may include a single body part and hence a single content type, as described in Chapter 4, Creating MIME-Compliant Messages. To keep things simple, this script will create all messages with a content type of multipart/mixed and include any number of non-nested attachments, each of the appropriate type. This means, of course, that if no textual body was read on STDIN and only one file was attached, the message produced (though legal) will not be created in the most efficient manner. In that case, a composite body part would wrap a Single inner part.

If just a textual body is given, though, MIME body parts are not a necessity. In this case, it is preferable to resort to a simple RFC 822 message structure. This script does that by writing MIME headers only if files have been designated for attachment.

The following code creates the message itself:

# ---------------------------------------------------------
# |CREATE THE MESSAGE |
# ---------------------------------------------------------
$boundary &createBoundaryMarker;

$message = "From: " . $from . $CRLF;
$message .= "To: $to" . $CRLF;

if ($cc) {
        $message .= "Cc: $cc" . $CRLF;
}
if ($bcc) {
        $message .= "Bcc: $bcc" . $CRLF;
}
if ($replyto) {
        $message .= "Reply-To: $replyto " . $CRLF;
}
$message .= "Subject: $subject" . $CRLF;

# If there are attachments, add MIME headers.
if ($attachment_list) {
        $message .= "MIME-Version: $MIMEversion" . $CRLF;
        $message .= "Content-Type: multipart/mixed;" .
                     "boundary="$boundary""
                     . $CRLF . $CRLF;
        $message .= "This is a multi-part message in MIME format."
                   . $CRLF . $CRLF;
        # If there are attachments. then this is a MIME message
        # so the textual message from STDIN (if present) needs
        # MIME headers, too.
        if ($body) {
             $message .= "--$boundary" . $CRLF;
             $message .= "Content-Type: text/plain; charset=us-ascii"
                        . $CRLF;
             $message .= "Content-Transfer-Encoding: 7bit" . $CRLF;
        }
} else {
        # End the message headers, il not a MIME message.
        $message .= $CRLF;
}
if ($body) {
        # If the user supplied a textual message on STDIN,
        # write it out.
        $message .= $body. $CRLF; # Add the rest of the message
        $message .= $CRLF;
}

if ($attachment_list) {
        # Start writing out the attachments. Begin by adding an
        # extended MIME boundary (preceded by two hyphens).
        $message .= $CRLF ."--". $boundary;

        # Each attachment variable already includes the appropriate
        # headers. Write them out, bounded by MIME boundaries.
        foreach $attachment (@attachments_encoded) {
            $message .= $CRLF. $attachment . $CRLF;
            $message .= "--$boundary";
        }

        # End the MIME message part by adding two hyphens to the
        # end of the last boundary.
        $message .= "--" . $CRLF;
}

The first thing done in the preceding code is to create a unique MIME boundary marker. The subroutine createBoundaryMarker performs that task by generating a pseudo-random integer and combining it with a longer string that would not likely appear in a message body.

sub createBoundaryMarker {
     # random number as integer
     $random_int = int(rand(lOOO)) * 98765;

     # create the marker
     $boundary="====PLUGGEDIN====" . $random_int . "====_";

     return $boundary;
} #  end sub createBoundaryMaarker

The attachments are encoded. The message has been created. Now all that is left is to send the message! In order to remain cross-platform and ensure that the script doesn’t rely on an operating system-specific mail forwarding mechanism, we chose to speak SMTP directly to a given MTA. Therefore, we have to speak a subset of the SMTP protocol to the MTA in order to send the message.

When speaking a protocol, one connects to a host on a known port via a socket, then generally opens the conversation. In response, you may get something that you expect or an error. To handle this, one generally writes a subroutine that reads the bit stream coming back from the server until either it gets what you expect or the error that you may have expected but didn’t want.

A very simple expect subroutine follows. It takes two arguments or test conditions: a string that you expect, and a string that should be encountered on error. The routine returns a 1 if the expected string was read and a 0 if the error string was found. If the connection times out, the same value will be returned as in the error condition.

sub expect {
  my($ok) = 0;
  my($mailtest) = shift;
  my($error_test) = shift;

  $mailcmd = "";
  while (<S>) {
    $mailcmd .= $_;
    if (/$mailtest/i) {
        $ok = 1;
        last;
    }
    if (/$error_test/i) {
        $ok = 0;
        last;
    }
  }
  return $ok;
}

The act of sending the message itself is reasonably straightforward. First, a socket is created, and a connection to the remote MTA’s mail port is established. Then, it is a simple matter of printing SMTP commands to the socket’s output stream and using calls to the expect subroutine to check the results of each command. The MTA will respond with SMTP codes, as well as textual messages. The textual messages vary greatly from implementation to implementation, but the numerical codes are specified in the standard. Therefore, we use the codes to control the conversation. See Chapter 9, The Extended Simple Mail Transfer Protocol, for a full description of SMTP commands and response codes.

Note that all SMTP commands are terminated with the network-standard line separator, CRLF. This is true regardless of the local operating system’s end-of-line convention.

The following extended SMTP commands are used: EHLO (the MIME version of HELO or hello), MAIL FROM (to give the MTA the sender’s address), RCPT TO: (to give the MTA the recipient’s address), DATA (to tell the MTA that we are ready to send the actual message), and QUIT (to close the session). When the DATA command is given, the server will respond that it is ready to accept the message. The message is sent, followed by a period (.) on a line by itself. Of course, a robust implementation would check to ensure that the original data did not include such a line.

When the SMTP conversation is complete, the socket is closed, and the script exits.

Note that the EHLO command takes an argument that names the hostname of the client. We are cheating here by giving it the server’s name! This will generally work but is not encouraged! It partially spoofs the message’s first Received header, although the IP address will still be correct since it is found by parsing the incoming packet headers. A real implementation should properly get the local hostname and provide it with the EHLO command.

# ---------------------------------------------------------
# | SEND THE MESSAGE |
# ---------------------------------------------------------

$in_addr = (gethostbyname($mailhost)) [4];
$addr = sockaddr_in($mailport, $in_addr);

$proto = getprotobyname('tcp'),

# Create a socket
socket(S, AF_INET, SOCK_STREAM, $proto) or die "socket: $!";

# Connect the socket to the host
connect(S, $addr) or die "Connect: $!";

# flush socket after every write
select (S); $| = 1; select (STDOUT);

# Send SMTP request to the server
die "ERROR: $!" unless &expect(220,500);

# Be nice, say hello to the mail host.
# Of course, say in MIME-wise with EHLO.
print S "EHLO $mailhost" . $CRLF;
die "ERROR: $!" unless &expect(250,501);

# Send the MAIL command.
print S "MAIL FROM: $from " . $CRLF;
die "ERROR: $!" unless &expect ("ok",500);

# Send the RCPT Command.
# Break the recipient list into individual addresses in
# order to create a proper envelope.
@recipients = split /,/, $to;
foreach $recipient (@recipients) {
     print S "RCPT TO: $recipient" . $CRLF;
     print "To: $recipient" . $CRLF;
     die "ERROR: $!" unless &expect ("ok", 500);
     print $mailcmd;
}
# Send the DATA Command.
print S "DATA" . $CRLF;
die "ERROR: $!" unless &expect(354,50);

# Send the message itself.
print S "$message" . $CRLF;
print S "." . $CRLF;
die "ERROR: $!" unless &expect(250,500);

# Quit the SMTP session.
print S "QUIT" . $CRLF;
die "ERROR: $!" unless &expect(22l,50);

# Close the socket and exit
close(S);
exit;

If we would like to document our script (which is always a good idea), Perl’s POD format is a good way to do so. A simple subroutine may be added to parse the script itself (referred to by the Perl interpreter as $0) with Perl’s pod2man utility. We can then display the output as text or a Unix manual page, as required by the terminal that we are using. Note that this is very Unix-like and may not work under all operating systems that can run the rest of the script.

sub manpage {

    my($pager) = 'more';
    $pager = $ENV{'PAGER'} if $ENV{'PAGER'};
    if ( $ENV{'TERM'} =~ /^(dumb|emacs)$/ ) {
    system ("pod2text $0");
    } else {
    system ("pod2man $0 | nroff -man | $pager");
    }
    exit (0);
}

The POD for this script is available with the script itself from O’Reilly’s FTP site.



[20] Yeah, this could have been object-oriented, but it’s not. So there. For me, switching between objectorientation styles in Perl and Java is like switching from vi to emacs. Painful, at best, dangerous at worst.

[21] When the recipient has an account on the SMTP server that the script sends to, for example, an address consisting of the username is sufficient.

[22] Really, this should have been @not_encoded_attachment_data.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.140.242.165