From 2c160627e51650f98d6ef01ae36bb86d6e91045f Mon Sep 17 00:00:00 2001 From: Diego Nehab Date: Thu, 18 Mar 2004 07:01:14 +0000 Subject: [PATCH] Message source in smtp.lua is a work of art. --- src/ltn12.lua | 24 +++++- src/luasocket.c | 2 + src/smtp.lua | 203 +++++++++++++++++++----------------------------- src/tp.lua | 54 ++++++------- 4 files changed, 123 insertions(+), 160 deletions(-) diff --git a/src/ltn12.lua b/src/ltn12.lua index de7103d..f43e975 100644 --- a/src/ltn12.lua +++ b/src/ltn12.lua @@ -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) diff --git a/src/luasocket.c b/src/luasocket.c index 5b19696..eadb758 100644 --- a/src/luasocket.c +++ b/src/luasocket.c @@ -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 diff --git a/src/smtp.lua b/src/smtp.lua index 6b02d14..0bebce3 100644 --- a/src/smtp.lua +++ b/src/smtp.lua @@ -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:"}) -assert(c:execute {check = "2.."}) -assert(c:execute {command = "RCPT", argument = "TO:"}) -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 diff --git a/src/tp.lua b/src/tp.lua index d8dabc0..3912fab 100644 --- a/src/tp.lua +++ b/src/tp.lua @@ -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()