Android邮件中的Base64和Quoted-Printable编码

田海立@CSDN

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



你可能感兴趣的:(android)