Mail sent by an application usually fails quietly. The function returns true, the message is handed to a local process, and nothing tells you it was rejected an hour later. The fix is to send over authenticated SMTP, where a failure is an error you can see.

Do not use mail()

  • It reports whether the message was handed over, not whether it was delivered.
  • It sends from the web server user, so SPF and DKIM usually fail.
  • It has no authentication, no retry and no log you can read.

Use SMTP

use PHPMailer\PHPMailer\PHPMailer;

$m = new PHPMailer(true);
$m->isSMTP();
$m->Host       = 'mail.yourdomain.com';
$m->SMTPAuth   = true;
$m->Username   = 'noreply@yourdomain.com';
$m->Password   = getenv('SMTP_PASS');
$m->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS;
$m->Port       = 465;

$m->setFrom('noreply@yourdomain.com', 'Your Shop');
$m->addReplyTo('support@yourdomain.com');
$m->addAddress($customerEmail);
$m->Subject = 'Your order';
$m->Body    = $text;
$m->send();
Never put the password in the code. Read it from the environment, and keep the environment file out of the document root and out of git.

From must be your own domain

Setting From to the customer's address so replies work is the classic mistake: you are then sending as gmail.com, which fails their SPF and DMARC outright. Send as your domain and set Reply-To to theirs.

Send it in the background

Sending during the request makes the visitor wait for a remote server and turns a mail outage into a broken checkout. Queue it and send from a worker.

And log the result

$m->SMTPDebug = 2;   // development only
// production: record the message ID and the accept/reject per recipient
For anything transactional - receipts, password resets - use a dedicated sending service. Deliverability is their whole business, and it separates your receipts from your newsletters: see transactional mail versus newsletters.