Skip to content

0x047-了解邮件MIME格式

基本知识

此处转载出处:
https://www.cnblogs.com/crystalray/p/3302427.html
https://abcdxyzk.github.io/blog/2023/01/23/mail-mine/

总体来说,MIME消息由消息头和消息体两大部分组成。这里,分别称为为邮件头、邮件体。

邮件头

邮件头包含了发件人、收件人、主题、时 间、MIME版本、邮件内容的类型等重要信息。每条信息称为一个域,由域名后加“: ”和信息内容构成,可以是一行,较长的也可以占用多行。域的首行必须“顶头”写,即左边不能有空白字符(空格和制表符);续行则必须以空白字符打头,且第 一个空白字符不是信息本身固有的,解码时要过滤掉。
邮件头中不允许出现空行。有一些邮件不能被邮件客户端软件识别,显示的是原始码,就是因为首行是空行。

例如:

Date: Mon, 29 Jun 2009 18:39:03 +0800
From: "=?gb2312?B?26zQocHB?=" <gaoxl@legendsec.com>
To: "moreorless" <moreorless@live.cn>
Cc: "gxl0620" <gxl0620@163.com>
BCC: "=?gb2312?B?26zQocHB?=" <venus.oso@gmail.com>
Subject: attach
Message-ID: <200906291839032504254@legendsec.com>
X-mailer: Foxmail 6, 15, 201, 21 [cn]
Mime-Version: 1.0
Date: Mon, 29 Jun 2009 18:39:03 +0800
From: "=?gb2312?B?26zQocHB?=" <gaoxl@legendsec.com>
To: "moreorless" <moreorless@live.cn>
Cc: "gxl0620" <gxl0620@163.com>
BCC: "=?gb2312?B?26zQocHB?=" <venus.oso@gmail.com>
Subject: attach
Message-ID: <200906291839032504254@legendsec.com>
X-mailer: Foxmail 6, 15, 201, 21 [cn]
Mime-Version: 1.0

邮件体

在邮件体中,大致有如下一些域:

有的域除了值之外,还带有参数。值与参数、参数与参数之间以“;”分隔。参数名与参数值之间以“=”分隔。

邮件体包含邮件的内容,它的类型由邮件头的“Content-Type”域指出。常见的简单类型有text/plain(纯文本)和text/html(超文本)。

multipart类型,是MIME邮件的精髓。邮件体被分为多个段,每个段又包含段头和段体两部分,这两部分之间也以空行分隔。常见的multipart类型有三种:multipart/mixed, multipart/related和multipart/alternative。从它们的名称,不难推知这些类型各自的含义和用处。它们之间的层次关系可归纳为下图所示:

可以看出,如果在邮件中要添加附件,必须定义multipart/mixed段;如果存在内嵌资源,至少要定义multipart/related段;如果纯文本与超文本共存,至少要定义multipart/alternative段。

MIME编码

参考rfc2047,MIME Part Three:Message Header Extensions for Non-ASCII Text

http://tools.ietf.org/html/rfc2047

MIME编码的两种方法:

对邮件进行编码最初的原因是因为Internet上的很多网关不能正确传输8bit内码的字符,比如汉字等。编码的原理就是把8bit的内容转换成7bit的形式以能正确传输,在接收方收到之后,再将其还原成8bit的内容

MIME是“多用途网际邮件扩充协议”的缩写,在MIME协议之前,邮件的编码曾经有过UUENCODE等编码方式,但是由于MIME协议算法简单,并且易于扩展,现在已经成为邮件编码方式的主流,不仅是用来传输8 bit的字符,也可以用来传送二进制的文件,如邮件附件中的图像、音频等信息,而且扩展了很多基于MIME的应用。

从编码方式来说,MIME 定义了两种编码方法Base64与QP(Quote-Printable):

Base64

Base64是一种通用的方法,其原理很简单,就是把三个Byte的数据用4个Byte表示,这样,这四个Byte中,实际用到的都只有前面6 bit,这样就不存在只能传输7bit的字符的问题了。Base64的缩写一般是“B”。

Base64将输入的字符串或一段数据编码成只含有{'A'-'Z', 'a'-'z', '0'-'9', '+', '/'}这64个字符的串,'='用于填充。其编码的方法是,将输入数据流每次取6bit,用此6bit的值(0-63)作为索引去查表,输出相应字符。这样,每3个字节将编码为4个字符(3×8 → 4×6);不满4个字符的以'='填充。 Base64的算法很简单,它将字符流顺序放入一个24位的缓冲区,缺字符的地方补零。   然后将缓冲区截断成为4个部分,高位在先,每个部分6位,用64个字符重新表示。如果输入只有一个或两个字节,那么输出将用等号“=”补足。这可以隔断附加的信息造成编码的混乱。

QP

另一种方法是QP(Quote-Printable)方法。

https://zh.wikipedia.org/wiki/Quoted-printable

任何8-bit字节值可编码为3个字符:一个等号“=”后跟随两个十六进制数字(0–9或A–F)表示该字节的数值。例如,ASCII码换页符(十进制值为12)可以表示为“=0C”, 等号“=”(十进制值为61)必须表示为“=3D”。除了可打印ASCII字符与换行符以外,所有字符必须表示为这种格式。

所有可打印ASCII字符(十进制值的范围为33到126)可用ASCII字符编码来直接表示, 但是等号“=”(十进制值为61)不可以这样直接表示。

ASCII的水平制表符(tab)与空格符, 十进制为9和32, 如果不出现在行尾则可以用其ASCII字符编码直接表示。如果这两个字符出现在行尾,必须QP编码表示为“=09”(tab)或“=20”(space)。

