#!/usr/local/bin/perl # Script: mailmerge.cgi # Copyright 1995, Lincoln D. Stein. You are free to use and # redistribute this script so long as this copyright statement # remains intact. # Customized for Ithaca College: # 4/18/96 CFK # Added a REQUIRED option to the template file, in which you can specify # required form fields, and a NEXTURL option, in which you can specify a # URL to display after the form has been successfully submitted. # 3/3/98 CFK # Eliminate case-sensitivity in keywords (e.g. required and REQUIRED should # be equivalent). # # Update to work with latest version of CGI.pm (i.e. make equivalent to # standard mailmerge v1.2). # 3/4/98 RHK # Added a NEXT_HTML option that allows a user to specify HTML code for a # "Thank You" page instead of a pre-written NEXTURL page. # 3/11/98 RHK # Added a new MULTIVALUE_FORMAT called LIST which outputs multiple values # with
(in the case of NEXT_HTML) or \n (in the case of OUTPUT_FORM) # between them. # 2/10/99 RHK # Modified &save_file subroutine so that it too could display the # NEXT_HTML parameter # 2/15/99 RHK # Added "append", "inserta" (insert after), "insertb" (insert before) and # "overwrite" options to the "File:" ACTION in the form of # ACTION=File:/path/to/file.log,{append,inserta,insertb,overwrite} # and in the logfile must be an identifier "" which stands for # "Mailmerge Insertion Point" # 2/25/99 RHK # Added "FILE_OUTPUT_FORM" and "MAIL_OUTPUT_FORM" to allow separation of # formatting between the file log and the email message when doing a # multi-action'ed template; written as: # ACTION=mailto:email@address.com;File:/path/to/file.log,{append,inserta,insertb,overwrite} # 8/6/99 CFK # Due to changes in the filesystem structure, if a template or a file # specification is not found, look up the directory in the "directory # move" database to see if it is in a different location. # This is the CGI.pm library, which must be installed in # /usr/local/lib/perl5 or equivalent. use CGI; # The path to the sendmail program may need to be set for # your site. $MAIL = '/usr/lib/sendmail'; $ENV{'PATH'}='/bin:/usr/bin:/usr/lib'; # For logfile pathname translation, change this if your site # uses something different for its user's home web directories. $PUBLIC_HTML = 'public_html'; # See the material following the __END__ for more user-configurable # options. ##################################################### # From here to __END__ should not need to be changed ##################################################### %VERBOTEN = ('ACTION'=>1,'DATE'=>1,'SCRIPT'=>1,'REFERER'=>1, 'REMOTE_USER'=>1,'USER_AGENT'=>1,'REMOTE_HOST'=>1, 'SERVER'=>1,'SERVER_HOST'=>1,'ADMINISTRATOR'=>1, 'ESCAPE_HTML'=>1); # Create a new CGI object $query = new CGI; # Read our configuration files and store the token # lookup table in "dictionary" ($dictionary,$forms) = &read_configuration($query); # Add information from the parsed form to the lookup table. foreach ($query->param()) { next if $VERBOTEN{$_}; # certain parameters can't be set by the remote user $dictionary->{$_}=&process_user_input($dictionary, $dictionary->{ESCAPE_HTML}, $query->param($_)); } # Check the REQUIRED fields (if any) and make sure they were filled in # before continuing to process the form. CFK if ($dictionary->{REQUIRED} ne "NONE") { # Parse the required fields @required = split(/\,/, $dictionary->{REQUIRED}); foreach (@required) { # Remove any white space from the field name $_ =~ s/\s//g; push (@missing, $_) if ($dictionary->{$_} =~ /^\s*$/); } if (@missing) { # Print out the HTTP header print $query->header; print $query->start_html("Information Missing"); print <<"EOF";

Information Missing

