How to send email from PHP reliably
mail() sends, but it rarely authenticates — here is how to send through SMTP instead so the message actually reaches the inbox.
PHP's built-in mail() function will genuinely send a message, but it does so with no authentication and often no meaningful control over the headers a receiving mail server checks. That combination is exactly what modern spam filtering is designed to distrust, so mail sent through a bare mail() call is disproportionately likely to be filtered, delayed, or dropped outright — not because the code is wrong, but because the method itself gives a receiving server nothing to verify the message against.
Send through SMTP with authentication instead
The reliable alternative is to connect to a real mail server over SMTP, authenticating with a genuine mailbox username and password, so the message is sent the same way an email client sends it — as mail from an authenticated account, not an anonymous local process. The most common way to do this from PHP is the PHPMailer library, installed with Composer:
composer require phpmailer/phpmailer
<?php
require 'vendor/autoload.php';
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;
$mail = new PHPMailer(true);
try {
$mail->isSMTP();
$mail->Host = 'mail.yourdomain.com';
$mail->SMTPAuth = true;
$mail->Username = 'sender@yourdomain.com';
$mail->Password = getenv('SMTP_PASSWORD');
$mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
$mail->Port = 587;
$mail->setFrom('sender@yourdomain.com', 'Your Site Name');
$mail->addAddress('recipient@example.com');
$mail->Subject = 'Order confirmation';
$mail->Body = 'Thanks for your order.';
$mail->send();
} catch (Exception $e) {
error_log('Mail failed: ' . $mail->ErrorInfo);
}
Store the mailbox password as an environment variable rather than typing it directly into the file, and keep the script out of version control if it ever needs to contain the value temporarily. See setting environment variables for the options available without root access.
Where the SMTP details come from
The host, port and encryption settings are the same ones you would use setting up the mailbox in an email client — your control panel's email section lists them for any mailbox on your hosting account. Port 587 with STARTTLS is the standard modern combination; port 465 with implicit TLS also works if that is what your mail server documents.
Why the From address matters more than it looks
The address in setFrom() should be a real mailbox on a domain you control and, ideally, the same domain the SMTP connection authenticates against. Sending mail claiming to be from a domain that has nothing to do with the sending server is exactly the pattern SPF and DKIM exist to catch — see SPF, DKIM and DMARC explained. Matching the From address to the authenticated domain, and having that domain's authentication records correctly published, does more for deliverability than any code-level tweak.
Handling failures without losing the message
Wrap the send in a try/catch as shown above, and always log a failure rather than letting it disappear silently. A form that appears to succeed to the visitor while the underlying email quietly failed is one of the more frustrating problems to track down after the fact, precisely because nothing on screen suggested anything had gone wrong. Logging the exact SMTP error — an authentication failure, a connection timeout, a rejected recipient — turns "email is not arriving" from a mystery into a two-minute diagnosis.
If you still need to use mail()
There are cases where the built-in function is unavoidable — a small script with no Composer dependencies allowed, or a genuinely low-stakes internal notification where deliverability barely matters. If you do use it, at minimum set a proper From header rather than leaving PHP to fill one in on its own:
<?php
$headers = "From: Your Site Name <notifications@yourdomain.com>\r\n";
mail('recipient@example.com', 'Notification', 'Message body', $headers);
This does not fix the underlying authentication gap, but it at least gives receiving servers a consistent, real address to evaluate rather than whatever default the server would otherwise construct. For anything a person is actually waiting on — an order confirmation, a password reset, a signup confirmation — the SMTP approach above is worth the extra setup.
Do not let a form become an open relay
A contact form that builds its email headers from unvalidated form fields is a well-known way for a script to be hijacked into sending spam through your own mailbox. Fix the From address and recipient in the code itself, and treat every value a visitor submits as content to place inside the message body, never as something that controls where the message is sent from or to.
Sending in bulk
The pattern above is for individual, transactional messages — order confirmations, password resets, contact form notifications. Sending genuine bulk or marketing email through the same mailbox and the same code path is a different problem with its own volume limits and reputation considerations; a dedicated transactional or marketing email service is usually the better tool once you are sending at that kind of scale, rather than pushing an ordinary mailbox past what it is meant for.
If email built this way still is not arriving after the SMTP send reports success, the fault has usually moved from the code to the domain's own mail configuration — fixing email that is not sending and sending email from a website form both cover the deliverability side from there.
Related reading
The most common reasons an email client refuses to send, checked in the order most likely to find the fault.
SPF, DKIM and DMARC explainedThree DNS records that between them answer one question: is this message really from you? Here is what each proves.
How to make a website contact form deliver reliablyWhy contact form emails go missing more often than direct email, and the setup that reliably fixes it.
How to call an external API from PHPcURL, an authentication header and a decoded JSON response — the three parts of calling any external API from a PHP script.