module (..., package.seeall) -- Load libraries require("modelfunctions") require("posix") require("fs") require("format") require("validator") require("luasql.sqlite3") require("session") -- Set variables local configfile = "/etc/freeswitchvmail.conf" local configcontent = fs.read_file(configfile) or "" local config = format.parse_ini_file(configcontent, "") or {} config.database = config.database or "/var/lib/freeswitch/db/voicemail_default.db" config.domain = config.domain or "voicemail" config.event_socket_ip = config.event_socket_ip or "127.0.0.1" config.event_socket_port = config.event_socket_port or "8021" config.event_socket_password = config.event_socket_password or "ClueCon" local env local con local voicemail_users_creation_script = { "CREATE TABLE voicemail_users (username VARCHAR(255))", } local voicemail_values_creation_script = { "CREATE TABLE voicemail_values (username VARCHAR(255), name VARCHAR(255), value VARCHAR(255))", } local voicemail_params_creation_script = { "CREATE TABLE voicemail_params (name VARCHAR(255) primary key, type VARCHAR(255), label VARCHAR(255), descr VARCHAR(255), value VARCHAR(255))", "INSERT INTO voicemail_params VALUES('username', 'text', 'Extension', '', '')", "INSERT INTO voicemail_params VALUES('firstname', 'text', 'User First Name', '', '')", "INSERT INTO voicemail_params VALUES('lastname', 'text', 'User Last Name', '', '')", "INSERT INTO voicemail_params VALUES('vm-password', 'text', 'Voicemail Password', '', '')", "INSERT INTO voicemail_params VALUES('vm-password-confirm', 'text', 'Enter again to confirm', '', '')", "INSERT INTO voicemail_params VALUES('vm-mailto', 'text', 'Email Address', 'Email a notification, including audio file if enabled', '')", "INSERT INTO voicemail_params VALUES('vm-email-all-messages', 'boolean', 'Email Enable', '', 'false')", "INSERT INTO voicemail_params VALUES('vm-attach-file', 'boolean', 'Attach voicemail to email', 'Option to attach audio file to email', 'false')", "INSERT INTO voicemail_params VALUES('vm-keep-local-after-email', 'boolean', 'Keep voicemail after emailed', 'When disabled the message will be deleted from the voicemailbox after the notification email is sent. This allows receiving voicemail via email alone, rather than having the voicemail available from the Web interface or by telephone. CAUTION: Attach voicemail to email must be enabled, OTHERWISE YOUR MESSAGES WILL BE LOST FOREVER.', 'true')", "INSERT INTO voicemail_params VALUES('vm-notify-mailto', 'text', 'Pager Email Address', 'Email a short notification', '')", "INSERT INTO voicemail_params VALUES('vm-notify-email-all-messages', 'boolean', 'Pager Email Enable', '', 'false')", } local voicemail_folders_creation_script = { "CREATE TABLE voicemail_folders (in_folder VARCHAR(255) primary key, label VARCHAR(255))", "INSERT INTO voicemail_folders VALUES('inbox', 'Inbox')", } local voicemail_prefs_creation_script = {"CREATE TABLE voicemail_prefs (username VARCHAR(255), domain VARCHAR(255), name_path VARCHAR(255), greeting_path VARCHAR(255), password VARCHAR(255))"} -- ################################################################################ -- LOCAL FUNCTIONS local function escape_quotes(str) return string.gsub(str or "", "'", "'\\''") end local function voicemail_inject(user, domain, sound_file, cid_num, cid_name) local cmd = "echo -e 'auth "..escape_quotes(config.event_socket_password).."\n\n" cmd = cmd.."api voicemail_inject "..escape_quotes(user).."@"..escape_quotes(domain).." "..escape_quotes(sound_file).." "..escape_quotes(cid_num).." "..string.gsub(escape_quotes(cid_name), " ", "%%20") cmd = cmd.."\n\nexit\n\n' | nc "..format.escapespecialcharacters(config.event_socket_ip).." "..format.escapespecialcharacters(config.event_socket_port).." 2>&1" local f = io.popen( cmd ) local result = f:read("*a") or "" f:close() return result end local function assert (v, m) if not v then m = m or "Assertion failed!" error(m, 0) end return v, m end -- Escape special characters in sql statements local escape = function(sql) sql = sql or "" sql = string.gsub(sql, "'", "''") return string.gsub(sql, "\\", "\\\\") end local databaseconnect = function() if not con then -- create environment object env = assert (luasql.sqlite3()) -- connect to data source con = assert (env:connect(config.database)) return true end return false end local databasedisconnect = function() if env then env:close() env = nil end if con then con:close() con = nil end end local runscript = function(script) for i,scr in ipairs(script) do logevent(scr) assert( con:execute(scr) ) end end local checktable = function(table) local success = false local errtxt local res, err = pcall(function() local sql = "SELECT * FROM "..table.." LIMIT 1" local cur = assert (con:execute(sql)) cur:close() success = true end) if not res and err then errtxt = err end return success, errtxt end local getselectresponse = function(sql) local retval = {} local cur = assert (con:execute(sql)) local row = cur:fetch ({}, "a") while row do local tmp = {} for name,val in pairs(row) do tmp[name] = val end retval[#retval + 1] = tmp row = cur:fetch (row, "a") end cur:close() return retval end local generatewhereclause = function(username, message, foldername) local sql = "" local where = {} if username and username ~= "" then where[#where+1] = "username = '"..escape(username).."'" end if message and type(message) == "string" and message ~= "" then where[#where+1] = "uuid = '"..escape(message).."'" elseif message and type(message) == "table" and #message > 0 then local where2 = {} for i,m in ipairs(message) do where2[#where2+1] = "uuid = '"..escape(m).."'" end where[#where+1] = "(" .. table.concat(where2, " OR ") .. ")" end if foldername and foldername ~= "" then where[#where+1] = "in_folder = '"..escape(foldername).."'" end if #where > 0 then sql = " WHERE " .. table.concat(where, " AND ") end return sql end -- These funtions access the new voicemail tables added for ACF local listfolders = function(foldername) if not checktable("voicemail_folders") then runscript(voicemail_folders_creation_script) end local sql = "SELECT * FROM voicemail_folders" .. generatewhereclause(nil, nil, foldername).." ORDER BY label" return getselectresponse(sql) end local validfolder = function(foldername) return foldername and (foldername ~= "") and (#listfolders(foldername) > 0) end local listusers = function(username) if not checktable("voicemail_users") then runscript(voicemail_users_creation_script) end local sql = "SELECT * FROM voicemail_users" .. generatewhereclause(username).." ORDER BY username" return getselectresponse(sql) end local validuser = function(username) return username and (username ~= "") and (#listusers(username) > 0) end local getuserparams = function(username) local retval = {} if not checktable("voicemail_params") then runscript(voicemail_params_creation_script) end local sql = "SELECT * FROM voicemail_params" local params = getselectresponse(sql) for i,parm in ipairs(params) do if parm.name then retval[parm.name] = {} for n,v in pairs(parm) do retval[parm.name][n] = v end if retval[parm.name].type == "boolean" then retval[parm.name].value = (retval[parm.name].value == "true") end end end if retval.username and username then retval.username.value = username end if validuser(username) then -- Get password from voicemail_prefs (don't fail for missing table) if checktable("voicemail_prefs") then local sql = "SELECT password FROM voicemail_prefs"..generatewhereclause(username) local password = getselectresponse(sql) if retval["vm-password"] and password[1] then retval["vm-password"].value = password[1].password end end -- Get other parameters from voicemail_values if not checktable("voicemail_values") then runscript(voicemail_values_creation_script) end sql = "SELECT * FROM voicemail_values"..generatewhereclause(username) local params = getselectresponse(sql) for i,param in ipairs(params) do if param.name and retval[param.name] and param.value then if retval[param.name].type == "boolean" then param.value = (param.value == "true") end retval[param.name].value = param.value end end end return retval end local setuserparams = function(userparams) if not userparams.username or not userparams.username.value or not validuser(userparams.username.value) then return false, "Invalid User" end local success = true if not checktable("voicemail_params") then runscript(voicemail_params_creation_script) end local sql = "SELECT * FROM voicemail_params" local params = getselectresponse(sql) -- There are a few params not to put in the voicemail_values table if not checktable("voicemail_values") then runscript(voicemail_values_creation_script) end local ignoreparam = { username=true, ["vm-password"]=true, ["vm-password-confirm"]=true } con:execute("START TRANSACTION") for i,parm in ipairs(params) do if parm.name and not ignoreparam[parm.name] then sql = "DELETE FROM voicemail_values"..generatewhereclause(userparams.username.value).." and name='"..parm.name.."'" assert( con:execute(sql) ) if userparams[parm.name] and (userparams[parm.name].value ~= nil) and tostring(userparams[parm.name].value) ~= parm.value then sql = "INSERT INTO voicemail_values VALUES('"..userparams.username.value.."', '"..parm.name.."', '"..tostring(userparams[parm.name].value).."')" assert( con:execute(sql) ) end end end -- Set password to voicemail_prefs if userparams["vm-password"] and userparams["vm-password"].value and userparams["vm-password"].value ~= "" then if not checktable("voicemail_prefs") then runscript(voicemail_prefs_creation_script) end sql = "SELECT password FROM voicemail_prefs"..generatewhereclause(userparams.username.value) local password = getselectresponse(sql) if #password > 0 then -- update sql = "UPDATE voicemail_prefs SET password='"..userparams["vm-password"].value.."'"..generatewhereclause(userparams.username.value) else -- insert sql = "INSERT INTO voicemail_prefs (username, domain, password) VALUES ('"..userparams.username.value.."', '"..config.domain.."', '"..userparams["vm-password"].value.."')" end assert( con:execute(sql) ) end con:execute("COMMIT") return success end local function validateconfig(newconfig) local success = true if newconfig.value.domain.value == "" then newconfig.value.domain.errtxt = "Cannot be blank" success = false end if newconfig.value.database.value == "" then newconfig.value.database.errtxt = "Cannot be blank" success = false end if newconfig.value.event_socket_ip.value == "" then newconfig.value.event_socket_ip.errtxt = "Cannot be blank" success = false end if newconfig.value.event_socket_port.value == "" then newconfig.value.event_socket_port.errtxt = "Cannot be blank" success = false end if newconfig.value.event_socket_password.value == "" then newconfig.value.event_socket_password.errtxt = "Cannot be blank" success = false end return success, newconfig end -- ################################################################################ -- PUBLIC FUNCTIONS get_config = function() local result = {} result.domain = cfe({ value=config.domain, label="Domain" }) result.database = cfe({ value=config.database, label="Database" }) result.event_socket_ip = cfe({ value=config.event_socket_ip, label="FS Event Socket IP" }) result.event_socket_port = cfe({ value=config.event_socket_port, label="FS Event Socket Port" }) result.event_socket_password = cfe({ value=config.event_socket_password, label="FS Event Socket Password" }) return cfe({ type="group", value=result, label="Voicemail Config" }) end update_config = function(newconfig) local success = validateconfig(newconfig) if success then for name,val in pairs(newconfig.value) do configcontent = format.update_ini_file(configcontent, "", name, tostring(val.value)) end fs.write_file(configfile, configcontent) config = format.parse_ini_file(configcontent, "") or {} else newconfig.errtxt = "Failed to update config" end return newconfig end list_messages = function(username) local retval = {} local errtxt local res, err = pcall(function() local connected = databaseconnect() local sql = "SELECT * FROM voicemail_msgs" sql = sql .. generatewhereclause(username) sql = sql .. " ORDER BY username ASC, created_epoch ASC" retval = getselectresponse(sql) if connected then databasedisconnect() end end) if not res and err then errtxt = err end return cfe({ type="structure", value=retval, label="List of Messages", errtxt=errtxt }) end get_message = function(message, username) local retval = cfe({ type="raw", label="error", option="audio/x-wav" }) local res, err = pcall(function() local connected = databaseconnect() local sql = "SELECT file_path FROM voicemail_msgs" sql = sql .. generatewhereclause(username, message) local tmp = getselectresponse(sql) if connected then databasedisconnect() end if #tmp == 0 then retval.errtxt = "Invalid message" else retval.label = posix.basename(tmp[1].file_path) retval.value = fs.read_file(tmp[1].file_path) retval.length = #retval.value end end) if not res and err then retval.errtxt = err end return retval end delete_message = function(message, username) local retval = cfe({ label="Delete message result", errtxt="Failed to delete message - message not found" }) local messages = format.string_to_table(message, "%s*,%s*") local res, err = pcall(function() local connected = databaseconnect() local sql = "SELECT * FROM voicemail_msgs" sql = sql .. generatewhereclause(username, messages) local tmp = getselectresponse(sql) if #tmp == #messages then sql = "DELETE FROM voicemail_msgs" .. generatewhereclause(username, messages) assert (con:execute(sql)) for i,t in ipairs(tmp) do os.remove(t.file_path) end if #messages == 1 then retval.value = "Deleted message" else retval.value = "Deleted "..#messages.." messages" end retval.errtxt = nil end if connected then databasedisconnect() end end) if not res and err then retval.errtxt = err end return retval end forward_message = function(message, newuser, username) local retval = cfe({ label="Forward message result" }) local messages = format.string_to_table(message, "%s*,%s*") local res, err = pcall(function() local connected = databaseconnect() -- Check if message exists local sql = "SELECT * FROM voicemail_msgs" .. generatewhereclause(username, messages) local mess = getselectresponse(sql) if #mess == #messages then -- Check if newuser exists if validuser(newuser) then for i,m in ipairs(mess) do -- Forward message using mod_voicemail API -- doesn't seem like there's any way to tell whether or not it worked voicemail_inject(newuser, config.domain, m.file_path, m.cid_number, m.cid_name) end if #mess == 1 then retval.value = "Forwarded message" else retval.value = "Forwarded "..#mess.." messages" end else retval.errtxt = "Failed to forward message - invalid user" end else retval.errtxt = "Failed to forward message - message not found" end if connected then databasedisconnect() end end) if not res and err then retval.errtxt = err end return retval end email_message = function(message, address, username) local retval = cfe({ label="E-mail message result" }) local messages = format.string_to_table(message, "%s*,%s*") local res, err = pcall(function() local connected = databaseconnect() -- Check if message exists local sql = "SELECT * FROM voicemail_msgs" .. generatewhereclause(username, messages) local mess = getselectresponse(sql) if #mess == #messages then -- Create a temporary user and settings local newuser = "tempuser"..session.random_hash(128) while validuser(newuser) do newuser = "tempuser"..session.random_hash(128) end local settings = get_usersettings(newuser) if settings.value["vm-mailto"] and settings.value["vm-email-all-messages"] and settings.value["vm-attach-file"] and settings.value["vm-keep-local-after-email"] then settings.value["vm-mailto"].value = address settings.value["vm-email-all-messages"].value = true settings.value["vm-attach-file"].value = true settings.value["vm-keep-local-after-email"].value = false if settings.value["vm-password"] then settings.value["vm-password"].value = "1234" end if settings.value["vm-password-confirm"] then settings.value["vm-password-confirm"].value = "1234" end settings = create_usersettings(settings) if not settings.errtxt then for i,m in ipairs(mess) do -- E-mail message using mod_voicemail API -- doesn't seem like there's any way to tell whether or not it worked voicemail_inject(newuser, config.domain, m.file_path, m.cid_number, m.cid_name) end if #mess == 1 then retval.value = "E-mailed message" else retval.value = "E-mailed "..#mess.." messages" end -- Now, delete the temporary user delete_user(newuser) else retval.errtxt = "Failed to e-mail message - "..settings.errtxt end else retval.errtxt = "Failed to e-mail message - unsupported" end else retval.errtxt = "Failed to e-mail message - message not found" end if connected then databasedisconnect() end end) if not res and err then retval.errtxt = err end return retval end move_message = function(message, newfolder, username) local retval = cfe({ label="Move message result" }) local messages = format.string_to_table(message, "%s*,%s*") local res, err = pcall(function() local connected = databaseconnect() -- Check if message exists local sql = "SELECT * FROM voicemail_msgs" .. generatewhereclause(username, messages) local mess = getselectresponse(sql) if #mess == #messages then -- Check if newfolder exists if validfolder(newfolder) then for i,m in ipairs(mess) do local sql = "UPDATE voicemail_msgs SET in_folder='"..newfolder.."'" .. generatewhereclause(username, messages) assert (con:execute(sql)) end if #mess == 1 then retval.value = "Moved message" else retval.value = "Moved "..#mess.." messages" end else retval.errtxt = "Failed to move message - invalid folder" end else retval.errtxt = "Failed to move message - message not found" end if connected then databasedisconnect() end end) if not res and err then retval.errtxt = err end return retval end list_folders = function() local errtxt local folders = {} local res, err = pcall(function() local connected = databaseconnect() folders = listfolders() if connected then databasedisconnect() end end) if not res and err then errtxt = err end return cfe({ type="structure", value=folders, label="Voicemail Folders", errtxt=errtxt }) end list_users = function() local errtxt local users = {} local res, err = pcall(function() local connected = databaseconnect() users = listusers() -- Go in reverse order to remove the temporary users used for e-mailing messages for i=#users,1,-1 do u = users[i] -- Remove the temporary users if string.find(u.username, "^tempuser") then table.remove(users, i) else local sql = "SELECT * FROM voicemail_values"..generatewhereclause(u.username).." and (name='firstname' or name='lastname')" local cur = con:execute(sql) local row = cur:fetch ({}, "a") while row do u[row.name] = row.value row = cur:fetch (row, "a") end cur:close() end end if connected then databasedisconnect() end end) if not res and err then errtxt = err end return cfe({ type="structure", value=users, label="Voicemail Users", errtxt=errtxt }) end delete_user = function(username) local result = "" local errtxt errtxt = nil local res, err = pcall(function() local connected = databaseconnect() local users = listusers(username) if #users == 0 then errtxt = "User does not exist" else -- Delete all of the user's voicemails local messages = list_messages(username) if #messages.value then for i,m in ipairs(messages.value) do delete_message(m.uuid) end end -- Remove the user parameters sql = "DELETE FROM voicemail_values " .. generatewhereclause(username) assert (con:execute(sql)) -- Remove the user sql = "DELETE FROM voicemail_users " .. generatewhereclause(username) assert (con:execute(sql)) result = "Voicemail User Deleted" end if connected then databasedisconnect() end end) if not res and err then errtxt = err end return cfe({ value=result, errtxt=errtxt, label="Delete User Result" }) end get_usersettings = function(username) local retval = {} local errtxt local res, err = pcall(function() local connected = databaseconnect() retval = getuserparams(username) if connected then databasedisconnect() end end) if not res and err then errtxt = err end if retval["vm-password"] and retval["vm-password-confirm"] then retval["vm-password-confirm"].value = retval["vm-password"].value end return cfe({ type="group", value=retval, label="Voicemail User Settings", errtxt=errtxt }) end create_usersettings = function(usersettings) return update_usersettings(usersettings, true) end update_usersettings = function(usersettings, create) local success = true local errtxt -- Validate the settings if not validator.is_integer(usersettings.value["vm-password"].value) then success = false usersettings.value["vm-password"].errtxt = "Password must be all numbers" end if usersettings.value["vm-password"].value ~= usersettings.value["vm-password-confirm"].value then success = false usersettings.value["vm-password-confirm"].errtxt = "Password does not match" end if success then local res, err = pcall(function() local connected = databaseconnect() local u = listusers(usersettings.value.username.value) if create and #u > 0 then success = false errtxt = "User already exists" elseif not create and #u == 0 then success = false errtxt = "User does not exist" else if create then sql = "INSERT INTO voicemail_users VALUES('"..escape(usersettings.value.username.value).."')" assert (con:execute(sql)) end success,errtxt = setuserparams(usersettings.value) end if connected then databasedisconnect() end end) if not res and err then success = false errtxt = err end end if not success then if create then usersettings.errtxt = errtxt or "Failed to create user" else usersettings.errtxt = errtxt or "Failed to save settings" end end return usersettings end process_directory_xml_request = function(input) local output = {} local errtxt local res, err = pcall(function() local connected = databaseconnect() if validuser(input.user) then output = getuserparams(input.user) -- Add the domain output.domain = cfe({ value=input.domain }) else errtxt = "User not found" end if connected then databasedisconnect() end end) if not res and err then errtxt = err end return cfe({ type="group", value=output, label="Directory Data", errtxt=errtxt }) end process_dialplan_xml_request = function(input) local output = {} local errtxt local res, err = pcall(function() local connected = databaseconnect() if validuser(input["Caller-Destination-Number"]) then output.domain = cfe({ value=config.domain }) output.username = cfe({ value=input["Caller-Destination-Number"] }) else errtxt = "User not found" end if connected then databasedisconnect() end end) if not res and err then errtxt = err end return cfe({ type="group", value=output, label="Dialplan Data", errtxt=errtxt }) end