MIME
The MIME component implements the Multipurpose Internet Mail Extensions (RFC 2045-2049) and related standards. It provides media type parsing, MIME part construction, multipart body streaming, content sniffing, S/MIME cryptography, and DKIM signing - all without filesystem coupling or temporary files.
Media Types
MediaType is an immutable value object representing a MIME media type like application/json or text/html; charset=utf-8. Type and subtype are always lowercased. Structured syntax suffixes and registration trees are extracted automatically.
use Psl\MIME\MediaType;
$type = MediaType::parse('application/vnd.api+json; charset=utf-8');
$type->type; // "application"
$type->subtype; // "vnd.api+json"
$type->suffix; // "json"
$type->tree; // "vnd"
$type->parameters; // Parameters with charset=utf-8
$type->essence(); // "application/vnd.api+json"
// From file extension
$json = MediaType::fromExtension('json'); // application/json
// IANA registration
$type->isRegistered(); // true
$type->extensions(); // ["json"]
Content Negotiation
MediaPreferences parses HTTP Accept headers and negotiates the best media type per RFC 9110.
use Psl\MIME\MediaPreferences;
use Psl\MIME\MediaType;
$prefs = MediaPreferences::parse('text/html, application/json;q=0.9, */*;q=0.1');
$best = $prefs->negotiate([
new MediaType('application', 'json'),
new MediaType('text', 'html'),
]);
// $best->essence() === "text/html"
Parts
Parts are the building blocks of MIME messages. Every part implements PartInterface, which exposes $mediaType, $headers, and body().
use Psl\IO;
use Psl\MIME\Headers;
use Psl\MIME\MediaType;
use Psl\MIME\Part;
// Raw part from headers + body
$raw = new Part\Part(Headers::fromPairs([['Content-Type', 'text/plain']]), new IO\MemoryHandle('Hello'));
// Text part with automatic transfer encoding (quoted-printable by default)
$text = new Part\Text(new IO\MemoryHandle('Hello, World!'));
$text->mediaType; // text/plain; charset=utf-8
// Binary attachment with base64 encoding
$data = new Part\Data(
new IO\MemoryHandle('binary content'),
filename: 'report.pdf',
mediaType: MediaType::parse('application/pdf'),
);
// Convert attachment to inline with Content-ID
$inline = $data->asInline(Psl\MIME\ContentId::generate());
Multipart Bodies
Multipart types compose multiple parts into a single body, delimited by a boundary string. All multipart types implement MultiPartInterface (which extends PartInterface), so they nest naturally.
use Psl\IO;
use Psl\MIME\MediaType;
use Psl\MIME\MultiPart;
use Psl\MIME\Part;
// multipart/alternative: text + HTML versions
$alt = new MultiPart\Alternative();
$alt->addPart(new Part\Text(new IO\MemoryHandle('Plain text')));
$alt->addPart(new Part\Text(new IO\MemoryHandle('<h1>HTML</h1>'), subtype: 'html'));
// multipart/mixed: body + attachments
$mixed = new MultiPart\Composite($alt);
$mixed->addPart(
new Part\Data(
new IO\MemoryHandle('pdf content'),
filename: 'doc.pdf',
mediaType: MediaType::parse('application/pdf'),
),
);
// Nested multipart is just $mixed->body() - a streaming read handle
// IO\copy($mixed->body(), $outputHandle);
Form Data
Form builds multipart/form-data bodies per RFC 7578, for HTML form submissions and file uploads.
use Psl\IO;
use Psl\MIME\MediaType;
use Psl\MIME\MultiPart\Form;
use Psl\MIME\Part;
$form = new Form();
$form->addField('username', 'azjezz');
$form->addField('bio', 'PSL maintainer');
$form->addPart(
'avatar',
new Part\Data(new IO\MemoryHandle('image bytes'), filename: 'avatar.png', mediaType: MediaType::parse('image/png')),
);
$form->mediaType; // multipart/form-data; boundary=...
$form->boundary; // random 24-char alphanumeric string
Streaming Parser
The streaming parser yields parts lazily from a read handle, with configurable limits to prevent abuse.
use Psl\IO;
use Psl\MIME\MultiPart\Parser;
$body = "--boundary\r\nContent-Type: text/plain\r\n\r\nHello\r\n--boundary\r\nContent-Type: text/html\r\n\r\n<b>World</b>\r\n--boundary--\r\n";
$parser = new Parser(boundary: 'boundary', maxParts: 100, maxPartSize: 10_000_000, spoolThreshold: 2_097_152);
foreach ($parser->parse(new IO\MemoryHandle($body)) as $part) {
IO\write_line('%s: %s', $part->mediaType->essence(), $part->body()->readAll());
}
S/MIME Cryptography
Sign, verify, encrypt, and decrypt using CMS (RFC 5652) - entirely in-memory without temporary files or openssl_pkcs7_* functions.
use Psl\MIME\SMIME;
// Signing requires a certificate and private key in PEM format.
// Verification can optionally check the certificate chain against trusted CAs.
// Encryption requires the recipient's certificate; decryption requires their private key.
// All operations are in-memory -- no temporary files or openssl_pkcs7_* calls.
/** @var string $certPem */
/** @var string $keyPem */
/** @var string $caCertPem */
/** @var string $recipientCertPem */
/** @var string $recipientKeyPem */
// Sign and verify
$signer = new SMIME\Signer($certPem, $keyPem);
$signed = $signer->sign('Hello');
$verifier = new SMIME\Verifier([$caCertPem]);
$result = $verifier->verify($signed, verifyCertificateChain: true);
$result->valid; // true
$result->content; // "Hello"
$result->digestAlgorithm; // DigestAlgorithm::Sha256
// Encrypt and decrypt
$encryptor = new SMIME\Encryptor([$recipientCertPem], SMIME\CipherAlgorithm::Aes256Cbc);
$encrypted = $encryptor->encrypt('Secret');
$decryptor = new SMIME\Decryptor($recipientKeyPem);
$decrypted = $decryptor->decrypt($encrypted); // "Secret"
DKIM Signing
Sign raw MIME messages with DKIM (RFC 6376) using RSA-SHA256 or Ed25519-SHA256 (RFC 8463). RSA keys below 1024 bits are rejected per RFC 8301.
use Psl\MIME\DKIM;
/** @var string $rsaPrivateKeyPem */
$config = new DKIM\SigningConfiguration(
domain: 'example.com',
selector: 'default',
algorithm: DKIM\Algorithm::RsaSha256,
headerCanonicalization: DKIM\Canonicalization::Relaxed,
bodyCanonicalization: DKIM\Canonicalization::Relaxed,
);
$signer = new DKIM\Signer($rsaPrivateKeyPem, $config);
$rawMessage = "From: [email protected]\r\nTo: [email protected]\r\nSubject: Test\r\n\r\nHello!";
$signedMessage = $signer->sign($rawMessage);
// $signedMessage now has a DKIM-Signature header prepended
Content Sniffing
Detect MIME types from content bytes using magic signatures, RIFF sub-formats, ISO BMFF brands, and text heuristics. Works on raw byte strings or seekable read handles.
RFC Compliance
| RFC | Coverage |
|---|---|
| RFC 2045 | Media types, parameters, transfer encoding |
| RFC 2046 | Multipart bodies, boundary rules |
| RFC 2183 | Content-Disposition with safe filename |
| RFC 2231 | Parameter encoding with continuations and charset |
| RFC 2387 | multipart/related |
| RFC 2392 | Content-ID parsing and generation |
| RFC 5322 | Header line folding |
| RFC 5652 | CMS signing, verification, encryption, decryption |
| RFC 6376 | DKIM signing with simple/relaxed canonicalization |
| RFC 6838 | Media type registration, suffix/tree extraction |
| RFC 7578 | multipart/form-data |
| RFC 8301 | DKIM minimum key size enforcement |
| RFC 8463 | Ed25519-SHA256 DKIM signing |
| RFC 8551 | S/MIME 4.0 message specification |
| RFC 9110 | Content negotiation (Accept header) |
See src/Psl/MIME/ for the full API.