Multibyte strings7 min readExamples run on PHP 8.5.10

mb_decode_mimeheader()

Learn how mb_decode_mimeheader() turns MIME encoded-words such as =?UTF-8?B?...?= into readable text in PHP's internal encoding, with examples and pitfalls.

Signature

mb_decode_mimeheader(string $string): string
Available since PHP 4.0.67.48.08.18.28.38.48.5All versions →

Purpose

Email headers such as Subject were designed for plain ASCII. To carry characters like é or ß, the sender wraps the text in an encoded-word as described by RFC 2047. An encoded-word looks like =?charset?encoding?payload?=, where the encoding is B (Base64) or Q (a quoted-printable variant).

mb_decode_mimeheader() reads a header value, finds the encoded-words, decodes them, converts the result from the declared charset to PHP's current internal encoding, and returns the readable string. Text outside encoded-words is passed through as it is.

Signature

The function has been available since PHP 4.0.6 and is part of the mbstring extension.

Parameters and defaults

  • $string: the header field value to decode. It may contain one or more encoded-words, ordinary text, or a mixture. There are no other parameters and therefore no defaults.

The target character set is not a parameter. It is whatever mb_internal_encoding() currently returns, so it is a good idea to set it explicitly before decoding.

Return value and failure cases

The function returns a string: the decoded text in the internal character encoding. The signature has no false return, so you do not need to check for a failure value. If the input contains no encoded-words, you get the same text back.

The PHP manual records one change. As of PHP 8.3.0, underscores are converted to spaces as RFC 2047 specifies. In Q encoding an underscore stands for a space, so on PHP 8.4 Caf=C3=A9_au_lait decodes to Café au lait.

Example 1: decoding a Base64 encoded-word

The payload is generated with base64_encode() so the example does not depend on a hand-typed Base64 string.

Example
<?php
mb_internal_encoding('UTF-8');
$word = '=?UTF-8?B?' . base64_encode('Grüße') . '?=';
echo json_encode(mb_decode_mimeheader($word), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
Output
"Grüße"

Example 2: Q encoding and underscores

In Q encoding, bytes outside plain ASCII are written as = followed by two hex digits, and _ stands for a space.

Example
<?php
mb_internal_encoding('UTF-8');
$header = '=?UTF-8?Q?Caf=C3=A9_au_lait?=';
echo json_encode(mb_decode_mimeheader($header), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
Output
"Café au lait"

Example 3: plain text, mixed text and another charset

A header without encoded-words comes back unchanged. When an encoded-word declares a charset other than the internal one, such as ISO-8859-1, the decoded bytes are converted to the internal encoding.

Example
<?php
mb_internal_encoding('UTF-8');
$values = [
    mb_decode_mimeheader('Hello world'),
    mb_decode_mimeheader('Re: =?ISO-8859-1?Q?Caf=E9?= menu'),
];
echo json_encode($values, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
Output
["Hello world","Re: Café menu"]

Edge cases

  • Internal encoding matters. The result is always in the current internal encoding. If that is not UTF-8, the output bytes will not be UTF-8 either, and JSON or HTML output may break. Call mb_internal_encoding('UTF-8') first, or make sure your application's default is what you expect.
  • Only the value is decoded. Pass Café menu, not Subject: Café menu. The function does not parse header names, unfold continuation lines or split address lists.
  • Mixed content is allowed. Encoded-words can sit between ordinary words, and the surrounding text is kept.
  • Unusual or malformed input. The manual page does not describe in detail what happens with unknown charsets, wrong payloads or truncated words. If you process untrusted mail, test those inputs against your runtime and validate the result instead of assuming it is clean.
  • Decoded text is still untrusted. A decoded header can contain any characters, including control characters. Escape it for the context where you display it, for example with htmlspecialchars() in HTML.

Common mistakes

  1. Decoding twice. Once an encoded-word has been decoded, the text is ordinary. Running it through the function again is harmless for plain text but pointless, and it can hide where a bug really is.
  2. Forgetting the internal encoding. Code that works on one server can print mojibake on another because the default internal encoding differs.
  3. Passing a whole raw email. The function is meant for one header field value, not an entire message body or a list of headers.
  4. Expecting + or % decoding. Q encoding in headers uses =XX and _, not URL-style escapes.
  5. Relying on old underscore behavior. Before PHP 8.3.0, underscores were not converted to spaces. On PHP 8.4 they are, so tests written for older behavior may need updating.
  • mb_encode_mimeheader() is the inverse. It builds encoded-words from text so it can be placed in a header.
  • mb_internal_encoding() gets or sets the character set of the decoded result.
  • mb_convert_encoding() converts text between encodings when you already have the raw bytes and a charset name.
  • iconv_mime_decode() is a similar decoder from the iconv extension, with different options and behavior.

Recap

mb_decode_mimeheader() takes a header value and returns a string in which RFC 2047 encoded-words have been decoded and converted to the internal encoding. It has a single parameter and no documented false return. Set the internal encoding deliberately, pass only the header value, remember that underscores become spaces on PHP 8.3 and later, and escape the result for whatever output context you use.

Tested examples

Decoding a Base64 encoded-word
<?php
mb_internal_encoding('UTF-8');
$word = '=?UTF-8?B?' . base64_encode('Grüße') . '?=';
echo json_encode(mb_decode_mimeheader($word), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
Output
"Grüße"
Q encoding with underscores as spaces
<?php
mb_internal_encoding('UTF-8');
$header = '=?UTF-8?Q?Caf=C3=A9_au_lait?=';
echo json_encode(mb_decode_mimeheader($header), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
Output
"Café au lait"
Plain text, mixed text and another charset
<?php
mb_internal_encoding('UTF-8');
$values = [
    mb_decode_mimeheader('Hello world'),
    mb_decode_mimeheader('Re: =?ISO-8859-1?Q?Caf=E9?= menu'),
];
echo json_encode($values, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
Output
["Hello world","Re: Café menu"]