-- furt-lua/src/smtp.lua -- Universal SMTP implementation with SSL compatibility -- Supports both luaossl (Arch/karl) and luasec (OpenBSD/walter) -- Dragons@Work Digital Sovereignty Project local socket = require("socket") local SMTP = {} -- SSL Compatibility Layer - Auto-detect available SSL library local SSLCompat = {} function SSLCompat:detect_ssl_library() -- Try luaossl first (more feature-complete) local success, ssl_lib = pcall(require, "ssl") if success and ssl_lib and ssl_lib.wrap then -- Check if it's luaossl (has more comprehensive API) if ssl_lib.newcontext or type(ssl_lib.wrap) == "function" then return "luaossl", ssl_lib end end -- Try luasec local success, ssl_lib = pcall(require, "ssl") if success and ssl_lib then -- luasec typically has ssl.wrap function but different API if ssl_lib.wrap and not ssl_lib.newcontext then return "luasec", ssl_lib end end return nil, "No compatible SSL library found (tried luaossl, luasec)" end function SSLCompat:wrap_socket(sock, options) local ssl_type, ssl_lib = self:detect_ssl_library() if not ssl_type then return nil, ssl_lib -- ssl_lib contains error message end if ssl_type == "luaossl" then return self:wrap_luaossl(sock, options, ssl_lib) elseif ssl_type == "luasec" then return self:wrap_luasec(sock, options, ssl_lib) end return nil, "Unknown SSL library type: " .. ssl_type end function SSLCompat:wrap_luaossl(sock, options, ssl_lib) -- luaossl API local ssl_sock, err = ssl_lib.wrap(sock, { mode = "client", protocol = "tlsv1_2", verify = "none" -- For self-signed certs }) if not ssl_sock then return nil, "luaossl wrap failed: " .. (err or "unknown error") end -- luaossl typically does handshake automatically, but explicit is safer local success, err = pcall(function() return ssl_sock:dohandshake() end) if not success then -- Some luaossl versions don't need explicit handshake -- Continue if dohandshake doesn't exist end return ssl_sock, nil end function SSLCompat:wrap_luasec(sock, options, ssl_lib) -- luasec API local ssl_sock, err = ssl_lib.wrap(sock, { protocol = "tlsv1_2", mode = "client", verify = "none", options = "all" }) if not ssl_sock then return nil, "luasec wrap failed: " .. (err or "unknown error") end -- luasec requires explicit handshake local success, err = ssl_sock:dohandshake() if not success then return nil, "luasec handshake failed: " .. (err or "unknown error") end return ssl_sock, nil end -- Create SMTP instance function SMTP:new(config) local instance = { server = config.smtp_server or "mail.dragons-at-work.de", port = config.smtp_port or 465, username = config.username, password = config.password, from_address = config.from_address or "noreply@dragons-at-work.de", use_ssl = config.use_ssl or true, debug = config.debug or false, ssl_compat = SSLCompat } setmetatable(instance, self) self.__index = self return instance end -- Base64 encoding for SMTP AUTH function SMTP:base64_encode(str) local b = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' return ((str:gsub('.', function(x) local r, b = '', x:byte() for i = 8, 1, -1 do r = r .. (b % 2^i - b % 2^(i-1) > 0 and '1' or '0') end return r; end) .. '0000'):gsub('%d%d%d?%d?%d?%d?', function(x) if (#x < 6) then return '' end local c = 0 for i = 1, 6 do c = c + (x:sub(i,i) == '1' and 2^(6-i) or 0) end return b:sub(c+1,c+1) end) .. ({ '', '==', '=' })[#str % 3 + 1]) end -- Send SMTP command and read response function SMTP:send_command(sock, command, expected_code) if self.debug then print("SMTP CMD: " .. (command or ""):gsub("\r\n", "\\r\\n")) end -- Only send if command is not nil (for server greeting, command is nil) if command then local success, err = sock:send(command .. "\r\n") if not success then return false, "Failed to send command: " .. (err or "unknown error") end end local response, err = sock:receive() if not response then return false, "Failed to receive response: " .. (err or "unknown error") end if self.debug then print("SMTP RSP: " .. response) end -- Handle multi-line responses (like EHLO) local full_response = response while response:match("^%d%d%d%-") do response, err = sock:receive() if not response then return false, "Failed to receive multi-line response: " .. (err or "unknown error") end if self.debug then print("SMTP RSP: " .. response) end full_response = full_response .. "\n" .. response end local code = response:match("^(%d+)") if expected_code and code ~= tostring(expected_code) then return false, "Unexpected response: " .. full_response end return true, full_response end -- Connect to SMTP server with universal SSL support function SMTP:connect() -- Create socket local sock, err = socket.tcp() if not sock then return false, "Failed to create socket: " .. (err or "unknown error") end -- Set timeout sock:settimeout(30) -- Connect to server local success, err = sock:connect(self.server, self.port) if not success then return false, "Failed to connect to " .. self.server .. ":" .. self.port .. " - " .. (err or "unknown error") end -- Wrap with SSL for port 465 using compatibility layer if self.use_ssl and self.port == 465 then local ssl_sock, err = self.ssl_compat:wrap_socket(sock, { mode = "client", protocol = "tlsv1_2" }) if not ssl_sock then sock:close() return false, "Failed to establish SSL connection: " .. (err or "unknown error") end sock = ssl_sock end -- Read server greeting local success, response = self:send_command(sock, nil, 220) if not success then sock:close() return false, "SMTP server greeting failed: " .. response end return sock, nil end -- Send email function SMTP:send_email(to_address, subject, message, from_name) if not self.username or not self.password then return false, "SMTP username or password not configured" end -- Connect to server local sock, err = self:connect() if not sock then return false, err end local function cleanup_and_fail(error_msg) sock:close() return false, error_msg end -- EHLO command local success, response = self:send_command(sock, "EHLO furt-lua", 250) if not success then return cleanup_and_fail("EHLO failed: " .. response) end -- AUTH LOGIN local success, response = self:send_command(sock, "AUTH LOGIN", 334) if not success then return cleanup_and_fail("AUTH LOGIN failed: " .. response) end -- Send username (base64 encoded) local username_b64 = self:base64_encode(self.username) local success, response = self:send_command(sock, username_b64, 334) if not success then return cleanup_and_fail("Username authentication failed: " .. response) end -- Send password (base64 encoded) local password_b64 = self:base64_encode(self.password) local success, response = self:send_command(sock, password_b64, 235) if not success then return cleanup_and_fail("Password authentication failed: " .. response) end -- MAIL FROM local mail_from = "MAIL FROM:<" .. self.from_address .. ">" local success, response = self:send_command(sock, mail_from, 250) if not success then return cleanup_and_fail("MAIL FROM failed: " .. response) end -- RCPT TO local rcpt_to = "RCPT TO:<" .. to_address .. ">" local success, response = self:send_command(sock, rcpt_to, 250) if not success then return cleanup_and_fail("RCPT TO failed: " .. response) end -- DATA command local success, response = self:send_command(sock, "DATA", 354) if not success then return cleanup_and_fail("DATA command failed: " .. response) end -- Build email message local display_name = from_name or "Furt Contact Form" local email_content = string.format( "From: %s <%s>\r\n" .. "To: <%s>\r\n" .. "Subject: %s\r\n" .. "Date: %s\r\n" .. "Content-Type: text/plain; charset=UTF-8\r\n" .. "\r\n" .. "%s\r\n" .. ".", display_name, self.from_address, to_address, subject, os.date("%a, %d %b %Y %H:%M:%S %z"), message ) -- Send email content local success, response = self:send_command(sock, email_content, 250) if not success then return cleanup_and_fail("Email sending failed: " .. response) end -- QUIT self:send_command(sock, "QUIT", 221) sock:close() return true, "Email sent successfully" end return SMTP