diff --git a/applications/callflow/doc/voicemail.md b/applications/callflow/doc/voicemail.md index 3b2c8792b3a..bcf02e2c104 100644 --- a/applications/callflow/doc/voicemail.md +++ b/applications/callflow/doc/voicemail.md @@ -91,3 +91,14 @@ For example, to monitor and check box 3456, the BLF key could be tied to `*98345 !!! note If you want to do MWI subscriptions, you must configure the account or system to do so. In the `voicemail` system_config document (or the account's config doc), set `dialog_subscribed_mwi_prefix` to the prefix (in this above case, `*98` would be the value): `sup kapps_config set_default voicemail dialog_subscribed_mwi_prefix '*98'`. + +## System configs + +### Callback option + +When checking voicemails, it is possible for the caller to use the `callback` feature to have the system place a call to the caller ID number on the voicemail. While convenient, a compromised voicemail box can be used to initiate fraudulent calls. + +System administrators can toggle two configurations to manage this feature: + +1. `voicemail.should_disable_callback`: if set to `true` no callers will be able to use the callback feature +2. `voicemail.should_disable_offnet_callback`: if set to `true`, the caller *must* be calling from an authorized device (not from outside the account). diff --git a/applications/callflow/src/module/cf_park.erl b/applications/callflow/src/module/cf_park.erl index 2c302717834..16e3cb4a423 100644 --- a/applications/callflow/src/module/cf_park.erl +++ b/applications/callflow/src/module/cf_park.erl @@ -24,15 +24,15 @@ -define(MOD_CONFIG_CAT, <<(?CF_CONFIG_CAT)/binary, ".park">>). -define(DB_DOC_NAME, kapps_config:get_binary(?MOD_CONFIG_CAT, <<"db_doc_name">>, <<"parked_calls">>)). --define(DEFAULT_RINGBACK_TM, kapps_config:get_integer(?MOD_CONFIG_CAT, <<"default_ringback_timeout">>, 120000)). --define(DEFAULT_CALLBACK_TM, kapps_config:get_integer(?MOD_CONFIG_CAT, <<"default_callback_timeout">>, 30000)). +-define(DEFAULT_RINGBACK_TM, kapps_config:get_integer(?MOD_CONFIG_CAT, <<"default_ringback_timeout">>, 120 * ?MILLISECONDS_IN_SECOND)). +-define(DEFAULT_CALLBACK_TM, kapps_config:get_integer(?MOD_CONFIG_CAT, <<"default_callback_timeout">>, 30 * ?MILLISECONDS_IN_SECOND)). -define(PARKED_CALLS_KEY(Db), {?MODULE, 'parked_calls', Db}). -define(DEFAULT_PARKED_TYPE, <<"early">>). -define(SYSTEM_PARKED_TYPE, kapps_config:get_ne_binary(?MOD_CONFIG_CAT, <<"parked_presence_type">>, ?DEFAULT_PARKED_TYPE)). -define(ACCOUNT_PARKED_TYPE(A), kapps_account_config:get(A, ?MOD_CONFIG_CAT, <<"parked_presence_type">>, ?SYSTEM_PARKED_TYPE)). -define(PRESENCE_TYPE_KEY, <<"Presence-Type">>). -define(PARK_DELAY_CHECK_TIME_KEY, <<"valet_reservation_cleanup_time_ms">>). --define(PARK_DELAY_CHECK_TIME, kapps_config:get_integer(?MOD_CONFIG_CAT, ?PARK_DELAY_CHECK_TIME_KEY, ?MILLISECONDS_IN_SECOND * 3)). +-define(PARK_DELAY_CHECK_TIME, kapps_config:get_integer(?MOD_CONFIG_CAT, ?PARK_DELAY_CHECK_TIME_KEY, 3 * ?MILLISECONDS_IN_SECOND)). -define(PARKING_APP_NAME, <<"park">>). %%------------------------------------------------------------------------------ diff --git a/applications/callflow/src/module/cf_voicemail.erl b/applications/callflow/src/module/cf_voicemail.erl index 57486ca5d2a..dae81603349 100644 --- a/applications/callflow/src/module/cf_voicemail.erl +++ b/applications/callflow/src/module/cf_voicemail.erl @@ -24,12 +24,12 @@ -module(cf_voicemail). -behaviour(gen_cf_action). --include("callflow.hrl"). --include_lib("kazoo_stdlib/include/kazoo_json.hrl"). - -export([handle/2]). -export([new_message/4]). +-include("callflow.hrl"). +-include_lib("kazoo_stdlib/include/kazoo_json.hrl"). + -define(KEY_VOICEMAIL, <<"voicemail">>). -define(KEY_MAX_MESSAGE_COUNT, <<"max_message_count">>). -define(KEY_MAX_MESSAGE_LENGTH, <<"max_message_length">>). @@ -115,6 +115,18 @@ ,'false' ) ). +-define(SHOULD_DISABLE_CALLBACK + ,kapps_config:is_true(?CF_CONFIG_CAT + ,[?KEY_VOICEMAIL, <<"should_disable_callback">>] + ,'false' + ) + ). +-define(SHOULD_DISABLE_OFFNET_CALLBACK + ,kapps_config:is_true(?CF_CONFIG_CAT + ,[?KEY_VOICEMAIL, <<"should_disable_offnet_callback">>] + ,'false' + ) + ). -define(DEFAULT_FIND_BOX_PROMPT, <<"vm-enter_id">>). @@ -913,9 +925,14 @@ play_messages(Messages, Count, Box, Call) -> -spec play_messages(kz_json:objects(), kz_json:objects(), non_neg_integer(), mailbox(), kapps_call:call()) -> 'ok' | 'complete'. -play_messages([H|T]=Messages, PrevMessages, Count, #mailbox{seek_duration=SeekDuration, mailbox_id=BoxId}=Box, Call) -> +play_messages([CurrentMessage|NextMessages]=Messages + ,PrevMessages + ,Count + ,#mailbox{seek_duration=SeekDuration, mailbox_id=BoxId}=Box + ,Call + ) -> AccountId = kapps_call:account_id(Call), - Message = kvm_message:media_url(AccountId, H), + Message = kvm_message:media_url(AccountId, CurrentMessage), lager:info("playing mailbox message ~p (~s)", [Count, Message]), Prompt = message_prompt(Messages, Message, Count, Box), case message_menu(Prompt, Box, Call) of @@ -923,8 +940,8 @@ play_messages([H|T]=Messages, PrevMessages, Count, #mailbox{seek_duration=SeekDu lager:info("caller chose to save the message"), _ = kapps_call_command:flush(Call), _ = kapps_call_command:b_prompt(<<"vm-saved">>, Call), - {_, NMessage} = kvm_message:set_folder(?VM_FOLDER_SAVED, H, AccountId), - play_messages(T, [NMessage|PrevMessages], Count, Box, Call); + {_, SavedMessage} = kvm_message:set_folder(?VM_FOLDER_SAVED, CurrentMessage, AccountId), + play_messages(NextMessages, [SavedMessage|PrevMessages], Count, Box, Call); {'ok', 'prev'} -> lager:info("caller chose to listen to previous message"), _ = kapps_call_command:flush(Call), @@ -937,16 +954,16 @@ play_messages([H|T]=Messages, PrevMessages, Count, #mailbox{seek_duration=SeekDu lager:info("caller chose to delete the message"), _ = kapps_call_command:flush(Call), _ = kapps_call_command:b_prompt(<<"vm-deleted">>, Call), - MessageId = kz_json:get_ne_binary_value(<<"media_id">>, H), + MessageId = kz_json:get_ne_binary_value(<<"media_id">>, CurrentMessage), JObj = hd(kz_json:get_list_value(<<"succeeded">>, kvm_messages:fetch(AccountId, [MessageId], BoxId))), kvm_util:publish_voicemail_deleted(BoxId, JObj, 'dtmf'), - _ = kvm_message:set_folder({?VM_FOLDER_DELETED, 'false'}, H, AccountId), - play_messages(T, PrevMessages, Count, Box, Call); + _ = kvm_message:set_folder({?VM_FOLDER_DELETED, 'false'}, CurrentMessage, AccountId), + play_messages(NextMessages, PrevMessages, Count, Box, Call); {'ok', 'return'} -> lager:info("caller chose to return to the main menu"), _ = kapps_call_command:flush(Call), _ = kapps_call_command:b_prompt(<<"vm-saved">>, Call), - _ = kvm_message:set_folder(?VM_FOLDER_SAVED, H, AccountId), + _ = kvm_message:set_folder(?VM_FOLDER_SAVED, CurrentMessage, AccountId), 'complete'; {'ok', 'replay'} -> lager:info("caller chose to replay"), @@ -955,23 +972,12 @@ play_messages([H|T]=Messages, PrevMessages, Count, #mailbox{seek_duration=SeekDu {'ok', 'forward'} -> lager:info("caller chose to forward the message"), _ = kapps_call_command:flush(Call), - forward_message(H, Box, Call), - {_, NMessage} = kvm_message:set_folder(?VM_FOLDER_SAVED, H, AccountId), + forward_message(CurrentMessage, Box, Call), + {_, SavedMessage} = kvm_message:set_folder(?VM_FOLDER_SAVED, CurrentMessage, AccountId), _ = kapps_call_command:prompt(<<"vm-saved">>, Call), - play_messages(T, [NMessage|PrevMessages], Count, Box, Call); + play_messages(NextMessages, [SavedMessage|PrevMessages], Count, Box, Call); {'ok', 'callback'} -> - case kz_json:get_value(<<"caller_id_number">>,H) of - 'undefined' -> - lager:info("message not contains caller_id_number and we cannot callback"), - _ = kapps_call_command:audio_macro([{'prompt', <<"vm-not_available">>}], Call), - play_messages(Messages, PrevMessages, Count, Box, Call); - Number -> - lager:info("caller chose to callback number ~s", [Number]), - case maybe_branch_call(Call, Number, Box) of - 'ok' -> 'ok'; - _ -> play_messages(Messages, PrevMessages, Count, Box, Call) - end - end; + maybe_handle_vm_callback(CurrentMessage, Messages, PrevMessages, Count, Box, Call); {'ok', 'rewind'} -> lager:info("caller chose to rewind 10 sec of the message"), _ = kapps_call_command:seek('rewind', SeekDuration, Call), @@ -988,62 +994,115 @@ play_messages([], _, _, _, _) -> lager:info("all messages in folder played to caller"), 'complete'. --spec maybe_branch_call(kapps_call:call(), kz_term:ne_binary(), mailbox()) -> 'ok'| 'error'. -maybe_branch_call(Call, Number, #mailbox{owner_id=OwnerId}) -> - EndpointId = case kapps_call:authorizing_id(Call) of - 'undefined' -> OwnerId; - AuthorizingId -> AuthorizingId - end, - case EndpointId =:= 'undefined' - andalso kz_endpoint:get(EndpointId, Call) of +maybe_handle_vm_callback(CurrentMessage, Messages, PrevMessages, Count, Box, Call) -> + case ?SHOULD_DISABLE_CALLBACK of + 'true' -> + lager:info("callback disabled by configuration"), + callback_not_available(Messages, PrevMessages, Count, Box, Call); 'false' -> + handle_vm_callback(Messages, PrevMessages, Count, Box, Call + ,kzd_vm_message_metadata:caller_id_number(CurrentMessage) + ) + end. + +handle_vm_callback(Messages, PrevMessages, Count, Box, Call, 'undefined') -> + lager:info("no caller_id_number on message"), + callback_not_available(Messages, PrevMessages, Count, Box, Call); +handle_vm_callback(Messages, PrevMessages, Count, Box, Call, CallerIdNumber) -> + lager:info("caller chose to callback number ~s", [CallerIdNumber]), + case maybe_branch_call(Box, Call, CallerIdNumber) of + 'ok' -> 'ok'; + 'error' -> play_messages(Messages, PrevMessages, Count, Box, Call) + end. + +callback_not_available(Messages, PrevMessages, Count, Box, Call) -> + _ = kapps_call_command:audio_macro([{'prompt', <<"vm-not_available">>}], Call), + play_messages(Messages, PrevMessages, Count, Box, Call). + +-spec maybe_branch_call(mailbox(), kapps_call:call(), kz_term:ne_binary()) -> + 'ok' | 'error'. +maybe_branch_call(Box, Call, CallerIdNumber) -> + maybe_branch_call(Box, Call, CallerIdNumber, kapps_call:authorizing_id(Call)). + +maybe_branch_call(#mailbox{owner_id='undefined'}, Call, CallerIdNumber, 'undefined') -> + case ?SHOULD_DISABLE_OFFNET_CALLBACK of + 'true' -> + lager:info("offnet callback disabled on this system"), + unauthorized_callback(Call); + 'false' -> + lager:info("no endpoint or user ID available, using account settings"), {'ok', AccountJObj} = kzd_accounts:fetch(kapps_call:account_id(Call)), - maybe_restrict_call(Number, Call, AccountJObj); - {'ok', JObj} -> maybe_restrict_call(Number, Call, JObj); - _ -> - lager:info("failed to find endpoint ~s", [EndpointId]), - _ = kapps_call_command:audio_macro([{'prompt', <<"cf-unauthorized_call">>}], Call), - 'error' + maybe_restrict_call(CallerIdNumber, Call, AccountJObj) + end; +maybe_branch_call(#mailbox{owner_id=OwnerId}, Call, CallerIdNumber, 'undefined') -> + case ?SHOULD_DISABLE_OFFNET_CALLBACK of + 'true' -> + lager:info("offnet callback disabled on this system"), + unauthorized_callback(Call); + 'false' -> + maybe_branch_to_endpoint(Call, CallerIdNumber, OwnerId) + end; +maybe_branch_call(_Box, Call, CallerIdNumber, EndpointId) -> + maybe_branch_to_endpoint(Call, CallerIdNumber, EndpointId). + +maybe_branch_to_endpoint(Call, CallerIdNumber, EndpointId) -> + case kz_endpoint:get(EndpointId, Call) of + {'ok', Endpoint} -> + lager:info("using endpoint for endpoint ID ~s", [EndpointId]), + maybe_restrict_call(CallerIdNumber, Call, Endpoint); + {'error', _E} -> + lager:info("failed to find endpoint ~s: ~p", [EndpointId, _E]), + unauthorized_callback(Call) end. --spec maybe_restrict_call( kz_term:ne_binary(), kapps_call:call(), kz_json:object()) -> 'ok' | 'error'. -maybe_restrict_call(Number, Call, JObj) -> - case should_restrict_call(Number, Call, JObj) of - {'true', _} -> - _ = kapps_call_command:audio_macro([{'prompt', <<"cf-unauthorized_call">>}], Call), - 'error'; - {'false', NewNumber} -> maybe_exist_callflow(NewNumber, Call) +unauthorized_callback(Call) -> + _ = kapps_call_command:audio_macro([{'prompt', <<"cf-unauthorized_call">>}], Call), + 'error'. + +%% @doc Endpoint could be AccountDoc or Owner/AuthzId endpoint +-spec maybe_restrict_call(kz_term:ne_binary(), kapps_call:call(), kz_json:object()) -> 'ok' | 'error'. +maybe_restrict_call(CallerIdNumber, Call, Endpoint) -> + case should_restrict_call(CallerIdNumber, Call, Endpoint) of + {'true', _} -> unauthorized_callback(Call); + {'false', NormalizedNumber} -> maybe_branch_to_callflow(NormalizedNumber, Call) end. --spec maybe_exist_callflow(kz_term:ne_binary(), kapps_call:call()) -> 'ok' | 'error'. -maybe_exist_callflow(Number, Call) -> +-spec maybe_branch_to_callflow(kz_term:ne_binary(), kapps_call:call()) -> 'ok' | 'error'. +maybe_branch_to_callflow(CallerIdNumber, Call) -> AccountId = kapps_call:account_id(Call), - case cf_flow:lookup(Number, AccountId) of + case cf_flow:lookup(CallerIdNumber, AccountId) of {'ok', Flow, _NoMatch} -> Updates = [{fun kapps_call:set_request/2 - ,list_to_binary([Number, "@", kapps_call:request_realm(Call)]) + ,list_to_binary([CallerIdNumber, "@", kapps_call:request_realm(Call)]) + } + ,{fun kapps_call:set_to/2 + ,list_to_binary([CallerIdNumber, "@", kapps_call:to_realm(Call)]) } - ,{fun kapps_call:set_to/2, list_to_binary([Number, "@", kapps_call:to_realm(Call)])} ], Call1 = cf_exe:update_call(kapps_call:exec(Updates, Call)), cf_exe:branch(kz_json:get_json_value(<<"flow">>, Flow), Call1); _ -> - lager:info("failed to find a callflow to satisfy ~s", [Number]), + lager:info("failed to find a callflow to satisfy ~s", [CallerIdNumber]), _ = kapps_call_command:audio_macro([{'prompt', <<"fault-can_not_be_completed_as_dialed">>}], Call), 'error' end. --spec should_restrict_call(kz_term:ne_binary(), kapps_call:call(), kz_json:object()) -> {boolean(), kz_term:ne_binary()}. -should_restrict_call(Number, Call, JObj) -> +%% @doc Endpoint could be AccountDoc or Owner/AuthzId endpoint +-spec should_restrict_call(kz_term:ne_binary(), kapps_call:call(), kz_json:object()) -> + {boolean(), kz_term:ne_binary()}. +should_restrict_call(CallerIdNumber, Call, Endpoint) -> AccountId = kapps_call:account_id(Call), - DialPlan = kz_json:get_json_value(<<"dial_plan">>, JObj, kz_json:new()), - NewNumber = knm_converters:normalize(Number, AccountId, DialPlan), - Classification = knm_converters:classify(NewNumber), + DialPlan = kz_json:get_json_value(<<"dial_plan">>, Endpoint, kz_json:new()), + NormalizedNumber = knm_converters:normalize(CallerIdNumber, AccountId, DialPlan), + Classification = knm_converters:classify(NormalizedNumber), lager:debug("classified number ~s as ~s, testing for call restrictions" - ,[Number, Classification] + ,[CallerIdNumber, Classification] ), - ShouldRestrict = kz_json:get_value([<<"call_restriction">>, Classification, <<"action">>], JObj) == <<"deny">>, - {ShouldRestrict, NewNumber}. + Restriction = kz_json:get_ne_binary_value([<<"call_restriction">>, Classification, <<"action">>], Endpoint), + + {<<"deny">> =:= Restriction + ,NormalizedNumber + }. -spec play_next_message(kz_json:objects(), kz_json:objects(), non_neg_integer(), mailbox(), kapps_call:call()) -> 'ok' | 'complete'. diff --git a/applications/crossbar/priv/api/swagger.json b/applications/crossbar/priv/api/swagger.json index 3af4a79d84e..75e743cf4e2 100644 --- a/applications/crossbar/priv/api/swagger.json +++ b/applications/crossbar/priv/api/swagger.json @@ -29970,6 +29970,16 @@ "minimum": 0, "type": "integer" }, + "should_disable_callback": { + "default": false, + "description": "If true, disallows callers to use voicemail callback feature", + "type": "boolean" + }, + "should_disable_offnet_callback": { + "default": false, + "description": "If true, requires caller to use an authorized device to use voicemail callback feature", + "type": "boolean" + }, "transcribe_default": { "default": false, "description": "callflow voicemail transcribe_default", diff --git a/applications/crossbar/priv/couchdb/schemas/system_config.callflow.json b/applications/crossbar/priv/couchdb/schemas/system_config.callflow.json index d21d69c7263..b25561be0bc 100644 --- a/applications/crossbar/priv/couchdb/schemas/system_config.callflow.json +++ b/applications/crossbar/priv/couchdb/schemas/system_config.callflow.json @@ -188,6 +188,16 @@ "minimum": 0, "type": "integer" }, + "should_disable_callback": { + "default": false, + "description": "If true, disallows callers to use voicemail callback feature", + "type": "boolean" + }, + "should_disable_offnet_callback": { + "default": false, + "description": "If true, requires caller to use an authorized device to use voicemail callback feature", + "type": "boolean" + }, "transcribe_default": { "default": false, "description": "callflow voicemail transcribe_default",