My *nix world

PHP send html mail with attachment

I've seen this topic placed all over at stackoverflow.com so I guess that people just want to send mail with attachments via PHP 🙂

You may found (incomplete) fragments of answer here and there but very few seem to be curious enough to study the problem from ground-up.

So, what is an email anyway? Well, the short version: it is an form of Internet message. You can track it back somewhere in the middle of 1982 (see RFC 822). The things changed since then and the format of these Internet messages (whether it is a HTML document or just an email message) improved over the time. Although there may exist different versions of that specification I will refer here the RFC 2045 as it contains exactly the specifications of "Format of Internet Message Bodies".

Basically an e-mail message looks like this:

internet-message

The simplest version of it would be a plain-text message which would have the following format/content:

From: my.email@mydomain.org
MIME-Version: 1.0
To: your.mail@your.domain.org
Subject: Very important message
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: 7bit

This is the content of a very important message.

We can clearly see two blocks, one on top of the other (separated by an empty line / a CRLF). The one above is the message header (lines 1-6), the next on (after the empty line) is the message body (line 8). Normally we should declare the format of the content we send, i.e. what is the Content-Type and how is encoded during transfer. If we skip this fields the email agents will assume by default text/plain and respectively 7bit. I just declared these fields in the example above for the rigorousness.

A message can be just a plain text (as the example above) or a mixed content message (like an email that supports both plain-text and HTML format and why not, it may even contain an attachment or two).

Let's analyze few cases!

Plain text with attachment

From: OtherName OtherPerson <your.email@your.domain.org>
To: FirstName LastName <your.email@your.domain.org>
Subject: Less important message
Content-Type: multipart/mixed; boundary="070501020301090304010409"

--070501020301090304010409
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: 7bit

A less important message with a very important attachment.

--070501020301090304010409
Content-Type: image/png; name="Tux.png"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="Tux.png"

iVBORw0KGgoAAAANSUhEUgAAAQkAAAE6CAMAAAAyWFdVAAACRlBMVEX///+DUgTSqFHmsz9b
OgVTNgbetFyCeGTovV7frELIokz24rReTjSPWgTiumRePix0TiLWrliiemSKfmQ5JiBHLx/x
0xHrvAvXrAnAhQfPlAfrxAyteArhtArx1Cq2fgrRmwnivAzHigh0UweXYQWkbQXstAvrzSjl
rAnBiwqlcgrzxAuzeQfXlg31vQy0kwaadwvfpQnZsw2bZgX1zTG5gwntzA7ytgz01UPQpA2I
XwfJjgeRZgqUbAnzzQydagf21jDaowny2izOjgj21Q3YnAj22hR4TgR8WwqwpY+iiVz22kqW
cgtqSwYpHAR0SQS4rZaqfgrJowt9UwWkdwq9nQS/taS0llSQYAfrsxbDmg6rcgWHWAVJMAa1
jDW4o3i6gzC3m1xSRiypeCplQwe0gyBgPgV+YgyjjmHCkiesjlheSgSefkSrjkeXci72xiyK
bCSqlGyjcT/ArJCdd0yceTPGtoTqxiBVOwa1fi+2iiA4JgWunGhsRQVoVDDPlBimm4e8lEec
bS3BnE6YfjReVkSkhDythDqSglzy0oyibiSVazGCcjwMDAwUFBMcHBssLCw0NDQKBgQkJCRE
REQ8PDxQUFBsbGvMzMzk5OR0dHTd3dysrKzs7OyDg4S1tLNcXFycnJxkZGSUlJS+vbz09PRK
Skx8fHyNjYzExMSkpKQCAgRWVlTU1NT+/vy2jAgCAi/CkwushAlALAcqIgRcQwlBMg==
--070501020301090304010409--

Here the header is between lines 1-4 and the body is between lines 6-28. Note that in these header I have fewer fields (eg. I skipped the MIME-Version and the Content-Transfer-Encoding). Some of them will be assumed by the email agent by default, some of them can't. For example if you miss the From field who could possibly guess from who comes the message? Likewise for the To field.

