2012-07-31
Email在网络上传输时,采用MIME(MultipurposeInternet Mail Extensions)。邮件传输只能传送US-ASCII字符,邮件中包含的其他字符必须通过一定的编码转换之后才能传输。对于Subject或/和附件名称为中文字符的邮件,有些邮件系统因为缺少编码(字符编码和传输编码)信息,导致乱码情况的发生。本文分析Android中Email系统的编码——Base64和Quoted-Printable。
邮件的Subject和附件名,用一种简短的格式指示传输编码和字符编码。字符编码是可以是UTF-8、GB2312等;传输编码常用的有BASE64和Quoted-Printable。本文主要看传输编码,关于字符编码的Unicode编码,可以参考《Unicode编码及其实现:UTF-16、UTF-8, and more》。
一、Base64编码
Base64编码在现在网络传输上应用广泛。Base64可以把要转换的内容,转换成可打印字符(包含字符表’A’~’Z’, ‘a’~’z’, ‘0’~’9’, ‘+’, ‘/’,共64个,以及’=’)。
字符表(64个字符,索引只需6bits,即最大0x3F):
索引 |
对应字符 |
索引 |
对应字符 |
索引 |
对应字符 |
索引 |
对应字符 |
0 |
A |
17 |
R |
34 |
i |
51 |
z |
1 |
B |
18 |
S |
35 |
j |
52 |
0 |
2 |
C |
19 |
T |
36 |
k |
53 |
1 |
3 |
D |
20 |
U |
37 |
l |
54 |
2 |
4 |
E |
21 |
V |
38 |
m |
55 |
3 |
5 |
F |
22 |
W |
39 |
n |
56 |
4 |
6 |
G |
23 |
X |
40 |
o |
57 |
5 |
7 |
H |
24 |
Y |
41 |
p |
58 |
6 |
8 |
I |
25 |
Z |
42 |
q |
59 |
7 |
9 |
J |
26 |
a |
43 |
r |
60 |
8 |
10 |
K |
27 |
b |
44 |
s |
61 |
9 |
11 |
L |
28 |
c |
45 |
t |
62 |
+ |
12 |
M |
29 |
d |
46 |
u |
63 |
/ |
13 |
N |
30 |
e |
47 |
v |
|
|
14 |
O |
31 |
f |
48 |
w |
|
|
15 |
P |
32 |
g |
49 |
x |
|
|
16 |
Q |
33 |
h |
50 |
y |
|
|
具体转换规则为:
1. 3字符转换成4个字符;
3个8Bits的字符有24Bits,每6个Bits组成一个BASE64字符表的索引,通过索引找到转换后的字符。
亦即,a7..a0 b7..b0c7..c0 -> A7..A2A1A0B7..B4 B3..B0C7C6C5..C0
A7..A2 第一个字符在字符表中索引;
A1A0B7..B4 第二个字符在字符表中索引;
B3..B0C7C6 第三个字符在字符表中索引;
C5..C0 第四个字符在字符表中索引。
2. 转换后的内容,每76个字符加一个换行符;
3. 最后的不足3个字符的字符要进行特别处理
3.1 若剩余两个字符未处理,则:
这两个剩余的字符与0x00组成一个数据,得到三个字符的索引,最后一个字符用’=’。
亦即,a7..a0 b7..b00..0 -> A7..A2A1A0B7..B4 B3..B000
A7..A2 第一个字符在字符表中索引;
A1A0B7..B4 第二个字符在字符表中索引;
B3..B0 00 第三个字符在字符表中索引;
第四个字符:’=’。
3.2 若剩余一个字符未处理,则:
这个剩余的字符与0x0000组成一个数据,得到两个字符的索引,最后两个字符都用’=’。
亦即,a7..a0 0..00..0 -> A7..A2A1A0 0..0
A7..A2 第一个字符在字符表中索引;
A1A0 0..0 第二个字符在字符表中索引;
第三、第四个字符:‘=’,‘=’。
二、Quoted-Printable编码
Quoted-Printable编码比较简单,扫描要编码的内容,对每个字节进行处理:
三、Email Subject和附件名的表达格式
有了Base64和Quoted-Printable的编码方式,要有一定的格式指示采用的哪种传输编码,同时还要指定编码的字符所采用的字符编码方式。
Email的Subject和附件名的表达格式:<prefix><charset>?<encodeMode>?<encodedContent><suffix>
其中,
比如要把“吕晶晶jj9.jpg”作为Subject或者附件名称通过Email传输。编码过程如下:
3.1.UTF-8编码
E59095 E699B6 E699B6 6A6A392E6A7067 吕 晶 晶 j j 9 . j p g
3.2.Base64编码
E59095 E699B6 E699B6 6A6A39 2E6A7067 3Bytes E59095 -> 111001011001000010010101 二进制 -> 111001 011001 000010 010101 6Bits(二进制) -> 57 25 2 21 索引(十进制) -> '5' 'Z' 'C' 'V' 编码后的字符 E699B6 -> 111001101001100110110110 二进制 -> 111001 101001 100110 110110 6Bits(二进制) -> 57 41 38 54 索引(十进制) -> '5' 'p' 'm' '2' 编码后的字符 E699B6 -> 111001101001100110110110 二进制 -> 111001 101001 100110 110110 6Bits(二进制) -> 57 41 38 54 索引(十进制) -> '5' 'p' 'm' '2' 编码后的字符 6A6A39 -> 011010100110101000111001 二进制 -> 011010 100110 101000 111001 6Bits(二进制) -> 26 38 40 57 索引(十进制) -> 'a' 'm' 'o' '5' 编码后的字符 2E6A70 -> 001011100110101001110000 二进制 -> 001011 100110 101001 110000 6Bits(二进制) -> 11 38 41 48 索引(十进制) -> 'L' 'm' 'p' 'w' 编码后的字符 670000 -> 011001110000000000000000 二进制 -> 011001 110000 000000 000000 6Bits(二进制) -> 25 48 索引(十进制) -> 'Z' 'w' '=' '=' 编码后的字符
编码过程:
所以,得到Base64编码[Line#5;Line#9; Line#13; Line#7; Line#21]:
5ZCV5pm25pm2amo5LmpwZw==
3.3. 最终Base64编码结果
再按格式,加上前缀、字符编码、传输编码及后缀,得到:
=?UTF-8?B?5ZCV5pm25pm2amo5LmpwZw==?=
3.4. Quoted-Printable编码结果
如果传输编码用Quoted-Printable编码,可以得到:
=?UTF-8?Q?=E5=90=95=E6=99=B6=E6=99=B6jj9.jpg?=
编码过程比较简单,读者可参照第二部分的Quoted-Printable编码自行分析。
四、Android中Email相关的实现
Android原生Email的实现中,对Base64、Quoted-Printable的编码和解码是采用第三方开源包mime4j实现的。具体来说,对所有Base64/Quoted-Printable编码过的字段是可以解码的,但是在发送邮件时,只是对Subject进行了编码,对附件名称没有进行编码。这也导致了中文附件名称乱码问题。
传输编码和解码的使用都是通过com.android.email.mail.internet.MimeUtility,调用org.apache.james.mime4j.decoder.DecoderUtil或org.apache.james.mime4j.codec.EncoderUtil实现的。
4.1 解码
com.android.email.mail.internet.MimeUtility中与解码相关的有下面几个static的方法:
public static StringunfoldAndDecode(String s); public static Stringunfold(String s); public static Stringdecode(String s);
unfoldAndDecode包含了unfold和decode两个操作过程。unfold去掉编码过内容的CRLF;decode是真正的解码实现。
decode调用org.apache.james.mime4j.decoder.DecoderUtil#decodeEncodedWords()
decodeEncodedWords()通过判定传输编码,选择通过decodeB()进行Base64解码;还是通过decodeQ()进行Quoted-Printable解码。
4.2 编码
com.android.email.mail.internet.MimeUtility中与编码相关的,有下面几个static的方法:
public static StringfoldAndEncode(String s); public static StringfoldAndEncode2(String s, int usedCharacters) public static Stringfold(String s, int usedCharacters)
foldAndEncode没有做任何操作,foldAndEncode2才真正实现了编码。foldAndEncode2通过org.apache.james.mime4j.codec.EncoderUtil#encodeIfNecessary实现。
4.2.1 是否需要编码
编码过后,会增加字串的长度,并不是非要编码不可的。EncoderUtil #hasToBeEncoded()通过对原始字串的分析,判定是否一定要编码。
4.2.2 编码的选择
编码的选择包括字符编码的选择和传输编码的选择。
字符编码的选择通过EncoderUtil#determineCharset()进行。
传输编码的选择通过EncoderUtil#determineEncoding ()进行。
determineEncoding查看要编码的字串中的需要Quoted-Printable编码的字符所占的比例,只有需要编码的比例低于30%时,才采用Quoted-Printable编码,不然一律采用Base64编码。
4.2.3 编码的实现
通过encodeB()进行Base64编码;还是通过encodeQ()进行Quoted-Printable编码。
4.3 通过加编码信息解决问题
Android Email的实现中,对
所以,在接收到Android Email客户端发送的带有中文附件的邮件,会发生附件名是乱码的问题。解决方式是在发送或保存邮件地方,对附件名称进行本文前段论述的编码。
五、仍然未决的问题
4.4 的解决方式,能够解决新发送邮件的问题,但是对于存量的已经存在的邮件,它们的附件名称还是乱码。而且没有经过编码的邮件用别的邮件客户端(比如Outlook)接收,能够正确解析出附件的名称,这也说明即便没有进行编码和指定编码格式,客户端也是可以解码的。只是笔者通过试验,还是没搞懂具体怎么隐含编码/解码的。如果有知道如何实现的,望读者不吝赐教!
下面是通过Android Email客户端发送附件名称为“吕晶晶jj9.jpg”,接收到的附件名称,不知道是如何编/解码的?
发送的UTF-8名称
E59095 E699B6 E699B6 6A6A392E6A7067 吕 晶 晶 j j 9 . j p g
接收到的名称(这是什么样的编码?下面的十六进制编码是从收到的邮件的附件名里抓取到的,有谁知道其编码原则,望不吝赐教!)
C3A5C290C295 C3A6C299C2B6 C3A6C299C2B6 6A6A392E6A7067 吕 晶 晶 j j 9 . j p g