Message source in smtp.lua is a work of art.

This commit is contained in:
Diego Nehab 2004-03-18 07:01:14 +00:00
parent bcc0c2a9f0
commit 2c160627e5
4 changed files with 123 additions and 160 deletions

View File

@ -46,7 +46,16 @@ function filter.chain(...)
end
-- create an empty source
function source.empty(err)
local function empty()
return nil
end
function source.empty()
return empty
end
-- returns a source that just outputs an error
function source.error(err)
return function()
return nil, err
end
@ -60,7 +69,7 @@ function source.file(handle, io_err)
if not chunk then handle:close() end
return chunk
end
else source.empty(io_err or "unable to open file") end
else source.error(io_err or "unable to open file") end
end
-- turns a fancy source into a simple source
@ -83,7 +92,7 @@ function source.string(s)
if chunk ~= "" then return chunk
else return nil end
end
else source.empty() end
else return source.empty() end
end
-- creates rewindable source
@ -166,7 +175,7 @@ function sink.file(handle, io_err)
end
return handle:write(chunk)
end
else sink.null() end
else return sink.error(io_err or "unable to open file") end
end
-- creates a sink that discards data
@ -178,6 +187,13 @@ function sink.null()
return null
end
-- creates a sink that just returns an error
function sink.error(err)
return function()
return nil, err
end
end
-- chains a sink with a filter
function sink.chain(f, snk)
return function(chunk, err)

View File

@ -76,6 +76,7 @@ static int mod_open(lua_State *L, const luaL_reg *mod)
#include "auxiliar.lch"
#include "url.lch"
#include "mime.lch"
#include "tp.lch"
#include "smtp.lch"
#include "http.lch"
#else
@ -83,6 +84,7 @@ static int mod_open(lua_State *L, const luaL_reg *mod)
lua_dofile(L, "auxiliar.lua");
lua_dofile(L, "url.lua");
lua_dofile(L, "mime.lua");
lua_dofile(L, "tp.lua");
lua_dofile(L, "smtp.lua");
lua_dofile(L, "http.lua");
#endif

View File

@ -22,140 +22,95 @@ function stuff()
return ltn12.filter.cycle(dot, 2)
end
-- tries to get a pattern from the server and closes socket on error
local function try_receiving(connection, pattern)
local data, message = connection:receive(pattern)
if not data then connection:close() end
print(data)
return data, message
local function skip(a, b, c)
return b, c
end
-- tries to send data to server and closes socket on error
local function try_sending(connection, data)
local sent, message = connection:send(data)
if not sent then connection:close() end
io.write(data)
return sent, message
end
-- gets server reply
local function get_reply(connection)
local code, current, separator, _
local line, message = try_receiving(connection)
local reply = line
if message then return nil, message end
_, _, code, separator = string.find(line, "^(%d%d%d)(.?)")
if not code then return nil, "invalid server reply" end
if separator == "-" then -- reply is multiline
repeat
line, message = try_receiving(connection)
if message then return nil, message end
_,_, current, separator = string.find(line, "^(%d%d%d)(.)")
if not current or not separator then
return nil, "invalid server reply"
end
reply = reply .. "\n" .. line
-- reply ends with same code
until code == current and separator == " "
end
return code, reply
end
-- metatable for server connection object
local metatable = { __index = {} }
-- switch handler for execute function
local switch = {}
-- execute the "check" instruction
function switch.check(connection, instruction)
local code, reply = get_reply(connection)
if not code then return nil, reply end
if type(instruction.check) == "function" then
return instruction.check(code, reply)
else
if string.find(code, instruction.check) then return code, reply
else return nil, reply end
end
end
-- stub for invalid instructions
function switch.invalid(connection, instruction)
return nil, "invalid instruction"
end
-- execute the "command" instruction
function switch.command(connection, instruction)
local line
if instruction.argument then
line = instruction.command .. " " .. instruction.argument .. "\r\n"
else line = instruction.command .. "\r\n" end
return try_sending(connection, line)
end
function switch.raw(connection, instruction)
if type(instruction.raw) == "function" then
local f = instruction.raw
while true do
local chunk, new_f = f()
if not chunk then return nil, new_f end
if chunk == "" then return true end
f = new_f or f
local code, message = try_sending(connection, chunk)
if not code then return nil, message end
function psend(control, mailt)
socket.try(control:command("EHLO", mailt.domain or DOMAIN))
socket.try(control:check("2.."))
socket.try(control:command("MAIL", "FROM:" .. mailt.from))
socket.try(control:check("2.."))
if type(mailt.rcpt) == "table" then
for i,v in ipairs(mailt.rcpt) do
socket.try(control:command("RCPT", "TO:" .. v))
end
else return try_sending(connection, instruction.raw) end
end
-- finds out what instruction are we dealing with
local function instruction_type(instruction)
if type(instruction) ~= "table" then return "invalid" end
if instruction.command then return "command" end
if instruction.check then return "check" end
if instruction.raw then return "raw" end
return "invalid"
end
-- execute a list of instructions
function metatable.__index:execute(instructions)
if type(instructions) ~= "table" then error("instruction expected", 1) end
if not instructions[1] then instructions = { instructions } end
local code, message
for _, instruction in ipairs(instructions) do
local type = instruction_type(instruction)
code, message = switch[type](self.connection, instruction)
if not code then break end
else
socket.try(control:command("RCPT", "TO:" .. mailt.rcpt))
end
return code, message
socket.try(control:check("2.."))
socket.try(control:command("DATA"))
socket.try(control:check("3.."))
socket.try(control:source(ltn12.source.chain(mailt.source, stuff())))
socket.try(control:send("\r\n.\r\n"))
socket.try(control:check("2.."))
socket.try(control:command("QUIT"))
socket.try(control:check("2.."))
end
-- closes the underlying connection
function metatable.__index:close()
self.connection:close()
local seqno = 0
local function newboundary()
seqno = seqno + 1
return string.format('%s%05d==%05u', os.date('%d%m%Y%H%M%S'),
math.random(0, 99999), seqno)
end
-- connect with server and return a smtp connection object
function connect(host)
local connection, message = socket.connect(host, PORT)
if not connection then return nil, message end
return setmetatable({ connection = connection }, metatable)
local function sendmessage(mesgt)
-- send headers
if mesgt.headers then
for i,v in pairs(mesgt.headers) do
coroutine.yield(i .. ':' .. v .. "\r\n")
end
end
-- deal with multipart
if type(mesgt.body) == "table" then
local bd = newboundary()
-- define boundary and finish headers
coroutine.yield('mime-version: 1.0\r\n')
coroutine.yield('content-type: multipart/mixed; boundary="' ..
bd .. '"\r\n\r\n')
-- send preamble
if mesgt.body.preamble then coroutine.yield(mesgt.body.preamble) end
-- send each part separated by a boundary
for i, m in ipairs(mesgt.body) do
coroutine.yield("\r\n--" .. bd .. "\r\n")
sendmessage(m)
end
-- send last boundary
coroutine.yield("\r\n--" .. bd .. "--\r\n\r\n")
-- send epilogue
if mesgt.body.epilogue then coroutine.yield(mesgt.body.epilogue) end
-- deal with a source
elseif type(mesgt.body) == "function" then
-- finish headers
coroutine.yield("\r\n")
while true do
local chunk, err = mesgt.body()
if err then return nil, err
elseif chunk then coroutine.yield(chunk)
else break end
end
-- deal with a simple string
else
-- finish headers
coroutine.yield("\r\n")
coroutine.yield(mesgt.body)
end
end
-- simple test drive
function message(mesgt)
local co = coroutine.create(function() sendmessage(mesgt) end)
return function() return skip(coroutine.resume(co)) end
end
--[[
c, m = connect("localhost")
assert(c, m)
assert(c:execute {check = "2.." })
assert(c:execute {{command = "EHLO", argument = "localhost"}, {check = "2.."}})
assert(c:execute {command = "MAIL", argument = "FROM:<diego@princeton.edu>"})
assert(c:execute {check = "2.."})
assert(c:execute {command = "RCPT", argument = "TO:<diego@cs.princeton.edu>"})
assert(c:execute {check = function (code) return code == "250" end})
assert(c:execute {{command = "DATA"}, {check = "3.."}})
assert(c:execute {{raw = "This is the message\r\n.\r\n"}, {check = "2.."}})
assert(c:execute {{command = "QUIT"}, {check = "2.."}})
c:close()
]]
function send(mailt)
local control, err = socket.tp.connect(mailt.server or SERVER,
mailt.port or PORT)
if not control then return nil, err end
local status, err = pcall(psend, control, mailt)
control:close()
if status then return true
else return nil, err end
end
return smtp

