Pop PHP
The Toolkit

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.

BASH
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:

PHP
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:

PHP
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:

PHP
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;
TEXT
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:

PHP
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;
TEXT
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:

PHP
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.

PHP
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:

PHP
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:

PHP
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:

PHP
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:

PHP
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";
}
TEXT
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:

PHP
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:

PHP
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:

PHP
use Pop\Mime\Message;

$form = Message::createForm([
    'username' => 'admin@test/whatever%DUDE!',
    'password' => '123456',
    'colors'   => ['Red', 'Green'],
]);

echo $form->renderRaw();
TEXT
--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:

PHP
use Pop\Mime\Message;

$form = Message::createForm([
    'note' => 'hello',
    'file' => [
        'filename'    => __DIR__ . '/../var/upload.txt',
        'contentType' => 'text/plain',
    ],
]);
PHP
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:

PHP
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-data request body comes from
  • Requests & Responses — the incoming side of the same format
  • pop-mime README — the whole part, header and value API