PHPMailer Examples

0

A complete set of working PHPMailer examples, one per sending transport and message type. Every snippet below is self-contained: install PHPMailer, paste, change the addresses and credentials, run. All examples target PHPMailer 6.x.

These pages replace the example scripts that were distributed with the original PHPMailer package (test_mail_basic.php, test_smtp_advanced.php and the rest). The library itself is maintained at github.com/PHPMailer/PHPMailer, where the current example folder also lives.

Before you start

composer require phpmailer/phpmailer

Every example assumes these four lines at the top of the file:

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

require 'vendor/autoload.php';

Passing true to the constructor turns on exceptions. Without it, send() returns false and you have to remember to check it — which is how silent email failures happen in production.

1. Basic mail()

The simplest possible send. Uses PHP’s built-in mail() function, which hands the message to whatever MTA is configured locally.

$mail = new PHPMailer(true);

try {
    $mail->setFrom('from@example.com', 'Your App');
    $mail->addAddress('recipient@example.com', 'Recipient Name');

    $mail->Subject = 'Basic mail() test';
    $mail->Body    = 'This is a plain-text message sent with PHP mail().';

    $mail->send();
    echo 'Sent.';
} catch (Exception $e) {
    echo 'Failed: ' . $mail->ErrorInfo;
}

mail() is the default transport, so there is nothing to configure. The catch: it reports success as soon as the message is accepted locally. If the MTA drops it thirty seconds later, your code never finds out. Fine for a contact form on a well-configured server, wrong for anything transactional.

2. Advanced mail() — HTML, attachment, envelope sender

$mail = new PHPMailer(true);

try {
    $mail->CharSet = PHPMailer::CHARSET_UTF8;

    $mail->setFrom('from@example.com', 'Your App');
    $mail->addAddress('recipient@example.com');
    $mail->addReplyTo('support@example.com', 'Support');
    $mail->Sender = 'bounces@example.com'; // envelope sender / Return-Path

    $mail->addAttachment('/path/to/invoice.pdf', 'invoice.pdf');

    $mail->isHTML(true);
    $mail->Subject = 'Your invoice';
    $mail->Body    = '<p>Hello,</p><p>Your invoice is <b>attached</b>.</p>';
    $mail->AltBody = "Hello,\n\nYour invoice is attached.";

    $mail->send();
} catch (Exception $e) {
    echo 'Failed: ' . $mail->ErrorInfo;
}

Set CharSet explicitly whenever the subject or body can contain accents or non-Latin characters — the default is ISO-8859-1 and will mangle them. Sender sets the envelope sender, which is where bounces go; it is separate from the visible From header.

3. Sendmail transport

Talks to the sendmail binary directly instead of going through mail(). Slightly more control, and it works when mail() is disabled.

$mail = new PHPMailer(true);

$mail->isSendmail();
$mail->Sendmail = '/usr/sbin/sendmail -bs'; // -bs = read SMTP on stdin

try {
    $mail->setFrom('from@example.com', 'Your App');
    $mail->addAddress('recipient@example.com');
    $mail->Subject = 'Sendmail test';
    $mail->Body    = 'Sent via the sendmail binary.';
    $mail->send();
} catch (Exception $e) {
    echo 'Failed: ' . $mail->ErrorInfo;
}

Use -bs rather than -t. With -bs, sendmail speaks SMTP over stdin and PHPMailer gets a real status back; with -t it parses headers and stays silent about failures. If you are on Postfix or Exim, the sendmail-compatible wrapper is still at that path.

4. Basic SMTP

The recommended transport for anything that matters. This is the one you want if the message must actually arrive.

$mail = new PHPMailer(true);

try {
    $mail->isSMTP();
    $mail->Host       = 'smtp.example.com';
    $mail->SMTPAuth   = true;
    $mail->Username   = 'user@example.com';
    $mail->Password   = 'your-password';
    $mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
    $mail->Port       = 587;

    $mail->setFrom('from@example.com', 'Your App');
    $mail->addAddress('recipient@example.com');

    $mail->Subject = 'SMTP test';
    $mail->Body    = 'Sent through authenticated SMTP.';

    $mail->send();
} catch (Exception $e) {
    echo 'Failed: ' . $mail->ErrorInfo;
}