Sorry, you did not fill out all of the required information in the form. The following required fields are missing: \nPlease use your browser's back button to go back and try again."; print $query->end_html; exit; } } # Start an HTML header if we aren't going to return some other page. print $query->header if ($dictionary->{NEXTURL} eq "NONE" || $dictionary->{NEXTURL} !~ /http\:\/\/.*\..*/); # Try to post mail if we're being called by a POST request # and the lookup table contains the TO field. if (($ENV{REQUEST_METHOD} eq 'POST') && $dictionary->{TO}) { # RHK - added conditional to handle multi-action templates if ($dictionary->{ACTION}=~/;/) { ($action1,$action2) = split(/\s*;\s*/,$dictionary->{ACTION}); if ($action1=~/^mailto:\s*(.+)/i) { $dictionary->{TO} = $1; $filelog = (split(/\:\s*/,$action2))[1]; ($filelog,$action) = split(/\s*,\s*/,$filelog); } elsif ($action1=~/^file:\s*(.+)/i) { $filelog = $1; ($filelog,$action) = split(/\s*,\s*/,$filelog); $dictionary->{TO} = (split(/\:\s*/,$action2))[1]; } if ($forms->{FILE_OUTPUT_FORM} && $forms->{MAIL_OUTPUT_FORM}) { &save_file($filelog,$forms->{FILE_OUTPUT_FORM},$dictionary,$forms->{NEXT_HTML},$action); &send_mail($forms->{MAIL_HEADER},$forms->{MAIL_OUTPUT_FORM},$forms->{NEXT_HTML},$dictionary); } else { &save_file($filelog,$forms->{OUTPUT_FORM},$dictionary,$forms->{NEXT_HTML},$action); &send_mail($forms->{MAIL_HEADER},$forms->{OUTPUT_FORM},$forms->{NEXT_HTML},$dictionary); } } elsif ($dictionary->{ACTION}=~/^mailto:\s*(.+)/i) { $dictionary->{TO} = $1; # RHK - added $forms->{NEXT_HTML} parameter &send_mail($forms->{MAIL_HEADER},$forms->{OUTPUT_FORM},$forms->{NEXT_HTML},$dictionary); } elsif ($dictionary->{ACTION}=~/^mail/i) { # RHK - added $forms->{NEXT_HTML} parameter &send_mail($forms->{MAIL_HEADER},$forms->{OUTPUT_FORM},$forms->{NEXT_HTML},$dictionary); } elsif ($dictionary->{ACTION}=~/^file:\s*(\S+)/i) { $temp = $1; # RHK - added $action variable to be read from ACTION= ($temp,$action) = split(/,/,$1); # after comma in filename should be action &save_file($temp,$forms->{OUTPUT_FORM},$dictionary,$forms->{NEXT_HTML},$action); } # Otherwise use the INPUT_FORM field to generate a new form # for the user to fill out. } else { &generate_form($forms->{INPUT_FORM},$dictionary); } # The rest of this is advertising. # RHK - if NEXT_HTML is defined, don't display footer if (($dictionary->{NEXTURL} eq "NONE") && !($forms->{NEXT_HTML})) { print < This form generated by mailmerge.
$dictionary->{AUTHOR}
$dictionary->{ADDRESS}
EOF ; print $query->end_html; } ################### SUBROUTINES ############### # ------------ Create the form ----------- sub generate_form { my($form,$dictionary) = @_; $form = &do_substitutions($form,$dictionary); print < $form EOF } # Join multivalued parameters according to the author's # preference. sub process_user_input { my($config,$escape_html,@values) = @_; # Bugfix for missing zero (i.e. "0") values. 10/27/98 CFK @values = grep (defined($_),@values); # @values = grep ($_,@values); # This is a convenient place to escape any HTML characters # in the user's input, if present. if ($escape_html) { foreach (@values) { $_ = &CGI::escapeHTML({},$_); } } return $values[0] unless @values > 1; if ($config->{MULTIVALUE_FORMAT} eq 'BRACES') { return "{" . join(",",@values) . "}"; } elsif ($config->{MULTIVALUE_FORMAT} eq 'LIST') { return join("
",@values); } elsif ($config->{MULTIVALUE_FORMAT} eq 'COMMA') { return join(",",@values); } else { my $last = pop @values; return join(", ",@values) . " and $last"; } } # ----------------- Write message to a file ------------------ sub save_file { my($file,$body,$dictionary,$html,$action) = @_; # RHK - added parameters $html & $action # RHK - added function call $html = &do_substitutions($html,$dictionary,2); $body = &do_substitutions($body,$dictionary,1); my $logfile = &find_logfile($query,$file); &croak("$0: Specified log file '$file' does not exist. It must already be created and writable.") unless $logfile; # RHK - added file action checks $done = "no"; $action =~ s/\s+$//; $action =~ s/^\s+//; if ($action eq "append") { $action = ">>"; } elsif ($action eq "overwrite") { $action = ">"; } elsif ($action eq "inserta" || $action eq "insertb") { open(LOGFILE,"+<$logfile") || &croak("Failed opening log file $file: $!\n"); $out = ""; while () { if (/^/) { if ($action eq "inserta") { $out .= $_; $out .= $body; } else { $out .= $body; $out .= $_; } } else { $out .= $_; } } seek(LOGFILE,0,0) or die "Seeking: $!"; print LOGFILE $out or die "Printing: $!"; truncate(LOGFILE,tell(LOGFILE)) or die "Truncating: $!"; close(LOGFILE) or die "Closing: $!"; $done = "yes"; } else { $action = ">>"; } if ($done eq "no") { open(LOGFILE,"$action$logfile") || &croak("Failed opening log file $file: $!\n"); lock(LOGFILE,1); # print '-'x50,"\n"; select(LOGFILE); &wordwrap($body,$dictionary->{WORDWRAP}); lock(LOGFILE,undef); close(LOGFILE); } # Default is to print a copy to the screen. If NEXTURL is specified, # display that instead. CFK # RHK - if we are doing multi-action, skip the output # to screen because the same stuff is done in &send_mail unless ($dictionary->{ACTION}=~/;/) { select STDOUT; if ($dictionary->{NEXTURL} =~ /http\:\/\/.*\..*/) { print $query->redirect($dictionary->{NEXTURL}); } # RHK - added conditional for printing custom HTML elsif ($html) { print $html; } else { # Print another copy to the screen so the user can see # what's going out: # Print out the title and the top of the HTML document. print $query->start_html($dictionary->{TITLE}); print <$dictionary->{TITLE} The following is a copy of the entry that was submitted. EOF ; &wordwrap($body,$dictionary->{WORDWRAP},$dictionary->{ESCAPE_HTML}); print "

