Library to create MIME emails with multiple attachments or HTML alternatives
Version 3.0 - Written by Mark Wilton-Jones updated 9/3/2009

Please see http://www.howtocreate.co.uk/php/ for details
Please see http://www.howtocreate.co.uk/jslibs/termsOfUse.html for terms and conditions of use

WARNING: The base64_encode function uses a system call to encode files. Some operating
systems (eg. AIX) have bugged implementations of base64 encoding and may fail to encode
files larger than about 5kB. There is nothing I can do about that.

To use this library, put the following line in your script before the part that needs it:

This library requires PHP 4+.
It is recommend that PHP versions lower than 5.3.0 should also have the php_imap extension
installed (if not, it may revert to using base64 encoding, which can upset spam filters).

If you are using a version of PHP older than 4.0.2, you will need to use the word-
wrapping library at http://morannon.the-infinite.org/archive/php/word_wrap
and you will need to change word_wrap to wordwrap in that file

If you are embedding images or files in the HTML email body using $filesToEmbed
(for example to use a background image), this function also requires the
mime_content_type function. If PHP has not been compiled to specifically include
that function, you can use my giveMimeType library (just include it before using this
function - it will be automatically detected and used).

If required, you can even use my html2text library to create the plain text body from HTML.
This script will automatically detect if you have included that library, and if you have
not specified a plain text body, it will use html2text (or strip_tags if html2text is not
provided) to generate the plain text body from the HTML one.

Remember that if using variables generated by HTML forms, you will need to use stripslashes
with most PHP installations to remove the escape characters.

WARNING: because this script hooks into the PHP mail function, it is not suitable for
mass/bulk mailing, because the mail function opens a new SMTP connection for each
recipient. You should consider creating your own SMTP sockets for mass/bulk mailing (or
consider leaving us all alone ...).

To send messages

To send a message:
        $senderEmail,        //in valid single email format - see Email list format (qv)
        $replyTo,            //a simple email address or '' if none
        $recipientEmailList, //in Email list format (qv)
        $ccEmailList,        //in Email list format (qv) or '' if none
        $bccEmailList,       //in Email list format (qv) or '' if none - NOT SUPPORTED in PHP < 4.3
        $priority,           //integer 1 (highest) - 5 (lowest) or 0 (default) [not supported by some email clients]
        $subject,            //a normal subject
        $mailBody,           //a plain text message body
        $mailHTML,           //a HTML message body or '' if none
        $filesToSend,        //in Attachment list format (qv) or '' if none
        $filesToEmbed,       //in Attachment list format (qv) or '' if none
        $additional_headers, //in valid message header format - separated by line breaks ("\r\n") - or '' if none
        $wrapText            //true = wrap body text if required, false = do not wrap body text (default = true)
The function returns the boolean result of the call to the mail function.
The function can accept one further parameter after wrapText - Debug - set to true to display the output
instead of sending the email (uses <pre> tags). Return value will be 1.
If the body text is already wrapped (for example, if you are using my html2text library to create it), you
should set $wrapText to false - otherwise set it to true (or leave it because it is optional).
If this script automatically uses html2text (see above), text will be wrapped ($wrapText has no effect).
The mailBody and mailHTML can either be specified as strings (which will leave the recipient client to try
to work out what charset they are using), or can be specified as an array, where the first cell contains the
contents as a string, and the second contains the charset information as a string:
Array('some body text goes here','ISO-8859-1');

Email list format:
You will need to check that all email addresses are valid
Emails can be written in any valid email list format
Usually, each single email address is written as a simple email address:
or in full email format:
    "Person's name" <email@address.com>
To create a list, email addresses can be separated by commas:
    email@address.com, "Person's name" <email@address.com>, anotherEmail@address.com
