CGI Programming on the World Wide WebBy Shishir Gundavaram1st Edition March 1996 This book is out of print, but it has been made available online through the O'Reilly Open Books Project. |
9.2 Mail Gateway
Ever wish you could send electronic mail from your web browser? This gateway allows you to do just that.
#!/usr/local/bin/perl $webmaster = "shishir\@bu\.edu"; $gateway = "CGI Mail Gateway [v1.0]"; $request_method = $ENV{'REQUEST_METHOD'}; $sendmail = "/usr/lib/sendmail -t -n -oi";This program uses the UNIX sendmail utility to actually send the message. The -t option instructs sendmail to scan the message for the "To:" mail header, and the n option prevents the user from entering aliases for the recipient's email address; you would not want some remote user to use your system's internal aliases, would you?
$address_file = "/home/shishir/httpd_1.4.2/cgi-bin/address.dat";The address file consists of a list of recipients' mail addresses from which the user is required to select one. The user cannot enter an address that is not included in the address file. The address.dat file should be formatted as follows:
Webmaster,webmaster@bu.edu Author,shishir@bu.edu . . .I have chosen a comma to separate nicknames from addresses because Internet standards prohibit a comma from being used in an address.
When the mail form is displayed, the program inserts all of the descriptive names in a scrolled list. If you do not want to have such a file, remove or comment out the line defining $address_file.
$exclusive_lock = 2; $unlock = 8; if ( defined ($address_file) && (-e $address_file) ) { &load_address (*address); }If the address_file variable is defined and the file exists, the load_address subroutine is called to load the list of addresses into the address associative array (for easy retrieval).
&parse_form_data (*MAIL);The form information is stored in the MAIL associative array. The parse_form_data subroutine is the same as the one used at the beginning of Chapter 7, Advanced Form Applications. Like the guestbook application I presented in Chapter 7, Advanced Form Applications, this program is two in one: Half of the program displays a form, and the other half retrieves the data after the user submits the form, and sends the mail.
if ($request_method eq "GET") { &display_form ();If the GET method was used to access this program, the display_form subroutine displays the form. This gateway can be accessed without any query information:
http://your.machine/cgi-bin/mail.plin which case, a mail form is displayed. Or, you can also access it by passing query information:
http://your.machine/cgi-bin/mail.pl?to=shishir@bu.edu&url=/thanks.htmlIn this case, the "to" and "url" fields in the form will contain the information passed to it. If an address file is being used, the address specified by the "to" field has to match one of the addresses in the list. Instead of specifying the full email address, you can also use the descriptive title from the address file:
http://your.machine/cgi-bin/mail.pl?to=Author&url=/thanks.htmlThe advantage of passing queries like this is that you can create links within a document, such as:
. . If you want to contact me, click <A HREF="/cgibin/mail.pl?to=Author">here.</A> . .All of the fields in the form, including "to" and "url," will be explained later in this section.
} elsif ($request_method eq "POST") { if ( defined (%address) ) { $check_status = &check_to_address (); if (!$check_status) { &return_error (500, "$gateway Error", "The address you specified is not allowed."); } }This block will be executed if the POST method was used to access this gateway (which means that the user filled out the form and submitted it). If the address associative array is defined, the check_to_address subroutine is called to check for the validity of the user-specified address. In other words, the address has to be listed in the address file. This subroutine returns a TRUE or FALSE value. If the address is not valid, an error message is returned.
if ( (!$MAIL{'from'}) || (!$MAIL{'email'}) ) { &return_error (500, "$gateway Error", "Who are you ?"); } else { &send_mail (); &return_thanks (); }If the user failed to enter any information into the "from" and "email" fields in the form, an error message is returned (which I will show later). Otherwise, the mail message is sent, and a thank-you note is returned.
} else { &return_error (500, "Server Error", "Server uses unsupported method"); } exit(0);Now for the load_address subroutine, which reads your address file:
sub load_address { local (*ADDRESS_DATA) = @_; local ($name, $address); open (FILE, $address_file) || &return_error (500, "$gateway Error", "Cannot open the address file [$address_file]."); flock (FILE, $exclusive_lock);This subroutine opens the address file, and loads all of the entries into an associative array. Note that $exclusive_lock and $unlock are global variables.
while (<FILE>) { chop if (/\n$/); ($name, $address) = split (/,/, $_, 2); $ADDRESS_DATA{$name} = $address; }The while loop iterates through the file one line at a time. If a line ends with a newline character, it is removed with the chop function. The chop function removes the last character of the line. The if clause is there as a precaution, because the last line of the file may not have a newline character, in which case part of the data would be lost. The split command, which should be familiar by now, separates the name from the address. Then, an entry in the associative array is created to hold the address.
flock (FILE, $unlock); close (FILE); }The display_form subroutine is executed when the client invokes the program without a query.
sub display_form { local ($address_to); print "Content-type: text/html", "\n\n"; $address_to = &determine_to_field ();The determine_to_field subroutine creates a scrolled list if the address file is defined. See Figure 9.4 for a snapshot of what this looks like. Otherwise, a simple text field is used. The HTML needed to accomplish these functions is returned by the subroutine, and is stored in the address_to variable.
print <<End_of_Mail_Form; <HTML> <HEAD><TITLE>A WWW Gateway to Mail</TITLE></HEAD> <BODY> <H1>$gateway</H1> This form can be used to send mail through the World Wide Web. Please fill out all the necessary information. <HR> <FORM METHOD="POST"> <PRE> Full Name: <INPUT TYPE="text" NAME="from" VALUE="$MAIL{'from'}" SIZE=40> E-Mail: <INPUT TYPE="text" NAME="email" VALUE="$MAIL{'email'}" SIZE=40> To: $address_to CC: <INPUT TYPE="text" NAME="cc" VALUE="$MAIL{'cc'}" SIZE=40> Subject: <INPUT TYPE="text" NAME="subject" VALUE="$MAIL{'subject'}" SIZE=40> <HR>Notice the use of the VALUE attributes in the INPUT statements. These values represent the query information that is passed to this program with a GET request.
Please type the message below: <TEXTAREA ROWS=10 COLS=60 NAME="message"></TEXTAREA> </PRE> <INPUT TYPE="hidden" NAME="url" VALUE="$MAIL{'url'}"> <INPUT TYPE="submit" VALUE="Send the Message"> <INPUT TYPE="reset" VALUE="Clear the Message"> </FORM> <HR> </BODY></HTML> End_of_Mail_Form }The "url" field is defined as a hidden field. This consists of the URL of the document that is displayed after the user completes the form.
The determine_to_field subroutine either creates a scrolled list of all the addresses in the file, or a simple text field in which the user can enter the recipient's address.
sub determine_to_field { local ($to_field, $key, $selected); if (%address) { $to_field = '<SELECT NAME="to">'; foreach $key (keys %address) {The keys function returns a normal array consisting of all of the keys of the associative array. The foreach construct then iterates through each key.
if ( ($MAIL{'to'} eq $key) || ($MAIL{'to'} eq $address{$key}) ) { $selected = "<OPTION SELECTED>"; } else { $selected = "<OPTION>"; }If the recipient specified by the user (through a query string) matches either the descriptive title in the address file-- the key--or the actual address, it is highlighted. Remember, this is how you can access this program with a query:
http://your.machine/cgi-bin/mail.pl?to=shishir@bu.edu&url=/thanks.htmlNow, the rest of the subroutine:
$to_field = join ("\n", $to_field, $selected, $key); } $to_field = join ("\n", $to_field, "</SELECT>");Finally, all of the <OPTION> tags are concatenated to create the kind of scrolled list shown above.
} else { $to_field = qq/<INPUT TYPE="text" NAME="to" VALUE="$MAIL{'to'}" SIZE=40>/; } return ($to_field); }If an address file is not used, a simple text field is displayed. The qq/../ construct builds a double-quoted string. It should be used when there are many double quotation marks within the string. The same string can be expressed inside the traditional double quotes:
$to_field = "<INPUT TYPE=\"text\" NAME=\"to\" VALUE=\"$MAIL{'to'}\" SIZE=40>";As you can see, all of the other double quotation marks within the string have to be escaped by putting backslashes in front of them. Using the qq notation in the regular expression is much easier.
Finally, the HTML needed to display the "to" field is returned.
The check_to_address subroutine checks the user-specified recipient to make sure that it is valid. If it is valid, the variable $MAIL{'to'} will be set to the corresponding email address. Finally, a status indicating success or failure is returned.
sub check_to_address { local ($status, $key); $status = 0; foreach $key (keys %address) { if ( ($MAIL{'to'} eq $key) || ($MAIL{'to'} eq $address{$key}) ) { $status = 1; $MAIL{'to'} = $address{$key}; } } return ($status); }In this next subroutine, the mail is sent using the UNIX sendmail utility.
sub send_mail { open (SENDMAIL, "| $sendmail");A pipe to the sendmail utility is opened for input. We do not need to check any of the form values for shell metacharacters because none of the values are "exposed" on the command line. The sendmail utility allows you to place the recipient's name in the input stream, rather than on the command-line.
If the regular mail utility is used, the form information must be checked for metacharacters. This is how we can send mail with the mail utility:
if ($MAIL{'to'} =~ /([\w\-\+]+)@([\w\-\+\.]+)/) { open (SENDMAIL, "/usr/ucb/mail $MAIL{'to'} |"); } else { &return_error (500, "$gateway Error", "Address is not valid."); }The regular expression is described by the figure below. Of course, this allows only Internet-style mail addresses; UUCP addresses are not recognized.
print SENDMAIL <<Mail_Headers; From: $MAIL{'from'} <$MAIL{'email'}> To: $MAIL{'to'} Reply-To: $MAIL{'email'} Subject: $MAIL{'subject'} X-Mailer: $gateway X-Remote-Host: $ENV{'REMOTE_ADDR'} Mail_HeadersVarious mail headers are output. Any headers starting with "X-" are user/program specified, and are usually ignored by mail readers. The remote IP address of the user (the environment variable REMOTE_ADDRESS) is output for possible security reasons. Imagine a situation where someone fills out a form with obnoxious information, and includes a "fake" address. This header will at least tell you where the message came from.
if ($MAIL{'cc'}) { print SENDMAIL "Cc: ", $MAIL{'cc'}, "\n"; } print SENDMAIL "\n", $MAIL{'message'}, "\n"; close (MAIL); }If the user entered an address in the "Cc:" field, a mail header is output. Finally, the body of the message is displayed, and the pipe is closed.
It is courteous to output a thank-you message:
sub return_thanks { if ($MAIL{'url'}) { print "Location: ", $MAIL{'url'}, "\n\n"; } else { print "Content-type: text/html", "\n\n"; print <<Thanks; <HTML> <HEAD><TITLE>$gateway</TITLE></HEAD> <BODY> <H1>Thank You!</H1> <HR> Thanks for using the mail gateway. Please feel free to use it again. </BODY></HTML> Thanks } }If a URL was specified as part of the GET request, a server redirect is done with the "Location" HTTP header. In other words, the server will get and display the specified document after the user submits the form. Otherwise, a simple thank-you note is issued.
Back to: CGI Programming on the World Wide Web
© 2001, O'Reilly & Associates, Inc.