MIME
MIME is the format an email or a multipart HTTP request travels in: headers, a body, and possibly nested
parts with their own headers. pop-mime reads that format into objects and writes it back out.
pop-mail and pop-http both build on it, so most applications meet it indirectly — you reach for it
directly when you have a raw message in hand, or need a part shaped in a way the higher-level component
does not offer.
composer require popphp/pop-mime
Parsing a MIME Message#
Message::parseMessage() takes the raw text and returns a Pop\Mime\Message with its headers and parts
already broken out:
use Pop\Mime\Message;
$raw = "Subject: Hello World\r\nTo: test@test.com\r\n\r\nHello World!\r\n";
$message = Message::parseMessage($raw);
$message->getHeader('Subject')->getValueAsString(); // 'Hello World'
The content is always in the parts, never on the message. Each part is a Pop\Mime\Part with its own
headers and body, and getContents() decodes that body according to its Content-Transfer-Encoding:
use Pop\Mime\Message;
$message = Message::parseMessage(file_get_contents(__DIR__ . '/../var/message.eml'));
foreach ($message->getParts() as $part) {
$type = $part->getHeader('Content-Type')->getValueAsString();
$body = $part->getContents();
}
Three narrower parsers handle a fragment rather than a whole message. Message::parseHeaders() takes a
header block and returns an array of Pop\Mime\Part\Header objects. Message::parsePart() takes one
part's text and returns a Part. Message::parseBody() takes a body and a boundary and returns the part
strings — the boundary is a required argument, because a bare body has no header to read it from.
MIME specifies CRLF line endings and the parser assumes them, so normalize a message to \r\n before
parsing it.
Building a Message#
A Pop\Mime\Message you construct yourself is headers plus either a body or a set of parts. The simplest
form is headers and one Pop\Mime\Part\Body:
use Pop\Mime\Message;
use Pop\Mime\Part\Body;
$message = new Message();
$message->addHeaders([
'Subject' => 'Hello World',
'To' => 'test@test.com',
]);
$message->setBody(new Body('Hello World!'));
echo $message;
Subject: Hello World
To: test@test.com
Hello World!
Add parts instead of a body and the message becomes multipart. Part::text(), Part::html() and
Part::attachment() build the common ones in a single call each, and inferSubType() picks the
multipart subtype from what is actually in the message:
use Pop\Mime\Message;
use Pop\Mime\Part;
$message = new Message();
$message->addHeaders(['Subject' => 'Hello World', 'To' => 'test@test.com']);
$message->addParts([
Part::html('<html><body><h1>This is the HTML message.</h1></body></html>'),
Part::text('This is the text message.'),
]);
$message->inferSubType();
echo $message;
Subject: Hello World
To: test@test.com
Content-Type: multipart/alternative; boundary=57d637de53ac2d3ac98fdbcc5d1f38e6450454ea
This is a multi-part message in MIME format.
--57d637de53ac2d3ac98fdbcc5d1f38e6450454ea
Content-Type: text/html
<html><body><h1>This is the HTML message.</h1></body></html>
--57d637de53ac2d3ac98fdbcc5d1f38e6450454ea
Content-Type: text/plain
This is the text message.
--57d637de53ac2d3ac98fdbcc5d1f38e6450454ea--
Boundaries are generated for you, and a fresh one appears on every render. inferSubType() picks
alternative for a text/HTML pair and mixed as soon as any part is a file — a file always wins, so an
attachment is never dropped by a client that renders only one branch of an alternative. It's opt-in;
setSubType('mixed') sets it by hand.
An attachment can come from disk or from memory, and the content type is detected from the extension unless you pass one:
use Pop\Mime\Message;
use Pop\Mime\Part;
$message = new Message();
$message->addHeaders(['Subject' => 'With file']);
$message->addParts([
Part::text('See attached.'),
Part::attachment(__DIR__ . '/../var/report.pdf'),
Part::attachmentFromContent('some,csv,data', 'data.csv'),
]);
$message->inferSubType();
An unrecognized extension falls back to application/octet-stream. Both factories base64-encode by
default, adding the matching Content-Transfer-Encoding header on render;
Pop\Mime\Part\Body\Encoding is the backed enum for choosing otherwise — BASE64, QUOTED_PRINTABLE,
BINARY, _7BIT, _8BIT, URL and RAW_URL.
use Pop\Mime\Part;
use Pop\Mime\Part\Body;
use Pop\Mime\Part\Body\Encoding;
$part = new Part();
$part->addHeader('Content-Type', 'text/plain');
$part->setBody(new Body('Hello World!', Encoding::QUOTED_PRINTABLE));
A unique Message-ID is worth setting on anything you send, and setMessageId() generates and sets it
in one call. The same mechanism on a nested part is setContentId(), which is how an inline image gets
the cid: reference an HTML part points at:
use Pop\Mime\Message;
use Pop\Mime\Part;
$message = new Message();
$message->setMessageId(null, 'example.com');
$logo = Part::attachment(__DIR__ . '/../var/logo.png');
$logo->setContentId(null, 'example.com');
Pass an ID as the first argument when you already have one. The domain defaults to
$_SERVER['SERVER_NAME'], falling back to localhost.
Parts and Headers#
A header is a name and one or more values, and a value is more than a string — it can carry a scheme,
parameters, a delimiter and a quoting rule. Pop\Mime\Part\Header\Value is where that structure lives:
use Pop\Mime\Part\Header;
$header = new Header('Content-Disposition');
$value = new Header\Value('attachment');
$value->addParameter('filename', 'filename.jpg');
$header->addValue($value);
echo $header; // Content-Disposition: attachment; filename=filename.jpg
Passing a plain string builds a Value for you, so new Header('Content-Type', 'text/html') works and
getValue() hands back the object. Where a header legitimately repeats, pass an array or call
addValue() more than once, and reach a specific one by index:
use Pop\Mime\Part\Header;
$header = new Header('X-Multi-Header', ['value-1', 'value-2', 'value-3']);
$header->getValue(2); // the third value object
$header->getValueAsString();
Address headers — To, From, Cc, Bcc, Reply-To — get their own type.
Pop\Mime\Part\Header\AddressList holds Pop\Mime\Part\Header\Address objects and tokenizes the header
rather than splitting on commas, which is what makes a display name containing a comma survive:
use Pop\Mime\Part\Header\AddressList;
$list = AddressList::parse('"Doe, John" <john@doe.com>, Jane Doe <jane@doe.com>');
foreach ($list->getAddresses() as $address) {
echo $address->getName() . ' <' . $address->getAddress() . ">\n";
}
Doe, John <john@doe.com>
Jane Doe <jane@doe.com>
Two addresses, not three. render() turns the list back into a header-ready string, quoting a display
name only where it needs it, and you can build a list directly instead of parsing one:
use Pop\Mime\Message;
use Pop\Mime\Part\Header\Address;
use Pop\Mime\Part\Header\AddressList;
$list = new AddressList([
new Address('john@doe.com', 'Doe, John'),
new Address('jane@doe.com', 'Jane Doe'),
]);
$message = new Message();
$message->addHeader('To', $list->render());
Non-ASCII in a display name is handled without your asking. Rendering RFC 2047 encoded-word encodes it, and parsing decodes it back:
use Pop\Mime\Part\Header\Address;
use Pop\Mime\Part\Header\EncodedWord;
$address = new Address('jose@example.com', 'José García');
echo $address->render(); // =?UTF-8?B?Sm9zw6kgR2FyY8OtYQ==?= <jose@example.com>
EncodedWord::encode('José García');
EncodedWord::decode('=?UTF-8?B?Sm9zw6kgR2FyY8OtYQ==?=');
Plain ASCII is left alone, so the encoding only appears where it is actually required.
getHeader() returns the Header object — cast it to a string for the full Name: value line, or call
getValue() for the value alone.
Multipart Form Data#
multipart/form-data is the same MIME machinery pointed at HTTP, and Message::createForm() builds it
from a plain array:
use Pop\Mime\Message;
$form = Message::createForm([
'username' => 'admin@test/whatever%DUDE!',
'password' => '123456',
'colors' => ['Red', 'Green'],
]);
echo $form->renderRaw();
--e23585ddf6a91b0957fe799b90d8e36f69efa9b0
Content-Disposition: form-data; name=username
admin%40test%2Fwhatever%25DUDE%21
--e23585ddf6a91b0957fe799b90d8e36f69efa9b0
Content-Disposition: form-data; name=password
123456
--e23585ddf6a91b0957fe799b90d8e36f69efa9b0
Content-Disposition: form-data; name=colors[]
Red
--e23585ddf6a91b0957fe799b90d8e36f69efa9b0
Content-Disposition: form-data; name=colors[]
Green
--e23585ddf6a91b0957fe799b90d8e36f69efa9b0--
Values are URL-encoded, and an array value becomes one part per element with [] on the field name.
renderRaw() gives the parts alone; render(), or echoing the object, adds the top-level
Content-Type header and the MIME preamble — which is the form a client needs when it is setting the
header itself from the message.
A file field is an array with a filename, either read from disk or supplied inline:
use Pop\Mime\Message;
$form = Message::createForm([
'note' => 'hello',
'file' => [
'filename' => __DIR__ . '/../var/upload.txt',
'contentType' => 'text/plain',
],
]);
use Pop\Mime\Message;
$form = Message::createForm([
'file' => [
'filename' => 'report.pdf',
'contents' => file_get_contents(__DIR__ . '/../var/report.pdf'),
'mimeType' => 'application/pdf',
],
]);
The content type key is read case-insensitively under any of Content-Type, contentType, Mime-Type,
mimeType or mime.
Going the other way, Message::parseForm() turns a form body back into the array:
use Pop\Mime\Message;
$formData = Message::parseForm(file_get_contents(__DIR__ . '/../var/form-body.txt'));
Field values come back decoded, array fields are reassembled as arrays, and a file field arrives as
['filename' => ..., 'contents' => ...]. The string has to include a Content-Type header carrying the
boundary — parseForm() reads the boundary from the header, so renderRaw() output alone is not enough
to feed back in.
Encoding cases, the full Part API and the lower-level parseBody() and parsePart() go past what this
page covers — see the pop-mime README.
See Also#
- Mail — the component that builds these messages for you when you are sending mail
- HTTP Client — where a
multipart/form-datarequest body comes from - Requests & Responses — the incoming side of the same format
- pop-mime README — the whole part, header and value API