Sending HTML Emails with Attachments Using a Multipart Structure
When sending HTML emails in PHP, there are many practical elements to consider, such as multipart structures for text and HTML, file attachments, and Cc/Bcc settings.
You can copy and paste the code on this page and use it as-is.
This guide explains how HTML email delivery works using practical, UTF‑8‑compatible PHP code.
To send an HTML email with attachments in PHP, you need a multipart structure that combines text and HTML, along with proper boundary handling.
Since mb_send_mail() has limitations, you must manually construct a multipart/mixed message.
This page provides working code based on RFC‑compliant methods.
Preparing mamsendmail.php
Prepare the mamsendmail.php file located at the bottom of this page.
Sending the Email
<?php
include(__DIR__.'/mamsendmail.php');
mb_language("Japanese");
mb_internal_encoding("UTF-8"); //Send email in UTF-8
//From
$from="mam <from@mam-mam.net>";
//Recipients
// At least one address must be specified in To or Cc (sending with only Bcc may fail)
// Multiple recipients can be separated by commas (,)
$to= "To Recipient 1 <to1@mam-mam.net>, To Recipient 2 <to2@mam-mam.net>";
$cc= "<cc1@mam-mam.net>, Cc Recipient 2<cc2@mam-mam.net>, cc3@mam-mam.net";
$bcc="Bcc Recipient 1 <bcc1@mam-mam.net>, bcc2@mam-mam.net, <bcc3@mam-mam.net>";
//Subject
$subject="Subject: You can also include long subjects. Half-width characters are allowed. 23784sdarew!#$%&()=-~^|";
//Plain text body (set to empty string if not needed);
$textBody="Title\r\n Body\r\n Test email";
//HTML body (set to empty string if not needed)
$htmlBody="
<html>
<head>
<style>
h3{color:red;font-size:32px;font-weight:bold;}
p{color:black;font-size:20px;}
img{width:100%;max-width:500px;}
</style>
</head>
<body>
<h3>Title</h3>
<p>
This is the message body.<br>
<img src=\"cid:img1\"><br>
<img src=\"cid:img2\"><br>
<img src=\"cid:img3\"><br>
This is the message body.
</p>
</body>
</html>";
//Attachments (set to [] if not needed)
$files=[__DIR__."/../imgs/0001s.jpg", __DIR__."/../imgs/0002s.jpg", __DIR__."/../imgs/0003s.jpg"];
//Send the email
mamSendMail::send(
$from,
$to,$cc,$bcc,
$subject,
$textBody, $htmlBody,
$files
);
mamsendmail.php
<?php
mb_language("Japanese");
mb_internal_encoding("UTF-8"); //Send email in UTF-8
/*
mamSendMail::send(arguments) — UTF‑8 email sending class
Meaning of each argument:
$from(required)
Specify exactly one email address.
例1: $from="from@mam-mam.net";
例2: $from="<from@mam-mam.net>";
例3: $from="Saito <from@mam-mam.net>";
$to,$cc,$bcc
At least one recipient must be included in either $to or $cc
(sending with only Bcc may fail depending on the server).
Multiple addresses can be separated with commas (,).
Example:
$to="Mr. Sato <to1@mam-mam.net>, Mr. Yamada <to2@mam-mam.net>, to3@mam-mam.net, <to4@mam-mam.net>";
If cc or bcc is not needed, set them to an empty string: $cc="";$bcc="";
$subject
Email subject line.
$textBody
Plain‑text email body.
例: $textBody="Test\r\n Body\r\n";
$htmlBody
HTML email body.
Example: $htmlBody="<html><body><h1 style='font-size:20px;'>Test</h1><p>Body</p></body></html>";
Notes:
• If both $textBody and $htmlBody contain content:
-> The email is sent as a multipart message (text + HTML).
• If $textBody is empty and $htmlBody contains content:
-> The email is sent as an HTML‑only message.
• If $textBody contains content and $htmlBody is empty:
-> The email is sent as a plain‑text message.
$files
If needed, specify attachment file paths in an array.
Example:
$files = ["C:/temp/1.jpg", "C:/temp/2.png", "C:/temp/3.png"];
If not needed, specify an empty array or omit the argument:
$files = [];
*/
class mamSendMail{
public static function send(
string $from,
string $to, string $cc, string $bcc,
string $subject,
string $textBody, string $htmlBody,
array $files=[]
){
//Validate attachment files
for($i=0;$i<count($files);$i++){
if(!file_exists($files[$i])){
unset($files[$i]);
$files=array_values($files);
}
}
$fFrom=self::mamExplodeMailAddress($from);
if(count($fFrom)==0){return false;}
if(empty($fFrom[0]["name"])){
$cFrom=$fFrom[0]["add"];
}else{
$cFrom=mb_encode_mimeheader($fFrom[0]["name"],"UTF-8","B","\r\n",10).$fFrom[0]["add"];
}
$fTo=self::mamExplodeMailAddress($to);
//if(count($fTo)==0){return false;}
$cTo="";
for($i=0;$i<count($fTo);$i++){
if(!empty($cTo)){$cTo.=",";}
if(empty($fTo[$i]["name"])){
$cTo.=$fTo[$i]["add"];
}else{
$cTo.=mb_encode_mimeheader($fTo[$i]["name"],"UTF-8").$fTo[$i]["add"];
}
}
$fCc=self::mamExplodeMailAddress($cc);
$cCc="";
for($i=0;$i<count($fCc);$i++){
if(!empty($cCc)){$cCc.=",";}
if(empty($fCc[$i]["name"])){
$cCc.=$fCc[$i]["add"];
}else{
$cCc.=mb_encode_mimeheader($fCc[$i]["name"],"UTF-8").$fCc[$i]["add"];
}
}
$fBcc=self::mamExplodeMailAddress($bcc);
$cBcc="";
for($i=0;$i<count($fBcc);$i++){
if(!empty($cBcc)){$cBcc.=",";}
if(empty($fBcc[$i]["name"])){
$cBcc.=$fBcc[$i]["add"];
}else{
$cBcc.=mb_encode_mimeheader($fBcc[$i]["name"],"UTF-8").$fBcc[$i]["add"];
}
}
$cSubject=mb_encode_mimeheader($subject);
$cHeaders=[
"From"=>$cFrom,
"Reply-To"=>$cFrom,
"Content-Transfer-Encoding"=>"base64",
"X-Mailer"=>"PHP Mailer",
];
if(!empty($textBody)&&!empty($htmlBody)){
//When Text+HTML email
$fHtmlBody=chunk_split(base64_encode($htmlBody));
$fTextBody=chunk_split(base64_encode($textBody));
$fBoundary="b1".uniqid("",true);
if(count($files)==0){
//When there are no attachments
$cHeaders["Content-Type"]='multipart/alternative; boundary="'.$fBoundary.'"';
$cBody=
"--".$fBoundary."\r\n".
"Content-Type: text/plain; charset=UTF-8\r\n".
"Content-Disposition: inline\r\n".
"Content-Transfer-Encoding: base64\r\n".
"\r\n".
$fTextBody.
"\r\n".
"--".$fBoundary."\r\n".
"Content-Type: text/html; charset=UTF-8\r\n".
"Content-Disposition: inline\r\n".
"Content-Transfer-Encoding: base64\r\n".
"\r\n".
$fHtmlBody.
"\r\n".
"--".$fBoundary."--\r\n";
}else{
//When there are attachments
$fBoundary2="b2".uniqid("",true);
$cHeaders["Content-Type"]='multipart/mixed; boundary="'.$fBoundary.'"';
$cBody=
"--".$fBoundary."\r\n".
"Content-Type: multipart/alternative; boundary=\"".$fBoundary2."\"\r\n".
"\r\n".
"--".$fBoundary2."\r\n".
"Content-Type: text/plain; charset=UTF-8\r\n".
"Content-Disposition: inline\r\n".
"Content-Transfer-Encoding: base64\r\n".
"\r\n".
$fTextBody.
"\r\n".
"--".$fBoundary2."\r\n".
"Content-Type: text/html; charset=UTF-8\r\n".
"Content-Disposition: inline\r\n".
"Content-Transfer-Encoding: base64\r\n".
"\r\n".
$fHtmlBody.
"\r\n".
"--".$fBoundary2."--\r\n".
"\r\n".
"--".$fBoundary."\r\n";
for($i=0;$i<count($files);$i++){
if(preg_match('/\.gif$/i',$files[$i])){
$hd="image/gif";
$cd="inline";
}else if(preg_match('/\.jpg$|\.jpeg$/i',$files[$i])){
$hd="image/jpeg";
$cd="inline";
}else if(preg_match('/\.png$/i',$files[$i])){
$hd="image/png";
$cd="inline";
}else{
$hd="application/octet-stream";
$cd="attachment";
}
$cBody.=
"Content-Type: ".$hd."; name=\"".basename($files[$i])."\"\r\n".
"Content-Disposition: ".$cd."; filename=\"".basename($files[$i])."\"\r\n".
"Content-Transfer-Encoding: base64\r\n".
"Content-ID: <img".($i+1).">\r\n".
"Content-Location: ".basename($files[$i])."\r\n".
"\r\n".
chunk_split(base64_encode(file_get_contents($files[$i])));
if($i==(count($files)-1)){
$cBody.="--".$fBoundary."--\r\n";
}else{
$cBody.="--".$fBoundary."\r\n";
}
}
}
}else if(!empty($htmlBody)){
//Only HTML email
if(count($files)==0){
$cHeaders["Content-Type"]='text/html;charset=utf-8';
$cBody=chunk_split(base64_encode($htmlBody));
}else{
$fBoundary="b1".uniqid("",true);
$cHeaders["Content-Type"]='multipart/mixed; boundary="'.$fBoundary.'"';
$fHtmlBody=chunk_split(base64_encode($htmlBody));
$cBody=
"--".$fBoundary."\r\n".
"Content-Type: text/html; charset=UTF-8\r\n".
"Content-Disposition: inline\r\n".
"Content-Transfer-Encoding: base64\r\n".
"\r\n".
$fHtmlBody."\r\n".
"--".$fBoundary."\r\n";
for($i=0;$i<count($files);$i++){
if(preg_match('/\.gif$/i',$files[$i])){
$hd="image/gif";
$cd="inline";
}else if(preg_match('/\.jpg$|\.jpeg$/i',$files[$i])){
$hd="image/jpeg";
$cd="inline";
}else if(preg_match('/\.png$/i',$files[$i])){
$hd="image/png";
$cd="inline";
}else{
$hd="application/octet-stream";
$cd="attachment";
}
$cBody.=
"Content-Type: ".$hd."; name=\"".basename($files[$i])."\"\r\n".
"Content-Disposition: ".$cd."; filename=\"".basename($files[$i])."\"\r\n".
"Content-Transfer-Encoding: base64\r\n".
"Content-ID: <img".($i+1).">\r\n".
"Content-Location: ".basename($files[$i])."\r\n".
"\r\n".
chunk_split(base64_encode(file_get_contents($files[$i])));
if($i==(count($files)-1)){
$cBody.="--".$fBoundary."--\r\n";
}else{
$cBody.="--".$fBoundary."\r\n";
}
}
}
}else{
//Only Text email
if(count($files)==0){
$cHeaders["Content-Type"]='text/plain;charset=utf-8';
$cBody=chunk_split(base64_encode($textBody));
}else{
$fBoundary="b1".uniqid("",true);
$cHeaders["Content-Type"]='multipart/mixed; boundary="'.$fBoundary.'"';
$fTextBody=chunk_split(base64_encode($textBody));
$cBody=
"--".$fBoundary."\r\n".
"Content-Type: text/plain; charset=UTF-8\r\n".
"Content-Disposition: inline\r\n".
"Content-Transfer-Encoding: base64\r\n".
"\r\n".
$fTextBody."\r\n".
"--".$fBoundary."\r\n";
for($i=0;$i<count($files);$i++){
if(preg_match('/\.gif$/i',$files[$i])){
$hd="image/gif";
$cd="inline";
}else if(preg_match('/\.jpg$|\.jpeg$/i',$files[$i])){
$hd="image/jpeg";
$cd="inline";
}else if(preg_match('/\.png$/i',$files[$i])){
$hd="image/png";
$cd="inline";
}else{
$hd="application/octet-stream";
$cd="attachment";
}
$cBody.=
"Content-Type: ".$hd."; name=\"".basename($files[$i])."\"\r\n".
"Content-Disposition: ".$cd."; filename=\"".basename($files[$i])."\"\r\n".
"Content-Transfer-Encoding: base64\r\n".
"Content-ID: <img".($i+1).">\r\n".
"Content-Location: ".basename($files[$i])."\r\n".
"\r\n".
chunk_split(base64_encode(file_get_contents($files[$i])));
if($i==(count($files)-1)){
$cBody.="--".$fBoundary."--\r\n";
}else{
$cBody.="--".$fBoundary."\r\n";
}
}
}
}
if(!empty($cCc)){
$cHeaders["Cc"]=$cCc;
}
if(!empty($cBcc)){
$cHeaders["Bcc"]=$cBcc;
}
mail($cTo,$cSubject,$cBody,$cHeaders);
}
private static function mamExplodeMailAddress(string $add){
$add=trim($add);
if(empty($add)){return [];}
$addresses=explode(",",$add);
$ret=[];
foreach($addresses as $address){
$mail=["name"=>"", "add"=>$address];
$s=mb_strpos($address,"<",0,"UTF-8");
if($s!==false && $s>0){
$e=mb_strpos($address,">",$s,"UTF-8");
if($e!==false && $e>0){
$mail["name"]=trim(mb_substr($address,0,$s));
$mail["add"] =mb_substr($address,$s,$e);
}
}
$ret[]=$mail;
}
return $ret;
}
}
