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 PHPMailerPHPMailerPHPMailer;
use PHPMailerPHPMailerSMTP;
use PHPMailerPHPMailerException;

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,nnYour 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 reportnnThe 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 PHPMailerPHPMailerOAuth;
use LeagueOAuth2ClientProviderGoogle;

$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.

Going further than the examples

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.