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) 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 local line
if param then line = command .. " " .. param .. "\r\n" if instruction.argument then
else line = command .. "\r\n" end line = instruction.command .. " " .. instruction.argument .. "\r\n"
return Private.try_send(sock, line) else line = instruction.command .. "\r\n" end
return try_sending(connection, line)
end end
----------------------------------------------------------------------------- function switch.raw(connection, instruction)
-- Gets command reply, (accepts multiple-line replies) if type(instruction.raw) == "function" then
-- Input local f = instruction.raw
-- control: control openion socket while true do
-- Returns local chunk, new_f = f()
-- answer: whole server reply, nil if error if not chunk then return nil, new_f end
-- code: reply status code or error message if chunk == "" then return true end
----------------------------------------------------------------------------- f = new_f or f
function Private.get_answer(control) local code, message = try_sending(connection, chunk)
local code, lastcode, sep, _ if not code then return nil, message end
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
line, err = Private.try_receive(control)
if err then return nil, err end
_,_, lastcode, sep = string.find(line, "^(%d%d%d)(.)")
answer = answer .. "\n" .. line
until code == lastcode and sep == " " -- answer ends with same code
end
return answer, tonumber(code)
end
-----------------------------------------------------------------------------
-- Checks if a message reply code is correct. Closes control openion
-- if not.
-- Input
-- control: control openion socket
-- success: table with successfull reply status code
-- Returns
-- code: reply code or nil in case of error
-- answer: complete server answer or system error message
-----------------------------------------------------------------------------
function Private.check_answer(control, success)
local answer, code = Private.get_answer(control)
if not answer then return nil, code end
if type(success) ~= "table" then success = {success} end
for i = 1, table.getn(success) do
if code == success[i] then
return code, answer
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
end end
control:close() return code, message
return nil, answer
end end
----------------------------------------------------------------------------- -- closes the underlying connection
-- Sends initial client greeting function metatable.__index:close()
-- Input self.connection:close()
-- 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
----------------------------------------------------------------------------- -- connect with server and return a smtp connection object
-- Sends openion termination command function connect(host)
-- Input local connection, message = socket.connect(host, PORT)
-- sock: server socket if not connection then return nil, message end
-- Returns return setmetatable({ connection = connection }, metatable)
-- code: server status code, nil if error
-- answer: complete server reply or error message
-----------------------------------------------------------------------------
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
----------------------------------------------------------------------------- -- simple test drive
-- Sends sender command
-- Input
-- sock: server socket
-- sender: e-mail of sender
-- Returns
-- code: server status code, nil if error
-- answer: complete server reply or error message
-----------------------------------------------------------------------------
function Private.send_mail(sock, sender)
local param = string.format("FROM:<%s>", sender or "")
local err = Private.send_command(sock, "MAIL", param)
if err then return nil, err end
return Private.check_answer(sock, 250)
end
----------------------------------------------------------------------------- --[[
-- Sends mime headers c, m = connect("localhost")
-- Input assert(c, m)
-- sock: server socket assert(c:execute {check = "2.." })
-- headers: table with mime headers to be sent assert(c:execute {{command = "EHLO", argument = "localhost"}, {check = "2.."}})
-- Returns assert(c:execute {command = "MAIL", argument = "FROM:<diego@princeton.edu>"})
-- err: error message if any assert(c:execute {check = "2.."})
----------------------------------------------------------------------------- assert(c:execute {command = "RCPT", argument = "TO:<diego@cs.princeton.edu>"})
function Private.send_headers(sock, headers) assert(c:execute {check = function (code) return code == "250" end})
local err assert(c:execute {{command = "DATA"}, {check = "3.."}})
-- send request headers assert(c:execute {{raw = "This is the message\r\n.\r\n"}, {check = "2.."}})
for i, v in headers or {} do assert(c:execute {{command = "QUIT"}, {check = "2.."}})
err = Private.try_send(sock, i .. ": " .. v .. "\r\n") c:close()
if err then return err end ]]
end
-- mark end of request headers
return Private.try_send(sock, "\r\n")
end
----------------------------------------------------------------------------- return smtp
-- Sends message mime headers and body
-- Input
-- sock: server socket
-- headers: table containing all mime headers to be sent
-- body: message body
-- Returns
-- code: server status code, nil if error
-- answer: complete server reply or error message
-----------------------------------------------------------------------------
function Private.send_data(sock, headers, body)
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
-----------------------------------------------------------------------------
-- Sends recipient list command
-- Input
-- 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
-----------------------------------------------------------------------------
-- Starts the connection and greets server
-- Input
-- parsed: parsed URL components
-- Returns
-- 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
-----------------------------------------------------------------------------
-- 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
-- Input
-- sock: socket connected to server
-- Returns
-- code: server status code, nil if error
-- answer: complete server reply
-----------------------------------------------------------------------------
function Private.close(sock)
-- QUIT
return Private.send_quit(sock)
end
-----------------------------------------------------------------------------
-- 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