Why the mail() Function Often Fails

The mail() function looks straightforward. You pass in a recipient address, subject, message body, and optional headers. It returns true, so the code moves on. The problem is that true only means the function accepted the message, not that it arrived anywhere.

Behind the scenes, mail() hands the message to the local mail transfer agent running on your server. That agent needs to be configured, running, and able to route email to the correct destination. On a typical shared hosting environment this often works out of the box, but on a VPS or cloud server it frequently fails silently. You get no error, no bounce, and no indication that the message disappeared.

The better approach is to skip the local MTA entirely and connect directly to a remote SMTP server. This gives you explicit control over authentication, encryption, and error reporting. PHPMailer is the most widely used PHP library for this purpose, with a long track record and active maintenance.

Installing PHPMailer

The standard installation method is through Composer, which handles dependencies automatically. If your project does not yet have a composer.json file, initialise one first and then pull in PHPMailer.

composer init --name="yourproject/contact-form"
composer require phpmailer/phpmailer

If Composer is not available in your environment, you can download PHPMailer directly from GitHub and include the library files manually. However, Composer is the recommended approach because it keeps the library updated and manages autoloading cleanly.

use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\SMTP;
use PHPMailer\PHPMailer\Exception;

require 'path/to/PHPMailer/src/Exception.php';
require 'path/to/PHPMailer/src/PHPMailer.php';
require 'path/to/PHPMailer/src/SMTP.php';

The examples below assume a Composer installation with the autoloader in place.

Building the Contact Form HTML

A working contact form needs a clean HTML structure with fields for the sender's name, email address, subject, and message. The form posts to a PHP handler via POST, and each required field should have the appropriate input type and validation attributes.

<form method="POST" action="submit.php">
    <input type="text" name="name" placeholder="Your Name" required>
    <input type="email" name="email" placeholder="Your Email" required>
    <input type="text" name="subject" placeholder="Subject" required>
    <textarea name="message" placeholder="Your Message" required></textarea>
    <button type="submit" name="submit">Send Message</button>
</form>

Keep the form simple. Adding too many optional fields reduces completion rates. Ask only for what you genuinely need to respond to the enquiry.

The PHP Handler with PHPMailer and SMTP

The PHP handler receives the form data, sanitises it, and sends the email through your chosen SMTP server. Using a try/catch block ensures that errors are caught and reported rather than silently ignored.

use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\SMTP;
use PHPMailer\PHPMailer\Exception;

require 'vendor/autoload.php';

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $name = trim($_POST['name']);
    $email = trim($_POST['email']);
    $subject = trim($_POST['subject']);
    $message = trim($_POST['message']);

    $mail = new PHPMailer(true);

    try {
        $mail->isSMTP();
        $mail->Host = 'smtp.example.com';
        $mail->SMTPAuth = true;
        $mail->Username = '[email protected]';
        $mail->Password = 'your_smtp_password';
        $mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
        $mail->Port = 587;

        $mail->setFrom('[email protected]', 'Your Website');
        $mail->addAddress('[email protected]', 'Your Name');
        $mail->addReplyTo($email, $name);

        $mail->isHTML(false);
        $mail->Subject = "Contact Form: $subject";
        $mail->Body = "Name: $name\nEmail: $email\n\n$message";

        $mail->send();
        echo 'Message sent successfully';
    } catch (Exception $e) {
        echo "Message could not be sent. Error: {$mail->ErrorInfo}";
    }
}

This basic setup gives you a working SMTP contact form with proper error handling. Without the try/catch block, failures are silent and difficult to debug.

Understanding SMTP Configuration Options

SMTP settings trip up many developers. Each parameter affects how the connection is established and how your message is treated by the receiving mail server.

Host is the address of your SMTP server. For Gmail use smtp.gmail.com. For transactional email services such as Mailgun, SendGrid, or Postmark, use the server address shown in your account dashboard. Your hosting provider may also offer an SMTP relay that you can use instead of configuring Postfix yourself.

