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
end -- reply ends with same code
until code == current and separator == " "
-----------------------------------------------------------------------------
-- 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
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
control:close()
return nil, answer
end 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 "")
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
-- Input
-- sock: server socket
-- headers: table with mime headers to be sent
-- Returns
-- err: error message if any
-----------------------------------------------------------------------------
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 end
-- mark end of request headers else return try_sending(connection, instruction.raw) end
return Private.try_send(sock, "\r\n")
end end
----------------------------------------------------------------------------- -- finds out what instruction are we dealing with
-- Sends message mime headers and body 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 containing all mime headers to be sent if instruction.check then return "check" end
-- body: message body if instruction.raw then return "raw" end
-- Returns return "invalid"
-- 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 end
----------------------------------------------------------------------------- -- execute a list of instructions
-- Sends recipient list command 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
-- rcpt: lua table with recipient list local code, message
-- Returns for _, instruction in ipairs(instructions) do
-- code: server status code, nil if error local type = instruction_type(instruction)
-- answer: complete server reply code, message = switch[type](self.connection, instruction)
----------------------------------------------------------------------------- if not code then break end
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 end
return code, answer return code, message
end end
----------------------------------------------------------------------------- -- closes the underlying connection
-- Starts the connection and greets server function metatable.__index:close()
-- Input self.connection:close()
-- 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 end
----------------------------------------------------------------------------- -- connect with server and return a smtp connection object
-- Sends a message using an opened server function connect(host)
-- Input local connection, message = socket.connect(host, PORT)
-- sock: socket connected to server if not connection then return nil, message end
-- message: a table with the following fields: return setmetatable({ connection = connection }, metatable)
-- 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 end
----------------------------------------------------------------------------- -- simple test drive
-- 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 c, m = connect("localhost")
-- Input assert(c, m)
-- message: a table with the following fields: assert(c:execute {check = "2.." })
-- from: message sender assert(c:execute {{command = "EHLO", argument = "localhost"}, {check = "2.."}})
-- rcpt: table containing message recipients assert(c:execute {command = "MAIL", argument = "FROM:<diego@princeton.edu>"})
-- headers: table containing mime headers assert(c:execute {check = "2.."})
-- body: message body assert(c:execute {command = "RCPT", argument = "TO:<diego@cs.princeton.edu>"})
-- server: smtp server to be used assert(c:execute {check = function (code) return code == "250" end})
-- Returns assert(c:execute {{command = "DATA"}, {check = "3.."}})
-- nil if successfull, error message in case of error assert(c:execute {{raw = "This is the message\r\n.\r\n"}, {check = "2.."}})
----------------------------------------------------------------------------- assert(c:execute {{command = "QUIT"}, {check = "2.."}})
function Public.mail(message) c:close()
local sock, err = Private.open(message.server) ]]
if not sock then return nil, err end
local code, answer = Private.send(sock, message) return smtp
if not code then return nil, answer end
code, answer = Private.close(sock)
if code then return 1
else return nil, answer end
end