summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTed Trask <ttrask01@yahoo.com>2010-09-09 14:34:52 +0000
committerTed Trask <ttrask01@yahoo.com>2010-09-09 14:34:52 +0000
commitba26ef8ecef31ae3bd145b276994f76e5f633ebd (patch)
tree237b0b89d7548109d73d7c213859514dd890f852
downloadacf-freeswitch-vmail-ba26ef8ecef31ae3bd145b276994f76e5f633ebd.tar.bz2
acf-freeswitch-vmail-ba26ef8ecef31ae3bd145b276994f76e5f633ebd.tar.xz
Initial cut - but pretty close to complete.v0.0.1
-rw-r--r--Makefile48
-rw-r--r--README16
-rw-r--r--authenticator-freeswitch-vmail.lua46
-rw-r--r--config.mk10
-rw-r--r--template-processdialplanxml-xml.lsp18
-rw-r--r--template-processdirectoryxml-xml.lsp32
-rw-r--r--vmail-controller.lua85
l---------vmail-createuser-html.lsp1
l---------vmail-editconfig-html.lsp1
l---------vmail-editmyusersettings-html.lsp1
l---------vmail-editusers-html.lsp1
-rw-r--r--vmail-editusersettings-html.lsp15
-rw-r--r--vmail-listmessages-html.lsp129
l---------vmail-listmymessages-html.lsp1
-rw-r--r--vmail-listusers-html.lsp55
-rw-r--r--vmail-model.lua638
-rw-r--r--vmail.menu7
-rw-r--r--vmail.roles5
18 files changed, 1109 insertions, 0 deletions
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..c3b23a5
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,48 @@
+APP_NAME=freeswitch-vmail
+PACKAGE=acf-$(APP_NAME)
+VERSION=0.0.1
+
+APP_DIST=\
+ vmail* \
+ template* \
+
+LIB_DIST=authenticator-freeswitch-vmail.lua
+
+EXTRA_DIST=README Makefile config.mk
+
+DISTFILES=$(APP_DIST) $(LIB_DIST) $(EXTRA_DIST)
+
+TAR=tar
+
+P=$(PACKAGE)-$(VERSION)
+tarball=$(P).tar.bz2
+install_dir=$(DESTDIR)/$(appdir)/$(APP_NAME)
+
+all:
+clean:
+ rm -rf $(tarball) $(P)
+
+dist: $(tarball)
+
+install:
+ mkdir -p "$(install_dir)"
+ cp -a $(APP_DIST) "$(install_dir)"
+ mkdir -p "$(acflibdir)"
+ cp -a $(LIB_DIST) "$(acflibdir)"
+
+$(tarball): $(DISTFILES)
+ rm -rf $(P)
+ mkdir -p $(P)
+ cp -a $(DISTFILES) $(P)
+ $(TAR) -jcf $@ $(P)
+ rm -rf $(P)
+
+# target that creates a tar package, unpacks is and install from package
+dist-install: $(tarball)
+ $(TAR) -jxf $(tarball)
+ $(MAKE) -C $(P) install DESTDIR=$(DESTDIR)
+ rm -rf $(P)
+
+include config.mk
+
+.PHONY: all clean dist install dist-install
diff --git a/README b/README
new file mode 100644
index 0000000..bd07f7e
--- /dev/null
+++ b/README
@@ -0,0 +1,16 @@
+acf-freeswitch-vmail is a web interface that works with Freeswitch to implement a single-domain voicemail server. Since it uses mod_xml_curl and mod_event_socket, these must be configured properly in Freeswitch for acf-freeswitch-mail to work.
+
+Be sure to load both modules in autoload_configs/modules.conf.xml
+
+The following content can be used in autoload_configs/xml_curl.conf.xml:
+
+<configuration name="xml_curl.conf" description="cURL XML Gateway">
+ <bindings>
+ <binding name="voicemaildialplan">
+ <param name="gateway-url" value="https://127.0.0.1/cgi-bin/acf/freeswitch-vmail/vmail/processdialplanxml" bindings="dialplan"/>
+ </binding>
+ <binding name="voicemaildirectory">
+ <param name="gateway-url" value="https://127.0.0.1/cgi-bin/acf/freeswitch-vmail/vmail/processdirectoryxml" bindings="directory"/>
+ </binding>
+ </bindings>
+</configuration>
diff --git a/authenticator-freeswitch-vmail.lua b/authenticator-freeswitch-vmail.lua
new file mode 100644
index 0000000..5824dc7
--- /dev/null
+++ b/authenticator-freeswitch-vmail.lua
@@ -0,0 +1,46 @@
+-- Copy of authenticator-plaintext, plus added authentication from voicemail DB
+module (..., package.seeall)
+
+require("md5")
+a = require("authenticator-plaintext")
+
+list_fields = function(self, tabl)
+ result = a.list_fields(self, tabl)
+ return result
+end
+
+read_field = function(self, tabl, field)
+ result = a.read_field(self, tabl, field)
+ if tabl == authenticator.usertable and field == "" then
+ -- authenticator is reading all users
+ local vmcontroller = self:new("freeswitch-vmail/vmail")
+ local users = vmcontroller:listusers()
+ for i,val in ipairs(users.value) do
+ local settings = vmcontroller.model.get_usersettings(val.username)
+ local string = md5.sumhexa(settings.value["vm-password"].value)..":Voicemail User:/freeswitch-vmail/vmail/USER"
+ result[#result+1] = { id=settings.value.username.value, entry=string }
+ end
+ vmcontroller:destroy()
+ end
+ return result
+end
+
+delete_field = function(self, tabl, field)
+ result = a.delete_field(self, tabl, field)
+ return result
+end
+
+write_entry = function(self, tabl, field, id, entry)
+ result = a.write_entry(self, tabl, field, id, entry)
+ return result
+end
+
+read_entry = function(self, tabl, field, id)
+ result = a.read_entry(self, tabl, field, id)
+ return result
+end
+
+delete_entry = function (self, tabl, field, id)
+ result = a.delete_entry(self, tabl, field, id)
+ return result
+end
diff --git a/config.mk b/config.mk
new file mode 100644
index 0000000..45f4d21
--- /dev/null
+++ b/config.mk
@@ -0,0 +1,10 @@
+prefix=/usr
+datadir=${prefix}/share
+sysconfdir=${prefix}/etc
+localstatedir=${prefix}/var
+acfdir=${datadir}/acf
+wwwdir=${acfdir}/www
+cgibindir=${acfdir}/cgi-bin
+appdir=${acfdir}/app
+acflibdir=${acfdir}/lib
+sessionsdir=${localstatedir}/lib/acf/sessions
diff --git a/template-processdialplanxml-xml.lsp b/template-processdialplanxml-xml.lsp
new file mode 100644
index 0000000..547d8b4
--- /dev/null
+++ b/template-processdialplanxml-xml.lsp
@@ -0,0 +1,18 @@
+<% local viewtable, viewlibrary, pageinfo, session = ... %>
+<% if viewtable and not viewtable.errtxt then %>
+Content-Type: Content Type: text/xml
+
+<document type="freeswitch/xml">
+ <section name="dialplan">
+ <context name="default">
+ <extension name="Voicemail">
+ <condition expression="(.*)" field="destination_number">
+ <action application="answer" />
+ <action application="sleep" data="1000" />
+ <action application="voicemail" data="default <%= viewtable.value.domain.value %> $1" />
+ </condition>
+ </extension>
+ </context>
+ </section>
+</document>
+<% end %>
diff --git a/template-processdirectoryxml-xml.lsp b/template-processdirectoryxml-xml.lsp
new file mode 100644
index 0000000..b8f9719
--- /dev/null
+++ b/template-processdirectoryxml-xml.lsp
@@ -0,0 +1,32 @@
+<% local viewtable, viewlibrary, pageinfo, session = ... %>
+<% if viewtable and not viewtable.errtxt then %>
+Content-Type: Content Type: text/xml
+
+<document type="freeswitch/xml">
+ <section name="directory">
+ <domain name="<%= viewtable.value.domain.value %>">
+ <params>
+ <param name="dial-string" value="{presence_id=${dialed_user}@${dialed_domain}}${sofia_contact(${dialed_user}@${dialed_domain})}" />
+ </params>
+ <groups>
+ <group name="default">
+ <users>
+ <user id="<%= viewtable.value.username.value %>">
+ <params>
+ <param name="vm-enable" value="true>" />
+ <%
+ local ignore = {username=true, fullname=true, domain=true, ["vm-password-confirm"]=true }
+ for name,val in pairs(viewtable.value) do
+ if not ignore[name] then %>
+ <param name="<%= name %>" value="<%= tostring(val.value) %>" />
+ <% end %>
+ <% end %>
+ </params>
+ </user>
+ </users>
+ </group>
+ </groups>
+ </domain>
+ </section>
+</document>
+<% end %>
diff --git a/vmail-controller.lua b/vmail-controller.lua
new file mode 100644
index 0000000..7b947fb
--- /dev/null
+++ b/vmail-controller.lua
@@ -0,0 +1,85 @@
+module (..., package.seeall)
+
+require("controllerfunctions")
+
+default_action = "listmessages"
+
+listusers = function( self )
+ return self.model.list_users()
+end
+
+editusers = function( self )
+ return self.model.list_users()
+end
+
+deleteuser = function( self )
+ return self:redirect_to_referrer(self.model.delete_user(self.clientdata.username))
+end
+
+listmessages = function( self )
+ return self.model.list_messages(self.clientdata.username)
+end
+
+listmymessages = function( self )
+ return self.model.list_messages(self.sessiondata.userinfo.userid)
+end
+
+downloadmessage = function( self )
+ self.conf.viewtype = "stream"
+ return self.model.get_message(self.clientdata.message)
+end
+
+downloadmymessage = function( self )
+ self.conf.viewtype = "stream"
+ return self.model.get_message(self.clientdata.message, self.sessiondata.userinfo.userid)
+end
+
+deletemessage = function( self )
+ return self:redirect_to_referrer(self.model.delete_message(self.clientdata.message))
+end
+
+deletemymessage = function( self )
+ return self:redirect_to_referrer(self.model.delete_message(self.clientdata.message, self.sessiondata.userinfo.userid))
+end
+
+forwardmessage = function( self )
+ return self:redirect_to_referrer(self.model.forward_message(self.clientdata.message, self.clientdata.newuser))
+end
+
+forwardmymessage = function( self )
+ return self:redirect_to_referrer(self.model.forward_message(self.clientdata.message, self.clientdata.newuser, self.sessiondata.userinfo.userid))
+end
+
+emailmessage = function( self )
+ return self:redirect_to_referrer(self.model.email_message(self.clientdata.message, self.clientdata.address))
+end
+
+emailmymessage = function( self )
+ return self:redirect_to_referrer(self.model.email_message(self.clientdata.message, self.clientdata.address, self.sessiondata.userinfo.userid))
+end
+
+editusersettings = function( self )
+ return controllerfunctions.handle_form(self, function() return self.model.get_usersettings(self.clientdata.username) end, self.model.update_usersettings, self.clientdata, "Save", "Edit Settings", "Settings Saved")
+end
+
+editmyusersettings = function( self )
+ return controllerfunctions.handle_form(self, function() return self.model.get_usersettings(self.sessiondata.userinfo.userid) end, self.model.update_usersettings, self.clientdata, "Save", "Edit Settings", "Settings Saved")
+end
+
+createuser = function( self )
+ return controllerfunctions.handle_form(self, function() return self.model.get_usersettings() end, self.model.create_usersettings, self.clientdata, "Create", "Create User", "User Created")
+end
+
+processdialplanxml = function( self )
+ self.conf.viewtype = "xml"
+ return self.model.process_dialplan_xml_request(self.clientdata)
+end
+
+processdirectoryxml = function( self )
+ self.conf.viewtype = "xml"
+ return self.model.process_directory_xml_request(self.clientdata)
+end
+
+editconfig = function( self )
+ return controllerfunctions.handle_form(self, self.model.get_config, self.model.update_config, self.clientdata, "Save", "Update Config", "Config Saved")
+end
diff --git a/vmail-createuser-html.lsp b/vmail-createuser-html.lsp
new file mode 120000
index 0000000..29fea1f
--- /dev/null
+++ b/vmail-createuser-html.lsp
@@ -0,0 +1 @@
+vmail-editusersettings-html.lsp \ No newline at end of file
diff --git a/vmail-editconfig-html.lsp b/vmail-editconfig-html.lsp
new file mode 120000
index 0000000..4b6b762
--- /dev/null
+++ b/vmail-editconfig-html.lsp
@@ -0,0 +1 @@
+../form-html.lsp \ No newline at end of file
diff --git a/vmail-editmyusersettings-html.lsp b/vmail-editmyusersettings-html.lsp
new file mode 120000
index 0000000..29fea1f
--- /dev/null
+++ b/vmail-editmyusersettings-html.lsp
@@ -0,0 +1 @@
+vmail-editusersettings-html.lsp \ No newline at end of file
diff --git a/vmail-editusers-html.lsp b/vmail-editusers-html.lsp
new file mode 120000
index 0000000..215a6d3
--- /dev/null
+++ b/vmail-editusers-html.lsp
@@ -0,0 +1 @@
+vmail-listusers-html.lsp \ No newline at end of file
diff --git a/vmail-editusersettings-html.lsp b/vmail-editusersettings-html.lsp
new file mode 100644
index 0000000..77cb5bf
--- /dev/null
+++ b/vmail-editusersettings-html.lsp
@@ -0,0 +1,15 @@
+<% local form, viewlibrary, page_info = ...
+require("viewfunctions")
+%>
+
+<H1>Settings for <%= html.html_escape(form.value.fullname.value) %> (<%= html.html_escape(form.value.username.value) %>)</H1>
+<%
+ form.action = page_info.script .. page_info.prefix .. page_info.controller .. "/" .. page_info.action
+ if page_info.action ~= "createuser" then
+ form.value.username.readonly = true
+ end
+ form.value["vm-password"].type = "password"
+ form.value["vm-password-confirm"].type = "password"
+ local order = {"username", "fullname", "vm-password", "vm-password-confirm", "vm-mailto", "vm-email-all-messages", "vm-attach-file", "vm-keep-local-after-email", "vm-notify-mailto", "vm-notify-email-all-messages", "vm-say-caller-id", "vm-say-envelope"}
+ displayform(form, order)
+%>
diff --git a/vmail-listmessages-html.lsp b/vmail-listmessages-html.lsp
new file mode 100644
index 0000000..853d05c
--- /dev/null
+++ b/vmail-listmessages-html.lsp
@@ -0,0 +1,129 @@
+<% local view, viewlibrary, page_info, session = ...
+require("viewfunctions")
+%>
+
+<% -- Pregenerate the list of users
+if viewlibrary.check_permission("listusers") and (viewlibrary.check_permission("forwardmessage") or viewlibrary.check_permission("forwardmymessage")) then
+ local users = viewlibrary.dispatch_component("listusers", nil, true)
+ options = {}
+ for i,u in ipairs(users.value) do
+ if u.username ~= session.userinfo.userid then
+ options[#options+1] = '<option value="'..html.html_escape(u.username)..'">'..html.html_escape(u.fullname)..' ('..html.html_escape(u.username)..')</option>'
+ end
+ end
+ options = table.concat(options)
+end
+%>
+
+<script type="text/javascript" src="<%= html.html_escape(page_info.wwwprefix) %>/js/jquery-latest.js"></script>
+<script type="text/javascript" src="<%= html.html_escape(page_info.wwwprefix) %>/js/jquery.tablesorter.js"></script>
+<script type="text/javascript">
+ function PlayMessage() {
+ $(".temporaryplayer").remove();
+ $(this).parent().parent().after("<tr class='temporaryplayer'><td colspan='8'><embed width='100%' height='25px' marginheight='0' marginwidth='0' frameborder='0' scrolling='no' autostart='false' autoplay='false' loop='false' src='" + $(this).attr("href") + "'></embed></TD></TR>");
+ return false;
+ }
+ function HandleMulti() {
+ var messages = [];
+ $(".multicheck:checked").each(function(){
+ messages[messages.length] = this.value;
+ });
+ $(this).parent().find("[name='message']").attr("value", messages.join(","));
+ }
+ $(document).ready(function() {
+ $("#list").tablesorter({headers: {0:{sorter: false}, 1:{sorter: false}}});
+ $("#list").bind("sortStart",function() {
+ $(".temporaryplayer").remove();
+ });
+ $(".playmessage").click(PlayMessage);
+<% if viewlibrary.check_permission("deletemessage") or viewlibrary.check_permission("deletemymessage") then %>
+ $("#multidelete").click(HandleMulti);
+<% end %>
+<% if viewlibrary.check_permission("forwardmessage") or viewlibrary.check_permission("forwardmymessage") then %>
+ $("#multiforward").click(HandleMulti);
+<% end %>
+<% if viewlibrary.check_permission("emailmessage") or viewlibrary.check_permission("emailmymessage") then %>
+ $("#multiemail").click(HandleMulti);
+<% end %>
+ });
+</script>
+
+<% displaycommandresults({"deletemessage", "deletemymessage", "forwardmessage", "forwardmymessage", "emailmessage", "emailmymessage"}, session) %>
+
+<h1>Messages</h1>
+<DL>
+<TABLE><TR><TD>
+<% if viewlibrary.check_permission("deletemessage") or viewlibrary.check_permission("deletemymessage") then %>
+ <form id="multidelete" action="<%= html.html_escape(page_info.script..page_info.prefix..page_info.controller) %>/
+ <% if viewlibrary.check_permission("deletemessage") then io.write("deletemessage") else io.write("deletemymessage") end %>
+ " method="POST">
+ <input class="hidden" type="hidden" name="redir" value="<%= html.html_escape(page_info.orig_action) %>" >
+ <input class="hidden" type="hidden" name="message" value="" >
+ <input class="submit" type="submit" value="Delete">
+ </form>
+<% end %>
+<% if viewlibrary.check_permission("forwardmessage") or viewlibrary.check_permission("forwardmymessage") then %>
+ <form id="multiforward" action="<%= html.html_escape(page_info.script .. page_info.prefix .. page_info.controller) %>/
+ <% if viewlibrary.check_permission("forwardmessage") then io.write("forwardmessage") else io.write("forwardmymessage") end %>
+ " method="POST">
+ <input class="hidden" type="hidden" name="redir" value="<%= html.html_escape(page_info.orig_action) %>" >
+ <input class="hidden" type="hidden" name="message" value="" >
+ <select name="newuser"><%= (options or "") %></select>
+ <input class="submit" type="submit" value="Forward">
+ </form>
+<% end %>
+<% if viewlibrary.check_permission("emailmessage") or viewlibrary.check_permission("emailmymessage") then %>
+ <form id="multiemail" action="<%= html.html_escape(page_info.script .. page_info.prefix .. page_info.controller) %>/
+ <% if viewlibrary.check_permission("emailmessage") then io.write("emailmessage") else io.write("emailmymessage") end %>
+ " method="POST">
+ <input class="hidden" type="hidden" name="redir" value="<%= html.html_escape(page_info.orig_action) %>" >
+ <input class="hidden" type="hidden" name="message" value="" >
+ <input class="text" type="text" name="address" value="" >
+ <input class="submit" type="submit" value="E-mail">
+ </form>
+<% end %>
+</TD></TR></TABLE>
+
+<TABLE id="list" class="tablesorter"><THEAD>
+ <TR style="background:#eee;font-weight:bold;">
+ <TH></TH>
+ <TH>Action</TH>
+ <TH>Date</TH>
+ <TH>Time</TH>
+ <TH>Caller ID</TH>
+ <TH>Priority</TH>
+ <TH>Orig Mailbox</Th>
+ <TH>Duration</TH>
+ </TR>
+</THEAD><TBODY>
+<% for k,v in ipairs( view.value ) do %>
+ <TR>
+ <TD><input type=checkbox class="multicheck" value="<%= html.html_escape(v.uuid) %>"></TD>
+ <TD>
+ <% if viewlibrary.check_permission("downloadmessage") then %>
+ <%= html.link{value=page_info.script..page_info.prefix..page_info.controller.."/downloadmessage?message="..v.uuid, label="Download "} %>
+ <%= html.link{value=page_info.script..page_info.prefix..page_info.controller.."/downloadmessage?message="..v.uuid, class="playmessage", label="Play "} %>
+ <% elseif viewlibrary.check_permission("downloadmymessage") then %>
+ <%= html.link{value=page_info.script..page_info.prefix..page_info.controller.."/downloadmymessage?message="..v.uuid, label="Download "} %>
+ <%= html.link{value=page_info.script..page_info.prefix..page_info.controller.."/downloadmymessage?message="..v.uuid, class="playmessage", label="Play "} %>
+ <% end %>
+ </TD>
+ <TD><%= html.html_escape(os.date("%x", v.created_epoch)) %></TD>
+ <TD><%= html.html_escape(os.date("%X", v.created_epoch)) %></TD>
+ <TD><%= html.html_escape(v.cid_number) %></TD>
+ <TD><%= html.html_escape(v.read_flags) %></TD>
+ <TD><%= html.html_escape(v.username) %></TD>
+ <TD><%= html.html_escape(v.message_len) %></TD>
+ </TR>
+<% end %>
+</TBODY>
+</TABLE>
+
+<% if view.errtxt then %>
+<p class="error"><%= html.html_escape(view.errtxt) %></p>
+<% end %>
+<% if #view.value == 0 then %>
+<p>No messages found</p>
+<% end %>
+
+</DL>
diff --git a/vmail-listmymessages-html.lsp b/vmail-listmymessages-html.lsp
new file mode 120000
index 0000000..a18ada3
--- /dev/null
+++ b/vmail-listmymessages-html.lsp
@@ -0,0 +1 @@
+vmail-listmessages-html.lsp \ No newline at end of file
diff --git a/vmail-listusers-html.lsp b/vmail-listusers-html.lsp
new file mode 100644
index 0000000..3445ae2
--- /dev/null
+++ b/vmail-listusers-html.lsp
@@ -0,0 +1,55 @@
+<% local view, viewlibrary, page_info, session = ...
+require("viewfunctions")
+%>
+
+<script type="text/javascript" src="<%= html.html_escape(page_info.wwwprefix) %>/js/jquery-latest.js"></script>
+<script type="text/javascript" src="<%= html.html_escape(page_info.wwwprefix) %>/js/jquery.tablesorter.js"></script>
+<script type="text/javascript">
+ $(document).ready(function() {
+ $("#list").tablesorter({headers: {0:{sorter: false}}});
+ });
+</script>
+
+<% displaycommandresults({"createuser", "deleteuser", "editusersettings"}, session) %>
+
+<h1>Messages</h1>
+<DL><TABLE id="list" class="tablesorter"><THEAD>
+ <TR style="background:#eee;font-weight:bold;">
+ <TH>Action</TH>
+ <TH>Extension</TH>
+ <TH>Full Name</TH>
+ </TR>
+</THEAD><TBODY>
+<% for k,v in ipairs( view.value ) do %>
+ <TR>
+ <TD>
+ <% if viewlibrary.check_permission("editusersettings") then %>
+ <%= html.link{value=page_info.script..page_info.prefix..page_info.controller.."/editusersettings?username="..v.username, label="Edit "} %>
+ <% end %>
+ <% if viewlibrary.check_permission("deleteuser") then %>
+ <%= html.link{value=page_info.script..page_info.prefix..page_info.controller.."/deleteuser?username="..v.username, label="Delete "} %>
+ <% end %>
+ </TD>
+ <TD><%= html.html_escape(v.username) %></TD>
+ <TD><%= html.html_escape(v.fullname) %></TD>
+ </TR>
+<% end %>
+</TBODY>
+</TABLE>
+
+<% if view.errtxt then %>
+<p class="error"><%= html.html_escape(view.errtxt) %></p>
+<% end %>
+<% if #view.value == 0 then %>
+<p>No users found</p>
+<% end %>
+
+<% if viewlibrary and viewlibrary.dispatch_component and viewlibrary.check_permission("createuser") then %>
+<H2>Create New User</H2>
+<form action="<%= html.html_escape(page_info.script .. page_info.prefix .. page_info.controller) %>/createuser" method="POST">
+<input class="hidden" type="hidden" name="redir" value="<%= html.html_escape(page_info.orig_action) %>" >
+<dl><dt></dt><dd><input class="submit" type="submit" value="Create"></dd></dl>
+</form>
+<% end %>
+
+</DL>
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
diff --git a/vmail.menu b/vmail.menu
new file mode 100644
index 0000000..e6c8895
--- /dev/null
+++ b/vmail.menu
@@ -0,0 +1,7 @@
+# Prefix and controller are already known at this point
+# Cat Group Tab Action
+Applications 88Voicemail Users editusers
+Applications 88Voicemail Voicemail listmessages
+Applications 88Voicemail Voicemail listmymessages
+Applications 88Voicemail Settings editmyusersettings
+Applications 88Voicemail Config editconfig
diff --git a/vmail.roles b/vmail.roles
new file mode 100644
index 0000000..6d72cb8
--- /dev/null
+++ b/vmail.roles
@@ -0,0 +1,5 @@
+GUEST=vmail:processdialplanxml,vmail:processdirectoryxml
+USER=vmail:listusers,vmail:listmymessages,vmail:downloadmymessage,vmail:deletemymessage,vmail:editmyusersettings,vmail:forwardmymessage,vmail:emailmymessage
+EDITOR=vmail:editusers,vmail:createuser,vmail:deleteuser,vmail:editusersettings
+EXPERT=vmail:editconfig,vmail:listmessages,vmail:downloadmessage,vmail:deletemessage,vmail:forwardmessage,vmail:emailmessage
+ADMIN=vmail:listmessages,vmail:downloadmessage,vmail:deletemessage,vmail:forwardmessage,vmail:emailmessage,vmail:editusersettings,vmail:listusers,vmail:editusers,vmail:createuser,vmail:deleteuser,vmail:editconfig