From 8b00d93e330ee72f7de82e442349435a52204dd9 Mon Sep 17 00:00:00 2001 From: Vsevolod Stakhov Date: Sat, 25 May 2019 14:12:44 +0100 Subject: [PATCH] [Feature] Add lua_smtp library --- lualib/lua_smtp.lua | 195 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 195 insertions(+) create mode 100644 lualib/lua_smtp.lua diff --git a/lualib/lua_smtp.lua b/lualib/lua_smtp.lua new file mode 100644 index 000000000..1b15249c6 --- /dev/null +++ b/lualib/lua_smtp.lua @@ -0,0 +1,195 @@ +--[[ +Copyright (c) 2019, Vsevolod Stakhov + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +]]-- + +local rspamd_tcp = require "rspamd_tcp" + +local exports = {} + +local CRLF = '\r\n' + +--[[[ +-- @function lua_smtp.sendmail(task, message, opts, callback) +--]] +local function sendmail(task, message, opts, callback) + local stage = 'connect' + + local function mail_cb(err, data, conn) + local function no_error_write(merr) + if merr then + callback(false, string.format('error on stage %s: %s', + stage, merr)) + if conn then + conn:close() + end + + return false + end + + return true + end + + local function no_error_read(merr, mdata, wantcode) + wantcode = wantcode or '2' + if merr then + callback(false, string.format('error on stage %s: %s', + stage, merr)) + if conn then + conn:close() + end + + return false + end + if mdata then + if type(mdata) ~= 'string' then + mdata = tostring(mdata) + end + if string.sub(mdata, 1, 1) ~= wantcode then + callback(false, string.format('bad smtp responce on stage %s: "%s" when "%s" expected', + stage, mdata, wantcode)) + if conn then + conn:close() + end + return false + end + else + callback(false, string.format('no data on stage %s', + stage)) + if conn then + conn:close() + end + return false + end + return true + end + + -- After quit + local function all_done_cb(merr, mdata) + if conn then + conn:close() + end + + callback(true, nil) + + return true + end + + -- QUIT stage + local function quit_done_cb(_, _) + conn:add_read(all_done_cb, CRLF) + end + local function quit_cb(merr, mdata) + if no_error_read(merr, mdata) then + conn:add_write(quit_done_cb, 'QUIT' .. CRLF) + end + end + local function pre_quit_cb(merr, _) + if no_error_write(merr) then + stage = 'quit' + conn:add_read(quit_cb, CRLF) + end + end + + -- DATA stage + local function data_done_cb(merr, mdata) + if no_error_read(merr, mdata, '3') then + conn:add_write(pre_quit_cb, {message, CRLF.. '.' .. CRLF}) + end + end + local function data_cb(merr, _) + if no_error_write(merr) then + conn:add_read(data_done_cb, CRLF) + end + end + + -- RCPT phase + local next_recipient + local function rcpt_done_cb_gen(i) + return function (merr, mdata) + if no_error_read(merr, mdata) then + if i == #opts.recipients then + conn:add_write(data_cb, 'DATA' .. CRLF) + else + next_recipient(i + 1) + end + end + end + end + + local function rcpt_cb_gen(i) + return function (merr, _) + if no_error_write(merr, '2') then + conn:add_read(rcpt_done_cb_gen(i), CRLF) + end + end + end + + next_recipient = function(i) + conn:add_write(rcpt_cb_gen(i), + string.format('RCPT TO: <%s>%s', opts.recipients[i], CRLF)) + end + + -- FROM stage + local function from_done_cb(merr, mdata) + -- We need to iterate over recipients sequentially + if no_error_read(merr, mdata, '2') then + stage = 'rcpt' + next_recipient(1) + end + end + local function from_cb(merr, _) + if no_error_write(merr) then + conn:add_read(from_done_cb, CRLF) + end + end + local function hello_done_cb(merr, mdata) + if no_error_read(merr, mdata) then + stage = 'from' + conn:add_write(from_cb, string.format( + 'MAIL FROM: <%s>%s', opts.from, CRLF)) + end + end + + -- HELLO stage + local function hello_cb(merr) + if no_error_write(merr) then + conn:add_read(hello_done_cb, CRLF) + end + end + if no_error_read(err, data) then + stage = 'helo' + conn:add_write(hello_cb, string.format('HELO %s%s', + opts.helo, CRLF)) + end + end + + if type(opts.recipients) == 'string' then + opts.recipients = {opts.recipients} + end + + if not rspamd_tcp.request({ + task = task, + callback = mail_cb, + stop_pattern = CRLF, + host = opts.host, + port = opts.port or 25, + }) then + callback(false, 'cannot make a TCP connection') + end +end + +exports.sendmail = sendmail + +return exports \ No newline at end of file -- 2.39.5