Sending through an authenticated SMTP server is the right default for any message that has to arrive. It gives you TLS, real authentication, and — unlike PHP’s mail() — an actual error when something goes wrong. This page covers the configuration in full, plus every failure mode worth knowing about.
All examples target PHPMailer 6.x. For the other transports, see the examples collection.
The complete example
<?php
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\SMTP;
use PHPMailer\PHPMailer\Exception;
require 'vendor/autoload.php';
$mail = new PHPMailer(true);
try {
// Transport
$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->CharSet = PHPMailer::CHARSET_UTF8;
// Envelope and headers
$mail->setFrom('noreply@example.com', 'Your App');
$mail->addAddress('recipient@example.com', 'Recipient Name');
$mail->addReplyTo('support@example.com', 'Support');
$mail->Sender = 'bounces@example.com';
// Content
$mail->isHTML(true);
$mail->Subject = 'Your order has shipped';
$mail->Body = '<h1>On its way</h1><p>Tracking number: ABC123.</p>';
$mail->AltBody = "On its way\n\nTracking number: ABC123.";
$mail->send();
echo 'Sent.';
} catch (Exception $e) {
echo 'Failed: ' . $mail->ErrorInfo;
}
Read the credentials from the environment rather than hard-coding them. A committed SMTP password means somebody else can send mail as your domain, and it will be your domain’s reputation that pays for it.
Choosing the port and encryption
| Port | Constant | Value | How it works |
|---|---|---|---|
| 587 | PHPMailer::ENCRYPTION_STARTTLS |
tls |
Opens a plain connection, issues STARTTLS, then negotiates TLS. The modern default, and what you should use unless your provider says otherwise. |
| 465 | PHPMailer::ENCRYPTION_SMTPS |
ssl |
TLS is established before any SMTP command. Once deprecated, now formally supported again and widely offered. |
| 25 | none | — | Server-to-server relay. Blocked outbound by almost every shared host, VPS provider and cloud platform. |
Mismatching the two is the most frequent SMTP error in PHP. Port 587 with ENCRYPTION_SMTPS waits for a TLS handshake that never comes and dies on the timeout. Port 465 with ENCRYPTION_STARTTLS sends a plaintext EHLO into an encrypted socket and fails immediately.
SMTPAutoTLS
SMTPAutoTLS defaults to true: whenever the server advertises STARTTLS, PHPMailer will upgrade even if you left SMTPSecure empty. That is the safe default and you should leave it alone for anything crossing a network. The one case to disable it is a local relay with a broken or absent certificate:
$mail->SMTPAutoTLS = false; // localhost relay only
Verify the port is reachable first
Before debugging PHP, confirm you can even reach the server. This is a two-second check that saves an hour:
# STARTTLS on 587
openssl s_client -starttls smtp -connect smtp.example.com:587
# Implicit TLS on 465
openssl s_client -connect smtp.example.com:465
A successful run prints the certificate chain and a 220 greeting. A hang or Connection refused means the problem is the network or the host’s outbound policy, not your code.
Turn on the SMTP conversation
$mail->SMTPDebug = SMTP::DEBUG_SERVER;
| Constant | Value | Output |
|---|---|---|
SMTP::DEBUG_OFF |
0 | Nothing. Production setting. |
SMTP::DEBUG_CLIENT |
1 | Your commands only. |
SMTP::DEBUG_SERVER |
2 | Commands and server replies. Start here. |
SMTP::DEBUG_CONNECTION |
3 | Adds connection-level events. |
SMTP::DEBUG_LOWLEVEL |
4 | Raw traffic. Rarely useful. |
Debug output goes to standard output, which will corrupt a JSON response or break headers already sent. Route it to a log instead:
$mail->SMTPDebug = SMTP::DEBUG_SERVER;
$mail->Debugoutput = function ($str, $level) {
error_log('PHPMailer: ' . trim($str));
};
What the errors actually mean
SMTP connect() failed
The TCP connection never completed, so authentication was never attempted. In order of likelihood: the port/encryption mismatch above, an outbound firewall, a wrong hostname, or a provider that blocks SMTP from web servers. The openssl check settles it.
SMTP Error: Could not authenticate
The connection worked and the credentials were rejected. On Gmail this is almost always a regular account password where an App Password is required. Elsewhere, check whether the username is the full email address or just the local part — providers differ, and both are common.
stream_socket_enable_crypto(): certificate verify failed
TLS negotiated but the certificate could not be validated, usually a missing or stale CA bundle on the server. The fix is in php.ini:
openssl.cafile = /etc/ssl/certs/ca-certificates.crt
You will find snippets online that disable verification instead:
// Do not do this in production.
$mail->SMTPOptions = [
'ssl' => ['verify_peer' => false, 'verify_peer_name' => false, 'allow_self_signed' => true],
];
It silences the error by switching off the only thing TLS was protecting you against. Acceptable against a local test server, never against a real one.
SMTP Error: data not accepted
The server took the envelope and refused the message body. Usually content-based rejection: a blocked attachment type, a size limit, or spam scoring. The server’s own text in ErrorInfo normally names the reason.
Detected an illegal character in header / Invalid address
An address or header value contains a newline. That is header-injection protection doing its job — if the value came from user input, validate it before use:
if (!PHPMailer::validateAddress($email)) {
throw new InvalidArgumentException("Invalid address: $email");
}
Timeouts
$mail->Timeout = 30; // seconds; default is 300
The 300-second default follows the RFC but is far too long for a web request — a user waiting on a form submission will give up first. Lower it for anything synchronous, and move bulk sending to a queue.
Sending several messages efficiently
$mail->SMTPKeepAlive = true;
foreach ($recipients as $r) {
try {
$mail->addAddress($r['email'], $r['name']);
$mail->Body = renderBody($r);
$mail->send();
} catch (Exception $e) {
error_log("Failed for {$r['email']}: {$mail->ErrorInfo}");
} finally {
$mail->clearAddresses();
}
}
$mail->smtpClose();
Without SMTPKeepAlive, PHPMailer opens and tears down a connection per message — slow, and a pattern that rate limiters treat as abuse. The finally block is mandatory: skip clearAddresses() and recipients accumulate, so the tenth message goes to all ten people.
Provider settings
| Provider | Host | Port | Notes |
|---|---|---|---|
| Gmail / Workspace | smtp.gmail.com |
587 | App Password or XOAUTH2. Daily limits apply. |
| Outlook / Microsoft 365 | smtp.office365.com |
587 | Basic auth is being retired; OAuth2 is the supported path. |
| Amazon SES | email-smtp.<region>.amazonaws.com |
587 | SMTP credentials are generated in SES, not your AWS keys. |
| Brevo | smtp-relay.brevo.com |
587 | Login is the account email, password is an SMTP key. |
| Mailgun | smtp.mailgun.org |
587 | Credentials are per sending domain. |
| Postmark | smtp.postmarkapp.com |
587 | Server API token used as both username and password. |
Always confirm against your provider’s own documentation — hostnames change and this table will age.
If it sends but lands in spam
Nothing on this page will fix that, because it is not an SMTP problem. Acceptance is decided by DNS records for your sending domain: SPF to list authorised senders, DKIM so the message can be verified cryptographically, and DMARC to tell receivers what to do when the first two fail. Missing DKIM is the usual culprit.
If your provider does not sign for you, PHPMailer can:
$mail->DKIM_domain = 'example.com';
$mail->DKIM_selector = 'phpmailer';
$mail->DKIM_private = '/secure/path/dkim_private.pem';
$mail->DKIM_identity = $mail->From;
Publish the public key at phpmailer._domainkey.example.com and keep the private key outside the web root.
See also
- PHPMailer Tutorial — the full walkthrough from installation.
- Examples — every transport, including Gmail and XOAUTH2.
- mail() Example — sending without SMTP.
- Properties Reference — every SMTP property and its default.
- Methods Reference — signatures for the methods used here.