You can use the prepareEmails function to prepare the email addresses from name/email lists:
    For one email address:
        $emailList = prepareEmails( Array( $recipientName, $recipientEmail ) );
    Or for multiple addresses (this format will also work for a single address if required):
        $emailList = prepareEmails( Array(
            Array( $recipientName, $recipientEmail ),
            Array( $recipientName2, $recipientEmail2 ),
            Array( $recipientName3, $recipientEmail3 )
        ) );

Attachment list format:
If using PHP versions before 4.1.0, you should use HTTP_POST_FILES instead of _FILES
    For no attachments:
        $filesToSend = '';
    For single file upload (it is important to use the uploaded file array entry, not its tmp_name):
        $filesToSend = $_FILES['fileAttachment'];
    For a single static file:
        $filesToSend = '/var/files/filename.txt';
    For multiple file upload - this format will also work with a single file if required:
        $filesToSend = Array( $_FILES['fileAttachment'], $_FILES['fileAttachment2'], etc. );
    For multiple static files - this format will also work with a single file if required:
        $filesToSend = Array( '/var/files/filename.txt', '../www/someFile.exe', 'myFile.php', etc. );
    For a mixture of file uploads and static files:
        $filesToSend = Array( '/var/files/filename.txt', $_FILES['fileAttachment'], 'myFile.php', etc. );
(Hint, content type of files is automatically detected. You can manually specify the content type of a
file by passing it as an associative array containing 'name', 'tmp_name' and 'type' keys, so that the
script thinks it was a file upload.)

To read the HTML body from a static file, use:
    file_get_contents( $filePath )
To read the HTML body from an uploaded file, use:
    file_get_contents( $_FILES['fileAttachment']['tmp_name'] )

To embed images or files in the HTML body you will need to include those files in the $filesToEmbed input.
These files will not appear as attachments unless they are also included in the $filesToSend input.
(Some email clients may still show them as attachments, but there is no requirement for them to.)
The HTML must be modified slightly to use the embedded file, by using '<useEmbed[1]>' as the data source.
The number starts at 1, and files will be used in the order that they are put into the $filesToEmbed input.
    <body background="<useEmbed[1]>">
    <img src="<useEmbed[2]>"><object data="<useEmbed[3]>"></object><img src="<useEmbed[2]>">
[NOTE: as this syntax is in fact invalid HTML, it should never accidentally appear in any HTML email]

