메일 본문의 =E2=80=94은 무엇인가
영문에는 base64보다 작고 한글에는 두 배 큽니다. quoted-printable이 무엇을 하고 언제 쓰이는지 정리했습니다.
메일 원문을 열어 보면 이런 줄이 나옵니다.
Total: 42 EUR =E2=80=94 thanks
=E2=80=94은 em 대시(—)의 UTF-8 바이트 세 개입니다. quoted-printable 인코딩입니다.
왜 인코딩이 필요한가
SMTP는 7비트 ASCII만 안전하게 전달하도록 설계됐습니다. 8비트 바이트를 그대로 보내면 중간 서버가 최상위 비트를 떨어뜨리거나 줄바꿈을 바꿀 수 있습니다. 그래서 MIME는 본문을 7비트로 바꿔 보내는 방식을 두 가지 정의했습니다 — base64와 quoted-printable입니다.
규칙
세 줄로 요약됩니다.
- 인쇄 가능한 ASCII는 그대로 둡니다
- 나머지 바이트는
=뒤에 16진수 두 자리로 적습니다 - 한 줄은 76자를 넘지 않습니다. 넘으면 줄 끝에
=를 붙여 이어짐을 표시합니다
= 자체도 이스케이프 대상입니다.
a=b → a=3Db
이걸 잊으면 디코더가 =b 다음 글자를 16진수로 읽으려다 깨집니다.
소프트 줄바꿈
76자 제한을 넘으면 줄을 끊고 끝에 =를 답니다.
100바이트를 인코딩하면 줄이 2개가 된다
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=
xxx
줄 끝의 =는 줄바꿈이 아니라 "여기서 끊긴 게 아니다"라는 표시입니다. 디코딩하면 그 자리에 아무것도 남지 않습니다. 반면 = 없이 끝나는 줄은 실제 줄바꿈이 있는 것입니다.
메일 본문을 정규식으로 파싱하다 이 둘을 구분하지 않으면 단어가 붙거나 문단이 어긋납니다.
base64와 어느 쪽이 작은가
내용에 따라 완전히 갈립니다.
| 입력 | 원본 | quoted-printable | base64 |
|---|---|---|---|
| 영문 위주 문장 | 57바이트 | 57 | 76 |
| 영문에 em 대시 하나 | 24바이트 | 30 | 32 |
| 한글 문장 | 65바이트 | 177 | 88 |
영문은 거의 그대로 통과하므로 원본과 같은 크기입니다. base64는 내용과 무관하게 4/3배가 됩니다.
한글은 반대입니다. 모든 바이트가 비ASCII라 전부 =XX 세 글자로 바뀌어 3배가 됩니다. base64의 1.33배보다 훨씬 큽니다.
원본 65바이트 → QP 177바이트 (2.7배)
→ base64 88바이트 (1.35배)
그래서 실무에서는 이렇게 갈립니다.
- 영문 위주 본문 → quoted-printable. 크기도 작고, 원문을 열었을 때 사람이 읽을 수 있습니다
- 한국어·일본어·중국어 본문 → base64
- 첨부 파일(바이너리) → base64
메일 클라이언트가 자동으로 고르므로 직접 정할 일은 드물지만, 왜 어떤 메일은 읽히고 어떤 메일은 문자 덩어리인지가 여기서 갈립니다.
헤더는 또 다른 규칙
본문과 헤더의 인코딩이 다릅니다. 헤더에는 encoded-word를 씁니다.
Subject: =?UTF-8?Q?=EC=95=88=EB=85=95?=
Subject: =?UTF-8?B?7JWI64WV?=
=?문자셋?인코딩?데이터?= 형식입니다. Q가 quoted-printable, B가 base64입니다. 본문의 quoted-printable과 규칙이 조금 다릅니다 — 공백을 _로 적을 수 있고, 한 encoded-word가 75자를 넘을 수 없습니다.
제목이 깨져 보일 때 이 형식이 그대로 노출되는 경우가 있습니다. 그러면 디코딩 단계가 빠진 것입니다.
문자셋을 알아야 읽을 수 있다
=C3=A9는 UTF-8에서 é지만, 인코딩된 바이트만 봐서는 어느 문자셋인지 알 수 없습니다. 그 정보는 헤더에 있습니다.
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: quoted-printable
charset이 EUC-KR이면 같은 바이트가 다른 글자가 됩니다. 옛 메일을 디코딩할 때 글자가 깨진다면 문자셋을 바꿔 시도해 봐야 합니다.
확인 순서
- 깨진 텍스트에
=다음에 16진수 두 자리가 반복되면 quoted-printable입니다 - 줄 끝의
=를 이어짐으로 처리했는지 봅니다. 처리하지 않으면 단어가 잘립니다 Content-Transfer-Encoding헤더를 확인합니다.quoted-printable,base64,7bit,8bit가 옵니다- 디코딩 후에도 깨지면
charset을 봅니다. UTF-8이 아닌 옛 인코딩일 수 있습니다 - 제목이
=?UTF-8?...?=형태로 보이면 본문이 아니라 헤더 디코딩이 빠진 것입니다
tools.onuel.dev의 Quoted-Printable 인코딩·디코딩에 메일에서 복사한 줄을 넣으면 문자 인코딩을 골라 디코딩할 수 있습니다. 같은 문장을 Base64로도 인코딩해 보면 위 표의 크기 차이가 그대로 나오고, 바이트를 직접 대조할 때는 Hex 덤프를 씁니다.
- #MIME
- #인코딩
- #이메일