In this case our message contains a mixed content: a text/plain on the one hand and a image/png on the other hand. In such case we have to declare the content type otherwise the email agent will not know how to display the message content (I mean we would like to see an image instead of some strange characters, don't we?).

This is done by specifying the content type followed by other parameter called boundary which should have a unique value (that under no circumstances may be part of your image/body/message/whatever. If that string appears in the message the email agent will regard it as a boundary separator. Capisci?

[x] The section (entity) encapsulated within boundaries should be prefixed by the boundary opening tag and by the boundary closing tag. The opening boundary tag is prefixed by the "--" plus the boundary (unique) string and the closing starts like earlier but should also be suffixed with two dashes (two minus ASCII characters).

So right after the header we leave one line empty (CRLF, ie. ASCII 13+ASCII 10) and then insert the boundary separator followed immediately by the entity content type. After this declaration we should leave one line empty followed by our message. When the message is done we leave one line empty after it. If there are more other sections to add then we repeat these steps (see [x] mark above). If there is no other entity to include then we end by inserting the boundary closing tag (ie. exactly the same thing as opening boundary but we add two more "--" at the end).

In our case we have one more section (the image) so we start by leaving one line empty then repeating the steps from [x] mark up till here. Note that the image (like any other attachments should be converted to base-64 otherwise that entity isn't recognized or event the whole message might suffer!

As I've said, when there is no more data to insert we just close the multipart/mixed boundary opened right after the message header.

Note that we've used the same value for boundary (and that's not just a fad) !

Mixed message only (plain-text and HTML)

From: FirstName LastName <your.email@your.domain.org>
MIME-Version: 1.0
To: FirstName LastName <your.email@your.domain.org>
Subject: Mixed martial arts
Content-Type: multipart/alternative; boundary="------------050300020304030800070400"

--------------050300020304030800070400
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: 7bit

A mixed message is like mixed artial marts.

--------------050300020304030800070400
Content-Type: text/html; charset=utf-8
Content-Transfer-Encoding: 7bit

<html>
  <head>
    <meta http-equiv="content-type" content="text/html; charset=utf-8">
  </head>
    A mixed message is like mixed artial marts.
  </body>
</html>

--------------050300020304030800070400--

In this case we sent an email that contains just the following text: "A mixed message is like mixed artial marts."

Because we want it to be displayed both, in HTML format when the email browser can handle it and in plain-text format when don't, we actually create the message by inserting two different entities, each with its own format, although they both represent the same part of the message: the body. In such cases with have a entity with two (or more) alternative content types.

We declare the whole message as having the format "multipart/alternative" and then insert each alternative separated by the boundary.

Note that the same logic/steps as above is valid here, except that we deal with the same entity that has two or more alternative representations.

Mixed message with attachment

From: FirstName LastName <your.email@your.domain.org>
To: FirstName LastName <your.email@your.domain.org>
Subject: I have no ideea what subject to use
X-Priority: 1 (Highest)
Content-Type: multipart/mixed; boundary="------------000009060109090609080008"

--------------000009060109090609080008
Content-Type: multipart/alternative; boundary="------------030402080203050305040505"

--------------030402080203050305040505
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: 7bit

This is just another email message that supports both, the HTML format 
and obviously the plain-text format.
Furthermore we add the same image 'cause we love the Tux - the Linux mascot.
Btw: I set the highest priority to this message, see the header field ;-)

--------------030402080203050305040505
Content-Type: text/html; charset=utf-8
Content-Transfer-Encoding: 7bit

<html>
  <head>
  </head>
  <body bgcolor="#FFFFFF" text="#000000">
    This is just another email message that supports both, the HTML
    format and obviously the plain-text format.<br>
    Furthermore we add the same image 'cause we love the Tux - the Linux
    mascot.<br>
    Btw: I set the highest priority to this message, see the header
    field ;-)<br>
  </body>
</html>

--------------030402080203050305040505--

--------------000009060109090609080008
Content-Type: image/svg+xml; name="Tux.svg"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="Tux.svg"

PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+
CjxzdmcgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50
YXgtbnMjIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTAw
JSIgd2lkdGg9IjEwMCUiIHZlcnNpb249IjEuMSIgeG1sbnM6Y2M9Imh0dHA6Ly9jcmVhdGl2
ZWNvbW1vbnMub3JnL25zIyIgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50
cy8xLjEvIiB2aWV3Qm94PSIwIDAgMzQ5LjQ2ODgzIDQwNS4xMjI3MiI+CiA8dGl0bGU+VHV4
PC90aXRsZT4KIDxkZXNjPkZvciBtb3JlIGluZm9ybWF0aW9uIHNlZTogaHR0cDovL2NvbW1v
bnMud2lraW1lZGlhLm9yZy93aWtpL0ltYWdlOlR1eC5zdmc8L2Rlc2M+CiA8cmFkaWFsR3Jh
ZGllbnQgaWQ9ImFnIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgY3k9==
--------------000009060109090609080008--

This is probably the message that will help you better the email message format because it contains both, an alternative representation of the same content plus an attachment.

Please note that the boundary used to mark the beginning and the end of the multiparted/mixed content differs from the boundary used for the multiparted/alternative content (eg. line 5 vs. line 8). The mixed boundary is used to separate one entity from the other. At line 7 we insert the body message, at line 38 we insert the attachment content. The alternative boundary on the other hand is used to separate alternative contents within the same entity (namely body message). For instance at line 10 we use it because right below that line we insert an alternative content type of the current entity (ie. body message in plain-text format). Then the same at line 19, when we insert the HTML format of the same body message. Nonetheless when we don't have any other alternative representation of that entity we just close the alternative boundary tag (see line 36).

When we don't have any other entity to insert in our message we insert (as the last line) the closing tag of the multiparted/mixed boundary. If for instance we would had one attachment more to insert then after the first attachment we would insert a new line then a new opening tag of multiparted/mixed boundary and the attachment section as the one between lines 38-51.

A working example of PHP send html mail with attachment is presented below:

