Guide

Send Email in PHP: mail(), PHPMailer, CLI

PHP's mail() function returns true even when nothing is delivered. This guide compares mail(), PHPMailer over SMTP, and a CLI subprocess that handles OAuth for Gmail and Outlook, with runnable code.

Written by Qasim Muhammad Staff SRE

VerifiedCLI 3.1.17 · Gmail, Outlook · last tested June 9, 2026

This guide's examples reference three commands: nylas email send for the exec() wrapper, nylas auth login for browser-based OAuth, and nylas auth config for API-key setup on headless servers.

Why does PHP's mail() function fail silently?

PHP's mail() function hands the message to a local mail transfer agent through the sendmail_path binary (default /usr/sbin/sendmail -t -i) and returns true the moment the MTA accepts it. The PHP manual is explicit: acceptance for delivery “does NOT mean the mail will actually reach the intended destination.”

That gap matters because PHP runs 71.1% of websites with a known server-side language as of June 2026, per W3Techs, yet most modern PHP deployments — Docker containers, serverless runtimes, minimal cloud images — ship without a configured MTA. On those hosts mail() either returns false immediately or queues messages that never leave the box. There's no error message, no bounce, no log line unless you configured one.

<?php
$ok = mail(
    "recipient@example.com",
    "Quarterly report",
    "See the attached spreadsheet.",
    "From: you@example.com\r\nContent-Type: text/plain; charset=utf-8"
);
var_dump($ok); // true means "accepted by local MTA", NOT "delivered"

Even when a local Postfix or sendmail is present, mail sent from an unauthenticated web server IP usually lands in spam because it fails SPF and DKIM checks defined in the SMTP standard's ecosystem (RFC 5321 and its companions). Use mail() only on legacy hosts where an admin already maintains the MTA.

How do I send email with PHPMailer over SMTP?

PHPMailer sends email from PHP by speaking SMTP directly to a provider's server, skipping the local MTA entirely. It's the most installed PHP email library — over 104 million total installs and roughly 2.6 million per month on Packagist as of June 2026. Unlike mail(), it throws an exception on failure.

The example below connects to Gmail's SMTP server on port 587 with STARTTLS and a 16-character app password (see Google's app password docs). For personal accounts that requirement dates to May 30, 2022, when Google removed Less Secure Apps access; Workspace tenants kept plain passwords until the staged shutdown ended on May 1, 2025. Personal Gmail caps sending at 500 messages per day; Workspace accounts get 2,000.

<?php
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;

require "vendor/autoload.php"; // composer require phpmailer/phpmailer

$mail = new PHPMailer(true); // true = throw exceptions
try {
    $mail->isSMTP();
    $mail->Host = "smtp.gmail.com";
    $mail->Port = 587;
    $mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
    $mail->SMTPAuth = true;
    $mail->Username = "you@gmail.com";
    $mail->Password = "your-16-char-app-password";

    $mail->setFrom("you@gmail.com", "Your Name");
    $mail->addAddress("recipient@example.com");
    $mail->Subject = "Quarterly report";
    $mail->Body = "See the attached spreadsheet.";

    $mail->send();
    echo "Sent";
} catch (Exception $e) {
    echo "Send failed: {$mail->ErrorInfo}";
}

Symfony Mailer covers the same ground with a one-line DSN such as smtp://user:pass@smtp.gmail.com:587 and is the better fit inside Symfony or Laravel 9+ apps (Laravel's mailer wraps it). Both libraries share the same limitation: credentials are provider-specific, and switching from Gmail to Outlook means changing host, port, and auth method.

How do I send OAuth-backed email from PHP with the CLI?

Calling the Nylas CLI from PHP's exec() function offloads OAuth 2.0 token refresh, MIME encoding, and provider differences to a static Go binary. One wrapper function covers Gmail, Outlook, Exchange, Yahoo, iCloud, and IMAP with zero Composer dependencies. Setup takes about 2 minutes: install the binary and authenticate once.

Install with Homebrew and run nylas auth login to complete the browser OAuth flow; other install methods are covered in the getting started guide. The CLI stores tokens in your system keyring and refreshes them automatically, so the app password rotation that SMTP setups need disappears.

brew install nylas/nylas-cli/nylas
nylas auth login

The nylas email send command below runs with --yes to skip the confirmation prompt and --json to return structured output, so PHP can parse the response and verify a message ID exists. The function uses escapeshellarg() on every argument — never interpolate raw input into a shell string.

<?php
function sendEmail(string $to, string $subject, string $body): array {
    $cmd = sprintf(
        "nylas email send --to %s --subject %s --body %s --yes --json",
        escapeshellarg($to),
        escapeshellarg($subject),
        escapeshellarg($body)
    );
    exec($cmd, $output, $exitCode);
    if ($exitCode !== 0) {
        throw new RuntimeException("Send failed (exit $exitCode)");
    }
    return json_decode(implode("\n", $output), true);
}

$response = sendEmail(
    "recipient@example.com",
    "Quarterly report",
    "See the attached spreadsheet."
);
echo "Sent: " . $response["id"];

Every send forks a child process from the PHP-FPM worker, costing roughly 100ms on top of the request — negligible for transactional mail triggered by a web request or cron job, slower than PHPMailer's persistent connection for batches over 1,000 messages.

How do the three PHP email methods compare?

The three ways to send email in PHP differ most on delivery feedback and provider coverage. mail() needs zero packages but gives no delivery signal. PHPMailer reports SMTP errors and pools connections. The CLI subprocess returns a JSON message ID across 6 providers. Setup times below assume a Gmail account with 2-Step Verification enabled.

Dimensionmail()PHPMailer SMTPCLI Subprocess
Setup time0 min (if MTA exists)~5 minutes~2 minutes
Auth methodNone (local MTA)App password / SMTP credsOAuth 2.0 (browser flow)
Delivery feedbackBoolean only (MTA acceptance)SMTP errors via exceptionsJSON message ID + exit code
ProvidersLocal MTA relay onlyAny SMTP serverGmail, Outlook, Exchange, Yahoo, iCloud, IMAP
DependenciesNone (PHP core)1 Composer packageNone (uses exec)
Spam riskHigh (no SPF/DKIM from web IPs)Low (provider-signed)Low (provider-signed)
Batch performanceFast (local handoff)SMTP keep-alive (fast)Process-per-send (~100ms overhead)

Which PHP email method should you choose?

Choose by delivery guarantee and provider count: PHPMailer for high-volume sends through one SMTP provider, the CLI subprocess for OAuth-backed sending or anything multi-provider, and mail() only on legacy shared hosting where the host maintains the MTA. The 104 million Packagist installs make PHPMailer the safest bet for Stack Overflow coverage.

  • Legacy shared hosting: mail(). The host's MTA is already configured, and you can't install binaries anyway.
  • High-volume transactional email: PHPMailer (or Symfony Mailer in framework apps). SMTP keep-alive beats spawning 1,000 subprocesses.
  • Scripts, cron jobs, and prototypes: CLI subprocess. 12 lines, no Composer install, and a parseable message ID for every send.
  • Multi-provider or OAuth-only accounts: CLI subprocess. Switching Gmail to Outlook requires zero code changes, and no app passwords exist to rotate.

The same three-way trade-off applies in other languages: see the Node.js guide, where Nodemailer plays PHPMailer's role, and the Python guide, where smtplib does. In all three languages the subprocess pattern is identical: one process per send, JSON out.

Next steps