From 2c160627e51650f98d6ef01ae36bb86d6e91045f Mon Sep 17 00:00:00 2001
From: Diego Nehab <diego@tecgraf.puc-rio.br>
Date: Thu, 18 Mar 2004 07:01:14 +0000
Subject: Message source in smtp.lua is a work of art.

---
 src/ltn12.lua   |  24 +++++--
 src/luasocket.c |   2 +
 src/smtp.lua    | 193 ++++++++++++++++++++++----------------------------------
 src/tp.lua      |  54 +++++++---------
 4 files changed, 118 insertions(+), 155 deletions(-)

(limited to 'src')

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)
+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
-        if string.find(code, instruction.check) then return code, reply
-        else return nil, reply end
+        socket.try(control:command("RCPT", "TO:" .. mailt.rcpt))
     end
+    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
 
--- 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)
+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
 
-function switch.raw(connection, instruction)
-    if type(instruction.raw) == "function" then
-        local f = instruction.raw
+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, 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
+            local chunk, err = mesgt.body()
+            if err then return nil, err
+            elseif chunk then coroutine.yield(chunk)
+            else break end
         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
+    -- deal with a simple string
+    else
+        -- finish headers
+        coroutine.yield("\r\n")
+        coroutine.yield(mesgt.body)
     end
-    return code, message
 end
 
--- closes the underlying connection
-function metatable.__index:close()
-    self.connection:close()
+function message(mesgt)
+    local co = coroutine.create(function() sendmessage(mesgt) end)
+    return function() return skip(coroutine.resume(co)) end
 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)
+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
 
--- simple test drive 
-
---[[
-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()
-]]
-
 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()
-- 
cgit v1.2.3-55-g6feb