Port and encryption pairs

Port Constant Behaviour
587 ENCRYPTION_STARTTLS Connect in plain text, then upgrade to TLS. The modern default.
465 ENCRYPTION_SMTPS TLS from the first byte. Still supported everywhere.
25 none Server-to-server relay. Blocked outbound by nearly every host.

Crossing them is the most common configuration error there is: 587 with ENCRYPTION_SMTPS hangs until timeout, 465 with ENCRYPTION_STARTTLS fails immediately.

5. Advanced SMTP — HTML, inline image, multiple recipients

$mail = new PHPMailer(true);

try {
    $mail->isSMTP();
    $mail->Host       = 'smtp.example.com';
    $mail->SMTPAuth   = true;
    $mail->Username   = 'user@example.com';
    $mail->Password   = 'your-password';
    $mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
    $mail->Port       = 587;
    $mail->CharSet    = PHPMailer::CHARSET_UTF8;

    $mail->setFrom('from@example.com', 'Your App');
    $mail->addAddress('first@example.com', 'First Recipient');
    $mail->addCC('cc@example.com');
    $mail->addBCC('archive@example.com');
    $mail->addReplyTo('support@example.com', 'Support');

    $mail->addEmbeddedImage('/path/to/logo.png', 'logo');
    $mail->addAttachment('/path/to/report.csv', 'report.csv');

    $mail->isHTML(true);
    $mail->Subject = 'Your monthly report';
    $mail->Body    = '<img src="cid:logo" alt="Logo"><h1>Monthly report</h1>'
                   . '<p>The full data is attached as CSV.</p>';
    $mail->AltBody = "Monthly report\n\nThe full data is attached as CSV.";

    $mail->send();
} catch (Exception $e) {
    echo 'Failed: ' . $mail->ErrorInfo;
}

Inline images work by content ID: addEmbeddedImage($path, $cid) then <img src="cid:$cid"> in the body. The alternative — msgHTML() — parses your HTML, embeds local images automatically and generates AltBody for you:

$mail->msgHTML(file_get_contents('email.html'), __DIR__);

6. SMTP without authentication

For an internal relay or a local MTA listening on 25 that accepts your host without credentials:

$mail = new PHPMailer(true);

$mail->isSMTP();
$mail->Host        = 'localhost';
$mail->SMTPAuth    = false;
$mail->Port        = 25;
$mail->SMTPAutoTLS = false; // do not attempt STARTTLS on a plain local relay

try {
    $mail->setFrom('from@example.com', 'Your App');
    $mail->addAddress('recipient@example.com');
    $mail->Subject = 'Relay test';
    $mail->Body    = 'Sent through an unauthenticated relay.';
    $mail->send();
} catch (Exception $e) {
    echo 'Failed: ' . $mail->ErrorInfo;
}

SMTPAutoTLS defaults to true, so PHPMailer will try to upgrade to TLS whenever the server advertises STARTTLS. On a relay with a self-signed or absent certificate that upgrade fails and takes the send with it — hence turning it off here. Only do this for traffic that never leaves your own network.

7. Gmail — basic

Gmail stopped accepting account passwords over SMTP. You need an App Password, which requires 2-Step Verification to be enabled on the account. Once you have the 16-character value, nothing else changes:

$mail = new PHPMailer(true);

try {
    $mail->isSMTP();
    $mail->Host       = 'smtp.gmail.com';
    $mail->SMTPAuth   = true;
    $mail->Username   = 'you@gmail.com';
    $mail->Password   = 'abcdefghijklmnop'; // App Password, not your login password
    $mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
    $mail->Port       = 587;

    $mail->setFrom('you@gmail.com', 'Your Name');
    $mail->addAddress('recipient@example.com');

    $mail->Subject = 'Gmail SMTP test';
    $mail->Body    = 'Sent through Gmail SMTP.';

    $mail->send();
} catch (Exception $e) {
    echo 'Failed: ' . $mail->ErrorInfo;
}

