Why Contact Form Emails Fail Without Warning
A contact form that looks correct but never delivers messages is one of the most common issues I see when reviewing PHP websites. The form submits without errors. The browser shows a success message. But the email never arrives, and there is no indication why.
This guide walks through building a reliable PHP contact form using PHPMailer and SMTP authentication. It covers installation, SMTP configuration, input validation, security practices, and testing. By the end you will have a working form that sends emails predictably and handles errors gracefully.
Why the mail() Function Often Fails
The mail() function is built into PHP and appears simple to use. You provide a recipient address, subject line, message body, and optional headers. The function returns true, so the script assumes everything worked. The reality is different.
When you call mail(), PHP hands the message to a local mail transfer agent running on your server. That agent must be configured, running, and capable of routing email to the destination mail server. On shared hosting this often works out of the box. On a VPS or cloud server it frequently fails silently. You receive no bounce notification, no error log entry, and no way to know the message disappeared.
The better approach is to bypass 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 active maintenance and a large community.
Installing PHPMailer
The recommended installation method is Composer, which handles dependencies and autoloading automatically. If your project does not yet have a composer.json file, initialise one first and then add PHPMailer.
composer init --name="yourproject/contact-form"
composer require phpmailer/phpmailer
Composer fetches PHPMailer and its dependencies, sets up the autoloader, and keeps everything organised. If Composer is not available in your environment, download PHPMailer directly from GitHub and include the library files manually. However, Composer is the cleaner approach and makes updating the library straightforward.
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 code 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 use the appropriate input type.
<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. Every additional field reduces completion rates. Ask only for what you genuinely need to respond to the enquiry. Name, email, and message are usually sufficient for most business websites.
The PHP Handler with PHPMailer and SMTP
The PHP handler receives the submitted data, sanitises it, and sends the email through your chosen SMTP server. Using a try/catch block ensures 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 = 'your@email.com';
$mail->Password = 'your_smtp_password';
$mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
$mail->Port = 587;
$mail->setFrom('no-reply@yourdomain.com', 'Your Website');
$mail->addAddress('you@yourdomain.com', '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 setup gives you a working SMTP contact form with error handling. Without the try/catch block, failures are silent and difficult to debug. PHPMailer throws exceptions on most errors, so catching them is essential for knowing when something goes wrong.
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 your own mail server.
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.
Tip: PHPMailer has a built-in SMTP debugging mode that prints the full SMTP conversation to your output. Enabling it during development helps diagnose connection problems quickly.
$mail->SMTPDebug = SMTP::DEBUG_SERVER;
Sending HTML Emails
Plain text works for simple messages, but HTML email lets you format the content, 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 Honeypot Fields
Honeypot fields are form inputs hidden from real users but visible to automated bots. When a bot fills them in, the submission is rejected. This approach adds no friction for legitimate visitors and stops a significant proportion of automated spam.
<input type="text" name="website_url" style="display:none" tabindex="-1" autocomplete="off">
At the top of your PHP handler, reject any submission where this hidden field has a value:
if (!empty($_POST['website_url'])) {
// Bot detected, silently reject
header('Location: /');
exit;
}
Beyond honeypots, combining server-side validation, rate limiting, and header configuration significantly reduces spam without relying on CAPTCHAs. The guide on how to stop contact form spam without CAPTCHAs covers these techniques in more detail.
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 = 'no-reply@yourdomain.com';
$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 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
SMTP_USERNAME=your@email.com
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.
Logging Errors for Production
Displaying raw error messages to users is not appropriate in production. Instead, log errors server-side so you can investigate issues without exposing sensitive configuration details to visitors.
try {
$mail->send();
} catch (Exception $e) {
error_log("Mailer Error: {$mail->ErrorInfo} for {$email}");
header('Location: /contact-error.php');
exit;
}
Check your server's error log location or configure a custom log file. Keeping a record of failed deliveries helps identify patterns, such as a particular SMTP credential expiring or a receiving mail server blocking your messages.
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.