You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

223 lines
6.5 KiB
Lua

local base = _G
local math = require("math")
local table = require("table")
local string = require("string")
module("resty.smtp.qp")
--[[
[Quoted-Printable Rules](http://en.wikipedia.org/wiki/Quoted-printable)
* "All characters except printable ASCII characters" or "end of line characters"
must be encoded.
* All printable ASCII characters (decimal 33-126) may be represented by
themselves, except "=" (decimal 61)
* ASCII tab (decimal 9) and space (decimal 32) may be represented by themselves,
except if these characters would appear at the end of the encoded line. In
that case, they would need to be escaped as "=09" or "=20" (what we use), or
be followed by a "=" (soft line break).
* If the data being encoded contains meaningful line breaks, they must be
encoded as an ASCII CRLF sequence. Conversely, if byte value 13 and 10 have
meanings other than end of line (in media types, for example), they must
be encoded as "=0D" and "=0A" respectively.
* Lines of Quoted-Printable encoded data must not be longer than 76 characters.
To satisfy this requirement without altering the encoded text,
_soft line breaks_ consists of an "=" at the end of an encoded line, and does
not appear as a line break in the decoded text.
Encoded-Word - A slightly modified version of Quoted-Printable used in message
headers.
[RFC 2045](http://tools.ietf.org/html/rfc2045#page-19)
--]]
local QP_PLAIN = 0
local QP_QUOTE = 1
local QP_IF_LAST = 3
local QP_BYTE_CR = 13 -- '\r'
local QP_BYTE_LF = 10 -- '\n'
qpte = {
-- 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 2, 1, 1, -- 0 - 15
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -- 16 - 31
3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -- 32 - 47
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, -- 48 - 63
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -- 64 - 79
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -- 80 - 95
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -- 96 - 111
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, -- 112 - 127
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -- 128 - 143
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -- 144 - 159
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -- 160 - 175
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -- 176 - 191
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -- 192 - 207
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -- 208 - 223
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -- 224 - 239
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -- 240 - 255
}
qptd = {
}
local HEX_BASE = "0123456789ABCDEF"
local quote = function(byte)
local f, s = math.floor(byte/16) + 1, math.fmod(byte, 16) + 1
return table.concat({ '=', HEX_BASE:sub(f, f), HEX_BASE:sub(s, s) })
end
local HEX_LOOKUP = {
['0']= 0, ['1']= 1, ['2']= 2, ['3']= 3,
['4']= 4, ['5']= 5, ['6']= 6, ['7']= 7,
['8']= 8, ['9']= 9, ['a']= 10, ['b']= 11,
['c']= 12, ['d']= 13, ['e']= 14, ['f']= 15,
['A']= 10, ['B']= 11,
['C']= 12, ['D']= 13, ['E']= 14, ['F']= 15,
}
local unquote = function(fp, sp)
local hp, lp = HEX_LOOKUP[fp], HEX_LOOKUP[sp]
if not hp or not lp then return nil
else return string.char(hp * 16 + lp) end
end
local printable = function(char)
local byte = string.byte(char)
return (byte > 31) and (byte < 127)
end
function pad(chunk)
local buffer, byte = "", 0
for i = 1, #chunk do
byte = string.byte(chunk, i)
if qpte[byte + 1] == QP_PLAIN then
buffer = buffer .. string.char(byte)
else
buffer = buffer .. quote(byte)
end
end
-- soft break
if #buffer > 0 then buffer = buffer .. "=\r\n" end
return buffer
end
function encode(chunk, marker)
local atom, buffer = {}, ""
for i = 1, #chunk do
table.insert(atom, string.byte(chunk, i))
repeat
local shift = 1
if atom[1] == QP_BYTE_CR then
if #atom < 2 then -- need more
break
elseif atom[2] == QP_BYTE_LF then
buffer, shift = buffer .. marker, 2
else
buffer = buffer .. quote(atom[1])
end
elseif qpte[atom[1] + 1] == QP_IF_LAST then
if #atom < 3 then -- need more
break
elseif atom[2] == QP_BYTE_CR and atom[3] == QP_BYTE_LF then
buffer, shift = buffer .. quote(atom[1]) .. marker, 3
else -- space not in the end
buffer = buffer .. string.char(atom[1])
end
elseif qpte[atom[1] + 1] == QP_QUOTE then
buffer = buffer .. quote(atom[1])
else -- printable char
buffer = buffer .. string.char(atom[1])
end
-- shift out used chars
for i = 1, 3 do atom[i] = atom[i + shift] end
until #atom == 0
end
for i = 1, 3 do
atom[i] = atom[i] and string.char(atom[i]) or ""
end
return buffer, table.concat(atom, "")
end
function decode(chunk)
local atom, buffer = {}, ""
for i = 1, #chunk do
table.insert(atom, chunk:sub(i, i))
repeat
local shift = 3
if atom[1] == '=' then
if #atom < 3 then -- need more
break
elseif atom[2] == '\r' and atom[3] == '\n' then
-- eliminate soft line break
else
local char = unquote(atom[2], atom[3])
if not char then
buffer = buffer .. table.concat(atom, "")
else
buffer = buffer .. char
end
end
elseif atom[1] == '\r' then
if #atom < 2 then -- need more
break
elseif atom[2] == '\n' then
buffer, shift = buffer .. "\r\n", 2
else -- neglect this '\r' and following char
shift = 2
end
else
if atom[1] == '\t' or printable(atom[1]) then
buffer, shift = buffer .. atom[1], 1
end
end
-- shift out used chars
for i = 1, 3 do atom[i] = atom[i + shift] end
until #atom == 0
end
return buffer, table.concat(atom, "")
end