PHP mail() not working: why it fails in production, and what to use instead

Updated

mail() looks like the simplest way to send email from PHP. It is also the reason a large share of "my contact form emails go missing" questions exist.

What mail() actually does

<?php
$ok = mail('ada@example.com', 'Hello', 'Body text', 'From: hello@yourdomain.com');
var_dump($ok); // true

true means PHP passed the message to the system's mail program, such as sendmail or a local Postfix. It says nothing about whether the message was delivered. There is no authentication, no error detail from the receiving server, and no log you can query from PHP.

Why the email never arrives

| Cause | What happens | |---|---| | No local mail program configured | mail() returns false, or true and drops it | | Hosting provider blocks outbound port 25 | The message sits in a queue and expires | | Sender domain has no SPF record covering the server's IP | Receivers mark it as spam or reject it | | No DKIM signature | Same, with a worse reputation signal | | From is a domain you do not control | Rejected for forgery | | Shared server IP with bad reputation | Everything from that IP is junked | | Bounce goes to a mailbox nobody reads | You never learn it failed |

Diagnose in five minutes

  1. Is a mail program running? On Linux, check which sendmail and your MTA's queue (mailq).
  2. Look at the headers of a message that arrived in spam. Find Authentication-Results and look for spf=fail or dkim=none.
  3. Check your records with the free SPF checker, DKIM checker and DMARC checker.
  4. Test with another recipient provider. If it reaches one and not another, it is reputation or authentication.
  5. Check the server's outbound policy. Many cloud providers block port 25 by default.

What to do instead

Use an email API. The message goes over HTTPS, so there is no port 25 problem. The sender is a verified domain with its own authentication. You get an error immediately if something is wrong, and a log afterwards.

composer require inboxili/inboxili
<?php
require 'vendor/autoload.php';

use Inboxili\Client;
use Inboxili\InboxiliException;

$inboxili = new Client(getenv('INBOXILI_API_KEY'));

try {
    $result = $inboxili->emails->send([
        'to' => 'ada@example.com',
        'from_email' => 'hello@yourdomain.com', // a sender on a verified domain
        'subject' => 'Hello',
        'html_body' => '<p>Body text</p>',
        'text_body' => 'Body text',
    ]);
    echo $result->messageId;
} catch (InboxiliException $e) {
    error_log("{$e->status} {$e->errorCode}: {$e->getMessage()}");
}

Unlike mail(), a failure here raises an exception you can log or show, for example sender_not_verified if the domain is not set up.

Things the API does not do

Be clear about the trade. The Inboxili API sends one recipient per request and has no attachment, CC, BCC or reply-to field, and there is no SMTP endpoint, so libraries that only speak SMTP cannot use it. If your mail() calls attach files or use Cc: headers, change the design: link to a hosted file and send separate emails.

Migrating a contact form

The usual mail() contact form needs three changes: send through the SDK, keep the recipient and sender fixed in code (never taken from the form), and escape every field. The contact form workflow shows a complete version, and the PHP integration page covers installation, errors and troubleshooting.

Frequently asked questions

Why does mail() return true but no email arrives?
mail() returns true when PHP hands the message to the local mail system, not when anyone accepts it. A local MTA may then queue it, or the receiving server may reject or junk it.
Can I use mail() with an email API?
No. mail() uses the server's local mail setup. To use an API you call it over HTTPS, for example with the Inboxili PHP SDK.

Send from PHP with Inboxili

Create a workspace, verify a domain, and make your first API call.

Related