summaryrefslogtreecommitdiffstats
path: root/vmail-model.lua
diff options
context:
space:
mode:
Diffstat (limited to 'vmail-model.lua')
-rw-r--r--vmail-model.lua638
1 files changed, 638 insertions, 0 deletions
diff --git a/vmail-model.lua b/vmail-model.lua
new file mode 100644
index 0000000..886e910
--- /dev/null
+++ b/vmail-model.lua
@@ -0,0 +1,638 @@
+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 text)",
+}
+
+local voicemail_values_creation_script = {
+ "CREATE TABLE voicemail_values (username text, name text, value text)",
+}
+
+local voicemail_params_creation_script = {
+ "CREATE TABLE voicemail_params (name text primary key, type text, label text, descr text, value text)",
+ "INSERT INTO voicemail_params VALUES('username', 'text', 'Extension', '', '')",
+ "INSERT INTO voicemail_params VALUES('fullname', 'text', 'Full User 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_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)
+ 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 #where > 0 then
+ sql = " WHERE " .. table.concat(where, " AND ")
+ end
+ return sql
+end
+
+-- These funtions access the new voicemail tables added for ACF
+
+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
+
+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 value FROM voicemail_values"..generatewhereclause(u.username).." and name='fullname'"
+ local cur = con:execute(sql)
+ if cur then
+ local row = cur:fetch ({}, "a")
+ if row and row.value then
+ u.fullname = row.value
+ end
+ cur:close()
+ end
+ 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