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>
<INPUT TYPE="hidden"
NAME=".email_target"
VALUE="user\@site.com">
</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
|