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编码比较简单,扫描要编码的内容,对每个字节进行处理:
- 如果是空格符(0x20),用‘_’替换;
- 如果是[33, 127),并且不是特殊限制字符{=_?\"#$%&'(),.:;<>@[\\]^`{|}~},直接用原始字符加入,不做处理;
- 其他字符,用‘=’加内码信息替换。
三、Email Subject和附件名的表达格式
有了Base64和Quoted-Printable的编码方式,要有一定的格式指示采用的哪种传输编码,同时还要指定编码的字符所采用的字符编码方式。
Email的Subject和附件名的表达格式:<prefix><charset>?<encodeMode>?<encodedContent><suffix>
其中,
- <prefix> 固定为“=?”;
- <charset> 为字符编码格式;
- <encodeMode> 为传输编码格式:B代表Base64;Q代表Quote-Printable
- <encodedContent> 为用encodeMode 编码过的字符编码为charset的字符串
- <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' '=' '=' 编码后的字符
编码过程:
- 把要编码的内容(“吕晶晶jj9.jpg”UTF-8编码的内容)按照3个字节一组分组[Line#1];
- 每6bits拆分,得到在字符表中的索引[Line#3&4;Line#7&8; Line#11&12; Line#15&16; Line#19&20];
- 通过索引查表,得到编码后的字符[Line#5; Line#9; Line#13; Line#7; Line#21];
- 对未最后一个字节做处理[Line#22~#25]。
所以,得到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()通过对原始字串的分析,判定是否一定要编码。
- 如果字串中只包含一般可打印字符,没必要编码;
- 如果字串中包含控制字符、大于127的字符,一定要进行编码。
4.2.2 编码的选择
编码的选择包括字符编码的选择和传输编码的选择。
字符编码的选择通过EncoderUtil#determineCharset()进行。
- 如果要编码的字串中的字符中UnicodeCodePoint有大于0xFF,进行UTF-8编码;
- 如果要编码的字串中的字符中UnicodeCodePoint有大于0x7F,进行ISO-8859-1编码;
- 否则,进行US-ASCII编码。
传输编码的选择通过EncoderUtil#determineEncoding ()进行。
determineEncoding查看要编码的字串中的需要Quoted-Printable编码的字符所占的比例,只有需要编码的比例低于30%时,才采用Quoted-Printable编码,不然一律采用Base64编码。
4.2.3 编码的实现
通过encodeB()进行Base64编码;还是通过encodeQ()进行Quoted-Printable编码。
4.3 通过加编码信息解决问题
Android Email的实现中,对
- 接收到邮件的Subject和附件名称以及其他字段,都进行了解码操作;
- 发送/保存邮件时,只是对Subject进行了编码,对附件名称没有进行编码。
所以,在接收到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