#!/usr/bin/perl ## $Id: mailserv,v 3.29 2002/10/23 00:51:32 fitz Exp $ ## ## MailServ - displays and processes forms for mailing list manager commands ## Copyright (C) 1996,1997 Patrick Fitzgerald ## See end of file for GNU Public License information. ## ## Complete documentation is provided at the following URL: ## http://csicop.org/~fitz/www/mailserv/ # Version of MailServ $version = &fixVersion('$Name: v4_4 $'); # Initial header for browser print "Content-type: text/html\n"; ##---------------------------------------------------------------------- ## Initial CGI setup ## ## MailServ uses the Perl include path to find the list ## config files, and to find the cgi-lib.pl file. ## ## For security reasons, we will wipe out the existing ## include path and replace it with our own. You might ## have to use the full directory paths if MailServ cannot ## find the required files. Also, be sure that all of ## the include files are write-protected, otherwise someone ## could add malicious code to them. @INC = (".", "./config"); &cgiRequire("cgi-lib.pl"); &ReadParse(); ##---------------------------------------------------------------------- ## Config - you *must* change some of these. ## You can also set these in the individual config files. ## # debug # Set to 1 for some debug info in the HTML output (use your # browser's "view source" command to view the HTML). You can also # set this with $config::debug or the "debug" form input. $debug = 0; # errors # Address to which errors should be sent. # Change this to your address. # You can override this with the "errors" form input. $errors = 'fitz@csicop.org'; # logfile # If you want to log the use of MailServ on your system, # (useful for hunting down certain problems) # specify the filename here. If MailServ cannot write # the file, it just ignores it and continues. $logfile = "LOGFILE"; # max # Maximum number of commands users can select. # You can override this with $config::max or the "max" form input. # $config::max always takes precedence. $max = 10; # mailer # Program into which mail is piped # You can also override this with $config::mailer. $mailer = "/usr/lib/sendmail -t"; # mailer_accepts_f # Use "-f address" for the mailer? # Your sendmail must be configured correctly for this to work. # This is used to properly direct mailer error messages to # the errors address (instead of bounce messages going to the # web server UID). # You can override this with $config::mailer_accepts_f $mailer_accepts_f = 1; # referrer_regexp # One or more regular expressions - the HTTP_REFERER env variable # must match one of these or the script will not be allowed to run. # This prevents people from linking to MailServ on your server if you # don't want them to. # # You should change this to your web server to prevent people from # "hijacking" your server to run MailServ from their web pages on # another server; or you can just comment out the lines to allow # MailServ to be run from any referrer. @referrer_regexp = ( 'csicop\.org', ); # address_allow_regexp # If you configure this variable, then only these email addresses # can be used for the "to" address. This prevents people from using # MailServ on your server for unwanted mailing lists. @address_allow_regexp = ( # 'foo\@bar\.com', ); # address_refuse_regexp # Mailing list addresses to lock out, and the reasons they # are being locked out. I use this if someone improperly # links to MailServ and causes bounces to be sent to me. %address_refuse_regexp = ( #'foo\@bar\.com', 'Put the reason for refusal here.', ); ##---------------------------------------------------------------------- ## END OF CONFIG ## You do not have to change anything beyond here. ## # Untaint the input (and keep a tainted copy) # This allows the input to be re-echoed without # worrying about embedded HTML characters foreach (keys %in) { # Delete the empty inputs if ($in{$_} eq "") { delete($in{$_}); next; } $in_tainted{$_} = $in{$_}; $in{$_} = &untaint( $in{$_} ); } # Size of some text fields $size = "20"; # Password markers (used in commands to delimit passwords # so I do not display them on the screen). For config files, # use the &config::hidePassword() subroutine $password{"begin"} = "__BEGINPASS__"; $password{"end"} = "__ENDPASS__"; $password{"placeholder"} = "PASS"; # Default subject for mail messages # You can override this with $config::subject or by setting # the "subject" form input. $subject = "mailing list request"; # Netscape cookie to remember e-mail address. # Comment this out if you do not want to use cookies. $cookie = "mailserv_from"; # HTML attributes for most tables $table_attr = "CELLSPACING=0 CELLPADDING=0"; ##---------------------------------------------------------------------- ## Load the MLM config file ## # Load the MLM config file - for example "majordomo.cf" or # "listserv.cf". We will use the name by which this script was invoked # to determine which config file to load, so if this script was # invoked as "majordomo", we will load the "majordomo.cf" config file. ($progname = $0) =~ s!.*/!!; &cgiRequire("$progname.cf"); ##---------------------------------------------------------------------- ## Override certain values depending on the config file and the form input ## $debug = $debug || $config::max || $in{"debug"}; $subject = $in_tainted{"subject"} || $config::subject || $subject; $max = $config::max || $in_tainted{"max"} || $max; $errors = $in_tainted{"errors"} || $errors; ##---------------------------------------------------------------------- ## Various templates ## $template{"mailserv"} = q!MailServ!; $template{"form start"} = < EOT $template{"form footer"} = <
EOT $template{"header"} = <
$template{"mailserv"} for $config::name mailing lists Help
EOT $template{"signature"} = <
$template{"mailserv"} $version is a freeware tool written by Patrick Fitzgerald
EOT # Set the Netscape cookie if we have a "from" value. # NOTE: this must be part of the MIME header. print &set_cookie($cookie, $in{"from"}) if ($cookie && defined $in{"from"} && $in{"from"} =~ /\w/); # Get the cookie if it is available and the from address has not been # entered yet $in{"from"} = &get_cookie($cookie) if ($cookie && ! defined($in{"from"})); # Print a blank line to mark the end of the headers print "\n"; ##---------------------------------------------------------------------- ## Display standard HTML info ## # Print some debug info print "\n"; print &debug() if $debug; print < MailServ by Patrick Fitzgerald $template{"header"}