Set setFrom() to the same mailbox you authenticate with. Gmail rewrites the From header to the authenticated account unless the address is a verified alias, so a mismatch just gets silently overwritten.

8. Gmail — advanced, with XOAUTH2

App Passwords are simple but tied to one account and revocable by an admin. For a service that sends on behalf of Google Workspace users, XOAUTH2 is the supported path. It needs one extra package:

composer require league/oauth2-google
use PHPMailer\PHPMailer\OAuth;
use League\OAuth2\Client\Provider\Google;

$provider = new Google([
    'clientId'     => 'YOUR_CLIENT_ID',
    'clientSecret' => 'YOUR_CLIENT_SECRET',
]);

$mail = new PHPMailer(true);
$mail->isSMTP();
$mail->Host       = 'smtp.gmail.com';
$mail->SMTPAuth   = true;
$mail->AuthType   = 'XOAUTH2';
$mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS;
$mail->Port       = 465;

$mail->setOAuth(new OAuth([
    'provider'     => $provider,
    'clientId'     => 'YOUR_CLIENT_ID',
    'clientSecret' => 'YOUR_CLIENT_SECRET',
    'refreshToken' => 'YOUR_REFRESH_TOKEN',
    'userName'     => 'you@yourdomain.com',
]));

You obtain the refresh token once, through Google’s OAuth consent flow, and store it. PHPMailer exchanges it for a short-lived access token on each send. Gmail’s daily sending limits still apply either way — for real volume, use a transactional provider.

9. Sending many messages on one connection

Opening a fresh SMTP connection per message is slow and looks like abuse to the receiving server. Keep the connection alive and reset the recipients between sends:

$mail = new PHPMailer(true);
$mail->isSMTP();
$mail->Host          = 'smtp.example.com';
$mail->SMTPAuth      = true;
$mail->Username      = 'user@example.com';
$mail->Password      = 'your-password';
$mail->SMTPSecure    = PHPMailer::ENCRYPTION_STARTTLS;
$mail->Port          = 587;
$mail->SMTPKeepAlive = true; // reuse the connection

$mail->setFrom('from@example.com', 'Your App');
$mail->Subject = 'Your weekly digest';

foreach ($subscribers as $subscriber) {
    try {
        $mail->addAddress($subscriber['email'], $subscriber['name']);
        $mail->Body = renderDigest($subscriber);
        $mail->send();
    } catch (Exception $e) {
        error_log("Failed for {$subscriber['email']}: {$mail->ErrorInfo}");
    } finally {
        $mail->clearAddresses(); // essential
    }
}

$mail->smtpClose();

clearAddresses() in a finally block is not optional. Without it, recipients accumulate and message five goes to all five people. Use clearAttachments() too if attachments vary per message.

Never put a large recipient list in a single To — every recipient sees every other address. Loop and send individually.

10. Validating addresses before sending

if (!PHPMailer::validateAddress($email)) {
    throw new InvalidArgumentException("Invalid address: $email");
}

Cheap, and it stops one malformed address from aborting a batch. It validates syntax only — it cannot tell you whether the mailbox exists.

Debugging any of the above

$mail->SMTPDebug = SMTP::DEBUG_SERVER; // client + server messages

This prints the actual SMTP conversation and identifies the problem on the first run in almost every case. The levels are DEBUG_OFF, DEBUG_CLIENT, DEBUG_SERVER, DEBUG_CONNECTION and DEBUG_LOWLEVEL. Start at DEBUG_SERVER. Always turn it off in production — it writes straight to output.

Where to go next

If messages send successfully but land in spam, none of the code above is the problem: that is SPF, DKIM and DMARC on your sending domain. PHPMailer can sign with DKIM itself through the DKIM_domain, DKIM_selector and DKIM_private properties.

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.