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
$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.
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
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
SMTP_HOST=smtp.sendgrid.net SMTP_USERNAME=apikey SMTP_PASSWORD=SG.xxxxxxxxxxxxxxxxxxxxxxxxxxxx
PHP's built-in
mail()hands a message to the local mail transport agent (usually sendmail) with no authentication and no delivery confirmation.mail()returningtrueonly 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
.envfile excluded from version control), never hardcoded directly in source files.