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 end
-- create an empty source -- 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 function()
return nil, err return nil, err
end end
@ -60,7 +69,7 @@ function source.file(handle, io_err)
if not chunk then handle:close() end if not chunk then handle:close() end
return chunk return chunk
end end
else source.empty(io_err or "unable to open file") end else source.error(io_err or "unable to open file") end
end end
-- turns a fancy source into a simple source -- turns a fancy source into a simple source
@ -83,7 +92,7 @@ function source.string(s)
if chunk ~= "" then return chunk if chunk ~= "" then return chunk
else return nil end else return nil end
end end
else source.empty() end else return source.empty() end
end end
-- creates rewindable source -- creates rewindable source
@ -166,7 +175,7 @@ function sink.file(handle, io_err)
end end
return handle:write(chunk) return handle:write(chunk)
end end
else sink.null() end else return sink.error(io_err or "unable to open file") end
end end
-- creates a sink that discards data -- creates a sink that discards data
@ -178,6 +187,13 @@ function sink.null()
return null return null
end 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 -- chains a sink with a filter
function sink.chain(f, snk) function sink.chain(f, snk)
return function(chunk, err) 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 "auxiliar.lch"
#include "url.lch" #include "url.lch"
#include "mime.lch" #include "mime.lch"
#include "tp.lch"
#include "smtp.lch" #include "smtp.lch"
#include "http.lch" #include "http.lch"
#else #else
@ -83,6 +84,7 @@ static int mod_open(lua_State *L, const luaL_reg *mod)
lua_dofile(L, "auxiliar.lua"); lua_dofile(L, "auxiliar.lua");
lua_dofile(L, "url.lua"); lua_dofile(L, "url.lua");
lua_dofile(L, "mime.lua"); lua_dofile(L, "mime.lua");
lua_dofile(L, "tp.lua");
lua_dofile(L, "smtp.lua"); lua_dofile(L, "smtp.lua");
lua_dofile(L, "http.lua"); lua_dofile(L, "http.lua");
#endif #endif

View File

@ -22,140 +22,95 @@ function stuff()
return ltn12.filter.cycle(dot, 2) return ltn12.filter.cycle(dot, 2)
end end
-- tries to get a pattern from the server and closes socket on error local function skip(a, b, c)
local function try_receiving(connection, pattern) return b, c
local data, message = connection:receive(pattern)
if not data then connection:close() end
print(data)
return data, message
end end
-- tries to send data to server and closes socket on error function psend(control, mailt)
local function try_sending(connection, data) socket.try(control:command("EHLO", mailt.domain or DOMAIN))
local sent, message = connection:send(data) socket.try(control:check("2.."))
if not sent then connection:close() end socket.try(control:command("MAIL", "FROM:" .. mailt.from))
io.write(data) socket.try(control:check("2.."))
return sent, message if type(mailt.rcpt) == "table" then
end for i,v in ipairs(mailt.rcpt) do
socket.try(control:command("RCPT", "TO:" .. v))
-- 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
end end
else return try_sending(connection, instruction.raw) end else
end socket.try(control:command("RCPT", "TO:" .. mailt.rcpt))
-- 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
end 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 end
-- closes the underlying connection local seqno = 0
function metatable.__index:close() local function newboundary()
self.connection:close() seqno = seqno + 1
return string.format('%s%05d==%05u', os.date('%d%m%Y%H%M%S'),
math.random(0, 99999), seqno)
end end
-- connect with server and return a smtp connection object local function sendmessage(mesgt)
function connect(host) -- send headers
local connection, message = socket.connect(host, PORT) if mesgt.headers then
if not connection then return nil, message end for i,v in pairs(mesgt.headers) do
return setmetatable({ connection = connection }, metatable) 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 end
-- simple test drive function message(mesgt)
local co = coroutine.create(function() sendmessage(mesgt) end)
return function() return skip(coroutine.resume(co)) end
end
--[[ function send(mailt)
c, m = connect("localhost") local control, err = socket.tp.connect(mailt.server or SERVER,
assert(c, m) mailt.port or PORT)
assert(c:execute {check = "2.." }) if not control then return nil, err end
assert(c:execute {{command = "EHLO", argument = "localhost"}, {check = "2.."}}) local status, err = pcall(psend, control, mailt)
assert(c:execute {command = "MAIL", argument = "FROM:<diego@princeton.edu>"}) control:close()
assert(c:execute {check = "2.."}) if status then return true
assert(c:execute {command = "RCPT", argument = "TO:<diego@cs.princeton.edu>"}) else return nil, err end
assert(c:execute {check = function (code) return code == "250" end}) 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()
]]
return smtp return smtp

View File

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