EOT ##---------------------------------------------------------------------- ## Make sure they are allowed to use this program. Check ## referrer_regexp, address_allow_regexp, and address_refuse_regexp ## (see above for more info). ## if (@referrer_regexp) { my($allow) = 0; foreach (@referrer_regexp) { if ($ENV{HTTP_REFERER} =~ /$_/) { $allow = 1; last; } } &error("This CGI script is not allowed to be called from ", "HTTP_REFERER=$ENV{HTTP_REFERER}. It can be called ", "only from authorized web servers." ) unless $allow; } if ($in{to}) { # If address_allow_regexp is configured, check to make sure the # "to" address is in the list, otherwise error out if (@address_allow_regexp) { my($allow) = 0; foreach (@address_allow_regexp) { if ($in_tainted{"to"} =~ /$_/) { $allow = 1; last; } } &error("The address", qq!$in{to} !, "(list $in{list}) is locked out", "of MailServ because it is not in the allowed address list." ) unless $allow; } # Ifaddress_refuse_regexp is configured, then error out if the # "to" address is in the list. if (%address_refuse_regexp) { foreach (keys %address_refuse_regexp) { &error("The address", qq!$in{to} !, "(list $in{list}) is locked out", "of MailServ because:", "

", $address_refuse_regexp{$_}, ) if $in_tainted{"to"} =~ /$_/; } } } ##-------------------------------------------------- ## Split the already-selected commands into an array ## @commands = &splitMultiple($::in_tainted{"commands"}); ##-------------------------------------------------- ## Main Loop ## ## The following variables control the flow of MailServ: ## ## restart Remove all commands and start over ## ## send Send the selected command(s) ## ## select Process the command that the user selected ## This also assumes that a "cmd_XXX" variable is set. ## ## default List the available commands ## if ($in{"restart"}) { delete $in{"commands"}, $in_tainted{"commands"}; undef @commands; &formList(); } elsif ($in{"send"}) { &processSend(); } elsif ($in{"select"}) { &processCommand(); } else { &formList(); } # If in demo mode, display a message print("

