First version of generic SMTP code and of the base64 encoding.

Gonna rewrite the base64 stuff (found a better way).
This commit is contained in:
Diego Nehab 2003-10-21 01:12:23 +00:00
parent 24fbcf3470
commit 9bc4e0648a
3 changed files with 171 additions and 295 deletions

33
TODO
View File

@ -1,3 +1,34 @@
Read about
250-ENHANCEDSTATUSCODES
250-PIPELINING
250-8BITMIME
250-SIZE
250-DSN
250-ETRN
250-AUTH GSSAPI
250-DELIVERBY
250 HELP
Change return of send and receive callbacks to allow for
new functions. "" signals end of transmission. Pass total
number of bytes in request table for HTTP. Callback has nothing
to do with it.
Make sure nobody can fuck up with the metatables...
Create a passive mode option for the FTP (good for firewall).
Use environments in module definitions or declare all local and create the
function with exported symbols later?
local P = {}
complex = P
setfenv(1, P)
Modules should return their namespace table in the end of the chunk.
Adjust dates in all files Adjust dates in all files
Test the library on every system possible Test the library on every system possible
@ -7,7 +38,7 @@ Document socket.time and socket.sleep
Implement time critical stuff from code module in C. Implement time critical stuff from code module in C.
Add service name translation. Add service name translation.
Ajeitar o protocolo da lua_socketlibopen()... Ajeitar o protocolo da luaopen_socket()... sei lá qual é.
- testar os options! - testar os options!
- adicionar exemplos de expansão: pipe, local, named pipe - adicionar exemplos de expansão: pipe, local, named pipe

View File