View File

@ -18,32 +18,18 @@ setfenv(1, socket.tp)
TIMEOUT = 60
-- tries to get a pattern from the server and closes socket on error
local function try_receiving(sock, pattern)
local data, message = sock:receive(pattern)
if not data then sock:close() end
return data, message
end
-- tries to send data to server and closes socket on error
local function try_sending(sock, data)
local sent, message = sock:send(data)
if not sent then sock:close() end
return sent, message
end
-- gets server reply
local function get_reply(sock)
local code, current, separator, _
local line, message = try_receiving(sock)
local line, err = sock:receive()
local reply = line
if message then return nil, message end
if err then return nil, err end
_, _, code, separator = string.find(line, "^(%d%d%d)(.?)")
if not code then return nil, "invalid server reply" end
if separator == "-" then -- reply is multiline
repeat
line, message = try_receiving(sock)
if message then return nil, message end
line, err = sock:receive()
if err then return nil, err end
_,_, current, separator = string.find(line, "^(%d%d%d)(.)")
if not current or not separator then
return nil, "invalid server reply"
@ -58,29 +44,25 @@ end
-- metatable for sock object
local metatable = { __index = {} }
-- execute the "check" instr
function metatable.__index:check(ok)
local code, reply = get_reply(self.sock)
if not code then return nil, reply end
if type(ok) ~= "function" then
if type(ok) ~= "table" then ok = {ok} end
for i, v in ipairs(ok) do
if string.find(code, v) then return code, reply end
if type(ok) == "table" then
for i, v in ipairs(ok) do
if string.find(code, v) then return code, reply end
end
return nil, reply
else
if string.find(code, ok) then return code, reply
else return nil, reply end
end
return nil, reply
else return ok(code, reply) end
end
function metatable.__index:cmdchk(cmd, arg, ok)
local code, err = self:command(cmd, arg)
if not code then return nil, err end
return self:check(ok)
end
-- execute the "command" instr
function metatable.__index:command(cmd, arg)
if arg then return try_sending(self.sock, cmd .. " " .. arg.. "\r\n")
return try_sending(self.sock, cmd .. "\r\n") end
if arg then return self.sock:send(cmd .. " " .. arg.. "\r\n")
else return self.sock:send(cmd .. "\r\n") end
end
function metatable.__index:sink(snk, pat)
@ -88,6 +70,14 @@ function metatable.__index:sink(snk, pat)
return snk(chunk, err)
end
function metatable.__index:send(data)
return self.sock:send(data)
end
function metatable.__index:receive(pat)
return self.sock:receive(pat)
end
function metatable.__index:source(src, instr)
while true do
local chunk, err = src()