", "Running in demo (no send) mode.\n", "
\n") if $in{"demo"}; # Append the signature line and exit. print $template{"form footer"}, $template{"signature"}; exit 0; ######################################################################## ##====================================================================== ## Subroutines ##====================================================================== ######################################################################## use strict ("subs", "vars"); sub retainState # # Returns a string that contains all of the information # that we want to remember in the next form: # # from, cc, to, list, password, commands, max, errors, # subject, demo, debug, url # { my($field,$cc_checked); $field = "\n"; # Display the from address $cc_checked = "CHECKED" if $::in{"cc"}; $field .= ("\n" . " \n" . " \n" . qq! \n! . " \n" . " \n" . " \n" . "
Your e-mail address:CC: yourself
\n" . "
\n"); # Display the mailing list name and the address within a table $field .= ("\n" . " \n"); # Display the mailing list name. unless ($config::noList) { $field .= (" \n" . " \n"); } # If we are going to also prompt for a password, # start a new row in the table; otherwise # keep everything in the same table row $field .= (" \n" . " \n") if $config::needPassword; # Display the mailing list request address. $field .= (" \n" . " \n" . " \n"); # Display a table with the list password if necessary if ($config::needPassword) { $field .= (" \n" . " \n" . " \n" . " \n"); # On the first screen, where they have not entered # their password yet, display a security warning unless ($::in{"password"}) { $field .= (" \n" . " \n" . " \n" . " \n"); } } $field .= "
List name:\n" . qq! \n! . "
List request address:\n" . qq! \n! . "
List password:\n" . qq! \n! . "
\n" . qq! Warning:\n! . " Passwords are not visible on the screen
\n" . " but are stored in the HTML source
\n" . " (in your browser cache).\n" . "
\n" . "
\n\n"; #-------------------------------------------------- # Display the already-selected commands # if (@::commands) { my($cmd, $i, $col, $max_col); local($*); $max_col = 3; $col = 1; $field .= ("
\n" . "You have selected the following commands:
\n" . "\n" . " \n"); # There can be multiple commands in the variable, # so loop for each one foreach (@::commands) { $_ = &untaint($_); if ($col > $max_col) { $field .= (" \n" . " \n"); $col = 1; } $col++; # Make a copy of the command # (one to put in a hidden variable, # the other to display on the screen). $cmd = $_; # In case the command contains multiple lines, # tell Perl to match across the end of line $* = 1; # If the command contains password info # delimited by $password{start} and $password{end}, # then do not display it on the screen; however, # the password still appears in the HTML source, # so do not consider this real security. # Warning: if the config file does not use the # password delimeters correctly (a start delimeter # followed by an end delimeter) then the whole # screen might be commented out! To be safe, always # use the &config::hidePassword() subroutine. $cmd = &obscurePassword($cmd); # If the command contains multiple lines, # only display the first one. For additional # lines, display "[X lines]" if ($cmd =~ m/\n/) { # Find the first CR $i = index($cmd, "\n"); # Remove everything after the CR $cmd = substr($cmd, 0, $i); # Count the number of lines, and add # "[X lines]" to the screen display $i = split(/\n/, $_) - 1; $cmd .= " [$i line" . ($i > 1 ? "s" : "") . "]"; } # Display the command next to a checkbox variable # with its value. The user can uncheck the checkbox # to cancel the command. $field .= (" \n" . " \n"); } $field .= (" \n" . "
\n" . qq! \n! . " $cmd
\n" . "
\n\n"); } # Remember the value of the max variable. $field .= qq!\n\n!; # Remember the value of the errors variable. $field .= qq!\n\n!; # Remember the value of the subject variable. $field .= qq!\n\n!; # Remember the value of the demo variable. $field .= qq!\n\n! if $::in{"demo"}; # Remember the value of the debug variable. $field .= qq!\n\n! if $::in{"debug"}; # Remember the URL that originally called MailServ. # This lets you sometimes track the starting link (for logging) if ($ENV{"HTTP_REFERER"}) { $::in{"url"} = &untaint($ENV{"HTTP_REFERER"}) unless $::in{"url"}; $field .= qq!\n\n!; } $field .= "\n\n"; # This subroutine does not print anything out, # it just returns the data $field; } sub formList # # Displays the list of mailserver commands, # with a submit button next to each command. # { my( $text, @text, @commands, $num, ); # Make sure we got the list of mail server commands # from the MLM config file &error("Config error - missing mail server commands.") unless @config::commands; # Display the list of commands print $::template{"form start"}, &retainState(); # If user has previously selected one or more commands, # Add the "send commands" options if (@::commands) { print "
\n", qq/\n/, "
\n", "

