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