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.
Major | Minor | Full type | Data |
---|---|---|---|
Text documents | |||
text | plain | text/plain | Plain text documents |
text | html | text/html | HTML documents |
text | csv | text/csv | Comma-separated data files |
Images | |||
image | jpeg | image/jpeg | JPEG-formatted images |
image | png | image/png | PNG-formatted images |
Application-specific types | |||
application | application/pdf | Portable Document Format (PDF) | |
application | zip | application/zip | PKZIP compressed archives |
application | msword | application/msword | MS Word documents |
Types with multiple components | |||
multipart | form-data | multipart/form-data | Web forms with uploaded files |
multipart | mixed | multipart/mixed | Messages with many types of component |
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
, calledContent-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:
quoted-printable
: a discriminate encoding, which allows standard text through without encoding, but translates non-standard characters into their hexadecimal ordinal values;base64
: an indiscriminate encoding, which takes the whole stream of data as one number, and translates it a chunk at a time, three bytes translating into a 4-character block.
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 ATTACHpublic 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 messagepublic function send($from, $to, $subject) { $this->endMessage($from); return mail($to, $subject, $this->output, $this->headers); }// writePartHeader: Helper function to add part headersprivate 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 headersprivate 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