package CGI::NMS::Script::FormMail;
use strict;
use vars qw($VERSION);
$VERSION = substr q$Revision: 1.12 $, 10, -1;
use Socket; # for the inet_aton()
use CGI::NMS::Script;
use CGI::NMS::Validator;
use CGI::NMS::Mailer::ByScheme;
use base qw(CGI::NMS::Script CGI::NMS::Validator);
=head1 NAME
CGI::NMS::Script::FormMail - FormMail CGI script
=head1 SYNOPSIS
#!/usr/bin/perl -wT
use strict;
use base qw(CGI::NMS::Script::FormMail);
use vars qw($script);
BEGIN {
$script = __PACKAGE__->new(
'DEBUGGING' => 1,
'postmaster' => 'me@my.domain',
'allow_mail_to' => 'me@my.domain',
);
}
$script->request;
=head1 DESCRIPTION
This module implements the NMS plugin replacement for Matt Wright's
FormMail.pl CGI script.
=head1 CONFIGURATION SETTINGS
As well as the generic NMS script configuration settings described in
L, the FormMail constructor recognizes the following
configuration settings:
=over
=item C
Some web proxies and office firewalls may strip certain headers from the
HTTP request that is sent by a browser. Among these is the HTTP_REFERER
that FormMail uses as an additional check of the requests validity - this
will cause the program to fail with a 'bad referer' message even though the
configuration seems fine.
In these cases, setting this configuration setting to 1 will stop the
program from complaining about requests where no referer header was sent
while leaving the rest of the security features intact.
Default: 1
=item C
The maximum number of e-mail addresses that any single form should be
allowed to send copies of the e-mail to. If none of your forms send
e-mail to more than one recipient, then we recommend that you improve
the security of FormMail by reducing this value to 1. Setting this
configuration setting to 0 removes all limits on the number of recipients
of each e-mail.
Default: 5
=item C
The system command that the script should invoke to send an outgoing email.
This should be the full path to a program that will read a message from
STDIN and determine the list of message recipients from the message headers.
Any switches that the program requires should be provided here.
For example:
'mailprog' => '/usr/lib/sendmail -oi -t',
An SMTP relay can be specified instead of a sendmail compatible mail program,
using the prefix C, for example:
'mailprog' => 'SMTP:mailhost.your.domain',
Default: C<'/usr/lib/sendmail -oi -t'>
=item C
The envelope sender address to use for all emails sent by the script.
Default: ''
=item C
This configuration setting must be an array reference, holding a list
of names and/or IP address of systems that will host forms that refer
to this FormMail. An empty array here turns off all referer checking.
Default: []
=item C
This configuration setting must be an array reference.
A list of the email addresses that FormMail can send email to. The
elements of this list can be either simple email addresses (like
'you@your.domain') or domain names (like 'your.domain'). If it's a
domain name then any address at that domain will be allowed.
Default: []
=item C
This configuration setting must be an array reference.
A list of Perl regular expression patterns that determine who the
script will allow mail to be sent to in addition to those set in
C. This is present only for compatibility with the
original FormMail script. We strongly advise against having anything
in C as it's easy to make a mistake with the regular
expression syntax and turn your FormMail into an open SPAM relay.
Default: []
=item C
This configuration setting must be a hash reference.
A hash for predefining a list of recipients in the script, and then
choosing between them using the recipient form field, while keeping
all the email addresses out of the HTML so that they don't get
collected by address harvesters and sent junk email.
For example, suppose you have three forms on your site, and you want
each to submit to a different email address and you want to keep the
addresses hidden. You might set up C like this:
%recipient_alias = (
'1' => 'one@your.domain',
'2' => 'two@your.domain',
'3' => 'three@your.domain',
);
In the HTML form that should submit to the recipient C,
you would then set the recipient with:
Default: {}
=item C
This configuration setting must be an array reference.
A list of all the environment variables that you want to be able to
include in the email.
Default: ['REMOTE_HOST','REMOTE_ADDR','REMOTE_USER','HTTP_USER_AGENT']
=item C
The format that the date will be displayed in, as a string suitable for
passing to strftime().
Default: '%A, %B %d, %Y at %H:%M:%S'
=item C
The empty string to use local time for the date, or an offset from GMT
in hours to fix the timezone independent of the server's locale settings.
Default: ''
=item C
If this is set to 1 then rather than returning the HTML confirmation page
or doing a redirect the script will output a header that indicates that no
content will be returned and that the submitted form should not be
replaced. This should be used carefully as an unwitting visitor may click
the submit button several times thinking that nothing has happened.
Default: 0
=item C
If this is set to 1 then a blank line is printed after each form value in
the e-mail. Change this value to 0 if you want the e-mail to be more
compact.
Default: 1
=item C
If an input occurs multiple times, the values are joined to make a
single string value. The value of this configuration setting is
inserted between each value when they are joined.
Default: ' '
=item C
If this is set to 1 then the content of any long text fields will be
wrapped at around 72 columns in the e-mail which is sent. The way that
this is done is controlled by the C configuration setting.
Default: 0
=item C
If C is set to 1 then if this is set to 1 then the text will
be wrapped in such a way that the left margin of the text is lined up
with the beginning of the text after the description of the field -
that is to say it is indented by the length of the field name plus 2.
If it is set to 2 then the subsequent lines of the text will not be
indented at all and will be flush with the start of the lines. The
choice of style is really a matter of taste although you might find
that style 1 does not work particularly well if your e-mail client
uses a proportional font where the spaces of the indent might be
smaller than the characters in the field name.
Default: 1
=item C
If C is set to 0 then the full address for the user who filled
in the form will be used as "$email ($realname)" - this is also what the
format will be if C is true.
If it is set to 1 then the address format will be "$realname <$email>".
Default: 0
=item C
Configuration settings of this form can be used to fix configuration
settings that would normally be set in hidden form fields. For
example, to force the email subject to be "Foo" irrespective of what's
in the C form field, you would set:
'force_config_subject' => 'Foo',
Default: none set
=item C
Configuration settings of this form can be used to treat particular
configuration inputs as normal data inputs as well as honoring their
special meaning. For example, a user might use C
to include the email address as a regular input as well as using it in
the email header.
Default: none set
=back
=head1 COMPILE TIME METHODS
These methods are invoked at CGI script compile time only, so long as
the new() call is placed inside a BEGIN block as shown above.
=over
=item default_configuration ()
Returns the default values for the configuration passed to the new()
method, as a key,value,key,value list.
=cut
sub default_configuration {
return (
allow_empty_ref => 1,
max_recipients => 5,
mailprog => '/usr/lib/sendmail -oi -t',
postmaster => '',
referers => [],
allow_mail_to => [],
recipients => [],
recipient_alias => {},
valid_ENV => [qw(REMOTE_HOST REMOTE_ADDR REMOTE_USER HTTP_USER_AGENT)],
date_fmt => '%A, %B %d, %Y at %H:%M:%S',
date_offset => '',
no_content => 0,
double_spacing => 1,
join_string => ' ',
wrap_text => 0,
wrap_style => 1,
address_style => 0,
);
}
=item init ()
Invoked from the new() method inherited from L,
this method performs FormMail specific initialization of the script
object.
=cut
sub init {
my ($self) = @_;
if ($self->{CFG}{wrap_text}) {
require Text::Wrap;
import Text::Wrap;
}
$self->{Valid_Env} = { map {$_=>1} @{ $self->{CFG}{valid_ENV} } };
$self->init_allowed_address_list;
$self->{Mailer} = CGI::NMS::Mailer::ByScheme->new($self->{CFG}{mailprog});
}
=item init_allowed_address_list ()
Invoked from init(), this method sets up a hash with a key for each
allowed recipient email address as C and a hash with a
key for each domain at which any address is allowed as C.
=cut
sub init_allowed_address_list {
my ($self) = @_;
my @allow_mail = ();
my @allow_domain = ();
foreach my $m (@{ $self->{CFG}{allow_mail_to} }) {
if ($m =~ /\@/) {
push @allow_mail, $m;
}
else {
push @allow_domain, $m;
}
}
my @alias_targets = split /\s*,\s*/, join ',', values %{ $self->{CFG}{recipient_alias} };
push @allow_mail, grep /\@/, @alias_targets;
# The username part of email addresses should be case sensitive, but the
# domain name part should not. Map all domain names to lower case for
# comparison.
my (%allow_mail, %allow_domain);
foreach my $m (@allow_mail) {
$m =~ /^([^@]+)\@([^@]+)$/ or die "internal failure [$m]";
$m = $1 . '@' . lc $2;
$allow_mail{$m} = 1;
}
foreach my $m (@allow_domain) {
$m = lc $m;
$allow_domain{$m} = 1;
}
$self->{Allow_Mail} = \%allow_mail;
$self->{Allow_Domain} = \%allow_domain;
}
=back
=head1 RUN TIME METHODS
These methods are invoked at script run time, as a result of the call
to the request() method inherited from L.
=over
=item handle_request ()
Handles the core of a single CGI request, outputting the HTML success
or error page or redirect header and sending emails.
Dies on error.
=cut
sub handle_request {
my ($self) = @_;
$self->{Hide_Recipient} = 0;
my $referer = $self->cgi_object->referer;
unless ($self->referer_is_ok($referer)) {
$self->referer_error_page;
return;
}
$self->check_method_is_post or return;
$self->parse_form;
$self->check_recipients( $self->get_recipients ) or return;
my @missing = $self->get_missing_fields;
if (scalar @missing) {
$self->missing_fields_output(@missing);
return;
}
my $date = $self->date_string;
my $email = $self->get_user_email;
my $realname = $self->get_user_realname;
$self->send_main_email($date, $email, $realname);
$self->send_conf_email($date, $email, $realname);
$self->success_page($date);
}
=item date_string ()
Returns a string giving the current date and time, in the configured
format.
=cut
sub date_string {
my ($self) = @_;
return $self->format_date( $self->{CFG}{date_fmt},
$self->{CFG}{date_offset} );
}
=item referer_is_ok ( REFERER )
Returns true if the referer is OK, false otherwise.
=cut
sub referer_is_ok {
my ($self, $referer) = @_;
unless ($referer) {
return ($self->{CFG}{allow_empty_ref} ? 1 : 0);
}
if ($referer =~ m!^https?://([^/]*\@)?([\w\-\.]+)!i) {
my $refhost = $2;
return $self->refering_host_is_ok($refhost);
}
else {
return 0;
}
}
=item refering_host_is_ok ( REFERING_HOST )
Returns true if the host name REFERING_HOST is on the list of allowed
referers, or resolves to an allowed IP address.
=cut
sub refering_host_is_ok {
my ($self, $refhost) = @_;
my @allow = @{ $self->{CFG}{referers} };
return 1 unless scalar @allow;
foreach my $test_ref (@allow) {
if ($refhost =~ m|\Q$test_ref\E$|i) {
return 1;
}
}
my $ref_ip = inet_aton($refhost) or return 0;
foreach my $test_ref (@allow) {
next unless $test_ref =~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;
my $test_ref_ip = inet_aton($test_ref) or next;
if ($ref_ip eq $test_ref_ip) {
return 1;
}
}
}
=item referer_error_page ()
Invoked if the referer is bad, this method outputs an error page
describing the problem with the referer.
=cut
sub referer_error_page {
my ($self) = @_;
my $referer = $self->cgi_object->referer || '';
my $escaped_referer = $self->escape_html($referer);
if ( $referer =~ m|^https?://([\w\.\-]+)|i) {
my $host = $1;
$self->error_page( 'Bad Referrer - Access Denied', <
The form attempting to use this script resides at $escaped_referer,
which is not allowed to access this program.
If you are attempting to configure FormMail to run with this form,
you need to add the following to \@referers, explained in detail in the
README file.
Add '$host' to your \@referers array.
END
}
elsif (length $referer) {
$self->error_page( 'Malformed Referrer - Access Denied', <
The referrer value $escaped_referer cannot be parsed, so
it is not possible to check that the referring page is allowed to
access this program.
END
}
else {
$self->error_page( 'Missing Referrer - Access Denied', <
Your browser did not send a Referer header with this
request, so it is not possible to check that the referring page
is allowed to access this program.
END
}
}
=item check_method_is_post ()
Unless the C configuration setting is false, this method checks
that the request method is POST. Returns true if OK, otherwise outputs
an error page and returns false.
=cut
sub check_method_is_post {
my ($self) = @_;
return 1 unless $self->{CFG}{secure};
my $method = $self->cgi_object->request_method || '';
if ($method ne 'POST') {
$self->error_page( 'Error: GET request', <
The HTML form fails to specify the POST method, so it would not
be correct for this script to take any action in response to
your request.
If you are attempting to configure this form to run with FormMail,
you need to set the request method to POST in the opening form tag,
like this:
<form action="/cgi-bin/FormMail.pl" method="post">
END
return 0;
}
else {
return 1;
}
}
=item parse_form ()
Parses the HTML form, storing the results in various fields in the
C object, as follows:
=over
=item C
A hash holding the values of the configuration inputs, such as
C and C.
=item C