{REFERER},"\">Back to the form.\n"; } } } # ---------- Send out the form via E-mail --------- # One copy gets e-mailed. The other gets displayed # on the screen so that the remote user knows something happened. sub send_mail { my($header,$body,$html,$dictionary) = @_; # RHK - added parameter $header = &do_substitutions($header,$dictionary); $header = &remove_empty_fields($header); $html = &do_substitutions($html,$dictionary,2); # RHK - added function call $body = &do_substitutions($body,$dictionary,1); # Check the TO address for shell meta-characters if ($dictionary->{TO}=~/[;|\`\/]/) { print <The To: address "$dictionary->{TO}" contains illegal shell metacharacters. If this was unintentional, please try again. EOF ; return; } # remove any HTML before sending in an email if ($dictionary->{HTML_MAIL}=0) { $body =~ s/<[^>]*>//gs; } # We fork ourselves in order to avoid passing characters # through a shell. open (MAIL, "|-") || exec($MAIL,'-t','-oi'); select MAIL; print $header,"\n"; &wordwrap($body,$dictionary->{WORDWRAP}); close MAIL; # Default is to print a copy to the screen. If NEXTURL is specified, # display that instead. CFK select STDOUT; if ($dictionary->{NEXTURL} =~ /http\:\/\/.*\..*/) { print $query->redirect($dictionary->{NEXTURL}); } # RHK - added conditional for printing custom HTML elsif ($html) { print $html; } else { # Print another copy to the screen so the user can see # what's going out: print $query->start_html($dictionary->{TITLE}); print <$dictionary->{TITLE} The following is a copy of the mail that was submitted.

$header EOF ; &wordwrap($body,$dictionary->{WORDWRAP}); print < EOF ; print "

