Quoted-Printable编码原理及代码实现

这篇文章是我之前在RYTong内部分享的一篇文章,摘取了有用的部分。当时帮助某项目邮件系统解决问题,期间了解到Quoted-Printable编码,在此与大家分享下该编码的原理和个人版本的代码实现。

关于规范

关于Quoted-Printable的编码规范,需要参考rfc2045

为了方便大家阅读,在此给大家看一下融合我个人理解的翻译:

  1. 除了换行(CRLF序列)中的CR或LF,所有(8bit)字节都可以表示为符号”=”后跟两个16进制数字(16进制数字表示该字节的值)的格式,16进制数字只能有数字和大写ABCDEF表示,不能用小写字母。例如,ASCII码换页符,数值为12,编码后表示为”=0C”。

  2. 值为33到126的字节(除61外),包含边界,也可以不进行编码。

  3. 制表符(值为9)和空格(值为32)可以不进行编码,但未编码的两者不能位于编码后的行尾。如果行尾由软换行符(符号”=”,参照规则5)时,软换行符前的制表符和空格可以不编码。两者若在行尾则必须编码。

  4. 换行(CRLF序列)不必进行编码。因为只有CRLF序列表示换行,因此包含CR或LF的其他序列须进行编码。

  5. Quoted-Printable编码要求编码后的文本每行长度不得超过76字符。如果原文的一行被编码后的文本长度超过76,那么需要在编码后的行尾添加软换行符”=”,表示原文此处没有换行。注意软换行符”=”也会计入编码后的字节长度,也就是说如果编码后行尾存在软换行符,那么该行其他字符数不得超过75。

  6. 建议对-!\”#$@[\]^`{|}~进行编码,以避免对其他协议或少数网关(如EBCDIC)的处理造成影响。

  7. 建议对CRLF序列进行编码,避免各平台不同的换行序列对解码造成影响。

关于代码实现

以下是本人基于以上规则实现的代码(Erlang实现)

-module(qp_encoder).

-export([qp_encode/1]).

-define(LINE_LENGTH, 76).
-define(is_suggested(C),
        C == 45 orelse C == 64 orelse C == 96 orelse C == 46 orelse
            (C >= 33 andalso C =< 36) orelse
            (C >= 91 andalso C =< 94) orelse
            (C >= 123 andalso C =< 126)).

-define(CR, 13).
-define(LF, 10).
-define(HT, 9).
-define(SPACE, 32).

%%%%%%%%%%%%%%%%%%%%%%%%%% API %%%%%%%%%%%%%%%%%%%%%%%%%%

%% @spec (Input :: binary() | string()) -> Output :: binary()
qp_encode(String) when is_list(String) ->
    qp_encode(list_to_binary(String));
qp_encode(Bin) when is_binary(Bin) ->
    qp_encode(0, Bin, <<>>, <<>>, <<>>).

%% @spec (Input :: binary() | string()) -> Output :: binary()
qp_decode(String) when is_list(String) ->
    qp_decode(list_to_binary(String));
qp_decode(Bin) when is_binary(Bin) ->
    qp_decode(Bin, <<>>, 1, 1).



%%%%%%%%%%%%%%%%%%%%%%%%%% internal %%%%%%%%%%%%%%%%%%%%%%%%%%

%% Num is the total number of characters of LineAcc + LastBuf
%% Bin is the input binary to be encoded
%% LastBuf is the encoded binary of last character
%% LineAcc is the current line of encoded binary
%% Acc is the output binary
qp_encode(Num, Bin, LastBuf, LineAcc, Acc) when Num >= ?LINE_LENGTH ->
    {CurrentLine, LastBuf2, NextLineNum} = qp_new_line(LastBuf, LineAcc),
    qp_encode(NextLineNum, Bin, LastBuf2, <<>>,
              <<Acc/binary, CurrentLine/binary>>);
qp_encode(_, <<>>, LastBuf, LineAcc, Acc) ->
    Encoded =
        case LastBuf of
            <<C>> when C == ?HT orelse C == ?SPACE ->
                do_qp_encode(C);
            _ ->
                LastBuf
        end,
    <<Acc/binary, LineAcc/binary, Encoded/binary>>;
qp_encode(Num, <<C, Rest/binary>>, LastBuf, LineAcc, Acc) ->
    {EncNum, Enc} = inline_qp_encode(C),
    qp_encode(Num + EncNum, Rest, Enc,
              <<LineAcc/binary, LastBuf/binary>>, Acc).

%% nethier HT nor SPACE could be presented at the end of an encoded line
qp_new_line(<<C>>, CurrentLine) ->
    {<<CurrentLine/binary, $=, ?CR, ?LF>>, <<C>>, 1};
qp_new_line(LastBuf, CurrentLine) ->
    {<<CurrentLine/binary, $=, ?CR, ?LF>>, LastBuf, 3}.

%% characters(-!\"#$@[\]^`{|}~.) are suggested to be encoded
inline_qp_encode(C) when ?is_suggested(C) ->
    {3, do_qp_encode(C)};
%% character "=" must be encoded
inline_qp_encode($=) ->
    {3, do_qp_encode($=)};
%% character whose ascii code is between 33 and 126 inclusively need not
%% be encoded
inline_qp_encode(C) when C >= 33 andalso C =< 126 ->
    {1, <<C>>};
%% HT and SPACE need not be encoded
inline_qp_encode(C) when C == ?HT orelse C == ?SPACE ->
    {1, <<C>>};
%% others must be encoded
inline_qp_encode(C) ->
    {3, do_qp_encode(C)}.

do_qp_encode(Int) when is_integer(Int) ->
    [Hex1, Hex2] =
        case integer_to_list(Int, 16) of
            [H] ->
                [$0, H];
            _Val ->
                _Val
        end,
    <<$=, Hex1, Hex2>>.

第一条规则由do_qp_encode/1函数实现,这部分值得注意的是integer_to_list/2方法的使用,可以直接将一个整数转成对应的16进制字符串。
第二条规则由67到70行的代码实现,此处用了Erlang模式匹配的技巧,由于会优先匹配68行的函数,因此72行函数判断条件并不需要将“=”号排除。
第三条规则由75和76行的代码实现,大家可能会觉得此处逻辑存在问题,因为编码后行尾是不能存在未编码的制表符(值为9)和空格(值为32)的。这里是因为我按规则7将CRLF进行了编码,因此编码后的每行都是以“=”结束的软换行,所以我并未对制表符和空格进行编码。由于最后一行的编码内容是不存在软换行的,因此我加了46到51行的代码处理,将最后一行末尾出现的制表符和空格进行了编码。
第四条规则和第七条有冲突,这里我按第7条进行了实现。
第五条规则由40到43行代码实现。
第六条规则由65和66行代码实现。大家可以注意到,我在代码逻辑里增加了对“.”号的编码,这是由于我们发现在实际使用(邮件转发)中,发现部分出现在行首的“.”号会被其他服务器删除掉(原因未知),因此特意对该符号进行了编码,编码之后此问题解决。

除了Quoted-Printable编码规则外,大家还可以通过这个例子了解一下Erlang binary的相关语法,用起来还是很方便的。有兴趣的同学可以先学习一下《Programming Erlang》这本书的5.2和5.3章节。

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