You want to send a decent-looking HTML email from a PHP app. Maybe it’s a booking confirmation, a welcome email, or a contact form notification. The built‑in mail() function kind of works… until you need images, attachments, proper SMTP, or reliable delivery.
In this guide, we’ll walk through sending HTML email in PHP using mail(), PHPMailer, SymfonyMailer, and a PHP email API client. You’ll see what each option can do, where it hurts, and how to pick a setup that’s faster, more stable, and easier to maintain.
Let’s start with the classic: PHP’s native mail() function.
It’s always available, it’s easy to call, and for quick tests it feels fine. But as soon as you send real HTML emails in production, the cracks show up.
Main limitations of mail():
Attachments are painful without manual MIME boundaries.
Embedding images via CID is tedious.
Sending to many recipients is slow because each call opens and closes the SMTP connection.
It usually sends through the local server, which often has poor IP reputation and no proper SMTP authentication.
Even if you set SPF, DKIM, DMARC, deliverability can still be bad.
So yes, mail() can send HTML emails, but it’s not built for serious transactional or bulk email in a modern web development stack.
And there’s another piece of the puzzle: the server you run this on. Shared hosting with noisy neighbors and blacklisted IPs can kill your deliverability even if your PHP code is perfect.
If you want your PHP mailer to live on a clean, fast box with a dedicated IP, moving to a proper server host is a big win.
With a stable server and a sane mailer library, HTML emails stop being such a headache.
If you still want to use mail(), here’s how to make it send basic HTML.
mail() takes four arguments:
Recipient email
Subject
Message body
Additional headers (string)
To send HTML, you must set the Content-Type header appropriately.
php
<?php
$to = 'contact@example.com';
$subject = 'Review request reminder';
// Build headers as an array, then join with CRLF
$headers = [
'From: sender@example.com',
'MIME-Version: 1.0',
'Content-Type: text/html; charset=UTF-8',
];
$message = '
Review Request Reminder
Here are the cases requiring your review in December:
Case title
Category
Status
Due date
Case 1
Development
Pending
Dec-20
Case 2
DevOps
Pending
Dec-21
';
$result = mail($to, $subject, $message, implode("\r\n", $headers));
if ($result) {
echo "Success!" . PHP_EOL;
} else {
echo "Error." . PHP_EOL;
}
You’ll see “Success!” if PHP hands the message off to the local mail transfer agent. That still doesn’t guarantee it lands in the inbox, but at least you know your script ran.
With mail(), the simplest way to “embed” images is not to embed them at all. Just host them on your web server and link to them by absolute URL.
Steps:
Upload the image to a public folder on your server.
Use an <img> tag with the full URL inside your HTML email.
html
Then you send that HTML via mail() exactly like in the previous example:
php
<?php
$to = 'recipient@example.com';
$subject = 'Email with Embedded Image';
$message = '
Email with Embedded Image
Here is an image embedded in this email:
';
$headers = [
'MIME-Version: 1.0',
'Content-Type: text/html; charset=UTF-8',
'From: sender@example.com',
'Reply-To: sender@example.com',
];
if (mail($to, $subject, $message, implode("\r\n", $headers))) {
echo "Email sent successfully.";
} else {
echo "Failed to send email.";
}
This method keeps email size small, because images are loaded over HTTP when the email opens.
If you really want a self-contained HTML email, you can encode the image as base64 and inline it in the src attribute.
First, read and encode the image:
php
<?php
$imagePath = 'path/to/your/image.jpg';
if (file_exists($imagePath) && is_readable($imagePath)) {
$imageData = base64_encode(file_get_contents($imagePath));
} else {
echo "Image file not found or not accessible.";
exit;
}
Then build the HTML with a data URI:
php
$message = '
Your Email Title
Here is the content of your email.
';
And send it:
php
<?php
$imagePath = 'path/to/your/image.jpg';
if (file_exists($imagePath) && is_readable($imagePath)) {
$imageData = base64_encode(file_get_contents($imagePath));
} else {
echo "Image file not found or not accessible.";
exit;
}
$message = '
Your Email Title
Here is the content of your email.
';
$headers = [
'MIME-Version: 1.0',
'Content-Type: text/html; charset=UTF-8',
'From: Your Name sender@example.com',
'Cc: Another Name another-email@example.com',
];
if (mail('recipient@example.com', 'Subject of Your Email', $message, implode("\r\n", $headers))) {
echo "Email sent successfully.";
} else {
echo "Failed to send email.";
}
Just keep in mind:
Base64 makes emails larger.
Big emails load slower.
Some servers or clients may reject or block heavy base64 images.
If you’re sending more than a few low‑volume transactional emails, it’s usually better to move to an SMTP library or email API instead of wrestling with mail().
PHPMailer is one of the most popular ways to send HTML email from PHP. It wraps all the boring SMTP details for you and adds things like attachments, authentication, TLS, and better error messages.
Using Composer:
bash
composer require phpmailer/phpmailer
In your PHP script:
php
<?php
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;
require DIR . '/vendor/autoload.php';
Here’s a minimal example that sends an HTML email through an SMTP server (could be your email API provider or your own SMTP):
php
<?php
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;
require DIR . '/vendor/autoload.php';
$mail = new PHPMailer(true);
try {
// SMTP configuration
$mail->isSMTP();
$mail->Host = 'smtp.your-provider.com';
$mail->SMTPAuth = true;
$mail->Username = 'smtp-user';
$mail->Password = 'smtp-password';
$mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS; // or PHPMailer::ENCRYPTION_SMTPS
$mail->Port = 587;
// From / To
$mail->setFrom('sender@example.com', 'Your App');
$mail->addAddress('recipient@example.com', 'John');
// Subject and body
$mail->Subject = 'Your Email Subject';
$mail->isHTML(true);
$mail->Body = '<html>Hi there, we are happy to<br>confirm your booking.<br>Please check the details.</html>';
$mail->AltBody = 'Hi there, we are happy to confirm your booking. Please check the details.';
$mail->send();
echo 'Message has been sent';
} catch (Exception $e) {
echo 'Message could not be sent. Error: ' . $mail->ErrorInfo;
}
Compared to mail(), you get:
Proper SMTP authentication.
TLS/SSL support.
Cleaner error handling.
Support for both plain text and HTML in one message.
PHPMailer makes CID embedded images straightforward.
First, attach the image with a CID:
php
$mail->addEmbeddedImage('path/to/image.jpg', 'image_cid');
Then reference it in your HTML:
php
$mail->isHTML(true);
$mail->Subject = 'Your Booking Confirmation';
$mail->Body = '
Your Booking Confirmation
Hi there, we are happy to
confirm your booking.
Please check the details:
';
The image travels with the email, and most clients will show it without extra clicks.
Adding an attachment is just one line:
php
$attachmentPath = './confirmations/yourbooking.pdf';
if (file_exists($attachmentPath)) {
$mail->addAttachment($attachmentPath, 'yourbooking.pdf');
}
You can call addAttachment() multiple times for more files.
If you need to send the same HTML email to several recipients, build an array and loop:
php
$recipients = [
['email' => 'recipient1@example.com', 'name' => 'John Doe'],
['email' => 'recipient2@example.com', 'name' => 'Jane Doe'],
];
foreach ($recipients as $recipient) {
$mail->addAddress($recipient['email'], $recipient['name']);
}
// Then call $mail->send();
PHPMailer reuses the same SMTP connection, which is much more efficient than calling mail() in a loop.
When you need more than pure SMTP—things like analytics, categories, advanced templates, or better scaling—you usually end up with an email API service.
Many providers ship an official PHP client (SDK). The idea is always similar:
You grab an API key.
You install a PHP package.
You build an Email object.
You send it over HTTPS.
Here’s an example pattern using a Mailtrap‑style PHP client. Exact namespaces and classes may differ for other providers, but the flow will feel familiar.
Typical steps:
Log into your email API provider.
Create an API token.
Store it in .env:
env
MAIL_API_KEY=your_secret_api_key
Install the client and an HTTP client, for example with Symfony HTTP client:
bash
composer require railsware/mailtrap-php symfony/http-client nyholm/psr7
Or Guzzle:
bash
composer require railsware/mailtrap-php guzzlehttp/guzzle php-http/guzzle7-adapter
Example using a Mailtrap PHP client style API:
php
<?php
use Mailtrap\Config;
use Mailtrap\MailtrapClient;
use Mailtrap\Helper\ResponseHelper;
use Mailtrap\EmailHeader\CategoryHeader;
use Mailtrap\EmailHeader\CustomVariableHeader;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Email;
use Symfony\Component\Mime\Header\UnstructuredHeader;
require DIR . '/vendor/autoload.php';
$apiKey = getenv('MAIL_API_KEY');
$mailtrap = new MailtrapClient(new Config($apiKey));
$email = (new Email())
->from(new Address('sender@your-domain.com', 'Mailtrap Test'))
->replyTo(new Address('reply@your-domain.com'))
->to(new Address('email@example.com', 'Jon'))
->priority(Email::PRIORITY_HIGH)
->cc('qa@example.com')
->bcc('dev@example.com')
->subject('Best practices of building HTML emails')
->text('Hey! Learn best practices for building HTML emails.')
->html('
Hey,
Learn the best practices of building HTML emails and play with ready-to-go templates.
Our guide on how to build HTML email is live on the blog.
')
->embed(fopen('https://example.com/logo.svg', 'r'), 'logo', 'image/svg+xml');
// Custom headers and variables
$email->getHeaders()
->addTextHeader('X-Message-Source', 'domain.com')
->add(new UnstructuredHeader('X-Mailer', 'Mailtrap PHP Client'))
->add(new CustomVariableHeader('user_id', '45982'))
->add(new CustomVariableHeader('batch_id', 'PSJ-12'))
->add(new CategoryHeader('Integration Test'));
try {
$response = $mailtrap->sending()->emails()->send($email);
var_dump(ResponseHelper::toArray($response));
} catch (\Exception $e) {
echo 'Caught exception: ' . $e->getMessage();
}
The big difference compared to raw SMTP:
You’re talking to an HTTPS API instead of port 587.
You get structured responses and often better logging.
You can tag emails with categories and custom variables for analytics.
Most email APIs let you build HTML templates in their UI and reference them by ID from PHP. The usual flow:
Create a template in the provider’s dashboard (e.g. “Welcome email”).
Add variables like {{user_name}} inside the template.
Grab the template UUID from the UI.
In PHP, add template headers and pass variables.
Example pattern:
php
<?php
use Mailtrap\Config;
use Mailtrap\MailtrapClient;
use Mailtrap\Helper\ResponseHelper;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Email;
use Mailtrap\EmailHeader\Template\TemplateUuidHeader;
use Mailtrap\EmailHeader\Template\TemplateVariableHeader;
require DIR . '/vendor/autoload.php';
$apiKey = getenv('MAIL_API_KEY');
$mailtrap = new MailtrapClient(new Config($apiKey));
$email = (new Email())
->from(new Address('sender@your-domain.com', 'Your Team'))
->to(new Address('recipient@example.com'));
$email->getHeaders()
->add(new TemplateUuidHeader('your_template_uuid'))
->add(new TemplateVariableHeader('user_name', 'John Doe'))
->add(new TemplateVariableHeader('next_step_link', 'https://example.com/next'))
->add(new TemplateVariableHeader('get_started_link', 'https://example.com/get-started'));
$response = $mailtrap->sending()->emails()->send($email);
var_dump(ResponseHelper::toArray($response));
You don’t build the HTML in PHP at all here; you just send data to a template. This keeps your PHP code light and your HTML email design in one place.
If you’re in the Symfony ecosystem, SymfonyMailer is the built‑in mailer component. It plays nicely with Twig templates and the rest of the framework.
Install:
bash
composer require symfony/mailer
Configure your mailer transport in .env:
env
MAILER_DSN=smtp://api:yourpassword@live.smtp.mailtrap.io:587/?encryption=ssl&auth_mode=login
(Replace with your actual SMTP provider, user, password, and encryption mode.)
Inside a controller:
php
<?php
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Email;
use Symfony\Component\Routing\Annotation\Route;
class MailerController extends AbstractController
{
#[Route('/mailer', name: 'app_mailer')]
public function index(MailerInterface $mailer): Response
{
$email = (new Email())
->from('no-reply@example.com')
->to(new Address('user@example.com'))
->cc('qa@example.com')
->bcc('dev@example.com')
->replyTo('support@example.com')
->subject('Here is the subject')
->text('Plain-text fallback: learn best practices for HTML emails.')
->html('
Hey!
Learn the best practices of building HTML emails and play with ready-to-go templates.
Read the full guide on our blog.
');
$mailer->send($email);
return new Response('Email was sent');
}
}
SymfonyMailer supports several ways to embed images:
php
$email = (new Email())
// Embed from a PHP resource
->embed(fopen('/path/to/newlogo.png', 'r'), 'logo')
// Embed from a local or remote path
->embedFromPath('/path/to/newcover.png', 'new-cover-image')
// Use them as CID in HTML
->html(' ... ...');
You can use Twig templates for your HTML email body instead of hard‑coding HTML in PHP.
Basic idea:
Create a Twig file, e.g. emails/welcome.html.twig.
In that template, write your HTML and use variables like {{ user_name }}.
In PHP, use TemplatedEmail:
php
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
// ...
$email = (new TemplatedEmail())
->from('no-reply@example.com')
->to('user@example.com')
->subject('Welcome!')
->htmlTemplate('emails/welcome.html.twig')
->context([
'user_name' => 'John Doe',
'next_step_link' => 'https://example.com/next',
]);
Twig templates give you CSS inlining, layouts, and good separation between code and markup.
Sending to multiple recipients:
php
$recipients = [
new Address('firstuser@example.com', 'First User'),
new Address('seconduser@example.com', 'Second User'),
];
$email = (new Email())
->from('no-reply@example.com')
->to(...$recipients) // spread operator
->subject('Here is the subject')
->html('
Hey, this email goes to multiple users.
');
$mailer->send($email);
Adding attachments:
php
$email = (new Email())
->from('no-reply@example.com')
->to('user@example.com')
->subject('Your confirmation')
// Local file
->attachFromPath('/path/to/yourconfirmation.pdf')
// External URL (requires allow_url_fopen)
->attachFromPath('http://example.com/path/to/yourconfirmation.doc', 'Your Confirmation', 'application/msword')
// PHP resource
->attach(fopen('/path/to/yourconfirmation.pdf', 'r'));
SymfonyMailer covers most email needs of a PHP app that already lives inside Symfony.
Now, the classic “Contact us” form.
You have an HTML form, maybe some JavaScript validation, and you want:
The user to fill the form.
PHP to validate and sanitize input.
PHP to send a nice HTML email (not just plain text) to you.
PHPMailer fits nicely here.
Frontend: an HTML form with fields like name, email, and message.
Optional: client‑side validation (e.g. validate.js).
Backend: a PHP script that:
Validates required fields.
Validates email format.
Optionally verifies Google reCAPTCHA.
Sends a nicely formatted HTML email with PHPMailer.
A simplified example (without all the error styling) might look like this:
php
<?php
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;
require DIR . '/vendor/autoload.php';
$errors = [];
$errorMessage = '';
$successMessage = '';
$siteKey = 'YOUR_RECAPTCHA_SITE_KEY';
$secret = 'YOUR_RECAPTCHA_SECRET_KEY';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$name = sanitizeInput($_POST['name'] ?? '');
$email = sanitizeInput($_POST['email'] ?? '');
$message = sanitizeInput($_POST['message'] ?? '');
$recaptchaResponse = sanitizeInput($_POST['g-recaptcha-response'] ?? '');
// Verify reCAPTCHA (optional)
$recaptchaUrl = "https://www.google.com/recaptcha/api/siteverify?secret={$secret}&response={$recaptchaResponse}";
$verify = json_decode(file_get_contents($recaptchaUrl));
if (!$verify || empty($verify->success)) {
$errors[] = 'Recaptcha failed';
}
if ($name === '') {
$errors[] = 'Name is empty';
}
if ($email === '') {
$errors[] = 'Email is empty';
} elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$errors[] = 'Email is invalid';
}
if ($message === '') {
$errors[] = 'Message is empty';
}
if (!empty($errors)) {
$errorMessage = "<p style='color:red;'>" . implode('<br/>', $errors) . '</p>';
} else {
$toEmail = 'myemail@example.com';
$emailSubject = 'New email from your contact form';
$mail = new PHPMailer(true);
try {
// SMTP config
$mail->isSMTP();
$mail->Host = 'smtp.your-provider.com';
$mail->SMTPAuth = true;
$mail->Username = 'smtp-user';
$mail->Password = 'smtp-password';
$mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
$mail->Port = 587;
// Email content
$mail->setFrom($email);
$mail->addAddress($toEmail);
$mail->Subject = $emailSubject;
$mail->isHTML(true);
$mail->Body = "
<p><strong>Name:</strong> {$name}</p>
<p><strong>Email:</strong> {$email}</p>
<p><strong>Message:</strong><br>" . nl2br($message) . '</p>
";
$mail->send();
$successMessage = "<p style='color:green;'>Thank you for contacting us :)</p>";
} catch (Exception $e) {
$errorMessage = "<p style='color:red;'>Oops, something went wrong. Please try again later.</p>";
}
}
}
function sanitizeInput(string $input): string
{
$input = trim($input);
$input = stripslashes($input);
return htmlspecialchars($input, ENT_QUOTES, 'UTF-8');
}
And a very simple HTML form:
html
<p>
<label>First Name:</label>
<input name="name" type="text" required />
</p>
<p>
<label>Email Address:</label>
<input name="email" type="email" required />
</p>
<p>
<label>Message:</label>
<textarea name="message" required></textarea>
</p>
<p>
<button class="g-recaptcha"
type="submit"
data-sitekey="<?php echo $siteKey; ?>"
data-callback="onRecaptchaSuccess">
Submit
</button>
</p>
This pattern works nicely for contact forms, quote requests, and other “small” workflows where PHP is your backend and HTML email makes the notification look more professional.
If your project is on WordPress, PHP is still underneath, but the email story is a bit different:
You usually hook into wp_mail().
You might use plugins to plug in SMTP or an email API.
Themes and plugins sometimes add their own HTML templates.
The ideas from this PHP guide still help, but WordPress deserves its own dedicated walkthrough for HTML email sending.
Sending HTML emails in PHP doesn’t have to be painful. For tiny scripts, mail() can work; for anything more serious, PHPMailer, SymfonyMailer, or a PHP email API client will give you more stable delivery, easier templating, and clean support for images, attachments, and multiple recipients.
If your app really cares about speed and inbox placement, the hosting side matters too. Running your PHP mailer on GTHost dedicated servers for email‑heavy PHP applications is a strong fit because you get fast, isolated hardware, clean IPs, and full control over your SMTP or email API stack.