{REFERER},"\">Back to the form.\n"; } } # -------- Parse configuration form ------- # Pass the routine a filehandle open to the configuration # file sub read_configuration { my($query) = @_; my($dictionary) = {}; my($forms) = {}; my($user_config_file, $new_path); &config(DATA,$dictionary,$forms); # read our built-in defaults if (-e $query->path_info) { $user_config_file = $query->path_info; } elsif (-e $query->path_translated) { $user_config_file = $query->path_translated; ##### CFK - the user config file may have moved... } elsif (-e ($new_path = &site_moved($query->path_info))) { $user_config_file = $new_path; } else { # still can't find it, so try a rel. path my($path) = $query->path_info=~m#^/(\S+)#; $user_config_file = "./$path" if -e "./$path"; } if ($user_config_file) { open (USER_CONFIG,"$user_config_file") || &croak("$user_config_file: $!"); &config(USER_CONFIG,$dictionary,$forms); close(USER_CONFIG); } return ($dictionary,$forms); } # Read an individual configuration file. sub config { my($filehandle,$dictionary,$forms)=@_; my($tag,$value,$old); &fill_in_predefines($dictionary); while (<$filehandle>) { chomp; next if /^#/; next unless ($tag,$value) = /(^\w+)\s*=\s*(.*)/; # Eliminate case-sensitivity by converting to upper case $tag =~ tr/a-z/A-Z/; # RHK - added NEXT_HTML, FILE_OUTPUT_FORM, and MAIL_OUTPUT_FORM to tag list if ($tag=~/MAIL_HEADER|NEXT_HTML|OUTPUT_FORM|FILE_OUTPUT_FORM|INPUT_FORM/) { ($old,$/)=($/,"\n.\n"); chomp($forms->{$tag}=<$filehandle>); $forms->{$tag} .= "\n"; # add back a newline $/=$old; } else { $dictionary->{$tag} = &do_substitutions($value,$dictionary); } } } # Fill in a few of the special predefined tags. sub fill_in_predefines { my $dictionary = shift; $dictionary->{DATE}=localtime; $dictionary->{SCRIPT}=$query->script_name; $dictionary->{REFERER}=$query->referer; $dictionary->{REMOTE_USER}=$ENV{HTTP_FROM} || $query->remote_ident || $query->remote_user; $dictionary->{USER_AGENT}=$query->user_agent; $dictionary->{REMOTE_HOST}=$query->remote_host; my($name,$junk,$junk,$junk,$junk,$junk,$gcos) = getpwuid($<); $dictionary->{SERVER} = "$name\@" . $query->server_name . " ($gcos)"; $dictionary->{SERVER_HOST} = $query->server_name; $dictionary->{ADMINISTRATOR} = $ENV{SERVER_ADMIN}; } # Do the variable substitutions. sub do_substitutions { my($scalar,$dictionary,$tag) = @_; my($key); my(@keys) = $scalar=~/\@(\w+)\@/g; # Note that we use the unoptimized s/// form here for # security reasons -- check the performance! foreach $key (@keys) { # RHK - if processing OUTPUT_FORM, use \n instead of
if ($dictionary->{MULTIVALUE_FORMAT} eq "LIST" && $tag == 1) { $dictionary->{$key} =~ s/
/\n/g; } $scalar=~s/\@$key\@/$dictionary->{$key}/g; } return $scalar; } # A bit of aesthetic silliness -- remove empty # fields in the e-mail header sub remove_empty_fields { my $header = shift; do {} while $header=~s/\n[\w-]+:\s*\n/\n/mg; return $header; } # Wordwrap paragraphs sub wordwrap { my ($text,$wordwraplength) = @_; unless ($wordwraplength) { print $text; return; } my (@paragraphs) = split("\n\n",$text); my $para; foreach $para (@paragraphs) { my (@lines) = split("\n",$para); if (grep(length($_) > $wordwraplength,@lines)) { &compile_format($wordwraplength); ($xText=$para)=~tr/\n//; # get rid of the newlines $-=9999; # no page breaks write; } else { print "$para\n\n"; } } } # Compile the report format used to wordwrap lines sub compile_format { my $length = shift; $~=OUT; return if $FMT; my ($linepat) = '<' x $length; $FMT=<

An error occurred while processing this form:\n"; print "