/**
 * Send email with attachements (optional) formated as described by RFC2045
 *
 * @param string $from
 *        	The originator email address
 * @param string $to
 *        	The recipient email address
 * @param string $subject
 *        	The message subject (recommended max 78 chars)
 * @param string $body
 *        	A plain-text/HTML formated message body
 * @param array $attachments
 *        	An array of attachments that has the same structure as the $_FILES global variable
 * @see http://www.ietf.org/rfc/rfc2045.txt
 */
function sendMail($from, $to, $subject, $body, $attachments = null) {
	$get_content_type = function ($mime) {
		return "Content-Type: $mime;";
	};
	$get_transfer_encoding = function ($encoding) {
		return "Content-Transfer-Encoding: $encoding";
	};
	$eol = "\r\n";
	$deol = $eol . $eol;
	
	$random_hash = md5 ( date ( 'r', time () ) );
	
	$boundary_signature = 'PHP';
	$boundary_prefix = "--$boundary_signature";
	$boundary_alt = "$boundary_prefix-alt-$random_hash";
	$boundary_mixed = "$boundary_prefix-mixed-$random_hash";
	$charset = 'utf-8';
	$charset_tag = 'charset="' . $charset . '";';
	$transfer_encoding = $get_transfer_encoding ( '7bit' );
	$html_mime = 'text/html';
	
	$headers = 'MIME-Version: 1.0' . $eol;
	$headers .= "From: $from" . $eol;
	$headers .= "To: $from" . $eol;
	$headers .= "Subject: $subject" . $eol;
	$headers .= "X-Priority: 2 (High)" . $eol;
	$headers .= $get_content_type ( 'multipart/mixed' ) . "boundary=\"$boundary_mixed\"" . $deol;
	
	ob_start ();
	
	// \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
	// -- BODY entity --
	echo "--$boundary_mixed" . $eol;
	echo $get_content_type ( 'multipart/alternative' ), "$charset_tag boundary=\"$boundary_alt\"", $deol;
	
	// ****************************************************************
	// -- as PLAIN-TEXT alternative --
	echo "--$boundary_alt", $eol;
	echo $get_content_type ( 'text/plain' ), $charset_tag, $eol;
	echo $transfer_encoding, $deol;
	
	echo strip_tags ( $body ), $deol;
	
	// ****************************************************************
	// -- as HTML alternative --
	echo "--$boundary_alt", $eol;
	echo $get_content_type ( $html_mime ), $charset_tag, $eol;
	echo $transfer_encoding, $deol;
	
	echo '<html>', $eol;
	echo '<head><meta http-equiv="content-type" content="', $html_mime, '";', $charset_tag, '></head>', $eol;
	echo '<body>', $eol;
	echo str_replace ( array (
			$eol,
			PHP_EOL 
	), '<br>', $body ), $eol;
	echo '</body></html>', $deol;
	
	echo "--$boundary_alt--", $deol;
	// \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
	
	// \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
	// -- ATTACHMENTS entities --
	
	if (is_array ( $attachments )) {
		$finfo = finfo_open ( FILEINFO_MIME_TYPE );
		foreach ( $attachments as $key => $file_item )
			if (is_array ( $file_item ) && file_exists ( $file_item ['tmp_name'] )) {
				echo "--$boundary_mixed", $eol;
				$name = $file_item ['name'];
				$filename = $file_item ['tmp_name'];
				$mime_type = empty ( $file_item ['type'] ) ? finfo_file ( $finfo, $filename ) : $file_item ['type'];
				
				(false == $mime_type) && $mime_type = 'application/octet-stream';
				
				echo $get_content_type ( $mime_type ), 'name="', $name, '"', $eol;
				echo $get_transfer_encoding ( 'base64' ), $eol;
				echo 'Content-Disposition: attachment; filename="', $name, '"', $deol;
				
				echo chunk_split ( base64_encode ( file_get_contents ( $filename ) ) ), $eol;
			}
		finfo_close ( $finfo );
	}
	
	echo "--$boundary_mixed--", $deol;
	
	$message = ob_get_clean ();
	
	file_put_contents ( 'debug.mail', $message ); // comment this line if you don't want to debug the message
	// send the email; note that mail function requires proper configuration in php.ini
	return @mail ( $to, $subject, $message, $headers );
}

@Edit: I should probably add that if you connect via telnet command at port 25 (SMTP) of your email server and type exactly a message like any of those presented above (+ end the message by pressing Ctrl+Z) then the server will eat-up the message, will parse it and if it's ok then it will deliver it as it would be written and sent via a normal email client. So you can turn telnet in a little email client. Actually I'm using telnet even for querying SQL databases or for getting/posting data to some HTML servers so, you see, telnet is kind of a swiss army knife.

Now, if you think that this article was interesting don't forget to rate it. It shows me that you care and thus I will continue write about these things.

The following two tabs change content below.
PHP send html mail with attachment

Eugen Mihailescu

Founder/programmer/one-man-show at Cubique Software
Always looking to learn more about *nix world, about the fundamental concepts of math, physics, electronics. I am also passionate about programming, database and systems administration. 16+ yrs experience in software development, designing enterprise systems, IT support and troubleshooting.
PHP send html mail with attachment

Latest posts by Eugen Mihailescu (see all)

Tagged on: ,

Leave a Reply

Your email address will not be published. Required fields are marked *