Building Complex Emails with PHP

Display mode

Back to Articles

One of the first things that a newcomer to PHP learns is how to send a simple email: using the mail function to send a few paragraphs of text to an email address. This is an easy and functional way to send status messages and other small emails, but there are disadvantages:

Unformatted text:
With plain text emails, only the most rudimentary structure can be given to a message; there's no inherent way to insert a heading or a bullet-point list. If HTML were allowed in the email, for example, this formatting could be provided to the message.
No attachments:
Because the email is a single plain-text message, there is no way to provide additional documents or other files in the body of the message. A compromise is to place the files in question on the public Web, and provide links to the files, but this also compromises the security of the documents.

These problems are not just limitations of PHP's mailing routines: they are limitations of the email transport mechanism. In order to get around them, a devious scheme was standardised in the 1990s.

Multipurpose Internet Mail Extensions

The MIME standard was designed to work inside the existing email transport system; as such, it doesn't need any special connection methods, and no complicated networking is required on the part of the developer. Instead, MIME allows for multi-part messages by inserting all the parts into a plain-text email, and separating the parts by a boundary.

Structure: Basic boundaries

Text outside the boundary (part #0)
--BOUNDARY
Part #1
--BOUNDARY
Part #2
--BOUNDARY--

As can be seen in the example above, two hyphens precede all instances of the boundary, and one boundary forms the end of one part and the start of the next. The end of the last part is denoted by two hyphens after the boundary closing that part.

The basic structure outlined above allows for the separation of parts, but all it can provide is multiple plain-text messages combined into one. To allow for more complex information to be encoded, headers must be provided in association with each part.

MIME Headers

An issue arises with the boundary structure: how is the email reading client to know which lines denote the boundary for a part, and which are simply part of the message? The client can be informed of which boundary is being used by providing a header for the message in total. Headers are often used to denote the originator of the message, the software version of the sending server, and other such information which may be pertinent to the client; the MIME boundary can be added to this.

Headers: MIME message boundary

From: "Imran Nazar" <tf@oopsilon.com>
MIME-Version: 1.0
Content-type: multipart/mixed; boundary="BOUNDARY"
<blank line>
Message body

The Content-type header tells the email client what kind of data is provided in the message; the text following Content-type is known as a MIME type. The concept of MIME types has been extended for use beyond email, and is now commonly provided by Web and file servers in response to a request for data.

The MIME type provided with a chunk of data can be used to identify the data in question. There are various classes of data that have MIME types associated with them, and subdefinitions for each class. The class and subclass of data are given in major/minor format; a few examples are provided below.

MajorMinorFull typeData
Text documents
textplaintext/plainPlain text documents
texthtmltext/htmlHTML documents
textcsvtext/csvComma-separated data files
Images
imagejpegimage/jpegJPEG-formatted images
imagepngimage/pngPNG-formatted images
Application-specific types
applicationpdfapplication/pdfPortable Document Format (PDF)
applicationzipapplication/zipPKZIP compressed archives
applicationmswordapplication/mswordMS Word documents
Types with multiple components
multipartform-datamultipart/form-dataWeb forms with uploaded files
multipartmixedmultipart/mixedMessages with many types of component
Table 1: Sample MIME types

As can be seen above, the multipart/mixed MIME type tells the email reader that each part of the message can be of a different type. Just as with the message, each part can have a header and a body. Taking this into account, a fuller MIME-compliant message can be built.

Multipart emails with headers

This is part #0.
--BOUNDARY
Content-type: text/html

This is part #1.
--BOUNDARY
Content-type: text/csv

id,content,date
"1","This is part #2.","2008-08-10"
--BOUNDARY--

Attachments and Content Headers

We've seen how to put multiple types of message into one email, but this is not sufficient for attaching documents and other files to an email message. There are two major problems with inserting documents into an email:

Naming:
As can be seen above, files can be inserted into an email as a MIME part, but they are not given a filename, and are not treated as attachments. This problem is solved by inserting another header along with the part's Content-type, called Content-disposition.
Encoding:
An email message has to be readable in its entirety by any mailserver that happens across it. Because mailservers may run in many places, under many languages and character sets, a binary data file is not guaranteed to make it to the destination intact: it has to be encoded into a more basic character set, and the email client has to be told how to decode the resultant email part. This is done with a third header, called Content-transfer-encoding.

The Content-disposition attached to a message part can be one of two types: inline, meaning this type is to be shown as part of the email, and attachment, which denotes a file attached for download. If it's an attachment, a filename can be provided as a parameter to the Content-disposition header. Using this header, we can make the CSV data file in the above example into an attachment:

Multipart emails with disposition

This is part #0.
--BOUNDARY
Content-type: text/html
Content-disposition: inline

This is part #1.
--BOUNDARY
Content-type: text/csv
Content-disposition: attachment; filename="data.csv"

id,content,date
"1","This is part #2.","2008-08-10"
--BOUNDARY--

This takes care of the first problem with attaching files to an email, but the second remains: encoding the attachment into a transferable format. There are two major encoding methods allowed by the MIME standard:

The base64 encoding is generally easier to produce, since the quoted-printable encoding requires specialised translation tables. With base64, the data is broken up into 48-byte "lines", and encoded into 64-character lines before insertion into the email.

Once an encoding has been picked, it should be provided in the header for the message part, as shown in the below example.

Attaching a binary file in base64 encoding

Content-type: image/gif
Content-disposition: attachment; filename="text-icon.gif"
Content-transfer-encoding: base64

R0lGODlhIAAgAKIEAISEhMbGxgAAAP///////wAAAAAAAAAAACH5BAEAAAQALAAA
AAAgACAAAAOaSKoi08/BKeW6Cgyg+e7gJwICRmjOM6hs6q5kUF7o+rZ2vgkypq3A
oHA4kPVoxCTROFv8lNAir5mxNa7ESorpi0a5yMg15QU7vVBzFZ1Un9jtaVeMRbuf
8OA9P9zTx4CAK358QH6BiIJSR2eFhnJhiZJbkI2Oi1Rvf5N1hI6ehYeKZZVrl6Jj
bKB8q3luJwGxsrO0taUXnLkXCQA7

Now we have all the pieces of the puzzle: the ability to create an email message with multiple parts, and a way to encode and attach files to the email. It's just a matter of implementation.

Using PHP to send MIME-compliant emails

With the information above, implementation is no issue. The only problem presented is how to define the MIME type of an arbitrary attachment. Fortunately, UNIX systems provide the file command, which can read any file and work out the MIME type of its contents. On a Windows server, no such analogue exists, but it is possible to obtain file through Microsoft Services for Unix, or UnxUtils.

A MIME-compliant email solution is provided below, making use of this tactic and the information presented in this article.

mimemail.php: Mail-building class for PHP

define('MIMEMAIL_HTML', 1);
define('MIMEMAIL_ATTACH', 2);
define('MIMEMAIL_TEXT', 3);

class MIMEMail
{
    private $plaintext;
    private $output;
    private $headers;
    private $boundary;

    public function __construct()
    {
        $this->output = '';
        $this->headers = '';
        $this->boundary = md5(microtime());
        $this->plaintext = 0;
    }

    // add: Add a part to the email
    // Parameters: type (Constant) - MIMEMAIL_TEXT, MIMEMAIL_HTML, MIMEMAIL_ATTACH
    //             name (String)   - Contents of email part if TEXT or HTML
    //                             - Attached name of file if ATTACH
    //             value (String)  - Source name of file if ATTACH
    public function add($type, $name, $value='')
    {
        switch($type)
        {
            case MIMEMAIL_TEXT:
                $this->plaintext = (strlen($this->output))?0:1;
                $this->output = "{$name}\r\n" . $this->output;
                break;

            case MIMEMAIL_HTML:
                $this->plaintext = 0;
                $this->writePartHeader($type, "text/html");
                $this->output .= "{$name}\r\n";
                break;

            case MIMEMAIL_ATTACH:
                $this->plaintext = 0;
                if(is_file($value))
                {
		    // If the file exists, get its MIME type from `file`
		    // NOTE: This will only work on systems which provide `file`: Unix, Windows/SFU
                    $mime = trim(exec('file -bi '.escapeshellarg($value)));
                    if($mime) $this->writePartHeader($type, $name, $mime);
                    else $this->writePartHeader($type, $name);

                    $b64 = base64_encode(file_get_contents($value));

                    // Cut up the encoded file into 64-character pieces
                    $i = 0;
                    while($i < strlen($b64))
                    {
                        $this->output .= substr($b64, $i, 64);
                        $this->output .= "\r\n";
                        $i += 64;
                    }
                }
                break;
        }
    }

    // addHeader: Provide additional message headers (Cc, Bcc)
    public function addHeader($name, $value)
    {
        $this->headers .= "{$name}: {$value}\r\n";
    }

    // send: Complete and send the message
    public function send($from, $to, $subject)
    {
        $this->endMessage($from);
        return mail($to, $subject, $this->output, $this->headers);
    }

    // writePartHeader: Helper function to add part headers
    private function writePartHeader($type, $name, $mime='application/octet-stream')
    {
        $this->output .= "--{$this->boundary}\r\n";
        switch($type)
        {
            case MIMEMAIL_HTML:
                $this->output .= "Content-type: {$name}; charset=\"iso8859-1\"\r\n";
                break;

            case MIMEMAIL_ATTACH:
                $this->output .= "Content-type: {$mime}\r\n";
                $this->output .= "Content-disposition: attachment; filename=\"{$name}\"\r\n";
                $this->output .= "Content-transfer-encoding: base64\r\n";
                break;
        }

        $this->output .= "\r\n";
    }

    // endMessage: Helper function to build message headers
    private function endMessage($from)
    {
        if(!$this->plaintext)
        {
            $this->output .= "--{$this->boundary}--\r\n";

            $this->headers .= "MIME-Version: 1.0\r\n";
            $this->headers .= "Content-type: multipart/mixed; boundary=\"{$this->boundary}\"\r\n";
            $this->headers .= "Content-length: ".strlen($this->output)."\r\n";
        }
        
        $this->headers .= "From: {$from}\r\n";
        $this->headers .= "X-Mailer: MIME-Mail v0.03, 20070419\r\n\r\n";
    }
}

Example usage of mimemail

include('mimemail.php');

$m = new MIMEMail();

// Provide the message body
$m->add(MIMEMAIL_TEXT, 'An example email message.');

// Attach file 'icons/txt.gif', and call it 'text-icon.gif' in the email
$m->add(MIMEMAIL_ATTACH, 'text-icon.gif', '/var/www/icons/txt.gif');

// Send to the author
$m->send('noreply@oopsilon.com', '"Imran Nazar" <tf@oopsilon.com>', 'Test message');

Download the script: mimemail.php

Imran Nazar <tf@oopsilon.com>, 2008