PHPSending Email

Sending Email

Sooner or later almost every web application needs to send email — a signup confirmation, a password reset link, an order receipt, an alert to an administrator. PHP ships with a built-in mail() function that looks like the obvious tool for the job, and it does technically send email. In practice, though, mail() is rarely the right choice for anything beyond a quick local test, and understanding why is more useful than memorizing its parameter list.

The built-in mail() function

mail() takes a recipient, a subject, a message body, and an optional set of extra headers, and hands the whole thing off to whatever mail transfer agent is configured on the server — typically sendmail on Linux. PHP itself doesn't speak the SMTP protocol here; it just delegates to that external program and trusts it to do the actual delivery.

A minimal mail() call

PHP
<?php
$to = 'customer@example.com';
$subject = 'Your order has shipped';
$message = "Hi there,\n\nYour order #4821 is on its way.";
$headers = 'From: orders@myshop.example';

$sent = mail($to, $subject, $message, $headers);

if (!$sent) {
    error_log('Failed to send order confirmation email');
}

Notice that mail() only returns a boolean saying whether the message was handed off successfully — not whether it was actually delivered, and certainly not whether it landed in the inbox rather than the spam folder. That distinction is exactly where mail() starts to fall apart for real applications.

Why mail() struggles in production

Modern email providers — Gmail, Outlook, and pretty much every corporate mail server — are aggressive about filtering spam, and they use signals that mail() simply doesn't provide. It sends directly through the local sendmail binary with no authentication and often no proper SPF, DKIM, or DMARC alignment with the sending domain, which are the mechanisms receiving servers use to verify that an email claiming to be from @myshop.example is actually authorized to be sent on that domain's behalf. A message sent this way frequently gets silently dropped or buried in spam, with no error and no way for the application to know. On top of that, many hosting providers block outbound port 25 entirely, so mail() may not even successfully hand the message to anything.

A true return value does not mean the email arrived
`mail()` returning `true` only confirms that the local mail transport agent accepted the message for processing — it says nothing about whether the receiving mail server accepted it, and nothing about whether it reached an inbox instead of a spam folder or a silent bounce. Relying on that return value as proof an email was "sent" to a customer is a common source of production bugs that are hard to diagnose, because everything looks successful from PHP's point of view.
Why production apps use SMTP libraries instead

Because of these limitations, virtually every real-world PHP application sends email by talking directly to an SMTP server — either the SMTP endpoint of a transactional email provider (like SendGrid, Postmark, Amazon SES, or Mailgun) or a properly configured outbound mail server — using a dedicated library rather than mail(). PHPMailer and Symfony Mailer are the two most widely used options in the PHP ecosystem. Both authenticate against the SMTP server with real credentials, support TLS encryption in transit, and give far better error information when something goes wrong, because they're actually holding an SMTP conversation instead of blindly delegating to a local binary.

Conceptual PHPMailer-style usage

PHP
<?php
require __DIR__ . '/vendor/autoload.php';

use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;

$mail = new PHPMailer(true);

try {
    $mail->isSMTP();
    $mail->Host = getenv('SMTP_HOST');
    $mail->SMTPAuth = true;
    $mail->Username = getenv('SMTP_USERNAME');
    $mail->Password = getenv('SMTP_PASSWORD');
    $mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
    $mail->Port = 587;

    $mail->setFrom('orders@myshop.example', 'My Shop');
    $mail->addAddress('customer@example.com');

    $mail->isHTML(true);
    $mail->Subject = 'Your order has shipped';
    $mail->Body = '<p>Your order <strong>#4821</strong> is on its way.</p>';
    $mail->AltBody = 'Your order #4821 is on its way.';

    $mail->send();
} catch (Exception $e) {
    error_log("Mailer error: {$mail->ErrorInfo}");
}

This is far more code than a single mail() call, but every extra line buys something real: an authenticated connection to a trusted SMTP relay, an encrypted transport, both an HTML and a plain-text version of the message for clients that don't render HTML, and a specific exception with a real error message when delivery fails instead of a bare false.

Never hardcode SMTP credentials

The example above reads SMTP_HOST, SMTP_USERNAME, and SMTP_PASSWORD from environment variables via getenv(), rather than writing them directly into the PHP file. This matters for two reasons: committing real credentials into version control leaks them to anyone with repository access (including, eventually, anyone who finds an old commit), and hardcoded values make it impossible to use different credentials for local development, staging, and production without editing code. A .env file — read by a library, or by the hosting platform's own environment variable configuration — combined with a .env entry in .gitignore keeps secrets out of the codebase entirely while still making them available to the running application.

.env — never committed to version control

Bash
SMTP_HOST=smtp.sendgrid.net
SMTP_USERNAME=apikey
SMTP_PASSWORD=SG.xxxxxxxxxxxxxxxxxxxxxxxxxxxx
Symfony Mailer is a first-class alternative
Symfony Mailer is built and maintained alongside the rest of the Symfony components, but — like most Symfony components — it's fully usable outside a Symfony application. It configures an SMTP transport from a single DSN-style connection string rather than a set of individual properties, which some teams find easier to manage across environments, but it solves exactly the same underlying problem as PHPMailer: a real, authenticated SMTP conversation instead of a bare call to `mail()`.
  • PHP's built-in mail() hands a message to the local mail transport agent (usually sendmail) with no authentication and no delivery confirmation.

  • mail() returning true only means the message was accepted locally — not that it was delivered or avoided the spam folder.

  • Modern receiving mail servers rely on authentication signals (like SPF/DKIM/DMARC alignment) that mail() typically cannot provide.

  • PHPMailer and Symfony Mailer send email over an authenticated SMTP connection and report real delivery errors instead of a bare boolean.

  • SMTP credentials belong in environment variables (e.g. a .env file excluded from version control), never hardcoded directly in source files.

Tip
If an application only ever needs to send a handful of test emails during local development, a tool like Mailhog or Mailtrap — which intercepts outgoing SMTP traffic locally and shows it in a web UI instead of actually delivering it — is safer and faster to iterate with than pointing a real SMTP provider at your development environment.