Docs / PHP / PHPMailer

Send email in PHP over SMTP

PHP's built-in mail() function is the reason so much PHP email never arrives: it hands the message to a local sendmail binary that usually is not there, and when it is, the message goes out unauthenticated and lands in spam. This guide uses PHPMailer to send through Postwing with authentication, DKIM signing and SPF alignment handled for you.

You can get them on the token management page. For security reasons, a token is shown only once — at the moment it is created.

SMTP connection settings

SettingValue
SMTP hostsmtp.postwing.app
Port587
EncryptionSTARTTLS (the connection is upgraded to TLS before login)
UsernameThe login of an SMTP token for your domain
PasswordThe password of that token — shown once, when the token is created
Every mode is also available on a high port: 8465 (SSL/TLS), 8587 (STARTTLS) and 8025 (plain). Many hosting providers and clouds block outbound 25, 465 and 587 — if the connection times out, switch to the matching high port.

Install PHPMailer

bash
composer require phpmailer/phpmailer

Send your first email

Construct PHPMailer with true so failures throw instead of returning false — otherwise a broken configuration is indistinguishable from a working one:

send.php
<?php

use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\SMTP;
use PHPMailer\PHPMailer\Exception;

require 'vendor/autoload.php';

$mail = new PHPMailer(true);   // true = throw exceptions instead of returning false

try {
    $mail->isSMTP();
    $mail->Host       = 'smtp.postwing.app';
    $mail->Port       = 587;
    $mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;   // 'tls'
    $mail->SMTPAuth   = true;
    $mail->Username   = getenv('SMTP_USER');
    $mail->Password   = getenv('SMTP_PASS');
    $mail->Timeout    = 10;
    $mail->CharSet    = PHPMailer::CHARSET_UTF8;

    $mail->setFrom('noreply@your-domain.com', 'Acme');
    $mail->addAddress('customer@example.com', 'Jane Doe');

    $mail->isHTML(true);
    $mail->Subject = 'Your order #4417 is confirmed';
    $mail->Body    = '<h1>Order confirmed</h1><p>Your order ships tomorrow.</p>';
    $mail->AltBody = 'Order confirmed. Your order ships tomorrow.';   // always set this

    $mail->send();
    echo 'Sent';
} catch (Exception $e) {
    // $mail->ErrorInfo carries the SMTP-level detail
    error_log('Mail failed: ' . $mail->ErrorInfo);
}

For implicit TLS on port 465:

php
// Implicit TLS on port 465 instead of STARTTLS
$mail->Port       = 465;
$mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS;   // 'ssl'
The SMTPOptions block with verify_peer => false that circulates on forums switches TLS validation off entirely, so the connection can be silently intercepted. If verification fails, fix the CA bundle (openssl.cafile) rather than bypassing the check.

Attachments and inline images

php
$mail->addAttachment('/srv/invoices/2026-03.pdf', 'invoice.pdf');

// From a string you generated in memory
$mail->addStringAttachment($pdfData, 'invoice.pdf', 'base64', 'application/pdf');

// Inline image, referenced from the HTML as <img src="cid:logo">
$mail->addEmbeddedImage('/var/www/assets/logo.png', 'logo', 'logo.png');

Send a batch on one connection

By default PHPMailer connects, authenticates and disconnects for every message. SMTPKeepAlive holds the connection open — just remember to clear the recipients between sends, or each message goes to everyone who came before:

php
// Reuse one authenticated connection for a batch of messages.
$mail->SMTPKeepAlive = true;

foreach ($users as $user) {
    $mail->clearAddresses();          // otherwise recipients accumulate
    $mail->addAddress($user->email);
    $mail->Subject = 'Your weekly report';
    $mail->Body    = renderReport($user);

    try {
        $mail->send();
    } catch (Exception $e) {
        error_log("Failed for {$user->email}: {$mail->ErrorInfo}");
    }
}

$mail->smtpClose();
Attention!
When using BCC, CC - a separate email will be created for each recipient in the system and your limits will be used.

Contact forms

Never put the visitor's address in setFrom(). Your domain's DKIM and SPF cannot vouch for someone else's address, so the message fails DMARC and is discarded. Use addReplyTo():

php
// Contact form: the From stays on YOUR domain, the visitor goes in Reply-To.
$mail->setFrom('noreply@your-domain.com', 'Website');
$mail->addAddress('sales@your-domain.com');
$mail->addReplyTo($_POST['email'], $_POST['name']);

$mail->Subject = 'New contact form submission';
$mail->Body    = nl2br(htmlspecialchars($_POST['message'], ENT_QUOTES, 'UTF-8'));
$mail->AltBody = $_POST['message'];

Debugging a failing connection

php
// Temporarily print the whole SMTP conversation while debugging.
$mail->SMTPDebug   = SMTP::DEBUG_SERVER;
$mail->Debugoutput = 'error_log';

Troubleshooting

ErrorCause and fix
SMTP connect() failed Blocked outbound port — use 8587 or 8465 — or encryption not matching the port.
SMTP Error: Could not authenticate Wrong credentials, or SMTPAuth left false.
Could not connect to SMTP host + SSL errorENCRYPTION_SMTPS on 587, or ENCRYPTION_STARTTLS on 465.
Accented characters are mangled Set $mail->CharSet = PHPMailer::CHARSET_UTF8.
Each message goes to more people than intended Missing clearAddresses() inside a send loop.
SMTP Error: data not accepted (550) The setFrom domain is not verified, or the token does not cover it.

Frequently asked questions

Why does PHP mail() not send email?

mail() hands the message to a local sendmail binary. Most modern hosting has no local mail server, so the call returns true and the message is discarded. Even when one exists the mail is unauthenticated, so it fails SPF and DKIM checks at the recipient and is filtered as spam. An authenticated SMTP client such as PHPMailer avoids both problems.

Which encryption constant should I use in PHPMailer?

PHPMailer::ENCRYPTION_STARTTLS with port 587, or PHPMailer::ENCRYPTION_SMTPS with port 465. The string values 'tls' and 'ssl' mean the same thing. Mismatching the constant and the port is the most common cause of a failed handshake.

Is it safe to set SMTPOptions to disable peer verification?

No. The allow_self_signed and verify_peer => false snippet found in many tutorials turns off TLS certificate validation entirely, which makes the connection interceptable. The real cause is usually a missing or outdated CA bundle — point openssl.cafile at a current one instead.

Should I use PHPMailer or Symfony Mailer?

Both are actively maintained. PHPMailer is the smaller dependency and drops into legacy code with no framework. Symfony Mailer has a cleaner API, DSN-based configuration and asynchronous transports, so it fits better in a modern application — Laravel is built on it.

Why does PHPMailer report 'SMTP connect() failed'?

Either the host blocks outbound ports 25, 465 and 587 — switch to 8587 or 8465 — or the encryption setting does not match the port. Enable SMTPDebug = SMTP::DEBUG_SERVER to see exactly where the conversation stops.

Do I need to set AltBody?

Yes, whenever Body is HTML. A message with only an HTML part is a recognised spam signal and renders as nothing in clients that prefer plain text. AltBody adds the text alternative that makes the message multipart/alternative.

Next steps