@ -33,6 +33,7 @@
#include "tcp.h" #include "tcp.h"
#include "udp.h" #include "udp.h"
#include "select.h" #include "select.h"
#include "code.h"
/*=========================================================================*\ /*=========================================================================*\
* Exported functions * Exported functions
@ -51,21 +52,22 @@ LUASOCKET_API int luaopen_socket(lua_State *L)
tcp_open(L); tcp_open(L);
udp_open(L); udp_open(L);
select_open(L); select_open(L);
code_open(L);
#ifdef LUASOCKET_COMPILED #ifdef LUASOCKET_COMPILED
#include "auxiliar.lch" #include "auxiliar.lch"
#include "concat.lch" #include "concat.lch"
#include "code.lch"
#include "url.lch" #include "url.lch"
#include "callback.lch" #include "callback.lch"
#include "code.lch"
#include "smtp.lch" #include "smtp.lch"
#include "ftp.lch" #include "ftp.lch"
#include "http.lch" #include "http.lch"
#else #else
lua_dofile(L, "auxiliar.lua"); lua_dofile(L, "auxiliar.lua");
lua_dofile(L, "concat.lua"); lua_dofile(L, "concat.lua");
lua_dofile(L, "code.lua");
lua_dofile(L, "url.lua"); lua_dofile(L, "url.lua");
lua_dofile(L, "callback.lua"); lua_dofile(L, "callback.lua");
lua_dofile(L, "code.lua");
lua_dofile(L, "smtp.lua"); lua_dofile(L, "smtp.lua");
lua_dofile(L, "ftp.lua"); lua_dofile(L, "ftp.lua");
lua_dofile(L, "http.lua"); lua_dofile(L, "http.lua");

View File

@ -1,314 +1,157 @@
----------------------------------------------------------------------------- -- make sure LuaSocket is loaded
-- SMTP support for the Lua language. if not LUASOCKET_LIBNAME then error('module requires LuaSocket') end
-- LuaSocket toolkit -- get LuaSocket namespace
-- Author: Diego Nehab local socket = _G[LUASOCKET_LIBNAME]
-- Conforming to: RFC 821, LTN7 if not socket then error('module requires LuaSocket') end
-- RCS ID: $Id$ -- create smtp namespace inside LuaSocket namespace
----------------------------------------------------------------------------- local smtp = {}
socket.smtp = smtp
-- make all module globals fall into smtp namespace
setmetatable(smtp, { __index = _G })
setfenv(1, smtp)
local Public, Private = {}, {} -- default port
local socket = _G[LUASOCKET_LIBNAME] -- get LuaSocket namespace PORT = 25
socket.smtp = Public -- create smtp sub namespace
-----------------------------------------------------------------------------
-- Program constants
-----------------------------------------------------------------------------
-- timeout in secconds before we give up waiting
Public.TIMEOUT = 180
-- port used for connection
Public.PORT = 25
-- domain used in HELO command and default sendmail -- domain used in HELO command and default sendmail
-- If we are under a CGI, try to get from environment -- If we are under a CGI, try to get from environment
Public.DOMAIN = os.getenv("SERVER_NAME") or "localhost" DOMAIN = os.getenv("SERVER_NAME") or "localhost"
-- default server used to send e-mails -- default server used to send e-mails
Public.SERVER = "localhost" SERVER = "localhost"
----------------------------------------------------------------------------- -- tries to get a pattern from the server and closes socket on error
-- Tries to get a pattern from the server and closes socket on error local function try_receiving(connection, pattern)
-- sock: socket connected to the server local data, message = connection:receive(pattern)
-- pattern: pattern to receive if not data then connection:close() end
-- Returns print(data)
-- received pattern on success return data, message
-- nil followed by error message on error
-----------------------------------------------------------------------------
function Private.try_receive(sock, pattern)
local data, err = sock:receive(pattern)
if not data then sock:close() end
return data, err
end end
----------------------------------------------------------------------------- -- tries to send data to server and closes socket on error
-- Tries to send data to the server and closes socket on error local function try_sending(connection, data)
-- sock: socket connected to the server local sent, message = connection:send(data)
-- data: data to send if not sent then connection:close() end
-- Returns io.write(data)
-- err: error message if any, nil if successfull return sent, message
-----------------------------------------------------------------------------
function Private.try_send(sock, data)
local sent, err = sock:send(data)
if not sent then sock:close() end
return err
end end
----------------------------------------------------------------------------- -- gets server reply
-- Sends a command to the server (closes sock on error) local function get_reply(connection)
-- Input local code, current, separator, _
-- sock: server socket local line, message = try_receiving(connection)
-- command: command to be sent local reply = line
-- param: command parameters if any if message then return nil, message end
-- Returns _, _, code, separator = string.find(line, "^(%d%d%d)(.?)")
-- err: error message if any if not code then return nil, "invalid server reply" end
----------------------------------------------------------------------------- if separator == "-" then -- reply is multiline
function Private.send_command(sock, command, param)
local line
if param then line = command .. " " .. param .. "\r\n"
else line = command .. "\r\n" end
return Private.try_send(sock, line)
end
-----------------------------------------------------------------------------
-- Gets command reply, (accepts multiple-line replies)
-- Input
-- control: control openion socket
-- Returns
-- answer: whole server reply, nil if error
-- code: reply status code or error message
-----------------------------------------------------------------------------
function Private.get_answer(control)
local code, lastcode, sep, _
local line, err = Private.try_receive(control)
local answer = line
if err then return nil, err end
_,_, code, sep = string.find(line, "^(%d%d%d)(.)")
if not code or not sep then return nil, answer end
if sep == "-" then -- answer is multiline
repeat repeat
line, err = Private.try_receive(control) line, message = try_receiving(connection)
if err then return nil, err end if message then return nil, message end
_,_, lastcode, sep = string.find(line, "^(%d%d%d)(.)") _,_, current, separator = string.find(line, "^(%d%d%d)(.)")
answer = answer .. "\n" .. line if not current or not separator then
until code == lastcode and sep == " " -- answer ends with same code return nil, "invalid server reply"
end end
return answer, tonumber(code) reply = reply .. "\n" .. line
-- reply ends with same code
until code == current and separator == " "
end
return code, reply
end end
----------------------------------------------------------------------------- -- metatable for server connection object
-- Checks if a message reply code is correct. Closes control openion local metatable = { __index = {} }
-- if not.
-- Input -- switch handler for execute function
-- control: control openion socket local switch = {}
-- success: table with successfull reply status code
-- Returns -- execute the "check" instruction
-- code: reply code or nil in case of error function switch.check(connection, instruction)
-- answer: complete server answer or system error message local code, reply = get_reply(connection)
----------------------------------------------------------------------------- if not code then return nil, reply end
function Private.check_answer(control, success) if type(instruction.check) == "function" then
local answer, code = Private.get_answer(control) return instruction.check(code, reply)
if not answer then return nil, code end else
if type(success) ~= "table" then success = {success} end if string.find(code, instruction.check) then return code, reply
for i = 1, table.getn(success) do else return nil, reply end
if code == success[i] then
return code, answer
end end
end end
control:close()
return nil, answer
end
----------------------------------------------------------------------------- -- stub for invalid instructions
-- Sends initial client greeting function switch.invalid(connection, instruction)
-- Input return nil, "invalid instruction"
-- sock: server socket
-- Returns
-- code: server code if ok, nil if error
-- answer: complete server reply
-----------------------------------------------------------------------------
function Private.send_helo(sock)
local err = Private.send_command(sock, "HELO", Public.DOMAIN)
if err then return nil, err end
return Private.check_answer(sock, 250)
end end
----------------------------------------------------------------------------- -- execute the "command" instruction
-- Sends openion termination command function switch.command(connection, instruction)
-- Input local line
-- sock: server socket if instruction.argument then
-- Returns line = instruction.command .. " " .. instruction.argument .. "\r\n"
-- code: server status code, nil if error else line = instruction.command .. "\r\n" end
-- answer: complete server reply or error message return try_sending(connection, line)
-----------------------------------------------------------------------------
function Private.send_quit(sock)
local err = Private.send_command(sock, "QUIT")
if err then return nil, err end
local code, answer = Private.check_answer(sock, 221)
sock:close()
return code, answer
end end
----------------------------------------------------------------------------- function switch.raw(connection, instruction)
-- Sends sender command if type(instruction.raw) == "function" then
-- Input local f = instruction.raw
-- sock: server socket while true do
-- sender: e-mail of sender local chunk, new_f = f()
-- Returns if not chunk then return nil, new_f end
-- code: server status code, nil if error if chunk == "" then return true end
-- answer: complete server reply or error message f = new_f or f
----------------------------------------------------------------------------- local code, message = try_sending(connection, chunk)
function Private.send_mail(sock, sender) if not code then return nil, message end
local param = string.format("FROM:<%s>", sender or "") end
local err = Private.send_command(sock, "MAIL", param) else return try_sending(connection, instruction.raw) end
if err then return nil, err end
return Private.check_answer(sock, 250)
end end
----------------------------------------------------------------------------- -- finds out what instruction are we dealing with
-- Sends mime headers local function instruction_type(instruction)
-- Input if type(instruction) ~= "table" then return "invalid" end
-- sock: server socket if instruction.command then return "command" end
-- headers: table with mime headers to be sent if instruction.check then return "check" end
-- Returns if instruction.raw then return "raw" end
-- err: error message if any return "invalid"
-----------------------------------------------------------------------------
function Private.send_headers(sock, headers)
local err
-- send request headers
for i, v in headers or {} do
err = Private.try_send(sock, i .. ": " .. v .. "\r\n")
if err then return err end
end
-- mark end of request headers
return Private.try_send(sock, "\r\n")
end end
----------------------------------------------------------------------------- -- execute a list of instructions
-- Sends message mime headers and body function metatable.__index:execute(instructions)
-- Input if type(instructions) ~= "table" then error("instruction expected", 1) end
-- sock: server socket if not instructions[1] then instructions = { instructions } end
-- headers: table containing all mime headers to be sent local code, message
-- body: message body for _, instruction in ipairs(instructions) do
-- Returns local type = instruction_type(instruction)
-- code: server status code, nil if error code, message = switch[type](self.connection, instruction)
-- answer: complete server reply or error message if not code then break end
----------------------------------------------------------------------------- end
function Private.send_data(sock, headers, body) return code, message
local err = Private.send_command(sock, "DATA")
if err then return nil, err end
local code, answer = Private.check_answer(sock, 354)
if not code then return nil, answer end
-- avoid premature end in message body
body = string.gsub(body or "", "\n%.", "\n%.%.")
-- mark end of message body
body = body .. "\r\n.\r\n"
err = Private.send_headers(sock, headers)
if err then return nil, err end
err = Private.try_send(sock, body)
return Private.check_answer(sock, 250)
end end
----------------------------------------------------------------------------- -- closes the underlying connection
-- Sends recipient list command function metatable.__index:close()
-- Input self.connection:close()
-- sock: server socket
-- rcpt: lua table with recipient list
-- Returns
-- code: server status code, nil if error
-- answer: complete server reply
-----------------------------------------------------------------------------
function Private.send_rcpt(sock, rcpt)
local err
local code, answer = nil, "No recipient specified"
if type(rcpt) ~= "table" then rcpt = {rcpt} end
for i = 1, table.getn(rcpt) do
err = Private.send_command(sock, "RCPT",
string.format("TO:<%s>", rcpt[i]))
if err then return nil, err end
code, answer = Private.check_answer(sock, {250, 251})
if not code then return code, answer end
end
return code, answer
end end
----------------------------------------------------------------------------- -- connect with server and return a smtp connection object
-- Starts the connection and greets server function connect(host)
-- Input local connection, message = socket.connect(host, PORT)
-- parsed: parsed URL components if not connection then return nil, message end
-- Returns return setmetatable({ connection = connection }, metatable)
-- sock: socket connected to server
-- err: error message if any
-----------------------------------------------------------------------------
function Private.open(server)
local code, answer
-- default server
server = server or Public.SERVER
-- connect to server and make sure we won't hang
local sock, err = socket.connect(server, Public.PORT)
if not sock then return nil, err end
sock:settimeout(Public.TIMEOUT)
-- initial server greeting
code, answer = Private.check_answer(sock, 220)
if not code then return nil, answer end
-- HELO
code, answer = Private.send_helo(sock)
if not code then return nil, answer end
return sock
end end
----------------------------------------------------------------------------- -- simple test drive
-- Sends a message using an opened server
-- Input
-- sock: socket connected to server
-- message: a table with the following fields:
-- from: message sender's e-mail
-- rcpt: message recipient's e-mail
-- headers: message mime headers
-- body: messge body
-- Returns
-- code: server status code, nil if error
-- answer: complete server reply
-----------------------------------------------------------------------------
function Private.send(sock, message)
local code, answer
-- MAIL
code, answer = Private.send_mail(sock, message.from)
if not code then return nil, answer end
-- RCPT
code, answer = Private.send_rcpt(sock, message.rcpt)
if not code then return nil, answer end
-- DATA
return Private.send_data(sock, message.headers, message.body)
end
----------------------------------------------------------------------------- --[[
-- Closes connection with server c, m = connect("localhost")
-- Input assert(c, m)
-- sock: socket connected to server assert(c:execute {check = "2.." })
-- Returns assert(c:execute {{command = "EHLO", argument = "localhost"}, {check = "2.."}})
-- code: server status code, nil if error assert(c:execute {command = "MAIL", argument = "FROM:<diego@princeton.edu>"})
-- answer: complete server reply assert(c:execute {check = "2.."})
----------------------------------------------------------------------------- assert(c:execute {command = "RCPT", argument = "TO:<diego@cs.princeton.edu>"})
function Private.close(sock) assert(c:execute {check = function (code) return code == "250" end})
-- QUIT assert(c:execute {{command = "DATA"}, {check = "3.."}})
return Private.send_quit(sock) assert(c:execute {{raw = "This is the message\r\n.\r\n"}, {check = "2.."}})
end assert(c:execute {{command = "QUIT"}, {check = "2.."}})
c:close()
]]
----------------------------------------------------------------------------- return smtp
-- Main mail function
-- Input
-- message: a table with the following fields:
-- from: message sender
-- rcpt: table containing message recipients
-- headers: table containing mime headers
-- body: message body
-- server: smtp server to be used
-- Returns
-- nil if successfull, error message in case of error
-----------------------------------------------------------------------------
function Public.mail(message)
local sock, err = Private.open(message.server)
if not sock then return nil, err end
local code, answer = Private.send(sock, message)
if not code then return nil, answer end
code, answer = Private.close(sock)
if code then return 1
else return nil, answer end
end