Por qué el correo necesita Base64
El correo de Internet se diseñó para texto ASCII de 7 bits en líneas de longitud limitada. El RFC 5322 sigue limitando una línea a 998 caracteres, y SMTP solo garantiza la entrega segura de datos de 7 bits salvo que el servidor receptor anuncie la extensión 8BITMIME. Incluso con transporte de 8 bits, un mensaje no puede contener bytes NUL ni retornos de carro o saltos de línea sueltos, y cualquier archivo binario acaba teniéndolos. Enviar bytes en bruto requiere la extensión BINARYMIME, y un remitente no puede contar con ella en todo el camino de servidor a servidor.
MIME (RFC 2045) lo resuelve con la cabecera Content-Transfer-Encoding. Le dice al receptor cómo se convirtió el cuerpo de una parte en texto seguro. Hay cinco valores:
| Valor | Significado |
|---|---|
7bit | Líneas ASCII normales, sin codificar |
8bit | Texto de 8 bits en líneas cortas, necesita 8BITMIME |
binary | Bytes en bruto, necesita BINARYMIME |
quoted-printable | Texto casi todo legible, con los bytes no ASCII escapados como =XX |
base64 | Base64 en líneas de 76 caracteres como máximo |
Para los adjuntos, el software de correo casi siempre elige base64. El algoritmo en sí se explica en cómo funciona Base64.
Base64 frente a quoted-printable
Quoted-printable deja los caracteres ASCII tal cual y escribe cualquier otro byte como = seguido de dos dígitos hexadecimales. Una é en UTF-8 (bytes C3 A9) se convierte en =C3=A9. Las líneas de más de 76 caracteres se parten con un salto de línea suave, un = al final de la línea.
Eso lo hace ideal para texto en inglés con algún acento ocasional y derrochador para todo lo demás, porque cada byte no ASCII cuesta 3 caracteres. Base64 cuesta siempre 4/3, sea cual sea el contenido. Codificamos 20 repeticiones de una frase en tres idiomas con quopri.encodestring y base64.encodebytes de Python (ambos con finales de línea LF):
| Muestra (UTF-8) | Bytes | Quoted-printable | Base64 |
|---|---|---|---|
| Pangrama en inglés | 900 | 926 | 1216 |
| Frase en francés con acentos | 1060 | 1830 | 1435 |
| Pangrama en ruso | 1600 | 4602 | 2165 |
Los clientes de correo suelen tomar esta decisión por ti: quoted-printable o 7bit para cuerpos de texto casi todo ASCII, Base64 para los adjuntos y para el texto en alfabetos no latinos. Si necesitas leer o generar quoted-printable a mano, la herramienta quoted-printable convierte en los dos sentidos.
Adjuntos en un mensaje en bruto
Un mensaje con un adjunto tiene un cuerpo multipart/mixed. Cada parte tiene sus propias cabeceras, una línea en blanco y después su contenido. Este es un mensaje completo y mínimo generado con el paquete email de Python (con el boundary acortado para que se lea mejor):
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--
Las piezas en las que fijarse:
Content-Typeindica el tipo de archivo yContent-Disposition, el nombre del archivo.Content-Transfer-Encoding: base64confirma que el cuerpo está en Base64.- El Base64 empieza después de la primera línea en blanco de la parte y termina antes de la siguiente línea
--boundary. - Las líneas tienen exactamente 76 caracteres, salvo la última. Cada línea completa contiene 57 bytes del archivo.
JVBERi0al principio es el Base64 de%PDF-, así que se trata de un PDF. Otras firmas aparecen en prefijos Base64 habituales.
Las imágenes incrustadas en correos HTML usan el mismo mecanismo dentro de una parte multipart/related. Cada imagen recibe una cabecera Content-ID, y el HTML se refiere a ella como <img src="cid:...">. En la transmisión, estas líneas terminan en CRLF, así que cada 76 caracteres de Base64 cuestan 78 bytes; las cuentas completas están en el aumento de tamaño de Base64. Para generar tú mismo una salida con cortes de línea, usa el codificador Base64 MIME.
Cabeceras codificadas: =?UTF-8?B?…?=
Cabeceras como Subject, los nombres visibles de From y algunos nombres de archivo deben ser ASCII. El RFC 2047 codifica el resto del texto como encoded-words:
=?charset?encoding?encoded-text?=
encoding es B para Base64 o Q para una variante de quoted-printable en la que _ representa un espacio. Las dos letras no distinguen entre mayúsculas y minúsculas, y por eso Python escribió =?utf-8?b? más arriba. Algunos ejemplos reales, todos comprobados con email.header.decode_header de Python:
| Codificado | Decodificado |
|---|---|
=?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 |
Una encoded-word puede tener como máximo 75 caracteres, así que los asuntos largos se dividen en varias, una por cada línea plegada de la cabecera. El RFC 2047 dice que se ignoran los espacios en blanco entre encoded-words adyacentes, y cada palabra debe contener caracteres completos, de modo que un carácter UTF-8 multibyte nunca se reparte entre dos palabras. Un asunto en ruso de 44 caracteres salió así de Python:
Subject: =?utf-8?b?0KHRh9GR0YIg0LfQsCDQvtC60YLRj9Cx0YDRjCDQuCDQsNC60YIg0YE=?=
=?utf-8?b?0LLQtdGA0LrQuCDQtNC70Y8g0LHRg9GF0LPQsNC70YLQtdGA0LjQuA==?=
Cada palabra es una cadena Base64 completa con su propio relleno. Decodificadas por separado, la primera da Счёт за октябрь и акт с y la segunda верки для бухгалтерии; un decodificador las une para recuperar el asunto original.
Los nombres de archivo de los adjuntos tienen un segundo estándar, el RFC 2231, que tiene este aspecto: filename*=UTF-8''%D0%BE%D1%82%D1%87%D1%91%D1%82.pdf. Muchos programas de correo también ponen encoded-words del RFC 2047 dentro de filename="...", así que un parser debería admitir ambas formas.
Cómo extraer un adjunto de un archivo .eml
En el navegador. Abre el .eml en el decodificador EML para leer los cuerpos del mensaje y guardar los adjuntos. Se ejecuta localmente en la página, algo importante cuando el mensaje contiene facturas o datos personales.
Con Python. La biblioteca estándar analiza MIME y decodifica tanto los cuerpos Base64 como las cabeceras RFC 2047:
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")
Ejecutado con el mensaje de ejemplo de arriba, imprimió Subject: Café menu – Größe y saved menu.pdf 83 bytes. En código real, sanea name antes de escribir, porque viene del remitente.
A mano. Copia las líneas Base64 de la parte en un archivo y decodifícalas. Los archivos .eml en bruto usan finales de línea CRLF, y GNU base64 -d se detiene con invalid input en el primer retorno de carro. Quítalos antes, o usa -i para saltarte los caracteres fuera del alfabeto:
tr -d '\r' < block.txt | base64 -d > menu.pdf
En nuestras pruebas, openssl base64 -d -in block.txt -out menu.pdf aceptó la versión con CRLF sin cambios. Si solo tienes el texto Base64, pégalo en Base64 a archivo, que detecta el tipo a partir de los primeros bytes, o directamente en Base64 a PDF si es un PDF.
Consulta las cabeceras, los cuerpos y los adjuntos de un correo en bruto, decodificados localmente en tu navegador.
Preguntas frecuentes
¿Cómo abro un adjunto Base64 desde el código fuente de un correo?
Copia las líneas Base64 que hay entre la línea en blanco de la parte y el siguiente boundary, y decodifícalas como archivo. El decodificador EML extrae por ti los adjuntos de un archivo .eml.
¿Qué significa =?UTF-8?B? en el asunto de un correo?
Abre una encoded-word del RFC 2047: el juego de caracteres es UTF-8 y la B indica que el texto entre el siguiente ? y ?= está en Base64. Una Q en lugar de la B indica una variante de quoted-printable.
¿Por qué las líneas Base64 del correo tienen 76 caracteres?
El RFC 2045 limita las líneas Base64 de MIME a 76 caracteres. 76 caracteres contienen exactamente 57 bytes, y el límite deja las líneas muy por debajo del máximo de 998 caracteres que permite el correo.
¿Qué es mejor para el texto de un correo, quoted-printable o Base64?
Quoted-printable para texto casi todo ASCII, porque sigue siendo legible y apenas crece. Base64 para datos binarios y para texto en alfabetos no latinos, donde quoted-printable puede hacerlo casi tres veces más grande.