Port determines the connection method. Port 587 uses STARTTLS, upgrading a plain connection to encrypted mid-session. Port 465 uses implicit TLS throughout and was more common historically but has inconsistent support. Port 587 with STARTTLS is the recommended choice for most modern setups.

$mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
$mail->Port = 587;

Authentication requires a username and password. For Gmail this is your Google account credentials, though you may need an app password if you have two-factor authentication enabled. For transactional email services this is usually an API key generated in your account dashboard, not your main account password. Always use API keys where the provider supports them because they can be revoked independently of your main credentials.

Note: PHPMailer has its own built-in SMTP debugging mode. Enabling it helps diagnose connection problems during development by printing the full SMTP conversation to your output.

$mail->SMTPDebug = SMTP::DEBUG_SERVER;

Sending HTML Emails

Plain text works for simple messages, but HTML email lets you format the message, include links, and present a professional appearance. Set isHTML(true) and compose the body as HTML markup.

$mail->isHTML(true);
$mail->Subject = "Contact Form: $subject";
$mail->Body = '
    <h2>New Contact Form Submission</h2>
    <p><strong>Name:</strong> ' . htmlspecialchars($name) . '</p>
    <p><strong>Email:</strong> ' . htmlspecialchars($email) . '</p>
    <hr>
    <p>' . nl2br(htmlspecialchars($message)) . '</p>
';
$mail->AltBody = "Name: $name\nEmail: $email\n\n$message";

The AltBody property provides a plain text fallback for email clients that do not render HTML. Always include it. Some spam filters score negatively when a message has no plain text alternative.

Notice the use of htmlspecialchars() when inserting user-provided data into HTML content. This prevents cross-site scripting if someone tries to submit malicious HTML or JavaScript through your form.

Preventing Spam with Correct Headers

Email that never arrives or lands in the spam folder often has missing or incorrect headers. PHPMailer handles most header configuration automatically when you use setFrom() and addReplyTo(), but for a production contact form you should also set explicit Sender and Message-ID headers.

$mail->Sender = '[email protected]';
$mail->MessageID = '<contact-form-' . time() . '@yourdomain.com>';

The Message-ID header helps receiving mail servers verify that the message is genuine. Including a domain matching your sending address improves deliverability scores.

Beyond headers, your sending domain's SPF, DKIM, and DMARC records must be configured correctly. These authentication records tell receiving servers that your server is authorised to send email for your domain. Setting these up properly is covered in more detail in the guide on SPF, DKIM and DMARC explained.

Server-Side Form Validation and Security

Never trust user input, even if the HTML form has validation attributes. Server-side checks are essential because attackers can bypass client-side validation by sending requests directly to your PHP handler.

if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
    die('Invalid email address');
}

if (strlen($message) < 10 || strlen($message) > 5000) {
    die('Message must be between 10 and 5000 characters');
}

// Reject submissions where the subject contains URLs
if (preg_match('/https?:\/\//', $subject)) {
    die('Invalid subject');
}

Beyond basic validation, implement rate limiting to prevent abuse. A simple approach stores the submitting IP address and a timestamp in a flat file or database, then rejects requests that exceed a threshold such as five submissions per minute from the same IP.

Storing Credentials Securely

Never hardcode SMTP credentials directly in your PHP files. If the code is committed to version control, credentials leak. If the file is accessible via the web server, they can be read directly from the browser.

Store credentials in environment variables or a configuration file placed outside the web root directory. PHPMailer can read these directly through getenv().

$mail->Username = getenv('SMTP_USERNAME');
$mail->Password = getenv('SMTP_PASSWORD');

Set these variables in your server configuration, your virtual host settings, or a .env file loaded at application startup.

SMTP_HOST=smtp.example.com
SMTP_PORT=587
[email protected]
SMTP_PASSWORD=your_api_key

Rotating credentials becomes straightforward because you update the environment without touching the code itself. For more on managing sensitive configuration across your server environment, the guide on Postfix configuration for SMTP authentication covers related concepts in depth.

Testing Before You Deploy