function sendMIME( $senderEmail = '', $replyTo = '', $recipientEmail = '', $ccEmailList = '', $bccEmailList = '', $priority = 0, $subject = 'No Subject', $usrMailBody = '', $mailHTML = '', $fileNamesAndPaths = '', $embedNamesAndPaths = '', $extraHeaders = '', $doWrap = true, $debug = false ) {

    //note, it uses regex without the 'u' flag - this is because it generally makes content disappear when replacing,
    //if PHP dislikes certain chars in the string (when PHP mis-detects what encoding the user is using)
    //the 2822 spec is not unicode aware anyway, so it needs ascii-aware pattern matching

    //prepare basic headers
    $extraHeaders = "From: ".preg_replace( "/[\n\r]/", '', $senderEmail )."\r\n".
        ( $replyTo ? ( "Reply-To: ".preg_replace( "/[\n\r]/", '', $replyTo )."\r\n" ) : '' ).
        ( $ccEmailList ? ( "Cc: ".preg_replace( "/[\n\r]/", '', $ccEmailList )."\r\n" ) : '' ).
        ( $bccEmailList ? ( "Bcc: ".preg_replace( "/[\n\r]/", '', $bccEmailList )."\r\n" ) : '' ).
        ( $priority ? ( "X-Priority: ".preg_replace( "/[\n\r]/", '', $priority )."\r\n" ) : '' ).
        "X-Mailer: makeMIME v3.0 in PHP by MTWJ\r\nMime-Version: 1.0\r\n".
        ( $extraHeaders ? ( preg_replace("/\r\n?|\n/","\r\n",preg_replace("/[\r\n]$/",'',$extraHeaders))."\r\n" ) : '' );

    //prepare body
    if( !is_array($usrMailBody) ) {
        $usrMailBody = Array($usrMailBody,'');
    if( !is_array($mailHTML) ) {
        $mailHTML = Array($mailHTML,'');
    //default to no charset
    if( !isset($usrMailBody[1]) ) {
        $usrMailBody[1] = '';
    if( !isset($mailHTML[1]) ) {
        $mailHTML[1] = '';
    $usrMailBody[2] = !$doWrap;
    if( !$usrMailBody[0] && $mailHTML[0] ) {
        //I forbid the use of HTML-only emails
        if( function_exists( 'html2text' ) ) {
            $usrMailBody[0] = html2text( $mailHTML[0] );
            $usrMailBody[1] = 'utf-8';
            $usrMailBody[2] = false;
        } else {
            $usrMailBody[0] = strip_tags( $mailHTML[0] );
            $usrMailBody[1] = $mailHTML[1];
    //use Windows linebreaking (spec requirement, even though it sucks)
    $usrMailBody[0] = preg_replace( "/\r\n?|\n/", "\r\n", $usrMailBody[0] );
    $mailHTML[0] = preg_replace( "/\r\n?|\n/", "\r\n", $mailHTML[0] );
    //do wrapping, and set flowed format if there are any remaining long lines
    if( $doWrap ) {
        $usrMailBody[0] = wordwrap( $usrMailBody[0], 75, "\r\n" );
    $usrMailBody[2] = $usrMailBody[2] && preg_match("/(^|\n)[^\r]{79,}(\r|$)/",$usrMailBody[0]);
    $mailHTML[2] = false;

    //prepare MIME boundaries that do not appear in the message body(s) (or base64 encoded attachments - hence the use of _)
    $MIMEboundary = '0';
    $randChars = Array('A','B','C');
    while( substr_count( $usrMailBody[0], $MIMEboundary ) || substr_count( $mailHTML[0], $MIMEboundary ) ) {
        $MIMEboundary .= $randChars[rand(0,2)];
    $MIMEMboundary = 'MTWJ_mixed_bound_'.$MIMEboundary;
    $MIMEAboundary = 'MTWJ_alt_bound_'.$MIMEboundary;
    $MIMERboundary = 'MTWJ_rel_bound_'.$MIMEboundary;
    $MIMEcid = 'MTWJ_cnid_'.$MIMEboundary.'_';

    //replace embed references with content-ID references
    //naive check first to see if it looks like they are embedding something - doesn't hurt in any case, just saves some time
    if( $embedNamesAndPaths ) {
        $mailHTML = preg_replace( "/<useEmbed\[(\d+)\]>/", 'cid:' . $MIMEcid . '$1@makeMIMEembed', $mailHTML );

    //transfer encoding to allow for 2822's ascii+998char(or 78char) limits
    //there is no risk of the transfer encoding introducing any conflicts with mime boundaries, since _ avoids base64, and QP cannot produce these chars
    if( function_exists('quoted_printable_encode') ) {
        $usrMailBody[0] = quoted_printable_encode($usrMailBody[0]);
        $mailHTML[0] = quoted_printable_encode($mailHTML[0]);
        $usrMailBody[3] = $mailHTML[3] = 'quoted-printable';
    } elseif( function_exists('imap_8bit') ) {
        $usrMailBody[0] = imap_8bit($usrMailBody[0]);
        $mailHTML[0] = imap_8bit($mailHTML[0]);
        $usrMailBody[3] = $mailHTML[3] = 'quoted-printable';
    } else {
        //test for non-ascii - not unicode compatible - that's a good thing here
        //length goes for maximum permissable (998), not recommended (78), to avoid using base64 as long as possible
        $usrMailBody[3] = $mailHTML[3] = '7bit';
        if( preg_match("/[^\x9\xa\xd\x20-\x7e]/",$usrMailBody[0]) || preg_match("/(^|\n)[^\r]{999,}(\r|$)/",$usrMailBody[0]) ) {
            $usrMailBody[0] = rtrim(chunk_split(base64_encode($usrMailBody[0])));
            $usrMailBody[3] = 'base64';
        if( preg_match("/[^\x9\xa\xd\x20-\x7e]/",$mailHTML[0]) || preg_match("/(^|\n)[^\r]{999,}(\r|$)/",$mailHTML[0]) ) {
            $mailHTML[0] = rtrim(chunk_split(base64_encode($mailHTML[0])));
            $mailHTML[3] = 'base64';

    //load and prepare attachments and embeds
    foreach( Array('fileNamesAndPaths','embedNamesAndPaths') as $inclusiontype ) {
        //if they only provide one file, put it in an array as if they provided more than one
        if( !is_array( $$inclusiontype ) || ( isset( ${$inclusiontype}['name'] ) && isset( ${$inclusiontype}['tmp_name'] ) ) ) {
            $$inclusiontype = Array( $$inclusiontype );
            if( !${$inclusiontype}[0] ) {
                unset( ${$inclusiontype}[0] );
        foreach( $$inclusiontype as $key => $fullPath ) { if( $fullPath ) {
            //if it was an upload, use its tmp_name and name, else use the name with and without the path
            if( is_array( $fullPath ) ) {
                $filemime = $fullPath['type'] ? $fullPath['type'] : ( function_exists( 'mime_content_type' ) ? mime_content_type( $fullPath['tmp_name'] ) : ( function_exists( 'ext2mime' ) ? ext2mime( $fullPath['name'] ) : 'application/octet-stream' ) );
                $justFileName = $fullPath['name'];
                $fullPath = $fullPath['tmp_name'];
            } else {
                $filemime = function_exists( 'mime_content_type' ) ? mime_content_type( $fullPath ) : ( function_exists( 'ext2mime' ) ? ext2mime( $fullPath ) : 'application/octet-stream' );
                //use \x5c to avoid having to double escape \\\\ for string and regex (it's easier to read this way)
                $justFileName = preg_replace( "/^.*\//", '', preg_replace( '/\x5c/', '/', $fullPath ) );
            ${$inclusiontype}[$key] = Array(
        } }

    $upgrademsg = 'This is a MIME email - please upgrade to a MIME compatible email client';

    build the body from the various parts, trying to keep as few multipart sections as possible
    (this avoids upsetting some mail clients that see a multipart/mixed and assume there are attachments
    without checking) - algorithm:

    body = headers+textbody
    if( htmlbody ) {
        htmlbody = headers+htmlbody
        if( embeds ) {
            htmlbody = relheaders+relbound+htmlbody+(relbound+headers+embed)*+relboundend
        body = altheaders+(attachments?'':upgrade)+altbound+body+altbound+htmlbody+altboundend
    if( attachments ) {
        body = mixheaders+upgrade+mixbound+body+(mixbound+headers+attachment)*+mixboundend

    Email client bugs relating to parts and boundaries:
    Thunderbird is stupid, and insists that when there are embeds in the HTML body, text/plain must be in the inner-most multipart;
    have to use alternative inside related, not related inside alternative - as if the embeds apply to the plain text as well as HTML.
    It then also needs the non-standard (AFAIK) type="multipart/alternative"; on the related header or it will ignore the HTML body.
    This is the format generated by Outlook Express. Thunderbird's own output matches the format used by this script, and indeed, it
    fails to use the text/plain content with its own output in this case. With the OE format, Thunderbird will then also show any
    embeds as attachments, even when showing the HTML body. Thunderbird is broken, and I do not intend to work around its bugs due to
    the sheer stupidity and messy headers it needs. They can fix the bug instead. https://bugzilla.mozilla.org/show_bug.cgi?id=482198
    If you tell Outlook to display the plain text part, it will display a text version of the HTML part instead.
    If you tell Outlook Express to display the plain text part, it will display the text part, then <P><HR></p> (in visible text),
    then a text version of the HTML part. So cute.
    There is no proper way to work around these Outlook-* bugs, and I don't intend to waste time looking for a hackaround.
    Too bad, so sad.

    $mailBody = sendMIME_getHeaders('text/plain',$usrMailBody[1],$usrMailBody[2],'',$usrMailBody[3]).$usrMailBody[0];
    if( $mailHTML[0] ) {
        $mailHTML[0] = sendMIME_getHeaders('text/html',$mailHTML[1],$mailHTML[2],'',$mailHTML[3]).$mailHTML[0];
        if( isset($embedNamesAndPaths[0]) ) {
            $mailHTML[0] = sendMIME_getHeaders('multipart/related','','',$MIMERboundary).
            $x = 0;
            foreach( $embedNamesAndPaths as $oneFile ) {
                $mailHTML[0] .= '--'.$MIMERboundary."\r\n".sendMIME_getHeaders($oneFile[1],'','','','base64','inline',$oneFile[0],$MIMEcid.(++$x).'@makeMIMEembed').$oneFile[2]."\r\n\r\n";
            $mailHTML[0] .= '--'.$MIMERboundary.'--';
        $mailBody = sendMIME_getHeaders('multipart/alternative','','',$MIMEAboundary).
    if( isset($fileNamesAndPaths[0]) ) {
        $mailBody = sendMIME_getHeaders('multipart/mixed','','',$MIMEMboundary).
        $x = 0;
        foreach( $fileNamesAndPaths as $oneFile ) {
            $mailBody .= '--'.$MIMEMboundary."\r\n".sendMIME_getHeaders($oneFile[1],'','','','base64','attachment',$oneFile[0]).$oneFile[2]."\r\n\r\n";
        $mailBody .= '--'.$MIMEMboundary.'--';
    $mailBody .= "\r\n";

    //now take the first set of headers from the body, whatever they are, and transfer them to the extraHeaders for use in the mail function
    $extraHeaders .= preg_replace("/\r\n\r\n[\w\W]*$/",'',$mailBody);
    $mailBody = preg_replace("/^[\w\W]*?\r\n\r\n/",'',$mailBody);

    //send the email
    if( $debug ) { return print "<pre>".htmlspecialchars("To: ".$recipientEmail."\r\nSubject: $subject\r\n$extraHeaders\r\n\r\n$mailBody")."</pre>";
    } else { return mail($recipientEmail,$subject,$mailBody,$extraHeaders); }


function sendMIME_getHeaders( $cType = '', $charset = '', $format = false, $boundary = '', $cTE = '', $cDisp = '', $filename = '', $cID = '' ) {
    //return headers for a MIME section - this only includes the specified headers, not the associated boundary
    if( !$cType ) { $cType = 'application/octet-stream'; }
    $headout = 'Content-Type: '.$cType.($charset?('; charset="'.$charset.'"'):'').($format?('; format=flowed'):'').($boundary?('; boundary="'.$boundary.'"'):'')."\r\n";
    if( $cDisp ) {
        $headout .= 'Content-Disposition: '.$cDisp.($filename?('; filename="'.$filename.'"'):'')."\r\n";
    if( $cTE ) {
        $headout .= 'Content-Transfer-Encoding: '.$cTE."\r\n";
    if( $cID ) {
        $headout .= 'Content-ID: <'.$cID.">\r\n";
    return $headout."\r\n";

function prepareEmails( $EMaddr ) {
    //with each email name/address pair, prepare it in email format
    if( !is_array( $EMaddr[0] ) ) { $EMaddr = Array( $EMaddr ); }
    $EMstr = ''; foreach( $EMaddr as $key => $emComb ) {
        $EMstr .= ( $EMstr ? ', ' : '' ).( $emComb[0] ? ( '"'.addslashes(preg_replace( "/[\n\r\"]/", '', $emComb[0] )).'" ' ) : '' ).'<'.addslashes(preg_replace( "/[\n\r<>]/", '', $emComb[1] )).">";
    return $EMstr;