local json = require "json" local auth_url = "http://app:3001/internal/dovecot/auth" local secret_file = "/run/secrets/dovecot_auth_secret" local http_client local shared_secret local function read_secret(path) local file = io.open(path, "r") if file == nil then error("Dovecot authentication secret is unavailable") end local value = file:read("*a") file:close() value = string.gsub(value or "", "^%s+", "") value = string.gsub(value, "%s+$", "") if #value < 32 or #value > 512 or string.find(value, "%s") ~= nil then error("Dovecot authentication secret is invalid") end return value end function script_init() shared_secret = read_secret(secret_file) http_client = dovecot.http.client { auto_retry = "no", request_max_attempts = 1, connect_timeout = "1s", request_timeout = "2s", request_absolute_timeout = "2s" } return 0 end local function failure(result) return result, nil end local function valid_user(value) if type(value) ~= "string" or #value == 0 or #value > 320 then return false end if string.find(value, "/", 1, true) ~= nil or string.find(value, "\\", 1, true) ~= nil or string.find(value, "\0", 1, true) ~= nil or string.find(value, "%s") ~= nil then return false end return string.find(value, "^[^@]+@[^@]+%.[^@]+$") ~= nil end local function first_nonempty_string(...) for index = 1, select("#", ...) do local value = select(index, ...) if value ~= nil then local normalized = tostring(value) if normalized ~= "" then return normalized end end end return "" end function auth_password_verify(request, password) local protocol = string.lower(first_nonempty_string(request.protocol, request.service)) local remote_ip = first_nonempty_string( request.remote_ip, request.real_remote_ip, request.rip, request.real_rip ) if protocol ~= "imap" and protocol ~= "pop3" then return failure(dovecot.auth.PASSDB_RESULT_USER_DISABLED) end local http_request = http_client:request { url = auth_url, method = "POST" } http_request:add_header("content-type", "application/json") http_request:add_header("authorization", "Bearer " .. shared_secret) http_request:set_payload(json.encode { username = request.user, password = password, service = protocol, remoteIp = remote_ip }) local submitted, response = pcall(function() return http_request:submit() end) if not submitted then return failure(dovecot.auth.PASSDB_RESULT_INTERNAL_FAILURE) end local status = response:status() if status ~= 200 then return failure(dovecot.auth.PASSDB_RESULT_INTERNAL_FAILURE) end local decoded, payload = pcall(json.decode, response:payload()) if not decoded or type(payload) ~= "table" then return failure(dovecot.auth.PASSDB_RESULT_INTERNAL_FAILURE) end if payload.authenticated == false then return failure(dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH) end if payload.authenticated ~= true then return failure(dovecot.auth.PASSDB_RESULT_INTERNAL_FAILURE) end if not valid_user(payload.user) then return failure(dovecot.auth.PASSDB_RESULT_INTERNAL_FAILURE) end return dovecot.auth.PASSDB_RESULT_OK, { user = string.lower(payload.user) } end