FormMail.pl

Matt Wright's FormMail.pl was the most downloaded CGI script in web history — over 2 million downloads since 1997. It parses any HTML form and sends the results to specified email addresses via sendmail.

Perl v1.6

Quick Info

  • Author: Matt Wright
  • Version: 1.6 (May 2, 1997)
  • Language: Perl 5
  • License: Freeware
  • Files: FormMail.pl, README
  • CVEs: CVE-2001-0357, CVE-2002-0564

Overview

FormMail is a Perl CGI script that takes the input of any HTML form and sends it as an email to specified recipients. Configuration is done entirely through hidden form fields — no programming required to add forms to a site.

Written by Matt Wright in 1995, FormMail became the standard way to handle contact forms before server-side languages like PHP had built-in mail functions. By 1997 it had been downloaded over 2 million times from Matt's Script Archive.

Timeline

  • 1995: FormMail created by Matt Wright
  • 1997: Version 1.6 released; over 2 million downloads
  • 2001: CVE-2001-0357 disclosed (header injection)
  • 2002: CVE-2002-0564; ranked #3 attack vector by SecurityFocus
  • 2002: London Perl Mongers release NMS FormMail as a secure replacement
  • 2009: Version 1.93 addresses long-standing security issues

Package Contents

File Description
FormMail.pl The Perl script that processes form data and sends emails via sendmail
README Installation instructions and configuration guide

Security History & Alternatives

FormMail's popularity made it a primary target for spammers. The script lacked input sanitization, allowing attackers to inject arbitrary email headers and use servers as open relays. The transition away from FormMail followed a clear path: CVE disclosures (2001–2002) led to secure Perl rewrites (NMS, 2002), then PHP alternatives (Tectite, PHPMailer), and eventually hosted form services that require no server at all.

Self-Hosted Replacements

NMS FormMail

Drop-in replacement by London Perl Mongers (2002). Uses strict mode, taint checking, and CGI.pm. Recommended for existing Perl/CGI setups.

Free Perl

Tectite FormMail

Active since 2001. Available in both PHP and Perl. 23-year track record with only 4 minor security advisories.

Free PHP/Perl

PHPMailer

PHP email library with SMTP authentication, attachments, and HTML email support.

Free MIT

Third-Party Services

Formspree

Form backend for static sites. Point your form action to their endpoint. Free tier: 50 submissions/month.

Free tier From $8/mo

Web3Forms

Free form API with spam filtering. 250 submissions/month on free plan.

Free tier

Netlify Forms

Built into Netlify hosting. Add a netlify attribute to any HTML form. 100 free submissions/month.

Free tier Netlify

Security Comparison

Feature Original FormMail NMS / Tectite Third-Party Services
Header injection protectionNoYesYes
CSRF protectionNoOptionalYes
Rate limitingNoOptionalYes
Spam filteringNoBasicBuilt-in
CAPTCHA supportNoYesYes
Requires serverYesYesNo

Features

Form Processing

  • Parses any HTML form (text, textarea, select, checkbox, radio)
  • Required field validation
  • Custom success/error redirect pages
  • Single script handles unlimited forms

Email Options

  • Multiple recipients (comma-separated)
  • Custom subject lines and reply-to addresses
  • Configurable field ordering (alphabetic or custom)
  • Environment variable reporting (IP, user agent, etc.)

Hidden Form Fields Reference

Field Name Required Description
recipient Yes Email address(es) to receive form data
subject No Subject line for the email
redirect No URL to redirect to after submission
required No Comma-separated list of required fields
env_report No Environment variables to include in email
sort No Field order in email: order:field1,field2 or alphabetic
print_config No Configuration fields to include in email body
print_blank_fields No Include empty fields in output
title No Title for the built-in success page
return_link_url No URL for "return" link on the success page
return_link_title No Text for the return link

Installation

Step 1: Download and Extract

Download the FormMail package and extract it to your server.

Step 2: Configure the Script

Edit FormMail.pl and set these variables:

# Restrict to your domain (prevents cross-site use)
@referers = ('yourdomain.com', 'www.yourdomain.com');

# Whitelist allowed recipients (prevents open relay abuse)
@recipients = ('[email protected]', '[email protected]');

# Path to sendmail binary
$mailprog = '/usr/sbin/sendmail';

Step 3: Upload and Set Permissions

# Upload to your cgi-bin directory, then:
chmod 755 FormMail.pl

Step 4: Create Your Form

<form action="/cgi-bin/FormMail.pl" method="POST">
    <input type="hidden" name="recipient" value="[email protected]">
    <input type="hidden" name="subject" value="Contact Form">
    <!-- Your visible form fields here -->
</form>

Configuration Variables

