aboutsummaryrefslogtreecommitdiffstats
path: root/lualib
diff options
context:
space:
mode:
Diffstat (limited to 'lualib')
-rw-r--r--lualib/lua_smtp.lua195
1 files changed, 195 insertions, 0 deletions
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 <vsevolod@highsecure.ru>
+
+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