diff --git a/src/smtp.lua b/src/smtp.lua index 6404e6c..7450792 100644 --- a/src/smtp.lua +++ b/src/smtp.lua @@ -1,88 +1,55 @@ ----------------------------------------------------------------------------- --- Simple SMTP support for the Lua language using the LuaSocket toolkit. +-- SMTP support for the Lua language. +-- LuaSocket 1.4 toolkit -- Author: Diego Nehab -- Date: 26/12/2000 --- Conforming to: RFC 821 +-- Conforming to: RFC 821, LTN7 +-- RCS ID: $Id$ ----------------------------------------------------------------------------- +local Public, Private = {}, {} +SMTP = Public + ----------------------------------------------------------------------------- -- Program constants ----------------------------------------------------------------------------- -- timeout in secconds before we give up waiting -local TIMEOUT = 180 +Public.TIMEOUT = 180 -- port used for connection -local PORT = 25 --- domain used in HELO command. If we are under a CGI, try to get from --- environment -local DOMAIN = getenv("SERVER_NAME") -if not DOMAIN then - DOMAIN = "localhost" -end +Public.PORT = 25 +-- domain used in HELO command and default sendmail +-- If we are under a CGI, try to get from environment +Public.DOMAIN = getenv("SERVER_NAME") or "localhost" +-- default server used to send e-mails +Public.SERVER = "localhost" ----------------------------------------------------------------------------- --- Tries to send DOS mode lines. Closes socket on error. +-- Tries to send data through socket. Closes socket on error. -- Input -- sock: server socket --- line: string to be sent +-- data: string to be sent -- Returns -- err: message in case of error, nil if successfull ----------------------------------------------------------------------------- -local try_send = function(sock, line) - local err = sock:send(line .. "\r\n") +function Private.try_send(sock, data) + local err = sock:send(data) if err then sock:close() end return err end ----------------------------------------------------------------------------- --- Gets command reply, (accepts multiple-line replies) --- Input --- control: control connection socket +-- Tries to get a pattern from the server and closes socket on error +-- sock: socket opened to the server +-- ...: pattern to receive -- Returns --- answer: whole server reply, nil if error --- code: reply status code or error message +-- ...: received pattern +-- err: error message if any ----------------------------------------------------------------------------- -local get_answer = function(control) - local code, lastcode, sep - local line, err = control:receive() - local answer = line - if err then return nil, err end - _,_, code, sep = strfind(line, "^(%d%d%d)(.)") - if not code or not sep then return nil, answer end - if sep == "-" then -- answer is multiline - repeat - line, err = control:receive() - if err then return nil, err end - _,_, lastcode, sep = strfind(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 connection --- if not. --- Input --- control: control connection 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 ------------------------------------------------------------------------------ -local check_answer = function(control, success) - local answer, code = %get_answer(control) - if not answer then - control:close() - return nil, code - end - if type(success) ~= "table" then success = {success} end - for i = 1, getn(success) do - if code == success[i] then - return code, answer - end - end - control:close() - return nil, answer +function Private.try_receive(...) + local sock = arg[1] + local data, err = call(sock.receive, arg) + if err then sock:close() end + return data, err end ----------------------------------------------------------------------------- @@ -94,11 +61,60 @@ end -- Returns -- err: error message if any ----------------------------------------------------------------------------- -local send_command = function(sock, command, param) +function Private.send_command(sock, command, param) local line - if param then line = command .. " " .. param - else line = command end - return %try_send(sock, 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 = strfind(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 = strfind(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, getn(success) do + if code == success[i] then + return code, answer + end + end + control:close() + return nil, answer end ----------------------------------------------------------------------------- @@ -109,49 +125,24 @@ end -- code: server code if ok, nil if error -- answer: complete server reply ----------------------------------------------------------------------------- -local send_helo = function(sock) - local err = %send_command(sock, "HELO", %DOMAIN) +function Private.send_helo(sock) + local err = %Private.send_command(sock, "HELO", %Public.DOMAIN) if err then return nil, err end - return %check_answer(sock, 250) + return %Private.check_answer(sock, 250) end ----------------------------------------------------------------------------- --- Sends mime headers --- Input --- sock: server socket --- mime: table with mime headers to be sent --- Returns --- err: error message if any ------------------------------------------------------------------------------ -local send_mime = function(sock, mime) - local err - mime = mime or {} - -- send all headers - for name,value in mime do - err = sock:send(name .. ": " .. value .. "\r\n") - if err then - sock:close() - return err - end - end - -- end mime part - err = sock:send("\r\n") - if err then sock:close() end - return err -end - ------------------------------------------------------------------------------ --- Sends connection termination command +-- Sends openion termination command -- Input -- sock: server socket -- Returns -- code: server status code, nil if error -- answer: complete server reply or error message ----------------------------------------------------------------------------- -local send_quit = function(sock) - local err = %send_command(sock, "QUIT") +function Private.send_quit(sock) + local err = %Private.send_command(sock, "QUIT") if err then return nil, err end - local code, answer = %check_answer(sock, 221) + local code, answer = %Private.check_answer(sock, 221) sock:close() return code, answer end @@ -165,36 +156,55 @@ end -- code: server status code, nil if error -- answer: complete server reply or error message ----------------------------------------------------------------------------- -local send_mail = function(sock, sender) - local param = format("FROM:<%s>", sender) - local err = %send_command(sock, "MAIL", param) +function Private.send_mail(sock, sender) + local param = format("FROM:<%s>", sender or "") + local err = %Private.send_command(sock, "MAIL", param) if err then return nil, err end - return %check_answer(sock, 250) + 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 + -- mark end of request headers + return %Private.try_send(sock, "\r\n") end ----------------------------------------------------------------------------- -- Sends message mime headers and body -- Input -- sock: server socket --- mime: table containing all mime headers to be sent +-- 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 ----------------------------------------------------------------------------- -local send_data = function (sock, mime, body) - local err = %send_command(sock, "DATA") +function Private.send_data(sock, headers, body) + local err = %Private.send_command(sock, "DATA") if err then return nil, err end - local code, answer = %check_answer(sock, 354) + local code, answer = %Private.check_answer(sock, 354) if not code then return nil, answer end -- avoid premature end in message body body = gsub(body or "", "\n%.", "\n%.%.") -- mark end of message body - body = body .. "\r\n." - err = %send_mime(sock, mime) + body = body .. "\r\n.\r\n" + err = %Private.send_headers(sock, headers) if err then return nil, err end - err = %try_send(sock, body) - return %check_answer(sock, 250) + err = %Private.try_send(sock, body) + return %Private.check_answer(sock, 250) end ----------------------------------------------------------------------------- @@ -206,107 +216,100 @@ end -- code: server status code, nil if error -- answer: complete server reply ----------------------------------------------------------------------------- -local send_rcpt = function(sock, rcpt) - local err, code, answer +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, getn(rcpt) do - err = %send_command(sock, "RCPT", format("TO:<%s>", rcpt[i])) + err = %Private.send_command(sock, "RCPT", format("TO:<%s>", rcpt[i])) if err then return nil, err end - code, answer = %check_answer(sock, {250, 251}) + code, answer = %Private.check_answer(sock, {250, 251}) if not code then return code, answer end end return code, answer end ----------------------------------------------------------------------------- --- Connection oriented mail functions +-- Starts the connection and greets server +-- Input +-- parsed: parsed URL components +-- Returns +-- sock: socket connected to server +-- err: error message if any ----------------------------------------------------------------------------- -function smtp_connect(server) +function Private.open(server) local code, answer - -- connect to server - local sock, err = connect(server, %PORT) + -- default server + server = server or %Public.SERVER + -- connect to server and make sure we won't hang + local sock, err = connect(server, %Public.PORT) if not sock then return nil, err end - sock:timeout(%TIMEOUT) + sock:timeout(%Public.TIMEOUT) -- initial server greeting - code, answer = %check_answer(sock, 220) + code, answer = %Private.check_answer(sock, 220) if not code then return nil, answer end -- HELO - code, answer = %send_helo(sock) + code, answer = %Private.send_helo(sock) if not code then return nil, answer end return sock end -function smtp_send(sock, from, rcpt, mime, body) +----------------------------------------------------------------------------- +-- 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 = %send_mail(sock, from) + code, answer = %Private.send_mail(sock, message.from) if not code then return nil, answer end -- RCPT - code, answer = %send_rcpt(sock, rcpt) + code, answer = %Private.send_rcpt(sock, message.rcpt) if not code then return nil, answer end -- DATA - return %send_data(sock, mime, body) + return %Private.send_data(sock, message.headers, message.body) end -function smtp_close(sock) +----------------------------------------------------------------------------- +-- 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 %send_quit(sock) + return %Private.send_quit(sock) end ----------------------------------------------------------------------------- -- Main mail function -- Input --- from: message sender --- rcpt: table containing message recipients --- mime: table containing mime headers --- body: message body --- server: smtp server to be used +-- 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 smtp_mail(from, rcpt, mime, body, server) - local sock, err = smtp_connect(server) +function Public.mail(message) + local sock, err = %Private.open(message.server) if not sock then return err end - local code, answer = smtp_send(sock, from, rcpt, mime, body) + local code, answer = %Private.send(sock, message) if not code then return answer end - code, answer = smtp_close(sock) + code, answer = %Private.close(sock) if code then return nil end return answer end - ---=========================================================================== --- Compatibility functions ---=========================================================================== ------------------------------------------------------------------------------ --- Converts a comma separated list into a Lua table with one entry for each --- list element. --- Input --- str: string containing the list to be converted --- tab: table to be filled with entries --- Returns --- a table t, where t.n is the number of elements with an entry t[i] --- for each element ------------------------------------------------------------------------------ -local fill = function(str, tab) - gsub(str, "([^%s,]+)", function (w) tinsert(%tab, w) end) - return tab -end - ------------------------------------------------------------------------------ --- Client mail function, implementing CGILUA 3.2 interface ------------------------------------------------------------------------------ -function mail(msg) - local rcpt = {} - local mime = {} - mime["Subject"] = msg.subject - mime["To"] = msg.to - mime["From"] = msg.from - %fill(msg.to, rcpt) - if msg.cc then - %fill(msg.cc, rcpt) - mime["Cc"] = msg.cc - end - if msg.bcc then %fill(msg.bcc, rcpt) end - rcpt.n = nil - return %smtp_mail(msg.from, rcpt, mime, msg.message, msg.mailserver) -end