emailverifier.dev
Posts

Quoted-Printable vs Base64 in Email: Which Should You Use?

| 5 min read | Usama Ejaz
One UTF-8 email body splits into quoted-printable and Base64 encoding paths that converge on the same decoded bytes.

Use quoted-printable for text that is mostly printable ASCII and benefits from readable source. Use Base64 for binary content or text with enough non-ASCII bytes that quoted-printable becomes larger and harder to inspect. In production, make that decision for each MIME part and let a proven mail library perform the encoding.

My practical rule is simple: choose the representation that fits the bytes, then prove that decoding returns those exact bytes. The recipient should not be able to tell which transfer encoding you chose.

One body, two legal wire forms

Start with this hypothetical UTF-8 text body:

Résumé reçu. Crème brûlée. São Paulo.

Quoted-printable leaves printable ASCII visible and escapes the remaining UTF-8 octets as an equals sign followed by two uppercase hexadecimal digits:

Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable

R=C3=A9sum=C3=A9 re=C3=A7u. Cr=C3=A8me br=C3=BBl=C3=A9e. S=C3=A3o Paulo.

Base64 transforms the same octets into a uniform alphabet:

Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: base64

UsOpc3Vtw6kgcmXDp3UuIENyw6htZSBicsO7bMOpZS4gU8OjbyBQYXVsby4NCg==

Both are valid because RFC 2045 defines Content-Transfer-Encoding as a reversible transformation of a body. The header belongs to this leaf part, not to the whole multipart container. If that distinction feels slippery, read the MIME message tree from the outside in.

This is also separate from the Q and B syntax used in subjects and other structured headers. Those are encoded-words with different placement and length rules, covered in my guide to RFC 2047 email subjects.

The crossover is visible in the byte counts

I would not turn “text means quoted-printable” into a hard rule. UTF-8 text is a byte sequence, and each non-ASCII byte becomes three visible characters under quoted-printable. Base64 instead maps every three input bytes to four output characters, plus padding and line wrapping.

This Python 3.12 fixture compares three canonical CRLF-terminated bodies and verifies six round trips:

import base64, quopri

samples = {
    "ascii": "Your verification code is 482931.\r\nIt expires in 10 minutes.\r\n",
    "accented": "Résumé reçu. Crème brûlée. São Paulo.\r\n",
    "emoji": "Status: ✅✅✅🚀\r\n",
}

for name, text in samples.items():
    raw = text.encode("utf-8")
    qp = quopri.encodestring(raw, quotetabs=False)
    b64 = base64.encodebytes(raw).replace(b"\n", b"\r\n")

    assert quopri.decodestring(qp) == raw
    assert base64.decodebytes(b64) == raw
    print(name, len(raw), len(qp), len(b64))

The measured encoded-body lengths, including the shown CRLFs but excluding MIME headers, were:

  • ASCII-heavy: 62 input bytes, 62 quoted-printable bytes, 88 Base64 bytes.
  • Accented: 46 input bytes, 74 quoted-printable bytes, 66 Base64 bytes.
  • Emoji-heavy: 23 input bytes, 49 quoted-printable bytes, 34 Base64 bytes.

The small sample is intentionally reproducible, not a universal crossover percentage. Change the language, HTML, whitespace, or line length and the result changes. Python's quoted-printable module and MIME Base64 functions make the same broad tradeoff explicit: quoted-printable suits data with relatively few nonprintable bytes, while Base64 stays compact when many bytes need escaping.

The sharp edges are in the encoder

RFC 2045's quoted-printable rules require more care than replacing non-ASCII characters:

  1. Encode octets, not Unicode characters. Convert the body to its declared charset first.
  2. Encode a literal equals sign as =3D.
  3. Encode spaces and tabs at the end of a line.
  4. Keep encoded lines at 76 characters or fewer. A trailing = creates a soft line break that disappears after decoding.
  5. Preserve meaningful text line breaks as CRLF in canonical form.

MIME Base64 has its own 76-character line limit. Its encoder also needs canonical CRLF line breaks before transforming text. Do not swap in URL-safe Base64, omit MIME padding casually, or Base64-encode an already serialized multipart message as one blob.

For attachments, Base64 is normally the sensible choice. It handles arbitrary octets uniformly, but its predictable expansion contributes to the size of the complete message. My Base64 and SMTP SIZE walkthrough counts that overhead at the message boundary.

Does 8BITMIME make both encodings obsolete?

No. This is the strongest fair objection because many SMTP servers advertise 8BITMIME. RFC 6152 allows an SMTP client to send 8-bit MIME only after the server advertises that extension. It still enforces SMTP line-length limits and explicitly does not permit unencoded binary content.

If the server does not advertise 8BITMIME, a client must not send octets outside US-ASCII. It must convert the message to valid 7-bit MIME or treat the route as a permanent failure. Quoted-printable and Base64 therefore remain useful transport-safe forms, even when the first hop you control accepts 8-bit content.

The decision I would ship

For a mostly ASCII text/plain or text/html part, I would prefer quoted-printable when readable raw source helps operations. For heavily accented, non-Latin, or emoji-rich text, I would compare the actual encoded output and often choose Base64. For binary parts, I would choose Base64.

I would not write either encoder inside application code. I would set the correct charset and content type, give the bytes to a mature MIME library, serialize the complete message, and inspect one captured message in a test. The pass condition is exact: every leaf part decodes to the bytes you intended, every encoded line respects the MIME limit, and the transfer-encoding header matches the body beneath it.

Continue reading