Email in PHP: Composing Messages, Alternatives, and Attachments
Understand at a working level how a PHP application composes an email with HTML and plain-text alternative bodies plus attachments, and know where the deeper deliverability mechanics (SMTP, transactional providers, SPF/DKIM/DMARC, delivery failures) are covered in full.
Sending email from PHP has two distinct concerns: composing the message itself (subject, HTML body, a plain-text fallback, any attachments, and templated content that plugs real data into a reusable layout), and getting that message actually delivered to an inbox (authenticated SMTP, a transactional provider, and the SPF/DKIM/DMARC trio that keeps it out of spam). This lesson is a short, focused pass on the composition side, since the delivery mechanics -- SMTP relays, transactional providers, SPF, DKIM, DMARC, and handling delivery failures/bounces -- are already covered in full depth in the 'Email' lesson (Sending Email from PHP: SMTP, Providers, Deliverability, and Queues); read that lesson for everything beyond what a message itself looks like.
HTML and plain-text alternative bodies
A well-formed transactional email is sent as a multipart/alternative MIME message containing two versions of the same content: an HTML body for clients that render it, and a plain-text body as a fallback for clients or accessibility tools that don't (or for a recipient who has HTML rendering disabled). The email client picks whichever version it prefers to display -- it is not two separate emails, it's one message with both representations attached, and a receiving mail server or spam filter also uses the presence (or conspicuous absence) of a reasonable plain-text alternative as one small deliverability signal.
Both PHPMailer ($mail->Body = $html; $mail->AltBody = $plainText; with isHTML(true)) and Symfony Mailer ((new Email())->html($htmlBody)->text($plainTextBody)) build this multipart structure for you -- the correct MIME boundary headers, content-type declarations, and encoding are handled by the library rather than hand-assembled, which is exactly the kind of detail that's easy to get subtly wrong (and that spam filters penalize) when composing raw MIME by hand.
Attachments and templated content
Attachments are added through the same libraries with a file path or an in-memory string plus a filename and MIME type -- $mail->addAttachment('/path/invoice.pdf', 'Invoice.pdf') in PHPMailer, or (new Email())->attachFromPath($path, 'Invoice.pdf', 'application/pdf') in Symfony Mailer, both of which handle the base64 encoding and multipart/mixed structure that a real attachment requires alongside an HTML/plain-text alternative body.
For the message content itself, the same template discipline covered for web pages applies directly to email: render the HTML body from a PHP/Twig/Blade template with a reusable layout (a consistent header/footer/branding wrapping per-email content) and escape every dynamic value with htmlspecialchars() exactly as in a web page template -- an email template is still producing HTML that an email client's renderer parses, and unescaped user data in an email body is exactly as exploitable (in clients that execute embedded scripts, or that render the message in a webview) as it would be on a web page.
• multipart/alternative: HTML body + plain-text AltBody, one message, client picks which to render. • Attachments: added as a file path or in-memory content plus filename/MIME type -- library handles base64 + multipart/mixed structure. • Email HTML templates need the same htmlspecialchars() escaping discipline as any other PHP template -- see the PHP Templates lesson. • Delivery mechanics -- SMTP, transactional providers, SPF/DKIM/DMARC, bounce handling -- are covered in full in the Email lesson, not repeated here.
Code Examples
Body carries the HTML version with the customer name escaped exactly as it would be in any PHP template, AltBody carries a plain-text equivalent for clients that prefer it, and addAttachment() attaches the PDF -- all three concerns (HTML, plain-text fallback, attachment) are handled through PHPMailer's API rather than hand-built MIME. Everything about the SMTP connection setup here (host, auth, STARTTLS) is exactly what the deeper Email lesson covers in full, including why a transactional provider is generally preferred over a self-managed SMTP relay in production.
$mail->Body = ...; $mail->AltBody = ...; $mail->addAttachment(...);
Common mistake: Setting only $mail->Body without $mail->AltBody sends an HTML-only message with no plain-text alternative, which some spam filters and text-only clients treat as a deliverability red flag.
<?php
declare(strict_types=1);
use PHPMailer\PHPMailer\PHPMailer;
function sendReceiptEmail(string $toEmail, string $customerName, float $amount, string $invoicePdfPath): void
{
$mail = new PHPMailer(true);
$mail->isSMTP();
$mail->Host = 'smtp.example-provider.com';
$mail->SMTPAuth = true;
$mail->Username = getenv('SMTP_USERNAME');
$mail->Password = getenv('SMTP_PASSWORD');
$mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
$mail->Port = 587;
$mail->setFrom('receipts@example.com', 'Example Store');
$mail->addAddress($toEmail);
$mail->Subject = 'Your receipt';
$safeName = htmlspecialchars($customerName, ENT_QUOTES, 'UTF-8');
$mail->isHTML(true);
$mail->Body = "<p>Hi {$safeName},</p><p>Thanks for your payment of \$" . number_format($amount, 2) . ".</p>";
$mail->AltBody = "Hi {$customerName},\nThanks for your payment of \$" . number_format($amount, 2) . ".";
$mail->addAttachment($invoicePdfPath, 'Receipt.pdf');
$mail->send();
}Important Rules
Always send both an HTML body and a plain-text AltBody/text() alternative -- never HTML-only.
Escape dynamic values in an email's HTML body with htmlspecialchars() exactly as in any web page template.
Use a library (PHPMailer or Symfony Mailer) to build multipart/alternative and multipart/mixed (attachment) structure -- do not hand-assemble MIME.
For SMTP setup, transactional providers, SPF/DKIM/DMARC, and delivery-failure handling, see the dedicated Email lesson -- this lesson covers composition only.
Where this shows up
Every transactional email a real application sends -- password resets, order receipts, notifications -- is composed exactly this way: an HTML body from a template, a plain-text fallback, and occasionally an attachment, handed to PHPMailer or Symfony Mailer rather than PHP's bare mail(). The composition concerns here are small and mechanical; the deliverability concerns (getting that well-formed message to actually land in an inbox instead of spam) are the harder, deeper problem covered fully in the Email lesson.
$mail->Body = '<p>Hi ' . $customerName . ', your total is $' . $amount . '</p>';
$mail->Body = '<p>Hi ' . htmlspecialchars($customerName, ENT_QUOTES, 'UTF-8') . ', your total is $' . number_format($amount, 2) . '</p>';
$customerName is interpolated into the HTML body completely unescaped -- if it ever originates from user-editable profile data, an HTML/script payload in that field would render as live markup in any email client (or webview) that executes it, exactly as it would in an unescaped web page template.
[multiple-choice] Why does a well-formed transactional email include both an HTML Body and a plain-text AltBody rather than only HTML? A. PHP requires both to be set or send() throws an exception B. It's a single multipart/alternative message offering both representations -- the client renders whichever it prefers, and some spam filters treat the total absence of a plain-text alternative as a deliverability signal C. AltBody is required to attach a PDF file D. HTML-only emails are rejected by all mail servers
B. It's a single multipart/alternative message offering both representations -- the client renders whichever it prefers, and some spam filters treat the total absence of a plain-text alternative as a deliverability signal — A multipart/alternative message carries both an HTML and a plain-text version of the same content in one email; the receiving client picks which to display. Omitting the plain-text alternative entirely is one of several small signals spam filtering can weigh against a message's legitimacy, on top of simply degrading the experience for text-only clients.