如果数据中包含有意义的行结束标志,必须转换为ASCII回车(CR)换行(LF)序列,既不能用原来的ASCII字符也不能用QP编码的“=”转义字符序列。 相反,如果字节值13与10有其它的不是行结束的含义,它们必须QP编码为=0D与=0A。

quoted-printable编码的数据的每行长度不能超过76个字符。为满足此要求又不改变被编码文本,在QP编码结果的每行末尾加上“软换行”(soft line break)。即在每行末尾加上一个“=”, 但并不会出现在解码得到的文本中。这种软换行也适用于文本的行非常长,超过了软件限制(例如,某些SMTP软件要求最大行长为1000个字符),这也是RFC 2821允许的。

解析的时候,可以每三个字符进行解析。满足 = 加两个16进制的字符的时候当做此类编码的数据。

代码示例

仅用于参考:

js
str := "=?GB18030?Q?=BD=BB=D2=D7=C1=F7=CB=AE=D6=A4=C3=F7=5F=D3=C3?= =?GB18030?Q?=D3=DA=B8=F6=C8=CB=B6=D4=D5=CB=5F20?= =?GB18030?Q?241120=5F142312.zip?="
// 按空格分割成多个待解析的子串
subStrs := strings.Fields(str)

var filteredChats []string
for _, subStr := range subStrs {
	subStr := strings.TrimPrefix(subStr, "=?GB18030?Q?")
	subStr = strings.TrimSuffix(subStr, "?=")
	filteredChats = append(filteredChats, subStr)
}
result := ""
decoder := simplifiedchinese.GB18030.NewDecoder()
for _, subStr := range filteredChats {
	// 凑够四个字符的时候转换成中文
	gbkTextBuffer := ""
	for i := 0; i < len(subStr); i += 3 {
		sb := subStr[i:Min(len(subStr), i+3)]
		if len(sb) == 3 && strings.HasPrefix(sb, "=") {
			dd := strings.TrimPrefix(sb, "=")
			number, err := strconv.ParseInt(dd, 16, 0)
			if err != nil {
				fmt.Println("ParseInt错误:", err)
				continue
			}
			if number > 128 {
				gbkTextBuffer += strings.TrimPrefix(sb, "=")
				if len(gbkTextBuffer) == 4 {
					decoded, err := hex.DecodeString(gbkTextBuffer)
					if err == nil {
						// 将解码后的字节数据转换为GB18030编码的字符串
						gbs, _, err := transform.String(decoder, string(decoded))
						fmt.Println(gbkTextBuffer, "=>", gbs)
						if err == nil {
							result += gbs
						} else {
							fmt.Println(err)
						}
					} else {
						fmt.Println(err)
					}
					gbkTextBuffer = ""
				}
			} else {
				// ascii
				d1, errr := hex.DecodeString(dd)
				if errr != nil {
					fmt.Println("ascii hex.DecodeString err", dd, errr)
				}
				gbkTextBuffer = ""
				result += string(d1)
			}
		} else {
			gbkTextBuffer = ""
			result += sb
		}
	}
}
fmt.Println(result)
// 交易流水证明_用于个人对账_20241120_142312.zip
str := "=?GB18030?Q?=BD=BB=D2=D7=C1=F7=CB=AE=D6=A4=C3=F7=5F=D3=C3?= =?GB18030?Q?=D3=DA=B8=F6=C8=CB=B6=D4=D5=CB=5F20?= =?GB18030?Q?241120=5F142312.zip?="
// 按空格分割成多个待解析的子串
subStrs := strings.Fields(str)

var filteredChats []string
for _, subStr := range subStrs {
	subStr := strings.TrimPrefix(subStr, "=?GB18030?Q?")
	subStr = strings.TrimSuffix(subStr, "?=")
	filteredChats = append(filteredChats, subStr)
}
result := ""
decoder := simplifiedchinese.GB18030.NewDecoder()
for _, subStr := range filteredChats {
	// 凑够四个字符的时候转换成中文
	gbkTextBuffer := ""
	for i := 0; i < len(subStr); i += 3 {
		sb := subStr[i:Min(len(subStr), i+3)]
		if len(sb) == 3 && strings.HasPrefix(sb, "=") {
			dd := strings.TrimPrefix(sb, "=")
			number, err := strconv.ParseInt(dd, 16, 0)
			if err != nil {
				fmt.Println("ParseInt错误:", err)
				continue
			}
			if number > 128 {
				gbkTextBuffer += strings.TrimPrefix(sb, "=")
				if len(gbkTextBuffer) == 4 {
					decoded, err := hex.DecodeString(gbkTextBuffer)
					if err == nil {
						// 将解码后的字节数据转换为GB18030编码的字符串
						gbs, _, err := transform.String(decoder, string(decoded))
						fmt.Println(gbkTextBuffer, "=>", gbs)
						if err == nil {
							result += gbs
						} else {
							fmt.Println(err)
						}
					} else {
						fmt.Println(err)
					}
					gbkTextBuffer = ""
				}
			} else {
				// ascii
				d1, errr := hex.DecodeString(dd)
				if errr != nil {
					fmt.Println("ascii hex.DecodeString err", dd, errr)
				}
				gbkTextBuffer = ""
				result += string(d1)
			}
		} else {
			gbkTextBuffer = ""
			result += sb
		}
	}
}
fmt.Println(result)
// 交易流水证明_用于个人对账_20241120_142312.zip