What RFC 2045 Actually Specifies
RFC 2045 §6.8 defines the base64 Content-Transfer-Encoding for MIME email bodies. Three rules:
- Alphabet: identical to RFC 4648 §4 —
A-Z,a-z,0-9,+,/, with=padding. - Line length: no line may be longer than 76 characters. Lines are terminated by CRLF (
\r\n). - Decoder tolerance: decoders MUST ignore all characters outside the alphabet (including whitespace, CRLF, and junk). Decoders MAY warn about unknown characters but MUST NOT fail.
76 is not arbitrary. 76 Base64 characters encode exactly 57 input bytes (76 × 6 / 8 = 57). So a MIME body is a stream of 57-byte input chunks, each emitted as a 76-char Base64 line + CRLF.
Content-Transfer-Encoding — Where It Fits in the Email
From: [email protected]
To: [email protected]
Subject: Report attached
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="BOUNDARY-abc123"
--BOUNDARY-abc123
Content-Type: text/plain; charset=utf-8
See attached report.
--BOUNDARY-abc123
Content-Type: application/pdf; name="report.pdf"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="report.pdf"
JVBERi0xLjQKJcfs8vUKMSAwIG9iago8PAovVHlwZSAvQ2F0YWxvZwovUGFnZXMg
MiAwIFIKPj4KZW5kb2JqCjIgMCBvYmoKPDwKL1R5cGUgL1BhZ2VzCi9LaWRzIFsz
IDAgUl0KL0NvdW50IDEKPj4KZW5kb2JqCjMgMCBvYmoKPDwKL1R5cGUgL1BhZ2UK
...76 characters per line, CRLF line endings...
--BOUNDARY-abc123--Note the blank line (CRLF CRLF) between the attachment part's headers and the Base64 body. The Content-Transfer-Encoding: base64header is required — without it, the mail client reads the Base64 as literal text and shows gibberish.
Python — base64.encodebytes vs b64encode
import base64
data = b"The quick brown fox jumps over the lazy dog. " * 10
# Standard Base64 — single line, API-ready
oneline = base64.b64encode(data).decode()
print(oneline[:80])
# VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZy4gVGhlIHF1aWNrIGJyb3du
print("---")
# MIME Base64 — wrapped at 76 chars with newlines
mime = base64.encodebytes(data).decode()
print(mime)
# VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZy4gVGhlIHF1aWNrIGJyb3du
# IGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZy4gVGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVy
# ...
# Verify the 76-char line length
for line in mime.strip().split("\n"):
assert len(line) <= 76, f"line too long: {len(line)}"
# One pitfall: encodebytes uses LF line endings on all platforms.
# For strict RFC 2045 CRLF, replace manually:
mime_crlf = mime.replace("\n", "\r\n")Python — Full Email Attachment (High-Level API)
from email.message import EmailMessage
import smtplib
msg = EmailMessage()
msg["From"] = "[email protected]"
msg["To"] = "[email protected]"
msg["Subject"] = "Report attached"
msg.set_content("See attached report.")
# High-level API — picks MIME Base64 and wraps correctly for you
with open("report.pdf", "rb") as f:
msg.add_attachment(
f.read(),
maintype="application",
subtype="pdf",
filename="report.pdf",
)
# Preview the raw MIME output
print(msg.as_string()[:800])
# Send
with smtplib.SMTP_SSL("smtp.example.com", 465) as s:
s.login("[email protected]", "app-password")
s.send_message(msg)
# add_attachment() auto-selects:
# Content-Type: application/pdf
# Content-Transfer-Encoding: base64 (because the data is binary)
# 76-char wrap with proper CRLF
# No manual encoding needed.Node.js — Manual 76-Char Wrap
import fs from "node:fs/promises";
function mimeBase64(bytes) {
const oneline = Buffer.from(bytes).toString("base64");
const chunks = [];
for (let i = 0; i < oneline.length; i += 76) {
chunks.push(oneline.slice(i, i + 76));
}
// RFC 2045 says CRLF between lines
return chunks.join("\r\n");
}
const data = await fs.readFile("report.pdf");
const encoded = mimeBase64(data);
console.log(encoded.slice(0, 300));
// Node 18+ built-in email tools (via Nodemailer or raw SMTP) handle this for you.
// Nodemailer attach() produces MIME Base64 by default for binary MIME types.Java — Base64.getMimeEncoder
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Base64;
public class MimeBase64Example {
public static void main(String[] args) throws Exception {
byte[] data = Files.readAllBytes(Path.of("report.pdf"));
// MIME encoder — 76-char line wrap, CRLF line separator
String mime = Base64.getMimeEncoder().encodeToString(data);
System.out.println(mime.substring(0, Math.min(mime.length(), 300)));
// Custom line length and separator (e.g. 64 chars with LF)
byte[] separator = "\n".getBytes();
String custom = Base64.getMimeEncoder(64, separator).encodeToString(data);
// Decode MIME-wrapped Base64 — tolerant of CRLF/LF/spaces
byte[] decoded = Base64.getMimeDecoder().decode(mime);
// The non-MIME decoder (Base64.getDecoder()) would throw IllegalArgumentException
// on the first CRLF because it is strict. Always use getMimeDecoder() for email bodies.
}
}Go — Build Your Own 76-Wrap Encoder
package main
import (
"bytes"
"encoding/base64"
"fmt"
"os"
)
// MIME Base64: standard alphabet, 76 chars per line, CRLF line ending.
func mimeBase64(data []byte) string {
oneline := base64.StdEncoding.EncodeToString(data)
var buf bytes.Buffer
const lineLen = 76
for i := 0; i < len(oneline); i += lineLen {
end := i + lineLen
if end > len(oneline) {
end = len(oneline)
}
buf.WriteString(oneline[i:end])
if end < len(oneline) {
buf.WriteString("\r\n")
}
}
return buf.String()
}
func main() {
data, err := os.ReadFile("report.pdf")
if err != nil {
panic(err)
}
encoded := mimeBase64(data)
fmt.Println(encoded[:300])
}
// To decode MIME Base64 in Go: base64.NewDecoder tolerates CRLF.
// base64.StdEncoding.DecodeString does NOT — it treats CRLF as invalid characters.CLI — Standard base64 Command With Line Wrap
# GNU coreutils base64 — wraps at 76 by default
base64 report.pdf > report.b64
# Explicit wrap length
base64 --wrap=76 report.pdf > report.b64
# No wrap (single line, API-ready)
base64 --wrap=0 report.pdf > report.b64
# BSD base64 (macOS) — use -b for wrap width
base64 -b 76 -i report.pdf -o report.b64
# Decode MIME-wrapped Base64 — both GNU and BSD base64 tolerate line wrap
base64 --decode report.b64 > report.pdf.restoredMIME Base64 vs API Base64 vs JWT Base64 — Side by Side
- MIME (RFC 2045): standard alphabet,
=padding, 76-char CRLF line wrap. Use for: SMTP email bodies, email attachments. - Standard (RFC 4648 §4): standard alphabet,
=padding, single line. Use for: HTTP APIs, data URLs, Basic Auth, binary payload in JSON. - URL-safe (RFC 4648 §5):
-and_instead of+and/,=padding, single line. Use for: URL query parameters. - JWT (RFC 7515): URL-safe alphabet, no padding, single line. Use for: JWT headers, JWT payloads, JWS/JWE segments.
The output alphabet is the same for MIME and Standard — the only visible difference is where the line breaks are. Decoders written for Standard typically fail on MIME input because they treat CRLF as invalid.
Common Pitfalls in MIME Base64 Code
- Using
b64encodeinstead ofencodebytesin Python —b64encodegives you one long line, which some strict SMTP servers reject. Useencodebytesfor MIME;b64encodefor APIs. - LF instead of CRLF line endings — Most Python/Node encoders default to LF on Unix. RFC 2045 says CRLF. Replace manually before piping to SMTP:
encoded.replace("\\n", "\\r\\n"). - Omitting the Content-Transfer-Encoding header — Without it, the receiving mail client reads your Base64 as plain text. Attachments appear as a wall of random characters. Always set the header to
base64. - Decoding MIME Base64 with a strict decoder —
base64.StdEncoding.DecodeStringin Go,Base64.getDecoder()in Java, andbase64.b64decode(validate=True)in Python all reject CRLF. Use the MIME variants:base64.NewDecoder,Base64.getMimeDecoder(),base64.decodebytes. - Not accounting for
=padding on the last line — The last line may be shorter than 76 chars because of padding. That is correct; do not pad it out to 76. The 76 is a maximum, not a requirement for every line.
Verification Checklist
- Every line in the Base64 body is ≤ 76 characters.
- Lines are separated by CRLF (
\r\n), not just LF. - Only the final line may be shorter than 76; all others are exactly 76.
- The MIME part has
Content-Transfer-Encoding: base64. - The MIME part has
Content-Typewith the correct media type. - A single blank line (CRLF CRLF) separates headers from the Base64 body.
- Round-trip test: decode the body and compare SHA-256 to the original file.
Key Facts
- Spec:
- RFC 2045 §6.8 — Multipurpose Internet Mail Extensions, Part One
- Alphabet:
- Same as RFC 4648 §4 (A-Z a-z 0-9 + /) with = padding
- Line length:
- Max 76 characters
- Line ending:
- CRLF (\r\n), not just LF
- Input per line:
- Exactly 57 bytes (76 × 6 / 8 = 57)
- Python:
- base64.encodebytes() — NOT b64encode()
- Java:
- Base64.getMimeEncoder() / getMimeDecoder()
- Required header:
- Content-Transfer-Encoding: base64
Related Base64 Tools
- Base64 Encode Online — general-purpose browser encoder
- Base64 Encode File — language-agnostic file encoder
- Base64 Encode in Python — includes
encodebytesdetails - Base64 Encode in Java —
getMimeEncoderdeep dive - Base64 Encode in Go — build a MIME wrapper
- URL-Safe Base64 — the other RFC 4648 variant
- Base64 Decode Online — decode wrapped or single-line input