Add Alternate Voicemail Location to voicemail box settings (#7932)

Adds a per-voicemail-box option that diverts the caller to an arbitrary
dialable destination instead of leaving a message. Useful for handing
voicemail off to an external service (e.g. Microsoft Teams, a paging
group, an answering service) while keeping the caller's original
Caller-ID intact for downstream logging.

The setting lives on the voicemail box (v_voicemails) — not on the
extension's follow-me/forwarding settings — so it only fires when a call
actually reaches voicemail.lua. It does not interact with the existing
"No Answer" forward on the call-forward page, nor with call-center,
ring-group, or FIFO no-answer handling: those mechanisms route the call
elsewhere before voicemail.lua ever runs, so the divert never triggers
on those paths in the first place.

Schema (v_voicemails, registered in app/voicemails/app_config.php so
upgrades pick it up automatically):

* alternate_voicemail_enabled       boolean
* alternate_voicemail_destination   text  (any dialable string —
                                            extension number, external
                                            number, etc.)

UI is added to the voicemail edit screen
(app/voicemails/voicemail_edit.php) directly under "Mail To", mirroring
the toggle + destination input pattern used on the call-forward page.
Sanitisation matches the existing forward_*_destination fields
([^\*0-9] stripped).

Voicemail behaviour change is in
app/switch/resources/scripts/app/voicemail/index.lua: when a caller hits
a valid voicemail box and the box has the alternate voicemail location
enabled, the script transfers the call via session:transfer(destination,
"XML", context) BEFORE the greeting plays. session:transfer preserves
channel variables so the original Caller-ID is carried into the diverted
leg.

Override semantics — when enabled, the alternate location always wins.
The voicemail menu (voicemail_action == "check") path is unaffected, so
extension owners can still log in and review their existing messages.

Group-call safety net: the divert is intentionally skipped when the leg
is part of a call-center, ring-group, or FIFO bridge (detected via the
cc_side, cc_queue, dialed_extension, ring_group_uuid, or fifo_role
channel variables). In normal use these paths shouldn't reach the
voicemail Lua at all — the queue / group manages its own no-answer
behaviour upstream — but if one does land here for any reason, we don't
want a session:transfer to yank the leg out of the queue's control and
hijack the call to the alternate destination.
This commit is contained in:
teknoprep
2026-05-19 15:13:26 -04:00
committed by GitHub
parent 06e4eda5df
commit ed9ba8a21a
4 changed files with 105 additions and 1 deletions
@@ -503,6 +503,61 @@
--valid voicemail --valid voicemail
if (voicemail_uuid ~= nil) then if (voicemail_uuid ~= nil) then
--check for an alternate voicemail location on this voicemail box; when
--enabled, divert the caller to that destination instead of leaving a
--message. caller id is preserved (session:transfer keeps channel vars).
--
--this is a per-voicemail-box setting (v_voicemails table) — it only
--fires when a call actually reaches this script with action=save, so
--it does NOT interfere with the dialplan's forward-on-no-answer or
--with call-center/ring-group/fifo no-answer handling. Those paths
--never enter this script in the first place if they're configured to
--route elsewhere before voicemail.
--
--the cc_side / ring_group_uuid / fifo_role / dialed_extension guard
--is kept as a safety net: if for any reason a call-center or
--ring-group leg lands here, we don't want a session:transfer to yank
--it out of the queue's control.
do
local group_signals = {
cc_side = session:getVariable("cc_side"),
cc_queue = session:getVariable("cc_queue"),
dialed_extension = session:getVariable("dialed_extension"),
ring_group_uuid = session:getVariable("ring_group_uuid"),
fifo_role = session:getVariable("fifo_role"),
};
local in_group_call = false;
for signal_name, signal_value in pairs(group_signals) do
if (signal_value ~= nil and signal_value ~= '') then
in_group_call = true;
freeswitch.consoleLog("notice", "[voicemail] alternate voicemail divert skipped: " .. signal_name .. "=" .. tostring(signal_value) .. " (call-center / ring-group / fifo leg)\n");
break;
end
end
if (not in_group_call) then
local alt_sql = [[SELECT
cast(alternate_voicemail_enabled as text) as alt_enabled,
alternate_voicemail_destination
FROM v_voicemails
WHERE voicemail_uuid = :voicemail_uuid
LIMIT 1]];
local alt_params = {voicemail_uuid = voicemail_uuid};
local alt_enabled = false;
local alt_destination = nil;
dbh:query(alt_sql, alt_params, function(row)
alt_enabled = (row["alt_enabled"] == "true");
alt_destination = row["alternate_voicemail_destination"];
end);
if (alt_enabled and alt_destination ~= nil and alt_destination ~= '') then
freeswitch.consoleLog("notice", "[voicemail] alternate voicemail location enabled — diverting caller " .. (caller_id_number or '') .. " to " .. alt_destination .. " instead of leaving a message for " .. voicemail_id .. "\n");
if (session ~= nil and session:ready()) then
session:transfer(alt_destination, "XML", context);
end
return;
end
end
end
--play the greeting --play the greeting
timeouts = 0; timeouts = 0;
play_greeting(); play_greeting();
+8
View File
@@ -501,6 +501,14 @@
$apps[$x]['db'][$y]['fields'][$z]['search'] = 'true'; $apps[$x]['db'][$y]['fields'][$z]['search'] = 'true';
$apps[$x]['db'][$y]['fields'][$z]['description']['en-us'] = "Enter the description."; $apps[$x]['db'][$y]['fields'][$z]['description']['en-us'] = "Enter the description.";
$z++; $z++;
$apps[$x]['db'][$y]['fields'][$z]['name'] = "alternate_voicemail_destination";
$apps[$x]['db'][$y]['fields'][$z]['type'] = "text";
$apps[$x]['db'][$y]['fields'][$z]['description']['en-us'] = "When set together with alternate_voicemail_enabled, callers reaching this voicemail box are transferred to this destination before the greeting plays. Caller ID is preserved.";
$z++;
$apps[$x]['db'][$y]['fields'][$z]['name'] = "alternate_voicemail_enabled";
$apps[$x]['db'][$y]['fields'][$z]['type'] = "boolean";
$apps[$x]['db'][$y]['fields'][$z]['description']['en-us'] = "When true and alternate_voicemail_destination is set, the call is diverted instead of leaving a message.";
$z++;
$apps[$x]['db'][$y]['fields'][$z]['name'] = "voicemail_name_base64"; $apps[$x]['db'][$y]['fields'][$z]['name'] = "voicemail_name_base64";
$apps[$x]['db'][$y]['fields'][$z]['type'] = "text"; $apps[$x]['db'][$y]['fields'][$z]['type'] = "text";
$apps[$x]['db'][$y]['fields'][$z]['description']['en-us'] = ""; $apps[$x]['db'][$y]['fields'][$z]['description']['en-us'] = "";
+6
View File
@@ -2672,4 +2672,10 @@ $text['message-emails_resent']['zh-cn'] = "电子邮件已重新发送";
$text['message-emails_resent']['ja-jp'] = "再送信メール"; $text['message-emails_resent']['ja-jp'] = "再送信メール";
$text['message-emails_resent']['ko-kr'] = "이메일 재전송"; $text['message-emails_resent']['ko-kr'] = "이메일 재전송";
$text['label-alternate_voicemail']['en-us'] = "Alternate Voicemail Location";
$text['label-alternate_voicemail']['en-gb'] = "Alternate Voicemail Location";
$text['description-alternate_voicemail']['en-us'] = "When enabled, callers reaching this voicemail box are routed to this destination instead of leaving a message. The original Caller ID is preserved.";
$text['description-alternate_voicemail']['en-gb'] = "When enabled, callers reaching this voicemail box are routed to this destination instead of leaving a message. The original Caller ID is preserved.";
?> ?>
+36 -1
View File
@@ -60,6 +60,8 @@
$voicemail_option_digits = ''; $voicemail_option_digits = '';
$voicemail_option_description = ''; $voicemail_option_description = '';
$voicemail_mail_to = ''; $voicemail_mail_to = '';
$alternate_voicemail_enabled = false;
$alternate_voicemail_destination = '';
$transcribe_enabled = $settings->get('transcribe', 'enabled', false); $transcribe_enabled = $settings->get('transcribe', 'enabled', false);
// Set variables from http GET parameters // Set variables from http GET parameters
@@ -130,6 +132,8 @@
$voicemail_destination = $_POST["voicemail_destination"]; $voicemail_destination = $_POST["voicemail_destination"];
$voicemail_enabled = $_POST["voicemail_enabled"]; $voicemail_enabled = $_POST["voicemail_enabled"];
$voicemail_description = $_POST["voicemail_description"]; $voicemail_description = $_POST["voicemail_description"];
$alternate_voicemail_enabled = filter_var($_POST["alternate_voicemail_enabled"] ?? false, FILTER_VALIDATE_BOOLEAN);
$alternate_voicemail_destination = $_POST["alternate_voicemail_destination"] ?? '';
$voicemail_tutorial = $_POST["voicemail_tutorial"]; $voicemail_tutorial = $_POST["voicemail_tutorial"];
$voicemail_recording_instructions = $_POST["voicemail_recording_instructions"]; $voicemail_recording_instructions = $_POST["voicemail_recording_instructions"];
$voicemail_recording_options = $_POST["voicemail_recording_options"]; $voicemail_recording_options = $_POST["voicemail_recording_options"];
@@ -138,6 +142,9 @@
//remove the space //remove the space
$voicemail_mail_to = str_replace(" ", "", $voicemail_mail_to); $voicemail_mail_to = str_replace(" ", "", $voicemail_mail_to);
//sanitise the alternate voicemail destination — digits and * only (matches forward_*_destination)
$alternate_voicemail_destination = preg_replace('#[^\*0-9]#', '', $alternate_voicemail_destination);
} }
//process the data //process the data
@@ -212,6 +219,8 @@
} }
$array['voicemails'][0]['voicemail_enabled'] = $voicemail_enabled; $array['voicemails'][0]['voicemail_enabled'] = $voicemail_enabled;
$array['voicemails'][0]['voicemail_description'] = $voicemail_description; $array['voicemails'][0]['voicemail_description'] = $voicemail_description;
$array['voicemails'][0]['alternate_voicemail_enabled'] = $alternate_voicemail_enabled ? 'true' : 'false';
$array['voicemails'][0]['alternate_voicemail_destination'] = $alternate_voicemail_destination;
//create permissions object //create permissions object
$p = permissions::new(); $p = permissions::new();
@@ -367,7 +376,9 @@
$sql .= "voicemail_file, "; $sql .= "voicemail_file, ";
$sql .= "voicemail_local_after_email, "; $sql .= "voicemail_local_after_email, ";
$sql .= "voicemail_enabled, "; $sql .= "voicemail_enabled, ";
$sql .= "voicemail_description "; $sql .= "voicemail_description, ";
$sql .= "alternate_voicemail_enabled, ";
$sql .= "alternate_voicemail_destination ";
$sql .= "from v_voicemails "; $sql .= "from v_voicemails ";
$sql .= "where domain_uuid = :domain_uuid "; $sql .= "where domain_uuid = :domain_uuid ";
$sql .= "and voicemail_uuid = :voicemail_uuid "; $sql .= "and voicemail_uuid = :voicemail_uuid ";
@@ -389,6 +400,8 @@
$voicemail_local_after_email = $row["voicemail_local_after_email"]; $voicemail_local_after_email = $row["voicemail_local_after_email"];
$voicemail_enabled = $row["voicemail_enabled"]; $voicemail_enabled = $row["voicemail_enabled"];
$voicemail_description = $row["voicemail_description"]; $voicemail_description = $row["voicemail_description"];
$alternate_voicemail_enabled = filter_var($row["alternate_voicemail_enabled"] ?? false, FILTER_VALIDATE_BOOLEAN);
$alternate_voicemail_destination = $row["alternate_voicemail_destination"] ?? '';
} }
unset($sql, $parameters, $row); unset($sql, $parameters, $row);
} }
@@ -854,6 +867,28 @@
echo "</td>\n"; echo "</td>\n";
echo "</tr>\n"; echo "</tr>\n";
echo "<tr>\n";
echo "<td class='vncell' valign='top' align='left' nowrap='nowrap'>\n";
echo " ".$text['label-alternate_voicemail']."\n";
echo "</td>\n";
echo "<td class='vtable' align='left'>\n";
if ($input_toggle_style_switch) {
echo " <span class='switch'>\n";
}
echo " <select class='formfld' id='alternate_voicemail_enabled' name='alternate_voicemail_enabled'>\n";
echo " <option value='true' ".($alternate_voicemail_enabled === true ? "selected='selected'" : '').">".$text['option-true']."</option>\n";
echo " <option value='false' ".($alternate_voicemail_enabled === false ? "selected='selected'" : '').">".$text['option-false']."</option>\n";
echo " </select>\n";
if ($input_toggle_style_switch) {
echo " <span class='slider'></span>\n";
echo " </span>\n";
echo "&nbsp;";
}
echo " <input class='formfld' type='text' name='alternate_voicemail_destination' id='alternate_voicemail_destination' ".($input_toggle_style_switch ? "style='margin-top: -21px;'" : null)." maxlength='255' placeholder=\"".$text['label-destination']."\" value=\"".escape($alternate_voicemail_destination ?? '')."\">\n";
echo " <br />".$text['description-alternate_voicemail']."\n";
echo "</td>\n";
echo "</tr>\n";
if (permission_exists('voicemail_sms_edit') && file_exists(dirname(__DIR__, 2).'/app/sms/')) { if (permission_exists('voicemail_sms_edit') && file_exists(dirname(__DIR__, 2).'/app/sms/')) {
echo "<tr>\n"; echo "<tr>\n";
echo "<td class='vncell' valign='top' align='left' nowrap='nowrap'>\n"; echo "<td class='vncell' valign='top' align='left' nowrap='nowrap'>\n";