\n"; $num = $#::commands + 1; return if ($::in{"multi"} eq "0" || $num >= $::max) } else { # The user has not yet selected any commands. # If the user is allowed (by the "max" parameter) # to submit multiple commands, display # "Select: * single command, o multiple commands" if ($::max > 1) { print "


\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
Send commands:immediately, orafter multiple commands
\n"; } else { print "\n", " \n", " \n", " \n", "
\n", " Send commands: immediately\n", " \n", "
\n\n"; } } # Display the commands from which the user can choose print "\n", "\n", "\n", "\n", " \n", " \n", " \n", "\n", "
\n", " \n", " \n"; my($need_hr); foreach (@config::commands) { # If the commands array contains a blank entry, # that means to add another column to the table. if (/^\s*$/) { print "
\n", " \n", "
\n", " \n", " \n"; # Since we started a new column, we do not need an
# in front of the next command. $need_hr = 0; # Skip (this blank line) to the next command next; } # Print a
before the command # unless it is at the top of the table column print " \n" if $need_hr++; # Make sure the command has a description if ($config::description{$_}) { print " \n", " \n", qq! \n!, " \n", " \n", " \n\n"; } else { # The command did not have a description. # Display a warning message print "\n\n"; } } print "

\n", " $config::description{$_}\n"; # Does the command require additional input? # If so it will have a variable called # $config::command{"command-name"}, which contains HTML print("

\n", " \n", $config::command{$_}, "\n", " \n") if $config::command{$_}; print "

Error: no description for command ", "$_
\n", "\n", "
\n", "\n"; } sub processCommand # # Runs the code for a single command. # { my($command, @new_commands, $command_function); $command = &commandInput(); unless ($command) { &writeLog("ERROR\t= no selected command for browser", $ENV{"HTTP_USER_AGENT"}); &error("The form was submitted, but no command was selected.", "This usually indicates:\n", "
    \n", "
  1. that you pressed the Enter key in a text field\n", " instead of clicking the command button, or\n", "
  2. that your browser ($ENV{HTTP_USER_AGENT}) does not\n", " support multiple named submit buttons (which are\n", " required for MailServ). This error has been logged,\n", " and if I get enough of them perhaps I will modify\n", " MailServ to support older browsers.\n", "
\n", ); } if ($command eq "send") { &processSend(); return; } $command_function = "config::process_$command"; if (defined &$command_function) { @new_commands = &$command_function(); } else { &error("Error processing command: $@"); } # If the command set the subject, override it here $::subject = $config::subject if $config::subject; push(@::commands, @new_commands); # Now send the command or display the list again if ($::in{"multi"} eq "0") { &processSend(); } else { &formList(); } } sub processSend # # Sends the commands. # If all needed values are there, mails the commands # { my($msg, $msg_disp, @headers, @headers_disp, $status); # Check for missing/invalid input &error("You must select one or more commands before you send mail.") unless @::commands; &error("The from address ($::in{from}) ", "is not valid: $::validAddressError

", "You must specify your email address ", "before you send mail.") unless &validAddress($::in_tainted{"from"}); &error("The to address ($::in{to}) is not valid: ", $::validAddressError, "

", "You must specify the list request address before you ", "send mail (for example, "majordomo\@stooge.com").") unless &validAddress($::in_tainted{"to"}); if ($::mailer_accepts_f) { # Make sure the errors address does not contain # illegal shell characters (to guard against security compromise) if (&safeForShell($::errors) && &validAddress($::errors)) { $::mailer .= " -f '$::errors'"; } else { if ($::validAddressError) { &error(sprintf(qq{Errors address "%s"}, &untaint($::errors)), "is invalid: $::validAddressError"); } else { &error(sprintf(qq{Errors address "%s"}, &untaint($::errors)), "contains illegal shell characters."); } } } #-------------------------------------------------- # Make the mail headers. # @headers_disp is the list that will be shown to the users, # @headers contains additional headers for the mailed message # @headers_disp = ( "To: $::in_tainted{to}", "From: $::in_tainted{from}", "Subject: $::subject", ); push(@headers_disp, "CC: $::in_tainted{from}") if $::in{"cc"}; @headers = ( @headers_disp, "X-Comments: this request sent via the $::progname WWW form", "X-Www-Server-Name: $ENV{SERVER_NAME}", "X-Www-Remote-Host: $ENV{REMOTE_HOST}", "X-Www-Remote-Addr: $ENV{REMOTE_ADDR}", "X-Mailer: $::mailer", ); push(@headers, "X-URL: $::in_tainted{url}") if $::in_tainted{"url"}; #-------------------------------------------------- # Create the message # $msg = join("\n", @headers) . "\n\n"; $msg_disp = join("\n", @headers_disp) . "\n\n"; unless ($config::noBody) { foreach (@::commands) { $msg .= &unhidePassword("$_\n"); $msg_disp .= &obscurePassword($_, "delete") . "\n"; } } #-------------------------------------------------- # Send the message # if ($::in{"demo"}) { print "

