Source: https://cli.nylas.com/guides/send-email-php

# 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](https://cli.nylas.com/authors/qasim-muhammad) Staff SRE

Updated June 9, 2026

> **TL;DR:** To send email in PHP, skip `mail()` unless a local MTA is configured — it reports success without confirming delivery. PHPMailer over SMTP works with an app password (5+ min setup). An `exec()` call to the Nylas CLI gives OAuth-backed sending across 6 providers in 12 lines. One of these three returns a message ID you can verify; the comparison table below shows which.

This guide's examples reference three commands: [`nylas email send`](https://cli.nylas.com/docs/commands/email-send) for the exec() wrapper, [`nylas auth login`](https://cli.nylas.com/docs/commands/auth-login) for browser-based OAuth, and [`nylas auth config`](https://cli.nylas.com/docs/commands/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](https://www.php.net/manual/en/function.mail.php) 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](https://w3techs.com/technologies/details/pl-php), 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
<?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](https://datatracker.ietf.org/doc/html/rfc5321) 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](https://packagist.org/packages/phpmailer/phpmailer) 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](https://support.google.com/accounts/answer/185833)). 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
<?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](https://symfony.com/doc/current/mailer.html) 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()`](https://www.php.net/manual/en/function.exec.php) 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](https://cli.nylas.com/guides/getting-started). The CLI stores tokens in your system keyring and refreshes them automatically, so the app password rotation that SMTP setups need disappears.

```bash
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
<?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.

| Dimension | mail() | PHPMailer SMTP | CLI Subprocess |
| --- | --- | --- | --- |
| Setup time | 0 min (if MTA exists) | ~5 minutes | ~2 minutes |
| Auth method | None (local MTA) | App password / SMTP creds | OAuth 2.0 (browser flow) |
| Delivery feedback | Boolean only (MTA acceptance) | SMTP errors via exceptions | JSON message ID + exit code |
| Providers | Local MTA relay only | Any SMTP server | Gmail, Outlook, Exchange, Yahoo, iCloud, IMAP |
| Dependencies | None (PHP core) | 1 Composer package | None (uses exec) |
| Spam risk | High (no SPF/DKIM from web IPs) | Low (provider-signed) | Low (provider-signed) |
| Batch performance | Fast (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](https://cli.nylas.com/guides/send-email-nodejs), where Nodemailer plays PHPMailer's role, and the [Python guide](https://cli.nylas.com/guides/send-email-python), where `smtplib` does. In all three languages the subprocess pattern is identical: one process per send, JSON out.

## Next steps

- [Send email from Node.js](https://cli.nylas.com/guides/send-email-nodejs) — the same 3-method comparison for JavaScript backends
- [Send email from Python](https://cli.nylas.com/guides/send-email-python) — smtplib, Gmail API, and subprocess compared
- [Send email from terminal](https://cli.nylas.com/guides/send-email-from-terminal) — send without any programming language
- [Twilio SendGrid vs Nylas](https://cli.nylas.com/guides/twilio-vs-nylas) — how a transactional email API compares for app sending
- [EmailEngine vs Nylas](https://cli.nylas.com/guides/emailengine-vs-nylas) — self-hosted email API vs hosted CLI workflow
- [Full command reference](https://cli.nylas.com/docs/commands) — every flag and subcommand documented