$msg\n\n"; die $msg; } # -------------- file locking utilities ----------- sub lock { my ($lockfh,$lockit) = @_; my($LOCK_SH,$LOCK_EX,$LOCK_UN) = (1,2,8); if ($lockit) { flock($lockfh,$LOCK_EX) || croak "Couldn't get lock in lock()\n"; seek($lockfh,0,2); } else { flock($lockfh,$LOCK_UN); } } #### # Figure out the file to write to # for log entries. Look first for a # absolute URL, then a relative URL, then a physical # path name. File must already be created. ### sub find_logfile { my($query,$filename) = @_; my $new_path; my $document_root = &document_root($query); $filename=~s@~([^/]+)@&getlogin($1)@e; return "$document_root$filename" if -e "$document_root/$filename"; return $filename if -e $filename; ##### CFK - also look in the "new" location return "$new_path" if (-e ($new_path = &site_moved($filename))); return "$new_path" if (-e ($new_path = &site_moved("$document_root/$filename"))); return undef; } #### # Get login account for ~name substitution # #### sub getlogin { return (getpwnam($_[0]))[7] . "/$PUBLIC_HTML"; } #### # Utility routine determines the # document root by comparing path_info to path_translated #### sub document_root { my $query = shift; my $partial_path = $query->path_info(); my $full_path = $query->path_translated(); # find the partial path in the full path return undef unless $full_path=~/$partial_path$/o; return $`; # everything before the match is good } #### # CFK - lookup a directory in the "moved" database to see if the pathname # has changed. #### sub site_moved { my $path = $_[0]; my $move_db = "/web/web_admin/directories.txt"; my ($old, $new, $new_path, $last_old); $path = "/web" . $path unless ($path =~ /^\/web/); open (MOVE, "$move_db"); while () { ($old, $new) = split(/\t/); if ($path =~ /^$old\//) { # Make sure this is a "better" match than any previous one unless ($old lt $last_old) { ($new_path = $path) =~ s/^$old/$new/; $last_old = $old; } } } close (MOVE); #open (DEBUG, ">>/web/web_admin/debug.txt"); #print DEBUG "$path $new_path\n"; #close (DEBUG); return ($new_path) ? $new_path : $path; } __END__ ########################################################### # More user-configurable defaults. These can (and are # intended to) be overridden by values provided in a # configuration file whose address is passed to mailmerge # at the end of its URL: # /cgi-bin/mailmerge.cgi/path/to/config/file ########################################################### ### # You'll want to adjust these for your site ### # The title for the mailmerge page TITLE=Mail Merge Gateway # Author and address are printed at the bottom of the page AUTHOR=Chantelle Keller ADDRESS=Ithaca College ############ # The default action is to mail the message to whoever is defined in # the TO field ACTION=mail ############ # The default required field is "NONE". CFK REQUIRED=NONE ############ # The default next URL is "NONE". CFK NEXTURL=NONE ############ # If you're going to display the user's input in an HTML file, you # might want to set the ESCAPE_HTML variable to a non-zero value ESCAPE_HTML=0 ############ # this variable controls whether or not to allow HTML tags inside # an email message (no by default) HTML_MAIL=0 ############ # Default values for some e-mail fields. You may # want to change the default TO field. TO=webmaster@ithaca.edu FROM=@REMOTE_USER@@@REMOTE_HOST@ REPLY_TO=@ADMINISTRATOR@ SUBJECT=Web mail via the mailmerge gateway CC= BCC= BODY= ############ # Values that determine how the e-mail message will # be formatted. # Wordwrap width. Leave the value blank to disable automatic # word wrapping of long lines. WORDWRAP=72 # Set this field to BRACES to show multivalued parameters as "{a,b,c}". # Set to COMMAS to show multivalued parameters as "a, b and c". # CFK - Set to COMMA to show multivalued parameters as "a,b,c". # RHK - Set to LIST to show multivalued parameters as: # a # b # c MULTIVALUE_FORMAT=COMMAS ############ # This defines the default e-mail header. # You probably won't need to change it. MAIL_HEADER= To: @TO@ From: @FROM@ Cc: @CC@ Bcc: @BCC@ Subject: @SUBJECT@ X-mail-agent: mailmerge v1.1 . #Reply-to: @REPLY_TO@ ############ # This defines the input form. Everything from the # line INPUT_FORM= to the dot (.) at the very bottom # is the form. INPUT_FORM=

@TITLE@

Your Name
Your E-mail address
Subject

. ############ # This defines the output form -- the way the # various input fields are formatted into the body text of # the e-mail message. Everything from OUTPUT_FORM= # to the dot (.) at the very bottom is the form. OUTPUT_FORM= On @DATE@, the following message was submitted via the mailmerge server running on @SERVER@: NAME: @NAME@ ADDRESS: @FROM@ REFERER: @REFERER@ MESSAGE TEXT: @BODY@ .