Why email needs Base64
Internet mail was built for 7-bit ASCII text in lines of limited length. RFC 5322 still caps a line at 998 characters, and SMTP only guarantees safe delivery of 7-bit data unless the receiving server announces the 8BITMIME extension. Even with 8-bit transport, a message must not contain NUL bytes or bare carriage returns and line feeds, which every binary file eventually does. Sending raw bytes needs the BINARYMIME extension, and a sender cannot count on that along the whole path from server to server.
MIME (RFC 2045) solves this with the Content-Transfer-Encoding header. It tells the receiver how the body of a part was turned into safe text. There are five values:
| Value | Meaning |
|---|---|
7bit | Plain ASCII lines, no encoding |
8bit | 8-bit text in short lines, needs 8BITMIME |
binary | Raw bytes, needs BINARYMIME |
quoted-printable | Mostly readable text, non-ASCII bytes escaped as =XX |
base64 | Base64 in lines of at most 76 characters |
For attachments, mail software almost always picks base64. The algorithm itself is covered in how Base64 works.
Base64 vs quoted-printable
Quoted-printable keeps ASCII characters as they are and writes every other byte as = plus two hex digits. A UTF-8 é (bytes C3 A9) becomes =C3=A9. Lines longer than 76 characters are broken with a soft line break, an = at the end of the line.
That makes it ideal for English text with the occasional accent, and wasteful for anything else, since each non-ASCII byte costs 3 characters. Base64 costs a flat 4/3 regardless of content. We encoded 20 repetitions of a sentence in three languages with Python’s quopri.encodestring and base64.encodebytes (both with LF line endings):
| Sample (UTF-8) | Bytes | Quoted-printable | Base64 |
|---|---|---|---|
| English pangram | 900 | 926 | 1,216 |
| French sentence with accents | 1,060 | 1,830 | 1,435 |
| Russian pangram | 1,600 | 4,602 | 2,165 |
Mail clients usually make this choice for you: quoted-printable or 7bit for mostly-ASCII text bodies, Base64 for attachments and for text in non-Latin scripts. If you need to read or produce quoted-printable by hand, the quoted-printable tool converts in both directions.
Attachments in a raw message
A message with an attachment is a multipart/mixed body. Each part has its own headers, a blank line, and then its content. Here is a complete, minimal message generated with Python’s email package (boundary shortened for readability):
From: Ana <ana@example.com>
To: bo@example.org
Subject: =?utf-8?b?Q2Fmw6kgbWVudSDigJMgR3LDtsOfZQ==?=
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="b1"
--b1
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: quoted-printable
Hi Bo,
The caf=C3=A9 menu is attached.
--b1
Content-Type: application/pdf
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="menu.pdf"
JVBERi0xLjcKJeLjz9MKMSAwIG9iago8PCAvVHlwZSAvQ2F0YWxvZyA+PgplbmRvYmoKdHJhaWxl
cgo8PCAvUm9vdCAxIDAgUiA+PgolJUVPRgo=
--b1--
The pieces to look for:
Content-Typegives the file type,Content-Dispositionthe file name.Content-Transfer-Encoding: base64confirms the body is Base64.- The Base64 starts after the first blank line of the part and ends before the next
--boundaryline. - Lines are exactly 76 characters except the last. Each full line holds 57 bytes of the file.
JVBERi0at the start is the Base64 of%PDF-, so this is a PDF. Other signatures are listed in common Base64 prefixes.
Images embedded in HTML mail use the same mechanism in a multipart/related part. Each image gets a Content-ID header, and the HTML refers to it as <img src="cid:...">. On the wire these lines end in CRLF, which makes every 76 characters of Base64 cost 78 bytes; the full arithmetic is in Base64 size overhead. To produce wrapped output yourself, use the MIME Base64 encoder.
Encoded headers: =?UTF-8?B?…?=
Headers such as Subject, From display names and some file names must be ASCII. RFC 2047 encodes other text as encoded-words:
=?charset?encoding?encoded-text?=
encoding is B for Base64 or Q for a quoted-printable variant in which _ stands for a space. Both letters are case-insensitive, which is why Python wrote =?utf-8?b? above. Some real examples, all checked with Python’s email.header.decode_header:
| Encoded | Decoded |
|---|---|
=?UTF-8?B?0J/RgNC40LLQtdGC?= | Привет |
=?utf-8?q?Caf=C3=A9_menu?= | Café menu |
=?ISO-8859-1?Q?Caf=E9_menu?= | Café menu |
=?utf-8?b?Q2Fmw6kgbWVudSDigJMgR3LDtsOfZQ==?= | Café menu – Größe |
An encoded-word may be at most 75 characters, so long subjects are split into several, one per folded header line. RFC 2047 says whitespace between adjacent encoded-words is ignored, and each word must contain whole characters, so a multi-byte UTF-8 character is never split across two words. A 44-character Russian subject came out of Python like this:
Subject: =?utf-8?b?0KHRh9GR0YIg0LfQsCDQvtC60YLRj9Cx0YDRjCDQuCDQsNC60YIg0YE=?=
=?utf-8?b?0LLQtdGA0LrQuCDQtNC70Y8g0LHRg9GF0LPQsNC70YLQtdGA0LjQuA==?=
Each word is a complete Base64 string with its own padding. Decoded separately, the first gives Счёт за октябрь и акт с and the second верки для бухгалтерии; a decoder joins them into the original subject.
Attachment file names have a second standard, RFC 2231, which looks like filename*=UTF-8''%D0%BE%D1%82%D1%87%D1%91%D1%82.pdf. Many mail programs also put RFC 2047 encoded-words inside filename="...", so a parser should handle both.
How to extract an attachment from a .eml file
In the browser. Open the .eml in the EML decoder to read the message bodies and save the attachments. It runs locally in the page, which matters when the message contains invoices or personal data.
With Python. The standard library parses MIME and decodes both Base64 bodies and RFC 2047 headers:
import sys
from email import policy
from email.parser import BytesParser
with open(sys.argv[1], "rb") as f:
msg = BytesParser(policy=policy.default).parse(f)
print("Subject:", msg["Subject"]) # encoded-words decoded
for part in msg.iter_attachments():
name = part.get_filename()
data = part.get_content() # Base64 already decoded to bytes
with open(name, "wb") as out:
out.write(data)
print("saved", name, len(data), "bytes")
Run against the sample message above, it printed Subject: Café menu – Größe and saved menu.pdf 83 bytes. In real code, sanitize name before writing, since it comes from the sender.
By hand. Copy the Base64 lines of the part into a file and decode them. Raw .eml files use CRLF line endings, and GNU base64 -d stops with invalid input at the first carriage return. Strip them first, or use -i to skip non-alphabet characters:
tr -d '\r' < block.txt | base64 -d > menu.pdf
openssl base64 -d -in block.txt -out menu.pdf accepted the CRLF version unchanged in our tests. If you only have the Base64 text, paste it into Base64 to file, which detects the type from the first bytes, or straight into Base64 to PDF for PDFs.
See headers, bodies and attachments of a raw email, decoded locally in your browser.
FAQ
How do I open a Base64 attachment from an email source?
Copy the Base64 lines between the part’s blank line and the next boundary, then decode them as a file. The EML decoder extracts attachments from a .eml file for you.
What does =?UTF-8?B? mean in an email subject?
It starts an RFC 2047 encoded-word: the charset is UTF-8 and B means the text between the next ? and ?= is Base64. A Q instead of B means a quoted-printable variant.
Why are email Base64 lines 76 characters long?
RFC 2045 limits Base64 lines in MIME to 76 characters. 76 characters hold exactly 57 bytes, and the limit keeps lines well below the 998-character maximum that email allows.
Is quoted-printable or Base64 better for email text?
Quoted-printable for text that is mostly ASCII, since it stays readable and barely grows. Base64 for binary data and for text in non-Latin scripts, where quoted-printable can make it nearly three times larger.