PHPMailer SMTP Example: Send Email via Authenticated SMTP

0

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

sunshyne works on technical SEO and email deliverability for French-speaking markets, and runs the digital consultancy at sunshyne.ch. Most of that work sits where the two overlap. On the SEO side: redirect mapping, crawl and indexation diagnostics, server log analysis, and recovering domains whose history has damaged them. On the email side: the authentication layer — SPF, DKIM and DMARC — sender reputation, and the reasons a technically valid message still gets filtered. These PHPMailer pages exist because the second half of that work keeps returning to the same questions: which transport to use, why an SMTP connection fails, and why a correctly formed message is rejected anyway. The examples here are the ones worth keeping after answering those questions more than once.

Comments are closed.