Por que o e-mail precisa de Base64
O correio da internet foi feito para texto ASCII de 7 bits em linhas de comprimento limitado. A RFC 5322 ainda limita a linha a 998 caracteres, e o SMTP só garante a entrega segura de dados de 7 bits, a menos que o servidor de destino anuncie a extensão 8BITMIME. Mesmo com transporte de 8 bits, uma mensagem não pode conter bytes NUL nem carriage returns e line feeds soltos, coisa que todo arquivo binário acaba tendo. Enviar bytes brutos exige a extensão BINARYMIME, e quem envia não pode contar com ela em todo o caminho de servidor em servidor.
O MIME (RFC 2045) resolve isso com o header Content-Transfer-Encoding. Ele diz a quem recebe como o corpo de uma parte foi transformado em texto seguro. Há cinco valores:
| Valor | Significado |
|---|---|
7bit | Linhas ASCII simples, sem codificação |
8bit | Texto de 8 bits em linhas curtas, exige 8BITMIME |
binary | Bytes brutos, exige BINARYMIME |
quoted-printable | Texto quase todo legível, bytes não ASCII escapados como =XX |
base64 | Base64 em linhas de no máximo 76 caracteres |
Para anexos, os programas de e-mail quase sempre escolhem base64. O algoritmo em si é explicado em como o Base64 funciona.
Base64 vs quoted-printable
O quoted-printable mantém os caracteres ASCII como estão e escreve todos os outros bytes como = mais dois dígitos hex. Um é em UTF-8 (bytes C3 A9) vira =C3=A9. Linhas com mais de 76 caracteres são divididas com uma quebra suave, um = no fim da linha.
Isso o torna ideal para texto em inglês com um acento aqui e ali, e um desperdício para todo o resto, já que cada byte não ASCII custa 3 caracteres. O Base64 custa 4/3 fixos, seja qual for o conteúdo. Codificamos 20 repetições de uma frase em três idiomas com quopri.encodestring e base64.encodebytes do Python (ambos com finais de linha LF):
| Amostra (UTF-8) | Bytes | Quoted-printable | Base64 |
|---|---|---|---|
| Pangrama em inglês | 900 | 926 | 1.216 |
| Frase em francês com acentos | 1.060 | 1.830 | 1.435 |
| Pangrama em russo | 1.600 | 4.602 | 2.165 |
Os clientes de e-mail costumam fazer essa escolha por você: quoted-printable ou 7bit para corpos de texto quase todo ASCII, Base64 para anexos e para texto em alfabetos não latinos. Se você precisa ler ou gerar quoted-printable à mão, a ferramenta quoted-printable converte nas duas direções.
Anexos numa mensagem bruta
Uma mensagem com anexo tem um corpo multipart/mixed. Cada parte tem os próprios headers, uma linha em branco e depois o conteúdo. Aqui está uma mensagem completa e mínima, gerada com o pacote email do Python (boundary encurtado para facilitar a leitura):
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--
O que procurar:
Content-Typeinforma o tipo do arquivo,Content-Dispositiono nome dele.Content-Transfer-Encoding: base64confirma que o corpo é Base64.- O Base64 começa depois da primeira linha em branco da parte e termina antes da próxima linha
--boundary. - As linhas têm exatamente 76 caracteres, menos a última. Cada linha completa guarda 57 bytes do arquivo.
- O
JVBERi0no início é o Base64 de%PDF-, então isto é um PDF. Outras assinaturas estão listadas em prefixos Base64 comuns.
Imagens embutidas em e-mails HTML usam o mesmo mecanismo numa parte multipart/related. Cada imagem ganha um header Content-ID, e o HTML se refere a ela como <img src="cid:...">. Na transmissão, essas linhas terminam em CRLF, o que faz cada 76 caracteres de Base64 custarem 78 bytes; a conta completa está em aumento de tamanho do Base64. Para gerar saída quebrada em linhas você mesmo, use o codificador Base64 MIME.
Headers codificados: =?UTF-8?B?…?=
Headers como Subject, os nomes de exibição em From e alguns nomes de arquivo precisam ser ASCII. A RFC 2047 codifica o resto do texto como encoded-words:
=?charset?encoding?encoded-text?=
O encoding é B para Base64 ou Q para uma variante de quoted-printable em que _ representa um espaço. As duas letras não diferenciam maiúsculas de minúsculas, e é por isso que o Python escreveu =?utf-8?b? acima. Alguns exemplos reais, todos conferidos com o email.header.decode_header do 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 |
Uma encoded-word pode ter no máximo 75 caracteres, então assuntos longos são divididos em várias, uma por linha dobrada do header. A RFC 2047 diz que o espaço em branco entre encoded-words vizinhas é ignorado, e cada palavra precisa conter caracteres inteiros, então um caractere UTF-8 de vários bytes nunca é dividido entre duas palavras. Um assunto em russo de 44 caracteres saiu do Python assim:
Subject: =?utf-8?b?0KHRh9GR0YIg0LfQsCDQvtC60YLRj9Cx0YDRjCDQuCDQsNC60YIg0YE=?=
=?utf-8?b?0LLQtdGA0LrQuCDQtNC70Y8g0LHRg9GF0LPQsNC70YLQtdGA0LjQuA==?=
Cada palavra é uma string Base64 completa, com o próprio preenchimento. Decodificadas separadamente, a primeira dá Счёт за октябрь и акт с e a segunda верки для бухгалтерии; um decodificador as junta no assunto original.
Os nomes de arquivo de anexos têm um segundo padrão, a RFC 2231, que se parece com filename*=UTF-8''%D0%BE%D1%82%D1%87%D1%91%D1%82.pdf. Muitos programas de e-mail também colocam encoded-words da RFC 2047 dentro de filename="...", então um parser deve lidar com as duas formas.
Como extrair um anexo de um arquivo .eml
No navegador. Abra o .eml no decodificador de EML para ler os corpos da mensagem e salvar os anexos. Ele roda localmente na página, o que importa quando a mensagem contém notas fiscais ou dados pessoais.
Com Python. A biblioteca padrão faz o parsing do MIME e decodifica tanto os corpos em Base64 quanto os headers da 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")
Executado com a mensagem de exemplo acima, ele imprimiu Subject: Café menu – Größe e saved menu.pdf 83 bytes. Em código de verdade, sanitize o name antes de gravar, já que ele vem de quem enviou.
À mão. Copie as linhas Base64 da parte para um arquivo e decodifique-as. Arquivos .eml brutos usam finais de linha CRLF, e o base64 -d do GNU para com invalid input no primeiro carriage return. Remova-os antes ou use -i para pular caracteres fora do alfabeto:
tr -d '\r' < block.txt | base64 -d > menu.pdf
openssl base64 -d -in block.txt -out menu.pdf aceitou a versão com CRLF sem alterações nos nossos testes. Se você só tem o texto Base64, cole-o em Base64 para arquivo, que detecta o tipo pelos primeiros bytes, ou direto em Base64 para PDF, no caso de PDFs.
Veja os headers, os corpos e os anexos de um e-mail bruto, decodificados localmente no seu navegador.
Perguntas frequentes
Como abrir um anexo em Base64 a partir do código-fonte de um e-mail?
Copie as linhas Base64 entre a linha em branco da parte e o boundary seguinte e decodifique-as como arquivo. O decodificador de EML extrai os anexos de um arquivo .eml para você.
O que significa =?UTF-8?B? no assunto de um e-mail?
É o início de uma encoded-word da RFC 2047: o charset é UTF-8 e o B indica que o texto entre o próximo ? e o ?= está em Base64. Um Q no lugar do B indica uma variante de quoted-printable.
Por que as linhas Base64 do e-mail têm 76 caracteres?
A RFC 2045 limita as linhas Base64 no MIME a 76 caracteres. 76 caracteres guardam exatamente 57 bytes, e o limite mantém as linhas bem abaixo do máximo de 998 caracteres que o e-mail permite.
Quoted-printable ou Base64: qual é melhor para texto de e-mail?
Quoted-printable para texto quase todo em ASCII, já que continua legível e quase não cresce. Base64 para dados binários e para texto em alfabetos não latinos, em que o quoted-printable pode deixá-lo quase três vezes maior.