El correo que envía una aplicación suele fallar en silencio. La función devuelve true, el mensaje se entrega a un proceso local, y nada le dice que lo rechazaron una hora después. El remedio es enviar por SMTP autenticado, donde un fallo es un error que usted ve.

No use mail()

  • Informa de si el mensaje se entregó al siguiente eslabón, no de si llegó a destino.
  • Envía con la identidad del usuario del servidor web, así que SPF y DKIM suelen fallar.
  • No tiene autenticación, ni reintento, ni un registro que pueda leer.

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();
Nunca ponga la contraseña en el código. Léala del entorno, y mantenga el archivo de entorno fuera de la raíz web y fuera de git.

El From tiene que ser su propio dominio

Poner en From la dirección del cliente para que funcionen las respuestas es el error clásico: entonces está enviando como gmail.com, y eso incumple su SPF y su DMARC de plano. Envíe con su dominio y ponga Reply-To con el de él.

Envíelo en segundo plano

Enviar durante la petición hace que el visitante espere a un servidor remoto y convierte una caída del correo en un pago roto. Póngalo en cola y envíelo desde un trabajador.

Y registre el resultado

$m->SMTPDebug = 2;   // development only
// production: record the message ID and the accept/reject per recipient
Para todo lo transaccional -recibos, restablecimientos de contraseña- use un servicio de envío dedicado. La entregabilidad es todo su negocio, y separa sus recibos de sus boletines: vea correo transaccional frente a boletines.