개발 이야기3분 읽기

메일 본문의 =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입니다.

규칙

세 줄로 요약됩니다.

  1. 인쇄 가능한 ASCII는 그대로 둡니다
  2. 나머지 바이트는 = 뒤에 16진수 두 자리로 적습니다
  3. 한 줄은 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

charsetEUC-KR이면 같은 바이트가 다른 글자가 됩니다. 옛 메일을 디코딩할 때 글자가 깨진다면 문자셋을 바꿔 시도해 봐야 합니다.

확인 순서

  1. 깨진 텍스트에 = 다음에 16진수 두 자리가 반복되면 quoted-printable입니다
  2. 줄 끝의 =를 이어짐으로 처리했는지 봅니다. 처리하지 않으면 단어가 잘립니다
  3. Content-Transfer-Encoding 헤더를 확인합니다. quoted-printable, base64, 7bit, 8bit가 옵니다
  4. 디코딩 후에도 깨지면 charset을 봅니다. UTF-8이 아닌 옛 인코딩일 수 있습니다
  5. 제목이 =?UTF-8?...?= 형태로 보이면 본문이 아니라 헤더 디코딩이 빠진 것입니다

tools.onuel.devQuoted-Printable 인코딩·디코딩에 메일에서 복사한 줄을 넣으면 문자 인코딩을 골라 디코딩할 수 있습니다. 같은 문장을 Base64로도 인코딩해 보면 위 표의 크기 차이가 그대로 나오고, 바이트를 직접 대조할 때는 Hex 덤프를 씁니다.

  • #MIME
  • #인코딩
  • #이메일