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



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章