Variable Description Example
@referers Domains allowed to submit to this script. Prevents cross-site abuse. ('yourdomain.com')
@recipients Whitelisted email recipients. Without this, the script is an open relay. ('[email protected]')
$mailprog Path to sendmail or compatible MTA. '/usr/sbin/sendmail'

Code Examples

Modern secure implementations of the same form-to-email functionality:

#!/usr/bin/perl
use strict;
use warnings;
use CGI;
use Email::Valid;
use HTML::Entities;

my $cgi = CGI->new;

# Configuration - whitelist recipients and referers
my @allowed_recipients = ('[email protected]');
my @allowed_referers = ('yourdomain.com');

# Validate referer
my $referer = $ENV{'HTTP_REFERER'} || '';
my $valid_referer = 0;
for my $allowed (@allowed_referers) {
    $valid_referer = 1 if $referer =~ /$allowed/i;
}
die "Invalid referer" unless $valid_referer;

# Get and validate form data
my $recipient = $cgi->param('recipient') || '';
my $subject = $cgi->param('subject') || 'Form Submission';
my $redirect = $cgi->param('redirect') || '';

my $valid_recipient = 0;
for my $allowed (@allowed_recipients) {
    $valid_recipient = 1 if lc($recipient) eq lc($allowed);
}
die "Invalid recipient" unless $valid_recipient;
die "Invalid email" unless Email::Valid->address($recipient);

# Build email body
my $body = "Form submission received:\n\n";
for my $param ($cgi->param) {
    next if $param =~ /^(recipient|subject|redirect)$/;
    my $value = encode_entities($cgi->param($param));
    $body .= "$param: $value\n";
}
$body .= "\n--- Environment ---\n";
$body .= "Remote IP: $ENV{'REMOTE_ADDR'}\n";
$body .= "User Agent: $ENV{'HTTP_USER_AGENT'}\n";

# Send via sendmail
open(my $mail, '|-', '/usr/sbin/sendmail', '-t') or die "Cannot send: $!";
print $mail "To: $recipient\n";
print $mail "Subject: $subject\n";
print $mail "Content-Type: text/plain; charset=UTF-8\n\n";
print $mail $body;
close($mail);

if ($redirect) {
    print $cgi->redirect($redirect);
} else {
    print $cgi->header();
    print "<h1>Thank You</h1><p>Your message has been sent.</p>";
}
<?php
// PHP Form Handler with security measures

$allowed_recipients = ['[email protected]', '[email protected]'];
$allowed_domains = ['yourdomain.com', 'www.yourdomain.com'];

// CSRF check
session_start();
if ($_POST['csrf_token'] !== $_SESSION['csrf_token']) {
    die('Invalid CSRF token');
}

// Validate referer
$referer = parse_url($_SERVER['HTTP_REFERER'] ?? '', PHP_URL_HOST);
if (!in_array($referer, $allowed_domains)) {
    die('Invalid referer');
}

// Sanitize inputs
$recipient = filter_var($_POST['recipient'] ?? '', FILTER_SANITIZE_EMAIL);
$subject = htmlspecialchars($_POST['subject'] ?? 'Form Submission', ENT_QUOTES, 'UTF-8');
$redirect = filter_var($_POST['redirect'] ?? '', FILTER_SANITIZE_URL);

// Validate recipient against whitelist
if (!in_array(strtolower($recipient), array_map('strtolower', $allowed_recipients))) {
    die('Invalid recipient');
}
if (!filter_var($recipient, FILTER_VALIDATE_EMAIL)) {
    die('Invalid email format');
}

// Build message body
$message = "Form submission received:\n\n";
$excluded = ['recipient', 'subject', 'redirect', 'csrf_token'];

foreach ($_POST as $key => $value) {
    if (in_array($key, $excluded)) continue;
    $clean_key = htmlspecialchars($key, ENT_QUOTES, 'UTF-8');
    $clean_value = htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
    $message .= "$clean_key: $clean_value\n";
}
$message .= "\n--- Environment ---\n";
$message .= "IP: " . $_SERVER['REMOTE_ADDR'] . "\n";
$message .= "User Agent: " . $_SERVER['HTTP_USER_AGENT'] . "\n";

// Send
$headers = [
    'From: [email protected]',
    'Reply-To: ' . ($_POST['email'] ?? '[email protected]'),
    'Content-Type: text/plain; charset=UTF-8'
];

if (mail($recipient, $subject, $message, implode("\r\n", $headers))) {
    if ($redirect) {
        header("Location: $redirect");
        exit;
    }
    echo "<h1>Thank You</h1><p>Your message has been sent.</p>";
} else {
    echo "<h1>Error</h1><p>Failed to send message.</p>";
}
// Node.js/Express form handler
const express = require('express');
const nodemailer = require('nodemailer');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');

const app = express();
app.use(helmet());
app.use(express.urlencoded({ extended: true }));

