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" config.callback_url = config.callback_url or "sofia/gateway/asterlink.com/$1" local env local con local table_creation_scripts = { voicemail_users = { "CREATE TABLE voicemail_users (uid INTEGER PRIMARY KEY, username VARCHAR(255) UNIQUE)", "CREATE INDEX users_username_idx ON voicemail_users (username)", }, voicemail_values = { "CREATE TABLE voicemail_values (uid INTEGER, nid INTEGER, value VARCHAR(255))", "CREATE INDEX values_uid_nid_idx on voicemail_values (uid, nid)", }, voicemail_params = { "CREATE TABLE voicemail_params (nid INTEGER PRIMARY KEY, name VARCHAR(255) UNIQUE, type VARCHAR(255), label VARCHAR(255), descr VARCHAR(255), value VARCHAR(255))", "CREATE INDEX params_name_idx ON voicemail_params (name)", "INSERT INTO voicemail_params VALUES(null, 'username', 'text', 'Extension', '', '')", "INSERT INTO voicemail_params VALUES(null, 'firstname', 'text', 'User First Name', '', '')", "INSERT INTO voicemail_params VALUES(null, 'lastname', 'text', 'User Last Name', '', '')", "INSERT INTO voicemail_params VALUES(null, 'vm-password', 'text', 'Voicemail Password', '', '')", "INSERT INTO voicemail_params VALUES(null, 'vm-password-confirm', 'text', 'Enter again to confirm', '', '')", "INSERT INTO voicemail_params VALUES(null, 'vm-mailto', 'text', 'Email Address', 'Email a notification, including audio file if enabled', '')", "INSERT INTO voicemail_params VALUES(null, 'vm-email-all-messages', 'boolean', 'Email Enable', '', 'false')", "INSERT INTO voicemail_params VALUES(null, 'vm-attach-file', 'boolean', 'Attach voicemail to email', 'Option to attach audio file to email', 'false')", "INSERT INTO voicemail_params VALUES(null, '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(null, 'vm-notify-mailto', 'text', 'Pager Email Address', 'Email a short notification', '')", "INSERT INTO voicemail_params VALUES(null, 'vm-notify-email-all-messages', 'boolean', 'Pager Email Enable', '', 'false')", "INSERT INTO voicemail_params VALUES(null, 'callmenumber', 'text', 'Call Me Number', '', '')", }, voicemail_folders = { "CREATE TABLE voicemail_folders (in_folder VARCHAR(255) PRIMARY KEY, label VARCHAR(255))", "INSERT INTO voicemail_folders VALUES('inbox', 'Inbox')", }, voicemail_prefs = {"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 voicemail_callback(extension, sound_file, cid_num, cid_name) local cmd = "echo -e 'auth "..escape_quotes(config.event_socket_password).."\n\n" cmd = cmd.."bgapi originate {ignore_early_media=true,origination_caller_id_name='\\''"..string.gsub(cid_name or "Voicemail", "'", "").."'\\'',origination_caller_id_number='\\''" cmd = cmd..string.gsub(cid_num or "", "'", "").."'\\''}"..escape_quotes(string.gsub(config.callback_url, "$1", extension)).." &playback("..escape_quotes(sound_file)..")" 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 runsqlcommand = function(sql) local res, err = con:execute(sql) if not res and err then -- Catch the error to see if it's caused by lack of table local table = string.match(err, "LuaSQL: no such table: (%S+)") if table and table_creation_scripts[table] then runscript(table_creation_scripts[table]) runsqlcommand(sql) else assert(res, err) end end end getselectresponse = function(sql) local retval = {} local res, err = pcall(function() 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() end) if not res and err then -- Catch the error to see if it's caused by lack of table local table = string.match(err, "LuaSQL: no such table: (%S+)") if table and table_creation_scripts[table] then runscript(table_creation_scripts[table]) return getselectresponse(sql) else assert(res, err) end end return retval end local generatewhereclause = function(username, message, foldername, uid) 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 uid and uid ~= "" then where[#where+1] = "uid = '"..escape(uid).."'" 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) 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) 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 = {} local sql = "SELECT * FROM voicemail_params" local params = getselectresponse(sql) local reverse_nids = {} for i,parm in ipairs(params) do if parm.name then reverse_nids[parm.nid] = parm.name 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 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 -- Get the uid that corresponds to this username sql = "SELECT uid FROM voicemail_users"..generatewhereclause(username) local uid = getselectresponse(sql) if #uid == 1 then -- Get other parameters from voicemail_values sql = "SELECT * FROM voicemail_values"..generatewhereclause(nil, nil, nil, uid[1].uid) local params = getselectresponse(sql) for i,param in ipairs(params) do if param.nid and reverse_nids[param.nid] and retval[reverse_nids[param.nid]] and param.value then if retval[reverse_nids[param.nid]].type == "boolean" then param.value = (param.value == "true") end retval[reverse_nids[param.nid]].value = param.value end 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 local sql = "SELECT * FROM voicemail_params" local params = getselectresponse(sql) -- Get the uid that corresponds to this username sql = "SELECT uid FROM voicemail_users"..generatewhereclause(userparams.username.value) local uid = getselectresponse(sql) if #uid == 1 then -- There are a few params not to put in the voicemail_values table 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(nil, nil, nil, uid[1].uid).." and nid='"..parm.nid.."'" runsqlcommand(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('"..uid[1].uid.."', '"..parm.nid.."', '"..tostring(userparams[parm.name].value).."')" runsqlcommand(sql) end end end end -- Set password to voicemail_prefs if userparams["vm-password"] and userparams["vm-password"].value and userparams["vm-password"].value ~= "" then 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 runsqlcommand(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 if newconfig.value.callback_url.value == "" then newconfig.value.callback_url.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" }) result.callback_url = cfe({ value=config.callback_url, label="FS SIP URL for Callback", desc="Use $1 for extension. No other parameters allowed." }) 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) 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) runsqlcommand(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*") if address == "" or string.find(address, "%s") or not string.find(address, "@") then retval.errtxt = "Failed to e-mail message - invalid address" else 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 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) runsqlcommand(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 callback_message = function(message, extension, username) local retval = cfe({ label="Callback message result" }) if string.find(message, ",") then retval.errtxt = "Failed to callback message - can only callback one message at a time" elseif extension == "" or string.find(extension, "[%s@]") then retval.errtxt = "Failed to callback message - invalid extension" else local res, err = pcall(function() local connected = databaseconnect() -- Check if message exists local sql = "SELECT * FROM voicemail_msgs" .. generatewhereclause(username, message) local mess = getselectresponse(sql) if #mess == 1 then -- Initiate the call to the extension voicemail_callback(extension, mess[1].file_path, username) retval.value = "Initiated callback" else retval.errtxt = "Failed to callback message - message not found" end if connected then databasedisconnect() end end) if not res and err then retval.errtxt = err end 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_passwords = function(username) local errtxt local users = {} local res, err = pcall(function() local connected = databaseconnect() local sql = "select username, password from voicemail_prefs"..generatewhereclause(username) users = getselectresponse(sql) if connected then databasedisconnect() end end) if not res and err then errtxt = err end return cfe({ type="structure", value=users, label="Voicemail User Passwords", errtxt=errtxt }) end list_users = function() local errtxt local users = {} local res, err = pcall(function() local connected = databaseconnect() -- This crazy query gets the username from voicemail_users, the firstname and lastname from two instances of voicemail_values (using voicemail_params to determine the corresponding nid values) drops usernames starting with "tempuser" and ordering by username local sql = "select u.username, v1.value lastname, v2.value firstname from voicemail_users u left outer join voicemail_values v1 on u.uid = v1.uid AND v1.nid=(select nid from voicemail_params where name='lastname') left outer join voicemail_values v2 on u.uid = v2.uid and v2.nid=(select nid from voicemail_params where name='firstname') where u.username NOT LIKE 'tempuser%' order by u.username" users = getselectresponse(sql) 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(nil, nil, nil, users[1].uid) runsqlcommand(sql) -- Remove the user sql = "DELETE FROM voicemail_users " .. generatewhereclause(nil, nil, nil, users[1].uid) runsqlcommand(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(null, '"..escape(usersettings.value.username.value).."')" runsqlcommand(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