收邮件这件事,协议层(SMTP 收信)其实没多难,真正磨人的是拿到原始报文之后:标题是一串 =?GBK?B?xPq1...?=,正文是一堆 =C4=FA=B5=C4,同一封信里 HTML 和纯文本各一份,外面还可能再套一层附
|
收邮件这件事,协议层(SMTP 收信)其实没多难,真正磨人的是拿到原始报文之后:标题是一串 =?GBK?B?xPq1...?=,正文是一堆 =C4=FA=B5=C4,同一封信里 HTML 和纯文本各一份,外面还可能再套一层附件。 测试用的原始报文先造一封国内系统很常见的验证码邮件:发件人和标题用 GBK/GB2312 编码,正文是 multipart/alternative,纯文本部分走 quoted-printable,HTML 部分走 base64。
注意 Subject:前半截是 GBK,后半截是 UTF-8,两个 encoded-word 之间隔了一个空格。这不是我故意刁难,转发、代发、模板拼接之后的真实邮件里经常出现。 坑一:标题解码,mime.WordDecoder 默认不认 GBKGo 的 mime.WordDecoder 能处理 RFC 2047 的 =?charset?B/Q?...?= 格式,但它只内置了 UTF-8、ISO-8859-1 和 US-ASCII,遇到 GBK 直接报错。要自己给它一个 CharsetReader:
两个细节:
坑二:multipart.Reader.NextPart 会偷偷帮你解 QP遍历 multipart 的时候,大部分教程写的是 mr.NextPart()。它有个不太显眼的行为:如果子部分是 quoted-printable,它会自动解码,并且把 Content-Transfer-Encoding 头从 Header 里删掉。 这本来是好意,但如果你的代码后面统一根据 CTE 头做解码,就会出现两种结果:QP 部分被"自动"解了,你拿不到 CTE 头以为它是原文;base64 部分它不管,你又得自己解。行为不一致,出问题很难查。 我的建议是用 NextRawPart()(Go 1.14 起有),所有传输编码自己处理,逻辑统一:
递归是必须的:multipart/mixed 里套 multipart/alternative 再套 multipart/related(HTML 内嵌图片)是很常见的三层结构。 解码顺序是"先传输编码、后字符集"。传输编码(QP/base64)是为了让二进制能走 7bit 通道加的一层壳,壳里面才是某个字符集的字节流。反过来做,GBK 的字节会被当成 UTF-8 去解 QP,全乱。 坑三:base64 正文里的杂质标准库的 base64.NewDecoder 能跳过 \r\n,但有些发信系统会在 base64 行尾留空格或 Tab,解码直接报 illegal base64 data。做个最小清洗:
这里为了简单直接 ReadAll 进内存了。附件很大的场景应该写一个流式过滤的 io.Reader,不然几十 MB 的附件会整个进内存两次。 跑起来
import 部分:
go mod init mimedemo && go get golang.org/x/text && go run .,输出:
还没覆盖的部分这份代码能对付大多数验证码、通知类邮件,但离一个完整的邮件解析器还差几块,列出来免得误用:
我在 forxi.cn 上做临时邮箱时,临时邮箱收到的基本就是这类验证码和通知邮件,字符集五花八门。如果你也在做收信相关的东西,建议一开始就把 GB18030 兜底和 NextRawPart 这两件事做对,后面能少查很多乱码工单。 |
2022-04-28
2022-04-21
2022-05-13
2022-08-17
2024-05-07