web2mail:
An Anonymous Web Form Mailer

Introduction
How to use the script
Customizing the Form Response
.environment .remove_indexing .mail_subject
.mail_intro .required_data .back_to_url
.thanks_url .test .custom_response_url
The Perl Source Code
Download the code

Return to: web2mail demo
CGI Resources

This page describes an anonymous web form remailer, a CGI script which will mail the form data submitted to a web page. The same script can be used by many users. This page explains both the use of the script and the script code itself. Comments and suggestions, including especially bug reports, are very welcome to sanford@halcyon.com. Version 1: 28 Dec 1995. Version 2: 28 May 1996. Version 3: 10 Jan 1997. Version 4: 03 March 97.

Introduction

After hit counters and guestbooks, the most desired CGI functionality is the ability to email a user's form data to the owner of a web page. Here's a Perl script that does that. You can use the script without trying to understand how it works, but it illustrates several basic features of CGI programming:
  • It shows how to mail from a CGI script, using the relatively secure sendmail -t mailer.
  • It uses a hidden form tag to pass the desired email address to the script, so different web pages can use the same script to send mail to different web owners. That's the anonymous part. (Hidden tags are generally used to maintain state information in a sequence of web pages..)
  • It can mail any form data, no matter how many form elements there are, their types or their values.
  • Any web form on the Internet can access the script, not just web pages on the same server. (Of course, it will work faster and more reliably if it's on your server.)
  • It has simple default functionality: at minimum, just point your web form to the script and supply an email address in a hidden tag. It can also be customized by the form owner at runtime to return specially formatted mail and sorted form data, the environment data, a customized thanks page, and to require some of the data be filled out before mailing.
First, we'll describe how to use this script in a web form, then how the source code works. The source for the script is also available for you to install on your own server.

How to use the script

The script is an anonymous mailer. It knows how to read the data from any form page (much like the example script in the CGI tutorials) but it can also send mail to any address. The key to doing this without letting the user see all the mechanics of the process lies in hidden form elements. The script looks for the target email address in a hidden tag in the web form.
     You'll see plenty of examples of hidden form tags below. They work very much like regular form tags, except they are not designed to be interactive since they don't display on the web form. (They aren't secret however, since you can see them if you view the source of the web form.) Hidden form tags are used for passing static information from the web form to the script. Since this script can be used by many different people, this is a good way to tell the script where to mail the form data it receives from your web page.
     After you have prepared your form, there are two steps minimally required to use the web2mail script. First, point your form to the script at:
    http://www.halcyon.com/sanford/cgi/web2mail/web2mail.cgi  
That is, include all of your form tags (including the SUBMIT button) between the following two form tags on your web page:
    
    <FORM ACTION=
        "http://www.halcyon.com/sanford/cgi/web2mail/web2mail.cgi" 
        METHOD=POST>
    </FORM>  
Or, if you have write access to a cgi-bin directory on your web server and have copied the script there, use its new URL. See the CGI tutorials for more on form tags and cgi-bin.
     Second, specify your email address using a hidden form variable called .email_target by including the following tag anywhere within that form region:
    
    <INPUT TYPE="hidden" NAME=".email_target" 
    VALUE="joe_user@your.site.com">  
but substitute your desired email address. The NAME field must be exactly as shown, including the leading period. However, don't begin any of your other form tag names with a leading period. We'll explain why in a moment.
     That's all it takes. You can try it now, but you might want to read on to see how you can customize the output of the script. (You might find the test mode handy.) You can also try the demonstration page.

Customizing the Form Response

There are three areas in which you can spice up the vanilla flavor of the script's output:
  • the formatting and types of information mailed,
  • the Thanks page that is returned to the user after she submits her data, and
  • requiring that certain data be submitted before the script mails.
Each of these options are turned on by including additional hidden tags in your web form with specific tag names. All of these option tag names begin with a leading period to distinguish them from your other form elements that you want to be mailed. None of the option form tags are mailed. Any tag with a name which does not begin with a leading period is mailed.
     Here's a list of the option tags, what they do, and how to use them in your web form.

.environment
Specifies that you want a list of the environment variables in the mail. They show, for example, the browser type and remote address. If so, include the following in your web form:

    <INPUT TYPE="hidden" NAME=".environment" VALUE=1>  
The Web Form Analyser can show you what environment variables are commonly available to most web forms.

.remove_indexing
The mail sent will list your form tags alphabetically by their NAME attribute. You can control the order by prefixing each NAME with an alphabetizing prefix. For example, if you want the user to enter her name and address, you could call the tags "aaName" and "abAddress" to have the name listed before the address in the mail. If you use alphabetizing prefixes, you can remove the first two characters of the names as they appear in your mail (but retain their order in the mail) by including the following tag in your web form:

    <INPUT TYPE="hidden" NAME=".remove_indexing" VALUE=1>  

.mail_subject
If you don't like the bland default subject header that appears in mail from your web page, then you can compose your own, by using

    <INPUT TYPE="hidden" NAME=".mail_subject" 
    VALUE="Wowie Zowie!! We Got Mail!">  
or something like that. All of this tag (or at least the VALUE attribute) should be on a single line in your web form.

.mail_intro
Similarly, if you don't like the first line that appears in the body of the mail, you can substitute your own by using

    <INPUT TYPE="hidden" NAME=".mail_intro" VALUE="Dear sir,
    Please be advised of the following information submitted 
    to your form:">  
Change the VALUE attribute as you wish. It will appear verbatim in the mail, including line breaks. A blank line will be inserted after this line and before the list of form data.

.required_data
If you wish to insist that the user submit certain of your form data before processing it, you can use this tag. For example, if you want at least the Name, Address and Phone fields filled in, then include in your web form

    <INPUT TYPE="hidden" NAME=".required_data" 
    VALUE="aaName::abAddress::acPhone">  
In the VALUE attribute, use the precise names (including spaces, capitals and any alphabetizing prefix) of the tags in your web form, and separate them by double colons. Place it on a single line in your form. If these fields aren't filled in, the user will be sent an error page listing the missing fields, and invited to go back and fill them in. Consequently, those names should be descriptive of the fields the user sees on the web page, so she can figure out which they are.

.back_to_url
After the form data is mailed, the user will receive a simple thank you page. You can include a link at the bottom of that page directing them to some other page in your web hierarchy.

    <INPUT TYPE="hidden" Name=".back_to_url"
    VALUE="http://your.site.com/~your_dir/page.html">  
Use an absolute, not a relative, URL.

.thanks_url
If you don't like the simple thanks page, then compose your own page and send its URL in the VALUE attribute of this option tag. Like this:

    <INPUT TYPE="hidden" NAME=".thanks_url"
    VALUE="http://your.site/path/thanks.page.html">  
After successfully mailing the form data, the script will return your thanks page to the user, instead of the simple default page.

.test
Often it takes several minutes to receive mail. When you are developing and testing your form, you'd like quicker response. You can ask the script not to mail, but instead write the mail to the Thanks page by including the following in your web form:

    <INPUT TYPE="hidden" NAME=".test" VALUE=1>  
This option tag won't work if you have supplied a .thanks_url or .custom_response_url tag, so test those last.

.custom_response_url
This one's a little more complicated. You might want to return different acknowledgement pages depending on the form data the user supplied. To do so, use this hidden input tag with a value attribute consisting of a sequence of triples. Specify for each custom response page, the name of the form tag, the value of the tag that you want to trigger the response, and the absolute URL of associated response page, in that order. Separate each of these by double colons (::). If you have several of these response pages triggered by different form data, concatenate them, also separated by double colons. Something like this:

    <INPUT TYPE="hidden" NAME=".custom_response_url"
    VALUE="form_tag1::value1::http://your.site/page1.html
    ::form_tag1::value2::http://your.site/page2.html
    ::form_tag3::value::http://your.site/page3.html">  
except place the VALUE all on one line. If a form tag doesn't have an obvious value, specify an arbitrary one, but make sure it's the same in your form and in this hidden tag. You can only return one response page, so multiple form data triggers will be evaluated in order (and you cannot specify combinations of triggers). If no custom response page is triggered, then the one (if specified) in the .thanks_url will be sent, or the default response page.

The Perl Source Code

Following is the Perl source for the web2mail script, together with some brief explanation of how it works. The explanation presumes that you have some acquaintance with both CGI and Perl, for example, the topics covered in one of the CGI tutorials. If you become stuck on some bit of code, you might find a fuller explanation there. Of course, a good book on Perl would help, too. A clean copy of the script code is also available for you to save for your own use.

    #!/usr/bin/perl

    ###
    ### Configuration Variables
    ###

    ### The local mail program  
    ### You may need to change the path to sendmail.
    ### If you don't have sendmail, you'll need to revise &send_mail
    $mail_program = "/usr/lib/sendmail -t";
    
    ### Instructions page 
    ### Used if the developer forgets to supply .email_target hidden tag
    $instructions_url = 
       "http://www.halcyon.com/sanford/cgi/web2mail/index.html";  
The path to sendmail in $mail_program should be the only site dependent variable you might need to change to install the script on your own server. I use sendmail with its - t switch since this is the most secure way of mailing from a CGI script. If you don't have sendmail on your server, I recommend you not install this script unless you know a lot about CGI security. You would also need to modify the send_mail function below.
     $instructions_url is just this page, used if someone forgets to provide their email address to the script.

    ###
    ### Main body of script
    ###
    
    ### parse the form data
    &ReadParse;  
The ReadParse function is from the cgi-lib.pl library, modified slightly to accept only method=POST data (and not method=GET). After it runs, all the form data is in the associative array, %in, keyed on the NAME attributes of the form tags.

    ### is required email address supplied?
    if ( ! $in{'.email_target'} ) { 
        print &instructions_page;
        exit;
    }  
We first check whether we have been given an email address, supplied as a hidden form element whose NAME attribute is .email_target If not, there's nothing else we can do, so return an error page, presuming that this is the web owner doing development rather than a user browsing the form. The instructions_page function simply returns the HTML source for the error page. Here it is:

    sub instructions_page {

        local ($page_source) = <<"INSTRUCTIONS_PAGE";
    Content-type: text/html\n\n 
    <TITLE>Oops, .email_target is missing</TITLE>
    <H1>Oops, .email_target is missing</H1>
    You must include a hidden form tag called 
    <CODE>.email_target</CODE>
    whose value is the email address to which you want to mail 
    the form data. Something like<PRE>
    &lt;INPUT TYPE="hidden" 
        NAME=".email_target" 
        VALUE="user\@site.com"&gt;
    </PRE> For your information, the form elements and their 
    values that actually were supplied in this page were:<UL>
    INSTRUCTIONS_PAGE
        
        foreach (keys %in) {
            $page_source .= "<LI>$_: $in{$_}\n";
        }
    
        $page_source .= <<"LAST_PART";
    </UL>You can return to the 
    <A HREF="$instructions_url">instructions page</A>
    describing the use of this mailing script, or press your 
    <STRONG>BACK</STRONG> button to return to the web form.
    LAST_PART
    
        return $page_source;  
    }  
This function uses some HERE documents (an odd but useful quoting mechanism; see CGI Tips for an explanation) to build up the HTML source for the error page, contained in its local variable $page_source. After building the first half of the page, we build an unordered list of the form data that was actually supplied, in case there were typos, and append it to $page_source Finally, we include a link to this page (the one you're reading now) for further information.
     Continuing with the main body of the script, we next check whether all the required data (if any) was submitted by the user:

    ### any required data? if so, has user supplied it?
    if ( $in{'.required_data'} && &missing_data  ) {  
        print &incomplete_page;
        exit;
    }  
We first check whether there were any required data -- if $in{'.required_data'} exists and contains a list of tag names. If so, the missing_data function splits the .required_data value, which is a string consisting of a :: separated list of required tag names, into individual names. If any names were not supplied by the user, it pushes them onto the global @missing_fields, and sets the return value to true.
     Here is the missing_data function. It splits the required data and asks whether each exists in %in, the hash that contains the form data. If not, it pushes the undefined data onto the global array @missing_fields and returns true.

    sub missing_data {
        local ($result) = 0;
        foreach ( split (/::/, $in{'.required_data'} ) ) {
            unless ($in{$_}) {
                s/^..// if $in{'.remove_indexing'};
                push (@missing_fields, $_);
                $result = 1;
            }
        }
        return $result;
    }  
If there were missing fields, the main body of the script calls the incomplete_page function to return a web page to the user, which lists the missing fields. It works almost identically to the earlier instructions_page function, except it forms its unordered list from the array @missing_fields. Like this:

    sub incomplete_page {

        local ($page_source) = <<"FIRST_HALF";
    Content-type: text/html\n\n
    <TITLE>Thanks, but oops ...</TITLE>
    <H2>Thanks, but oops ...</H2>
    Your information is important to us, and the following items
    on the form were not filled in:
    <UL>
    FIRST_HALF
    
        foreach ( @missing_fields ) {
            $page_source .= "<LI>$_\n";
        }
    
        $page_source .= <<"SECOND_HALF";
    </UL>
    Please click your <STRONG>BACK</STRONG> button 
    to return to the form and add that information. 
    Your other answers should still be there.
    SECOND_HALF
    
        return $page_source;
    }  
Continuing with the main body of the script, if we have survived this far, the user's form data is correct as far as we can tell, so we proceed to format it for mailing, mail it, and return a thanks page. We'll do this first by building the body of the mail in the $message scalar variable.

    ### prepare first line of mail message             
    $message = $in{'.mail_intro'} ? "$in{'.mail_intro'}\n\n"
        : "Form data submitted to $ENV{'HTTP_REFERER'}:\n\n";  
This first line will be either a supplied introductory line (if we've been given one) or a default line. We use a CGI environment variable to determine the URL of the web form. Next, loop over the form tags, appending them to our message:

    ### write the form data to the mail message
    foreach (sort keys %in) {

        next if /^\./;      # skip hidden form data in mail

        $item = "$_: $in{$_}";
        $item =~ s/^..// if $in{'.remove_indexing'};

        # if multiple values, indent them on new lines
        $item =~ s/\0/"\n\t".(" "x(2+length($_)))/ge;

        $message .= "\t$item\n";
    }  
The sort function alphabetizes the form data by NAME. If the tag NAME begins with a period, it's one of our special hidden tags, so we skip to the next one. Otherwise, write it and its correspnding value to a temporary $item variable. Then remove the first two characters if .remove_indexing is turned on.
     If there are multiple values for a form tag (eg, a <select multiple> tag or several hidden tags with the same name but different values) then the &ReadParse function will have supplied them in the form value separated with null characters (\0). We test for this, and generate text that looks like

       name: value1
             value2
             value3  
The single line of code that does this checks for \0 and substitutes for it a new line, a tab, and enough spaces to align the values. Calculating the number of spaces requires the 'e' switch be used on the substitution operator, indicating the replacement is an expression to be evaluated, not merely a pattern. Finally, the last line of code appends the item to the message string that will later be mailed.
     The next task is to similarly loop through all the environment variables, appending to the message string, if the web form has turned on the .environment tag.

    ## do you want the environment variables
    if ( $in{'.environment'} ) {

        $message .= "\nThe environment variables are:\n";

        foreach (keys %ENV) {
            $message .= "\t$_: $ENV{$_}\n";
        }
    } else {
      ## let's at least report a few
      $message .= "\nReferring page: $ENV{HTTP_REFERER}";
      $message .= "\nUser address: $ENV{REMOTE_ADDR}";
      $message .= "\nUser host: $ENV{REMOTE_HOST}";
    }  
The message body is complete. We now prepare the email's subject line the same way we prepared the first line of the message. After that we send the mail.

    ## prepare subject line in mail message 
    $subject_line = $in{'.mail_subject'} 
        ? "$in{'.mail_subject'}"
        : "Form data submitted to $ENV{'HTTP_REFERER'}";

    ## remove everything after the first comma, whitespace or semicolon
    ## in .email_target to prevent multiple recipients, eg, spam broadcasts
    $in{'.email_target'} =~ s/[,\s;].*//;

    ## mail it (or web it if in test mode)
    &send_mail ($mail_program, $in{'.email_target'}, 
        $subject_line, $message);  
We explicitly disallow multiple email target recipients, to guard against this script being used for spam (with potentially hundreds of recipients). So everything after the first comma, whitespace or semicolon, if any, in .email_target is removed. If you need this feature, you'll have to install the script on your own server. I recommend you hard code a comma separated list of email addresses into the script.
     Here is the first half of the send_mail function. We check whether .test is turned on. If so, print what we would have mailed to the user on STDOUT, using a HERE document, rather than actually mail it.

    sub send_mail {
        local ($mail_program, $email_address, 
               $subject, $message) = @_;

        ### test mode. Send mail to web page
        if ($in{'.test'}) {  
            print <<"TEST";
    Content-type: text/html\n\n   
    <H1>Here is the letter that would be mailed:</H1><PRE>
    To: $email_address
    Subject: $subject
    \n\n$message\n</PRE>
    <H1>The rest of this page is the Thanks page</H1><HR>
    TEST
        }  
If we're not testing, we first open a pipe to our mailing program. If this open succeeds, we simply print our email to the pipe, using the format required by our mail program, sendmail -t. Then close the pipe.

        else {    
        if ( !open(MAIL, "|$mail_program") ) { 
            print &error_page ($message);
            exit;
        }
        print MAIL <<"EOM";
    To: $email_address
    Subject: $subject
    \n\n$message
    \n.\n
    EOM
            close (MAIL);
        }
    }  
If for some reason, this pipe fails to open, we ought not simply die silently; we ought to inform the user of that so she can try again later or in a mailto. In this case we print an error page and then exit the script. The function that returns the HTML source for the error page is:

    sub error_page {
        local ($message) = @_;
        local ($page_source) = <<"ERROR_PAGE"; 
    Content-type: text/html\n\n   
    <TITLE>System Form Error</TITLE>
    <H2>Thanks, but oops ...</H2>
    The system seems a bit confused right now, so your form
    was not processed. You can try again a little later, 
    or you can mail us directly at 
        <A HREF="mailto:$in{'.email_target'}">
        $in{'.email_target'}</A>.
    You can simply quote this page, since the following is the 
    message that would have been sent: 
    <P><PRE>$message</PRE>
    ERROR_PAGE
    
        return $page_source;
    }  
This function assigns the HTML source to a local variable, using a HERE document, and returns it. The mail body has been passed in as an argument, so we can include it and a mailto: tag. The user can quote this web page and send it by hand.
     Returning to the final part of the main body of the script, if everything has run successfully, we prepare an acknowledgement page to inform the user of success. Easy to say:

    ## Return an acknowledgement page
    &acknowledge_page;  
but a bit harder to implement. If there is a custom response tag which matches some of the user's form data, we redirect the acknowledgement page to that URL, using the HTTP Location: header. If not, and the web form specifies a thanks page, we redirect to that URL. If none of these, we print the HTML source for a simple default page.
     If there is a custom response tag, the page owner will have supplied one or more triples, each specifying the tag name and value which trigger the URL response, in that order. We store all of these in an array @triples and cycle through them by threes, looking for a match on the name and value. If there is a match, print the URL to STDOUT if in test mode, or redirect to that URL.

    sub acknowledge_page {
    	local($i, @triples);
    
    	## search for a match among custom responses
    	if ( $in{'.custom_response_url'} ) {
    
    	    ## for each custom response, the triple is:
    	    ## form tag name :: triggering value :: response url
    	    @triples = split( /::/, $in{'.custom_response_url'} );
    
    	    for ($i=0; $i<$#triples; $i+=3) {
    
    		## if name matches value
    		if ( $in{"$triples[$i]"} eq $triples[$i+1] ) {  
    
    		    ## print URL
    		    if ( $in{'.test'} ) {
    			print "The acknowledgement page that would be 
    			       returned is: 
    			       <A HREF=\"$triples[$i+2]\">
    			       $triples[$i+2]</A>";
    		    } else {  
    			print "Location:  $triples[$i+2]\n\n"; 
    		    }
    		    exit;
    		}
    	    }
    	}  
If there is no match, then we return the thanks page if it was specified, or the built-in default thanks page if not.
    
    	## if no custom response matches, return the supplied 
    	## base thanks page, if any
    	if ( $in{'.thanks_url'} ) {
    	    if ( $in{'.test'} ) {  # test mode
    		print "The acknowledgement page that would be 
    		       returned is:
    		       <A HREF=\"$in{'.thanks_url'}\">
    		       $in{'.thanks_url'}</A>";
    	    } else { 
    		print "Location:  $in{'.thanks_url'}\n\n";
    	    }
    
    	## or just return the built-in default
    	} else {
    	    print &thanks_page;
    	}
    }  
The thanks_page function returns the HTML source for a very modest thanks page. It also checks for the existence of a hidden tag directing the user to another page. If one exists, one more line is appended with that link in it.

    sub thanks_page {
        local ($page_source) = <<"THANKS_PAGE"; 
    Content-type: text/html\n\n   
    <TITLE>Form Acknowledgement</TITLE>
    <H2>Thank You</H2>
    Your form data have been submitted to 
        <A HREF="mailto:$in{'.email_target'}">
        $in{'.email_target'}</A>.
    Thank you for taking the time to fill out the form.
    THANKS_PAGE
        
        if ( $in{'.back_to_url'} ) { 
            $page_source .= 
                "<P>Return to <A 
                HREF=\"$in{'.back_to_url'}\">
                $back_to_url</A>";
        }
        return $page_source;
    }  
That's all.

Download the code

You can view a clean copy of the code. To install it on your server, save it to a file in your cgi-bin directory, make it executable, perhaps rename it according to your server's cgi naming requirements, and perhaps change the path to perl on the first line. Server configurations differ a lot, so I can't give you precise instructions on the necessary steps. If you've never installed a cgi script before, you should talk to your system administrator about how to do it, or even if it is permitted at all.
     It's better to save the file from your browser rather than copy and paste. This is because the script uses "here document" quoting, which is sensitive to whitespace. If you do copy and paste, make sure there in no leading or trailing whitespace in the lines which contain only the following bare words:

    INSTRUCTIONS_PAGE 
    LAST_PART 
    FIRST_HALF 
    SECOND_HALF 
    THANKS_PAGE  

     You may also wish to modify the script so that only you can use it. The easiest way to do this is to add the following line immediately the &ReadParse function is called near the top of the script:

    $in{'.email_target'} = 'you@yourSite.com';  
Have fun.


CGI Resources. Copyright 1995-97, Sanford Morton
Last modified: Tue Mar 11 19:29:20 PDT