Encoded Email Subjects: How RFC 2047 Q and B Work
=?UTF-8?Q?R=C3=A9sum=C3=A9_ready?= is not a damaged email subject. It is an RFC 2047 encoded-word. A mail client decodes it to Résumé ready.
The four pieces are the character set, the encoding, the encoded bytes, and the closing delimiter. Q uses a quoted-printable-like form. B uses Base64. Neither is encryption, and neither says anything about whether the sender is legitimate.
Read the wrapper before the text
encoded-word = "=?" charset "?" encoding "?" encoded-text "?="
Subject: =?UTF-8?Q?R=C3=A9sum=C3=A9_ready?=
└─┬──┘ │ └──────────┬──────────┘
charset Q encoded text
RFC 2047 defines that exact wrapper. The encoded-word contains printable ASCII so older mail transport and header-processing software can carry non-ASCII text without treating its bytes as header syntax.
The character set tells the reader how to turn the decoded octets into text. Modern messages normally use UTF-8, but old mail may name ISO-8859-1 or another registered character set. Ignoring that field is how caf=C3=A9 becomes mojibake instead of café.
An encoded-word may be at most 75 characters including its delimiters. Longer text is split into multiple self-contained encoded-words. A multibyte character cannot be cut between them.
Q and B encode the same bytes differently
RFC 2047 defines two encodings. B is Base64. Q resembles quoted-printable, but its header rules matter:
=HHrepresents one octet using two hexadecimal digits.- An underscore represents byte
20, which is a space. - A literal underscore must therefore be encoded as
=5F. - Spaces and tabs cannot appear literally inside an encoded-word.
Raw: =?utf-8?Q?one_two=5Fthree?=
Bytes: one two_three
Display: one two_three
Raw: =?utf-8?B?U3ViamVjdCBkZWNvZGluZyDinIU=?=
Display: Subject decoding ✅
Q is convenient when most of the subject is already readable ASCII. B is usually more compact for text dominated by non-ASCII bytes. The receiving application must support both rather than choosing one based on appearance.
Why do two encoded-words sometimes touch?
A long subject may be folded across header lines:
Subject: =?utf-8?B?SGVsbG8=?=
=?utf-8?B?4pyF?=
Those two words display as Hello✅, not Hello ✅. Under RFC 2047's display rules, linear whitespace separating adjacent encoded-words is ignored. That rule lets a sender fold a long encoded value without inserting a visible space.
Whitespace between ordinary ASCII and an encoded-word is different. Re: =?utf-8?Q?caf=C3=A9?= displays as Re: café. A parser needs the header grammar, not a global search-and-replace for strings beginning with =?.
Parse the header before using the subject
My rule is to parse the complete header with a mail library, then use its decoded value. Do not Base64-decode a substring and assume the job is finished. The code also has to handle Q encoding, character sets, header folding, adjacent words, ordinary ASCII, and malformed input.
from email import policy
from email.parser import BytesParser
def decoded_subject(raw_message: bytes) -> str:
message = BytesParser(policy=policy.default).parsebytes(raw_message)
subject = message["Subject"]
return str(subject) if subject is not None else ""
Python's BytesParser documentation recommends specifying a policy. Its EmailPolicy documentation says the parser implements RFC 2047 and RFC 5322 header handling. Parsing bytes also preserves the boundary between the serialized message and decoded application text.
For this guide, I ran eight subject fixtures through BytesParser(policy=policy.default): plain ASCII, UTF-8 Q encoding, UTF-8 B encoding, mixed ASCII and encoded text, adjacent folded words, ISO-8859-1, the Q underscore rule, and Arabic text in B encoding. All eight decoded to the expected Unicode string with zero parser defects.
The parser should not make the subject trusted. Escape it in HTML, cap its stored and displayed size, and retain raw evidence only under your normal privacy and debugging policy. RFC 2047 says a malformed encoded-word must not prevent the message from being displayed or handled, so a decoding failure belongs in an observable error path, not a silent message drop.
Encoded-words are not valid everywhere
The wrapper can appear in an unstructured field such as Subject, in certain comments, and as a display-name word in an address phrase. It cannot be pasted into every header value.
RFC 2047 section 5 specifically excludes encoded-words from an addr-spec, a Received field, and MIME parameters such as those in Content-Type or Content-Disposition. Decoding also occurs after a structured field has been parsed into tokens. That ordering matters because decoded text can contain characters that look like structural punctuation.
If you are inspecting routing, keep encoded subject text away from the trust decision. The Received-header guide shows where the reliable hop boundary begins. If you are grouping conversations, do not use the decoded subject as the only thread key. The Message-ID guide covers that separate job.
Does SMTPUTF8 make RFC 2047 obsolete?
No. RFC 6532 allows direct UTF-8 in fields such as Subject when the internationalized-email extensions are in use. It also says encoded-words should not be generated for a message using that extension.
Receiving systems still encounter both forms. Old messages, compatibility paths, forwarded content, and mail generated without SMTPUTF8 continue to use RFC 2047. A robust parser accepts the legal form present in the message and returns Unicode to the application.
SMTPUTF8 also covers more than subject text. Internationalized address local parts have their own transport and verification constraints, which the SMTPUTF8 address guide handles separately.
The practical boundary is simple: preserve the raw header for evidence, parse it with the message grammar, use the decoded Unicode value for display and search, and never treat a readable subject as proof about the sender.