The short answer: install PHPMailer with Composer and send over SMTP on port 587 with authentication, or call a transactional email API over HTTPS. The built-in mail() function only works if the server has a configured mail transfer agent (MTA), and it cannot log in to an SMTP server, so its messages often fail SPF, DKIM, and DMARC checks. Here is the minimal PHPMailer version:
<?php
use PHPMailer\PHPMailer\PHPMailer;
require 'vendor/autoload.php';
$mail = new PHPMailer(true);
$mail->isSMTP();
$mail->Host = 'smtp.example.com';
$mail->SMTPAuth = true;
$mail->Username = getenv('SMTP_USER');
$mail->Password = getenv('SMTP_PASS');
$mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
$mail->Port = 587;
$mail->setFrom('[email protected]');
$mail->addAddress('[email protected]');
$mail->Subject = 'Hello from PHP';
$mail->Body = 'This is a test email sent from PHP.';
$mail->send();
Every snippet below passes php -l on PHP 8.5.11, and each method was run against a local test server (October 2026). The same walkthrough exists for Python and Node.js.
Which PHP email method should you use?
mail() | PHPMailer | Symfony Mailer | Email API | |
|---|---|---|---|---|
| SMTP authentication | No | Yes | Yes | API key |
| TLS | Depends on the local MTA | STARTTLS or SMTPS | STARTTLS or SMTPS | HTTPS |
| HTML and attachments | Build MIME by hand | Built in | Built in | Built in |
| Delivery events | No | No | No | Webhooks |
| Best for | Local alerts to yourself | Apps and sites using an SMTP relay | Symfony or Laravel projects | Production volume |
Method 1: PHPMailer over SMTP
PHPMailer handles SMTP authentication, TLS, HTML bodies, and attachments. Its README states it is “Compatible with PHP 5.5 and later, including PHP 8.5”, and the current release is 7.1.1. Install it with composer require phpmailer/phpmailer.
This fuller example adds an HTML body with a plain-text fallback, an attachment, and exception handling:
<?php
declare(strict_types=1);
use PHPMailer\PHPMailer\Exception;
use PHPMailer\PHPMailer\PHPMailer;
require __DIR__ . '/vendor/autoload.php';
$mail = new PHPMailer(true); // true: throw exceptions
try {
$mail->isSMTP();
$mail->Host = getenv('SMTP_HOST') ?: 'smtp.example.com';
$mail->SMTPAuth = true;
$mail->Username = getenv('SMTP_USER');
$mail->Password = getenv('SMTP_PASS');
$mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
$mail->Port = 587;
$mail->setFrom('[email protected]', 'Your App');
$mail->addAddress('[email protected]');
$mail->isHTML(true);
$mail->Subject = 'Monthly report';
$mail->Body = '<p>The <strong>monthly report</strong> is attached.</p>';
$mail->AltBody = 'The monthly report is attached.';
$mail->addAttachment(__DIR__ . '/report.pdf');
$mail->send();
} catch (Exception $e) {
error_log("Not sent: {$mail->ErrorInfo}");
}
Port 587 (STARTTLS) vs. port 465 (SMTPS)
Port 587 starts a plain connection and upgrades it with STARTTLS, so use PHPMailer::ENCRYPTION_STARTTLS. Port 465 encrypts from the first byte (implicit TLS), so use PHPMailer::ENCRYPTION_SMTPS with Port = 465. Pick whichever your provider documents; for a longer comparison, see port 587 vs 465. Both ports expect SMTP authentication.
Gmail app passwords
Gmail SMTP logins use an app password rather than your account password. Google’s help page says “App passwords aren’t recommended and are unnecessary in most cases” and recommends “Sign in with Google” where an app supports it; an app password is a 16-digit passcode and only works on accounts with 2-Step Verification. For the host, port, and limits, see the Gmail SMTP settings. Keep the passcode in an environment variable, never in the script.
Debug output
Set $mail->SMTPDebug = SMTP::DEBUG_SERVER; (import PHPMailer\PHPMailer\SMTP) to print the full SMTP conversation. Remove it before deploying, because the output includes server responses.
Method 2: Symfony Mailer
Symfony Mailer does the same job with a DSN string instead of properties. Install it with composer require symfony/mailer. The current 8.1 release requires PHP 8.4.1 or newer, so older PHP versions need an earlier major version.
<?php
use Symfony\Component\Mailer\Mailer;
use Symfony\Component\Mailer\Transport;
use Symfony\Component\Mime\Email;
require __DIR__ . '/vendor/autoload.php';
// MAILER_DSN=smtp://user:[email protected]:587
$mailer = new Mailer(Transport::fromDsn(getenv('MAILER_DSN')));
$mailer->send(
(new Email())
->from('[email protected]')
->to('[email protected]')
->subject('Hello from Symfony Mailer')
->text('Plain-text body.')
->html('<p>HTML body.</p>')
);
Laravel’s mail is “powered by the popular Symfony Mailer component”; Laravel 9 made the switch from SwiftMailer.
Method 3: Send through a transactional email API
At volume, an HTTPS API beats SMTP: providers push delivery, bounce, and complaint events to your webhooks, store templates server-side, and you avoid SMTP connection timeouts. The pattern is the same everywhere. Authenticate with a key, POST JSON, check the status code. This example uses PHP’s curl extension; replace the URL and payload keys with your provider’s API reference.
<?php
$payload = json_encode([
'from' => '[email protected]',
'to' => ['[email protected]'],
'subject' => 'Welcome to the app',
'text' => 'Thanks for signing up.',
'html' => '<p>Thanks for signing up.</p>',
], JSON_THROW_ON_ERROR);
$ch = curl_init('https://api.your-provider.com/v1/email/send');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('EMAIL_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($body === false || $status >= 400) {
throw new RuntimeException("Send failed (HTTP $status): " . ($body ?: curl_error($ch)));
}
Compare providers in this list of transactional email APIs. If you want templating, automation, and API sending in one tool, Coldletter is built for that.
The mail() function, and why it lands in spam
The signature from the PHP manual is mail(string $to, string $subject, string $message, array|string $additional_headers = [], string $additional_params = ""): bool. It hands the message to whatever program sendmail_path points to, which defaults to /usr/sbin/sendmail -t -i. On Windows it connects to the host in the SMTP ini setting (default localhost, smtp_port 25), and the manual lists no username or password setting.
<?php
$sent = mail(
'[email protected]',
'Disk usage alert',
"The disk is 91% full.\r\n",
['From' => '[email protected]', 'Content-Type' => 'text/plain; charset=UTF-8'],
'[email protected]' // envelope sender, passed to sendmail
);
The fifth argument matters: the manual says additional_params can set “the envelope sender address when using sendmail with the -f sendmail option”. That address becomes the Return-Path, where bounces go.
Why it ends up in spam: mail() returns true once the message is handed to the local MTA, not when anyone receives it. On shared hosting that MTA sends from the host’s IP with the host’s hostname, so SPF for your domain does not authorize it and there is no DKIM signature for your domain. Receivers see a message claiming your domain that your DNS never approved.
Header injection: the manual warns that if outside data is used in the headers, “the data should be sanitized so that no unwanted headers could be injected”. A \r\n inside a Subject or From value lets an attacker add Bcc headers and turn your contact form into a spam relay. Reject any input containing \r or \n before it reaches a header. PHPMailer strips \r and \n from header values (tested: a Subject containing \r\nBcc: produced no Bcc header).
Use mail() for a cron alert to yourself on a server you control. For user-facing email, use Method 1 or 3.
Common errors and fixes
These are PHPMailer’s own English messages, taken from PHPMailer.php v7.1.1:
| Error | Cause | Fix |
|---|---|---|
SMTP Error: Could not connect to SMTP host. | Wrong host or port, firewall, or the provider blocks the port | Check host and port; try 587 instead of 25 |
SMTP Error: Could not authenticate. | Wrong username or password, or Gmail without an app password | Use an app password or the provider’s SMTP credentials |
SMTP Error: The following recipients failed: | The server refused an address | Read the server reply after the message; check the address |
Invalid address: | Malformed address passed to addAddress() or setFrom() | Validate the address before adding it |
Could not instantiate mail function. | mail() failed, so no working sendmail | Configure sendmail_path or switch to isSMTP() |
Outbound port 25 is often blocked from cloud servers. AWS “blocks outbound traffic on port 25 for all EC2 instances and Lambda functions” by default, and Google Cloud blocks connections to port 25 when the destination is external to your VPC network. Use 587 or 465 instead. To check credentials outside PHP, test the SMTP server first.
Test locally without sending real email
Mailpit is a local SMTP catcher with a web inbox. Its docs say it “listens by default on port 8025 for the web UI, and port 1025 for SMTP”, so point PHPMailer at it and nothing leaves your machine:
$mail->isSMTP();
$mail->Host = 'localhost';
$mail->Port = 1025;
$mail->SMTPAuth = false;
$mail->SMTPAutoTLS = false;
Make sure it reaches the inbox
Send from an address on a domain you own, never a free webmail address you do not control. Publish SPF, DKIM, and DMARC records for that domain, and always include a plain-text alternative (AltBody). For markup tips, see how to send HTML email.
Frequently Asked Questions
How do I send email in PHP without SMTP?
Call a transactional email API over HTTPS with curl or Guzzle, as in Method 3. You POST JSON with an API key and no SMTP port is involved. The only SMTP-free built-in option is mail(), which relies on the server’s local MTA.
Why is PHP mail() not working?
Usually the server has no MTA at the path in sendmail_path, or the host blocks it. mail() also returns true as soon as the message is handed off, so a true result does not prove delivery. Check the mail log, then move to PHPMailer with authenticated SMTP.
Why do my PHP emails go to spam?
Most often the sending server is not authorized by your domain’s SPF record and the message carries no DKIM signature for your domain. Fix the DNS records, send from your own domain, include a plain-text part, and avoid mail() on shared hosts.
How do I send email from PHP using Gmail?
Use PHPMailer with host smtp.gmail.com, port 587 and STARTTLS (or port 465 with SMTPS), your Gmail address as the username, and an app password from an account with 2-Step Verification. Details are in the Gmail SMTP settings.
PHPMailer vs. mail(): which should I use?
PHPMailer for anything user-facing. It authenticates against an SMTP server, builds MIME bodies and attachments, and throws exceptions you can catch. mail() suits a local alert script on a server with a configured MTA.
How do I send HTML email with PHP?
In PHPMailer call isHTML(true), set Body to the HTML, and set AltBody to a plain-text version. With mail(), you must add MIME-Version and a Content-Type: text/html header yourself.
How do I send email to multiple recipients in PHP?
Call addAddress() once per recipient, or addCC() and addBCC(). Each call adds one address. For large lists, use an email API and send in batches.
I’ve spent my career building software at scale with a soft spot for email: deliverability, lifecycle campaigns, and getting messages to actually land. I started Coldletter to fix what bugged me about transactional and marketing email tools. I’m based in Vancouver.