Before sending to real addresses, test the configuration against a service that intercepts emails during development. Mailtrap, Mailhog, and similar tools capture outgoing messages and display them in a web interface without delivering anywhere.

// Mailtrap test configuration
$mail->Host = 'smtp.mailtrap.io';
$mail->SMTPAuth = true;
$mail->Username = 'your_mailtrap_user_id';
$mail->Password = 'your_mailtrap_api_key';
$mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
$mail->Port = 587;

Use this configuration to verify the form works, emails render correctly, and errors are caught properly. Once everything behaves as expected, swap in your production SMTP credentials and test once more from your live server.

Adding File Attachments

Some contact forms need to accept file uploads, such as a CV for a job enquiry or a document for a quote request. PHPMailer handles attachments through the addAttachment() method.

if (isset($_FILES['attachment']) && $_FILES['attachment']['error'] === UPLOAD_ERR_OK) {
    $uploadfile = tempnam(sys_get_temp_dir(), 'upload_');
    move_uploaded_file($_FILES['attachment']['tmp_name'], $uploadfile);
    $mail->addAttachment($uploadfile, $_FILES['attachment']['name']);
}

Always validate uploaded files on the server side before attaching them. Check the file extension, MIME type, and size.

$allowed_extensions = ['pdf', 'doc', 'docx', 'jpg', 'png'];
$extension = strtolower(pathinfo($_FILES['attachment']['name'], PATHINFO_EXTENSION));

if (!in_array($extension, $allowed_extensions)) {
    die('File type not allowed');
}

if ($_FILES['attachment']['size'] > 5 * 1024 * 1024) {
    die('File too large');
}

Reject anything over a reasonable size limit. Five megabytes is sufficient for most business forms. Larger files risk filling up mailbox storage and slow down the submission process.

Redirecting After Submission

After processing the form, redirect the user to a confirmation page rather than leaving them on the form or outputting raw text. This prevents accidental duplicate submissions if the user refreshes the browser.

if ($mail->send()) {
    header('Location: /contact-success.php');
    exit;
} else {
    header('Location: /contact-error.php');
    exit;
}

Always call exit immediately after a redirect header. Without it, the script continues executing even though the user has moved to a different page, which can cause unexpected behaviour or security issues.

Common Errors and How to Fix Them

SMTP connect failed usually indicates a wrong host address, port, or encryption setting. Verify these match your provider's documentation exactly. Check that your server can reach the SMTP host on the specified port using a basic connectivity test.

telnet smtp.example.com 587

If the connection times out, your server may be blocking outbound port 587. Many cloud providers block port 25 by default. Using port 465 or contacting your provider to unblock the port are the main options.

Authentication failed means the username or password is incorrect. For services that use API keys, confirm you are using the SMTP credential and not your main account password. Some providers require creating a specific SMTP-style credential rather than using general API keys.

Email goes to spam can result from several issues. Your sending domain's SPF, DKIM, and DMARC records must be configured correctly. The message content should not contain obvious spam signals such as excessive links, all-caps text, or suspicious phrases. Sending from a new IP address or domain that has not built up reputation also affects delivery.

When to Use a Transactional Email Service

PHPMailer can send directly through your server's local MTA, but for anything beyond low-volume personal use, a transactional email service is worth the cost. Services like Mailgun, SendGrid, Postmark, and Amazon SES provide dedicated IP addresses with established sender reputations, delivery analytics, and infrastructure that major email providers trust.

If your contact form sends more than a few dozen emails per day, or if reliable delivery matters for your business operations, moving to a transactional service removes a significant operational burden. The PHPMailer configuration is the same, you simply change the host, port, and credentials.

Getting the Setup Right

A well-configured PHP contact form using PHPMailer and SMTP gives you reliable email delivery, clear error reporting, and control over how your messages are sent. The key points are using the right SMTP settings for your provider, validating and sanitising all user input, storing credentials outside your code, and testing thoroughly before going live.

If you are working through this setup and run into persistent delivery issues or are unsure whether your domain's authentication records are configured correctly, preparing a note with your SMTP host, port, credentials source, and the exact error message will help identify the problem quickly.