PHPMailer is the most widely used library for sending email from PHP. It builds a correct message, handles HTML and attachments, and talks to an SMTP server with encryption and authentication, which the built-in mail() function cannot do on its own. This guide shows a complete working example, the right settings for ports 587 and 465, how to read the debug output, the errors you are most likely to meet and the habits that keep PHP mail out of the spam folder. The examples use placeholders for the host and credentials. Always use the SMTP host, user name and password from your provider's dashboard, and keep them in environment variables or a configuration file outside the web root rather than in your code. If you only need the settings, see SMTP settings: ports, TLS and authentication.
Why not use mail()?
PHP's mail() function hands the message to a local program, usually sendmail, and returns true when that program accepts it, not when the message is delivered. It has no authentication, no encryption and no error detail. On shared hosting the server's shared IP address sends the message, with no DKIM signature for your domain, which is a poor starting point for the inbox. Many hosts also restrict or disable it. Sending through an SMTP relay with PHPMailer gives you authenticated, encrypted delivery, a proper error message when something fails and a log on the provider side. Our overview of what an SMTP relay is explains the model.
Install PHPMailer
Use Composer. It installs the library and sets up the autoloader. Run this command in your project folder.
composer require phpmailer/phpmailerIf you cannot use Composer, download the library and include the three source files by hand, but Composer makes updates, which include security fixes, much easier. PHPMailer needs PHP 5.5 or later at a minimum, and a currently supported PHP version is strongly recommended.
A complete example with STARTTLS on port 587
This script sends an HTML message with a plain-text alternative. Credentials come from environment variables. Port 587 with STARTTLS is the standard choice for applications.
<?php
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;
require __DIR__ . '/vendor/autoload.php';
$mail = new PHPMailer(true); // true = throw exceptions
try {
$mail->isSMTP();
$mail->Host = getenv('SMTP_HOST'); // e.g. 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 = 'UTF-8';
$mail->setFrom('[email protected]', 'Example App');
$mail->addReplyTo('[email protected]', 'Example Support');
$mail->addAddress('[email protected]', 'A Customer');
$mail->isHTML(true);
$mail->Subject = 'Welcome to Example';
$mail->Body = '<p>Thanks for signing up.</p>';
$mail->AltBody = 'Thanks for signing up.';
$mail->send();
echo 'Sent';
} catch (Exception $e) {
error_log('Mailer error: ' . $mail->ErrorInfo);
echo 'The message could not be sent.';
}The constant ENCRYPTION_STARTTLS corresponds to the string tls and means "connect in plain text, then upgrade". Use PHPMailer::ENCRYPTION_SMTPS (the string ssl) together with port 465 if you want encryption from the first byte. Never mix them: STARTTLS on port 465, or SMTPS on port 587, fails with a handshake error.
Which settings go together
| Port | SMTPSecure constant | Behaviour |
|---|---|---|
| 587 | PHPMailer::ENCRYPTION_STARTTLS | Starts unencrypted and upgrades with STARTTLS. Recommended for most applications |
| 465 | PHPMailer::ENCRYPTION_SMTPS | Encrypted from the first byte (implicit TLS) |
| 25 | none | Server-to-server port, often blocked for applications. Avoid it |
| 2525 | PHPMailer::ENCRYPTION_STARTTLS | Offered by some providers as an alternative if 587 is blocked. Check your provider |
If you are unsure which port to choose, read which SMTP port to use. Do not set SMTPAutoTLS to false or turn off certificate checks to "make it work". Disabling verification hides a real problem and exposes your credentials to interception.
Attachments, Cc and Bcc, and multiple recipients
Add files with addAttachment, passing a path and an optional display name. Add copies with addCC and addBCC. Each recipient address is one call. For embedded images, use addEmbeddedImage with a content ID and reference it in the HTML as cid:logo. Keep attachments small: the combined size of the message, after encoding, has to fit within the receiving server's limit, and encoding makes files about one third larger. See email attachment size limits for common limits.
$mail->addAttachment('/path/to/invoice.pdf', 'Invoice-1042.pdf');
$mail->addCC('[email protected]');
$mail->addBCC('[email protected]');Debug output: see the whole SMTP conversation
When a send fails, turn on debug output. It prints every command and reply, which usually shows the cause in seconds. Use it in development only, and never leave it on in production, because it can print credentials in logs.
use PHPMailer\PHPMailer\SMTP;
$mail->SMTPDebug = SMTP::DEBUG_SERVER; // client and server messages
$mail->Debugoutput = function ($str, $level) {
error_log("SMTP debug [$level]: $str");
};Read the output in order: the connection, the EHLO reply that lists the server's features, the STARTTLS exchange, the AUTH step and finally the MAIL FROM, RCPT TO and DATA commands. The first line that shows an error code is the one to search for.
Common errors and what they mean
| Message | Usual cause | Fix |
|---|---|---|
| SMTP connect() failed | The server cannot reach the host or port, or a firewall blocks it | Check the host name, the port and outbound firewall rules, and test with openssl s_client |
| Could not authenticate / 535 | Wrong credentials, or credentials in the wrong format | See our guide to SMTP error 535 |
| Failed to connect to server (connection timed out) | Outbound connections on that port are blocked, common on some shared hosts | Try a different supported port or ask the host to allow outbound SMTP |
| SSL: wrong version number | STARTTLS used on port 465 or SMTPS used on port 587 | Match the encryption constant to the port |
| certificate verify failed | Outdated CA bundle on the server, or a proxy that intercepts TLS | Update the CA certificates. Do not disable verification |
| Sender address rejected | The From address is not allowed for this account or domain is not verified | Send from an address on a verified domain |
| Message body empty | Neither Body nor AltBody was set | Set a Body and an AltBody |
| Invalid address | A malformed recipient address | Validate input with filter_var before calling addAddress |
Structuring mail code in a real application
A script like the one above is fine for a first test, but in an application you should wrap the library once and reuse it. Create a small class or service that builds a configured PHPMailer instance from your settings, so that host, port and credentials live in one place. Pass in the recipient, subject and content. Return a result that your code can check. Keep message templates separate from sending logic so a designer can change a template without touching delivery code. Reuse one connection for several messages with SMTPKeepAlive = true and call clearAddresses() and clearAttachments() between messages, then call smtpClose() at the end. Without clearing, the second message would also be sent to the first message's recipient.
Do not send mail inside the web request when a user is waiting, if you can avoid it. SMTP is slower than a database query, and a provider timeout would freeze the page. Put messages on a queue and let a worker send them, with retries that follow the rules in rate limits, retries and backoff. For high volume, an email API can be a better fit than SMTP, which our comparison of SMTP and API sending explains.
Security notes
- Keep credentials in environment variables or a protected config file, never in version control.
- Never build the From address or recipient from raw user input. An attacker who can inject a line break into a header can add recipients. PHPMailer rejects newlines in addresses, but validate input anyway.
- Rate limit contact and sign-up forms and add a CAPTCHA or token check so that your form cannot be used to send spam.
- Keep PHPMailer updated through Composer. Older versions have had security fixes.
- Set a sensible timeout so that a slow server cannot hang your process:
$mail->Timeout = 15;.
Deliverability for PHP mail
A correct library does not guarantee the inbox. Three things matter most. First, authenticate your domain with SPF, DKIM and DMARC, and send from an address on that domain. Second, send a clear plain-text alternative along with the HTML, and keep the content honest, because filters penalise empty or image-only messages. Third, separate transactional mail from marketing mail, so that a marketing complaint does not harm password reset delivery. The guides on authenticating your sending domain and separate transactional and marketing streams cover these steps.
Send from PHP through a managed relay Get the SMTP host and credentials from your dashboard and connect PHPMailer in minutes.
References and further reading
Frequently asked questions
Which port should I use with PHPMailer?
Use port 587 with STARTTLS for most applications. Use port 465 with SMTPS if your provider recommends implicit TLS. Match the encryption setting to the port.
Why does PHP mail() not work?
mail() relies on a local mail program, often disabled or restricted on hosts, and it has no authentication or error detail. Sending through SMTP with PHPMailer is more reliable.
How do I see the SMTP conversation?
Set SMTPDebug to SMTP::DEBUG_SERVER in development and send a test. The output shows each command and server reply, which usually reveals the cause.
Is it safe to turn off certificate verification?
No. It hides real TLS problems and exposes credentials to interception. Fix the certificate or CA bundle instead.
Should I send email inside the page request?
Avoid it when possible. Use a queue and a worker so that a slow SMTP server does not delay the page, and so that failed messages can be retried.