Demo Complete

\n", "Use your browser's Back function to return ", "to the form.\n", "

Here is the message that would have been sent:\n"; } else { open(MAILER, "| $::mailer") || &error("Cannot fork mailer process."); print MAILER $msg; close(MAILER); &error("Mailer '$::mailer' returned error status '$status'") if ($status = ($? >> 8)); &writeLog(); print "

Your commands have been sent!

\n", "Check your e-mail ($::in{from}) to get the results ", "of your commands. It might take several minutes ", "(or longer) before you receive the results."; print "

You should also receive a copy of the message " . "that was sent to the mailing list server." if $::in{"cc"}; print "

The following message was sent to the list request address:\n"; } delete $::in{"commands"}, $::in_tainted{"commands"}; undef @::commands; print $::template{"form start"}, "

\n", "\n", "
\n", "
\n", &retainState(), $::template{"form footer"}; } sub commandInput # # From the main "select" screen, checks to see # if the user clicked one of the command buttons. # Returns the name of the selected command, or "send" # if they clicked the "send" button. # { local($_); if ($::in{"cmd-out"}) { return $::in{"cmd-out"}; } else { return "send" if $::in{"send"}; foreach (@config::commands) { next unless $config::description{$_}; return $_ if $::in{"cmd_$_"}; } } } sub writeLog # # Makes an entry in the MailServ log # { my($date,$entry); if ($::logfile && open(LOGFILE, ">>$::logfile")) { $entry = "\n"; if (@_) { $entry = "@_\n"; } chop($date = `date`); $entry .= "DATE\t= $date\n"; $entry .= "FROM\t= $::in_tainted{from}\n" if $::in{"from"}; $entry .= "TO\t= $::in_tainted{to}\n" if $::in{"to"}; $entry .= "LIST\t= $::in_tainted{list}\n" if $::in{"list"}; $entry .= "REFERER\t= $::in_tainted{url}\n" if $::in{"url"}; $entry .= "REMOTE_HOST\t= $ENV{REMOTE_HOST}\n" if $ENV{"REMOTE_HOST"}; $entry .= ("CMDS\t= " . &obscurePassword( join("\\n", @::commands), "remove") . "\n") if @::commands; print LOGFILE "$entry\n"; close LOGFILE; } } sub splitMultiple # # Uses "\0" to separate multiple selects. # # Example: # @array = &splitMultiple($string) # { my( $line, ) = @_; split(/\\0/, $line); } sub debug # # Returns a string containing the form input and environment, # for debugging purposes. # { my($key, $out, $output); local($*) = 1; $output .= ("\n" . "\n\n"; $output; } sub validAddress # # Returns 0 if the email address fails the test, otherwise # returns 1 and sets $::validAddressError. # USAGE: &validAddress($address) || die "bad address: $::validAddressError"; # # Thanks to Aaron Marco for some of the code, # originally taken from Matt's Script Archive mailing list. # { my($address) = @_; my($valid) = 1; $::validAddressError = ""; if (!$address) { $valid = 0; $::validAddressError = "Address is missing."; } elsif ($address =~ /[,|\/\\]/) { $valid = 0; $::validAddressError = "Address contains invalid character '$&'."; } elsif ($address =~ /@.*@/) { $valid = 0; $::validAddressError = "Address has more than one '\@' character."; } elsif ($address =~ /\.\./) { $valid = 0; $::validAddressError = "Address contains adjacent '.' characters."; } elsif ($address =~ /\.$/) { $valid = 0; $::validAddressError = "Address ends with a '.' character."; } elsif (($address !~ /^[\w\-\.]+[\%\+]?[\w\-\.]*\@[0-9a-zA-Z\-]+\.[0-9a-zA-Z\-\.]+$/)) { $valid = 0; $::validAddressError = "Address has invalid format."; } $valid; } sub safeForShell # # Returns 0 if the shell command fails the test, 1 otherwise. # { local($_) = @_; local($*) = 1; return 0 if m/[;<>\*\|`&\$!#\(\)\[\]\{\}:'"\n\s]/; 1; } sub config::hidePassword # # This function attempts to hide a password by placing # special delimeters around it that will be removed later on. # It is meant for use in the config files; thus, it is # placed in the config package. # # Example (within a config file): # $command = "approve $list " . hidePassword($pass) . " $address"; # { $::password{"begin"} . "@_" . $::password{"end"}; } sub unhidePassword # # This function removes all of the hidePassword markers from a # string. # # Example: # $command = &unhidePassword($command); # { my($command) = @_; $command =~ s/$::password{"begin"}//g; $command =~ s/$::password{"end"}//g; $command; } sub obscurePassword # # This function takes a string that contains hidden # passwords and replaces the marked passwords with # something that you can display on the screen without # revealing the password. The actual password still appears # in the HTML source, but it is commented out. # # If you specify the optional DELETE argument, the # entire password will be removed, but this has the # (unlikely) possibility of removing too much of the command. # This option is used when logging the command, so # other people using MailServ will not reveal their # passwords to me. # # Example: # print &obscurePassword($command), "\n"; # printf(LOG "PASS=%s\n", &obscurePassword($command, "delete")); # { local($_) = shift; my($delete) = @_; if ($delete) { # This has the potential to remove too much, # but better be safe than sorry. s/$::password{"begin"}\S*$::password{"end"}/$::password{"placeholder"}/g; } else { s/$::password{"begin"}/$::password{"placeholder"}/g; } $_; } sub untaint # # Removes the special HTML characters from a string, # so you can safely display it output HTML # { my($string) = @_; $string =~ s/&/&/g; $string =~ s//>/g; $string =~ s/\"/"/g; $string; } sub setInput # # Change the CGI input, plus the untainted CGI input # Used by the config files # { my($name,$value) = @_; $::in_tainted{$name} = $value; $::in{$name} = &untaint($value); $value; # return the value } #-------------------------------------------------- # Netscape Cookie Subroutines # # The Netscape browser can remember simple values for future use. # The value is stored in a variable called a "cookie". # I use a cookie to remember the from address, so # if they use MailServ again, they will not have to type it in. sub set_cookie # # Returns a Netscape Set-Cookie header. # Example: # print "Content-type: text/html\n", &set_cookie("cookiename", "value"), "\n"; # { my($name, $value) = @_; $name =~ s/;=//g; $value =~ s/;=//g; "Set-Cookie: $name=$value; path=/; expires=Fri, 31-Dec-99 00:00:00 GMT\n"; } sub get_cookie # # Returns the value of a cookie sent by the Netscape browser. # Returns the undefined value if a cookie was not received. # { my($name) = @_; my(@cookies,$name_value); @cookies = split(/[\s;]/, $ENV{"HTTP_COOKIE"}); foreach $name_value (@cookies) { return $1 if $name_value =~ /^$name=(.*)/; } undef; } sub fixVersion # # Fix the RCS version for display to the screen # { my($ver) = "@_"; while ($ver =~ s/\$\w+: ([^\$]*) \$/\1/g) { } while ($ver =~ s/,v//g) { } while ($ver =~ s/_/./g) { } $ver = "($ver)" if ($ver); $ver; } sub error # # Displays an error message and then exits. # The first blank line is important to end the MIME headers. # { print <Error
@_

Use your browser's Back function to return to previous page. $::template{"signature"} EOT exit 1; } sub cgiRequire # # Require the file and exit if an error occurs # { my($file) = @_; eval(qq/require "$file"/) || &error("$@

\n", # eval error "Current directory is ", `pwd`, "

\n", "Include path is:\n", "

" . join("\n", @INC) . "
\n"); } __END__ This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSEŽ See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.