FormMail vs PHP mail(): How Web Forms Evolved
In 1995, if you wanted your website's contact form to send an email, you needed FormMail.pl — a Perl CGI script. By 2000, PHP's built-in mail() function did the same thing in one line. This is the story of how web form handling went from a downloaded script to a language feature to a cloud service.
In Brief
FormMail (1995) piped form data to sendmail via a Perl CGI script. PHP mail() (1998) did the same thing in one function call. The pattern — user need to language feature to cloud service — repeats across all web infrastructure.
The Problem: HTML Forms Couldn't Send Email
HTML has always been a markup language, not a programming language. The <form> element, introduced in HTML 2.0 in 1995, could collect user input — text fields, radio buttons, checkboxes, textareas — and submit it somewhere. But "somewhere" was the critical word. The form itself had no idea what to do with the data. It simply packaged the field names and values into an HTTP request and sent them to whatever URL was specified in the action attribute.
If the action pointed to an HTML file, nothing useful happened. The web server would return the HTML page and ignore the submitted data entirely. For the form to actually do something — store the data in a file, insert it into a database, or send it as an email — the action had to point to a program running on the server. In 1995, that meant a CGI script: an executable file sitting in the server's cgi-bin directory, typically written in Perl, that the web server would invoke in response to an HTTP request.
There were no alternatives. JavaScript existed in its infancy (Netscape shipped it in December 1995), but it ran entirely in the browser and had no ability to send email or communicate with a server. PHP did not exist yet — Rasmus Lerdorf's "Personal Home Page Tools" were still a set of C binaries for tracking visitors. ASP, JSP, ColdFusion — none of them had reached the market. The only path from an HTML form to an email was a server-side script, and the most accessible server-side scripts were written in Perl.
This was the problem that FormMail.pl solved. You downloaded it, uploaded it to /cgi-bin/, pointed your form at it, and it turned form submissions into emails. It was the bridge between an HTML page and the Unix mail system, and for several years it was the only bridge most webmasters had access to.
FormMail.pl: The CGI Solution (1995–2002)
Matt Wright wrote FormMail in 1995 and published it through Matt's Script Archive, hosted here on worldwidemart.com. The script was free, the installation was straightforward, and it worked on any Unix web server with Perl and sendmail installed — which, in the mid-1990s, was nearly every web server in existence.
The workflow was simple. A webmaster created an HTML form and set its action attribute to point at the FormMail script:
<form action="/cgi-bin/formmail.pl" method="POST">
<input type="hidden" name="recipient" value="[email protected]">
<input type="hidden" name="subject" value="Contact Form">
<input type="hidden" name="redirect" value="/thanks.html">
Name: <input type="text" name="name">
Email: <input type="text" name="email">
Message: <textarea name="message"></textarea>
<input type="submit" value="Send">
</form>
When a visitor clicked "Send," the browser sent a POST request to /cgi-bin/formmail.pl. The web server launched the Perl interpreter, executed the script, and FormMail did the rest: it parsed the form fields from the POST body, constructed an email message with the visitor's input as the body, and piped it to the system's sendmail binary. The core of the script's email-sending logic was roughly ten lines of Perl:
# Core logic from FormMail.pl (simplified)
$mailprog = '/usr/lib/sendmail';
open(MAIL, "|$mailprog -t") || die "Can't open $mailprog!\n";
print MAIL "To: $FORM{'recipient'}\n";
print MAIL "From: $FORM{'email'}\n";
print MAIL "Subject: $FORM{'subject'}\n\n";
foreach $key (keys %FORM) {
print MAIL "$key: $FORM{$key}\n";
}
close(MAIL);
The open(MAIL, "|$mailprog -t") call opened a pipe to sendmail. Everything printed to that pipe was treated as an email message. The -t flag told sendmail to read the recipient address from the email headers rather than the command line. After all the fields were printed, close(MAIL) triggered sendmail to deliver the message. The script then redirected the visitor's browser to a "thank you" page specified in the hidden redirect field.
Configuration was done entirely through hidden HTML form fields: recipient set the destination email address, subject set the email subject line, redirect set the post-submission redirect URL, and required listed fields that had to be filled in before submission. No configuration files. No admin panel. No database. The HTML form was the configuration.
This design was elegant in its simplicity but catastrophic in its security implications. The recipient field came directly from the HTML form, which meant it came from the user's browser. A "hidden" input field is only hidden from the browser's rendering — anyone could edit the HTML, craft a raw HTTP request, or use a form submission tool to change the recipient to any email address they wanted. By 2001, this flaw had turned FormMail installations worldwide into open spam relays, leading to CVE-2001-0357 and one of the early web's most significant security incidents.
Despite the security issues, FormMail's impact was enormous. It was downloaded over two million times. It was bundled by hosting providers, recommended in web development tutorials, and referenced in books. For the majority of webmasters between 1995 and 2000, FormMail was the way contact forms worked.
PHP mail(): The Language Feature (1998–2010)
PHP 3 was released in June 1998. Among its many built-in functions was mail() — a single function that sent email. The signature was almost absurdly simple:
mail($to, $subject, $message, $additional_headers);
Under the hood, mail() did exactly the same thing FormMail did: it piped the message to the system's sendmail binary (or whatever MTA was configured in php.ini via the sendmail_path directive). The function was a thin wrapper around the same Unix mail plumbing that FormMail used. But the developer experience was fundamentally different.
With FormMail, handling a contact form required: downloading a Perl script, uploading it to cgi-bin, setting chmod 755, configuring hidden fields in the HTML, and hoping the server's Perl interpreter and sendmail were both working correctly. With PHP, the entire form-processing logic could live in a single .php file alongside the HTML:
<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$to = '[email protected]'; // Hardcoded, not from form
$subject = 'Contact Form';
$name = htmlspecialchars($_POST['name']);
$email = filter_var($_POST['email'], FILTER_VALIDATE_EMAIL);
$message = htmlspecialchars($_POST['message']);
$headers = "From: $email\r\n";
$body = "Name: $name\nEmail: $email\n\n$message";
mail($to, $subject, $body, $headers);
header('Location: /thanks.html');
exit;
}
?>
No separate script to download. No cgi-bin directory. No chmod. No hidden form fields controlling the recipient address. The destination email was hardcoded in the PHP file on the server, which meant a visitor could not change it by editing the HTML. This alone eliminated FormMail's most critical vulnerability — though PHP introduced its own problems.
The biggest issue was header injection. If user input was passed directly into the $additional_headers parameter without sanitization, an attacker could inject newline characters followed by additional headers — CC:, BCC:, or even a second To: — effectively turning the script into a spam relay. The vulnerability was conceptually identical to FormMail's recipient manipulation: user-controlled data was being inserted into email headers without validation.
// VULNERABLE: header injection
$from = $_POST['email']; // Attacker sends: "[email protected]\r\nBCC: [email protected]"
mail($to, $subject, $message, "From: $from");
// Result: email sent to both $to AND [email protected]
Beyond security, mail() had practical limitations. It couldn't authenticate with an SMTP server — it only worked by piping to the local MTA. It couldn't send HTML-formatted emails without manually constructing MIME headers. It couldn't handle attachments. It provided no delivery confirmation, no bounce handling, and no logging. On shared hosting where sendmail was disabled or rate-limited, mail() simply failed silently.
These limitations drove the creation of PHP email libraries. PHPMailer appeared in 2001, offering SMTP authentication, HTML emails, attachments, and proper MIME handling. SwiftMailer followed in 2005 with a more object-oriented API. Symfony Mailer (2019) eventually replaced SwiftMailer as the modern standard. Each library was, in essence, a more complete and secure version of what mail() tried to do — just as mail() itself was a more convenient version of what FormMail tried to do.
The progression was clear: FormMail piped to sendmail from an external script. PHP mail() piped to sendmail from within the language. PHPMailer spoke SMTP directly, bypassing sendmail entirely. Each step removed a layer of indirection and added a layer of security. But the fundamental operation — take form data, construct an email, deliver it — remained the same across all three.
Side-by-Side Comparison
The following table compares the four major approaches to web form email handling, from FormMail in 1995 to modern form services:
| Feature | FormMail.pl (1995) | PHP mail() (1998) | PHPMailer (2001) | Formspree (2014) |
|---|---|---|---|---|
| Language | Perl | PHP | PHP | None (API) |
| Setup | Download + upload to cgi-bin + chmod 755 + configure hidden fields | One function call in a .php file | Composer install + SMTP config | Change form action URL |
| Mail transport | Pipe to sendmail | Pipe to sendmail | SMTP with authentication | Cloud infrastructure |
| HTML email | No | Manual MIME headers | Full support | Full support |
| Attachments | No | No | Yes | Yes |
| Spam protection | @referers check (trivially bypassed) | None built-in | DKIM signing, SPF alignment | reCAPTCHA, honeypot, rate limiting |
| Primary vulnerability | Open relay via user-controlled recipient | Header injection via unsanitized input | Misconfiguration (credentials in code) | Account abuse (mitigated by service) |
| Server requirement | Perl + sendmail + CGI support | PHP + sendmail | PHP + outbound SMTP access | None (static HTML only) |
| Cost | Free | Free | Free (open source) | Free tier / $8+/month |
| Peak usage period | 1995–2002 | 1999–2010 | 2005–present | 2016–present |
The table reveals a consistent pattern: each generation solved the previous generation's biggest problem. FormMail solved the problem of "how do I make a form send email?" PHP mail() solved "why do I need a separate script for this?" PHPMailer solved "why can't I use a real SMTP server?" And Formspree solved "why do I need a server at all?"
The Modern Era: Forms as a Service (2014+)
In 2014, a developer named Rohit Datta launched Formspree. The service worked on a principle that would have been immediately familiar to anyone who had used FormMail two decades earlier: point your HTML form's action attribute at a URL, and the service handles the rest. The HTML looked almost identical to a FormMail-era form:
<form action="https://formspree.io/f/xyzabcde" method="POST">
<input type="text" name="name" placeholder="Name">
<input type="email" name="email" placeholder="Email">
<textarea name="message"></textarea>
<button type="submit">Send</button>
</form>
No server-side code. No sendmail. No PHP. No hosting requirements beyond the ability to serve a static HTML file. The form submitted data to Formspree's cloud infrastructure, which validated the submission, checked for spam, formatted it as an email, and delivered it via authenticated SMTP with proper DKIM signatures and SPF records. The entire email delivery stack — the stack that FormMail accessed through a pipe to sendmail, that PHP accessed through mail(), that PHPMailer accessed through SMTP — was now someone else's problem.
Formspree was not alone. Netlify Forms (2016) took the concept further by detecting forms automatically in deployed static sites — no action URL change needed, no API key, no configuration. The Netlify build process parsed the HTML, found <form> elements with a netlify attribute, and wired them to the submission processing pipeline. Web3Forms offered a similar service with an emphasis on privacy (no account required, just an access key). Basin, Getform, FormKeep, and Fieldgoal each found their niche in the market.
WordPress had its own parallel evolution. Contact Form 7, released in 2007, became the dominant form plugin with over five million active installations. It abstracted the form-to-email pipeline into a WordPress shortcode, handling validation, spam filtering (via Akismet integration), and email delivery through the WordPress wp_mail() function (which itself could be configured to use SMTP via plugins like WP Mail SMTP). Gravity Forms (2009) and WPForms (2016) added drag-and-drop builders, payment integrations, and conditional logic — transforming the form from a simple email trigger into a full data-processing workflow.
The conceptual arc is unmistakable. FormMail outsourced form processing to an external script on your own server. PHP mail() brought form processing into the page itself. PHPMailer replaced the local MTA with a remote SMTP server. And form-as-a-service platforms outsourced the entire operation to third-party cloud infrastructure. At every stage, the pattern was the same: move complexity away from the individual webmaster and toward a shared, managed system. The recipient of the email still gets a message with the visitor's name, email, and comments — exactly what FormMail delivered in 1995. Only the plumbing changed.
What We Can Learn
The evolution from FormMail to Formspree spans nearly thirty years, but it follows a single trajectory: complexity moves from user to language to cloud. Each transition is worth examining, because the same pattern repeats across almost every category of web infrastructure.
FormMail proved that "form to email" was a universal need. Matt Wright didn't invent the concept of processing form submissions — the CGI specification already described how to do it. What FormMail did was package the solution in a way that anyone could use. Two million downloads demonstrated that the demand was real and that the existing tools (raw Perl CGI programming) were too complex for the average webmaster. Every time a technology gets downloaded two million times, it tells you something about the gap between what people need and what the platform provides natively.
PHP mail() proved that language-level support beats an external script. When PHP embedded email sending as a built-in function, it eliminated the entire download-upload-chmod-configure workflow. The same sendmail pipe that required 50 lines of Perl and a separate file now required one line of PHP inside the page that displayed the form. The lesson: if every developer needs to do something, it should be part of the standard library. PHP applied this aggressively — mail(), mysql_connect(), file_get_contents(), json_encode() — building into the language everything that Perl handled through external modules and CGI scripts.
PHPMailer proved that abstracting the transport layer is essential. Both FormMail and PHP mail() relied on the local server having a working sendmail installation. PHPMailer spoke SMTP directly, which meant the PHP application could authenticate with Gmail, SendGrid, Mailgun, or any other SMTP server regardless of the local server's mail configuration. This decoupled the application from the infrastructure — a principle that would later drive the entire container and cloud-native movement.
Formspree proved that self-hosting is unnecessary for commoditized operations. Sending an email in response to a form submission is not a differentiating feature. No business gains competitive advantage from running their own sendmail. Form-as-a-service platforms recognized that form processing was infrastructure, not application logic, and offered it as a managed service. The same reasoning drove the rise of Stripe for payments, Auth0 for authentication, Twilio for SMS, and Algolia for search — operations that every application needs but no application should have to build from scratch.
The trajectory is consistent and predictable: Script → Library → Language feature → Platform service → Cloud API. FormMail was the script. PHPMailer was the library. PHP mail() was the language feature. Contact Form 7 was the platform service. Formspree was the cloud API. The next step in the pattern is invisible infrastructure — email delivery so deeply integrated into hosting platforms that developers don't configure it at all. Netlify Forms is already there. The gap between FormMail's open(MAIL, "|$mailprog -t") and Netlify's zero-configuration form detection is thirty years of this pattern playing out.
Frequently Asked Questions
FormMail.pl is a Perl CGI script written by Matt Wright in 1995 and distributed through Matt's Script Archive on worldwidemart.com. It processed HTML form submissions by parsing the POST data, formatting it as an email message, and piping it to the Unix sendmail program. It was downloaded over two million times and was the most widely used contact form solution on the early web. Configuration was done through hidden HTML form fields — including the recipient email address, subject line, and redirect URL.
FormMail.pl is an external Perl script that runs as a separate CGI process — the HTML form posts to /cgi-bin/formmail.pl, which parses the input and pipes it to sendmail. PHP mail() is a built-in language function that does the same sendmail pipe in a single line of code: mail($to, $subject, $message). The key difference is deployment: FormMail required downloading a script, uploading it to the cgi-bin directory, setting chmod 755, and configuring hidden form fields. PHP mail() required only adding a few lines to any .php file. Both ultimately called the same underlying sendmail binary, but PHP eliminated the external script dependency and — critically — allowed the recipient address to be hardcoded on the server rather than controlled by the user's browser.
The modern replacement for FormMail is a form-handling API service such as Formspree, Netlify Forms, Web3Forms, Basin, or Getform. These services work on the same principle as FormMail — you point your HTML form's action attribute at an external URL — but the processing happens on cloud infrastructure with built-in spam protection, DKIM/SPF email authentication, and no server-side code to maintain. For self-hosted solutions, PHPMailer with SMTP authentication or a serverless function (AWS Lambda, Cloudflare Workers) handling form submissions are the standard approaches.
PHP mail() is not inherently secure. It is vulnerable to header injection attacks if user input is passed directly into the $additional_headers parameter without sanitization — an attacker can inject CC:, BCC:, or additional To: headers to send spam through your server. This is conceptually the same class of vulnerability that affected FormMail. Additionally, mail() lacks SMTP authentication, DKIM signing, and SPF alignment, which means emails sent through it are frequently flagged as spam by modern mail providers like Gmail and Outlook. For production use, PHPMailer or Symfony Mailer with authenticated SMTP is strongly recommended over the raw mail() function.
Related Pages
FormMail Script Archive
Original documentation, source code, and configuration guide for FormMail.pl v1.6.
The FormMail Security Crisis
How FormMail became the internet's biggest spam relay — CVE-2001-0357, the NMS Project, and lessons for modern developers.
FormMail Troubleshooting Guide
Common installation errors, permission issues, and sendmail configuration for FormMail.pl.
What Replaced CGI Scripts?
The complete evolution from Perl CGI to mod_perl, PHP, Node.js, and serverless functions.