const limiter = rateLimit({
    windowMs: 15 * 60 * 1000, // 15 minutes
    max: 10
});

const ALLOWED_RECIPIENTS = ['[email protected]'];
const ALLOWED_DOMAINS = ['yourdomain.com'];

const transporter = nodemailer.createTransport({
    host: 'smtp.yourdomain.com',
    port: 587,
    auth: {
        user: process.env.SMTP_USER,
        pass: process.env.SMTP_PASS
    }
});

app.post('/contact', limiter, async (req, res) => {
    try {
        const { recipient, subject, redirect, ...formData } = req.body;

        // Validate referer
        const referer = new URL(req.headers.referer || '').hostname;
        if (!ALLOWED_DOMAINS.includes(referer)) {
            return res.status(403).send('Invalid referer');
        }

        // Validate recipient against whitelist
        if (!ALLOWED_RECIPIENTS.includes(recipient?.toLowerCase())) {
            return res.status(400).send('Invalid recipient');
        }

        // Build message
        let message = 'Form submission:\n\n';
        for (const [key, value] of Object.entries(formData)) {
            message += `${key}: ${value}\n`;
        }
        message += `\nIP: ${req.ip}\nUser Agent: ${req.headers['user-agent']}\n`;

        await transporter.sendMail({
            from: '[email protected]',
            to: recipient,
            subject: subject || 'Form Submission',
            text: message
        });

        redirect ? res.redirect(redirect) : res.send('Message sent.');
    } catch (error) {
        console.error(error);
        res.status(500).send('Error sending message');
    }
});

app.listen(3000);

HTML Form Template

<form action="/cgi-bin/FormMail.pl" method="POST">
    <input type="hidden" name="recipient" value="[email protected]">
    <input type="hidden" name="subject" value="Website Contact Form">
    <input type="hidden" name="redirect" value="https://yourdomain.com/thank-you.html">
    <input type="hidden" name="required" value="name,email,message">
    <input type="hidden" name="sort" value="order:name,email,phone,message">

    <div class="mb-3">
        <label for="name">Name *</label>
        <input type="text" id="name" name="name" required>
    </div>

    <div class="mb-3">
        <label for="email">Email *</label>
        <input type="email" id="email" name="email" required>
    </div>

    <div class="mb-3">
        <label for="phone">Phone</label>
        <input type="tel" id="phone" name="phone">
    </div>

    <div class="mb-3">
        <label for="message">Message *</label>
        <textarea id="message" name="message" rows="5" required></textarea>
    </div>

    <button type="submit">Send Message</button>
</form>

Download

View Individual Files

Frequently Asked Questions

  1. Check spam/junk folder — FormMail emails lack SPF/DKIM and are often filtered
  2. Verify $mailprog path — run which sendmail on your server to confirm
  3. Check @recipients whitelist — the recipient address must be listed
  4. Check CGI error log — usually at /var/log/httpd/error_log or /var/log/apache2/error.log

Yes. Separate addresses with commas:

<input type="hidden" name="recipient" value="[email protected],[email protected]">

All addresses must appear in the @recipients whitelist in FormMail.pl.

The redirect field requires a full URL with protocol:

<!-- Correct -->
<input type="hidden" name="redirect" value="https://yourdomain.com/thank-you.html">

<!-- Will not work -->
<input type="hidden" name="redirect" value="thank-you.html">

Also verify that no Perl errors occur before the redirect header is sent (check the CGI error log).

The original FormMail (pre-1.93) is trivially exploitable. Minimum steps:

  1. Set @recipients — only allow specific email addresses
  2. Set @referers — restrict to your domain
  3. Upgrade to NMS FormMail or Tectite FormMail, which fix the header injection vulnerability

The NMS version is a drop-in replacement — same form fields, same configuration approach, but with taint checking and input sanitization.

Two options:

  1. Redirect: Set the redirect hidden field to your own thank-you page URL
  2. Built-in page: Use title, return_link_url, and return_link_title to customize the default output
<input type="hidden" name="title" value="Thank You!">
<input type="hidden" name="return_link_url" value="https://yourdomain.com">
<input type="hidden" name="return_link_title" value="Back to Homepage">

View All FAQs | FormMail FAQ Archive

FormMail Extras

Third-party add-ons and modifications built on top of FormMail:

BFormMail

Adds courtesy reply email, flatfile database logging, and fax-to-email support.

yForm.cgi

Adds optional courtesy reply and database logging of all submissions.

XFormMail

Automated personalized email responses to form submissions.

MailFile Script

Stripped-down FormMail variant for emailing files to users.

SendCGI

FormMail derivative for emailing files to users on request.

Related Scripts

Guestbook Perl

Visitor comment and feedback system with public display.

Mailing List Perl

Email subscription management and newsletter distribution.